/ *
* Copyright © 2018 - 2020 A Bunch Tell LLC .
*
* This file is part of WriteFreely .
*
* WriteFreely is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License , included
* in the LICENSE file in this source code package .
* /
package writefreely
import (
"bytes"
"crypto/sha256"
"database/sql"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"time"
"github.com/gorilla/mux"
"github.com/writeas/activity/streams"
"github.com/writeas/httpsig"
"github.com/writeas/impart"
"github.com/writeas/nerds/store"
"github.com/writeas/web-core/activitypub"
"github.com/writeas/web-core/activitystreams"
"github.com/writeas/web-core/log"
)
const (
// TODO: delete. don't use this!
apCustomHandleDefault = "blog"
)
type RemoteUser struct {
ID int64
ActorID string
Inbox string
SharedInbox string
}
func ( ru * RemoteUser ) AsPerson ( ) * activitystreams . Person {
return & activitystreams . Person {
BaseObject : activitystreams . BaseObject {
Type : "Person" ,
Context : [ ] interface { } {
activitystreams . Namespace ,
} ,
ID : ru . ActorID ,
} ,
Inbox : ru . Inbox ,
Endpoints : activitystreams . Endpoints {
SharedInbox : ru . SharedInbox ,
} ,
}
}
func activityPubClient ( ) * http . Client {
return & http . Client {
Timeout : 15 * time . Second ,
}
}
func handleFetchCollectionActivities ( app * App , w http . ResponseWriter , r * http . Request ) error {
w . Header ( ) . Set ( "Server" , serverSoftware )
vars := mux . Vars ( r )
alias := vars [ "alias" ]
// TODO: enforce visibility
// Get base Collection data
var c * Collection
var err error
if app . cfg . App . SingleUser {
c , err = app . db . GetCollectionByID ( 1 )
} else {
c , err = app . db . GetCollection ( alias )
}
if err != nil {
return err
}
suspended , err := app . db . IsUserSuspended ( c . OwnerID )
if err != nil {
log . Error ( "fetch collection activities: %v" , err )
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c . hostName = app . cfg . App . Host
p := c . PersonObject ( )
return impart . RenderActivityJSON ( w , p , http . StatusOK )
}
func handleFetchCollectionOutbox ( app * App , w http . ResponseWriter , r * http . Request ) error {
w . Header ( ) . Set ( "Server" , serverSoftware )
vars := mux . Vars ( r )
alias := vars [ "alias" ]
// TODO: enforce visibility
// Get base Collection data
var c * Collection
var err error
if app . cfg . App . SingleUser {
c , err = app . db . GetCollectionByID ( 1 )
} else {
c , err = app . db . GetCollection ( alias )
}
if err != nil {
return err
}
suspended , err := app . db . IsUserSuspended ( c . OwnerID )
if err != nil {
log . Error ( "fetch collection outbox: %v" , err )
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c . hostName = app . cfg . App . Host
if app . cfg . App . SingleUser {
if alias != c . Alias {
return ErrCollectionNotFound
}
}
res := & CollectionObj { Collection : * c }
app . db . GetPostsCount ( res , false )
accountRoot := c . FederatedAccount ( )
page := r . FormValue ( "page" )
p , err := strconv . Atoi ( page )
if err != nil || p < 1 {
// Return outbox
oc := activitystreams . NewOrderedCollection ( accountRoot , "outbox" , res . TotalPosts )
return impart . RenderActivityJSON ( w , oc , http . StatusOK )
}
// Return outbox page
ocp := activitystreams . NewOrderedCollectionPage ( accountRoot , "outbox" , res . TotalPosts , p )
ocp . OrderedItems = [ ] interface { } { }
posts , err := app . db . GetPosts ( app . cfg , c , p , false , true , false )
for _ , pp := range * posts {
pp . Collection = res
o := pp . ActivityObject ( app . cfg )
a := activitystreams . NewCreateActivity ( o )
ocp . OrderedItems = append ( ocp . OrderedItems , * a )
}
return impart . RenderActivityJSON ( w , ocp , http . StatusOK )
}
func handleFetchCollectionFollowers ( app * App , w http . ResponseWriter , r * http . Request ) error {
w . Header ( ) . Set ( "Server" , serverSoftware )
vars := mux . Vars ( r )
alias := vars [ "alias" ]
// TODO: enforce visibility
// Get base Collection data
var c * Collection
var err error
if app . cfg . App . SingleUser {
c , err = app . db . GetCollectionByID ( 1 )
} else {
c , err = app . db . GetCollection ( alias )
}
if err != nil {
return err
}
suspended , err := app . db . IsUserSuspended ( c . OwnerID )
if err != nil {
log . Error ( "fetch collection followers: %v" , err )
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c . hostName = app . cfg . App . Host
accountRoot := c . FederatedAccount ( )
folls , err := app . db . GetAPFollowers ( c )
if err != nil {
return err
}
page := r . FormValue ( "page" )
p , err := strconv . Atoi ( page )
if err != nil || p < 1 {
// Return outbox
oc := activitystreams . NewOrderedCollection ( accountRoot , "followers" , len ( * folls ) )
return impart . RenderActivityJSON ( w , oc , http . StatusOK )
}
// Return outbox page
ocp := activitystreams . NewOrderedCollectionPage ( accountRoot , "followers" , len ( * folls ) , p )
ocp . OrderedItems = [ ] interface { } { }
/ *
for _ , f := range * folls {
ocp . OrderedItems = append ( ocp . OrderedItems , f . ActorID )
}
* /
return impart . RenderActivityJSON ( w , ocp , http . StatusOK )
}
func handleFetchCollectionFollowing ( app * App , w http . ResponseWriter , r * http . Request ) error {
w . Header ( ) . Set ( "Server" , serverSoftware )
vars := mux . Vars ( r )
alias := vars [ "alias" ]
// TODO: enforce visibility
// Get base Collection data
var c * Collection
var err error
if app . cfg . App . SingleUser {
c , err = app . db . GetCollectionByID ( 1 )
} else {
c , err = app . db . GetCollection ( alias )
}
if err != nil {
return err
}
suspended , err := app . db . IsUserSuspended ( c . OwnerID )
if err != nil {
log . Error ( "fetch collection following: %v" , err )
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c . hostName = app . cfg . App . Host
accountRoot := c . FederatedAccount ( )
page := r . FormValue ( "page" )
p , err := strconv . Atoi ( page )
if err != nil || p < 1 {
// Return outbox
oc := activitystreams . NewOrderedCollection ( accountRoot , "following" , 0 )
return impart . RenderActivityJSON ( w , oc , http . StatusOK )
}
// Return outbox page
ocp := activitystreams . NewOrderedCollectionPage ( accountRoot , "following" , 0 , p )
ocp . OrderedItems = [ ] interface { } { }
return impart . RenderActivityJSON ( w , ocp , http . StatusOK )
}
func handleFetchCollectionInbox ( app * App , w http . ResponseWriter , r * http . Request ) error {
w . Header ( ) . Set ( "Server" , serverSoftware )
vars := mux . Vars ( r )
alias := vars [ "alias" ]
var c * Collection
var err error
if app . cfg . App . SingleUser {
c , err = app . db . GetCollectionByID ( 1 )
} else {
c , err = app . db . GetCollection ( alias )
}
if err != nil {
// TODO: return Reject?
return err
}
suspended , err := app . db . IsUserSuspended ( c . OwnerID )
if err != nil {
log . Error ( "fetch collection inbox: %v" , err )
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c . hostName = app . cfg . App . Host
if debugging {
dump , err := httputil . DumpRequest ( r , true )
if err != nil {
log . Error ( "Can't dump: %v" , err )
} else {
log . Info ( "Rec'd! %q" , dump )
}
}
var m map [ string ] interface { }
if err := json . NewDecoder ( r . Body ) . Decode ( & m ) ; err != nil {
return err
}
a := streams . NewAccept ( )
p := c . PersonObject ( )
var to * url . URL
var isFollow , isUnfollow bool
fullActor := & activitystreams . Person { }
var remoteUser * RemoteUser
res := & streams . Resolver {
FollowCallback : func ( f * streams . Follow ) error {
isFollow = true
// 1) Use the Follow concrete type here
// 2) Errors are propagated to res.Deserialize call below
m [ "@context" ] = [ ] string { activitystreams . Namespace }
b , _ := json . Marshal ( m )
if debugging {
log . Info ( "Follow: %s" , b )
}
_ , followID := f . GetId ( )
if followID == nil {
log . Error ( "Didn't resolve follow ID" )
} else {
aID := c . FederatedAccount ( ) + "#accept-" + store . GenerateFriendlyRandomString ( 20 )
acceptID , err := url . Parse ( aID )
if err != nil {
log . Error ( "Couldn't parse generated Accept URL '%s': %v" , aID , err )
}
a . SetId ( acceptID )
}
a . AppendObject ( f . Raw ( ) )
_ , to = f . GetActor ( 0 )
obj := f . Raw ( ) . GetObjectIRI ( 0 )
a . AppendActor ( obj )
// First get actor information
if to == nil {
return fmt . Errorf ( "No valid `to` string" )
}
fullActor , remoteUser , err = getActor ( app , to . String ( ) )
if err != nil {
return err
}
return impart . RenderActivityJSON ( w , m , http . StatusOK )
} ,
UndoCallback : func ( u * streams . Undo ) error {
isUnfollow = true
m [ "@context" ] = [ ] string { activitystreams . Namespace }
b , _ := json . Marshal ( m )
if debugging {
log . Info ( "Undo: %s" , b )
}
a . AppendObject ( u . Raw ( ) )
_ , to = u . GetActor ( 0 )
// TODO: get actor from object.object, not object
obj := u . Raw ( ) . GetObjectIRI ( 0 )
a . AppendActor ( obj )
if to != nil {
// Populate fullActor from DB?
remoteUser , err = getRemoteUser ( app , to . String ( ) )
if err != nil {
if iErr , ok := err . ( * impart . HTTPError ) ; ok {
if iErr . Status == http . StatusNotFound {
log . Error ( "No remoteuser info for Undo event!" )
}
}
return err
} else {
fullActor = remoteUser . AsPerson ( )
}
} else {
log . Error ( "No to on Undo!" )
}
return impart . RenderActivityJSON ( w , m , http . StatusOK )
} ,
}
if err := res . Deserialize ( m ) ; err != nil {
// 3) Any errors from #2 can be handled, or the payload is an unknown type.
log . Error ( "Unable to resolve Follow: %v" , err )
if debugging {
log . Error ( "Map: %s" , m )
}
return err
}
go func ( ) {
time . Sleep ( 2 * time . Second )
am , err := a . Serialize ( )
if err != nil {
log . Error ( "Unable to serialize Accept: %v" , err )
return
}
am [ "@context" ] = [ ] string { activitystreams . Namespace }
if to == nil {
log . Error ( "No to! %v" , err )
return
}
err = makeActivityPost ( app . cfg . App . Host , p , fullActor . Inbox , am )
if err != nil {
log . Error ( "Unable to make activity POST: %v" , err )
return
}
if isFollow {
t , err := app . db . Begin ( )
if err != nil {
log . Error ( "Unable to start transaction: %v" , err )
return
}
var followerID int64
if remoteUser != nil {
followerID = remoteUser . ID
} else {
// Add follower locally, since it wasn't found before
res , err := t . Exec ( "INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)" , fullActor . ID , fullActor . Inbox , fullActor . Endpoints . SharedInbox )
if err != nil {
// if duplicate key, res will be nil and panic on
// res.LastInsertId below
t . Rollback ( )
log . Error ( "Couldn't add new remoteuser in DB: %v\n" , err )
return
}
followerID , err = res . LastInsertId ( )
if err != nil {
t . Rollback ( )
log . Error ( "no lastinsertid for followers, rolling back: %v" , err )
return
}
// Add in key
_ , err = t . Exec ( "INSERT INTO remoteuserkeys (id, remote_user_id, public_key) VALUES (?, ?, ?)" , fullActor . PublicKey . ID , followerID , fullActor . PublicKey . PublicKeyPEM )
if err != nil {
if ! app . db . isDuplicateKeyErr ( err ) {
t . Rollback ( )
log . Error ( "Couldn't add follower keys in DB: %v\n" , err )
return
}
}
}
// Add follow
_ , err = t . Exec ( "INSERT INTO remotefollows (collection_id, remote_user_id, created) VALUES (?, ?, " + app . db . now ( ) + ")" , c . ID , followerID )
if err != nil {
if ! app . db . isDuplicateKeyErr ( err ) {
t . Rollback ( )
log . Error ( "Couldn't add follower in DB: %v\n" , err )
return
}
}
err = t . Commit ( )
if err != nil {
t . Rollback ( )
log . Error ( "Rolling back after Commit(): %v\n" , err )
return
}
} else if isUnfollow {
// Remove follower locally
_ , err = app . db . Exec ( "DELETE FROM remotefollows WHERE collection_id = ? AND remote_user_id = (SELECT id FROM remoteusers WHERE actor_id = ?)" , c . ID , to . String ( ) )
if err != nil {
log . Error ( "Couldn't remove follower from DB: %v\n" , err )
}
}
} ( )
return nil
}
func makeActivityPost ( hostName string , p * activitystreams . Person , url string , m interface { } ) error {
log . Info ( "POST %s" , url )
b , err := json . Marshal ( m )
if err != nil {
return err
}
r , _ := http . NewRequest ( "POST" , url , bytes . NewBuffer ( b ) )
r . Header . Add ( "Content-Type" , "application/activity+json" )
r . Header . Set ( "User-Agent" , "Go (" + serverSoftware + "/" + softwareVer + "; +" + hostName + ")" )
h := sha256 . New ( )
h . Write ( b )
r . Header . Add ( "Digest" , "SHA-256=" + base64 . StdEncoding . EncodeToString ( h . Sum ( nil ) ) )
// Sign using the 'Signature' header
privKey , err := activitypub . DecodePrivateKey ( p . GetPrivKey ( ) )
if err != nil {
return err
}
signer := httpsig . NewSigner ( p . PublicKey . ID , privKey , httpsig . RSASHA256 , [ ] string { "(request-target)" , "date" , "host" , "digest" } )
err = signer . SignSigHeader ( r )
if err != nil {
log . Error ( "Can't sign: %v" , err )
}
if debugging {
dump , err := httputil . DumpRequestOut ( r , true )
if err != nil {
log . Error ( "Can't dump: %v" , err )
} else {
log . Info ( "%s" , dump )
}
}
resp , err := activityPubClient ( ) . Do ( r )
if err != nil {
return err
}
if resp != nil && resp . Body != nil {
defer resp . Body . Close ( )
}
body , err := ioutil . ReadAll ( resp . Body )
if err != nil {
return err
}
if debugging {
log . Info ( "Status : %s" , resp . Status )
log . Info ( "Response: %s" , body )
}
return nil
}
func resolveIRI ( hostName , url string ) ( [ ] byte , error ) {
log . Info ( "GET %s" , url )
r , _ := http . NewRequest ( "GET" , url , nil )
r . Header . Add ( "Accept" , "application/activity+json" )
r . Header . Set ( "User-Agent" , "Go (" + serverSoftware + "/" + softwareVer + "; +" + hostName + ")" )
if debugging {
dump , err := httputil . DumpRequestOut ( r , true )
if err != nil {
log . Error ( "Can't dump: %v" , err )
} else {
log . Info ( "%s" , dump )
}
}
resp , err := activityPubClient ( ) . Do ( r )
if err != nil {
return nil , err
}
if resp != nil && resp . Body != nil {
defer resp . Body . Close ( )
}
body , err := ioutil . ReadAll ( resp . Body )
if err != nil {
return nil , err
}
if debugging {
log . Info ( "Status : %s" , resp . Status )
log . Info ( "Response: %s" , body )
}
return body , nil
}
func deleteFederatedPost ( app * App , p * PublicPost , collID int64 ) error {
if debugging {
log . Info ( "Deleting federated post!" )
}
p . Collection . hostName = app . cfg . App . Host
actor := p . Collection . PersonObject ( collID )
na := p . ActivityObject ( app . cfg )
// Add followers
p . Collection . ID = collID
followers , err := app . db . GetAPFollowers ( & p . Collection . Collection )
if err != nil {
log . Error ( "Couldn't delete post (get followers)! %v" , err )
return err
}
inboxes := map [ string ] [ ] string { }
for _ , f := range * followers {
inbox := f . SharedInbox
if inbox == "" {
inbox = f . Inbox
}
if _ , ok := inboxes [ inbox ] ; ok {
inboxes [ inbox ] = append ( inboxes [ inbox ] , f . ActorID )
} else {
inboxes [ inbox ] = [ ] string { f . ActorID }
}
}
for si , instFolls := range inboxes {
na . CC = [ ] string { }
for _ , f := range instFolls {
na . CC = append ( na . CC , f )
}
err = makeActivityPost ( app . cfg . App . Host , actor , si , activitystreams . NewDeleteActivity ( na ) )
if err != nil {
log . Error ( "Couldn't delete post! %v" , err )
}
}
return nil
}
func federatePost ( app * App , p * PublicPost , collID int64 , isUpdate bool ) error {
if debugging {
if isUpdate {
log . Info ( "Federating updated post!" )
} else {
log . Info ( "Federating new post!" )
}
}
actor := p . Collection . PersonObject ( collID )
na := p . ActivityObject ( app . cfg )
// Add followers
p . Collection . ID = collID
followers , err := app . db . GetAPFollowers ( & p . Collection . Collection )
if err != nil {
log . Error ( "Couldn't post! %v" , err )
return err
}
log . Info ( "Followers for %d: %+v" , collID , followers )
inboxes := map [ string ] [ ] string { }
for _ , f := range * followers {
inbox := f . SharedInbox
if inbox == "" {
inbox = f . Inbox
}
if _ , ok := inboxes [ inbox ] ; ok {
inboxes [ inbox ] = append ( inboxes [ inbox ] , f . ActorID )
} else {
inboxes [ inbox ] = [ ] string { f . ActorID }
}
}
for si , instFolls := range inboxes {
na . CC = [ ] string { }
for _ , f := range instFolls {
na . CC = append ( na . CC , f )
}
var activity * activitystreams . Activity
if isUpdate {
activity = activitystreams . NewUpdateActivity ( na )
} else {
activity = activitystreams . NewCreateActivity ( na )
activity . To = na . To
activity . CC = na . CC
}
err = makeActivityPost ( app . cfg . App . Host , actor , si , activity )
if err != nil {
log . Error ( "Couldn't post! %v" , err )
}
}
return nil
}
func getRemoteUser ( app * App , actorID string ) ( * RemoteUser , error ) {
u := RemoteUser { ActorID : actorID }
err := app . db . QueryRow ( "SELECT id, inbox, shared_inbox FROM remoteusers WHERE actor_id = ?" , actorID ) . Scan ( & u . ID , & u . Inbox , & u . SharedInbox )
switch {
case err == sql . ErrNoRows :
return nil , impart . HTTPError { http . StatusNotFound , "No remote user with that ID." }
case err != nil :
log . Error ( "Couldn't get remote user %s: %v" , actorID , err )
return nil , err
}
return & u , nil
}
func getActor ( app * App , actorIRI string ) ( * activitystreams . Person , * RemoteUser , error ) {
log . Info ( "Fetching actor %s locally" , actorIRI )
actor := & activitystreams . Person { }
remoteUser , err := getRemoteUser ( app , actorIRI )
if err != nil {
if iErr , ok := err . ( impart . HTTPError ) ; ok {
if iErr . Status == http . StatusNotFound {
// Fetch remote actor
log . Info ( "Not found; fetching actor %s remotely" , actorIRI )
actorResp , err := resolveIRI ( app . cfg . App . Host , actorIRI )
if err != nil {
log . Error ( "Unable to get actor! %v" , err )
return nil , nil , impart . HTTPError { http . StatusInternalServerError , "Couldn't fetch actor." }
}
if err := unmarshalActor ( actorResp , actor ) ; err != nil {
log . Error ( "Unable to unmarshal actor! %v" , err )
return nil , nil , impart . HTTPError { http . StatusInternalServerError , "Couldn't parse actor." }
}
} else {
return nil , nil , err
}
} else {
return nil , nil , err
}
} else {
actor = remoteUser . AsPerson ( )
}
return actor , remoteUser , nil
}
// unmarshal actor normalizes the actor response to conform to
// the type Person from github.com/writeas/web-core/activitysteams
//
// some implementations return different context field types
// this converts any non-slice contexts into a slice
func unmarshalActor ( actorResp [ ] byte , actor * activitystreams . Person ) error {
// FIXME: Hubzilla has an object for the Actor's url: cannot unmarshal object into Go struct field Person.url of type string
// flexActor overrides the Context field to allow
// all valid representations during unmarshal
flexActor := struct {
activitystreams . Person
Context json . RawMessage ` json:"@context,omitempty" `
} { }
if err := json . Unmarshal ( actorResp , & flexActor ) ; err != nil {
return err
}
actor . Endpoints = flexActor . Endpoints
actor . Followers = flexActor . Followers
actor . Following = flexActor . Following
actor . ID = flexActor . ID
actor . Icon = flexActor . Icon
actor . Inbox = flexActor . Inbox
actor . Name = flexActor . Name
actor . Outbox = flexActor . Outbox
actor . PreferredUsername = flexActor . PreferredUsername
actor . PublicKey = flexActor . PublicKey
actor . Summary = flexActor . Summary
actor . Type = flexActor . Type
actor . URL = flexActor . URL
func ( val interface { } ) {
switch val . ( type ) {
case [ ] interface { } :
// already a slice, do nothing
actor . Context = val . ( [ ] interface { } )
default :
actor . Context = [ ] interface { } { val }
}
} ( flexActor . Context )
return nil
}