Skip to content

Commit de3da93

Browse files
committed
refactor so external servers can use Finger as a handler
1 parent e2ea9cd commit de3da93

File tree

12 files changed

+763
-656
lines changed

12 files changed

+763
-656
lines changed

README.md

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,65 @@
11
# Finger
22

3-
Webfinger server written in Go.
3+
Webfinger handler / standalone server written in Go.
44

55
## Features
66
- 🍰 Easy YAML configuration
77
- 🪶 Single 8MB binary / 0% idle CPU / 4MB idle RAM
88
- ⚡️ Sub millisecond responses at 10,000 request per second
99
- 🐳 10MB Docker image
1010

11-
## Install
11+
## In your existing server
12+
13+
To use Finger in your existing server, download the package as a dependency:
14+
15+
```bash
16+
go get git.maronato.dev/maronato/finger@latest
17+
```
18+
19+
Then, use it as a regular `http.Handler`:
20+
21+
```go
22+
package main
23+
24+
import (
25+
"log"
26+
"net/http"
27+
28+
"git.maronato.dev/maronato/finger/handler"
29+
"git.maronato.dev/maronato/finger/webfingers"
30+
)
31+
32+
func main() {
33+
// Create the webfingers map that will be served by the handler
34+
fingers, err := webfingers.NewWebFingers(
35+
// Pass a map of your resources (Subject key followed by it's properties and links)
36+
// the syntax is the same as the fingers.yml file (see below)
37+
webfingers.Resources{
38+
"user@example.com": {
39+
"name": "Example User",
40+
},
41+
},
42+
// Optionally, pass a map of URN aliases (see urns.yml for more)
43+
// If nil is provided, no aliases will be used
44+
webfingers.URNAliases{
45+
"name": "http://schema.org/name",
46+
},
47+
)
48+
if err != nil {
49+
log.Fatal(err)
50+
}
51+
52+
mux := http.NewServeMux()
53+
// Then use the handler as a regular http.Handler
54+
mux.Handle("/.well-known/webfinger", handler.WebfingerHandler(fingers))
55+
56+
log.Fatal(http.ListenAndServe("localhost:8080", mux))
57+
}
58+
```
59+
60+
## As a standalone server
61+
62+
If you don't have a server, Finger can also serve itself. You can install it via `go install` or use the Docker image.
1263

1364
Via `go install`:
1465

cmd/serve.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import (
66
"os"
77

88
"git.maronato.dev/maronato/finger/internal/config"
9+
"git.maronato.dev/maronato/finger/internal/fingerreader"
910
"git.maronato.dev/maronato/finger/internal/log"
1011
"git.maronato.dev/maronato/finger/internal/server"
11-
"git.maronato.dev/maronato/finger/internal/webfinger"
1212
"github.com/peterbourgon/ff/v4"
1313
)
1414

@@ -25,21 +25,21 @@ func newServerCmd(cfg *config.Config) *ff.Command {
2525
ctx = log.WithLogger(ctx, l)
2626

2727
// Read the webfinger files
28-
r := webfinger.NewFingerReader()
28+
r := fingerreader.NewFingerReader()
2929
err := r.ReadFiles(cfg)
3030
if err != nil {
3131
return fmt.Errorf("error reading finger files: %w", err)
3232
}
3333

34-
webfingers, err := r.ReadFingerFile(ctx)
34+
fingers, err := r.ReadFingerFile(ctx)
3535
if err != nil {
3636
return fmt.Errorf("error parsing finger files: %w", err)
3737
}
3838

39-
l.Info(fmt.Sprintf("Loaded %d webfingers", len(webfingers)))
39+
l.Info(fmt.Sprintf("Loaded %d webfingers", len(fingers)))
4040

4141
// Start the server
42-
if err := server.StartServer(ctx, cfg, webfingers); err != nil {
42+
if err := server.StartServer(ctx, cfg, fingers); err != nil {
4343
return fmt.Errorf("error running server: %w", err)
4444
}
4545

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
1-
package server
1+
package handler
22

33
import (
44
"encoding/json"
55
"net/http"
66

7-
"git.maronato.dev/maronato/finger/internal/config"
8-
"git.maronato.dev/maronato/finger/internal/log"
9-
"git.maronato.dev/maronato/finger/internal/webfinger"
7+
"git.maronato.dev/maronato/finger/webfingers"
108
)
119

12-
func WebfingerHandler(_ *config.Config, webfingers webfinger.WebFingers) http.Handler {
10+
func WebfingerHandler(fingers webfingers.WebFingers) http.Handler {
1311
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14-
ctx := r.Context()
15-
l := log.FromContext(ctx)
16-
1712
// Only handle GET requests
1813
if r.Method != http.MethodGet {
19-
l.Debug("Method not allowed")
2014
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
2115

2216
return
@@ -28,16 +22,14 @@ func WebfingerHandler(_ *config.Config, webfingers webfinger.WebFingers) http.Ha
2822
// Get the resource
2923
resource := q.Get("resource")
3024
if resource == "" {
31-
l.Debug("No resource provided")
3225
http.Error(w, "No resource provided", http.StatusBadRequest)
3326

3427
return
3528
}
3629

3730
// Get and validate resource
38-
finger, ok := webfingers[resource]
31+
finger, ok := fingers[resource]
3932
if !ok {
40-
l.Debug("Resource not found")
4133
http.Error(w, "Resource not found", http.StatusNotFound)
4234

4335
return
@@ -48,12 +40,9 @@ func WebfingerHandler(_ *config.Config, webfingers webfinger.WebFingers) http.Ha
4840

4941
// Write the response
5042
if err := json.NewEncoder(w).Encode(finger); err != nil {
51-
l.Debug("Error encoding json")
5243
http.Error(w, "Error encoding json", http.StatusInternalServerError)
5344

5445
return
5546
}
56-
57-
l.Debug("Webfinger request successful")
5847
})
5948
}

internal/server/webfinger_test.go renamed to handler/handler_test.go

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package server_test
1+
package handler_test
22

33
import (
44
"context"
@@ -10,19 +10,19 @@ import (
1010
"strings"
1111
"testing"
1212

13+
"git.maronato.dev/maronato/finger/handler"
1314
"git.maronato.dev/maronato/finger/internal/config"
1415
"git.maronato.dev/maronato/finger/internal/log"
15-
"git.maronato.dev/maronato/finger/internal/server"
16-
"git.maronato.dev/maronato/finger/internal/webfinger"
16+
"git.maronato.dev/maronato/finger/webfingers"
1717
)
1818

1919
func TestWebfingerHandler(t *testing.T) {
2020
t.Parallel()
2121

22-
webfingers := webfinger.WebFingers{
22+
fingers := webfingers.WebFingers{
2323
"acct:user@example.com": {
2424
Subject: "acct:user@example.com",
25-
Links: []webfinger.Link{
25+
Links: []webfingers.Link{
2626
{
2727
Rel: "http://webfinger.net/rel/profile-page",
2828
Href: "https://example.com/user",
@@ -104,7 +104,7 @@ func TestWebfingerHandler(t *testing.T) {
104104
w := httptest.NewRecorder()
105105

106106
// Create a new handler
107-
h := server.WebfingerHandler(cfg, webfingers)
107+
h := handler.WebfingerHandler(fingers)
108108

109109
// Serve the request
110110
h.ServeHTTP(w, r)
@@ -121,8 +121,8 @@ func TestWebfingerHandler(t *testing.T) {
121121
t.Errorf("expected content type %s, got %s", "application/jrd+json", w.Header().Get("Content-Type"))
122122
}
123123

124-
fingerWant := webfingers[tc.resource]
125-
fingerGot := &webfinger.WebFinger{}
124+
fingerWant := fingers[tc.resource]
125+
fingerGot := &webfingers.WebFinger{}
126126

127127
// Decode the response body
128128
if err := json.NewDecoder(w.Body).Decode(fingerGot); err != nil {
@@ -147,3 +147,30 @@ func TestWebfingerHandler(t *testing.T) {
147147
})
148148
}
149149
}
150+
151+
func BenchmarkWebfingerHandler(b *testing.B) {
152+
fingers, err := webfingers.NewWebFingers(
153+
webfingers.Resources{
154+
"user@example.com": {
155+
"prop1": "value1",
156+
},
157+
},
158+
nil,
159+
)
160+
if err != nil {
161+
b.Fatal(err)
162+
}
163+
164+
h := handler.WebfingerHandler(fingers)
165+
r := httptest.NewRequest(http.MethodGet, "/.well-known/webfinger?resource=acct:user@example.com", http.NoBody)
166+
167+
for i := 0; i < b.N; i++ {
168+
w := httptest.NewRecorder()
169+
170+
h.ServeHTTP(w, r)
171+
172+
if w.Code != http.StatusOK {
173+
b.Errorf("expected status code %d, got %d", http.StatusOK, w.Code)
174+
}
175+
}
176+
}

internal/fingerreader/fingerreader.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package fingerreader
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
"net/url"
8+
"os"
9+
10+
"git.maronato.dev/maronato/finger/internal/config"
11+
"git.maronato.dev/maronato/finger/internal/log"
12+
"git.maronato.dev/maronato/finger/webfingers"
13+
"gopkg.in/yaml.v3"
14+
)
15+
16+
type FingerReader struct {
17+
URNSFile []byte
18+
FingersFile []byte
19+
}
20+
21+
func NewFingerReader() *FingerReader {
22+
return &FingerReader{}
23+
}
24+
25+
func (f *FingerReader) ReadFiles(cfg *config.Config) error {
26+
// Read URNs file
27+
file, err := os.ReadFile(cfg.URNPath)
28+
if err != nil {
29+
// If the file does not exist and the path is the default, set the URNs to an empty map
30+
if os.IsNotExist(err) && cfg.URNPath == config.DefaultURNPath {
31+
f.URNSFile = []byte("")
32+
} else {
33+
return fmt.Errorf("error opening URNs file: %w", err)
34+
}
35+
}
36+
37+
f.URNSFile = file
38+
39+
// Read fingers file
40+
file, err = os.ReadFile(cfg.FingerPath)
41+
if err != nil {
42+
// If the file does not exist and the path is the default, set the fingers to an empty map
43+
if os.IsNotExist(err) && cfg.FingerPath == config.DefaultFingerPath {
44+
f.FingersFile = []byte("")
45+
} else {
46+
return fmt.Errorf("error opening fingers file: %w", err)
47+
}
48+
}
49+
50+
f.FingersFile = file
51+
52+
return nil
53+
}
54+
55+
func (f *FingerReader) ReadFingerFile(ctx context.Context) (webfingers.WebFingers, error) {
56+
l := log.FromContext(ctx)
57+
58+
urnAliases := make(webfingers.URNAliases)
59+
resources := make(webfingers.Resources)
60+
61+
// Parse the URNs file
62+
if err := yaml.Unmarshal(f.URNSFile, &urnAliases); err != nil {
63+
return nil, fmt.Errorf("error unmarshalling URNs file: %w", err)
64+
}
65+
66+
// The URNs file must be a map of strings to valid URLs
67+
for _, v := range urnAliases {
68+
if _, err := url.ParseRequestURI(v); err != nil {
69+
return nil, fmt.Errorf("error parsing URN URIs: %w", err)
70+
}
71+
}
72+
73+
l.Debug("URNs file parsed successfully", slog.Int("number", len(urnAliases)), slog.Any("data", urnAliases))
74+
75+
// Parse the fingers file
76+
if err := yaml.Unmarshal(f.FingersFile, &resources); err != nil {
77+
return nil, fmt.Errorf("error unmarshalling fingers file: %w", err)
78+
}
79+
80+
l.Debug("Fingers file parsed successfully", slog.Int("number", len(resources)), slog.Any("data", resources))
81+
82+
// Parse raw data
83+
fingers, err := webfingers.NewWebFingers(resources, urnAliases)
84+
if err != nil {
85+
return nil, fmt.Errorf("error parsing raw fingers: %w", err)
86+
}
87+
88+
return fingers, nil
89+
}

0 commit comments

Comments
 (0)