hell yeah

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

View File

@ -34,10 +34,10 @@ const (
UsernameKey = "username"
// UsersBasePath is the base path for serving information about Users eg https://example.org/users
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.
// Eg https://example.org/users/:username
UsersBasePathWithID = UsersBasePath + "/:" + UsernameKey
UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey
)
// 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
func (m *Module) Route(s router.Router) error {
s.AttachHandler(http.MethodGet, UsersBasePathWithID, m.UsersGETHandler)
s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler)
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 {
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!
algo := httpsig.RSA_SHA256 // TODO: make this more robust

View File

@ -108,15 +108,15 @@ type FileType string
const (
// 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 FileType = "gif"
FileTypeGif FileType = "Gif"
// FileTypeAudio is for audio-only files (no video)
FileTypeAudio FileType = "audio"
FileTypeAudio FileType = "Audio"
// FileTypeVideo is for files with audio + visual
FileTypeVideo FileType = "video"
FileTypeVideo FileType = "Video"
// FileTypeUnknown is for unknown file types (surprise surprise!)
FileTypeUnknown FileType = "unknown"
FileTypeUnknown FileType = "Unknown"
)
// 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 {
return nil, err
}
if !supportedImageType(contentType) {
if !SupportedImageType(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]
switch mainType {
case "video":
if !supportedVideoType(contentType) {
case MIMEVideo:
if !SupportedVideoType(contentType) {
return nil, fmt.Errorf("video type %s not supported", contentType)
}
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 mh.processVideoAttachment(attachment, accountID, contentType)
case "image":
if !supportedImageType(contentType) {
case MIMEImage:
if !SupportedImageType(contentType) {
return nil, fmt.Errorf("image type %s not supported", contentType)
}
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)
}
// clean any exif data from image/png type but leave gifs alone
// clean any exif data from png but leave gifs alone
switch contentType {
case "image/png":
case MIMEPng:
if clean, err = purgeExif(emojiBytes); err != nil {
return nil, fmt.Errorf("error cleaning exif data: %s", err)
}
case "image/gif":
case MIMEGif:
clean = emojiBytes
default:
return nil, errors.New("media type unrecognized")
@ -275,7 +275,7 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) (
ImagePath: emojiPath,
ImageStaticPath: emojiStaticPath,
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),
ImageStaticFileSize: len(static.image),
ImageUpdatedAt: time.Now(),
@ -302,7 +302,7 @@ func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, co
var small *imageAndMeta
switch contentType {
case "image/jpeg", "image/png":
case MIMEJpeg, MIMEPng:
if clean, err = purgeExif(data); err != nil {
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 {
return nil, fmt.Errorf("error parsing image: %s", err)
}
case "image/gif":
case MIMEGif:
clean = data
original, err = deriveGif(clean, contentType)
if err != nil {
@ -380,7 +380,7 @@ func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, co
},
Thumbnail: gtsmodel.Thumbnail{
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),
UpdatedAt: time.Now(),
URL: smallURL,
@ -411,15 +411,15 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string
var err error
switch contentType {
case "image/jpeg":
case MIMEJpeg:
if clean, err = purgeExif(imageBytes); err != nil {
return nil, fmt.Errorf("error cleaning exif data: %s", err)
}
case "image/png":
case MIMEPng:
if clean, err = purgeExif(imageBytes); err != nil {
return nil, fmt.Errorf("error cleaning exif data: %s", err)
}
case "image/gif":
case MIMEGif:
clean = imageBytes
default:
return nil, errors.New("media type unrecognized")

View File

@ -33,6 +33,26 @@ import (
"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").
// Returns an error if the content type is not something we can process.
func parseContentType(content []byte) (string, error) {
@ -54,13 +74,13 @@ func parseContentType(content []byte) (string, error) {
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.
func supportedImageType(mimeType string) bool {
func SupportedImageType(mimeType string) bool {
acceptedImageTypes := []string{
"image/jpeg",
"image/gif",
"image/png",
MIMEJpeg,
MIMEGif,
MIMEPng,
}
for _, accepted := range acceptedImageTypes {
if mimeType == accepted {
@ -70,13 +90,13 @@ func supportedImageType(mimeType string) bool {
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.
func supportedVideoType(mimeType string) bool {
func SupportedVideoType(mimeType string) bool {
acceptedVideoTypes := []string{
"video/mp4",
"video/mpeg",
"video/webm",
MIMEMp4,
MIMEMpeg,
MIMEWebm,
}
for _, accepted := range acceptedVideoTypes {
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.
func supportedEmojiType(mimeType string) bool {
acceptedEmojiTypes := []string{
"image/gif",
"image/png",
MIMEGif,
MIMEPng,
}
for _, accepted := range acceptedEmojiTypes {
if mimeType == accepted {
@ -121,7 +141,7 @@ func deriveGif(b []byte, extension string) (*imageAndMeta, error) {
var g *gif.GIF
var err error
switch extension {
case "image/gif":
case MIMEGif:
g, err = gif.DecodeAll(bytes.NewReader(b))
if err != nil {
return nil, err
@ -161,12 +181,12 @@ func deriveImage(b []byte, contentType string) (*imageAndMeta, error) {
var err error
switch contentType {
case "image/jpeg":
case MIMEJpeg:
i, err = jpeg.Decode(bytes.NewReader(b))
if err != nil {
return nil, err
}
case "image/png":
case MIMEPng:
i, err = png.Decode(bytes.NewReader(b))
if err != nil {
return nil, err
@ -210,17 +230,17 @@ func deriveThumbnail(b []byte, contentType string, x uint, y uint) (*imageAndMet
var err error
switch contentType {
case "image/jpeg":
case MIMEJpeg:
i, err = jpeg.Decode(bytes.NewReader(b))
if err != nil {
return nil, err
}
case "image/png":
case MIMEPng:
i, err = png.Decode(bytes.NewReader(b))
if err != nil {
return nil, err
}
case "image/gif":
case MIMEGif:
i, err = gif.Decode(bytes.NewReader(b))
if err != nil {
return nil, err
@ -254,12 +274,12 @@ func deriveStaticEmoji(b []byte, contentType string) (*imageAndMeta, error) {
var err error
switch contentType {
case "image/png":
case MIMEPng:
i, err = png.Decode(bytes.NewReader(b))
if err != nil {
return nil, err
}
case "image/gif":
case MIMEGif:
i, err = gif.Decode(bytes.NewReader(b))
if err != nil {
return nil, err

View File

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

View File

@ -1,71 +1,197 @@
package typeutils
import (
"errors"
"fmt"
"net/url"
"github.com/go-fed/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
)
func (c *converter) ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsmodel.Account, error) {
// first check if we actually already know this person
uriProp := person.GetJSONLDId()
if uriProp == nil || !uriProp.IsIRI() {
return nil, errors.New("no id property found on person, or id was not an iri")
}
uri := uriProp.GetIRI()
// acct := &gtsmodel.Account{
// URI: "",
// URL: "",
// ID: "",
// Username: "",
// Domain: "",
// AvatarMediaAttachmentID: "",
// AvatarRemoteURL: "",
// HeaderMediaAttachmentID: "",
// HeaderRemoteURL: "",
// DisplayName: "",
// Fields: nil,
// Note: "",
// Memorial: false,
// MovedToAccountID: "",
// CreatedAt: time.Time{},
// UpdatedAt: time.Time{},
// Bot: false,
// Reason: "",
// Locked: false,
// Discoverable: true,
// Privacy: "",
// Sensitive: false,
// Language: "",
// LastWebfingeredAt: time.Now(),
// InboxURI: "",
// OutboxURI: "",
// FollowingURI: "",
// FollowersURI: "",
// FeaturedCollectionURI: "",
// ActorType: gtsmodel.ActivityStreamsPerson,
// AlsoKnownAs: "",
// PrivateKey: nil,
// PublicKey: nil,
// PublicKeyURI: "",
// SensitizedAt: time.Time{},
// SilencedAt: time.Time{},
// SuspendedAt: time.Time{},
// HideCollections: false,
// SuspensionOrigin: "",
// }
acct := &gtsmodel.Account{}
if err := c.db.GetWhere("uri", uri.String(), acct); err == nil {
// we already know this account so we can skip generating it
return acct, nil
} else {
if _, ok := err.(db.ErrNoEntries); !ok {
// we don't know the account and there's been a real error
return nil, fmt.Errorf("error getting account with uri %s from the database: %s", uri.String(), err)
}
}
// // ID
// // Generate a new uuid for our particular database.
// // This is distinct from the AP ID of the person.
// id := uuid.NewString()
// acct.ID = id
// we don't know the account so we need to generate it from the person -- at least we already have the URI!
acct = &gtsmodel.Account{}
acct.URI = uri.String()
// // Username
// // We need this one so bail if it's not set.
// username := person.GetActivityStreamsPreferredUsername()
// if username == nil || username.GetXMLSchemaString() == "" {
// return nil, errors.New("preferredusername was empty")
// }
// acct.Username = username.GetXMLSchemaString()
// Username
// We need this one so bail if it's not set.
username, err := extractUsername(person)
if err != nil {
return nil, fmt.Errorf("couldn't extract username: %s", err)
}
acct.Username = username
// // Domain
// // We need this one as well
// acct.Domain = domain
// Domain
// We need this one as well
acct.Domain = uri.Host
return nil, nil
// avatar aka icon
// if this one isn't extractable in a format we recognise we'll just skip it
if avatarURL, err := extractIconURL(person); err == nil {
acct.AvatarRemoteURL = avatarURL.String()
}
// 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 {
acct.HeaderRemoteURL = headerURL.String()
}
return acct, nil
}
type usernameable interface {
GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty
}
func extractUsername(i usernameable) (string, error) {
u := i.GetActivityStreamsPreferredUsername()
if u == nil || !u.IsXMLSchemaString() {
return "", errors.New("preferredUsername was not a string")
}
if u.GetXMLSchemaString() == "" {
return "", errors.New("preferredUsername was empty")
}
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,
Sensitive: false,
Language: "en",
URI: "https://fossbros-anonymous.io/users/foss_satan",
URL: "https://fossbros-anonymous.io/@foss_satan",
URI: "http://fossbros-anonymous.io/users/foss_satan",
URL: "http://fossbros-anonymous.io/@foss_satan",
LastWebfingeredAt: time.Time{},
InboxURI: "https://fossbros-anonymous.io/users/foss_satan/inbox",
OutboxURI: "https://fossbros-anonymous.io/users/foss_satan/outbox",
FollowersURI: "https://fossbros-anonymous.io/users/foss_satan/followers",
FollowingURI: "https://fossbros-anonymous.io/users/foss_satan/following",
FeaturedCollectionURI: "https://fossbros-anonymous.io/users/foss_satan/collections/featured",
InboxURI: "http://fossbros-anonymous.io/users/foss_satan/inbox",
OutboxURI: "http://fossbros-anonymous.io/users/foss_satan/outbox",
FollowersURI: "http://fossbros-anonymous.io/users/foss_satan/followers",
FollowingURI: "http://fossbros-anonymous.io/users/foss_satan/following",
FeaturedCollectionURI: "http://fossbros-anonymous.io/users/foss_satan/collections/featured",
ActorType: gtsmodel.ActivityStreamsPerson,
AlsoKnownAs: "",
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
// 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) {
streams.NewActivityStreamsPerson()
// 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) {
@ -1093,6 +1101,39 @@ func getSignatureForActivity(activity pub.Activity, pubKeyID string, privkey cry
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
func newNote(
noteID *url.URL,