Files
boringproxy/api.go
Anders Pitman 2b74f6ee0c Implement selecting tunnel port
This is useful for when you want to use boringproxy more like a
normal reverse proxy, ie if boringproxy and your service are on
the same machine, you can just use the port of the service as the
tunnel port and boringproxy will forward directly to it without
using SSH.

This would normally be used with no client set.
2021-02-16 18:37:31 -07:00

525 lines
11 KiB
Go

package boringproxy
import (
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
type Api struct {
config *Config
db *Database
auth *Auth
tunMan *TunnelManager
mux *http.ServeMux
}
func NewApi(config *Config, db *Database, auth *Auth, tunMan *TunnelManager) *Api {
mux := http.NewServeMux()
api := &Api{config, db, auth, tunMan, mux}
mux.Handle("/tunnels", http.StripPrefix("/tunnels", http.HandlerFunc(api.handleTunnels)))
mux.Handle("/users/", http.StripPrefix("/users", http.HandlerFunc(api.handleUsers)))
mux.Handle("/tokens/", http.StripPrefix("/tokens", http.HandlerFunc(api.handleTokens)))
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) {
token, err := extractToken("access_token", r)
if err != nil {
w.WriteHeader(401)
w.Write([]byte("No token provided"))
return
}
tokenData, exists := a.db.GetTokenData(token)
if !exists {
w.WriteHeader(403)
w.Write([]byte("Not authorized"))
return
}
switch r.Method {
case "GET":
query := r.URL.Query()
tunnels := a.GetTunnels(tokenData)
if len(query["client-name"]) == 1 {
clientName := query["client-name"][0]
for k, tun := range tunnels {
if tun.ClientName != clientName {
delete(tunnels, k)
} else {
tun.ServerPort = a.config.SshServerPort
tunnels[k] = tun
}
}
}
body, err := json.Marshal(tunnels)
if err != nil {
w.WriteHeader(500)
w.Write([]byte("Error encoding tunnels"))
return
}
hash := md5.Sum(body)
hashStr := fmt.Sprintf("%x", hash)
w.Header()["ETag"] = []string{hashStr}
w.Write([]byte(body))
case "POST":
r.ParseForm()
_, err := a.CreateTunnel(tokenData, r.Form)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
}
case "DELETE":
r.ParseForm()
err := a.DeleteTunnel(tokenData, r.Form)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
}
default:
w.WriteHeader(405)
w.Write([]byte("Invalid method for /tunnels"))
}
}
func (a *Api) handleUsers(w http.ResponseWriter, r *http.Request) {
token, err := extractToken("access_token", r)
if err != nil {
w.WriteHeader(401)
io.WriteString(w, "Invalid token")
return
}
tokenData, exists := a.db.GetTokenData(token)
if !exists {
w.WriteHeader(401)
io.WriteString(w, "Failed to get token")
return
}
path := r.URL.Path
parts := strings.Split(path[1:], "/")
r.ParseForm()
if path == "/" {
switch r.Method {
case "POST":
err := a.CreateUser(tokenData, r.Form)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
default:
w.WriteHeader(405)
io.WriteString(w, "Invalid method for /users")
return
}
} else if len(parts) == 3 && parts[1] == "clients" {
ownerId := parts[0]
clientId := parts[2]
if r.Method == "PUT" {
err := a.SetClient(tokenData, r.Form, ownerId, clientId)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
} else if r.Method == "DELETE" {
err := a.DeleteClient(tokenData, ownerId, clientId)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
}
} else {
w.WriteHeader(400)
io.WriteString(w, "Invalid /users/<username>/clients request")
return
}
}
func (a *Api) handleTokens(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
}
tokenData, exists := a.db.GetTokenData(token)
if !exists {
w.WriteHeader(403)
w.Write([]byte("Not authorized"))
return
}
switch r.Method {
case "POST":
r.ParseForm()
token, err := a.CreateToken(tokenData, r.Form)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
}
io.WriteString(w, token)
default:
w.WriteHeader(405)
w.Write([]byte(err.Error()))
}
}
func (a *Api) GetTunnel(tokenData TokenData, params url.Values) (Tunnel, error) {
domain := params.Get("domain")
if domain == "" {
return Tunnel{}, errors.New("Invalid domain parameter")
}
tun, exists := a.db.GetTunnel(domain)
if !exists {
return Tunnel{}, errors.New("Tunnel doesn't exist for domain")
}
user, _ := a.db.GetUser(tokenData.Owner)
if user.IsAdmin || tun.Owner == tokenData.Owner {
return tun, nil
} else {
return Tunnel{}, errors.New("Unauthorized")
}
}
func (a *Api) GetTunnels(tokenData TokenData) map[string]Tunnel {
user, _ := a.db.GetUser(tokenData.Owner)
var tunnels map[string]Tunnel
if user.IsAdmin {
tunnels = a.db.GetTunnels()
} else {
tunnels = make(map[string]Tunnel)
for domain, tun := range a.db.GetTunnels() {
if tokenData.Owner == tun.Owner {
tunnels[domain] = tun
}
}
}
return tunnels
}
func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, error) {
domain := params.Get("domain")
if domain == "" {
return nil, errors.New("Invalid domain parameter")
}
owner := params.Get("owner")
if owner == "" {
return nil, errors.New("Invalid owner parameter")
}
// Only admins can create tunnels for other users
if tokenData.Owner != owner {
user, _ := a.db.GetUser(tokenData.Owner)
if !user.IsAdmin {
return nil, errors.New("Unauthorized")
}
}
sshKeyId := params.Get("ssh-key-id")
var sshKey SshKey
if sshKeyId != "" {
var exists bool
sshKey, exists = a.db.GetSshKey(sshKeyId)
if !exists {
return nil, errors.New("SSH key does not exist")
}
}
clientName := params.Get("client-name")
clientPort := 0
clientPortParam := params.Get("client-port")
if clientPortParam != "" {
var err error
clientPort, err = strconv.Atoi(clientPortParam)
if err != nil {
return nil, errors.New("Invalid client-port parameter")
}
}
clientAddr := params.Get("client-addr")
if clientAddr == "" {
clientAddr = "127.0.0.1"
}
tunnelPort := 0
tunnelPortParam := params.Get("tunnel-port")
if tunnelPortParam != "" && tunnelPortParam != "Random" {
var err error
tunnelPort, err = strconv.Atoi(tunnelPortParam)
if err != nil {
return nil, errors.New("Invalid tunnel-port parameter")
}
}
allowExternalTcp := params.Get("allow-external-tcp") == "on"
passwordProtect := params.Get("password-protect") == "on"
var username string
var password string
if passwordProtect {
username = params.Get("username")
if username == "" {
return nil, errors.New("Username required")
}
password = params.Get("password")
if password == "" {
return nil, errors.New("Password required")
}
}
tlsTerm := params.Get("tls-termination")
if tlsTerm != "server" && tlsTerm != "client" && tlsTerm != "passthrough" {
return nil, errors.New("Invalid tls-termination parameter")
}
request := Tunnel{
Domain: domain,
SshKey: sshKey.Key,
Owner: owner,
ClientName: clientName,
ClientPort: clientPort,
ClientAddress: clientAddr,
TunnelPort: tunnelPort,
AllowExternalTcp: allowExternalTcp,
AuthUsername: username,
AuthPassword: password,
TlsTermination: tlsTerm,
}
tunnel, err := a.tunMan.RequestCreateTunnel(request)
if err != nil {
return nil, err
}
return &tunnel, nil
}
func (a *Api) DeleteTunnel(tokenData TokenData, params url.Values) error {
domain := params.Get("domain")
if domain == "" {
return errors.New("Invalid domain parameter")
}
tun, exists := a.db.GetTunnel(domain)
if !exists {
return errors.New("Tunnel doesn't exist")
}
if tokenData.Owner != tun.Owner {
user, _ := a.db.GetUser(tokenData.Owner)
if !user.IsAdmin {
return errors.New("Unauthorized")
}
}
a.tunMan.DeleteTunnel(domain)
return nil
}
func (a *Api) CreateToken(tokenData TokenData, params url.Values) (string, error) {
owner := params.Get("owner")
if owner == "" {
return "", errors.New("Invalid owner paramater")
}
if tokenData.Owner != owner {
user, _ := a.db.GetUser(tokenData.Owner)
if !user.IsAdmin {
return "", errors.New("Unauthorized")
}
}
token, err := a.db.AddToken(owner)
if err != nil {
return "", errors.New("Failed to create token")
}
return token, nil
}
func (a *Api) DeleteToken(tokenData TokenData, params url.Values) error {
token := params.Get("token")
if token == "" {
return errors.New("Invalid token parameter")
}
delTokenData, exists := a.db.GetTokenData(token)
if !exists {
return errors.New("Token doesn't exist")
}
if tokenData.Owner != delTokenData.Owner {
user, _ := a.db.GetUser(tokenData.Owner)
if !user.IsAdmin {
return errors.New("Unauthorized")
}
}
a.db.DeleteTokenData(token)
return nil
}
func (a *Api) CreateUser(tokenData TokenData, params url.Values) error {
user, _ := a.db.GetUser(tokenData.Owner)
if !user.IsAdmin {
return errors.New("Unauthorized")
}
username := params.Get("username")
minUsernameLen := 6
if len(username) < minUsernameLen {
errStr := fmt.Sprintf("Username must be at least %d characters", minUsernameLen)
return errors.New(errStr)
}
isAdmin := params.Get("is-admin") == "on"
err := a.db.AddUser(username, isAdmin)
if err != nil {
return err
}
return nil
}
func (a *Api) DeleteUser(tokenData TokenData, params url.Values) error {
user, _ := a.db.GetUser(tokenData.Owner)
if !user.IsAdmin {
return errors.New("Unauthorized")
}
username := params.Get("username")
if username == "" {
return errors.New("Invalid username parameter")
}
_, exists := a.db.GetUser(username)
if !exists {
return errors.New("User doesn't exist")
}
a.db.DeleteUser(username)
for token, tokenData := range a.db.GetTokens() {
if tokenData.Owner == username {
a.db.DeleteTokenData(token)
}
}
return nil
}
func (a *Api) SetClient(tokenData TokenData, params url.Values, ownerId, clientId string) error {
if tokenData.Owner != ownerId {
user, _ := a.db.GetUser(tokenData.Owner)
if !user.IsAdmin {
return errors.New("Unauthorized")
}
}
// TODO: what if two users try to get then set at the same time?
owner, _ := a.db.GetUser(ownerId)
owner.Clients[clientId] = DbClient{}
a.db.SetUser(ownerId, owner)
return nil
}
func (a *Api) DeleteClient(tokenData TokenData, ownerId, clientId string) error {
if tokenData.Owner != ownerId {
user, _ := a.db.GetUser(tokenData.Owner)
if !user.IsAdmin {
return errors.New("Unauthorized")
}
}
owner, _ := a.db.GetUser(ownerId)
delete(owner.Clients, clientId)
a.db.SetUser(ownerId, owner)
return nil
}
func (a *Api) GetSshKeys(tokenData TokenData) map[string]SshKey {
user, _ := a.db.GetUser(tokenData.Owner)
var keys map[string]SshKey
if user.IsAdmin {
keys = a.db.GetSshKeys()
} else {
keys = make(map[string]SshKey)
for id, key := range a.db.GetSshKeys() {
if tokenData.Owner == key.Owner {
keys[id] = key
}
}
}
return keys
}
func (a *Api) DeleteSshKey(tokenData TokenData, params url.Values) error {
id := params.Get("id")
if id == "" {
return errors.New("Invalid id parameter")
}
a.db.DeleteSshKey(id)
return nil
}