diff options
-rw-r--r-- | activitypub/activityPubStruct.go | 12 | ||||
-rw-r--r-- | cacheDatabase.go | 314 | ||||
-rw-r--r-- | client.go | 12 | ||||
-rw-r--r-- | db/cache.go | 325 | ||||
-rw-r--r-- | db/database.go | 47 | ||||
-rw-r--r-- | db/follow.go | 481 | ||||
-rw-r--r-- | db/pem.go | 313 | ||||
-rw-r--r-- | db/verification.go | 342 | ||||
-rw-r--r-- | follow.go | 331 | ||||
-rw-r--r-- | main.go | 298 | ||||
-rw-r--r-- | util/proxy.go | 39 | ||||
-rw-r--r-- | util/util.go | 37 | ||||
-rw-r--r-- | webfinger.go | 12 | ||||
-rw-r--r-- | webfinger/webfinger.go | 145 |
14 files changed, 1408 insertions, 1300 deletions
diff --git a/activitypub/activityPubStruct.go b/activitypub/activityPubStruct.go index 002d514..637982d 100644 --- a/activitypub/activityPubStruct.go +++ b/activitypub/activityPubStruct.go @@ -204,3 +204,15 @@ type Collection struct { AtContext CollectionBase } + +type ObjectBaseSortDesc []ObjectBase + +func (a ObjectBaseSortDesc) Len() int { return len(a) } +func (a ObjectBaseSortDesc) Less(i, j int) bool { return a[i].Updated.After(a[j].Updated) } +func (a ObjectBaseSortDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type ObjectBaseSortAsc []ObjectBase + +func (a ObjectBaseSortAsc) Len() int { return len(a) } +func (a ObjectBaseSortAsc) Less(i, j int) bool { return a[i].Published.Before(a[j].Published) } +func (a ObjectBaseSortAsc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } diff --git a/cacheDatabase.go b/cacheDatabase.go deleted file mode 100644 index b50065f..0000000 --- a/cacheDatabase.go +++ /dev/null @@ -1,314 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - - _ "github.com/lib/pq" -) - -func WriteObjectToCache(db *sql.DB, obj ObjectBase) ObjectBase { - - if IsPostBlacklist(db, obj.Content) { - fmt.Println("\n\nBlacklist post blocked\n\n") - return obj - } - - if len(obj.Attachment) > 0 { - if obj.Preview.Href != "" { - WritePreviewToCache(db, *obj.Preview) - } - - for i, _ := range obj.Attachment { - WriteAttachmentToCache(db, obj.Attachment[i]) - WriteActivitytoCacheWithAttachment(db, obj, obj.Attachment[i], *obj.Preview) - } - - } else { - WriteActivitytoCache(db, obj) - } - - WriteObjectReplyToDB(db, obj) - - if obj.Replies != nil { - for _, e := range obj.Replies.OrderedItems { - WriteObjectToCache(db, e) - } - } - - return obj -} - -func WriteActorObjectToCache(db *sql.DB, obj ObjectBase) ObjectBase { - - if IsPostBlacklist(db, obj.Content) { - return obj - } - - if len(obj.Attachment) > 0 { - - if IsIDLocal(db, obj.Id) { - return obj - } - if obj.Preview.Href != "" { - WritePreviewToCache(db, *obj.Preview) - } - - for i, _ := range obj.Attachment { - WriteAttachmentToCache(db, obj.Attachment[i]) - WriteActivitytoCacheWithAttachment(db, obj, obj.Attachment[i], *obj.Preview) - } - - } else { - WriteActivitytoCache(db, obj) - } - - WriteActorObjectReplyToDB(db, obj) - - if obj.Replies != nil { - for _, e := range obj.Replies.OrderedItems { - WriteActorObjectToCache(db, e) - } - } - - return obj -} - -func WriteActivitytoCache(db *sql.DB, obj ObjectBase) { - - obj.Name = EscapeString(obj.Name) - obj.Content = EscapeString(obj.Content) - obj.AttributedTo = EscapeString(obj.AttributedTo) - - query := `select id from cacheactivitystream where id=$1` - - rows, err := db.Query(query, obj.Id) - - CheckError(err, "error selecting obj id from cache") - - var id string - defer rows.Close() - rows.Next() - rows.Scan(&id) - - if id != "" { - return - } - - if obj.Updated.IsZero() { - obj.Updated = obj.Published - } - - query = `insert into cacheactivitystream (id, type, name, content, published, updated, attributedto, actor, tripcode, sensitive) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` - - _, e := db.Exec(query, obj.Id, obj.Type, obj.Name, obj.Content, obj.Published, obj.Updated, obj.AttributedTo, obj.Actor, obj.TripCode, obj.Sensitive) - - if e != nil { - fmt.Println("error inserting new activity cache") - panic(e) - } -} - -func WriteActivitytoCacheWithAttachment(db *sql.DB, obj ObjectBase, attachment ObjectBase, preview NestedObjectBase) { - - obj.Name = EscapeString(obj.Name) - obj.Content = EscapeString(obj.Content) - obj.AttributedTo = EscapeString(obj.AttributedTo) - - query := `select id from cacheactivitystream where id=$1` - - rows, err := db.Query(query, obj.Id) - - CheckError(err, "error selecting activity with attachment obj id cache") - - var id string - defer rows.Close() - rows.Next() - rows.Scan(&id) - - if id != "" { - return - } - - if obj.Updated.IsZero() { - obj.Updated = obj.Published - } - - query = `insert into cacheactivitystream (id, type, name, content, attachment, preview, published, updated, attributedto, actor, tripcode, sensitive) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)` - - _, e := db.Exec(query, obj.Id, obj.Type, obj.Name, obj.Content, attachment.Id, preview.Id, obj.Published, obj.Updated, obj.AttributedTo, obj.Actor, obj.TripCode, obj.Sensitive) - - if e != nil { - fmt.Println("error inserting new activity with attachment cache") - panic(e) - } -} - -func WriteAttachmentToCache(db *sql.DB, obj ObjectBase) { - - query := `select id from cacheactivitystream where id=$1` - - rows, err := db.Query(query, obj.Id) - - CheckError(err, "error selecting attachment obj id cache") - - var id string - defer rows.Close() - rows.Next() - rows.Scan(&id) - - if id != "" { - return - } - - if obj.Updated.IsZero() { - obj.Updated = obj.Published - } - - query = `insert into cacheactivitystream (id, type, name, href, published, updated, attributedTo, mediatype, size) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)` - - _, e := db.Exec(query, obj.Id, obj.Type, obj.Name, obj.Href, obj.Published, obj.Updated, obj.AttributedTo, obj.MediaType, obj.Size) - - if e != nil { - fmt.Println("error inserting new attachment cache") - panic(e) - } -} - -func WritePreviewToCache(db *sql.DB, obj NestedObjectBase) { - - query := `select id from cacheactivitystream where id=$1` - - rows, err := db.Query(query, obj.Id) - - CheckError(err, "error selecting preview obj id cache") - - var id string - defer rows.Close() - rows.Next() - rows.Scan(&id) - - if id != "" { - return - } - - if obj.Updated.IsZero() { - obj.Updated = obj.Published - } - - query = `insert into cacheactivitystream (id, type, name, href, published, updated, attributedTo, mediatype, size) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)` - - _, e := db.Exec(query, obj.Id, obj.Type, obj.Name, obj.Href, obj.Published, obj.Updated, obj.AttributedTo, obj.MediaType, obj.Size) - - if e != nil { - fmt.Println("error inserting new preview cache") - panic(e) - } -} - -func WriteObjectReplyToCache(db *sql.DB, obj ObjectBase) { - - for i, e := range obj.InReplyTo { - if i == 0 || IsReplyInThread(db, obj.InReplyTo[0].Id, e.Id) { - - query := `select id from replies where id=$1` - - rows, err := db.Query(query, obj.Id) - - CheckError(err, "error selecting obj id cache reply") - - var id string - defer rows.Close() - rows.Next() - rows.Scan(&id) - - if id != "" { - return - } - - query = `insert into cachereplies (id, inreplyto) values ($1, $2)` - - _, err = db.Exec(query, obj.Id, e.Id) - - if err != nil { - fmt.Println("error inserting replies cache") - panic(err) - } - } - } - - if len(obj.InReplyTo) < 1 { - query := `insert into cachereplies (id, inreplyto) values ($1, $2)` - - _, err := db.Exec(query, obj.Id, "") - - if err != nil { - fmt.Println("error inserting replies cache") - panic(err) - } - } -} - -func WriteObjectReplyCache(db *sql.DB, obj ObjectBase) { - - if obj.Replies != nil { - for _, e := range obj.Replies.OrderedItems { - - query := `select inreplyto from cachereplies where id=$1` - - rows, err := db.Query(query, obj.Id) - - CheckError(err, "error selecting obj id cache reply") - - var inreplyto string - defer rows.Close() - rows.Next() - rows.Scan(&inreplyto) - - if inreplyto != "" { - return - } - - query = `insert into cachereplies (id, inreplyto) values ($1, $2)` - - _, err = db.Exec(query, e.Id, obj.Id) - - if err != nil { - fmt.Println("error inserting replies cache") - panic(err) - } - - if !IsObjectLocal(db, e.Id) { - WriteObjectToCache(db, e) - } - - } - return - } -} - -func WriteActorToCache(db *sql.DB, actorID string) { - actor := FingerActor(actorID) - collection := GetActorCollection(actor.Outbox) - - for _, e := range collection.OrderedItems { - WriteActorObjectToCache(db, e) - } -} - -func DeleteActorCache(db *sql.DB, actorID string) { - query := `select id from cacheactivitystream where id in (select id from cacheactivitystream where actor=$1)` - - rows, err := db.Query(query, actorID) - - CheckError(err, "error selecting actors activity from cache") - - defer rows.Close() - - for rows.Next() { - var id string - rows.Scan(&id) - - DeleteObject(db, id) - } -} @@ -689,18 +689,6 @@ func GetActorsFollowPostFromId(db *sql.DB, actors []string, id string) Collectio return collection } -type ObjectBaseSortDesc []ObjectBase - -func (a ObjectBaseSortDesc) Len() int { return len(a) } -func (a ObjectBaseSortDesc) Less(i, j int) bool { return a[i].Updated.After(a[j].Updated) } -func (a ObjectBaseSortDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } - -type ObjectBaseSortAsc []ObjectBase - -func (a ObjectBaseSortAsc) Len() int { return len(a) } -func (a ObjectBaseSortAsc) Less(i, j int) bool { return a[i].Published.Before(a[j].Published) } -func (a ObjectBaseSortAsc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } - type BoardSortAsc []Board func (a BoardSortAsc) Len() int { return len(a) } diff --git a/db/cache.go b/db/cache.go new file mode 100644 index 0000000..0831f02 --- /dev/null +++ b/db/cache.go @@ -0,0 +1,325 @@ +package db + +import ( + "fmt" + + "github.com/FChannel0/FChannel-Server/activitypub" + "github.com/FChannel0/FChannel-Server/webfinger" + _ "github.com/lib/pq" +) + +func WriteObjectToCache(obj activitypub.ObjectBase) (activitypub.ObjectBase, error) { + if res, err := IsPostBlacklist(obj.Content); err == nil && res { + fmt.Println("\n\nBlacklist post blocked\n\n") + return obj, nil + } else { + return obj, err + } + + if len(obj.Attachment) > 0 { + if obj.Preview.Href != "" { + WritePreviewToCache(*obj.Preview) + } + + for i, _ := range obj.Attachment { + WriteAttachmentToCache(obj.Attachment[i]) + WriteActivitytoCacheWithAttachment(obj, obj.Attachment[i], *obj.Preview) + } + + } else { + WriteActivitytoCache(obj) + } + + WriteObjectReplyToDB(obj) + + if obj.Replies != nil { + for _, e := range obj.Replies.OrderedItems { + WriteObjectToCache(e) + } + } + + return obj, nil +} + +func WriteActorObjectToCache(obj activitypub.ObjectBase) (activitypub.ObjectBase, error) { + if res, err := IsPostBlacklist(obj.Content); err == nil && res { + fmt.Println("\n\nBlacklist post blocked\n\n") + return obj, nil + } else if err != nil { + return obj, err + } + + if len(obj.Attachment) > 0 { + if res, err := IsIDLocal(obj.Id); err == nil && res { + return obj, err + } else if err != nil { + return obj, err + } + + if obj.Preview.Href != "" { + WritePreviewToCache(*obj.Preview) + } + + for i, _ := range obj.Attachment { + WriteAttachmentToCache(obj.Attachment[i]) + WriteActivitytoCacheWithAttachment(obj, obj.Attachment[i], *obj.Preview) + } + + } else { + WriteActivitytoCache(obj) + } + + WriteActorObjectReplyToDB(obj) + + if obj.Replies != nil { + for _, e := range obj.Replies.OrderedItems { + WriteActorObjectToCache(e) + } + } + + return obj, nil +} + +func WriteActivitytoCache(obj activitypub.ObjectBase) error { + obj.Name = EscapeString(obj.Name) + obj.Content = EscapeString(obj.Content) + obj.AttributedTo = EscapeString(obj.AttributedTo) + + query := `select id from cacheactivitystream where id=$1` + + rows, err := db.Query(query, obj.Id) + if err != nil { + return err + } + defer rows.Close() + + var id string + rows.Next() + err = rows.Scan(&id) + if err != nil { + return err + } else if id != "" { + return nil // TODO: error? + } + + if obj.Updated.IsZero() { + obj.Updated = obj.Published + } + + query = `insert into cacheactivitystream (id, type, name, content, published, updated, attributedto, actor, tripcode, sensitive) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` + + _, err = db.Exec(query, obj.Id, obj.Type, obj.Name, obj.Content, obj.Published, obj.Updated, obj.AttributedTo, obj.Actor, obj.TripCode, obj.Sensitive) + return err +} + +func WriteActivitytoCacheWithAttachment(obj activitypub.ObjectBase, attachment activitypub.ObjectBase, preview activitypub.NestedObjectBase) error { + obj.Name = EscapeString(obj.Name) + obj.Content = EscapeString(obj.Content) + obj.AttributedTo = EscapeString(obj.AttributedTo) + + query := `select id from cacheactivitystream where id=$1` + + rows, err := db.Query(query, obj.Id) + if err != nil { + return err + } + defer rows.Close() + + var id string + rows.Next() + err = rows.Scan(&id) + if err != nil { + return err + } else if id != "" { + return nil // TODO: error? + } + + if obj.Updated.IsZero() { + obj.Updated = obj.Published + } + + query = `insert into cacheactivitystream (id, type, name, content, attachment, preview, published, updated, attributedto, actor, tripcode, sensitive) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)` + + _, err = db.Exec(query, obj.Id, obj.Type, obj.Name, obj.Content, attachment.Id, preview.Id, obj.Published, obj.Updated, obj.AttributedTo, obj.Actor, obj.TripCode, obj.Sensitive) + return err +} + +func WriteAttachmentToCache(obj activitypub.ObjectBase) error { + query := `select id from cacheactivitystream where id=$1` + + rows, err := db.Query(query, obj.Id) + if err != nil { + return err + } + defer rows.Close() + + var id string + rows.Next() + err = rows.Scan(&id) + if err != nil { + return err + } else if id != "" { + return nil // TODO: error? + } + + if obj.Updated.IsZero() { + obj.Updated = obj.Published + } + + query = `insert into cacheactivitystream (id, type, name, href, published, updated, attributedTo, mediatype, size) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)` + + _, err = db.Exec(query, obj.Id, obj.Type, obj.Name, obj.Href, obj.Published, obj.Updated, obj.AttributedTo, obj.MediaType, obj.Size) + return err +} + +func WritePreviewToCache(obj activitypub.NestedObjectBase) error { + query := `select id from cacheactivitystream where id=$1` + + rows, err := db.Query(query, obj.Id) + if err != nil { + return err + } + defer rows.Close() + + var id string + rows.Next() + err = rows.Scan(&id) + if err != nil { + return err + } else if id != "" { + return nil // TODO: error? + } + + if obj.Updated.IsZero() { + obj.Updated = obj.Published + } + + query = `insert into cacheactivitystream (id, type, name, href, published, updated, attributedTo, mediatype, size) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)` + + _, err = db.Exec(query, obj.Id, obj.Type, obj.Name, obj.Href, obj.Published, obj.Updated, obj.AttributedTo, obj.MediaType, obj.Size) + return err +} + +func WriteObjectReplyToCache(obj activitypub.ObjectBase) error { + for i, e := range obj.InReplyTo { + res, err := IsReplyInThread(obj.InReplyTo[0].Id, e.Id) + if err != nil { + return err + } + + if i == 0 || res { + query := `select id from replies where id=$1` + + rows, err := db.Query(query, obj.Id) + if err != nil { + return err + } + defer rows.Close() + + var id string + rows.Next() + err = rows.Scan(&id) + if err != nil { + return err + } else if id != "" { + return nil // TODO: error? + } + + query = `insert into cachereplies (id, inreplyto) values ($1, $2)` + + _, err = db.Exec(query, obj.Id, e.Id) + if err != nil { + return err + } + } + } + + if len(obj.InReplyTo) < 1 { + query := `insert into cachereplies (id, inreplyto) values ($1, $2)` + + _, err := db.Exec(query, obj.Id, "") + return err + } + + return nil +} + +func WriteObjectReplyCache(obj activitypub.ObjectBase) error { + if obj.Replies != nil { + for _, e := range obj.Replies.OrderedItems { + + query := `select inreplyto from cachereplies where id=$1` + + rows, err := db.Query(query, obj.Id) + if err != nil { + return err + } + defer rows.Close() + + var inreplyto string + rows.Next() + err = rows.Scan(&inreplyto) + if err != nil { + return err + } else if inreplyto != "" { + return nil // TODO: error? + } + + query = `insert into cachereplies (id, inreplyto) values ($1, $2)` + + if _, err := db.Exec(query, e.Id, obj.Id); err != nil { + return err + } + + if res, err := IsObjectLocal(e.Id); err == nil && !res { + if _, err := WriteObjectToCache(e); err != nil { + return err + } + } else if err != nil { + return err + } + + } + } + + return nil +} + +func WriteActorToCache(actorID string) error { + actor, err := webfinger.FingerActor(actorID) + if err != nil { + return err + } + collection := GetActorCollection(actor.Outbox) + + for _, e := range collection.OrderedItems { + if _, err := WriteActorObjectToCache(e); err != nil { + return err + } + } + + return nil +} + +func DeleteActorCache(actorID string) error { + query := `select id from cacheactivitystream where id in (select id from cacheactivitystream where actor=$1)` + + rows, err := db.Query(query, actorID) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var id string + if err := rows.Scan(&id); err != nil { + return err + } + + if err := DeleteObject(id); err != nil { + return err + } + } + + return nil +} diff --git a/db/database.go b/db/database.go index b06bbf2..8bb7568 100644 --- a/db/database.go +++ b/db/database.go @@ -4,6 +4,7 @@ import ( "database/sql" "fmt" "html/template" + "io/ioutil" "os" "regexp" "sort" @@ -55,7 +56,7 @@ func Close() error { return db.Close() } -func CreateUniqueID(db *sql.DB, actor string) (string, error) { +func CreateUniqueID(actor string) (string, error) { var newID string isUnique := false for !isUnique { @@ -83,6 +84,16 @@ func CreateUniqueID(db *sql.DB, actor string) (string, error) { return newID, nil } +func RunDatabaseSchema() error { + query, err := ioutil.ReadFile("databaseschema.psql") + if err != nil { + return err + } + + _, err = db.Exec(string(query)) + return err +} + func GetActorFromDB(id string) (activitypub.Actor, error) { var nActor activitypub.Actor @@ -222,8 +233,12 @@ func CreateNewBoardDB(actor activitypub.Actor) (activitypub.Actor, error) { nActivity.To = append(nActivity.To, actor.Id) response := AcceptFollow(nActivity) - SetActorFollowingDB(db, response) - MakeActivityRequest(db, nActivity) + if _, err := SetActorFollowingDB(response); err != nil { + return actor, err + } + if err := MakeActivityRequest(nActivity); err != nil { + return actor, err + } } } @@ -256,7 +271,7 @@ func GetBoards() ([]activitypub.Actor, error) { } func WriteObjectToDB(obj activitypub.ObjectBase) (activitypub.ObjectBase, error) { - id, err := CreateUniqueID(db, obj.Actor) + id, err := CreateUniqueID(obj.Actor) if err != nil { return obj, err } @@ -264,7 +279,7 @@ func WriteObjectToDB(obj activitypub.ObjectBase) (activitypub.ObjectBase, error) obj.Id = fmt.Sprintf("%s/%s", obj.Actor, id) if len(obj.Attachment) > 0 { if obj.Preview.Href != "" { - id, err := CreateUniqueID(db, obj.Actor) + id, err := CreateUniqueID(obj.Actor) if err != nil { return obj, err } @@ -277,7 +292,7 @@ func WriteObjectToDB(obj activitypub.ObjectBase) (activitypub.ObjectBase, error) } for i, _ := range obj.Attachment { - id, err := CreateUniqueID(db, obj.Actor) + id, err := CreateUniqueID(obj.Actor) if err != nil { return obj, err } @@ -1023,7 +1038,7 @@ func GetObjectRepliesDBLimit(parent activitypub.ObjectBase, limit int) (*activit nColl.OrderedItems = result - sort.Sort(ObjectBaseSortAsc(nColl.OrderedItems)) + sort.Sort(activitypub.ObjectBaseSortAsc(nColl.OrderedItems)) return &nColl, postCount, attachCount, nil } @@ -2244,6 +2259,9 @@ func UpdateObjectTypeDB(id string, nType string) error { func UnArchiveLast(actorId string) error { col, err := GetActorCollectionDBTypeLimit(actorId, "Archive", 1) + if err != nil { + return err + } for _, e := range col.OrderedItems { for _, k := range e.Replies.OrderedItems { @@ -2296,3 +2314,18 @@ func GetObjectTypeDB(id string) (string, error) { return nType, nil } + +func IsReplyInThread(inReplyTo string, id string) (bool, error) { + obj, _, err := CheckValidActivity(inReplyTo) + if err != nil { + return false, err + } + + for _, e := range obj.OrderedItems[0].Replies.OrderedItems { + if e.Id == id { + return true, nil + } + } + + return false, nil +} diff --git a/db/follow.go b/db/follow.go new file mode 100644 index 0000000..386de2b --- /dev/null +++ b/db/follow.go @@ -0,0 +1,481 @@ +package db + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "regexp" + "strings" + "time" + + "github.com/FChannel0/FChannel-Server/activitypub" + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/util" + "github.com/FChannel0/FChannel-Server/webfinger" + _ "github.com/lib/pq" +) + +func GetActorFollowing(w http.ResponseWriter, id string) error { + var following activitypub.Collection + var err error + + following.AtContext.Context = "https://www.w3.org/ns/activitystreams" + following.Type = "Collection" + following.TotalItems, _, err = GetActorFollowTotal(id) + if err != nil { + return err + } + + following.Items, err = GetActorFollowingDB(id) + if err != nil { + return err + } + + enc, _ := json.MarshalIndent(following, "", "\t") + w.Header().Set("Content-Type", config.ActivityStreams) + _, err = w.Write(enc) + + return err +} + +func GetActorFollowers(w http.ResponseWriter, id string) error { + var following activitypub.Collection + var err error + + following.AtContext.Context = "https://www.w3.org/ns/activitystreams" + following.Type = "Collection" + _, following.TotalItems, err = GetActorFollowTotal(id) + if err != nil { + return err + } + + following.Items, err = GetActorFollowDB(id) + if err != nil { + return err + } + + enc, _ := json.MarshalIndent(following, "", "\t") + w.Header().Set("Content-Type", config.ActivityStreams) + _, err = w.Write(enc) + return err +} + +func GetActorFollowingDB(id string) ([]activitypub.ObjectBase, error) { + var followingCollection []activitypub.ObjectBase + query := `select following from following where id=$1` + + rows, err := db.Query(query, id) + if err != nil { + return followingCollection, err + } + defer rows.Close() + + for rows.Next() { + var obj activitypub.ObjectBase + + if err := rows.Scan(&obj.Id); err != nil { + return followingCollection, err + } + + followingCollection = append(followingCollection, obj) + } + + return followingCollection, nil +} + +func GetActorFollowDB(id string) ([]activitypub.ObjectBase, error) { + var followerCollection []activitypub.ObjectBase + + query := `select follower from follower where id=$1` + + rows, err := db.Query(query, id) + if err != nil { + return followerCollection, err + } + defer rows.Close() + + for rows.Next() { + var obj activitypub.ObjectBase + + if err := rows.Scan(&obj.Id); err != nil { + return followerCollection, err + } + + followerCollection = append(followerCollection, obj) + } + + return followerCollection, nil +} + +func GetActorFollowTotal(id string) (int, int, error) { + var following int + var followers int + + query := `select count(following) from following where id=$1` + + rows, err := db.Query(query, id) + if err != nil { + return 0, 0, err + } + defer rows.Close() + + for rows.Next() { + if err := rows.Scan(&following); err != nil { + return following, 0, err + } + } + + query = `select count(follower) from follower where id=$1` + + rows, err = db.Query(query, id) + if err != nil { + return 0, 0, err + } + defer rows.Close() + + for rows.Next() { + if err := rows.Scan(&followers); err != nil { + return following, followers, err + } + + } + + return following, followers, nil +} + +func AcceptFollow(activity activitypub.Activity) activitypub.Activity { + var accept activitypub.Activity + accept.AtContext.Context = activity.AtContext.Context + accept.Type = "Accept" + var nActor activitypub.Actor + accept.Actor = &nActor + accept.Actor.Id = activity.Object.Actor + var nObj activitypub.ObjectBase + accept.Object = &nObj + accept.Object.Actor = activity.Actor.Id + var nNested activitypub.NestedObjectBase + accept.Object.Object = &nNested + accept.Object.Object.Actor = activity.Object.Actor + accept.Object.Object.Type = "Follow" + accept.To = append(accept.To, activity.Object.Actor) + + return accept +} + +func RejectActivity(activity activitypub.Activity) activitypub.Activity { + var accept activitypub.Activity + accept.AtContext.Context = activity.AtContext.Context + accept.Type = "Reject" + var nObj activitypub.ObjectBase + accept.Object = &nObj + var nActor activitypub.Actor + accept.Actor = &nActor + accept.Actor.Id = activity.Object.Actor + accept.Object.Actor = activity.Actor.Id + var nNested activitypub.NestedObjectBase + accept.Object.Object = &nNested + accept.Object.Object.Actor = activity.Object.Actor + accept.Object.Object.Type = "Follow" + accept.To = append(accept.To, activity.Actor.Id) + + return accept +} + +func IsAlreadyFollowing(actor string, follow string) (bool, error) { + followers, err := GetActorFollowingDB(actor) + if err != nil { + return false, err + } + + for _, e := range followers { + if e.Id == follow { + return true, nil + } + } + + return false, nil +} + +func IsAlreadyFollower(actor string, follow string) (bool, error) { + followers, err := GetActorFollowDB(actor) + if err != nil { + return false, err + } + + for _, e := range followers { + if e.Id == follow { + return true, nil + } + } + + return false, nil +} + +func SetActorFollowerDB(activity activitypub.Activity) (activitypub.Activity, error) { + var query string + alreadyFollow, err := IsAlreadyFollower(activity.Actor.Id, activity.Object.Actor) + if err != nil { + return activity, err + } + + activity.Type = "Reject" + if activity.Actor.Id == activity.Object.Actor { + return activity, nil + } + + if alreadyFollow { + query = `delete from follower where id=$1 and follower=$2` + activity.Summary = activity.Object.Actor + " Unfollow " + activity.Actor.Id + + if _, err := db.Exec(query, activity.Actor.Id, activity.Object.Actor); err != nil { + return activity, err + } + + activity.Type = "Accept" + return activity, err + } + + query = `insert into follower (id, follower) values ($1, $2)` + activity.Summary = activity.Object.Actor + " Follow " + activity.Actor.Id + + if _, err := db.Exec(query, activity.Actor.Id, activity.Object.Actor); err != nil { + return activity, err + } + + activity.Type = "Accept" + return activity, nil +} + +func SetActorFollowingDB(activity activitypub.Activity) (activitypub.Activity, error) { + var query string + alreadyFollowing := false + alreadyFollower := false + following, err := GetActorFollowingDB(activity.Object.Actor) + if err != nil { + return activity, err + } + + actor, err := webfinger.FingerActor(activity.Actor.Id) + if err != nil { + return activity, err + } + + remoteActorFollowerCol := GetCollectionFromReq(actor.Followers) + + for _, e := range following { + if e.Id == activity.Actor.Id { + alreadyFollowing = true + } + } + + for _, e := range remoteActorFollowerCol.Items { + if e.Id == activity.Object.Actor { + alreadyFollower = true + } + } + + activity.Type = "Reject" + + if activity.Actor.Id == activity.Object.Actor { + return activity, nil + } + + if alreadyFollowing && alreadyFollower { + query = `delete from following where id=$1 and following=$2` + activity.Summary = activity.Object.Actor + " Unfollowing " + activity.Actor.Id + if res, err := IsActorLocal(activity.Actor.Id); err == nil && !res { + go DeleteActorCache(activity.Actor.Id) + } else { + return activity, err + } + + if _, err := db.Exec(query, activity.Object.Actor, activity.Actor.Id); err != nil { + return activity, err + } + + activity.Type = "Accept" + return activity, nil + } + + if !alreadyFollowing && !alreadyFollower { + + query = `insert into following (id, following) values ($1, $2)` + activity.Summary = activity.Object.Actor + " Following " + activity.Actor.Id + if res, err := IsActorLocal(activity.Actor.Id); err == nil && !res { + go WriteActorToCache(activity.Actor.Id) + } + if _, err := db.Exec(query, activity.Object.Actor, activity.Actor.Id); err != nil { + return activity, err + } + + activity.Type = "Accept" + return activity, nil + } + + return activity, nil +} + +func AutoFollow(actor string) error { + following, err := GetActorFollowingDB(actor) + if err != nil { + return err + } + + follower, err := GetActorFollowDB(actor) + if err != nil { + return err + } + + isFollowing := false + + for _, e := range follower { + for _, k := range following { + if e.Id == k.Id { + isFollowing = true + } + } + + if !isFollowing && e.Id != config.Domain && e.Id != actor { + followActivity, err := MakeFollowActivity(actor, e.Id) + if err != nil { + return err + } + + nActor, err := webfinger.FingerActor(e.Id) + if err != nil { + return err + } + + if nActor.Id != "" { + MakeActivityRequestOutbox(followActivity) + } + } + } + + return nil +} + +func MakeFollowActivity(actor string, follow string) (activitypub.Activity, error) { + var followActivity activitypub.Activity + var err error + + followActivity.AtContext.Context = "https://www.w3.org/ns/activitystreams" + followActivity.Type = "Follow" + + var obj activitypub.ObjectBase + var nactor activitypub.Actor + if actor == config.Domain { + nactor, err = GetActorFromDB(actor) + } else { + nactor, err = webfinger.FingerActor(actor) + } + + if err != nil { + return followActivity, err + } + + followActivity.Actor = &nactor + followActivity.Object = &obj + + followActivity.Object.Actor = follow + followActivity.To = append(followActivity.To, follow) + + return followActivity, nil +} + +func MakeActivityRequestOutbox(activity activitypub.Activity) error { + j, _ := json.Marshal(activity) + + if activity.Actor.Outbox == "" { + // TODO: good enough? + return errors.New("invalid outbox") + } + + req, err := http.NewRequest("POST", activity.Actor.Outbox, bytes.NewBuffer(j)) + if err != nil { + return err + } + + re := regexp.MustCompile("https?://(www.)?") + + var instance string + if activity.Actor.Id == config.Domain { + instance = re.ReplaceAllString(config.Domain, "") + } else { + _, instance = util.GetActorInstance(activity.Actor.Id) + } + + date := time.Now().UTC().Format(time.RFC1123) + path := strings.Replace(activity.Actor.Outbox, instance, "", 1) + + path = re.ReplaceAllString(path, "") + + sig := fmt.Sprintf("(request-target): %s %s\nhost: %s\ndate: %s", "post", path, instance, date) + encSig, err := ActivitySign(*activity.Actor, sig) + if err != nil { + return err + } + + signature := fmt.Sprintf("keyId=\"%s\",headers=\"(request-target) host date\",signature=\"%s\"", activity.Actor.PublicKey.Id, encSig) + + req.Header.Set("Content-Type", config.ActivityStreams) + req.Header.Set("Date", date) + req.Header.Set("Signature", signature) + req.Host = instance + + _, err = util.RouteProxy(req) + return err +} + +func MakeActivityRequest(activity activitypub.Activity) error { + j, _ := json.MarshalIndent(activity, "", "\t") + + for _, e := range activity.To { + if e != activity.Actor.Id { + actor, err := webfinger.FingerActor(e) + if err != nil { + return err + } + + if actor.Id != "" { + _, instance := util.GetActorInstance(actor.Id) + + if actor.Inbox != "" { + req, err := http.NewRequest("POST", actor.Inbox, bytes.NewBuffer(j)) + if err != nil { + return err + } + + date := time.Now().UTC().Format(time.RFC1123) + path := strings.Replace(actor.Inbox, instance, "", 1) + + re := regexp.MustCompile("https?://(www.)?") + path = re.ReplaceAllString(path, "") + + sig := fmt.Sprintf("(request-target): %s %s\nhost: %s\ndate: %s", "post", path, instance, date) + encSig, err := ActivitySign(*activity.Actor, sig) + if err != nil { + return err + } + + signature := fmt.Sprintf("keyId=\"%s\",headers=\"(request-target) host date\",signature=\"%s\"", activity.Actor.PublicKey.Id, encSig) + + req.Header.Set("Content-Type", config.ActivityStreams) + req.Header.Set("Date", date) + req.Header.Set("Signature", signature) + req.Host = instance + + _, err = util.RouteProxy(req) + if err != nil { + fmt.Println("error with sending activity resp to actor " + instance) + return err // TODO: needs further testing + } + } + } + } + } + + return nil +} @@ -1,10 +1,24 @@ package db import ( + "crypto" + crand "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "net/http" "os" + "regexp" "strings" + "time" "github.com/FChannel0/FChannel-Server/activitypub" + "github.com/FChannel0/FChannel-Server/webfinger" ) func GetActorPemFromDB(pemID string) (activitypub.PublicKeyPem, error) { @@ -46,3 +60,302 @@ func GetActorPemFileFromDB(pemID string) (string, error) { return file, nil } + +func CreatePem(actor activitypub.Actor) error { + privatekey, err := rsa.GenerateKey(crand.Reader, 2048) + if err != nil { + return err + } + + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privatekey) + + privateKeyBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + } + + privatePem, err := os.Create("./pem/board/" + actor.Name + "-private.pem") + if err != nil { + return err + } + + if err := pem.Encode(privatePem, privateKeyBlock); err != nil { + return err + } + + publickey := &privatekey.PublicKey + publicKeyBytes, err := x509.MarshalPKIXPublicKey(publickey) + if err != nil { + return err + } + + publicKeyBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicKeyBytes, + } + + publicPem, err := os.Create("./pem/board/" + actor.Name + "-public.pem") + if err != nil { + return err + } + + if err := pem.Encode(publicPem, publicKeyBlock); err != nil { + return err + } + + _, err = os.Stat("./pem/board/" + actor.Name + "-public.pem") + if os.IsNotExist(err) { + return err + } else { + return StorePemToDB(actor) + } + + fmt.Println(`Created PEM keypair for the "` + actor.Name + `" board. Please keep in mind that +the PEM key is crucial in identifying yourself as the legitimate owner of the board, +so DO NOT LOSE IT!!! If you lose it, YOU WILL LOSE ACCESS TO YOUR BOARD!`) + + return nil +} + +func CreatePublicKeyFromPrivate(actor *activitypub.Actor, publicKeyPem string) error { + publicFilename, err := GetActorPemFileFromDB(publicKeyPem) + if err != nil { + return err + } + + privateFilename := strings.ReplaceAll(publicFilename, "public.pem", "private.pem") + if _, err := os.Stat(privateFilename); err == nil { + // Not a lost cause + priv, err := ioutil.ReadFile(privateFilename) + if err != nil { + return err + } + + block, _ := pem.Decode([]byte(priv)) + if block == nil || block.Type != "RSA PRIVATE KEY" { + return errors.New("failed to decode PEM block containing public key") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return err + } + + publicKeyDer, err := x509.MarshalPKIXPublicKey(&key.PublicKey) + if err != nil { + return err + } + + pubKeyBlock := pem.Block{ + Type: "PUBLIC KEY", + Headers: nil, + Bytes: publicKeyDer, + } + + publicFileWriter, err := os.Create(publicFilename) + if err != nil { + return err + } + + if err := pem.Encode(publicFileWriter, &pubKeyBlock); err != nil { + return err + } + } else { + fmt.Println(`\nUnable to locate private key from public key generation. Now, +this means that you are now missing the proof that you are the +owner of the "` + actor.Name + `" board. If you are the developer, +then your job is just as easy as generating a new keypair, but +if this board is live, then you'll also have to convince the other +owners to switch their public keys for you so that they will start +accepting your posts from your board from this site. Good luck ;)`) + return errors.New("unable to locate private key") + } + return nil +} + +func StorePemToDB(actor activitypub.Actor) error { + query := "select publicKeyPem from actor where id=$1" + rows, err := db.Query(query, actor.Id) + if err != nil { + return err + } + + defer rows.Close() + + var result string + rows.Next() + rows.Scan(&result) + + if result != "" { + return errors.New("already storing public key for actor") + } + + publicKeyPem := actor.Id + "#main-key" + query = "update actor set publicKeyPem=$1 where id=$2" + if _, err := db.Exec(query, publicKeyPem, actor.Id); err != nil { + return err + } + + file := "./pem/board/" + actor.Name + "-public.pem" + query = "insert into publicKeyPem (id, owner, file) values($1, $2, $3)" + _, err = db.Exec(query, publicKeyPem, actor.Id, file) + return err +} +func ActivitySign(actor activitypub.Actor, signature string) (string, error) { + query := `select file from publicKeyPem where id=$1 ` + + rows, err := db.Query(query, actor.PublicKey.Id) + if err != nil { + return "", err + } + + defer rows.Close() + + var file string + rows.Next() + rows.Scan(&file) + + file = strings.ReplaceAll(file, "public.pem", "private.pem") + _, err = os.Stat(file) + if err != nil { + fmt.Println(`\n Unable to locate private key. Now, +this means that you are now missing the proof that you are the +owner of the "` + actor.Name + `" board. If you are the developer, +then your job is just as easy as generating a new keypair, but +if this board is live, then you'll also have to convince the other +owners to switch their public keys for you so that they will start +accepting your posts from your board from this site. Good luck ;)`) + return "", errors.New("unable to locate private key") + } + + publickey, err := ioutil.ReadFile(file) + if err != nil { + return "", err + } + + block, _ := pem.Decode(publickey) + + pub, _ := x509.ParsePKCS1PrivateKey(block.Bytes) + rng := crand.Reader + hashed := sha256.New() + hashed.Write([]byte(signature)) + cipher, _ := rsa.SignPKCS1v15(rng, pub, crypto.SHA256, hashed.Sum(nil)) + + return base64.StdEncoding.EncodeToString(cipher), nil +} + +func ActivityVerify(actor activitypub.Actor, signature string, verify string) error { + sig, _ := base64.StdEncoding.DecodeString(signature) + + if actor.PublicKey.PublicKeyPem == "" { + _actor, err := webfinger.FingerActor(actor.Id) + if err != nil { + return err + } + actor = _actor + } + + block, _ := pem.Decode([]byte(actor.PublicKey.PublicKeyPem)) + pub, _ := x509.ParsePKIXPublicKey(block.Bytes) + + hashed := sha256.New() + hashed.Write([]byte(verify)) + + return rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA256, hashed.Sum(nil), sig) +} + +func VerifyHeaderSignature(r *http.Request, actor activitypub.Actor) bool { + s := ParseHeaderSignature(r.Header.Get("Signature")) + + var method string + var path string + var host string + var date string + var digest string + var contentLength string + + var sig string + for i, e := range s.Headers { + var nl string + if i < len(s.Headers)-1 { + nl = "\n" + } + + switch e { + case "(request-target)": + method = strings.ToLower(r.Method) + path = r.URL.Path + sig += "(request-target): " + method + " " + path + "" + nl + break + case "host": + host = r.Host + sig += "host: " + host + "" + nl + break + case "date": + date = r.Header.Get("date") + sig += "date: " + date + "" + nl + break + case "digest": + digest = r.Header.Get("digest") + sig += "digest: " + digest + "" + nl + break + case "content-length": + contentLength = r.Header.Get("content-length") + sig += "content-length: " + contentLength + "" + nl + break + } + } + + if s.KeyId != actor.PublicKey.Id { + return false + } + + t, _ := time.Parse(time.RFC1123, date) + + if time.Now().UTC().Sub(t).Seconds() > 75 { + return false + } + + if ActivityVerify(actor, s.Signature, sig) != nil { + return false + } + + return true +} + +func ParseHeaderSignature(signature string) Signature { + var nsig Signature + + keyId := regexp.MustCompile(`keyId=`) + headers := regexp.MustCompile(`headers=`) + sig := regexp.MustCompile(`signature=`) + algo := regexp.MustCompile(`algorithm=`) + + signature = strings.ReplaceAll(signature, "\"", "") + parts := strings.Split(signature, ",") + + for _, e := range parts { + if keyId.MatchString(e) { + nsig.KeyId = keyId.ReplaceAllString(e, "") + continue + } + + if headers.MatchString(e) { + header := headers.ReplaceAllString(e, "") + nsig.Headers = strings.Split(header, " ") + continue + } + + if sig.MatchString(e) { + nsig.Signature = sig.ReplaceAllString(e, "") + continue + } + + if algo.MatchString(e) { + nsig.Algorithm = algo.ReplaceAllString(e, "") + continue + } + } + + return nsig +} diff --git a/db/verification.go b/db/verification.go index b976908..8be2ffe 100644 --- a/db/verification.go +++ b/db/verification.go @@ -1,13 +1,6 @@ package db import ( - "crypto" - "crypto/rsa" - "crypto/sha256" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "errors" "fmt" "math/rand" "net/smtp" @@ -15,14 +8,9 @@ import ( "os/exec" "time" - "github.com/FChannel0/FChannel-Server/activitypub" + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/util" _ "github.com/lib/pq" - - crand "crypto/rand" - "io/ioutil" - "net/http" - "regexp" - "strings" ) type Verify struct { @@ -97,7 +85,7 @@ func GetBoardMod(identifier string) (Verify, error) { } func CreateBoardMod(verify Verify) error { - pass := CreateKey(50) + pass := util.CreateKey(50) query := `select code from verification where identifier=$1 and type=$2` @@ -307,8 +295,8 @@ func VerficationCooldownRemove() error { func SendVerification(verify Verify) error { fmt.Println("sending email") - from := SiteEmail - pass := SiteEmailPassword + from := config.SiteEmail + pass := config.SiteEmailPassword to := verify.Identifier body := fmt.Sprintf("You can use either\r\nEmail: %s \r\n Verfication Code: %s\r\n for the board %s", verify.Identifier, verify.Code, verify.Board) @@ -317,23 +305,13 @@ func SendVerification(verify Verify) error { "Subject: Image Board Verification\n\n" + body - return smtp.SendMail(SiteEmailServer+":"+SiteEmailPort, - smtp.PlainAuth("", from, pass, SiteEmailServer), + return smtp.SendMail(config.SiteEmailServer+":"+config.SiteEmailPort, + smtp.PlainAuth("", from, pass, config.SiteEmailServer), from, []string{to}, []byte(msg)) } func IsEmailSetup() bool { - if SiteEmail == "" { - return false - } else if SiteEmailPassword == "" { - return false - } else if SiteEmailServer == "" { - return false - } else if SiteEmailPort == "" { - return false - } - - return true + return config.SiteEmail != "" || config.SiteEmailPassword != "" || config.SiteEmailServer != "" || config.SiteEmailPort != "" } func HasAuth(code string, board string) (bool, error) { @@ -342,8 +320,10 @@ func HasAuth(code string, board string) (bool, error) { return false, err } - if verify.Board == Domain || (HasBoardAccess(db, verify) && verify.Board == board) { + if res, err := HasBoardAccess(verify); err != nil && (verify.Board == config.Domain || (res && verify.Board == board)) { return true, nil + } else { + return false, err } return false, nil @@ -377,12 +357,12 @@ func GetVerify(access string) (Verify, error) { } func CreateNewCaptcha() error { - id := RandomID(8) + id := util.RandomID(8) file := "public/" + id + ".png" for true { if _, err := os.Stat("./" + file); err == nil { - id = RandomID(8) + id = util.RandomID(8) file = "public/" + id + ".png" } else { break @@ -506,299 +486,3 @@ func Captcha() string { return newID } - -func CreatePem(actor Actor) error { - privatekey, err := rsa.GenerateKey(crand.Reader, 2048) - if err != nil { - return err - } - - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privatekey) - - privateKeyBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privateKeyBytes, - } - - privatePem, err := os.Create("./pem/board/" + actor.Name + "-private.pem") - if err != nil { - return err - } - - if err := pem.Encode(privatePem, privateKeyBlock); err != nil { - return err - } - - publickey := &privatekey.PublicKey - publicKeyBytes, err := x509.MarshalPKIXPublicKey(publickey) - if err != nil { - return err - } - - publicKeyBlock := &pem.Block{ - Type: "PUBLIC KEY", - Bytes: publicKeyBytes, - } - - publicPem, err := os.Create("./pem/board/" + actor.Name + "-public.pem") - if err != nil { - return err - } - - if err := pem.Encode(publicPem, publicKeyBlock); err != nil { - return err - } - - _, err = os.Stat("./pem/board/" + actor.Name + "-public.pem") - if os.IsNotExist(err) { - return err - } else { - return StorePemToDB(actor) - } - - fmt.Println(`Created PEM keypair for the "` + actor.Name + `" board. Please keep in mind that -the PEM key is crucial in identifying yourself as the legitimate owner of the board, -so DO NOT LOSE IT!!! If you lose it, YOU WILL LOSE ACCESS TO YOUR BOARD!`) - - return nil -} - -func CreatePublicKeyFromPrivate(actor *activitypub.Actor, publicKeyPem string) error { - publicFilename, err := GetActorPemFileFromDB(publicKeyPem) - if err != nil { - return err - } - - privateFilename := strings.ReplaceAll(publicFilename, "public.pem", "private.pem") - if _, err := os.Stat(privateFilename); err == nil { - // Not a lost cause - priv, err := ioutil.ReadFile(privateFilename) - if err != nil { - return err - } - - block, _ := pem.Decode([]byte(priv)) - if block == nil || block.Type != "RSA PRIVATE KEY" { - return errors.New("failed to decode PEM block containing public key") - } - - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return err - } - - publicKeyDer, err := x509.MarshalPKIXPublicKey(&key.PublicKey) - if err != nil { - return err - } - - pubKeyBlock := pem.Block{ - Type: "PUBLIC KEY", - Headers: nil, - Bytes: publicKeyDer, - } - - publicFileWriter, err := os.Create(publicFilename) - if err != nil { - return err - } - - if err := pem.Encode(publicFileWriter, &pubKeyBlock); err != nil { - return err - } - } else { - fmt.Println(`\nUnable to locate private key from public key generation. Now, -this means that you are now missing the proof that you are the -owner of the "` + actor.Name + `" board. If you are the developer, -then your job is just as easy as generating a new keypair, but -if this board is live, then you'll also have to convince the other -owners to switch their public keys for you so that they will start -accepting your posts from your board from this site. Good luck ;)`) - return errors.New("unable to locate private key") - } - return nil -} - -func StorePemToDB(actor activitypub.Actor) error { - query := "select publicKeyPem from actor where id=$1" - rows, err := db.Query(query, actor.Id) - if err != nil { - return err - } - - defer rows.Close() - - var result string - rows.Next() - rows.Scan(&result) - - if result != "" { - return errors.New("already storing public key for actor") - } - - publicKeyPem := actor.Id + "#main-key" - query = "update actor set publicKeyPem=$1 where id=$2" - if _, err := db.Exec(query, publicKeyPem, actor.Id); err != nil { - return err - } - - file := "./pem/board/" + actor.Name + "-public.pem" - query = "insert into publicKeyPem (id, owner, file) values($1, $2, $3)" - _, err = db.Exec(query, publicKeyPem, actor.Id, file) - return err -} - -func ActivitySign(actor activitypub.Actor, signature string) (string, error) { - query := `select file from publicKeyPem where id=$1 ` - - rows, err := db.Query(query, actor.PublicKey.Id) - if err != nil { - return "", err - } - - defer rows.Close() - - var file string - rows.Next() - rows.Scan(&file) - - file = strings.ReplaceAll(file, "public.pem", "private.pem") - _, err = os.Stat(file) - if err != nil { - fmt.Println(`\n Unable to locate private key. Now, -this means that you are now missing the proof that you are the -owner of the "` + actor.Name + `" board. If you are the developer, -then your job is just as easy as generating a new keypair, but -if this board is live, then you'll also have to convince the other -owners to switch their public keys for you so that they will start -accepting your posts from your board from this site. Good luck ;)`) - return "", errors.New("unable to locate private key") - } - - publickey, err := ioutil.ReadFile(file) - if err != nil { - return "", err - } - - block, _ := pem.Decode(publickey) - - pub, _ := x509.ParsePKCS1PrivateKey(block.Bytes) - rng := crand.Reader - hashed := sha256.New() - hashed.Write([]byte(signature)) - cipher, _ := rsa.SignPKCS1v15(rng, pub, crypto.SHA256, hashed.Sum(nil)) - - return base64.StdEncoding.EncodeToString(cipher), nil -} - -func ActivityVerify(actor activitypub.Actor, signature string, verify string) error { - sig, _ := base64.StdEncoding.DecodeString(signature) - - if actor.PublicKey.PublicKeyPem == "" { - actor = FingerActor(actor.Id) - } - - block, _ := pem.Decode([]byte(actor.PublicKey.PublicKeyPem)) - pub, _ := x509.ParsePKIXPublicKey(block.Bytes) - - hashed := sha256.New() - hashed.Write([]byte(verify)) - - return rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA256, hashed.Sum(nil), sig) -} - -func VerifyHeaderSignature(r *http.Request, actor activitypub.Actor) bool { - s := ParseHeaderSignature(r.Header.Get("Signature")) - - var method string - var path string - var host string - var date string - var digest string - var contentLength string - - var sig string - for i, e := range s.Headers { - var nl string - if i < len(s.Headers)-1 { - nl = "\n" - } - - switch e { - case "(request-target)": - method = strings.ToLower(r.Method) - path = r.URL.Path - sig += "(request-target): " + method + " " + path + "" + nl - break - case "host": - host = r.Host - sig += "host: " + host + "" + nl - break - case "date": - date = r.Header.Get("date") - sig += "date: " + date + "" + nl - break - case "digest": - digest = r.Header.Get("digest") - sig += "digest: " + digest + "" + nl - break - case "content-length": - contentLength = r.Header.Get("content-length") - sig += "content-length: " + contentLength + "" + nl - break - } - } - - if s.KeyId != actor.PublicKey.Id { - return false - } - - t, _ := time.Parse(time.RFC1123, date) - - if time.Now().UTC().Sub(t).Seconds() > 75 { - return false - } - - if ActivityVerify(actor, s.Signature, sig) != nil { - return false - } - - return true -} - -func ParseHeaderSignature(signature string) Signature { - var nsig Signature - - keyId := regexp.MustCompile(`keyId=`) - headers := regexp.MustCompile(`headers=`) - sig := regexp.MustCompile(`signature=`) - algo := regexp.MustCompile(`algorithm=`) - - signature = strings.ReplaceAll(signature, "\"", "") - parts := strings.Split(signature, ",") - - for _, e := range parts { - if keyId.MatchString(e) { - nsig.KeyId = keyId.ReplaceAllString(e, "") - continue - } - - if headers.MatchString(e) { - header := headers.ReplaceAllString(e, "") - nsig.Headers = strings.Split(header, " ") - continue - } - - if sig.MatchString(e) { - nsig.Signature = sig.ReplaceAllString(e, "") - continue - } - - if algo.MatchString(e) { - nsig.Algorithm = algo.ReplaceAllString(e, "") - continue - } - } - - return nsig -} diff --git a/follow.go b/follow.go deleted file mode 100644 index 49558cd..0000000 --- a/follow.go +++ /dev/null @@ -1,331 +0,0 @@ -package main - -import ( - "database/sql" - "encoding/json" - "net/http" - - _ "github.com/lib/pq" -) - -func GetActorFollowing(w http.ResponseWriter, db *sql.DB, id string) { - var following Collection - - following.AtContext.Context = "https://www.w3.org/ns/activitystreams" - following.Type = "Collection" - following.TotalItems, _ = GetActorFollowTotal(db, id) - following.Items = GetActorFollowingDB(db, id) - - enc, _ := json.MarshalIndent(following, "", "\t") - w.Header().Set("Content-Type", activitystreams) - w.Write(enc) -} - -func GetActorFollowers(w http.ResponseWriter, db *sql.DB, id string) { - var following Collection - - following.AtContext.Context = "https://www.w3.org/ns/activitystreams" - following.Type = "Collection" - _, following.TotalItems = GetActorFollowTotal(db, id) - following.Items = GetActorFollowDB(db, id) - - enc, _ := json.MarshalIndent(following, "", "\t") - w.Header().Set("Content-Type", activitystreams) - w.Write(enc) -} - -func GetActorFollowingDB(db *sql.DB, id string) []ObjectBase { - var followingCollection []ObjectBase - query := `select following from following where id=$1` - - rows, err := db.Query(query, id) - - CheckError(err, "error with following db query") - - defer rows.Close() - - for rows.Next() { - var obj ObjectBase - - err := rows.Scan(&obj.Id) - - CheckError(err, "error with following db scan") - - followingCollection = append(followingCollection, obj) - } - - return followingCollection -} - -func GetActorFollowDB(db *sql.DB, id string) []ObjectBase { - var followerCollection []ObjectBase - - query := `select follower from follower where id=$1` - - rows, err := db.Query(query, id) - - CheckError(err, "error with follower db query") - - defer rows.Close() - - for rows.Next() { - var obj ObjectBase - - err := rows.Scan(&obj.Id) - - CheckError(err, "error with followers db scan") - - followerCollection = append(followerCollection, obj) - } - - return followerCollection -} - -func GetActorFollowTotal(db *sql.DB, id string) (int, int) { - var following int - var followers int - - query := `select count(following) from following where id=$1` - - rows, err := db.Query(query, id) - - CheckError(err, "error with following total db query") - - defer rows.Close() - - for rows.Next() { - err := rows.Scan(&following) - - CheckError(err, "error with following total db scan") - } - - query = `select count(follower) from follower where id=$1` - - rows, err = db.Query(query, id) - - CheckError(err, "error with followers total db query") - - defer rows.Close() - - for rows.Next() { - err := rows.Scan(&followers) - - CheckError(err, "error with followers total db scan") - } - - return following, followers -} - -func AcceptFollow(activity Activity) Activity { - var accept Activity - accept.AtContext.Context = activity.AtContext.Context - accept.Type = "Accept" - var nActor Actor - accept.Actor = &nActor - accept.Actor.Id = activity.Object.Actor - var nObj ObjectBase - accept.Object = &nObj - accept.Object.Actor = activity.Actor.Id - var nNested NestedObjectBase - accept.Object.Object = &nNested - accept.Object.Object.Actor = activity.Object.Actor - accept.Object.Object.Type = "Follow" - accept.To = append(accept.To, activity.Object.Actor) - - return accept -} - -func RejectActivity(activity Activity) Activity { - var accept Activity - accept.AtContext.Context = activity.AtContext.Context - accept.Type = "Reject" - var nObj ObjectBase - accept.Object = &nObj - var nActor Actor - accept.Actor = &nActor - accept.Actor.Id = activity.Object.Actor - accept.Object.Actor = activity.Actor.Id - var nNested NestedObjectBase - accept.Object.Object = &nNested - accept.Object.Object.Actor = activity.Object.Actor - accept.Object.Object.Type = "Follow" - accept.To = append(accept.To, activity.Actor.Id) - - return accept -} - -func IsAlreadyFollowing(db *sql.DB, actor string, follow string) bool { - followers := GetActorFollowingDB(db, actor) - - for _, e := range followers { - if e.Id == follow { - return true - } - } - - return false -} - -func IsAlreadyFollower(db *sql.DB, actor string, follow string) bool { - followers := GetActorFollowDB(db, actor) - - for _, e := range followers { - if e.Id == follow { - return true - } - } - - return false -} - -func SetActorFollowerDB(db *sql.DB, activity Activity) Activity { - var query string - alreadyFollow := IsAlreadyFollower(db, activity.Actor.Id, activity.Object.Actor) - - activity.Type = "Reject" - if activity.Actor.Id == activity.Object.Actor { - return activity - } - - if alreadyFollow { - query = `delete from follower where id=$1 and follower=$2` - activity.Summary = activity.Object.Actor + " Unfollow " + activity.Actor.Id - - _, err := db.Exec(query, activity.Actor.Id, activity.Object.Actor) - - if CheckError(err, "error with follower db delete") != nil { - activity.Type = "Reject" - return activity - } - - activity.Type = "Accept" - return activity - } else { - query = `insert into follower (id, follower) values ($1, $2)` - activity.Summary = activity.Object.Actor + " Follow " + activity.Actor.Id - - _, err := db.Exec(query, activity.Actor.Id, activity.Object.Actor) - - if CheckError(err, "error with follower db insert") != nil { - activity.Type = "Reject" - return activity - } - - activity.Type = "Accept" - return activity - } -} - -func SetActorFollowingDB(db *sql.DB, activity Activity) Activity { - var query string - alreadyFollowing := false - alreadyFollower := false - following := GetActorFollowingDB(db, activity.Object.Actor) - - actor := FingerActor(activity.Actor.Id) - - remoteActorFollowerCol := GetCollectionFromReq(actor.Followers) - - for _, e := range following { - if e.Id == activity.Actor.Id { - alreadyFollowing = true - } - } - - for _, e := range remoteActorFollowerCol.Items { - if e.Id == activity.Object.Actor { - alreadyFollower = true - } - } - - activity.Type = "Reject" - - if activity.Actor.Id == activity.Object.Actor { - return activity - } - - if alreadyFollowing && alreadyFollower { - query = `delete from following where id=$1 and following=$2` - activity.Summary = activity.Object.Actor + " Unfollowing " + activity.Actor.Id - if !IsActorLocal(db, activity.Actor.Id) { - go DeleteActorCache(db, activity.Actor.Id) - } - _, err := db.Exec(query, activity.Object.Actor, activity.Actor.Id) - - if CheckError(err, "error with following db delete") != nil { - activity.Type = "Reject" - return activity - } - - activity.Type = "Accept" - return activity - } - - if !alreadyFollowing && !alreadyFollower { - - query = `insert into following (id, following) values ($1, $2)` - activity.Summary = activity.Object.Actor + " Following " + activity.Actor.Id - if !IsActorLocal(db, activity.Actor.Id) { - go WriteActorToCache(db, activity.Actor.Id) - } - _, err := db.Exec(query, activity.Object.Actor, activity.Actor.Id) - - if CheckError(err, "error with following db insert") != nil { - activity.Type = "Reject" - return activity - } - - activity.Type = "Accept" - return activity - } - - return activity -} - -func AutoFollow(db *sql.DB, actor string) { - following := GetActorFollowingDB(db, actor) - follower := GetActorFollowDB(db, actor) - - isFollowing := false - - for _, e := range follower { - for _, k := range following { - if e.Id == k.Id { - isFollowing = true - } - } - - if !isFollowing && e.Id != Domain && e.Id != actor { - followActivity := MakeFollowActivity(db, actor, e.Id) - - nActor := FingerActor(e.Id) - - if nActor.Id != "" { - MakeActivityRequestOutbox(db, followActivity) - } - } - } -} - -func MakeFollowActivity(db *sql.DB, actor string, follow string) Activity { - var followActivity Activity - - followActivity.AtContext.Context = "https://www.w3.org/ns/activitystreams" - followActivity.Type = "Follow" - - var obj ObjectBase - var nactor Actor - if actor == Domain { - nactor = GetActorFromDB(db, actor) - } else { - nactor = FingerActor(actor) - } - - followActivity.Actor = &nactor - followActivity.Object = &obj - - followActivity.Object.Actor = follow - followActivity.To = append(followActivity.To, follow) - - return followActivity -} @@ -1,7 +1,6 @@ package main import ( - "bytes" "crypto/sha256" "database/sql" "encoding/hex" @@ -26,7 +25,6 @@ import ( "math/rand" "mime/multipart" "net/http" - "net/url" "os" "os/exec" "path" @@ -37,14 +35,8 @@ import ( var authReq = []string{"captcha", "email", "passphrase"} -var supportedFiles = []string{"image/gif", "image/jpeg", "image/png", "image/webp", "image/apng", "video/mp4", "video/ogg", "video/webm", "audio/mpeg", "audio/ogg", "audio/wav", "audio/wave", "audio/x-wav"} - -var activitystreams = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - var MediaHashs = make(map[string]string) -var ActorCache = make(map[string]activitypub.Actor) - var Themes []string func init() { @@ -60,7 +52,7 @@ func main() { db.ConnectDB() defer db.Close() - RunDatabaseSchema(DB) + db.RunDatabaseSchema() go MakeCaptchas(DB, 100) @@ -573,49 +565,8 @@ func CheckValidActivity(id string) (Collection, bool) { return respCollection, false } -func GetActor(id string) Actor { - - var respActor Actor - - if id == "" { - return respActor - } - - actor, instance := GetActorInstance(id) - - if ActorCache[actor+"@"+instance].Id != "" { - respActor = ActorCache[actor+"@"+instance] - } else { - req, err := http.NewRequest("GET", strings.TrimSpace(id), nil) - - CheckError(err, "error with getting actor req") - - req.Header.Set("Accept", activitystreams) - - resp, err := RouteProxy(req) - - if err != nil { - return respActor - } - - defer resp.Body.Close() - - body, _ := ioutil.ReadAll(resp.Body) - - err = json.Unmarshal(body, &respActor) - - if err != nil { - return respActor - } - - ActorCache[actor+"@"+instance] = respActor - } - - return respActor -} - -func GetActorCollection(collection string) Collection { - var nCollection Collection +func GetActorCollection(collection string) activitypub.Collection { + var nCollection activitypub.Collection if collection == "" { return nCollection @@ -708,17 +659,6 @@ func GetFileContentType(out multipart.File) (string, error) { return contentType, nil } -func IsReplyInThread(db *sql.DB, inReplyTo string, id string) bool { - obj, _ := CheckValidActivity(inReplyTo) - - for _, e := range obj.OrderedItems[0].Replies.OrderedItems { - if e.Id == id { - return true - } - } - return false -} - func SupportedMIMEType(mime string) bool { for _, e := range supportedFiles { if e == mime { @@ -758,91 +698,6 @@ func GetActorReported(w http.ResponseWriter, r *http.Request, db *sql.DB, id str w.Write(enc) } -func MakeActivityRequestOutbox(db *sql.DB, activity Activity) { - j, _ := json.Marshal(activity) - - if activity.Actor.Outbox == "" { - return - } - - req, err := http.NewRequest("POST", activity.Actor.Outbox, bytes.NewBuffer(j)) - - CheckError(err, "error with sending activity req to outbox") - - re := regexp.MustCompile("https?://(www.)?") - - var instance string - if activity.Actor.Id == Domain { - instance = re.ReplaceAllString(Domain, "") - } else { - _, instance = GetActorInstance(activity.Actor.Id) - } - - date := time.Now().UTC().Format(time.RFC1123) - path := strings.Replace(activity.Actor.Outbox, instance, "", 1) - - path = re.ReplaceAllString(path, "") - - sig := fmt.Sprintf("(request-target): %s %s\nhost: %s\ndate: %s", "post", path, instance, date) - encSig, err := ActivitySign(db, *activity.Actor, sig) - CheckError(err, "unable to sign activity response") - signature := fmt.Sprintf("keyId=\"%s\",headers=\"(request-target) host date\",signature=\"%s\"", activity.Actor.PublicKey.Id, encSig) - - req.Header.Set("Content-Type", activitystreams) - req.Header.Set("Date", date) - req.Header.Set("Signature", signature) - req.Host = instance - - _, err = RouteProxy(req) - - CheckError(err, "error with sending activity resp to") -} - -func MakeActivityRequest(db *sql.DB, activity Activity) { - - j, _ := json.MarshalIndent(activity, "", "\t") - - for _, e := range activity.To { - if e != activity.Actor.Id { - - actor := FingerActor(e) - - if actor.Id != "" { - _, instance := GetActorInstance(actor.Id) - - if actor.Inbox != "" { - - req, err := http.NewRequest("POST", actor.Inbox, bytes.NewBuffer(j)) - - CheckError(err, "error with sending activity req to") - - date := time.Now().UTC().Format(time.RFC1123) - path := strings.Replace(actor.Inbox, instance, "", 1) - - re := regexp.MustCompile("https?://(www.)?") - path = re.ReplaceAllString(path, "") - - sig := fmt.Sprintf("(request-target): %s %s\nhost: %s\ndate: %s", "post", path, instance, date) - encSig, err := ActivitySign(db, *activity.Actor, sig) - CheckError(err, "unable to sign activity response") - signature := fmt.Sprintf("keyId=\"%s\",headers=\"(request-target) host date\",signature=\"%s\"", activity.Actor.PublicKey.Id, encSig) - - req.Header.Set("Content-Type", activitystreams) - req.Header.Set("Date", date) - req.Header.Set("Signature", signature) - req.Host = instance - - _, err = RouteProxy(req) - - if err != nil { - fmt.Println("error with sending activity resp to actor " + instance) - } - } - } - } - } -} - func GetCollectionFromID(id string) Collection { var nColl Collection @@ -1178,43 +1033,6 @@ func remoteShort(url string) string { return "f" + actorname + "-" + id } -func RouteProxy(req *http.Request) (*http.Response, error) { - - var proxyType = GetPathProxyType(req.URL.Host) - - if proxyType == "tor" { - proxyUrl, err := url.Parse("socks5://" + TorProxy) - - CheckError(err, "error parsing tor proxy url") - - proxyTransport := &http.Transport{Proxy: http.ProxyURL(proxyUrl)} - client := &http.Client{Transport: proxyTransport, Timeout: time.Second * 15} - return client.Do(req) - } - - return http.DefaultClient.Do(req) -} - -func GetPathProxyType(path string) string { - if TorProxy != "" { - re := regexp.MustCompile(`(http://|http://)?(www.)?\w+\.onion`) - onion := re.MatchString(path) - if onion { - return "tor" - } - } - - return "clearnet" -} - -func RunDatabaseSchema(db *sql.DB) { - query, err := ioutil.ReadFile("databaseschema.psql") - CheckError(err, "could not read databaseschema.psql file") - if _, err := db.Exec(string(query)); err != nil { - CheckError(err, "could not exec databaseschema.psql") - } -} - func CreatedNeededDirectories() { if _, err := os.Stat("./public"); os.IsNotExist(err) { os.Mkdir("./public", 0755) @@ -1225,116 +1043,6 @@ func CreatedNeededDirectories() { } } -//looks for actor with pattern of board@instance -func FingerActor(path string) Actor { - - var nActor Actor - - actor, instance := GetActorInstance(path) - - if actor == "" && instance == "" { - return nActor - } - - if ActorCache[actor+"@"+instance].Id != "" { - nActor = ActorCache[actor+"@"+instance] - } else { - r := FingerRequest(actor, instance) - if r != nil && r.StatusCode == 200 { - defer r.Body.Close() - - body, _ := ioutil.ReadAll(r.Body) - - err := json.Unmarshal(body, &nActor) - - CheckError(err, "error getting fingerrequet resp from json body") - - ActorCache[actor+"@"+instance] = nActor - } - } - - return nActor -} - -func FingerRequest(actor string, instance string) *http.Response { - acct := "acct:" + actor + "@" + instance - req, err := http.NewRequest("GET", "http://"+instance+"/.well-known/webfinger?resource="+acct, nil) - - CheckError(err, "could not get finger request from id req") - - resp, err := RouteProxy(req) - - var finger Webfinger - - if err != nil { - return resp - } - - if resp.StatusCode == 200 { - defer resp.Body.Close() - - body, _ := ioutil.ReadAll(resp.Body) - - err := json.Unmarshal(body, &finger) - - CheckError(err, "error getting fingerrequet resp from json body") - } - - if len(finger.Links) > 0 { - for _, e := range finger.Links { - if e.Type == "application/activity+json" { - req, err := http.NewRequest("GET", e.Href, nil) - - CheckError(err, "could not get finger request from id req") - - req.Header.Set("Accept", activitystreams) - - resp, err := RouteProxy(req) - return resp - } - } - } - - return resp -} - -func GetActorInstance(path string) (string, string) { - re := regexp.MustCompile(`([@]?([\w\d.-_]+)[@](.+))`) - atFormat := re.MatchString(path) - - if atFormat { - match := re.FindStringSubmatch(path) - if len(match) > 2 { - return match[2], match[3] - } - } - - re = regexp.MustCompile(`(https?://)(www)?([\w\d-_.:]+)(/|\s+|\r|\r\n)?$`) - mainActor := re.MatchString(path) - if mainActor { - match := re.FindStringSubmatch(path) - if len(match) > 2 { - return "main", match[3] - } - } - - re = regexp.MustCompile(`(https?://)?(www)?([\w\d-_.:]+)\/([\w\d-_.]+)(\/([\w\d-_.]+))?`) - httpFormat := re.MatchString(path) - - if httpFormat { - match := re.FindStringSubmatch(path) - if len(match) > 3 { - if match[4] == "users" { - return match[6], match[3] - } - - return match[4], match[3] - } - } - - return "", "" -} - func AddInstanceToIndex(actor string) { // if local testing enviroment do not add to index re := regexp.MustCompile(`(.+)?(localhost|\d+\.\d+\.\d+\.\d+)(.+)?`) diff --git a/util/proxy.go b/util/proxy.go new file mode 100644 index 0000000..1fc9b03 --- /dev/null +++ b/util/proxy.go @@ -0,0 +1,39 @@ +package util + +import ( + "net/http" + "net/url" + "regexp" + "time" + + "github.com/FChannel0/FChannel-Server/config" +) + +func RouteProxy(req *http.Request) (*http.Response, error) { + var proxyType = GetPathProxyType(req.URL.Host) + + if proxyType == "tor" { + proxyUrl, err := url.Parse("socks5://" + config.TorProxy) + if err != nil { + return nil, err + } + + proxyTransport := &http.Transport{Proxy: http.ProxyURL(proxyUrl)} + client := &http.Client{Transport: proxyTransport, Timeout: time.Second * 15} + return client.Do(req) + } + + return http.DefaultClient.Do(req) +} + +func GetPathProxyType(path string) string { + if config.TorProxy != "" { + re := regexp.MustCompile(`(http://|http://)?(www.)?\w+\.onion`) + onion := re.MatchString(path) + if onion { + return "tor" + } + } + + return "clearnet" +} diff --git a/util/util.go b/util/util.go index 7164937..4ac8267 100644 --- a/util/util.go +++ b/util/util.go @@ -12,3 +12,40 @@ func IsOnion(url string) bool { return false } + +func GetActorInstance(path string) (string, string) { + re := regexp.MustCompile(`([@]?([\w\d.-_]+)[@](.+))`) + atFormat := re.MatchString(path) + + if atFormat { + match := re.FindStringSubmatch(path) + if len(match) > 2 { + return match[2], match[3] + } + } + + re = regexp.MustCompile(`(https?://)(www)?([\w\d-_.:]+)(/|\s+|\r|\r\n)?$`) + mainActor := re.MatchString(path) + if mainActor { + match := re.FindStringSubmatch(path) + if len(match) > 2 { + return "main", match[3] + } + } + + re = regexp.MustCompile(`(https?://)?(www)?([\w\d-_.:]+)\/([\w\d-_.]+)(\/([\w\d-_.]+))?`) + httpFormat := re.MatchString(path) + + if httpFormat { + match := re.FindStringSubmatch(path) + if len(match) > 3 { + if match[4] == "users" { + return match[6], match[3] + } + + return match[4], match[3] + } + } + + return "", "" +} diff --git a/webfinger.go b/webfinger.go deleted file mode 100644 index c8fd0ae..0000000 --- a/webfinger.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -type Webfinger struct { - Subject string `json:"subject,omitempty"` - Links []WebfingerLink `json:"links,omitempty"` -} - -type WebfingerLink struct { - Rel string `json:"rel,omitempty"` - Type string `json:"type,omitempty"` - Href string `json:"href,omitempty"` -} diff --git a/webfinger/webfinger.go b/webfinger/webfinger.go new file mode 100644 index 0000000..ffe7c6d --- /dev/null +++ b/webfinger/webfinger.go @@ -0,0 +1,145 @@ +package webfinger + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "strings" + + "github.com/FChannel0/FChannel-Server/activitypub" + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/util" +) + +type Webfinger struct { + Subject string `json:"subject,omitempty"` + Links []WebfingerLink `json:"links,omitempty"` +} + +type WebfingerLink struct { + Rel string `json:"rel,omitempty"` + Type string `json:"type,omitempty"` + Href string `json:"href,omitempty"` +} + +var ActorCache = make(map[string]activitypub.Actor) + +func GetActor(id string) (activitypub.Actor, error) { + var respActor activitypub.Actor + + if id == "" { + return respActor, nil + } + + actor, instance := util.GetActorInstance(id) + + if ActorCache[actor+"@"+instance].Id != "" { + respActor = ActorCache[actor+"@"+instance] + return respActor, nil + } + + req, err := http.NewRequest("GET", strings.TrimSpace(id), nil) + if err != nil { + return respActor, err + } + req.Header.Set("Accept", config.ActivityStreams) + + resp, err := util.RouteProxy(req) + + if err != nil { + return respActor, err + } + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + if err := json.Unmarshal(body, &respActor); err != nil { + return respActor, err + } + + ActorCache[actor+"@"+instance] = respActor + + return respActor, nil +} + +//looks for actor with pattern of board@instance +func FingerActor(path string) (activitypub.Actor, error) { + var nActor activitypub.Actor + + actor, instance := util.GetActorInstance(path) + + if actor == "" && instance == "" { + return nActor, nil + } + + if ActorCache[actor+"@"+instance].Id != "" { + nActor = ActorCache[actor+"@"+instance] + return nActor, nil + } + + r, err := FingerRequest(actor, instance) + if err != nil { + return nActor, err + } + + if r != nil && r.StatusCode == 200 { + defer r.Body.Close() + + body, _ := ioutil.ReadAll(r.Body) + + if err := json.Unmarshal(body, &nActor); err != nil { + return nActor, err + } + + ActorCache[actor+"@"+instance] = nActor + } + + // TODO: this just falls through and returns a blank Actor object. do something? + return nActor, nil +} + +func FingerRequest(actor string, instance string) (*http.Response, error) { + acct := "acct:" + actor + "@" + instance + + // TODO: respect https + req, err := http.NewRequest("GET", "http://"+instance+"/.well-known/webfinger?resource="+acct, nil) + if err != nil { + return nil, err + } + + resp, err := util.RouteProxy(req) + if err != nil { + return resp, err + } + + var finger Webfinger + + if resp.StatusCode == 200 { + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + if err := json.Unmarshal(body, &finger); err != nil { + return resp, err + } + } + + if len(finger.Links) > 0 { + for _, e := range finger.Links { + if e.Type == "application/activity+json" { + req, err := http.NewRequest("GET", e.Href, nil) + if err != nil { + return resp, err + } + + req.Header.Set("Accept", config.ActivityStreams) + + resp, err := util.RouteProxy(req) + return resp, err + } + } + } + + return resp, nil +} |