Skip to content

Commit 6fc1469

Browse files
authored
feat: Create v1 of handles-server using Go (#13)
Closes #12
1 parent 8bd3f31 commit 6fc1469

21 files changed

+1394
-0
lines changed

.github/workflows/format.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Format
2+
3+
on:
4+
- push
5+
6+
jobs:
7+
golangci-lint:
8+
name: Run golangci-lint
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v3
12+
- uses: actions/setup-go@v4
13+
with:
14+
go-version-file: "go.mod"
15+
- run: go build -v .
16+
- uses: golangci/golangci-lint-action@v3
17+
tidy:
18+
name: Tidy go modules
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v3
22+
- uses: actions/setup-go@v4
23+
with:
24+
go-version-file: "go.mod"
25+
- uses: katexochen/go-tidy-check@v2

.github/workflows/test.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Test
2+
3+
on:
4+
- push
5+
6+
jobs:
7+
test:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v3
11+
- uses: actions/setup-go@v4
12+
with:
13+
go-version-file: "go.mod"
14+
- name: Build
15+
run: "go build -v ."
16+
- name: Test
17+
run: "go test -v ."

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
handles-server

LICENSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Shrink Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Handles Server
2+
3+
A very simple server that verifies Bluesky (atproto) handles using the
4+
[HTTPS well-known Method][atproto/resolution/well-known]; an alternative to
5+
managing many DNS records.
6+
7+
## Implementation
8+
9+
A `handle` is a hostname (e.g: `alice.example.com`) which the server may or may
10+
not be able to provide a Decentralized ID for. A handle is made up of a `domain`
11+
(e.g: `example.com`) and a `username` (e.g: `alice`). A provider
12+
(`ProvidesDecentralizedIDs`) is responsible for getting a Decentralized ID from
13+
a handle.
14+
15+
## Providers
16+
17+
- [x] Postgres
18+
- [x] Memory
19+
- [ ] Google Sheets
20+
- [ ] Filesystem
21+
22+
## Configuration
23+
24+
| Environment Variable | Description | Example |
25+
| -------------------------- | -------------------------------------------------- | -------------------------------------- |
26+
| **`DID_PROVIDER`** | **Required** Name of a supported provider | `postgres` `memory` |
27+
| `REDIRECT_DID_TEMPLATE` | URL template for redirects when a DID is found | `https://bsky.app/profile/{did}` |
28+
| `REDIRECT_HANDLE_TEMPLATE` | URL template for redirects when a DID is not found | `https://example.com/?handle={handle}` |
29+
30+
### `memory` provider
31+
32+
| Environment Variable | Description | Example |
33+
| -------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------- |
34+
| **`MEMORY_DIDS`** | **Required** Comma separated list of handle@did pairs | `alice.example.com@did:plc:example001,bob.example.com@did:plc:example002` |
35+
| **`MEMORY_DOMAINS`** | **Required** Comma separate list of supported domains | `example.com,example.net` |
36+
37+
### `postgres` provider
38+
39+
| Environment Variable | Description | Example |
40+
| ------------------------ | -------------------------------------- | -------------------------------------------- |
41+
| **`DATABASE_URL`** | **Required** Postgres database URL | `postgres://postgres@localhost:5432/handles` |
42+
| `DATABASE_TABLE_DIDS` | Table containing `handle` + `did` rows | `dids` `active_handles` |
43+
| `DATABASE_TABLE_DOMAINS` | Table containing `domain` rows | `domains` `active_domains` |
44+
45+
### URL templates
46+
47+
A string containing zero or more tokens which are replaced when rendering.
48+
49+
| Token | Value | Example(s) |
50+
| ------------------- | ----------------------------------------------- | -------------------------- |
51+
| `{handle}` | Formatted handle from the request | `alice.example.com` |
52+
| `{did}` | Decentralized ID found for the request's handle | `did:plc:example001` ` ` |
53+
| `{handle.domain}` | Top level domain from the handle | `example.com` |
54+
| `{handle.username}` | Username part of the handle | `alice` `bob` |
55+
| `{request.scheme}` | Request's scheme | `https` `http` |
56+
| `{request.host}` | Request's host | `alice.example.com` |
57+
| `{request.path}` | Path included in the request | `/hello-world` ` ` |
58+
| `{request.query}` | Query included in the request | `greeting=Hello+World` ` ` |
59+
60+
[atproto/resolution/well-known]: https://atproto.com/specs/handle#handle-resolution

Taskfile.dist.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
version: "3"
2+
3+
silent: true
4+
5+
dotenv:
6+
- "Taskfile.env.example"
7+
- "Taskfile.env"
8+
9+
tasks:
10+
default:
11+
cmds:
12+
- task --list
13+
14+
run:
15+
desc: "Run local development copy of the server"
16+
cmds:
17+
- go run .
18+
19+
test:
20+
desc: "Run test suite"
21+
cmds:
22+
- go test . -v
23+
24+
format:
25+
desc: "Format code"
26+
deps:
27+
- golang-ci
28+
cmds:
29+
- go fmt .
30+
- golangci-lint run
31+
- go mod tidy
32+
33+
golang-ci:
34+
status:
35+
- golangci-lint version && exit 0
36+
cmds:
37+
- >
38+
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh |
39+
sh -s -- -b $(go env GOPATH)/bin v1.53.3

Taskfile.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
PORT=8888
2+
LOG_LEVEL=debug
3+
DATABASE_URL="postgres://postgres@localhost:5432/handles"
4+
DID_PROVIDER="postgres"

config.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"log/slog"
6+
"reflect"
7+
8+
"github.com/caarlos0/env/v11"
9+
"github.com/jackc/pgx/v5/pgxpool"
10+
)
11+
12+
type Config struct {
13+
Host string `env:"HOST" envDefault:"localhost"`
14+
Port string `env:"PORT" envDefault:"80"`
15+
LogLevel slog.Level `env:"LOG_LEVEL" envDefault:"error"`
16+
17+
RedirectDIDTemplate URLTemplate `env:"REDIRECT_DID_TEMPLATE" envDefault:"https://bsky.app/profile/{did}"`
18+
RedirectHandleTemplate URLTemplate `env:"REDIRECT_HANDLE_TEMPLATE" envDefault:"https://{handle.domain}?handle={handle}"`
19+
20+
Postgres *pgxpool.Config `env:"DATABASE_URL"`
21+
PostgresDidsTable string `env:"DATABASE_TABLE_DIDS" envDefault:"dids"`
22+
PostgresDomainsTable string `env:"DATABASE_TABLE_DOMAINS" envDefault:"domains"`
23+
24+
MemoryDids map[string]string `env:"MEMORY_DIDS" envKeyValSeparator:"@"`
25+
MemoryDomains []string `env:"MEMORY_DOMAINS"`
26+
27+
Provider ProvidesDecentralizedIDs `env:"DID_PROVIDER,required"`
28+
}
29+
30+
func ConfigFromEnvironment() (Config, error) {
31+
config := Config{}
32+
33+
err := env.ParseWithOptions(&config, env.Options{
34+
FuncMap: map[reflect.Type]env.ParserFunc{
35+
reflect.TypeFor[slog.Level](): func(v string) (interface{}, error) {
36+
var level slog.Level
37+
return level, level.UnmarshalText([]byte(v))
38+
},
39+
reflect.TypeFor[pgxpool.Config](): func(v string) (interface{}, error) {
40+
config, err := pgxpool.ParseConfig(v)
41+
return *config, err
42+
},
43+
reflect.TypeFor[ProvidesDecentralizedIDs](): func(v string) (interface{}, error) {
44+
switch v {
45+
case "postgres":
46+
if config.Postgres == nil {
47+
return &PostgresHandles{}, errors.New("a database connection (`DATABASE_URL`) is required to use the postgres provider")
48+
}
49+
50+
return NewPostgresHandlesProvider(
51+
config.Postgres,
52+
config.PostgresDidsTable,
53+
config.PostgresDomainsTable,
54+
)
55+
case "memory":
56+
if config.MemoryDids == nil || config.MemoryDomains == nil {
57+
return nil, errors.New("a map of Decentralized IDs (`MEMORY_DIDS`) and domains (`MEMORY_DOMAINS`) is required to use the memory provider")
58+
}
59+
60+
dids := make(MapOfDids)
61+
62+
for handle, did := range config.MemoryDids {
63+
dids[Hostname(handle)] = DecentralizedID(did)
64+
}
65+
66+
domains := make(MapOfDomains)
67+
68+
for _, domain := range config.MemoryDomains {
69+
domains[Domain(domain)] = true
70+
}
71+
72+
provider := NewInMemoryProvider(dids, domains)
73+
return provider, nil
74+
default:
75+
return nil, errors.New("no valid provider of decentralized IDs specified")
76+
}
77+
},
78+
},
79+
})
80+
81+
if err != nil {
82+
return Config{}, err
83+
}
84+
85+
return config, nil
86+
}

go.mod

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
module handles-server
2+
3+
go 1.23.1
4+
5+
require (
6+
github.com/caarlos0/env/v11 v11.3.1
7+
github.com/gin-gonic/gin v1.10.0
8+
github.com/jackc/pgx/v5 v5.7.2
9+
github.com/samber/slog-gin v1.14.1
10+
github.com/stretchr/testify v1.10.0
11+
)
12+
13+
require (
14+
github.com/bytedance/sonic v1.11.9 // indirect
15+
github.com/bytedance/sonic/loader v0.1.1 // indirect
16+
github.com/cloudwego/base64x v0.1.4 // indirect
17+
github.com/cloudwego/iasm v0.2.0 // indirect
18+
github.com/davecgh/go-spew v1.1.1 // indirect
19+
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
20+
github.com/gin-contrib/sse v0.1.0 // indirect
21+
github.com/go-playground/locales v0.14.1 // indirect
22+
github.com/go-playground/universal-translator v0.18.1 // indirect
23+
github.com/go-playground/validator/v10 v10.22.0 // indirect
24+
github.com/goccy/go-json v0.10.3 // indirect
25+
github.com/google/uuid v1.6.0 // indirect
26+
github.com/jackc/pgpassfile v1.0.0 // indirect
27+
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
28+
github.com/jackc/puddle/v2 v2.2.2 // indirect
29+
github.com/json-iterator/go v1.1.12 // indirect
30+
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
31+
github.com/leodido/go-urn v1.4.0 // indirect
32+
github.com/mattn/go-isatty v0.0.20 // indirect
33+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
34+
github.com/modern-go/reflect2 v1.0.2 // indirect
35+
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
36+
github.com/pmezard/go-difflib v1.0.0 // indirect
37+
github.com/rogpeppe/go-internal v1.13.1 // indirect
38+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
39+
github.com/ugorji/go/codec v1.2.12 // indirect
40+
go.opentelemetry.io/otel v1.29.0 // indirect
41+
go.opentelemetry.io/otel/trace v1.29.0 // indirect
42+
golang.org/x/arch v0.8.0 // indirect
43+
golang.org/x/crypto v0.31.0 // indirect
44+
golang.org/x/net v0.26.0 // indirect
45+
golang.org/x/sync v0.10.0 // indirect
46+
golang.org/x/sys v0.28.0 // indirect
47+
golang.org/x/text v0.21.0 // indirect
48+
google.golang.org/protobuf v1.34.2 // indirect
49+
gopkg.in/yaml.v3 v3.0.1 // indirect
50+
)

0 commit comments

Comments
 (0)