- commit
- 677655a
- parent
- 97f9643
- author
- Eric Bower
- date
- 2024-09-04 02:34:45 +0000 UTC
feat: pubsub service
27 files changed,
+1218,
-14
M
Makefile
+17,
-0
1@@ -145,3 +145,20 @@ AUTH_REAL_CERT_MOUNT=
2 AUTH_DOMAIN=http://auth.dev.pico.sh:3006
3 AUTH_ISSUER=pico.sh
4 AUTH_WEB_PORT=3000
5+
6+PUBSUB_CADDYFILE=./caddy/Caddyfile
7+PUBSUB_V4=
8+PUBSUB_V6=
9+PUBSUB_HTTP_V4=$PUBSUB_V4:80
10+PUBSUB_HTTP_V6=[$PUBSUB_V6]:80
11+PUBSUB_HTTPS_V4=$PUBSUB_V4:443
12+PUBSUB_HTTPS_V6=[$PUBSUB_V6]:443
13+PUBSUB_SSH_V4=$PUBSUB_V4:22
14+PUBSUB_SSH_V6=[$PUBSUB_V6]:22
15+PUBSUB_HOST=
16+PUBSUB_SSH_PORT=2222
17+PUBSUB_WEB_PORT=3000
18+PUBSUB_PROM_PORT=9222
19+PUBSUB_DOMAIN=pubsub.dev.pico.sh:3001
20+PUBSUB_PROTOCOL=http
21+PUBSUB_DEBUG=1
+1,
-1
1@@ -41,7 +41,7 @@ jobs:
2 needs: test
3 strategy:
4 matrix:
5- APP: [prose, pastes, imgs, pgs, feeds]
6+ APP: [prose, pastes, imgs, pgs, feeds, pubsub]
7 steps:
8 - name: Checkout repo
9 uses: actions/checkout@v3
M
Makefile
+2,
-2
1@@ -53,7 +53,7 @@ bp-%: bp-setup
2 $(DOCKER_BUILDX_BUILD) --build-arg "APP=$*" -t "ghcr.io/picosh/pico/$*-web:$(DOCKER_TAG)" --target release-web .
3 .PHONY: bp-%
4
5-bp-all: bp-prose bp-pastes bp-imgs bp-feeds bp-pgs bp-auth bp-bouncer
6+bp-all: bp-prose bp-pastes bp-imgs bp-feeds bp-pgs bp-auth bp-bouncer bp-pubsub
7 .PHONY: bp-all
8
9 build-auth:
10@@ -69,7 +69,7 @@ build-%:
11 go build -o "build/$*-ssh" "./cmd/$*/ssh"
12 .PHONY: build-%
13
14-build: build-prose build-pastes build-imgs build-feeds build-pgs build-auth build-pico
15+build: build-prose build-pastes build-imgs build-feeds build-pgs build-auth build-pico build-pubsub
16 .PHONY: build
17
18 store-clean:
+1,
-1
1@@ -656,7 +656,7 @@ func StartApiServer() {
2 Port: shared.GetEnv("AUTH_WEB_PORT", "3000"),
3 }
4
5- logger := shared.CreateLogger(true)
6+ logger := shared.CreateLogger()
7 db := postgres.NewDB(cfg.DbURL, logger)
8 defer db.Close()
9
+7,
-0
1@@ -0,0 +1,7 @@
2+package main
3+
4+import "github.com/picosh/pico/pubsub"
5+
6+func main() {
7+ pubsub.StartSshServer()
8+}
+7,
-0
1@@ -0,0 +1,7 @@
2+package main
3+
4+import "github.com/picosh/pico/pubsub"
5+
6+func main() {
7+ pubsub.StartApiServer()
8+}
+1,
-1
1@@ -30,6 +30,6 @@ func NewConfigSite() *shared.ConfigSite {
2 Space: "feeds",
3 AllowedExt: []string{".txt"},
4 HiddenPosts: []string{"_header.txt", "_readme.txt"},
5- Logger: shared.CreateLogger(debug == "1"),
6+ Logger: shared.CreateLogger(),
7 }
8 }
+1,
-1
1@@ -27,7 +27,7 @@ func NewConfigSite() *shared.ConfigSite {
2 MinioPass: minioPass,
3 Space: "imgs",
4 AllowedExt: []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico"},
5- Logger: shared.CreateLogger(debug == "1"),
6+ Logger: shared.CreateLogger(),
7 }
8
9 return &cfg
+1,
-1
1@@ -268,7 +268,7 @@ func StartSshServer() {
2 minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
3 minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
4
5- logger := shared.CreateLogger(false)
6+ logger := shared.CreateLogger()
7 logger.Info("bootup", "registry", registryUrl, "minio", minioUrl)
8 dbh := postgres.NewDB(dbUrl, logger)
9 st, err := storage.NewStorageMinio(minioUrl, minioUser, minioPass)
+1,
-1
1@@ -26,6 +26,6 @@ func NewConfigSite() *shared.ConfigSite {
2 MinioUser: minioUser,
3 MinioPass: minioPass,
4 Space: "pastes",
5- Logger: shared.CreateLogger(debug == "1"),
6+ Logger: shared.CreateLogger(),
7 }
8 }
+1,
-3
1@@ -8,7 +8,6 @@ var maxSize = uint64(25 * shared.MB)
2 var maxAssetSize = int64(10 * shared.MB)
3
4 func NewConfigSite() *shared.ConfigSite {
5- debug := shared.GetEnv("PGS_DEBUG", "0")
6 domain := shared.GetEnv("PGS_DOMAIN", "pgs.sh")
7 port := shared.GetEnv("PGS_WEB_PORT", "3000")
8 protocol := shared.GetEnv("PGS_PROTOCOL", "https")
9@@ -23,7 +22,6 @@ func NewConfigSite() *shared.ConfigSite {
10 }
11
12 cfg := shared.ConfigSite{
13- Debug: debug == "1",
14 Secret: secret,
15 Domain: domain,
16 Port: port,
17@@ -36,7 +34,7 @@ func NewConfigSite() *shared.ConfigSite {
18 Space: "pgs",
19 MaxSize: maxSize,
20 MaxAssetSize: maxAssetSize,
21- Logger: shared.CreateLogger(debug == "1"),
22+ Logger: shared.CreateLogger(),
23 }
24
25 return &cfg
+1,
-1
1@@ -10,6 +10,6 @@ func NewConfigSite() *shared.ConfigSite {
2 return &shared.ConfigSite{
3 DbURL: dbURL,
4 Space: "pico",
5- Logger: shared.CreateLogger(false),
6+ Logger: shared.CreateLogger(),
7 }
8 }
+1,
-1
1@@ -44,7 +44,7 @@ func NewConfigSite() *shared.ConfigSite {
2 ".ico",
3 },
4 HiddenPosts: []string{"_readme.md", "_styles.css", "_footer.md", "_404.md"},
5- Logger: shared.CreateLogger(debug == "1"),
6+ Logger: shared.CreateLogger(),
7 MaxSize: maxSize,
8 MaxAssetSize: maxImgSize,
9 }
+90,
-0
1@@ -0,0 +1,90 @@
2+package pubsub
3+
4+import (
5+ "fmt"
6+ "net/http"
7+ "os"
8+
9+ "github.com/picosh/pico/db/postgres"
10+ "github.com/picosh/pico/shared"
11+)
12+
13+func serveFile(file string, contentType string) http.HandlerFunc {
14+ return func(w http.ResponseWriter, r *http.Request) {
15+ logger := shared.GetLogger(r)
16+ cfg := shared.GetCfg(r)
17+
18+ contents, err := os.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
19+ if err != nil {
20+ logger.Error(err.Error())
21+ http.Error(w, "file not found", 404)
22+ }
23+ w.Header().Add("Content-Type", contentType)
24+
25+ _, err = w.Write(contents)
26+ if err != nil {
27+ logger.Error(err.Error())
28+ http.Error(w, "server error", 500)
29+ }
30+ }
31+}
32+
33+func createStaticRoutes() []shared.Route {
34+ return []shared.Route{
35+ shared.NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
36+ shared.NewRoute("GET", "/smol.css", serveFile("smol.css", "text/css")),
37+ shared.NewRoute("GET", "/syntax.css", serveFile("syntax.css", "text/css")),
38+ shared.NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
39+ shared.NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
40+ shared.NewRoute("GET", "/favicon-32x32.png", serveFile("favicon-32x32.png", "image/png")),
41+ shared.NewRoute("GET", "/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")),
42+ shared.NewRoute("GET", "/favicon.ico", serveFile("favicon.ico", "image/x-icon")),
43+ shared.NewRoute("GET", "/robots.txt", serveFile("robots.txt", "text/plain")),
44+ }
45+}
46+
47+func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
48+ routes := []shared.Route{
49+ shared.NewRoute("GET", "/", shared.CreatePageHandler("html/marketing.page.tmpl")),
50+ shared.NewRoute("GET", "/check", shared.CheckHandler),
51+ }
52+
53+ routes = append(
54+ routes,
55+ staticRoutes...,
56+ )
57+
58+ return routes
59+}
60+
61+func StartApiServer() {
62+ cfg := NewConfigSite()
63+ db := postgres.NewDB(cfg.DbURL, cfg.Logger)
64+ defer db.Close()
65+ logger := cfg.Logger
66+
67+ staticRoutes := createStaticRoutes()
68+
69+ if cfg.Debug {
70+ staticRoutes = shared.CreatePProfRoutes(staticRoutes)
71+ }
72+
73+ mainRoutes := createMainRoutes(staticRoutes)
74+ subdomainRoutes := staticRoutes
75+
76+ apiConfig := &shared.ApiConfig{
77+ Cfg: cfg,
78+ Dbpool: db,
79+ }
80+ handler := shared.CreateServe(mainRoutes, subdomainRoutes, apiConfig)
81+ router := http.HandlerFunc(handler)
82+
83+ portStr := fmt.Sprintf(":%s", cfg.Port)
84+ logger.Info(
85+ "Starting server on port",
86+ "port", cfg.Port,
87+ "domain", cfg.Domain,
88+ )
89+
90+ logger.Error(http.ListenAndServe(portStr, router).Error())
91+}
+159,
-0
1@@ -0,0 +1,159 @@
2+package pubsub
3+
4+import (
5+ "fmt"
6+ "log/slog"
7+ "strings"
8+
9+ "github.com/charmbracelet/ssh"
10+ "github.com/charmbracelet/wish"
11+ "github.com/google/uuid"
12+ "github.com/picosh/pico/db"
13+ "github.com/picosh/pico/shared"
14+ "github.com/picosh/pico/shared/storage"
15+ "github.com/picosh/pico/tui/common"
16+ psub "github.com/picosh/pubsub"
17+ "github.com/picosh/send/send/utils"
18+)
19+
20+func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
21+ var err error
22+ key, err := shared.KeyText(s)
23+ if err != nil {
24+ return nil, fmt.Errorf("key not found")
25+ }
26+
27+ user, err := dbpool.FindUserForKey(s.User(), key)
28+ if err != nil {
29+ return nil, err
30+ }
31+
32+ if user.Name == "" {
33+ return nil, fmt.Errorf("must have username set")
34+ }
35+
36+ return user, nil
37+}
38+
39+type Cmd struct {
40+ User *db.User
41+ Session shared.CmdSession
42+ Log *slog.Logger
43+ Dbpool db.DB
44+ Styles common.Styles
45+}
46+
47+func (c *Cmd) output(out string) {
48+ _, _ = c.Session.Write([]byte(out + "\r\n"))
49+}
50+
51+func (c *Cmd) error(err error) {
52+ _, _ = fmt.Fprint(c.Session.Stderr(), err, "\r\n")
53+ _ = c.Session.Exit(1)
54+ _ = c.Session.Close()
55+}
56+
57+func (c *Cmd) bail(err error) {
58+ if err == nil {
59+ return
60+ }
61+ c.Log.Error(err.Error())
62+ c.error(err)
63+}
64+
65+func (c *Cmd) help() {
66+ helpStr := "Commands: [pub, sub, ls]\n"
67+ c.output(helpStr)
68+}
69+
70+func (c *Cmd) ls() error {
71+ helpStr := "TODO\n"
72+ c.output(helpStr)
73+ return nil
74+}
75+
76+type CliHandler struct {
77+ DBPool db.DB
78+ Logger *slog.Logger
79+ Storage storage.StorageServe
80+ RegistryUrl string
81+ PubSub *psub.Cfg
82+}
83+
84+func WishMiddleware(handler *CliHandler) wish.Middleware {
85+ dbpool := handler.DBPool
86+ log := handler.Logger
87+ pubsub := handler.PubSub
88+
89+ return func(next ssh.Handler) ssh.Handler {
90+ return func(sesh ssh.Session) {
91+ user, err := getUser(sesh, dbpool)
92+ if err != nil {
93+ utils.ErrorHandler(sesh, err)
94+ return
95+ }
96+
97+ args := sesh.Command()
98+
99+ opts := Cmd{
100+ Session: sesh,
101+ User: user,
102+ Log: log,
103+ Dbpool: dbpool,
104+ }
105+
106+ if len(args) == 0 {
107+ next(sesh)
108+ return
109+ }
110+
111+ cmd := strings.TrimSpace(args[0])
112+ if len(args) == 1 {
113+ if cmd == "help" {
114+ opts.help()
115+ return
116+ } else if cmd == "ls" {
117+ err := opts.ls()
118+ opts.bail(err)
119+ return
120+ } else {
121+ next(sesh)
122+ return
123+ }
124+ }
125+
126+ repoName := strings.TrimSpace(args[1])
127+ cmdArgs := args[2:]
128+ log.Info(
129+ "imgs middleware detected command",
130+ "args", args,
131+ "cmd", cmd,
132+ "repoName", repoName,
133+ "cmdArgs", cmdArgs,
134+ )
135+
136+ if cmd == "pub" {
137+ err = pubsub.PubSub.Pub(&psub.Msg{
138+ Name: fmt.Sprintf("%s@%s", user.Name, repoName),
139+ Reader: sesh,
140+ })
141+ if err != nil {
142+ wish.Errorln(sesh, err)
143+ }
144+ } else if cmd == "sub" {
145+ err = pubsub.PubSub.Sub(&psub.Subscriber{
146+ ID: uuid.NewString(),
147+ Name: fmt.Sprintf("%s@%s", user.Name, repoName),
148+ Session: sesh,
149+ Chan: make(chan error),
150+ })
151+ if err != nil {
152+ wish.Errorln(sesh, err)
153+ }
154+ } else {
155+ next(sesh)
156+ return
157+ }
158+ }
159+ }
160+}
+20,
-0
1@@ -0,0 +1,20 @@
2+package pubsub
3+
4+import (
5+ "github.com/picosh/pico/shared"
6+)
7+
8+func NewConfigSite() *shared.ConfigSite {
9+ domain := shared.GetEnv("PUBSUB_DOMAIN", "send.pico.sh")
10+ port := shared.GetEnv("PUBSUB_WEB_PORT", "3000")
11+ dbURL := shared.GetEnv("DATABASE_URL", "")
12+ protocol := shared.GetEnv("PUBSUB_PROTOCOL", "https")
13+
14+ return &shared.ConfigSite{
15+ Domain: domain,
16+ Port: port,
17+ Protocol: protocol,
18+ DbURL: dbURL,
19+ Logger: shared.CreateLogger(),
20+ }
21+}
+18,
-0
1@@ -0,0 +1,18 @@
2+{{define "base"}}
3+<!doctype html>
4+<html lang="en">
5+ <head>
6+ <meta charset='utf-8'>
7+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8+ <title>{{template "title" .}}</title>
9+
10+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
11+
12+ <meta name="keywords" content="pastebin, paste, copy, snippets" />
13+ {{template "meta" .}}
14+
15+ <link rel="stylesheet" href="/smol.css" />
16+ </head>
17+ <body {{template "attrs" .}}>{{template "body" .}}</body>
18+</html>
19+{{end}}
1@@ -0,0 +1,6 @@
2+{{define "marketing-footer"}}
3+<footer>
4+ <hr />
5+ <p class="font-italic">Built and maintained by <a href="https://pico.sh">pico.sh</a>.</p>
6+</footer>
7+{{end}}
+38,
-0
1@@ -0,0 +1,38 @@
2+{{template "base" .}}
3+
4+{{define "title"}}{{.Site.Domain}} -- pubsub using ssh{{end}}
5+
6+{{define "meta"}}
7+<meta name="description" content="pubsub using ssh" />
8+
9+<meta property="og:type" content="website">
10+<meta property="og:site_name" content="{{.Site.Domain}}">
11+<meta property="og:url" content="https://{{.Site.Domain}}">
12+<meta property="og:title" content="{{.Site.Domain}}">
13+<meta property="og:description" content="pubsub using ssh">
14+
15+<meta name="twitter:card" content="summary" />
16+<meta property="twitter:url" content="https://{{.Site.Domain}}">
17+<meta property="twitter:title" content="{{.Site.Domain}}">
18+<meta property="twitter:description" content="pubsub using ssh">
19+<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
20+<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
21+
22+<meta property="og:image:width" content="300" />
23+<meta property="og:image:height" content="300" />
24+<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
25+<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
26+{{end}}
27+
28+{{define "attrs"}}{{end}}
29+
30+{{define "body"}}
31+<header class="text-center">
32+ <h1 class="text-2xl font-bold">{{.Site.Domain}}</h1>
33+ <p class="text-lg">pubsub using ssh</p>
34+ <pre>ssh {{.Site.Domain}} sub mykey</pre>
35+ <pre>echo "hello world!" | ssh {{.Site.Domain}} pub mykey</pre>
36+</header>
37+
38+{{template "marketing-footer" .}}
39+{{end}}
+0,
-0
+0,
-0
+0,
-0
+0,
-0
+2,
-0
1@@ -0,0 +1,2 @@
2+User-agent: *
3+Allow: /
+768,
-0
1@@ -0,0 +1,768 @@
2+*,
3+::before,
4+::after {
5+ box-sizing: border-box;
6+}
7+
8+::-moz-focus-inner {
9+ border-style: none;
10+ padding: 0;
11+}
12+:-moz-focusring {
13+ outline: 1px dotted ButtonText;
14+}
15+:-moz-ui-invalid {
16+ box-shadow: none;
17+}
18+
19+@media (prefers-color-scheme: light) {
20+ :root {
21+ --main-hue: 250;
22+ --white: #2e3f53;
23+ --white-light: #cfe0f4;
24+ --white-dark: #6c6a6a;
25+ --code: #52576f;
26+ --pre: #e1e7ee;
27+ --bg-color: #f4f4f4;
28+ --text-color: #24292f;
29+ --link-color: #005cc5;
30+ --visited: #6f42c1;
31+ --blockquote: #005cc5;
32+ --blockquote-bg: #cfe0f4;
33+ --hover: #c11e7a;
34+ --grey: #ccc;
35+ --grey-light: #6a708e;
36+ --shadow: #e8e8e8;
37+ }
38+}
39+
40+@media (prefers-color-scheme: dark) {
41+ :root {
42+ --main-hue: 250;
43+ --white: #f2f2f2;
44+ --white-light: #f2f2f2;
45+ --white-dark: #e8e8e8;
46+ --code: #414558;
47+ --pre: #252525;
48+ --bg-color: #282a36;
49+ --text-color: #f2f2f2;
50+ --link-color: #8be9fd;
51+ --visited: #bd93f9;
52+ --blockquote: #bd93f9;
53+ --blockquote-bg: #353548;
54+ --hover: #ff80bf;
55+ --grey: #414558;
56+ --grey-light: #6a708e;
57+ --shadow: #252525;
58+ }
59+}
60+
61+html {
62+ background-color: var(--bg-color);
63+ color: var(--text-color);
64+ font-size: 18px;
65+ line-height: 1.5;
66+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
67+ Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
68+ sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
69+ -webkit-text-size-adjust: 100%;
70+ -moz-tab-size: 4;
71+ -o-tab-size: 4;
72+ tab-size: 4;
73+}
74+
75+body {
76+ margin: 0 auto;
77+}
78+
79+img {
80+ max-width: 100%;
81+ height: auto;
82+}
83+
84+b,
85+strong {
86+ font-weight: bold;
87+}
88+
89+code,
90+kbd,
91+samp,
92+pre {
93+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
94+ monospace;
95+}
96+
97+code,
98+kbd,
99+samp {
100+ border: 2px solid var(--code);
101+}
102+
103+pre > code {
104+ background-color: inherit;
105+ padding: 0;
106+ border: none;
107+}
108+
109+code {
110+ font-size: 90%;
111+ border-radius: 0.3rem;
112+ padding: 0.1rem 0.3rem;
113+}
114+
115+pre {
116+ font-size: 14px;
117+ border-radius: 5px;
118+ padding: 1rem;
119+ margin: 1rem 0;
120+ overflow-x: auto;
121+ background-color: var(--pre) !important;
122+}
123+
124+small {
125+ font-size: 0.8rem;
126+}
127+
128+summary {
129+ display: list-item;
130+ cursor: pointer;
131+}
132+
133+h1,
134+h2,
135+h3,
136+h4 {
137+ margin: 0;
138+ padding: 0.5rem 0 0 0;
139+ border: 0;
140+ font-style: normal;
141+ font-weight: inherit;
142+ font-size: inherit;
143+}
144+
145+path {
146+ fill: var(--text-color);
147+ stroke: var(--text-color);
148+}
149+
150+hr {
151+ color: inherit;
152+ border: 0;
153+ margin: 0;
154+ height: 2px;
155+ background: var(--grey);
156+ margin: 1rem auto;
157+ text-align: center;
158+}
159+
160+a {
161+ text-decoration: none;
162+ color: var(--link-color);
163+}
164+
165+a:hover,
166+a:visited:hover {
167+ text-decoration: underline;
168+ color: var(--hover);
169+}
170+
171+a:visited {
172+ color: var(--visited);
173+}
174+
175+a.link-grey {
176+ text-decoration: underline;
177+ color: var(--white);
178+}
179+
180+a.link-grey:visited {
181+ color: var(--white);
182+}
183+
184+section {
185+ margin-bottom: 1.4rem;
186+}
187+
188+section:last-child {
189+ margin-bottom: 0;
190+}
191+
192+header {
193+ margin: 1rem auto;
194+}
195+
196+p {
197+ margin: 0.5rem 0;
198+}
199+
200+article {
201+ overflow-wrap: break-word;
202+}
203+
204+blockquote {
205+ border-left: 5px solid var(--blockquote);
206+ background-color: var(--blockquote-bg);
207+ padding: 0.5rem 0.75rem;
208+ margin: 0.5rem 0;
209+}
210+
211+blockquote > p {
212+ margin: 0;
213+}
214+
215+blockquote code {
216+ border: 1px solid var(--blockquote);
217+}
218+
219+ul,
220+ol {
221+ padding: 0 0 0 1rem;
222+ list-style-position: outside;
223+}
224+
225+ul[style*="list-style-type: none;"] {
226+ padding: 0;
227+}
228+
229+li {
230+ margin: 0.5rem 0;
231+}
232+
233+li > pre {
234+ padding: 0;
235+}
236+
237+footer {
238+ text-align: center;
239+ margin-bottom: 4rem;
240+}
241+
242+dt {
243+ font-weight: bold;
244+}
245+
246+dd {
247+ margin-left: 0;
248+}
249+
250+dd:not(:last-child) {
251+ margin-bottom: 0.5rem;
252+}
253+
254+figure {
255+ margin: 0;
256+}
257+
258+.container {
259+ max-width: 50em;
260+ width: 100%;
261+}
262+
263+.container-sm {
264+ max-width: 40em;
265+ width: 100%;
266+}
267+
268+.container-center {
269+ width: 100%;
270+ height: 100%;
271+ display: flex;
272+ justify-content: center;
273+}
274+
275+.mono {
276+ font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
277+ monospace;
278+}
279+
280+.link-alt-adj,
281+.link-alt-adj:visited,
282+.link-alt-adj:visited:hover,
283+.link-alt-adj:hover {
284+ color: var(--link-color);
285+ text-decoration: none;
286+}
287+
288+.link-alt-adj:visited:hover,
289+.link-alt-adj:hover {
290+ text-decoration: underline;
291+}
292+
293+.link-alt-hover,
294+.link-alt-hover:visited,
295+.link-alt-hover:visited:hover,
296+.link-alt-hover:hover {
297+ color: var(--hover);
298+ text-decoration: none;
299+}
300+
301+.link-alt-hover:visited:hover,
302+.link-alt-hover:hover {
303+ text-decoration: underline;
304+}
305+
306+.link-alt,
307+.link-alt:visited,
308+.link-alt:visited:hover,
309+.link-alt:hover {
310+ color: var(--white);
311+ text-decoration: none;
312+}
313+
314+.link-alt:visited:hover,
315+.link-alt:hover {
316+ text-decoration: underline;
317+}
318+
319+.text-3xl {
320+ font-size: 2.5rem;
321+}
322+
323+.text-2xl {
324+ font-size: 1.9rem;
325+ line-height: 1.15;
326+}
327+
328+.text-xl {
329+ font-size: 1.55rem;
330+ line-height: 1.15;
331+}
332+
333+.text-lg {
334+ font-size: 1.35rem;
335+ line-height: 1.15;
336+}
337+
338+.text-md {
339+ font-size: 1.15rem;
340+ line-height: 1.15;
341+}
342+
343+.text-sm {
344+ font-size: 0.875rem;
345+}
346+
347+.text-xs {
348+ font-size: 0.775rem;
349+}
350+
351+.cursor-pointer {
352+ cursor: pointer;
353+}
354+
355+.w-full {
356+ width: 100%;
357+}
358+
359+.h-full {
360+ height: 100%;
361+}
362+
363+.border {
364+ border: 2px solid var(--grey-light);
365+}
366+
367+.text-left {
368+ text-align: left;
369+}
370+
371+.text-center {
372+ text-align: center;
373+}
374+
375+.text-underline {
376+ border-bottom: 3px solid var(--text-color);
377+ padding-bottom: 3px;
378+}
379+
380+.text-hdr {
381+ color: var(--hover);
382+}
383+
384+.text-underline-hdr {
385+ border-bottom: 3px solid var(--hover);
386+ padding-bottom: 3px;
387+}
388+
389+.font-bold {
390+ font-weight: bold;
391+}
392+
393+.font-italic {
394+ font-style: italic;
395+}
396+
397+.inline {
398+ display: inline;
399+}
400+
401+.inline-block {
402+ display: inline-block;
403+}
404+
405+.max-w-half {
406+ max-width: 50%;
407+}
408+
409+.h-screen {
410+ height: 100vh;
411+}
412+
413+.w-screen {
414+ width: 100vw;
415+}
416+
417+.flex {
418+ display: flex;
419+}
420+
421+.flex-col {
422+ flex-direction: column;
423+}
424+
425+.items-center {
426+ align-items: center;
427+}
428+
429+.m-0 {
430+ margin: 0;
431+}
432+
433+.mt {
434+ margin-top: 0.5rem;
435+}
436+
437+.mt-2 {
438+ margin-top: 1rem;
439+}
440+
441+.mt-4 {
442+ margin-top: 2rem;
443+}
444+
445+.mt-8 {
446+ margin-top: 4rem;
447+}
448+
449+.mb {
450+ margin-bottom: 0.5rem;
451+}
452+
453+.mb-2 {
454+ margin-bottom: 1rem;
455+}
456+
457+.mb-4 {
458+ margin-bottom: 2rem;
459+}
460+
461+.mb-8 {
462+ margin-bottom: 4rem;
463+}
464+
465+.mb-16 {
466+ margin-bottom: 8rem;
467+}
468+
469+.mr {
470+ margin-right: 0.5rem;
471+}
472+
473+.ml-sm {
474+ margin-left: 0.25rem;
475+}
476+
477+.ml {
478+ margin-left: 0.5rem;
479+}
480+
481+.pt-0 {
482+ padding-top: 0;
483+}
484+
485+.my {
486+ margin-top: 0.5rem;
487+ margin-bottom: 0.5rem;
488+}
489+
490+.my-2 {
491+ margin-top: 1rem;
492+ margin-bottom: 1rem;
493+}
494+
495+.my-4 {
496+ margin-top: 2rem;
497+ margin-bottom: 2rem;
498+}
499+
500+.my-8 {
501+ margin-top: 4rem;
502+ margin-bottom: 4rem;
503+}
504+
505+.mx {
506+ margin-left: 0.5rem;
507+ margin-right: 0.5rem;
508+}
509+
510+.mx-2 {
511+ margin-left: 1rem;
512+ margin-right: 1rem;
513+}
514+
515+.m-1 {
516+ margin: 0.5rem;
517+}
518+
519+.p-1 {
520+ padding: 0.5rem;
521+}
522+
523+.p-0 {
524+ padding: 0;
525+}
526+
527+.px-2 {
528+ padding-left: 1rem;
529+ padding-right: 1rem;
530+}
531+
532+.px-4 {
533+ padding-left: 2rem;
534+ padding-right: 2rem;
535+}
536+
537+.py {
538+ padding-top: 0.5rem;
539+ padding-bottom: 0.5rem;
540+}
541+
542+.py-2 {
543+ padding-top: 1rem;
544+ padding-bottom: 1rem;
545+}
546+
547+.py-4 {
548+ padding-top: 2rem;
549+ padding-bottom: 2rem;
550+}
551+
552+.py-8 {
553+ padding-top: 4rem;
554+ padding-bottom: 4rem;
555+}
556+
557+.justify-between {
558+ justify-content: space-between;
559+}
560+
561+.justify-center {
562+ justify-content: center;
563+}
564+
565+.gap {
566+ gap: 0.5rem;
567+}
568+
569+.gap-2 {
570+ gap: 1rem;
571+}
572+
573+.group {
574+ display: flex;
575+ flex-direction: column;
576+ gap: 0.5rem;
577+}
578+
579+.group-2 {
580+ display: flex;
581+ flex-direction: column;
582+ gap: 1rem;
583+}
584+
585+.group-h {
586+ display: flex;
587+ gap: 0.5rem;
588+ align-items: center;
589+}
590+
591+.flex-1 {
592+ flex: 1;
593+}
594+
595+.items-end {
596+ align-items: end;
597+}
598+
599+.items-start {
600+ align-items: start;
601+}
602+
603+.justify-end {
604+ justify-content: end;
605+}
606+
607+.font-grey-light {
608+ color: var(--grey-light);
609+}
610+
611+.hidden {
612+ display: none;
613+}
614+
615+.align-right {
616+ text-align: right;
617+}
618+
619+/* ==== MARKDOWN ==== */
620+
621+.md h1,
622+.md h2,
623+.md h3,
624+.md h4 {
625+ padding: 0;
626+ margin: 1.5rem 0 0.9rem 0;
627+ font-weight: bold;
628+}
629+
630+.md h1 a,
631+.md h2 a,
632+.md h3 a,
633+.md h4 a {
634+ color: var(--grey-light);
635+ text-decoration: none;
636+}
637+
638+.md h1 {
639+ font-size: 1.6rem;
640+ line-height: 1.15;
641+ border-bottom: 2px solid var(--grey);
642+ padding-bottom: 0.7rem;
643+}
644+
645+.md h2 {
646+ font-size: 1.3rem;
647+ line-height: 1.15;
648+ color: var(--white-dark);
649+}
650+
651+.md h3 {
652+ font-size: 1.2rem;
653+ color: var(--white-dark);
654+}
655+
656+.md h4 {
657+ font-size: 1rem;
658+ color: var(--white-dark);
659+}
660+
661+/* ==== HELPERS ==== */
662+
663+.logo-header {
664+ line-height: 1;
665+ display: inline-block;
666+ background-color: #FF79C6;
667+ background-image: linear-gradient(to right, #FF5555, #FF79C6, #F8F859);
668+ color: transparent;
669+ background-clip: text;
670+ border: 3px solid #FF79C6;
671+ padding: 8px 10px 10px 10px;
672+ border-radius: 10px;
673+ box-shadow: 0px 5px 0px 0px var(--shadow);
674+ background-size: 100%;
675+ -webkit-background-clip: text;
676+ -moz-background-clip: text;
677+ -webkit-text-fill-color: transparent;
678+ -moz-text-fill-color: transparent;
679+}
680+
681+.btn {
682+ border: 2px solid var(--link-color);
683+ color: var(--link-color);
684+ padding: 0.4rem 1rem;
685+ font-weight: bold;
686+ display: inline-block;
687+}
688+
689+.btn-link,
690+.btn-link:visited {
691+ border: 2px solid var(--link-color);
692+ color: var(--link-color);
693+ padding: 0.4rem 1rem;
694+ text-decoration: none;
695+ font-weight: bold;
696+ display: inline-block;
697+}
698+
699+.btn-link:visited:hover,
700+.btn-link:hover {
701+ border: 2px solid var(--hover);
702+}
703+
704+.btn-link-alt,
705+.btn-link-alt:visited {
706+ border: 2px solid var(--white);
707+ color: var(--white);
708+}
709+
710+.box {
711+ border: 2px solid var(--grey-light);
712+ padding: 0.5rem 0.75rem;
713+}
714+
715+.box-sm {
716+ border: 2px solid var(--grey-light);
717+ padding: 0.15rem 0.35rem;
718+}
719+
720+.box-alert {
721+ border: 2px solid var(--hover);
722+ padding: 0.5rem 0.75rem;
723+}
724+
725+.box-sm-alert {
726+ border: 2px solid var(--hover);
727+ padding: 0.15rem 0.35rem;
728+}
729+
730+.list-none {
731+ list-style-type: none;
732+}
733+
734+.list-disc {
735+ list-style-type: disc;
736+}
737+
738+.list-decimal {
739+ list-style-type: decimal;
740+}
741+
742+.pill {
743+ border: 1px solid var(--link-color);
744+ color: var(--link-color);
745+}
746+
747+.pill-alert {
748+ border: 1px solid var(--hover);
749+ color: var(--hover);
750+}
751+
752+.pill-info {
753+ border: 1px solid var(--visited);
754+ color: var(--visited);
755+}
756+
757+@media only screen and (max-width: 40em) {
758+ body {
759+ padding: 0 1rem;
760+ }
761+
762+ header {
763+ margin: 0;
764+ }
765+
766+ .flex-collapse {
767+ flex-direction: column;
768+ }
769+}
+74,
-0
1@@ -0,0 +1,74 @@
2+package pubsub
3+
4+import (
5+ "context"
6+ "fmt"
7+ "os"
8+ "os/signal"
9+ "syscall"
10+ "time"
11+
12+ "github.com/charmbracelet/promwish"
13+ "github.com/charmbracelet/wish"
14+ "github.com/picosh/pico/db/postgres"
15+ "github.com/picosh/pico/filehandlers/util"
16+ "github.com/picosh/pico/shared"
17+ wsh "github.com/picosh/pico/wish"
18+ psub "github.com/picosh/pubsub"
19+)
20+
21+func StartSshServer() {
22+ host := shared.GetEnv("PUBSUB_HOST", "0.0.0.0")
23+ port := shared.GetEnv("PUBSUB_SSH_PORT", "2222")
24+ promPort := shared.GetEnv("PUBSUB_PROM_PORT", "9222")
25+ cfg := NewConfigSite()
26+ logger := cfg.Logger
27+ dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
28+ defer dbh.Close()
29+
30+ pubsub := &psub.Cfg{
31+ Logger: logger,
32+ PubSub: &psub.PubSubMulticast{
33+ Logger: logger,
34+ },
35+ }
36+
37+ handler := &CliHandler{
38+ Logger: logger,
39+ DBPool: dbh,
40+ PubSub: pubsub,
41+ }
42+
43+ sshAuth := util.NewSshAuthHandler(dbh, logger, cfg)
44+ s, err := wish.NewServer(
45+ wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
46+ wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
47+ wish.WithPublicKeyAuth(sshAuth.PubkeyAuthHandler),
48+ wish.WithMiddleware(WishMiddleware(handler)),
49+ wish.WithMiddleware(
50+ promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "pubsub-ssh"),
51+ wsh.LogMiddleware(logger),
52+ ),
53+ )
54+ if err != nil {
55+ logger.Error(err.Error())
56+ return
57+ }
58+
59+ done := make(chan os.Signal, 1)
60+ signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
61+ logger.Info("Starting SSH server", "host", host, "port", port)
62+ go func() {
63+ if err = s.ListenAndServe(); err != nil {
64+ logger.Error(err.Error())
65+ }
66+ }()
67+
68+ <-done
69+ logger.Info("Stopping SSH server")
70+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
71+ defer func() { cancel() }()
72+ if err := s.Shutdown(ctx); err != nil {
73+ logger.Error(err.Error())
74+ }
75+}
1@@ -261,7 +261,7 @@ func (c *ConfigSite) AssetURL(username, projectName, fpath string) string {
2 )
3 }
4
5-func CreateLogger(debug bool) *slog.Logger {
6+func CreateLogger() *slog.Logger {
7 opts := &slog.HandlerOptions{
8 AddSource: true,
9 }