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}