[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:
parent
9ba35c65eb
commit
a312238e79
|
@ -43,6 +43,9 @@ host: "localhost"
|
||||||
# to "gts.example.org/.well-known/webfinger" so that GtS can handle them properly.
|
# 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/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.
|
# 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!
|
# DO NOT change this after your server has already run once, or you will break things!
|
||||||
|
|
|
@ -32,6 +32,9 @@ host: "localhost"
|
||||||
# to "gts.example.org/.well-known/webfinger" so that GtS can handle them properly.
|
# 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/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.
|
# 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!
|
# 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;
|
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 {
|
location /.well-known/nodeinfo {
|
||||||
rewrite ^.*$ https://fedi.example.org/.well-known/nodeinfo permanent;
|
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:
|
labels:
|
||||||
- 'traefik.http.routers.myservice.rule=Host(`example.org`)'
|
- 'traefik.http.routers.myservice.rule=Host(`example.org`)'
|
||||||
- 'traefik.http.middlewares.myservice-gts.redirectregex.permanent=true'
|
- '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.middlewares.myservice-gts.redirectregex.replacement=https://fedi.$${1}/.well-known/$${2}'
|
||||||
- 'traefik.http.routers.myservice.middlewares=myservice-gts@docker'
|
- '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/).
|
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:
|
You'll need to configure two things:
|
||||||
|
|
||||||
|
@ -311,7 +318,7 @@ server {
|
||||||
|
|
||||||
### NEW STUFF STARTS HERE ###
|
### NEW STUFF STARTS HERE ###
|
||||||
|
|
||||||
location /.well-known/webfinger {
|
location ~ /.well-known/(webfinger|host-meta)$ {
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
|
@ -55,6 +55,11 @@ host: "localhost"
|
||||||
# to "gts.example.org/.well-known/webfinger" so that GtS can handle them properly.
|
# 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/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.
|
# 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!
|
# DO NOT change this after your server has already run once, or you will break things!
|
||||||
|
|
|
@ -25,6 +25,7 @@ type MIME string
|
||||||
const (
|
const (
|
||||||
AppJSON MIME = `application/json`
|
AppJSON MIME = `application/json`
|
||||||
AppXML MIME = `application/xml`
|
AppXML MIME = `application/xml`
|
||||||
|
AppXMLXRD MIME = `application/xrd+xml`
|
||||||
AppRSSXML MIME = `application/rss+xml`
|
AppRSSXML MIME = `application/rss+xml`
|
||||||
AppActivityJSON MIME = `application/activity+json`
|
AppActivityJSON MIME = `application/activity+json`
|
||||||
AppActivityLDJSON MIME = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
|
AppActivityLDJSON MIME = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
|
||||||
|
|
|
@ -58,6 +58,11 @@ var HTMLOrActivityPubHeaders = []MIME{
|
||||||
AppActivityLDJSON,
|
AppActivityLDJSON,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var HostMetaHeaders = []MIME{
|
||||||
|
AppXMLXRD,
|
||||||
|
AppXML,
|
||||||
|
}
|
||||||
|
|
||||||
// NegotiateAccept takes the *gin.Context from an incoming request, and a
|
// NegotiateAccept takes the *gin.Context from an incoming request, and a
|
||||||
// slice of Offers, and performs content negotiation for the given request
|
// slice of Offers, and performs content negotiation for the given request
|
||||||
// with the given content-type offers. It will return a string representation
|
// with the given content-type offers. It will return a string representation
|
||||||
|
|
|
@ -20,6 +20,7 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"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/nodeinfo"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger"
|
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/middleware"
|
"github.com/superseriousbusiness/gotosocial/internal/middleware"
|
||||||
|
@ -30,6 +31,7 @@ import (
|
||||||
type WellKnown struct {
|
type WellKnown struct {
|
||||||
nodeInfo *nodeinfo.Module
|
nodeInfo *nodeinfo.Module
|
||||||
webfinger *webfinger.Module
|
webfinger *webfinger.Module
|
||||||
|
hostMeta *hostmeta.Module
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WellKnown) Route(r router.Router, m ...gin.HandlerFunc) {
|
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.nodeInfo.Route(wellKnownGroup.Handle)
|
||||||
w.webfinger.Route(wellKnownGroup.Handle)
|
w.webfinger.Route(wellKnownGroup.Handle)
|
||||||
|
w.hostMeta.Route(wellKnownGroup.Handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWellKnown(p *processing.Processor) *WellKnown {
|
func NewWellKnown(p *processing.Processor) *WellKnown {
|
||||||
return &WellKnown{
|
return &WellKnown{
|
||||||
nodeInfo: nodeinfo.New(p),
|
nodeInfo: nodeinfo.New(p),
|
||||||
webfinger: webfinger.New(p),
|
webfinger: webfinger.New(p),
|
||||||
|
hostMeta: hostmeta.New(p),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -28,6 +28,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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"
|
nodeInfoVersion = "2.0"
|
||||||
nodeInfoSoftwareName = "gotosocial"
|
nodeInfoSoftwareName = "gotosocial"
|
||||||
nodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/" + nodeInfoVersion
|
nodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/" + nodeInfoVersion
|
||||||
|
@ -96,6 +100,22 @@ func (p *Processor) NodeInfoGet(ctx context.Context) (*apimodel.Nodeinfo, gtserr
|
||||||
}, nil
|
}, 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.
|
// 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) {
|
func (p *Processor) WebfingerGet(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) {
|
||||||
// Get the local account the request is referring to.
|
// Get the local account the request is referring to.
|
||||||
|
|
Loading…
Reference in New Issue