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}