linstrom/web/debug/users.go

500 lines
14 KiB
Go

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/activitypub/translators"
"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"
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)
}
err = dbgen.Feed.Create(&models.Feed{
Owner: user,
OwnerId: user.ID,
Name: user.Username + models.FeedDefaultSuffix,
IsDefault: true,
PublicKey: sql.NullString{Valid: false},
})
if err != nil {
log.Error().Err(err).Msg("Failed to create default feed for 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 := translators.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))
}
func updateUser(w http.ResponseWriter, r *http.Request) {
type Inbound struct {
UserId string
Displayname *string
Description *string
RestrictedFollow *bool
}
log := hlog.FromRequest(r)
var data Inbound
err := json.NewDecoder(r.Body).Decode(&data)
if err != nil {
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusBadRequest)
return
}
queryStart := dbgen.User.Where(dbgen.User.ID.Eq(data.UserId))
user, err := queryStart.First()
switch err {
case gorm.ErrRecordNotFound:
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
return
case nil:
default:
log.Error().Err(err).Msg("Db error while trying to fetch user for updating")
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
updateNeeded := false
if data.Displayname != nil {
user.DisplayName = *data.Displayname
updateNeeded = true
}
if data.Description != nil {
user.Description = *data.Description
updateNeeded = true
}
if data.RestrictedFollow != nil {
user.RestrictedFollow = *data.RestrictedFollow
updateNeeded = true
}
if !updateNeeded {
return
}
err = queryStart.Save(user)
if err != nil {
log.Error().Err(err).Msg("Failed to update user with new data")
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
}