forked from mirrors/gotosocial
separate public key handler (#64)
This commit is contained in:
parent
82d2544d7d
commit
b6c62309f2
8 changed files with 162 additions and 18 deletions
45
internal/api/s2s/user/publickeyget.go
Normal file
45
internal/api/s2s/user/publickeyget.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,13 +98,30 @@ 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
|
||||
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)
|
||||
|
@ -116,11 +135,13 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
|
|||
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)
|
||||
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)
|
||||
if err != nil {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue