separate public key handler (#64)
This commit is contained in:
parent
82d2544d7d
commit
b6c62309f2
|
@ -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.
|
// Use this anywhere you need to know the username of the user being queried.
|
||||||
// Eg https://example.org/users/:username
|
// Eg https://example.org/users/:username
|
||||||
UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey
|
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 is for serving POST requests to a user's inbox with the given username key.
|
||||||
UsersInboxPath = UsersBasePathWithUsername + "/" + util.InboxPath
|
UsersInboxPath = UsersBasePathWithUsername + "/" + util.InboxPath
|
||||||
// UsersFollowersPath is for serving GET request's to a user's followers list, with the given username key.
|
// 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, UsersFollowersPath, m.FollowersGETHandler)
|
||||||
s.AttachHandler(http.MethodGet, UsersFollowingPath, m.FollowingGETHandler)
|
s.AttachHandler(http.MethodGet, UsersFollowingPath, m.FollowingGETHandler)
|
||||||
s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler)
|
s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler)
|
||||||
|
s.AttachHandler(http.MethodGet, UsersPublicKeyPath, m.PublicKeyGETHandler)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,7 @@ type Account struct {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Does this account need an approval for new followers?
|
// 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?
|
// Should this account be shown in the instance's profile directory?
|
||||||
Discoverable bool `pg:",default:false"`
|
Discoverable bool `pg:",default:false"`
|
||||||
// Default post privacy for this account
|
// Default post privacy for this account
|
||||||
|
|
|
@ -25,6 +25,8 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
"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"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"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) {
|
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
|
// get the account the request is referring to
|
||||||
requestedAccount := >smodel.Account{}
|
requestedAccount := >smodel.Account{}
|
||||||
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
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))
|
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)
|
requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorNotAuthorized(err)
|
return nil, gtserror.NewErrorNotAuthorized(err)
|
||||||
|
@ -116,11 +135,13 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
|
||||||
if blocked {
|
if blocked {
|
||||||
return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
|
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 {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
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)
|
data, err := streams.Serialize(requestedPerson)
|
||||||
if err != nil {
|
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 converts a gts model account into an activity streams person, suitable for federation
|
||||||
AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error)
|
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 converts a gts model status into an activity streams note, suitable for federation
|
||||||
StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error)
|
StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error)
|
||||||
// FollowToASFollow converts a gts model Follow into an activity streams Follow, suitable for federation
|
// 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
|
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) {
|
func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) {
|
||||||
// ensure prerequisites here before we get stuck in
|
// 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 parses a path that validates and captures the username part from eg /users/example_username
|
||||||
userPathRegex = regexp.MustCompile(userPathRegexString)
|
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)
|
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 parses a path that validates and captures the username part from eg /users/example_username/inbox
|
||||||
inboxPathRegex = regexp.MustCompile(inboxPathRegexString)
|
inboxPathRegex = regexp.MustCompile(inboxPathRegexString)
|
||||||
|
|
|
@ -140,7 +140,7 @@ func GenerateURIsForAccount(username string, protocol string, host string) *User
|
||||||
followingURI := fmt.Sprintf("%s/%s", userURI, FollowingPath)
|
followingURI := fmt.Sprintf("%s/%s", userURI, FollowingPath)
|
||||||
likedURI := fmt.Sprintf("%s/%s", userURI, LikedPath)
|
likedURI := fmt.Sprintf("%s/%s", userURI, LikedPath)
|
||||||
collectionURI := fmt.Sprintf("%s/%s/%s", userURI, CollectionsPath, FeaturedPath)
|
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{
|
return &UserURIs{
|
||||||
HostURL: hostURL,
|
HostURL: hostURL,
|
||||||
|
@ -209,6 +209,11 @@ func IsStatusesPath(id *url.URL) bool {
|
||||||
return statusesPathRegex.MatchString(id.Path)
|
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
|
// 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) {
|
func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) {
|
||||||
matches := statusesPathRegex.FindStringSubmatch(id.Path)
|
matches := statusesPathRegex.FindStringSubmatch(id.Path)
|
||||||
|
|
Loading…
Reference in New Issue