repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

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
M cmd/imgs/ssh/main.go
+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 }
M docker-compose.prod.yml
+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
M docker-compose.yml
+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
A imgs/registry.yml
+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
A imgs/ssh.go
+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+}