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 · 13 Dec 24

tunnel.go

  1package pgs
  2
  3import (
  4	"context"
  5	"net/http"
  6	"strings"
  7
  8	"github.com/charmbracelet/ssh"
  9	"github.com/picosh/pico/db"
 10	"github.com/picosh/pico/shared"
 11)
 12
 13type TunnelWebRouter struct {
 14	*WebRouter
 15	subdomain string
 16}
 17
 18func (web *TunnelWebRouter) InitRouter() {
 19	router := http.NewServeMux()
 20	router.HandleFunc("GET /{fname...}", web.AssetRequest)
 21	router.HandleFunc("GET /{$}", web.AssetRequest)
 22	web.UserRouter = router
 23}
 24
 25func (web *TunnelWebRouter) Perm(proj *db.Project) bool {
 26	return true
 27}
 28
 29func (web *TunnelWebRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 30	ctx := r.Context()
 31	ctx = context.WithValue(ctx, shared.CtxSubdomainKey{}, web.subdomain)
 32	web.UserRouter.ServeHTTP(w, r.WithContext(ctx))
 33}
 34
 35type CtxHttpBridge = func(ssh.Context) http.Handler
 36
 37func getInfoFromUser(user string) (string, string) {
 38	if strings.Contains(user, "__") {
 39		results := strings.SplitN(user, "__", 2)
 40		return results[0], results[1]
 41	}
 42
 43	return "", user
 44}
 45
 46func createHttpHandler(apiConfig *shared.ApiConfig) CtxHttpBridge {
 47	return func(ctx ssh.Context) http.Handler {
 48		dbh := apiConfig.Dbpool
 49		logger := apiConfig.Cfg.Logger
 50		asUser, subdomain := getInfoFromUser(ctx.User())
 51		log := logger.With(
 52			"subdomain", subdomain,
 53			"impersonating", asUser,
 54		)
 55
 56		pubkey := ctx.Permissions().Extensions["pubkey"]
 57		if pubkey == "" {
 58			log.Error("pubkey not found in extensions", "subdomain", subdomain)
 59			return http.HandlerFunc(shared.UnauthorizedHandler)
 60		}
 61
 62		log = log.With(
 63			"pubkey", pubkey,
 64		)
 65
 66		props, err := shared.GetProjectFromSubdomain(subdomain)
 67		if err != nil {
 68			log.Error("could not get project from subdomain", "err", err.Error())
 69			return http.HandlerFunc(shared.UnauthorizedHandler)
 70		}
 71
 72		owner, err := dbh.FindUserForName(props.Username)
 73		if err != nil {
 74			log.Error(
 75				"could not find user from name",
 76				"name", props.Username,
 77				"err", err.Error(),
 78			)
 79			return http.HandlerFunc(shared.UnauthorizedHandler)
 80		}
 81		log = log.With(
 82			"owner", owner.Name,
 83		)
 84
 85		project, err := dbh.FindProjectByName(owner.ID, props.ProjectName)
 86		if err != nil {
 87			log.Error("could not get project by name", "project", props.ProjectName, "err", err.Error())
 88			return http.HandlerFunc(shared.UnauthorizedHandler)
 89		}
 90
 91		requester, _ := dbh.FindUserForKey("", pubkey)
 92		if requester != nil {
 93			log = log.With(
 94				"requester", requester.Name,
 95			)
 96		}
 97
 98		// impersonation logic
 99		if asUser != "" {
100			isAdmin := dbh.HasFeatureForUser(requester.ID, "admin")
101			if !isAdmin {
102				log.Error("impersonation attempt failed")
103				return http.HandlerFunc(shared.UnauthorizedHandler)
104			}
105			requester, _ = dbh.FindUserForName(asUser)
106		}
107
108		ctx.Permissions().Extensions["user_id"] = requester.ID
109		publicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubkey))
110		if err != nil {
111			log.Error("could not parse public key", "pubkey", pubkey, "err", err)
112			return http.HandlerFunc(shared.UnauthorizedHandler)
113		}
114		if !HasProjectAccess(project, owner, requester, publicKey) {
115			log.Error("no access")
116			return http.HandlerFunc(shared.UnauthorizedHandler)
117		}
118
119		log.Info("user has access to site")
120
121		routes := NewWebRouter(
122			apiConfig.Cfg,
123			logger,
124			apiConfig.Dbpool,
125			apiConfig.Storage,
126		)
127		tunnelRouter := TunnelWebRouter{routes, subdomain}
128		tunnelRouter.initRouters()
129		return &tunnelRouter
130	}
131}