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{} lastItem string privateKey crypto.PrivateKey publicKeyID string } type ActorToSave struct { Name string Followers map[string]interface{} LastItem string } 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{}), lastItem: "", 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.LastItem != "" { actor.lastItem = savedActor.LastItem } 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, LastItem: a.lastItem, } 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) error { 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 errors.New("failed to create request to fetch article") } req.Header.Add("User-Agent", fmt.Sprintf("%s %s", libName, version)) req.Header.Add("Accept", ContentTypeAs2) resp, err := http.DefaultClient.Do(req) if err != nil || !isSuccess(resp.StatusCode) { return errors.New("failed to fetch article") } respBody, err := ioutil.ReadAll(resp.Body) defer func() { _ = resp.Body.Close() }() if err != nil { return errors.New("failed to read response from fetched article") } err = json.Unmarshal(respBody, &article) if err != nil { return errors.New("failed to unmarshal fetched article") } create["object"] = article go a.sendToFollowers(create) return nil } // 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 := http.DefaultClient.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 { if _, ok := a.followers[iri]; ok { delete(a.followers, iri) fmt.Println("Removed follower: ", iri) return a.save() } fmt.Println(iri, "is not following") return nil } // send to followers sends a batch of http posts to each one of the followers func (a *Actor) sendToFollowers(activity map[string]interface{}) { recipients := make([]string, len(a.followers)) i := 0 for _, inbox := range a.followers { recipients[i] = inbox.(string) i++ } for _, v := range recipients { _ = a.signedHTTPPost(activity, v) } 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) fmt.Println("New follow request:", newFollower) // check we aren't following ourselves if newFollower == follow["object"] { // actor and object are equal return } follower, err := NewRemoteActor(newFollower) if err != nil { // Couldn't retrieve remote actor info fmt.Println("Failed to retrieve remote actor info:", newFollower) 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() { err = a.signedHTTPPost(accept, follower.inbox) if err != nil { fmt.Println("Failed to accept:", follower.iri) fmt.Println(err.Error()) } else { fmt.Println("Accepted:", follower.iri) } }() }