mirror of
https://github.com/boringproxy/boringproxy.git
synced 2024-07-04 11:22:57 -05:00
626 lines
13 KiB
Go
626 lines
13 KiB
Go
package boringproxy
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
)
|
|
|
|
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)))
|
|
mux.Handle("/clients/", http.StripPrefix("/clients", http.HandlerFunc(api.handleClients)))
|
|
|
|
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 the token is limited to a specific client, filter out
|
|
// tunnels for any other clients.
|
|
if tokenData.Client != "" {
|
|
for k, tun := range tunnels {
|
|
if tokenData.Client != tun.ClientName {
|
|
delete(tunnels, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
clientName := query.Get("client-name")
|
|
if clientName != "" && tokenData.Client != "" && clientName != tokenData.Client {
|
|
w.WriteHeader(403)
|
|
w.Write([]byte("Token is not valid for this client"))
|
|
return
|
|
}
|
|
|
|
if clientName != "" {
|
|
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":
|
|
|
|
if tokenData.Client != "" {
|
|
w.WriteHeader(403)
|
|
io.WriteString(w, "Token cannot be used to create tunnels")
|
|
return
|
|
}
|
|
|
|
r.ParseForm()
|
|
_, err := a.CreateTunnel(tokenData, r.Form)
|
|
if err != nil {
|
|
w.WriteHeader(500)
|
|
w.Write([]byte(err.Error()))
|
|
}
|
|
case "DELETE":
|
|
if tokenData.Client != "" {
|
|
w.WriteHeader(403)
|
|
io.WriteString(w, "Token cannot be used to delete tunnels")
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
r.ParseForm()
|
|
|
|
if tokenData.Client != "" {
|
|
w.WriteHeader(403)
|
|
io.WriteString(w, "Token cannot be used to manage users")
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case "GET":
|
|
users := a.GetUsers(tokenData, r.Form)
|
|
json.NewEncoder(w).Encode(users)
|
|
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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if tokenData.Client != "" {
|
|
w.WriteHeader(403)
|
|
io.WriteString(w, "Token cannot be used to manage tokens")
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case "GET":
|
|
tokens := a.GetTokens(tokenData, r.Form)
|
|
json.NewEncoder(w).Encode(tokens)
|
|
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)
|
|
fmt.Fprintf(w, "Invalid method for /api/tokens")
|
|
}
|
|
}
|
|
|
|
func (a *Api) handleClients(w http.ResponseWriter, r *http.Request) {
|
|
|
|
r.ParseForm()
|
|
|
|
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
|
|
}
|
|
|
|
clientName := r.Form.Get("client-name")
|
|
if clientName == "" {
|
|
if tokenData.Client == "" {
|
|
w.WriteHeader(400)
|
|
w.Write([]byte("Missing client-name parameter"))
|
|
return
|
|
} else {
|
|
clientName = tokenData.Client
|
|
}
|
|
}
|
|
|
|
if tokenData.Client != "" && tokenData.Client != clientName {
|
|
w.WriteHeader(403)
|
|
io.WriteString(w, "Token does not have proper permissions")
|
|
return
|
|
}
|
|
|
|
user := r.Form.Get("user")
|
|
if user == "" {
|
|
user = tokenData.Owner
|
|
}
|
|
|
|
switch r.Method {
|
|
case "POST":
|
|
err := a.SetClient(tokenData, r.Form, user, clientName)
|
|
if err != nil {
|
|
w.WriteHeader(500)
|
|
w.Write([]byte(err.Error()))
|
|
}
|
|
case "DELETE":
|
|
err := a.DeleteClient(tokenData, user, clientName)
|
|
if err != nil {
|
|
w.WriteHeader(500)
|
|
io.WriteString(w, err.Error())
|
|
return
|
|
}
|
|
default:
|
|
w.WriteHeader(405)
|
|
fmt.Fprintf(w, "Invalid method for /api/clients")
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
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" && tlsTerm != "client-tls" && tlsTerm != "server-tls" {
|
|
return nil, errors.New("Invalid tls-termination parameter")
|
|
}
|
|
|
|
sshServerAddr := a.db.GetAdminDomain()
|
|
sshServerAddrParam := params.Get("ssh-server-addr")
|
|
if sshServerAddrParam != "" {
|
|
sshServerAddr = sshServerAddrParam
|
|
}
|
|
|
|
sshServerPort := a.config.SshServerPort
|
|
sshServerPortParam := params.Get("ssh-server-port")
|
|
if sshServerPortParam != "" {
|
|
var err error
|
|
sshServerPort, err = strconv.Atoi(sshServerPortParam)
|
|
if err != nil {
|
|
return nil, errors.New("Invalid ssh-server-port parameter")
|
|
}
|
|
}
|
|
|
|
request := Tunnel{
|
|
Domain: domain,
|
|
Owner: owner,
|
|
ClientName: clientName,
|
|
ClientPort: clientPort,
|
|
ClientAddress: clientAddr,
|
|
TunnelPort: tunnelPort,
|
|
AllowExternalTcp: allowExternalTcp,
|
|
AuthUsername: username,
|
|
AuthPassword: password,
|
|
TlsTermination: tlsTerm,
|
|
ServerAddress: sshServerAddr,
|
|
ServerPort: sshServerPort,
|
|
}
|
|
|
|
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) {
|
|
|
|
ownerId := params.Get("owner")
|
|
if ownerId == "" {
|
|
return "", errors.New("Invalid owner paramater")
|
|
}
|
|
|
|
user, _ := a.db.GetUser(tokenData.Owner)
|
|
|
|
if tokenData.Owner != ownerId && !user.IsAdmin {
|
|
return "", errors.New("Unauthorized")
|
|
}
|
|
|
|
var owner User
|
|
|
|
if tokenData.Owner == ownerId {
|
|
owner = user
|
|
} else {
|
|
owner, _ = a.db.GetUser(ownerId)
|
|
}
|
|
|
|
client := params.Get("client")
|
|
|
|
if client != "any" {
|
|
if _, exists := owner.Clients[client]; !exists {
|
|
return "", fmt.Errorf("Client %s does not exist for user %s", client, ownerId)
|
|
}
|
|
} else {
|
|
client = ""
|
|
}
|
|
|
|
token, err := a.db.AddToken(ownerId, client)
|
|
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) GetTokens(tokenData TokenData, params url.Values) map[string]TokenData {
|
|
|
|
tokens := a.db.GetTokens()
|
|
|
|
user, _ := a.db.GetUser(tokenData.Owner)
|
|
|
|
for key, tok := range tokens {
|
|
if !user.IsAdmin && tok.Owner != tokenData.Owner {
|
|
delete(tokens, key)
|
|
}
|
|
}
|
|
|
|
return tokens
|
|
}
|
|
|
|
func (a *Api) GetUsers(tokenData TokenData, params url.Values) map[string]User {
|
|
|
|
user, _ := a.db.GetUser(tokenData.Owner)
|
|
users := a.db.GetUsers()
|
|
|
|
if user.IsAdmin {
|
|
return users
|
|
} else {
|
|
return map[string]User{
|
|
tokenData.Owner: user,
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|