hell yeah

This commit is contained in:
tsmethurst 2021-05-06 22:02:07 +02:00
commit 9e1f61c373
10 changed files with 496 additions and 112 deletions

View file

@ -34,10 +34,10 @@ const (
UsernameKey = "username" UsernameKey = "username"
// UsersBasePath is the base path for serving information about Users eg https://example.org/users // UsersBasePath is the base path for serving information about Users eg https://example.org/users
UsersBasePath = "/" + util.UsersPath UsersBasePath = "/" + util.UsersPath
// UsersBasePathWithID is just the users base path with the Username key in it. // UsersBasePathWithUsername is just the users base path with the Username key in it.
// 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
UsersBasePathWithID = UsersBasePath + "/:" + UsernameKey UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey
) )
// ActivityPubAcceptHeaders represents the Accept headers mentioned here: // ActivityPubAcceptHeaders represents the Accept headers mentioned here:
@ -65,6 +65,6 @@ func New(config *config.Config, processor message.Processor, log *logrus.Logger)
// Route satisfies the RESTAPIModule interface // Route satisfies the RESTAPIModule interface
func (m *Module) Route(s router.Router) error { func (m *Module) Route(s router.Router) error {
s.AttachHandler(http.MethodGet, UsersBasePathWithID, m.UsersGETHandler) s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler)
return nil return nil
} }

View file

@ -0,0 +1,39 @@
package user_test
import (
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/message"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
type UserStandardTestSuite struct {
// standard suite interfaces
suite.Suite
config *config.Config
db db.DB
log *logrus.Logger
tc typeutils.TypeConverter
federator federation.Federator
processor message.Processor
storage storage.Storage
// standard suite models
testTokens map[string]*oauth.Token
testClients map[string]*oauth.Client
testApplications map[string]*gtsmodel.Application
testUsers map[string]*gtsmodel.User
testAccounts map[string]*gtsmodel.Account
testAttachments map[string]*gtsmodel.MediaAttachment
testStatuses map[string]*gtsmodel.Status
// module being tested
userModule *user.Module
}

View file

@ -0,0 +1,155 @@
package user_test
import (
"bytes"
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user"
"github.com/superseriousbusiness/gotosocial/testrig"
)
type UserGetTestSuite struct {
UserStandardTestSuite
}
func (suite *UserGetTestSuite) SetupSuite() {
suite.testTokens = testrig.NewTestTokens()
suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications()
suite.testUsers = testrig.NewTestUsers()
suite.testAccounts = testrig.NewTestAccounts()
suite.testAttachments = testrig.NewTestAttachments()
suite.testStatuses = testrig.NewTestStatuses()
}
func (suite *UserGetTestSuite) SetupTest() {
suite.config = testrig.NewTestConfig()
suite.db = testrig.NewTestDB()
suite.tc = testrig.NewTestTypeConverter(suite.db)
suite.storage = testrig.NewTestStorage()
suite.log = testrig.NewTestLog()
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)))
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
suite.userModule = user.New(suite.config, suite.processor, suite.log).(*user.Module)
testrig.StandardDBSetup(suite.db)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
}
func (suite *UserGetTestSuite) TearDownTest() {
testrig.StandardDBTeardown(suite.db)
testrig.StandardStorageTeardown(suite.storage)
}
func (suite *UserGetTestSuite) TestGetUser() {
// the dereference we're gonna use
signedRequest := testrig.NewTestDereferenceRequests(suite.testAccounts)["foss_satan_dereference_zork"]
requestingAccount := suite.testAccounts["remote_account_1"]
targetAccount := suite.testAccounts["local_account_1"]
encodedPublicKey, err := x509.MarshalPKIXPublicKey(requestingAccount.PublicKey)
assert.NoError(suite.T(), err)
publicKeyBytes := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: encodedPublicKey,
})
publicKeyString := strings.ReplaceAll(string(publicKeyBytes), "\n", "\\n")
// for this test we need the client to return the public key of the requester on the 'remote' instance
responseBodyString := fmt.Sprintf(`
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"id": "%s",
"type": "Person",
"preferredUsername": "%s",
"inbox": "%s",
"publicKey": {
"id": "%s",
"owner": "%s",
"publicKeyPem": "%s"
}
}`, requestingAccount.URI, requestingAccount.Username, requestingAccount.InboxURI, requestingAccount.PublicKeyURI, requestingAccount.URI, publicKeyString)
// create a transport controller whose client will just return the response body string we specified above
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(func(req *http.Request) (*http.Response, error) {
r := ioutil.NopCloser(bytes.NewReader([]byte(responseBodyString)))
return &http.Response{
StatusCode: 200,
Body: r,
}, nil
}))
// get this transport controller embedded right in the user module we're testing
federator := testrig.NewTestFederator(suite.db, tc)
processor := testrig.NewTestProcessor(suite.db, suite.storage, federator)
userModule := user.New(suite.config, processor, suite.log).(*user.Module)
// setup request
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:8080%s", strings.Replace(user.UsersBasePathWithUsername, ":username", targetAccount.Username, 1)), nil) // the endpoint we're hitting
// normally the router would populate these params from the path values,
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
Key: user.UsernameKey,
Value: targetAccount.Username,
},
}
// we need these headers for the request to be validated
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
ctx.Request.Header.Set("Digest", signedRequest.DigestHeader)
// trigger the function being tested
userModule.UsersGETHandler(ctx)
// check response
suite.EqualValues(http.StatusOK, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err)
// should be a Person
m := make(map[string]interface{})
err = json.Unmarshal(b, &m)
assert.NoError(suite.T(), err)
t, err := streams.ToType(context.Background(), m)
assert.NoError(suite.T(), err)
person, ok := t.(vocab.ActivityStreamsPerson)
assert.True(suite.T(), ok)
// convert person to account
// since this account is already known, we should get a pretty full model of it from the conversion
a, err := suite.tc.ASPersonToAccount(person)
assert.NoError(suite.T(), err)
assert.EqualValues(suite.T(), targetAccount.Username, a.Username)
}
func TestUserGetTestSuite(t *testing.T) {
suite.Run(t, new(UserGetTestSuite))
}

View file

@ -157,6 +157,9 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques
if err != nil { if err != nil {
return nil, fmt.Errorf("could not parse public key from block bytes: %s", err) return nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
} }
if p == nil {
return nil, errors.New("returned public key was empty")
}
// do the actual authentication here! // do the actual authentication here!
algo := httpsig.RSA_SHA256 // TODO: make this more robust algo := httpsig.RSA_SHA256 // TODO: make this more robust

View file

@ -108,15 +108,15 @@ type FileType string
const ( const (
// FileTypeImage is for jpegs and pngs // FileTypeImage is for jpegs and pngs
FileTypeImage FileType = "image" FileTypeImage FileType = "Image"
// FileTypeGif is for native gifs and soundless videos that have been converted to gifs // FileTypeGif is for native gifs and soundless videos that have been converted to gifs
FileTypeGif FileType = "gif" FileTypeGif FileType = "Gif"
// FileTypeAudio is for audio-only files (no video) // FileTypeAudio is for audio-only files (no video)
FileTypeAudio FileType = "audio" FileTypeAudio FileType = "Audio"
// FileTypeVideo is for files with audio + visual // FileTypeVideo is for files with audio + visual
FileTypeVideo FileType = "video" FileTypeVideo FileType = "Video"
// FileTypeUnknown is for unknown file types (surprise surprise!) // FileTypeUnknown is for unknown file types (surprise surprise!)
FileTypeUnknown FileType = "unknown" FileTypeUnknown FileType = "Unknown"
) )
// FileMeta describes metadata about the actual contents of the file. // FileMeta describes metadata about the actual contents of the file.

View file

@ -113,7 +113,7 @@ func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID strin
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !supportedImageType(contentType) { if !SupportedImageType(contentType) {
return nil, fmt.Errorf("%s is not an accepted image type", contentType) return nil, fmt.Errorf("%s is not an accepted image type", contentType)
} }
@ -146,8 +146,8 @@ func (mh *mediaHandler) ProcessLocalAttachment(attachment []byte, accountID stri
} }
mainType := strings.Split(contentType, "/")[0] mainType := strings.Split(contentType, "/")[0]
switch mainType { switch mainType {
case "video": case MIMEVideo:
if !supportedVideoType(contentType) { if !SupportedVideoType(contentType) {
return nil, fmt.Errorf("video type %s not supported", contentType) return nil, fmt.Errorf("video type %s not supported", contentType)
} }
if len(attachment) == 0 { if len(attachment) == 0 {
@ -157,8 +157,8 @@ func (mh *mediaHandler) ProcessLocalAttachment(attachment []byte, accountID stri
return nil, fmt.Errorf("video size %d bytes exceeded max video size of %d bytes", len(attachment), mh.config.MediaConfig.MaxVideoSize) return nil, fmt.Errorf("video size %d bytes exceeded max video size of %d bytes", len(attachment), mh.config.MediaConfig.MaxVideoSize)
} }
return mh.processVideoAttachment(attachment, accountID, contentType) return mh.processVideoAttachment(attachment, accountID, contentType)
case "image": case MIMEImage:
if !supportedImageType(contentType) { if !SupportedImageType(contentType) {
return nil, fmt.Errorf("image type %s not supported", contentType) return nil, fmt.Errorf("image type %s not supported", contentType)
} }
if len(attachment) == 0 { if len(attachment) == 0 {
@ -199,13 +199,13 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) (
return nil, fmt.Errorf("emoji size %d bytes exceeded max emoji size of %d bytes", len(emojiBytes), EmojiMaxBytes) return nil, fmt.Errorf("emoji size %d bytes exceeded max emoji size of %d bytes", len(emojiBytes), EmojiMaxBytes)
} }
// clean any exif data from image/png type but leave gifs alone // clean any exif data from png but leave gifs alone
switch contentType { switch contentType {
case "image/png": case MIMEPng:
if clean, err = purgeExif(emojiBytes); err != nil { if clean, err = purgeExif(emojiBytes); err != nil {
return nil, fmt.Errorf("error cleaning exif data: %s", err) return nil, fmt.Errorf("error cleaning exif data: %s", err)
} }
case "image/gif": case MIMEGif:
clean = emojiBytes clean = emojiBytes
default: default:
return nil, errors.New("media type unrecognized") return nil, errors.New("media type unrecognized")
@ -275,7 +275,7 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) (
ImagePath: emojiPath, ImagePath: emojiPath,
ImageStaticPath: emojiStaticPath, ImageStaticPath: emojiStaticPath,
ImageContentType: contentType, ImageContentType: contentType,
ImageStaticContentType: "image/png", // static version will always be a png ImageStaticContentType: MIMEPng, // static version will always be a png
ImageFileSize: len(original.image), ImageFileSize: len(original.image),
ImageStaticFileSize: len(static.image), ImageStaticFileSize: len(static.image),
ImageUpdatedAt: time.Now(), ImageUpdatedAt: time.Now(),
@ -302,7 +302,7 @@ func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, co
var small *imageAndMeta var small *imageAndMeta
switch contentType { switch contentType {
case "image/jpeg", "image/png": case MIMEJpeg, MIMEPng:
if clean, err = purgeExif(data); err != nil { if clean, err = purgeExif(data); err != nil {
return nil, fmt.Errorf("error cleaning exif data: %s", err) return nil, fmt.Errorf("error cleaning exif data: %s", err)
} }
@ -310,7 +310,7 @@ func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, co
if err != nil { if err != nil {
return nil, fmt.Errorf("error parsing image: %s", err) return nil, fmt.Errorf("error parsing image: %s", err)
} }
case "image/gif": case MIMEGif:
clean = data clean = data
original, err = deriveGif(clean, contentType) original, err = deriveGif(clean, contentType)
if err != nil { if err != nil {
@ -380,7 +380,7 @@ func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, co
}, },
Thumbnail: gtsmodel.Thumbnail{ Thumbnail: gtsmodel.Thumbnail{
Path: smallPath, Path: smallPath,
ContentType: "image/jpeg", // all thumbnails/smalls are encoded as jpeg ContentType: MIMEJpeg, // all thumbnails/smalls are encoded as jpeg
FileSize: len(small.image), FileSize: len(small.image),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
URL: smallURL, URL: smallURL,
@ -411,15 +411,15 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string
var err error var err error
switch contentType { switch contentType {
case "image/jpeg": case MIMEJpeg:
if clean, err = purgeExif(imageBytes); err != nil { if clean, err = purgeExif(imageBytes); err != nil {
return nil, fmt.Errorf("error cleaning exif data: %s", err) return nil, fmt.Errorf("error cleaning exif data: %s", err)
} }
case "image/png": case MIMEPng:
if clean, err = purgeExif(imageBytes); err != nil { if clean, err = purgeExif(imageBytes); err != nil {
return nil, fmt.Errorf("error cleaning exif data: %s", err) return nil, fmt.Errorf("error cleaning exif data: %s", err)
} }
case "image/gif": case MIMEGif:
clean = imageBytes clean = imageBytes
default: default:
return nil, errors.New("media type unrecognized") return nil, errors.New("media type unrecognized")

View file

@ -33,6 +33,26 @@ import (
"github.com/superseriousbusiness/exifremove/pkg/exifremove" "github.com/superseriousbusiness/exifremove/pkg/exifremove"
) )
const (
// MIMEImage is the mime type for image
MIMEImage = "image"
// MIMEJpeg is the jpeg image mime type
MIMEJpeg = "image/jpeg"
// MIMEGif is the gif image mime type
MIMEGif = "image/gif"
// MIMEPng is the png image mime type
MIMEPng = "image/png"
// MIMEVideo is the mime type for video
MIMEVideo = "video"
// MIMEmp4 is the mp4 video mime type
MIMEMp4 = "video/mp4"
// MIMEMpeg is the mpeg video mime type
MIMEMpeg = "video/mpeg"
// MIMEWebm is the webm video mime type
MIMEWebm = "video/webm"
)
// parseContentType parses the MIME content type from a file, returning it as a string in the form (eg., "image/jpeg"). // parseContentType parses the MIME content type from a file, returning it as a string in the form (eg., "image/jpeg").
// Returns an error if the content type is not something we can process. // Returns an error if the content type is not something we can process.
func parseContentType(content []byte) (string, error) { func parseContentType(content []byte) (string, error) {
@ -54,13 +74,13 @@ func parseContentType(content []byte) (string, error) {
return kind.MIME.Value, nil return kind.MIME.Value, nil
} }
// supportedImageType checks mime type of an image against a slice of accepted types, // SupportedImageType checks mime type of an image against a slice of accepted types,
// and returns True if the mime type is accepted. // and returns True if the mime type is accepted.
func supportedImageType(mimeType string) bool { func SupportedImageType(mimeType string) bool {
acceptedImageTypes := []string{ acceptedImageTypes := []string{
"image/jpeg", MIMEJpeg,
"image/gif", MIMEGif,
"image/png", MIMEPng,
} }
for _, accepted := range acceptedImageTypes { for _, accepted := range acceptedImageTypes {
if mimeType == accepted { if mimeType == accepted {
@ -70,13 +90,13 @@ func supportedImageType(mimeType string) bool {
return false return false
} }
// supportedVideoType checks mime type of a video against a slice of accepted types, // SupportedVideoType checks mime type of a video against a slice of accepted types,
// and returns True if the mime type is accepted. // and returns True if the mime type is accepted.
func supportedVideoType(mimeType string) bool { func SupportedVideoType(mimeType string) bool {
acceptedVideoTypes := []string{ acceptedVideoTypes := []string{
"video/mp4", MIMEMp4,
"video/mpeg", MIMEMpeg,
"video/webm", MIMEWebm,
} }
for _, accepted := range acceptedVideoTypes { for _, accepted := range acceptedVideoTypes {
if mimeType == accepted { if mimeType == accepted {
@ -89,8 +109,8 @@ func supportedVideoType(mimeType string) bool {
// supportedEmojiType checks that the content type is image/png -- the only type supported for emoji. // supportedEmojiType checks that the content type is image/png -- the only type supported for emoji.
func supportedEmojiType(mimeType string) bool { func supportedEmojiType(mimeType string) bool {
acceptedEmojiTypes := []string{ acceptedEmojiTypes := []string{
"image/gif", MIMEGif,
"image/png", MIMEPng,
} }
for _, accepted := range acceptedEmojiTypes { for _, accepted := range acceptedEmojiTypes {
if mimeType == accepted { if mimeType == accepted {
@ -121,7 +141,7 @@ func deriveGif(b []byte, extension string) (*imageAndMeta, error) {
var g *gif.GIF var g *gif.GIF
var err error var err error
switch extension { switch extension {
case "image/gif": case MIMEGif:
g, err = gif.DecodeAll(bytes.NewReader(b)) g, err = gif.DecodeAll(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
@ -161,12 +181,12 @@ func deriveImage(b []byte, contentType string) (*imageAndMeta, error) {
var err error var err error
switch contentType { switch contentType {
case "image/jpeg": case MIMEJpeg:
i, err = jpeg.Decode(bytes.NewReader(b)) i, err = jpeg.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "image/png": case MIMEPng:
i, err = png.Decode(bytes.NewReader(b)) i, err = png.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
@ -210,17 +230,17 @@ func deriveThumbnail(b []byte, contentType string, x uint, y uint) (*imageAndMet
var err error var err error
switch contentType { switch contentType {
case "image/jpeg": case MIMEJpeg:
i, err = jpeg.Decode(bytes.NewReader(b)) i, err = jpeg.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "image/png": case MIMEPng:
i, err = png.Decode(bytes.NewReader(b)) i, err = png.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "image/gif": case MIMEGif:
i, err = gif.Decode(bytes.NewReader(b)) i, err = gif.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
@ -254,12 +274,12 @@ func deriveStaticEmoji(b []byte, contentType string) (*imageAndMeta, error) {
var err error var err error
switch contentType { switch contentType {
case "image/png": case MIMEPng:
i, err = png.Decode(bytes.NewReader(b)) i, err = png.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "image/gif": case MIMEGif:
i, err = gif.Decode(bytes.NewReader(b)) i, err = gif.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -135,10 +135,10 @@ func (suite *MediaUtilTestSuite) TestDeriveThumbnailFromJPEG() {
} }
func (suite *MediaUtilTestSuite) TestSupportedImageTypes() { func (suite *MediaUtilTestSuite) TestSupportedImageTypes() {
ok := supportedImageType("image/jpeg") ok := SupportedImageType("image/jpeg")
assert.True(suite.T(), ok) assert.True(suite.T(), ok)
ok = supportedImageType("image/bmp") ok = SupportedImageType("image/bmp")
assert.False(suite.T(), ok) assert.False(suite.T(), ok)
} }

View file

@ -1,71 +1,197 @@
package typeutils package typeutils
import ( import (
"errors"
"fmt"
"net/url"
"github.com/go-fed/activity/streams/vocab" "github.com/go-fed/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
) )
func (c *converter) ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsmodel.Account, error) { func (c *converter) ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsmodel.Account, error) {
// first check if we actually already know this person
// acct := &gtsmodel.Account{ uriProp := person.GetJSONLDId()
// URI: "", if uriProp == nil || !uriProp.IsIRI() {
// URL: "", return nil, errors.New("no id property found on person, or id was not an iri")
// ID: "", }
// Username: "", uri := uriProp.GetIRI()
// Domain: "",
// AvatarMediaAttachmentID: "", acct := &gtsmodel.Account{}
// AvatarRemoteURL: "", if err := c.db.GetWhere("uri", uri.String(), acct); err == nil {
// HeaderMediaAttachmentID: "", // we already know this account so we can skip generating it
// HeaderRemoteURL: "", return acct, nil
// DisplayName: "", } else {
// Fields: nil, if _, ok := err.(db.ErrNoEntries); !ok {
// Note: "", // we don't know the account and there's been a real error
// Memorial: false, return nil, fmt.Errorf("error getting account with uri %s from the database: %s", uri.String(), err)
// MovedToAccountID: "", }
// CreatedAt: time.Time{}, }
// UpdatedAt: time.Time{},
// Bot: false, // we don't know the account so we need to generate it from the person -- at least we already have the URI!
// Reason: "", acct = &gtsmodel.Account{}
// Locked: false, acct.URI = uri.String()
// Discoverable: true,
// Privacy: "", // Username
// Sensitive: false, // We need this one so bail if it's not set.
// Language: "", username, err := extractUsername(person)
// LastWebfingeredAt: time.Now(), if err != nil {
// InboxURI: "", return nil, fmt.Errorf("couldn't extract username: %s", err)
// OutboxURI: "", }
// FollowingURI: "", acct.Username = username
// FollowersURI: "",
// FeaturedCollectionURI: "", // Domain
// ActorType: gtsmodel.ActivityStreamsPerson, // We need this one as well
// AlsoKnownAs: "", acct.Domain = uri.Host
// PrivateKey: nil,
// PublicKey: nil, // avatar aka icon
// PublicKeyURI: "", // if this one isn't extractable in a format we recognise we'll just skip it
// SensitizedAt: time.Time{}, if avatarURL, err := extractIconURL(person); err == nil {
// SilencedAt: time.Time{}, acct.AvatarRemoteURL = avatarURL.String()
// SuspendedAt: time.Time{}, }
// HideCollections: false,
// SuspensionOrigin: "", // header aka image
// } // if this one isn't extractable in a format we recognise we'll just skip it
if headerURL, err := extractImageURL(person); err == nil {
// // ID acct.HeaderRemoteURL = headerURL.String()
// // Generate a new uuid for our particular database. }
// // This is distinct from the AP ID of the person.
// id := uuid.NewString() return acct, nil
// acct.ID = id }
// // Username type usernameable interface {
// // We need this one so bail if it's not set. GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty
// username := person.GetActivityStreamsPreferredUsername() }
// if username == nil || username.GetXMLSchemaString() == "" {
// return nil, errors.New("preferredusername was empty") func extractUsername(i usernameable) (string, error) {
// } u := i.GetActivityStreamsPreferredUsername()
// acct.Username = username.GetXMLSchemaString() if u == nil || !u.IsXMLSchemaString() {
return "", errors.New("preferredUsername was not a string")
// // Domain }
// // We need this one as well if u.GetXMLSchemaString() == "" {
// acct.Domain = domain return "", errors.New("preferredUsername was empty")
}
return nil, nil return u.GetXMLSchemaString(), nil
}
type iconable interface {
GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty
}
// extractIconURL extracts a URL to a supported image file from something like:
// "icon": {
// "mediaType": "image/jpeg",
// "type": "Image",
// "url": "http://example.org/path/to/some/file.jpeg"
// },
func extractIconURL(i iconable) (*url.URL, error) {
iconProp := i.GetActivityStreamsIcon()
if iconProp == nil {
return nil, errors.New("icon property was nil")
}
// icon can potentially contain multiple entries, so we iterate through all of them
// here in order to find the first one that meets these criteria:
// 1. is an image
// 2. is a supported type
// 3. has a URL so we can grab it
for iconIter := iconProp.Begin(); iconIter != iconProp.End(); iconIter = iconIter.Next() {
// 1. is an image
if !iconIter.IsActivityStreamsImage() {
continue
}
imageValue := iconIter.GetActivityStreamsImage()
if imageValue == nil {
continue
}
// 2. is a supported type
imageType := imageValue.GetActivityStreamsMediaType()
if imageType == nil || !media.SupportedImageType(imageType.Get()) {
continue
}
// 3. has a URL so we can grab it
imageURLProp := imageValue.GetActivityStreamsUrl()
if imageURLProp == nil {
continue
}
// URL is also an iterable!
// so let's take the first valid one we can find
for urlIter := imageURLProp.Begin(); urlIter != imageURLProp.End(); urlIter = urlIter.Next() {
if !urlIter.IsIRI() {
continue
}
if urlIter.GetIRI() == nil {
continue
}
// found it!!!
return urlIter.GetIRI(), nil
}
}
// if we get to this point we didn't find an icon meeting our criteria :'(
return nil, errors.New("could not extract valid image from icon")
}
type imageable interface {
GetActivityStreamsImage() vocab.ActivityStreamsImageProperty
}
// extractImageURL extracts a URL to a supported image file from something like:
// "image": {
// "mediaType": "image/jpeg",
// "type": "Image",
// "url": "http://example.org/path/to/some/file.jpeg"
// },
func extractImageURL(i imageable) (*url.URL, error) {
imageProp := i.GetActivityStreamsImage()
if imageProp == nil {
return nil, errors.New("icon property was nil")
}
// icon can potentially contain multiple entries, so we iterate through all of them
// here in order to find the first one that meets these criteria:
// 1. is an image
// 2. is a supported type
// 3. has a URL so we can grab it
for imageIter := imageProp.Begin(); imageIter != imageProp.End(); imageIter = imageIter.Next() {
// 1. is an image
if !imageIter.IsActivityStreamsImage() {
continue
}
imageValue := imageIter.GetActivityStreamsImage()
if imageValue == nil {
continue
}
// 2. is a supported type
imageType := imageValue.GetActivityStreamsMediaType()
if imageType == nil || !media.SupportedImageType(imageType.Get()) {
continue
}
// 3. has a URL so we can grab it
imageURLProp := imageValue.GetActivityStreamsUrl()
if imageURLProp == nil {
continue
}
// URL is also an iterable!
// so let's take the first valid one we can find
for urlIter := imageURLProp.Begin(); urlIter != imageURLProp.End(); urlIter = urlIter.Next() {
if !urlIter.IsIRI() {
continue
}
if urlIter.GetIRI() == nil {
continue
}
// found it!!!
return urlIter.GetIRI(), nil
}
}
// if we get to this point we didn't find an image meeting our criteria :'(
return nil, errors.New("could not extract valid image from image property")
} }

View file

@ -440,14 +440,14 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
Discoverable: true, Discoverable: true,
Sensitive: false, Sensitive: false,
Language: "en", Language: "en",
URI: "https://fossbros-anonymous.io/users/foss_satan", URI: "http://fossbros-anonymous.io/users/foss_satan",
URL: "https://fossbros-anonymous.io/@foss_satan", URL: "http://fossbros-anonymous.io/@foss_satan",
LastWebfingeredAt: time.Time{}, LastWebfingeredAt: time.Time{},
InboxURI: "https://fossbros-anonymous.io/users/foss_satan/inbox", InboxURI: "http://fossbros-anonymous.io/users/foss_satan/inbox",
OutboxURI: "https://fossbros-anonymous.io/users/foss_satan/outbox", OutboxURI: "http://fossbros-anonymous.io/users/foss_satan/outbox",
FollowersURI: "https://fossbros-anonymous.io/users/foss_satan/followers", FollowersURI: "http://fossbros-anonymous.io/users/foss_satan/followers",
FollowingURI: "https://fossbros-anonymous.io/users/foss_satan/following", FollowingURI: "http://fossbros-anonymous.io/users/foss_satan/following",
FeaturedCollectionURI: "https://fossbros-anonymous.io/users/foss_satan/collections/featured", FeaturedCollectionURI: "http://fossbros-anonymous.io/users/foss_satan/collections/featured",
ActorType: gtsmodel.ActivityStreamsPerson, ActorType: gtsmodel.ActivityStreamsPerson,
AlsoKnownAs: "", AlsoKnownAs: "",
PrivateKey: nil, PrivateKey: nil,
@ -1047,12 +1047,20 @@ func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]Activit
} }
} }
func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[string]ActivityWithSignature {
sig, digest, date := getSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_1"].URI))
return map[string]ActivityWithSignature{
"foss_satan_dereference_zork": {
SignatureHeader: sig,
DigestHeader: digest,
DateHeader: date,
},
}
}
// getSignatureForActivity does some sneaky sneaky work with a mock http client and a test transport controller, in order to derive // getSignatureForActivity does some sneaky sneaky work with a mock http client and a test transport controller, in order to derive
// the HTTP Signature for the given activity, public key ID, private key, and destination. // the HTTP Signature for the given activity, public key ID, private key, and destination.
func getSignatureForActivity(activity pub.Activity, pubKeyID string, privkey crypto.PrivateKey, destination *url.URL) (signatureHeader string, digestHeader string, dateHeader string) { func getSignatureForActivity(activity pub.Activity, pubKeyID string, privkey crypto.PrivateKey, destination *url.URL) (signatureHeader string, digestHeader string, dateHeader string) {
streams.NewActivityStreamsPerson()
// create a client that basically just pulls the signature out of the request and sets it // create a client that basically just pulls the signature out of the request and sets it
client := &mockHTTPClient{ client := &mockHTTPClient{
do: func(req *http.Request) (*http.Response, error) { do: func(req *http.Request) (*http.Response, error) {
@ -1093,6 +1101,39 @@ func getSignatureForActivity(activity pub.Activity, pubKeyID string, privkey cry
return return
} }
// getSignatureForDereference does some sneaky sneaky work with a mock http client and a test transport controller, in order to derive
// the HTTP Signature for the given derefence GET request using public key ID, private key, and destination.
func getSignatureForDereference(pubKeyID string, privkey crypto.PrivateKey, destination *url.URL) (signatureHeader string, digestHeader string, dateHeader string) {
// create a client that basically just pulls the signature out of the request and sets it
client := &mockHTTPClient{
do: func(req *http.Request) (*http.Response, error) {
signatureHeader = req.Header.Get("Signature")
digestHeader = req.Header.Get("Digest")
dateHeader = req.Header.Get("Date")
r := ioutil.NopCloser(bytes.NewReader([]byte{})) // we only need this so the 'close' func doesn't nil out
return &http.Response{
StatusCode: 200,
Body: r,
}, nil
},
}
// use the client to create a new transport
c := NewTestTransportController(client)
tp, err := c.NewTransport(pubKeyID, privkey)
if err != nil {
panic(err)
}
// trigger the delivery function, which will trigger the 'do' function of the recorder above
if _, err := tp.Dereference(context.Background(), destination); err != nil {
panic(err)
}
// headers should now be populated
return
}
// newNote returns a new activity streams note for the given parameters // newNote returns a new activity streams note for the given parameters
func newNote( func newNote(
noteID *url.URL, noteID *url.URL,