This commit is contained in:
parent
98191fd098
commit
d272fa90b4
20 changed files with 574 additions and 27 deletions
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
10
go.mod
10
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
|
||||
|
|
26
go.sum
26
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=
|
||||
|
|
10
shared/constants.go
Normal file
10
shared/constants.go
Normal 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
1
shared/rsaKey.go
Normal file
|
@ -0,0 +1 @@
|
|||
package shared
|
|
@ -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
|
||||
//
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
15
web/public/api/activitypub/activitypub.go
Normal file
15
web/public/api/activitypub/activitypub.go
Normal 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
|
||||
}
|
96
web/public/api/activitypub/user.go
Normal file
96
web/public/api/activitypub/user.go
Normal 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
|
||||
}
|
||||
*/
|
25
web/public/api/activitypub/util.go
Normal file
25
web/public/api/activitypub/util.go
Normal 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
20
web/public/api/api.go
Normal 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
167
web/public/api/webfinger.go
Normal 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
22
web/public/errorpages.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
|
|
Loading…
Reference in a new issue