[feature] Provide .well-known/host-meta endpoint (#1604)

* [feature] Provide .well-known/host-meta endpoint

This adds the host-meta endpoint as Mastodon clients use this to
discover the API domain to use when the host and account domains aren't
the same.

* Address review comments
This commit is contained in:
Daenney 2023-03-09 18:55:45 +01:00 committed by GitHub
parent 9ba35c65eb
commit a312238e79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 167 additions and 4 deletions

View file

@ -43,6 +43,9 @@ host: "localhost"
# to "gts.example.org/.well-known/webfinger" so that GtS can handle them properly.
#
# You should also redirect requests at "example.org/.well-known/nodeinfo" in the same way.
#
# You should also redirect requests at "example.org/.well-known/host-meta" in the same way. This endpoint is used by a number of clients to discover the API endpoint to use when the host and account domain are different.
#
# An empty string (ie., not set) means that the same value as 'host' will be used.
#
# DO NOT change this after your server has already run once, or you will break things!

View file

@ -32,6 +32,9 @@ host: "localhost"
# to "gts.example.org/.well-known/webfinger" so that GtS can handle them properly.
#
# You should also redirect requests at "example.org/.well-known/nodeinfo" in the same way.
#
# You should also redirect requests at "example.org/.well-known/host-meta" in the same way. This endpoint is used by a number of clients to discover the API endpoint to use when the host and account domain are different.
#
# An empty string (ie., not set) means that the same value as 'host' will be used.
#
# DO NOT change this after your server has already run once, or you will break things!
@ -71,6 +74,10 @@ http {
rewrite ^.*$ https://fedi.example.org/.well-known/webfinger permanent;
}
location /.well-known/host-meta {
rewrite ^.*$ https://fedi.example.org/.well-known/host-meta permanent;
}
location /.well-known/nodeinfo {
rewrite ^.*$ https://fedi.example.org/.well-known/nodeinfo permanent;
}
@ -91,7 +98,7 @@ If `example.org` is running on [Traefik](https://doc.traefik.io/traefik/), we co
labels:
- 'traefik.http.routers.myservice.rule=Host(`example.org`)'
- 'traefik.http.middlewares.myservice-gts.redirectregex.permanent=true'
- 'traefik.http.middlewares.myservice-gts.redirectregex.regex=^https://(.*)/.well-known/(webfinger|nodeinfo)$$'
- 'traefik.http.middlewares.myservice-gts.redirectregex.regex=^https://(.*)/.well-known/(webfinger|nodeinfo|host-meta)$$'
- 'traefik.http.middlewares.myservice-gts.redirectregex.replacement=https://fedi.$${1}/.well-known/$${2}'
- 'traefik.http.routers.myservice.middlewares=myservice-gts@docker'
```
@ -279,9 +286,9 @@ This section contains a number of additional things for configuring nginx.
If you want to harden up your NGINX deployment with advanced configuration options, there are many guides online for doing so ([for example](https://beaglesecurity.com/blog/article/nginx-server-security.html)). Try to find one that's up to date. Mozilla also publishes best-practice ssl configuration [here](https://ssl-config.mozilla.org/).
### Caching Webfinger and Public Key responses
### Caching Webfinger, Webhost Metadata and Public Key responses
It's possible to use nginx to cache webfinger and public key responses. This may be useful in order to ensure clients still get a response on these endpoints even if your GoToSocial instance is (temporarily) down, or requests are being throttled.
It's possible to use nginx to cache webfinger, host-meta and public key responses. This may be useful in order to ensure clients still get a response on these endpoints even if your GoToSocial instance is (temporarily) down, or requests are being throttled.
You'll need to configure two things:
@ -311,7 +318,7 @@ server {
### NEW STUFF STARTS HERE ###
location /.well-known/webfinger {
location ~ /.well-known/(webfinger|host-meta)$ {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;

View file

@ -55,6 +55,11 @@ host: "localhost"
# to "gts.example.org/.well-known/webfinger" so that GtS can handle them properly.
#
# You should also redirect requests at "example.org/.well-known/nodeinfo" in the same way.
#
# You should also redirect requests at "example.org/.well-known/host-meta" in the same way. This endpoint
# is used by a number of clients to discover the API endpoint to use when the host and account domain are
# different.
#
# An empty string (ie., not set) means that the same value as 'host' will be used.
#
# DO NOT change this after your server has already run once, or you will break things!

View file

@ -25,6 +25,7 @@
const (
AppJSON MIME = `application/json`
AppXML MIME = `application/xml`
AppXMLXRD MIME = `application/xrd+xml`
AppRSSXML MIME = `application/rss+xml`
AppActivityJSON MIME = `application/activity+json`
AppActivityLDJSON MIME = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`

View file

@ -58,6 +58,11 @@
AppActivityLDJSON,
}
var HostMetaHeaders = []MIME{
AppXMLXRD,
AppXML,
}
// NegotiateAccept takes the *gin.Context from an incoming request, and a
// slice of Offers, and performs content negotiation for the given request
// with the given content-type offers. It will return a string representation

View file

@ -20,6 +20,7 @@
import (
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/hostmeta"
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/nodeinfo"
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger"
"github.com/superseriousbusiness/gotosocial/internal/middleware"
@ -30,6 +31,7 @@
type WellKnown struct {
nodeInfo *nodeinfo.Module
webfinger *webfinger.Module
hostMeta *hostmeta.Module
}
func (w *WellKnown) Route(r router.Router, m ...gin.HandlerFunc) {
@ -45,11 +47,13 @@ func (w *WellKnown) Route(r router.Router, m ...gin.HandlerFunc) {
w.nodeInfo.Route(wellKnownGroup.Handle)
w.webfinger.Route(wellKnownGroup.Handle)
w.hostMeta.Route(wellKnownGroup.Handle)
}
func NewWellKnown(p *processing.Processor) *WellKnown {
return &WellKnown{
nodeInfo: nodeinfo.New(p),
webfinger: webfinger.New(p),
hostMeta: hostmeta.New(p),
}
}

View file

@ -0,0 +1,45 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package hostmeta
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/processing"
)
const (
HostMetaContentType = "application/xrd+xml"
HostMetaPath = "/host-meta"
)
type Module struct {
processor *processing.Processor
}
func New(processor *processing.Processor) *Module {
return &Module{
processor: processor,
}
}
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
attachHandler(http.MethodGet, HostMetaPath, m.HostMetaGETHandler)
}

View file

@ -0,0 +1,73 @@
/*
GoToSocial
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package hostmeta
import (
"bytes"
"encoding/xml"
"net/http"
"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// HostMetaGETHandler swagger:operation GET /.well-known/host-meta hostMetaGet
//
// Returns a compliant hostmeta response to web host metadata queries.
//
// See: https://www.rfc-editor.org/rfc/rfc6415.html
//
// ---
// tags:
// - .well-known
//
// produces:
// - application/xrd+xml"
//
// responses:
// '200':
// schema:
// "$ref": "#/definitions/hostmeta"
func (m *Module) HostMetaGETHandler(c *gin.Context) {
if _, err := apiutil.NegotiateAccept(c, apiutil.HostMetaHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
hostMeta := m.processor.Fedi().HostMetaGet()
// this setup with a separate buffer we encode into is used because
// xml.Marshal does not emit xml.Header by itself
var buf bytes.Buffer
// Preallocate buffer of reasonable length.
buf.Grow(len(xml.Header) + 64)
// No need to check for error on write to buffer.
_, _ = buf.WriteString(xml.Header)
// Encode host-meta as XML to in-memory buffer.
if err := xml.NewEncoder(&buf).Encode(hostMeta); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
return
}
c.Data(http.StatusOK, HostMetaContentType, buf.Bytes())
}

View file

@ -28,6 +28,10 @@
)
const (
hostMetaXMLNS = "http://docs.oasis-open.org/ns/xri/xrd-1.0"
hostMetaRel = "lrdd"
hostMetaType = "application/xrd+xml"
hostMetaTemplate = ".well-known/webfinger?resource={uri}"
nodeInfoVersion = "2.0"
nodeInfoSoftwareName = "gotosocial"
nodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/" + nodeInfoVersion
@ -96,6 +100,22 @@ func (p *Processor) NodeInfoGet(ctx context.Context) (*apimodel.Nodeinfo, gtserr
}, nil
}
// HostMetaGet returns a host-meta struct in response to a host-meta request.
func (p *Processor) HostMetaGet() *apimodel.HostMeta {
protocol := config.GetProtocol()
host := config.GetHost()
return &apimodel.HostMeta{
XMLNS: hostMetaXMLNS,
Link: []apimodel.Link{
{
Rel: hostMetaRel,
Type: hostMetaType,
Template: fmt.Sprintf("%s://%s/%s", protocol, host, hostMetaTemplate),
},
},
}
}
// WebfingerGet handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
func (p *Processor) WebfingerGet(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) {
// Get the local account the request is referring to.