diff --git a/api/api.go b/api/api.go
new file mode 100644
index 000000000..0047e622e
--- /dev/null
+++ b/api/api.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+ "crypto/tls"
+ "log"
+ "net/http"
+ "net/url"
+)
+
+type (
+ api struct {
+ endpoint *url.URL
+ bindAddress string
+ assetPath string
+ dataPath string
+ tlsConfig *tls.Config
+ }
+
+ apiConfig struct {
+ Endpoint string
+ BindAddress string
+ AssetPath string
+ DataPath string
+ SwarmSupport bool
+ TLSEnabled bool
+ TLSCACertPath string
+ TLSCertPath string
+ TLSKeyPath string
+ }
+)
+
+func (a *api) run(settings *Settings) {
+ handler := a.newHandler(settings)
+ 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/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/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
new file mode 100644
index 000000000..47578a748
--- /dev/null
+++ b/api/flags.go
@@ -0,0 +1,46 @@
+package main
+
+import (
+ "fmt"
+ "gopkg.in/alecthomas/kingpin.v2"
+ "strings"
+)
+
+// 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
+}
diff --git a/api/handler.go b/api/handler.go
new file mode 100644
index 000000000..3e48f258e
--- /dev/null
+++ b/api/handler.go
@@ -0,0 +1,75 @@
+package main
+
+import (
+ "golang.org/x/net/websocket"
+ "log"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "os"
+)
+
+// newHandler creates a new http.Handler with CSRF protection
+func (a *api) newHandler(settings *Settings) http.Handler {
+ var (
+ mux = http.NewServeMux()
+ fileHandler = http.FileServer(http.Dir(a.assetPath))
+ )
+
+ handler := a.newAPIHandler()
+ CSRFHandler := newCSRFHandler(a.dataPath)
+
+ mux.Handle("/", fileHandler)
+ mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler))
+ mux.Handle("/ws/exec", websocket.Handler(a.execContainer))
+ mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
+ settingsHandler(w, r, settings)
+ })
+ return CSRFHandler(newCSRFWrapper(mux))
+}
+
+// newAPIHandler initializes a new http.Handler based on the URL scheme
+func (a *api) newAPIHandler() http.Handler {
+ var handler http.Handler
+ var endpoint = *a.endpoint
+ if endpoint.Scheme == "tcp" {
+ if a.tlsConfig != nil {
+ handler = a.newTCPHandlerWithTLS(&endpoint)
+ } else {
+ handler = a.newTCPHandler(&endpoint)
+ }
+ } 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 = a.newUnixHandler(socketPath)
+ } else {
+ log.Fatalf("Bad Docker enpoint: %v. Only unix:// and tcp:// are supported.", &endpoint)
+ }
+ return handler
+}
+
+// newUnixHandler initializes a new UnixHandler
+func (a *api) newUnixHandler(e string) http.Handler {
+ return &unixHandler{e}
+}
+
+// newTCPHandler initializes a HTTP reverse proxy
+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 (a *api) newTCPHandlerWithTLS(u *url.URL) http.Handler {
+ u.Scheme = "https"
+ proxy := httputil.NewSingleHostReverseProxy(u)
+ proxy.Transport = &http.Transport{
+ 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..a2c5a0f15
--- /dev/null
+++ b/api/main.go
@@ -0,0 +1,43 @@
+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.6.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'))
+ )
+ kingpin.Parse()
+
+ apiConfig := apiConfig{
+ Endpoint: *endpoint,
+ BindAddress: *addr,
+ AssetPath: *assets,
+ DataPath: *data,
+ SwarmSupport: *swarm,
+ TLSEnabled: *tlsverify,
+ TLSCACertPath: *tlscacert,
+ TLSCertPath: *tlscert,
+ TLSKeyPath: *tlskey,
+ }
+
+ settings := &Settings{
+ Swarm: *swarm,
+ HiddenLabels: *labels,
+ }
+
+ api := newAPI(apiConfig)
+ api.run(settings)
+}
diff --git a/api/settings.go b/api/settings.go
new file mode 100644
index 000000000..801904f61
--- /dev/null
+++ b/api/settings.go
@@ -0,0 +1,17 @@
+package main
+
+import (
+ "encoding/json"
+ "net/http"
+)
+
+// Settings defines the settings available under the /settings endpoint
+type Settings struct {
+ Swarm bool `json:"swarm"`
+ HiddenLabels pairList `json:"hiddenLabels"`
+}
+
+// configurationHandler defines a handler function used to encode the configuration in JSON
+func settingsHandler(w http.ResponseWriter, r *http.Request, s *Settings) {
+ json.NewEncoder(w).Encode(*s)
+}
diff --git a/api/ssl.go b/api/ssl.go
new file mode 100644
index 000000000..89f76e85e
--- /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 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(caCertPath)
+ 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/app/app.js b/app/app.js
index 0b7d4fa58..206161c75 100644
--- a/app/app.js
+++ b/app/app.js
@@ -5,16 +5,18 @@ angular.module('uifordocker', [
'ui.select',
'ngCookies',
'ngSanitize',
- 'dockerui.services',
- 'dockerui.filters',
+ 'uifordocker.services',
+ 'uifordocker.filters',
'dashboard',
'container',
+ 'containerConsole',
+ 'containerLogs',
'containers',
'createContainer',
'docker',
+ 'events',
'images',
'image',
- 'containerLogs',
'stats',
'swarm',
'network',
@@ -56,6 +58,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",
@@ -86,6 +93,11 @@ angular.module('uifordocker', [
templateUrl: 'app/components/docker/docker.html',
controller: 'DockerController'
})
+ .state('events', {
+ url: '/events/',
+ templateUrl: 'app/components/events/events.html',
+ controller: 'EventsController'
+ })
.state('images', {
url: '/images/',
templateUrl: 'app/components/images/images.html',
@@ -143,5 +155,5 @@ angular.module('uifordocker', [
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
.constant('DOCKER_ENDPOINT', 'dockerapi')
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243
- .constant('CONFIG_ENDPOINT', '/config')
- .constant('UI_VERSION', 'v1.5.0');
+ .constant('CONFIG_ENDPOINT', 'settings')
+ .constant('UI_VERSION', 'v1.6.0');
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 @@