mirror of
https://github.com/Maronato/go-finger.git
synced 2025-03-15 08:44:46 +00:00
Compare commits
No commits in common. "main" and "v1.1.0" have entirely different histories.
14 changed files with 855 additions and 999 deletions
|
@ -27,8 +27,6 @@ COPY Makefile ./
|
|||
COPY main.go ./
|
||||
COPY cmd cmd
|
||||
COPY internal internal
|
||||
COPY webfingers webfingers
|
||||
COPY handler handler
|
||||
|
||||
# Build it
|
||||
RUN --mount=type=cache,target=/tmp/.go-build-cache \
|
||||
|
|
2
Makefile
2
Makefile
|
@ -9,7 +9,7 @@ build:
|
|||
test:
|
||||
go test -v ./...
|
||||
|
||||
run:
|
||||
serve:
|
||||
go run main.go serve
|
||||
|
||||
clean:
|
||||
|
|
134
README.md
134
README.md
|
@ -1,6 +1,6 @@
|
|||
# Finger
|
||||
|
||||
Webfinger handler / standalone server written in Go.
|
||||
Webfinger server written in Go.
|
||||
|
||||
## Features
|
||||
- 🍰 Easy YAML configuration
|
||||
|
@ -8,58 +8,7 @@ Webfinger handler / standalone server written in Go.
|
|||
- ⚡️ Sub millisecond responses at 10,000 request per second
|
||||
- 🐳 10MB Docker image
|
||||
|
||||
## In your existing server
|
||||
|
||||
To use Finger in your existing server, download the package as a dependency:
|
||||
|
||||
```bash
|
||||
go get git.maronato.dev/maronato/finger@latest
|
||||
```
|
||||
|
||||
Then, use it as a regular `http.Handler`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.maronato.dev/maronato/finger/handler"
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create the webfingers map that will be served by the handler
|
||||
fingers, err := webfingers.NewWebFingers(
|
||||
// Pass a map of your resources (Subject key followed by it's properties and links)
|
||||
// the syntax is the same as the fingers.yml file (see below)
|
||||
webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"name": "Example User",
|
||||
},
|
||||
},
|
||||
// Optionally, pass a map of URN aliases (see urns.yml for more)
|
||||
// If nil is provided, no aliases will be used
|
||||
webfingers.URNAliases{
|
||||
"name": "http://schema.org/name",
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
// Then use the handler as a regular http.Handler
|
||||
mux.Handle("/.well-known/webfinger", handler.WebfingerHandler(fingers))
|
||||
|
||||
log.Fatal(http.ListenAndServe("localhost:8080", mux))
|
||||
}
|
||||
```
|
||||
|
||||
## As a standalone server
|
||||
|
||||
If you don't have a server, Finger can also serve itself. You can install it via `go install` or use the Docker image.
|
||||
## Install
|
||||
|
||||
Via `go install`:
|
||||
|
||||
|
@ -70,10 +19,8 @@ go install git.maronato.dev/maronato/finger@latest
|
|||
Via Docker:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
--name finger \
|
||||
-p 8080:8080 \
|
||||
-v ${PWD}/fingers.yml:/app/fingers.yml \
|
||||
docker run --name finger /
|
||||
-p 8080:8080 /
|
||||
git.maronato.dev/maronato/finger
|
||||
```
|
||||
|
||||
|
@ -138,14 +85,15 @@ https://example.com/user/charlie:
|
|||
"rel": "openid",
|
||||
"href": "https://sso.example.com/"
|
||||
},
|
||||
{
|
||||
"rel": "name",
|
||||
"href": "Alice Doe"
|
||||
},
|
||||
{
|
||||
"rel": "http://webfinger.net/rel/profile-page",
|
||||
"href": "https://example.com/user/alice"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"name": "Alice Doe"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
@ -159,13 +107,14 @@ https://example.com/user/charlie:
|
|||
"subject": "acct:bob@example.com",
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://openid.net/specs/connect/1.0/issuer",
|
||||
"rel": "name",
|
||||
"href": "Bob Foo"
|
||||
},
|
||||
{
|
||||
"rel": "openid",
|
||||
"href": "https://sso.example.com/"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"http://schema.org/name": "Bob Foo"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
@ -179,56 +128,21 @@ https://example.com/user/charlie:
|
|||
"subject": "https://example.com/user/charlie",
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://webfinger.net/rel/profile-page",
|
||||
"rel": "name",
|
||||
"href": "Charlie Baz"
|
||||
},
|
||||
{
|
||||
"rel": "profile",
|
||||
"href": "https://example.com/user/charlie"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"http://schema.org/name": "Charlie Baz"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Commands
|
||||
|
||||
Finger exposes two commands: `serve` and `healthcheck`. `serve` is the default command and starts the server. `healthcheck` is used by the Docker healthcheck to check if the server is up.
|
||||
|
||||
## Configs
|
||||
Here are the config options available. You can change them via command line flags or environment variables:
|
||||
|
||||
| CLI flag | Env variable | Default | Description |
|
||||
| ------------------- | ---------------- | -------------------------------------- | -------------------------------------- |
|
||||
| `-p, --port` | `WF_PORT` | `8080` | Port where the server listens to |
|
||||
| `-h, --host` | `WF_HOST` | `localhost` (`0.0.0.0` when in Docker) | Host where the server listens to |
|
||||
| `-f, --finger-file` | `WF_FINGER_FILE` | `fingers.yml` | Path to the webfingers definition file |
|
||||
| `-u, --urn-file` | `WF_URN_FILE` | `urns.yml` | Path to the URNs alias file |
|
||||
| `-d, --debug` | `WF_DEBUG` | `false` | Enable debug logging |
|
||||
|
||||
### Docker config
|
||||
If you're using the Docker image, you can mount your `fingers.yml` file to `/app/fingers.yml` and the `urns.yml` to `/app/urns.yml`.
|
||||
|
||||
To run the docker image with flags or a different command, specify the command followed by the flags:
|
||||
```bash
|
||||
# Start the server on port 3030 in debug mode with a different fingers file
|
||||
docker run git.maronato.dev/maronato/finger serve --port 3030 --debug --finger-file /app/my-fingers.yml
|
||||
|
||||
# or run a healthcheck on a different finger container
|
||||
docker run git.maronato.dev/maronato/finger healthcheck --host otherhost --port 3030
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
You need to have [Go](https://golang.org/) installed to build the project.
|
||||
|
||||
Clone the repo and run `make build` to build the binary. You can then run `./finger serve` to start the server.
|
||||
|
||||
A few other commands are:
|
||||
- `make run` to run the server
|
||||
- `make test` to run the tests
|
||||
- `make lint` to run the linter
|
||||
- `make clean` to clean the build files
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
| CLI flag | Env variable | Default | Description |
|
||||
| -------- | ------------ | ------- | ----------- |
|
||||
| fdsfds | gsfgfs | fgfsdgf | gdfsgdf |
|
||||
|
|
10
cmd/serve.go
10
cmd/serve.go
|
@ -6,9 +6,9 @@ import (
|
|||
"os"
|
||||
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/fingerreader"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/internal/server"
|
||||
"git.maronato.dev/maronato/finger/internal/webfinger"
|
||||
"github.com/peterbourgon/ff/v4"
|
||||
)
|
||||
|
||||
|
@ -25,21 +25,21 @@ func newServerCmd(cfg *config.Config) *ff.Command {
|
|||
ctx = log.WithLogger(ctx, l)
|
||||
|
||||
// Read the webfinger files
|
||||
r := fingerreader.NewFingerReader()
|
||||
r := webfinger.NewFingerReader()
|
||||
err := r.ReadFiles(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading finger files: %w", err)
|
||||
}
|
||||
|
||||
fingers, err := r.ReadFingerFile(ctx)
|
||||
webfingers, err := r.ReadFingerFile(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing finger files: %w", err)
|
||||
}
|
||||
|
||||
l.Info(fmt.Sprintf("Loaded %d webfingers", len(fingers)))
|
||||
l.Info(fmt.Sprintf("Loaded %d webfingers", len(webfingers)))
|
||||
|
||||
// Start the server
|
||||
if err := server.StartServer(ctx, cfg, fingers); err != nil {
|
||||
if err := server.StartServer(ctx, cfg, webfingers); err != nil {
|
||||
return fmt.Errorf("error running server: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
package fingerreader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type FingerReader struct {
|
||||
URNSFile []byte
|
||||
FingersFile []byte
|
||||
}
|
||||
|
||||
func NewFingerReader() *FingerReader {
|
||||
return &FingerReader{}
|
||||
}
|
||||
|
||||
func (f *FingerReader) ReadFiles(cfg *config.Config) error {
|
||||
// Read URNs file
|
||||
file, err := os.ReadFile(cfg.URNPath)
|
||||
if err != nil {
|
||||
// If the file does not exist and the path is the default, set the URNs to an empty map
|
||||
if os.IsNotExist(err) && cfg.URNPath == config.DefaultURNPath {
|
||||
f.URNSFile = []byte("")
|
||||
} else {
|
||||
return fmt.Errorf("error opening URNs file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
f.URNSFile = file
|
||||
|
||||
// Read fingers file
|
||||
file, err = os.ReadFile(cfg.FingerPath)
|
||||
if err != nil {
|
||||
// If the file does not exist and the path is the default, set the fingers to an empty map
|
||||
if os.IsNotExist(err) && cfg.FingerPath == config.DefaultFingerPath {
|
||||
f.FingersFile = []byte("")
|
||||
} else {
|
||||
return fmt.Errorf("error opening fingers file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
f.FingersFile = file
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FingerReader) ReadFingerFile(ctx context.Context) (webfingers.WebFingers, error) {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
urnAliases := make(webfingers.URNAliases)
|
||||
resources := make(webfingers.Resources)
|
||||
|
||||
// Parse the URNs file
|
||||
if err := yaml.Unmarshal(f.URNSFile, &urnAliases); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling URNs file: %w", err)
|
||||
}
|
||||
|
||||
// The URNs file must be a map of strings to valid URLs
|
||||
for _, v := range urnAliases {
|
||||
if _, err := url.ParseRequestURI(v); err != nil {
|
||||
return nil, fmt.Errorf("error parsing URN URIs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
l.Debug("URNs file parsed successfully", slog.Int("number", len(urnAliases)), slog.Any("data", urnAliases))
|
||||
|
||||
// Parse the fingers file
|
||||
if err := yaml.Unmarshal(f.FingersFile, &resources); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling fingers file: %w", err)
|
||||
}
|
||||
|
||||
l.Debug("Fingers file parsed successfully", slog.Int("number", len(resources)), slog.Any("data", resources))
|
||||
|
||||
// Parse raw data
|
||||
fingers, err := webfingers.NewWebFingers(resources, urnAliases)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing raw fingers: %w", err)
|
||||
}
|
||||
|
||||
return fingers, nil
|
||||
}
|
|
@ -1,242 +0,0 @@
|
|||
package fingerreader_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/fingerreader"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
)
|
||||
|
||||
func newTempFile(t *testing.T, content string) (name string, remove func()) {
|
||||
t.Helper()
|
||||
|
||||
f, err := os.CreateTemp("", "finger-test")
|
||||
if err != nil {
|
||||
t.Fatalf("error creating temp file: %v", err)
|
||||
}
|
||||
|
||||
_, err = f.WriteString(content)
|
||||
if err != nil {
|
||||
t.Fatalf("error writing to temp file: %v", err)
|
||||
}
|
||||
|
||||
return f.Name(), func() {
|
||||
err = os.Remove(f.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("error removing temp file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFingerReader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := fingerreader.NewFingerReader()
|
||||
|
||||
if f == nil {
|
||||
t.Errorf("NewFingerReader() = %v, want: %v", f, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFingerReader_ReadFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
urnsContent string
|
||||
fingersContent string
|
||||
useURNFile bool
|
||||
useFingerFile bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "reads files",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
useURNFile: true,
|
||||
useFingerFile: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "errors on missing URNs file",
|
||||
urnsContent: "invalid",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
useURNFile: false,
|
||||
useFingerFile: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on missing fingers file",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "invalid",
|
||||
useFingerFile: false,
|
||||
useURNFile: true,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.NewConfig()
|
||||
|
||||
urnsFileName, urnsCleanup := newTempFile(t, tc.urnsContent)
|
||||
defer urnsCleanup()
|
||||
|
||||
fingersFileName, fingersCleanup := newTempFile(t, tc.fingersContent)
|
||||
defer fingersCleanup()
|
||||
|
||||
if !tc.useURNFile {
|
||||
cfg.URNPath = "invalid"
|
||||
} else {
|
||||
cfg.URNPath = urnsFileName
|
||||
}
|
||||
|
||||
if !tc.useFingerFile {
|
||||
cfg.FingerPath = "invalid"
|
||||
} else {
|
||||
cfg.FingerPath = fingersFileName
|
||||
}
|
||||
|
||||
f := fingerreader.NewFingerReader()
|
||||
|
||||
err := f.ReadFiles(cfg)
|
||||
if err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Errorf("ReadFiles() error = %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
} else if tc.wantErr {
|
||||
t.Errorf("ReadFiles() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(f.URNSFile, []byte(tc.urnsContent)) {
|
||||
t.Errorf("ReadFiles() URNsFile = %v, want: %v", f.URNSFile, tc.urnsContent)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(f.FingersFile, []byte(tc.fingersContent)) {
|
||||
t.Errorf("ReadFiles() FingersFile = %v, want: %v", f.FingersFile, tc.fingersContent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFingerFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
urnsContent string
|
||||
fingersContent string
|
||||
wantURN webfingers.URNAliases
|
||||
wantFinger webfingers.Resources
|
||||
returns webfingers.WebFingers
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "reads files",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
wantURN: webfingers.URNAliases{
|
||||
"name": "https://schema/name",
|
||||
"profile": "https://schema/profile",
|
||||
},
|
||||
wantFinger: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"name": "John Doe",
|
||||
},
|
||||
},
|
||||
returns: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "John Doe",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "uses custom URNs",
|
||||
urnsContent: "favorite_food: https://schema/favorite_food",
|
||||
fingersContent: "user@example.com:\n favorite_food: Apple",
|
||||
wantURN: webfingers.URNAliases{
|
||||
"favorite_food": "https://schema/favorite_food",
|
||||
},
|
||||
wantFinger: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"https://schema/favorite_food": "Apple",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid URNs file",
|
||||
urnsContent: "invalid",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid fingers file",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid URNs values",
|
||||
urnsContent: "name: invalid",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid fingers values",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "invalid:\n name: John Doe",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
cfg := config.NewConfig()
|
||||
l := log.NewLogger(&strings.Builder{}, cfg)
|
||||
|
||||
ctx = log.WithLogger(ctx, l)
|
||||
|
||||
f := fingerreader.NewFingerReader()
|
||||
|
||||
f.FingersFile = []byte(tc.fingersContent)
|
||||
f.URNSFile = []byte(tc.urnsContent)
|
||||
|
||||
got, err := f.ReadFingerFile(ctx)
|
||||
if err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Errorf("ReadFingerFile() error = %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
} else if tc.wantErr {
|
||||
t.Errorf("ReadFingerFile() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
|
||||
if tc.returns != nil && !reflect.DeepEqual(got, tc.returns) {
|
||||
t.Errorf("ReadFingerFile() got = %v, want: %v", got, tc.returns)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -8,11 +8,10 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.maronato.dev/maronato/finger/handler"
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/internal/middleware"
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
"git.maronato.dev/maronato/finger/internal/webfinger"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
|
@ -34,17 +33,20 @@ const (
|
|||
RequestTimeout = 7 * 24 * time.Hour
|
||||
)
|
||||
|
||||
func StartServer(ctx context.Context, cfg *config.Config, fingers webfingers.WebFingers) error {
|
||||
func StartServer(ctx context.Context, cfg *config.Config, webfingers webfinger.WebFingers) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
// Create the server mux
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/.well-known/webfinger", handler.WebfingerHandler(fingers))
|
||||
mux.Handle("/.well-known/webfinger", WebfingerHandler(cfg, webfingers))
|
||||
mux.Handle("/healthz", HealthCheckHandler(cfg))
|
||||
|
||||
// Create a new server
|
||||
srv := &http.Server{
|
||||
Addr: cfg.GetAddr(),
|
||||
BaseContext: func(_ net.Listener) context.Context {
|
||||
return ctx
|
||||
},
|
||||
Handler: middleware.RequestLogger(
|
||||
middleware.Recoverer(
|
||||
http.TimeoutHandler(mux, RequestTimeout, "request timed out"),
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/internal/server"
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
"git.maronato.dev/maronato/finger/internal/webfinger"
|
||||
)
|
||||
|
||||
func getPortGenerator() func() int {
|
||||
|
@ -96,8 +96,8 @@ func TestStartServer(t *testing.T) {
|
|||
cfg.Port = fmt.Sprint(portGenerator())
|
||||
|
||||
resource := "acct:user@example.com"
|
||||
fingers := webfingers.WebFingers{
|
||||
resource: &webfingers.WebFinger{
|
||||
webfingers := webfinger.WebFingers{
|
||||
resource: &webfinger.WebFinger{
|
||||
Subject: resource,
|
||||
Properties: map[string]string{
|
||||
"http://webfinger.net/rel/name": "John Doe",
|
||||
|
@ -107,7 +107,7 @@ func TestStartServer(t *testing.T) {
|
|||
|
||||
go func() {
|
||||
// Start the server
|
||||
err := server.StartServer(ctx, cfg, fingers)
|
||||
err := server.StartServer(ctx, cfg, webfingers)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ func TestStartServer(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the response body
|
||||
fingerGot := &webfingers.WebFinger{}
|
||||
fingerGot := &webfinger.WebFinger{}
|
||||
|
||||
// Decode the response body
|
||||
if err := json.NewDecoder(resp.Body).Decode(fingerGot); err != nil {
|
||||
|
@ -148,7 +148,7 @@ func TestStartServer(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the response body
|
||||
fingerWant := fingers[resource]
|
||||
fingerWant := webfingers[resource]
|
||||
|
||||
if !reflect.DeepEqual(fingerGot, fingerWant) {
|
||||
t.Errorf("expected %v, got %v", fingerWant, fingerGot)
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
package handler
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/internal/webfinger"
|
||||
)
|
||||
|
||||
func WebfingerHandler(fingers webfingers.WebFingers) http.Handler {
|
||||
func WebfingerHandler(_ *config.Config, webfingers webfinger.WebFingers) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
// Only handle GET requests
|
||||
if r.Method != http.MethodGet {
|
||||
l.Debug("Method not allowed")
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
|
||||
return
|
||||
|
@ -22,14 +28,16 @@ func WebfingerHandler(fingers webfingers.WebFingers) http.Handler {
|
|||
// Get the resource
|
||||
resource := q.Get("resource")
|
||||
if resource == "" {
|
||||
l.Debug("No resource provided")
|
||||
http.Error(w, "No resource provided", http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Get and validate resource
|
||||
finger, ok := fingers[resource]
|
||||
finger, ok := webfingers[resource]
|
||||
if !ok {
|
||||
l.Debug("Resource not found")
|
||||
http.Error(w, "Resource not found", http.StatusNotFound)
|
||||
|
||||
return
|
||||
|
@ -40,9 +48,12 @@ func WebfingerHandler(fingers webfingers.WebFingers) http.Handler {
|
|||
|
||||
// Write the response
|
||||
if err := json.NewEncoder(w).Encode(finger); err != nil {
|
||||
l.Debug("Error encoding json")
|
||||
http.Error(w, "Error encoding json", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
l.Debug("Webfinger request successful")
|
||||
})
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package handler_test
|
||||
package server_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -10,19 +10,19 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.maronato.dev/maronato/finger/handler"
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
"git.maronato.dev/maronato/finger/internal/server"
|
||||
"git.maronato.dev/maronato/finger/internal/webfinger"
|
||||
)
|
||||
|
||||
func TestWebfingerHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fingers := webfingers.WebFingers{
|
||||
webfingers := webfinger.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Links: []webfingers.Link{
|
||||
Links: []webfinger.Link{
|
||||
{
|
||||
Rel: "http://webfinger.net/rel/profile-page",
|
||||
Href: "https://example.com/user",
|
||||
|
@ -104,7 +104,7 @@ func TestWebfingerHandler(t *testing.T) {
|
|||
w := httptest.NewRecorder()
|
||||
|
||||
// Create a new handler
|
||||
h := handler.WebfingerHandler(fingers)
|
||||
h := server.WebfingerHandler(cfg, webfingers)
|
||||
|
||||
// Serve the request
|
||||
h.ServeHTTP(w, r)
|
||||
|
@ -121,8 +121,8 @@ func TestWebfingerHandler(t *testing.T) {
|
|||
t.Errorf("expected content type %s, got %s", "application/jrd+json", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
fingerWant := fingers[tc.resource]
|
||||
fingerGot := &webfingers.WebFinger{}
|
||||
fingerWant := webfingers[tc.resource]
|
||||
fingerGot := &webfinger.WebFinger{}
|
||||
|
||||
// Decode the response body
|
||||
if err := json.NewDecoder(w.Body).Decode(fingerGot); err != nil {
|
||||
|
@ -147,30 +147,3 @@ func TestWebfingerHandler(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWebfingerHandler(b *testing.B) {
|
||||
fingers, err := webfingers.NewWebFingers(
|
||||
webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
h := handler.WebfingerHandler(fingers)
|
||||
r := httptest.NewRequest(http.MethodGet, "/.well-known/webfinger?resource=acct:user@example.com", http.NoBody)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
b.Errorf("expected status code %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
}
|
170
internal/webfinger/webfinger.go
Normal file
170
internal/webfinger/webfinger.go
Normal file
|
@ -0,0 +1,170 @@
|
|||
package webfinger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Link struct {
|
||||
Rel string `json:"rel"`
|
||||
Href string `json:"href,omitempty"`
|
||||
}
|
||||
|
||||
type WebFinger struct {
|
||||
Subject string `json:"subject"`
|
||||
Links []Link `json:"links,omitempty"`
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
type WebFingers map[string]*WebFinger
|
||||
|
||||
type (
|
||||
URNMap = map[string]string
|
||||
RawFingersMap = map[string]map[string]string
|
||||
)
|
||||
|
||||
type FingerReader struct {
|
||||
URNSFile []byte
|
||||
FingersFile []byte
|
||||
}
|
||||
|
||||
func NewFingerReader() *FingerReader {
|
||||
return &FingerReader{}
|
||||
}
|
||||
|
||||
func (f *FingerReader) ReadFiles(cfg *config.Config) error {
|
||||
// Read URNs file
|
||||
file, err := os.ReadFile(cfg.URNPath)
|
||||
if err != nil {
|
||||
// If the file does not exist and the path is the default, set the URNs to an empty map
|
||||
if os.IsNotExist(err) && cfg.URNPath == config.DefaultURNPath {
|
||||
f.URNSFile = []byte("")
|
||||
} else {
|
||||
return fmt.Errorf("error opening URNs file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
f.URNSFile = file
|
||||
|
||||
// Read fingers file
|
||||
file, err = os.ReadFile(cfg.FingerPath)
|
||||
if err != nil {
|
||||
// If the file does not exist and the path is the default, set the fingers to an empty map
|
||||
if os.IsNotExist(err) && cfg.FingerPath == config.DefaultFingerPath {
|
||||
f.FingersFile = []byte("")
|
||||
} else {
|
||||
return fmt.Errorf("error opening fingers file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
f.FingersFile = file
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FingerReader) ParseFingers(ctx context.Context, urns URNMap, rawFingers RawFingersMap) (WebFingers, error) {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
webfingers := make(WebFingers)
|
||||
|
||||
// Parse the webfinger file
|
||||
for k, v := range rawFingers {
|
||||
resource := k
|
||||
|
||||
// Remove leading acct: if present
|
||||
if len(k) > 5 && resource[:5] == "acct:" {
|
||||
resource = resource[5:]
|
||||
}
|
||||
|
||||
// The key must be a URL or email address
|
||||
if _, err := mail.ParseAddress(resource); err != nil {
|
||||
if _, err := url.ParseRequestURI(resource); err != nil {
|
||||
return nil, fmt.Errorf("error parsing webfinger key (%s): %w", k, err)
|
||||
}
|
||||
} else {
|
||||
// Add acct: back to the key if it is an email address
|
||||
resource = fmt.Sprintf("acct:%s", resource)
|
||||
}
|
||||
|
||||
// Create a new webfinger
|
||||
webfinger := &WebFinger{
|
||||
Subject: resource,
|
||||
}
|
||||
|
||||
// Parse the fields
|
||||
for field, value := range v {
|
||||
fieldUrn := field
|
||||
|
||||
// If the key is present in the URNs file, use the value
|
||||
if _, ok := urns[field]; ok {
|
||||
fieldUrn = urns[field]
|
||||
}
|
||||
|
||||
// If the value is a valid URI, add it to the links
|
||||
if _, err := url.ParseRequestURI(value); err == nil {
|
||||
webfinger.Links = append(webfinger.Links, Link{
|
||||
Rel: fieldUrn,
|
||||
Href: value,
|
||||
})
|
||||
} else {
|
||||
// Otherwise add it to the properties
|
||||
if webfinger.Properties == nil {
|
||||
webfinger.Properties = make(map[string]string)
|
||||
}
|
||||
|
||||
webfinger.Properties[fieldUrn] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Add the webfinger to the map
|
||||
webfingers[resource] = webfinger
|
||||
}
|
||||
|
||||
l.Debug("Webfinger map built successfully", slog.Int("number", len(webfingers)), slog.Any("data", webfingers))
|
||||
|
||||
return webfingers, nil
|
||||
}
|
||||
|
||||
func (f *FingerReader) ReadFingerFile(ctx context.Context) (WebFingers, error) {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
urnMap := make(URNMap)
|
||||
fingerData := make(RawFingersMap)
|
||||
|
||||
// Parse the URNs file
|
||||
if err := yaml.Unmarshal(f.URNSFile, &urnMap); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling URNs file: %w", err)
|
||||
}
|
||||
|
||||
// The URNs file must be a map of strings to valid URLs
|
||||
for _, v := range urnMap {
|
||||
if _, err := url.ParseRequestURI(v); err != nil {
|
||||
return nil, fmt.Errorf("error parsing URN URIs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
l.Debug("URNs file parsed successfully", slog.Int("number", len(urnMap)), slog.Any("data", urnMap))
|
||||
|
||||
// Parse the fingers file
|
||||
if err := yaml.Unmarshal(f.FingersFile, &fingerData); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling fingers file: %w", err)
|
||||
}
|
||||
|
||||
l.Debug("Fingers file parsed successfully", slog.Int("number", len(fingerData)), slog.Any("data", fingerData))
|
||||
|
||||
// Parse raw data
|
||||
webfingers, err := f.ParseFingers(ctx, urnMap, fingerData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing raw fingers: %w", err)
|
||||
}
|
||||
|
||||
return webfingers, nil
|
||||
}
|
444
internal/webfinger/webfinger_test.go
Normal file
444
internal/webfinger/webfinger_test.go
Normal file
|
@ -0,0 +1,444 @@
|
|||
package webfinger_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/internal/webfinger"
|
||||
)
|
||||
|
||||
func newTempFile(t *testing.T, content string) (name string, remove func()) {
|
||||
t.Helper()
|
||||
|
||||
f, err := os.CreateTemp("", "finger-test")
|
||||
if err != nil {
|
||||
t.Fatalf("error creating temp file: %v", err)
|
||||
}
|
||||
|
||||
_, err = f.WriteString(content)
|
||||
if err != nil {
|
||||
t.Fatalf("error writing to temp file: %v", err)
|
||||
}
|
||||
|
||||
return f.Name(), func() {
|
||||
err = os.Remove(f.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("error removing temp file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFingerReader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := webfinger.NewFingerReader()
|
||||
|
||||
if f == nil {
|
||||
t.Errorf("NewFingerReader() = %v, want: %v", f, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFingerReader_ReadFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
urnsContent string
|
||||
fingersContent string
|
||||
useURNFile bool
|
||||
useFingerFile bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "reads files",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
useURNFile: true,
|
||||
useFingerFile: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "errors on missing URNs file",
|
||||
urnsContent: "invalid",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
useURNFile: false,
|
||||
useFingerFile: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on missing fingers file",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "invalid",
|
||||
useFingerFile: false,
|
||||
useURNFile: true,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.NewConfig()
|
||||
|
||||
urnsFileName, urnsCleanup := newTempFile(t, tc.urnsContent)
|
||||
defer urnsCleanup()
|
||||
|
||||
fingersFileName, fingersCleanup := newTempFile(t, tc.fingersContent)
|
||||
defer fingersCleanup()
|
||||
|
||||
if !tc.useURNFile {
|
||||
cfg.URNPath = "invalid"
|
||||
} else {
|
||||
cfg.URNPath = urnsFileName
|
||||
}
|
||||
|
||||
if !tc.useFingerFile {
|
||||
cfg.FingerPath = "invalid"
|
||||
} else {
|
||||
cfg.FingerPath = fingersFileName
|
||||
}
|
||||
|
||||
f := webfinger.NewFingerReader()
|
||||
|
||||
err := f.ReadFiles(cfg)
|
||||
if err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Errorf("ReadFiles() error = %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
} else if tc.wantErr {
|
||||
t.Errorf("ReadFiles() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(f.URNSFile, []byte(tc.urnsContent)) {
|
||||
t.Errorf("ReadFiles() URNsFile = %v, want: %v", f.URNSFile, tc.urnsContent)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(f.FingersFile, []byte(tc.fingersContent)) {
|
||||
t.Errorf("ReadFiles() FingersFile = %v, want: %v", f.FingersFile, tc.fingersContent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFingers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rawFingers webfinger.RawFingersMap
|
||||
want webfinger.WebFingers
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "parses links",
|
||||
rawFingers: webfinger.RawFingersMap{
|
||||
"user@example.com": {
|
||||
"profile": "https://example.com/profile",
|
||||
"invalidalias": "https://example.com/invalidalias",
|
||||
"https://something": "https://somethingelse",
|
||||
},
|
||||
},
|
||||
want: webfinger.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Links: []webfinger.Link{
|
||||
{
|
||||
Rel: "https://schema/profile",
|
||||
Href: "https://example.com/profile",
|
||||
},
|
||||
{
|
||||
Rel: "invalidalias",
|
||||
Href: "https://example.com/invalidalias",
|
||||
},
|
||||
{
|
||||
Rel: "https://something",
|
||||
Href: "https://somethingelse",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "parses properties",
|
||||
rawFingers: webfinger.RawFingersMap{
|
||||
"user@example.com": {
|
||||
"name": "John Doe",
|
||||
"invalidalias": "value1",
|
||||
"https://mylink": "value2",
|
||||
},
|
||||
},
|
||||
want: webfinger.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "John Doe",
|
||||
"invalidalias": "value1",
|
||||
"https://mylink": "value2",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "accepts acct: prefix",
|
||||
rawFingers: webfinger.RawFingersMap{
|
||||
"acct:user@example.com": {
|
||||
"name": "John Doe",
|
||||
},
|
||||
},
|
||||
want: webfinger.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "John Doe",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "accepts urls as resource",
|
||||
rawFingers: webfinger.RawFingersMap{
|
||||
"https://example.com": {
|
||||
"name": "John Doe",
|
||||
},
|
||||
},
|
||||
want: webfinger.WebFingers{
|
||||
"https://example.com": {
|
||||
Subject: "https://example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "John Doe",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "accepts multiple resources",
|
||||
rawFingers: webfinger.RawFingersMap{
|
||||
"user@example.com": {
|
||||
"name": "John Doe",
|
||||
},
|
||||
"other@example.com": {
|
||||
"name": "Jane Doe",
|
||||
},
|
||||
},
|
||||
want: webfinger.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "John Doe",
|
||||
},
|
||||
},
|
||||
"acct:other@example.com": {
|
||||
Subject: "acct:other@example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "Jane Doe",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid resource",
|
||||
rawFingers: webfinger.RawFingersMap{
|
||||
"invalid": {
|
||||
"name": "John Doe",
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a urn map
|
||||
urns := webfinger.URNMap{
|
||||
"name": "https://schema/name",
|
||||
"profile": "https://schema/profile",
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
cfg := config.NewConfig()
|
||||
l := log.NewLogger(&strings.Builder{}, cfg)
|
||||
|
||||
ctx = log.WithLogger(ctx, l)
|
||||
|
||||
f := webfinger.NewFingerReader()
|
||||
|
||||
got, err := f.ParseFingers(ctx, urns, tc.rawFingers)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("ParseFingers() error = %v, wantErr %v", err, tc.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Sort links to make it easier to compare
|
||||
for _, v := range got {
|
||||
for range v.Links {
|
||||
sort.Slice(v.Links, func(i, j int) bool {
|
||||
return v.Links[i].Rel < v.Links[j].Rel
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range tc.want {
|
||||
for range v.Links {
|
||||
sort.Slice(v.Links, func(i, j int) bool {
|
||||
return v.Links[i].Rel < v.Links[j].Rel
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
// Unmarshal the structs to JSON to make it easier to print
|
||||
gotstr := &strings.Builder{}
|
||||
gotenc := json.NewEncoder(gotstr)
|
||||
|
||||
wantstr := &strings.Builder{}
|
||||
wantenc := json.NewEncoder(wantstr)
|
||||
|
||||
_ = gotenc.Encode(got)
|
||||
_ = wantenc.Encode(tc.want)
|
||||
|
||||
t.Errorf("ParseFingers() got = \n%s want: \n%s", gotstr.String(), wantstr.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFingerFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
urnsContent string
|
||||
fingersContent string
|
||||
wantURN webfinger.URNMap
|
||||
wantFinger webfinger.RawFingersMap
|
||||
returns *webfinger.WebFingers
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "reads files",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
wantURN: webfinger.URNMap{
|
||||
"name": "https://schema/name",
|
||||
"profile": "https://schema/profile",
|
||||
},
|
||||
wantFinger: webfinger.RawFingersMap{
|
||||
"user@example.com": {
|
||||
"name": "John Doe",
|
||||
},
|
||||
},
|
||||
returns: &webfinger.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "John Doe",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "uses custom URNs",
|
||||
urnsContent: "favorite_food: https://schema/favorite_food",
|
||||
fingersContent: "user@example.com:\n favorite_food: Apple",
|
||||
wantURN: webfinger.URNMap{
|
||||
"favorite_food": "https://schema/favorite_food",
|
||||
},
|
||||
wantFinger: webfinger.RawFingersMap{
|
||||
"user@example.com": {
|
||||
"https://schema/favorite_food": "Apple",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid URNs file",
|
||||
urnsContent: "invalid",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
wantURN: webfinger.URNMap{},
|
||||
wantFinger: webfinger.RawFingersMap{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid fingers file",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "invalid",
|
||||
wantURN: webfinger.URNMap{},
|
||||
wantFinger: webfinger.RawFingersMap{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid URNs values",
|
||||
urnsContent: "name: invalid",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
wantURN: webfinger.URNMap{},
|
||||
wantFinger: webfinger.RawFingersMap{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid fingers values",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "invalid:\n name: John Doe",
|
||||
wantURN: webfinger.URNMap{},
|
||||
wantFinger: webfinger.RawFingersMap{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
cfg := config.NewConfig()
|
||||
l := log.NewLogger(&strings.Builder{}, cfg)
|
||||
|
||||
ctx = log.WithLogger(ctx, l)
|
||||
|
||||
f := webfinger.NewFingerReader()
|
||||
|
||||
f.FingersFile = []byte(tc.fingersContent)
|
||||
f.URNSFile = []byte(tc.urnsContent)
|
||||
|
||||
got, err := f.ReadFingerFile(ctx)
|
||||
if err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Errorf("ReadFingerFile() error = %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
} else if tc.wantErr {
|
||||
t.Errorf("ReadFingerFile() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
|
||||
if tc.returns != nil && !reflect.DeepEqual(got, *tc.returns) {
|
||||
t.Errorf("ReadFingerFile() got = %v, want: %v", got, *tc.returns)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package webfingers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Link is a link in a webfinger.
|
||||
type Link struct {
|
||||
Rel string `json:"rel"`
|
||||
Href string `json:"href,omitempty"`
|
||||
}
|
||||
|
||||
// WebFinger is a webfinger.
|
||||
type WebFinger struct {
|
||||
Subject string `json:"subject"`
|
||||
Links []Link `json:"links,omitempty"`
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// Resources is a simplified webfinger map.
|
||||
type Resources map[string]map[string]string
|
||||
|
||||
// URNAliases is a map of URN aliases.
|
||||
type URNAliases map[string]string
|
||||
|
||||
// WebFingers is a map of webfingers.
|
||||
type WebFingers map[string]*WebFinger
|
||||
|
||||
// NewWebFingers creates a new webfinger map from a simplified webfinger map and an optional URN aliases map.
|
||||
func NewWebFingers(resources Resources, urnAliases URNAliases) (WebFingers, error) {
|
||||
fingers := make(WebFingers)
|
||||
|
||||
// If the aliases map is nil, create an empty one.
|
||||
if urnAliases == nil {
|
||||
urnAliases = make(URNAliases)
|
||||
}
|
||||
|
||||
// Parse the resources.
|
||||
for k, v := range resources {
|
||||
subject := k
|
||||
|
||||
// Remove leading acct: if present.
|
||||
if len(k) > 5 && subject[:5] == "acct:" {
|
||||
subject = subject[5:]
|
||||
}
|
||||
|
||||
// The subject must be a URL or email address.
|
||||
if _, err := mail.ParseAddress(subject); err != nil {
|
||||
if _, err := url.ParseRequestURI(subject); err != nil {
|
||||
return nil, fmt.Errorf("error parsing resource subject (%s): %w", k, err)
|
||||
}
|
||||
} else {
|
||||
// Add acct: back to the subject if it is an email address.
|
||||
subject = fmt.Sprintf("acct:%s", subject)
|
||||
}
|
||||
|
||||
// Create a new webfinger.
|
||||
finger := &WebFinger{
|
||||
Subject: subject,
|
||||
}
|
||||
|
||||
// Parse the resource fields.
|
||||
for field, value := range v {
|
||||
fieldUrn := field
|
||||
|
||||
// If the key is present in the aliases map, use its value.
|
||||
if _, ok := urnAliases[field]; ok {
|
||||
fieldUrn = urnAliases[field]
|
||||
}
|
||||
|
||||
// If the value is a valid URI, add it to the links.
|
||||
if _, err := url.ParseRequestURI(value); err == nil {
|
||||
finger.Links = append(finger.Links, Link{
|
||||
Rel: fieldUrn,
|
||||
Href: value,
|
||||
})
|
||||
} else {
|
||||
// Otherwise add it to the properties.
|
||||
if finger.Properties == nil {
|
||||
finger.Properties = make(map[string]string)
|
||||
}
|
||||
|
||||
finger.Properties[fieldUrn] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Add the webfinger to the map.
|
||||
fingers[subject] = finger
|
||||
}
|
||||
|
||||
return fingers, nil
|
||||
}
|
|
@ -1,231 +0,0 @@
|
|||
package webfingers_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
)
|
||||
|
||||
func TestNewWebFingers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
resources webfingers.Resources
|
||||
urnAliases webfingers.URNAliases
|
||||
want webfingers.WebFingers
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
resources: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"name": "Example User",
|
||||
},
|
||||
},
|
||||
urnAliases: webfingers.URNAliases{
|
||||
"name": "http://schema.org/name",
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"http://schema.org/name": "Example User",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses links",
|
||||
resources: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"link1": "https://example.com/link1",
|
||||
"link2": "https://example.com/link2",
|
||||
},
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Links: []webfingers.Link{
|
||||
{
|
||||
Rel: "link1",
|
||||
Href: "https://example.com/link1",
|
||||
},
|
||||
{
|
||||
Rel: "link2",
|
||||
Href: "https://example.com/link2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses links with URN aliases",
|
||||
resources: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"link1": "https://example.com/link1",
|
||||
},
|
||||
},
|
||||
urnAliases: webfingers.URNAliases{
|
||||
"link1": "http://schema.com/link",
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Links: []webfingers.Link{
|
||||
{
|
||||
Rel: "http://schema.com/link",
|
||||
Href: "https://example.com/link1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses properties",
|
||||
resources: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"prop1": "value1",
|
||||
"prop2": "value2",
|
||||
},
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"prop1": "value1",
|
||||
"prop2": "value2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses properties with URN aliases",
|
||||
resources: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
urnAliases: webfingers.URNAliases{
|
||||
"prop1": "http://schema.com/prop",
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"http://schema.com/prop": "value1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses multiple resources",
|
||||
resources: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"prop1": "value1",
|
||||
},
|
||||
"user2@example.com": {
|
||||
"prop2": "value2",
|
||||
},
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
"acct:user2@example.com": {
|
||||
Subject: "acct:user2@example.com",
|
||||
Properties: map[string]string{
|
||||
"prop2": "value2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses URI resources",
|
||||
resources: webfingers.Resources{
|
||||
"https://example.com": {
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"https://example.com": {
|
||||
Subject: "https://example.com",
|
||||
Properties: map[string]string{
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses email resource with acct:",
|
||||
resources: webfingers.Resources{
|
||||
"acct:user@example.com": {
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "errors on invalid resource",
|
||||
resources: webfingers.Resources{
|
||||
"invalid": {
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := webfingers.NewWebFingers(tc.resources, tc.urnAliases)
|
||||
if err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
} else if tc.wantErr {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
|
||||
// Sort the links.
|
||||
for _, finger := range got {
|
||||
sort.Slice(finger.Links, func(i, j int) bool {
|
||||
return finger.Links[i].Rel < finger.Links[j].Rel
|
||||
})
|
||||
}
|
||||
|
||||
for _, finger := range tc.want {
|
||||
sort.Slice(finger.Links, func(i, j int) bool {
|
||||
return finger.Links[i].Rel < finger.Links[j].Rel
|
||||
})
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
// Marshall both so we can visualize the differences.
|
||||
gotJSON, _ := json.MarshalIndent(got, "", " ")
|
||||
wantJSON, _ := json.MarshalIndent(tc.want, "", " ")
|
||||
|
||||
t.Errorf("got:\n%s\nwant:\n%s", gotJSON, wantJSON)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue