Some fixes + refactoring + logic to receive sharedInboxEndpoints and to add new one

This commit is contained in:
Armin 2019-02-26 17:11:41 +01:00
parent f74a45379f
commit 577157c505
10 changed files with 192 additions and 141 deletions

View File

@ -70,6 +70,7 @@
"sanitize-html": "~1.20.0",
"slug": "~1.0.0",
"trunc-html": "~1.1.2",
"uuid": "^3.3.2",
"wait-on": "~3.2.0"
},
"devDependencies": {

View File

@ -1,4 +1,10 @@
import { sendAcceptActivity, sendRejectActivity, extractNameFromId, extractDomainFromUrl } from './utils'
import {
sendAcceptActivity,
sendRejectActivity,
extractNameFromId,
extractDomainFromUrl,
signAndSend
} from './utils'
import request from 'request'
import as from 'activitystrea.ms'
import NitroDatasource from './NitroDatasource'
@ -69,6 +75,8 @@ export default class ActivityPub {
}, async (err, response, toActorObject) => {
if (err) return reject(err)
debug(`name = ${toActorName}@${this.domain}`)
// save shared inbox
await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
let followersCollectionPage = await this.dataSource.getFollowersCollectionPage(activity.object)
@ -123,9 +131,9 @@ export default class ActivityPub {
case 'Note':
const articleObject = activity.object
if (articleObject.inReplyTo) {
return this.dataSource.createComment(articleObject)
return this.dataSource.createComment(activity)
} else {
return this.dataSource.createPost(articleObject)
return this.dataSource.createPost(activity)
}
default:
}
@ -134,4 +142,17 @@ export default class ActivityPub {
handleDeleteActivity (activity) {
debug('inside delete')
}
async sendActivity (activity) {
if (Array.isArray(activity.to) && activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
delete activity.send
const fromName = extractNameFromId(activity.actor)
const sharedInboxEndpoints = await this.dataSource.getSharedInboxEndpoints()
await Promise.all(
sharedInboxEndpoints.map((el) => {
return signAndSend(activity, fromName, new URL(el).host, el)
})
)
}
}
}

View File

@ -278,15 +278,17 @@ export default class NitroDatasource {
)
}
async createPost (postObject) {
async createPost (activity) {
// TODO how to handle the to field? Now the post is just created, doesn't matter who is the recipient
// createPost
const postObject = activity.object
const title = postObject.summary ? postObject.summary : postObject.content.split(' ').slice(0, 5).join(' ')
const id = extractIdFromActivityId(postObject.id)
const postId = extractIdFromActivityId(postObject.id)
const activityId = extractIdFromActivityId(activity.id)
let result = await this.client.mutate({
mutation: gql`
mutation {
CreatePost(content: "${postObject.content}", title: "${title}", id: "${id}") {
CreatePost(content: "${postObject.content}", title: "${title}", id: "${postId}", activityId: "${activityId}") {
id
}
}
@ -300,7 +302,7 @@ export default class NitroDatasource {
result = await this.client.mutate({
mutation: gql`
mutation {
AddPostAuthor(from: {id: "${userId}"}, to: {id: "${id}"})
AddPostAuthor(from: {id: "${userId}"}, to: {id: "${postId}"})
}
`
})
@ -308,11 +310,41 @@ export default class NitroDatasource {
throwErrorIfGraphQLErrorOccurred(result)
}
async createComment (postObject) {
async getSharedInboxEndpoints () {
const result = await this.client.query({
query: gql`
query {
SharedInboxEndpoint {
uri
}
}
`
})
throwErrorIfGraphQLErrorOccurred(result)
return result.data.SharedInboxEnpoint
}
async addSharedInboxEndpoint (uri) {
try {
const result = await this.client.mutate({
mutation: gql`
mutation {
CreateSharedInboxEndpoint(uri: "${uri}")
}
`
})
throwErrorIfGraphQLErrorOccurred(result)
return true
} catch (e) {
return false
}
}
async createComment (activity) {
const postObject = activity.object
let result = await this.client.mutate({
mutation: gql`
mutation {
CreateComment(content: "${postObject.content}") {
CreateComment(content: "${postObject.content}", activityId: "${extractIdFromActivityId(activity.id)}") {
id
}
}

View File

@ -1,5 +1,5 @@
import express from 'express'
import { verify } from '../security'
import { verifySignature } from '../security'
const debug = require('debug')('ea:inbox')
const router = express.Router()
@ -10,21 +10,25 @@ router.post('/', async function (req, res, next) {
debug(`Content-Type = ${req.get('Content-Type')}`)
debug(`body = ${JSON.stringify(req.body, null, 2)}`)
debug(`Request headers = ${JSON.stringify(req.headers, null, 2)}`)
debug(`verify = ${await verify(`${req.protocol}://${req.hostname}:${req.port}${req.originalUrl}`, req.headers)}`)
debug(`verify = ${await verifySignature(`${req.protocol}://${req.hostname}:${req.port}${req.originalUrl}`, req.headers)}`)
switch (req.body.type) {
case 'Create':
await await req.app.get('activityPub').handleCreateActivity(req.body).catch(next)
if (req.body.send) {
await req.app.get('ap').sendActivity(req.body).catch(next)
break
}
await req.app.get('ap').handleCreateActivity(req.body).catch(next)
break
case 'Undo':
await await req.app.get('activityPub').handleUndoActivity(req.body).catch(next)
await req.app.get('ap').handleUndoActivity(req.body).catch(next)
break
case 'Follow':
debug('handleFollow')
await req.app.get('activityPub').handleFollowActivity(req.body)
await req.app.get('ap').handleFollowActivity(req.body)
debug('handledFollow')
break
case 'Delete':
await await req.app.get('activityPub').handleDeleteActivity(req.body).catch(next)
await req.app.get('ap').handleDeleteActivity(req.body).catch(next)
break
/* eslint-disable */
case 'Update':

View File

@ -1,7 +1,7 @@
import { sendCollection } from '../utils'
import express from 'express'
import { serveUser } from '../utils/serveUser'
import { verify } from '../security'
import { verifySignature } from '../security'
const router = express.Router()
const debug = require('debug')('ea:user')
@ -47,7 +47,7 @@ router.get('/:name/outbox', (req, res) => {
router.post('/:name/inbox', async function (req, res, next) {
debug(`body = ${JSON.stringify(req.body, null, 2)}`)
debug(`actorId = ${req.body.actor}`)
debug(`verify = ${await verify(`${req.protocol}://${req.hostname}:${req.port}${req.originalUrl}`, req.headers)}`)
debug(`verify = ${await verifySignature(`${req.protocol}://${req.hostname}:${req.port}${req.originalUrl}`, req.headers)}`)
// const result = await saveActorId(req.body.actor)
switch (req.body.type) {
case 'Create':

View File

@ -1,8 +1,6 @@
import dotenv from 'dotenv'
import { resolve } from 'path'
import crypto from 'crypto'
import { activityPub } from '../ActivityPub'
import gql from 'graphql-tag'
import request from 'request'
const debug = require('debug')('ea:security')
@ -24,71 +22,19 @@ export function generateRsaKeyPair () {
})
}
export function signAndSend (activity, fromName, targetDomain, url) {
// fix for development: replace with http
url = url.indexOf('localhost') > -1 ? url.replace('https', 'http') : url
debug(`passhprase = ${process.env.PRIVATE_KEY_PASSPHRASE}`)
return new Promise(async (resolve, reject) => {
debug('inside signAndSend')
// get the private key
const result = await activityPub.dataSource.client.query({
query: gql`
query {
User(slug: "${fromName}") {
privateKey
}
}
`
})
if (result.error) {
reject(result.error)
} else {
const parsedActivity = JSON.parse(activity)
if (Array.isArray(parsedActivity['@context'])) {
parsedActivity['@context'].push('https://w3id.org/security/v1')
} else {
const context = [parsedActivity['@context']]
context.push('https://w3id.org/security/v1')
parsedActivity['@context'] = context
}
// deduplicate context strings
parsedActivity['@context'] = [...new Set(parsedActivity['@context'])]
const privateKey = result.data.User[0].privateKey
const date = new Date().toUTCString()
debug(`url = ${url}`)
request({
url: url,
headers: {
'Host': targetDomain,
'Date': date,
'Signature': createSignature(privateKey, `http://${activityPub.domain}/activitypub/users/${fromName}#main-key`, url,
{
'Host': targetDomain,
'Date': date,
'Content-Type': 'application/activity+json'
}),
'Content-Type': 'application/activity+json'
},
method: 'POST',
body: JSON.stringify(parsedActivity)
}, (error, response) => {
if (error) {
debug(`Error = ${JSON.stringify(error, null, 2)}`)
reject(error)
} else {
debug('Response Headers:', JSON.stringify(response.headers, null, 2))
debug('Response Body:', JSON.stringify(response.body, null, 2))
resolve()
}
})
}
})
// signing
export function createSignature (privKey, keyId, url, headers = {}, algorithm = 'rsa-sha256') {
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { return throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) }
const signer = crypto.createSign(algorithm)
const signingString = constructSigningString(url, headers)
signer.update(signingString)
const signatureB64 = signer.sign({ key: privKey, passphrase: process.env.PRIVATE_KEY_PASSPHRASE }, 'base64')
const headersString = Object.keys(headers).reduce((result, key) => { return result + ' ' + key.toLowerCase() }, '')
return `keyId="${keyId}",algorithm="${algorithm}",headers="(request-target)${headersString}",signature="${signatureB64}"`
}
export function verify (url, headers) {
// verifying
export function verifySignature (url, headers) {
return new Promise((resolve, reject) => {
const signatureHeader = headers['signature'] ? headers['signature'] : headers['Signature']
if (!signatureHeader) {
@ -130,20 +76,36 @@ export function verify (url, headers) {
})
}
// specify the public key owner object
/* const testPublicKeyOwner = {
"@context": jsig.SECURITY_CONTEXT_URL,
'@id': 'https://example.com/i/alice',
publicKey: [testPublicKey]
} */
// private: signing
function constructSigningString (url, headers) {
const urlObj = new URL(url)
let signingString = `(request-target): post ${urlObj.pathname}${urlObj.search !== '' ? urlObj.search : ''}`
return Object.keys(headers).reduce((result, key) => {
return result + `\n${key.toLowerCase()}: ${headers[key]}`
}, signingString)
}
/* const publicKey = {
'@context': jsig.SECURITY_CONTEXT_URL,
type: 'RsaVerificationKey2018',
id: `https://${ActivityPub.domain}/users/${fromName}#main-key`,
controller: `https://${ActivityPub.domain}/users/${fromName}`,
publicKeyPem
} */
// private: verifying
function httpVerify (pubKey, signature, signingString, algorithm) {
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) }
const verifier = crypto.createVerify(algorithm)
verifier.update(signingString)
return verifier.verify(pubKey, signature, 'base64')
}
// private: verifying
// This function can be used to extract the signature,headers,algorithm etc. out of the Signature Header.
// Just pass what you want as key
function extractKeyValueFromSignatureHeader (signatureHeader, key) {
const keyString = signatureHeader.split(',').filter((el) => {
return !!el.startsWith(key)
})[0]
let firstEqualIndex = keyString.search('=')
// When headers are requested add 17 to the index to remove "(request-target) " from the string
if (key === 'headers') { firstEqualIndex += 17 }
return keyString.substring(firstEqualIndex + 2, keyString.length - 1)
}
// Obtained from invoking crypto.getHashes()
export const SUPPORTED_HASH_ALGORITHMS = [
@ -183,45 +145,3 @@ export const SUPPORTED_HASH_ALGORITHMS = [
'ssl3-md5',
'ssl3-sha1',
'whirlpool']
// signing
function createSignature (privKey, keyId, url, headers = {}, algorithm = 'rsa-sha256') {
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { return throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) }
const signer = crypto.createSign(algorithm)
const signingString = constructSigningString(url, headers)
signer.update(signingString)
const signatureB64 = signer.sign({ key: privKey, passphrase: process.env.PRIVATE_KEY_PASSPHRASE }, 'base64')
const headersString = Object.keys(headers).reduce((result, key) => { return result + ' ' + key.toLowerCase() }, '')
return `keyId="${keyId}",algorithm="${algorithm}",headers="(request-target)${headersString}",signature="${signatureB64}"`
}
// signing
function constructSigningString (url, headers) {
const urlObj = new URL(url)
let signingString = `(request-target): post ${urlObj.pathname}${urlObj.search !== '' ? urlObj.search : ''}`
return Object.keys(headers).reduce((result, key) => {
return result + `\n${key.toLowerCase()}: ${headers[key]}`
}, signingString)
}
// verifying
function httpVerify (pubKey, signature, signingString, algorithm) {
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) }
const verifier = crypto.createVerify(algorithm)
verifier.update(signingString)
return verifier.verify(pubKey, signature, 'base64')
}
// verifying
// This function can be used to extract the signature,headers,algorithm etc. out of the Signature Header.
// Just pass what you want as key
function extractKeyValueFromSignatureHeader (signatureHeader, key) {
const keyString = signatureHeader.split(',').filter((el) => {
return !!el.startsWith(key)
})[0]
let firstEqualIndex = keyString.search('=')
// When headers are requested add 17 to the index to remove "(request-target) " from the string
if (key === 'headers') { firstEqualIndex += 17 }
return keyString.substring(firstEqualIndex + 2, keyString.length - 1)
}

View File

@ -1,7 +1,9 @@
import crypto from 'crypto'
import { signAndSend } from '../security'
import as from 'activitystrea.ms'
import { activityPub } from '../ActivityPub'
import gql from 'graphql-tag'
import { createSignature } from '../security'
import request from 'request'
const debug = require('debug')('ea:utils')
export function extractNameFromId (uri) {
@ -213,3 +215,68 @@ export function throwErrorIfGraphQLErrorOccurred (result) {
throw new Error(`${result.error.message ? result.error.message : result.error.errors[0].message}`)
}
}
export function signAndSend (activity, fromName, targetDomain, url) {
// fix for development: replace with http
url = url.indexOf('localhost') > -1 ? url.replace('https', 'http') : url
debug(`passhprase = ${process.env.PRIVATE_KEY_PASSPHRASE}`)
return new Promise(async (resolve, reject) => {
debug('inside signAndSend')
// get the private key
const result = await activityPub.dataSource.client.query({
query: gql`
query {
User(slug: "${fromName}") {
privateKey
}
}
`
})
if (result.error) {
reject(result.error)
} else {
// add security context
const parsedActivity = JSON.parse(activity)
if (Array.isArray(parsedActivity['@context'])) {
parsedActivity['@context'].push('https://w3id.org/security/v1')
} else {
const context = [parsedActivity['@context']]
context.push('https://w3id.org/security/v1')
parsedActivity['@context'] = context
}
// deduplicate context strings
parsedActivity['@context'] = [...new Set(parsedActivity['@context'])]
const privateKey = result.data.User[0].privateKey
const date = new Date().toUTCString()
debug(`url = ${url}`)
request({
url: url,
headers: {
'Host': targetDomain,
'Date': date,
'Signature': createSignature(privateKey, `http://${activityPub.domain}/activitypub/users/${fromName}#main-key`, url,
{
'Host': targetDomain,
'Date': date,
'Content-Type': 'application/activity+json'
}),
'Content-Type': 'application/activity+json'
},
method: 'POST',
body: JSON.stringify(parsedActivity)
}, (error, response) => {
if (error) {
debug(`Error = ${JSON.stringify(error, null, 2)}`)
reject(error)
} else {
debug('Response Headers:', JSON.stringify(response.headers, null, 2))
debug('Response Body:', JSON.stringify(response.body, null, 2))
resolve()
}
})
}
})
}

View File

@ -10,7 +10,7 @@ export default function generateJwt (user) {
audience: process.env.CLIENT_URI,
subject: user.id.toString()
})
// jwt.verify(token, process.env.JWT_SECRET, (err, data) => {
// jwt.verifySignature(token, process.env.JWT_SECRET, (err, data) => {
// console.log('token verification:', err, data)
// })
return token

View File

@ -137,6 +137,7 @@ type User {
type Post {
id: ID!
activityId: String
author: User @relation(name: "WROTE", direction: "IN")
title: String!
slug: String
@ -167,6 +168,7 @@ type Post {
type Comment {
id: ID!
activityId: String
author: User @relation(name: "WROTE", direction: "IN")
content: String!
contentExcerpt: String
@ -241,3 +243,7 @@ type Tag {
deleted: Boolean
disabled: Boolean
}
type SharedInboxEndpoint {
id: ID!
uri: String
}

View File

@ -1576,9 +1576,9 @@ bitcore-lib@^0.13.7:
inherits "=2.0.1"
lodash "=3.10.1"
"bitcore-message@github:comakery/bitcore-message#dist":
"bitcore-message@github:CoMakery/bitcore-message#dist":
version "1.0.2"
resolved "https://codeload.github.com/comakery/bitcore-message/tar.gz/8799cc327029c3d34fc725f05b2cf981363f6ebf"
resolved "https://codeload.github.com/CoMakery/bitcore-message/tar.gz/8799cc327029c3d34fc725f05b2cf981363f6ebf"
dependencies:
bitcore-lib "^0.13.7"