repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

pico / pkg / apps / pgs
Antonio Mika  ·  2025-03-12

tunnel.go

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