Skip to content

Commit

Permalink
Merge pull request #49 from immers-space/reliability
Browse files Browse the repository at this point in the history
Misc reliability fixes
  • Loading branch information
quinn-madson authored May 17, 2021
2 parents ed913c8 + 1c934ca commit eed85e7
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 29 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ store | Replace the default storage model & database backend with your own (see
threadDepth | Controls how far up apex will follow links in incoming activities in order to display the conversation thread & check for inbox forwarding needs (default 10)
systemUser | Actor object representing system and used for signing GETs (see below)
offlineMode | Disable delivery. Useful for running migrations and queueing deliveries to be sent when app is running
requestTimeout | Timeout for requests to other servers, ms (default 5000)

Blocked, rejections, and rejected: these routes must be defined in order to track
these items internally for each actor, but they do not need to be exposed endpoints
Expand Down
5 changes: 4 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ module.exports = function (settings) {
apex.systemUser = settings.systemUser
apex.logger = settings.logger || console
apex.offlineMode = settings.offlineMode
apex.requestTimeout = settings.requestTimeout ?? 5000
apex.utils = {
usernameToIRI: apex.idToIRIFactory(apex.domain, settings.routes.actor, apex.actorParam),
objectIdToIRI: apex.idToIRIFactory(apex.domain, settings.routes.object, apex.objectParam),
Expand All @@ -56,7 +57,9 @@ module.exports = function (settings) {
function onFinishedHandler (err, res) {
if (err) return
const apexLocal = res.locals.apex
Promise.all(apexLocal.postWork.map(task => task.call(res)))
// execute postWork tasks in sequence (not parallel)
apexLocal.postWork
.reduce((acc, task) => acc.then(() => task(res)), Promise.resolve())
.then(() => {
if (apexLocal.eventName) {
res.app.emit(apexLocal.eventName, apexLocal.eventMessage)
Expand Down
20 changes: 9 additions & 11 deletions net/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,15 @@ function inboxActivity (req, res, next) {
return next()
}
} else if (type === 'accept') {
// the activity being accepted was sent to the actor trying to accept it
if (!object.to.includes(actor.id)) {
resLocal.status = 403
return next()
}
// for follows, also confirm the follow object was the actor trying to accept it
const isFollow = object.type.toLowerCase() === 'follow'
if (isFollow && !apex.validateTarget(object, actor.id)) {
resLocal.status = 403
return next()
} else if (!isFollow && !object.to?.includes(actor.id)) {
// the activity being accepted was sent to the actor trying to accept it
resLocal.status = 403
return next()
}
}
tasks.push(apex.embedCollections(activity))
Expand Down Expand Up @@ -360,16 +359,15 @@ function outboxActivity (req, res, next) {
return next()
}
if (type === 'accept') {
// the activity being accepted was sent to the actor trying to accept it
if (!object.to.includes(actor.id)) {
resLocal.status = 403
return next()
}
// for follows, also confirm the follow object was the actor trying to accept it
// for follows, confirm the follow object was the actor trying to accept it
const isFollow = object.type.toLowerCase() === 'follow'
if (isFollow && !apex.validateTarget(object, actor.id)) {
resLocal.status = 403
return next()
} else if (!isFollow && !object.to?.includes(actor.id)) {
// for other accepts, check the activity being accepted was sent to the actor trying to accept it
resLocal.status = 403
return next()
}
} else if (type === 'create') {
// per spec, ensure attributedTo and audience fields in object are correct
Expand Down
4 changes: 3 additions & 1 deletion pub/federation.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ function requestObject (id) {
const req = {
url: id,
headers: { Accept: 'application/activity+json' },
json: true
json: true,
timeout: this.requestTimeout
}
if (this.systemUser) {
req.httpSignature = {
Expand Down Expand Up @@ -78,6 +79,7 @@ function deliver (actorId, activity, address, signingKey) {
},
resolveWithFullResponse: true,
simple: false,
timeout: this.requestTimeout,
body: activity
})
}
Expand Down
22 changes: 21 additions & 1 deletion pub/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
'use strict'
const fs = require('fs')
const path = require('path')
const jsonld = require('jsonld')
const merge = require('deepmerge')
const actorStreamNames = ['inbox', 'outbox', 'following', 'followers', 'liked', 'blocked', 'rejected', 'rejections']
Expand Down Expand Up @@ -336,10 +338,27 @@ function validateTarget (object, targetId, prop = 'object') {
return false
}

// keep main contexts in memory for speedy access
const coreContexts = {
'https://w3id.org/security/v1': {
contextUrl: null,
documentUrl: 'https://w3id.org/security/v1',
document: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../vocab/security.json')))
},
'https://www.w3.org/ns/activitystreams': {
contextUrl: null,
documentUrl: 'https://www.w3.org/ns/activitystreams',
document: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../vocab/as.json')))
}
}
// cached JSONLD contexts to reduce requests an eliminate
// failures caused when context servers are unavailable
const nodeDocumentLoader = jsonld.documentLoaders.node()

async function jsonldContextLoader (url, options) {
if (coreContexts[url]) {
return coreContexts[url]
}
try {
const cached = await this.store.getContext(url)
if (cached) {
Expand All @@ -351,12 +370,13 @@ async function jsonldContextLoader (url, options) {
const context = await nodeDocumentLoader(url)
if (context && context.document) {
try {
// save original url in case of redirects
context.documentUrl = url
await this.store.saveContext(context)
} catch (err) {
this.logger.error('Error saving jsonld contact cache', err.message)
}
}
// call the default documentLoader
return context
}

Expand Down
16 changes: 16 additions & 0 deletions spec/functional/inbox.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,23 @@ describe('inbox', function () {
.expect(200)
.end(err => { if (err) done(err) })
})
it('handles accept to follow without a to field', async function (done) {
delete follow.to
await apex.store.saveActivity(follow)
app.once('apex-inbox', msg => {
expect(msg.object.id).toEqual(follow.id)
expect(msg.object._meta.collection).toContain(testUser.following[0])
done()
})
request(app)
.post('/inbox/test')
.set('Content-Type', 'application/activity+json')
.send(accept)
.expect(200)
.end(err => { if (err) done(err) })
})
it('rejects accept from non-recipients of original activity', async function (done) {
follow.type = 'Offer'
follow.to = ['https://ignore.com/sally']
await apex.store.saveActivity(follow)
app.once('apex-inbox', msg => {
Expand Down
20 changes: 20 additions & 0 deletions spec/functional/outbox.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,26 @@ describe('outbox', function () {
.expect(201)
.end(err => { if (err) done(err) })
})
it('handles accept of follow without to field', async function (done) {
delete follow.to
await apex.store.saveActivity(follow)
app.once('apex-outbox', msg => {
expect(msg.actor).toEqual(testUser)
const exp = merge({ _meta: { collection: ['https://localhost/outbox/test'] } }, activityNormalized)
exp.type = 'Accept'
exp.object = [follow.id]
expect(global.stripIds(msg.activity)).toEqual(exp)
follow._meta.collection.push(testUser.followers[0])
expect(msg.object).toEqual(follow)
done()
})
request(app)
.post('/authorized/outbox/test')
.set('Content-Type', 'application/activity+json')
.send(accept)
.expect(201)
.end(err => { if (err) done(err) })
})
it('publishes collection update', async function (done) {
const mockedUser = 'https://mocked.com/user/mocked'
nock('https://mocked.com')
Expand Down
18 changes: 3 additions & 15 deletions spec/helpers/nocks.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
/* global beforeAll, afterAll */
const fs = require('fs')
/* global beforeEach, afterEach */
const nock = require('nock')
const activities = fs.readFileSync('vocab/as.json')
const security = fs.readFileSync('vocab/security.json')
beforeAll(() => {
nock('https://www.w3.org')
.get('/ns/activitystreams')
.reply(200, activities)
.persist(true)
nock('https://w3id.org')
.get('/security/v1')
.reply(200, security)
.persist(true)
// block federation attempts
beforeEach(() => {
nock('https://ignore.com')
.get(uri => uri.startsWith('/s/'))
.reply(200, uri => ({ id: `https://ignore.com${uri}`, type: 'Activity', actor: 'https://ignore.com/u/bob' }))
Expand All @@ -24,6 +12,6 @@ beforeAll(() => {
.reply(200)
.persist()
})
afterAll(() => {
afterEach(() => {
nock.cleanAll()
})
28 changes: 28 additions & 0 deletions spec/unit/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,34 @@ describe('utils', function () {
contextUrl: null
})
})
it('caches redirected contexts by original url', async function () {
nock('https://mocked.com')
.get('/context/v1')
.reply(302, undefined, {
Location: 'http://redirect.com/context/v1'
})
nock('http://redirect.com')
.get('/context/v1')
.reply(200, context)
const doc = {
'@context': 'https://mocked.com/context/v1',
id: 'https://mocked.com/s/abc123',
customProp: 'https://mocked.com/s/123abc'
}
const ld = await apex.toJSONLD(doc)
expect(ld).toEqual({
'@context': apex.context,
id: 'https://mocked.com/s/abc123',
'https://mocked.com/context/v1#customProp': {
id: 'https://mocked.com/s/123abc'
}
})
expect(await apex.store.getContext('https://mocked.com/context/v1')).toEqual({
documentUrl: 'https://mocked.com/context/v1',
document: JSON.stringify(context),
contextUrl: null
})
})
it('uses cached context', async function () {
await apex.store.saveContext({
documentUrl: 'https://mocked.com/context/v1',
Expand Down

0 comments on commit eed85e7

Please sign in to comment.