Initial Commit

This commit is contained in:
2024-03-08 20:56:26 -08:00
commit 7c06d3b66d
15 changed files with 1362 additions and 0 deletions

83
webfinger/README.md Normal file
View File

@@ -0,0 +1,83 @@
# WebFinger
This is a simple implementation of a Go server handler and client for the WebFinger discovery protocol ([RFC7033](https://datatracker.ietf.org/doc/html/rfc7033)).
## `wflookup`
This package includes a command that looks up WebFinger descriptors. You can install it with the following command:
```bash
go install queerdevs.org/profilefed/webfinger/cmd/wflookup@latest
```
Here are some examples for how to use it:
```bash
wflookup acct:user@example.com
wflookup http://example.com/resource/1
wflookup user@example.com # wflookup will infer the acct scheme
```
If you'd like to specify the server that's going to be used instead of it being inferred, you can do so using the `--server` flag.
## Example library usage
### Server
```go
package main
import (
"net/http"
"queerdevs.org/profilefed/webfinger"
)
func main() {
mux := http.NewServeMux()
mux.Handle("GET /.well-known/webfinger", Handler{
DescriptorFunc: func(resource string) (*Descriptor, error) {
// You can query a database here, or do whatever you need
// to in order to get the descriptor data.
return desc, nil
},
})
err := http.ListenAndServe(":8080", mux)
if err != nil {
panic(err)
}
}
```
### Client
```go
package main
import (
"fmt"
"queerdevs.org/profilefed/webfinger"
)
func main() {
desc, err := webfinger.Lookup("acct:user@example.com", "example.com:8080")
if err != nil {
panic(err)
}
fmt.Println(desc)
desc, err = webfinger.LookupAcct("user@example.com")
if err != nil {
panic(err)
}
fmt.Println(desc)
desc, err = webfinger.LookupURL("http://example.com/resource/1")
if err != nil {
panic(err)
}
fmt.Println(desc)
}

View File

@@ -0,0 +1,43 @@
package main
import (
"encoding/json"
"flag"
"log"
"os"
"strings"
"queerdevs.org/go-webfinger"
)
func main() {
server := flag.String("server", "", "The server to query for the WebFinger descriptor (e.g. example.com)")
flag.Parse()
if len(os.Args) < 2 {
log.Fatalln("wflookup requires at least one argument")
}
res := os.Args[1]
var desc *webfinger.Descriptor
var err error
if *server != "" {
desc, err = webfinger.Lookup(res, *server)
} else if strings.HasPrefix(res, "http") {
desc, err = webfinger.LookupURL(res)
} else if strings.HasPrefix(res, "acct:") || strings.Contains(res, "@") {
desc, err = webfinger.LookupAcct(res)
}
if err != nil {
log.Fatalln("Lookup error:", err)
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
err = enc.Encode(desc)
if err != nil {
log.Fatalln("JSON encode error:", err)
}
}

46
webfinger/handler.go Normal file
View File

@@ -0,0 +1,46 @@
package webfinger
import (
"encoding/json"
"net/http"
)
// Handler handles WebFinger requests to an HTTP server
type Handler struct {
// DescriptorFunc is the function used to resolve resource strings
// to WebFinger descriptors. It's called on every request to the
// WebFinger endpoint. The errors it returns are handled by ErrorHandler.
DescriptorFunc func(resource string) (*Descriptor, error)
// ErrorHandler handles any errors that occur in the process of performing
// a WebFinger lookup. If not provided, a simple default handler is used.
ErrorHandler func(err error, res http.ResponseWriter)
}
// ServeHTTP implements the http.Handler interface
func (h Handler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
if h.ErrorHandler == nil {
h.ErrorHandler = func(err error, res http.ResponseWriter) {
http.Error(res, err.Error(), http.StatusInternalServerError)
}
}
descriptor, err := h.DescriptorFunc(req.URL.Query().Get("resource"))
if err != nil {
h.ErrorHandler(err, res)
return
}
data, err := json.Marshal(descriptor)
if err != nil {
h.ErrorHandler(err, res)
return
}
res.Header().Set("Content-Type", "application/jrd+json")
_, err = res.Write(data)
if err != nil {
h.ErrorHandler(err, res)
return
}
}

75
webfinger/handler_test.go Normal file
View File

@@ -0,0 +1,75 @@
package webfinger
import (
"errors"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
func TestHandler(t *testing.T) {
testdata := map[string]*Descriptor{
"acct:user@example.com": {
Subject: "acct:user@example.com",
Aliases: []string{
"mailto:user@example.com",
"https://www.example.com/user",
},
Links: []Link{
{
Rel: "http://webfinger.net/rel/profile-page",
Type: "text/html",
Href: "https://www.example.com/user",
},
},
},
"http://example.com/resource/1": {
Subject: "http://example.com/resource/1",
Properties: map[string]string{
"http://example.com/ns/example#publish-date": "2023-04-26",
},
},
}
srv := httptest.NewServer(Handler{
DescriptorFunc: func(resource string) (*Descriptor, error) {
// Look for the descriptor in the testdata map
desc, ok := testdata[resource]
if !ok {
return nil, errors.New("descriptor not found")
}
return desc, nil
},
ErrorHandler: func(err error, res http.ResponseWriter) {
http.Error(res, err.Error(), http.StatusInternalServerError)
},
})
defer srv.Close()
// Look up acct resource
desc, err := Lookup("acct:user@example.com", srv.Listener.Addr().String())
if err != nil {
t.Fatalf("Lookup error: %s", err)
}
if !reflect.DeepEqual(desc, testdata["acct:user@example.com"]) {
t.Errorf("Descriptors are not equal:\n%#v\n\n%#v", desc, testdata["acct:user@example.com"])
}
// Look up URL resource
desc, err = Lookup("http://example.com/resource/1", srv.Listener.Addr().String())
if err != nil {
t.Fatalf("Lookup error: %s", err)
}
if !reflect.DeepEqual(desc, testdata["http://example.com/resource/1"]) {
t.Errorf("Descriptors are not equal:\n%#v\n\n%#v", desc, testdata["http://example.com/resource/1"])
}
// Look up a non-existent resource to test error handling
desc, err = Lookup("http://example.com/resource/2", srv.Listener.Addr().String())
if err == nil {
t.Fatalf("Expected error, got nil")
}
}

63
webfinger/lookup.go Normal file
View File

@@ -0,0 +1,63 @@
package webfinger
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"strings"
)
// Lookup looks up the given resource string at the given server.
// The server parameter shouldn't contain a URL scheme.
func Lookup(resource, server string) (desc *Descriptor, err error) {
u := url.URL{
Scheme: "http",
Host: server,
Path: "/.well-known/webfinger",
RawQuery: "resource=" + url.QueryEscape(resource),
}
res, err := http.Get(u.String())
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, errors.New(res.Status)
}
desc = &Descriptor{}
err = json.NewDecoder(res.Body).Decode(desc)
if err != nil {
return nil, err
}
return desc, nil
}
// LookupAcct looks up the given account ID. It uses the
// server in the ID to do the lookup. For example, user@example.com
// would use example.com as the server.
func LookupAcct(id string) (*Descriptor, error) {
_, server, ok := strings.Cut(id, "@")
if !ok {
return nil, errors.New("invalid acct id")
}
if !strings.HasPrefix(id, "acct:") {
id = "acct:" + id
}
return Lookup(id, server)
}
// LookupURL looks up the given resource URL. It uses the
// URL host to do the lookup. For example, http://example.com/1
// would use example.com as the server.
func LookupURL(resource string) (*Descriptor, error) {
u, err := url.ParseRequestURI(resource)
if err != nil {
return nil, err
}
return Lookup(resource, u.Host)
}

38
webfinger/types.go Normal file
View File

@@ -0,0 +1,38 @@
package webfinger
// Descriptor represents a WebFinger JSON Resource Descriptor (JRD)
type Descriptor struct {
Subject string `json:"subject"`
Aliases []string `json:"aliases"`
Properties map[string]string `json:"properties,omitempty"`
Links []Link `json:"links"`
}
// Link represents a JRD link item
type Link struct {
Rel string `json:"rel"`
Type string `json:"type,omitempty"`
Href string `json:"href"`
}
// LinkByType searches for a link with the given type. If found, it returns
// the link and true. Otherwise, it returns the zero value and false.
func (d *Descriptor) LinkByType(linkType string) (Link, bool) {
for _, link := range d.Links {
if link.Type == linkType {
return link, true
}
}
return Link{}, false
}
// LinkByRel searches for a link with the given rel value. If found, it returns
// the link and true. Otherwise, it returns the zero value and false.
func (d *Descriptor) LinkByRel(rel string) (Link, bool) {
for _, link := range d.Links {
if link.Rel == rel {
return link, true
}
}
return Link{}, false
}