mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Some fixes + refactoring + logic to receive sharedInboxEndpoints and to add new one
This commit is contained in:
parent
f74a45379f
commit
577157c505
@ -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": {
|
||||
|
||||
@ -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)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user