forked from jimmidyson/kuisp
/
kuisp.go
210 lines (189 loc) · 7.03 KB
/
kuisp.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// KUISP - A utility to serve static content & reverse proxy to RESTful services
//
// Copyright 2015 Red Hat, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httputil"
"os"
"strings"
"time"
"github.com/bradfitz/http2"
"github.com/gorilla/handlers"
flag "github.com/spf13/pflag"
)
type Options struct {
Port int
StaticDir string
StaticPrefix string
DefaultPage string
StaticCacheMaxAge time.Duration
Services services
FailOnUnknownServices bool
Configs configs
CACerts caCerts
SkipCertValidation bool
TlsCertFile string
TlsKeyFile string
AccessLogging bool
CompressHandler bool
ServeWww bool
}
var options = &Options{}
func initFlags() {
flag.IntVarP(&options.Port, "port", "p", 80, "The port to listen on")
flag.StringVarP(&options.StaticDir, "www", "w", ".", "Directory to serve static files from")
flag.StringVar(&options.StaticPrefix, "www-prefix", "/", "Prefix to serve static files on")
flag.DurationVar(&options.StaticCacheMaxAge, "max-age", 0, "Set the Cache-Control header for static content with the max-age set to this value, e.g. 24h. Must confirm to http://golang.org/pkg/time/#ParseDuration")
flag.StringVarP(&options.DefaultPage, "default-page", "d", "", "Default page to send if page not found")
flag.VarP(&options.Services, "service", "s", "The Kubernetes services to proxy to in the form \"<prefix>=<serviceUrl>\"")
flag.VarP(&options.Configs, "config-file", "c", "The configuration files to create in the form \"<template>=<output>\"")
flag.Var(&options.CACerts, "ca-cert", "CA certs used to verify proxied server certificates")
flag.StringVar(&options.TlsCertFile, "tls-cert", "", "Certificate file to use to serve using TLS")
flag.StringVar(&options.TlsKeyFile, "tls-key", "", "Certificate file to use to serve using TLS")
flag.BoolVar(&options.SkipCertValidation, "skip-cert-validation", false, "Skip remote certificate validation - dangerous!")
flag.BoolVarP(&options.AccessLogging, "access-logging", "l", false, "Enable access logging")
flag.BoolVar(&options.CompressHandler, "compress", false, "Enable gzip/deflate response compression")
flag.BoolVar(&options.FailOnUnknownServices, "fail-on-unknown-services", false, "Fail on unknown services in DNS")
flag.BoolVar(&options.ServeWww, "serve-www", true, "Whether to serve static content")
flag.Parse()
}
func main() {
initFlags()
if len(options.Configs) > 0 {
for _, configDef := range options.Configs {
log.Printf("Creating config file: %v => %v\n", configDef.template, configDef.output)
createConfig(configDef.template, configDef.output)
}
log.Println()
}
if len(options.Services) > 0 {
tlsConfig := &tls.Config{
RootCAs: x509.NewCertPool(),
InsecureSkipVerify: options.SkipCertValidation,
}
transport := &http.Transport{TLSClientConfig: tlsConfig}
if len(options.CACerts) > 0 {
for _, caFile := range options.CACerts {
// Load our trusted certificate path
pemData, err := ioutil.ReadFile(caFile)
if err != nil {
log.Fatal("Couldn't read CA file, ", caFile, ": ", err)
}
if ok := tlsConfig.RootCAs.AppendCertsFromPEM(pemData); !ok {
log.Fatal("Couldn't load PEM data from CA file, ", caFile)
}
}
}
for _, serviceDef := range options.Services {
actualHost, port, err := validateServiceHost(serviceDef.url.Host)
if err != nil {
if options.FailOnUnknownServices {
log.Fatalf("Unknown service host: %s", serviceDef.url.Host)
} else {
log.Printf("Unknown service host: %s", serviceDef.url.Host)
}
} else {
if len(port) > 0 {
actualHost += ":" + port
}
serviceDef.url.Host = actualHost
}
log.Printf("Creating service proxy: %v => %v\n", serviceDef.prefix, serviceDef.url.String())
rp := httputil.NewSingleHostReverseProxy(serviceDef.url)
rp.Transport = transport
http.Handle(serviceDef.prefix, http.StripPrefix(serviceDef.prefix, rp))
}
log.Println()
}
if options.ServeWww {
httpDir := http.Dir(options.StaticDir)
staticHandler := http.FileServer(httpDir)
if options.StaticCacheMaxAge > 0 {
staticHandler = maxAgeHandler(options.StaticCacheMaxAge.Seconds(), staticHandler)
}
if len(options.DefaultPage) > 0 {
staticHandler = defaultPageHandler(options.DefaultPage, httpDir, staticHandler)
}
if options.CompressHandler {
staticHandler = handlers.CompressHandler(staticHandler)
}
http.Handle(options.StaticPrefix, staticHandler)
}
log.Printf("Listening on :%d\n", options.Port)
log.Println()
registerMimeTypes()
srv := &http.Server{
Addr: fmt.Sprintf(":%d", options.Port),
}
http2.ConfigureServer(srv, &http2.Server{})
var handler http.Handler = http.DefaultServeMux
if options.AccessLogging {
handler = handlers.CombinedLoggingHandler(os.Stdout, handler)
}
srv.Handler = handler
if len(options.TlsCertFile) > 0 && len(options.TlsKeyFile) > 0 {
log.Fatal(srv.ListenAndServeTLS(options.TlsCertFile, options.TlsKeyFile))
} else {
log.Fatal(srv.ListenAndServe())
}
}
func defaultPageHandler(defaultPage string, httpDir http.Dir, fsHandler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, err := httpDir.Open(r.URL.Path); err != nil {
if defaultFile, err := httpDir.Open(defaultPage); err == nil {
if stat, err := defaultFile.Stat(); err == nil {
http.ServeContent(w, r, stat.Name(), stat.ModTime(), defaultFile)
}
}
} else {
fsHandler.ServeHTTP(w, r)
}
})
}
func maxAgeHandler(seconds float64, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", fmt.Sprintf("max-age=%g, public, must-revalidate, proxy-revalidate", seconds))
h.ServeHTTP(w, r)
})
}
func validateServiceHost(host string) (string, string, error) {
actualHost, port, err := net.SplitHostPort(host)
if err != nil {
actualHost = host
}
if ip := net.ParseIP(actualHost); ip != nil {
return actualHost, port, nil
}
_, err = net.LookupIP(actualHost)
if err != nil {
if !strings.Contains(actualHost, ".") {
actualHost = strings.ToUpper(actualHost)
actualHost = strings.Replace(actualHost, "-", "_", -1)
serviceHostEnvVar := os.Getenv(actualHost + "_SERVICE_HOST")
if net.ParseIP(serviceHostEnvVar) != nil {
return serviceHostEnvVar, os.Getenv(actualHost + "_SERVICE_PORT"), nil
}
}
return "", "", err
}
return actualHost, port, nil
}