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}