From 503a6637b8294aeb8e5e5546f8acbd2b3d6c4744 Mon Sep 17 00:00:00 2001 From: FChannel <> Date: Sat, 30 Apr 2022 22:17:32 -0700 Subject: first steps in posting connected. can make reply with no quote or quote OP do not recommend working on this branch for the time being since things are being moved around a lot --- activitypub/actor.go | 34 ++-- activitypub/object.go | 31 +--- activitypub/pem.go | 11 +- config/config.go | 1 + db/database.go | 3 +- main.go | 182 +-------------------- outboxPost.go | 400 --------------------------------------------- post/tripcode.go | 116 +++++++++++++ post/util.go | 285 ++++++++++++++++++++++++++------ routes/actor.go | 381 +++++++++++++++++++++++++++++++++++++++++- routes/admin.go | 4 +- routes/post.go | 6 + routes/structs.go | 4 +- tripcode.go | 116 ------------- util/blacklist.go | 76 +++++++++ util/util.go | 26 +++ views/403.html | 10 +- views/partials/bottom.html | 2 +- views/partials/top.html | 4 +- webfinger/webfinger.go | 116 +++++++++---- 20 files changed, 977 insertions(+), 831 deletions(-) create mode 100644 post/tripcode.go delete mode 100644 tripcode.go create mode 100644 util/blacklist.go diff --git a/activitypub/actor.go b/activitypub/actor.go index d9399ab..70681d1 100644 --- a/activitypub/actor.go +++ b/activitypub/actor.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/FChannel0/FChannel-Server/config" - "github.com/FChannel0/FChannel-Server/post" "github.com/FChannel0/FChannel-Server/util" "github.com/gofiber/fiber/v2" ) @@ -413,19 +412,12 @@ func GetActorFromDB(id string) (Actor, error) { query := `select type, id, name, preferedusername, inbox, outbox, following, followers, restricted, summary, publickeypem from actor where id=$1` - rows, err := config.DB.Query(query, id) + var publicKeyPem string + err := config.DB.QueryRow(query, id).Scan(&nActor.Type, &nActor.Id, &nActor.Name, &nActor.PreferredUsername, &nActor.Inbox, &nActor.Outbox, &nActor.Following, &nActor.Followers, &nActor.Restricted, &nActor.Summary, &publicKeyPem) if err != nil { return nActor, err } - var publicKeyPem string - defer rows.Close() - for rows.Next() { - if err := rows.Scan(&nActor.Type, &nActor.Id, &nActor.Name, &nActor.PreferredUsername, &nActor.Inbox, &nActor.Outbox, &nActor.Following, &nActor.Followers, &nActor.Restricted, &nActor.Summary, &publicKeyPem); err != nil { - return nActor, err - } - } - nActor.PublicKey, err = GetActorPemFromDB(publicKeyPem) if err != nil { return nActor, err @@ -629,6 +621,26 @@ func GetActorsFollowPostFromId(actors []string, id string) (Collection, error) { return collection, nil } +func GetActorPost(ctx *fiber.Ctx, path string) error { + collection, err := GetCollectionFromPath(config.Domain + "" + path) + if err != nil { + return err + } + + if len(collection.OrderedItems) > 0 { + enc, err := json.MarshalIndent(collection, "", "\t") + if err != nil { + return err + } + + ctx.Response().Header.Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + _, err = ctx.Write(enc) + return err + } + + return nil +} + func GetAllActorArchiveDB(id string, offset int) (Collection, error) { var nColl Collection var result []ObjectBase @@ -814,7 +826,7 @@ func WriteActorObjectReplyToDB(obj ObjectBase) error { } func WriteActorObjectToCache(obj ObjectBase) (ObjectBase, error) { - if res, err := post.IsPostBlacklist(obj.Content); err == nil && res { + if res, err := util.IsPostBlacklist(obj.Content); err == nil && res { fmt.Println("\n\nBlacklist post blocked\n\n") return obj, nil } else if err != nil { diff --git a/activitypub/object.go b/activitypub/object.go index 692fa5b..1256fac 100644 --- a/activitypub/object.go +++ b/activitypub/object.go @@ -14,7 +14,6 @@ import ( "time" "github.com/FChannel0/FChannel-Server/config" - "github.com/FChannel0/FChannel-Server/post" "github.com/FChannel0/FChannel-Server/util" ) @@ -42,7 +41,7 @@ func CheckIfObjectOP(id string) (bool, error) { } func CreateAttachmentObject(file multipart.File, header *multipart.FileHeader) ([]ObjectBase, *os.File, error) { - contentType, err := post.GetFileContentType(file) + contentType, err := util.GetFileContentType(file) if err != nil { return nil, nil, err } @@ -1563,7 +1562,6 @@ func AddFollower(id string, follower string) error { func WriteObjectReplyToDB(obj ObjectBase) error { for i, e := range obj.InReplyTo { - if res, err := CheckIfObjectOP(obj.Id); err == nil && !res && i == 0 { nType, err := GetObjectTypeDB(e.Id) if err != nil { @@ -1581,19 +1579,8 @@ func WriteObjectReplyToDB(obj ObjectBase) error { query := `select id from replies where id=$1 and inreplyto=$2` - rows, err := config.DB.Query(query, obj.Id, e.Id) - if err != nil { - return err - } - defer rows.Close() - var id string - rows.Next() - if err := rows.Scan(&id); err != nil { - return err - } - - if id == "" { + if err := config.DB.QueryRow(query, obj.Id, e.Id).Scan(&id); err != nil { query := `insert into replies (id, inreplyto) values ($1, $2)` _, err := config.DB.Exec(query, obj.Id, e.Id) @@ -1620,17 +1607,8 @@ func WriteObjectReplyToDB(obj ObjectBase) error { if len(obj.InReplyTo) < 1 { query := `select id from replies where id=$1 and inreplyto=$2` - rows, err := config.DB.Query(query, obj.Id, "") - if err != nil { - return err - } - defer rows.Close() - var id string - rows.Next() - rows.Scan(&id) - - if id == "" { + if err := config.DB.QueryRow(query, obj.Id, "").Scan(&id); err != nil { query := `insert into replies (id, inreplyto) values ($1, $2)` if _, err := config.DB.Exec(query, obj.Id, ""); err != nil { @@ -1688,7 +1666,7 @@ func WriteObjectReplyToLocalDB(id string, replyto string) error { } func WriteObjectToCache(obj ObjectBase) (ObjectBase, error) { - if res, err := post.IsPostBlacklist(obj.Content); err == nil && res { + if res, err := util.IsPostBlacklist(obj.Content); err == nil && res { fmt.Println("\n\nBlacklist post blocked\n\n") return obj, nil } else { @@ -1742,7 +1720,6 @@ func WriteObjectToDB(obj ObjectBase) (ObjectBase, error) { return obj, err } } - for i := range obj.Attachment { id, err := util.CreateUniqueID(obj.Actor) if err != nil { diff --git a/activitypub/pem.go b/activitypub/pem.go index ca6c068..ab225a9 100644 --- a/activitypub/pem.go +++ b/activitypub/pem.go @@ -139,16 +139,13 @@ func GetActorPemFromDB(pemID string) (PublicKeyPem, error) { query := `select id, owner, file from publickeypem where id=$1` - rows, err := config.DB.Query(query, pemID) - if err != nil { + if err := config.DB.QueryRow(query, pemID).Scan(&pem.Id, &pem.Owner, &pem.PublicKeyPem); err != nil { return pem, err } - defer rows.Close() - - rows.Next() - rows.Scan(&pem.Id, &pem.Owner, &pem.PublicKeyPem) - f, err := os.ReadFile(pem.PublicKeyPem) + dir, _ := os.Getwd() + dir = dir + "" + strings.Replace(pem.PublicKeyPem, ".", "", 1) + f, err := os.ReadFile(dir) if err != nil { return pem, err } diff --git a/config/config.go b/config/config.go index 10c029c..eb8cbdf 100644 --- a/config/config.go +++ b/config/config.go @@ -28,6 +28,7 @@ var DBPassword = GetConfigValue("dbpass", "password") var DBName = GetConfigValue("dbname", "server") var Redis = GetConfigValue("redis", "redis://localhost") var ActivityStreams = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" +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 Key string var Themes []string diff --git a/db/database.go b/db/database.go index 5cc1335..1ceac40 100644 --- a/db/database.go +++ b/db/database.go @@ -463,6 +463,7 @@ func IsReplyToOP(op string, link string) (string, bool, error) { if err != nil { return op, false, err } + defer rows.Close() var id string @@ -491,7 +492,7 @@ func GetReplyOP(link string) (string, error) { } func StartupArchive() error { - for _, e := range FollowingBoards { + for _, e := range webfinger.FollowingBoards { actor, err := activitypub.GetActorFromDB(e.Id) if err != nil { return err diff --git a/main.go b/main.go index 22783a2..125369a 100644 --- a/main.go +++ b/main.go @@ -31,8 +31,6 @@ import ( "time" ) -var authReq = []string{"captcha", "email", "passphrase"} - var MediaHashs = make(map[string]string) var Themes []string @@ -77,7 +75,7 @@ func main() { // root actor is used to follow remote feeds that are not local //name, prefname, summary, auth requirements, restricted if config.InstanceName != "" { - if _, err = db.CreateNewBoardDB(*activitypub.CreateNewActor("", config.InstanceName, config.InstanceSummary, authReq, false)); err != nil { + if _, err = db.CreateNewBoardDB(*activitypub.CreateNewActor("", config.InstanceName, config.InstanceSummary, config.AuthReq, false)); err != nil { //panic(err) } @@ -225,13 +223,13 @@ func main() { */ app.Get("/:actor", routes.OutboxGet) - app.Get("/:actor/catalog", routes.CatalogGet) + app.Post("/:actor", routes.ActorPost) + app.Get("/:actor/catalog", routes.CatalogGet) app.Get("/:actor/:post", routes.PostGet) - app.Get("/post", routes.ActorPost) app.Get("/:actor/inbox", routes.ActorInbox) - app.Get("/:actor/outbox", routes.ActorOutbox) + app.Post("/:actor/outbox", routes.ActorOutbox) app.Get("/:actor/following", routes.ActorFollowing) app.Get("/:actor/followers", routes.ActorFollowers) @@ -259,139 +257,6 @@ func neuter(next http.Handler) http.Handler { }) } -func GetContentType(location string) string { - elements := strings.Split(location, ";") - if len(elements) > 0 { - return elements[0] - } else { - return location - } -} - -func GetActorPost(w http.ResponseWriter, path string) error { - collection, err := activitypub.GetCollectionFromPath(config.Domain + "" + path) - if err != nil { - return err - } - - if len(collection.OrderedItems) > 0 { - enc, err := json.MarshalIndent(collection, "", "\t") - if err != nil { - return err - } - - w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") - _, err = w.Write(enc) - return err - } - - return nil -} - -func AddFollowersToActivity(activity activitypub.Activity) (activitypub.Activity, error) { - activity.To = append(activity.To, activity.Actor.Id) - - for _, e := range activity.To { - aFollowers, err := webfinger.GetActorCollection(e + "/followers") - if err != nil { - return activity, err - } - - for _, k := range aFollowers.Items { - activity.To = append(activity.To, k.Id) - } - } - - var nActivity activitypub.Activity - - for _, e := range activity.To { - var alreadyTo = false - for _, k := range nActivity.To { - if e == k || e == activity.Actor.Id { - alreadyTo = true - } - } - - if !alreadyTo { - nActivity.To = append(nActivity.To, e) - } - } - - activity.To = nActivity.To - - return activity, nil -} - -func CreateActivity(activityType string, obj activitypub.ObjectBase) (activitypub.Activity, error) { - var newActivity activitypub.Activity - - actor, err := webfinger.FingerActor(obj.Actor) - if err != nil { - return newActivity, err - } - - newActivity.AtContext.Context = "https://www.w3.org/ns/activitystreams" - newActivity.Type = activityType - newActivity.Published = obj.Published - newActivity.Actor = &actor - newActivity.Object = &obj - - for _, e := range obj.To { - if obj.Actor != e { - newActivity.To = append(newActivity.To, e) - } - } - - for _, e := range obj.Cc { - if obj.Actor != e { - newActivity.Cc = append(newActivity.Cc, e) - } - } - - return newActivity, nil -} - -func ParseCommentForReplies(comment string, op string) ([]activitypub.ObjectBase, error) { - - re := regexp.MustCompile(`(>>(https?://[A-Za-z0-9_.:\-~]+\/[A-Za-z0-9_.\-~]+\/)(f[A-Za-z0-9_.\-~]+-)?([A-Za-z0-9_.\-~]+)?#?([A-Za-z0-9_.\-~]+)?)`) - match := re.FindAllStringSubmatch(comment, -1) - - var links []string - - for i := 0; i < len(match); i++ { - str := strings.Replace(match[i][0], ">>", "", 1) - str = strings.Replace(str, "www.", "", 1) - str = strings.Replace(str, "http://", "", 1) - str = strings.Replace(str, "https://", "", 1) - str = config.TP + "" + str - _, isReply, err := db.IsReplyToOP(op, str) - if err != nil { - return nil, err - } - - if !util.IsInStringArray(links, str) && isReply { - links = append(links, str) - } - } - - var validLinks []activitypub.ObjectBase - for i := 0; i < len(links); i++ { - _, isValid, err := webfinger.CheckValidActivity(links[i]) - if err != nil { - return nil, err - } - - if isValid { - var reply activitypub.ObjectBase - reply.Id = links[i] - reply.Published = time.Now().UTC() - validLinks = append(validLinks, reply) - } - } - - return validLinks, nil -} - func IsValidActor(id string) (activitypub.Actor, bool, error) { actor, err := webfinger.FingerActor(id) return actor, actor.Id != "", err @@ -414,16 +279,6 @@ func MakeCaptchas(total int) error { return nil } -func SupportedMIMEType(mime string) bool { - for _, e := range config.SupportedFiles { - if e == mime { - return true - } - } - - return false -} - func GetActorReported(w http.ResponseWriter, r *http.Request, id string) error { auth := r.Header.Get("Authorization") verification := strings.Split(auth, " ") @@ -484,7 +339,7 @@ func DeleteObjectRequest(id string) error { nObj.Id = id nObj.Actor = nActor.Id - activity, err := CreateActivity("Delete", nObj) + activity, err := webfinger.CreateActivity("Delete", nObj) if err != nil { return err } @@ -526,7 +381,7 @@ func DeleteObjectAndRepliesRequest(id string) error { nObj.Id = id nObj.Actor = nActor.Id - activity, err := CreateActivity("Delete", nObj) + activity, err := webfinger.CreateActivity("Delete", nObj) if err != nil { return err } @@ -618,31 +473,6 @@ func ResizeAttachmentToPreview() error { }) } -func ParseCommentForReply(comment string) (string, error) { - re := regexp.MustCompile(`(>>(https?://[A-Za-z0-9_.:\-~]+\/[A-Za-z0-9_.\-~]+\/)(f[A-Za-z0-9_.\-~]+-)?([A-Za-z0-9_.\-~]+)?#?([A-Za-z0-9_.\-~]+)?)`) - match := re.FindAllStringSubmatch(comment, -1) - - var links []string - - for i := 0; i < len(match); i++ { - str := strings.Replace(match[i][0], ">>", "", 1) - links = append(links, str) - } - - if len(links) > 0 { - _, isValid, err := webfinger.CheckValidActivity(strings.ReplaceAll(links[0], ">", "")) - if err != nil { - return "", err - } - - if isValid { - return links[0], nil - } - } - - return "", nil -} - func CreatedNeededDirectories() { if _, err := os.Stat("./public"); os.IsNotExist(err) { os.Mkdir("./public", 0755) diff --git a/outboxPost.go b/outboxPost.go index d0aa84f..677fa79 100644 --- a/outboxPost.go +++ b/outboxPost.go @@ -4,401 +4,17 @@ import ( "encoding/json" "fmt" "io/ioutil" - "mime/multipart" "net/http" - "os" - "os/exec" - "regexp" - "strings" "github.com/FChannel0/FChannel-Server/activitypub" "github.com/FChannel0/FChannel-Server/config" "github.com/FChannel0/FChannel-Server/db" - "github.com/FChannel0/FChannel-Server/post" "github.com/FChannel0/FChannel-Server/util" "github.com/FChannel0/FChannel-Server/webfinger" "github.com/gofiber/fiber/v2" _ "github.com/lib/pq" ) -func ParseOutboxRequest(ctx *fiber.Ctx) error { - //var activity activitypub.Activity - - actor, err := webfinger.GetActorFromPath(ctx.Path(), "/") - if err != nil { - return err - } - - contentType := GetContentType(ctx.Get("content-type")) - - if contentType == "multipart/form-data" || contentType == "application/x-www-form-urlencoded" { - - hasCaptcha, err := db.BoardHasAuthType(actor.Name, "captcha") - if err != nil { - return err - } - - valid, err := CheckCaptcha(ctx.FormValue("captcha")) - if err == nil && hasCaptcha && valid { - header, _ := ctx.FormFile("file") - - if header != nil { - f, _ := header.Open() - defer f.Close() - if header.Size > (7 << 20) { - return ctx.Render("403", fiber.Map{ - "message": "7MB max file size", - }) - } else if res, err := IsMediaBanned(f); err == nil && res { - //Todo add logging - fmt.Println("media banned") - return ctx.Redirect("/", 301) - } else if err != nil { - return err - } - - contentType, _ := post.GetFileContentType(f) - - if !SupportedMIMEType(contentType) { - return ctx.Render("403", fiber.Map{ - "message": "file type not supported", - }) - } - } - - var nObj = activitypub.CreateObject("Note") - nObj, err := ObjectFromForm(ctx, nObj) - if err != nil { - return err - } - - nObj.Actor = config.Domain + "/" + actor.Name - - nObj, err = activitypub.WriteObjectToDB(nObj) - if err != nil { - return err - } - - if len(nObj.To) == 0 { - if err := db.ArchivePosts(actor); err != nil { - return err - } - } - - activity, err := CreateActivity("Create", nObj) - if err != nil { - return err - } - - activity, err = AddFollowersToActivity(activity) - if err != nil { - return err - } - - go db.MakeActivityRequest(activity) - - var id string - op := len(nObj.InReplyTo) - 1 - if op >= 0 { - if nObj.InReplyTo[op].Id == "" { - id = nObj.Id - } else { - id = nObj.InReplyTo[0].Id + "|" + nObj.Id - } - } - - ctx.Response().Header.Add("status", "200") - _, err = ctx.Write([]byte(id)) - return err - } - - ctx.Response().Header.Add("status", "403") - _, err = ctx.Write([]byte("captcha could not auth")) - return err - } else { - activity, err := activitypub.GetActivityFromJson(ctx) - if err != nil { - return err - } - - if res, err := activitypub.IsActivityLocal(activity); err == nil && res { - if res := db.VerifyHeaderSignature(ctx, *activity.Actor); err == nil && !res { - ctx.Response().Header.Add("status", "403") - _, err = ctx.Write([]byte("")) - return err - } - - switch activity.Type { - case "Create": - ctx.Response().Header.Add("status", "403") - _, err = ctx.Write([]byte("")) - break - - case "Follow": - var validActor bool - var validLocalActor bool - - validActor = (activity.Object.Actor != "") - validLocalActor = (activity.Actor.Id == actor.Id) - - var rActivity activitypub.Activity - if validActor && validLocalActor { - rActivity = db.AcceptFollow(activity) - rActivity, err = db.SetActorFollowingDB(rActivity) - if err != nil { - return err - } - if err := db.MakeActivityRequest(activity); err != nil { - return err - } - } - - webfinger.FollowingBoards, err = activitypub.GetActorFollowingDB(config.Domain) - if err != nil { - return err - } - - webfinger.Boards, err = webfinger.GetBoardCollection() - if err != nil { - return err - } - break - - case "Delete": - fmt.Println("This is a delete") - ctx.Response().Header.Add("status", "403") - _, err = ctx.Write([]byte("could not process activity")) - break - - case "Note": - ctx.Response().Header.Add("status", "403") - _, err = ctx.Write([]byte("could not process activity")) - break - - case "New": - name := activity.Object.Alias - prefname := activity.Object.Name - summary := activity.Object.Summary - restricted := activity.Object.Sensitive - - actor, err := db.CreateNewBoardDB(*activitypub.CreateNewActor(name, prefname, summary, authReq, restricted)) - if err != nil { - return err - } - - if actor.Id != "" { - var board []activitypub.ObjectBase - var item activitypub.ObjectBase - var removed bool = false - - item.Id = actor.Id - for _, e := range webfinger.FollowingBoards { - if e.Id != item.Id { - board = append(board, e) - } else { - removed = true - } - } - - if !removed { - board = append(board, item) - } - - webfinger.FollowingBoards = board - webfinger.Boards, err = webfinger.GetBoardCollection() - return err - } - - ctx.Response().Header.Add("status", "403") - _, err = ctx.Write([]byte("")) - break - - default: - ctx.Response().Header.Add("status", "403") - _, err = ctx.Write([]byte("could not process activity")) - } - } else if err != nil { - return err - } else { - fmt.Println("is NOT activity") - ctx.Response().Header.Add("status", "403") - _, err = ctx.Write([]byte("could not process activity")) - return err - } - } - - return nil -} - -func ObjectFromForm(ctx *fiber.Ctx, obj activitypub.ObjectBase) (activitypub.ObjectBase, error) { - header, _ := ctx.FormFile("file") - file, _ := header.Open() - var err error - - if file != nil { - defer file.Close() - - var tempFile = new(os.File) - obj.Attachment, tempFile, err = activitypub.CreateAttachmentObject(file, header) - if err != nil { - return obj, err - } - - defer tempFile.Close() - - fileBytes, _ := ioutil.ReadAll(file) - - tempFile.Write(fileBytes) - - re := regexp.MustCompile(`image/(jpe?g|png|webp)`) - if re.MatchString(obj.Attachment[0].MediaType) { - fileLoc := strings.ReplaceAll(obj.Attachment[0].Href, config.Domain, "") - - cmd := exec.Command("exiv2", "rm", "."+fileLoc) - - if err := cmd.Run(); err != nil { - return obj, err - } - } - - obj.Preview = activitypub.CreatePreviewObject(obj.Attachment[0]) - } - - obj.AttributedTo = util.EscapeString(ctx.FormValue("name")) - obj.TripCode = util.EscapeString(ctx.FormValue("tripcode")) - obj.Name = util.EscapeString(ctx.FormValue("subject")) - obj.Content = util.EscapeString(ctx.FormValue("comment")) - obj.Sensitive = (ctx.FormValue("sensitive") != "") - - obj = ParseOptions(ctx, obj) - - var originalPost activitypub.ObjectBase - originalPost.Id = util.EscapeString(ctx.FormValue("inReplyTo")) - - obj.InReplyTo = append(obj.InReplyTo, originalPost) - - var activity activitypub.Activity - - if !util.IsInStringArray(activity.To, originalPost.Id) { - activity.To = append(activity.To, originalPost.Id) - } - - if originalPost.Id != "" { - if res, err := activitypub.IsActivityLocal(activity); err == nil && !res { - actor, err := webfinger.FingerActor(originalPost.Id) - if err != nil { - return obj, err - } - - if !util.IsInStringArray(obj.To, actor.Id) { - obj.To = append(obj.To, actor.Id) - } - } else if err != nil { - return obj, err - } - } - - replyingTo, err := ParseCommentForReplies(ctx.FormValue("comment"), originalPost.Id) - if err != nil { - return obj, err - } - - for _, e := range replyingTo { - has := false - - for _, f := range obj.InReplyTo { - if e.Id == f.Id { - has = true - break - } - } - - if !has { - obj.InReplyTo = append(obj.InReplyTo, e) - - var activity activitypub.Activity - - activity.To = append(activity.To, e.Id) - - if res, err := activitypub.IsActivityLocal(activity); err == nil && !res { - actor, err := webfinger.FingerActor(e.Id) - if err != nil { - return obj, err - } - - if !util.IsInStringArray(obj.To, actor.Id) { - obj.To = append(obj.To, actor.Id) - } - } else if err != nil { - return obj, err - } - } - } - - return obj, nil -} - -func ParseOptions(ctx *fiber.Ctx, obj activitypub.ObjectBase) activitypub.ObjectBase { - options := util.EscapeString(ctx.FormValue("options")) - if options != "" { - option := strings.Split(options, ";") - email := regexp.MustCompile(".+@.+\\..+") - wallet := regexp.MustCompile("wallet:.+") - delete := regexp.MustCompile("delete:.+") - for _, e := range option { - if e == "noko" { - obj.Option = append(obj.Option, "noko") - } else if e == "sage" { - obj.Option = append(obj.Option, "sage") - } else if e == "nokosage" { - obj.Option = append(obj.Option, "nokosage") - } else if email.MatchString(e) { - obj.Option = append(obj.Option, "email:"+e) - } else if wallet.MatchString(e) { - obj.Option = append(obj.Option, "wallet") - var wallet activitypub.CryptoCur - value := strings.Split(e, ":") - wallet.Type = value[0] - wallet.Address = value[1] - obj.Wallet = append(obj.Wallet, wallet) - } else if delete.MatchString(e) { - obj.Option = append(obj.Option, e) - } - } - } - - return obj -} - -func CheckCaptcha(captcha string) (bool, error) { - parts := strings.Split(captcha, ":") - - if strings.Trim(parts[0], " ") == "" || strings.Trim(parts[1], " ") == "" { - return false, nil - } - - path := "public/" + parts[0] + ".png" - code, err := db.GetCaptchaCodeDB(path) - if err != nil { - return false, err - } - - if code != "" { - err = db.DeleteCaptchaCodeDB(path) - if err != nil { - return false, err - } - - err = db.CreateNewCaptcha() - if err != nil { - return false, err - } - - } - - return code == strings.ToUpper(parts[1]), nil -} - func ParseInboxRequest(ctx *fiber.Ctx) error { activity, err := activitypub.GetActivityFromJson(ctx) if err != nil { @@ -596,22 +212,6 @@ func MakeActivityFollowingReq(w http.ResponseWriter, r *http.Request, activity a return respActivity.Type == "Accept", err } -func IsMediaBanned(f multipart.File) (bool, error) { - f.Seek(0, 0) - - fileBytes := make([]byte, 2048) - - _, err := f.Read(fileBytes) - if err != nil { - return true, err - } - - hash := util.HashBytes(fileBytes) - - // f.Seek(0, 0) - return db.IsHashBanned(hash) -} - func SendToFollowers(actor string, activity activitypub.Activity) error { nActor, err := activitypub.GetActorFromDB(actor) if err != nil { diff --git a/post/tripcode.go b/post/tripcode.go new file mode 100644 index 0000000..3b7e48b --- /dev/null +++ b/post/tripcode.go @@ -0,0 +1,116 @@ +package post + +import ( + "bytes" + "regexp" + "strings" + + "github.com/FChannel0/FChannel-Server/config" + "github.com/gofiber/fiber/v2" + _ "github.com/lib/pq" + "github.com/simia-tech/crypt" + "golang.org/x/text/encoding/japanese" + "golang.org/x/text/transform" +) + +const SaltTable = "" + + "................................" + + ".............../0123456789ABCDEF" + + "GABCDEFGHIJKLMNOPQRSTUVWXYZabcde" + + "fabcdefghijklmnopqrstuvwxyz....." + + "................................" + + "................................" + + "................................" + + "................................" + +func TripCode(pass string) (string, error) { + pass = TripCodeConvert(pass) + + var salt [2]rune + + s := []rune(pass + "H..")[1:3] + + for i, r := range s { + salt[i] = rune(SaltTable[r%256]) + } + + enc, err := crypt.Crypt(pass, "$1$"+string(salt[:])) + if err != nil { + return "", err + } + + // normally i would just return error here but if the encrypt fails, this operation may fail and as a result cause a panic + return enc[len(enc)-10 : len(enc)], nil +} + +func TripCodeSecure(pass string) (string, error) { + pass = TripCodeConvert(pass) + + enc, err := crypt.Crypt(pass, "$1$"+config.Salt) + if err != nil { + return "", err + } + + return enc[len(enc)-10 : len(enc)], nil +} + +func TripCodeConvert(str string) string { + var s bytes.Buffer + + transform.NewWriter(&s, japanese.ShiftJIS.NewEncoder()).Write([]byte(str)) + + re := strings.NewReplacer( + "&", "&", + "\"", """, + "<", "<", + ">", ">", + ) + + return re.Replace(s.String()) +} + +func CreateNameTripCode(ctx *fiber.Ctx) (string, string, error) { + // TODO: to allow this to compile, this will fail for the case of the admin + // this can be easily fixed when the rest of the code gets converted to fiber + + input := ctx.FormValue("name") + + tripSecure := regexp.MustCompile("##(.+)?") + + if tripSecure.MatchString(input) { + chunck := tripSecure.FindString(input) + chunck = strings.Replace(chunck, "##", "", 1) + + //ce := regexp.MustCompile(`(?i)Admin`) + //admin := ce.MatchString(chunck) + + //board, modcred := GetPasswordFromSession(r) + + //if admin && HasAuth(modcred, board) { + // return tripSecure.ReplaceAllString(input, ""), "#Admin" + //} + + hash, err := TripCodeSecure(chunck) + return tripSecure.ReplaceAllString(input, ""), "!!" + hash, err + } + + trip := regexp.MustCompile("#(.+)?") + + if trip.MatchString(input) { + chunck := trip.FindString(input) + chunck = strings.Replace(chunck, "#", "", 1) + + //ce := regexp.MustCompile(`(?i)Admin`) + //admin := ce.MatchString(chunck) + //board, modcred := GetPasswordFromSession(r) + + //if admin && HasAuth(db, modcred, board) { + // return trip.ReplaceAllString(input, ""), "#Admin" + //} + + hash, err := TripCode(chunck) + return trip.ReplaceAllString(input, ""), "!" + hash, err + } + + return input, "", nil +} diff --git a/post/util.go b/post/util.go index c4920f7..cead842 100644 --- a/post/util.go +++ b/post/util.go @@ -1,93 +1,286 @@ package post import ( + "io/ioutil" "mime/multipart" - "net/http" + "os" + "os/exec" "regexp" + "strings" + "time" + "github.com/FChannel0/FChannel-Server/activitypub" "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/db" + "github.com/FChannel0/FChannel-Server/util" + "github.com/FChannel0/FChannel-Server/webfinger" + "github.com/gofiber/fiber/v2" ) -type PostBlacklist struct { - Id int - Regex string -} +func ParseCommentForReplies(comment string, op string) ([]activitypub.ObjectBase, error) { + re := regexp.MustCompile(`(>>(https?://[A-Za-z0-9_.:\-~]+\/[A-Za-z0-9_.\-~]+\/)(f[A-Za-z0-9_.\-~]+-)?([A-Za-z0-9_.\-~]+)?#?([A-Za-z0-9_.\-~]+)?)`) + match := re.FindAllStringSubmatch(comment, -1) + + var links []string -func DeleteRegexBlacklistDB(id int) error { - query := `delete from postblacklist where id=$1` + for i := 0; i < len(match); i++ { + str := strings.Replace(match[i][0], ">>", "", 1) + str = strings.Replace(str, "www.", "", 1) + str = strings.Replace(str, "http://", "", 1) + str = strings.Replace(str, "https://", "", 1) + str = config.TP + "" + str + _, isReply, err := db.IsReplyToOP(op, str) + if err != nil { + return nil, err + } - _, err := config.DB.Exec(query, id) - return err + if !util.IsInStringArray(links, str) && isReply { + links = append(links, str) + } + } + + var validLinks []activitypub.ObjectBase + for i := 0; i < len(links); i++ { + _, isValid, err := webfinger.CheckValidActivity(links[i]) + if err != nil { + return nil, err + } + + if isValid { + var reply activitypub.ObjectBase + reply.Id = links[i] + reply.Published = time.Now().UTC() + validLinks = append(validLinks, reply) + } + } + + return validLinks, nil } -func GetFileContentType(out multipart.File) (string, error) { - buffer := make([]byte, 512) +func ParseCommentForReply(comment string) (string, error) { + re := regexp.MustCompile(`(>>(https?://[A-Za-z0-9_.:\-~]+\/[A-Za-z0-9_.\-~]+\/)(f[A-Za-z0-9_.\-~]+-)?([A-Za-z0-9_.\-~]+)?#?([A-Za-z0-9_.\-~]+)?)`) + match := re.FindAllStringSubmatch(comment, -1) - _, err := out.Read(buffer) - if err != nil { - return "", err + var links []string + + for i := 0; i < len(match); i++ { + str := strings.Replace(match[i][0], ">>", "", 1) + links = append(links, str) + } + + if len(links) > 0 { + _, isValid, err := webfinger.CheckValidActivity(strings.ReplaceAll(links[0], ">", "")) + if err != nil { + return "", err + } + + if isValid { + return links[0], nil + } } - out.Seek(0, 0) + return "", nil +} - contentType := http.DetectContentType(buffer) +func ParseOptions(ctx *fiber.Ctx, obj activitypub.ObjectBase) activitypub.ObjectBase { + options := util.EscapeString(ctx.FormValue("options")) + if options != "" { + option := strings.Split(options, ";") + email := regexp.MustCompile(".+@.+\\..+") + wallet := regexp.MustCompile("wallet:.+") + delete := regexp.MustCompile("delete:.+") + for _, e := range option { + if e == "noko" { + obj.Option = append(obj.Option, "noko") + } else if e == "sage" { + obj.Option = append(obj.Option, "sage") + } else if e == "nokosage" { + obj.Option = append(obj.Option, "nokosage") + } else if email.MatchString(e) { + obj.Option = append(obj.Option, "email:"+e) + } else if wallet.MatchString(e) { + obj.Option = append(obj.Option, "wallet") + var wallet activitypub.CryptoCur + value := strings.Split(e, ":") + wallet.Type = value[0] + wallet.Address = value[1] + obj.Wallet = append(obj.Wallet, wallet) + } else if delete.MatchString(e) { + obj.Option = append(obj.Option, e) + } + } + } - return contentType, nil + return obj } -func GetRegexBlacklistDB() ([]PostBlacklist, error) { - var list []PostBlacklist +func CheckCaptcha(captcha string) (bool, error) { + parts := strings.Split(captcha, ":") - query := `select id, regex from postblacklist` + if strings.Trim(parts[0], " ") == "" || strings.Trim(parts[1], " ") == "" { + return false, nil + } - rows, err := config.DB.Query(query) + path := "public/" + parts[0] + ".png" + code, err := db.GetCaptchaCodeDB(path) if err != nil { - return list, err + return false, err } - defer rows.Close() - for rows.Next() { - var temp PostBlacklist - rows.Scan(&temp.Id, &temp.Regex) + if code != "" { + err = db.DeleteCaptchaCodeDB(path) + if err != nil { + return false, err + } + + err = db.CreateNewCaptcha() + if err != nil { + return false, err + } - list = append(list, temp) } - return list, nil + return code == strings.ToUpper(parts[1]), nil } -func IsPostBlacklist(comment string) (bool, error) { - postblacklist, err := GetRegexBlacklistDB() +func IsMediaBanned(f multipart.File) (bool, error) { + f.Seek(0, 0) + fileBytes := make([]byte, 2048) + + _, err := f.Read(fileBytes) if err != nil { - return false, err + return true, err } - for _, e := range postblacklist { - re := regexp.MustCompile(e.Regex) + hash := util.HashBytes(fileBytes) + + // f.Seek(0, 0) + return db.IsHashBanned(hash) +} - if re.MatchString(comment) { - return true, nil +func SupportedMIMEType(mime string) bool { + for _, e := range config.SupportedFiles { + if e == mime { + return true } } - return false, nil + return false } -func WriteRegexBlacklistDB(regex string) error { - var re string +func ObjectFromForm(ctx *fiber.Ctx, obj activitypub.ObjectBase) (activitypub.ObjectBase, error) { - query := `select from postblacklist where regex=$1` - if err := config.DB.QueryRow(query, regex).Scan(&re); err != nil { - return err + header, _ := ctx.FormFile("file") + + var file multipart.File + + if header != nil { + file, _ = header.Open() } - if re != "" { - return nil + var err error + + if file != nil { + defer file.Close() + + var tempFile = new(os.File) + obj.Attachment, tempFile, err = activitypub.CreateAttachmentObject(file, header) + if err != nil { + return obj, err + } + + defer tempFile.Close() + + fileBytes, _ := ioutil.ReadAll(file) + + tempFile.Write(fileBytes) + + re := regexp.MustCompile(`image/(jpe?g|png|webp)`) + if re.MatchString(obj.Attachment[0].MediaType) { + fileLoc := strings.ReplaceAll(obj.Attachment[0].Href, config.Domain, "") + + cmd := exec.Command("exiv2", "rm", "."+fileLoc) + + if err := cmd.Run(); err != nil { + return obj, err + } + } + + obj.Preview = activitypub.CreatePreviewObject(obj.Attachment[0]) } - query = `insert into postblacklist (regex) values ($1)` + obj.AttributedTo = util.EscapeString(ctx.FormValue("name")) + obj.TripCode = util.EscapeString(ctx.FormValue("tripcode")) + obj.Name = util.EscapeString(ctx.FormValue("subject")) + obj.Content = util.EscapeString(ctx.FormValue("comment")) + obj.Sensitive = (ctx.FormValue("sensitive") != "") + + obj = ParseOptions(ctx, obj) + + var originalPost activitypub.ObjectBase + originalPost.Id = util.EscapeString(ctx.FormValue("inReplyTo")) + + obj.InReplyTo = append(obj.InReplyTo, originalPost) + + var activity activitypub.Activity + + if !util.IsInStringArray(activity.To, originalPost.Id) { + activity.To = append(activity.To, originalPost.Id) + } + + if originalPost.Id != "" { + if local, _ := activitypub.IsActivityLocal(activity); !local { + actor, err := webfinger.FingerActor(originalPost.Id) + if err != nil { + return obj, err + } + + if !util.IsInStringArray(obj.To, actor.Id) { + obj.To = append(obj.To, actor.Id) + } + } else if err != nil { + return obj, err + } + } + + replyingTo, err := ParseCommentForReplies(ctx.FormValue("comment"), originalPost.Id) + + if err != nil { + return obj, err + } + + for _, e := range replyingTo { + has := false + + for _, f := range obj.InReplyTo { + if e.Id == f.Id { + has = true + break + } + } + + if !has { + obj.InReplyTo = append(obj.InReplyTo, e) + + var activity activitypub.Activity + + activity.To = append(activity.To, e.Id) + + if local, err := activitypub.IsActivityLocal(activity); err == nil && !local { + actor, err := webfinger.FingerActor(e.Id) + if err != nil { + return obj, err + } + + if !util.IsInStringArray(obj.To, actor.Id) { + obj.To = append(obj.To, actor.Id) + } + } else if err != nil { + return obj, err + } + } + } - _, err := config.DB.Exec(query, regex) - return err + return obj, nil } diff --git a/routes/actor.go b/routes/actor.go index f107ed7..6e5955e 100644 --- a/routes/actor.go +++ b/routes/actor.go @@ -1,6 +1,22 @@ package routes -import "github.com/gofiber/fiber/v2" +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + + "github.com/FChannel0/FChannel-Server/activitypub" + "github.com/FChannel0/FChannel-Server/config" + "github.com/FChannel0/FChannel-Server/db" + "github.com/FChannel0/FChannel-Server/post" + "github.com/FChannel0/FChannel-Server/util" + "github.com/FChannel0/FChannel-Server/webfinger" + "github.com/gofiber/fiber/v2" +) func ActorInbox(c *fiber.Ctx) error { // STUB @@ -8,10 +24,211 @@ func ActorInbox(c *fiber.Ctx) error { return c.SendString("actor inbox") } -func ActorOutbox(c *fiber.Ctx) error { - // STUB +func ActorOutbox(ctx *fiber.Ctx) error { + //var activity activitypub.Activity + actor, err := webfinger.GetActorFromPath(ctx.Path(), "/") + if err != nil { + return err + } + + contentType := util.GetContentType(ctx.Get("content-type")) + + if contentType == "multipart/form-data" || contentType == "application/x-www-form-urlencoded" { + hasCaptcha, err := db.BoardHasAuthType(actor.Name, "captcha") + if err != nil { + return err + } + + valid, err := post.CheckCaptcha(ctx.FormValue("captcha")) + if err == nil && hasCaptcha && valid { + header, _ := ctx.FormFile("file") + + if header != nil { + f, _ := header.Open() + defer f.Close() + if header.Size > (7 << 20) { + return ctx.Render("403", fiber.Map{ + "message": "7MB max file size", + }) + } else if res, err := post.IsMediaBanned(f); err == nil && res { + //Todo add logging + fmt.Println("media banned") + return ctx.Redirect("/", 301) + } else if err != nil { + return err + } + + contentType, _ := util.GetFileContentType(f) + + if !post.SupportedMIMEType(contentType) { + return ctx.Render("403", fiber.Map{ + "message": "file type not supported", + }) + } + } + + var nObj = activitypub.CreateObject("Note") + nObj, err := post.ObjectFromForm(ctx, nObj) + if err != nil { + return err + } + + nObj.Actor = config.Domain + "/" + actor.Name + + nObj, err = activitypub.WriteObjectToDB(nObj) + if err != nil { + return err + } + + if len(nObj.To) == 0 { + if err := db.ArchivePosts(actor); err != nil { + return err + } + } + + activity, err := webfinger.CreateActivity("Create", nObj) + if err != nil { + return err + } + + activity, err = webfinger.AddFollowersToActivity(activity) + if err != nil { + return err + } + + go db.MakeActivityRequest(activity) + + var id string + op := len(nObj.InReplyTo) - 1 + if op >= 0 { + if nObj.InReplyTo[op].Id == "" { + id = nObj.Id + } else { + id = nObj.InReplyTo[0].Id + "|" + nObj.Id + } + } + + ctx.Response().Header.Set("Status", "200") + _, err = ctx.Write([]byte(id)) + return err + } + + ctx.Response().Header.Set("Status", "403") + _, err = ctx.Write([]byte("captcha could not auth")) + return err + } else { // json request + activity, err := activitypub.GetActivityFromJson(ctx) + if err != nil { + return err + } + + if res, err := activitypub.IsActivityLocal(activity); err == nil && res { + if res := db.VerifyHeaderSignature(ctx, *activity.Actor); err == nil && !res { + ctx.Response().Header.Set("Status", "403") + _, err = ctx.Write([]byte("")) + return err + } + + switch activity.Type { + case "Create": + ctx.Response().Header.Set("Status", "403") + _, err = ctx.Write([]byte("")) + break + + case "Follow": + var validActor bool + var validLocalActor bool + + validActor = (activity.Object.Actor != "") + validLocalActor = (activity.Actor.Id == actor.Id) + + var rActivity activitypub.Activity + if validActor && validLocalActor { + rActivity = db.AcceptFollow(activity) + rActivity, err = db.SetActorFollowingDB(rActivity) + if err != nil { + return err + } + if err := db.MakeActivityRequest(activity); err != nil { + return err + } + } + + webfinger.FollowingBoards, err = activitypub.GetActorFollowingDB(config.Domain) + if err != nil { + return err + } + + webfinger.Boards, err = webfinger.GetBoardCollection() + if err != nil { + return err + } + break + + case "Delete": + fmt.Println("This is a delete") + ctx.Response().Header.Set("Status", "403") + _, err = ctx.Write([]byte("could not process activity")) + break + + case "Note": + ctx.Response().Header.Set("Satus", "403") + _, err = ctx.Write([]byte("could not process activity")) + break + + case "New": + name := activity.Object.Alias + prefname := activity.Object.Name + summary := activity.Object.Summary + restricted := activity.Object.Sensitive + + actor, err := db.CreateNewBoardDB(*activitypub.CreateNewActor(name, prefname, summary, config.AuthReq, restricted)) + if err != nil { + return err + } + + if actor.Id != "" { + var board []activitypub.ObjectBase + var item activitypub.ObjectBase + var removed bool = false + + item.Id = actor.Id + for _, e := range webfinger.FollowingBoards { + if e.Id != item.Id { + board = append(board, e) + } else { + removed = true + } + } + + if !removed { + board = append(board, item) + } + + webfinger.FollowingBoards = board + webfinger.Boards, err = webfinger.GetBoardCollection() + return err + } + + ctx.Response().Header.Set("Status", "403") + _, err = ctx.Write([]byte("")) + break + + default: + ctx.Response().Header.Set("status", "403") + _, err = ctx.Write([]byte("could not process activity")) + } + } else if err != nil { + return err + } else { + fmt.Println("is NOT activity") + ctx.Response().Header.Set("Status", "403") + _, err = ctx.Write([]byte("could not process activity")) + return err + } + } - return c.SendString("actor outbox") + return nil } func ActorFollowing(c *fiber.Ctx) error { @@ -38,8 +255,158 @@ func ActorArchive(c *fiber.Ctx) error { return c.SendString("actor archive") } -func ActorPost(c *fiber.Ctx) error { - // STUB +func ActorPost(ctx *fiber.Ctx) error { + header, _ := ctx.FormFile("file") + + if ctx.FormValue("inReplyTo") == "" && header == nil { + return ctx.Render("403", fiber.Map{ + "message": "Media is required for new posts", + }) + } + + var file multipart.File + + if header != nil { + file, _ = header.Open() + } + + if file != nil && header.Size > (7<<20) { + return ctx.Render("403", fiber.Map{ + "message": "7MB max file size", + }) + } + + if is, _ := util.IsPostBlacklist(ctx.FormValue("comment")); is { + errors.New("\n\nBlacklist post blocked\n\n") + return ctx.Redirect("/", 301) + } + + if ctx.FormValue("inReplyTo") == "" || file == nil { + if ctx.FormValue("comment") == "" && ctx.FormValue("subject") == "" { + return ctx.Render("403", fiber.Map{ + "message": "Comment or Subject required", + }) + } + } + + if len(ctx.FormValue("comment")) > 2000 { + return ctx.Render("403", fiber.Map{ + "message": "Comment limit 2000 characters", + }) + } + + if len(ctx.FormValue("subject")) > 100 || len(ctx.FormValue("name")) > 100 || len(ctx.FormValue("options")) > 100 { + return ctx.Render("403", fiber.Map{ + "message": "Name, Subject or Options limit 100 characters", + }) + } + + if ctx.FormValue("captcha") == "" { + return ctx.Render("403", fiber.Map{ + "message": "Incorrect Captcha", + }) + } + + b := bytes.Buffer{} + we := multipart.NewWriter(&b) + + if file != nil { + var fw io.Writer + + fw, err := we.CreateFormFile("file", header.Filename) + + if err != nil { + errors.New("error with form file create") + } + _, err = io.Copy(fw, file) + + if err != nil { + errors.New("error with form file copy") + } + } + + reply, _ := post.ParseCommentForReply(ctx.FormValue("comment")) + + form, _ := ctx.MultipartForm() + + for key, r0 := range form.Value { + if key == "captcha" { + err := we.WriteField(key, ctx.FormValue("captchaCode")+":"+ctx.FormValue("captcha")) + if err != nil { + errors.New("error with writing captcha field") + } + } else if key == "name" { + name, tripcode, _ := post.CreateNameTripCode(ctx) + + err := we.WriteField(key, name) + if err != nil { + errors.New("error with writing name field") + } + + err = we.WriteField("tripcode", tripcode) + if err != nil { + errors.New("error with writing tripcode field") + } + } else { + err := we.WriteField(key, r0[0]) + if err != nil { + errors.New("error with writing field") + } + } + } + + if ctx.FormValue("inReplyTo") == "" && reply != "" { + err := we.WriteField("inReplyTo", reply) + if err != nil { + errors.New("error with writing inReplyTo field") + } + } + + we.Close() + + sendTo := ctx.FormValue("sendTo") + + req, err := http.NewRequest("POST", sendTo, &b) + + if err != nil { + errors.New("error with post form req") + } + + req.Header.Set("Content-Type", we.FormDataContentType()) + + resp, err := util.RouteProxy(req) + + if err != nil { + errors.New("error with post form resp") + } + + defer resp.Body.Close() + + if resp.StatusCode == 200 { + + body, _ := ioutil.ReadAll(resp.Body) + + var obj activitypub.ObjectBase + + obj = post.ParseOptions(ctx, obj) + for _, e := range obj.Option { + if e == "noko" || e == "nokosage" { + return ctx.Redirect(config.Domain+"/"+ctx.FormValue("boardName")+"/"+util.ShortURL(ctx.FormValue("sendTo"), string(body)), 301) + } + } + + if ctx.FormValue("returnTo") == "catalog" { + return ctx.Redirect(config.Domain+"/"+ctx.FormValue("boardName")+"/catalog", 301) + } else { + return ctx.Redirect(config.Domain+"/"+ctx.FormValue("boardName"), 301) + } + } + + if resp.StatusCode == 403 { + return ctx.Render("403", fiber.Map{ + "message": "Incorrect Captcha", + }) + } - return c.SendString("actor post") + return ctx.Redirect(config.Domain+"/"+ctx.FormValue("boardName"), 301) } diff --git a/routes/admin.go b/routes/admin.go index c714b06..a2f7cd2 100644 --- a/routes/admin.go +++ b/routes/admin.go @@ -3,7 +3,7 @@ package routes import ( "github.com/FChannel0/FChannel-Server/config" "github.com/FChannel0/FChannel-Server/db" - "github.com/FChannel0/FChannel-Server/post" + "github.com/FChannel0/FChannel-Server/util" "github.com/FChannel0/FChannel-Server/webfinger" "github.com/gofiber/fiber/v2" ) @@ -54,7 +54,7 @@ func AdminIndex(ctx *fiber.Ctx) error { adminData.Board.Post.Actor = actor.Id - adminData.PostBlacklist, _ = post.GetRegexBlacklistDB() + adminData.PostBlacklist, _ = util.GetRegexBlacklistDB() adminData.Themes = &config.Themes diff --git a/routes/post.go b/routes/post.go index 016e533..64df600 100644 --- a/routes/post.go +++ b/routes/post.go @@ -17,6 +17,12 @@ func PostGet(ctx *fiber.Ctx) error { return err } + // this is a activitpub json request return json instead of html page + if activitypub.AcceptActivity(ctx.Get("Accept")) { + activitypub.GetActorPost(ctx, ctx.Path()) + return nil + } + postId := ctx.Params("post") inReplyTo := actor.Id + "/" + postId diff --git a/routes/structs.go b/routes/structs.go index 8cfb6ee..7c6e980 100644 --- a/routes/structs.go +++ b/routes/structs.go @@ -3,7 +3,7 @@ package routes import ( "github.com/FChannel0/FChannel-Server/activitypub" "github.com/FChannel0/FChannel-Server/db" - "github.com/FChannel0/FChannel-Server/post" + "github.com/FChannel0/FChannel-Server/util" "github.com/FChannel0/FChannel-Server/webfinger" ) @@ -40,7 +40,7 @@ type AdminPage struct { Reported []db.Report Domain string IsLocal bool - PostBlacklist []post.PostBlacklist + PostBlacklist []util.PostBlacklist AutoSubscribe bool Themes *[]string diff --git a/tripcode.go b/tripcode.go deleted file mode 100644 index 44651bb..0000000 --- a/tripcode.go +++ /dev/null @@ -1,116 +0,0 @@ -package main - -import ( - "bytes" - "net/http" - "regexp" - "strings" - - "github.com/FChannel0/FChannel-Server/config" - _ "github.com/lib/pq" - "github.com/simia-tech/crypt" - "golang.org/x/text/encoding/japanese" - "golang.org/x/text/transform" -) - -const SaltTable = "" + - "................................" + - ".............../0123456789ABCDEF" + - "GABCDEFGHIJKLMNOPQRSTUVWXYZabcde" + - "fabcdefghijklmnopqrstuvwxyz....." + - "................................" + - "................................" + - "................................" + - "................................" - -func TripCode(pass string) (string, error) { - pass = TripCodeConvert(pass) - - var salt [2]rune - - s := []rune(pass + "H..")[1:3] - - for i, r := range s { - salt[i] = rune(SaltTable[r%256]) - } - - enc, err := crypt.Crypt(pass, "$1$"+string(salt[:])) - if err != nil { - return "", err - } - - // normally i would just return error here but if the encrypt fails, this operation may fail and as a result cause a panic - return enc[len(enc)-10 : len(enc)], nil -} - -func TripCodeSecure(pass string) (string, error) { - pass = TripCodeConvert(pass) - - enc, err := crypt.Crypt(pass, "$1$"+config.Salt) - if err != nil { - return "", err - } - - return enc[len(enc)-10 : len(enc)], nil -} - -func TripCodeConvert(str string) string { - var s bytes.Buffer - - transform.NewWriter(&s, japanese.ShiftJIS.NewEncoder()).Write([]byte(str)) - - re := strings.NewReplacer( - "&", "&", - "\"", """, - "<", "<", - ">", ">", - ) - - return re.Replace(s.String()) -} - -func CreateNameTripCode(r *http.Request) (string, string, error) { - // TODO: to allow this to compile, this will fail for the case of the admin - // this can be easily fixed when the rest of the code gets converted to fiber - - input := r.FormValue("name") - - tripSecure := regexp.MustCompile("##(.+)?") - - if tripSecure.MatchString(input) { - chunck := tripSecure.FindString(input) - chunck = strings.Replace(chunck, "##", "", 1) - - //ce := regexp.MustCompile(`(?i)Admin`) - //admin := ce.MatchString(chunck) - - //board, modcred := GetPasswordFromSession(r) - - //if admin && HasAuth(modcred, board) { - // return tripSecure.ReplaceAllString(input, ""), "#Admin" - //} - - hash, err := TripCodeSecure(chunck) - return tripSecure.ReplaceAllString(input, ""), "!!" + hash, err - } - - trip := regexp.MustCompile("#(.+)?") - - if trip.MatchString(input) { - chunck := trip.FindString(input) - chunck = strings.Replace(chunck, "#", "", 1) - - //ce := regexp.MustCompile(`(?i)Admin`) - //admin := ce.MatchString(chunck) - //board, modcred := GetPasswordFromSession(r) - - //if admin && HasAuth(db, modcred, board) { - // return trip.ReplaceAllString(input, ""), "#Admin" - //} - - hash, err := TripCode(chunck) - return trip.ReplaceAllString(input, ""), "!" + hash, err - } - - return input, "", nil -} diff --git a/util/blacklist.go b/util/blacklist.go new file mode 100644 index 0000000..5368037 --- /dev/null +++ b/util/blacklist.go @@ -0,0 +1,76 @@ +package util + +import ( + "regexp" + + "github.com/FChannel0/FChannel-Server/config" +) + +type PostBlacklist struct { + Id int + Regex string +} + +func DeleteRegexBlacklistDB(id int) error { + query := `delete from postblacklist where id=$1` + + _, err := config.DB.Exec(query, id) + return err +} + +func GetRegexBlacklistDB() ([]PostBlacklist, error) { + var list []PostBlacklist + + query := `select id, regex from postblacklist` + + rows, err := config.DB.Query(query) + if err != nil { + return list, err + } + + defer rows.Close() + for rows.Next() { + var temp PostBlacklist + rows.Scan(&temp.Id, &temp.Regex) + + list = append(list, temp) + } + + return list, nil +} + +func IsPostBlacklist(comment string) (bool, error) { + postblacklist, err := GetRegexBlacklistDB() + + if err != nil { + return false, err + } + + for _, e := range postblacklist { + re := regexp.MustCompile(e.Regex) + + if re.MatchString(comment) { + return true, nil + } + } + + return false, nil +} + +func WriteRegexBlacklistDB(regex string) error { + var re string + + query := `select from postblacklist where regex=$1` + if err := config.DB.QueryRow(query, regex).Scan(&re); err != nil { + return err + } + + if re != "" { + return nil + } + + query = `insert into postblacklist (regex) values ($1)` + + _, err := config.DB.Exec(query, regex) + return err +} diff --git a/util/util.go b/util/util.go index 381db94..ade5eae 100644 --- a/util/util.go +++ b/util/util.go @@ -4,6 +4,8 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "mime/multipart" + "net/http" "os" "regexp" "strings" @@ -225,3 +227,27 @@ func CreateUniqueID(actor string) (string, error) { return newID, nil } + +func GetFileContentType(out multipart.File) (string, error) { + buffer := make([]byte, 512) + + _, err := out.Read(buffer) + if err != nil { + return "", err + } + + out.Seek(0, 0) + + contentType := http.DetectContentType(buffer) + + return contentType, nil +} + +func GetContentType(location string) string { + elements := strings.Split(location, ";") + if len(elements) > 0 { + return elements[0] + } else { + return location + } +} diff --git a/views/403.html b/views/403.html index 13d3cea..21ee188 100644 --- a/views/403.html +++ b/views/403.html @@ -1,7 +1,7 @@
diff --git a/views/partials/bottom.html b/views/partials/bottom.html index 35e8c4a..f995cf2 100644 --- a/views/partials/bottom.html +++ b/views/partials/bottom.html @@ -3,7 +3,7 @@ -