From dc338dc881ead40723f0540aac7fe894f58b174d Mon Sep 17 00:00:00 2001 From: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com> Date: Sun, 9 May 2021 20:34:27 +0200 Subject: [PATCH] Webfinger + Small fixes (#20) --- PROGRESS.md | 7 ++- internal/api/model/webfinger.go | 39 +++++++++++++ internal/api/s2s/webfinger/webfinger.go | 56 ++++++++++++++++++ internal/api/s2s/webfinger/webfingerget.go | 68 ++++++++++++++++++++++ internal/db/pg.go | 6 +- internal/federation/util.go | 9 ++- internal/gotosocial/actions.go | 6 ++ internal/gtsmodel/instance.go | 2 +- internal/message/fediprocess.go | 30 ++++++++++ internal/message/instanceprocess.go | 2 +- internal/message/processor.go | 3 + internal/router/router.go | 21 +++---- internal/transport/controller.go | 4 +- internal/typeutils/astointernal.go | 21 +++---- internal/typeutils/internaltofrontend.go | 8 +-- internal/util/uri.go | 4 +- 16 files changed, 246 insertions(+), 40 deletions(-) create mode 100644 internal/api/model/webfinger.go create mode 100644 internal/api/s2s/webfinger/webfinger.go create mode 100644 internal/api/s2s/webfinger/webfingerget.go diff --git a/PROGRESS.md b/PROGRESS.md index 18bcedfa3..20190ef32 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -77,7 +77,7 @@ * [x] /api/v1/statuses/:id/favourited_by GET (See who has faved a status) * [x] /api/v1/statuses/:id/favourite POST (Fave a status) * [x] /api/v1/statuses/:id/unfavourite POST (Unfave a status) - * [ ] /api/v1/statuses/:id/reblog POST (Reblog a status) + * [x] /api/v1/statuses/:id/reblog POST (Reblog a status) * [ ] /api/v1/statuses/:id/unreblog POST (Undo a reblog) * [ ] /api/v1/statuses/:id/bookmark POST (Bookmark a status) * [ ] /api/v1/statuses/:id/unbookmark POST (Undo a bookmark) @@ -133,7 +133,7 @@ * [ ] Search * [ ] /api/v2/search GET (Get search query results) * [ ] Instance - * [ ] /api/v1/instance GET (Get instance information) + * [x] /api/v1/instance GET (Get instance information) * [ ] /api/v1/instance PATCH (Update instance information) * [ ] /api/v1/instance/peers GET (Get list of federated servers) * [ ] /api/v1/instance/activity GET (Instance activity over the last 3 months, binned weekly.) @@ -169,7 +169,8 @@ * [ ] Oembed * [ ] /api/oembed GET (Get oembed metadata for a status URL) * [ ] Server-To-Server (Federation protocol) - * [ ] Mechanism to trigger side effects from client AP + * [x] Mechanism to trigger side effects from client AP + * [x] Webfinger account lookups * [ ] Federation modes * [ ] 'Slow' federation * [ ] Reputation scoring system for instances diff --git a/internal/api/model/webfinger.go b/internal/api/model/webfinger.go new file mode 100644 index 000000000..bb5008949 --- /dev/null +++ b/internal/api/model/webfinger.go @@ -0,0 +1,39 @@ +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/s2s/webfinger/webfinger.go b/internal/api/s2s/webfinger/webfinger.go new file mode 100644 index 000000000..c11d3fb61 --- /dev/null +++ b/internal/api/s2s/webfinger/webfinger.go @@ -0,0 +1,56 @@ +/* + 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 webfinger + +import ( + "net/http" + + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/message" + "github.com/superseriousbusiness/gotosocial/internal/router" +) + +const ( + // The base path for serving webfinger lookup requests + WebfingerBasePath = ".well-known/webfinger" +) + +// Module implements the FederationModule interface +type Module struct { + config *config.Config + processor message.Processor + log *logrus.Logger +} + +// New returns a new webfinger module +func New(config *config.Config, processor message.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, WebfingerBasePath, m.WebfingerGETRequest) + return nil +} diff --git a/internal/api/s2s/webfinger/webfingerget.go b/internal/api/s2s/webfinger/webfingerget.go new file mode 100644 index 000000000..44d60670d --- /dev/null +++ b/internal/api/s2s/webfinger/webfingerget.go @@ -0,0 +1,68 @@ +/* + 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 webfinger + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +// WebfingerGETRequest handles requests to, for example, https://example.org/.well-known/webfinger?resource=acct:some_user@example.org +func (m *Module) WebfingerGETRequest(c *gin.Context) { + + q, set := c.GetQuery("resource") + if !set || q == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no 'resource' in request query"}) + return + } + + withAcct := strings.Split(q, "acct:") + if len(withAcct) != 2 { + c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + return + } + + usernameDomain := strings.Split(withAcct[1], "@") + if len(usernameDomain) != 2 { + c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + return + } + username := strings.ToLower(usernameDomain[0]) + domain := strings.ToLower(usernameDomain[1]) + if username == "" || domain == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + return + } + + if domain != m.config.Host { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("domain %s does not belong to this instance", domain)}) + return + } + + resp, err := m.processor.GetWebfingerAccount(username, c.Request) + if err != nil { + c.JSON(err.Code(), gin.H{"error": err.Safe()}) + return + } + + c.JSON(http.StatusOK, resp) +} diff --git a/internal/db/pg.go b/internal/db/pg.go index f59103af7..c0fbcc9e0 100644 --- a/internal/db/pg.go +++ b/internal/db/pg.go @@ -344,8 +344,8 @@ func (ps *postgresService) CreateInstanceAccount() error { func (ps *postgresService) CreateInstanceInstance() error { i := >smodel.Instance{ Domain: ps.config.Host, - Title: ps.config.Host, - URI: fmt.Sprintf("%s://%s", ps.config.Protocol, ps.config.Host), + Title: ps.config.Host, + URI: fmt.Sprintf("%s://%s", ps.config.Protocol, ps.config.Host), } inserted, err := ps.conn.Model(i).Where("domain = ?", ps.config.Host).SelectOrInsert() if err != nil { @@ -354,7 +354,7 @@ func (ps *postgresService) CreateInstanceInstance() error { if inserted { ps.log.Infof("created instance instance %s with id %s", ps.config.Host, i.ID) } else { - ps.log.Infof("instance instance %s already exists with id %s", ps.config.Host, i.ID) + ps.log.Infof("instance instance %s already exists with id %s", ps.config.Host, i.ID) } return nil } diff --git a/internal/federation/util.go b/internal/federation/util.go index ab854db7c..d76ce853d 100644 --- a/internal/federation/util.go +++ b/internal/federation/util.go @@ -112,6 +112,9 @@ func getPublicKeyFromResponse(c context.Context, b []byte, keyID *url.URL) (voca // Also note that this function *does not* dereference the remote account that the signature key is associated with. // Other functions should use the returned URL to dereference the remote account, if required. func (f *federator) AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, error) { + // set this extra field for signature validation + r.Header.Set("host", f.config.Host) + verifier, err := httpsig.NewVerifier(r) if err != nil { return nil, fmt.Errorf("could not create http sig verifier: %s", err) @@ -208,7 +211,11 @@ func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *u } return p, nil case string(gtsmodel.ActivityStreamsApplication): - // TODO: convert application into person + p, ok := t.(vocab.ActivityStreamsApplication) + if !ok { + return nil, errors.New("error resolving type as activitystreams application") + } + return p, nil } return nil, fmt.Errorf("type name %s not supported", t.GetTypeName()) diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go index 6d130ed2d..5790456dd 100644 --- a/internal/gotosocial/actions.go +++ b/internal/gotosocial/actions.go @@ -37,6 +37,8 @@ "github.com/superseriousbusiness/gotosocial/internal/api/client/instance" mediaModule "github.com/superseriousbusiness/gotosocial/internal/api/client/media" "github.com/superseriousbusiness/gotosocial/internal/api/client/status" + "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" + "github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger" "github.com/superseriousbusiness/gotosocial/internal/api/security" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -109,6 +111,8 @@ accountModule := account.New(c, processor, log) instanceModule := instance.New(c, processor, log) appsModule := app.New(c, processor, log) + webfingerModule := webfinger.New(c, processor, log) + usersModule := user.New(c, processor, log) mm := mediaModule.New(c, processor, log) fileServerModule := fileserver.New(c, processor, log) adminModule := admin.New(c, processor, log) @@ -128,6 +132,8 @@ fileServerModule, adminModule, statusModule, + webfingerModule, + usersModule, } for _, m := range apis { diff --git a/internal/gtsmodel/instance.go b/internal/gtsmodel/instance.go index ac7c990e3..6860627e2 100644 --- a/internal/gtsmodel/instance.go +++ b/internal/gtsmodel/instance.go @@ -15,7 +15,7 @@ type Instance struct { // When was this instance created in the db? CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // When was this instance last updated in the db? - UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` + UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // When was this instance suspended, if at all? SuspendedAt time.Time // ID of any existing domain block for this instance in the database diff --git a/internal/message/fediprocess.go b/internal/message/fediprocess.go index 6dc6330cf..dad1e848c 100644 --- a/internal/message/fediprocess.go +++ b/internal/message/fediprocess.go @@ -5,6 +5,7 @@ "net/http" "github.com/go-fed/activity/streams" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) @@ -100,3 +101,32 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request) return data, nil } + +func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) { + // get the account the request is referring to + requestedAccount := >smodel.Account{} + if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { + return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + // return the webfinger representation + return &apimodel.WebfingerAccountResponse{ + Subject: fmt.Sprintf("acct:%s@%s", requestedAccount.Username, p.config.Host), + Aliases: []string{ + requestedAccount.URI, + requestedAccount.URL, + }, + Links: []apimodel.WebfingerLink{ + { + Rel: "http://webfinger.net/rel/profile-page", + Type: "text/html", + Href: requestedAccount.URL, + }, + { + Rel: "self", + Type: "application/activity+json", + Href: requestedAccount.URI, + }, + }, + }, nil +} diff --git a/internal/message/instanceprocess.go b/internal/message/instanceprocess.go index 16a5594de..0b0f15501 100644 --- a/internal/message/instanceprocess.go +++ b/internal/message/instanceprocess.go @@ -18,5 +18,5 @@ func (p *processor) InstanceGet(domain string) (*apimodel.Instance, ErrorWithCod return nil, NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err)) } - return ai, nil + return ai, nil } diff --git a/internal/message/processor.go b/internal/message/processor.go index 0c0334e20..d150d56e6 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -108,6 +108,9 @@ type Processor interface { // GetFediUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication // before returning a JSON serializable interface to the caller. GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) + + // 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, ErrorWithCode) } // processor just implements the Processor interface diff --git a/internal/router/router.go b/internal/router/router.go index 0f1f288bd..cdd079634 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -25,6 +25,7 @@ "net/http" "os" "path/filepath" + "time" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/memstore" @@ -140,7 +141,13 @@ func New(config *config.Config, logger *logrus.Logger) (Router, error) { engine.LoadHTMLGlob(tmPath) // create the actual http server here - var s *http.Server + s := &http.Server{ + Handler: engine, + ReadTimeout: 1 * time.Second, + WriteTimeout: 1 * time.Second, + IdleTimeout: 30 * time.Second, + ReadHeaderTimeout: 2 * time.Second, + } var m *autocert.Manager // We need to spawn the underlying server slightly differently depending on whether lets encrypt is enabled or not. @@ -154,17 +161,11 @@ func New(config *config.Config, logger *logrus.Logger) (Router, error) { Email: config.LetsEncryptConfig.EmailAddress, } // and create an HTTPS server - s = &http.Server{ - Addr: ":https", - TLSConfig: m.TLSConfig(), - Handler: engine, - } + s.Addr = ":https" + s.TLSConfig = m.TLSConfig() } else { // le is NOT enabled, so just serve bare requests on port 8080 - s = &http.Server{ - Addr: ":8080", - Handler: engine, - } + s.Addr = ":8080" } return &router{ diff --git a/internal/transport/controller.go b/internal/transport/controller.go index 525141025..2ee23f141 100644 --- a/internal/transport/controller.go +++ b/internal/transport/controller.go @@ -54,8 +54,8 @@ func NewController(config *config.Config, clock pub.Clock, client pub.HttpClient func (c *controller) NewTransport(pubKeyID string, privkey crypto.PrivateKey) (pub.Transport, error) { prefs := []httpsig.Algorithm{httpsig.RSA_SHA256, httpsig.RSA_SHA512} digestAlgo := httpsig.DigestSha256 - getHeaders := []string{"(request-target)", "date"} - postHeaders := []string{"(request-target)", "date", "digest"} + getHeaders := []string{"(request-target)", "date", "accept"} + postHeaders := []string{"(request-target)", "date", "accept", "digest"} getSigner, _, err := httpsig.NewSigner(prefs, digestAlgo, getHeaders, httpsig.Signature) if err != nil { diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index 5e3b6b052..7842411ea 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -119,31 +119,26 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode acct.URL = url.String() // InboxURI - if accountable.GetActivityStreamsInbox() == nil || accountable.GetActivityStreamsInbox().GetIRI() == nil { - return nil, fmt.Errorf("person with id %s had no inbox uri", uri.String()) + if accountable.GetActivityStreamsInbox() != nil || accountable.GetActivityStreamsInbox().GetIRI() != nil { + acct.InboxURI = accountable.GetActivityStreamsInbox().GetIRI().String() } - acct.InboxURI = accountable.GetActivityStreamsInbox().GetIRI().String() // OutboxURI - if accountable.GetActivityStreamsOutbox() == nil || accountable.GetActivityStreamsOutbox().GetIRI() == nil { - return nil, fmt.Errorf("person with id %s had no outbox uri", uri.String()) + if accountable.GetActivityStreamsOutbox() != nil && accountable.GetActivityStreamsOutbox().GetIRI() != nil { + acct.OutboxURI = accountable.GetActivityStreamsOutbox().GetIRI().String() } - acct.OutboxURI = accountable.GetActivityStreamsOutbox().GetIRI().String() // FollowingURI - if accountable.GetActivityStreamsFollowing() == nil || accountable.GetActivityStreamsFollowing().GetIRI() == nil { - return nil, fmt.Errorf("person with id %s had no following uri", uri.String()) + if accountable.GetActivityStreamsFollowing() != nil && accountable.GetActivityStreamsFollowing().GetIRI() != nil { + acct.FollowingURI = accountable.GetActivityStreamsFollowing().GetIRI().String() } - acct.FollowingURI = accountable.GetActivityStreamsFollowing().GetIRI().String() // FollowersURI - if accountable.GetActivityStreamsFollowers() == nil || accountable.GetActivityStreamsFollowers().GetIRI() == nil { - return nil, fmt.Errorf("person with id %s had no followers uri", uri.String()) + if accountable.GetActivityStreamsFollowers() != nil && accountable.GetActivityStreamsFollowers().GetIRI() != nil { + acct.FollowersURI = accountable.GetActivityStreamsFollowers().GetIRI().String() } - acct.FollowersURI = accountable.GetActivityStreamsFollowers().GetIRI().String() // FeaturedURI - // very much optional if accountable.GetTootFeatured() != nil && accountable.GetTootFeatured().GetIRI() != nil { acct.FeaturedCollectionURI = accountable.GetTootFeatured().GetIRI().String() } diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 6b0c743ff..861350b44 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -554,11 +554,11 @@ func (c *converter) VisToMasto(m gtsmodel.Visibility) model.Visibility { func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, error) { mi := &model.Instance{ - URI: i.URI, - Title: i.Title, - Description: i.Description, + URI: i.URI, + Title: i.Title, + Description: i.Description, ShortDescription: i.ShortDescription, - Email: i.ContactEmail, + Email: i.ContactEmail, } if i.Domain == c.config.Host { diff --git a/internal/util/uri.go b/internal/util/uri.go index 9b96edc61..538df9210 100644 --- a/internal/util/uri.go +++ b/internal/util/uri.go @@ -46,7 +46,7 @@ // FeaturedPath represents the webfinger featured location FeaturedPath = "featured" // PublicKeyPath is for serving an account's public key - PublicKeyPath = "publickey" + PublicKeyPath = "main-key" ) // APContextKey is a type used specifically for settings values on contexts within go-fed AP request chains @@ -113,7 +113,7 @@ func GenerateURIsForAccount(username string, protocol string, host string) *User followingURI := fmt.Sprintf("%s/%s", userURI, FollowingPath) likedURI := fmt.Sprintf("%s/%s", userURI, LikedPath) collectionURI := fmt.Sprintf("%s/%s/%s", userURI, CollectionsPath, FeaturedPath) - publicKeyURI := fmt.Sprintf("%s/%s", userURI, PublicKeyPath) + publicKeyURI := fmt.Sprintf("%s#%s", userURI, PublicKeyPath) return &UserURIs{ HostURL: hostURL,