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
This commit is contained in:
Jonathan Leibiusky
2017-05-12 16:20:09 -03:00
committed by Marcos Nils
parent 61a0bb4db1
commit 8df6373327
8 changed files with 267 additions and 54 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
play-with-docker
node_modules
docker-compose.single.yml

View File

@@ -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 && \

29
api.go
View File

@@ -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)
}

47
handlers/file_upload.go Normal file
View File

@@ -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
}
}

View File

@@ -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)

90
handlers/tlsproxy.go Normal file
View File

@@ -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)
}
}

View File

@@ -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})
}

View File

@@ -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]
}