Webfinger + Small fixes (#20)

This commit is contained in:
Tobi Smethurst 2021-05-09 20:34:27 +02:00 committed by GitHub
parent 41915ab371
commit dc338dc881
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 246 additions and 40 deletions

View file

@ -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

View file

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

View file

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

View file

@ -344,8 +344,8 @@ func (ps *postgresService) CreateInstanceAccount() error {
func (ps *postgresService) CreateInstanceInstance() error {
i := &gtsmodel.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
}

View file

@ -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())

View file

@ -37,6 +37,8 @@ import (
"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 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr
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 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr
fileServerModule,
adminModule,
statusModule,
webfingerModule,
usersModule,
}
for _, m := range apis {

View file

@ -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

View file

@ -5,6 +5,7 @@ import (
"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 := &gtsmodel.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
}

View file

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

View file

@ -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

View file

@ -25,6 +25,7 @@ import (
"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{

View file

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

View file

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

View file

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

View file

@ -46,7 +46,7 @@ const (
// 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,