2020-02-05 00:48:55 +01:00
const axios = require ( 'axios' )
2019-08-10 15:00:08 +02:00
// const request = require('request')
2019-07-31 02:01:21 +02:00
const crypto = require ( 'crypto' )
2019-07-30 18:57:45 +02:00
const config = require ( 'config' )
2019-08-02 17:29:55 +02:00
const httpSignature = require ( 'http-signature' )
2020-06-27 02:10:10 +02:00
const APUser = require ( '../api/models/ap_user' )
const Instance = require ( '../api/models/instance' )
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 } ` )
log . debug ( req )
return res . status ( 404 ) . send ( 'Not Found' )
}
2019-09-13 11:08:18 +02:00
next ( )
} ,
2019-12-04 00:50:15 +01:00
async signAndSend ( message , inbox ) {
2021-03-18 16:30:56 +01:00
log . debug ( 'sign and send' , inbox )
2019-07-30 18:32:26 +02:00
// get the URI of the actor object and append 'inbox' to it
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 ( )
2021-03-18 17:15:50 +01:00
// digest header added for Mastodon 3.2.1 compatibility
const 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: ${ digest } `
2019-07-30 18:32:26 +02:00
signer . update ( stringToSign )
signer . end ( )
const signature = signer . sign ( privkey )
const signature _b64 = signature . toString ( 'base64' )
2021-03-18 17:17:49 +01:00
const header = ` keyId=" ${ config . baseurl } /federation/u/ ${ settingsController . settings . instance _name } ",headers="(request-target) host date digest",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 ,
2021-03-18 17:15:50 +01:00
Digest : ` SHA256= ${ digest } ` ,
2020-01-27 00:47:03 +01:00
'Content-Type' : 'application/activity+json; charset=utf-8' ,
Accept : 'application/activity+json, application/json; chartset=utf-8'
} ,
2020-02-05 00:48:55 +01:00
method : 'post' ,
2021-03-18 17:15:50 +01:00
data : message
2020-01-27 00:47:03 +01:00
} )
2021-03-05 14:17:10 +01:00
log . debug ( ` sign ${ ret . status } => ${ ret . data } ` )
2020-01-27 00:47:03 +01:00
} catch ( e ) {
2021-03-18 17:15:50 +01:00
log . error ( ` Response: ${ e . response . status } ${ e . response . data } ` )
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-03-05 14:17:10 +01:00
log . debug ( '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 } } )
2019-10-30 15:01:15 +01:00
const recipients = { }
2019-12-04 00:50:15 +01:00
followers . forEach ( follower => {
2019-09-18 12:55:33 +02:00
const sharedInbox = follower . object . endpoints . sharedInbox
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 } ` ,
2020-11-06 11:05:05 +01:00
object : event . toAPNote ( settingsController . settings . instance _name ,
2020-02-20 18:37:10 +01:00
settingsController . settings . instance _locale ,
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
{
Hashtag : 'as:Hashtag'
} ]
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
} ,
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 ) {
if ( ! fedi _user . instances ) {
fedi _user . setInstance ( instance )
}
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
2020-02-05 00:48:55 +01:00
fedi _user = await axios . get ( URL , { headers : { Accept : 'application/jrd+json, application/json' } } )
2019-08-09 00:19:57 +02:00
. then ( res => {
2020-02-05 00:48:55 +01:00
if ( res . status !== 200 ) {
2021-03-05 14:17:10 +01:00
log . warn ( ` Actor ${ URL } => ${ res . statusText } ` )
2019-08-09 00:19:57 +02:00
return false
}
2020-02-05 00:48:55 +01:00
return res . data
2019-08-08 17:48:12 +02:00
} )
2020-06-01 19:14:46 +02:00
. catch ( e => {
2021-03-05 14:17:10 +01:00
log . error ( ` ${ URL } : ${ 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 ) {
2021-03-05 14:17:10 +01:00
log . debug ( ` Create a new AP User => ${ URL } ` )
2019-12-04 00:50:15 +01:00
fedi _user = await APUser . create ( { ap _id : URL , object : fedi _user } )
2019-09-12 14:59:51 +02:00
}
return fedi _user
2019-08-02 13:43:28 +02:00
} ,
2019-10-30 15:01:15 +01:00
async getInstance ( actor _url , force = false ) {
actor _url = new url . URL ( actor _url )
const domain = actor _url . host
const instance _url = ` ${ actor _url . protocol } // ${ actor _url . host } `
2021-03-05 14:17:10 +01:00
log . debug ( ` getInstance ${ domain } ` )
2019-10-30 15:01:15 +01:00
let instance
if ( ! force ) {
2019-12-04 00:50:15 +01:00
instance = await Instance . findByPk ( domain )
2019-10-30 15:01:15 +01:00
if ( instance ) { return instance }
}
2020-06-03 22:52:10 +02:00
// TODO: is this a standard? don't think so
2020-02-05 00:58:43 +01:00
instance = await axios . get ( ` ${ instance _url } /api/v1/instance ` , { headers : { Accept : 'application/json' } } )
. then ( res => res . data )
2019-10-30 15:01:15 +01:00
. then ( instance => {
const data = {
stats : instance . stats ,
thumbnail : instance . thumbnail
}
2019-12-04 00:50:15 +01:00
return Instance . create ( { name : instance . title , domain , data , blocked : false } )
2019-10-30 15:01:15 +01:00
} )
. catch ( e => {
2021-03-05 14:17:10 +01:00
log . error ( e )
2019-10-30 15:01:15 +01:00
return false
} )
return instance
} ,
2019-08-02 17:29:55 +02:00
// ref: https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/
2019-09-11 19:12:24 +02:00
async verifySignature ( req , res , next ) {
2019-10-30 15:01:15 +01:00
const instance = await Helpers . getInstance ( req . body . actor )
2021-03-05 14:17:10 +01:00
if ( ! instance ) {
log . warn ( ` [AP] Verify Signature: Instance not found ${ req . body . actor } ` )
return res . status ( 401 ) . send ( 'Instance not found' )
}
2019-10-30 15:01:15 +01:00
if ( instance . blocked ) {
2021-03-05 14:17:10 +01:00
log . warn ( ` Instance ${ instance . domain } blocked ` )
2019-10-30 15:01:15 +01:00
return res . status ( 401 ) . send ( 'Instance blocked' )
}
let user = await Helpers . getActor ( req . body . actor , instance )
2021-03-05 14:17:10 +01:00
if ( ! user ) {
log . info ( ` Actor ${ req . body . actor } not found ` )
return res . status ( 401 ) . send ( 'Actor not found' )
}
2019-11-13 10:56:01 +01:00
if ( user . blocked ) {
2021-03-05 14:17:10 +01:00
log . info ( ` User ${ user . ap _id } blocked ` )
2019-11-13 10:56:01 +01:00
return res . status ( 401 ) . send ( 'User blocked' )
}
2019-10-30 15:01:15 +01:00
2019-08-02 17:29:55 +02:00
// little hack -> https://github.com/joyent/node-http-signature/pull/83
2020-06-03 22:52:10 +02:00
// req.headers.authorization = 'Signature ' + req.headers.signature
2019-10-30 15:01:15 +01:00
2019-09-12 14:59:51 +02:00
req . fedi _user = user
2019-08-08 17:48:12 +02:00
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 )
2019-11-13 10:56:01 +01:00
if ( httpSignature . verifySignature ( parsed , user . 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
2019-10-30 15:01:15 +01:00
user = await Helpers . getActor ( req . body . actor , instance , true )
2021-03-05 14:17:10 +01:00
if ( ! user ) {
log . debug ( ` Actor ${ req . body . actor } not found ` )
return res . status ( 401 ) . send ( 'Actor not found' )
}
2019-11-13 10:56:01 +01:00
if ( httpSignature . verifySignature ( parsed , user . object . publicKey . publicKeyPem ) ) { return next ( ) }
2019-10-30 15:01:15 +01:00
2019-08-02 17:29:55 +02:00
// still not valid
2021-03-05 14:17:10 +01:00
log . debug ( ` Invalid signature from user ${ req . body . actor } ` )
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