commit b6c62309f236410a2dabdcf254473be99d7e60a7
parent 82d2544d7d777c82db0c9e5949a1b34b96a433d8
Author: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com>
Date: Sat, 26 Jun 2021 16:21:40 +0200
separate public key handler (#64)
Diffstat:
8 files changed, 164 insertions(+), 20 deletions(-)
diff --git a/internal/api/s2s/user/publickeyget.go b/internal/api/s2s/user/publickeyget.go
@@ -0,0 +1,45 @@
+package user
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/sirupsen/logrus"
+)
+
+// PublicKeyGETHandler should be served at eg https://example.org/users/:username/main-key.
+//
+// The goal here is to return a MINIMAL activitypub representation of an account
+// in the form of a vocab.ActivityStreamsPerson. The account will only contain the id,
+// public key, username, and type of the account.
+func (m *Module) PublicKeyGETHandler(c *gin.Context) {
+ l := m.log.WithFields(logrus.Fields{
+ "func": "PublicKeyGETHandler",
+ "url": c.Request.RequestURI,
+ })
+
+ requestedUsername := c.Param(UsernameKey)
+ if requestedUsername == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
+ return
+ }
+
+ // make sure this actually an AP request
+ format := c.NegotiateFormat(ActivityPubAcceptHeaders...)
+ if format == "" {
+ c.JSON(http.StatusNotAcceptable, gin.H{"error": "could not negotiate format with given Accept header(s)"})
+ return
+ }
+ l.Tracef("negotiated format: %s", format)
+
+ // make a copy of the context to pass along so we don't break anything
+ cp := c.Copy()
+ user, err := m.processor.GetFediUser(requestedUsername, cp.Request) // GetFediUser handles auth as well
+ if err != nil {
+ l.Info(err.Error())
+ c.JSON(err.Code(), gin.H{"error": err.Safe()})
+ return
+ }
+
+ c.JSON(http.StatusOK, user)
+}
diff --git a/internal/api/s2s/user/user.go b/internal/api/s2s/user/user.go
@@ -40,6 +40,8 @@ const (
// Use this anywhere you need to know the username of the user being queried.
// Eg https://example.org/users/:username
UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey
+ // UsersPublicKeyPath is a path to a user's public key, for serving bare minimum AP representations.
+ UsersPublicKeyPath = UsersBasePathWithUsername + "/" + util.PublicKeyPath
// UsersInboxPath is for serving POST requests to a user's inbox with the given username key.
UsersInboxPath = UsersBasePathWithUsername + "/" + util.InboxPath
// UsersFollowersPath is for serving GET request's to a user's followers list, with the given username key.
@@ -80,5 +82,6 @@ func (m *Module) Route(s router.Router) error {
s.AttachHandler(http.MethodGet, UsersFollowersPath, m.FollowersGETHandler)
s.AttachHandler(http.MethodGet, UsersFollowingPath, m.FollowingGETHandler)
s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler)
+ s.AttachHandler(http.MethodGet, UsersPublicKeyPath, m.PublicKeyGETHandler)
return nil
}
diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go
@@ -76,7 +76,7 @@ type Account struct {
*/
// Does this account need an approval for new followers?
- Locked bool `pg:",default:false"`
+ Locked bool `pg:",default:true"`
// Should this account be shown in the instance's profile directory?
Discoverable bool `pg:",default:false"`
// Default post privacy for this account
diff --git a/internal/processing/federation.go b/internal/processing/federation.go
@@ -25,6 +25,8 @@ import (
"net/url"
"github.com/go-fed/activity/streams"
+ "github.com/go-fed/activity/streams/vocab"
+ "github.com/sirupsen/logrus"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
@@ -96,30 +98,49 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht
}
func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
+ l := p.log.WithFields(logrus.Fields{
+ "func": "GetFediUser",
+ "requestedUsername": requestedUsername,
+ "requestURL": request.URL.String(),
+ })
+
// get the account the request is referring to
requestedAccount := >smodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
}
- // authenticate the request
- requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
- if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
- }
-
- blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
- }
-
- requestedPerson, err := p.tc.AccountToAS(requestedAccount)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
+ var requestedPerson vocab.ActivityStreamsPerson
+ var err error
+ if util.IsPublicKeyPath(request.URL) {
+ l.Debug("serving from public key path")
+ // if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key
+ requestedPerson, err = p.tc.AccountToASMinimal(requestedAccount)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ } else if util.IsUserPath(request.URL) {
+ l.Debug("serving from user path")
+ // if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile
+ requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
+ if err != nil {
+ return nil, gtserror.NewErrorNotAuthorized(err)
+ }
+
+ blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ }
+ requestedPerson, err = p.tc.AccountToAS(requestedAccount)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ } else {
+ return nil, gtserror.NewErrorBadRequest(fmt.Errorf("path was not public key path or user path"))
}
data, err := streams.Serialize(requestedPerson)
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
@@ -122,6 +122,7 @@ type TypeConverter interface {
// AccountToAS converts a gts model account into an activity streams person, suitable for federation
AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error)
+ AccountToASMinimal(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error)
// StatusToAS converts a gts model status into an activity streams note, suitable for federation
StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error)
// FollowToASFollow converts a gts model Follow into an activity streams Follow, suitable for federation
diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go
@@ -258,6 +258,72 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso
return person, nil
}
+// Converts a gts model account into a VERY MINIMAL Activity Streams person type, following
+// the spec laid out for mastodon here: https://docs.joinmastodon.org/spec/activitypub/
+//
+// The returned account will just have the Type, Username, PublicKey, and ID properties set.
+func (c *converter) AccountToASMinimal(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) {
+ person := streams.NewActivityStreamsPerson()
+
+ // id should be the activitypub URI of this user
+ // something like https://example.org/users/example_user
+ profileIDURI, err := url.Parse(a.URI)
+ if err != nil {
+ return nil, err
+ }
+ idProp := streams.NewJSONLDIdProperty()
+ idProp.SetIRI(profileIDURI)
+ person.SetJSONLDId(idProp)
+
+ // preferredUsername
+ // Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI.
+ preferredUsernameProp := streams.NewActivityStreamsPreferredUsernameProperty()
+ preferredUsernameProp.SetXMLSchemaString(a.Username)
+ person.SetActivityStreamsPreferredUsername(preferredUsernameProp)
+
+ // publicKey
+ // Required for signatures.
+ publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty()
+
+ // create the public key
+ publicKey := streams.NewW3IDSecurityV1PublicKey()
+
+ // set ID for the public key
+ publicKeyIDProp := streams.NewJSONLDIdProperty()
+ publicKeyURI, err := url.Parse(a.PublicKeyURI)
+ if err != nil {
+ return nil, err
+ }
+ publicKeyIDProp.SetIRI(publicKeyURI)
+ publicKey.SetJSONLDId(publicKeyIDProp)
+
+ // set owner for the public key
+ publicKeyOwnerProp := streams.NewW3IDSecurityV1OwnerProperty()
+ publicKeyOwnerProp.SetIRI(profileIDURI)
+ publicKey.SetW3IDSecurityV1Owner(publicKeyOwnerProp)
+
+ // set the pem key itself
+ encodedPublicKey, err := x509.MarshalPKIXPublicKey(a.PublicKey)
+ if err != nil {
+ return nil, err
+ }
+ publicKeyBytes := pem.EncodeToMemory(&pem.Block{
+ Type: "PUBLIC KEY",
+ Bytes: encodedPublicKey,
+ })
+ publicKeyPEMProp := streams.NewW3IDSecurityV1PublicKeyPemProperty()
+ publicKeyPEMProp.Set(string(publicKeyBytes))
+ publicKey.SetW3IDSecurityV1PublicKeyPem(publicKeyPEMProp)
+
+ // append the public key to the public key property
+ publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKey)
+
+ // set the public key property on the Person
+ person.SetW3IDSecurityV1PublicKey(publicKeyProp)
+
+ return person, nil
+}
+
func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) {
// ensure prerequisites here before we get stuck in
diff --git a/internal/util/regexes.go b/internal/util/regexes.go
@@ -60,6 +60,9 @@ var (
// userPathRegex parses a path that validates and captures the username part from eg /users/example_username
userPathRegex = regexp.MustCompile(userPathRegexString)
+ userPublicKeyPathRegexString = fmt.Sprintf(`^?/%s/(%s)/%s`, UsersPath, usernameRegexString, PublicKeyPath)
+ userPublicKeyPathRegex = regexp.MustCompile(userPublicKeyPathRegexString)
+
inboxPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, InboxPath)
// inboxPathRegex parses a path that validates and captures the username part from eg /users/example_username/inbox
inboxPathRegex = regexp.MustCompile(inboxPathRegexString)
diff --git a/internal/util/uri.go b/internal/util/uri.go
@@ -140,7 +140,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,
@@ -209,6 +209,11 @@ func IsStatusesPath(id *url.URL) bool {
return statusesPathRegex.MatchString(id.Path)
}
+// IsPublicKeyPath returns true if the given URL path corresponds to eg /users/example_username/main-key
+func IsPublicKeyPath(id *url.URL) bool {
+ return userPublicKeyPathRegex.MatchString(id.Path)
+}
+
// ParseStatusesPath returns the username and ulid from a path such as /users/example_username/statuses/SOME_ULID_OF_A_STATUS
func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) {
matches := statusesPathRegex.FindStringSubmatch(id.Path)