- commit
- 536c609
- parent
- f650e5e
- author
- Eric Bower
- date
- 2024-03-13 19:24:58 +0000 UTC
chore(imgs): move ssh app into pico
5 files changed,
+292,
-2
+4,
-2
1@@ -1,7 +1,9 @@
2 package main
3
4-import "github.com/picosh/pico/prose"
5+import (
6+ "github.com/picosh/pico/imgs"
7+)
8
9 func main() {
10- prose.StartSshServer()
11+ imgs.StartSshServer()
12 }
+5,
-0
1@@ -31,6 +31,11 @@ services:
2 - .env.prod
3 volumes:
4 - ./data/minio-data:/data
5+ registry:
6+ env_file:
7+ - .env.prod
8+ volumes:
9+ - ./imgs/registry.yml:/etc/docker/registry/config.yml
10 imgproxy:
11 env_file:
12 - .env.prod
+10,
-0
1@@ -13,6 +13,16 @@ 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
+10,
-0
1@@ -0,0 +1,10 @@
2+version: 0.1
3+http:
4+ addr: :5000
5+storage:
6+ s3:
7+ region: us-east-1
8+ bucket: imgs
9+ regionendpoint: https://minio.pico.sh
10+ redirect:
11+ disable: true
+263,
-0
1@@ -0,0 +1,263 @@
2+package imgs
3+
4+import (
5+ "bytes"
6+ "context"
7+ "encoding/base64"
8+ "encoding/json"
9+ "fmt"
10+ "io"
11+ "log"
12+ "log/slog"
13+ "net/http"
14+ "net/http/httputil"
15+ "net/url"
16+ "os"
17+ "os/signal"
18+ "strconv"
19+ "strings"
20+ "syscall"
21+ "time"
22+
23+ "github.com/charmbracelet/ssh"
24+ "github.com/charmbracelet/wish"
25+ "github.com/picosh/pico/db"
26+ "github.com/picosh/pico/db/postgres"
27+ "github.com/picosh/ptun"
28+)
29+
30+type ctxUserKey struct{}
31+
32+func getUserCtx(ctx ssh.Context) (*db.User, error) {
33+ user, ok := ctx.Value(ctxUserKey{}).(*db.User)
34+ if user == nil || !ok {
35+ return user, fmt.Errorf("user not set on `ssh.Context()` for connection")
36+ }
37+ return user, nil
38+}
39+func setUserCtx(ctx ssh.Context, user *db.User) {
40+ ctx.SetValue(ctxUserKey{}, user)
41+}
42+
43+func AuthHandler(dbh db.DB, log *slog.Logger) func(ssh.Context, ssh.PublicKey) bool {
44+ return func(ctx ssh.Context, key ssh.PublicKey) bool {
45+ kb := base64.StdEncoding.EncodeToString(key.Marshal())
46+ if kb == "" {
47+ return false
48+ }
49+ kk := fmt.Sprintf("%s %s", key.Type(), kb)
50+
51+ user, err := dbh.FindUserForKey("", kk)
52+ if err != nil {
53+ log.Error("user not found", "err", err)
54+ return false
55+ }
56+
57+ if user != nil {
58+ setUserCtx(ctx, user)
59+ return true
60+ }
61+
62+ return false
63+ }
64+}
65+
66+type ErrorHandler struct {
67+ Err error
68+}
69+
70+func (e *ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
71+ log.Println(e.Err.Error())
72+ http.Error(w, e.Err.Error(), http.StatusInternalServerError)
73+}
74+
75+func serveMux(ctx ssh.Context) http.Handler {
76+ router := http.NewServeMux()
77+
78+ slug := ""
79+ user, err := getUserCtx(ctx)
80+ if err == nil && user != nil {
81+ slug = user.Name
82+ }
83+
84+ proxy := httputil.NewSingleHostReverseProxy(&url.URL{
85+ Scheme: "http",
86+ Host: "registry:5000",
87+ })
88+
89+ oldDirector := proxy.Director
90+
91+ proxy.Director = func(r *http.Request) {
92+ log.Printf("%+v", r)
93+ oldDirector(r)
94+
95+ if strings.HasSuffix(r.URL.Path, "_catalog") || r.URL.Path == "/v2" || r.URL.Path == "/v2/" {
96+ return
97+ }
98+
99+ fullPath := strings.TrimPrefix(r.URL.Path, "/v2")
100+
101+ newPath, err := url.JoinPath("/v2", slug, fullPath)
102+ if err != nil {
103+ return
104+ }
105+
106+ r.URL.Path = newPath
107+
108+ query := r.URL.Query()
109+
110+ if query.Has("from") {
111+ joinedFrom, err := url.JoinPath(slug, query.Get("from"))
112+ if err != nil {
113+ return
114+ }
115+ query.Set("from", joinedFrom)
116+
117+ r.URL.RawQuery = query.Encode()
118+ }
119+
120+ log.Printf("%+v", r)
121+ }
122+
123+ proxy.ModifyResponse = func(r *http.Response) error {
124+ log.Printf("%+v", r)
125+
126+ if slug != "" && r.Request.Method == http.MethodGet && strings.HasSuffix(r.Request.URL.Path, "_catalog") {
127+ b, err := io.ReadAll(r.Body)
128+ if err != nil {
129+ return err
130+ }
131+
132+ err = r.Body.Close()
133+ if err != nil {
134+ return err
135+ }
136+
137+ var data map[string]any
138+ err = json.Unmarshal(b, &data)
139+ if err != nil {
140+ return err
141+ }
142+
143+ var newRepos []string
144+
145+ if repos, ok := data["repositories"].([]any); ok {
146+ for _, repo := range repos {
147+ if repoStr, ok := repo.(string); ok && strings.HasPrefix(repoStr, slug) {
148+ newRepos = append(newRepos, strings.Replace(repoStr, fmt.Sprintf("%s/", slug), "", 1))
149+ }
150+ }
151+ }
152+
153+ data["repositories"] = newRepos
154+
155+ newB, err := json.Marshal(data)
156+ if err != nil {
157+ return err
158+ }
159+
160+ jsonBuf := bytes.NewBuffer(newB)
161+
162+ r.ContentLength = int64(jsonBuf.Len())
163+ r.Header.Set("Content-Length", strconv.FormatInt(r.ContentLength, 10))
164+ r.Body = io.NopCloser(jsonBuf)
165+ }
166+
167+ if slug != "" && r.Request.Method == http.MethodGet && (strings.Contains(r.Request.URL.Path, "/tags/") || strings.Contains(r.Request.URL.Path, "/manifests/")) {
168+ splitPath := strings.Split(r.Request.URL.Path, "/")
169+
170+ if len(splitPath) > 1 {
171+ ele := splitPath[len(splitPath)-2]
172+ if ele == "tags" || ele == "manifests" {
173+ b, err := io.ReadAll(r.Body)
174+ if err != nil {
175+ return err
176+ }
177+
178+ err = r.Body.Close()
179+ if err != nil {
180+ return err
181+ }
182+
183+ var data map[string]any
184+ err = json.Unmarshal(b, &data)
185+ if err != nil {
186+ return err
187+ }
188+
189+ if name, ok := data["name"].(string); ok {
190+ if strings.HasPrefix(name, slug) {
191+ data["name"] = strings.Replace(name, fmt.Sprintf("%s/", slug), "", 1)
192+ }
193+ }
194+
195+ newB, err := json.Marshal(data)
196+ if err != nil {
197+ return err
198+ }
199+
200+ jsonBuf := bytes.NewBuffer(newB)
201+
202+ r.ContentLength = int64(jsonBuf.Len())
203+ r.Header.Set("Content-Length", strconv.FormatInt(r.ContentLength, 10))
204+ r.Body = io.NopCloser(jsonBuf)
205+ }
206+ }
207+ }
208+
209+ locationHeader := r.Header.Get("location")
210+ if slug != "" && strings.Contains(locationHeader, fmt.Sprintf("/v2/%s", slug)) {
211+ r.Header.Set("location", strings.ReplaceAll(locationHeader, fmt.Sprintf("/v2/%s", slug), "/v2"))
212+ }
213+
214+ return nil
215+ }
216+
217+ router.HandleFunc("/", proxy.ServeHTTP)
218+
219+ return router
220+}
221+
222+func StartSshServer() {
223+ host := os.Getenv("SSH_HOST")
224+ if host == "" {
225+ host = "0.0.0.0"
226+ }
227+ port := os.Getenv("SSH_PORT")
228+ if port == "" {
229+ port = "2222"
230+ }
231+ dbUrl := os.Getenv("DATABASE_URL")
232+ logger := slog.Default()
233+ dbh := postgres.NewDB(dbUrl, logger)
234+
235+ s, err := wish.NewServer(
236+ wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
237+ wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
238+ wish.WithPublicKeyAuth(AuthHandler(dbh, logger)),
239+ ptun.WithWebTunnel(ptun.NewWebTunnelHandler(serveMux, logger)),
240+ )
241+
242+ if err != nil {
243+ logger.Error("could not create server", "err", err)
244+ }
245+
246+ done := make(chan os.Signal, 1)
247+ signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
248+ logger.Info("starting SSH server", "host", host, "port", port)
249+ go func() {
250+ if err = s.ListenAndServe(); err != nil {
251+ logger.Error("serve error", "err", err)
252+ os.Exit(1)
253+ }
254+ }()
255+
256+ <-done
257+ logger.Info("stopping SSH server")
258+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
259+ defer func() { cancel() }()
260+ if err := s.Shutdown(ctx); err != nil {
261+ logger.Error("shutdown", "err", err)
262+ os.Exit(1)
263+ }
264+}