├── Dockerfile ├── Makefile ├── README.md ├── assets └── templates │ ├── bad_gateway.html │ └── not_found.html ├── bin └── boot ├── bindata.go ├── handlers.go ├── http.go ├── kubernetes.go ├── main.go ├── reverseproxy.go ├── template.go └── websocketproxy.go /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu-debootstrap:14.04 2 | MAINTAINER Eldarion, Inc. 3 | 4 | ENV DEBIAN_FRONTEND noninteractive 5 | 6 | RUN apt-get update \ 7 | && apt-get install -y --no-install-recommends \ 8 | ca-certificates curl net-tools \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | RUN cd /tmp \ 12 | && curl -LO https://github.com/coreos/etcd/releases/download/v2.0.10/etcd-v2.0.10-linux-amd64.tar.gz \ 13 | && tar xzvf etcd-v2.0.10-linux-amd64.tar.gz \ 14 | && mv etcd-v2.0.10-linux-amd64/etcdctl /usr/local/bin/ \ 15 | && rm -rf etcd-v2.0.10-linux-amd64 16 | 17 | ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 18 | 19 | EXPOSE 80 443 20 | CMD ["/app/bin/boot"] 21 | ADD . /app 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | bindata.go: assets 3 | go get github.com/jteeuwen/go-bindata 4 | GOOS="" GOARCH="" go install github.com/jteeuwen/go-bindata/go-bindata 5 | go-bindata -o bindata.go -pkg="main" -prefix=assets -nocompress assets/... 6 | go fmt ./bindata.go 7 | 8 | build: bindata.go 9 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 godep go build -a -installsuffix cgo -ldflags '-s' -o bin/router . 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # k8s-http-router 2 | 3 | > Warning: this project is very new and might have some issues. We have been using it successfully in our testing clusters for a couple of months. Bug reports are welcome!g 4 | 5 | This project has been pulled from the new Gondor backend built on Kubernetes. It simply routes HTTP requests to Kubernetes services using the HTTP Host header. Additional configuration can be specified, but nothing else is currently supported. 6 | 7 | # Setup 8 | 9 | To get k8s-http-router running on your Kubernetes cluster you can use the following replication controller: 10 | 11 | kind: ReplicationController 12 | apiVersion: v1beta3 13 | metadata: 14 | name: router 15 | spec: 16 | replicas: 3 17 | selector: 18 | name: router 19 | template: 20 | metadata: 21 | labels: 22 | name: router 23 | spec: 24 | containers: 25 | - name: router 26 | image: quay.io/eldarion/k8s-http-router 27 | ports: 28 | - name: router-http 29 | containerPort: 80 30 | - name: router-https 31 | containerPort: 443 32 | 33 | and service: 34 | 35 | kind: Service 36 | apiVersion: v1beta3 37 | metadata: 38 | name: router 39 | labels: 40 | name: router 41 | spec: 42 | selector: 43 | name: router 44 | ports: 45 | - name: router-http 46 | port: 80 47 | - name: router-https 48 | port: 443 49 | createExternalLoadBalancer: true 50 | 51 | To add a service to route, use the `router` annotation: 52 | 53 | kind: Service 54 | apiVersion: v1beta3 55 | metadata: 56 | name: my-website 57 | labels: 58 | name: my-website 59 | annotations: 60 | router: '{"config": {}, "hosts": ["example.com"]}' 61 | spec: 62 | selector: 63 | name: my-website 64 | ports: 65 | - port: 80 66 | targetPort: 8000 67 | 68 | k8s-http-router will watch for service changes and keep its internal data structures up-to-date with changes in the Kubernetes service objects. 69 | 70 | # Credit 71 | 72 | Thanks to Google for the amazing [Kubernetes](https://github.com/GoogleCloudPlatform/kubernetes) project. 73 | 74 | Thanks to the [flynn](https://github.com/flynn/flynn) project for a great router design this is inspired by, but stripped down and modified for Kubernetes. 75 | -------------------------------------------------------------------------------- /assets/templates/bad_gateway.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |Unable to contact backend service. Please try again later.
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/templates/not_found.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |Unable to route this request to a backend.
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /bin/boot: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | [[ $DEBUG ]] && set -x 6 | 7 | export ETCD="http://etcd.default:2379" 8 | export ETCD_PATH=${ETCD_PATH:-/gondor/router} 9 | export ETCD_TTL=${ETCD_TTL:-20} 10 | 11 | # wait for etcd to be available 12 | until etcdctl --no-sync -C $ETCD ls >/dev/null 2>&1; do 13 | echo "router: waiting for etcd at $ETCD..." 14 | sleep $(($ETCD_TTL/2)) # sleep for half the TTL 15 | done 16 | 17 | # wait until etcd has discarded potentially stale values 18 | sleep $(($ETCD_TTL+1)) 19 | 20 | function etcd_set_default { 21 | set +e 22 | etcdctl --no-sync -C $ETCD mk $ETCD_PATH/$1 $2 >/dev/null 2>&1 23 | if [[ $? -ne 0 && $? -ne 4 ]]; then 24 | echo "etcd_set_default: an etcd error occurred. aborting..." 25 | exit 1 26 | fi 27 | set -e 28 | } 29 | 30 | mkdir -p /etc/router 31 | etcdctl --no-sync -C $ETCD get $ETCD_PATH/tls/certificate > /etc/router/tls.crt 32 | etcdctl --no-sync -C $ETCD get $ETCD_PATH/tls/key > /etc/router/tls.key 33 | chmod 0600 /etc/router/tls.{crt,key} 34 | 35 | /app/bin/router & 36 | SERVICE_PID=$! 37 | 38 | # smart shutdown on SIGINT and SIGTERM 39 | function on_exit() { 40 | kill -TERM $SERVICE_PID 41 | wait $SERVICE_PID 2>/dev/null 42 | exit 0 43 | } 44 | trap on_exit INT TERM 45 | 46 | # wait for the service to become available 47 | sleep 1 && while [[ -z $(netstat -lnt | awk "\$6 == \"LISTEN\" && \$4 ~ \".80\" && \$1 ~ \"tcp.?\"") ]] ; do sleep 1; done 48 | 49 | echo "router: up and running..." 50 | 51 | wait 52 | -------------------------------------------------------------------------------- /bindata.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type asset struct { 14 | bytes []byte 15 | info os.FileInfo 16 | } 17 | 18 | type bindata_file_info struct { 19 | name string 20 | size int64 21 | mode os.FileMode 22 | modTime time.Time 23 | } 24 | 25 | func (fi bindata_file_info) Name() string { 26 | return fi.name 27 | } 28 | func (fi bindata_file_info) Size() int64 { 29 | return fi.size 30 | } 31 | func (fi bindata_file_info) Mode() os.FileMode { 32 | return fi.mode 33 | } 34 | func (fi bindata_file_info) ModTime() time.Time { 35 | return fi.modTime 36 | } 37 | func (fi bindata_file_info) IsDir() bool { 38 | return false 39 | } 40 | func (fi bindata_file_info) Sys() interface{} { 41 | return nil 42 | } 43 | 44 | var _templates_bad_gateway_html = []byte(` 45 | 46 | 47 |Unable to contact backend service. Please try again later.
64 | 65 | 66 | 67 | `) 68 | 69 | func templates_bad_gateway_html_bytes() ([]byte, error) { 70 | return _templates_bad_gateway_html, nil 71 | } 72 | 73 | func templates_bad_gateway_html() (*asset, error) { 74 | bytes, err := templates_bad_gateway_html_bytes() 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | info := bindata_file_info{name: "templates/bad_gateway.html", size: 587, mode: os.FileMode(420), modTime: time.Unix(1432687489, 0)} 80 | a := &asset{bytes: bytes, info: info} 81 | return a, nil 82 | } 83 | 84 | var _templates_not_found_html = []byte(` 85 | 86 | 87 |Unable to route this request to a backend.
104 | 105 | 106 | 107 | `) 108 | 109 | func templates_not_found_html_bytes() ([]byte, error) { 110 | return _templates_not_found_html, nil 111 | } 112 | 113 | func templates_not_found_html() (*asset, error) { 114 | bytes, err := templates_not_found_html_bytes() 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | info := bindata_file_info{name: "templates/not_found.html", size: 567, mode: os.FileMode(420), modTime: time.Unix(1431463101, 0)} 120 | a := &asset{bytes: bytes, info: info} 121 | return a, nil 122 | } 123 | 124 | // Asset loads and returns the asset for the given name. 125 | // It returns an error if the asset could not be found or 126 | // could not be loaded. 127 | func Asset(name string) ([]byte, error) { 128 | cannonicalName := strings.Replace(name, "\\", "/", -1) 129 | if f, ok := _bindata[cannonicalName]; ok { 130 | a, err := f() 131 | if err != nil { 132 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) 133 | } 134 | return a.bytes, nil 135 | } 136 | return nil, fmt.Errorf("Asset %s not found", name) 137 | } 138 | 139 | // MustAsset is like Asset but panics when Asset would return an error. 140 | // It simplifies safe initialization of global variables. 141 | func MustAsset(name string) []byte { 142 | a, err := Asset(name) 143 | if err != nil { 144 | panic("asset: Asset(" + name + "): " + err.Error()) 145 | } 146 | 147 | return a 148 | } 149 | 150 | // AssetInfo loads and returns the asset info for the given name. 151 | // It returns an error if the asset could not be found or 152 | // could not be loaded. 153 | func AssetInfo(name string) (os.FileInfo, error) { 154 | cannonicalName := strings.Replace(name, "\\", "/", -1) 155 | if f, ok := _bindata[cannonicalName]; ok { 156 | a, err := f() 157 | if err != nil { 158 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) 159 | } 160 | return a.info, nil 161 | } 162 | return nil, fmt.Errorf("AssetInfo %s not found", name) 163 | } 164 | 165 | // AssetNames returns the names of the assets. 166 | func AssetNames() []string { 167 | names := make([]string, 0, len(_bindata)) 168 | for name := range _bindata { 169 | names = append(names, name) 170 | } 171 | return names 172 | } 173 | 174 | // _bindata is a table, holding each asset generator, mapped to its name. 175 | var _bindata = map[string]func() (*asset, error){ 176 | "templates/bad_gateway.html": templates_bad_gateway_html, 177 | "templates/not_found.html": templates_not_found_html, 178 | } 179 | 180 | // AssetDir returns the file names below a certain 181 | // directory embedded in the file by go-bindata. 182 | // For example if you run go-bindata on data/... and data contains the 183 | // following hierarchy: 184 | // data/ 185 | // foo.txt 186 | // img/ 187 | // a.png 188 | // b.png 189 | // then AssetDir("data") would return []string{"foo.txt", "img"} 190 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 191 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 192 | // AssetDir("") will return []string{"data"}. 193 | func AssetDir(name string) ([]string, error) { 194 | node := _bintree 195 | if len(name) != 0 { 196 | cannonicalName := strings.Replace(name, "\\", "/", -1) 197 | pathList := strings.Split(cannonicalName, "/") 198 | for _, p := range pathList { 199 | node = node.Children[p] 200 | if node == nil { 201 | return nil, fmt.Errorf("Asset %s not found", name) 202 | } 203 | } 204 | } 205 | if node.Func != nil { 206 | return nil, fmt.Errorf("Asset %s not found", name) 207 | } 208 | rv := make([]string, 0, len(node.Children)) 209 | for name := range node.Children { 210 | rv = append(rv, name) 211 | } 212 | return rv, nil 213 | } 214 | 215 | type _bintree_t struct { 216 | Func func() (*asset, error) 217 | Children map[string]*_bintree_t 218 | } 219 | 220 | var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ 221 | "templates": &_bintree_t{nil, map[string]*_bintree_t{ 222 | "bad_gateway.html": &_bintree_t{templates_bad_gateway_html, map[string]*_bintree_t{}}, 223 | "not_found.html": &_bintree_t{templates_not_found_html, map[string]*_bintree_t{}}, 224 | }}, 225 | }} 226 | 227 | // Restore an asset under the given directory 228 | func RestoreAsset(dir, name string) error { 229 | data, err := Asset(name) 230 | if err != nil { 231 | return err 232 | } 233 | info, err := AssetInfo(name) 234 | if err != nil { 235 | return err 236 | } 237 | err = os.MkdirAll(_filePath(dir, path.Dir(name)), os.FileMode(0755)) 238 | if err != nil { 239 | return err 240 | } 241 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) 242 | if err != nil { 243 | return err 244 | } 245 | err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) 246 | if err != nil { 247 | return err 248 | } 249 | return nil 250 | } 251 | 252 | // Restore assets under the given directory recursively 253 | func RestoreAssets(dir, name string) error { 254 | children, err := AssetDir(name) 255 | if err != nil { // File 256 | return RestoreAsset(dir, name) 257 | } else { // Dir 258 | for _, child := range children { 259 | err = RestoreAssets(dir, path.Join(name, child)) 260 | if err != nil { 261 | return err 262 | } 263 | } 264 | } 265 | return nil 266 | } 267 | 268 | func _filePath(dir, name string) string { 269 | cannonicalName := strings.Replace(name, "\\", "/", -1) 270 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) 271 | } 272 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | fwdForHeaderName = "X-Forwarded-For" 11 | fwdProtoHeaderName = "X-Forwarded-Proto" 12 | fwdPortHeaderName = "X-Forwarded-Port" 13 | ) 14 | 15 | // fwdProtoHandler is an http.Handler that sets the X-Forwarded-For header on 16 | // inbound requests to match the remote IP address, and sets X-Forwarded-Proto 17 | // and X-Forwarded-Port headers to match the values in Proto and Port. If those 18 | // headers already exist, the new values will be appended. 19 | type fwdProtoHandler struct { 20 | http.Handler 21 | Proto string 22 | Port string 23 | } 24 | 25 | func (h fwdProtoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 26 | // If we aren't the first proxy retain prior X-Forwarded-* information as a 27 | // comma+space separated list and fold multiple headers into one. 28 | if clientIP, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { 29 | if prior, ok := r.Header[fwdForHeaderName]; ok { 30 | clientIP = strings.Join(prior, ", ") + ", " + clientIP 31 | } 32 | r.Header.Set(fwdForHeaderName, clientIP) 33 | } 34 | 35 | proto, port := h.Proto, h.Port 36 | if prior, ok := r.Header[fwdProtoHeaderName]; ok { 37 | proto = strings.Join(prior, ", ") + ", " + proto 38 | } 39 | if prior, ok := r.Header[fwdPortHeaderName]; ok { 40 | port = strings.Join(prior, ", ") + ", " + port 41 | } 42 | r.Header.Set(fwdProtoHeaderName, proto) 43 | r.Header.Set(fwdPortHeaderName, port) 44 | 45 | h.Handler.ServeHTTP(w, r) 46 | } 47 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | type HTTPRouter struct { 12 | mtx sync.RWMutex 13 | hosts map[string]*url.URL 14 | } 15 | 16 | func NewHTTPRouter() *HTTPRouter { 17 | hr := HTTPRouter{ 18 | hosts: make(map[string]*url.URL), 19 | } 20 | return &hr 21 | } 22 | 23 | func (hr *HTTPRouter) SetHost(host string, target *url.URL) { 24 | hr.mtx.Lock() 25 | defer hr.mtx.Unlock() 26 | hr.hosts[host] = target 27 | } 28 | 29 | func (hr *HTTPRouter) RemoveHost(host string) { 30 | hr.mtx.Lock() 31 | defer hr.mtx.Unlock() 32 | delete(hr.hosts, host) 33 | } 34 | 35 | func (hr *HTTPRouter) isWebsocket(req *http.Request) bool { 36 | conn_hdr := "" 37 | conn_hdrs := req.Header["Connection"] 38 | if len(conn_hdrs) > 0 { 39 | conn_hdr = conn_hdrs[0] 40 | } 41 | do := false 42 | if strings.ToLower(conn_hdr) == "upgrade" { 43 | upgrade_hdrs := req.Header["Upgrade"] 44 | if len(upgrade_hdrs) > 0 { 45 | do = (strings.ToLower(upgrade_hdrs[0]) == "websocket") 46 | } 47 | } 48 | return do 49 | } 50 | 51 | func (hr *HTTPRouter) makeHandlerForReq(req *http.Request, target *url.URL) http.Handler { 52 | if hr.isWebsocket(req) { 53 | return WebsocketProxy(target) 54 | } 55 | return NewSingleHostReverseProxy(target) 56 | } 57 | 58 | func (hr *HTTPRouter) findHandlerForReq(req *http.Request) http.Handler { 59 | host := strings.ToLower(req.Host) 60 | if strings.Contains(host, ":") { 61 | host, _, _ = net.SplitHostPort(host) 62 | } 63 | hr.mtx.RLock() 64 | defer hr.mtx.RUnlock() 65 | if target, ok := hr.hosts[host]; ok { 66 | return hr.makeHandlerForReq(req, target) 67 | } 68 | // handle wildcard domains up to 2 subdomains deep 69 | d := strings.SplitN(host, ".", 2) 70 | for i := len(d); i > 0; i-- { 71 | if target, ok := hr.hosts["*."+strings.Join(d[len(d)-i:], ".")]; ok { 72 | return hr.makeHandlerForReq(req, target) 73 | } 74 | } 75 | return nil 76 | } 77 | 78 | func (hr *HTTPRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { 79 | var handler http.Handler 80 | handler = hr.findHandlerForReq(req) 81 | if handler == nil { 82 | handler = TemplateHandler(http.StatusNotFound, "not_found.html") 83 | } 84 | handler.ServeHTTP(w, req) 85 | } 86 | -------------------------------------------------------------------------------- /kubernetes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/url" 8 | "os" 9 | "time" 10 | 11 | kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" 12 | kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" 13 | "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" 14 | "github.com/GoogleCloudPlatform/kubernetes/pkg/controller/framework" 15 | kcontrollerFramework "github.com/GoogleCloudPlatform/kubernetes/pkg/controller/framework" 16 | kSelector "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" 17 | "golang.org/x/net/context" 18 | ) 19 | 20 | const ( 21 | // Resync period for the kube controller loop. 22 | resyncPeriod = 5 * time.Minute 23 | ) 24 | 25 | type SyncHandler interface { 26 | SetHost(host string, target *url.URL) 27 | RemoveHost(host string) 28 | } 29 | 30 | type kubernetesServiceConfig struct { 31 | Config struct { 32 | } `json:"config,omitempty"` 33 | Hosts []string `json:"hosts,omitempty"` 34 | } 35 | 36 | type kubernetesDataStore struct { 37 | kubClient *kclient.Client 38 | srvCache []*kapi.Service 39 | } 40 | 41 | func NewKubernetesDataStore() (*kubernetesDataStore, error) { 42 | masterHost := os.Getenv("KUBERNETES_RO_SERVICE_HOST") 43 | if masterHost == "" { 44 | log.Fatalf("KUBERNETES_RO_SERVICE_HOST is not defined") 45 | } 46 | masterPort := os.Getenv("KUBERNETES_RO_SERVICE_PORT") 47 | if masterPort == "" { 48 | log.Fatalf("KUBERNETES_RO_SERVICE_PORT is not defined") 49 | } 50 | config := &kclient.Config{ 51 | Host: fmt.Sprintf("http://%s:%s", masterHost, masterPort), 52 | Version: "v1beta3", 53 | } 54 | kubClient, err := kclient.New(config) 55 | if err != nil { 56 | return nil, err 57 | } 58 | return &kubernetesDataStore{ 59 | kubClient: kubClient, 60 | }, nil 61 | } 62 | 63 | func (ds *kubernetesDataStore) createServiceLW() *cache.ListWatch { 64 | return cache.NewListWatchFromClient(ds.kubClient, "services", kapi.NamespaceAll, kSelector.Everything()) 65 | } 66 | 67 | func (ds *kubernetesDataStore) Watch(ctx context.Context, sh SyncHandler) { 68 | var serviceController *kcontrollerFramework.Controller 69 | _, serviceController = framework.NewInformer( 70 | ds.createServiceLW(), 71 | &kapi.Service{}, 72 | resyncPeriod, 73 | framework.ResourceEventHandlerFuncs{ 74 | AddFunc: func(obj interface{}) { 75 | if s, ok := obj.(*kapi.Service); ok { 76 | ds.setService(s, sh) 77 | } 78 | }, 79 | DeleteFunc: func(obj interface{}) { 80 | if s, ok := obj.(*kapi.Service); ok { 81 | ds.removeService(s, sh) 82 | } 83 | }, 84 | UpdateFunc: func(oldObj, newObj interface{}) { 85 | if s, ok := newObj.(*kapi.Service); ok { 86 | ds.setService(s, sh) 87 | } 88 | }, 89 | }, 90 | ) 91 | serviceController.Run(ctx.Done()) 92 | } 93 | 94 | func (ds *kubernetesDataStore) setService(service *kapi.Service, sh SyncHandler) error { 95 | if _, ok := service.Annotations["router"]; !ok { 96 | return nil 97 | } 98 | var res kubernetesServiceConfig 99 | if err := json.Unmarshal([]byte(service.Annotations["router"]), &res); err != nil { 100 | return nil 101 | } 102 | for i := range res.Hosts { 103 | host := res.Hosts[i] 104 | if len(service.Spec.Ports) == 0 { 105 | continue 106 | } 107 | target, err := url.Parse( 108 | fmt.Sprintf( 109 | "http://%s:%d", 110 | service.Spec.PortalIP, 111 | service.Spec.Ports[0].Port, 112 | ), 113 | ) 114 | if err != nil { 115 | return fmt.Errorf("cannot parse URL: %s", err) 116 | } 117 | sh.SetHost(host, target) 118 | } 119 | log.Printf("watcher: added %s", service.Name) 120 | return nil 121 | } 122 | 123 | func (ds *kubernetesDataStore) removeService(service *kapi.Service, sh SyncHandler) error { 124 | if _, ok := service.Annotations["router"]; !ok { 125 | return nil 126 | } 127 | var res kubernetesServiceConfig 128 | if err := json.Unmarshal([]byte(service.Annotations["router"]), &res); err != nil { 129 | return nil 130 | } 131 | for i := range res.Hosts { 132 | host := res.Hosts[i] 133 | sh.RemoveHost(host) 134 | } 135 | log.Printf("watcher: removed %s", service.Name) 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "log" 7 | "net" 8 | "net/http" 9 | 10 | "github.com/flynn/flynn/pkg/tlsconfig" 11 | "golang.org/x/net/context" 12 | ) 13 | 14 | func main() { 15 | httpRouter := NewHTTPRouter() 16 | errc := make(chan error) 17 | 18 | ds, err := NewKubernetesDataStore() 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | syncCtx := context.Background() 24 | syncCtx, stopSync := context.WithCancel(syncCtx) 25 | 26 | // watch kubernetes 27 | log.Println("starting kubernetes watcher...") 28 | go ds.Watch(syncCtx, httpRouter) 29 | 30 | if err := httpListenAndServe(httpRouter, errc); err != nil { 31 | stopSync() 32 | log.Fatal(fmt.Sprintf("error: http server: %s", err)) 33 | } 34 | 35 | if err := httpsListenAndServe(httpRouter, errc); err != nil { 36 | stopSync() 37 | log.Fatal(fmt.Sprintf("error: https server: %s", err)) 38 | } 39 | 40 | if err := <-errc; err != nil { 41 | log.Fatal(fmt.Sprintf("serve error: %s", err)) 42 | } 43 | 44 | log.Println("exited") 45 | } 46 | 47 | func httpListenAndServe(handler http.Handler, errc chan error) error { 48 | listener, err := net.Listen("tcp4", ":80") 49 | if err != nil { 50 | return err 51 | } 52 | server := &http.Server{ 53 | Addr: listener.Addr().String(), 54 | Handler: fwdProtoHandler{ 55 | Handler: handler, 56 | Proto: "http", 57 | Port: mustPortFromAddr(listener.Addr().String()), 58 | }, 59 | } 60 | go func() { 61 | errc <- server.Serve(listener) 62 | }() 63 | return nil 64 | } 65 | 66 | func httpsListenAndServe(handler http.Handler, errc chan error) error { 67 | tlsKeyPair, err := tls.LoadX509KeyPair("/etc/router/tls.crt", "/etc/router/tls.key") 68 | if err != nil { 69 | return err 70 | } 71 | certForHandshake := func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { 72 | return nil, nil 73 | } 74 | tlsConfig := tlsconfig.SecureCiphers(&tls.Config{ 75 | GetCertificate: certForHandshake, 76 | Certificates: []tls.Certificate{tlsKeyPair}, 77 | }) 78 | listener, err := net.Listen("tcp4", ":443") 79 | if err != nil { 80 | return err 81 | } 82 | tlsListener := tls.NewListener(listener, tlsConfig) 83 | server := &http.Server{ 84 | Addr: tlsListener.Addr().String(), 85 | Handler: fwdProtoHandler{ 86 | Handler: handler, 87 | Proto: "https", 88 | Port: mustPortFromAddr(tlsListener.Addr().String()), 89 | }, 90 | } 91 | go func() { 92 | errc <- server.Serve(tlsListener) 93 | }() 94 | return nil 95 | } 96 | 97 | func mustPortFromAddr(addr string) string { 98 | _, port, err := net.SplitHostPort(addr) 99 | if err != nil { 100 | panic(err) 101 | } 102 | return port 103 | } 104 | -------------------------------------------------------------------------------- /reverseproxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // HTTP reverse proxy handler 6 | 7 | package main 8 | 9 | import ( 10 | "io" 11 | "log" 12 | "net" 13 | "net/http" 14 | "net/url" 15 | "strings" 16 | "sync" 17 | "time" 18 | ) 19 | 20 | // onExitFlushLoop is a callback set by tests to detect the state of the 21 | // flushLoop() goroutine. 22 | var onExitFlushLoop func() 23 | 24 | // ReverseProxy is an HTTP Handler that takes an incoming request and 25 | // sends it to another server, proxying the response back to the 26 | // client. 27 | type ReverseProxy struct { 28 | // Director must be a function which modifies 29 | // the request into a new request to be sent 30 | // using Transport. Its response is then copied 31 | // back to the original client unmodified. 32 | Director func(*http.Request) 33 | 34 | // The transport used to perform proxy requests. 35 | // If nil, http.DefaultTransport is used. 36 | Transport http.RoundTripper 37 | 38 | // FlushInterval specifies the flush interval 39 | // to flush to the client while copying the 40 | // response body. 41 | // If zero, no periodic flushing is done. 42 | FlushInterval time.Duration 43 | 44 | // ErrorLog specifies an optional logger for errors 45 | // that occur when attempting to proxy the request. 46 | // If nil, logging goes to os.Stderr via the log package's 47 | // standard logger. 48 | ErrorLog *log.Logger 49 | } 50 | 51 | func singleJoiningSlash(a, b string) string { 52 | aslash := strings.HasSuffix(a, "/") 53 | bslash := strings.HasPrefix(b, "/") 54 | switch { 55 | case aslash && bslash: 56 | return a + b[1:] 57 | case !aslash && !bslash: 58 | return a + "/" + b 59 | } 60 | return a + b 61 | } 62 | 63 | // NewSingleHostReverseProxy returns a new ReverseProxy that rewrites 64 | // URLs to the scheme, host, and base path provided in target. If the 65 | // target's path is "/base" and the incoming request was for "/dir", 66 | // the target request will be for /base/dir. 67 | func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy { 68 | targetQuery := target.RawQuery 69 | director := func(req *http.Request) { 70 | req.URL.Scheme = target.Scheme 71 | req.URL.Host = target.Host 72 | req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) 73 | if targetQuery == "" || req.URL.RawQuery == "" { 74 | req.URL.RawQuery = targetQuery + req.URL.RawQuery 75 | } else { 76 | req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery 77 | } 78 | } 79 | return &ReverseProxy{Director: director} 80 | } 81 | 82 | func copyHeader(dst, src http.Header) { 83 | for k, vv := range src { 84 | for _, v := range vv { 85 | dst.Add(k, v) 86 | } 87 | } 88 | } 89 | 90 | // Hop-by-hop headers. These are removed when sent to the backend. 91 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html 92 | var hopHeaders = []string{ 93 | "Connection", 94 | "Keep-Alive", 95 | "Proxy-Authenticate", 96 | "Proxy-Authorization", 97 | "Te", // canonicalized version of "TE" 98 | "Trailers", 99 | "Transfer-Encoding", 100 | "Upgrade", 101 | } 102 | 103 | func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 104 | transport := p.Transport 105 | if transport == nil { 106 | transport = http.DefaultTransport 107 | } 108 | 109 | outreq := new(http.Request) 110 | *outreq = *req // includes shallow copies of maps, but okay 111 | 112 | p.Director(outreq) 113 | outreq.Proto = "HTTP/1.1" 114 | outreq.ProtoMajor = 1 115 | outreq.ProtoMinor = 1 116 | outreq.Close = false 117 | 118 | // Remove hop-by-hop headers to the backend. Especially 119 | // important is "Connection" because we want a persistent 120 | // connection, regardless of what the client sent to us. This 121 | // is modifying the same underlying map from req (shallow 122 | // copied above) so we only copy it if necessary. 123 | copiedHeaders := false 124 | for _, h := range hopHeaders { 125 | if outreq.Header.Get(h) != "" { 126 | if !copiedHeaders { 127 | outreq.Header = make(http.Header) 128 | copyHeader(outreq.Header, req.Header) 129 | copiedHeaders = true 130 | } 131 | outreq.Header.Del(h) 132 | } 133 | } 134 | 135 | if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { 136 | // If we aren't the first proxy retain prior 137 | // X-Forwarded-For information as a comma+space 138 | // separated list and fold multiple headers into one. 139 | if prior, ok := outreq.Header["X-Forwarded-For"]; ok { 140 | clientIP = strings.Join(prior, ", ") + ", " + clientIP 141 | } 142 | outreq.Header.Set("X-Forwarded-For", clientIP) 143 | } 144 | 145 | res, err := transport.RoundTrip(outreq) 146 | if err != nil { 147 | p.logf("http: proxy error: %v", err) 148 | TemplateHandler(http.StatusBadGateway, "bad_gateway.html").ServeHTTP(rw, req) 149 | return 150 | } 151 | defer res.Body.Close() 152 | 153 | for _, h := range hopHeaders { 154 | res.Header.Del(h) 155 | } 156 | 157 | copyHeader(rw.Header(), res.Header) 158 | 159 | rw.WriteHeader(res.StatusCode) 160 | p.copyResponse(rw, res.Body) 161 | } 162 | 163 | func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) { 164 | if p.FlushInterval != 0 { 165 | if wf, ok := dst.(writeFlusher); ok { 166 | mlw := &maxLatencyWriter{ 167 | dst: wf, 168 | latency: p.FlushInterval, 169 | done: make(chan bool), 170 | } 171 | go mlw.flushLoop() 172 | defer mlw.stop() 173 | dst = mlw 174 | } 175 | } 176 | 177 | io.Copy(dst, src) 178 | } 179 | 180 | func (p *ReverseProxy) logf(format string, args ...interface{}) { 181 | if p.ErrorLog != nil { 182 | p.ErrorLog.Printf(format, args...) 183 | } else { 184 | log.Printf(format, args...) 185 | } 186 | } 187 | 188 | type writeFlusher interface { 189 | io.Writer 190 | http.Flusher 191 | } 192 | 193 | type maxLatencyWriter struct { 194 | dst writeFlusher 195 | latency time.Duration 196 | 197 | lk sync.Mutex // protects Write + Flush 198 | done chan bool 199 | } 200 | 201 | func (m *maxLatencyWriter) Write(p []byte) (int, error) { 202 | m.lk.Lock() 203 | defer m.lk.Unlock() 204 | return m.dst.Write(p) 205 | } 206 | 207 | func (m *maxLatencyWriter) flushLoop() { 208 | t := time.NewTicker(m.latency) 209 | defer t.Stop() 210 | for { 211 | select { 212 | case <-m.done: 213 | if onExitFlushLoop != nil { 214 | onExitFlushLoop() 215 | } 216 | return 217 | case <-t.C: 218 | m.lk.Lock() 219 | m.dst.Flush() 220 | m.lk.Unlock() 221 | } 222 | } 223 | } 224 | 225 | func (m *maxLatencyWriter) stop() { m.done <- true } 226 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | func TemplateHandler(statusCode int, name string) http.Handler { 9 | data, _ := Asset(fmt.Sprintf("templates/%s", name)) 10 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 11 | w.WriteHeader(statusCode) 12 | w.Write(data) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /websocketproxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net" 7 | "net/http" 8 | "net/url" 9 | ) 10 | 11 | func WebsocketProxy(target *url.URL) http.Handler { 12 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | d, err := net.Dial("tcp", target.Host) 14 | if err != nil { 15 | TemplateHandler(http.StatusBadGateway, "bad_gateway.html").ServeHTTP(w, r) 16 | log.Printf("error dialing websocket backend %s: %v", target, err) 17 | return 18 | } 19 | hj, ok := w.(http.Hijacker) 20 | if !ok { 21 | http.Error(w, "Not a hijacker?", 500) 22 | return 23 | } 24 | nc, _, err := hj.Hijack() 25 | if err != nil { 26 | log.Printf("hijack error: %v", err) 27 | return 28 | } 29 | defer nc.Close() 30 | defer d.Close() 31 | err = r.Write(d) 32 | if err != nil { 33 | log.Printf("error copying request to target: %v", err) 34 | return 35 | } 36 | errc := make(chan error, 2) 37 | cp := func(dst io.Writer, src io.Reader) { 38 | _, err := io.Copy(dst, src) 39 | errc <- err 40 | } 41 | go cp(d, nc) 42 | go cp(nc, d) 43 | <-errc 44 | }) 45 | } 46 | --------------------------------------------------------------------------------