diff --git a/api/api.go b/api/api.go index 5a8f12d04..9d45f28a0 100644 --- a/api/api.go +++ b/api/api.go @@ -1,34 +1,57 @@ -package main // import "github.com/cloudinovasi/ui-for-docker" +package main import ( - "gopkg.in/alecthomas/kingpin.v2" + "crypto/tls" "log" "net/http" + "net/url" ) -// 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() +type ( + api struct { + endpoint *url.URL + bindAddress string + assetPath string + dataPath string + tlsConfig *tls.Config + } - configuration := newConfig(*swarm, *labels, *registries) - tlsFlags := newTLSFlags(*tlsverify, *tlscacert, *tlscert, *tlskey) + apiConfig struct { + Endpoint string + BindAddress string + AssetPath string + DataPath string + SwarmSupport bool + TLSEnabled bool + TLSCACertPath string + TLSCertPath string + TLSKeyPath string + } +) - handler := newHandler(*assets, *data, *endpoint, configuration, tlsFlags) - if err := http.ListenAndServe(*addr, handler); err != nil { +func (a *api) run(configuration *Config) { + handler := a.newHandler(configuration) + if err := http.ListenAndServe(a.bindAddress, handler); err != nil { log.Fatal(err) } } + +func newAPI(apiConfig apiConfig) *api { + endpointURL, err := url.Parse(apiConfig.Endpoint) + if err != nil { + log.Fatal(err) + } + + var tlsConfig *tls.Config + if apiConfig.TLSEnabled { + tlsConfig = newTLSConfig(apiConfig.TLSCACertPath, apiConfig.TLSCertPath, apiConfig.TLSKeyPath) + } + + return &api{ + endpoint: endpointURL, + bindAddress: apiConfig.BindAddress, + assetPath: apiConfig.AssetPath, + dataPath: apiConfig.DataPath, + tlsConfig: tlsConfig, + } +} diff --git a/api/config.go b/api/config.go index 4d4590daf..48fecd0bb 100644 --- a/api/config.go +++ b/api/config.go @@ -22,6 +22,6 @@ func newConfig(swarm bool, labels, registries pairList) Config { } // 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) +func configurationHandler(w http.ResponseWriter, r *http.Request, c *Config) { + json.NewEncoder(w).Encode(*c) } diff --git a/api/exec.go b/api/exec.go new file mode 100644 index 000000000..4b139aeb7 --- /dev/null +++ b/api/exec.go @@ -0,0 +1,24 @@ +package main + +import ( + "golang.org/x/net/websocket" + "log" +) + +// execContainer is used to create a websocket communication with an exec instance +func (a *api) execContainer(ws *websocket.Conn) { + qry := ws.Request().URL.Query() + execID := qry.Get("id") + + var host string + if a.endpoint.Scheme == "tcp" { + host = a.endpoint.Host + } else if a.endpoint.Scheme == "unix" { + host = a.endpoint.Path + } + + if err := hijack(host, a.endpoint.Scheme, "POST", "/exec/"+execID+"/start", a.tlsConfig, true, ws, ws, ws, nil, nil); err != nil { + log.Fatalf("error during hijack: %s", err) + return + } +} diff --git a/api/flags.go b/api/flags.go index 938613daa..47578a748 100644 --- a/api/flags.go +++ b/api/flags.go @@ -6,14 +6,6 @@ import ( "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"` @@ -52,13 +44,3 @@ func pairs(s kingpin.Settings) (target *[]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 index 9a6ca1059..b38417bb4 100644 --- a/api/handler.go +++ b/api/handler.go @@ -1,6 +1,7 @@ package main import ( + "golang.org/x/net/websocket" "log" "net/http" "net/http/httputil" @@ -9,22 +10,18 @@ import ( ) // newHandler creates a new http.Handler with CSRF protection -func newHandler(dir string, d string, e string, c Config, tlsFlags TLSFlags) http.Handler { +func (a *api) newHandler(c *Config) http.Handler { var ( mux = http.NewServeMux() - fileHandler = http.FileServer(http.Dir(dir)) + fileHandler = http.FileServer(http.Dir(a.assetPath)) ) - u, perr := url.Parse(e) - if perr != nil { - log.Fatal(perr) - } + handler := a.newAPIHandler() + CSRFHandler := newCSRFHandler(a.dataPath) - handler := newAPIHandler(u, tlsFlags) - CSRFHandler := newCSRFHandler(d) - - mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler)) mux.Handle("/", fileHandler) + mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler)) + mux.Handle("/ws/exec", websocket.Handler(a.execContainer)) mux.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) { configurationHandler(w, r, c) }) @@ -32,47 +29,47 @@ func newHandler(dir string, d string, e string, c Config, tlsFlags TLSFlags) htt } // newAPIHandler initializes a new http.Handler based on the URL scheme -func newAPIHandler(u *url.URL, tlsFlags TLSFlags) http.Handler { +func (a *api) newAPIHandler() http.Handler { var handler http.Handler - if u.Scheme == "tcp" { - if tlsFlags.tls { - handler = newTCPHandlerWithTLS(u, tlsFlags) + var endpoint = *a.endpoint + if endpoint.Scheme == "tcp" { + if a.tlsConfig != nil { + handler = a.newTCPHandlerWithTLS(&endpoint) } else { - handler = newTCPHandler(u) + handler = a.newTCPHandler(&endpoint) } - } else if u.Scheme == "unix" { - socketPath := u.Path + } else if endpoint.Scheme == "unix" { + socketPath := endpoint.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) + handler = a.newUnixHandler(socketPath) } else { - log.Fatalf("Bad Docker enpoint: %s. Only unix:// and tcp:// are supported.", u) + log.Fatalf("Bad Docker enpoint: %v. Only unix:// and tcp:// are supported.", &endpoint) } return handler } // newUnixHandler initializes a new UnixHandler -func newUnixHandler(e string) http.Handler { +func (a *api) newUnixHandler(e string) http.Handler { return &unixHandler{e} } // newTCPHandler initializes a HTTP reverse proxy -func newTCPHandler(u *url.URL) http.Handler { +func (a *api) 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 { +func (a *api) newTCPHandlerWithTLS(u *url.URL) http.Handler { u.Scheme = "https" - var tlsConfig = newTLSConfig(tlsFlags) proxy := httputil.NewSingleHostReverseProxy(u) proxy.Transport = &http.Transport{ - TLSClientConfig: tlsConfig, + TLSClientConfig: a.tlsConfig, } return proxy } diff --git a/api/hijack.go b/api/hijack.go new file mode 100644 index 000000000..ff6cd9071 --- /dev/null +++ b/api/hijack.go @@ -0,0 +1,123 @@ +package main + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/http/httputil" + "time" +) + +type execConfig struct { + Tty bool + Detach bool +} + +// hijack allows to upgrade an HTTP connection to a TCP connection +// It redirects IO streams for stdin, stdout and stderr to a websocket +func hijack(addr, scheme, method, path string, tlsConfig *tls.Config, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer, data interface{}) error { + execConfig := &execConfig{ + Tty: true, + Detach: false, + } + + buf, err := json.Marshal(execConfig) + if err != nil { + return fmt.Errorf("error marshaling exec config: %s", err) + } + + rdr := bytes.NewReader(buf) + + req, err := http.NewRequest(method, path, rdr) + if err != nil { + return fmt.Errorf("error during hijack request: %s", err) + } + + req.Header.Set("User-Agent", "Docker-Client") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Connection", "Upgrade") + req.Header.Set("Upgrade", "tcp") + req.Host = addr + + var ( + dial net.Conn + dialErr error + ) + + if tlsConfig == nil { + dial, dialErr = net.Dial(scheme, addr) + } else { + dial, dialErr = tls.Dial(scheme, addr, tlsConfig) + } + + if dialErr != nil { + return dialErr + } + + // When we set up a TCP connection for hijack, there could be long periods + // of inactivity (a long running command with no output) that in certain + // network setups may cause ECONNTIMEOUT, leaving the client in an unknown + // state. Setting TCP KeepAlive on the socket connection will prohibit + // ECONNTIMEOUT unless the socket connection truly is broken + if tcpConn, ok := dial.(*net.TCPConn); ok { + tcpConn.SetKeepAlive(true) + tcpConn.SetKeepAlivePeriod(30 * time.Second) + } + if err != nil { + return err + } + clientconn := httputil.NewClientConn(dial, nil) + defer clientconn.Close() + + // Server hijacks the connection, error 'connection closed' expected + clientconn.Do(req) + + rwc, br := clientconn.Hijack() + defer rwc.Close() + + if started != nil { + started <- rwc + } + + var receiveStdout chan error + + if stdout != nil || stderr != nil { + go func() (err error) { + if setRawTerminal && stdout != nil { + _, err = io.Copy(stdout, br) + } + return err + }() + } + + go func() error { + if in != nil { + io.Copy(rwc, in) + } + + if conn, ok := rwc.(interface { + CloseWrite() error + }); ok { + if err := conn.CloseWrite(); err != nil { + } + } + return nil + }() + + if stdout != nil || stderr != nil { + if err := <-receiveStdout; err != nil { + return err + } + } + go func() { + for { + fmt.Println(br) + } + }() + + return nil +} diff --git a/api/main.go b/api/main.go new file mode 100644 index 000000000..d86c74a8d --- /dev/null +++ b/api/main.go @@ -0,0 +1,45 @@ +package main // import "github.com/cloudinovasi/ui-for-docker" + +import ( + "gopkg.in/alecthomas/kingpin.v2" +) + +// 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() + 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() + swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool() + 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() + + apiConfig := apiConfig{ + Endpoint: *endpoint, + BindAddress: *addr, + AssetPath: *assets, + DataPath: *data, + SwarmSupport: *swarm, + TLSEnabled: *tlsverify, + TLSCACertPath: *tlscacert, + TLSCertPath: *tlscert, + TLSKeyPath: *tlskey, + } + + configuration := &Config{ + Swarm: *swarm, + HiddenLabels: *labels, + Registries: *registries, + } + + api := newAPI(apiConfig) + api.run(configuration) +} diff --git a/api/ssl.go b/api/ssl.go index 6d86db5b8..89f76e85e 100644 --- a/api/ssl.go +++ b/api/ssl.go @@ -7,13 +7,13 @@ import ( "log" ) -// newTLSConfig initializes a tls.Config from the TLS flags -func newTLSConfig(tlsFlags TLSFlags) *tls.Config { - cert, err := tls.LoadX509KeyPair(tlsFlags.certPath, tlsFlags.keyPath) +// newTLSConfig initializes a tls.Config using a CA certificate, a certificate and a key +func newTLSConfig(caCertPath, certPath, keyPath string) *tls.Config { + cert, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { log.Fatal(err) } - caCert, err := ioutil.ReadFile(tlsFlags.caPath) + caCert, err := ioutil.ReadFile(caCertPath) if err != nil { log.Fatal(err) } diff --git a/app/app.js b/app/app.js index 6ea224302..51cde3982 100644 --- a/app/app.js +++ b/app/app.js @@ -9,13 +9,14 @@ angular.module('uifordocker', [ 'uifordocker.filters', 'dashboard', 'container', + 'containerConsole', + 'containerLogs', 'containers', 'createContainer', 'docker', 'events', 'images', 'image', - 'containerLogs', 'stats', 'swarm', 'network', @@ -55,6 +56,11 @@ angular.module('uifordocker', [ templateUrl: 'app/components/containerLogs/containerlogs.html', controller: 'ContainerLogsController' }) + .state('console', { + url: "^/containers/:id/console", + templateUrl: 'app/components/containerConsole/containerConsole.html', + controller: 'ContainerConsoleController' + }) .state('actions', { abstract: true, url: "/actions", diff --git a/app/components/container/container.html b/app/components/container/container.html index d3e334f28..bc76f31cc 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -64,6 +64,7 @@