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 }