diff --git a/1602787120 Baseline Measurement.csv b/1602787120 Baseline Measurement.csv new file mode 100644 index 0000000..3e61a7b --- /dev/null +++ b/1602787120 Baseline Measurement.csv @@ -0,0 +1,11 @@ +Iteration,Start,End +1,1602787120,1602787240 +2,1602787240,1602787360 +3,1602787360,1602787480 +4,1602787480,1602787600 +5,1602787600,1602787720 +6,1602787720,1602787840 +7,1602787840,1602787961 +8,1602787961,1602788081 +9,1602788081,1602788201 +10,1602788201,1602788321 diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..313e8aa --- /dev/null +++ b/config/config.go @@ -0,0 +1,36 @@ +package config + +import ( + "time" +) + +var ServerName string = "Hoernschen's ActivityPub Server" +var Version string = "0.1" + +var Homeserver string +var Port string + +var PrivateKey []byte +var PublicKey []byte +var KeyId string +var VerifyKeys map[string]map[string][]byte + +var Packetloss int +var UnavailableTill int64 +var Consensus bool +var AuthentificationCheck bool +var Signing bool +var Encryption bool +var HttpString string + +//var BackoffPolicy *backoff.Exponential + +func SetDefaultParams() { + Packetloss = 0.0 + UnavailableTill = time.Now().Unix() + Consensus = true + AuthentificationCheck = true + Signing = true + Encryption = true + HttpString = "https" +} diff --git a/config/configController.go b/config/configController.go new file mode 100644 index 0000000..74029ce --- /dev/null +++ b/config/configController.go @@ -0,0 +1,69 @@ +package config + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + + "git.nutfactory.org/hoernschen/ActivityPub/utils/database" +) + +type SetParamBody struct { + Packetloss int `json:"packetloss,omitempty"` + UnavailableTill int64 `json:"unavailableTill,omitempty"` + Consensus bool `json:"consensus,omitempty"` + AuthentificationCheck bool `json:"authentificationCheck,omitempty"` + Signing bool `json:"signing,omitempty"` + Encryption bool `json:"encryption,omitempty"` +} + +func SetParams(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + request := SetParamBody{} + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&request) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Could not parse JSON: %s", err)); err != nil { + panic(err) + } + return + } + + Packetloss = request.Packetloss + UnavailableTill = request.UnavailableTill + Consensus = request.Consensus + AuthentificationCheck = request.AuthentificationCheck + Signing = request.Signing + Encryption = request.Signing + HttpString = "https" + if !Encryption { + HttpString = "http" + } + + w.WriteHeader(http.StatusOK) +} + +func Reset(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + /* + if err := device.InitServerSigningKey(); err != nil { + log.Fatal(err) + } + config.VerifyKeys = make(map[string]map[string][]byte) + */ + + database.DB.Close() + os.Remove("sqlite.db") + + if err := database.InitDB("sqlite.db"); err != nil { + log.Fatal(err) + } + + SetDefaultParams() + + w.WriteHeader(http.StatusOK) +} diff --git a/entities/activity/activity.go b/entities/activity/activity.go new file mode 100644 index 0000000..8417476 --- /dev/null +++ b/entities/activity/activity.go @@ -0,0 +1,13 @@ +package activity + +import "git.nutfactory.org/hoernschen/ActivityPub/entities/object" + +type Activity struct { + Context string `json:"@context,omitempty"` + Id string `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Actor string `json:"actor,omitempty"` + Object *object.Object `json:"object,omitempty"` + Published int64 `json:"published,omitempty"` + To string `json:"to,omitempty"` +} diff --git a/entities/activity/activityController.go b/entities/activity/activityController.go new file mode 100644 index 0000000..38769c7 --- /dev/null +++ b/entities/activity/activityController.go @@ -0,0 +1,29 @@ +package activity + +import ( + "time" + + "git.nutfactory.org/hoernschen/ActivityPub/entities/object" + "git.nutfactory.org/hoernschen/ActivityPub/utils" +) + +func New(id string, actorOfActivity string, objectOfActivity *object.Object, userId string) (newActivity *Activity) { + published := objectOfActivity.Published + to := objectOfActivity.To + if published == 0 { + published = time.Now().Unix() + } + if to == "" { + to = utils.GenerateFollowersUrl(userId) + } + newActivity = &Activity{ + Context: utils.GetDefaultContext(), + Id: id, + Type: "Create", + Actor: actorOfActivity, + Object: objectOfActivity, + Published: published, + To: to, + } + return +} diff --git a/entities/actor/actor.go b/entities/actor/actor.go new file mode 100644 index 0000000..a1d778a --- /dev/null +++ b/entities/actor/actor.go @@ -0,0 +1,15 @@ +package actor + +type Actor struct { + Context string `json:"@context,omitempty"` + Id string `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferredUsername,omitempty"` + Summary string `json:"summary,omitempty"` + Inbox string `json:"inbox,omitempty"` + Outbox string `json:"outbox,omitempty"` + Followers string `json:"followers,omitempty"` + Following string `json:"following,omitempty"` + Liked string `json:"liked,omitempty"` +} diff --git a/entities/actor/actorController.go b/entities/actor/actorController.go new file mode 100644 index 0000000..b89b729 --- /dev/null +++ b/entities/actor/actorController.go @@ -0,0 +1,75 @@ +package actor + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "git.nutfactory.org/hoernschen/ActivityPub/config" + "git.nutfactory.org/hoernschen/ActivityPub/utils" +) + +func New(userId string) (newActor *Actor) { + id := utils.GenerateProfileUrl(userId) + newActor = GenerateProfile(id, userId) + return +} + +func GenerateProfile(id string, userId string) (profile *Actor) { + profile = &Actor{ + Context: utils.GetDefaultContext(), + Id: id, + Name: userId, + PreferredUsername: userId, + Type: "Person", + Inbox: utils.GenerateInboxUrl(userId), + Outbox: utils.GenerateOutboxUrl(userId), + Followers: utils.GenerateFollowersUrl(userId), + Following: utils.GenerateFollowingUrl(userId), + Liked: utils.GenerateLikedUrl(userId), + } + return +} + +func GetProfileHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", utils.GetContentTypeString()) + foundActor, err := ReadActor(fmt.Sprintf("%s://%s%s", config.HttpString, config.Homeserver, r.URL.RequestURI())) + if foundActor == nil || err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Actor not found: %s", err)); err != nil { + panic(err) + } + return + } + profile := GenerateProfile(foundActor.Id, foundActor.Name) + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(profile); err != nil { + panic(err) + } +} + +func GetProfile(actorId string) (err error, profile *Actor) { + client := &http.Client{Timeout: 2 * time.Second} + req, err := http.NewRequest(http.MethodGet, actorId, bytes.NewBuffer(nil)) + if err != nil { + return + } + req.Header["Content-Type"] = []string{utils.GetContentTypeString()} + r, err := client.Do(req) + if err != nil { + return + } + if r.StatusCode != http.StatusOK { + err = utils.HandleHTTPError(r) + return + } else { + decoder := json.NewDecoder(r.Body) + err = decoder.Decode(&profile) + if err != nil { + return + } + } + return +} diff --git a/entities/actor/actorDatabaseConnector.go b/entities/actor/actorDatabaseConnector.go new file mode 100644 index 0000000..c4f96f5 --- /dev/null +++ b/entities/actor/actorDatabaseConnector.go @@ -0,0 +1,76 @@ +package actor + +import ( + "fmt" + + "git.nutfactory.org/hoernschen/ActivityPub/utils/database" +) + +func CreateActor(actor *Actor) (err error) { + sqlStmt := fmt.Sprintf(`INSERT INTO actor + (id, type, name, preferredUsername, summary, inbox, outbox, followers, following, liked) + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) + + tx, err := database.DB.Begin() + if err != nil { + return + } + stmt, err := tx.Prepare(sqlStmt) + if err != nil { + return + } + defer stmt.Close() + _, err = stmt.Exec( + actor.Id, + actor.Type, + actor.Name, + actor.PreferredUsername, + actor.Summary, + actor.Inbox, + actor.Outbox, + actor.Followers, + actor.Following, + actor.Liked, + ) + if err != nil { + tx.Rollback() + return + } + tx.Commit() + return +} + +func ReadActor(id string) (foundActor *Actor, err error) { + queryStmt := fmt.Sprintf(`SELECT id, type, name, preferredUsername, summary, inbox, outbox, followers, following, liked + FROM actor + WHERE id = '%s'`, id) + + rows, err := database.DB.Query(queryStmt) + if err != nil { + return + } + + defer rows.Close() + + if rows.Next() { + foundActor = &Actor{} + err = rows.Scan( + &foundActor.Id, + &foundActor.Type, + &foundActor.Name, + &foundActor.PreferredUsername, + &foundActor.Summary, + &foundActor.Inbox, + &foundActor.Outbox, + &foundActor.Followers, + &foundActor.Following, + &foundActor.Liked, + ) + if err != nil { + return + } + } + + return +} diff --git a/entities/collection/collection.go b/entities/collection/collection.go new file mode 100644 index 0000000..c8d9616 --- /dev/null +++ b/entities/collection/collection.go @@ -0,0 +1,5 @@ +package collection + +type OutboxResponse struct { + Location string `json:"Location,omitempty"` +} diff --git a/entities/collection/collectionController.go b/entities/collection/collectionController.go new file mode 100644 index 0000000..1d4c8ce --- /dev/null +++ b/entities/collection/collectionController.go @@ -0,0 +1,350 @@ +package collection + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "log" + "math/rand" + "net/http" + "strings" + "time" + + "git.nutfactory.org/hoernschen/ActivityPub/config" + "git.nutfactory.org/hoernschen/ActivityPub/entities/activity" + "git.nutfactory.org/hoernschen/ActivityPub/entities/actor" + "git.nutfactory.org/hoernschen/ActivityPub/entities/object" + "git.nutfactory.org/hoernschen/ActivityPub/entities/user" + "git.nutfactory.org/hoernschen/ActivityPub/utils" + + "github.com/cenkalti/backoff/v4" + "github.com/gorilla/mux" +) + +func PostInboxHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", utils.GetContentTypeString()) + newActivity := &activity.Activity{} + err := utils.CheckRequest(r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(err); err != nil { + panic(err) + } + return + } + token, err := utils.GetAccessToken(r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(err); err != nil { + panic(err) + } + return + } + if token == "" { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode("Missing Token"); err != nil { + panic(err) + } + return + } + vars := mux.Vars(r) + actorName := vars["actorName"] + if actorName == "" { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode("Missing Actor"); err != nil { + panic(err) + } + return + } + decoder := json.NewDecoder(r.Body) + err = decoder.Decode(&newActivity) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Could not parse JSON: %s", err)); err != nil { + panic(err) + } + return + } + + if newActivity.Type == "Follow" { + err = CreateCollectionObject(utils.GenerateFollowersUrl(actorName), newActivity.Actor) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Database Error Creating Collection Object: %s", err)); err != nil { + panic(err) + } + return + } + log.Printf("%s follows %s", newActivity.Actor, newActivity.To) + } else if newActivity.Type == "Create" { + foundObject, err := object.ReadObject(newActivity.Object.Id) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Database Error Reading Object: %s", err)); err != nil { + panic(err) + } + return + } + if foundObject == nil { + _ = object.CreateObject(newActivity.Object) + /* + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Database Error Creating Object: %s", err)); err != nil { + panic(err) + } + return + } + */ + } + _ = CreateCollectionObject(utils.GenerateInboxUrl(actorName), newActivity.Object.Id) + /* + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Database Error Creating Collection Object: %s", err)); err != nil { + panic(err) + } + return + } + */ + log.Printf("%s to %s: %s", newActivity.Actor, newActivity.To, newActivity.Object.Content) + } else { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode("Unsupported Activity Type"); err != nil { + panic(err) + } + return + } + + w.WriteHeader(http.StatusOK) +} + +func PostOutboxHandler(w http.ResponseWriter, r *http.Request) { + packetLossNumber := rand.Intn(100) + if packetLossNumber > config.Packetloss { + w.Header().Set("Content-Type", utils.GetContentTypeString()) + newActivity := &activity.Activity{} + err := utils.CheckRequest(r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(err); err != nil { + panic(err) + } + return + } + token, err := utils.GetAccessToken(r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(err); err != nil { + panic(err) + } + return + } + foundUser, err := user.ReadUserFromToken(token) + if err != nil || foundUser == nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Unknown Token: %s", err)); err != nil { + panic(err) + } + return + } + buf := new(bytes.Buffer) + buf.ReadFrom(r.Body) + err = json.Unmarshal(buf.Bytes(), newActivity) + if err != nil || newActivity.Type == "Note" { + postedObject := &object.Object{} + err = json.Unmarshal(buf.Bytes(), postedObject) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Could not parse JSON: %s")); err != nil { + panic(err) + } + return + } + var newObject *object.Object + if postedObject.Id == "" { + err, newObject = object.New( + postedObject.Type, + postedObject.AttributedTo, + postedObject.Content, + postedObject.Published, + postedObject.To, + foundUser.Id, + ) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Error Creating Object: %s")); err != nil { + panic(err) + } + return + } + err = object.CreateObject(newObject) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Database Error Creating Object: %s")); err != nil { + panic(err) + } + return + } + } else { + newObject = postedObject + } + newActivity = activity.New( + newObject.Id, + newObject.AttributedTo, + newObject, + foundUser.Id, + ) + } + + var recipients []string + if newActivity.Type == "Follow" { + err = CreateCollectionObject(utils.GenerateFollowingUrl(foundUser.Id), newActivity.To) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Database Error Creating Collection Object: %s")); err != nil { + panic(err) + } + return + } + recipients = append(recipients, newActivity.To) + } else if newActivity.Type == "Create" { + err = CreateCollectionObject(utils.GenerateOutboxUrl(foundUser.Id), newActivity.Id) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Database Error Creating Collection Object: %s")); err != nil { + panic(err) + } + return + } + err, recipients = GetReciepientsForActivity(newActivity) + } + + for _, recipient := range recipients { + log.Printf("Send Activity to Recipient %s", recipient) + + operation := func() error { + return SendActivity(newActivity, recipient, token) + } + notify := func(err error, duration time.Duration) { + log.Printf("Error Sending Activity, retrying in %ss: %s", duration/1000000000, err) + } + backoff.RetryNotify(operation, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 16), notify) + + //go retryActivity(newActivity, recipient, token) + } + + response := OutboxResponse{ + Location: newActivity.Id, + } + + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(response); err != nil { + panic(err) + } + } +} + +/* +func retryActivity(activityToSend *activity.Activity, recipient string, token string) (err error) { + b, cancel := config.BackoffPolicy.Start(context.Background()) + defer cancel() + + for backoff.Continue(b) { + err := SendActivity(activityToSend, recipient, token) + if err == nil { + return nil + } + } + err = errors.New("Not able to send activity") + return +} +*/ + +func GetReciepientsForActivity(activityToSend *activity.Activity) (err error, recipientsWithoutDuplicates []string) { + recipients := []string{} + if strings.Contains(activityToSend.To, config.Homeserver) { + var foundActor *actor.Actor + foundActor, err = actor.ReadActor(activityToSend.To) + if err != nil { + return + } + if foundActor == nil { + var foundCollectionObjects []string + foundCollectionObjects, err = ReadCollectionObjects(activityToSend.To) + if err != nil { + return + } + if len(foundCollectionObjects) <= 0 { + err = errors.New("No Recipients") + return + } + recipients = append(recipients, foundCollectionObjects...) + } else { + recipients = append(recipients, foundActor.Id) + } + } else { + // Not Implemented + } + recipientsWithoutDuplicates = utils.RemoveDuplicates(recipients) + return +} + +func SendActivity(activityToSend *activity.Activity, recipient string, token string) (err error) { + if strings.Contains(recipient, config.Homeserver) { + if activityToSend.Type == "Follow" { + followers := fmt.Sprintf("%sfollowers/", activityToSend.To) + err = CreateCollectionObject(followers, activityToSend.Actor) + if err != nil { + return + } + log.Printf("%s follows %s", activityToSend.Actor, activityToSend.To) + } else if activityToSend.Type == "Create" { + id := activityToSend.Id + if activityToSend.Object != nil { + id = activityToSend.Object.Id + } + inbox := fmt.Sprintf("%sinbox", recipient) + err = CreateCollectionObject(inbox, id) + if err != nil { + return + } + log.Printf("%s to %s: %s", activityToSend.Actor, recipient, activityToSend.Object.Content) + } + if err != nil { + return + } + } else { + activityToSend.To = recipient + var profile *actor.Actor + err, profile = actor.GetProfile(recipient) + if err != nil { + return + } + var reqBody []byte + reqBody, err = json.Marshal(activityToSend) + if err != nil { + return + } + client := &http.Client{Timeout: 2 * time.Second} + var req *http.Request + log.Printf("Inbox: %s", profile.Inbox) + req, err = http.NewRequest(http.MethodPost, profile.Inbox, bytes.NewBuffer(reqBody)) + if err != nil { + return + } + req.Header["Content-Type"] = []string{utils.GetContentTypeString()} + req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s")} + var res *http.Response + res, err = client.Do(req) + if err != nil { + return + } + if res.StatusCode != http.StatusOK { + err = utils.HandleHTTPError(res) + return + } + } + return +} diff --git a/entities/collection/collectionDatabaseConnector.go b/entities/collection/collectionDatabaseConnector.go new file mode 100644 index 0000000..7d00302 --- /dev/null +++ b/entities/collection/collectionDatabaseConnector.go @@ -0,0 +1,62 @@ +package collection + +import ( + "fmt" + + "git.nutfactory.org/hoernschen/ActivityPub/utils/database" +) + +func CreateCollectionObject(collectionId string, objectId string) (err error) { + sqlStmt := fmt.Sprintf(`INSERT INTO collectionObject + (collectionId, objectId) + VALUES + (?, ?)`) + + tx, err := database.DB.Begin() + if err != nil { + return + } + + stmt, err := tx.Prepare(sqlStmt) + if err != nil { + return + } + defer stmt.Close() + + _, err = stmt.Exec( + collectionId, + objectId, + ) + if err != nil { + tx.Rollback() + return + } + tx.Commit() + return +} + +func ReadCollectionObjects(collectionId string) (collectionObjects []string, err error) { + queryStmt := fmt.Sprintf(`SELECT objectId + FROM collectionObject + WHERE collectionId = '%s'`, collectionId) + + rows, err := database.DB.Query(queryStmt) + if err != nil { + return + } + + defer rows.Close() + + for rows.Next() { + var collectionObject string + err = rows.Scan( + &collectionObject, + ) + if err != nil { + return + } + collectionObjects = append(collectionObjects, collectionObject) + } + + return +} diff --git a/entities/object/object.go b/entities/object/object.go new file mode 100644 index 0000000..d53ef8c --- /dev/null +++ b/entities/object/object.go @@ -0,0 +1,11 @@ +package object + +type Object struct { + Context string `json:"@context,omitempty"` + Id string `json:"id,omitempty"` + Type string `json:"type,omitempty"` + AttributedTo string `json:"attributedTo,omitempty"` + Content string `json:"content,omitempty"` + Published int64 `json:"published,omitempty"` + To string `json:"to,omitempty"` +} diff --git a/entities/object/objectController.go b/entities/object/objectController.go new file mode 100644 index 0000000..0d1d956 --- /dev/null +++ b/entities/object/objectController.go @@ -0,0 +1,49 @@ +package object + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "git.nutfactory.org/hoernschen/ActivityPub/config" + "git.nutfactory.org/hoernschen/ActivityPub/utils" +) + +func New(objectType string, attributedTo string, content string, published int64, to string, userId string) (err error, newObject *Object) { + err, id := utils.CreateUUID() + url := fmt.Sprintf("%s://%s/%s/posts/%s", config.HttpString, config.Homeserver, userId, id) + if published == 0 { + published = time.Now().Unix() + } + if attributedTo == "" { + attributedTo = utils.GenerateProfileUrl(userId) + } + newObject = &Object{ + Context: utils.GetDefaultContext(), + Id: url, + Type: objectType, + AttributedTo: attributedTo, + Content: content, + Published: published, + To: to, + } + return +} + +func GetPostHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", utils.GetContentTypeString()) + foundObject, err := ReadObject(r.URL.RequestURI()) + if foundObject == nil || err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Post not found: %s", err)); err != nil { + panic(err) + } + return + } + foundObject.Context = utils.GetDefaultContext() + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(foundObject); err != nil { + panic(err) + } +} diff --git a/entities/object/objectDatabaseConnector.go b/entities/object/objectDatabaseConnector.go new file mode 100644 index 0000000..516cc18 --- /dev/null +++ b/entities/object/objectDatabaseConnector.go @@ -0,0 +1,70 @@ +package object + +import ( + "fmt" + + "git.nutfactory.org/hoernschen/ActivityPub/utils/database" +) + +func CreateObject(object *Object) (err error) { + sqlStmt := fmt.Sprintf(`INSERT INTO object + (id, type, attributedTo, content, published, toActor) + VALUES + (?, ?, ?, ?, ?, ?)`) + + tx, err := database.DB.Begin() + if err != nil { + return + } + + stmt, err := tx.Prepare(sqlStmt) + if err != nil { + return + } + defer stmt.Close() + + _, err = stmt.Exec( + object.Id, + object.Type, + object.AttributedTo, + object.Content, + object.Published, + object.To, + ) + if err != nil { + tx.Rollback() + return + } + tx.Commit() + return +} + +func ReadObject(id string) (foundObject *Object, err error) { + queryStmt := fmt.Sprintf(`SELECT id, type, attributedTo, content, published, toActor + FROM object + WHERE id = '%s'`, id) + + rows, err := database.DB.Query(queryStmt) + if err != nil { + return + } + + defer rows.Close() + + if rows.Next() { + foundObject = &Object{} + err = rows.Scan( + &foundObject.Id, + &foundObject.Type, + &foundObject.AttributedTo, + &foundObject.Content, + &foundObject.Published, + &foundObject.To, + ) + if err != nil { + return + } + } + + return +} diff --git a/entities/user/user.go b/entities/user/user.go new file mode 100644 index 0000000..1c103ca --- /dev/null +++ b/entities/user/user.go @@ -0,0 +1,19 @@ +package user + +type User struct { + Id string `json:"id,omitempty"` + Password string `json:"password,omitempty"` + Actor string `json:"actor,omitempty"` + Sessions []string `json:"sessions,omitempty"` +} + +type RegisterRequest struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` +} + +type RegisterResponse struct { + UserId string `json:"user_id,omitempty"` + Token string `json:"token,omitempty"` + Actor string `json:"actor,omitempty"` +} diff --git a/entities/user/userController.go b/entities/user/userController.go new file mode 100644 index 0000000..a6271b8 --- /dev/null +++ b/entities/user/userController.go @@ -0,0 +1,103 @@ +package user + +import ( + "encoding/json" + "fmt" + "net/http" + + "git.nutfactory.org/hoernschen/ActivityPub/entities/actor" + "git.nutfactory.org/hoernschen/ActivityPub/utils" +) + +func New(id string, password string) (err error, newUser *User) { + err, hashedPassword := utils.Hash([]byte(password)) + if err != nil { + return + } + newUser = &User{ + Id: id, + Password: hashedPassword, + } + return +} + +func RegisterHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + request := RegisterRequest{} + err := utils.CheckRequest(r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(err); err != nil { + panic(err) + } + return + } + decoder := json.NewDecoder(r.Body) + err = decoder.Decode(&request) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode("Could not parse JSON: %s"); err != nil { + panic(err) + } + return + } + err, newUser := New(request.Username, request.Password) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Error creating User Object: %s")); err != nil { + panic(err) + } + return + } + foundUser, err := ReadUser(newUser.Id) + if foundUser != nil || err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Username already in use: %s", err)); err != nil { + panic(err) + } + return + } + newActor := actor.New(newUser.Id) + err = actor.CreateActor(newActor) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode(fmt.Sprintf("Database Error Creating Actor: %s")); err != nil { + panic(err) + } + return + } + newUser.Actor = newActor.Id + err = CreateUser(newUser) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode("Database Error Creating User: %s"); err != nil { + panic(err) + } + return + } + err, token := utils.CreateToken() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode("Error Creating Token: %s"); err != nil { + panic(err) + } + return + } + err = CreateSession(newUser.Id, token) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + if err := json.NewEncoder(w).Encode("Error Creating Session: %s"); err != nil { + panic(err) + } + return + } + response := RegisterResponse{ + UserId: newUser.Id, + Token: token, + Actor: newActor.Id, + } + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(response); err != nil { + panic(err) + } +} diff --git a/entities/user/userDatabaseConnector.go b/entities/user/userDatabaseConnector.go new file mode 100644 index 0000000..fd633d5 --- /dev/null +++ b/entities/user/userDatabaseConnector.go @@ -0,0 +1,132 @@ +package user + +import ( + "fmt" + + "git.nutfactory.org/hoernschen/ActivityPub/utils/database" +) + +func CreateUser(user *User) (err error) { + sqlStmt := fmt.Sprintf(`INSERT INTO user + (id, password, actor) + VALUES + (?, ?, ?)`) + + tx, err := database.DB.Begin() + if err != nil { + return + } + + stmt, err := tx.Prepare(sqlStmt) + if err != nil { + return + } + defer stmt.Close() + + _, err = stmt.Exec(user.Id, user.Password, user.Actor) + if err != nil { + tx.Rollback() + return + } + tx.Commit() + return +} + +func CreateSession(userId string, token string) (err error) { + sqlStmt := fmt.Sprintf(`INSERT INTO session + (token, userId) + VALUES + (?, ?)`) + + tx, err := database.DB.Begin() + if err != nil { + return + } + + stmt, err := tx.Prepare(sqlStmt) + if err != nil { + return + } + defer stmt.Close() + + _, err = stmt.Exec(token, userId) + if err != nil { + tx.Rollback() + return + } + tx.Commit() + return +} + +func ReadUser(id string) (foundUser *User, err error) { + queryStmt := fmt.Sprintf(`SELECT id, password, actor + FROM user + WHERE id = '%s'`, id) + + rows, err := database.DB.Query(queryStmt) + if err != nil { + return + } + + defer rows.Close() + + if rows.Next() { + foundUser = &User{} + err = rows.Scan(&foundUser.Id, &foundUser.Password, &foundUser.Actor) + if err != nil { + return + } + foundUser.Sessions, err = ReadSessionsForUser(foundUser.Id) + } + + return +} + +func ReadUserFromToken(token string) (foundUser *User, err error) { + queryStmt := fmt.Sprintf(`SELECT u.id, u.password, u.actor + FROM user as u + join session as s on u.id = s.userId + WHERE s.token = '%s'`, token) + + rows, err := database.DB.Query(queryStmt) + if err != nil { + return + } + + defer rows.Close() + + if rows.Next() { + foundUser = &User{} + err = rows.Scan(&foundUser.Id, &foundUser.Password, &foundUser.Actor) + if err != nil { + return + } + foundUser.Sessions, err = ReadSessionsForUser(foundUser.Id) + } + + return +} + +func ReadSessionsForUser(userId string) (sessions []string, err error) { + queryStmt := fmt.Sprintf(`SELECT token + FROM session + WHERE userId = '%s'`, userId) + + rows, err := database.DB.Query(queryStmt) + if err != nil { + return + } + + defer rows.Close() + + for rows.Next() { + var session string + err = rows.Scan(&session) + if err != nil { + return + } + sessions = append(sessions, session) + } + + return +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c4444fd --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module git.nutfactory.org/hoernschen/ActivityPub + +go 1.14 + +require ( + github.com/cenkalti/backoff/v4 v4.1.0 + github.com/gorilla/mux v1.8.0 + github.com/lestrrat-go/backoff v1.0.0 + github.com/mattn/go-sqlite3 v1.14.4 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7a899de --- /dev/null +++ b/go.sum @@ -0,0 +1,55 @@ +git.nutfactory.org/hoernschen/Matrix v0.0.0-20201012141628-da9196f38986 h1:kR6rjqDOGSfjWYQqTiy1HGLrAnbZvAwSkum4DMzj+5U= +git.nutfactory.org/hoernschen/Matrix v0.0.0-20201012141628-da9196f38986/go.mod h1:ewiecRT3bLcsnjp5kKuEXOrOGk/0nojoJFNnGaAyGXw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cenkalti/backoff v1.1.0 h1:QnvVp8ikKCDWOsFheytRCoYWYPO/ObCTBGxT19Hc+yE= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc= +github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lestrrat-go/backoff v1.0.0 h1:nR+UgAhdhwfw2i+xznuHRlj81oMYa7u3lXun0xcsXUU= +github.com/lestrrat-go/backoff v1.0.0/go.mod h1:c7OnDlnHsFXbH1vyIS8+txH+THcc+QFlSQTrJVe4EIM= +github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA= +github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI= +github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= +github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d h1:/iIZNFGxc/a7C3yWjGcnboV+Tkc7mxr+p6fDztwoxuM= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.1-2020.1.5 h1:nI5egYTGJakVyOryqLs1cQO5dO0ksin5XXs2pspk75k= +honnef.co/go/tools v0.0.1-2020.1.5/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= diff --git a/main.go b/main.go new file mode 100644 index 0000000..53eeac9 --- /dev/null +++ b/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "crypto/tls" + "log" + "net/http" + "os" + + "git.nutfactory.org/hoernschen/ActivityPub/config" + "git.nutfactory.org/hoernschen/ActivityPub/entities/actor" + "git.nutfactory.org/hoernschen/ActivityPub/entities/collection" + "git.nutfactory.org/hoernschen/ActivityPub/entities/object" + "git.nutfactory.org/hoernschen/ActivityPub/entities/user" + "git.nutfactory.org/hoernschen/ActivityPub/utils/database" + "git.nutfactory.org/hoernschen/ActivityPub/utils/router" +) + +var keyPath = "./ssl.key" +var certPath = "./ssl.crt" + +var routes = router.Routes{ + // General + router.Route{"Reset", "GET", "/reset", config.Reset}, + router.Route{"SetParams", "GET", "/setparams", config.SetParams}, + + // Users + router.Route{"Register", "POST", "/register", user.RegisterHandler}, + + // Actors + router.Route{"GetProfile", "GET", "/{actorName}/", actor.GetProfileHandler}, + + // Objects + router.Route{"GetPost", "GET", "/{actorName}/posts/{postId}", object.GetPostHandler}, + + // Collections + /* + router.Route{"GetInbox", "GET", "/{actorName}/inbox", collection.GetInboxHandler}, + router.Route{"GetOutbox", "GET", "/{actorName}/outbox", collection.GetOutboxHandler}, + router.Route{"GetFollowers", "GET", "/{actorName}/followers", collection.GetFollowersHandler}, + router.Route{"GetFollowing", "GET", "/{actorName}/following", collection.GetFollowingHandler}, + router.Route{"GetLiked", "GET", "/{actorName}/liked", collection.GetLikedHandler}, + */ + router.Route{"PostInbox", "POST", "/{actorName}/inbox/", collection.PostInboxHandler}, + router.Route{"PostOutbox", "POST", "/{actorName}/outbox/", collection.PostOutboxHandler}, + /* + router.Route{"PostFollowers", "POST", "/{actorName}/followers", collection.PostFollowersHandler}, + router.Route{"PostFollowing", "POST", "/{actorName}/following", collection.PostFollowingHandler}, + router.Route{"PostLiked", "POST", "/{actorName}/liked", collection.PostLikedHandler}, + */ +} + +func main() { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + config.Homeserver = "localhost" + if len(os.Args) > 1 { + config.Homeserver = os.Args[1] + } + log.Printf("Start homeserver on name %s", config.Homeserver) + + /* + if err := device.InitServerSigningKey(); err != nil { + log.Fatal(err) + } + config.VerifyKeys = make(map[string]map[string][]byte) + */ + /* + config.BackoffPolicy = backoff.NewExponential( + backoff.WithInterval(500*time.Millisecond), + backoff.WithMaxRetries(16), + ) + */ + os.Remove("sqlite.db") + + if err := database.InitDB("sqlite.db"); err != nil { + log.Fatal(err) + } + defer database.DB.Close() + + config.SetDefaultParams() + + router := router.NewRouter(routes) + + httpErr := http.ListenAndServeTLS(":443", certPath, keyPath, router) + if httpErr != nil { + log.Fatal(httpErr) + } + +} diff --git a/sqlite.db b/sqlite.db new file mode 100644 index 0000000..f7bc898 Binary files /dev/null and b/sqlite.db differ diff --git a/ssl.crt b/ssl.crt new file mode 100644 index 0000000..b81b656 --- /dev/null +++ b/ssl.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEGDCCAwCgAwIBAgIJANNCajLtjHULMA0GCSqGSIb3DQEBCwUAMIGgMQswCQYD +VQQGEwJERTEWMBQGA1UECAwNTmllZGVyc2FjaHNlbjERMA8GA1UEBwwIQnJhbXNj +aGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDETMBEGA1UEAwwK +aG9lcm5zY2hlbjEuMCwGCSqGSIb3DQEJARYfanVsaWFuLmhvZXJuc2NoZW1leWVy +QGdtYWlsLmNvbTAeFw0xODA4MTAxMzQ0MDJaFw0yODA4MDcxMzQ0MDJaMIGgMQsw +CQYDVQQGEwJERTEWMBQGA1UECAwNTmllZGVyc2FjaHNlbjERMA8GA1UEBwwIQnJh +bXNjaGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDETMBEGA1UE +AwwKaG9lcm5zY2hlbjEuMCwGCSqGSIb3DQEJARYfanVsaWFuLmhvZXJuc2NoZW1l +eWVyQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL7O +m1fevM12EI2vP+4h7E+FpOXNVzTaG7dK1m65NTU6qgf5EV/My92S6xn0OVWVYtQ+ +VqiXmGlm8MK17ssu2+6DK7NKv6PqNBXvVacEOmBElL9F4NHR/s5XBR2AyqnWWy8D +EnhZkm1rEaEYe3Nb+savnZfjkBKKOGGuQykMbeFziw6Ba57n3uZ8kgOqb/2koWyq +QDqmlpfCaUD328MyuluZkc3UVdv7kQzRqwWI4QmXNsr2gbJv/lCmte7rcKoSMDyD +xDsN/JbnN45k0ct3Ix4aCTnS9bQv0qgrEXREbnB5xkV+9SaW1GgQewFx5R8Miebu +EZsCdSNui2CoeoBHqJMCAwEAAaNTMFEwHQYDVR0OBBYEFNAdK12auACumWcsOCsS +sEvwZXS+MB8GA1UdIwQYMBaAFNAdK12auACumWcsOCsSsEvwZXS+MA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBALnNSNiMympZcFjhyO8uTxfPQK4x +2XdWPiGV+1N/zOn9LXfB5O2NY0g4cfuo9pZLpXMCF06qbc4yJtuqr2urCUhlVoYn +FzKJTVMMKP90XzC0hSMic/kTh+N5pKDlhGhrAmjktHs+4GOqi7oY1SIrvo0KWM3s +KpaOAJYeZNg44RisbRz1JDOoqpjb7bipF6oaNogC8uhRj3qJ1nkT2UlZjdZB5at2 +k1hFkQi1SjJxSg4+fkHVwY/wxzCfYmk4QHt2Zc8wWLpDM6TOePX/eXenphSs0v7P +DO9gVD45i612oiBSJ9UtjHLwZrLm008SglZTudtlz6EDYhCAsSmNZKppaPs= +-----END CERTIFICATE----- diff --git a/ssl.key b/ssl.key new file mode 100644 index 0000000..c3b5dd2 --- /dev/null +++ b/ssl.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAvs6bV968zXYQja8/7iHsT4Wk5c1XNNobt0rWbrk1NTqqB/kR +X8zL3ZLrGfQ5VZVi1D5WqJeYaWbwwrXuyy7b7oMrs0q/o+o0Fe9VpwQ6YESUv0Xg +0dH+zlcFHYDKqdZbLwMSeFmSbWsRoRh7c1v6xq+dl+OQEoo4Ya5DKQxt4XOLDoFr +nufe5nySA6pv/aShbKpAOqaWl8JpQPfbwzK6W5mRzdRV2/uRDNGrBYjhCZc2yvaB +sm/+UKa17utwqhIwPIPEOw38luc3jmTRy3cjHhoJOdL1tC/SqCsRdERucHnGRX71 +JpbUaBB7AXHlHwyJ5u4RmwJ1I26LYKh6gEeokwIDAQABAoIBAQC7S72K7JZyLIGl +QrDDhUMc8DfkZ8NBmxN3wZtpxp2nKXW8K83VNweq8UucB3K8Qs5nPuX7ygsO88BD +sSi9A7tZjiK4dRhWw0/rdCqkrm7LDqbgdqxv6e1wCFV6F3FYc5TAOgjIYExu4ZnF +g22y2Ef6/mn4raU/vbQIlnFQeuXlVb4V5UxJRFyOd5Bd270qIE2Y2uDrNjAjaPzz +H3Vdmr39XvTAoXi5Z+ANwPZV+haVWMrTSW0Qtk3crNnO/GggDv/ugAL/UurNsm6r +B1IEVyqFf4inkugDnajkszVoHfoVMhyf91J3R5u3ZltDElXizamQc/+Pr+r6wvwf +gnMM69kRAoGBAOt7tuhXzP/wLTljcJoord8/eSrElPxtdX47/0P7km0CJFLyrmsa +PasyhpadggVHwtj3VdOinrY/2B/71fi4flMP/HUFvtwD5ViWODE6bbVwijZsyEH/ +wPM594QG9yuVZ0za+M+pQdqnMxQRT79k4SLlR7CskzP5vhbOPIv8YCA5AoGBAM9u +a+/s8trHzErrSQlcpvrvMcj9BLUek5tlEH/Rx/6WnSmfnnzu6/Y3sbRgEPbDg+0E +jbl8dESs3nYxxldg02Mw2bWRyodM7cYBNCPifqutvJdfMxxSWH+ayVBAA7VxQzsY +GDOK4KlC+siTSYUY8PJ9FM+6RTy4F8oVbwmS5jcrAoGBAK4prusC3SzCH0Cdqk9q +HMbL9DrMcACOmGKHz1EhhHe5KNJsiNHP86Jl2SMWVW7AV30O2VyQnt/eMmPdZ7Dw +CwY2AZsvZ6zj+MFfQSovs6qJFMASDr65gKSjz8vHNxH2CxPNtE4qOfmUxfNmplvB +Kb4cY7xotuqvIIdPe3pxa0sJAoGBAMflLXdE7LQRHrqECxpOg0wG/f8mdUbldHGn +70J+MzEQi9v0ypKy3AmmmkWs3iwvNg9O+BTr7k/QF4Hnba/+yzcneGYVXQsOA4Vw +24JJXrCq+LcXMvX0FPzDeYUwa2KLB7MHASuKhf4XYf2wkoUFCA1mpIuageaFscc4 +6IxdWCWJAoGAXXk8bBRj0YksNCP41KVmxLqoky+vd115BjBxmiEK8K9NK1eFUGZe +K8/FeX3JmFrxAS+N+LeP+XVgyF1triDZ0ix8gn2bSY3skZHsUw62B6w7xWXzqgx4 +rZM+GfS/QY2N9ubqze1m/vROSf65iHakZf+mxE+uf2BCi1WOxp7KARE= +-----END RSA PRIVATE KEY----- diff --git a/utils/database/databaseConnector.go b/utils/database/databaseConnector.go new file mode 100644 index 0000000..7901b81 --- /dev/null +++ b/utils/database/databaseConnector.go @@ -0,0 +1,117 @@ +package database + +import ( + "database/sql" + "fmt" + + _ "github.com/mattn/go-sqlite3" +) + +var DB *sql.DB + +// TODO: Change DB Structure +func InitDB(filepath string) (err error) { + DB, err = sql.Open("sqlite3", filepath) + if err != nil { + panic(err) + } + if DB == nil { + panic("DB couldn't be initialized") + } + + handleError(initObjectTable()) + handleError(initCollectionTable()) + handleError(initActorTable()) + handleError(initUserTable()) + + return +} + +func initObjectTable() (err error) { + statement, err := DB.Prepare(`CREATE TABLE IF NOT EXISTS object ( + id TEXT PRIMARY KEY, + type TEXT, + attributedTo TEXT, + content TEXT, + published INTEGER, + toActor TEXT + )`) + if err != nil { + return + } + statement.Exec() + + return +} + +func initCollectionTable() (err error) { + statement, err := DB.Prepare(`CREATE TABLE IF NOT EXISTS collection ( + id TEXT PRIMARY KEY + )`) + if err != nil { + return + } + statement.Exec() + + statement, err = DB.Prepare(`CREATE TABLE IF NOT EXISTS collectionObject ( + collectionId TEXT, + objectId TEXT, + PRIMARY KEY (collectionId, objectId) + )`) + if err != nil { + return + } + statement.Exec() + + return +} + +func initActorTable() (err error) { + statement, err := DB.Prepare(`CREATE TABLE IF NOT EXISTS actor ( + id TEXT PRIMARY KEY, + type TEXT, + name TEXT, + preferredUsername TEXT, + summary TEXT, + inbox TEXT, + outbox TEXT, + followers TEXT, + following TEXT, + liked TEXT + )`) + if err != nil { + return + } + statement.Exec() + + return +} + +func initUserTable() (err error) { + statement, err := DB.Prepare(`CREATE TABLE IF NOT EXISTS user ( + id TEXT PRIMARY KEY, + password TEXT, + actor TEXT + )`) + if err != nil { + return + } + statement.Exec() + + statement, err = DB.Prepare(`CREATE TABLE IF NOT EXISTS session ( + token TEXT PRIMARY KEY, + userId TEXT + )`) + if err != nil { + return + } + statement.Exec() + + return +} + +func handleError(err error) { + if err != nil { + panic(fmt.Sprintf("Could not execute Database Query: %s", err)) + } +} diff --git a/utils/encryptionService.go b/utils/encryptionService.go new file mode 100644 index 0000000..4960cb4 --- /dev/null +++ b/utils/encryptionService.go @@ -0,0 +1,76 @@ +package utils + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "fmt" + "log" + + "git.nutfactory.org/hoernschen/ActivityPub/config" +) + +func CreateToken() (err error, token string) { + b := make([]byte, 8) + _, err = rand.Read(b) + if err != nil { + log.Fatal(err) + return + } + token = fmt.Sprintf("%x", b) + return +} + +func CreateUUID() (err error, uuid string) { + b := make([]byte, 16) + _, err = rand.Read(b) + if err != nil { + log.Fatal(err) + return + } + uuid = fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + return +} + +// TODO: Check if needed +func Hash(s []byte) (err error, hashString string) { + h := sha256.New() + _, err = h.Write(s) + if nil != err { + return + } + hash := h.Sum(nil) + hashString = base64.StdEncoding.EncodeToString(hash) + return +} + +//TODO: Signing Mechanism? +func GenerateKeyPair() (publicKey ed25519.PublicKey, privateKey ed25519.PrivateKey, err error) { + publicKey, privateKey, err = ed25519.GenerateKey(nil) + return +} + +func Sign(message []byte) string { + signatureBytes := ed25519.Sign(config.PrivateKey, message) + return base64.RawStdEncoding.EncodeToString(signatureBytes) +} + +func SignContent(content []byte) (signatures map[string]map[string]string) { + if !config.Signing { + return + } + signatures = make(map[string]map[string]string) + signatures[config.Homeserver] = make(map[string]string) + signatures[config.Homeserver][config.KeyId] = Sign(content) + return +} + +func VerifySignature(publicKey []byte, message []byte, signature string) bool { + signatureBytes, err := base64.RawStdEncoding.DecodeString(signature) + if err != nil { + return false + } + ed25519.Verify(config.PublicKey, message, signatureBytes) + return true +} diff --git a/utils/logger.go b/utils/logger.go new file mode 100644 index 0000000..e50c9c9 --- /dev/null +++ b/utils/logger.go @@ -0,0 +1,23 @@ +package utils + +import ( + "log" + "net/http" + "time" +) + +func APILogger(inner http.Handler, name string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + inner.ServeHTTP(w, r) + + log.Printf( + "%s\t%s\t%s\t%s", + r.Method, + r.RequestURI, + name, + time.Since(start), + ) + }) +} diff --git a/utils/requestChecker.go b/utils/requestChecker.go new file mode 100644 index 0000000..8f50a5e --- /dev/null +++ b/utils/requestChecker.go @@ -0,0 +1,89 @@ +package utils + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "strings" + + "git.nutfactory.org/hoernschen/ActivityPub/config" +) + +type ErrorResponse struct { + ErrorCode string `json:"errcode,omitempty"` + ErrorMessage string `json:"error,omitempty"` + RetryTime int `json:"retry_after_ms,omitempty"` +} + +func CheckRequest(r *http.Request) (err error) { + if !strings.Contains(r.Header.Get("Content-Type"), "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") { + err = errors.New("Content Type not JSON") + } + return +} + +func GetContentTypeString() string { + return "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" +} + +func GetDefaultContext() string { + return "https://www.w3.org/ns/activitystreams" +} + +func GenerateProfileUrl(userId string) string { + return fmt.Sprintf("%s://%s/%s/", config.HttpString, config.Homeserver, userId) +} + +func GenerateInboxUrl(userId string) string { + return fmt.Sprintf("%s://%s/%s/inbox/", config.HttpString, config.Homeserver, userId) +} + +func GenerateOutboxUrl(userId string) string { + return fmt.Sprintf("%s://%s/%s/outbox/", config.HttpString, config.Homeserver, userId) +} + +func GenerateFollowersUrl(userId string) string { + return fmt.Sprintf("%s://%s/%s/followers/", config.HttpString, config.Homeserver, userId) +} + +func GenerateFollowingUrl(userId string) string { + return fmt.Sprintf("%s://%s/%s/following/", config.HttpString, config.Homeserver, userId) +} + +func GenerateLikedUrl(userId string) string { + return fmt.Sprintf("%s://%s/%s/liked/", config.HttpString, config.Homeserver, userId) +} + +func GetAccessToken(r *http.Request) (token string, err error) { + token = r.URL.Query().Get("access_token") + if token == "" { + token = r.Header.Get("Authorization") + if token == "" || !strings.Contains(token, "Bearer") { + err = errors.New("Missing Token") + } else { + token = strings.Split(token, " ")[1] + } + } + return +} + +func HandleHTTPError(res *http.Response) (err error) { + buf := new(bytes.Buffer) + buf.ReadFrom(res.Body) + err = errors.New(fmt.Sprintf("%s (%s)", buf.String(), res.Status)) + return +} + +func RemoveDuplicates(array []string) []string { + keys := make(map[string]bool) + list := []string{} + + for _, entry := range array { + if _, value := keys[entry]; !value { + keys[entry] = true + list = append(list, entry) + } + } + return list +} diff --git a/utils/router/route.go b/utils/router/route.go new file mode 100644 index 0000000..585d016 --- /dev/null +++ b/utils/router/route.go @@ -0,0 +1,12 @@ +package router + +import "net/http" + +type Route struct { + Name string + Method string + Pattern string + HandlerFunc http.HandlerFunc +} + +type Routes []Route diff --git a/utils/router/router.go b/utils/router/router.go new file mode 100644 index 0000000..c08f456 --- /dev/null +++ b/utils/router/router.go @@ -0,0 +1,27 @@ +package router + +import ( + "net/http" + + "git.nutfactory.org/hoernschen/ActivityPub/utils" + "github.com/gorilla/mux" +) + +func NewRouter(routes Routes) *mux.Router { + router := mux.NewRouter().StrictSlash(true) + + for _, route := range routes { + var handler http.Handler + handler = route.HandlerFunc + handler = utils.APILogger(handler, route.Name) + + router. + Methods(route.Method). + Path(route.Pattern). + Name(route.Name). + Handler(handler) + + } + + return router +} diff --git a/workloadGenerator.go b/workloadGenerator.go new file mode 100644 index 0000000..21d20c1 --- /dev/null +++ b/workloadGenerator.go @@ -0,0 +1,516 @@ +package main + +import ( + "bytes" + "crypto/tls" + "encoding/base64" + "encoding/csv" + "encoding/json" + "fmt" + "log" + "math/rand" + "net/http" + "os" + "strconv" + "time" + + "git.nutfactory.org/hoernschen/ActivityPub/config" + "git.nutfactory.org/hoernschen/ActivityPub/entities/activity" + "git.nutfactory.org/hoernschen/ActivityPub/entities/object" + "git.nutfactory.org/hoernschen/ActivityPub/entities/user" + "git.nutfactory.org/hoernschen/ActivityPub/utils" +) + +var BaseLineTest = false + +var systemParamsIndex = 0 + +type SystemParams struct { + Id string + BytesToSend int + MessagesPerSecond float32 + Distribution map[string][]string + Packetloss int + MinutesNotAvailable int + Consensus bool + AuthentificationCheck bool + Signing bool + Encryption bool +} + +var httpString string +var users = []map[string][]string{ + map[string][]string{ + "143.93.38.208": []string{ + "user1", + "user2", + "user3", + "user4", + "user5", + "user6", + }, + }, + map[string][]string{ + "143.93.38.207": []string{ + "user1", + "user2", + "user3", + "user4", + }, + "143.93.38.208": []string{ + "user1", + }, + "143.93.38.209": []string{ + "user1", + }, + }, + map[string][]string{ + "143.93.38.207": []string{ + "user1", + "user2", + "user3", + "user4", + }, + "143.93.38.208": []string{ + "user1", + "user2", + "user3", + "user4", + }, + "143.93.38.209": []string{ + "user1", + "user2", + "user3", + "user4", + }, + }, +} + +var servers = []string{ + "143.93.38.207", + "143.93.38.208", + "143.93.38.209", +} + +var systemParams = []SystemParams{ + SystemParams{ + Id: "111110000", + BytesToSend: 280, + MessagesPerSecond: 1.0, + Distribution: users[1], + Packetloss: 1, + MinutesNotAvailable: 0, + Consensus: true, + AuthentificationCheck: true, + Signing: true, + Encryption: true, + }, + SystemParams{ + Id: "011110000", + BytesToSend: 8, + MessagesPerSecond: 1.0, + Distribution: users[1], + Packetloss: 1, + MinutesNotAvailable: 0, + Consensus: true, + AuthentificationCheck: true, + Signing: true, + Encryption: true, + }, + SystemParams{ + Id: "211110000", + BytesToSend: 512, + MessagesPerSecond: 1.0, + Distribution: users[1], + Packetloss: 1, + MinutesNotAvailable: 0, + Consensus: true, + AuthentificationCheck: true, + Signing: true, + Encryption: true, + }, + SystemParams{ + Id: "101110000", + BytesToSend: 280, + MessagesPerSecond: 0.1, + Distribution: users[1], + Packetloss: 1, + MinutesNotAvailable: 0, + Consensus: true, + AuthentificationCheck: true, + Signing: true, + Encryption: true, + }, + SystemParams{ + Id: "121110000", + BytesToSend: 280, + MessagesPerSecond: 10.0, + Distribution: users[1], + Packetloss: 1, + MinutesNotAvailable: 0, + Consensus: true, + AuthentificationCheck: true, + Signing: true, + Encryption: true, + }, + SystemParams{ + Id: "110110000", + BytesToSend: 280, + MessagesPerSecond: 1.0, + Distribution: users[0], + Packetloss: 1, + MinutesNotAvailable: 0, + Consensus: true, + AuthentificationCheck: true, + Signing: true, + Encryption: true, + }, + SystemParams{ + Id: "112110000", + BytesToSend: 280, + MessagesPerSecond: 1.0, + Distribution: users[2], + Packetloss: 1, + MinutesNotAvailable: 0, + Consensus: true, + AuthentificationCheck: true, + Signing: true, + Encryption: true, + }, + SystemParams{ + Id: "111010000", + BytesToSend: 280, + MessagesPerSecond: 1.0, + Distribution: users[1], + Packetloss: 0, + MinutesNotAvailable: 0, + Consensus: true, + AuthentificationCheck: true, + Signing: true, + Encryption: true, + }, + SystemParams{ + Id: "111210000", + BytesToSend: 280, + MessagesPerSecond: 1.0, + Distribution: users[1], + Packetloss: 20, + MinutesNotAvailable: 0, + Consensus: true, + AuthentificationCheck: true, + Signing: true, + Encryption: true, + }, + /* + SystemParams{ + Id: "111120000", + BytesToSend: 280, + MessagesPerSecond: 1.0, + Distribution: users[1], + Packetloss: 1, + MinutesNotAvailable: 1, + Consensus: true, + AuthentificationCheck: true, + Signing: true, + Encryption: true, + }, + */ +} + +var userIds []string +var accessTokens map[string]string +var collectionId string + +func main() { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + httpString = "https" + iteration := 0 + if BaseLineTest { + file, err := os.Create(fmt.Sprintf("%s Baseline Measurement ActivityPub.csv", strconv.FormatInt(time.Now().Unix(), 10))) + if err != nil { + log.Fatalf("Error Creating CSV: %s", err) + } + defer file.Close() + writer := csv.NewWriter(file) + defer writer.Flush() + err = writer.Write([]string{"Iteration", "Start", "End"}) + if err != nil { + log.Fatalf("Error in Writing CSV: %s", err) + } + for iteration < 10 { + start := time.Now().Unix() + time.Sleep(2 * time.Minute) + end := time.Now().Unix() + iteration++ + err = writer.Write([]string{strconv.Itoa(iteration), strconv.FormatInt(start, 10), strconv.FormatInt(end, 10)}) + if err != nil { + log.Fatalf("Error in Writing CSV: %s", err) + } + } + } else { + for _, systemParam := range systemParams { + file, err := os.Create(fmt.Sprintf("%s %s Measurement ActivityPub.csv", strconv.FormatInt(time.Now().Unix(), 10), systemParam.Id)) + if err != nil { + log.Fatalf("Error Creating CSV: %s", err) + } + writer := csv.NewWriter(file) + err = writer.Write([]string{"Iteration", "Start", "End", "Actions Send"}) + if err != nil { + log.Fatalf("Error in Writing CSV: %s", err) + } + millisecondsToWait := 1000 / systemParam.MessagesPerSecond + iteration = 0 + for iteration < 30 { + err := setUp(systemParam) + if err != nil { + log.Fatalf("Error in SetUp: %s", err) + } + b := make([]byte, systemParam.BytesToSend) + _, err = rand.Read(b) + message := base64.RawStdEncoding.EncodeToString(b) + start := time.Now() + end := start.Add(2 * time.Minute).Unix() + log.Printf("Id: %s - Iteration: %s - Start: %s - End: %s", systemParam.Id, strconv.Itoa(iteration), strconv.FormatInt(start.Unix(), 10), strconv.FormatInt(end, 10)) + actionsSend := 0 + for time.Now().Unix() < end { + time.Sleep(time.Duration(millisecondsToWait) * time.Millisecond) + + err = sendMessage(message) + if err != nil { + log.Fatalf("Error sending Message %s", err) + } + + actionsSend++ + } + iteration++ + err = writer.Write([]string{strconv.Itoa(iteration), strconv.FormatInt(start.Unix(), 10), strconv.FormatInt(end, 10), strconv.Itoa(actionsSend)}) + if err != nil { + log.Fatalf("Error in Writing CSV: %s", err) + } + time.Sleep(1 * time.Second) + err = reset() + if err != nil { + log.Fatalf("Error in Reset: %s", err) + } + time.Sleep(1 * time.Second) + } + writer.Flush() + file.Close() + } + } +} + +func sendMessage(message string) (err error) { + userId := userIds[0] + accessToken := accessTokens[userId] + requestUrl := fmt.Sprintf("%soutbox/", userId) + messageObject := object.Object{ + Type: "Note", + Content: message, + } + reqBody, err := json.Marshal(messageObject) + if err != nil { + return + } + client := &http.Client{} + var req *http.Request + req, err = http.NewRequest(http.MethodPost, requestUrl, bytes.NewBuffer(reqBody)) + if err != nil { + return + } + req.Header["Content-Type"] = []string{utils.GetContentTypeString()} + req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", accessToken)} + res, err := client.Do(req) + if err != nil { + return + } + if res.StatusCode != http.StatusOK { + handleError(res) + } + return +} + +func setParams(systemParamsToUse SystemParams) (err error) { + serverNotAvailableIndex := 1 + for i, server := range servers { + minutesNotAvailable := 0 + if serverNotAvailableIndex == i { + minutesNotAvailable = systemParamsToUse.MinutesNotAvailable + } + requestUrl := fmt.Sprintf("%s://%s/setparams", httpString, server) + request := config.SetParamBody{ + Packetloss: systemParamsToUse.Packetloss, + UnavailableTill: time.Now().Add(time.Duration(minutesNotAvailable) * time.Minute).Unix(), + Consensus: systemParamsToUse.Consensus, + AuthentificationCheck: systemParamsToUse.AuthentificationCheck, + Signing: systemParamsToUse.Signing, + Encryption: systemParamsToUse.Encryption, + } + var reqBody []byte + reqBody, err = json.Marshal(request) + if err != nil { + return + } + client := &http.Client{Timeout: 2 * time.Second} + var req *http.Request + req, err = http.NewRequest(http.MethodGet, requestUrl, bytes.NewBuffer(reqBody)) + if err != nil { + return + } + req.Header["Content-Type"] = []string{"application/json"} + var res *http.Response + res, err = client.Do(req) + if err != nil { + return + } + if res.StatusCode != http.StatusOK { + handleError(res) + } + } + return +} + +func setUp(systemParamsToUse SystemParams) (err error) { + accessTokens = make(map[string]string) + err = createUsers(systemParamsToUse.Distribution) + if err != nil { + log.Printf("Error in User-Creation: %s", err) + return + } + + // Follow User + log.Printf("Userids: %s", userIds) + err = followUser(userIds[0], userIds[1:]) + if err != nil { + log.Printf("Error follow User: %s", err) + return + } + + err = setParams(systemParamsToUse) + return +} + +func reset() (err error) { + userIds = []string{} + accessTokens = make(map[string]string) + for _, server := range servers { + requestUrl := fmt.Sprintf("%s://%s/reset", httpString, server) + client := &http.Client{Timeout: 2 * time.Second} + var req *http.Request + req, err = http.NewRequest(http.MethodGet, requestUrl, bytes.NewBuffer(nil)) + if err != nil { + return + } + req.Header["Content-Type"] = []string{"application/json"} + var res *http.Response + res, err = client.Do(req) + if err != nil { + return + } + if res.StatusCode != http.StatusOK { + handleError(res) + } + } + return +} + +func followUser(userIdToFollow string, userIdsThatFollow []string) (err error) { + for _, userId := range userIdsThatFollow { + log.Printf("UserIdThatFollow: %s", userId) + accessToken := accessTokens[userId] + requestUrl := fmt.Sprintf("%soutbox/", userId) + followActivity := activity.Activity{ + Type: "Follow", + Actor: userId, + To: userIdToFollow, + } + var reqBody []byte + reqBody, err = json.Marshal(followActivity) + if err != nil { + return + } + client := &http.Client{Timeout: 2 * time.Second} + var req *http.Request + req, err = http.NewRequest(http.MethodPost, requestUrl, bytes.NewBuffer(reqBody)) + if err != nil { + return + } + req.Header["Content-Type"] = []string{utils.GetContentTypeString()} + req.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", accessToken)} + var res *http.Response + res, err = client.Do(req) + if err != nil { + return + } + if res.StatusCode != http.StatusOK { + err = utils.HandleHTTPError(res) + return + } + //time.Sleep(time.Duration(1) * time.Second) + } + return +} + +func createUsers(serverUserMap map[string][]string) (err error) { + log.Println("Create Users") + for server, usersToCreate := range serverUserMap { + for _, userToCreate := range usersToCreate { + var userId string + var accessToken string + userId, accessToken, err = createUser(userToCreate, server) + if err != nil { + return + } + if userId != "" && accessToken != "" { + log.Printf("%s created - AccessToken: %s", userId, accessToken) + accessTokens[userId] = accessToken + userIds = append(userIds, userId) + } + time.Sleep(time.Duration(20) * time.Millisecond) + } + } + return +} + +func createUser(userToCreate string, homeserver string) (userId string, accessToken string, err error) { + requestUrl := fmt.Sprintf("%s://%s/register", httpString, homeserver) + request := user.RegisterRequest{ + Username: userToCreate, + Password: "password", + } + reqBody, err := json.Marshal(request) + if err != nil { + return + } + client := &http.Client{Timeout: 2 * time.Second} + req, err := http.NewRequest(http.MethodPost, requestUrl, bytes.NewBuffer(reqBody)) + if err != nil { + return + } + req.Header["Content-Type"] = []string{utils.GetContentTypeString()} + res, err := client.Do(req) + if err != nil { + return + } + if res.StatusCode != http.StatusOK { + err = utils.HandleHTTPError(res) + return + } else { + response := user.RegisterResponse{} + decoder := json.NewDecoder(res.Body) + err = decoder.Decode(&response) + if err != nil { + return + } + userId = response.Actor + accessToken = response.Token + } + return +} + +func handleError(res *http.Response) { + err := utils.HandleHTTPError(res) + if err != nil { + log.Printf("%s", err) + } +}