Browse Source

Initial commit of webfinger service

tags/0.1.0
Sheena Artrip 7 years ago
commit
d773028eb1
No known key found for this signature in database GPG Key ID: C7DC7E41D1A6909D
14 changed files with 787 additions and 0 deletions
  1. +21
    -0
      LICENSE
  2. +54
    -0
      README.md
  3. +30
    -0
      account.go
  4. +51
    -0
      account_test.go
  5. +23
    -0
      doc.go
  6. +31
    -0
      error.go
  7. +47
    -0
      error_test.go
  8. +113
    -0
      http.go
  9. +288
    -0
      http_test.go
  10. +13
    -0
      link.go
  11. +21
    -0
      middleware.go
  12. +21
    -0
      resolver.go
  13. +9
    -0
      resource.go
  14. +65
    -0
      service.go

+ 21
- 0
LICENSE View File

@@ -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.

+ 54
- 0
README.md View File

@@ -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].`

+ 30
- 0
account.go View File

@@ -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
}

+ 51
- 0
account_test.go View File

@@ -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())
}
}
}

+ 23
- 0
doc.go View File

@@ -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

+ 31
- 0
error.go View File

@@ -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
}

+ 47
- 0
error_test.go View File

@@ -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)
}
}

}

+ 113
- 0
http.go View File

@@ -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)
}

+ 288
- 0
http_test.go View File

@@ -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)
}
}

}

+ 13
- 0
link.go View File

@@ -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

+ 21
- 0
middleware.go View File

@@ -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")
}

+ 21
- 0
resolver.go View File

@@ -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
}

+ 9
- 0
resource.go View File

@@ -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"`
}

+ 65
- 0
service.go View File

@@ -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
}

Loading…
Cancel
Save