From c1e107266fc47e59657825f1178f5e79c78ab0e6 Mon Sep 17 00:00:00 2001 From: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com> Date: Thu, 24 Jun 2021 14:26:08 +0200 Subject: [PATCH] nodeinfo compliance (#61) --- internal/api/model/webfinger.go | 39 ------------ internal/api/model/well-known.go | 78 +++++++++++++++++++++++ internal/api/s2s/nodeinfo/nodeinfo.go | 59 +++++++++++++++++ internal/api/s2s/nodeinfo/nodeinfoget.go | 44 +++++++++++++ internal/api/s2s/nodeinfo/wellknownget.go | 44 +++++++++++++ internal/api/security/extraheaders.go | 2 +- internal/cliactions/server/server.go | 3 + internal/cliactions/testrig/testrig.go | 3 + internal/config/config.go | 5 +- internal/config/default.go | 6 ++ internal/federation/finger.go | 2 +- internal/processing/federation.go | 37 ++++++++++- internal/processing/processor.go | 8 ++- internal/typeutils/internaltofrontend.go | 1 + 14 files changed, 285 insertions(+), 46 deletions(-) delete mode 100644 internal/api/model/webfinger.go create mode 100644 internal/api/model/well-known.go create mode 100644 internal/api/s2s/nodeinfo/nodeinfo.go create mode 100644 internal/api/s2s/nodeinfo/nodeinfoget.go create mode 100644 internal/api/s2s/nodeinfo/wellknownget.go diff --git a/internal/api/model/webfinger.go b/internal/api/model/webfinger.go deleted file mode 100644 index bb5008949..000000000 --- a/internal/api/model/webfinger.go +++ /dev/null @@ -1,39 +0,0 @@ -package model - -/* - GoToSocial - Copyright (C) 2021 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 . -*/ - -// WebfingerAccountResponse represents the response to a webfinger request for an 'acct' resource. -// For example, it would be returned from https://example.org/.well-known/webfinger?resource=acct:some_username@example.org -// -// See https://webfinger.net/ -type WebfingerAccountResponse struct { - Subject string `json:"subject"` - Aliases []string `json:"aliases"` - Links []WebfingerLink `json:"links"` -} - -// WebfingerLink represents one 'link' in a slice of webfinger links returned from a lookup request. -// -// See https://webfinger.net/ -type WebfingerLink struct { - Rel string `json:"rel"` - Type string `json:"type,omitempty"` - Href string `json:"href,omitempty"` - Template string `json:"template,omitempty"` -} diff --git a/internal/api/model/well-known.go b/internal/api/model/well-known.go new file mode 100644 index 000000000..945215e4e --- /dev/null +++ b/internal/api/model/well-known.go @@ -0,0 +1,78 @@ +/* + GoToSocial + Copyright (C) 2021 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 . +*/ + +package model + +// WellKnownResponse represents the response to either a webfinger request for an 'acct' resource, or a request to nodeinfo. +// For example, it would be returned from https://example.org/.well-known/webfinger?resource=acct:some_username@example.org +// +// See https://webfinger.net/ +type WellKnownResponse struct { + Subject string `json:"subject,omitempty"` + Aliases []string `json:"aliases,omitempty"` + Links []Link `json:"links,omitempty"` +} + +// Link represents one 'link' in a slice of links returned from a lookup request. +// +// See https://webfinger.net/ +type Link struct { + Rel string `json:"rel"` + Type string `json:"type,omitempty"` + Href string `json:"href,omitempty"` + Template string `json:"template,omitempty"` +} + +// Nodeinfo represents a version 2.1 or version 2.0 nodeinfo schema. +// See: https://nodeinfo.diaspora.software/schema.html +type Nodeinfo struct { + // The schema version + Version string `json:"version"` + // Metadata about server software in use. + Software NodeInfoSoftware `json:"software"` + // The protocols supported on this server. + Protocols []string `json:"protocols"` + // The third party sites this server can connect to via their application API. + Services NodeInfoServices `json:"services"` + // Whether this server allows open self-registration. + OpenRegistrations bool `json:"openRegistrations"` + // Usage statistics for this server. + Usage NodeInfoUsage `json:"usage"` + // Free form key value pairs for software specific values. Clients should not rely on any specific key present. + Metadata map[string]interface{} `json:"metadata"` +} + +// NodeInfoSoftware represents the name and version number of the software of this node. +type NodeInfoSoftware struct { + Name string `json:"name"` + Version string `json:"version"` +} + +// NodeInfoServices represents inbound and outbound services that this node offers connections to. +type NodeInfoServices struct { + Inbound []string `json:"inbound"` + Outbound []string `json:"outbound"` +} + +// NodeInfoUsage represents usage information about this server, such as number of users. +type NodeInfoUsage struct { + Users NodeInfoUsers `json:"users"` +} + +// NodeInfoUsers is a stub for usage information, currently empty. +type NodeInfoUsers struct{} diff --git a/internal/api/s2s/nodeinfo/nodeinfo.go b/internal/api/s2s/nodeinfo/nodeinfo.go new file mode 100644 index 000000000..5febaf41f --- /dev/null +++ b/internal/api/s2s/nodeinfo/nodeinfo.go @@ -0,0 +1,59 @@ +/* + GoToSocial + Copyright (C) 2021 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 . +*/ + +package nodeinfo + +import ( + "net/http" + + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/processing" + "github.com/superseriousbusiness/gotosocial/internal/router" +) + +const ( + // NodeInfoWellKnownPath is the base path for serving responses to nodeinfo lookup requests. + NodeInfoWellKnownPath = ".well-known/nodeinfo" + // NodeInfoBasePath is the path for serving nodeinfo responses. + NodeInfoBasePath = "/nodeinfo/2.0" +) + +// Module implements the FederationModule interface +type Module struct { + config *config.Config + processor processing.Processor + log *logrus.Logger +} + +// New returns a new nodeinfo module +func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.FederationModule { + return &Module{ + config: config, + processor: processor, + log: log, + } +} + +// Route satisfies the FederationModule interface +func (m *Module) Route(s router.Router) error { + s.AttachHandler(http.MethodGet, NodeInfoWellKnownPath, m.NodeInfoWellKnownGETHandler) + s.AttachHandler(http.MethodGet, NodeInfoBasePath, m.NodeInfoGETHandler) + return nil +} diff --git a/internal/api/s2s/nodeinfo/nodeinfoget.go b/internal/api/s2s/nodeinfo/nodeinfoget.go new file mode 100644 index 000000000..a54c8b190 --- /dev/null +++ b/internal/api/s2s/nodeinfo/nodeinfoget.go @@ -0,0 +1,44 @@ +/* + GoToSocial + Copyright (C) 2021 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 . +*/ + +package nodeinfo + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// NodeInfoGETHandler returns a compliant nodeinfo response to node info queries. +// See: https://nodeinfo.diaspora.software/ +func (m *Module) NodeInfoGETHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "NodeInfoGETHandler", + "user-agent": c.Request.UserAgent(), + }) + + ni, err := m.processor.GetNodeInfo(c.Request) + if err != nil { + l.Debugf("error with get node info request: %s", err) + c.JSON(err.Code(), err.Safe()) + return + } + + c.JSON(http.StatusOK, ni) +} diff --git a/internal/api/s2s/nodeinfo/wellknownget.go b/internal/api/s2s/nodeinfo/wellknownget.go new file mode 100644 index 000000000..614d2a9c6 --- /dev/null +++ b/internal/api/s2s/nodeinfo/wellknownget.go @@ -0,0 +1,44 @@ +/* + GoToSocial + Copyright (C) 2021 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 . +*/ + +package nodeinfo + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// NodeInfoWellKnownGETHandler returns a well known response to a query to /.well-known/nodeinfo, +// directing (but not redirecting...) callers to the NodeInfoGETHandler. +func (m *Module) NodeInfoWellKnownGETHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "NodeInfoWellKnownGETHandler", + "user-agent": c.Request.UserAgent(), + }) + + niRel, err := m.processor.GetNodeInfoRel(c.Request) + if err != nil { + l.Debugf("error with get node info rel request: %s", err) + c.JSON(err.Code(), err.Safe()) + return + } + + c.JSON(http.StatusOK, niRel) +} diff --git a/internal/api/security/extraheaders.go b/internal/api/security/extraheaders.go index dfcddfbe1..bcb41e0c0 100644 --- a/internal/api/security/extraheaders.go +++ b/internal/api/security/extraheaders.go @@ -4,5 +4,5 @@ // ExtraHeaders adds any additional required headers to the response func (m *Module) ExtraHeaders(c *gin.Context) { - c.Header("Server", "Mastodon") + c.Header("Server", "gotosocial") } diff --git a/internal/cliactions/server/server.go b/internal/cliactions/server/server.go index c27ec7fac..775b622d7 100644 --- a/internal/cliactions/server/server.go +++ b/internal/cliactions/server/server.go @@ -26,6 +26,7 @@ "github.com/superseriousbusiness/gotosocial/internal/api/client/status" "github.com/superseriousbusiness/gotosocial/internal/api/client/streaming" "github.com/superseriousbusiness/gotosocial/internal/api/client/timeline" + "github.com/superseriousbusiness/gotosocial/internal/api/s2s/nodeinfo" "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" "github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger" "github.com/superseriousbusiness/gotosocial/internal/api/security" @@ -124,6 +125,7 @@ appsModule := app.New(c, processor, log) followRequestsModule := followrequest.New(c, processor, log) webfingerModule := webfinger.New(c, processor, log) + nodeInfoModule := nodeinfo.New(c, processor, log) webBaseModule := web.New(c, processor, log) usersModule := user.New(c, processor, log) timelineModule := timeline.New(c, processor, log) @@ -155,6 +157,7 @@ adminModule, statusModule, webfingerModule, + nodeInfoModule, usersModule, timelineModule, notificationModule, diff --git a/internal/cliactions/testrig/testrig.go b/internal/cliactions/testrig/testrig.go index 04832bfc1..88206cb76 100644 --- a/internal/cliactions/testrig/testrig.go +++ b/internal/cliactions/testrig/testrig.go @@ -28,6 +28,7 @@ "github.com/superseriousbusiness/gotosocial/internal/api/client/status" "github.com/superseriousbusiness/gotosocial/internal/api/client/streaming" "github.com/superseriousbusiness/gotosocial/internal/api/client/timeline" + "github.com/superseriousbusiness/gotosocial/internal/api/s2s/nodeinfo" "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" "github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger" "github.com/superseriousbusiness/gotosocial/internal/api/security" @@ -70,6 +71,7 @@ appsModule := app.New(c, processor, log) followRequestsModule := followrequest.New(c, processor, log) webfingerModule := webfinger.New(c, processor, log) + nodeInfoModule := nodeinfo.New(c, processor, log) webBaseModule := web.New(c, processor, log) usersModule := user.New(c, processor, log) timelineModule := timeline.New(c, processor, log) @@ -101,6 +103,7 @@ adminModule, statusModule, webfingerModule, + nodeInfoModule, usersModule, timelineModule, notificationModule, diff --git a/internal/config/config.go b/internal/config/config.go index b0263b170..3705c364f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -59,9 +59,9 @@ type Config struct { /* Not parsed from .yaml configuration file. - For short running commands (admin CLI tools etc). */ AccountCLIFlags map[string]string + SoftwareVersion string } // FromFile returns a new config from a file, or an error if something goes amiss. @@ -252,6 +252,8 @@ func (c *Config) ParseCLIFlags(f KeyedFlags) error { c.LetsEncryptConfig.EmailAddress = f.String(fn.LetsEncryptEmailAddress) } + c.SoftwareVersion = GetDefaults().SoftwareVersion + // command-specific flags // admin account CLI flags @@ -323,6 +325,7 @@ type Defaults struct { ConfigPath string Host string Protocol string + SoftwareVersion string DbType string DbAddress string diff --git a/internal/config/default.go b/internal/config/default.go index 89360bb26..2cc46c996 100644 --- a/internal/config/default.go +++ b/internal/config/default.go @@ -1,5 +1,7 @@ package config +const softwareVersion = "0.1.0-SNAPSHOT" + // TestDefault returns a default config for testing func TestDefault() *Config { defaults := GetTestDefaults() @@ -8,6 +10,7 @@ func TestDefault() *Config { ApplicationName: defaults.ApplicationName, Host: defaults.Host, Protocol: defaults.Protocol, + SoftwareVersion: defaults.SoftwareVersion, DBConfig: &DBConfig{ Type: defaults.DbType, Address: defaults.DbAddress, @@ -62,6 +65,7 @@ func Default() *Config { ApplicationName: defaults.ApplicationName, Host: defaults.Host, Protocol: defaults.Protocol, + SoftwareVersion: defaults.SoftwareVersion, DBConfig: &DBConfig{ Type: defaults.DbType, Address: defaults.DbAddress, @@ -117,6 +121,7 @@ func GetDefaults() Defaults { ConfigPath: "", Host: "", Protocol: "https", + SoftwareVersion: softwareVersion, DbType: "postgres", DbAddress: "localhost", @@ -163,6 +168,7 @@ func GetTestDefaults() Defaults { ConfigPath: "", Host: "localhost:8080", Protocol: "http", + SoftwareVersion: softwareVersion, DbType: "postgres", DbAddress: "localhost", diff --git a/internal/federation/finger.go b/internal/federation/finger.go index 9afe83edf..047f8c95a 100644 --- a/internal/federation/finger.go +++ b/internal/federation/finger.go @@ -41,7 +41,7 @@ func (f *federator) FingerRemoteAccount(requestingUsername string, targetUsernam return nil, fmt.Errorf("FingerRemoteAccount: error doing request on behalf of username %s while dereferencing @%s@%s: %s", requestingUsername, targetUsername, targetDomain, err) } - resp := &apimodel.WebfingerAccountResponse{} + resp := &apimodel.WellKnownResponse{} if err := json.Unmarshal(b, resp); err != nil { return nil, fmt.Errorf("FingerRemoteAccount: could not unmarshal server response as WebfingerAccountResponse on behalf of username %s while dereferencing @%s@%s: %s", requestingUsername, targetUsername, targetDomain, err) } diff --git a/internal/processing/federation.go b/internal/processing/federation.go index 5693caf90..ab84421d0 100644 --- a/internal/processing/federation.go +++ b/internal/processing/federation.go @@ -265,7 +265,7 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st return data, nil } -func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, gtserror.WithCode) { +func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) { // get the account the request is referring to requestedAccount := >smodel.Account{} if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { @@ -273,13 +273,13 @@ func (p *processor) GetWebfingerAccount(requestedUsername string, request *http. } // return the webfinger representation - return &apimodel.WebfingerAccountResponse{ + return &apimodel.WellKnownResponse{ Subject: fmt.Sprintf("acct:%s@%s", requestedAccount.Username, p.config.Host), Aliases: []string{ requestedAccount.URI, requestedAccount.URL, }, - Links: []apimodel.WebfingerLink{ + Links: []apimodel.Link{ { Rel: "http://webfinger.net/rel/profile-page", Type: "text/html", @@ -294,6 +294,37 @@ func (p *processor) GetWebfingerAccount(requestedUsername string, request *http. }, nil } +func (p *processor) GetNodeInfoRel(request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) { + return &apimodel.WellKnownResponse{ + Links: []apimodel.Link{ + { + Rel: "http://nodeinfo.diaspora.software/ns/schema/2.0", + Href: fmt.Sprintf("%s://%s/nodeinfo/2.0", p.config.Protocol, p.config.Host), + }, + }, + }, nil +} + +func (p *processor) GetNodeInfo(request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode) { + return &apimodel.Nodeinfo{ + Version: "2.0", + Software: apimodel.NodeInfoSoftware{ + Name: "gotosocial", + Version: p.config.SoftwareVersion, + }, + Protocols: []string{"activitypub"}, + Services: apimodel.NodeInfoServices{ + Inbound: []string{}, + Outbound: []string{}, + }, + OpenRegistrations: p.config.AccountsConfig.OpenRegistration, + Usage: apimodel.NodeInfoUsage{ + Users: apimodel.NodeInfoUsers{}, + }, + Metadata: make(map[string]interface{}), + }, nil +} + func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator) posted, err := p.federator.FederatingActor().PostInbox(contextWithChannel, w, r) diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 2cfa6e4e3..566bec8e5 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -169,7 +169,13 @@ type Processor interface { GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, gtserror.WithCode) // GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups. - GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, gtserror.WithCode) + GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) + + // GetNodeInfoRel returns a well known response giving the path to node info. + GetNodeInfoRel(request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) + + // GetNodeInfo returns a node info struct in response to a node info request. + GetNodeInfo(request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode) // InboxPost handles POST requests to a user's inbox for new activitypub messages. // diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index a5984e068..c2f00c77d 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -543,6 +543,7 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro mi.URLS = &model.InstanceURLs{ StreamingAPI: fmt.Sprintf("wss://%s", c.config.Host), } + mi.Version = c.config.SoftwareVersion } // get the instance account if it exists and just skip if it doesn't