diff --git a/api.go b/api.go new file mode 100644 index 0000000..af1ce6e --- /dev/null +++ b/api.go @@ -0,0 +1,107 @@ +package main + +import ( + //"fmt" + //"strings" + "net/http" + "io" + "encoding/json" +) + +type Api struct { + auth *Auth + tunMan *TunnelManager + mux *http.ServeMux +} + +type CreateTunnelResponse struct { + ServerAddress string `json:"server_address"` + ServerPort int `json:"server_port"` + ServerPublicKey string `json:"server_public_key"` + TunnelPort int `json:"tunnel_port"` + TunnelPrivateKey string `json:"tunnel_private_key"` +} + + +func NewApi(auth *Auth, tunMan *TunnelManager) *Api { + + api := &Api{auth, tunMan, nil} + + mux := http.NewServeMux() + + mux.Handle("/tunnels", http.StripPrefix("/tunnels", http.HandlerFunc(api.handleTunnels))) + + api.mux = mux + + return api +} + + +func (a *Api) ServeHTTP(w http.ResponseWriter, r *http.Request) { + a.mux.ServeHTTP(w, r) +} + +func (a *Api) handleTunnels(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "POST": + a.validateSession(http.HandlerFunc(a.handleCreateTunnel)).ServeHTTP(w, r) + default: + w.WriteHeader(405) + w.Write([]byte("Invalid method for /tunnels")) + } +} + +func (a *Api) handleCreateTunnel(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + + if len(query["domain"]) != 1 { + w.WriteHeader(400) + w.Write([]byte("Invalid domain parameter")) + return + } + domain := query["domain"][0] + + port, privKey, err := a.tunMan.CreateTunnel(domain) + if err != nil { + w.WriteHeader(400) + io.WriteString(w, err.Error()) + return + } + + response := &CreateTunnelResponse{ + ServerAddress: "anders.boringproxy.io", + ServerPort: 22, + ServerPublicKey: "", + TunnelPort: port, + TunnelPrivateKey: privKey, + } + + responseJson, err := json.MarshalIndent(response, "", " ") + if err != nil { + w.WriteHeader(500) + io.WriteString(w, "Error encoding response") + return + } + + w.Write(responseJson) +} + +func (a *Api) validateSession(h http.Handler) http.Handler { + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token, err := extractToken("access_token", r) + if err != nil { + w.WriteHeader(401) + w.Write([]byte("No token provided")) + return + } + + if !a.auth.Authorized(token) { + w.WriteHeader(403) + w.Write([]byte("Not authorized")) + return + } + + h.ServeHTTP(w, r) + }) +} diff --git a/boringproxy.go b/boringproxy.go index 6401e83..38ec1e7 100644 --- a/boringproxy.go +++ b/boringproxy.go @@ -34,7 +34,6 @@ type BoringProxy struct { tunMan *TunnelManager adminListener *AdminListener certConfig *certmagic.Config - sshServer *SshServer } func NewBoringProxy() *BoringProxy { @@ -68,11 +67,15 @@ func NewBoringProxy() *BoringProxy { auth := NewAuth() - sshServer := NewSshServer() + p := &BoringProxy{config, auth, tunMan, adminListener, certConfig} - p := &BoringProxy{config, auth, tunMan, adminListener, certConfig, sshServer} + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + p.handleAdminRequest(w, r) + }) + + api := NewApi(auth, tunMan) + http.Handle("/api/", http.StripPrefix("/api", api)) - http.HandleFunc("/", p.handleAdminRequest) go http.Serve(adminListener, nil) log.Println("BoringProxy ready") diff --git a/tunnel_manager.go b/tunnel_manager.go index 9e500a9..f4ac771 100644 --- a/tunnel_manager.go +++ b/tunnel_manager.go @@ -1,12 +1,19 @@ package main import ( + "fmt" + "strings" "encoding/json" "errors" "github.com/caddyserver/certmagic" "io/ioutil" "log" "sync" + "crypto/rsa" + "crypto/rand" + "encoding/pem" + "crypto/x509" + "golang.org/x/crypto/ssh" ) type Tunnel struct { @@ -20,6 +27,7 @@ func NewTunnels() Tunnels { } type TunnelManager struct { + nextPort int tunnels Tunnels mutex *sync.Mutex certConfig *certmagic.Config @@ -49,8 +57,10 @@ func NewTunnelManager(certConfig *certmagic.Config) *TunnelManager { } } + nextPort := 9001 + mutex := &sync.Mutex{} - return &TunnelManager{tunnels, mutex, certConfig} + return &TunnelManager{nextPort, tunnels, mutex, certConfig} } func (m *TunnelManager) SetTunnel(host string, port int) error { @@ -69,6 +79,35 @@ func (m *TunnelManager) SetTunnel(host string, port int) error { return nil } +func (m *TunnelManager) CreateTunnel(domain string) (int, string, error) { + err := m.certConfig.ManageSync([]string{domain}) + if err != nil { + log.Println(err) + return 0, "", errors.New("Failed to get cert") + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + _, exists := m.tunnels[domain] + if exists { + return 0, "", errors.New("Tunnel exists for domain " + domain) + } + + port := m.nextPort + m.nextPort += 1 + tunnel := &Tunnel{port} + m.tunnels[domain] = tunnel + saveJson(m.tunnels, "tunnels.json") + + privKey, err := m.addToAuthorizedKeys(port) + if err != nil { + return 0, "", err + } + + return port, privKey, nil +} + func (m *TunnelManager) DeleteTunnel(host string) { m.mutex.Lock() delete(m.tunnels, host) @@ -87,3 +126,60 @@ func (m *TunnelManager) GetPort(serverName string) (int, error) { return tunnel.Port, nil } + +func (m *TunnelManager) addToAuthorizedKeys(port int) (string, error) { + + akBytes, err := ioutil.ReadFile("/home/anders/.ssh/authorized_keys") + if err != nil { + return "", err + } + + akStr := string(akBytes) + + pubKey, privKey, err := MakeSSHKeyPair() + if err != nil { + return "", err + } + + options := fmt.Sprintf(`command="echo This key permits tunnels only",permitopen="fakehost:1",permitlisten="localhost:%d"`, port) + + pubKeyNoNewline := pubKey[:len(pubKey) - 1] + newAk := fmt.Sprintf("%s%s %s %s%d\n", akStr, options, pubKeyNoNewline, "boringproxy-", port) + + err = ioutil.WriteFile("/home/anders/.ssh/authorized_keys", []byte(newAk), 0600) + if err != nil { + return "", err + } + + return privKey, nil +} + +// Adapted from https://stackoverflow.com/a/34347463/943814 +// MakeSSHKeyPair make a pair of public and private keys for SSH access. +// Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file. +// Private Key generated is PEM encoded +func MakeSSHKeyPair() (string, string, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return "", "", err + } + + // generate and write private key as PEM + var privKeyBuf strings.Builder + + privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} + if err := pem.Encode(&privKeyBuf, privateKeyPEM); err != nil { + return "", "", err + } + + // generate and write public key + pub, err := ssh.NewPublicKey(&privateKey.PublicKey) + if err != nil { + return "", "", err + } + + var pubKeyBuf strings.Builder + pubKeyBuf.Write(ssh.MarshalAuthorizedKey(pub)) + + return pubKeyBuf.String(), privKeyBuf.String(), nil +}