From d272fa90b45762df9ab89467d90d3e59a75e3672 Mon Sep 17 00:00:00 2001 From: mstar Date: Wed, 9 Apr 2025 17:35:31 +0200 Subject: [PATCH] AP stuff almost works --- ap/getRemoteUser.go | 2 +- config/config.go | 22 ++- devserver/linstrom.toml | 2 + go.mod | 10 +- go.sum | 26 ++++ shared/constants.go | 10 ++ shared/rsaKey.go | 1 + storage-new/dbgen/users.gen.go | 30 +++- storage-new/models/User.go | 13 +- storage-new/self.go | 40 ++++-- storage-new/storage.go | 75 ++++++++++ web/debug/posts.go | 2 + web/debug/users.go | 12 +- web/public/api/activitypub/activitypub.go | 15 ++ web/public/api/activitypub/user.go | 96 +++++++++++++ web/public/api/activitypub/util.go | 25 ++++ web/public/api/api.go | 20 +++ web/public/api/webfinger.go | 167 ++++++++++++++++++++++ web/public/errorpages.go | 22 +++ web/public/server.go | 11 +- 20 files changed, 574 insertions(+), 27 deletions(-) create mode 100644 shared/constants.go create mode 100644 shared/rsaKey.go create mode 100644 web/public/api/activitypub/activitypub.go create mode 100644 web/public/api/activitypub/user.go create mode 100644 web/public/api/activitypub/util.go create mode 100644 web/public/api/api.go create mode 100644 web/public/api/webfinger.go create mode 100644 web/public/errorpages.go diff --git a/ap/getRemoteUser.go b/ap/getRemoteUser.go index aabe689..da12981 100644 --- a/ap/getRemoteUser.go +++ b/ap/getRemoteUser.go @@ -7,7 +7,7 @@ import ( "net/http" "time" - "gitlab.com/mstarongitlab/goap" + "git.mstar.dev/mstar/goap" ) var ErrNoApUrl = errors.New("no Activitypub url in webfinger") diff --git a/config/config.go b/config/config.go index dd3ebb3..222aa7b 100644 --- a/config/config.go +++ b/config/config.go @@ -50,6 +50,7 @@ type ConfigAdmin struct { // The password has to be supplied in the `password` GET form value for all requests // to /profiling/* ProfilingPassword string `toml:"profiling_password"` + AllowRegistration bool `toml:"allow_registration"` } type ConfigStorage struct { @@ -71,6 +72,9 @@ type ConfigStorage struct { // Key used for encrypting sensitive information in the db // DO NOT CHANGE THIS AFTER SETUP EncryptionKey string `toml:"encryption_key"` + // Maximum number of reconnection attempts if the connection to the db + // breaks for some reason. Server will exit if the last attempt fails + MaxReconnectAttempts int `toml:"max_reconnect_attempts"` } type ConfigS3 struct { @@ -131,6 +135,7 @@ var defaultConfig Config = Config{ Username: "server-admin", FirstTimeSetupOTP: "Example otp password", ProfilingPassword: "Example profiling password", + AllowRegistration: true, }, Webauthn: ConfigWebAuthn{ DisplayName: "Linstrom", @@ -149,6 +154,7 @@ var defaultConfig Config = Config{ MaxInMemoryCacheTTL: 5, MaxRedisCacheTTL: nil, EncryptionKey: "Encryption key for sensitive information. DO NOT CHANGE THIS AFTER SETUP", + MaxReconnectAttempts: 3, }, Mail: ConfigMail{ Host: "localhost", @@ -182,11 +188,11 @@ func (gc *ConfigGeneral) GetFullDomain() string { } func (gc *ConfigGeneral) GetFullPublicUrl() string { - str := gc.Protocol + gc.GetFullDomain() + str := gc.Protocol + "://" + gc.GetFullDomain() if gc.PublicPort != nil { - str += fmt.Sprint(*gc.PublicPort) + str += generatePortAppender(gc.Protocol, *gc.PublicPort) } else { - str += fmt.Sprint(gc.PrivatePort) + str += generatePortAppender(gc.Protocol, gc.PrivatePort) } return str } @@ -269,3 +275,13 @@ func ReadAndWriteToGlobal(fileName string) error { log.Info().Str("config-file", fileName).Msg("Read and applied config file") return nil } + +func generatePortAppender(protocol string, portNum int) string { + if protocol == "http" && portNum == 80 { + return "" + } else if protocol == "https" && portNum == 443 { + return "" + } else { + return fmt.Sprintf(":%d", portNum) + } +} diff --git a/devserver/linstrom.toml b/devserver/linstrom.toml index a35cb47..970c676 100644 --- a/devserver/linstrom.toml +++ b/devserver/linstrom.toml @@ -10,6 +10,7 @@ username = "server-admin" first_time_setup_otp = "Example otp password" profiling_password = "" + allow_registration = true [webauthn] display_name = "Linstrom" @@ -24,6 +25,7 @@ time_zone = "Europe/Berlin" max_in_memory_cache_size = 1000000 max_in_memory_cache_ttl = 5 + max_reconnect_attempts = 3 [mail] host = "localhost" diff --git a/go.mod b/go.mod index a387f7c..6fb5f6a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.0 toolchain go1.23.7 require ( - git.mstar.dev/mstar/goutils v1.10.0 + git.mstar.dev/mstar/goutils v1.12.0 github.com/BurntSushi/toml v1.4.0 github.com/dgraph-io/ristretto v0.2.0 github.com/eko/gocache/lib/v4 v4.1.6 @@ -22,7 +22,6 @@ require ( github.com/pquerna/otp v1.4.0 github.com/redis/go-redis/v9 v9.0.2 github.com/rs/zerolog v1.33.0 - gitlab.com/mstarongitlab/goap v1.1.0 golang.org/x/crypto v0.36.0 golang.org/x/image v0.20.0 gorm.io/driver/postgres v1.5.7 @@ -33,6 +32,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + git.mstar.dev/mstar/goap v1.2.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -51,9 +51,9 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-tpm v0.9.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect - github.com/jackc/pgx/v5 v5.5.5 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.4 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.17.11 // indirect diff --git a/go.sum b/go.sum index 0867e3b..09774c7 100644 --- a/go.sum +++ b/go.sum @@ -33,10 +33,30 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +git.mstar.dev/mstar/goap v0.0.0-20250407153813-45fa095a1597 h1:KUdA5J1ArvD7X4z8ttE41xaCo+Hv/vWB8scoT+HIpOE= +git.mstar.dev/mstar/goap v0.0.0-20250407153813-45fa095a1597/go.mod h1:cVCXcMGdYk8pySulIYSqMuvGG6lYEkibM5pPy97gylQ= +git.mstar.dev/mstar/goap v1.2.0 h1:jiWgU7zria+uMEq40lyvrNqofmw4Voe+sRboxxcnbZE= +git.mstar.dev/mstar/goap v1.2.0/go.mod h1:cVCXcMGdYk8pySulIYSqMuvGG6lYEkibM5pPy97gylQ= +git.mstar.dev/mstar/goap v1.2.1 h1:nnun+52fpOPhcRerIppFd506EFvPRHZk6tpZXpplwb8= +git.mstar.dev/mstar/goap v1.2.1/go.mod h1:cVCXcMGdYk8pySulIYSqMuvGG6lYEkibM5pPy97gylQ= +git.mstar.dev/mstar/goap v1.2.2 h1:JKXGH2zUnvibjSLDBSCYMAE9MrxkGajhjFKHHPNC7OU= +git.mstar.dev/mstar/goap v1.2.2/go.mod h1:cVCXcMGdYk8pySulIYSqMuvGG6lYEkibM5pPy97gylQ= +git.mstar.dev/mstar/goap v1.2.3 h1:PL4GQ6bvMy8JIzn7oj+PdbSztJLNhO54i49lyVKcuog= +git.mstar.dev/mstar/goap v1.2.3/go.mod h1:cVCXcMGdYk8pySulIYSqMuvGG6lYEkibM5pPy97gylQ= +git.mstar.dev/mstar/goap v1.2.4 h1:3CpL6nPuCwUbnEuwIyErY9sahbPaQGjs2a5fx9FYrOE= +git.mstar.dev/mstar/goap v1.2.4/go.mod h1:cVCXcMGdYk8pySulIYSqMuvGG6lYEkibM5pPy97gylQ= +git.mstar.dev/mstar/goap v1.2.5 h1:K17XzUC+ozJQQy/W6MgSmgMAsczvSLpL0Lvmq6KsB44= +git.mstar.dev/mstar/goap v1.2.5/go.mod h1:cVCXcMGdYk8pySulIYSqMuvGG6lYEkibM5pPy97gylQ= git.mstar.dev/mstar/goutils v1.9.1 h1:B4km2Xj0Yq8GHIlAYo45NGMRQRdkr+hV9qdvhTJKuuA= git.mstar.dev/mstar/goutils v1.9.1/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA= git.mstar.dev/mstar/goutils v1.10.0 h1:TXTz+RA4c5tNZRtdb4eVGjeL15xSjQOOSxfQ5ZwmKeE= git.mstar.dev/mstar/goutils v1.10.0/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA= +git.mstar.dev/mstar/goutils v1.11.0 h1:iHpMkGIypKNg4egYdwyx25Bk1pe4aPMFEK76y1JATmo= +git.mstar.dev/mstar/goutils v1.11.0/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA= +git.mstar.dev/mstar/goutils v1.11.1 h1:G21MjZzQDnpC7h+ZkfITbqX+jBQtqZ4FB7rj4K6idE0= +git.mstar.dev/mstar/goutils v1.11.1/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA= +git.mstar.dev/mstar/goutils v1.12.0 h1:d88hLS8KnLUCI+8aWBR6228M43hxHdJpj8WuSqm4LAM= +git.mstar.dev/mstar/goutils v1.12.0/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -202,10 +222,16 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= +github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= diff --git a/shared/constants.go b/shared/constants.go new file mode 100644 index 0000000..dbb1b47 --- /dev/null +++ b/shared/constants.go @@ -0,0 +1,10 @@ +package shared + +const ( + // Linstrom version + // + // TODO: Replace with "YYYY-MM-DD(.revision)" versioning + // where (.revision) is optional and only used for cases + // where multiple releases in a day are required + Version = "0.0.1 pre-alpha" +) diff --git a/shared/rsaKey.go b/shared/rsaKey.go new file mode 100644 index 0000000..a29b5e4 --- /dev/null +++ b/shared/rsaKey.go @@ -0,0 +1 @@ +package shared diff --git a/storage-new/dbgen/users.gen.go b/storage-new/dbgen/users.gen.go index 4e247d3..a71294a 100644 --- a/storage-new/dbgen/users.gen.go +++ b/storage-new/dbgen/users.gen.go @@ -1456,6 +1456,7 @@ type IUserDo interface { UnderlyingDB() *gorm.DB schema.Tabler + GetByUsernameUnrestricted(username string) (result *models.User, err error) GetByUsername(username string) (result *models.User, err error) GetPagedTruePublic(pageNr uint) (result []models.User, err error) GetPagedAllDeleted(pageNr uint) (result []models.User, err error) @@ -1463,10 +1464,10 @@ type IUserDo interface { GdprUsers() (err error) } -// Get a user by a username +// Get a user by a username, ignoring all restrictions on that user // // SELECT * FROM @@table WHERE username = @username AND deleted_at IS NULL LIMIT 1 -func (u userDo) GetByUsername(username string) (result *models.User, err error) { +func (u userDo) GetByUsernameUnrestricted(username string) (result *models.User, err error) { var params []interface{} var generateSQL strings.Builder @@ -1480,6 +1481,31 @@ func (u userDo) GetByUsername(username string) (result *models.User, err error) return } +// Get a user by the username. +// Restricted to users visible to ActivityPub +// +// SELECT * FROM @@table WHERE +// +// username = @username AND +// deleted_at IS NULL AND +// finished_registration = true AND +// verified = true +// +// LIMIT 1 +func (u userDo) GetByUsername(username string) (result *models.User, err error) { + var params []interface{} + + var generateSQL strings.Builder + params = append(params, username) + generateSQL.WriteString("SELECT * FROM users WHERE username = ? AND deleted_at IS NULL AND finished_registration = true AND verified = true LIMIT 1 ") + + var executeSQL *gorm.DB + executeSQL = u.UnderlyingDB().Raw(generateSQL.String(), params...).Take(&result) // ignore_security_alert + err = executeSQL.Error + + return +} + // Get all true public accounts (verified & no restricted follow & indexable) // in a paged manner, sorted by date saved // diff --git a/storage-new/models/User.go b/storage-new/models/User.go index 87c4d37..20b4cb0 100644 --- a/storage-new/models/User.go +++ b/storage-new/models/User.go @@ -80,9 +80,20 @@ type User struct { } type IUser interface { - // Get a user by a username + // Get a user by a username, ignoring all restrictions on that user // // SELECT * FROM @@table WHERE username = @username AND deleted_at IS NULL LIMIT 1 + GetByUsernameUnrestricted(username string) (*gen.T, error) + + // Get a user by the username. + // Restricted to users visible to ActivityPub + // + // SELECT * FROM @@table WHERE + // username = @username AND + // deleted_at IS NULL AND + // finished_registration = true AND + // verified = true + // LIMIT 1 GetByUsername(username string) (*gen.T, error) // Get all true public accounts (verified & no restricted follow & indexable) diff --git a/storage-new/self.go b/storage-new/self.go index 8e12888..d98fb96 100644 --- a/storage-new/self.go +++ b/storage-new/self.go @@ -1,8 +1,9 @@ package storage import ( - "crypto/ed25519" "crypto/rand" + "crypto/rsa" + "crypto/x509" "database/sql" "git.mstar.dev/mstar/goutils/other" @@ -102,7 +103,16 @@ func insertUser(server *models.RemoteServer) (*models.User, error) { if err != gorm.ErrRecordNotFound { return nil, err } - publicKey, privateKey, err := ed25519.GenerateKey(nil) + // publicKey, privateKey, err := ed25519.GenerateKey(nil) + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + if err = privateKey.Validate(); err != nil { + return nil, err + } + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + publicKeyBytes := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey) pkeyId := make([]byte, 64) _, err = rand.Read(pkeyId) if err != nil { @@ -126,8 +136,8 @@ func insertUser(server *models.RemoteServer) (*models.User, error) { Banner: nil, BannerId: sql.NullString{Valid: false}, Indexable: false, - PublicKey: publicKey, - PrivateKey: privateKey, + PublicKey: publicKeyBytes, + PrivateKey: privateKeyBytes, Verified: true, FinishedRegistration: true, PasskeyId: pkeyId, @@ -155,11 +165,21 @@ func insertUserPronoun(user *models.User) error { } func attachUserToRole(user *models.User) error { - u2r := models.UserToRole{ - User: *user, - UserId: user.ID, - Role: models.FullAdminRole, - RoleId: models.FullAdminRole.ID, + _, err := dbgen.UserToRole.Where(dbgen.UserToRole.UserId.Eq(user.ID)). + Where(dbgen.UserToRole.RoleId.Eq(models.FullAdminRole.ID)). + First() + switch err { + case nil: + return nil + case gorm.ErrRecordNotFound: + u2r := models.UserToRole{ + User: *user, + UserId: user.ID, + Role: models.FullAdminRole, + RoleId: models.FullAdminRole.ID, + } + return dbgen.UserToRole.Save(&u2r) + default: + return err } - return dbgen.UserToRole.Save(&u2r) } diff --git a/storage-new/storage.go b/storage-new/storage.go index 9c96ffb..8632b32 100644 --- a/storage-new/storage.go +++ b/storage-new/storage.go @@ -1,3 +1,78 @@ package storage +import ( + "time" + + "github.com/jackc/pgx/v5/pgconn" + "github.com/rs/zerolog/log" + "gorm.io/driver/postgres" + "gorm.io/gorm" + + "git.mstar.dev/mstar/linstrom/config" + "git.mstar.dev/mstar/linstrom/shared" + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" +) + //go:generate go run ../cmd/NewRoleHelperGenerator/main.go -input ./models/Role.go -output role_generated.go -mod storage + +// "Lock" to only have one active reconnection attempt going at any time +var activeReconnectionAttempt = false + +// Attempt to connect to the configured DB. +// If successful, also configures dbgen to use that connection +func AttemptReconnect() { + // Ensure that only one attempt is going at the same time + // since a connection failure could easily cause multiple calls + if activeReconnectionAttempt { + log.Info().Msg("Already attempting to reconnect") + return + } + activeReconnectionAttempt = true + defer func() { + activeReconnectionAttempt = false + }() + log.Warn().Msg("DB connection failure! Attempting to reconnect") + maxAttempts := config.GlobalConfig.Storage.MaxReconnectAttempts + for i := range maxAttempts { + // If not the first attempt, sleep for 5 seconds and hope that this helps + if i > 0 { + time.Sleep(time.Second * 5) + } + log.Warn().Msg("Attempting to reconnect to db") + db, err := gorm.Open( + postgres.Open(config.GlobalConfig.Storage.BuildPostgresDSN()), + &gorm.Config{ + Logger: shared.NewGormLogger(log.Logger), + }, + ) + // If reconnect failed, log, then enter next loop iteration + if err != nil { + log.Error(). + Err(err). + Int("remaining-attempts", maxAttempts-(i+1)). + Msg("Reconnect attempt failed") + continue + } + // No errors, reconnect successful. Give dbgen the new connection and return + dbgen.SetDefault(db) + return + } + // All attempts to reconnect have failed. + // The situation is not recoverable. + // Log it and exit + // This is not a panic reason as it is expected that a db connection + // could always drop due to outside influence + log.Fatal().Msg("Failed to reconnect to the database! Exiting") +} + +// HandleReconnectError checks if the given error requires +// a reconnect attempt. If it does, it also starts +// a reconnection attempt. +func HandleReconnectError(errToCheck error) bool { + if _, ok := errToCheck.(*pgconn.ConnectError); ok { + go AttemptReconnect() + return true + } else { + return false + } +} diff --git a/web/debug/posts.go b/web/debug/posts.go index 211d67f..179fd85 100644 --- a/web/debug/posts.go +++ b/web/debug/posts.go @@ -12,6 +12,7 @@ import ( "github.com/rs/zerolog/log" "gorm.io/gorm" + "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" @@ -67,6 +68,7 @@ func notesFrom(w http.ResponseWriter, r *http.Request) { user, err := dbgen.User.GetByUsername(username) if err != nil { log.Error().Err(err).Str("name", username).Msg("Failed to get user") + storage.HandleReconnectError(err) httputils.HttpErr(w, 0, "failed to get user", http.StatusInternalServerError) return } diff --git a/web/debug/users.go b/web/debug/users.go index 37d0220..9e95dfb 100644 --- a/web/debug/users.go +++ b/web/debug/users.go @@ -1,8 +1,9 @@ package webdebug import ( - "crypto/ed25519" "crypto/rand" + "crypto/rsa" + "crypto/x509" "database/sql" "encoding/json" "fmt" @@ -64,7 +65,10 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) { return } - publicKey, privateKey, err := ed25519.GenerateKey(nil) + // publicKey, privateKey, err := ed25519.GenerateKey(nil) + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + publicKeyBytes := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey) pkeyId := make([]byte, 64) _, err = rand.Read(pkeyId) if err != nil { @@ -96,8 +100,8 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) { Description: data.Description, IsBot: data.IsBot, ServerId: 1, // Hardcoded, Self is always first ID - PublicKey: publicKey, - PrivateKey: privateKey, + PublicKey: publicKeyBytes, + PrivateKey: privateKeyBytes, PasskeyId: pkeyId, } if data.Birthday != nil { diff --git a/web/public/api/activitypub/activitypub.go b/web/public/api/activitypub/activitypub.go new file mode 100644 index 0000000..8b64bea --- /dev/null +++ b/web/public/api/activitypub/activitypub.go @@ -0,0 +1,15 @@ +package activitypub + +import ( + "fmt" + "net/http" +) + +func BuildActivitypubRouter() http.Handler { + router := http.NewServeMux() + router.HandleFunc("/user/{id}", users) + router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "in ap") + }) + return router +} diff --git a/web/public/api/activitypub/user.go b/web/public/api/activitypub/user.go new file mode 100644 index 0000000..68669f5 --- /dev/null +++ b/web/public/api/activitypub/user.go @@ -0,0 +1,96 @@ +package activitypub + +import ( + "encoding/json" + "fmt" + "net/http" + + webutils "git.mstar.dev/mstar/goutils/http" + "github.com/rs/zerolog/log" + + "git.mstar.dev/mstar/linstrom/storage-new" + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" +) + +var baseLdContext = []any{ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", +} + +func users(w http.ResponseWriter, r *http.Request) { + type OutboundKey struct { + Id string `json:"id"` + Owner string `json:"owner"` + Pem string `json:"publicKeyPem"` + } + type Outbound struct { + Context []any `json:"@context"` + Id string `json:"id"` + Type string `json:"type"` + PreferredUsername string `json:"preferredUsername"` + Inbox string `json:"inbox"` + // FIXME: Public key stuff is borken. Focus on fixing + // PublicKey OutboundKey `json:"publicKey"` + } + userId := r.PathValue("id") + user, err := dbgen.User.Where(dbgen.User.ID.Eq(userId)).First() + if err != nil { + webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil) + if storage.HandleReconnectError(err) { + log.Warn().Msg("Connection to db lost. Reconnect attempt started") + } else { + log.Error().Err(err).Msg("Failed to get total user count from db") + } + return + } + // fmt.Println(x509.ParsePKCS1PublicKey(user.PublicKey)) + + apUrl := userIdToApUrl(user.ID) + data := Outbound{ + Context: baseLdContext, + Id: apUrl, + Type: "Person", + PreferredUsername: user.DisplayName, + Inbox: apUrl + "/inbox", + // PublicKey: OutboundKey{ + // Id: apUrl + "#main-key", + // Owner: apUrl, + // Pem: keyBytesToPem(user.PublicKey), + // }, + } + + encoded, err := json.Marshal(data) + w.Header().Add("Content-Type", "application/activity+json") + fmt.Fprint(w, string(encoded)) +} + +/* +Fine. You win JsonLD. I can't get you to work properly. I'll just treat you like normal json then +Fuck you. + +If anyone wants to get this shit working *the propper way* with JsonLD, here's the +original code +var chain goap.BaseApChain = &goap.EmptyBaseObject{} + chain = goap.AppendUDIdData(chain, apUrl) + chain = goap.AppendUDTypeData(chain, "Person") + chain = goap.AppendASPreferredNameData(chain, goap.ValueValue[string]{Value: user.DisplayName}) + chain = goap.AppendW3SecurityPublicKeyData( + chain, + apUrl+"#main-key", + apUrl, + keyBytesToPem(user.PublicKey), + ) + + chainMap := chain.MarshalToMap() + proc := ld.NewJsonLdProcessor() + options := ld.NewJsonLdOptions("") + tmp, tmperr := json.Marshal(chainMap) + fmt.Println(string(tmp), tmperr) + data, err := proc.Compact(chainMap, baseLdContext, options) + // data, err := goap.Compact(chain, baseLdContext) + if err != nil { + log.Error().Err(err).Msg("Failed to marshal ap chain") + webutils.ProblemDetailsStatusOnly(w, 500) + return + } +*/ diff --git a/web/public/api/activitypub/util.go b/web/public/api/activitypub/util.go new file mode 100644 index 0000000..100da5e --- /dev/null +++ b/web/public/api/activitypub/util.go @@ -0,0 +1,25 @@ +package activitypub + +import ( + "encoding/pem" + "fmt" + + "git.mstar.dev/mstar/linstrom/config" +) + +func userIdToApUrl(id string) string { + return fmt.Sprintf( + "%s/api/ap/users/%s", + config.GlobalConfig.General.GetFullPublicUrl(), + id, + ) +} + +func keyBytesToPem(bytes []byte) string { + block := pem.Block{ + Type: "PUBLIC KEY", + Headers: nil, + Bytes: bytes, + } + return string(pem.EncodeToMemory(&block)) +} diff --git a/web/public/api/api.go b/web/public/api/api.go new file mode 100644 index 0000000..9340392 --- /dev/null +++ b/web/public/api/api.go @@ -0,0 +1,20 @@ +package api + +import ( + "fmt" + "net/http" + + "git.mstar.dev/mstar/linstrom/web/public/api/activitypub" +) + +func BuildApiRouter() http.Handler { + router := http.NewServeMux() + router.Handle( + "/activitypub/", + http.StripPrefix("/activitypub", activitypub.BuildActivitypubRouter()), + ) + router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "in api") + }) + return router +} diff --git a/web/public/api/webfinger.go b/web/public/api/webfinger.go new file mode 100644 index 0000000..fb48f91 --- /dev/null +++ b/web/public/api/webfinger.go @@ -0,0 +1,167 @@ +package api + +import ( + "errors" + "fmt" + "net/http" + "regexp" + + webutils "git.mstar.dev/mstar/goutils/http" + "git.mstar.dev/mstar/goutils/other" + "github.com/rs/zerolog/log" + "gorm.io/gorm" + + "git.mstar.dev/mstar/linstrom/config" + "git.mstar.dev/mstar/linstrom/shared" + "git.mstar.dev/mstar/linstrom/storage-new" + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" +) + +var webfingerResourceRegex = regexp.MustCompile(`acct:(?P[\w-]+)@(?[\w\.-]+)`) + +func WellKnownWebfinger(w http.ResponseWriter, r *http.Request) { + type OutboundLink struct { + Relation string `json:"rel"` + Type string `json:"type"` + Href *string `json:"href,omitempty"` + } + type Outbound struct { + Subject string `json:"subject"` + Links []OutboundLink `json:"links"` + Aliases []string `json:"aliases,omitempty"` + } + requestedResource := r.FormValue("resource") + matches := webfingerResourceRegex.FindStringSubmatch(requestedResource) + if len(matches) == 0 { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/webfinger-bad-resource", + "Bad resource format", + other.IntoPointer( + `The "resource" parameter must be available and of the form "acct:@`, + ), + nil, + ) + return + } + // NOTE: Safe to access since, if the regex matches, it must include both groups + // Index 0 is the full matching string + // Index 1 is the username + // Index 2 is something vaguely domain-like + username := matches[1] + domain := matches[2] + // Fail if requested user is a different domain + // TODO: Decide whether to include the info that it's a different domain + if domain != config.GlobalConfig.General.GetFullDomain() { + webutils.ProblemDetailsStatusOnly(w, 404) + return + } + user, err := dbgen.User.GetByUsername(username) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + webutils.ProblemDetailsStatusOnly(w, 404) + } else { + // Fail the request, then attempt to reconnect + webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil) + if storage.HandleReconnectError(err) { + log.Warn().Msg("Connection to db lost. Reconnect attempt started") + } else { + log.Error().Err(err).Msg("Failed to get user from db") + } + } + return + } + data := Outbound{ + Subject: matches[0], + Links: []OutboundLink{ + { + Relation: "self", + Type: "application/activity+json", + Href: other.IntoPointer( + fmt.Sprintf( + "%s/api/activitypub/user/%s", + config.GlobalConfig.General.GetFullPublicUrl(), + user.ID, + ), + ), + }, + { + Relation: "http://webfinger.net/rel/profile-page", + Type: "text/html", + Href: other.IntoPointer( + fmt.Sprintf( + "%s/user/%s", + config.GlobalConfig.General.GetFullPublicUrl(), + user.ID, + ), + ), + }, + }, + } + webutils.SendJson(w, &data) +} + +func Nodeinfo(w http.ResponseWriter, r *http.Request) { + u := dbgen.User + userCount, err := u.Where(u.DeletedAt.IsNull(), u.Verified.Is(true)).Count() + if err != nil { + webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil) + if storage.HandleReconnectError(err) { + log.Warn().Msg("Connection to db lost. Reconnect attempt started") + } else { + log.Error().Err(err).Msg("Failed to get total user count from db") + } + return + } + n := dbgen.Note + noteCount, err := n.Where(n.DeletedAt.IsNull(), n.OriginId.Eq(1)).Count() + if err != nil { + webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil) + if storage.HandleReconnectError(err) { + log.Warn().Msg("Connection to db lost. Reconnect attempt started") + } else { + log.Error().Err(err).Msg("Failed to get total user count from db") + } + return + } + + data := map[string]any{ + "version": "2.1", + "software": map[string]string{ + "name": "linstrom", + "version": shared.Version, + "homepage": "https://git.mstar.dev/mstar/linstrom", + "repository": "https://git.mstar.dev/mstar/linstrom", + }, + "protocols": []string{"activitypub"}, + "services": map[string]any{ + "inbound": []string{}, + "outbound": []string{}, + }, + "openRegistrations": config.GlobalConfig.Admin.AllowRegistration, + "usage": map[string]any{ + "users": map[string]any{ + "total": userCount, + "activeHalfyear": nil, + "activeMonth": nil, + }, + "localPosts": noteCount, + "localComments": 0, + }, + "metadata": map[string]any{}, + } + webutils.SendJson(w, data) +} + +func WellKnownNodeinfo(w http.ResponseWriter, r *http.Request) { + data := map[string]any{ + "links": []map[string]any{ + { + "rel": "http://nodeinfo.diaspora.software/ns/schema/2.1", + "href": config.GlobalConfig.General.GetFullPublicUrl() + "/nodeinfo/2.1", + }, + }, + } + webutils.SendJson(w, data) +} diff --git a/web/public/errorpages.go b/web/public/errorpages.go new file mode 100644 index 0000000..74be297 --- /dev/null +++ b/web/public/errorpages.go @@ -0,0 +1,22 @@ +package webpublic + +import ( + "fmt" + "net/http" + + webutils "git.mstar.dev/mstar/goutils/http" +) + +var errorDescriptions = map[string]string{ + "webfinger-bad-resource": "The given format for the \"resource\" url parameter was missing or invalid. It must follow the form \"acct:@\"", + "db-failure": "The database query for this request failed for an undisclosed reason. This is often caused by bad input data conflicting with existing information. Try to submit different data or wait for some time", +} + +func errorTypeHandler(w http.ResponseWriter, r *http.Request) { + errName := r.PathValue("name") + if description, ok := errorDescriptions[errName]; ok { + fmt.Fprint(w, description) + } else { + webutils.ProblemDetailsStatusOnly(w, 404) + } +} diff --git a/web/public/server.go b/web/public/server.go index 0d462b1..45350b9 100644 --- a/web/public/server.go +++ b/web/public/server.go @@ -30,6 +30,10 @@ import ( "context" "fmt" "net/http" + + webutils "git.mstar.dev/mstar/goutils/http" + + "git.mstar.dev/mstar/linstrom/web/public/api" ) type Server struct { @@ -42,8 +46,13 @@ func New(addr string) *Server { w.WriteHeader(500) fmt.Fprint(w, "not implemented") }) + handler.Handle("/api/", http.StripPrefix("/api", api.BuildApiRouter())) + handler.HandleFunc("GET /.well-known/webfinger", api.WellKnownWebfinger) + handler.HandleFunc("GET /.well-known/nodeinfo", api.WellKnownNodeinfo) + handler.HandleFunc("GET /nodeinfo/2.1", api.Nodeinfo) + handler.HandleFunc("GET /errors/{name}", errorTypeHandler) server := http.Server{ - Handler: handler, + Handler: webutils.ChainMiddlewares(handler, webutils.LoggingMiddleware), Addr: addr, } return &Server{&server}