2020-10-26 16:37:31 +00:00
package main
import (
"bytes"
"context"
"crypto/rsa"
"crypto/x509"
2020-11-09 15:40:12 +00:00
"database/sql"
2020-10-26 16:37:31 +00:00
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"regexp"
"strings"
"sync"
"time"
"github.com/go-chi/chi"
"github.com/go-fed/httpsig"
)
var (
apPrivateKey * rsa . PrivateKey
apPostSigner httpsig . Signer
apPostSignMutex * sync . Mutex = & sync . Mutex { }
)
func initActivityPub ( ) error {
2020-11-17 21:10:13 +00:00
if ! appConfig . ActivityPub . Enabled {
return nil
}
// Add hooks
postHooks [ postPostHook ] = append ( postHooks [ postPostHook ] , func ( p * post ) {
if p . isPublishedSectionPost ( ) {
p . apPost ( )
}
} )
postHooks [ postUpdateHook ] = append ( postHooks [ postUpdateHook ] , func ( p * post ) {
if p . isPublishedSectionPost ( ) {
p . apUpdate ( )
}
} )
postHooks [ postDeleteHook ] = append ( postHooks [ postDeleteHook ] , func ( p * post ) {
p . apDelete ( )
} )
// Read key and prepare signing
2020-10-26 16:37:31 +00:00
pkfile , err := ioutil . ReadFile ( appConfig . ActivityPub . KeyPath )
if err != nil {
return err
}
privateKeyDecoded , _ := pem . Decode ( pkfile )
if privateKeyDecoded == nil {
return errors . New ( "failed to decode private key" )
}
apPrivateKey , err = x509 . ParsePKCS1PrivateKey ( privateKeyDecoded . Bytes )
if err != nil {
return err
}
prefs := [ ] httpsig . Algorithm { httpsig . RSA_SHA256 }
digestAlgorithm := httpsig . DigestSha256
headersToSign := [ ] string { httpsig . RequestTarget , "date" , "host" , "digest" }
apPostSigner , _ , err = httpsig . NewSigner ( prefs , digestAlgorithm , headersToSign , httpsig . Signature , 0 )
if err != nil {
return err
}
return nil
}
func apHandleWebfinger ( w http . ResponseWriter , r * http . Request ) {
re , err := regexp . Compile ( ` ^acct:(.*)@ ` + regexp . QuoteMeta ( appConfig . Server . Domain ) + ` $ ` )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
name := re . ReplaceAllString ( r . URL . Query ( ) . Get ( "resource" ) , "$1" )
blog := appConfig . Blogs [ name ]
if blog == nil {
http . Error ( w , "Not found" , http . StatusNotFound )
return
}
w . Header ( ) . Set ( contentType , "application/jrd+json" + charsetUtf8Suffix )
_ = json . NewEncoder ( w ) . Encode ( map [ string ] interface { } {
"subject" : "acct:" + name + "@" + appConfig . Server . Domain ,
"links" : [ ] map [ string ] string {
{
"rel" : "self" ,
"type" : contentTypeAS ,
"href" : blog . apIri ( ) ,
} ,
} ,
} )
}
func apHandleInbox ( w http . ResponseWriter , r * http . Request ) {
blogName := chi . URLParam ( r , "blog" )
blog := appConfig . Blogs [ blogName ]
if blog == nil {
http . Error ( w , "Inbox not found" , http . StatusNotFound )
return
}
2020-11-06 17:45:31 +00:00
blogIri := blog . apIri ( )
2020-11-13 14:19:09 +00:00
// Verify request
2020-11-13 15:29:22 +00:00
requestActor , requestKey , requestActorStatus , err := apVerifySignature ( r )
2020-11-13 14:19:09 +00:00
if err != nil {
2020-11-13 20:29:09 +00:00
// Send 401 because signature could not be verified
http . Error ( w , err . Error ( ) , http . StatusUnauthorized )
2020-11-13 14:19:09 +00:00
return
}
2020-11-13 15:29:22 +00:00
if requestActorStatus != 0 {
if requestActorStatus == http . StatusGone || requestActorStatus == http . StatusNotFound {
u , err := url . Parse ( requestKey )
if err == nil {
u . Fragment = ""
u . RawFragment = ""
apRemoveFollower ( blogName , u . String ( ) )
2020-11-13 20:29:09 +00:00
w . WriteHeader ( http . StatusOK )
2020-11-13 15:29:22 +00:00
return
}
}
http . Error ( w , "Error when trying to get request actor" , http . StatusBadRequest )
return
}
2020-11-13 14:19:09 +00:00
// Parse activity
2020-10-26 16:37:31 +00:00
activity := make ( map [ string ] interface { } )
2020-11-13 14:19:09 +00:00
err = json . NewDecoder ( r . Body ) . Decode ( & activity )
2020-10-26 16:37:31 +00:00
_ = r . Body . Close ( )
if err != nil {
http . Error ( w , "Failed to decode body" , http . StatusBadRequest )
return
}
2020-11-13 14:19:09 +00:00
// Get and check activity actor
activityActor , ok := activity [ "actor" ] . ( string )
if ! ok {
http . Error ( w , "actor in activity is no string" , http . StatusBadRequest )
return
}
if activityActor != requestActor . ID {
http . Error ( w , "Request actor isn't activity actor" , http . StatusForbidden )
return
}
// Do
2020-10-26 16:37:31 +00:00
switch activity [ "type" ] {
case "Follow" :
apAccept ( blogName , blog , activity )
case "Undo" :
{
if object , ok := activity [ "object" ] . ( map [ string ] interface { } ) ; ok {
if objectType , ok := object [ "type" ] . ( string ) ; ok && objectType == "Follow" {
2020-11-13 14:19:09 +00:00
if iri , ok := object [ "actor" ] . ( string ) ; ok && iri == activityActor {
_ = apRemoveFollower ( blogName , activityActor )
2020-10-26 16:37:31 +00:00
}
}
}
}
case "Create" :
{
if object , ok := activity [ "object" ] . ( map [ string ] interface { } ) ; ok {
inReplyTo , hasReplyToString := object [ "inReplyTo" ] . ( string )
2020-11-06 17:45:31 +00:00
id , hasID := object [ "id" ] . ( string )
if hasReplyToString && hasID && len ( inReplyTo ) > 0 && len ( id ) > 0 && strings . Contains ( inReplyTo , blogIri ) {
// It's an ActivityPub reply; save reply as webmention
createWebmention ( id , inReplyTo )
} else if content , hasContent := object [ "content" ] . ( string ) ; hasContent && hasID && len ( id ) > 0 {
// May be a mention; find links to blog and save them as webmentions
2020-11-16 13:18:14 +00:00
if links , err := allLinksFromHTML ( strings . NewReader ( content ) , id ) ; err == nil {
for _ , link := range links {
2020-11-06 17:45:31 +00:00
if strings . Contains ( link , blogIri ) {
createWebmention ( id , link )
}
}
}
2020-10-26 16:37:31 +00:00
}
}
}
case "Delete" :
2020-11-06 17:45:31 +00:00
case "Block" :
2020-10-26 16:37:31 +00:00
{
2020-11-13 14:19:09 +00:00
if object , ok := activity [ "object" ] . ( string ) ; ok && len ( object ) > 0 && object == activityActor {
_ = apRemoveFollower ( blogName , activityActor )
2020-10-26 16:37:31 +00:00
}
}
case "Like" :
2020-11-06 17:45:31 +00:00
{
likeObject , likeObjectOk := activity [ "object" ] . ( string )
2020-11-13 14:19:09 +00:00
if likeObjectOk && len ( likeObject ) > 0 && strings . Contains ( likeObject , blogIri ) {
sendNotification ( fmt . Sprintf ( "%s liked %s" , activityActor , likeObject ) )
2020-11-06 17:45:31 +00:00
}
}
2020-10-26 16:37:31 +00:00
case "Announce" :
{
2020-11-06 17:45:31 +00:00
announceObject , announceObjectOk := activity [ "object" ] . ( string )
2020-11-13 14:19:09 +00:00
if announceObjectOk && len ( announceObject ) > 0 && strings . Contains ( announceObject , blogIri ) {
sendNotification ( fmt . Sprintf ( "%s announced %s" , activityActor , announceObject ) )
2020-11-06 17:45:31 +00:00
}
2020-10-26 16:37:31 +00:00
}
}
2020-11-13 20:29:09 +00:00
// Return 200
w . WriteHeader ( http . StatusOK )
2020-11-13 14:19:09 +00:00
}
2020-10-26 16:37:31 +00:00
2020-11-13 15:29:22 +00:00
func apVerifySignature ( r * http . Request ) ( * asPerson , string , int , error ) {
2020-11-13 14:19:09 +00:00
verifier , err := httpsig . NewVerifier ( r )
if err != nil {
// Error with signature header etc.
2020-11-13 15:29:22 +00:00
return nil , "" , 0 , err
2020-11-13 14:19:09 +00:00
}
keyID := verifier . KeyId ( )
2020-11-13 15:29:22 +00:00
actor , statusCode , err := apGetRemoteActor ( keyID )
if err != nil || actor == nil || statusCode != 0 {
2020-11-13 14:19:09 +00:00
// Actor not found or something else bad
2020-11-13 15:29:22 +00:00
return nil , keyID , statusCode , err
2020-11-13 14:19:09 +00:00
}
2020-11-13 15:29:22 +00:00
if actor . PublicKey == nil || actor . PublicKey . PublicKeyPem == "" {
return nil , keyID , 0 , errors . New ( "Actor has no public key" )
2020-11-13 14:19:09 +00:00
}
block , _ := pem . Decode ( [ ] byte ( actor . PublicKey . PublicKeyPem ) )
if block == nil {
2020-11-13 15:29:22 +00:00
return nil , keyID , 0 , errors . New ( "Public key invalid" )
2020-11-13 14:19:09 +00:00
}
pubKey , err := x509 . ParsePKIXPublicKey ( block . Bytes )
if err != nil {
// Unable to parse public key
2020-11-13 15:29:22 +00:00
return nil , keyID , 0 , err
2020-11-13 14:19:09 +00:00
}
2020-11-13 15:29:22 +00:00
return actor , keyID , 0 , verifier . Verify ( pubKey , httpsig . RSA_SHA256 )
2020-10-26 16:37:31 +00:00
}
func handleWellKnownHostMeta ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( contentType , "application/xrd+xml" + charsetUtf8Suffix )
w . Write ( [ ] byte ( ` <?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="https:// ` + r . Host + ` /.well-known/webfinger?resource= { uri}"/></XRD> ` ) )
}
2020-11-13 15:29:22 +00:00
func apGetRemoteActor ( iri string ) ( * asPerson , int , error ) {
2020-10-26 16:37:31 +00:00
req , err := http . NewRequest ( http . MethodGet , iri , nil )
if err != nil {
2020-11-13 15:29:22 +00:00
return nil , 0 , err
2020-10-26 16:37:31 +00:00
}
2020-11-16 13:18:14 +00:00
req . Header . Set ( "Accept" , contentTypeAS )
2020-11-16 17:34:29 +00:00
req . Header . Set ( userAgent , appUserAgent )
2020-10-26 16:37:31 +00:00
resp , err := http . DefaultClient . Do ( req )
if err != nil {
2020-11-13 15:29:22 +00:00
return nil , 0 , err
2020-10-26 16:37:31 +00:00
}
if ! apRequestIsSuccess ( resp . StatusCode ) {
2020-11-13 15:29:22 +00:00
return nil , resp . StatusCode , nil
2020-10-26 16:37:31 +00:00
}
actor := & asPerson { }
err = json . NewDecoder ( resp . Body ) . Decode ( actor )
defer resp . Body . Close ( )
if err != nil {
2020-11-13 15:29:22 +00:00
return nil , 0 , err
2020-10-26 16:37:31 +00:00
}
2020-11-13 15:29:22 +00:00
return actor , 0 , nil
2020-10-26 16:37:31 +00:00
}
func apGetAllFollowers ( blog string ) ( map [ string ] string , error ) {
2020-11-09 15:40:12 +00:00
rows , err := appDbQuery ( "select follower, inbox from activitypub_followers where blog = @blog" , sql . Named ( "blog" , blog ) )
2020-10-26 16:37:31 +00:00
if err != nil {
return nil , err
}
followers := map [ string ] string { }
for rows . Next ( ) {
var follower , inbox string
err = rows . Scan ( & follower , & inbox )
if err != nil {
return nil , err
}
followers [ follower ] = inbox
}
2020-11-06 17:45:31 +00:00
return followers , nil
2020-10-26 16:37:31 +00:00
}
func apAddFollower ( blog , follower , inbox string ) error {
2020-11-09 15:40:12 +00:00
_ , err := appDbExec ( "insert or replace into activitypub_followers (blog, follower, inbox) values (@blog, @follower, @inbox)" , sql . Named ( "blog" , blog ) , sql . Named ( "follower" , follower ) , sql . Named ( "inbox" , inbox ) )
return err
2020-10-26 16:37:31 +00:00
}
func apRemoveFollower ( blog , follower string ) error {
2020-11-09 15:40:12 +00:00
_ , err := appDbExec ( "delete from activitypub_followers where blog = @blog and follower = @follower" , sql . Named ( "blog" , blog ) , sql . Named ( "follower" , follower ) )
return err
2020-10-26 16:37:31 +00:00
}
2020-11-09 15:40:12 +00:00
func ( p * post ) apPost ( ) {
2020-10-26 16:37:31 +00:00
n := p . toASNote ( )
2020-11-08 22:04:32 +00:00
createActivity := make ( map [ string ] interface { } )
createActivity [ "@context" ] = asContext
createActivity [ "actor" ] = appConfig . Blogs [ p . Blog ] . apIri ( )
2020-11-16 17:34:29 +00:00
createActivity [ "id" ] = p . fullURL ( )
2020-11-08 22:04:32 +00:00
createActivity [ "published" ] = n . Published
createActivity [ "type" ] = "Create"
createActivity [ "object" ] = n
apSendToAllFollowers ( p . Blog , createActivity )
2020-11-13 14:19:09 +00:00
if n . InReplyTo != "" {
// Is reply, so announce it
time . Sleep ( 30 * time . Second )
p . apAnnounce ( )
}
2020-10-26 16:37:31 +00:00
}
2020-11-09 15:40:12 +00:00
func ( p * post ) apUpdate ( ) {
n := p . toASNote ( )
updateActivity := make ( map [ string ] interface { } )
updateActivity [ "@context" ] = asContext
updateActivity [ "actor" ] = appConfig . Blogs [ p . Blog ] . apIri ( )
2020-11-16 17:34:29 +00:00
updateActivity [ "id" ] = p . fullURL ( )
2020-11-13 14:19:09 +00:00
updateActivity [ "published" ] = time . Now ( ) . Format ( "2006-01-02T15:04:05-07:00" )
2020-11-09 15:40:12 +00:00
updateActivity [ "type" ] = "Update"
updateActivity [ "object" ] = n
apSendToAllFollowers ( p . Blog , updateActivity )
2020-10-26 16:37:31 +00:00
}
2020-11-13 14:19:09 +00:00
func ( p * post ) apAnnounce ( ) {
announceActivity := make ( map [ string ] interface { } )
announceActivity [ "@context" ] = asContext
announceActivity [ "actor" ] = appConfig . Blogs [ p . Blog ] . apIri ( )
2020-11-16 17:34:29 +00:00
announceActivity [ "id" ] = p . fullURL ( ) + "#announce"
2020-11-13 14:19:09 +00:00
announceActivity [ "published" ] = p . toASNote ( ) . Published
announceActivity [ "type" ] = "Announce"
2020-11-16 17:34:29 +00:00
announceActivity [ "object" ] = p . fullURL ( )
2020-11-13 14:19:09 +00:00
apSendToAllFollowers ( p . Blog , announceActivity )
}
2020-11-09 15:40:12 +00:00
func ( p * post ) apDelete ( ) {
2020-11-08 22:04:32 +00:00
deleteActivity := make ( map [ string ] interface { } )
deleteActivity [ "@context" ] = asContext
deleteActivity [ "actor" ] = appConfig . Blogs [ p . Blog ] . apIri ( )
2020-11-16 17:34:29 +00:00
deleteActivity [ "id" ] = p . fullURL ( ) + "#delete"
2020-11-08 22:04:32 +00:00
deleteActivity [ "type" ] = "Delete"
deleteActivity [ "object" ] = map [ string ] string {
2020-11-16 17:34:29 +00:00
"id" : p . fullURL ( ) ,
2020-11-08 22:04:32 +00:00
"type" : "Tombstone" ,
}
apSendToAllFollowers ( p . Blog , deleteActivity )
2020-10-26 16:37:31 +00:00
}
func apAccept ( blogName string , blog * configBlog , follow map [ string ] interface { } ) {
// it's a follow, write it down
newFollower := follow [ "actor" ] . ( string )
log . Println ( "New follow request:" , newFollower )
// check we aren't following ourselves
if newFollower == follow [ "object" ] {
// actor and object are equal
return
}
2020-11-13 15:29:22 +00:00
follower , status , err := apGetRemoteActor ( newFollower )
if err != nil || status != 0 {
2020-10-26 16:37:31 +00:00
// Couldn't retrieve remote actor info
log . Println ( "Failed to retrieve remote actor info:" , newFollower )
return
}
// Add or update follower
apAddFollower ( blogName , follower . ID , follower . Inbox )
// remove @context from the inner activity
delete ( follow , "@context" )
accept := make ( map [ string ] interface { } )
accept [ "@context" ] = asContext
accept [ "to" ] = follow [ "actor" ]
_ , accept [ "id" ] = apNewID ( blog )
accept [ "actor" ] = blog . apIri ( )
accept [ "object" ] = follow
accept [ "type" ] = "Accept"
err = apSendSigned ( blog , accept , follower . Inbox )
if err != nil {
log . Printf ( "Failed to accept: %s\n%s\n" , follower . ID , err . Error ( ) )
return
}
log . Println ( "Follower accepted:" , follower . ID )
}
func apSendToAllFollowers ( blog string , activity interface { } ) {
followers , err := apGetAllFollowers ( blog )
if err != nil {
log . Println ( "Failed to retrieve followers:" , err . Error ( ) )
return
}
apSendTo ( appConfig . Blogs [ blog ] , activity , followers )
}
func apSendTo ( blog * configBlog , activity interface { } , followers map [ string ] string ) {
for _ , i := range followers {
go func ( inbox string ) {
_ = apSendSigned ( blog , activity , inbox )
} ( i )
}
}
func apSendSigned ( blog * configBlog , activity interface { } , to string ) error {
// Marshal to json
body , err := json . Marshal ( activity )
if err != nil {
return err
}
// Copy body to sign it
bodyCopy := make ( [ ] byte , len ( body ) )
copy ( bodyCopy , body )
// Create request context with timeout
ctx , cancel := context . WithTimeout ( context . Background ( ) , time . Minute )
defer cancel ( )
// Create request
r , err := http . NewRequestWithContext ( ctx , http . MethodPost , to , bytes . NewBuffer ( body ) )
if err != nil {
return err
}
iri , err := url . Parse ( to )
if err != nil {
return err
}
2020-11-16 13:18:14 +00:00
r . Header . Set ( "Accept-Charset" , "utf-8" )
r . Header . Set ( "Date" , time . Now ( ) . UTC ( ) . Format ( "Mon, 02 Jan 2006 15:04:05" ) + " GMT" )
2020-11-16 17:34:29 +00:00
r . Header . Set ( userAgent , appUserAgent )
2020-11-16 13:18:14 +00:00
r . Header . Set ( "Accept" , contentTypeASUTF8 )
r . Header . Set ( contentType , contentTypeASUTF8 )
r . Header . Set ( "Host" , iri . Host )
2020-10-26 16:37:31 +00:00
// Sign request
apPostSignMutex . Lock ( )
err = apPostSigner . SignRequest ( apPrivateKey , blog . apIri ( ) + "#main-key" , r , bodyCopy )
apPostSignMutex . Unlock ( )
if err != nil {
return err
}
// Do request
resp , err := http . DefaultClient . Do ( r )
2020-11-11 08:03:20 +00:00
if err != nil {
return err
}
2020-10-26 16:37:31 +00:00
if ! apRequestIsSuccess ( resp . StatusCode ) {
body , _ := ioutil . ReadAll ( resp . Body )
2020-11-11 08:03:20 +00:00
_ = resp . Body . Close ( )
2020-10-26 16:37:31 +00:00
return fmt . Errorf ( "signed request failed with status %d: %s" , resp . StatusCode , string ( body ) )
}
2020-11-11 08:03:20 +00:00
return nil
2020-10-26 16:37:31 +00:00
}
func apNewID ( blog * configBlog ) ( hash string , url string ) {
return hash , blog . apIri ( ) + generateRandomString ( 16 )
}
func ( b * configBlog ) apIri ( ) string {
return appConfig . Server . PublicAddress + b . Path
}
func apRequestIsSuccess ( code int ) bool {
return code == http . StatusOK || code == http . StatusCreated || code == http . StatusAccepted || code == http . StatusNoContent
}