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