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,