Initial Commit
This commit is contained in:
83
webfinger/README.md
Normal file
83
webfinger/README.md
Normal 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)
|
||||
}
|
||||
43
webfinger/cmd/wflookup/main.go
Normal file
43
webfinger/cmd/wflookup/main.go
Normal 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
46
webfinger/handler.go
Normal 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
75
webfinger/handler_test.go
Normal 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
63
webfinger/lookup.go
Normal 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
38
webfinger/types.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user