commit
f67906c96a
|
@ -0,0 +1,2 @@
|
||||||
|
*.snip
|
||||||
|
storage
|
|
@ -0,0 +1,75 @@
|
||||||
|
[ ] Load outbox of users and parse latest posts
|
||||||
|
[ ] Write these posts to local file
|
||||||
|
(normally we would need to sort by time but this is a
|
||||||
|
temporary solution until we really follow the actors
|
||||||
|
and get notifications in timely manner)
|
||||||
|
[ ] Follow users
|
||||||
|
[ ] Announcements (read up how boost json looks like)
|
||||||
|
[ ] Federate the post to our followers (hardcoded for now)
|
||||||
|
[ ] Actor should have pubActor not the other way around
|
||||||
|
[ ] Handle more than one local actors
|
||||||
|
[ ] Fix the json to host those multiple actors
|
||||||
|
[ ] Fix the json unmarshalling code to read multiple actors
|
||||||
|
[ ] Handle the /actor endpoint
|
||||||
|
[ ] Create configuration file
|
||||||
|
[ ] Implement database backend
|
||||||
|
[ ] Create a file with the actors we have, their following
|
||||||
|
and their followers.
|
||||||
|
[ ] `MakeActor` should create a file with that actor.
|
||||||
|
[ ] Implement `LoadActor`
|
||||||
|
[ ] All but `main.go` should run LoadActor instead of MakeActor
|
||||||
|
(Actually nobody should run LoadActor except GetActor)
|
||||||
|
[ ] `actor.Follow` should write the new following to file
|
||||||
|
[ ] Handle being followed
|
||||||
|
[ ] When followed, the handler should write the new follower to file
|
||||||
|
[ ] Make sure we send our boosts to all our followers
|
||||||
|
Code is there but it works sometimes (I hate when this happens)
|
||||||
|
[ ] Check why I get calls to get with an id that consists only of an actor's name
|
||||||
|
[ ] Implement `db.followers` and `db.following`
|
||||||
|
[ ] Write all the announcements (boosts) to the database to
|
||||||
|
their correct actors
|
||||||
|
[ ] Check if we are already following users
|
||||||
|
[ ] On GetOutbox read the database and present a list of the
|
||||||
|
last posts.
|
||||||
|
[ ] Make OS-independent (mosty directory separators)
|
||||||
|
[ ] Create outbox.json programmatically
|
||||||
|
[ ] Make storage configurable (search for "storage" in project)
|
||||||
|
[ ] Check if we're boosting only stuff from actors we follow, not whatever comes
|
||||||
|
through in our inbox
|
||||||
|
[ ] Boost not only articles but other things too
|
||||||
|
[ ] Handle post uri's
|
||||||
|
[ ] Sanitize input, never allow slashes or dots
|
||||||
|
[ ] Add summary to actors.json
|
||||||
|
[ ] Check local actor names for characters illegal for filenames and ban them
|
||||||
|
[ ] Create debug flag
|
||||||
|
[ ] Write to following only upon accept
|
||||||
|
(waiting to actually get an accept so that I can test this)
|
||||||
|
[ ] Implement webfinger
|
||||||
|
[ ] Make sure masto finds signature
|
||||||
|
[ ] Implement Unfollow
|
||||||
|
[ ] Implement accept (accept when other follow us)
|
||||||
|
(done but can't test it pending http signatures)
|
||||||
|
Works in pleroma/pixelfed not working on masto
|
||||||
|
(nothing works on masto)
|
||||||
|
[ ] Implement nodeinfo and statistics
|
||||||
|
[ ] Accept even if already follows us
|
||||||
|
[ ] Implement db.Update
|
||||||
|
[ ] Implement db.Delete
|
||||||
|
[ ] Handle paging
|
||||||
|
[ ] Handle http signatures
|
||||||
|
masto can't find the signature
|
||||||
|
[ ] Verify http signatures
|
||||||
|
[ ] Why doesn't our outbox being fetched by others?
|
||||||
|
[ ] Refactor, comment and clean up
|
||||||
|
[ ] Make sure we never show <actor>.json to the public
|
||||||
|
[ ] Split to pherephone and activityServe
|
||||||
|
[ ] Decide what's to be done with actors removed from `actors.json`.
|
||||||
|
[ ] Remove them?
|
||||||
|
[ ] Leave them read-only?
|
||||||
|
[ ] Leave them as is?
|
||||||
|
[ ] Check if an early failure in announcing posts causes a problem to the following ones
|
||||||
|
[ ] Handle followers and following uri's
|
||||||
|
[ ] Do I care about the inbox?
|
||||||
|
[ ] Maybe look at implementing lock files?
|
||||||
|
[ ] Check if it's worth it to reuse pubActor instead of creating
|
||||||
|
a new one every time
|
|
@ -0,0 +1,435 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
package activityserve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gologme/log"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupHTTP starts an http server with all the required handlers
|
||||||
|
func Serve() {
|
||||||
|
|
||||||
|
var webfingerHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("content-type", "application/jrd+json; charset=utf-8")
|
||||||
|
account := r.URL.Query().Get("resource") // should be something like acct:user@example.com
|
||||||
|
account = strings.Replace(account, "acct:", "", 1) // remove acct:
|
||||||
|
server := strings.Split(baseURL, "://")[1] // remove protocol from baseURL. Should get example.com
|
||||||
|
server = strings.TrimSuffix(server, "/") // remove protocol from baseURL. Should get example.com
|
||||||
|
account = strings.Replace(account, "@"+server, "", 1) // remove server from handle. Should get user
|
||||||
|
actor, err := LoadActor(account)
|
||||||
|
// error out if this actor does not exist
|
||||||
|
if err != nil {
|
||||||
|
log.Info("No such actor")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprintf(w, "404 - actor not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// response := `{"subject":"acct:` + actor.name + `@` + server + `","aliases":["` + baseURL + actor.name + `","` + baseURL + actor.name + `"],"links":[{"href":"` + baseURL + `","type":"text/html","rel":"https://webfinger.net/rel/profile-page"},{"href":"` + baseURL + actor.name + `","type":"application/activity+json","rel":"self"}]}`
|
||||||
|
|
||||||
|
responseMap := make(map[string]interface{})
|
||||||
|
|
||||||
|
responseMap["subject"] = "acct:" + actor.name + "@" + server
|
||||||
|
links := make(map[string]string)
|
||||||
|
links["rel"] = "self"
|
||||||
|
links["type"] = "application/activity+json"
|
||||||
|
links["href"] = baseURL + actor.name
|
||||||
|
responseMap["links"] = links
|
||||||
|
|
||||||
|
response, err := json.Marshal(responseMap)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("problem creating the webfinger response json")
|
||||||
|
}
|
||||||
|
log.Info(string(response))
|
||||||
|
w.Write([]byte(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
var actorHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("content-type", "application/activity+json; charset=utf-8")
|
||||||
|
log.Info("Remote server just fetched our /actor endpoint")
|
||||||
|
username := mux.Vars(r)["actor"]
|
||||||
|
log.Info(username)
|
||||||
|
if username == ".well-known" || username == "favicon.ico" {
|
||||||
|
log.Info("well-known, skipping...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
actor, err := LoadActor(username)
|
||||||
|
// error out if this actor does not exist (or there are dots or slashes in his name)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprintf(w, "404 - page not found")
|
||||||
|
log.Info("Can't create local actor")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, actor.whoAmI())
|
||||||
|
log.Info(r.RemoteAddr)
|
||||||
|
log.Info(r.Body)
|
||||||
|
log.Info(r.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
var outboxHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("content-type", "application/activity+json; charset=utf-8")
|
||||||
|
username := mux.Vars(r)["actor"] // get the needed actor from the muxer (url variable {actor} below)
|
||||||
|
actor, err := LoadActor(username) // load the actor from disk
|
||||||
|
if err != nil { // either actor requested has illegal characters or
|
||||||
|
log.Info("Can't load local actor") // we don't have such actor
|
||||||
|
fmt.Fprintf(w, "404 - page not found")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var response string
|
||||||
|
if r.URL.Query().Get("page") == "" {
|
||||||
|
//TODO fix total items
|
||||||
|
response = `{
|
||||||
|
"@context" : "https://www.w3.org/ns/activitystreams",
|
||||||
|
"first" : "` + baseURL + actor.name + `/outbox?page=true",
|
||||||
|
"id" : "` + baseURL + actor.name + `/outbox",
|
||||||
|
"last" : "` + baseURL + actor.name + `/outbox?min_id=0&page=true",
|
||||||
|
"totalItems" : 10,
|
||||||
|
"type" : "OrderedCollection"
|
||||||
|
}`
|
||||||
|
} else {
|
||||||
|
content := "Hello, World!"
|
||||||
|
id := "asfdasdf"
|
||||||
|
response = `
|
||||||
|
{
|
||||||
|
"@context" : "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id" : "` + baseURL + actor.name + `/outbox?min_id=0&page=true",
|
||||||
|
"next" : "` + baseURL + actor.name + `/outbox?max_id=99524642494530460&page=true",
|
||||||
|
"orderedItems" :[
|
||||||
|
{
|
||||||
|
"actor" : "https://` + baseURL + actor.name + `",
|
||||||
|
"cc" : [
|
||||||
|
"https://` + baseURL + actor.name + `/followers"
|
||||||
|
],
|
||||||
|
"id" : "https://` + baseURL + actor.name + `/` + id + `",
|
||||||
|
"object" : {
|
||||||
|
"attributedTo" : "https://` + baseURL + actor.name + `",
|
||||||
|
"cc" : [
|
||||||
|
"https://` + baseURL + actor.name + `/followers"
|
||||||
|
],
|
||||||
|
"content" : "` + content + `",
|
||||||
|
"id" : "https://` + baseURL + actor.name + `/` + id + `",
|
||||||
|
"inReplyTo" : null,
|
||||||
|
"published" : "2019-08-26T16:25:26Z",
|
||||||
|
"to" : [
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
],
|
||||||
|
"type" : "Note",
|
||||||
|
"url" : "https://` + baseURL + actor.name + `/` + id + `"
|
||||||
|
},
|
||||||
|
"published" : "2019-08-26T16:25:26Z",
|
||||||
|
"to" : [
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
],
|
||||||
|
"type" : "Create"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"partOf" : "` + baseURL + actor.name + `/outbox",
|
||||||
|
"prev" : "` + baseURL + actor.name + `/outbox?min_id=99982453036184436&page=true",
|
||||||
|
"type" : "OrderedCollectionPage"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
w.Write([]byte(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the handlers to a HTTP server
|
||||||
|
gorilla := mux.NewRouter()
|
||||||
|
gorilla.HandleFunc("/.well-known/webfinger", webfingerHandler)
|
||||||
|
gorilla.HandleFunc("/{actor}/outbox", outboxHandler)
|
||||||
|
gorilla.HandleFunc("/{actor}/outbox/", outboxHandler)
|
||||||
|
// gorilla.HandleFunc("/{actor}/inbox", inboxHandler)
|
||||||
|
// gorilla.HandleFunc("/{actor}/inbox/", inboxHandler)
|
||||||
|
gorilla.HandleFunc("/{actor}/", actorHandler)
|
||||||
|
gorilla.HandleFunc("/{actor}", actorHandler)
|
||||||
|
// gorilla.HandleFunc("/{actor}/post/{hash}", postHandler)
|
||||||
|
http.Handle("/", gorilla)
|
||||||
|
|
||||||
|
log.Fatal(http.ListenAndServe(":8081", nil))
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package activityserve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gologme/log"
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var slash = string(os.PathSeparator)
|
||||||
|
var baseURL = "http://example.com/"
|
||||||
|
var storage = "storage"
|
||||||
|
var userAgent = "activityserve"
|
||||||
|
|
||||||
|
const libName = "activityserve"
|
||||||
|
const version = "0.99"
|
||||||
|
|
||||||
|
var client = http.Client{}
|
||||||
|
|
||||||
|
// Setup sets our environment up
|
||||||
|
func Setup(configurationFile string, debug bool) {
|
||||||
|
// read configuration file (config.ini)
|
||||||
|
|
||||||
|
if configurationFile == "" {
|
||||||
|
configurationFile = "config.ini"
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := ini.Load("config.ini")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Fail to read file: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load base url from configuration file
|
||||||
|
baseURL = cfg.Section("general").Key("baseURL").String()
|
||||||
|
// check if it ends with a / and append one if not
|
||||||
|
if baseURL[len(baseURL)-1:] != "/" {
|
||||||
|
baseURL += "/"
|
||||||
|
}
|
||||||
|
// print it for our users
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Domain Name:", baseURL)
|
||||||
|
|
||||||
|
// Load storage location (only local filesystem supported for now) from config
|
||||||
|
storage = cfg.Section("general").Key("storage").String()
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
fmt.Println("Storage Location:", cwd+slash+storage)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
SetupStorage(storage)
|
||||||
|
|
||||||
|
// Load user agent
|
||||||
|
userAgent = cfg.Section("general").Key("userAgent").String()
|
||||||
|
|
||||||
|
// I prefer long file so that I can click it in the terminal and open it
|
||||||
|
// in the editor above
|
||||||
|
log.SetFlags(log.Llongfile)
|
||||||
|
// log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
log.EnableLevel("warn")
|
||||||
|
// create a logger with levels but without prefixes for easier to read
|
||||||
|
// debug output
|
||||||
|
printer := log.New(os.Stdout, " ", 0)
|
||||||
|
|
||||||
|
if debug == true {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("debug mode on")
|
||||||
|
log.EnableLevel("info")
|
||||||
|
printer.EnableLevel("info")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupStorage creates storage
|
||||||
|
func SetupStorage(storage string) {
|
||||||
|
// prepare storage for foreign activities (activities we store that don't
|
||||||
|
// belong to us)
|
||||||
|
foreignDir := storage + slash + "foreign"
|
||||||
|
if _, err := os.Stat(foreignDir); os.IsNotExist(err) {
|
||||||
|
os.MkdirAll(foreignDir, 0755)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package activityserve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
// "net/url"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
// "time"
|
||||||
|
// "fmt"
|
||||||
|
"github.com/gologme/log"
|
||||||
|
// "github.com/go-fed/httpsig"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isSuccess(code int) bool {
|
||||||
|
return code == http.StatusOK ||
|
||||||
|
code == http.StatusCreated ||
|
||||||
|
code == http.StatusAccepted
|
||||||
|
}
|
||||||
|
|
||||||
|
//PrettyPrint maps
|
||||||
|
func PrettyPrint(themap map[string]interface{}) {
|
||||||
|
b, err := json.MarshalIndent(themap, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Info("error:", err)
|
||||||
|
}
|
||||||
|
log.Print(string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
//PrettyPrintJSON does what it's name says
|
||||||
|
func PrettyPrintJSON(theJSON []byte) {
|
||||||
|
dst := new(bytes.Buffer)
|
||||||
|
json.Indent(dst, theJSON, "", "\t")
|
||||||
|
log.Info(dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatJSON(theJSON []byte) string{
|
||||||
|
dst := new(bytes.Buffer)
|
||||||
|
json.Indent(dst, theJSON, "", "\t")
|
||||||
|
return dst.String()
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
[general]
|
||||||
|
|
||||||
|
baseURL = https://floorb.qwazix.com
|
||||||
|
storage = storage ; can be relative or absolute path
|
||||||
|
userAgent = "pherephone"
|
|
@ -0,0 +1,59 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"flag"
|
||||||
|
// "os"
|
||||||
|
// "strings"
|
||||||
|
|
||||||
|
// "errors"
|
||||||
|
|
||||||
|
// "encoding/json"
|
||||||
|
// "io/ioutil"
|
||||||
|
// "net/http"
|
||||||
|
|
||||||
|
// "net/url"
|
||||||
|
// "context"
|
||||||
|
// "html"
|
||||||
|
|
||||||
|
"github.com/gologme/log"
|
||||||
|
|
||||||
|
// "github.com/go-fed/activity/streams"
|
||||||
|
// "github.com/gorilla/mux"
|
||||||
|
// "gopkg.in/ini.v1"
|
||||||
|
// "github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
"./activityserve"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
// This is here for debugging purposes. I want to be able to easily spot in the terminal
|
||||||
|
// when a single execution starts
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("======================= PHeRePHoNe ==========================")
|
||||||
|
|
||||||
|
// introduce ourselves
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Pherephone follows some accounts and boosts")
|
||||||
|
fmt.Println("whatever they post to our followers. See config.ini ")
|
||||||
|
fmt.Println("for more information and how to set up. ")
|
||||||
|
|
||||||
|
debugFlag := flag.Bool("debug", false, "set to true to get debugging information in the console")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *debugFlag == true {
|
||||||
|
log.EnableLevel("info")
|
||||||
|
}
|
||||||
|
|
||||||
|
activityserve.Setup("config.ini", *debugFlag)
|
||||||
|
|
||||||
|
actor, _ := activityserve.MakeActor("activityserve_test_actor_2", "This is an activityserve test actor", "Service")
|
||||||
|
// actor, _ := activityserve.LoadActor("activityserve_test_actor_2")
|
||||||
|
actor.CreateNote("Hello World!")
|
||||||
|
|
||||||
|
activityserve.Serve()
|
||||||
|
}
|
Loading…
Reference in New Issue