AP stuff almost works
Some checks are pending
/ test (push) Waiting to run

This commit is contained in:
Melody Becker 2025-04-09 17:35:31 +02:00
parent 98191fd098
commit d272fa90b4
Signed by: mstar
SSH key fingerprint: SHA256:9VAo09aaVNTWKzPW7Hq2LW+ox9OdwmTSHRoD4mlz1yI
20 changed files with 574 additions and 27 deletions

View file

@ -7,7 +7,7 @@ import (
"net/http" "net/http"
"time" "time"
"gitlab.com/mstarongitlab/goap" "git.mstar.dev/mstar/goap"
) )
var ErrNoApUrl = errors.New("no Activitypub url in webfinger") var ErrNoApUrl = errors.New("no Activitypub url in webfinger")

View file

@ -50,6 +50,7 @@ type ConfigAdmin struct {
// The password has to be supplied in the `password` GET form value for all requests // The password has to be supplied in the `password` GET form value for all requests
// to /profiling/* // to /profiling/*
ProfilingPassword string `toml:"profiling_password"` ProfilingPassword string `toml:"profiling_password"`
AllowRegistration bool `toml:"allow_registration"`
} }
type ConfigStorage struct { type ConfigStorage struct {
@ -71,6 +72,9 @@ type ConfigStorage struct {
// Key used for encrypting sensitive information in the db // Key used for encrypting sensitive information in the db
// DO NOT CHANGE THIS AFTER SETUP // DO NOT CHANGE THIS AFTER SETUP
EncryptionKey string `toml:"encryption_key"` 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 { type ConfigS3 struct {
@ -131,6 +135,7 @@ var defaultConfig Config = Config{
Username: "server-admin", Username: "server-admin",
FirstTimeSetupOTP: "Example otp password", FirstTimeSetupOTP: "Example otp password",
ProfilingPassword: "Example profiling password", ProfilingPassword: "Example profiling password",
AllowRegistration: true,
}, },
Webauthn: ConfigWebAuthn{ Webauthn: ConfigWebAuthn{
DisplayName: "Linstrom", DisplayName: "Linstrom",
@ -149,6 +154,7 @@ var defaultConfig Config = Config{
MaxInMemoryCacheTTL: 5, MaxInMemoryCacheTTL: 5,
MaxRedisCacheTTL: nil, MaxRedisCacheTTL: nil,
EncryptionKey: "Encryption key for sensitive information. DO NOT CHANGE THIS AFTER SETUP", EncryptionKey: "Encryption key for sensitive information. DO NOT CHANGE THIS AFTER SETUP",
MaxReconnectAttempts: 3,
}, },
Mail: ConfigMail{ Mail: ConfigMail{
Host: "localhost", Host: "localhost",
@ -182,11 +188,11 @@ func (gc *ConfigGeneral) GetFullDomain() string {
} }
func (gc *ConfigGeneral) GetFullPublicUrl() string { func (gc *ConfigGeneral) GetFullPublicUrl() string {
str := gc.Protocol + gc.GetFullDomain() str := gc.Protocol + "://" + gc.GetFullDomain()
if gc.PublicPort != nil { if gc.PublicPort != nil {
str += fmt.Sprint(*gc.PublicPort) str += generatePortAppender(gc.Protocol, *gc.PublicPort)
} else { } else {
str += fmt.Sprint(gc.PrivatePort) str += generatePortAppender(gc.Protocol, gc.PrivatePort)
} }
return str return str
} }
@ -269,3 +275,13 @@ func ReadAndWriteToGlobal(fileName string) error {
log.Info().Str("config-file", fileName).Msg("Read and applied config file") log.Info().Str("config-file", fileName).Msg("Read and applied config file")
return nil 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)
}
}

View file

@ -10,6 +10,7 @@
username = "server-admin" username = "server-admin"
first_time_setup_otp = "Example otp password" first_time_setup_otp = "Example otp password"
profiling_password = "" profiling_password = ""
allow_registration = true
[webauthn] [webauthn]
display_name = "Linstrom" display_name = "Linstrom"
@ -24,6 +25,7 @@
time_zone = "Europe/Berlin" time_zone = "Europe/Berlin"
max_in_memory_cache_size = 1000000 max_in_memory_cache_size = 1000000
max_in_memory_cache_ttl = 5 max_in_memory_cache_ttl = 5
max_reconnect_attempts = 3
[mail] [mail]
host = "localhost" host = "localhost"

10
go.mod
View file

@ -5,7 +5,7 @@ go 1.23.0
toolchain go1.23.7 toolchain go1.23.7
require ( 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/BurntSushi/toml v1.4.0
github.com/dgraph-io/ristretto v0.2.0 github.com/dgraph-io/ristretto v0.2.0
github.com/eko/gocache/lib/v4 v4.1.6 github.com/eko/gocache/lib/v4 v4.1.6
@ -22,7 +22,6 @@ require (
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
github.com/redis/go-redis/v9 v9.0.2 github.com/redis/go-redis/v9 v9.0.2
github.com/rs/zerolog v1.33.0 github.com/rs/zerolog v1.33.0
gitlab.com/mstarongitlab/goap v1.1.0
golang.org/x/crypto v0.36.0 golang.org/x/crypto v0.36.0
golang.org/x/image v0.20.0 golang.org/x/image v0.20.0
gorm.io/driver/postgres v1.5.7 gorm.io/driver/postgres v1.5.7
@ -33,6 +32,7 @@ require (
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect 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/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cespare/xxhash/v2 v2.3.0 // 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/golang/protobuf v1.5.4 // indirect
github.com/google/go-tpm v0.9.1 // indirect github.com/google/go-tpm v0.9.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect github.com/jackc/pgx/v5 v5.7.4 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/compress v1.17.11 // indirect

26
go.sum
View file

@ -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= 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 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 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 h1:B4km2Xj0Yq8GHIlAYo45NGMRQRdkr+hV9qdvhTJKuuA=
git.mstar.dev/mstar/goutils v1.9.1/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA= 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 h1:TXTz+RA4c5tNZRtdb4eVGjeL15xSjQOOSxfQ5ZwmKeE=
git.mstar.dev/mstar/goutils v1.10.0/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA= 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 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 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 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/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 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 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 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 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 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 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= github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=

10
shared/constants.go Normal file
View file

@ -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"
)

1
shared/rsaKey.go Normal file
View file

@ -0,0 +1 @@
package shared

View file

@ -1456,6 +1456,7 @@ type IUserDo interface {
UnderlyingDB() *gorm.DB UnderlyingDB() *gorm.DB
schema.Tabler schema.Tabler
GetByUsernameUnrestricted(username string) (result *models.User, err error)
GetByUsername(username string) (result *models.User, err error) GetByUsername(username string) (result *models.User, err error)
GetPagedTruePublic(pageNr uint) (result []models.User, err error) GetPagedTruePublic(pageNr uint) (result []models.User, err error)
GetPagedAllDeleted(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) 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 // 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 params []interface{}
var generateSQL strings.Builder var generateSQL strings.Builder
@ -1480,6 +1481,31 @@ func (u userDo) GetByUsername(username string) (result *models.User, err error)
return 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) // Get all true public accounts (verified & no restricted follow & indexable)
// in a paged manner, sorted by date saved // in a paged manner, sorted by date saved
// //

View file

@ -80,9 +80,20 @@ type User struct {
} }
type IUser interface { 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 // 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) GetByUsername(username string) (*gen.T, error)
// Get all true public accounts (verified & no restricted follow & indexable) // Get all true public accounts (verified & no restricted follow & indexable)

View file

@ -1,8 +1,9 @@
package storage package storage
import ( import (
"crypto/ed25519"
"crypto/rand" "crypto/rand"
"crypto/rsa"
"crypto/x509"
"database/sql" "database/sql"
"git.mstar.dev/mstar/goutils/other" "git.mstar.dev/mstar/goutils/other"
@ -102,7 +103,16 @@ func insertUser(server *models.RemoteServer) (*models.User, error) {
if err != gorm.ErrRecordNotFound { if err != gorm.ErrRecordNotFound {
return nil, err 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) pkeyId := make([]byte, 64)
_, err = rand.Read(pkeyId) _, err = rand.Read(pkeyId)
if err != nil { if err != nil {
@ -126,8 +136,8 @@ func insertUser(server *models.RemoteServer) (*models.User, error) {
Banner: nil, Banner: nil,
BannerId: sql.NullString{Valid: false}, BannerId: sql.NullString{Valid: false},
Indexable: false, Indexable: false,
PublicKey: publicKey, PublicKey: publicKeyBytes,
PrivateKey: privateKey, PrivateKey: privateKeyBytes,
Verified: true, Verified: true,
FinishedRegistration: true, FinishedRegistration: true,
PasskeyId: pkeyId, PasskeyId: pkeyId,
@ -155,6 +165,13 @@ func insertUserPronoun(user *models.User) error {
} }
func attachUserToRole(user *models.User) error { func attachUserToRole(user *models.User) error {
_, 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{ u2r := models.UserToRole{
User: *user, User: *user,
UserId: user.ID, UserId: user.ID,
@ -162,4 +179,7 @@ func attachUserToRole(user *models.User) error {
RoleId: models.FullAdminRole.ID, RoleId: models.FullAdminRole.ID,
} }
return dbgen.UserToRole.Save(&u2r) return dbgen.UserToRole.Save(&u2r)
default:
return err
}
} }

View file

@ -1,3 +1,78 @@
package storage 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 //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
}
}

View file

@ -12,6 +12,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gorm.io/gorm" "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/dbgen"
"git.mstar.dev/mstar/linstrom/storage-new/models" "git.mstar.dev/mstar/linstrom/storage-new/models"
webshared "git.mstar.dev/mstar/linstrom/web/shared" 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) user, err := dbgen.User.GetByUsername(username)
if err != nil { if err != nil {
log.Error().Err(err).Str("name", username).Msg("Failed to get user") 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) httputils.HttpErr(w, 0, "failed to get user", http.StatusInternalServerError)
return return
} }

View file

@ -1,8 +1,9 @@
package webdebug package webdebug
import ( import (
"crypto/ed25519"
"crypto/rand" "crypto/rand"
"crypto/rsa"
"crypto/x509"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -64,7 +65,10 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
return 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) pkeyId := make([]byte, 64)
_, err = rand.Read(pkeyId) _, err = rand.Read(pkeyId)
if err != nil { if err != nil {
@ -96,8 +100,8 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) {
Description: data.Description, Description: data.Description,
IsBot: data.IsBot, IsBot: data.IsBot,
ServerId: 1, // Hardcoded, Self is always first ID ServerId: 1, // Hardcoded, Self is always first ID
PublicKey: publicKey, PublicKey: publicKeyBytes,
PrivateKey: privateKey, PrivateKey: privateKeyBytes,
PasskeyId: pkeyId, PasskeyId: pkeyId,
} }
if data.Birthday != nil { if data.Birthday != nil {

View file

@ -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
}

View file

@ -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
}
*/

View file

@ -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))
}

20
web/public/api/api.go Normal file
View file

@ -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
}

167
web/public/api/webfinger.go Normal file
View file

@ -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<username>[\w-]+)@(?<domain>[\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:<username>@<domain>`,
),
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)
}

22
web/public/errorpages.go Normal file
View file

@ -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:<username>@<domain>\"",
"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)
}
}

View file

@ -30,6 +30,10 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
webutils "git.mstar.dev/mstar/goutils/http"
"git.mstar.dev/mstar/linstrom/web/public/api"
) )
type Server struct { type Server struct {
@ -42,8 +46,13 @@ func New(addr string) *Server {
w.WriteHeader(500) w.WriteHeader(500)
fmt.Fprint(w, "not implemented") 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{ server := http.Server{
Handler: handler, Handler: webutils.ChainMiddlewares(handler, webutils.LoggingMiddleware),
Addr: addr, Addr: addr,
} }
return &Server{&server} return &Server{&server}