activityserve/activityserve/actor.go

435 lines
13 KiB
Go

package activityserve
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/gologme/log"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"github.com/dchest/uniuri"
"github.com/go-fed/httpsig"
)
// Actor represents a local actor we can act on
// behalf of.
type Actor struct {
name, summary, actorType, iri string
followersIRI string
nuIri *url.URL
followers, following map[string]interface{}
posts map[int]map[string]interface{}
publicKey crypto.PublicKey
privateKey crypto.PrivateKey
publicKeyPem string
privateKeyPem string
publicKeyID string
}
// ActorToSave is a stripped down actor representation
// with exported properties in order for json to be
// able to marshal it.
// see https://stackoverflow.com/questions/26327391/json-marshalstruct-returns
type ActorToSave struct {
Name, Summary, ActorType, IRI, PublicKey, PrivateKey string
Followers, Following map[string]interface{}
}
// MakeActor returns a new local actor we can act
// on behalf of
func MakeActor(name, summary, actorType string) (Actor, error) {
followers := make(map[string]interface{})
following := make(map[string]interface{})
followersIRI := baseURL + name + "/followers"
publicKeyID := baseURL + name + "#main-key"
iri := baseURL + "/" + name
nuIri, err := url.Parse(iri)
if err != nil {
log.Info("Something went wrong when parsing the local actor uri into net/url")
return Actor{}, err
}
actor := Actor{
name: name,
summary: summary,
actorType: actorType,
iri: iri,
nuIri: nuIri,
followers: followers,
following: following,
followersIRI: followersIRI,
publicKeyID: publicKeyID,
}
// create actor's keypair
rng := rand.Reader
privateKey, err := rsa.GenerateKey(rng, 2048)
publicKey := privateKey.PublicKey
actor.publicKey = publicKey
actor.privateKey = privateKey
// marshal the crypto to pem
privateKeyDer := x509.MarshalPKCS1PrivateKey(privateKey)
privateKeyBlock := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: privateKeyDer,
}
actor.privateKeyPem = string(pem.EncodeToMemory(&privateKeyBlock))
publicKeyDer, err := x509.MarshalPKIXPublicKey(&publicKey)
if err != nil {
log.Info("Can't marshal public key")
return Actor{}, err
}
publicKeyBlock := pem.Block{
Type: "PUBLIC KEY",
Headers: nil,
Bytes: publicKeyDer,
}
actor.publicKeyPem = string(pem.EncodeToMemory(&publicKeyBlock))
err = actor.save()
if err != nil {
return actor, err
}
return actor, nil
}
// GetOutboxIRI returns the outbox iri in net/url
func (a *Actor) GetOutboxIRI() *url.URL {
iri := a.iri + "/outbox"
nuiri, _ := url.Parse(iri)
return nuiri
}
// LoadActor searches the filesystem and creates an Actor
// from the data in name.json
func LoadActor(name string) (Actor, error) {
// make sure our users can't read our hard drive
if strings.ContainsAny(name, "./ ") {
log.Info("Illegal characters in actor name")
return Actor{}, errors.New("Illegal characters in actor name")
}
jsonFile := storage + slash + "actors" + slash + name + slash + name + ".json"
fileHandle, err := os.Open(jsonFile)
if os.IsNotExist(err) {
log.Info("We don't have this kind of actor stored")
return Actor{}, err
}
byteValue, err := ioutil.ReadAll(fileHandle)
if err != nil {
log.Info("Error reading actor file")
return Actor{}, err
}
jsonData := make(map[string]interface{})
json.Unmarshal(byteValue, &jsonData)
nuIri, err := url.Parse(jsonData["IRI"].(string))
if err != nil {
log.Info("Something went wrong when parsing the local actor uri into net/url")
return Actor{}, err
}
// publicKeyNewLines := strings.ReplaceAll(jsonData["PublicKey"].(string), "\\n", "\n")
// privateKeyNewLines := strings.ReplaceAll(jsonData["PrivateKey"].(string), "\\n", "\n")
publicKeyDecoded, rest := pem.Decode([]byte(jsonData["PublicKey"].(string)))
if publicKeyDecoded == nil {
log.Info(rest)
panic("failed to parse PEM block containing the public key")
}
publicKey, err := x509.ParsePKIXPublicKey(publicKeyDecoded.Bytes)
if err != nil {
log.Info("Can't parse public keys")
log.Info(err)
return Actor{}, err
}
privateKeyDecoded, rest := pem.Decode([]byte(jsonData["PrivateKey"].(string)))
if privateKeyDecoded == nil {
log.Info(rest)
panic("failed to parse PEM block containing the private key")
}
privateKey, err := x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes)
if err != nil {
log.Info("Can't parse private keys")
log.Info(err)
return Actor{}, err
}
actor := Actor{
name: name,
summary: jsonData["Summary"].(string),
actorType: jsonData["ActorType"].(string),
iri: jsonData["IRI"].(string),
nuIri: nuIri,
followers: jsonData["Followers"].(map[string]interface{}),
following: jsonData["Following"].(map[string]interface{}),
publicKey: publicKey,
privateKey: privateKey,
publicKeyPem: jsonData["PublicKey"].(string),
privateKeyPem: jsonData["PrivateKey"].(string),
followersIRI: baseURL + name + "/followers",
publicKeyID: baseURL + name + "#main-key",
}
return actor, nil
}
// save the actor to file
func (a *Actor) save() error {
// check if we already have a directory to save actors
// and if not, create it
dir := storage + slash + "actors" + slash + a.name + slash + "items"
if _, err := os.Stat(dir); os.IsNotExist(err) {
os.MkdirAll(dir, 0755)
}
actorToSave := ActorToSave{
Name: a.name,
Summary: a.summary,
ActorType: a.actorType,
IRI: a.iri,
Followers: a.followers,
Following: a.following,
PublicKey: a.publicKeyPem,
PrivateKey: a.privateKeyPem,
}
actorJSON, err := json.MarshalIndent(actorToSave, "", "\t")
if err != nil {
log.Info("error Marshalling actor json")
return err
}
// log.Info(actorToSave)
// log.Info(string(actorJSON))
err = ioutil.WriteFile(storage+slash+"actors"+slash+a.name+slash+a.name+".json", actorJSON, 0644)
if err != nil {
log.Printf("WriteFileJson ERROR: %+v", err)
return err
}
return nil
}
func (a *Actor) whoAmI() string {
return `{"@context": "https://www.w3.org/ns/activitystreams",
"type": "` + a.actorType + `",
"id": "` + baseURL + a.name + `",
"name": "` + a.name + `",
"preferredUsername": "` + a.name + `",
"summary": "` + a.summary + `",
"inbox": "` + baseURL + a.name + `/inbox",
"outbox": "` + baseURL + a.name + `/outbox",
"followers": "` + baseURL + a.name + `/followers",
"following": "` + baseURL + a.name + `/following",
"liked": "` + baseURL + a.name + `/liked",
"publicKey": {
"id": "` + baseURL + a.name + `#main-key",
"owner": "` + baseURL + a.name + `",
"publicKeyPem": "` + strings.ReplaceAll(a.publicKeyPem, "\n", "\\n") + `"
}
}`
}
func (a *Actor) newID() string {
return uniuri.New()
}
// CreateNote posts an activityPub note to our followers
func (a *Actor) CreateNote(content string) {
// for now I will just write this to the outbox
id := a.newID()
create := make(map[string]interface{})
note := make(map[string]interface{})
context := make([]string, 1)
context[0] = "https://www.w3.org/ns/activitystreams"
create["@context"] = context
create["actor"] = baseURL + a.name
create["cc"] = a.followersIRI
create["id"] = baseURL + a.name + "/" + id
create["object"] = note
note["attributedTo"] = baseURL + a.name
note["cc"] = a.followersIRI
note["content"] = content
note["inReplyTo"] = "https://cybre.space/@qwazix/102688373602724023"
note["id"] = baseURL + a.name + "/note/" + id
note["published"] = time.Now().Format(time.RFC3339)
note["url"] = create["id"]
note["type"] = "Note"
note["to"] = "https://www.w3.org/ns/activitystreams#Public"
create["published"] = note["published"]
create["type"] = "Create"
// note := `{
// "actor" : "https://` + baseURL + a.name + `",
// "cc" : [
// "https://` + baseURL + a.name + `/followers"
// ],
// "id" : "https://` + baseURL + a.name + `/` + id +`",
// "object" : {
// "attributedTo" : "https://` + baseURL + a.name + `",
// "cc" : [
// "https://` + baseURL + a.name + `/followers"
// ],
// "content" : "`+ content + `",
// "id" : "https://` + baseURL + a.name + `/` + id +`",
// "inReplyTo" : null,
// "published" : "2019-08-26T16:25:26Z",
// "to" : [
// "https://www.w3.org/ns/activitystreams#Public"
// ],
// "type" : "Note",
// "url" : "https://` + baseURL + a.name + `/` + id +`"
// },
// "published" : "2019-08-26T16:25:26Z",
// "to" : [
// "https://www.w3.org/ns/activitystreams#Public"
// ],
// "type" : "Create"
// }`
to, _ := url.Parse("https://cybre.space/inbox")
go a.send(create, to)
a.saveItem(id, create)
}
func (a *Actor) saveItem(id string, content map[string]interface{}) error {
JSON, _ := json.MarshalIndent(content, "", "\t")
dir := storage + slash + "actors" + slash + a.name + slash + "items"
err := ioutil.WriteFile(dir+slash+id+".json", JSON, 0644)
if err != nil {
log.Printf("WriteFileJson ERROR: %+v", err)
return err
}
return nil
}
// send is here for backward compatibility and maybe extra pre-processing
// not always required
func (a *Actor) send(content map[string]interface{}, to *url.URL) (err error) {
return a.signedHTTPPost(content, to.String())
}
func (a *Actor) signedHTTPPost(content map[string]interface{}, to string) (err error) {
b, err := json.Marshal(content)
if err != nil {
log.Info("Can't marshal JSON")
log.Info(err)
return
}
postSigner, _, _ := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, []string{"(request-target)", "date", "host", "digest"}, httpsig.Signature)
byteCopy := make([]byte, len(b))
copy(byteCopy, b)
buf := bytes.NewBuffer(byteCopy)
req, err := http.NewRequest("POST", to, buf)
if err != nil {
log.Info(err)
return
}
// I prefer to deal with strings and just parse to net/url if and when
// needed, even if here we do one extra round trip
iri, err := url.Parse(to)
if err != nil {
log.Error("cannot parse url for POST, check your syntax")
return err
}
req.Header.Add("Accept-Charset", "utf-8")
req.Header.Add("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", fmt.Sprintf("activityserve 0.0"))
req.Header.Add("Host", iri.Host)
req.Header.Add("Accept", "application/activity+json")
sum := sha256.Sum256(b)
req.Header.Add("Digest",
fmt.Sprintf("SHA-256=%s",
base64.StdEncoding.EncodeToString(sum[:])))
err = postSigner.SignRequest(a.privateKey, a.publicKeyID, req)
if err != nil {
log.Info(err)
return
}
resp, err := client.Do(req)
if err != nil {
log.Info(err)
return
}
defer resp.Body.Close()
if !isSuccess(resp.StatusCode) {
responseData, _ := ioutil.ReadAll(resp.Body)
err = fmt.Errorf("POST request to %s failed (%d): %s\nResponse: %s \nRequest: %s \nHeaders: %s", to, resp.StatusCode, resp.Status, formatJSON(responseData), formatJSON(byteCopy), req.Header)
log.Info(err)
return
}
responseData, _ := ioutil.ReadAll(resp.Body)
fmt.Printf("POST request to %s succeeded (%d): %s \nResponse: %s \nRequest: %s \nHeaders: %s", to, resp.StatusCode, resp.Status, formatJSON(responseData), formatJSON(byteCopy), req.Header)
return
}
func (a *Actor) signedHTTPGet(address string) (string, error){
req, err := http.NewRequest("GET", address, nil)
if err != nil {
log.Error("cannot create new http.request")
return "", err
}
iri, err := url.Parse(address)
if err != nil {
log.Error("cannot parse url for GET, check your syntax")
return "", err
}
req.Header.Add("Accept-Charset", "utf-8")
req.Header.Add("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", fmt.Sprintf("%s %s %s", userAgent, libName, version))
req.Header.Add("host", iri.Host)
req.Header.Add("digest", "")
req.Header.Add("Accept", "application/activity+json; profile=\"https://www.w3.org/ns/activitystreams\"")
// set up the http signer
signer, _, _ := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, []string{"(request-target)", "date", "host", "digest"}, httpsig.Signature)
err = signer.SignRequest(a.privateKey, a.publicKeyID, req)
if err != nil {
log.Error("Can't sign the request")
return "", err
}
resp, err := client.Do(req)
if err != nil {
log.Error("Cannot perform the GET request")
log.Error(err)
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
responseData, _ := ioutil.ReadAll(resp.Body)
return "", fmt.Errorf("GET request to %s failed (%d): %s \n%s", iri.String(), resp.StatusCode, resp.Status, formatJSON(responseData))
}
responseData, _ := ioutil.ReadAll(resp.Body)
fmt.Println("GET request succeeded:", iri.String(), req.Header, resp.StatusCode, resp.Status, "\n", formatJSON(responseData))
responseText := string(responseData)
return responseText, nil
}