diff --git a/README.md b/README.md index 556b23e50..62a5f538a 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ It supports several backends ([Docker :whale:](https://www.docker.com/), [Mesos/ - Tiny docker image included - SSL backends support - SSL frontend support -- WebUI +- Clean AngularJS Web UI +- Websocket support ## Demo @@ -33,6 +34,13 @@ Here is a demo of Træfɪk using Docker backend, showing a load-balancing betwee [![asciicast](https://asciinema.org/a/4tcyde7riou5vxulo6my3mtko.png)](https://asciinema.org/a/4tcyde7riou5vxulo6my3mtko) +## Web UI + +You can access to a simple HTML frontend of Træfik. + +![Web UI Providers](docs/img/web.frontend.png) +![Web UI Health](docs/img/traefik-health.png) + ## Plumbing - [Oxy](https://github.com/mailgun/oxy/): an awsome proxy library made by Mailgun guys @@ -68,13 +76,6 @@ You can find the complete documentation [here](docs/index.md). Refer to the [benchmarks section](docs/index.md#benchmarks) in the documentation. -## Web UI - -You can access to a simple HTML frontend of Træfik. - -![Web UI Providers](docs/img/web.frontend.png) -![Web UI Health](docs/img/traefik-health.png) - ## Contributing ### Building diff --git a/glide.yaml b/glide.yaml index 0ed2272ae..1bd29b5a4 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,81 +1,11 @@ package: main import: - - package: github.com/mailgun/timetools - ref: fd192d755b00c968d312d23f521eb0cdc6f66bd0 - package: github.com/coreos/go-etcd ref: cc90c7b091275e606ad0ca7102a23fb2072f3f5e subpackages: - etcd - - package: github.com/davecgh/go-spew - ref: 2df174808ee097f90d259e432cc04442cf60be21 - subpackages: - - spew - - package: gopkg.in/fsnotify.v1 - ref: 96c060f6a6b7e0d6f75fddd10efeaca3e5d1bcb0 - - package: github.com/BurntSushi/ty - ref: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74 - - package: github.com/hashicorp/consul - ref: de080672fee9e6104572eeea89eccdca135bb918 - subpackages: - - api - - package: github.com/alecthomas/template - ref: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0 - - package: github.com/thoas/stats - ref: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8 - - package: github.com/vdemeester/shakers - ref: 8fe734f75f3a70b651cbfbf8a55a009da09e8dc5 - - package: github.com/samuel/go-zookeeper - ref: fa6674abf3f4580b946a01bf7a1ce4ba8766205b - subpackages: - - zk - - package: github.com/alecthomas/units - ref: 6b4e7dc5e3143b85ea77909c72caf89416fc2915 - - package: github.com/unrolled/render - ref: 26b4e3aac686940fe29521545afad9966ddfc80c - - package: github.com/flynn/go-shlex - ref: 3f9db97f856818214da2e1057f8ad84803971cff - - package: github.com/fsouza/go-dockerclient - ref: 0239034d42f665efa17fd77c39f891c2f9f32922 - - package: github.com/codegangsta/negroni - ref: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b - - package: gopkg.in/yaml.v2 - ref: 7ad95dd0798a40da1ccdff6dff35fd177b5edf40 - - package: github.com/opencontainers/runc - ref: 4ab132458fc3e9dbeea624153e0331952dc4c8d5 - subpackages: - - libcontainer/user - - package: github.com/boltdb/bolt - ref: 51f99c862475898df9773747d3accd05a7ca33c1 - - package: github.com/docker/libtrust - ref: 9cbd2a1374f46905c68a4eb3694a130610adc62a - - package: github.com/elazarl/go-bindata-assetfs - ref: d5cac425555ca5cf00694df246e04f05e6a55150 - package: github.com/docker/distribution ref: 9038e48c3b982f8e82281ea486f078a73731ac4e - - package: github.com/BurntSushi/toml - ref: bd2bdf7f18f849530ef7a1c29a4290217cab32a1 - - package: github.com/samalba/dockerclient - ref: cfb489c624b635251a93e74e1e90eb0959c5367f - - package: gopkg.in/check.v1 - ref: 11d3bc7aa68e238947792f30573146a3231fc0f1 - - package: gopkg.in/alecthomas/kingpin.v2 - ref: 639879d6110b1b0409410c7b737ef0bb18325038 - - package: github.com/Sirupsen/logrus - ref: 418b41d23a1bf978c06faea5313ba194650ac088 - - package: golang.org/x/net - ref: d9558e5c97f85372afee28cf2b6059d7d3818919 - subpackages: - - context - - package: gopkg.in/mgo.v2 - ref: 22287bab4379e1fbf6002fb4eb769888f3fb224c - subpackages: - - bson - - package: github.com/gambol99/go-marathon - ref: 0ba31bcb0d7633ba1888d744c42990eb15281cf1 - - package: github.com/mailgun/manners - ref: 37136f736785d7c6aa3b9a27b4b2dd1028ca6d79 - - package: github.com/gorilla/handlers - ref: 40694b40f4a928c062f56849989d3e9cd0570e5f - package: github.com/mailgun/log ref: 44874009257d4d47ba9806f1b7f72a32a015e4d8 - package: github.com/mailgun/oxy @@ -86,6 +16,54 @@ import: - memmetrics - roundrobin - utils + - package: github.com/hashicorp/consul + ref: de080672fee9e6104572eeea89eccdca135bb918 + subpackages: + - api + - package: github.com/samuel/go-zookeeper + ref: fa6674abf3f4580b946a01bf7a1ce4ba8766205b + subpackages: + - zk + - package: github.com/docker/libtrust + ref: 9cbd2a1374f46905c68a4eb3694a130610adc62a + - package: gopkg.in/check.v1 + ref: 11d3bc7aa68e238947792f30573146a3231fc0f1 + - package: golang.org/x/net + ref: d9558e5c97f85372afee28cf2b6059d7d3818919 + subpackages: + - context + - package: github.com/gorilla/handlers + ref: 40694b40f4a928c062f56849989d3e9cd0570e5f + - package: github.com/docker/libkv + ref: 3732f7ff1b56057c3158f10bceb1e79133025373 + - package: github.com/alecthomas/template + ref: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0 + - package: github.com/vdemeester/shakers + ref: 8fe734f75f3a70b651cbfbf8a55a009da09e8dc5 + - package: github.com/alecthomas/units + ref: 6b4e7dc5e3143b85ea77909c72caf89416fc2915 + - package: github.com/gambol99/go-marathon + ref: 0ba31bcb0d7633ba1888d744c42990eb15281cf1 + - package: github.com/mailgun/predicate + ref: cb0bff91a7ab7cf7571e661ff883fc997bc554a3 + - package: github.com/thoas/stats + ref: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8 + - package: github.com/samalba/dockerclient + ref: cfb489c624b635251a93e74e1e90eb0959c5367f + - package: github.com/Sirupsen/logrus + ref: 418b41d23a1bf978c06faea5313ba194650ac088 + - package: github.com/unrolled/render + ref: 26b4e3aac686940fe29521545afad9966ddfc80c + - package: github.com/flynn/go-shlex + ref: 3f9db97f856818214da2e1057f8ad84803971cff + - package: github.com/fsouza/go-dockerclient + ref: 0239034d42f665efa17fd77c39f891c2f9f32922 + - package: github.com/boltdb/bolt + ref: 51f99c862475898df9773747d3accd05a7ca33c1 + - package: gopkg.in/mgo.v2 + ref: 22287bab4379e1fbf6002fb4eb769888f3fb224c + subpackages: + - bson - package: github.com/docker/docker ref: f39987afe8d611407887b3094c03d6ba6a766a67 subpackages: @@ -125,6 +103,26 @@ import: - runconfig - utils - volume + - package: github.com/mailgun/timetools + ref: fd192d755b00c968d312d23f521eb0cdc6f66bd0 + - package: github.com/codegangsta/negroni + ref: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b + - package: gopkg.in/yaml.v2 + ref: 7ad95dd0798a40da1ccdff6dff35fd177b5edf40 + - package: github.com/opencontainers/runc + ref: 4ab132458fc3e9dbeea624153e0331952dc4c8d5 + subpackages: + - libcontainer/user + - package: github.com/gorilla/mux + ref: f15e0c49460fd49eebe2bcc8486b05d1bef68d3a + - package: github.com/BurntSushi/ty + ref: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74 + - package: github.com/elazarl/go-bindata-assetfs + ref: d5cac425555ca5cf00694df246e04f05e6a55150 + - package: github.com/BurntSushi/toml + ref: bd2bdf7f18f849530ef7a1c29a4290217cab32a1 + - package: gopkg.in/alecthomas/kingpin.v2 + ref: 639879d6110b1b0409410c7b737ef0bb18325038 - package: github.com/docker/libcompose ref: 79ef5d150f053a5b12f16b02d8844ed7cf33611a subpackages: @@ -135,13 +133,12 @@ import: - utils - package: github.com/cenkalti/backoff ref: 4dc77674aceaabba2c7e3da25d4c823edfb73f99 + - package: gopkg.in/fsnotify.v1 + ref: 96c060f6a6b7e0d6f75fddd10efeaca3e5d1bcb0 + - package: github.com/mailgun/manners + ref: 37136f736785d7c6aa3b9a27b4b2dd1028ca6d79 - package: github.com/gorilla/context ref: 215affda49addc4c8ef7e2534915df2c8c35c6cd - - package: github.com/docker/libkv - ref: 3732f7ff1b56057c3158f10bceb1e79133025373 - package: github.com/codahale/hdrhistogram ref: 954f16e8b9ef0e5d5189456aa4c1202758e04f17 - - package: github.com/mailgun/predicate - ref: cb0bff91a7ab7cf7571e661ff883fc997bc554a3 - - package: github.com/gorilla/mux - ref: f15e0c49460fd49eebe2bcc8486b05d1bef68d3a + - package: github.com/gorilla/websocket diff --git a/middlewares/websocket.go b/middlewares/websocket.go new file mode 100644 index 000000000..ac7d6a035 --- /dev/null +++ b/middlewares/websocket.go @@ -0,0 +1,52 @@ +/* +Copyright +*/ +package middlewares + +import ( + log "github.com/Sirupsen/logrus" + "github.com/mailgun/oxy/roundrobin" + "net/http" + "strings" + "time" +) + +type WebsocketUpgrader struct { + rr *roundrobin.RoundRobin +} + +func NewWebsocketUpgrader(rr *roundrobin.RoundRobin) *WebsocketUpgrader { + wu := WebsocketUpgrader{ + rr: rr, + } + return &wu +} + +func (u *WebsocketUpgrader) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // If request is websocket, serve with golang websocket server to do protocol handshake + if strings.Join(req.Header["Upgrade"], "") == "websocket" { + start := time.Now().UTC() + url, err := u.rr.NextServer() + if err != nil { + log.Errorf("Can't round robin in websocket middleware") + return + } + log.Debugf("Websocket forward to %s", url.String()) + NewProxy(url).ServeHTTP(w, req) + + if req.TLS != nil { + log.Debugf("Round trip: %v, duration: %v tls:version: %x, tls:resume:%t, tls:csuite:%x, tls:server:%v", + req.URL, time.Now().UTC().Sub(start), + req.TLS.Version, + req.TLS.DidResume, + req.TLS.CipherSuite, + req.TLS.ServerName) + } else { + log.Debugf("Round trip: %v, duration: %v", + req.URL, time.Now().UTC().Sub(start)) + } + + return + } + u.rr.ServeHTTP(w, req) +} diff --git a/middlewares/websocketproxy.go b/middlewares/websocketproxy.go new file mode 100644 index 000000000..abb82688f --- /dev/null +++ b/middlewares/websocketproxy.go @@ -0,0 +1,170 @@ +package middlewares + +import ( + "io" + "net" + "net/http" + "net/url" + "strings" + + log "github.com/Sirupsen/logrus" + "github.com/gorilla/websocket" +) + +// Original developpement made by https://github.com/koding/websocketproxy +var ( + // DefaultUpgrader specifies the parameters for upgrading an HTTP + // connection to a WebSocket connection. + DefaultUpgrader = &websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } + + // DefaultDialer is a dialer with all fields set to the default zero values. + DefaultDialer = websocket.DefaultDialer +) + +// WebsocketProxy is an HTTP Handler that takes an incoming WebSocket +// connection and proxies it to another server. +type WebsocketProxy struct { + // Backend returns the backend URL which the proxy uses to reverse proxy + // the incoming WebSocket connection. Request is the initial incoming and + // unmodified request. + Backend func(*http.Request) *url.URL + + // Upgrader specifies the parameters for upgrading a incoming HTTP + // connection to a WebSocket connection. If nil, DefaultUpgrader is used. + Upgrader *websocket.Upgrader + + // Dialer contains options for connecting to the backend WebSocket server. + // If nil, DefaultDialer is used. + Dialer *websocket.Dialer +} + +// ProxyHandler returns a new http.Handler interface that reverse proxies the +// request to the given target. +func ProxyHandler(target *url.URL) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + NewProxy(target).ServeHTTP(rw, req) + }) +} + +// NewProxy returns a new Websocket reverse proxy that rewrites the +// URL's to the scheme, host and base path provider in target. +func NewProxy(target *url.URL) *WebsocketProxy { + backend := func(r *http.Request) *url.URL { + // Shallow copy + u := *target + u.Fragment = r.URL.Fragment + u.Path = r.URL.Path + u.RawQuery = r.URL.RawQuery + rurl := u.String() + if strings.HasPrefix(rurl, "http") { + u.Scheme = "ws" + } + if strings.HasPrefix(rurl, "https") { + u.Scheme = "wss" + } + return &u + } + return &WebsocketProxy{Backend: backend} +} + +// ServeHTTP implements the http.Handler that proxies WebSocket connections. +func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if w.Backend == nil { + log.Println("websocketproxy: backend function is not defined") + http.Error(rw, "internal server error (code: 1)", http.StatusInternalServerError) + return + } + + backendURL := w.Backend(req) + if backendURL == nil { + log.Println("websocketproxy: backend URL is nil") + http.Error(rw, "internal server error (code: 2)", http.StatusInternalServerError) + return + } + + dialer := w.Dialer + if w.Dialer == nil { + dialer = DefaultDialer + } + + // Pass headers from the incoming request to the dialer to forward them to + // the final destinations. + requestHeader := http.Header{} + requestHeader.Add("Origin", req.Header.Get("Origin")) + for _, prot := range req.Header[http.CanonicalHeaderKey("Sec-WebSocket-Protocol")] { + requestHeader.Add("Sec-WebSocket-Protocol", prot) + } + for _, cookie := range req.Header[http.CanonicalHeaderKey("Cookie")] { + requestHeader.Add("Cookie", cookie) + } + + // Pass X-Forwarded-For headers too, code below is a part of + // httputil.ReverseProxy. See http://en.wikipedia.org/wiki/X-Forwarded-For + // for more information + // TODO: use RFC7239 http://tools.ietf.org/html/rfc7239 + if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { + // If we aren't the first proxy retain prior + // X-Forwarded-For information as a comma+space + // separated list and fold multiple headers into one. + if prior, ok := req.Header["X-Forwarded-For"]; ok { + clientIP = strings.Join(prior, ", ") + ", " + clientIP + } + requestHeader.Set("X-Forwarded-For", clientIP) + } + + // Set the originating protocol of the incoming HTTP request. The SSL might + // be terminated on our site and because we doing proxy adding this would + // be helpful for applications on the backend. + requestHeader.Set("X-Forwarded-Proto", "http") + if req.TLS != nil { + requestHeader.Set("X-Forwarded-Proto", "https") + } + + // Connect to the backend URL, also pass the headers we get from the requst + // together with the Forwarded headers we prepared above. + // TODO: support multiplexing on the same backend connection instead of + // opening a new TCP connection time for each request. This should be + // optional: + // http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-01 + connBackend, resp, err := dialer.Dial(backendURL.String(), nil) + if err != nil { + log.Printf("websocketproxy: couldn't dial to remote backend url %s, %s, %+v", backendURL.String(), err, resp) + return + } + defer connBackend.Close() + + upgrader := w.Upgrader + if w.Upgrader == nil { + upgrader = DefaultUpgrader + } + + // Only pass those headers to the upgrader. + upgradeHeader := http.Header{} + upgradeHeader.Set("Sec-WebSocket-Protocol", + resp.Header.Get(http.CanonicalHeaderKey("Sec-WebSocket-Protocol"))) + upgradeHeader.Set("Set-Cookie", + resp.Header.Get(http.CanonicalHeaderKey("Set-Cookie"))) + + // Now upgrade the existing incoming request to a WebSocket connection. + // Also pass the header that we gathered from the Dial handshake. + connPub, err := upgrader.Upgrade(rw, req, upgradeHeader) + if err != nil { + log.Printf("websocketproxy: couldn't upgrade %s\n", err) + return + } + defer connPub.Close() + + errc := make(chan error, 2) + cp := func(dst io.Writer, src io.Reader) { + _, err := io.Copy(dst, src) + errc <- err + } + + // Start our proxy now, everything is ready... + go cp(connBackend.UnderlyingConn(), connPub.UnderlyingConn()) + go cp(connPub.UnderlyingConn(), connBackend.UnderlyingConn()) + <-errc +} diff --git a/traefik.go b/traefik.go index fe023583f..8b63f3dbf 100644 --- a/traefik.go +++ b/traefik.go @@ -16,7 +16,6 @@ import ( "github.com/BurntSushi/toml" log "github.com/Sirupsen/logrus" "github.com/codegangsta/negroni" - "github.com/davecgh/go-spew/spew" "github.com/emilevauge/traefik/middlewares" "github.com/gorilla/mux" "github.com/mailgun/manners" @@ -92,7 +91,7 @@ func main() { } else { log.SetFormatter(&log.TextFormatter{FullTimestamp: true, DisableSorting: true}) } - log.Debugf("Global configuration loaded %s", spew.Sdump(globalConfiguration)) + log.Debugf("Global configuration loaded %+v", globalConfiguration) configurationRouter = LoadDefaultConfig(globalConfiguration) // listen new configurations from providers @@ -101,7 +100,6 @@ func main() { for { configMsg := <-configurationChan log.Infof("Configuration receveived from provider %s: %#v", configMsg.providerName, configMsg.configuration) - log.Debugf("Configuration %s", spew.Sdump(configMsg.configuration)) if configMsg.configuration == nil { log.Info("Skipping empty configuration") } else if reflect.DeepEqual(currentConfigurations[configMsg.providerName], configMsg.configuration) { @@ -195,7 +193,6 @@ func main() { func startServer(srv *manners.GracefulServer, globalConfiguration *GlobalConfiguration) { log.Info("Starting server") - log.Debugf("Server %s", spew.Sdump(srv)) if len(globalConfiguration.CertFile) > 0 && len(globalConfiguration.KeyFile) > 0 { err := srv.ListenAndServeTLS(globalConfiguration.CertFile, globalConfiguration.KeyFile) if err != nil { @@ -285,7 +282,7 @@ func LoadConfig(configurations configs, globalConfiguration *GlobalConfiguration } case wrr: log.Infof("Creating load-balancer wrr") - lb = rr + lb = middlewares.NewWebsocketUpgrader(rr) for serverName, server := range configuration.Backends[frontend.Backend].Servers { url, err := url.Parse(server.URL) if err != nil {