- commit
- d3e2409
- parent
- e6c6e2f
- author
- Eric Bower
- date
- 2025-01-18 08:14:31 -0500 EST
chore: rm imgs
11 files changed,
+0,
-969
+0,
-19
1@@ -66,25 +66,6 @@ PROSE_DOMAIN=prose.dev.pico.sh:3002
2 PROSE_PROTOCOL=http
3 PROSE_DEBUG=1
4
5-IMGS_CADDYFILE=./caddy/Caddyfile
6-IMGS_V4=
7-IMGS_V6=
8-IMGS_HTTP_V4=$IMGS_V4:80
9-IMGS_HTTP_V6=[$IMGS_V6]:80
10-IMGS_HTTPS_V4=$IMGS_V4:443
11-IMGS_HTTPS_V6=[$IMGS_V6]:443
12-IMGS_SSH_V4=$IMGS_V4:22
13-IMGS_SSH_V6=[$IMGS_V6]:22
14-IMGS_HOST=
15-IMGS_SSH_PORT=2222
16-IMGS_WEB_PORT=3000
17-IMGS_PROM_PORT=9222
18-IMGS_DOMAIN=imgs.dev.pico.sh:3003
19-IMGS_EMAIL=hello@pico.sh
20-IMGS_PROTOCOL=http
21-IMGS_STORAGE_DIR=.storage
22-IMGS_DEBUG=1
23-
24 SENDGRID_API_KEY=
25 FEEDS_CADDYFILE=./caddy/Caddyfile
26 FEEDS_V4=
+0,
-9
1@@ -1,9 +0,0 @@
2-package main
3-
4-import (
5- "github.com/picosh/pico/imgs"
6-)
7-
8-func main() {
9- imgs.StartSshServer()
10-}
+0,
-7
1@@ -1,7 +0,0 @@
2-package main
3-
4-import "github.com/picosh/pico/imgs"
5-
6-func main() {
7- imgs.StartApiServer()
8-}
+0,
-30
1@@ -11,13 +11,6 @@ services:
2 ports:
3 - "9000:9000"
4 - "9001:9001"
5- registry:
6- env_file:
7- - .env.example
8- ports:
9- - "5000:5000"
10- volumes:
11- - ./imgs/registry.yml:/etc/docker/registry/config.yml
12 imgproxy:
13 env_file:
14 - .env.example
15@@ -87,29 +80,6 @@ services:
16 - ./data/prose-ssh/data:/app/ssh_data
17 ports:
18 - "2222:2222"
19- imgs-web:
20- build:
21- args:
22- APP: imgs
23- target: release-web
24- env_file:
25- - .env.example
26- volumes:
27- - ./data/storage/data:/app/.storage
28- ports:
29- - "3003:3000"
30- imgs-ssh:
31- build:
32- args:
33- APP: imgs
34- target: release-ssh
35- env_file:
36- - .env.example
37- volumes:
38- - ./data/storage/data:/app/.storage
39- - ./data/imgs-ssh/data:/app/ssh_data
40- ports:
41- - "2223:2222"
42 pgs-web:
43 build:
44 args:
+0,
-60
1@@ -35,13 +35,6 @@ services:
2 - .env.prod
3 volumes:
4 - ./data/pipemgr/data/term_info_ed25519:/key:ro
5- registry:
6- env_file:
7- - .env.prod
8- volumes:
9- - ./imgs/registry.yml:/etc/docker/registry/config.yml
10- networks:
11- - imgs
12 imgproxy:
13 env_file:
14 - .env.prod
15@@ -186,53 +179,6 @@ services:
16 ports:
17 - "${PROSE_SSH_V4:-22}:2222"
18 - "${PROSE_SSH_V6:-[::1]:22}:2222"
19- imgs-caddy:
20- image: ghcr.io/picosh/pico/caddy:latest
21- restart: always
22- networks:
23- - imgs
24- env_file:
25- - .env.prod
26- environment:
27- APP_DOMAIN: ${IMGS_DOMAIN:-imgs.sh}
28- APP_EMAIL: ${IMGS_EMAIL:-hello@pico.sh}
29- volumes:
30- - ${IMGS_CADDYFILE}:/etc/caddy/Caddyfile
31- - ./data/imgs-caddy/data:/data
32- - ./data/imgs-caddy/config:/config
33- ports:
34- - "${IMGS_HTTPS_V4:-443}:443"
35- - "${IMGS_HTTP_V4:-80}:80"
36- - "${IMGS_HTTPS_V6:-[::1]:443}:443"
37- - "${IMGS_HTTP_V6:-[::1]:80}:80"
38- profiles:
39- - imgs
40- - caddy
41- - all
42- imgs-web:
43- networks:
44- imgs:
45- aliases:
46- - web
47- env_file:
48- - .env.prod
49- volumes:
50- - ./data/storage/data:/app/.storage
51- - ./data/imgs-ssh/data:/app/ssh_data
52- imgs-ssh:
53- networks:
54- imgs:
55- aliases:
56- - ssh
57- env_file:
58- - .env.prod
59- volumes:
60- - ./data/storage/data:/app/.storage
61- - ./data/imgs-ssh/data:/app/ssh_data
62- - ./data/imgs-tmp:/tmp
63- ports:
64- - "${IMGS_SSH_V4:-22}:2222"
65- - "${IMGS_SSH_V6:-[::1]:22}:2222"
66 pgs-caddy:
67 image: ghcr.io/picosh/pico/caddy:latest
68 restart: always
69@@ -392,12 +338,6 @@ networks:
70 ipam:
71 config:
72 - subnet: 172.19.0.0/16
73- imgs:
74- driver_opts:
75- com.docker.network.bridge.name: imgs
76- ipam:
77- config:
78- - subnet: 172.21.0.0/16
79 feeds:
80 driver_opts:
81 com.docker.network.bridge.name: feeds
+0,
-24
1@@ -12,16 +12,6 @@ services:
2 profiles:
3 - db
4 - all
5- registry:
6- image: registry
7- restart: always
8- profiles:
9- - imgs
10- - services
11- - all
12- environment:
13- REGISTRY_STORAGE_S3_ACCESSKEY: ${MINIO_ROOT_USER}
14- REGISTRY_STORAGE_S3_SECRETKEY: ${MINIO_ROOT_PASSWORD}
15 imgproxy:
16 image: darthsim/imgproxy:latest
17 restart: always
18@@ -85,20 +75,6 @@ services:
19 - prose
20 - services
21 - all
22- imgs-web:
23- image: ghcr.io/picosh/pico/imgs-web:latest
24- restart: always
25- profiles:
26- - imgs
27- - services
28- - all
29- imgs-ssh:
30- image: ghcr.io/picosh/pico/imgs-ssh:latest
31- restart: always
32- profiles:
33- - imgs
34- - services
35- - all
36 pgs-web:
37 image: ghcr.io/picosh/pico/pgs-web:latest
38 restart: always
+0,
-181
1@@ -1,19 +1,13 @@
2 package imgs
3
4 import (
5- "bytes"
6 "fmt"
7 "html/template"
8 "net/http"
9 "net/url"
10 "path/filepath"
11- "time"
12
13- _ "net/http/pprof"
14-
15- "github.com/gorilla/feeds"
16 "github.com/picosh/pico/db"
17- "github.com/picosh/pico/db/postgres"
18 "github.com/picosh/pico/pgs"
19 "github.com/picosh/pico/shared"
20 "github.com/picosh/pico/shared/storage"
21@@ -90,82 +84,6 @@ func ImgsListHandler(w http.ResponseWriter, r *http.Request) {
22 }
23 }
24
25-func ImgsRssHandler(w http.ResponseWriter, r *http.Request) {
26- dbpool := shared.GetDB(r)
27- logger := shared.GetLogger(r)
28- cfg := shared.GetCfg(r)
29-
30- pager, err := dbpool.FindAllPosts(&db.Pager{Num: 25, Page: 0}, Space)
31- if err != nil {
32- logger.Error(err.Error())
33- http.Error(w, err.Error(), http.StatusInternalServerError)
34- return
35- }
36-
37- ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl"))
38- if err != nil {
39- logger.Error(err.Error())
40- http.Error(w, err.Error(), http.StatusInternalServerError)
41- return
42- }
43-
44- feed := &feeds.Feed{
45- Title: fmt.Sprintf("%s imgs feed", cfg.Domain),
46- Link: &feeds.Link{Href: cfg.HomeURL()},
47- Description: fmt.Sprintf("%s latest image", cfg.Domain),
48- Author: &feeds.Author{Name: cfg.Domain},
49- Created: time.Now(),
50- }
51-
52- curl := shared.CreateURLFromRequest(cfg, r)
53-
54- var feedItems []*feeds.Item
55- for _, post := range pager.Data {
56- var tpl bytes.Buffer
57- data := &PostPageData{
58- ImgURL: template.URL(cfg.ImgURL(curl, post.Username, post.Filename)),
59- }
60- if err := ts.Execute(&tpl, data); err != nil {
61- continue
62- }
63-
64- realUrl := cfg.FullPostURL(curl, post.Username, post.Filename)
65- if !curl.Subdomain && !curl.UsernameInRoute {
66- realUrl = fmt.Sprintf("%s://%s%s", cfg.Protocol, r.Host, realUrl)
67- }
68-
69- item := &feeds.Item{
70- Id: realUrl,
71- Title: post.Title,
72- Link: &feeds.Link{Href: realUrl},
73- Content: tpl.String(),
74- Created: *post.PublishAt,
75- Updated: *post.UpdatedAt,
76- Description: post.Description,
77- Author: &feeds.Author{Name: post.Username},
78- }
79-
80- if post.Description != "" {
81- item.Description = post.Description
82- }
83-
84- feedItems = append(feedItems, item)
85- }
86- feed.Items = feedItems
87-
88- rss, err := feed.ToAtom()
89- if err != nil {
90- logger.Error(err.Error())
91- http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
92- }
93-
94- w.Header().Add("Content-Type", "application/atom+xml")
95- _, err = w.Write([]byte(rss))
96- if err != nil {
97- logger.Error(err.Error())
98- }
99-}
100-
101 func anyPerm(proj *db.Project) bool {
102 return true
103 }
104@@ -248,102 +166,3 @@ func FindImgPost(r *http.Request, user *db.User, slug string) (*db.Post, error)
105 dbpool := shared.GetDB(r)
106 return dbpool.FindPostWithSlug(slug, user.ID, Space)
107 }
108-
109-func redirectHandler(w http.ResponseWriter, r *http.Request) {
110- username := shared.GetUsernameFromRequest(r)
111- url := fmt.Sprintf("https://%s.prose.sh/i", username)
112- http.Redirect(w, r, url, http.StatusMovedPermanently)
113-}
114-
115-func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
116- routes := []shared.Route{
117- shared.NewRoute("GET", "/check", shared.CheckHandler),
118- shared.NewRoute("GET", "/", func(w http.ResponseWriter, r *http.Request) {
119- http.Redirect(w, r, "https://prose.sh", http.StatusMovedPermanently)
120- }),
121- }
122-
123- routes = append(
124- routes,
125- staticRoutes...,
126- )
127-
128- routes = append(
129- routes,
130- shared.NewRoute("GET", "/rss", ImgsRssHandler),
131- shared.NewRoute("GET", "/rss.xml", ImgsRssHandler),
132- shared.NewRoute("GET", "/atom.xml", ImgsRssHandler),
133- shared.NewRoute("GET", "/feed.xml", ImgsRssHandler),
134-
135- shared.NewRoute("GET", "/([^/]+)", redirectHandler),
136- shared.NewRoute("GET", "/([^/]+)/o/([^/]+)", ImgRequest),
137- shared.NewRoute("GET", "/([^/]+)/([^/]+)", ImgRequest),
138- shared.NewRoute("GET", "/([^/]+)/([^/]+)/(.+)", ImgRequest),
139- )
140-
141- return routes
142-}
143-
144-func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
145- routes := []shared.Route{}
146-
147- routes = append(
148- routes,
149- staticRoutes...,
150- )
151-
152- routes = append(
153- routes,
154- shared.NewRoute("GET", "/", redirectHandler),
155- shared.NewRoute("GET", "/o/([^/]+)", ImgRequest),
156- shared.NewRoute("GET", "/([^/]+)", ImgRequest),
157- shared.NewRoute("GET", "/([^/]+)/(.+)", ImgRequest),
158- )
159-
160- return routes
161-}
162-
163-func StartApiServer() {
164- cfg := NewConfigSite()
165- logger := cfg.Logger
166-
167- db := postgres.NewDB(cfg.DbURL, cfg.Logger)
168- defer db.Close()
169-
170- var st storage.StorageServe
171- var err error
172- if cfg.MinioURL == "" {
173- st, err = storage.NewStorageFS(cfg.Logger, cfg.StorageDir)
174- } else {
175- st, err = storage.NewStorageMinio(cfg.Logger, cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
176- }
177-
178- if err != nil {
179- logger.Error(err.Error())
180- }
181-
182- staticRoutes := []shared.Route{}
183- if cfg.Debug {
184- staticRoutes = shared.CreatePProfRoutes(staticRoutes)
185- }
186-
187- mainRoutes := createMainRoutes(staticRoutes)
188- subdomainRoutes := createSubdomainRoutes(staticRoutes)
189-
190- apiConfig := &shared.ApiConfig{
191- Cfg: cfg,
192- Dbpool: db,
193- Storage: st,
194- }
195- handler := shared.CreateServe(mainRoutes, subdomainRoutes, apiConfig)
196- router := http.HandlerFunc(handler)
197-
198- portStr := fmt.Sprintf(":%s", cfg.Port)
199- logger.Info(
200- "Starting server on port",
201- "port", cfg.Port,
202- "domain", cfg.Domain,
203- )
204-
205- logger.Error(http.ListenAndServe(portStr, router).Error())
206-}
+0,
-275
1@@ -1,275 +0,0 @@
2-package imgs
3-
4-import (
5- "encoding/json"
6- "flag"
7- "fmt"
8- "io"
9- "log/slog"
10- "net/http"
11- "path/filepath"
12- "strings"
13-
14- "github.com/charmbracelet/ssh"
15- "github.com/charmbracelet/wish"
16- "github.com/google/uuid"
17- "github.com/picosh/pico/db"
18- "github.com/picosh/pico/shared/storage"
19- "github.com/picosh/pico/tui/common"
20- sst "github.com/picosh/pobj/storage"
21- psub "github.com/picosh/pubsub"
22- sendutils "github.com/picosh/send/utils"
23- "github.com/picosh/utils"
24-)
25-
26-func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
27- if s.PublicKey() == nil {
28- return nil, fmt.Errorf("key not found")
29- }
30-
31- key := utils.KeyForKeyText(s.PublicKey())
32-
33- user, err := dbpool.FindUserForKey(s.User(), key)
34- if err != nil {
35- return nil, err
36- }
37-
38- if user.Name == "" {
39- return nil, fmt.Errorf("must have username set")
40- }
41-
42- return user, nil
43-}
44-
45-func flagSet(cmdName string, sesh ssh.Session) (*flag.FlagSet, *bool) {
46- cmd := flag.NewFlagSet(cmdName, flag.ContinueOnError)
47- cmd.SetOutput(sesh)
48- write := cmd.Bool("write", false, "apply changes")
49- return cmd, write
50-}
51-
52-func flagCheck(cmd *flag.FlagSet, posArg string, cmdArgs []string) bool {
53- _ = cmd.Parse(cmdArgs)
54-
55- if posArg == "-h" || posArg == "--help" || posArg == "-help" {
56- cmd.Usage()
57- return false
58- }
59- return true
60-}
61-
62-type Cmd struct {
63- User *db.User
64- Session utils.CmdSession
65- Log *slog.Logger
66- Dbpool db.DB
67- Write bool
68- Styles common.Styles
69- Storage sst.ObjectStorage
70- RegistryUrl string
71-}
72-
73-func (c *Cmd) output(out string) {
74- _, _ = c.Session.Write([]byte(out + "\r\n"))
75-}
76-
77-func (c *Cmd) error(err error) {
78- _, _ = fmt.Fprint(c.Session.Stderr(), err, "\r\n")
79- _ = c.Session.Exit(1)
80- _ = c.Session.Close()
81-}
82-
83-func (c *Cmd) bail(err error) {
84- if err == nil {
85- return
86- }
87- c.Log.Error(err.Error())
88- c.error(err)
89-}
90-
91-func (c *Cmd) notice() {
92- if !c.Write {
93- c.output("\nNOTICE: changes not commited, use `--write` to save operation")
94- }
95-}
96-
97-func (c *Cmd) help() {
98- helpStr := "Commands: [help, ls, rm]\n"
99- helpStr += "NOTICE: *must* append with `--write` for the changes to persist.\n"
100- c.output(helpStr)
101-}
102-
103-func (c *Cmd) rm(repo string) error {
104- bucket, err := c.Storage.GetBucket("imgs")
105- if err != nil {
106- return err
107- }
108-
109- fp := filepath.Join("docker/registry/v2/repositories", c.User.Name, repo)
110-
111- fileList, err := c.Storage.ListObjects(bucket, fp, true)
112- if err != nil {
113- return err
114- }
115-
116- if len(fileList) == 0 {
117- c.output(fmt.Sprintf("repo not found (%s)", repo))
118- return nil
119- }
120- c.output(fmt.Sprintf("found (%d) objects for repo (%s), removing", len(fileList), repo))
121-
122- for _, obj := range fileList {
123- fname := filepath.Join(fp, obj.Name())
124- intent := fmt.Sprintf("deleted (%s)", obj.Name())
125- c.Log.Info(
126- "attempting to delete file",
127- "user", c.User.Name,
128- "bucket", bucket.Name,
129- "repo", repo,
130- "filename", fname,
131- )
132- if c.Write {
133- err := c.Storage.DeleteObject(bucket, fname)
134- if err != nil {
135- return err
136- }
137- }
138- c.output(intent)
139- }
140-
141- return nil
142-}
143-
144-type RegistryCatalog struct {
145- Repos []string `json:"repositories"`
146-}
147-
148-func (c *Cmd) ls() error {
149- res, err := http.Get(
150- fmt.Sprintf("http://%s/v2/_catalog", c.RegistryUrl),
151- )
152- if err != nil {
153- return err
154- }
155-
156- body, err := io.ReadAll(res.Body)
157- if err != nil {
158- return err
159- }
160-
161- var data RegistryCatalog
162- err = json.Unmarshal(body, &data)
163-
164- if err != nil {
165- return err
166- }
167-
168- if len(data.Repos) == 0 {
169- c.output("You don't have any repos on imgs.sh")
170- return nil
171- }
172-
173- user := c.User.Name
174- out := "repos\n"
175- out += "-----\n"
176- for _, repo := range data.Repos {
177- if !strings.HasPrefix(repo, user+"/") {
178- continue
179- }
180- rr := strings.TrimPrefix(repo, user+"/")
181- out += fmt.Sprintf("%s\n", rr)
182- }
183- c.output(out)
184- return nil
185-}
186-
187-type CliHandler struct {
188- DBPool db.DB
189- Logger *slog.Logger
190- Storage storage.StorageServe
191- RegistryUrl string
192- PubSub psub.PubSub
193-}
194-
195-func WishMiddleware(handler *CliHandler) wish.Middleware {
196- dbpool := handler.DBPool
197- log := handler.Logger
198- st := handler.Storage
199- pubsub := handler.PubSub
200-
201- return func(next ssh.Handler) ssh.Handler {
202- return func(sesh ssh.Session) {
203- user, err := getUser(sesh, dbpool)
204- if err != nil {
205- sendutils.ErrorHandler(sesh, err)
206- return
207- }
208-
209- args := sesh.Command()
210-
211- opts := Cmd{
212- Session: sesh,
213- User: user,
214- Log: log,
215- Dbpool: dbpool,
216- Write: false,
217- Storage: st,
218- RegistryUrl: handler.RegistryUrl,
219- }
220-
221- if len(args) == 0 {
222- next(sesh)
223- return
224- }
225-
226- cmd := strings.TrimSpace(args[0])
227- if len(args) == 1 {
228- if cmd == "help" {
229- opts.help()
230- return
231- } else if cmd == "ls" {
232- err := opts.ls()
233- opts.bail(err)
234- return
235- } else {
236- next(sesh)
237- return
238- }
239- }
240-
241- repoName := strings.TrimSpace(args[1])
242- cmdArgs := args[2:]
243- log.Info(
244- "imgs middleware detected command",
245- "args", args,
246- "cmd", cmd,
247- "repoName", repoName,
248- "cmdArgs", cmdArgs,
249- )
250-
251- if cmd == "rm" {
252- rmCmd, write := flagSet("rm", sesh)
253- if !flagCheck(rmCmd, repoName, cmdArgs) {
254- return
255- }
256- opts.Write = *write
257-
258- err := opts.rm(repoName)
259- opts.notice()
260- opts.bail(err)
261- return
262- } else if cmd == "sub" {
263- err = pubsub.Sub(sesh.Context(), uuid.NewString(), sesh, []*psub.Channel{
264- psub.NewChannel(fmt.Sprintf("%s/%s", user.Name, repoName)),
265- }, false)
266-
267- if err != nil {
268- wish.Errorln(sesh, err)
269- }
270- } else {
271- next(sesh)
272- return
273- }
274- }
275- }
276-}
+0,
-35
1@@ -1,35 +0,0 @@
2-package imgs
3-
4-import (
5- "github.com/picosh/pico/shared"
6- "github.com/picosh/utils"
7-)
8-
9-func NewConfigSite() *shared.ConfigSite {
10- debug := utils.GetEnv("IMGS_DEBUG", "0")
11- domain := utils.GetEnv("IMGS_DOMAIN", "prose.sh")
12- port := utils.GetEnv("IMGS_WEB_PORT", "3000")
13- protocol := utils.GetEnv("IMGS_PROTOCOL", "https")
14- storageDir := utils.GetEnv("IMGS_STORAGE_DIR", ".storage")
15- minioURL := utils.GetEnv("MINIO_URL", "")
16- minioUser := utils.GetEnv("MINIO_ROOT_USER", "")
17- minioPass := utils.GetEnv("MINIO_ROOT_PASSWORD", "")
18- dbURL := utils.GetEnv("DATABASE_URL", "")
19-
20- cfg := shared.ConfigSite{
21- Debug: debug == "1",
22- Domain: domain,
23- Port: port,
24- Protocol: protocol,
25- DbURL: dbURL,
26- StorageDir: storageDir,
27- MinioURL: minioURL,
28- MinioUser: minioUser,
29- MinioPass: minioPass,
30- Space: "imgs",
31- AllowedExt: []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico"},
32- Logger: shared.CreateLogger("imgs"),
33- }
34-
35- return &cfg
36-}
+0,
-11
1@@ -1,11 +0,0 @@
2-version: 0.1
3-http:
4- addr: :5000
5-storage:
6- s3:
7- region: us-east-1
8- bucket: imgs
9- # regionendpoint: http://minio:9000
10- regionendpoint: https://minio.pico.sh
11- redirect:
12- disable: true
+0,
-318
1@@ -1,318 +0,0 @@
2-package imgs
3-
4-import (
5- "bytes"
6- "context"
7- "encoding/json"
8- "fmt"
9- "io"
10- "log"
11- "log/slog"
12- "net/http"
13- "net/http/httputil"
14- "net/url"
15- "os"
16- "os/signal"
17- "strconv"
18- "strings"
19- "syscall"
20- "time"
21-
22- "github.com/charmbracelet/promwish"
23- "github.com/charmbracelet/ssh"
24- "github.com/charmbracelet/wish"
25- "github.com/google/uuid"
26- "github.com/picosh/pico/db"
27- "github.com/picosh/pico/db/postgres"
28- "github.com/picosh/pico/shared"
29- "github.com/picosh/pico/shared/storage"
30- wsh "github.com/picosh/pico/wish"
31- psub "github.com/picosh/pubsub"
32- "github.com/picosh/tunkit"
33- "github.com/picosh/utils"
34-)
35-
36-type ctxUserKey struct{}
37-
38-func getUserCtx(ctx ssh.Context) (*db.User, error) {
39- user, ok := ctx.Value(ctxUserKey{}).(*db.User)
40- if user == nil || !ok {
41- return user, fmt.Errorf("user not set on `ssh.Context()` for connection")
42- }
43- return user, nil
44-}
45-func setUserCtx(ctx ssh.Context, user *db.User) {
46- ctx.SetValue(ctxUserKey{}, user)
47-}
48-
49-func AuthHandler(dbh db.DB, log *slog.Logger) func(ssh.Context, ssh.PublicKey) bool {
50- return func(ctx ssh.Context, key ssh.PublicKey) bool {
51- kk := utils.KeyForKeyText(key)
52-
53- user, err := dbh.FindUserForKey("", kk)
54- if err != nil {
55- log.Error("user not found", "err", err)
56- return false
57- }
58-
59- if user == nil {
60- log.Error("user not found", "err", err)
61- return false
62- }
63-
64- setUserCtx(ctx, user)
65-
66- if !dbh.HasFeatureForUser(user.ID, "plus") {
67- log.Error("not a pico+ user", "user", user.Name)
68- return false
69- }
70-
71- return true
72- }
73-}
74-
75-type ErrorHandler struct {
76- Err error
77-}
78-
79-func (e *ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
80- log.Println(e.Err.Error())
81- http.Error(w, e.Err.Error(), http.StatusInternalServerError)
82-}
83-
84-func createServeMux(handler *CliHandler, pubsub psub.PubSub) func(ctx ssh.Context) http.Handler {
85- return func(ctx ssh.Context) http.Handler {
86- router := http.NewServeMux()
87-
88- slug := ""
89- user, err := getUserCtx(ctx)
90- if err == nil && user != nil {
91- slug = user.Name
92- }
93-
94- proxy := httputil.NewSingleHostReverseProxy(&url.URL{
95- Scheme: "http",
96- Host: handler.RegistryUrl,
97- })
98-
99- oldDirector := proxy.Director
100-
101- proxy.Director = func(r *http.Request) {
102- handler.Logger.Info("director", "request", r)
103- oldDirector(r)
104-
105- if strings.HasSuffix(r.URL.Path, "_catalog") || r.URL.Path == "/v2" || r.URL.Path == "/v2/" {
106- return
107- }
108-
109- fullPath := strings.TrimPrefix(r.URL.Path, "/v2")
110-
111- newPath, err := url.JoinPath("/v2", slug, fullPath)
112- if err != nil {
113- return
114- }
115-
116- r.URL.Path = newPath
117-
118- query := r.URL.Query()
119-
120- if query.Has("from") {
121- joinedFrom, err := url.JoinPath(slug, query.Get("from"))
122- if err != nil {
123- return
124- }
125- query.Set("from", joinedFrom)
126-
127- r.URL.RawQuery = query.Encode()
128- }
129- }
130-
131- proxy.ModifyResponse = func(r *http.Response) error {
132- handler.Logger.Info("modify", "request", r)
133- shared.CorsHeaders(r.Header)
134-
135- if r.Request.Method == http.MethodGet && strings.HasSuffix(r.Request.URL.Path, "_catalog") {
136- b, err := io.ReadAll(r.Body)
137- if err != nil {
138- return err
139- }
140-
141- err = r.Body.Close()
142- if err != nil {
143- return err
144- }
145-
146- var data map[string]any
147- err = json.Unmarshal(b, &data)
148- if err != nil {
149- return err
150- }
151-
152- newRepos := []string{}
153-
154- if repos, ok := data["repositories"].([]any); ok {
155- for _, repo := range repos {
156- if repoStr, ok := repo.(string); ok && strings.HasPrefix(repoStr, slug) {
157- newRepos = append(newRepos, strings.Replace(repoStr, fmt.Sprintf("%s/", slug), "", 1))
158- }
159- }
160- }
161-
162- data["repositories"] = newRepos
163-
164- newB, err := json.Marshal(data)
165- if err != nil {
166- return err
167- }
168-
169- jsonBuf := bytes.NewBuffer(newB)
170-
171- r.ContentLength = int64(jsonBuf.Len())
172- r.Header.Set("Content-Length", strconv.FormatInt(r.ContentLength, 10))
173- r.Body = io.NopCloser(jsonBuf)
174- }
175-
176- if r.Request.Method == http.MethodGet && (strings.Contains(r.Request.URL.Path, "/tags/") || strings.Contains(r.Request.URL.Path, "/manifests/")) {
177- splitPath := strings.Split(r.Request.URL.Path, "/")
178-
179- if len(splitPath) > 1 {
180- ele := splitPath[len(splitPath)-2]
181- if ele == "tags" || ele == "manifests" {
182- b, err := io.ReadAll(r.Body)
183- if err != nil {
184- return err
185- }
186-
187- err = r.Body.Close()
188- if err != nil {
189- return err
190- }
191-
192- var data map[string]any
193- err = json.Unmarshal(b, &data)
194- if err != nil {
195- return err
196- }
197-
198- if name, ok := data["name"].(string); ok {
199- if strings.HasPrefix(name, slug) {
200- data["name"] = strings.Replace(name, fmt.Sprintf("%s/", slug), "", 1)
201- }
202- }
203-
204- newB, err := json.Marshal(data)
205- if err != nil {
206- return err
207- }
208-
209- jsonBuf := bytes.NewBuffer(newB)
210-
211- r.ContentLength = int64(jsonBuf.Len())
212- r.Header.Set("Content-Length", strconv.FormatInt(r.ContentLength, 10))
213- r.Body = io.NopCloser(jsonBuf)
214- }
215- }
216- }
217-
218- if r.Request.Method == http.MethodPut && strings.Contains(r.Request.URL.Path, "/manifests/") {
219- digest := r.Header.Get("Docker-Content-Digest")
220- // [ ]/v2/erock/alpine/manifests/latest
221- splitPath := strings.Split(r.Request.URL.Path, "/")
222- img := splitPath[3]
223- tag := splitPath[5]
224-
225- furl := fmt.Sprintf(
226- "digest=%s&image=%s&tag=%s",
227- url.QueryEscape(digest),
228- img,
229- tag,
230- )
231- handler.Logger.Info("sending event", "url", furl)
232-
233- err := pubsub.Pub(ctx, uuid.NewString(), bytes.NewBufferString(furl), []*psub.Channel{
234- psub.NewChannel(fmt.Sprintf("%s/%s:%s", user.Name, img, tag)),
235- }, false)
236-
237- if err != nil {
238- handler.Logger.Error("pub error", "err", err)
239- }
240- }
241-
242- locationHeader := r.Header.Get("location")
243- if strings.Contains(locationHeader, fmt.Sprintf("/v2/%s", slug)) {
244- r.Header.Set("location", strings.ReplaceAll(locationHeader, fmt.Sprintf("/v2/%s", slug), "/v2"))
245- }
246-
247- return nil
248- }
249-
250- router.HandleFunc("/", proxy.ServeHTTP)
251-
252- return router
253- }
254-}
255-
256-func StartSshServer() {
257- host := utils.GetEnv("IMGS_HOST", "0.0.0.0")
258- port := utils.GetEnv("IMGS_SSH_PORT", "2222")
259- promPort := utils.GetEnv("IMGS_PROM_PORT", "9222")
260- dbUrl := os.Getenv("DATABASE_URL")
261- registryUrl := utils.GetEnv("REGISTRY_URL", "0.0.0.0:5000")
262- minioUrl := utils.GetEnv("MINIO_URL", "http://0.0.0.0:9000")
263- minioUser := utils.GetEnv("MINIO_ROOT_USER", "")
264- minioPass := utils.GetEnv("MINIO_ROOT_PASSWORD", "")
265-
266- logger := shared.CreateLogger("imgs")
267- logger.Info("bootup", "registry", registryUrl, "minio", minioUrl)
268- dbh := postgres.NewDB(dbUrl, logger)
269- st, err := storage.NewStorageMinio(logger, minioUrl, minioUser, minioPass)
270- if err != nil {
271- panic(err)
272- }
273-
274- pubsub := psub.NewMulticast(logger)
275- handler := &CliHandler{
276- Logger: logger,
277- DBPool: dbh,
278- Storage: st,
279- RegistryUrl: registryUrl,
280- PubSub: pubsub,
281- }
282-
283- s, err := wish.NewServer(
284- wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
285- wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
286- wish.WithPublicKeyAuth(AuthHandler(dbh, logger)),
287- wish.WithMiddleware(
288- WishMiddleware(handler),
289- promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "imgs-ssh"),
290- wsh.LogMiddleware(logger),
291- ),
292- tunkit.WithWebTunnel(
293- tunkit.NewWebTunnelHandler(createServeMux(handler, pubsub), logger),
294- ),
295- )
296-
297- if err != nil {
298- logger.Error("could not create server", "err", err)
299- }
300-
301- done := make(chan os.Signal, 1)
302- signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
303- logger.Info("starting SSH server", "host", host, "port", port)
304- go func() {
305- if err = s.ListenAndServe(); err != nil {
306- logger.Error("serve error", "err", err)
307- os.Exit(1)
308- }
309- }()
310-
311- <-done
312- logger.Info("stopping SSH server")
313- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
314- defer func() { cancel() }()
315- if err := s.Shutdown(ctx); err != nil {
316- logger.Error("shutdown", "err", err)
317- os.Exit(1)
318- }
319-}