nodeinfo compliance (#61)

This commit is contained in:
Tobi Smethurst 2021-06-24 14:26:08 +02:00 committed by GitHub
parent 16e486ad96
commit c1e107266f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 285 additions and 46 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
// 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"`
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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{}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}

View file

@ -4,5 +4,5 @@
// ExtraHeaders adds any additional required headers to the response // ExtraHeaders adds any additional required headers to the response
func (m *Module) ExtraHeaders(c *gin.Context) { func (m *Module) ExtraHeaders(c *gin.Context) {
c.Header("Server", "Mastodon") c.Header("Server", "gotosocial")
} }

View file

@ -26,6 +26,7 @@
"github.com/superseriousbusiness/gotosocial/internal/api/client/status" "github.com/superseriousbusiness/gotosocial/internal/api/client/status"
"github.com/superseriousbusiness/gotosocial/internal/api/client/streaming" "github.com/superseriousbusiness/gotosocial/internal/api/client/streaming"
"github.com/superseriousbusiness/gotosocial/internal/api/client/timeline" "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/user"
"github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger" "github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger"
"github.com/superseriousbusiness/gotosocial/internal/api/security" "github.com/superseriousbusiness/gotosocial/internal/api/security"
@ -124,6 +125,7 @@
appsModule := app.New(c, processor, log) appsModule := app.New(c, processor, log)
followRequestsModule := followrequest.New(c, processor, log) followRequestsModule := followrequest.New(c, processor, log)
webfingerModule := webfinger.New(c, processor, log) webfingerModule := webfinger.New(c, processor, log)
nodeInfoModule := nodeinfo.New(c, processor, log)
webBaseModule := web.New(c, processor, log) webBaseModule := web.New(c, processor, log)
usersModule := user.New(c, processor, log) usersModule := user.New(c, processor, log)
timelineModule := timeline.New(c, processor, log) timelineModule := timeline.New(c, processor, log)
@ -155,6 +157,7 @@
adminModule, adminModule,
statusModule, statusModule,
webfingerModule, webfingerModule,
nodeInfoModule,
usersModule, usersModule,
timelineModule, timelineModule,
notificationModule, notificationModule,

View file

@ -28,6 +28,7 @@
"github.com/superseriousbusiness/gotosocial/internal/api/client/status" "github.com/superseriousbusiness/gotosocial/internal/api/client/status"
"github.com/superseriousbusiness/gotosocial/internal/api/client/streaming" "github.com/superseriousbusiness/gotosocial/internal/api/client/streaming"
"github.com/superseriousbusiness/gotosocial/internal/api/client/timeline" "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/user"
"github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger" "github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger"
"github.com/superseriousbusiness/gotosocial/internal/api/security" "github.com/superseriousbusiness/gotosocial/internal/api/security"
@ -70,6 +71,7 @@
appsModule := app.New(c, processor, log) appsModule := app.New(c, processor, log)
followRequestsModule := followrequest.New(c, processor, log) followRequestsModule := followrequest.New(c, processor, log)
webfingerModule := webfinger.New(c, processor, log) webfingerModule := webfinger.New(c, processor, log)
nodeInfoModule := nodeinfo.New(c, processor, log)
webBaseModule := web.New(c, processor, log) webBaseModule := web.New(c, processor, log)
usersModule := user.New(c, processor, log) usersModule := user.New(c, processor, log)
timelineModule := timeline.New(c, processor, log) timelineModule := timeline.New(c, processor, log)
@ -101,6 +103,7 @@
adminModule, adminModule,
statusModule, statusModule,
webfingerModule, webfingerModule,
nodeInfoModule,
usersModule, usersModule,
timelineModule, timelineModule,
notificationModule, notificationModule,

View file

@ -59,9 +59,9 @@ type Config struct {
/* /*
Not parsed from .yaml configuration file. Not parsed from .yaml configuration file.
For short running commands (admin CLI tools etc).
*/ */
AccountCLIFlags map[string]string AccountCLIFlags map[string]string
SoftwareVersion string
} }
// FromFile returns a new config from a file, or an error if something goes amiss. // 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.LetsEncryptConfig.EmailAddress = f.String(fn.LetsEncryptEmailAddress)
} }
c.SoftwareVersion = GetDefaults().SoftwareVersion
// command-specific flags // command-specific flags
// admin account CLI flags // admin account CLI flags
@ -323,6 +325,7 @@ type Defaults struct {
ConfigPath string ConfigPath string
Host string Host string
Protocol string Protocol string
SoftwareVersion string
DbType string DbType string
DbAddress string DbAddress string

View file

@ -1,5 +1,7 @@
package config package config
const softwareVersion = "0.1.0-SNAPSHOT"
// TestDefault returns a default config for testing // TestDefault returns a default config for testing
func TestDefault() *Config { func TestDefault() *Config {
defaults := GetTestDefaults() defaults := GetTestDefaults()
@ -8,6 +10,7 @@ func TestDefault() *Config {
ApplicationName: defaults.ApplicationName, ApplicationName: defaults.ApplicationName,
Host: defaults.Host, Host: defaults.Host,
Protocol: defaults.Protocol, Protocol: defaults.Protocol,
SoftwareVersion: defaults.SoftwareVersion,
DBConfig: &DBConfig{ DBConfig: &DBConfig{
Type: defaults.DbType, Type: defaults.DbType,
Address: defaults.DbAddress, Address: defaults.DbAddress,
@ -62,6 +65,7 @@ func Default() *Config {
ApplicationName: defaults.ApplicationName, ApplicationName: defaults.ApplicationName,
Host: defaults.Host, Host: defaults.Host,
Protocol: defaults.Protocol, Protocol: defaults.Protocol,
SoftwareVersion: defaults.SoftwareVersion,
DBConfig: &DBConfig{ DBConfig: &DBConfig{
Type: defaults.DbType, Type: defaults.DbType,
Address: defaults.DbAddress, Address: defaults.DbAddress,
@ -117,6 +121,7 @@ func GetDefaults() Defaults {
ConfigPath: "", ConfigPath: "",
Host: "", Host: "",
Protocol: "https", Protocol: "https",
SoftwareVersion: softwareVersion,
DbType: "postgres", DbType: "postgres",
DbAddress: "localhost", DbAddress: "localhost",
@ -163,6 +168,7 @@ func GetTestDefaults() Defaults {
ConfigPath: "", ConfigPath: "",
Host: "localhost:8080", Host: "localhost:8080",
Protocol: "http", Protocol: "http",
SoftwareVersion: softwareVersion,
DbType: "postgres", DbType: "postgres",
DbAddress: "localhost", DbAddress: "localhost",

View file

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

View file

@ -265,7 +265,7 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st
return data, nil 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 // get the account the request is referring to
requestedAccount := &gtsmodel.Account{} requestedAccount := &gtsmodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { 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 the webfinger representation
return &apimodel.WebfingerAccountResponse{ return &apimodel.WellKnownResponse{
Subject: fmt.Sprintf("acct:%s@%s", requestedAccount.Username, p.config.Host), Subject: fmt.Sprintf("acct:%s@%s", requestedAccount.Username, p.config.Host),
Aliases: []string{ Aliases: []string{
requestedAccount.URI, requestedAccount.URI,
requestedAccount.URL, requestedAccount.URL,
}, },
Links: []apimodel.WebfingerLink{ Links: []apimodel.Link{
{ {
Rel: "http://webfinger.net/rel/profile-page", Rel: "http://webfinger.net/rel/profile-page",
Type: "text/html", Type: "text/html",
@ -294,6 +294,37 @@ func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.
}, nil }, 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) { func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator) contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator)
posted, err := p.federator.FederatingActor().PostInbox(contextWithChannel, w, r) posted, err := p.federator.FederatingActor().PostInbox(contextWithChannel, w, r)

View file

@ -169,7 +169,13 @@ type Processor interface {
GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, gtserror.WithCode) 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 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. // InboxPost handles POST requests to a user's inbox for new activitypub messages.
// //

View file

@ -543,6 +543,7 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro
mi.URLS = &model.InstanceURLs{ mi.URLS = &model.InstanceURLs{
StreamingAPI: fmt.Sprintf("wss://%s", c.config.Host), 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 // get the instance account if it exists and just skip if it doesn't