package webdebug import ( "context" "crypto/rand" "database/sql" "encoding/json" "encoding/pem" "fmt" "io" "net/http" "strconv" "time" webutils "git.mstar.dev/mstar/goutils/http" "git.mstar.dev/mstar/goutils/other" "git.mstar.dev/mstar/goutils/sliceutils" "github.com/rs/zerolog/hlog" "gorm.io/gorm" "git.mstar.dev/mstar/linstrom/activitypub" "git.mstar.dev/mstar/linstrom/shared" "git.mstar.dev/mstar/linstrom/storage-new" "git.mstar.dev/mstar/linstrom/storage-new/dbgen" "git.mstar.dev/mstar/linstrom/storage-new/models" webap "git.mstar.dev/mstar/linstrom/web/public/api/activitypub" webshared "git.mstar.dev/mstar/linstrom/web/shared" ) func getNonDeletedUsers(w http.ResponseWriter, r *http.Request) { log := hlog.FromRequest(r) pageStr := r.FormValue("page") page := 0 if pageStr != "" { var err error page, err = strconv.Atoi(pageStr) if err != nil { _ = webutils.ProblemDetails( w, http.StatusBadRequest, "/errors/bad-page", "bad page number", other.IntoPointer("page number must be an uint"), nil, ) return } if page < 0 { _ = webutils.ProblemDetails( w, http.StatusBadRequest, "/errors/bad-page", "bad page number", other.IntoPointer("page number must be >= 0"), nil, ) return } } users, err := dbgen.User.GetPagedAllNonDeleted(uint(page)) if err != nil { log.Error().Err(err).Int("page", page).Msg("Failed to get non-deleted users") _ = webutils.ProblemDetails( w, http.StatusInternalServerError, "/errors/db-failure", "database failure", nil, nil, ) return } _ = webutils.SendJson(w, sliceutils.Map(users, func(t models.User) webshared.User { u := webshared.User{} u.FromModel(&t) return u })) } func createLocalUser(w http.ResponseWriter, r *http.Request) { type Inbound struct { Username string `json:"username"` Displayname string `json:"displayname"` Description string `json:"description"` Birthday *time.Time `json:"birthday"` Location *string `json:"location"` IsBot bool `json:"is_bot"` } log := hlog.FromRequest(r) jsonDecoder := json.NewDecoder(r.Body) data := Inbound{} err := jsonDecoder.Decode(&data) if err != nil { _ = webutils.ProblemDetails( w, http.StatusBadRequest, "/errors/bad-request-data", "bad request data", nil, map[string]any{ "sample": Inbound{ Username: "bob", Displayname: "Bob Bobbington", Description: "Bobbing Bobs bop to Bobs bobbing beats", Birthday: other.IntoPointer(time.Now()), Location: nil, IsBot: false, }, }, ) return } publicKeyEdBytes, privateKeyEdBytes, err := shared.GenerateKeypair(true) if err != nil { log.Error().Err(err).Msg("Failed to generate and marshal public key") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } publicKeyRsaBytes, privateKeyRsaBytes, err := shared.GenerateKeypair(false) if err != nil { log.Error().Err(err).Msg("Failed to generate and marshal public key") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } pkeyId := make([]byte, 64) _, err = rand.Read(pkeyId) if err != nil { log.Error().Err(err).Msg("Failed to generate passkey id") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } u := dbgen.User query := u.Select( u.ID, u.Username, u.DisplayName, u.Description, u.IsBot, u.ServerId, u.PrivateKeyEd, u.PublicKeyEd, u.PrivateKeyRsa, u.PublicKeyRsa, u.PasskeyId, u.Verified, u.FinishedRegistration, ) if data.Birthday != nil { query = query.Select(u.Birthday) } if data.Location != nil { query = query.Select(u.Location) } user := models.User{ ID: shared.NewId(), Username: data.Username, DisplayName: data.Displayname, Description: data.Description, IsBot: data.IsBot, ServerId: 1, // Hardcoded, Self is always first ID PublicKeyRsa: publicKeyRsaBytes, PublicKeyEd: publicKeyEdBytes, PrivateKeyRsa: privateKeyRsaBytes, PrivateKeyEd: privateKeyEdBytes, PasskeyId: pkeyId, Verified: true, FinishedRegistration: true, } if data.Birthday != nil { user.Birthday = sql.NullString{Valid: true, String: data.Birthday.Format("2006-Jan-02")} // user.Birthday = sql.NullTime{Valid: true, Time: *data.Birthday} } if data.Location != nil { user.Location = sql.NullString{Valid: true, String: *data.Location} } if err = query.Create(&user); err != nil { log.Error().Err(err).Msg("failed to create new local user") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) } if err = storage.EnsureLocalUserIdHasLinks(user.ID); err != nil { log.Error().Err(err).Msg("Failed to add links to new user") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) } } func deleteUser(w http.ResponseWriter, r *http.Request) { id := r.FormValue("id") _, _ = dbgen.User.Where(dbgen.User.ID.Eq(id)).Delete() w.WriteHeader(http.StatusOK) } func returnKeypair(w http.ResponseWriter, r *http.Request) { id := r.FormValue("id") user, err := dbgen.User.Where(dbgen.User.ID.Eq(id)).First() if err != nil { return } err = shared.SanityCheckX509dRsaKeys(user.PublicKeyRsa, user.PrivateKeyRsa) if err != nil { hlog.FromRequest(r).Error().Err(err).Msg("Sanity check failed") } privKeyBlock := pem.Block{ Type: "RSA PRIVATE KEY", Bytes: user.PrivateKeyRsa, } if err != nil { hlog.FromRequest(r).Error().Err(err).Msg("Sanity check failed") } privKeyPem := pem.EncodeToMemory(&privKeyBlock) pubKeyPen := []byte(shared.KeyBytesToPem(user.PublicKeyRsa, false)) err = shared.SanityCheckPemdRsaKeys(pubKeyPen, privKeyPem) if err != nil { hlog.FromRequest(r).Error().Err(err).Msg("Pem Sanity check failed") } _, _ = fmt.Fprintf(w, "%s\n\n%s", privKeyPem, pubKeyPen) } func issueUserImport(w http.ResponseWriter, r *http.Request) { target := r.FormValue("target") _, err := activitypub.ImportRemoteAccountByHandle(target) if err != nil { hlog.FromRequest(r).Info().Err(err).Msg("Err from import request") } } func proxyMessageToTarget(w http.ResponseWriter, r *http.Request) { type Inbound struct { From string `json:"from"` Target string `json:"target"` Message any `json:"message"` } log := hlog.FromRequest(r) data := Inbound{} dec := json.NewDecoder(r.Body) err := dec.Decode(&data) if err != nil { log.Warn().Err(err).Msg("Failed to decode json body") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } log.Debug().Any("data", data).Msg("Received message") user, err := dbgen.User.GetByUsername(data.From) if err != nil { log.Error().Err(err).Msg("Failed to get user from storage") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } targetId, err := activitypub.ImportRemoteAccountByHandle(data.Target) if err != nil { log.Error().Err(err).Msg("Failed to import target user") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } target, err := dbgen.User.Where(dbgen.User.ID.Eq(targetId)). Preload(dbgen.User.RemoteInfo). First() if err != nil { log.Error().Err(err).Msg("Failed to get target user from db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } outBody, err := json.Marshal(data.Message) if err != nil { log.Error().Err(err).Msg("Failed to marshal out data") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } log.Debug().Bytes("request-body", outBody).Msg("Body of proxied request") response, _, err := webshared.RequestSigned( "POST", target.RemoteInfo.InboxLink, outBody, user, ) if err != nil { log.Error().Err(err).Msg("Request failed") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } defer func() { _ = response.Body.Close() }() respBody, _ := io.ReadAll(response.Body) log.Debug(). Int("status-code", response.StatusCode). Bytes("body", respBody). Msg("Response from message") } func requestFollow(w http.ResponseWriter, r *http.Request) { type Inbound struct { From string `json:"from"` // username To string `json:"to"` // username } log := hlog.FromRequest(r) var data Inbound dec := json.NewDecoder(r.Body) err := dec.Decode(&data) if err != nil { _ = webutils.ProblemDetailsStatusOnly(w, http.StatusBadRequest) return } follower, err := dbgen.User.GetByUsername(data.From) switch err { case nil: case gorm.ErrRecordNotFound: _ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) return default: log.Error().Err(err).Str("username", data.From).Msg("Failed to get account from db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } followingId, err := activitypub.ImportRemoteAccountByHandle(data.To) if err != nil { log.Error().Err(err).Str("followed-username", data.To).Msg("Failed to import follow target") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } u2u := dbgen.UserToUserRelation relCount, err := u2u.Where( u2u.UserId.Eq(follower.ID), u2u.TargetUserId.Eq(followingId), u2u.Where(u2u.Relation.Eq(string(models.RelationFollowRequest))). Or(u2u.Relation.Eq(string(models.RelationFollow))), ). Count() if err != nil { log.Error(). Err(err). Str("followed-username", data.To). Str("follower-username", data.From). Msg("Failed to check if follow relation already exists") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } if relCount > 0 { // Follower is already following / has requested a follow from the followed. Nothing to do return } followRelation := models.UserToUserRelation{ TargetUserId: followingId, UserId: follower.ID, Relation: string(models.RelationFollowRequest), } err = u2u.Create(&followRelation) if err != nil { log.Error(). Err(err). Str("followed-username", data.To). Str("follower-username", data.From). Msg("Failed to insert follow request relation in db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } activity := models.Activity{ Id: shared.NewId(), Type: string(models.ActivityFollow), ObjectId: strconv.FormatUint(followRelation.ID, 10), ObjectType: uint32(models.ActivitystreamsActivityTargetFollow), } err = dbgen.Activity.Create(&activity) if err != nil { log.Err(err). Uint64("relation-id", followRelation.ID). Msg("Failed to store activity for relation in db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } go func() { user, err := dbgen.User.Preload(dbgen.User.RemoteInfo). Where(dbgen.User.ID.Eq(followingId)). First() if err != nil { log.Error().Err(err).Msg("Failed to get target user with remote links") return } activity, err := webap.FollowFromStorage(context.Background(), activity.Id) if err != nil { log.Error().Err(err).Msg("Failed to retrieve and format follow request") return } activity.Context = activitypub.BaseLdContext outData, err := json.Marshal(activity) if err != nil { log.Error().Err(err).Msg("Failed to marshal outbound follow request") return } log.Debug().Bytes("request-body", outData).Msg("Data to send") res, _, err := webshared.RequestSigned("POST", user.RemoteInfo.InboxLink, outData, follower) if err != nil { log.Error().Err(err).Msg("Failed to send follow request") return } if res.StatusCode > 299 || res.StatusCode < 200 { body, _ := io.ReadAll(res.Body) log.Error(). Err(err). Bytes("body", body). Int("status-code", res.StatusCode). Msg("Bad reply to follow request") } }() } func requestAs(w http.ResponseWriter, r *http.Request) { type Inbound struct { Username string TargetUrl string } log := hlog.FromRequest(r) data := Inbound{} dec := json.NewDecoder(r.Body) err := dec.Decode(&data) if err != nil { log.Warn().Err(err).Msg("Failed to decode json body") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } user, err := dbgen.User.GetByUsername(data.Username) if err != nil { _ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) return } res, _, err := webshared.RequestSigned("GET", data.TargetUrl, nil, user) if err != nil { log.Warn().Err(err).Msg("Request failed") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } if res.StatusCode != 200 { _ = webutils.ProblemDetailsStatusOnly(w, res.StatusCode) return } body, _ := io.ReadAll(res.Body) _, _ = fmt.Fprint(w, string(body)) }