package main import ( "bytes" "crypto" "crypto/x509" "encoding/json" "encoding/pem" "errors" "fmt" "github.com/dchest/uniuri" "github.com/go-fed/httpsig" "io/ioutil" "net/http" "net/url" "os" "strings" "time" ) type Actor struct { Name, iri, feed string followers map[string]interface{} sentItems map[string]bool privateKey crypto.PrivateKey publicKeyID string } type ActorToSave struct { Name string Followers map[string]interface{} SentItems map[string]bool } func GetActor(name, iri, feed, pk string) (Actor, error) { // make sure users can't read hard drive if strings.ContainsAny(name, "./ ") { return Actor{}, errors.New("illegal characters in actor name") } // populate actor pkfile, err := ioutil.ReadFile(pk) if err != nil { return Actor{}, errors.New("failed to read private key file") } privateKeyDecoded, rest := pem.Decode(pkfile) if privateKeyDecoded == nil { fmt.Println(rest) return Actor{}, errors.New("failed to decode the private key PEM") } privateKey, err := x509.ParsePKCS1PrivateKey(privateKeyDecoded.Bytes) if err != nil { return Actor{}, errors.New("failed to parse the private key") } actor := Actor{ Name: name, iri: iri, feed: feed, followers: make(map[string]interface{}), sentItems: make(map[string]bool), privateKey: privateKey, publicKeyID: iri + "#main-key", } jsonFile := storage + slash + "actors" + slash + name + slash + name + ".json" fileHandle, err := os.Open(jsonFile) if os.IsNotExist(err) { // File doesn't exist, maybe it's a new actor return actor, nil } byteValue, err := ioutil.ReadAll(fileHandle) if err != nil { // Ignore error, but return actor return actor, nil } savedActor := ActorToSave{} err = json.Unmarshal(byteValue, &savedActor) if err != nil { // Ignore error, but return actor return actor, nil } if savedActor.Followers != nil { actor.followers = savedActor.Followers } if savedActor.SentItems != nil { actor.sentItems = savedActor.SentItems } return actor, nil } // save the actor to file func (a *Actor) save() error { // check if directory already exists dir := storage + slash + "actors" + slash + a.Name if _, err := os.Stat(dir); os.IsNotExist(err) { _ = os.MkdirAll(dir, 0755) } actorToSave := ActorToSave{ Name: a.Name, Followers: a.followers, SentItems: a.sentItems, } actorJSON, err := json.MarshalIndent(actorToSave, "", "\t") if err != nil { return err } err = ioutil.WriteFile(storage+slash+"actors"+slash+a.Name+slash+a.Name+".json", actorJSON, 0644) if err != nil { return err } return nil } func (a *Actor) PostArticle(url string) { create := make(map[string]interface{}) note := make(map[string]interface{}) create["@context"] = context() create["actor"] = a.iri create["id"] = url create["published"] = note["published"] create["type"] = "Create" article := make(map[string]interface{}) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return } req.Header.Add("User-Agent", fmt.Sprintf("%s %s", libName, version)) req.Header.Add("Accept", ContentTypeAs2) resp, err := client.Do(req) if err != nil || isSuccess(resp.StatusCode) { fmt.Println("Failed to fetch article") return } respBody, err := ioutil.ReadAll(resp.Body) defer func() { _ = resp.Body.Close() }() if err != nil { fmt.Println("Failed to read response from fetched article") return } err = json.Unmarshal(respBody, &article) if err != nil { fmt.Println("Failed to unmarshal fetched article") return } create["object"] = article go func() { _ = a.sendToFollowers(create) }() } // signedHTTPPost performs an HTTP post on behalf of Actor with the // request-target, date, host and digest headers signed // with the actor's private key. func (a *Actor) signedHTTPPost(content map[string]interface{}, to string) (err error) { b, err := json.Marshal(content) if err != nil { return } postSigner, _, _ := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, "SHA-256", []string{"(request-target)", "date", "host", "digest"}, httpsig.Signature) byteCopy := make([]byte, len(b)) copy(byteCopy, b) buf := bytes.NewBuffer(byteCopy) req, err := http.NewRequest(http.MethodPost, to, buf) if err != nil { return } iri, err := url.Parse(to) if err != nil { 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", libName, version)) req.Header.Add("Host", iri.Host) req.Header.Add("Accept", "application/activity+json; charset=utf-8") req.Header.Add("Content-Type", "application/activity+json; charset=utf-8") err = postSigner.SignRequest(a.privateKey, a.publicKeyID, req, byteCopy) if err != nil { return } resp, err := client.Do(req) if err != nil { return } if !isSuccess(resp.StatusCode) { err = errors.New("post request failed") return } return } // NewFollower records a new follower to the actor file func (a *Actor) NewFollower(iri string, inbox string) error { a.followers[iri] = inbox return a.save() } func (a *Actor) RemoveFollower(iri string) error { delete(a.followers, iri) return a.save() } // batchSend sends a batch of http posts to a list of recipients func (a *Actor) batchSend(activity map[string]interface{}, recipients []string) (err error) { for _, v := range recipients { _ = a.signedHTTPPost(activity, v) } return } // send to followers sends a batch of http posts to each one of the followers func (a *Actor) sendToFollowers(activity map[string]interface{}) (err error) { recipients := make([]string, len(a.followers)) i := 0 for _, inbox := range a.followers { recipients[i] = inbox.(string) i++ } _ = a.batchSend(activity, recipients) return } func (a *Actor) newID() (hash string, url string) { hash = uniuri.New() return hash, a.iri + hash } // Accept a follow request func (a *Actor) Accept(follow map[string]interface{}) { // it's a follow, write it down newFollower := follow["actor"].(string) // check we aren't following ourselves if newFollower == follow["object"] { // actor and object are equal return } follower, err := NewRemoteActor(follow["actor"].(string)) if err != nil { // Couldn't retrieve remote actor info return } // Add or update follower _ = a.NewFollower(newFollower, follower.inbox) // remove @context from the inner activity delete(follow, "@context") accept := make(map[string]interface{}) accept["@context"] = "https://www.w3.org/ns/activitystreams" accept["to"] = follow["actor"] _, accept["id"] = a.newID() accept["actor"] = a.iri accept["object"] = follow accept["type"] = "Accept" go func() { _ = a.signedHTTPPost(accept, follower.inbox) }() }