diff --git a/api/api.go b/api/api.go new file mode 100644 index 000000000..5a8f12d04 --- /dev/null +++ b/api/api.go @@ -0,0 +1,34 @@ +package main // import "github.com/cloudinovasi/ui-for-docker" + +import ( + "gopkg.in/alecthomas/kingpin.v2" + "log" + "net/http" +) + +// main is the entry point of the program +func main() { + kingpin.Version("1.5.0") + var ( + endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String() + addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String() + assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String() + data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String() + swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool() + tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool() + tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String() + tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String() + tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String() + labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')) + registries = pairs(kingpin.Flag("registries", "Supported Docker registries").Short('r')) + ) + kingpin.Parse() + + configuration := newConfig(*swarm, *labels, *registries) + tlsFlags := newTLSFlags(*tlsverify, *tlscacert, *tlscert, *tlskey) + + handler := newHandler(*assets, *data, *endpoint, configuration, tlsFlags) + if err := http.ListenAndServe(*addr, handler); err != nil { + log.Fatal(err) + } +} diff --git a/api/config.go b/api/config.go new file mode 100644 index 000000000..4d4590daf --- /dev/null +++ b/api/config.go @@ -0,0 +1,27 @@ +package main + +import ( + "encoding/json" + "net/http" +) + +// Config defines the configuration available under the /config endpoint +type Config struct { + Swarm bool `json:"swarm"` + HiddenLabels pairList `json:"hiddenLabels"` + Registries pairList `json:"registries"` +} + +// newConfig creates a new Config from command flags +func newConfig(swarm bool, labels, registries pairList) Config { + return Config{ + Swarm: swarm, + HiddenLabels: labels, + Registries: registries, + } +} + +// configurationHandler defines a handler function used to encode the configuration in JSON +func configurationHandler(w http.ResponseWriter, r *http.Request, c Config) { + json.NewEncoder(w).Encode(c) +} diff --git a/api/csrf.go b/api/csrf.go new file mode 100644 index 000000000..4377ee343 --- /dev/null +++ b/api/csrf.go @@ -0,0 +1,48 @@ +package main + +import ( + "github.com/gorilla/csrf" + "github.com/gorilla/securecookie" + "io/ioutil" + "log" + "net/http" +) + +const keyFile = "authKey.dat" + +// newAuthKey reuses an existing CSRF authkey if present or generates a new one +func newAuthKey(path string) []byte { + var authKey []byte + authKeyPath := path + "/" + keyFile + data, err := ioutil.ReadFile(authKeyPath) + if err != nil { + log.Print("Unable to find an existing CSRF auth key. Generating a new key.") + authKey = securecookie.GenerateRandomKey(32) + err := ioutil.WriteFile(authKeyPath, authKey, 0644) + if err != nil { + log.Fatal("Unable to persist CSRF auth key.") + log.Fatal(err) + } + } else { + authKey = data + } + return authKey +} + +// newCSRF initializes a new CSRF handler +func newCSRFHandler(keyPath string) func(h http.Handler) http.Handler { + authKey := newAuthKey(keyPath) + return csrf.Protect( + authKey, + csrf.HttpOnly(false), + csrf.Secure(false), + ) +} + +// newCSRFWrapper wraps a http.Handler to add the CSRF token +func newCSRFWrapper(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-CSRF-Token", csrf.Token(r)) + h.ServeHTTP(w, r) + }) +} diff --git a/api/flags.go b/api/flags.go new file mode 100644 index 000000000..938613daa --- /dev/null +++ b/api/flags.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "gopkg.in/alecthomas/kingpin.v2" + "strings" +) + +// TLSFlags defines all the flags associated to the SSL configuration +type TLSFlags struct { + tls bool + caPath string + certPath string + keyPath string +} + +// pair defines a key/value pair +type pair struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// pairList defines an array of Label +type pairList []pair + +// Set implementation for Labels +func (l *pairList) Set(value string) error { + parts := strings.SplitN(value, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("expected NAME=VALUE got '%s'", value) + } + p := new(pair) + p.Name = parts[0] + p.Value = parts[1] + *l = append(*l, *p) + return nil +} + +// String implementation for Labels +func (l *pairList) String() string { + return "" +} + +// IsCumulative implementation for Labels +func (l *pairList) IsCumulative() bool { + return true +} + +// LabelParser defines a custom parser for Labels flags +func pairs(s kingpin.Settings) (target *[]pair) { + target = new([]pair) + s.SetValue((*pairList)(target)) + return +} + +// newTLSFlags creates a new TLSFlags from command flags +func newTLSFlags(tls bool, cacert string, cert string, key string) TLSFlags { + return TLSFlags{ + tls: tls, + caPath: cacert, + certPath: cert, + keyPath: key, + } +} diff --git a/api/handler.go b/api/handler.go new file mode 100644 index 000000000..9a6ca1059 --- /dev/null +++ b/api/handler.go @@ -0,0 +1,78 @@ +package main + +import ( + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" +) + +// newHandler creates a new http.Handler with CSRF protection +func newHandler(dir string, d string, e string, c Config, tlsFlags TLSFlags) http.Handler { + var ( + mux = http.NewServeMux() + fileHandler = http.FileServer(http.Dir(dir)) + ) + + u, perr := url.Parse(e) + if perr != nil { + log.Fatal(perr) + } + + handler := newAPIHandler(u, tlsFlags) + CSRFHandler := newCSRFHandler(d) + + mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler)) + mux.Handle("/", fileHandler) + mux.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) { + configurationHandler(w, r, c) + }) + return CSRFHandler(newCSRFWrapper(mux)) +} + +// newAPIHandler initializes a new http.Handler based on the URL scheme +func newAPIHandler(u *url.URL, tlsFlags TLSFlags) http.Handler { + var handler http.Handler + if u.Scheme == "tcp" { + if tlsFlags.tls { + handler = newTCPHandlerWithTLS(u, tlsFlags) + } else { + handler = newTCPHandler(u) + } + } else if u.Scheme == "unix" { + socketPath := u.Path + if _, err := os.Stat(socketPath); err != nil { + if os.IsNotExist(err) { + log.Fatalf("Unix socket %s does not exist", socketPath) + } + log.Fatal(err) + } + handler = newUnixHandler(socketPath) + } else { + log.Fatalf("Bad Docker enpoint: %s. Only unix:// and tcp:// are supported.", u) + } + return handler +} + +// newUnixHandler initializes a new UnixHandler +func newUnixHandler(e string) http.Handler { + return &unixHandler{e} +} + +// newTCPHandler initializes a HTTP reverse proxy +func newTCPHandler(u *url.URL) http.Handler { + u.Scheme = "http" + return httputil.NewSingleHostReverseProxy(u) +} + +// newTCPHandlerWithL initializes a HTTPS reverse proxy with a TLS configuration +func newTCPHandlerWithTLS(u *url.URL, tlsFlags TLSFlags) http.Handler { + u.Scheme = "https" + var tlsConfig = newTLSConfig(tlsFlags) + proxy := httputil.NewSingleHostReverseProxy(u) + proxy.Transport = &http.Transport{ + TLSClientConfig: tlsConfig, + } + return proxy +} diff --git a/api/ssl.go b/api/ssl.go new file mode 100644 index 000000000..6d86db5b8 --- /dev/null +++ b/api/ssl.go @@ -0,0 +1,27 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "io/ioutil" + "log" +) + +// newTLSConfig initializes a tls.Config from the TLS flags +func newTLSConfig(tlsFlags TLSFlags) *tls.Config { + cert, err := tls.LoadX509KeyPair(tlsFlags.certPath, tlsFlags.keyPath) + if err != nil { + log.Fatal(err) + } + caCert, err := ioutil.ReadFile(tlsFlags.caPath) + if err != nil { + log.Fatal(err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + } + return tlsConfig +} diff --git a/api/unix_handler.go b/api/unix_handler.go new file mode 100644 index 000000000..15a5119d3 --- /dev/null +++ b/api/unix_handler.go @@ -0,0 +1,47 @@ +package main + +import ( + "io" + "log" + "net" + "net/http" + "net/http/httputil" +) + +// unixHandler defines a handler holding the path to a socket under UNIX +type unixHandler struct { + path string +} + +// ServeHTTP implementation for unixHandler +func (h *unixHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + conn, err := net.Dial("unix", h.path) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Println(err) + return + } + c := httputil.NewClientConn(conn, nil) + defer c.Close() + + res, err := c.Do(r) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Println(err) + return + } + defer res.Body.Close() + + copyHeader(w.Header(), res.Header) + if _, err := io.Copy(w, res.Body); err != nil { + log.Println(err) + } +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} diff --git a/dockerui.go b/dockerui.go deleted file mode 100644 index c8c8ea497..000000000 --- a/dockerui.go +++ /dev/null @@ -1,246 +0,0 @@ -package main // import "github.com/cloudinovasi/ui-for-docker" - -import ( - "io" - "log" - "net" - "net/http" - "net/http/httputil" - "net/url" - "encoding/json" - "os" - "strings" - "github.com/gorilla/csrf" - "io/ioutil" - "fmt" - "github.com/gorilla/securecookie" - "gopkg.in/alecthomas/kingpin.v2" - "crypto/tls" - "crypto/x509" -) - -var ( - endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String() - addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String() - assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String() - data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String() - swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool() - registries = LabelParser(kingpin.Flag("registries", "Supported Docker registries").Short('r')) - tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool() - tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String() - tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String() - tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String() - labels = LabelParser(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')) - authKey []byte - authKeyFile = "authKey.dat" -) - -type UnixHandler struct { - path string -} - -type TlsFlags struct { - tls bool - caPath string - certPath string - keyPath string -} - -type Config struct { - Swarm bool `json:"swarm"` - HiddenLabels Labels `json:"hiddenLabels"` - Registries Labels `json:"registries"` -} - -type Label struct { - Name string `json:"name"` - Value string `json:"value"` -} - -type Labels []Label - -func (l *Labels) Set(value string) error { - parts := strings.SplitN(value, "=", 2) - if len(parts) != 2 { - return fmt.Errorf("expected NAME=VALUE got '%s'", value) - } - label := new(Label) - label.Name = parts[0] - label.Value = parts[1] - *l = append(*l, *label) - return nil -} - -func (l *Labels) String() string { - return "" -} - -func (l *Labels) IsCumulative() bool { - return true -} - -func LabelParser(s kingpin.Settings) (target *[]Label) { - target = new([]Label) - s.SetValue((*Labels)(target)) - return -} - -func (h *UnixHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - conn, err := net.Dial("unix", h.path) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - log.Println(err) - return - } - c := httputil.NewClientConn(conn, nil) - defer c.Close() - - res, err := c.Do(r) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - log.Println(err) - return - } - defer res.Body.Close() - - copyHeader(w.Header(), res.Header) - if _, err := io.Copy(w, res.Body); err != nil { - log.Println(err) - } -} - -func copyHeader(dst, src http.Header) { - for k, vv := range src { - for _, v := range vv { - dst.Add(k, v) - } - } -} - -func configurationHandler(w http.ResponseWriter, r *http.Request, c Config) { - json.NewEncoder(w).Encode(c) -} - -func createTcpHandler(u *url.URL) http.Handler { - u.Scheme = "http"; - return httputil.NewSingleHostReverseProxy(u) -} - -func createTlsConfig(tlsFlags TlsFlags) *tls.Config { - cert, err := tls.LoadX509KeyPair(tlsFlags.certPath, tlsFlags.keyPath) - if err != nil { - log.Fatal(err) - } - caCert, err := ioutil.ReadFile(tlsFlags.caPath) - if err != nil { - log.Fatal(err) - } - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(caCert) - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{cert}, - RootCAs: caCertPool, - } - return tlsConfig; -} - -func createTcpHandlerWithTLS(u *url.URL, tlsFlags TlsFlags) http.Handler { - u.Scheme = "https"; - var tlsConfig = createTlsConfig(tlsFlags) - proxy := httputil.NewSingleHostReverseProxy(u) - proxy.Transport = &http.Transport{ - TLSClientConfig: tlsConfig, - } - return proxy; -} - -func createUnixHandler(e string) http.Handler { - return &UnixHandler{e} -} - -func createHandler(dir string, d string, e string, c Config, tlsFlags TlsFlags) http.Handler { - var ( - mux = http.NewServeMux() - fileHandler = http.FileServer(http.Dir(dir)) - h http.Handler - ) - u, perr := url.Parse(e) - if perr != nil { - log.Fatal(perr) - } - if u.Scheme == "tcp" { - if tlsFlags.tls { - h = createTcpHandlerWithTLS(u, tlsFlags) - } else { - h = createTcpHandler(u) - } - } else if u.Scheme == "unix" { - var socketPath = u.Path - if _, err := os.Stat(socketPath); err != nil { - if os.IsNotExist(err) { - log.Fatalf("unix socket %s does not exist", socketPath) - } - log.Fatal(err) - } - h = createUnixHandler(socketPath) - } else { - log.Fatalf("Bad Docker enpoint: %s. Only unix:// and tcp:// are supported.", e) - } - - // Use existing csrf authKey if present or generate a new one. - var authKeyPath = d + "/" + authKeyFile - dat, err := ioutil.ReadFile(authKeyPath) - if err != nil { - fmt.Println(err) - authKey = securecookie.GenerateRandomKey(32) - err := ioutil.WriteFile(authKeyPath, authKey, 0644) - if err != nil { - fmt.Println("unable to persist auth key", err) - } - } else { - authKey = dat - } - - CSRF := csrf.Protect( - authKey, - csrf.HttpOnly(false), - csrf.Secure(false), - ) - - mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", h)) - mux.Handle("/", fileHandler) - mux.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) { - configurationHandler(w, r, c) - }) - return CSRF(csrfWrapper(mux)) -} - -func csrfWrapper(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-CSRF-Token", csrf.Token(r)) - h.ServeHTTP(w, r) - }) -} - -func main() { - kingpin.Version("1.5.0") - kingpin.Parse() - - configuration := Config{ - Swarm: *swarm, - HiddenLabels: *labels, - Registries: *registries, - } - - tlsFlags := TlsFlags{ - tls: *tlsverify, - caPath: *tlscacert, - certPath: *tlscert, - keyPath: *tlskey, - } - - handler := createHandler(*assets, *data, *endpoint, configuration, tlsFlags) - if err := http.ListenAndServe(*addr, handler); err != nil { - log.Fatal(err) - } -} diff --git a/gruntFile.js b/gruntFile.js index 7482b3ecb..94e290422 100644 --- a/gruntFile.js +++ b/gruntFile.js @@ -254,10 +254,10 @@ module.exports = function (grunt) { }, buildBinary: { command: [ - 'docker run --rm -v $(pwd):/src centurylink/golang-builder', - 'shasum ui-for-docker > ui-for-docker-checksum.txt', + 'docker run --rm -v $(pwd)/api:/src centurylink/golang-builder', + 'shasum api/ui-for-docker > ui-for-docker-checksum.txt', 'mkdir -p dist', - 'mv ui-for-docker dist/' + 'mv api/ui-for-docker dist/' ].join(' && ') }, run: {