repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

pico / pgs
Eric Bower · 19 Apr 24

tunnel.go

  1package pgs
  2
  3import (
  4	"context"
  5	"encoding/json"
  6	"net/http"
  7	"strings"
  8
  9	"github.com/charmbracelet/ssh"
 10	"github.com/picosh/pico/db"
 11	"github.com/picosh/pico/shared"
 12	"github.com/picosh/pico/ui"
 13)
 14
 15func allowPerm(proj *db.Project) bool {
 16	return true
 17}
 18
 19type CtxHttpBridge = func(ssh.Context) http.Handler
 20
 21func getInfoFromUser(user string) (string, string) {
 22	if strings.Contains(user, "__") {
 23		results := strings.SplitN(user, "__", 2)
 24		return results[0], results[1]
 25	}
 26
 27	return "", user
 28}
 29
 30func createHttpHandler(apiConfig *shared.ApiConfig) CtxHttpBridge {
 31	return func(ctx ssh.Context) http.Handler {
 32		dbh := apiConfig.Dbpool
 33		logger := apiConfig.Cfg.Logger
 34		asUser, subdomain := getInfoFromUser(ctx.User())
 35		log := logger.With(
 36			"subdomain", subdomain,
 37			"impersonating", asUser,
 38		)
 39
 40		pubkey, err := shared.GetPublicKeyCtx(ctx)
 41		if err != nil {
 42			log.Error(err.Error(), "subdomain", subdomain)
 43			return http.HandlerFunc(shared.UnauthorizedHandler)
 44		}
 45		pubkeyStr, err := shared.KeyForKeyText(pubkey)
 46		if err != nil {
 47			log.Error(err.Error())
 48			return http.HandlerFunc(shared.UnauthorizedHandler)
 49		}
 50		log = log.With(
 51			"pubkey", pubkeyStr,
 52		)
 53
 54		props, err := getProjectFromSubdomain(subdomain)
 55		if err != nil {
 56			log.Error(err.Error())
 57			return http.HandlerFunc(shared.UnauthorizedHandler)
 58		}
 59
 60		owner, err := dbh.FindUserForName(props.Username)
 61		if err != nil {
 62			log.Error(err.Error())
 63			return http.HandlerFunc(shared.UnauthorizedHandler)
 64		}
 65		log = log.With(
 66			"owner", owner.Name,
 67		)
 68
 69		project, err := dbh.FindProjectByName(owner.ID, props.ProjectName)
 70		if err != nil {
 71			log.Error(err.Error())
 72			return http.HandlerFunc(shared.UnauthorizedHandler)
 73		}
 74
 75		requester, _ := dbh.FindUserForKey("", pubkeyStr)
 76		if requester != nil {
 77			log = log.With(
 78				"requester", requester.Name,
 79			)
 80		}
 81
 82		// impersonation logic
 83		if asUser != "" {
 84			isAdmin := dbh.HasFeatureForUser(requester.ID, "admin")
 85			if !isAdmin {
 86				log.Error("impersonation attempt failed")
 87				return http.HandlerFunc(shared.UnauthorizedHandler)
 88			}
 89			requester, _ = dbh.FindUserForName(asUser)
 90		}
 91
 92		shared.SetUserCtx(ctx, requester)
 93
 94		if !HasProjectAccess(project, owner, requester, pubkey) {
 95			log.Error("no access")
 96			return http.HandlerFunc(shared.UnauthorizedHandler)
 97		}
 98
 99		log.Info("user has access to site")
100
101		routes := []shared.Route{
102			// special API endpoint for tunnel users accessing site
103			shared.NewCorsRoute("GET", "/api/current_user", func(w http.ResponseWriter, r *http.Request) {
104				w.Header().Set("Content-Type", "application/json")
105				user, err := shared.GetUserCtx(ctx)
106				if err != nil {
107					logger.Error("could not find user", "err", err.Error())
108					shared.JSONError(w, err.Error(), http.StatusNotFound)
109					return
110				}
111				pico := shared.NewUserApi(user, pubkey)
112				err = json.NewEncoder(w).Encode(pico)
113				if err != nil {
114					log.Error(err.Error())
115				}
116			}),
117		}
118
119		if subdomain == "pico-ui" || subdomain == "erock-ui" {
120			rts := ui.CreateRoutes(apiConfig, ctx)
121			routes = append(routes, rts...)
122		}
123
124		subdomainRoutes := createSubdomainRoutes(allowPerm)
125		routes = append(routes, subdomainRoutes...)
126		finctx := apiConfig.CreateCtx(context.Background(), subdomain)
127		finctx = context.WithValue(finctx, shared.CtxSshKey{}, ctx)
128		httpHandler := shared.CreateServeBasic(routes, finctx)
129		httpRouter := http.HandlerFunc(httpHandler)
130		return httpRouter
131	}
132}