package main import ( "database/sql" "encoding/json" "fmt" "io/ioutil" "mime/multipart" "net/http" "os" "os/exec" "regexp" "strings" "github.com/FChannel0/FChannel-Server/activitypub" _ "github.com/lib/pq" "golang.org/x/perf/storage/db" ) func ParseOutboxRequest(w http.ResponseWriter, r *http.Request, db *sql.DB) { var activity Activity actor := GetActorFromPath(r.URL.Path, "/") contentType := GetContentType(r.Header.Get("content-type")) defer r.Body.Close() if contentType == "multipart/form-data" || contentType == "application/x-www-form-urlencoded" { r.ParseMultipartForm(5 << 20) if BoardHasAuthType(actor.Name, "captcha") && CheckCaptcha(db, r.FormValue("captcha")) { f, header, _ := r.FormFile("file") if header != nil { defer f.Close() if header.Size > (7 << 20) { w.WriteHeader(http.StatusRequestEntityTooLarge) w.Write([]byte("7MB max file size")) return } if IsMediaBanned(f) { fmt.Println("media banned") http.Redirect(w, r, Domain, http.StatusSeeOther) return } contentType, _ := GetFileContentType(f) if !SupportedMIMEType(contentType) { w.WriteHeader(http.StatusNotAcceptable) w.Write([]byte("file type not supported")) return } } var nObj = CreateObject("Note") nObj = ObjectFromForm(r, db, nObj) nObj.Actor = Domain + "/" + actor.Name nObj = WriteObjectToDB(nObj) if len(nObj.To) == 0 { ArchivePosts(actor) } activity := CreateActivity("Create", nObj) activity = AddFollowersToActivity(activity) go 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 } } w.WriteHeader(http.StatusOK) w.Write([]byte(id)) return } w.WriteHeader(http.StatusForbidden) w.Write([]byte("captcha could not auth")) } else { activity = GetActivityFromJson(r, db) if IsActivityLocal(activity) { if !VerifyHeaderSignature(r, *activity.Actor) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("")) return } switch activity.Type { case "Create": w.WriteHeader(http.StatusBadRequest) w.Write([]byte("")) break case "Follow": var validActor bool var validLocalActor bool validActor = (activity.Object.Actor != "") validLocalActor = (activity.Actor.Id == actor.Id) var rActivity Activity if validActor && validLocalActor { rActivity = AcceptFollow(activity) rActivity = SetActorFollowingDB(rActivity) MakeActivityRequest(activity) } FollowingBoards = GetActorFollowingDB(Domain) Boards = GetBoardCollection(db) break case "Delete": fmt.Println("This is a delete") w.WriteHeader(http.StatusBadRequest) w.Write([]byte("could not process activity")) break case "Note": w.WriteHeader(http.StatusBadRequest) w.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 := CreateNewBoardDB(*CreateNewActor(name, prefname, summary, authReq, restricted)) if actor.Id != "" { var board []ObjectBase var item ObjectBase var removed bool = false item.Id = actor.Id for _, e := range FollowingBoards { if e.Id != item.Id { board = append(board, e) } else { removed = true } } if !removed { board = append(board, item) } FollowingBoards = board Boards = GetBoardCollection(db) return } w.WriteHeader(http.StatusBadRequest) w.Write([]byte("")) break default: w.WriteHeader(http.StatusBadRequest) w.Write([]byte("could not process activity")) } } else { fmt.Println("is NOT activity") w.WriteHeader(http.StatusBadRequest) w.Write([]byte("could not process activity")) } } } func ObjectFromJson(r *http.Request, obj activitypub.ObjectBase) activitypub.ObjectBase { body, _ := ioutil.ReadAll(r.Body) var respActivity activitypub.ActivityRaw err := json.Unmarshal(body, &respActivity) CheckError(err, "error with object from json") if HasContextFromJson(respActivity.AtContextRaw.Context) { var jObj activitypub.ObjectBase jObj = GetObjectFromJson(respActivity.ObjectRaw) jObj.To = GetToFromJson(respActivity.ToRaw) jObj.Cc = GetToFromJson(respActivity.CcRaw) } return obj } func GetObjectFromJson(obj []byte) activitypub.ObjectBase { var generic interface{} err := json.Unmarshal(obj, &generic) CheckError(err, "error with getting obj from json") var nObj activitypub.ObjectBase if generic != nil { switch generic.(type) { case []interface{}: var lObj activitypub.ObjectBase var arrContext activitypub.ObjectArray err = json.Unmarshal(obj, &arrContext.Object) CheckError(err, "error with []interface{} oject from json") if len(arrContext.Object) > 0 { lObj = arrContext.Object[0] } nObj = lObj break case map[string]interface{}: var arrContext activitypub.Object err = json.Unmarshal(obj, &arrContext.Object) CheckError(err, "error with object from json") nObj = *arrContext.Object break case string: var lObj activitypub.ObjectBase var arrContext activitypub.ObjectString err = json.Unmarshal(obj, &arrContext.Object) CheckError(err, "error with string object from json") lObj.Id = arrContext.Object nObj = lObj break } } return nObj } func GetActorFromJson(actor []byte) activitypub.Actor { var generic interface{} var nActor activitypub.Actor err := json.Unmarshal(actor, &generic) if err != nil { return nActor } if generic != nil { switch generic.(type) { case map[string]interface{}: err = json.Unmarshal(actor, &nActor) CheckError(err, "error with To []interface{}") case string: var str string err = json.Unmarshal(actor, &str) CheckError(err, "error with To string") nActor.Id = str } return nActor } return nActor } func GetToFromJson(to []byte) []string { var generic interface{} err := json.Unmarshal(to, &generic) if err != nil { return nil } if generic != nil { var nStr []string switch generic.(type) { case []interface{}: err = json.Unmarshal(to, &nStr) CheckError(err, "error with To []interface{}") return nStr case string: var str string err = json.Unmarshal(to, &str) CheckError(err, "error with To string") nStr = append(nStr, str) return nStr } } return nil } func HasContextFromJson(context []byte) bool { var generic interface{} err := json.Unmarshal(context, &generic) CheckError(err, "error with getting context") hasContext := false switch generic.(type) { case []interface{}: var arrContext activitypub.AtContextArray err = json.Unmarshal(context, &arrContext.Context) CheckError(err, "error with []interface{}") if len(arrContext.Context) > 0 { if arrContext.Context[0] == "https://www.w3.org/ns/activitystreams" { hasContext = true } } case string: var arrContext activitypub.AtContextString err = json.Unmarshal(context, &arrContext.Context) CheckError(err, "error with string") if arrContext.Context == "https://www.w3.org/ns/activitystreams" { hasContext = true } } return hasContext } func ObjectFromForm(r *http.Request, db *sql.DB, obj activitypub.ObjectBase) activitypub.ObjectBase { file, header, _ := r.FormFile("file") if file != nil { defer file.Close() var tempFile = new(os.File) obj.Attachment, tempFile = CreateAttachmentObject(file, header) 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, Domain, "") cmd := exec.Command("exiv2", "rm", "."+fileLoc) err := cmd.Run() CheckError(err, "error with removing exif data from image") } obj.Preview = CreatePreviewObject(obj.Attachment[0]) } obj.AttributedTo = EscapeString(r.FormValue("name")) obj.TripCode = EscapeString(r.FormValue("tripcode")) obj.Name = EscapeString(r.FormValue("subject")) obj.Content = EscapeString(r.FormValue("comment")) obj.Sensitive = (r.FormValue("sensitive") != "") obj = ParseOptions(r, obj) var originalPost activitypub.ObjectBase originalPost.Id = EscapeString(r.FormValue("inReplyTo")) obj.InReplyTo = append(obj.InReplyTo, originalPost) var activity activitypub.Activity if !IsInStringArray(activity.To, originalPost.Id) { activity.To = append(activity.To, originalPost.Id) } if originalPost.Id != "" { if !IsActivityLocal(activity) { actor := FingerActor(originalPost.Id) if !IsInStringArray(obj.To, actor.Id) { obj.To = append(obj.To, actor.Id) } } } replyingTo := ParseCommentForReplies(r.FormValue("comment"), originalPost.Id) 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 !IsActivityLocal(activity) { actor := FingerActor(e.Id) if !IsInStringArray(obj.To, actor.Id) { obj.To = append(obj.To, actor.Id) } } } } return obj } func ParseOptions(r *http.Request, obj activitypub.ObjectBase) activitypub.ObjectBase { options := EscapeString(r.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 GetActivityFromJson(r *http.Request, db *sql.DB) activitypub.Activity { body, _ := ioutil.ReadAll(r.Body) var respActivity activitypub.ActivityRaw var nActivity activitypub.Activity var nType string err := json.Unmarshal(body, &respActivity) CheckError(err, "error with activity from json") if HasContextFromJson(respActivity.AtContextRaw.Context) { var jObj activitypub.ObjectBase if respActivity.Type == "Note" { jObj = GetObjectFromJson(body) nType = "Create" } else { jObj = GetObjectFromJson(respActivity.ObjectRaw) nType = respActivity.Type } actor := GetActorFromJson(respActivity.ActorRaw) to := GetToFromJson(respActivity.ToRaw) cc := GetToFromJson(respActivity.CcRaw) nActivity.AtContext.Context = "https://www.w3.org/ns/activitystreams" nActivity.Type = nType nActivity.Actor = &actor nActivity.Published = respActivity.Published nActivity.Auth = respActivity.Auth if len(to) > 0 { nActivity.To = to } if len(cc) > 0 { nActivity.Cc = cc } nActivity.Name = respActivity.Name nActivity.Object = &jObj } return nActivity } func CheckCaptcha(captcha string) bool { parts := strings.Split(captcha, ":") if strings.Trim(parts[0], " ") == "" || strings.Trim(parts[1], " ") == "" { return false } path := "public/" + parts[0] + ".png" code := GetCaptchaCodeDB(path) if code != "" { DeleteCaptchaCodeDB(path) CreateNewCaptcha(db) } if code == strings.ToUpper(parts[1]) { return true } return false } func ParseInboxRequest(w http.ResponseWriter, r *http.Request, db *sql.DB) { activity := GetActivityFromJson(r, db) if activity.Actor.PublicKey.Id == "" { nActor := FingerActor(activity.Actor.Id) activity.Actor = &nActor } if !VerifyHeaderSignature(r, *activity.Actor) { response := RejectActivity(activity) MakeActivityRequest(response) return } switch activity.Type { case "Create": for _, e := range activity.To { if IsActorLocal(e) { if !IsActorLocal(activity.Actor.Id) { col := GetCollectionFromID(activity.Object.Id) if len(col.OrderedItems) < 1 { break } WriteObjectToCache(*activity.Object) ArchivePosts(GetActorFromDB(db, e)) //SendToFollowers(e, activity) } } } break case "Delete": for _, e := range activity.To { actor := GetActorFromDB(e) if actor.Id != "" && actor.Id != Domain { if activity.Object.Replies != nil { for _, k := range activity.Object.Replies.OrderedItems { TombstoneObject(k.Id) } } TombstoneObject(activity.Object.Id) UnArchiveLast(actor.Id) break } } break case "Follow": for _, e := range activity.To { if GetActorFromDB(e).Id != "" { response := AcceptFollow(activity) response = SetActorFollowerDB(response) MakeActivityRequest(response) alreadyFollow := false alreadyFollowing := false autoSub := GetActorAutoSubscribeDB(response.Actor.Id) following := GetActorFollowingDB(response.Actor.Id) for _, e := range following { if e.Id == response.Object.Id { alreadyFollow = true } } actor := FingerActor(response.Object.Actor) remoteActorFollowingCol := GetCollectionFromReq(actor.Following) for _, e := range remoteActorFollowingCol.Items { if e.Id == response.Actor.Id { alreadyFollowing = true } } if autoSub && !alreadyFollow && alreadyFollowing { followActivity := MakeFollowActivity(response.Actor.Id, response.Object.Actor) if FingerActor(response.Object.Actor).Id != "" { MakeActivityRequestOutbox(followActivity) } } } else { fmt.Println("follow request for rejected") response := RejectActivity(activity) MakeActivityRequest(response) return } } break case "Reject": if activity.Object.Object.Type == "Follow" { fmt.Println("follow rejected") SetActorFollowingDB(activity) } break } } func MakeActivityFollowingReq(w http.ResponseWriter, r *http.Request, activity activitypub.Activity) bool { actor := GetActor(activity.Object.Id) req, err := http.NewRequest("POST", actor.Inbox, nil) CheckError(err, "Cannot make new get request to actor inbox for following req") resp, err := RouteProxy(req) CheckError(err, "could not make remote actor auth resp") defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) var respActivity activitypub.Activity err = json.Unmarshal(body, &respActivity) if respActivity.Type == "Accept" { return true } return false } func IsMediaBanned(f multipart.File) bool { f.Seek(0, 0) fileBytes := make([]byte, 2048) _, err := f.Read(fileBytes) if err != nil { fmt.Println("error readin bytes for media ban") } hash := HashBytes(fileBytes) f.Seek(0, 0) query := `select hash from bannedmedia where hash=$1` rows, err := db.Query(query, hash) CheckError(err, "could not get hash from banned media in db") var h string defer rows.Close() rows.Next() rows.Scan(&h) if h == hash { return true } return false } func SendToFollowers(actor string, activity activitypub.Activity) { nActor := GetActorFromDB(actor) activity.Actor = &nActor followers := GetActorFollowDB(actor) var to []string for _, e := range followers { for _, k := range activity.To { if e.Id != k { to = append(to, e.Id) } } } activity.To = to if len(activity.Object.InReplyTo) > 0 { MakeActivityRequest(activity) } }