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:
committed by
Marcos Nils
parent
61a0bb4db1
commit
8df6373327
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
play-with-docker
|
||||
node_modules
|
||||
docker-compose.single.yml
|
||||
|
||||
@@ -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
29
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)
|
||||
}
|
||||
|
||||
47
handlers/file_upload.go
Normal file
47
handlers/file_upload.go
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
90
handlers/tlsproxy.go
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user