Finished Prototype

This commit is contained in:
Hoernschen 2020-10-17 12:13:15 +02:00
parent ea54a27796
commit 6de476260d
30 changed files with 2189 additions and 0 deletions

View file

@ -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
1 Iteration Start End
2 1 1602787120 1602787240
3 2 1602787240 1602787360
4 3 1602787360 1602787480
5 4 1602787480 1602787600
6 5 1602787600 1602787720
7 6 1602787720 1602787840
8 7 1602787840 1602787961
9 8 1602787961 1602788081
10 9 1602788081 1602788201
11 10 1602788201 1602788321

36
config/config.go Normal file
View file

@ -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"
}

View file

@ -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)
}

View file

@ -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"`
}

View file

@ -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
}

15
entities/actor/actor.go Normal file
View file

@ -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"`
}

View file

@ -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
}

View file

@ -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
}

View file

@ -0,0 +1,5 @@
package collection
type OutboxResponse struct {
Location string `json:"Location,omitempty"`
}

View file

@ -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
}

View file

@ -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
}

11
entities/object/object.go Normal file
View file

@ -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"`
}

View file

@ -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)
}
}

View file

@ -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
}

19
entities/user/user.go Normal file
View file

@ -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"`
}

View file

@ -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)
}
}

View file

@ -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
}

10
go.mod Normal file
View file

@ -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
)

55
go.sum Normal file
View file

@ -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=

88
main.go Normal file
View file

@ -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)
}
}

BIN
sqlite.db Normal file

Binary file not shown.

24
ssl.crt Normal file
View file

@ -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-----

27
ssl.key Normal file
View file

@ -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-----

View file

@ -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))
}
}

View file

@ -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
}

23
utils/logger.go Normal file
View file

@ -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),
)
})
}

89
utils/requestChecker.go Normal file
View file

@ -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
}

12
utils/router/route.go Normal file
View file

@ -0,0 +1,12 @@
package router
import "net/http"
type Route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
}
type Routes []Route

27
utils/router/router.go Normal file
View file

@ -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
}

516
workloadGenerator.go Normal file
View file

@ -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)
}
}