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 } ` } ) ) ,
'Content-Type' : 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ,
Accept : 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
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 } )
} ,
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
2023-12-20 21:57:30 +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 ) {
2021-04-28 12:44:26 +02:00
log . info ( ` 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
} ,
2023-11-21 22:12:21 +01:00
async getNodeInfo ( instance _url ) {
const versions = await axios . get ( ` ${ instance _url } /.well-known/nodeinfo ` , { headers : { Accept : 'application/json' } } ) . then ( res => res . data )
console . error ( versions )
if ( versions . links ) {
const choosen = versions . links . find ( l => l . rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1' || 'http://nodeinfo.diaspora.software/ns/schema/2.0' )
console . error ( choosen )
if ( ! choosen ) {
throw new Error ( 'Not found!' )
}
const data = await axios . get ( choosen . href ) . then ( res => res . data )
console . error ( 'INSTANCE' , data )
return data
}
} ,
2019-10-30 15:01:15 +01:00
async getInstance ( actor _url , force = false ) {
2023-12-20 21:57:30 +01:00
log . debug ( ` [FEDI] getInstance ${ 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 } `
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 }
}
2023-11-21 22:12:21 +01:00
try {
instance = await Helpers . getNodeInfo ( instance _url )
return Instance . create ( { name : instance ? . metadata ? . nodeLabel || instance ? . metadata ? . nodeName || domain , domain , data : instance , blocked : false } )
} catch ( e ) {
log . error ( 'NodeInfo not supported' , e )
return Instance . create ( { name : domain , domain , blocked : false } )
}
2019-10-30 15:01:15 +01:00
} ,
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 ) {
2021-04-26 11:25:35 +02:00
// TODO: why do I need instance?
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 ) {
2021-04-26 11:25:35 +02:00
log . warn ( ` Verify Signature: Instance not found ${ req . body . actor } ` )
2021-03-05 14:17:10 +01:00
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 ) {
2021-12-02 11:39:27 +01:00
log . info ( ` Actor ${ req . body . actor } not found ` )
if ( req . body . type === 'Delete' ) {
return res . sendStatus ( 201 )
}
2021-03-05 14:17:10 +01:00
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
2022-02-26 21:27:40 +01:00
res . locals . fedi _user = user
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 )
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 ) {
2021-04-28 12:44:26 +02:00
log . info ( ` Actor ${ req . body . actor } not found ` )
2021-03-05 14:17:10 +01:00
return res . status ( 401 ) . send ( 'Actor not found' )
}
2021-06-25 11:43:50 +02:00
if ( httpSignature . verifySignature ( parsed , user . object . publicKey . publicKeyPem ) ) {
log . debug ( ` Valid signature from ${ req . body . actor } ` )
return next ( )
}
2019-10-30 15:01:15 +01:00
2019-08-02 17:29:55 +02:00
// still not valid
2021-04-28 12:44:26 +02:00
log . info ( ` 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