this repo has no description

rework config schema

Changed files
+84 -225
cmd
examples
.smallweb
schemas
worker
+1
.gitignore
··· 7 7 8 8 __debug_bin* 9 9 /smallweb 10 + /www
+2 -3
cmd/crons.go
··· 8 8 "log" 9 9 "os" 10 10 "path/filepath" 11 - "slices" 12 11 "strings" 13 12 "time" 14 13 ··· 73 72 continue 74 73 } 75 74 76 - app, err := app.LoadApp(name, k.String("dir"), k.String("domain"), slices.Contains(k.Strings("adminApps"), name)) 75 + app, err := app.LoadApp(name, k.String("dir"), k.String("domain"), k.Bool(fmt.Sprintf("apps.%s.admin", name))) 77 76 if err != nil { 78 77 return fmt.Errorf("failed to load app: %w", err) 79 78 } ··· 150 149 } 151 150 152 151 for _, name := range apps { 153 - a, err := app.LoadApp(name, k.String("dir"), k.String("domain"), slices.Contains(k.Strings("adminApps"), name)) 152 + a, err := app.LoadApp(name, k.String("dir"), k.String("domain"), k.Bool(fmt.Sprintf("apps.%s.admin", name))) 154 153 if err != nil { 155 154 fmt.Println(err) 156 155 continue
+1 -2
cmd/fetch.go
··· 5 5 "io" 6 6 "net/http/httptest" 7 7 "os" 8 - "slices" 9 8 "strings" 10 9 11 10 "github.com/pomdtr/smallweb/app" ··· 52 51 53 52 req.Host = fmt.Sprintf("%s.%s", args[0], k.String("domain")) 54 53 55 - a, err := app.LoadApp(args[0], k.String("dir"), k.String("domain"), slices.Contains(k.Strings("adminApps"), args[0])) 54 + a, err := app.LoadApp(args[0], k.String("dir"), k.String("domain"), k.Bool(fmt.Sprintf("apps.%s.admin", args[0]))) 56 55 if err != nil { 57 56 return fmt.Errorf("failed to load app: %w", err) 58 57 }
+1 -1
cmd/logs.go
··· 170 170 hosts := make(map[string]struct{}) 171 171 hosts[fmt.Sprintf("%s.%s", appName, k.String("domain"))] = struct{}{} 172 172 173 - for domain, app := range k.StringMap("customDomains") { 173 + for domain, app := range k.StringMap("additionalDomains") { 174 174 if appName != "" && app != appName { 175 175 continue 176 176 }
+3 -14
cmd/root.go
··· 35 35 return "dir", v 36 36 case "SMALLWEB_DOMAIN": 37 37 return "domain", v 38 - case "SMALLWEB_REMOTE": 39 - return "remote", v 40 - case "SMALLWEB_CUSTOM_DOMAINS": 41 - customDomains := make(map[string]string) 42 - for _, entry := range strings.Split(v, ";") { 43 - parts := strings.Split(entry, "=") 44 - if len(parts) != 2 { 45 - continue 46 - } 47 - 48 - customDomains[parts[0]] = parts[1] 49 - } 50 - 51 - return "customDomains", customDomains 38 + case "SMALLWEB_ADDITIONAL_DOMAINS": 39 + additionalDomains := strings.Split(v, ";") 40 + return "additional_domains", additionalDomains 52 41 } 53 42 54 43 return "", nil
+1 -2
cmd/run.go
··· 3 3 import ( 4 4 "fmt" 5 5 "os" 6 - "slices" 7 6 8 7 "github.com/pomdtr/smallweb/app" 9 8 "github.com/pomdtr/smallweb/worker" ··· 24 23 return cmd.Help() 25 24 } 26 25 27 - a, err := app.LoadApp(args[0], k.String("dir"), k.String("domain"), slices.Contains(k.Strings("adminApps"), args[0])) 26 + a, err := app.LoadApp(args[0], k.String("dir"), k.String("domain"), k.Bool(fmt.Sprintf("apps.%s.admin", args[0]))) 28 27 if err != nil { 29 28 return fmt.Errorf("failed to load app: %w", err) 30 29 }
+26 -66
cmd/up.go
··· 102 102 if flags.onDemandTLS { 103 103 certmagic.Default.OnDemand = &certmagic.OnDemandConfig{ 104 104 DecisionFunc: func(ctx context.Context, name string) error { 105 - domain := k.String("domain") 106 - if name == domain { 107 - return nil 108 - } 109 - 110 - customDomains := k.StringMap("customDomains") 111 - if _, ok := customDomains[name]; ok { 112 - return nil 113 - } 114 - 115 - parts := strings.SplitN(name, ".", 2) 116 - if len(parts) != 2 { 117 - return fmt.Errorf("invalid domain: %s", name) 118 - } 119 - 120 - base := parts[1] 121 - if base == domain { 105 + if _, _, ok := lookupApp(name); ok { 122 106 return nil 123 107 } 124 108 125 - if target, ok := customDomains[base]; ok && target == "*" { 109 + if _, err := os.Stat(filepath.Join(k.String("dir"), name)); err == nil { 126 110 return nil 127 111 } 128 112 129 - return fmt.Errorf("domain not found: %s", name) 113 + return fmt.Errorf("domain not found") 130 114 }, 131 115 } 132 116 fmt.Fprintf(cmd.ErrOrStderr(), "Serving *.%s from %s on %s...\n", k.String("domain"), utils.AddTilde(k.String("dir")), ":443") ··· 195 179 wish.WithAddress(flags.sshAddr), 196 180 wish.WithHostKeyPath(hostKey), 197 181 wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool { 198 - authorizedKeyPaths := []string{filepath.Join(k.String("dir"), ".smallweb", "authorized_keys")} 199 - 182 + authorizedKeys := k.Strings("authorizedKeys") 200 183 if ctx.User() != "_" { 201 - authorizedKeyPaths = append(authorizedKeyPaths, filepath.Join(k.String("dir"), ctx.User(), "authorized_keys")) 184 + authorizedKeys = append(authorizedKeys, k.Strings(fmt.Sprintf("apps.%s.authorizedKeys"))...) 202 185 } 203 186 204 - for _, authorizedKeysPath := range authorizedKeyPaths { 205 - if _, err := os.Stat(authorizedKeysPath); err != nil { 206 - return false 207 - } 208 - 209 - authorizedKeysBytes, err := os.ReadFile(authorizedKeysPath) 187 + for _, authorizedKey := range authorizedKeys { 188 + k, _, _, _, err := gossh.ParseAuthorizedKey([]byte(authorizedKey)) 210 189 if err != nil { 211 - return false 190 + continue 212 191 } 213 192 214 - for len(authorizedKeysBytes) > 0 { 215 - k, _, _, rest, err := gossh.ParseAuthorizedKey(authorizedKeysBytes) 216 - if err != nil { 217 - return false 218 - } 219 - 220 - if ssh.KeysEqual(k, key) { 221 - return true 222 - } 223 - 224 - authorizedKeysBytes = rest 193 + if ssh.KeysEqual(k, key) { 194 + return true 225 195 } 226 196 } 227 197 228 198 return false 229 199 }), 230 - // TODO: re-implement sftp 231 200 sftp.SSHOption(k.String("dir"), nil), 232 201 wish.WithMiddleware(func(next ssh.Handler) ssh.Handler { 233 202 return func(sess ssh.Session) { ··· 245 214 cmd.Env = os.Environ() 246 215 cmd.Env = append(cmd.Env, "SMALLWEB_DISABLE_PLUGINS=true") 247 216 } else { 248 - a, err := app.LoadApp(sess.User(), k.String("dir"), k.String("domain"), slices.Contains(k.Strings("adminApps"), sess.User())) 217 + a, err := app.LoadApp(sess.User(), k.String("dir"), k.String("domain"), k.Bool(fmt.Sprintf("apps.%s.admin", sess.User()))) 249 218 if err != nil { 250 219 fmt.Fprintf(sess, "failed to load app: %v\n", err) 251 220 sess.Exit(1) ··· 391 360 } 392 361 393 362 func (me *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 394 - appname, redirect, ok := lookupApp(r.Host, k.String("domain"), k.StringMap("customDomains")) 363 + appname, redirect, ok := lookupApp(r.Host) 395 364 if !ok { 396 365 w.WriteHeader(http.StatusNotFound) 397 366 w.Write([]byte(fmt.Sprintf("No app found for host %s", r.Host))) ··· 426 395 wk.ServeHTTP(w, r) 427 396 } 428 397 429 - func lookupApp(host string, domain string, customDomains map[string]string) (app string, redirect bool, found bool) { 398 + func lookupApp(domain string) (app string, redirect bool, found bool) { 430 399 // check exact matches first 431 - for key, value := range customDomains { 432 - if value == "*" { 433 - continue 434 - } 435 - 436 - if key == host { 437 - return value, false, true 438 - } 400 + if domain == k.String("domain") { 401 + return "www", true, true 439 402 } 440 403 441 - if host == domain { 442 - return "www", true, true 404 + if strings.HasSuffix(domain, fmt.Sprintf(".%s", k.String("domain"))) { 405 + return strings.TrimSuffix(domain, fmt.Sprintf(".%s", k.String("domain"))), false, true 443 406 } 444 407 445 - // check for subdomains 446 - for key, value := range customDomains { 447 - if value != "*" { 448 - continue 449 - } 450 - 451 - if key == host { 408 + for _, additionalDomain := range k.Strings("additionalDomains") { 409 + if domain == additionalDomain { 452 410 return "www", true, true 453 411 } 454 412 455 - if strings.HasSuffix(host, "."+key) { 456 - return strings.TrimSuffix(host, "."+key), false, true 413 + if strings.HasSuffix(domain, fmt.Sprintf(".%s", additionalDomain)) { 414 + return strings.TrimSuffix(domain, fmt.Sprintf(".%s", additionalDomain)), false, true 457 415 } 458 416 } 459 417 460 - if strings.HasSuffix(host, "."+domain) { 461 - return strings.TrimSuffix(host, "."+domain), false, true 418 + for _, app := range k.MapKeys("apps") { 419 + if slices.Contains(k.Strings(fmt.Sprintf("apps.%s.additionalDomains")), domain) { 420 + return app, false, true 421 + } 462 422 } 463 423 464 424 return "", false, false ··· 472 432 me.mu.Lock() 473 433 defer me.mu.Unlock() 474 434 475 - a, err := app.LoadApp(appname, rootDir, domain, slices.Contains(k.Strings("adminApps"), appname)) 435 + a, err := app.LoadApp(appname, rootDir, domain, k.Bool(fmt.Sprintf("apps.%s.admin", appname))) 476 436 if err != nil { 477 437 return nil, fmt.Errorf("failed to load app: %w", err) 478 438 }
-122
cmd/up_test.go
··· 1 - package cmd 2 - 3 - import ( 4 - "testing" 5 - ) 6 - 7 - func TestLookupApp(t *testing.T) { 8 - type TestCaseWant struct { 9 - app string 10 - redirect bool 11 - ok bool 12 - } 13 - type TestCase struct { 14 - name string 15 - host string 16 - domain string 17 - customDomains map[string]string 18 - want TestCaseWant 19 - } 20 - 21 - cases := []TestCase{ 22 - { 23 - name: "apex domain", 24 - host: "smallweb.run", 25 - domain: "smallweb.run", 26 - customDomains: map[string]string{ 27 - "smallweb.cloud": "cloud", 28 - "custom.smallweb.run": "www", 29 - "pomdtr.me": "*", 30 - }, 31 - want: TestCaseWant{ 32 - app: "www", 33 - redirect: true, 34 - ok: true, 35 - }, 36 - }, 37 - { 38 - name: "subdomain", 39 - host: "example.smallweb.run", 40 - domain: "smallweb.run", 41 - customDomains: map[string]string{ 42 - "smallweb.cloud": "cloud", 43 - "custom.smallweb.run": "www", 44 - "pomdtr.me": "*", 45 - }, 46 - want: TestCaseWant{ 47 - app: "example", 48 - redirect: false, 49 - ok: true, 50 - }, 51 - }, 52 - { 53 - name: "custom subdomain", 54 - host: "custom.smallweb.run", 55 - domain: "smallweb.run", 56 - customDomains: map[string]string{ 57 - "smallweb.cloud": "cloud", 58 - "custom.smallweb.run": "www", 59 - "pomdtr.me": "*", 60 - }, 61 - want: TestCaseWant{ 62 - app: "www", 63 - redirect: false, 64 - ok: true, 65 - }, 66 - }, 67 - { 68 - name: "custom domain exact match", 69 - host: "smallweb.cloud", 70 - domain: "smallweb.run", 71 - customDomains: map[string]string{ 72 - "smallweb.cloud": "cloud", 73 - "custom.smallweb.run": "www", 74 - "pomdtr.me": "*", 75 - }, 76 - want: TestCaseWant{ 77 - app: "cloud", 78 - redirect: false, 79 - ok: true, 80 - }, 81 - }, 82 - { 83 - name: "custom domain wildcard apex domain", 84 - host: "pomdtr.me", 85 - domain: "smallweb.run", 86 - customDomains: map[string]string{ 87 - "smallweb.cloud": "cloud", 88 - "custom.smallweb.run": "www", 89 - "pomdtr.me": "*", 90 - }, 91 - want: TestCaseWant{ 92 - app: "www", 93 - redirect: true, 94 - ok: true, 95 - }, 96 - }, 97 - { 98 - name: "custom domain wildcard subdomain", 99 - host: "example.pomdtr.me", 100 - domain: "smallweb.run", 101 - customDomains: map[string]string{ 102 - "smallweb.cloud": "cloud", 103 - "custom.smallweb.run": "www", 104 - "pomdtr.me": "*", 105 - }, 106 - want: TestCaseWant{ 107 - app: "example", 108 - redirect: false, 109 - ok: true, 110 - }, 111 - }, 112 - } 113 - 114 - for _, c := range cases { 115 - t.Run(c.name, func(t *testing.T) { 116 - app, redirect, ok := lookupApp(c.host, c.domain, c.customDomains) 117 - if app != c.want.app || redirect != c.want.redirect || ok != c.want.ok { 118 - t.Errorf("lookupApp() = %v, %v, %v; want %v, %v, %v", app, redirect, ok, c.want.app, c.want.redirect, c.want.ok) 119 - } 120 - }) 121 - } 122 - }
+8 -3
examples/.smallweb/config.json
··· 1 1 { 2 - "adminApps": [ 3 - "ls" 4 - ] 2 + "authorizedKeys": [ 3 + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJW+GQk0KCvSreL+y3AZdtCu82+13E2eEled+sGRkIEv" 4 + ], 5 + "apps": { 6 + "ls": { 7 + "admin": true 8 + } 9 + } 5 10 }
+39 -8
schemas/config.schema.json
··· 3 3 "type": "object", 4 4 "properties": { 5 5 "domain": { 6 - "description": "Domain name", 6 + "description": "Smallweb name", 7 7 "type": "string" 8 8 }, 9 - "customDomains": { 10 - "description": "Custom domains", 11 - "type": "object", 12 - "additionalProperties": { 9 + "additionalDomains": { 10 + "description": "Additional wildcard domains", 11 + "type": "array", 12 + "items": { 13 13 "type": "string" 14 14 } 15 15 }, 16 - "adminApps": { 17 - "description": "Admin apps", 16 + "authorizedKeys": { 17 + "description": "Authorized SSH keys", 18 18 "type": "array", 19 19 "items": { 20 20 "type": "string" 21 21 } 22 + }, 23 + "apps": { 24 + "type": "object", 25 + "additionalProperties": { 26 + "$ref": "#/definitions/appConfig" 27 + } 28 + } 29 + }, 30 + "definitions": { 31 + "appConfig": { 32 + "type": "object", 33 + "properties": { 34 + "admin": { 35 + "description": "Give the app admin privileges", 36 + "type": "boolean" 37 + }, 38 + "additionalDomains": { 39 + "description": "Additional app domains", 40 + "type": "array", 41 + "items": { 42 + "type": "string" 43 + } 44 + }, 45 + "authorizedKeys": { 46 + "description": "Authorized SSH keys", 47 + "type": "array", 48 + "items": { 49 + "type": "string" 50 + } 51 + } 52 + } 22 53 } 23 54 } 24 - } 55 + }
+2 -4
worker/worker.go
··· 133 133 return args 134 134 } 135 135 136 - authorizedKeysPath := filepath.Join(me.App.RootDir, ".smallweb", "authorized_keys") 137 - 138 136 // if root is not a symlink 139 137 if fi, err := os.Lstat(appDir); err == nil && fi.Mode()&os.ModeSymlink == 0 { 140 138 args = append( 141 139 args, 142 - fmt.Sprintf("--allow-read=%s,%s,%s,%s,%s", appDir, authorizedKeysPath, sandboxPath, deno, npmCache), 140 + fmt.Sprintf("--allow-read=%s,%s,%s,%s", appDir, sandboxPath, deno, npmCache), 143 141 fmt.Sprintf("--allow-write=%s", me.App.DataDir()), 144 142 ) 145 143 ··· 157 155 158 156 args = append( 159 157 args, 160 - fmt.Sprintf("--allow-read=%s,%s,%s,%s,%s,%s", appDir, authorizedKeysPath, target, sandboxPath, deno, npmCache), 158 + fmt.Sprintf("--allow-read=%s,%s,%s,%s,%s", appDir, target, sandboxPath, deno, npmCache), 161 159 fmt.Sprintf("--allow-write=%s,%s", me.App.DataDir(), filepath.Join(target, "data")), 162 160 ) 163 161 return args