Merge pull request #254 from Human-Connection/fix_performance_issues_during_tests

Bundle all activityPub in middleware
This commit is contained in:
Robert Schäfer 2019-03-20 19:38:59 +01:00 committed by GitHub
commit 7d26b03f88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 150 additions and 115 deletions

View File

@ -1,69 +1,84 @@
import { createSignature, verifySignature } from '.' import { generateRsaKeyPair, createSignature, verifySignature } from '.'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
import { GraphQLClient } from 'graphql-request'
import crypto from 'crypto' import crypto from 'crypto'
import { expect } from 'chai' import request from 'request'
const factory = Factory() jest.mock('request')
describe('Signature creation and verification', () => { let privateKey
let user = null let publicKey
let client = null let headers
const headers = { const passphrase = 'a7dsf78sadg87ad87sfagsadg78'
'Date': '2019-03-08T14:35:45.759Z',
'Host': 'democracy-app.de',
'Content-Type': 'application/json'
}
beforeEach(async () => { describe('activityPub/security', () => {
await factory.create('User', { beforeEach(() => {
'slug': 'test-user', const pair = generateRsaKeyPair({ passphrase })
'name': 'Test User', privateKey = pair.privateKey
'email': 'user@example.org', publicKey = pair.publicKey
'password': 'swordfish' headers = {
'Date': '2019-03-08T14:35:45.759Z',
'Host': 'democracy-app.de',
'Content-Type': 'application/json'
}
})
describe('createSignature', () => {
describe('returned http signature', () => {
let signatureB64
let httpSignature
beforeEach(() => {
const signer = crypto.createSign('rsa-sha256')
signer.update('(request-target): post /activitypub/users/max/inbox\ndate: 2019-03-08T14:35:45.759Z\nhost: democracy-app.de\ncontent-type: application/json')
signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64')
httpSignature = createSignature({ privateKey, keyId: 'https://human-connection.org/activitypub/users/lea#main-key', url: 'https://democracy-app.de/activitypub/users/max/inbox', headers, passphrase })
})
it('contains keyId', () => {
expect(httpSignature).toContain('keyId="https://human-connection.org/activitypub/users/lea#main-key"')
})
it('contains default algorithm "rsa-sha256"', () => {
expect(httpSignature).toContain('algorithm="rsa-sha256"')
})
it('contains headers', () => {
expect(httpSignature).toContain('headers="(request-target) date host content-type"')
})
it('contains signature', () => {
expect(httpSignature).toContain('signature="' + signatureB64 + '"')
})
}) })
const headers = await login({ email: 'user@example.org', password: 'swordfish' }) })
client = new GraphQLClient(host, { headers })
const result = await client.request(`query { describe('verifySignature', () => {
User(slug: "test-user") { let httpSignature
privateKey
publicKey beforeEach(() => {
httpSignature = createSignature({ privateKey, keyId: 'http://localhost:4001/activitypub/users/test-user#main-key', url: 'https://democracy-app.de/activitypub/users/max/inbox', headers, passphrase })
const body = {
'publicKey': {
'id': 'https://localhost:4001/activitypub/users/test-user#main-key',
'owner': 'https://localhost:4001/activitypub/users/test-user',
'publicKeyPem': publicKey
}
} }
}`)
user = result.User[0]
})
afterEach(async () => { const mockedRequest = jest.fn((_, callback) => callback(null, null, JSON.stringify(body)))
await factory.cleanDatabase() request.mockImplementation(mockedRequest)
})
describe('Signature creation', () => {
let signatureB64 = ''
beforeEach(() => {
const signer = crypto.createSign('rsa-sha256')
signer.update('(request-target): post /activitypub/users/max/inbox\ndate: 2019-03-08T14:35:45.759Z\nhost: democracy-app.de\ncontent-type: application/json')
signatureB64 = signer.sign({ key: user.privateKey, passphrase: 'a7dsf78sadg87ad87sfagsadg78' }, 'base64')
})
it('creates a Signature with given privateKey, keyId, url and headers (default algorithm: "rsa-sha256")', () => {
const httpSignature = createSignature(user.privateKey, 'https://human-connection.org/activitypub/users/lea#main-key', 'https://democracy-app.de/activitypub/users/max/inbox', headers)
expect(httpSignature).to.contain('keyId="https://human-connection.org/activitypub/users/lea#main-key"')
expect(httpSignature).to.contain('algorithm="rsa-sha256"')
expect(httpSignature).to.contain('headers="(request-target) date host content-type"')
expect(httpSignature).to.contain('signature="' + signatureB64 + '"')
})
})
describe('Signature verification', () => {
let httpSignature = ''
beforeEach(() => {
httpSignature = createSignature(user.privateKey, 'http://localhost:4001/activitypub/users/test-user#main-key', 'https://democracy-app.de/activitypub/users/max/inbox', headers)
}) })
it('verifies a Signature correct', async () => { it('resolves false', async () => {
headers['Signature'] = httpSignature await expect(verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers)).resolves.toEqual(false)
const isVerified = await verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers) })
expect(isVerified).to.equal(true)
describe('valid signature', () => {
beforeEach(() => {
headers.Signature = httpSignature
})
it('resolves true', async () => {
await expect(verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers)).resolves.toEqual(true)
})
}) })
}) })
}) })

View File

@ -6,7 +6,8 @@ const debug = require('debug')('ea:security')
dotenv.config({ path: resolve('src', 'activitypub', '.env') }) dotenv.config({ path: resolve('src', 'activitypub', '.env') })
export function generateRsaKeyPair () { export function generateRsaKeyPair (options = {}) {
const { passphrase = process.env.PRIVATE_KEY_PASSPHRASE } = options
return crypto.generateKeyPairSync('rsa', { return crypto.generateKeyPairSync('rsa', {
modulusLength: 4096, modulusLength: 4096,
publicKeyEncoding: { publicKeyEncoding: {
@ -17,18 +18,24 @@ export function generateRsaKeyPair () {
type: 'pkcs8', type: 'pkcs8',
format: 'pem', format: 'pem',
cipher: 'aes-256-cbc', cipher: 'aes-256-cbc',
passphrase: process.env.PRIVATE_KEY_PASSPHRASE passphrase
} }
}) })
} }
// signing // signing
export function createSignature (privKey, keyId, url, headers = {}, algorithm = 'rsa-sha256') { export function createSignature (options) {
const {
privateKey, keyId, url,
headers = {},
algorithm = 'rsa-sha256',
passphrase = process.env.PRIVATE_KEY_PASSPHRASE
} = options
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) } if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) }
const signer = crypto.createSign(algorithm) const signer = crypto.createSign(algorithm)
const signingString = constructSigningString(url, headers) const signingString = constructSigningString(url, headers)
signer.update(signingString) signer.update(signingString)
const signatureB64 = signer.sign({ key: privKey, passphrase: process.env.PRIVATE_KEY_PASSPHRASE }, 'base64') const signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64')
const headersString = Object.keys(headers).reduce((result, key) => { return result + ' ' + key.toLowerCase() }, '') const headersString = Object.keys(headers).reduce((result, key) => { return result + ' ' + key.toLowerCase() }, '')
return `keyId="${keyId}",algorithm="${algorithm}",headers="(request-target)${headersString}",signature="${signatureB64}"` return `keyId="${keyId}",algorithm="${algorithm}",headers="(request-target)${headersString}",signature="${signatureB64}"`
} }

View File

@ -75,12 +75,15 @@ export function signAndSend (activity, fromName, targetDomain, url) {
headers: { headers: {
'Host': targetDomain, 'Host': targetDomain,
'Date': date, 'Date': date,
'Signature': createSignature(privateKey, `http://${activityPub.domain}/activitypub/users/${fromName}#main-key`, url, 'Signature': createSignature({ privateKey,
{ keyId: `http://${activityPub.domain}/activitypub/users/${fromName}#main-key`,
url,
headers: {
'Host': targetDomain, 'Host': targetDomain,
'Date': date, 'Date': date,
'Content-Type': 'application/activity+json' 'Content-Type': 'application/activity+json'
}), }
}),
'Content-Type': 'application/activity+json' 'Content-Type': 'application/activity+json'
}, },
method: 'POST', method: 'POST',

View File

@ -13,7 +13,7 @@ export default async (driver, authorizationHeader) => {
const session = driver.session() const session = driver.session()
const query = ` const query = `
MATCH (user:User {id: {id} }) MATCH (user:User {id: {id} })
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled} RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
LIMIT 1 LIMIT 1
` `
const result = await session.run(query, { id }) const result = await session.run(query, { id })

View File

@ -0,0 +1,56 @@
import { generateRsaKeyPair } from '../activitypub/security'
import { activityPub } from '../activitypub/ActivityPub'
import as from 'activitystrea.ms'
import dotenv from 'dotenv'
const debug = require('debug')('backend:schema')
dotenv.config()
export default {
Mutation: {
CreatePost: async (resolve, root, args, context, info) => {
args.activityId = activityPub.generateStatusId(context.user.slug)
args.objectId = activityPub.generateStatusId(context.user.slug)
const post = await resolve(root, args, context, info)
const { user: author } = context
const actorId = author.actorId
debug(`actorId = ${actorId}`)
const createActivity = await new Promise((resolve, reject) => {
as.create()
.id(`${actorId}/status/${args.activityId}`)
.actor(`${actorId}`)
.object(
as.article()
.id(`${actorId}/status/${post.id}`)
.content(post.content)
.to('https://www.w3.org/ns/activitystreams#Public')
.publishedNow()
.attributedTo(`${actorId}`)
).prettyWrite((err, doc) => {
if (err) {
reject(err)
} else {
debug(doc)
const parsedDoc = JSON.parse(doc)
parsedDoc.send = true
resolve(JSON.stringify(parsedDoc))
}
})
})
try {
await activityPub.sendActivity(createActivity)
} catch (e) {
debug(`error sending post activity\n${e}`)
}
return post
},
CreateUser: async (resolve, root, args, context, info) => {
const keys = generateRsaKeyPair()
Object.assign(args, keys)
args.actorId = `${process.env.GRAPHQL_URI}/activitypub/users/${args.slug}`
return resolve(root, args, context, info)
}
}
}

View File

@ -1,3 +1,4 @@
import activityPubMiddleware from './activityPubMiddleware'
import passwordMiddleware from './passwordMiddleware' import passwordMiddleware from './passwordMiddleware'
import softDeleteMiddleware from './softDeleteMiddleware' import softDeleteMiddleware from './softDeleteMiddleware'
import sluggifyMiddleware from './sluggifyMiddleware' import sluggifyMiddleware from './sluggifyMiddleware'
@ -25,6 +26,7 @@ export default schema => {
// add permisions middleware at the first position (unless we're seeding) // add permisions middleware at the first position (unless we're seeding)
// NOTE: DO NOT SET THE PERMISSION FLAT YOUR SELF // NOTE: DO NOT SET THE PERMISSION FLAT YOUR SELF
if (process.env.PERMISSIONS !== 'disabled' && process.env.NODE_ENV !== 'production') { if (process.env.PERMISSIONS !== 'disabled' && process.env.NODE_ENV !== 'production') {
middleware.unshift(activityPubMiddleware)
middleware.unshift(permissionsMiddleware.generate(schema)) middleware.unshift(permissionsMiddleware.generate(schema))
} }
return middleware return middleware

View File

@ -1,5 +1,4 @@
import createOrUpdateLocations from './nodes/locations' import createOrUpdateLocations from './nodes/locations'
import { generateRsaKeyPair } from '../activitypub/security'
import dotenv from 'dotenv' import dotenv from 'dotenv'
dotenv.config() dotenv.config()
@ -7,9 +6,6 @@ dotenv.config()
export default { export default {
Mutation: { Mutation: {
CreateUser: async (resolve, root, args, context, info) => { CreateUser: async (resolve, root, args, context, info) => {
const keys = generateRsaKeyPair()
Object.assign(args, keys)
args.actorId = `${process.env.GRAPHQL_URI}/activitypub/users/${args.slug}`
const result = await resolve(root, args, context, info) const result = await resolve(root, args, context, info)
await createOrUpdateLocations(args.id, args.locationName, context.driver) await createOrUpdateLocations(args.id, args.locationName, context.driver)
return result return result

View File

@ -1,24 +1,12 @@
import { neo4jgraphql } from 'neo4j-graphql-js' import { neo4jgraphql } from 'neo4j-graphql-js'
import { activityPub } from '../activitypub/ActivityPub'
import as from 'activitystrea.ms'
import dotenv from 'dotenv'
/*
import as from 'activitystrea.ms'
import request from 'request'
*/
const debug = require('debug')('backend:schema')
dotenv.config()
export default { export default {
Mutation: { Mutation: {
CreatePost: async (object, params, context, resolveInfo) => { CreatePost: async (object, params, context, resolveInfo) => {
params.activityId = activityPub.generateStatusId(context.user.slug)
params.objectId = activityPub.generateStatusId(context.user.slug)
const result = await neo4jgraphql(object, params, context, resolveInfo, false) const result = await neo4jgraphql(object, params, context, resolveInfo, false)
const session = context.driver.session() const session = context.driver.session()
const author = await session.run( await session.run(
'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' + 'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' +
'MERGE (post)<-[:WROTE]-(author) ' + 'MERGE (post)<-[:WROTE]-(author) ' +
'RETURN author', { 'RETURN author', {
@ -28,38 +16,6 @@ export default {
) )
session.close() session.close()
debug(`actorId = ${author.records[0]._fields[0].properties.actorId}`)
if (Array.isArray(author.records) && author.records.length > 0) {
const actorId = author.records[0]._fields[0].properties.actorId
const createActivity = await new Promise((resolve, reject) => {
as.create()
.id(`${actorId}/status/${params.activityId}`)
.actor(`${actorId}`)
.object(
as.article()
.id(`${actorId}/status/${result.id}`)
.content(result.content)
.to('https://www.w3.org/ns/activitystreams#Public')
.publishedNow()
.attributedTo(`${actorId}`)
).prettyWrite((err, doc) => {
if (err) {
reject(err)
} else {
debug(doc)
const parsedDoc = JSON.parse(doc)
parsedDoc.send = true
resolve(JSON.stringify(parsedDoc))
}
})
})
try {
await activityPub.sendActivity(createActivity)
} catch (e) {
debug(`error sending post activity\n${e}`)
}
}
return result return result
} }
} }