Eric Bower
·
19 Apr 24
tunnel.go
1package pgs
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "strings"
8
9 "github.com/charmbracelet/ssh"
10 "github.com/picosh/pico/db"
11 "github.com/picosh/pico/shared"
12 "github.com/picosh/pico/ui"
13)
14
15func allowPerm(proj *db.Project) bool {
16 return true
17}
18
19type CtxHttpBridge = func(ssh.Context) http.Handler
20
21func getInfoFromUser(user string) (string, string) {
22 if strings.Contains(user, "__") {
23 results := strings.SplitN(user, "__", 2)
24 return results[0], results[1]
25 }
26
27 return "", user
28}
29
30func createHttpHandler(apiConfig *shared.ApiConfig) CtxHttpBridge {
31 return func(ctx ssh.Context) http.Handler {
32 dbh := apiConfig.Dbpool
33 logger := apiConfig.Cfg.Logger
34 asUser, subdomain := getInfoFromUser(ctx.User())
35 log := logger.With(
36 "subdomain", subdomain,
37 "impersonating", asUser,
38 )
39
40 pubkey, err := shared.GetPublicKeyCtx(ctx)
41 if err != nil {
42 log.Error(err.Error(), "subdomain", subdomain)
43 return http.HandlerFunc(shared.UnauthorizedHandler)
44 }
45 pubkeyStr, err := shared.KeyForKeyText(pubkey)
46 if err != nil {
47 log.Error(err.Error())
48 return http.HandlerFunc(shared.UnauthorizedHandler)
49 }
50 log = log.With(
51 "pubkey", pubkeyStr,
52 )
53
54 props, err := getProjectFromSubdomain(subdomain)
55 if err != nil {
56 log.Error(err.Error())
57 return http.HandlerFunc(shared.UnauthorizedHandler)
58 }
59
60 owner, err := dbh.FindUserForName(props.Username)
61 if err != nil {
62 log.Error(err.Error())
63 return http.HandlerFunc(shared.UnauthorizedHandler)
64 }
65 log = log.With(
66 "owner", owner.Name,
67 )
68
69 project, err := dbh.FindProjectByName(owner.ID, props.ProjectName)
70 if err != nil {
71 log.Error(err.Error())
72 return http.HandlerFunc(shared.UnauthorizedHandler)
73 }
74
75 requester, _ := dbh.FindUserForKey("", pubkeyStr)
76 if requester != nil {
77 log = log.With(
78 "requester", requester.Name,
79 )
80 }
81
82 // impersonation logic
83 if asUser != "" {
84 isAdmin := dbh.HasFeatureForUser(requester.ID, "admin")
85 if !isAdmin {
86 log.Error("impersonation attempt failed")
87 return http.HandlerFunc(shared.UnauthorizedHandler)
88 }
89 requester, _ = dbh.FindUserForName(asUser)
90 }
91
92 shared.SetUserCtx(ctx, requester)
93
94 if !HasProjectAccess(project, owner, requester, pubkey) {
95 log.Error("no access")
96 return http.HandlerFunc(shared.UnauthorizedHandler)
97 }
98
99 log.Info("user has access to site")
100
101 routes := []shared.Route{
102 // special API endpoint for tunnel users accessing site
103 shared.NewCorsRoute("GET", "/api/current_user", func(w http.ResponseWriter, r *http.Request) {
104 w.Header().Set("Content-Type", "application/json")
105 user, err := shared.GetUserCtx(ctx)
106 if err != nil {
107 logger.Error("could not find user", "err", err.Error())
108 shared.JSONError(w, err.Error(), http.StatusNotFound)
109 return
110 }
111 pico := shared.NewUserApi(user, pubkey)
112 err = json.NewEncoder(w).Encode(pico)
113 if err != nil {
114 log.Error(err.Error())
115 }
116 }),
117 }
118
119 if subdomain == "pico-ui" || subdomain == "erock-ui" {
120 rts := ui.CreateRoutes(apiConfig, ctx)
121 routes = append(routes, rts...)
122 }
123
124 subdomainRoutes := createSubdomainRoutes(allowPerm)
125 routes = append(routes, subdomainRoutes...)
126 finctx := apiConfig.CreateCtx(context.Background(), subdomain)
127 finctx = context.WithValue(finctx, shared.CtxSshKey{}, ctx)
128 httpHandler := shared.CreateServeBasic(routes, finctx)
129 httpRouter := http.HandlerFunc(httpHandler)
130 return httpRouter
131 }
132}