+2
-3
cmd/crons.go
+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
+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
+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
+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
+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
+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
-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
+8
-3
examples/.smallweb/config.json
+39
-8
schemas/config.schema.json
+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
+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