@@ -0,0 +1,21 @@ | |||
MIT License | |||
Copyright (c) 2017 Sheena Artrip | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
SOFTWARE. |
@@ -0,0 +1,54 @@ | |||
# go-webfinger | |||
go-webfinger is a golang webfinger server implementation. | |||
## Usage | |||
`webfinger.Service` is implemented as a net/http handler, which means | |||
usage is simply registering the object with your http service. | |||
Using the webfinger service as the main ServeHTTP: | |||
```go | |||
myResolver = ... | |||
wf := webfinger.Default(myResolver{}) | |||
wf.NotFoundHandler = // the rest of your app | |||
http.ListenAndService(":8080", wf) | |||
``` | |||
Using the webfinger service as a route on an existing HTTP router: | |||
```go | |||
myResolver = ... | |||
wf := webfinger.Default(myResolver{}) | |||
http.Handle(webfinger.WebFingerPath, http.HandlerFunc(wf.Webfinger)) | |||
http.ListenAndService(":8080", nil) | |||
``` | |||
## Defaults | |||
The webfinger service is installed with a few defaults. Some of these | |||
defaults ensure we stick closely to the webfinger specification (tls-only, CORS, Content-Type) | |||
and other defaults are simply useful for development (no-cache) | |||
The full list of defaults can be found in the godoc for `webfinger.Service`. They are exposed | |||
as public variables which can be overriden. | |||
`PreHandlers` are the list of preflight HTTP handlers to run. You can add your own via `wf.PreHandlers["my-custom-name"] = ...`, however, | |||
execution order is not guaranteed. | |||
### TLS-Only | |||
Handler which routes to the TLS version of the page. Disable via `wf.NoTLSHandler = nil`. | |||
### No-Cache | |||
A PreFlight handler which sets no-cache headers on anything under `/.well-known/webfinger`. Disable or override via `wf.PreHandlers[webfinger.NoCacheMiddleware]` | |||
### Content Type as application/jrd+json | |||
A PreFlight handler which sets the Content-Type to `application/jrd+json`. Disable or override via `wf.PreHandlers[webfinger.ContentTypeMiddleware]`. | |||
### CORS | |||
A PreFlight handler which adds the CORS headers. Disable or override via `wf.PreHandlers[webfinger.CorsMiddleware].` |
@@ -0,0 +1,30 @@ | |||
package webfinger | |||
import ( | |||
"errors" | |||
"strings" | |||
) | |||
type account struct { | |||
Name string | |||
Hostname string | |||
} | |||
func (a *account) ParseString(str string) (err error) { | |||
if !strings.HasPrefix(str, "acct:") { | |||
err = errors.New("URI is not an account") | |||
return | |||
} | |||
items := strings.Split(str, "@") | |||
a.Name = items[0][5:] | |||
if len(items) < 2 { | |||
//TODO: this might not be required | |||
err = errors.New("No domain on account") | |||
return | |||
} | |||
a.Hostname = strings.Split(items[1], "/")[0] | |||
return | |||
} |
@@ -0,0 +1,51 @@ | |||
package webfinger | |||
import ( | |||
"testing" | |||
"reflect" | |||
"github.com/pkg/errors" | |||
) | |||
type acp struct { | |||
Input string | |||
Output account | |||
Error string | |||
} | |||
func (a *acp) Invoke() error { | |||
var ax account | |||
err := ax.ParseString(a.Input) | |||
failed := false | |||
failed = failed || err != nil && a.Error == "" | |||
failed = failed || err == nil && a.Error != "" | |||
failed = failed || err != nil && a.Error != err.Error() | |||
failed = failed || err != nil && a.Error != errors.Cause(err).Error() | |||
failed = failed || !reflect.DeepEqual(a.Output, ax) | |||
if failed { | |||
return errors.Errorf("ax.ParseString('%v') => '%v', '%v'; expected '%v', '%v'", a.Input, err, ax, a.Error, a.Output) | |||
} | |||
return nil | |||
} | |||
func TestAccountParse(t *testing.T) { | |||
var tests = []acp{ | |||
{"", account{}, "URI is not an account"}, | |||
{"http://hello.world", account{}, "URI is not an account"}, | |||
{"acct:hello", account{"hello", ""}, "No domain on account"}, | |||
{"acct:hello@domain", account{"hello", "domain"}, ""}, | |||
{"acct:hello@domain/uri", account{"hello", "domain"}, ""}, | |||
} | |||
for _, tx := range tests { | |||
if err := tx.Invoke(); err != nil { | |||
t.Error(err.Error()) | |||
} | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
// Package webfinger is a server implementation of the webfinger specification. This | |||
// is a general-case package which provides the HTTP handlers and interfaces | |||
// for adding webfinger support for your system and resources. | |||
// | |||
// The simplest way to use this is to call webfinger.Default() and | |||
// then register the object as an HTTP handler: | |||
// | |||
// myResolver = ... | |||
// wf := webfinger.Default(myResolver{}) | |||
// wf.NotFoundHandler = // the rest of your app | |||
// http.ListenAndService(":8080", wf) | |||
// | |||
// However, you can also register the specific webfinger handler to a path. This should | |||
// work on any router that supports net/http. | |||
// | |||
// myResolver = ... | |||
// wf := webfinger.Default(myResolver{}) | |||
// http.Handle(webfinger.WebFingerPath, http.HandlerFunc(wf.Webfinger)) | |||
// http.ListenAndService(":8080", nil) | |||
// | |||
// In either case, the handlers attached to the webfinger service get invoked as | |||
// needed. | |||
package webfinger |
@@ -0,0 +1,31 @@ | |||
package webfinger | |||
import ( | |||
"context" | |||
"net/http" | |||
) | |||
// ErrorKeyType is the type for the context error key | |||
type ErrorKeyType int | |||
// ErrorKey is the key for the context error | |||
var ErrorKey ErrorKeyType | |||
// ErrorFromContext gets the error from the context | |||
func ErrorFromContext(ctx context.Context) error { | |||
v, ok := ctx.Value(ErrorKey).(error) | |||
if !ok { | |||
return nil | |||
} | |||
return v | |||
} | |||
func addError(r *http.Request, err error) *http.Request { | |||
if err == nil { | |||
return r | |||
} | |||
ctx := r.Context() | |||
ctx = context.WithValue(ctx, ErrorKey, err) | |||
r = r.WithContext(ctx) | |||
return r | |||
} |
@@ -0,0 +1,47 @@ | |||
package webfinger | |||
import ( | |||
"context" | |||
"errors" | |||
"net/http" | |||
"testing" | |||
) | |||
func TestAddAndGetError(t *testing.T) { | |||
{ // test with no error | |||
ctx := context.Background() | |||
r := &http.Request{} | |||
r = r.WithContext(ctx) | |||
err2 := ErrorFromContext(r.Context()) | |||
if err2 != nil { | |||
t.Errorf("No error should result in no error") | |||
} | |||
} | |||
{ // Test with addError(nil) | |||
ctx := context.Background() | |||
r := &http.Request{} | |||
r = r.WithContext(ctx) | |||
r = addError(r, nil) | |||
err2 := ErrorFromContext(r.Context()) | |||
if err2 != nil { | |||
t.Errorf("Error was nil, is now not nil") | |||
} | |||
} | |||
{ // Test with addError(errors.New("X")) | |||
ctx := context.Background() | |||
r := &http.Request{} | |||
r = r.WithContext(ctx) | |||
r = addError(r, errors.New("X")) | |||
err2 := ErrorFromContext(r.Context()) | |||
if err2 == nil || err2.Error() != "X" { | |||
t.Errorf("Err is %v, expected 'X'", err2) | |||
} | |||
} | |||
} |
@@ -0,0 +1,113 @@ | |||
package webfinger | |||
import ( | |||
"encoding/json" | |||
"errors" | |||
"net/http" | |||
) | |||
// WebFingerPath defines the default path of the webfinger handler. | |||
const WebFingerPath = "/.well-known/webfinger" | |||
func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
//TODO: support host-meta as a path | |||
path := r.URL.Path | |||
switch path { | |||
case WebFingerPath: | |||
s.Webfinger(w, r) | |||
default: | |||
s.NotFoundHandler.ServeHTTP(w, r) | |||
} | |||
} | |||
// Webfinger is the webfinger handler | |||
func (s *Service) Webfinger(w http.ResponseWriter, r *http.Request) { | |||
s.runPrehandlers(w, r) | |||
if r.TLS == nil && s.NoTLSHandler != nil { | |||
s.NoTLSHandler.ServeHTTP(w, r) | |||
return | |||
} | |||
//NOTE: should this run before or after the pre-run handlers? | |||
if r.Method != "GET" { | |||
s.MethodNotSupportedHandler.ServeHTTP(w, r) | |||
return | |||
} | |||
if len(r.URL.Query()["resource"]) != 1 { | |||
s.MalformedRequestHandler.ServeHTTP(w, addError(r, errors.New("Malformed resource parameter"))) | |||
return | |||
} | |||
resource := r.URL.Query().Get("resource") | |||
var a account | |||
if err := a.ParseString(resource); err != nil { | |||
s.MalformedRequestHandler.ServeHTTP(w, addError(r, err)) | |||
return | |||
} | |||
relStrings := r.URL.Query()["rel"] | |||
var rels []Rel | |||
for _, r := range relStrings { | |||
rels = append(rels, Rel(r)) | |||
} | |||
rsc, err := s.Resolver.FindUser(a.Name, a.Hostname, rels) | |||
if err != nil { | |||
if !s.Resolver.IsNotFoundError(err) { | |||
s.ErrorHandler.ServeHTTP(w, addError(r, err)) | |||
return | |||
} | |||
rsc, err = s.Resolver.DummyUser(a.Name, a.Hostname, rels) | |||
if err != nil && !s.Resolver.IsNotFoundError(err) { | |||
s.ErrorHandler.ServeHTTP(w, addError(r, err)) | |||
return | |||
} else if s.Resolver.IsNotFoundError(err) { | |||
s.NotFoundHandler.ServeHTTP(w, r) | |||
return | |||
} | |||
} | |||
if err := json.NewEncoder(w).Encode(&rsc); err != nil { | |||
s.ErrorHandler.ServeHTTP(w, addError(r, err)) | |||
return | |||
} | |||
} | |||
func (s *Service) runPrehandlers(w http.ResponseWriter, r *http.Request) { | |||
if s.PreHandlers == nil { | |||
return | |||
} | |||
for _, val := range s.PreHandlers { | |||
if val != nil { | |||
val.ServeHTTP(w, r) | |||
} | |||
} | |||
} | |||
func (s *Service) defaultErrorHandler(w http.ResponseWriter, r *http.Request) { | |||
w.WriteHeader(http.StatusInternalServerError) | |||
} | |||
func (s *Service) defaultNotFoundHandler(w http.ResponseWriter, r *http.Request) { | |||
w.WriteHeader(http.StatusNotFound) | |||
} | |||
func (s *Service) defaultMethodNotSupportedHandler(w http.ResponseWriter, r *http.Request) { | |||
w.WriteHeader(http.StatusMethodNotAllowed) | |||
} | |||
func (s *Service) defaultMalformedRequestHandler(w http.ResponseWriter, r *http.Request) { | |||
w.WriteHeader(http.StatusBadRequest) | |||
} | |||
func (s *Service) defaultNoTLSHandler(w http.ResponseWriter, r *http.Request) { | |||
u := *r.URL | |||
u.Scheme = "https" | |||
w.Header().Set("Location", u.String()) | |||
w.WriteHeader(http.StatusSeeOther) | |||
} |
@@ -0,0 +1,288 @@ | |||
package webfinger | |||
import ( | |||
"bytes" | |||
"net/http" | |||
"net/url" | |||
"sort" | |||
"testing" | |||
"reflect" | |||
"crypto/tls" | |||
"strings" | |||
"github.com/pkg/errors" | |||
) | |||
type dummyUserResolver struct { | |||
} | |||
func (d *dummyUserResolver) FindUser(username string, hostname string, rel []Rel) (*Resource, error) { | |||
if username == "hello" { | |||
if len(rel) == 2 && rel[0] == "x" && rel[1] == "y" { | |||
return &Resource{ | |||
Links: []Link{ | |||
Link{ | |||
HRef: string(rel[0]), | |||
Rel: string(rel[0]), | |||
}, | |||
Link{ | |||
HRef: string(rel[1]), | |||
Rel: string(rel[1]), | |||
}, | |||
}, | |||
}, nil | |||
} | |||
return &Resource{ | |||
Links: []Link{ | |||
Link{ | |||
HRef: string("x"), | |||
Rel: string("x"), | |||
}, | |||
Link{ | |||
HRef: string("y"), | |||
Rel: string("y"), | |||
}, | |||
Link{ | |||
HRef: string("z"), | |||
Rel: string("z"), | |||
}, | |||
}, | |||
}, nil | |||
} | |||
return nil, errors.New("User not found") | |||
} | |||
func (d *dummyUserResolver) DummyUser(username string, hostname string, rel []Rel) (*Resource, error) { | |||
return nil, errors.New("User not found") | |||
} | |||
func (d *dummyUserResolver) IsNotFoundError(err error) bool { | |||
if err == nil { | |||
return false | |||
} | |||
if err.Error() == "User not found" { | |||
return true | |||
} | |||
if errors.Cause(err).Error() == "User not found" { | |||
return true | |||
} | |||
return false | |||
} | |||
type dummyResponseWriter struct { | |||
bytes.Buffer | |||
code int | |||
headers http.Header | |||
} | |||
func (d *dummyResponseWriter) Header() http.Header { | |||
if d.headers == nil { | |||
d.headers = make(http.Header) | |||
} | |||
return d.headers | |||
} | |||
func (d *dummyResponseWriter) WriteHeader(i int) { | |||
d.code = i | |||
} | |||
type serveHTTPTest struct { | |||
Description string | |||
Input *http.Request | |||
OutputCode int | |||
OutputHeaders http.Header | |||
OutputBody string | |||
} | |||
type kv struct { | |||
key string | |||
val string | |||
} | |||
func buildRequest(method string, path string, query string, kvx ...kv) *http.Request { | |||
r := &http.Request{} | |||
r.Host = "http://localhost" | |||
r.Header = make(http.Header) | |||
r.URL = &url.URL{ | |||
Path: path, | |||
RawQuery: query, | |||
Host: "localhost", | |||
Scheme: "http", | |||
} | |||
for _, k := range kvx { | |||
r.Header.Add(k.key, k.val) | |||
} | |||
r.Method = method | |||
return r | |||
} | |||
func buildRequestTLS(method string, path string, query string, kvx ...kv) *http.Request { | |||
r := &http.Request{} | |||
r.Host = "https://localhost" | |||
r.Header = make(http.Header) | |||
r.URL = &url.URL{ | |||
Path: path, | |||
RawQuery: query, | |||
Host: "localhost", | |||
Scheme: "https", | |||
} | |||
r.TLS = &tls.ConnectionState{} // marks the request as TLS | |||
for _, k := range kvx { | |||
r.Header.Add(k.key, k.val) | |||
} | |||
r.Method = method | |||
return r | |||
} | |||
var defaultHeaders = http.Header(map[string][]string{ | |||
"Cache-Control": []string{"no-cache"}, | |||
"Pragma": []string{"no-cache"}, | |||
"Content-Type": []string{"application/jrd+json"}, | |||
}) | |||
func plusHeader(h http.Header, kvx ...kv) http.Header { | |||
var h2 = make(http.Header) | |||
for k, vx := range h { | |||
for _, v := range vx { | |||
h2.Add(k, v) | |||
} | |||
} | |||
for _, k := range kvx { | |||
h2.Add(k.key, k.val) | |||
} | |||
return h2 | |||
} | |||
func compareHeaders(h1 http.Header, h2 http.Header) bool { | |||
if len(h1) != len(h2) { | |||
return false | |||
} | |||
keys := []string{} | |||
for k := range h1 { | |||
keys = append(keys, k) | |||
} | |||
sort.Strings(keys) | |||
for _, k := range keys { | |||
if !reflect.DeepEqual(h1[k], h2[k]) { | |||
return false | |||
} | |||
} | |||
return true | |||
} | |||
func TestServiceServeHTTP(t *testing.T) { | |||
var tests = []serveHTTPTest{ | |||
{"GET root URL should return 404 not found with no headers", | |||
buildRequestTLS("GET", "/", ""), http.StatusNotFound, make(http.Header), ""}, | |||
{"POST root URL should return 404 not found with no headers", | |||
buildRequestTLS("POST", "/", ""), http.StatusNotFound, make(http.Header), ""}, | |||
{"GET /.well-known should return 404 not found with no headers", | |||
buildRequestTLS("GET", "/.well-known", ""), http.StatusNotFound, make(http.Header), ""}, | |||
{"POST /.well-known should return 404 not found with no headers", | |||
buildRequestTLS("POST", "/.well-known", ""), http.StatusNotFound, make(http.Header), ""}, | |||
/* | |||
4.2. Performing a WebFinger Query | |||
*/ | |||
/* | |||
A WebFinger client issues a query using the GET method to the well- | |||
known [3] resource identified by the URI whose path component is | |||
"/.well-known/webfinger" and whose query component MUST include the | |||
"resource" parameter exactly once and set to the value of the URI for | |||
which information is being sought. | |||
*/ | |||
{"GET Webfinger resource should return valid response with default headers", | |||
buildRequestTLS("GET", WebFingerPath, "resource=acct:hello@domain"), http.StatusOK, defaultHeaders, | |||
`{"links":[{"href":"x","ref":"x"},{"href":"y","ref":"y"},{"href":"z","ref":"z"}]}`}, | |||
{"POST Webfinger URL should fail with MethodNotAllowed", | |||
buildRequestTLS("POST", WebFingerPath, ""), http.StatusMethodNotAllowed, defaultHeaders, ""}, | |||
{"GET multiple resources should fail with BadRequest", | |||
buildRequestTLS("GET", WebFingerPath, "resource=acct:hello@domain&resource=acct:hello2@domain"), http.StatusBadRequest, defaultHeaders, ""}, | |||
/* | |||
The "rel" parameter MAY be included multiple times in order to | |||
request multiple link relation types. | |||
*/ | |||
{"GET Webfinger resource with rel should filter results", | |||
buildRequestTLS("GET", WebFingerPath, "resource=acct:hello@domain&rel=x&rel=y"), http.StatusOK, defaultHeaders, | |||
`{"links":[{"href":"x","ref":"x"},{"href":"y","ref":"y"}]}`}, | |||
/* | |||
A WebFinger resource MAY redirect the client; if it does, the | |||
redirection MUST only be to an "https" URI and the client MUST | |||
perform certificate validation again when redirected. | |||
*/ | |||
{"GET non-TLS should redirect to TLS", | |||
buildRequest("GET", WebFingerPath, "resource=acct:hello@domain"), http.StatusSeeOther, plusHeader( | |||
defaultHeaders, kv{"Location", "https://localhost/.well-known/webfinger?resource=acct:hello@domain"}), ""}, | |||
/* | |||
A WebFinger resource MUST return a JRD as the representation for the | |||
resource if the client requests no other supported format explicitly | |||
via the HTTP "Accept" header. The client MAY include the "Accept" | |||
header to indicate a desired representation; representations other | |||
than JRD might be defined in future specifications. The WebFinger | |||
resource MUST silently ignore any requested representations that it | |||
does not understand or support. The media type used for the JSON | |||
Resource Descriptor (JRD) is "application/jrd+json" (see Section | |||
10.2). | |||
*/ | |||
{"GET with Accept should return as normal", | |||
buildRequestTLS("GET", WebFingerPath, "resource=acct:hello@domain", kv{"Accept", "application/json"}), http.StatusOK, defaultHeaders, | |||
`{"links":[{"href":"x","ref":"x"},{"href":"y","ref":"y"},{"href":"z","ref":"z"}]}`}, | |||
/* | |||
If the "resource" parameter is a value for which the server has no | |||
information, the server MUST indicate that it was unable to match the | |||
request as per Section 10.4.5 of RFC 2616. | |||
*/ | |||
{"GET with a missing user should return 404", | |||
buildRequestTLS("GET", WebFingerPath, "resource=acct:missinguser@domain"), http.StatusNotFound, defaultHeaders, ""}, | |||
/* | |||
If the "resource" parameter is absent or malformed, the WebFinger | |||
resource MUST indicate that the request is bad as per Section 10.4.1 | |||
of RFC 2616 [2]. (400 bad request) | |||
*/ | |||
{"GET with no resource should fail with BadRequest", | |||
buildRequestTLS("GET", WebFingerPath, ""), http.StatusBadRequest, defaultHeaders, ""}, | |||
{"GET with malformed resource URI should fail with BadRequest", | |||
buildRequestTLS("GET", WebFingerPath, "resource=hello-world"), http.StatusBadRequest, defaultHeaders, ""}, | |||
{"GET with http resource URI should fail with BadRequest", | |||
buildRequestTLS("GET", WebFingerPath, "resource=http://hello-world"), http.StatusBadRequest, defaultHeaders, ""}, | |||
} | |||
svc := Default(&dummyUserResolver{}) | |||
for _, tx := range tests { | |||
w := &dummyResponseWriter{code: 200} | |||
svc.ServeHTTP(w, tx.Input) | |||
// code should be 404 | |||
// headers should be empty | |||
body := strings.TrimSpace(string(w.Buffer.Bytes())) | |||
failed := false | |||
failed = failed || w.code != tx.OutputCode | |||
failed = failed || !compareHeaders(tx.OutputHeaders, w.headers) | |||
failed = failed || body != tx.OutputBody | |||
if failed { | |||
t.Errorf("%s\nHTTP '%v' '%v' => \n\t'%v'\n\t'%v'\n\t'%v';\nexpected \n\t'%v \n\t'%v' \n\t'%v'", | |||
tx.Description, | |||
tx.Input.Method, tx.Input.URL, | |||
w.code, w.headers, body, | |||
tx.OutputCode, tx.OutputHeaders, tx.OutputBody) | |||
} | |||
} | |||
} |
@@ -0,0 +1,13 @@ | |||
package webfinger | |||
// A Link is a series of user details | |||
type Link struct { | |||
HRef string `json:"href"` | |||
Type string `json:"type,omitempty"` | |||
Rel string `json:"ref"` | |||
Properties map[string]*string `json:"properties,omitempty"` | |||
Titles map[string]string `json:"titles,omitempty"` | |||
} | |||
// Rel allows referencing a subset of the users details | |||
type Rel string |
@@ -0,0 +1,21 @@ | |||
package webfinger | |||
import "net/http" | |||
// Middleware constant keys | |||
const ( | |||
NoCacheMiddleware string = "NoCache" | |||
CorsMiddleware string = "Cors" | |||
ContentTypeMiddleware string = "Content-Type" | |||
) | |||
// noCache sets the headers to disable caching | |||
func noCache(w http.ResponseWriter, r *http.Request) { | |||
w.Header().Set("Cache-Control", "no-cache") | |||
w.Header().Set("Pragma", "no-cache") | |||
} | |||
// jrdSetup sets the content-type | |||
func jrdSetup(w http.ResponseWriter, r *http.Request) { | |||
w.Header().Set("Content-Type", "application/jrd+json") | |||
} |
@@ -0,0 +1,21 @@ | |||
package webfinger | |||
// The Resolver is how the webfinger service looks up a user or resource. The resolver | |||
// must be provided by the developer using this package, as each webfinger | |||
// service may be exposing a different set or users or resources or services. | |||
type Resolver interface { | |||
// FindUser finds the user given the username and hostname. | |||
FindUser(username string, hostname string, r []Rel) (*Resource, error) | |||
// DummyUser allows us to return a dummy user to avoid user-enumeration via webfinger 404s. This | |||
// can be done in the webfinger code itself but then it would be obvious which users are real | |||
// and which are not real via differences in how the implementation works vs how | |||
// the general webfinger code works. This does not match the webfinger specification | |||
// but is an extra precaution. Returning a NotFound error here will | |||
// keep the webfinger 404 behavior. | |||
DummyUser(username string, hostname string, r []Rel) (*Resource, error) | |||
// IsNotFoundError returns true if the given error is a not found error. | |||
IsNotFoundError(err error) bool | |||
} |
@@ -0,0 +1,9 @@ | |||
package webfinger | |||
// A Resource is the top-level JRD resource object. | |||
type Resource struct { | |||
Subject string `json:"subject,omitempty"` | |||
Aliases []string `json:"aliases,omitempty"` | |||
Properties map[string]string `json:"properties,omitempty"` | |||
Links []Link `json:"links"` | |||
} |
@@ -0,0 +1,65 @@ | |||
package webfinger | |||
import ( | |||
"net/http" | |||
"github.com/captncraig/cors" | |||
) | |||
// Service is the webfinger service containing the required | |||
// HTTP handlers and defaults for webfinger implementations. | |||
type Service struct { | |||
// PreHandlers are invoked at the start of each HTTP method, used to | |||
// setup things like CORS, caching, etc. You can delete or replace | |||
// a handler by setting service.PreHandlers[name] = nil | |||
PreHandlers map[string]http.Handler | |||
// NotFoundHandler is the handler to invoke when a URL is not matched. It does NOT | |||
// handle the case of a non-existing users unless your Resolver.DummyUser returns | |||
// an error that matches Resolver.IsNotFoundError(err) == true. | |||
NotFoundHandler http.Handler | |||
// MethodNotSupportedHandler is the handler invoked when an unsupported | |||
// method is called on the webfinger HTTP service. | |||
MethodNotSupportedHandler http.Handler | |||
// MalformedRequestHandler is the handler invoked if the request routes | |||
// but is malformed in some way. The default behavior is to return 400 BadRequest, | |||
// per the webfinger specification | |||
MalformedRequestHandler http.Handler | |||
// NoTLSHandler is the handler invoked if the request is not | |||
// a TLS request. The default behavior is to redirect to the TLS | |||
// version of the URL, per the webfinger specification. Setting | |||
// this to nil will allow nonTLS connections, but that is not advised. | |||
NoTLSHandler http.Handler | |||
// ErrorHandler is the handler invoked when an error is called. The request | |||
// context contains the error in the webfinger.ErrorKey and can be fetched | |||
// via webfinger.ErrorFromContext(ctx) | |||
ErrorHandler http.Handler | |||
// Resolver is the interface for resolving user details | |||
Resolver Resolver | |||
} | |||
// Default creates a new service with the default registered handlers | |||
func Default(ur Resolver) *Service { | |||
var c = cors.Default() | |||
s := &Service{} | |||
s.Resolver = ur | |||
s.ErrorHandler = http.HandlerFunc(s.defaultErrorHandler) | |||
s.NotFoundHandler = http.HandlerFunc(s.defaultNotFoundHandler) | |||
s.MethodNotSupportedHandler = http.HandlerFunc(s.defaultMethodNotSupportedHandler) | |||
s.MalformedRequestHandler = http.HandlerFunc(s.defaultMalformedRequestHandler) | |||
s.NoTLSHandler = http.HandlerFunc(s.defaultNoTLSHandler) | |||
s.PreHandlers = make(map[string]http.Handler) | |||
s.PreHandlers[NoCacheMiddleware] = http.HandlerFunc(noCache) | |||
s.PreHandlers[CorsMiddleware] = http.HandlerFunc(c.HandleRequest) | |||
s.PreHandlers[ContentTypeMiddleware] = http.HandlerFunc(jrdSetup) | |||
return s | |||
} |