diff options
Diffstat (limited to 'db')
-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 |
5 files changed, 1172 insertions, 336 deletions
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 -} |