commit a312238e7909c6451e608a91c326ad250dda875c
parent 9ba35c65eb71d4ee586b6405e1c8566e895eb0ba
Author: Daenney <daenney@users.noreply.github.com>
Date: Thu, 9 Mar 2023 18:55:45 +0100
[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
Diffstat:
9 files changed, 167 insertions(+), 4 deletions(-)
diff --git a/docs/configuration/general.md b/docs/configuration/general.md
@@ -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!
diff --git a/docs/installation_guide/advanced.md b/docs/installation_guide/advanced.md
@@ -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;
diff --git a/example/config.yaml b/example/config.yaml
@@ -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!
diff --git a/internal/api/util/mime.go b/internal/api/util/mime.go
@@ -25,6 +25,7 @@ type MIME string
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"`
diff --git a/internal/api/util/negotiate.go b/internal/api/util/negotiate.go
@@ -58,6 +58,11 @@ var HTMLOrActivityPubHeaders = []MIME{
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
diff --git a/internal/api/wellknown.go b/internal/api/wellknown.go
@@ -20,6 +20,7 @@ package api
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 @@ import (
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),
}
}
diff --git a/internal/api/wellknown/hostmeta/hostmeta.go b/internal/api/wellknown/hostmeta/hostmeta.go
@@ -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)
+}
diff --git a/internal/api/wellknown/hostmeta/hostmetaget.go b/internal/api/wellknown/hostmeta/hostmetaget.go
@@ -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())
+}
diff --git a/internal/processing/fedi/wellknown.go b/internal/processing/fedi/wellknown.go
@@ -28,6 +28,10 @@ import (
)
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.