- commit
- 43ef9bb
- parent
- 0df20ce
- author
- Eric Bower
- date
- 2024-02-24 16:55:44 +0000 UTC
feat(pgs): private projects with web tunnels (#80) We can support private projects via local forward ssh tunnels. Eric sets ACL for project `hey-docs-dev`: ``` ssh pgs.sh acl hey-docs-dev pico antonio ``` Antonio creates SSH tunnel to project: ``` ssh -L 5000:localhost:80 -N hey-docs-dev@pgs.sh ``` Antonio can access site via http://localhost:5000 Other options we support: - `acl public` -> default, open to everyone - `acl pico` -> private to any pico user - `acl pico xxx,yyy` -> private to specific pico users - `acl public_keys` -> private to anyone with a pubkey - `acl public_keys sha256:xxx,sha256:yyy` -> private to specific pubkeys Further, there is a special API endpoint for any sites accessed via a tunnel: GET http://localhost:5000/pico This will return information about the user accessing the site, like pico username, public key used, etc.
M
Makefile
+2,
-1
1@@ -110,10 +110,11 @@ migrate:
2 $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20230707_add_projects_table.sql
3 $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20230921_add_tokens_table.sql
4 $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20240120_add_payment_history.sql
5+ $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20240221_add_project_acl.sql
6 .PHONY: migrate
7
8 latest:
9- $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20240120_add_payment_history.sql
10+ $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20240221_add_project_acl.sql
11 .PHONY: latest
12
13 psql:
M
db/db.go
+26,
-2
1@@ -32,25 +32,48 @@ type PostData struct {
2 LastDigest *time.Time `json:"last_digest"`
3 }
4
5+// Make the Attrs struct implement the driver.Valuer interface. This method
6+// simply returns the JSON-encoded representation of the struct.
7+func (p PostData) Value() (driver.Value, error) {
8+ return json.Marshal(p)
9+}
10+
11+// Make the Attrs struct implement the sql.Scanner interface. This method
12+// simply decodes a JSON-encoded value into the struct fields.
13+func (p *PostData) Scan(value interface{}) error {
14+ b, ok := value.([]byte)
15+ if !ok {
16+ return errors.New("type assertion to []byte failed")
17+ }
18+
19+ return json.Unmarshal(b, &p)
20+}
21+
22 type Project struct {
23 ID string `json:"id"`
24 UserID string `json:"user_id"`
25 Name string `json:"name"`
26 ProjectDir string `json:"project_dir"`
27 Username string `json:"username"`
28+ Acl ProjectAcl `json:"acl"`
29 CreatedAt *time.Time `json:"created_at"`
30 UpdatedAt *time.Time `json:"updated_at"`
31 }
32
33+type ProjectAcl struct {
34+ Type string `json:"type"`
35+ Data []string `json:"data"`
36+}
37+
38 // Make the Attrs struct implement the driver.Valuer interface. This method
39 // simply returns the JSON-encoded representation of the struct.
40-func (p PostData) Value() (driver.Value, error) {
41+func (p ProjectAcl) Value() (driver.Value, error) {
42 return json.Marshal(p)
43 }
44
45 // Make the Attrs struct implement the sql.Scanner interface. This method
46 // simply decodes a JSON-encoded value into the struct fields.
47-func (p *PostData) Scan(value interface{}) error {
48+func (p *ProjectAcl) Scan(value interface{}) error {
49 b, ok := value.([]byte)
50 if !ok {
51 return errors.New("type assertion to []byte failed")
52@@ -288,6 +311,7 @@ type DB interface {
53
54 InsertProject(userID, name, projectDir string) (string, error)
55 UpdateProject(userID, name string) error
56+ UpdateProjectAcl(userID, name string, acl ProjectAcl) error
57 LinkToProject(userID, projectID, projectDir string, commit bool) error
58 RemoveProject(projectID string) error
59 FindProjectByName(userID, name string) (*Project, error)
+16,
-5
1@@ -255,17 +255,18 @@ const (
2
3 sqlInsertProject = `INSERT INTO projects (user_id, name, project_dir) VALUES ($1, $2, $3) RETURNING id;`
4 sqlUpdateProject = `UPDATE projects SET updated_at = $3 WHERE user_id = $1 AND name = $2;`
5- sqlFindProjectByName = `SELECT id, user_id, name, project_dir, created_at, updated_at FROM projects WHERE user_id = $1 AND name = $2;`
6+ sqlUpdateProjectAcl = `UPDATE projects SET acl = $3, updated_at = $4 WHERE user_id = $1 AND name = $2;`
7+ sqlFindProjectByName = `SELECT id, user_id, name, project_dir, acl, created_at, updated_at FROM projects WHERE user_id = $1 AND name = $2;`
8 sqlSelectProjectCount = `SELECT count(id) FROM projects`
9 sqlFindAllProjects = `
10- SELECT projects.id, user_id, app_users.name as username, projects.name, project_dir, projects.created_at, projects.updated_at
11+ SELECT projects.id, user_id, app_users.name as username, projects.name, project_dir, projects.acl, projects.created_at, projects.updated_at
12 FROM projects
13 LEFT JOIN app_users ON app_users.id = projects.user_id
14 ORDER BY created_at ASC
15 LIMIT $1 OFFSET $2`
16- sqlFindProjectsByUser = `SELECT id, user_id, name, project_dir, created_at, updated_at FROM projects WHERE user_id = $1 ORDER BY updated_at DESC;`
17- sqlFindProjectsByPrefix = `SELECT id, user_id, name, project_dir, created_at, updated_at FROM projects WHERE user_id = $1 AND name = project_dir AND name ILIKE $2 ORDER BY updated_at ASC, name ASC;`
18- sqlFindProjectLinks = `SELECT id, user_id, name, project_dir, created_at, updated_at FROM projects WHERE user_id = $1 AND name != project_dir AND project_dir = $2 ORDER BY name ASC;`
19+ sqlFindProjectsByUser = `SELECT id, user_id, name, project_dir, acl, created_at, updated_at FROM projects WHERE user_id = $1 ORDER BY updated_at DESC;`
20+ sqlFindProjectsByPrefix = `SELECT id, user_id, name, project_dir, acl, created_at, updated_at FROM projects WHERE user_id = $1 AND name = project_dir AND name ILIKE $2 ORDER BY updated_at ASC, name ASC;`
21+ sqlFindProjectLinks = `SELECT id, user_id, name, project_dir, acl, created_at, updated_at FROM projects WHERE user_id = $1 AND name != project_dir AND project_dir = $2 ORDER BY name ASC;`
22 sqlLinkToProject = `UPDATE projects SET project_dir = $1, updated_at = $2 WHERE id = $3;`
23 sqlRemoveProject = `DELETE FROM projects WHERE id = $1;`
24 )
25@@ -1257,6 +1258,11 @@ func (me *PsqlDB) UpdateProject(userID, name string) error {
26 return err
27 }
28
29+func (me *PsqlDB) UpdateProjectAcl(userID, name string, acl db.ProjectAcl) error {
30+ _, err := me.Db.Exec(sqlUpdateProjectAcl, userID, name, acl, time.Now())
31+ return err
32+}
33+
34 func (me *PsqlDB) LinkToProject(userID, projectID, projectDir string, commit bool) error {
35 linkToProject, err := me.FindProjectByName(userID, projectDir)
36 if err != nil {
37@@ -1314,6 +1320,7 @@ func (me *PsqlDB) FindProjectByName(userID, name string) (*db.Project, error) {
38 &project.UserID,
39 &project.Name,
40 &project.ProjectDir,
41+ &project.Acl,
42 &project.CreatedAt,
43 &project.UpdatedAt,
44 )
45@@ -1337,6 +1344,7 @@ func (me *PsqlDB) FindProjectLinks(userID, name string) ([]*db.Project, error) {
46 &project.UserID,
47 &project.Name,
48 &project.ProjectDir,
49+ &project.Acl,
50 &project.CreatedAt,
51 &project.UpdatedAt,
52 )
53@@ -1367,6 +1375,7 @@ func (me *PsqlDB) FindProjectsByPrefix(userID, prefix string) ([]*db.Project, er
54 &project.UserID,
55 &project.Name,
56 &project.ProjectDir,
57+ &project.Acl,
58 &project.CreatedAt,
59 &project.UpdatedAt,
60 )
61@@ -1397,6 +1406,7 @@ func (me *PsqlDB) FindProjectsByUser(userID string) ([]*db.Project, error) {
62 &project.UserID,
63 &project.Name,
64 &project.ProjectDir,
65+ &project.Acl,
66 &project.CreatedAt,
67 &project.UpdatedAt,
68 )
69@@ -1428,6 +1438,7 @@ func (me *PsqlDB) FindAllProjects(page *db.Pager) (*db.Paginate[*db.Project], er
70 &project.Username,
71 &project.Name,
72 &project.ProjectDir,
73+ &project.Acl,
74 &project.CreatedAt,
75 &project.UpdatedAt,
76 )
+1,
-1
1@@ -41,7 +41,7 @@ func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
2 scp.Middleware(handler),
3 wishrsync.Middleware(handler),
4 auth.Middleware(handler),
5- bm.Middleware(cms.Middleware(&handler.Cfg.ConfigCms, handler.Cfg)),
6+ wsh.PtyMdw(bm.Middleware(cms.Middleware(&handler.Cfg.ConfigCms, handler.Cfg))),
7 wsh.LogMiddleware(handler.GetLogger()),
8 }
9 }
+1,
-2
1@@ -16,7 +16,6 @@ import (
2 futil "github.com/picosh/pico/filehandlers/util"
3 "github.com/picosh/pico/shared"
4 "github.com/picosh/pico/shared/storage"
5- "github.com/picosh/pico/wish/cms/util"
6 "github.com/picosh/pobj"
7 sst "github.com/picosh/pobj/storage"
8 "github.com/picosh/send/send/utils"
9@@ -165,7 +164,7 @@ func (h *UploadAssetHandler) List(s ssh.Session, fpath string, isDir bool, recur
10
11 func (h *UploadAssetHandler) Validate(s ssh.Session) error {
12 var err error
13- key, err := util.KeyText(s)
14+ key, err := shared.KeyText(s)
15 if err != nil {
16 return fmt.Errorf("key not found")
17 }
M
go.mod
+14,
-9
1@@ -6,11 +6,11 @@ require (
2 github.com/alecthomas/chroma v0.10.0
3 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
4 github.com/charmbracelet/bubbles v0.16.1
5- github.com/charmbracelet/bubbletea v0.24.2
6+ github.com/charmbracelet/bubbletea v0.25.0
7 github.com/charmbracelet/lipgloss v0.9.1
8 github.com/charmbracelet/promwish v0.7.0
9- github.com/charmbracelet/ssh v0.0.0-20230822194956-1a051f898e09
10- github.com/charmbracelet/wish v1.2.0
11+ github.com/charmbracelet/ssh v0.0.0-20240130183930-33d2a30e8568
12+ github.com/charmbracelet/wish v1.3.0
13 github.com/google/go-cmp v0.6.0
14 github.com/gorilla/feeds v1.1.2
15 github.com/lib/pq v1.10.9
16@@ -21,13 +21,14 @@ require (
17 github.com/neurosnap/go-exif-remove v0.0.0-20221010134343-50d1e3c35577
18 github.com/patrickmn/go-cache v2.1.0+incompatible
19 github.com/picosh/pobj v0.0.0-20240218150308-1dc70e819bbf
20+ github.com/picosh/ptun v0.0.0-20240220214714-1c94994f91cc
21 github.com/picosh/send v0.0.0-20240217194807-77b972121e63
22 github.com/sendgrid/sendgrid-go v3.13.0+incompatible
23 github.com/yuin/goldmark v1.6.0
24 github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
25 github.com/yuin/goldmark-meta v1.1.0
26 go.abhg.dev/goldmark/anchor v0.1.1
27- golang.org/x/crypto v0.17.0
28+ golang.org/x/crypto v0.18.0
29 gopkg.in/yaml.v2 v2.4.0
30 )
31
32@@ -43,8 +44,11 @@ require (
33 github.com/beorn7/perks v1.0.1 // indirect
34 github.com/cespare/xxhash/v2 v2.2.0 // indirect
35 github.com/charmbracelet/keygen v0.5.0 // indirect
36- github.com/charmbracelet/log v0.3.0 // indirect
37+ github.com/charmbracelet/log v0.3.1 // indirect
38+ github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 // indirect
39+ github.com/charmbracelet/x/exp/term v0.0.0-20240130180102-bafe6fbaee60 // indirect
40 github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
41+ github.com/creack/pty v1.1.21 // indirect
42 github.com/dlclark/regexp2 v1.10.0 // indirect
43 github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d // indirect
44 github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
45@@ -105,12 +109,13 @@ require (
46 github.com/tinylib/msgp v1.1.9 // indirect
47 github.com/tklauser/go-sysconf v0.3.12 // indirect
48 github.com/tklauser/numcpus v0.6.1 // indirect
49+ github.com/u-root/u-root v0.11.0 // indirect
50 github.com/yusufpapurcu/wmi v1.2.3 // indirect
51 golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
52- golang.org/x/net v0.18.0 // indirect
53- golang.org/x/sync v0.5.0 // indirect
54- golang.org/x/sys v0.15.0 // indirect
55- golang.org/x/term v0.15.0 // indirect
56+ golang.org/x/net v0.19.0 // indirect
57+ golang.org/x/sync v0.6.0 // indirect
58+ golang.org/x/sys v0.16.0 // indirect
59+ golang.org/x/term v0.16.0 // indirect
60 golang.org/x/text v0.14.0 // indirect
61 google.golang.org/protobuf v1.31.0 // indirect
62 gopkg.in/ini.v1 v1.67.0 // indirect
M
go.sum
+30,
-20
1@@ -25,22 +25,28 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
2 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
3 github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
4 github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
5-github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=
6-github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
7+github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
8+github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
9 github.com/charmbracelet/keygen v0.5.0 h1:XY0fsoYiCSM9axkrU+2ziE6u6YjJulo/b9Dghnw6MZc=
10 github.com/charmbracelet/keygen v0.5.0/go.mod h1:DfvCgLHxZ9rJxdK0DGw3C/LkV4SgdGbnliHcObV3L+8=
11 github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
12 github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
13-github.com/charmbracelet/log v0.3.0 h1:u5aB2KJDgNZo4WOfOC8C+KvGIkJ2rCFNlPWDu6xhnqI=
14-github.com/charmbracelet/log v0.3.0/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g=
15+github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw=
16+github.com/charmbracelet/log v0.3.1/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g=
17 github.com/charmbracelet/promwish v0.7.0 h1:oaMH+ey6W4DDIv1xucS8jL1ik/Q46qxjNXlh6XxEm+s=
18 github.com/charmbracelet/promwish v0.7.0/go.mod h1:WbRJN9irg8LmsBU8G2rFF8md9O3rSg63qrnqquP/+cs=
19-github.com/charmbracelet/ssh v0.0.0-20230822194956-1a051f898e09 h1:ZDIQmTtohv0S/AAYE//w8mYTxCzqphhF1+4ACPDMiLU=
20-github.com/charmbracelet/ssh v0.0.0-20230822194956-1a051f898e09/go.mod h1:F1vgddWsb/Yr/OZilFeRZEh5sE/qU0Dt1mKkmke6Zvg=
21-github.com/charmbracelet/wish v1.2.0 h1:h5Wj9pr97IQz/l4gM5Xep2lXcY/YM+6O2RC2o3x0JIQ=
22-github.com/charmbracelet/wish v1.2.0/go.mod h1:JX3fC+178xadJYAhPu6qWtVDpJTwpnFvpdjz9RKJlUE=
23+github.com/charmbracelet/ssh v0.0.0-20240130183930-33d2a30e8568 h1:augSeqKvp+CMPYCHEZ2/OcZ0em1w8A3pGSmUyrixPNs=
24+github.com/charmbracelet/ssh v0.0.0-20240130183930-33d2a30e8568/go.mod h1:IHy7o73i1MrQ5lmyJjjJ0g7y4+V+g69cm+Y7JCiZWPo=
25+github.com/charmbracelet/wish v1.3.0 h1:SYV5TIlzDb6WaxjkkYXxv2WZsTu/QZGwfGVc0UB5M48=
26+github.com/charmbracelet/wish v1.3.0/go.mod h1:1U/bI7zX+IE26ThD5gxtLgeRzctVhSrTpjucPqw4Pos=
27+github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 h1:3RXpZWGWTOeVXCTv0Dnzxdv/MhNUkBfEcbaTY0zrTQI=
28+github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
29+github.com/charmbracelet/x/exp/term v0.0.0-20240130180102-bafe6fbaee60 h1:IV19YKUZVf6ATrhiPSCirZ4Bs7EsenYwOWcUHngV+q0=
30+github.com/charmbracelet/x/exp/term v0.0.0-20240130180102-bafe6fbaee60/go.mod h1:kOOxxyxgAFQVcR5yQJWTuLjzt5dR2pcgwy3WaLEudjE=
31 github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
32 github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
33+github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
34+github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
35 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
36 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
37 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
38@@ -184,6 +190,8 @@ github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
39 github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
40 github.com/picosh/pobj v0.0.0-20240218150308-1dc70e819bbf h1:xtr5DoSgoOqbPFnm8p5YjTqrRKVW8KYI0bDwwRiG8yI=
41 github.com/picosh/pobj v0.0.0-20240218150308-1dc70e819bbf/go.mod h1:ILtZ0GOqkozrrGCvyJqCSUIwal2XQqzSfbKCNdS+HyU=
42+github.com/picosh/ptun v0.0.0-20240220214714-1c94994f91cc h1:DYPOyRg5z5BLGwbQMGnnTP4iBCEmb3k7OxQNJguD7iA=
43+github.com/picosh/ptun v0.0.0-20240220214714-1c94994f91cc/go.mod h1:uQfDebjN3JJPsI3PAx8T5rmJwdpfmjvdRe7fXY33Kbw=
44 github.com/picosh/send v0.0.0-20240217194807-77b972121e63 h1:VSSbAejFzj2KBThfVnMcNXQwzHmwjPUridgi29LxihU=
45 github.com/picosh/send v0.0.0-20240217194807-77b972121e63/go.mod h1:1JCq0NVOdTDenQ0/Kd8e4rP80lu06UHJJ+6dQxhcpew=
46 github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
47@@ -244,6 +252,10 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA
48 github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
49 github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
50 github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
51+github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90 h1:zTk5683I9K62wtZ6eUa6vu6IWwVHXPnoKK5n2unAwv0=
52+github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90/go.mod h1:lYt+LVfZBBwDZ3+PHk4k/c/TnKOkjJXiJO73E32Mmpc=
53+github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8=
54+github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY=
55 github.com/yuin/goldmark v1.4.5/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
56 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
57 github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
58@@ -259,10 +271,9 @@ go.abhg.dev/goldmark/anchor v0.1.1/go.mod h1:zYKiaHXTdugwVJRZqInVdmNGQRM3ZRJ6AGB
59 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
60 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
61 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
62-golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
63 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
64-golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
65-golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
66+golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
67+golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
68 golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w=
69 golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
70 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
71@@ -276,20 +287,19 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/
72 golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
73 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
74 golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
75-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
76 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
77 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
78 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
79 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
80 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
81-golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
82-golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
83+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
84+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
85 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
86 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
87 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
88 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
89-golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
90-golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
91+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
92+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
93 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
94 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
95 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
96@@ -309,15 +319,15 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
97 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
98 golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
99 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
100-golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
101-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
102+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
103+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
104 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
105 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
106 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
107 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
108 golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
109-golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
110-golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
111+golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
112+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
113 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
114 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
115 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+1,
-1
1@@ -41,7 +41,7 @@ func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
2 scp.Middleware(handler),
3 wishrsync.Middleware(handler),
4 auth.Middleware(handler),
5- bm.Middleware(cms.Middleware(&handler.Cfg.ConfigCms, handler.Cfg)),
6+ wsh.PtyMdw(bm.Middleware(cms.Middleware(&handler.Cfg.ConfigCms, handler.Cfg))),
7 wsh.LogMiddleware(handler.GetLogger()),
8 }
9 }
+44,
-0
1@@ -0,0 +1,44 @@
2+package pgs
3+
4+import (
5+ "slices"
6+
7+ "github.com/picosh/pico/db"
8+ "golang.org/x/crypto/ssh"
9+)
10+
11+func HasProjectAccess(project *db.Project, owner *db.User, requester *db.User, pubkey ssh.PublicKey) bool {
12+ aclType := project.Acl.Type
13+ data := project.Acl.Data
14+
15+ if aclType == "public" {
16+ return true
17+ }
18+
19+ if requester != nil {
20+ if owner.ID == requester.ID {
21+ return true
22+ }
23+ }
24+
25+ if aclType == "pico" {
26+ if requester == nil {
27+ return false
28+ }
29+
30+ if len(data) == 0 {
31+ return true
32+ }
33+ return slices.Contains(data, requester.Name)
34+ }
35+
36+ if aclType == "public_keys" {
37+ key := ssh.FingerprintSHA256(pubkey)
38+ if len(data) == 0 {
39+ return true
40+ }
41+ return slices.Contains(data, key)
42+ }
43+
44+ return true
45+}
+24,
-19
1@@ -394,6 +394,30 @@ func AssetRequest(w http.ResponseWriter, r *http.Request) {
2 ServeAsset(fname, nil, false, w, r)
3 }
4
5+func PrivateAssetRequest(w http.ResponseWriter, r *http.Request) {
6+ fname, _ := url.PathUnescape(shared.GetField(r, 0))
7+ ServeAsset(fname, nil, false, w, r)
8+}
9+
10+var mainRoutes = []shared.Route{
11+ shared.NewRoute("GET", "/main.css", shared.ServeFile("main.css", "text/css")),
12+ shared.NewRoute("GET", "/card.png", shared.ServeFile("card.png", "image/png")),
13+ shared.NewRoute("GET", "/favicon-16x16.png", shared.ServeFile("favicon-16x16.png", "image/png")),
14+ shared.NewRoute("GET", "/apple-touch-icon.png", shared.ServeFile("apple-touch-icon.png", "image/png")),
15+ shared.NewRoute("GET", "/favicon.ico", shared.ServeFile("favicon.ico", "image/x-icon")),
16+ shared.NewRoute("GET", "/robots.txt", shared.ServeFile("robots.txt", "text/plain")),
17+
18+ shared.NewRoute("GET", "/", shared.CreatePageHandler("html/marketing.page.tmpl")),
19+ shared.NewRoute("GET", "/check", checkHandler),
20+ shared.NewRoute("GET", "/rss", rssHandler),
21+ shared.NewRoute("GET", "/(.+)", shared.CreatePageHandler("html/marketing.page.tmpl")),
22+}
23+var subdomainRoutes = []shared.Route{
24+ shared.NewRoute("GET", "/", AssetRequest),
25+ shared.NewRoute("GET", "(/.+.(?:jpg|jpeg|png|gif|webp|svg))(/.+)", ImgAssetRequest),
26+ shared.NewRoute("GET", "/(.+)", AssetRequest),
27+}
28+
29 func StartApiServer() {
30 cfg := NewConfigSite()
31 logger := cfg.Logger
32@@ -419,25 +443,6 @@ func StartApiServer() {
33 return
34 }
35
36- mainRoutes := []shared.Route{
37- shared.NewRoute("GET", "/main.css", shared.ServeFile("main.css", "text/css")),
38- shared.NewRoute("GET", "/card.png", shared.ServeFile("card.png", "image/png")),
39- shared.NewRoute("GET", "/favicon-16x16.png", shared.ServeFile("favicon-16x16.png", "image/png")),
40- shared.NewRoute("GET", "/apple-touch-icon.png", shared.ServeFile("apple-touch-icon.png", "image/png")),
41- shared.NewRoute("GET", "/favicon.ico", shared.ServeFile("favicon.ico", "image/x-icon")),
42- shared.NewRoute("GET", "/robots.txt", shared.ServeFile("robots.txt", "text/plain")),
43-
44- shared.NewRoute("GET", "/", shared.CreatePageHandler("html/marketing.page.tmpl")),
45- shared.NewRoute("GET", "/check", checkHandler),
46- shared.NewRoute("GET", "/rss", rssHandler),
47- shared.NewRoute("GET", "/(.+)", shared.CreatePageHandler("html/marketing.page.tmpl")),
48- }
49- subdomainRoutes := []shared.Route{
50- shared.NewRoute("GET", "/", AssetRequest),
51- shared.NewRoute("GET", "(/.+.(?:jpg|jpeg|png|gif|webp|svg))(/.+)", ImgAssetRequest),
52- shared.NewRoute("GET", "/(.+)", AssetRequest),
53- }
54-
55 handler := shared.CreateServe(mainRoutes, subdomainRoutes, cfg, db, st, logger, cache)
56 router := http.HandlerFunc(handler)
57
+21,
-0
1@@ -7,6 +7,7 @@ import (
2 "log/slog"
3 "os"
4 "path/filepath"
5+ "strings"
6
7 "github.com/picosh/pico/db"
8 "github.com/picosh/pico/shared"
9@@ -30,6 +31,7 @@ func getHelpText(userName, projectName string) string {
10 helpStr += fmt.Sprintf("`%s prune %s`: removes all projects that match prefix `%s` and is not linked to another project\n", sshCmdStr, projectName, projectName)
11 helpStr += fmt.Sprintf("`%s retain %s`: alias for `prune` but retains the (3) most recently updated projects\n", sshCmdStr, projectName)
12 helpStr += fmt.Sprintf("`%s depends %s`: lists all projects linked to `%s`\n", sshCmdStr, projectName, projectName)
13+ helpStr += fmt.Sprintf("`%s acl %s [public, public_keys, pico] [comma delimited shasum keys or pico usernames]`: control access to project `%s`\n", sshCmdStr, projectName, projectName)
14 return helpStr
15 }
16
17@@ -407,3 +409,22 @@ func (c *Cmd) rm(projectName string) error {
18 err = c.RmProjectAssets(project.Name)
19 return err
20 }
21+
22+func (c *Cmd) acl(projectName, aclType string, acls []string) error {
23+ c.Log.Info(
24+ "user running `acl` command",
25+ "user", c.User.Name,
26+ "project", projectName,
27+ "actType", aclType,
28+ "acls", acls,
29+ )
30+ c.output(fmt.Sprintf("setting acl for %s to %s (%s)", projectName, aclType, strings.Join(acls, ",")))
31+ acl := db.ProjectAcl{
32+ Type: aclType,
33+ Data: acls,
34+ }
35+ if c.Write {
36+ return c.Dbpool.UpdateProjectAcl(c.User.ID, projectName, acl)
37+ }
38+ return nil
39+}
+3,
-2
1@@ -14,6 +14,7 @@ import (
2 "github.com/muesli/reflow/wrap"
3 "github.com/picosh/pico/db"
4 "github.com/picosh/pico/db/postgres"
5+ "github.com/picosh/pico/shared"
6 "github.com/picosh/pico/shared/storage"
7 "github.com/picosh/pico/wish/cms/config"
8 "github.com/picosh/pico/wish/cms/ui/account"
9@@ -22,7 +23,6 @@ import (
10 "github.com/picosh/pico/wish/cms/ui/keys"
11 "github.com/picosh/pico/wish/cms/ui/tokens"
12 "github.com/picosh/pico/wish/cms/ui/username"
13- "github.com/picosh/pico/wish/cms/util"
14 )
15
16 type status int
17@@ -80,6 +80,7 @@ type GotDBMsg db.DB
18
19 func CmsMiddleware(cfg *config.ConfigCms, urls config.ConfigURL) bm.Handler {
20 return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
21+ fmt.Println("MADE IT HERE")
22 logger := cfg.Logger
23
24 _, _, active := s.Pty()
25@@ -87,7 +88,7 @@ func CmsMiddleware(cfg *config.ConfigCms, urls config.ConfigURL) bm.Handler {
26 logger.Info("no active terminal, skipping")
27 return nil, nil
28 }
29- key, err := util.KeyText(s)
30+ key, err := shared.KeyText(s)
31 if err != nil {
32 logger.Error(err.Error())
33 }
+122,
-2
1@@ -2,7 +2,9 @@ package pgs
2
3 import (
4 "context"
5+ "encoding/json"
6 "fmt"
7+ "net/http"
8 "os"
9 "os/signal"
10 "syscall"
11@@ -12,11 +14,13 @@ import (
12 "github.com/charmbracelet/ssh"
13 "github.com/charmbracelet/wish"
14 bm "github.com/charmbracelet/wish/bubbletea"
15+ gocache "github.com/patrickmn/go-cache"
16 "github.com/picosh/pico/db/postgres"
17 uploadassets "github.com/picosh/pico/filehandlers/assets"
18 "github.com/picosh/pico/shared"
19 "github.com/picosh/pico/shared/storage"
20 wsh "github.com/picosh/pico/wish"
21+ "github.com/picosh/ptun"
22 "github.com/picosh/send/list"
23 "github.com/picosh/send/pipe"
24 "github.com/picosh/send/proxy"
25@@ -28,7 +32,21 @@ import (
26
27 type SSHServer struct{}
28
29+type ctxPublicKey struct{}
30+
31+func getPublicKeyCtx(ctx ssh.Context) (ssh.PublicKey, error) {
32+ pk, ok := ctx.Value(ctxPublicKey{}).(ssh.PublicKey)
33+ if !ok {
34+ return nil, fmt.Errorf("public key not set on `ssh.Context()` for connection")
35+ }
36+ return pk, nil
37+}
38+func setPublicKeyCtx(ctx ssh.Context, pk ssh.PublicKey) {
39+ ctx.SetValue(ctxPublicKey{}, pk)
40+}
41+
42 func (me *SSHServer) authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
43+ setPublicKeyCtx(ctx, key)
44 return true
45 }
46
47@@ -37,11 +55,11 @@ func createRouter(cfg *shared.ConfigSite, handler *uploadassets.UploadAssetHandl
48 return []wish.Middleware{
49 pipe.Middleware(handler, ""),
50 list.Middleware(handler),
51- WishMiddleware(handler),
52 scp.Middleware(handler),
53 wishrsync.Middleware(handler),
54 auth.Middleware(handler),
55- bm.Middleware(CmsMiddleware(&cfg.ConfigCms, cfg)),
56+ wsh.PtyMdw(bm.Middleware(CmsMiddleware(&cfg.ConfigCms, cfg))),
57+ WishMiddleware(handler),
58 wsh.LogMiddleware(handler.GetLogger()),
59 }
60 }
61@@ -58,6 +76,16 @@ func withProxy(cfg *shared.ConfigSite, handler *uploadassets.UploadAssetHandler,
62 }
63 }
64
65+func unauthorizedHandler(w http.ResponseWriter, r *http.Request) {
66+ http.Error(w, "You do not have access to this site", http.StatusUnauthorized)
67+}
68+
69+type PicoApi struct {
70+ UserID string `json:"user_id"`
71+ UserName string `json:"username"`
72+ PublicKey string `json:"public_key"`
73+}
74+
75 func StartSshServer() {
76 host := shared.GetEnv("PGS_HOST", "0.0.0.0")
77 port := shared.GetEnv("PGS_SSH_PORT", "2222")
78@@ -85,12 +113,104 @@ func StartSshServer() {
79 cfg,
80 st,
81 )
82+ cache := gocache.New(2*time.Minute, 5*time.Minute)
83+
84+ webTunnel := &ptun.WebTunnelHandler{
85+ HttpHandler: func(ctx ssh.Context) http.Handler {
86+ subdomain := ctx.User()
87+ log := logger.With(
88+ "subdomain", subdomain,
89+ )
90+
91+ pubkey, err := getPublicKeyCtx(ctx)
92+ if err != nil {
93+ log.Error(err.Error(), "subdomain", subdomain)
94+ return http.HandlerFunc(unauthorizedHandler)
95+ }
96+ pubkeyStr, err := shared.KeyForKeyText(pubkey)
97+ if err != nil {
98+ log.Error(err.Error())
99+ return http.HandlerFunc(unauthorizedHandler)
100+ }
101+ log = log.With(
102+ "pubkey", pubkeyStr,
103+ )
104+
105+ props, err := getProjectFromSubdomain(subdomain)
106+ if err != nil {
107+ log.Error(err.Error())
108+ return http.HandlerFunc(unauthorizedHandler)
109+ }
110+
111+ owner, err := dbh.FindUserForName(props.Username)
112+ if err != nil {
113+ log.Error(err.Error())
114+ return http.HandlerFunc(unauthorizedHandler)
115+ }
116+ log = log.With(
117+ "owner", owner.Name,
118+ )
119+
120+ project, err := dbh.FindProjectByName(owner.ID, props.ProjectName)
121+ if err != nil {
122+ log.Error(err.Error())
123+ return http.HandlerFunc(unauthorizedHandler)
124+ }
125+
126+ requester, _ := dbh.FindUserForKey("", pubkeyStr)
127+ if requester != nil {
128+ log = logger.With(
129+ "requester", requester.Name,
130+ )
131+ }
132+
133+ if !HasProjectAccess(project, owner, requester, pubkey) {
134+ log.Error("no access")
135+ return http.HandlerFunc(unauthorizedHandler)
136+ }
137+
138+ log.Info("user has access to site")
139+
140+ routes := []shared.Route{
141+ // special API endpoint for tunnel users accessing site
142+ shared.NewRoute("GET", "/pico", func(w http.ResponseWriter, r *http.Request) {
143+ w.Header().Set("Content-Type", "application/json")
144+ pico := &PicoApi{
145+ UserID: "",
146+ UserName: "",
147+ PublicKey: pubkeyStr,
148+ }
149+ if requester != nil {
150+ pico.UserID = requester.ID
151+ pico.UserName = requester.Name
152+ }
153+ err := json.NewEncoder(w).Encode(pico)
154+ if err != nil {
155+ log.Error(err.Error())
156+ }
157+ }),
158+ }
159+ routes = append(routes, subdomainRoutes...)
160+ httpHandler := shared.CreateServeBasic(
161+ routes,
162+ subdomain,
163+ cfg,
164+ dbh,
165+ st,
166+ logger,
167+ cache,
168+ )
169+ httpRouter := http.HandlerFunc(httpHandler)
170+ return httpRouter
171+ },
172+ }
173
174 sshServer := &SSHServer{}
175 s, err := wish.NewServer(
176 wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
177 wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
178 wish.WithPublicKeyAuth(sshServer.authHandler),
179+ ptun.WithWebTunnel(webTunnel),
180 withProxy(
181 cfg,
182 handler,
+48,
-17
1@@ -2,19 +2,20 @@ package pgs
2
3 import (
4 "fmt"
5+ "slices"
6 "strings"
7
8 "github.com/charmbracelet/ssh"
9 "github.com/charmbracelet/wish"
10 "github.com/picosh/pico/db"
11 uploadassets "github.com/picosh/pico/filehandlers/assets"
12- "github.com/picosh/pico/wish/cms/util"
13+ "github.com/picosh/pico/shared"
14 "github.com/picosh/send/send/utils"
15 )
16
17 func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
18 var err error
19- key, err := util.KeyText(s)
20+ key, err := shared.KeyText(s)
21 if err != nil {
22 return nil, fmt.Errorf("key not found")
23 }
24@@ -37,25 +38,24 @@ func WishMiddleware(handler *uploadassets.UploadAssetHandler) wish.Middleware {
25 cfg := handler.Cfg
26 store := handler.Storage
27
28- return func(sshHandler ssh.Handler) ssh.Handler {
29- return func(session ssh.Session) {
30- _, _, activePty := session.Pty()
31+ return func(next ssh.Handler) ssh.Handler {
32+ return func(sesh ssh.Session) {
33+ _, _, activePty := sesh.Pty()
34 if activePty {
35- _ = session.Exit(0)
36- _ = session.Close()
37+ next(sesh)
38 return
39 }
40
41- user, err := getUser(session, dbpool)
42+ user, err := getUser(sesh, dbpool)
43 if err != nil {
44- utils.ErrorHandler(session, err)
45+ utils.ErrorHandler(sesh, err)
46 return
47 }
48
49- args := session.Command()
50+ args := sesh.Command()
51
52 opts := Cmd{
53- Session: session,
54+ Session: sesh,
55 User: user,
56 Store: store,
57 Log: log,
58@@ -77,8 +77,7 @@ func WishMiddleware(handler *uploadassets.UploadAssetHandler) wish.Middleware {
59 opts.bail(err)
60 return
61 } else {
62- e := fmt.Errorf("%s not a valid command", args)
63- opts.bail(e)
64+ next(sesh)
65 return
66 }
67 }
68@@ -87,13 +86,13 @@ func WishMiddleware(handler *uploadassets.UploadAssetHandler) wish.Middleware {
69 projectName := strings.TrimSpace(args[1])
70
71 if projectName == "--write" {
72- utils.ErrorHandler(session, fmt.Errorf("`--write` should be placed at end of command"))
73+ utils.ErrorHandler(sesh, fmt.Errorf("`--write` should be placed at end of command"))
74 return
75 }
76
77 if cmd == "link" {
78 if len(args) < 3 {
79- utils.ErrorHandler(session, fmt.Errorf("must supply link command like: `projectA link projectB`"))
80+ utils.ErrorHandler(sesh, fmt.Errorf("must supply link command like: `projectA link projectB`"))
81 return
82 }
83 linkTo := strings.TrimSpace(args[2])
84@@ -137,9 +136,41 @@ func WishMiddleware(handler *uploadassets.UploadAssetHandler) wish.Middleware {
85 opts.notice()
86 opts.bail(err)
87 return
88+ } else if cmd == "acl" {
89+ aclType := strings.TrimSpace(args[2])
90+ if !slices.Contains([]string{"public", "public_keys", "pico"}, aclType) {
91+ err := fmt.Errorf("acl type must be one of the following: [public, public_keys, pico], found %s", aclType)
92+ opts.bail(err)
93+ return
94+ }
95+
96+ aclsRaw := ""
97+ if len(args) == 4 {
98+ if strings.TrimSpace(args[3]) == "--write" {
99+ opts.Write = true
100+ } else {
101+ aclsRaw = strings.TrimSpace(args[3])
102+ }
103+ }
104+ if len(args) == 5 {
105+ aclsRaw = strings.TrimSpace(args[3])
106+ if strings.TrimSpace(args[4]) == "--write" {
107+ opts.Write = true
108+ }
109+ }
110+ acls := []string{}
111+ for _, key := range strings.Split(aclsRaw, ",") {
112+ st := strings.TrimSpace(key)
113+ if st == "" {
114+ continue
115+ }
116+ acls = append(acls, st)
117+ }
118+ err := opts.acl(projectName, aclType, acls)
119+ opts.notice()
120+ opts.bail(err)
121 } else {
122- e := fmt.Errorf("%s not a valid command", args)
123- opts.bail(e)
124+ next(sesh)
125 return
126 }
127 }
+1,
-1
1@@ -42,7 +42,7 @@ func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
2 scp.Middleware(handler),
3 wishrsync.Middleware(handler),
4 auth.Middleware(handler),
5- bm.Middleware(cms.Middleware(&handler.Cfg.ConfigCms, handler.Cfg)),
6+ wsh.PtyMdw(bm.Middleware(cms.Middleware(&handler.Cfg.ConfigCms, handler.Cfg))),
7 wsh.LogMiddleware(handler.GetLogger()),
8 }
9 }
1@@ -256,6 +256,6 @@ func CreateLogger(debug bool) *slog.Logger {
2 AddSource: true,
3 }
4 return slog.New(
5- slog.NewJSONHandler(os.Stdout, opts),
6+ slog.NewTextHandler(os.Stdout, opts),
7 )
8 }
1@@ -46,44 +46,23 @@ func CreatePProfRoutes(routes []Route) []Route {
2
3 type ServeFn func(http.ResponseWriter, *http.Request)
4
5-func CreateServe(routes []Route, subdomainRoutes []Route, cfg *ConfigSite, dbpool db.DB, st storage.StorageServe, logger *slog.Logger, cache *cache.Cache) ServeFn {
6+func CreateServeBasic(routes []Route, subdomain string, cfg *ConfigSite, dbpool db.DB, st storage.StorageServe, logger *slog.Logger, cache *cache.Cache) ServeFn {
7 return func(w http.ResponseWriter, r *http.Request) {
8 var allow []string
9- var subdomain string
10- curRoutes := routes
11-
12- if cfg.IsCustomdomains() || cfg.IsSubdomains() {
13- hostDomain := strings.ToLower(strings.Split(r.Host, ":")[0])
14- appDomain := strings.ToLower(strings.Split(cfg.ConfigCms.Domain, ":")[0])
15-
16- if hostDomain != appDomain {
17- if strings.Contains(hostDomain, appDomain) {
18- subdomain = strings.TrimSuffix(hostDomain, fmt.Sprintf(".%s", appDomain))
19- if subdomain != "" {
20- curRoutes = subdomainRoutes
21- }
22- } else {
23- subdomain = GetCustomDomain(hostDomain, cfg.Space)
24- if subdomain != "" {
25- curRoutes = subdomainRoutes
26- }
27- }
28- }
29- }
30-
31- for _, route := range curRoutes {
32+ loggerCtx := context.WithValue(r.Context(), ctxLoggerKey{}, logger)
33+ subdomainCtx := context.WithValue(loggerCtx, ctxSubdomainKey{}, subdomain)
34+ dbCtx := context.WithValue(subdomainCtx, ctxDBKey{}, dbpool)
35+ storageCtx := context.WithValue(dbCtx, ctxStorageKey{}, st)
36+ cfgCtx := context.WithValue(storageCtx, ctxCfg{}, cfg)
37+ cacheCtx := context.WithValue(cfgCtx, ctxCacheKey{}, cache)
38+
39+ for _, route := range routes {
40 matches := route.Regex.FindStringSubmatch(r.URL.Path)
41 if len(matches) > 0 {
42 if r.Method != route.Method {
43 allow = append(allow, route.Method)
44 continue
45 }
46- loggerCtx := context.WithValue(r.Context(), ctxLoggerKey{}, logger)
47- subdomainCtx := context.WithValue(loggerCtx, ctxSubdomainKey{}, subdomain)
48- dbCtx := context.WithValue(subdomainCtx, ctxDBKey{}, dbpool)
49- storageCtx := context.WithValue(dbCtx, ctxStorageKey{}, st)
50- cfgCtx := context.WithValue(storageCtx, ctxCfg{}, cfg)
51- cacheCtx := context.WithValue(cfgCtx, ctxCacheKey{}, cache)
52 ctx := context.WithValue(cacheCtx, ctxKey{}, matches[1:])
53 route.Handler(w, r.WithContext(ctx))
54 return
55@@ -98,6 +77,39 @@ func CreateServe(routes []Route, subdomainRoutes []Route, cfg *ConfigSite, dbpoo
56 }
57 }
58
59+func findRouteConfig(r *http.Request, routes []Route, subdomainRoutes []Route, cfg *ConfigSite) ([]Route, string) {
60+ var subdomain string
61+ curRoutes := routes
62+
63+ if cfg.IsCustomdomains() || cfg.IsSubdomains() {
64+ hostDomain := strings.ToLower(strings.Split(r.Host, ":")[0])
65+ appDomain := strings.ToLower(strings.Split(cfg.ConfigCms.Domain, ":")[0])
66+
67+ if hostDomain != appDomain {
68+ if strings.Contains(hostDomain, appDomain) {
69+ subdomain = strings.TrimSuffix(hostDomain, fmt.Sprintf(".%s", appDomain))
70+ if subdomain != "" {
71+ curRoutes = subdomainRoutes
72+ }
73+ } else {
74+ subdomain = GetCustomDomain(hostDomain, cfg.Space)
75+ if subdomain != "" {
76+ curRoutes = subdomainRoutes
77+ }
78+ }
79+ }
80+ }
81+
82+ return curRoutes, subdomain
83+}
84+
85+func CreateServe(routes []Route, subdomainRoutes []Route, cfg *ConfigSite, dbpool db.DB, st storage.StorageServe, logger *slog.Logger, cache *cache.Cache) ServeFn {
86+ return func(w http.ResponseWriter, r *http.Request) {
87+ curRoutes, subdomain := findRouteConfig(r, routes, subdomainRoutes, cfg)
88+ CreateServeBasic(curRoutes, subdomain, cfg, dbpool, st, logger, cache)
89+ }
90+}
91+
92 type ctxDBKey struct{}
93 type ctxStorageKey struct{}
94 type ctxKey struct{}
1@@ -56,10 +56,14 @@ func SanitizeFileExt(fname string) string {
2
3 func KeyText(s ssh.Session) (string, error) {
4 if s.PublicKey() == nil {
5- return "", fmt.Errorf("session doesn't have public key")
6+ return "", fmt.Errorf("Session doesn't have public key")
7 }
8- kb := base64.StdEncoding.EncodeToString(s.PublicKey().Marshal())
9- return fmt.Sprintf("%s %s", s.PublicKey().Type(), kb), nil
10+ return KeyForKeyText(s.PublicKey())
11+}
12+
13+func KeyForKeyText(pk ssh.PublicKey) (string, error) {
14+ kb := base64.StdEncoding.EncodeToString(pk.Marshal())
15+ return fmt.Sprintf("%s %s", pk.Type(), kb), nil
16 }
17
18 func GetEnv(key string, defaultVal string) string {
1@@ -0,0 +1,2 @@
2+-- type could be: "public", "public_keys", "pico"
3+ALTER TABLE projects ADD COLUMN acl jsonb NOT NULL DEFAULT '{"type":"public","data":[]}'::jsonb;
+2,
-2
1@@ -15,6 +15,7 @@ import (
2 "github.com/muesli/reflow/wrap"
3 "github.com/picosh/pico/db"
4 "github.com/picosh/pico/db/postgres"
5+ "github.com/picosh/pico/shared"
6 "github.com/picosh/pico/shared/storage"
7 "github.com/picosh/pico/wish/cms/config"
8 "github.com/picosh/pico/wish/cms/ui/account"
9@@ -24,7 +25,6 @@ import (
10 "github.com/picosh/pico/wish/cms/ui/posts"
11 "github.com/picosh/pico/wish/cms/ui/tokens"
12 "github.com/picosh/pico/wish/cms/ui/username"
13- "github.com/picosh/pico/wish/cms/util"
14 )
15
16 type status int
17@@ -93,7 +93,7 @@ func Middleware(cfg *config.ConfigCms, urls config.ConfigURL) bm.Handler {
18 logger.Info("no active terminal, skipping")
19 return nil, nil
20 }
21- key, err := util.KeyText(s)
22+ key, err := shared.KeyText(s)
23 if err != nil {
24 logger.Error(err.Error())
25 }
+0,
-16
1@@ -1,16 +0,0 @@
2-package util
3-
4-import (
5- "encoding/base64"
6- "fmt"
7-
8- "github.com/charmbracelet/ssh"
9-)
10-
11-func KeyText(s ssh.Session) (string, error) {
12- if s.PublicKey() == nil {
13- return "", fmt.Errorf("Session doesn't have public key")
14- }
15- kb := base64.StdEncoding.EncodeToString(s.PublicKey().Marshal())
16- return fmt.Sprintf("%s %s", s.PublicKey().Type(), kb), nil
17-}
+19,
-0
1@@ -0,0 +1,19 @@
2+package wish
3+
4+import (
5+ "github.com/charmbracelet/ssh"
6+ "github.com/charmbracelet/wish"
7+)
8+
9+func PtyMdw(mdw wish.Middleware) wish.Middleware {
10+ return func(next ssh.Handler) ssh.Handler {
11+ return func(sesh ssh.Session) {
12+ _, _, ok := sesh.Pty()
13+ if !ok {
14+ next(sesh)
15+ return
16+ }
17+ mdw(next)(sesh)
18+ }
19+ }
20+}