2020-02-05 00:48:55 +01:00
const axios = require ( 'axios' )
2019-07-31 02:01:21 +02:00
const crypto = require ( 'crypto' )
2021-09-27 10:42:17 +02:00
const config = require ( '../config' )
2023-12-20 21:57:30 +01:00
const httpSignature = require ( '@peertube/http-signature' )
2022-12-23 01:08:14 +01:00
const { APUser , Instance } = require ( '../api/models/models' )
2019-09-11 12:00:13 +02:00
const url = require ( 'url' )
2019-09-25 14:38:16 +02:00
const settingsController = require ( '../api/controller/settings' )
2021-03-05 14:17:10 +01:00
const log = require ( '../log' )
2019-07-30 18:32:26 +02:00
const Helpers = {
2019-09-13 11:08:18 +02:00
// ignore unimplemented ping url from fediverse
2019-10-28 17:33:20 +01:00
spamFilter ( req , res , next ) {
2019-09-13 11:08:18 +02:00
const urlToIgnore = [
'/api/v1/instance' ,
'/api/meta' ,
2019-09-18 12:55:33 +02:00
'/api/statusnet/version.json' ,
'/api/gnusocial/version.json' ,
2019-09-13 11:08:18 +02:00
'/api/statusnet/config.json' ,
2020-01-30 15:33:12 +01:00
'/status.php' ,
'/siteinfo.json' ,
'/friendika/json' ,
'/friendica/json' ,
2019-10-28 17:33:20 +01:00
'/poco'
2019-09-13 11:08:18 +02:00
]
2021-03-05 14:17:10 +01:00
if ( urlToIgnore . includes ( req . path ) ) {
log . debug ( ` Ignore noisy fediverse ${ req . path } ` )
return res . status ( 404 ) . send ( 'Not Found' )
}
2019-09-13 11:08:18 +02:00
next ( )
} ,
2023-12-20 21:57:30 +01:00
async signAndSend ( message , inbox , method = 'post' ) {
log . debug ( '[FEDI] Sign and send %s to %s' , message , inbox )
2019-10-30 15:01:15 +01:00
const inboxUrl = new url . URL ( inbox )
2019-12-04 00:50:15 +01:00
const privkey = settingsController . secretSettings . privateKey
2019-07-30 18:32:26 +02:00
const signer = crypto . createSign ( 'sha256' )
2019-07-31 01:43:08 +02:00
const d = new Date ( )
2023-12-20 21:57:30 +01:00
let header
let digest
if ( method === 'post' ) {
digest = crypto . createHash ( 'sha256' )
. update ( message )
. digest ( 'base64' )
const stringToSign = ` (request-target): post ${ inboxUrl . pathname } \n host: ${ inboxUrl . hostname } \n date: ${ d . toUTCString ( ) } \n digest: SHA-256= ${ digest } `
signer . update ( stringToSign )
signer . end ( )
const signature = signer . sign ( privkey )
const signature _b64 = signature . toString ( 'base64' )
header = ` keyId=" ${ config . baseurl } /federation/u/ ${ settingsController . settings . instance _name } #main-key",algorithm="rsa-sha256",headers="(request-target) host date digest",signature=" ${ signature _b64 } " `
} else {
const stringToSign = ` (request-target): get ${ inboxUrl . pathname } \n host: ${ inboxUrl . hostname } \n date: ${ d . toUTCString ( ) } `
signer . update ( stringToSign )
signer . end ( )
const signature = signer . sign ( privkey )
const signature _b64 = signature . toString ( 'base64' )
header = ` keyId=" ${ config . baseurl } /federation/u/ ${ settingsController . settings . instance _name } #main-key",algorithm="rsa-sha256",headers="(request-target) host date",signature=" ${ signature _b64 } " `
}
2020-01-27 00:47:03 +01:00
try {
2020-02-05 00:48:55 +01:00
const ret = await axios ( inbox , {
2020-01-27 00:47:03 +01:00
headers : {
Host : inboxUrl . hostname ,
Date : d . toUTCString ( ) ,
Signature : header ,
2023-12-20 21:57:30 +01:00
... ( method === 'post' && ( { Digest : ` SHA-256= ${ digest } ` } ) ) ,
2023-12-22 14:53:53 +01:00
'Content-Type' : 'application/activity+json' ,
Accept : 'application/activity+json'
2020-01-27 00:47:03 +01:00
} ,
2023-12-20 21:57:30 +01:00
method ,
... ( method === 'post' && ( { data : message } ) )
2020-01-27 00:47:03 +01:00
} )
2023-12-20 21:57:30 +01:00
log . debug ( ` [FEDI] signed ${ ret . status } => %s ` , ret . data )
return ret . data
2020-01-27 00:47:03 +01:00
} catch ( e ) {
2023-12-20 21:57:30 +01:00
log . error ( "[FEDI] Error in sign and send [%s]: %s" , inbox , e ? . response ? . data ? . error ? ? e ? . response ? . statusMessage + ' ' + String ( e ) )
2020-01-27 00:47:03 +01:00
}
2019-07-31 01:43:08 +02:00
} ,
2019-09-11 12:00:13 +02:00
2019-12-04 00:50:15 +01:00
async sendEvent ( event , type = 'Create' ) {
2019-09-25 14:38:16 +02:00
if ( ! settingsController . settings . enable _federation ) {
2021-04-28 12:44:26 +02:00
log . info ( 'event not send, federation disabled' )
2019-09-13 10:17:44 +02:00
return
}
2019-12-06 00:49:44 +01:00
const followers = await APUser . findAll ( { where : { follower : true } } )
2023-12-20 21:57:30 +01:00
log . debug ( "[FEDI] Sending to [%s]" , followers . map ( f => f . ap _id ) . join ( ', ' ) )
2019-10-30 15:01:15 +01:00
const recipients = { }
2019-12-04 00:50:15 +01:00
followers . forEach ( follower => {
2023-12-20 21:57:30 +01:00
const sharedInbox = follower ? . object ? . endpoints ? . sharedInbox ? ? follower ? . object ? . inbox
2019-10-28 17:33:20 +01:00
if ( ! recipients [ sharedInbox ] ) { recipients [ sharedInbox ] = [ ] }
2019-09-18 12:55:33 +02:00
recipients [ sharedInbox ] . push ( follower . ap _id )
2019-09-13 10:17:44 +02:00
} )
2019-10-28 17:33:20 +01:00
for ( const sharedInbox in recipients ) {
2021-03-05 14:17:10 +01:00
log . debug ( ` Notify ${ sharedInbox } with event ${ event . title } cc => ${ recipients [ sharedInbox ] . length } ` )
2019-09-11 13:12:05 +02:00
const body = {
2019-09-11 21:20:44 +02:00
id : ` ${ config . baseurl } /federation/m/ ${ event . id } #create ` ,
2019-10-02 21:04:03 +02:00
type ,
2020-11-06 11:05:05 +01:00
to : [ 'https://www.w3.org/ns/activitystreams#Public' ] ,
cc : [ ... recipients [ sharedInbox ] , ` ${ config . baseurl } /federation/u/ ${ settingsController . settings . instance _name } /followers ` ] ,
2019-12-04 00:50:15 +01:00
actor : ` ${ config . baseurl } /federation/u/ ${ settingsController . settings . instance _name } ` ,
2023-03-28 19:02:08 +02:00
object : event . toAP ( settingsController . settings , recipients [ sharedInbox ] )
2019-09-11 13:12:05 +02:00
}
2019-09-26 22:46:04 +02:00
body [ '@context' ] = [
'https://www.w3.org/ns/activitystreams' ,
'https://w3id.org/security/v1' ,
2020-11-06 11:05:05 +01:00
{
2021-07-19 12:29:35 +02:00
Hashtag : 'as:Hashtag' ,
focalPoint : { '@container' : '@list' , '@id' : 'toot:focalPoint' }
2020-11-06 11:05:05 +01:00
} ]
2021-03-18 17:15:50 +01:00
await Helpers . signAndSend ( JSON . stringify ( body ) , sharedInbox )
2019-09-11 12:00:13 +02:00
}
2019-08-02 13:43:28 +02:00
} ,
2023-11-21 22:12:21 +01:00
async followActor ( actor ) {
log . debug ( ` Following actor ${ actor . ap _id } ` )
const body = {
'@context' : 'https://www.w3.org/ns/activitystreams' ,
id : ` ${ config . baseurl } /federation/m/ ${ actor . ap _id } #follow ` ,
type : 'Follow' ,
actor : ` ${ config . baseurl } /federation/u/ ${ settingsController . settings . instance _name } ` ,
object : actor . ap _id
}
await Helpers . signAndSend ( JSON . stringify ( body ) , actor . object . endpoints ? . sharedInbox || actor . object . inbox )
await actor . update ( { following : 1 } )
} ,
async unfollowActor ( actor ) {
log . debug ( ` Unfollowing actor ${ actor . ap _id } ` )
const body = {
'@context' : 'https://www.w3.org/ns/activitystreams' ,
id : ` ${ config . baseurl } /federation/m/ ${ actor . ap _id } #follow ` ,
type : 'Unfollow' ,
actor : ` ${ config . baseurl } /federation/u/ ${ settingsController . settings . instance _name } ` ,
object : actor . ap _id
}
await Helpers . signAndSend ( JSON . stringify ( body ) , actor . object . endpoints ? . sharedInbox || actor . object . inbox )
return actor . update ( { following : 0 } )
} ,
2023-12-26 13:04:20 +01:00
// get Actor from URL using GET HTTP Signature
2019-10-30 15:01:15 +01:00
async getActor ( URL , instance , force = false ) {
2019-09-12 14:59:51 +02:00
let fedi _user
2019-10-30 15:01:15 +01:00
2019-08-09 01:58:11 +02:00
// try with cache first
2019-10-30 15:01:15 +01:00
if ( ! force ) {
2019-12-04 00:50:15 +01:00
fedi _user = await APUser . findByPk ( URL , { include : Instance } )
2019-10-30 15:01:15 +01:00
if ( fedi _user ) {
2019-11-13 10:56:01 +01:00
return fedi _user
2019-10-30 15:01:15 +01:00
}
}
2019-09-12 14:59:51 +02:00
2023-12-26 13:04:20 +01:00
fedi _user = await Helpers . signAndSend ( '' , URL , 'get' )
2020-06-01 19:14:46 +02:00
. catch ( e => {
2023-12-20 21:57:30 +01:00
log . error ( ` [FEDI] getActor ${ URL } : %s ` , e ? . response ? . data ? . error ? ? String ( e ) )
2020-06-01 19:14:46 +02:00
return false
} )
2019-10-30 15:01:15 +01:00
2019-09-12 14:59:51 +02:00
if ( fedi _user ) {
2023-12-26 13:04:20 +01:00
log . info ( '[FEDI] Create a new AP User "%s" and associate it to instance "%s"' , URL , instance . domain )
fedi _user = await APUser . create ( { ap _id : URL , object : fedi _user , instanceDomain : instance . domain , blocked : false } )
2019-09-12 14:59:51 +02:00
}
return fedi _user
2019-08-02 13:43:28 +02:00
} ,
2023-11-21 22:12:21 +01:00
async getNodeInfo ( instance _url ) {
2023-12-22 09:28:20 +01:00
let nodeInfo = await axios . get ( ` ${ instance _url } /.well-known/nodeinfo ` , { headers : { Accept : 'application/json' } } ) . then ( res => res . data )
2023-12-22 20:58:38 +01:00
if ( nodeInfo ? . links ) {
2023-12-22 09:28:20 +01:00
const supportedVersion = nodeInfo . links . find ( l => l . rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1' || 'http://nodeinfo.diaspora.software/ns/schema/2.0' )
if ( ! supportedVersion ) {
return false
}
const applicationActor = nodeInfo . links . find ( l => l . rel === 'https://www.w3.org/ns/activitystreams#Application' )
nodeInfo = await axios . get ( supportedVersion . href ) . then ( res => res . data )
log . debug ( '[FEDI] getNodeInfo "%s", applicationActor: %s, nodeInfo: %s' , instance _url , applicationActor ? . href , nodeInfo )
return { applicationActor : applicationActor ? . href , nodeInfo }
2023-11-21 22:12:21 +01:00
}
2023-12-22 09:28:20 +01:00
throw new Error ( nodeInfo )
} ,
2023-11-21 22:12:21 +01:00
2019-10-30 15:01:15 +01:00
async getInstance ( actor _url , force = false ) {
2023-12-26 13:04:20 +01:00
log . debug ( ` [FEDI] getInstance for ${ actor _url } ` )
2019-10-30 15:01:15 +01:00
actor _url = new url . URL ( actor _url )
const domain = actor _url . host
const instance _url = ` ${ actor _url . protocol } // ${ actor _url . host } `
let instance
if ( ! force ) {
2019-12-04 00:50:15 +01:00
instance = await Instance . findByPk ( domain )
2023-12-26 21:04:48 +01:00
if ( instance ) {
log . debug ( '[FEDI] Use cached instance: %s' , instance . name )
return instance
}
2019-10-30 15:01:15 +01:00
}
2023-11-21 22:12:21 +01:00
try {
2023-12-26 13:04:20 +01:00
const { applicationActor , nodeInfo } = await Helpers . getNodeInfo ( instance _url )
const instance = Instance . create ( {
2023-12-28 00:47:31 +01:00
name : nodeInfo ? . metadata ? . nodeName ? ? nodeInfo ? . metadata ? . nodeDescription ? ? domain ,
2023-12-26 13:04:20 +01:00
domain ,
data : nodeInfo ,
blocked : false ,
applicationActor
} )
2023-12-26 21:04:48 +01:00
log . debug ( '[FEDI] Create a new instance from %s: %s %s' , instance _url , instance . name , nodeInfo )
2023-12-26 13:04:20 +01:00
return instance
2023-11-21 22:12:21 +01:00
} catch ( e ) {
2023-12-22 09:28:20 +01:00
log . error ( '[FEDI] Wrong nodeInfo returned for "%s": %s' , instance _url , e ? . response ? . data ? ? String ( e ) )
2023-12-26 21:04:48 +01:00
return false
2023-11-21 22:12:21 +01:00
}
2019-10-30 15:01:15 +01:00
} ,
2023-12-26 13:04:20 +01:00
/ * *
* HTTP Signature middleware
* https : //www.w3.org/wiki/SocialCG/ActivityPub/Authentication_Authorization#Signing_requests_using_HTTP_Signatures
* Each POST to / inbox coming from fediverse has to be verified .
* Signature checking needs Actor ' s public key
* /
2019-09-11 19:12:24 +02:00
async verifySignature ( req , res , next ) {
2023-12-26 13:04:20 +01:00
const actor _url = req ? . body ? . actor
// do we have an actor?
if ( ! actor _url ) {
log . warn ( ` [FEDI] Verify Signature: No actor url or empty body ` )
return res . status ( 401 ) . send ( 'Actor not found' )
}
// Get instance's nodeinfo
// getting this from db if it is not the first time we interact with it
const instance = await Helpers . getInstance ( actor _url )
2021-03-05 14:17:10 +01:00
if ( ! instance ) {
2023-12-26 13:04:20 +01:00
log . warn ( ` [FEDI] Verify Signature: Instance not found ${ actor _url } ` )
2021-03-05 14:17:10 +01:00
return res . status ( 401 ) . send ( 'Instance not found' )
}
2023-12-26 13:04:20 +01:00
// Is this instance blocked?
2019-10-30 15:01:15 +01:00
if ( instance . blocked ) {
2023-12-26 13:04:20 +01:00
log . warn ( ` [FEDI] Instance ${ instance . domain } blocked ` )
2019-10-30 15:01:15 +01:00
return res . status ( 401 ) . send ( 'Instance blocked' )
}
2023-12-26 13:04:20 +01:00
// get actor
let ap _actor = await Helpers . getActor ( actor _url , instance )
if ( ! ap _actor ) {
log . info ( ` [FEDI] Actor ${ actor _url } not found ` )
if ( req ? . body ? . type === 'Delete' ) {
2021-12-02 11:39:27 +01:00
return res . sendStatus ( 201 )
}
2021-03-05 14:17:10 +01:00
return res . status ( 401 ) . send ( 'Actor not found' )
}
2023-12-26 13:04:20 +01:00
if ( ap _actor . blocked ) {
log . info ( ` [FEDI] Actor ${ ap _actor . ap _id } blocked ` )
return res . status ( 401 ) . send ( 'Actor blocked' )
}
if ( ! ap _actor ? . object ? . publicKey ? . publicKeyPem ) {
log . info ( ` [FEDI] Actor %s has no publicKey at %s ` , ap _actor . ap _id , actor _url )
return res . status ( 401 ) . send ( 'No public key' )
2019-11-13 10:56:01 +01:00
}
2019-10-30 15:01:15 +01:00
2023-12-26 13:04:20 +01:00
res . locals . fedi _user = ap _actor
2019-08-08 17:48:12 +02:00
2021-04-26 11:25:35 +02:00
// TODO: check Digest // cannot do this with json bodyparser
// const digest = crypto.createHash('sha256')
// .update(req.body)
// .digest('base64')
// if (`SHA-256=${digest}` !== req.headers.signature) {
2021-07-04 00:46:08 +02:00
// log.warn(`Signature mismatch ${req.headers.signature} - ${digest}`)
2021-04-26 11:25:35 +02:00
// return res.status(401).send('Signature mismatch')
// }
2019-09-11 19:12:24 +02:00
// another little hack :/
2019-08-08 17:48:12 +02:00
// https://github.com/joyent/node-http-signature/issues/87
req . url = '/federation' + req . url
2019-08-02 17:29:55 +02:00
const parsed = httpSignature . parseRequest ( req )
2023-12-26 13:04:20 +01:00
if ( httpSignature . verifySignature ( parsed , ap _actor . object . publicKey . publicKeyPem ) ) { return next ( ) }
2019-10-30 15:01:15 +01:00
2019-08-02 17:29:55 +02:00
// signature not valid, try without cache
2023-12-26 13:04:20 +01:00
ap _actor = await Helpers . getActor ( actor _url , instance , true )
if ( ! ap _actor ) {
log . info ( ` [FEDI] Actor ${ actor _url } not found ` )
2021-03-05 14:17:10 +01:00
return res . status ( 401 ) . send ( 'Actor not found' )
}
2023-12-26 13:04:20 +01:00
if ( ! ap _actor ? . object ? . publicKey ? . publicKeyPem ) {
log . info ( ` [FEDI] Actor %s has no publicKey at %s ` , ap _actor . ap _id , actor _url )
return res . status ( 401 ) . send ( 'No public key' )
}
if ( httpSignature . verifySignature ( parsed , ap _actor . object . publicKey . publicKeyPem ) ) {
log . debug ( ` [FEDI] Valid signature from ${ actor _url } ` )
2021-06-25 11:43:50 +02:00
return next ( )
}
2019-10-30 15:01:15 +01:00
2019-08-02 17:29:55 +02:00
// still not valid
2023-12-26 13:04:20 +01:00
log . info ( ` [FEDI] Invalid signature from Actor ${ actor _url } ` )
2019-08-08 17:48:12 +02:00
res . send ( 'Request signature could not be verified' , 401 )
2019-07-30 18:32:26 +02:00
}
}
2019-07-30 18:57:45 +02:00
module . exports = Helpers