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" "sync" "time" ) type Actor struct { Name, iri, feed string followers map[string]interface{} privateKey crypto.PrivateKey publicKeyID string postSigner httpsig.Signer postSignMutex *sync.Mutex } type ActorToSave struct { Name string Followers map[string]interface{} } 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") } postSigner, _, _ := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, "SHA-256", []string{"(request-target)", "date", "host", "digest"}, httpsig.Signature) actor := Actor{ Name: name, iri: iri, feed: feed, followers: make(map[string]interface{}), privateKey: privateKey, publicKeyID: iri + "#main-key", postSigner: postSigner, postSignMutex: &sync.Mutex{}, } jsonFile := storage + slash + "actors" + slash + name + slash + name + ".json" fileHandle, err := os.Open(jsonFile) defer func() { _ = fileHandle.Close() }() if os.IsNotExist(err) { // File doesn't exist, maybe it's a new actor return actor, nil } savedActor := ActorToSave{} err = json.NewDecoder(fileHandle).Decode(&savedActor) if err != nil { // Ignore error, but return actor return actor, nil } if savedActor.Followers != nil { actor.followers = savedActor.Followers } 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, } f, err := os.OpenFile(storage+slash+"actors"+slash+a.Name+slash+a.Name+".json", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) defer func() { _ = f.Close() }() if err != nil { return err } jsonEncoder := json.NewEncoder(f) jsonEncoder.SetIndent("", "\t") err = jsonEncoder.Encode(actorToSave) 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") } err = json.NewDecoder(resp.Body).Decode(&article) _ = resp.Body.Close() if err != nil { return errors.New("failed to decode fetched article") } // create["object"] = url create["object"] = article a.sendToFollowers(create) // Boost article if it contains "inReplyTo" if article["inReplyTo"] != nil { announce := make(map[string]interface{}) announce["@context"] = context() announce["id"] = url + "#Announce" announce["type"] = "Announce" announce["object"] = url announce["actor"] = a.iri announce["to"] = []string{"https://www.w3.org/ns/activitystreams#Public"} announce["published"] = article["published"] a.sendToFollowers(announce) } // Send update event if it contains "updated" and "updated" != "published" if article["updated"] != nil && article["published"] != nil && article["updated"] != article["published"] { update := make(map[string]interface{}) update["@context"] = context() update["type"] = "Update" update["object"] = url update["actor"] = a.iri a.sendToFollowers(update) } 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 } 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 } a.postSignMutex.Lock() 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 = a.postSigner.SignRequest(a.privateKey, a.publicKeyID, req, byteCopy) a.postSignMutex.Unlock() 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) return a.save() } 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{}) { for _, follower := range a.followers { inbox := follower.(string) go func() { _ = a.signedHTTPPost(activity, inbox) }() } 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" 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) if telegramBot != nil { _ = telegramBot.Post(follower.iri + " followed") } } }