From 8df6373327802217c2bcc3152d384e2ceee69687 Mon Sep 17 00:00:00 2001 From: Jonathan Leibiusky Date: Fri, 12 May 2017 16:20:09 -0300 Subject: [PATCH] HTTPS and File Uploads (#139) * Add a few fixes * Use CopyToContainer instead of bind mounts * Remove a local compose file * Changes according to the comments * Rebase with master --- .gitignore | 1 + Dockerfile.dind | 2 + api.go | 29 ++--------- handlers/file_upload.go | 47 ++++++++++++++++++ handlers/new_instance.go | 4 +- handlers/tlsproxy.go | 90 ++++++++++++++++++++++++++++++++++ services/docker.go | 102 +++++++++++++++++++++++++++++++-------- services/instance.go | 46 +++++++++++++++--- 8 files changed, 267 insertions(+), 54 deletions(-) create mode 100644 handlers/file_upload.go create mode 100644 handlers/tlsproxy.go diff --git a/.gitignore b/.gitignore index 088f7fb..2e785fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ play-with-docker node_modules +docker-compose.single.yml diff --git a/Dockerfile.dind b/Dockerfile.dind index 15d0d06..bcb49ac 100644 --- a/Dockerfile.dind +++ b/Dockerfile.dind @@ -38,6 +38,8 @@ ENV DOCKER_STORAGE_DRIVER=$docker_storage_driver # Move to our home WORKDIR /root +RUN mkdir -p /var/run/pwd/certs && mkdir -p /var/run/pwd/uploads + # Remove IPv6 alias for localhost and start docker in the background ... CMD cat /etc/hosts >/etc/hosts.bak && \ sed 's/^::1.*//' /etc/hosts.bak > /etc/hosts && \ diff --git a/api.go b/api.go index 630a250..e411224 100644 --- a/api.go +++ b/api.go @@ -1,12 +1,9 @@ package main import ( - "crypto/tls" - "fmt" "log" "net/http" "os" - "strings" gh "github.com/gorilla/handlers" "github.com/gorilla/mux" @@ -71,9 +68,9 @@ func main() { corsRouter.HandleFunc("/instances/images", handlers.GetInstanceImages).Methods("GET") corsRouter.HandleFunc("/sessions/{sessionId}", handlers.GetSession).Methods("GET") corsRouter.HandleFunc("/sessions/{sessionId}/instances", handlers.NewInstance).Methods("POST") + corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/uploads", handlers.FileUpload).Methods("POST") corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}", handlers.DeleteInstance).Methods("DELETE") corsRouter.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/exec", handlers.Exec).Methods("POST") - r.HandleFunc("/sessions/{sessionId}/instances/{instanceName}/keys", handlers.SetKeys).Methods("POST") h := func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./www/index.html") @@ -115,26 +112,6 @@ func main() { log.Fatal(http.ListenAndServe("0.0.0.0:"+config.PortNumber, n)) }() - ssl := mux.NewRouter() - sslProxyHandler := handlers.NewSSLDaemonHandler() - ssl.Host(`{subdomain:.*}{node:pwd[0-9]{1,3}_[0-9]{1,3}_[0-9]{1,3}_[0-9]{1,3}}-2375.{tld:.*}`).Handler(sslProxyHandler) - log.Println("Listening TLS on port " + config.SSLPortNumber) - - s := &http.Server{Addr: "0.0.0.0:" + config.SSLPortNumber, Handler: ssl} - s.TLSConfig = &tls.Config{} - s.TLSConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - - chunks := strings.Split(clientHello.ServerName, ".") - chunks = strings.Split(chunks[0], "-") - ip := strings.Replace(strings.TrimPrefix(chunks[0], "pwd"), "_", ".", -1) - i := services.FindInstanceByIP(ip) - if i == nil { - return nil, fmt.Errorf("Instance %s doesn't exist", clientHello.ServerName) - } - if i.GetCertificate() == nil { - return nil, fmt.Errorf("Instance %s doesn't have a certificate", clientHello.ServerName) - } - return i.GetCertificate(), nil - } - log.Fatal(s.ListenAndServeTLS("", "")) + // Now listen for TLS connections that need to be proxied + handlers.StartTLSProxy(config.SSLPortNumber) } diff --git a/handlers/file_upload.go b/handlers/file_upload.go new file mode 100644 index 0000000..cd8099d --- /dev/null +++ b/handlers/file_upload.go @@ -0,0 +1,47 @@ +package handlers + +import ( + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/play-with-docker/play-with-docker/services" +) + +func FileUpload(rw http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + sessionId := vars["sessionId"] + instanceName := vars["instanceName"] + + s := services.GetSession(sessionId) + i := services.GetInstance(s, instanceName) + + // allow up to 32 MB which is the default + + // has a url query parameter, ignore body + if url := req.URL.Query().Get("url"); url != "" { + err := i.UploadFromURL(req.URL.Query().Get("url")) + if err != nil { + log.Println(err) + rw.WriteHeader(http.StatusInternalServerError) + return + } + rw.WriteHeader(http.StatusOK) + return + } else { + // This is for multipart upload + log.Println("Not implemented yet") + + /* + err := req.ParseMultipartForm(32 << 20) + if err != nil { + log.Println(err) + rw.WriteHeader(http.StatusBadRequest) + return + } + */ + rw.WriteHeader(http.StatusInternalServerError) + return + } + +} diff --git a/handlers/new_instance.go b/handlers/new_instance.go index 789f2a5..b9d959e 100644 --- a/handlers/new_instance.go +++ b/handlers/new_instance.go @@ -13,7 +13,7 @@ func NewInstance(rw http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) sessionId := vars["sessionId"] - body := struct{ ImageName, Alias string }{} + body := services.InstanceConfig{} json.NewDecoder(req.Body).Decode(&body) @@ -26,7 +26,7 @@ func NewInstance(rw http.ResponseWriter, req *http.Request) { return } - i, err := services.NewInstance(s, body.ImageName, body.Alias) + i, err := services.NewInstance(s, body) if err != nil { log.Println(err) rw.WriteHeader(http.StatusInternalServerError) diff --git a/handlers/tlsproxy.go b/handlers/tlsproxy.go new file mode 100644 index 0000000..4551750 --- /dev/null +++ b/handlers/tlsproxy.go @@ -0,0 +1,90 @@ +package handlers + +import ( + "fmt" + "io" + "log" + "net" + "regexp" + "strings" + + vhost "github.com/inconshreveable/go-vhost" + "github.com/play-with-docker/play-with-docker/config" +) + +func StartTLSProxy(port string) { + var validProxyHost = regexp.MustCompile(`^.*pwd([0-9]{1,3}_[0-9]{1,3}_[0-9]{1,3}_[0-9]{1,3}(?:-[0-9]{1,5})?)\..*$`) + + tlsListener, tlsErr := net.Listen("tcp", fmt.Sprintf(":%s", port)) + log.Println("Listening on port " + port) + if tlsErr != nil { + log.Fatal(tlsErr) + } + defer tlsListener.Close() + for { + // Wait for TLS Connection + conn, err := tlsListener.Accept() + if err != nil { + log.Printf("Could not accept new TLS connection. Error: %s", err) + continue + } + // Handle connection on a new goroutine and continue accepting other new connections + go func(c net.Conn) { + defer c.Close() + vhostConn, err := vhost.TLS(conn) + if err != nil { + log.Printf("Incoming TLS connection produced an error. Error: %s", err) + return + } + defer vhostConn.Close() + + host := vhostConn.ClientHelloMsg.ServerName + match := validProxyHost.FindStringSubmatch(host) + if len(match) == 2 { + // This is a valid proxy host, keep only the important part + host = match[1] + } else { + // Not a valid proxy host, just close connection. + return + } + + var targetIP string + targetPort := "443" + + hostPort := strings.Split(host, ":") + if len(hostPort) > 1 && hostPort[1] != config.SSLPortNumber { + targetPort = hostPort[1] + } + + target := strings.Split(hostPort[0], "-") + if len(target) > 1 { + targetPort = target[1] + } + + ip := strings.Replace(target[0], "_", ".", -1) + + if net.ParseIP(ip) == nil { + // Not a valid IP, so treat this is a hostname. + } else { + targetIP = ip + } + + dest := fmt.Sprintf("%s:%s", targetIP, targetPort) + d, err := net.Dial("tcp", dest) + if err != nil { + log.Printf("Error dialing backend %s: %v\n", dest, err) + return + } + + errc := make(chan error, 2) + cp := func(dst io.Writer, src io.Reader) { + _, err := io.Copy(dst, src) + errc <- err + } + go cp(d, vhostConn) + go cp(vhostConn, d) + <-errc + }(conn) + } + +} diff --git a/services/docker.go b/services/docker.go index bb64616..34a28b5 100644 --- a/services/docker.go +++ b/services/docker.go @@ -1,8 +1,11 @@ package services import ( + "archive/tar" + "bytes" "fmt" "io" + "io/ioutil" "log" "os" "strconv" @@ -199,7 +202,64 @@ func ResizeConnection(name string, cols, rows uint) error { return c.ContainerResize(context.Background(), name, types.ResizeOptions{Height: rows, Width: cols}) } -func CreateInstance(session *Session, dindImage string) (*Instance, error) { +func CopyToContainer(containerName, destination, fileName string, content io.Reader) error { + r, w := io.Pipe() + b, readErr := ioutil.ReadAll(content) + if readErr != nil { + return readErr + } + t := tar.NewWriter(w) + go func() { + t.WriteHeader(&tar.Header{Name: fileName, Mode: 0600, Size: int64(len(b))}) + t.Write(b) + t.Close() + w.Close() + }() + return c.CopyToContainer(context.Background(), containerName, destination, r, types.CopyToContainerOptions{AllowOverwriteDirWithFile: true}) +} + +func CreateInstance(session *Session, conf InstanceConfig) (*Instance, error) { + var nodeName string + var containerName string + for i := 1; ; i++ { + nodeName = fmt.Sprintf("node%d", i) + containerName = fmt.Sprintf("%s_%s", session.Id[:8], nodeName) + exists := false + for _, instance := range session.Instances { + if instance.Name == containerName { + exists = true + break + } + } + if !exists { + break + } + } + + // Make sure directories are available for the new instance container + containerDir := "/var/run/pwd" + containerCertDir := fmt.Sprintf("%s/certs", containerDir) + + env := []string{} + + // Write certs to container cert dir + if len(conf.ServerCert) > 0 { + env = append(env, "DOCKER_TLSCERT=/var/run/pwd/certs/cert.pem") + } + if len(conf.ServerKey) > 0 { + env = append(env, "DOCKER_TLSKEY=/var/run/pwd/certs/key.pem") + } + if len(conf.CACert) > 0 { + // if ca cert is specified, verify that clients that connects present a certificate signed by the CA + env = append(env, "DOCKER_TLSCACERT=/var/run/pwd/certs/ca.pem") + } + if len(conf.ServerCert) > 0 || len(conf.ServerKey) > 0 || len(conf.CACert) > 0 { + // if any of the certs is specified, enable TLS + env = append(env, "DOCKER_TLSENABLE=true") + } else { + env = append(env, "DOCKER_TLSENABLE=false") + } + h := &container.HostConfig{ NetworkMode: container.NetworkMode(session.Id), Privileged: true, @@ -222,42 +282,37 @@ func CreateInstance(session *Session, dindImage string) (*Instance, error) { t := true h.Resources.OomKillDisable = &t - var nodeName string - var containerName string - for i := 1; ; i++ { - nodeName = fmt.Sprintf("node%d", i) - containerName = fmt.Sprintf("%s_%s", session.Id[:8], nodeName) - exists := false - for _, instance := range session.Instances { - if instance.Name == containerName { - exists = true - break - } - } - if !exists { - break - } - } - conf := &container.Config{Hostname: nodeName, + env = append(env, fmt.Sprintf("PWD_IP_ADDRESS=%s", session.PwdIpAddress)) + cf := &container.Config{Hostname: nodeName, Image: dindImage, Tty: true, OpenStdin: true, AttachStdin: true, AttachStdout: true, AttachStderr: true, - Env: []string{fmt.Sprintf("PWD_IP_ADDRESS=%s", session.PwdIpAddress)}, + Env: env, } networkConf := &network.NetworkingConfig{ map[string]*network.EndpointSettings{ session.Id: &network.EndpointSettings{Aliases: []string{nodeName}}, }, } - container, err := c.ContainerCreate(context.Background(), conf, h, networkConf, containerName) + container, err := c.ContainerCreate(context.Background(), cf, h, networkConf, containerName) if err != nil { return nil, err } + if err := copyIfSet(conf.ServerCert, "cert.pem", containerCertDir, containerName); err != nil { + return nil, err + } + if err := copyIfSet(conf.ServerKey, "key.pem", containerCertDir, containerName); err != nil { + return nil, err + } + if err := copyIfSet(conf.CACert, "ca.pem", containerCertDir, containerName); err != nil { + return nil, err + } + err = c.ContainerStart(context.Background(), container.ID, types.ContainerStartOptions{}) if err != nil { return nil, err @@ -271,6 +326,13 @@ func CreateInstance(session *Session, dindImage string) (*Instance, error) { return &Instance{Name: containerName, Hostname: cinfo.Config.Hostname, IP: cinfo.NetworkSettings.Networks[session.Id].IPAddress}, nil } +func copyIfSet(content []byte, fileName, path, containerName string) error { + if len(content) > 0 { + return CopyToContainer(containerName, path, fileName, bytes.NewReader(content)) + } + return nil +} + func DeleteContainer(id string) error { return c.ContainerRemove(context.Background(), id, types.ContainerRemoveOptions{Force: true, RemoveVolumes: true}) } diff --git a/services/instance.go b/services/instance.go index 8003a3b..4f1ae08 100644 --- a/services/instance.go +++ b/services/instance.go @@ -3,9 +3,12 @@ package services import ( "context" "crypto/tls" + "fmt" "io" "log" + "net/http" "os" + "path/filepath" "strings" "sync" @@ -42,6 +45,14 @@ type Instance struct { cert *tls.Certificate `json:"-"` } +type InstanceConfig struct { + ImageName string + Alias string + ServerCert []byte + ServerKey []byte + CACert []byte +} + func (i *Instance) setUsedPort(port uint16) { rw.Lock() defer rw.Unlock() @@ -97,17 +108,17 @@ func getDindImageName() string { return dindImage } -func NewInstance(session *Session, imageName, alias string) (*Instance, error) { - if imageName == "" { - imageName = dindImage +func NewInstance(session *Session, conf InstanceConfig) (*Instance, error) { + if conf.ImageName == "" { + conf.ImageName = dindImage } - log.Printf("NewInstance - using image: [%s]\n", imageName) - instance, err := CreateInstance(session, imageName) + log.Printf("NewInstance - using image: [%s]\n", conf.ImageName) + instance, err := CreateInstance(session, conf) if err != nil { return nil, err } - instance.Alias = alias + instance.Alias = conf.Alias instance.session = session @@ -163,6 +174,29 @@ func (i *Instance) Attach() { case <-i.ctx.Done(): } } + +func (i *Instance) UploadFromURL(url string) error { + log.Printf("Downloading file [%s]\n", url) + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("Could not download file [%s]. Error: %s\n", url, err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return fmt.Errorf("Could not download file [%s]. Status code: %d\n", url, resp.StatusCode) + } + + _, fileName := filepath.Split(url) + + copyErr := CopyToContainer(i.Name, "/var/run/pwd/uploads", fileName, resp.Body) + + if copyErr != nil { + return fmt.Errorf("Error while downloading file [%s]. Error: %s\n", url, copyErr) + } + + return nil +} + func GetInstance(session *Session, name string) *Instance { return session.Instances[name] }