Skip to content

Commit

Permalink
switch to bearer token auth
Browse files Browse the repository at this point in the history
  • Loading branch information
pomdtr committed Sep 2, 2024
1 parent fd71429 commit b5e1620
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 23 deletions.
14 changes: 11 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,21 @@ func NewCmdRoot(version string) *cobra.Command {
"host": "127.0.0.1",
"dir": "~/smallweb",
"domain": "localhost",
"tokens": []string{},
"env": map[string]string{
"DENO_TLS_CA_STORE": "system",
},
}, "")

envProvider := env.Provider("SMALLWEB_", ".", func(s string) string {
return strings.Replace(strings.ToLower(
strings.TrimPrefix(s, "SMALLWEB_")), "_", ".", -1)
envProvider := env.ProviderWithValue("SMALLWEB_", ".", func(s, v string) (string, interface{}) {
key := strings.Replace(strings.ToLower(strings.TrimPrefix(s, "MYVAR_")), "_", ".", -1)
// If there is a space in the value, split the value into a slice by the space.
if strings.Contains(v, ":") {
return key, strings.Split(v, ":")
}

// Otherwise, return the plain string.
return key, v
})

fileProvider := file.Provider(flags.config)
Expand Down Expand Up @@ -155,6 +162,7 @@ func NewCmdRoot(version string) *cobra.Command {
cmd.AddCommand(NewCmdCron())
cmd.AddCommand(NewCmdUpgrade())
cmd.AddCommand(NewCmdInit())
cmd.AddCommand(NewCmdToken())

path := os.Getenv("PATH")
for _, dir := range filepath.SplitList(path) {
Expand Down
48 changes: 48 additions & 0 deletions cmd/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package cmd

import (
"crypto/rand"
"fmt"
"os"

"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
)

func generateToken(n int) (string, error) {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
bytes := make([]byte, n)
if _, err := rand.Read(bytes); err != nil {
return "", err
}

for i := 0; i < n; i++ {
bytes[i] = letters[bytes[i]%byte(len(letters))]
}

return string(bytes), nil
}

func NewCmdToken() *cobra.Command {
cmd := &cobra.Command{
Use: "token",
Short: "Generate a random token",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
token, err := generateToken(16)
if err != nil {
return fmt.Errorf("failed to generate token: %w", err)
}

if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Println(token)
} else {
fmt.Print(token)
}

return nil
},
}

return cmd
}
49 changes: 38 additions & 11 deletions cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,39 @@ func StripAnsi(b []byte) []byte {
return re.ReplaceAll(b, nil)
}

func basicAuth(h http.Handler, user, pass string) http.Handler {
func authMiddleware(h http.Handler, tokens []string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, p, ok := r.BasicAuth()
if !ok || u != user || p != pass {
token, _, ok := r.BasicAuth()
if ok {
for _, t := range tokens {
if token == t {
h.ServeHTTP(w, r)
return
}
}

w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
http.Error(w, "Unauthorized", http.StatusForbidden)
return
}

h.ServeHTTP(w, r)
authorization := r.Header.Get("Authorization")
if authorization != "" {
token := strings.TrimPrefix(authorization, "Bearer ")
for _, t := range tokens {
if token == t {
h.ServeHTTP(w, r)
return
}
}

w.Header().Set("WWW-Authenticate", `Bearer realm="Restricted"`)
http.Error(w, "Unauthorized", http.StatusForbidden)
return
}

w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
})
}

Expand Down Expand Up @@ -77,9 +100,8 @@ func NewCmdUp() *cobra.Command {
FileSystem: webdav.Dir(utils.ExpandTilde(k.String("dir"))),
LockSystem: webdav.NewMemLS(),
}

if k.String("auth.username") != "" || k.String("auth.password") != "" {
handler = basicAuth(handler, k.String("auth.username"), k.String("auth.password"))
if k.String("tokens") != "" {
handler = authMiddleware(handler, k.Strings("tokens"))
}

handler.ServeHTTP(w, r)
Expand All @@ -88,8 +110,8 @@ func NewCmdUp() *cobra.Command {

if r.Host == fmt.Sprintf("cli.%s", domain) {
var handler http.Handler = cliHandler
if k.String("auth.username") != "" || k.String("auth.password") != "" {
handler = basicAuth(handler, k.String("auth.username"), k.String("auth.password"))
if k.String("tokens") != "" {
handler = authMiddleware(handler, k.Strings("tokens"))
}

handler.ServeHTTP(w, r)
Expand Down Expand Up @@ -144,7 +166,7 @@ func NewCmdUp() *cobra.Command {

var handler http.Handler = a
if a.Config.Private {
handler = basicAuth(a, k.String("auth.username"), k.String("auth.password"))
handler = authMiddleware(a, k.Strings("tokens"))
}
handler.ServeHTTP(w, r)
}),
Expand Down Expand Up @@ -217,6 +239,11 @@ func NewCmdUp() *cobra.Command {
}

var cliHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/favicon.ico" {
w.WriteHeader(http.StatusNotFound)
return
}

executable, err := os.Executable()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
Expand Down
8 changes: 5 additions & 3 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
# Guides

- [Routing](./guides/routing.md)
- [Hosting Servers](./guides/server.md)
- [CLi Commands](./guides/commands.md)
- [HTTP Servers](./guides/server.md)
- [Cli Commands](./guides/commands.md)
- [Environment Variables](./guides/env.md)
- [Cron Jobs](./guides/cron.md)
- [Cron Tasks](./guides/cron.md)
- [Plugins](./guides/plugins.md)
- [Templates](./guides/templates.md)
- [File Sync](./guides/file-sync.md)
- [App Sandbox](./guides/sandbox.md)

# Hosting

Expand Down
37 changes: 37 additions & 0 deletions docs/src/guides/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Private Apps

You can automatically protects private apps behind a login prompt. In order to achieve this, you'll need to:

1. Use the `smallweb token` command to generate a new token.

```console
$ smallweb token
SF7RZt9shD6UnUcl
```

1. Add an `tokens` property to your global config.

```json
// ~/.config/smallweb/config.json
{
"tokens": ["SF7RZt9shD6UnUcl"]
}
```

1. Set the private field to true in your app's config.

```json
// ~/smallweb/private-app/smallweb.json
{
"private": true
}
```

The next time you'll try to access the app, you'll be prompted with a basic auth dialog.

Use the token as the username, and leave the password empty.

Your tokens are also used to protect internal services that smallweb provides, such as:

- `webdav.<domain>`: A webdav server allowing you to access your files.
- `cli.<domain>`: A web interface to run cli commands.
23 changes: 21 additions & 2 deletions docs/src/guides/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
To add a cli command to your app, you'll need to add a `run` method to your app's default export.

```ts
// File: ~/smallweb/cli-example/main.ts
// File: ~/smallweb/custom-command/main.ts
export default {
run(args: string[]) {
console.log("Hello world");
Expand All @@ -14,7 +14,7 @@ export default {
Use `smallweb run` to execute the command:

```console
$ smallweb run cli-example
$ smallweb run custom-command
Hello world
```

Expand All @@ -38,5 +38,24 @@ export default {
await command.parse(args);
}
}
```

## Accessing cli commands from your browser

Smallweb automatically serves its own cli commands at `<cli>.<domain>`.

Positional args are mapped to path segments, and flags are mapped to query parameters.

- `smallweb cron ls --json` becomes `https://cli.<domain>/cron/ls?json`

It also allows you to access your app commands and crons:

- `smallweb run custom-command` becomes `https://cli.<domain>/run/custom-command`
- `smallweb cron trigger daily-task` becomes `https://cli.<domain>/cron/trigger/daily-task`

You can specify stdin by sending a POST request with the body as the input.

```sh
# stdin will be "Hello world"
curl -X POST https://cli.<domain>/run/custom-command --data "Hello world"
```
2 changes: 1 addition & 1 deletion docs/src/guides/cron.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Cron Jobs
# Cron Tasks

You can register configure cron tasks from your `smallweb.json[c]` or the `smallweb` field from your `deno.json[c]`.

Expand Down
9 changes: 9 additions & 0 deletions docs/src/guides/sandbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# App Sandbox

Smallweb apps have access to:

- read and write access to their own directory, and the deno cache directory.
- access to the network, to make HTTP requests.
- access to the env files defined in the global config and in the `.env` file in the app directory.

This sandbox protects the host system from malicious code, and ensures that apps can only access the resources they need.
13 changes: 11 additions & 2 deletions docs/src/guides/templates.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
# Templates

Smallweb comes with a list of templates to help you get started with your project. You can use the `smallweb init` command to create a new project from a template.
Smallweb comes with a list of templates to help you get started with your project. You can use the `smallweb init` command to create a new project.

```sh
# create a new project in the current directory
smallweb init
# create a new project in a specific directory
smallweb init <directory>
```

You can also specify custom a template to use:

```sh
smallweb init --template pomdtr/smallweb-template-http
```

Any github repository can be used as a template. View a list of the available templates [here](https://github.com/topic/smallweb-template).

To create your own template, just add the `smallweb-template` topic to your repository.
To create your own template, just add the `smallweb-template` topic to your repository, and it will be automatically added to the list of available templates.
22 changes: 21 additions & 1 deletion docs/src/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -672,12 +672,32 @@ smallweb service uninstall [flags]
-c, --config string config file (default "/Users/pomdtr/.config/smallweb/config.json")
```

## smallweb types

Print smallweb types

```
smallweb types [flags]
```

### Options

```
-h, --help help for types
```

### Options inherited from parent commands

```
-c, --config string config file (default "/Users/pomdtr/.config/smallweb/config.json")
```

## smallweb up

Start the smallweb evaluation server

```
smallweb up <domain> [flags]
smallweb up [flags]
```

### Options
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sethvargo/go-password v0.3.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.9.0 // indirect
golang.org/x/sys v0.22.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
Expand Down

0 comments on commit b5e1620

Please sign in to comment.