Update config examples and test them

This commit is contained in:
Evan Goode 2025-03-28 20:53:45 -04:00
parent 65328999a5
commit d7ffab2612
10 changed files with 104 additions and 53 deletions

2
api.go
View File

@ -72,7 +72,7 @@ func (app *App) HandleAPIError(err error, c *echo.Context) error {
} }
if log { if log {
app.LogError(err, c) LogError(err, c)
} }
return (*c).JSON(code, APIError{Message: message}) return (*c).JSON(code, APIError{Message: message})

View File

@ -294,7 +294,7 @@ func (app *App) HandleYggdrasilError(err error, c *echo.Context) error {
return (*c).JSON(httpError.Code, YggdrasilErrorResponse{Path: &path_}) return (*c).JSON(httpError.Code, YggdrasilErrorResponse{Path: &path_})
} }
} }
app.LogError(err, c) LogError(err, c)
return (*c).JSON(http.StatusInternalServerError, YggdrasilErrorResponse{Path: &path_, ErrorMessage: Ptr("internal server error")}) return (*c).JSON(http.StatusInternalServerError, YggdrasilErrorResponse{Path: &path_, ErrorMessage: Ptr("internal server error")})
} }

View File

@ -11,7 +11,6 @@ import (
"github.com/dgraph-io/ristretto" "github.com/dgraph-io/ristretto"
"github.com/samber/mo" "github.com/samber/mo"
"golang.org/x/net/idna" "golang.org/x/net/idna"
"log"
"net/url" "net/url"
"os" "os"
"path" "path"
@ -131,7 +130,6 @@ type Config struct {
SkinSizeLimit int SkinSizeLimit int
OfflineSkins bool OfflineSkins bool
StateDirectory string StateDirectory string
TestMode bool
TokenExpireSec int TokenExpireSec int
TokenStaleSec int TokenStaleSec int
TransientUsers transientUsersConfig TransientUsers transientUsersConfig
@ -199,7 +197,6 @@ func DefaultConfig() Config {
SignPublicKeys: true, SignPublicKeys: true,
SkinSizeLimit: 128, SkinSizeLimit: 128,
StateDirectory: GetDefaultStateDirectory(), StateDirectory: GetDefaultStateDirectory(),
TestMode: false,
TokenExpireSec: 0, TokenExpireSec: 0,
TokenStaleSec: 0, TokenStaleSec: 0,
TransientUsers: transientUsersConfig{ TransientUsers: transientUsersConfig{
@ -279,9 +276,6 @@ func CleanConfig(config *Config) error {
if config.ListenAddress == "" { if config.ListenAddress == "" {
return errors.New("ListenAddress must be set. Example: 0.0.0.0:25585") return errors.New("ListenAddress must be set. Example: 0.0.0.0:25585")
} }
if _, err := os.Open(config.DataDirectory); err != nil {
return fmt.Errorf("Couldn't open DataDirectory: %s", err)
}
if config.DefaultMaxPlayerCount < 0 && config.DefaultMaxPlayerCount != Constants.MaxPlayerCountUnlimited { if config.DefaultMaxPlayerCount < 0 && config.DefaultMaxPlayerCount != Constants.MaxPlayerCountUnlimited {
return fmt.Errorf("DefaultMaxPlayerCount must be >= 0, or %d to indicate unlimited players", Constants.MaxPlayerCountUnlimited) return fmt.Errorf("DefaultMaxPlayerCount must be >= 0, or %d to indicate unlimited players", Constants.MaxPlayerCountUnlimited)
} }
@ -421,54 +415,73 @@ Allow = true
RequireInvite = true RequireInvite = true
` `
func HandleDeprecations(config Config, metadata *toml.MetaData) { func HandleDeprecations(config Config, metadata *toml.MetaData) [][]string {
deprecatedPaths := make([][]string, 0, 0)
warningTemplate := "Warning: config option %s is deprecated and will be removed in a future version. Use %s instead." warningTemplate := "Warning: config option %s is deprecated and will be removed in a future version. Use %s instead."
if metadata.IsDefined("RegistrationNewPlayer", "AllowChoosingUUID") {
log.Printf(warningTemplate, "RegistrationNewPlayer.AllowChoosingUUID", "CreateNewPlayer.AllowChoosingUUID") path_ := []string{"RegistrationNewPlayer", "AllowChoosingUUID"}
if metadata.IsDefined(path_...) {
LogInfo(warningTemplate, strings.Join(path_, "."), "CreateNewPlayer.AllowChoosingUUID")
deprecatedPaths = append(deprecatedPaths, path_)
if !metadata.IsDefined("CreateNewPlayer", "AllowChoosingUUID") { if !metadata.IsDefined("CreateNewPlayer", "AllowChoosingUUID") {
config.CreateNewPlayer.AllowChoosingUUID = config.RegistrationNewPlayer.AllowChoosingUUID config.CreateNewPlayer.AllowChoosingUUID = config.RegistrationNewPlayer.AllowChoosingUUID
} }
} }
if metadata.IsDefined("RegistrationExistingPlayer", "Nickname") { path_ = []string{"RegistrationExistingPlayer", "Nickname"}
log.Printf(warningTemplate, "RegistrationExistingPlayer.Nickname", "ImportExistingPlayer.Nickname") if metadata.IsDefined(path_...) {
LogInfo(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.Nickname")
deprecatedPaths = append(deprecatedPaths, path_)
if !metadata.IsDefined("ImportExistingPlayer", "Nickname") { if !metadata.IsDefined("ImportExistingPlayer", "Nickname") {
config.ImportExistingPlayer.Nickname = config.RegistrationExistingPlayer.Nickname config.ImportExistingPlayer.Nickname = config.RegistrationExistingPlayer.Nickname
} }
} }
if metadata.IsDefined("RegistrationExistingPlayer", "SessionURL") { path_ = []string{"RegistrationExistingPlayer", "SessionURL"}
log.Printf(warningTemplate, "RegistrationExistingPlayer.SessionURL", "ImportExistingPlayer.SessionURL") if metadata.IsDefined(path_...) {
LogInfo(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.SessionURL")
deprecatedPaths = append(deprecatedPaths, path_)
if !metadata.IsDefined("ImportExistingPlayer", "SessionURL") { if !metadata.IsDefined("ImportExistingPlayer", "SessionURL") {
config.ImportExistingPlayer.SessionURL = config.RegistrationExistingPlayer.SessionURL config.ImportExistingPlayer.SessionURL = config.RegistrationExistingPlayer.SessionURL
} }
} }
if metadata.IsDefined("RegistrationExistingPlayer", "AccountURL") { path_ = []string{"RegistrationExistingPlayer", "AccountURL"}
log.Printf(warningTemplate, "RegistrationExistingPlayer.AccountURL", "ImportExistingPlayer.AccountURL") if metadata.IsDefined(path_...) {
LogInfo(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.AccountURL")
deprecatedPaths = append(deprecatedPaths, path_)
if !metadata.IsDefined("ImportExistingPlayer", "AccountURL") { if !metadata.IsDefined("ImportExistingPlayer", "AccountURL") {
config.ImportExistingPlayer.AccountURL = config.RegistrationExistingPlayer.AccountURL config.ImportExistingPlayer.AccountURL = config.RegistrationExistingPlayer.AccountURL
} }
} }
if metadata.IsDefined("RegistrationExistingPlayer", "SetSkinURL") { path_ = []string{"RegistrationExistingPlayer", "SetSkinURL"}
log.Printf(warningTemplate, "RegistrationExistingPlayer.SetSkinURL", "ImportExistingPlayer.SetSkinURL") if metadata.IsDefined(path_...) {
LogInfo(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.SetSkinURL")
deprecatedPaths = append(deprecatedPaths, path_)
if !metadata.IsDefined("ImportExistingPlayer", "SetSkinURL") { if !metadata.IsDefined("ImportExistingPlayer", "SetSkinURL") {
config.ImportExistingPlayer.SetSkinURL = config.RegistrationExistingPlayer.SetSkinURL config.ImportExistingPlayer.SetSkinURL = config.RegistrationExistingPlayer.SetSkinURL
} }
} }
if metadata.IsDefined("RegistrationExistingPlayer", "RequireSkinVerification") { path_ = []string{"RegistrationExistingPlayer", "RequireSkinVerification"}
log.Printf(warningTemplate, "RegistrationExistingPlayer.RequireSkinVerification", "ImportExistingPlayer.RequireSkinVerification") if metadata.IsDefined(path_...) {
LogInfo(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.RequireSkinVerification")
deprecatedPaths = append(deprecatedPaths, path_)
if !metadata.IsDefined("ImportExistingPlayer", "RequireSkinVerification") { if !metadata.IsDefined("ImportExistingPlayer", "RequireSkinVerification") {
config.ImportExistingPlayer.RequireSkinVerification = config.RegistrationExistingPlayer.RequireSkinVerification config.ImportExistingPlayer.RequireSkinVerification = config.RegistrationExistingPlayer.RequireSkinVerification
} }
} }
return deprecatedPaths
} }
func ReadOrCreateConfig(path string) *Config { func ReadConfig(path string, createIfNotExists bool) (Config, [][]string, error) {
config := DefaultConfig() config := DefaultConfig()
_, err := os.Stat(path) _, err := os.Stat(path)
if err != nil { if err != nil {
// File doesn't exist? Try to create it if !createIfNotExists {
return Config{}, nil, err
}
log.Println("Config file at", path, "doesn't exist, creating it with template values.") LogInfo("Config file at", path, "doesn't exist, creating it with template values.")
dir := filepath.Dir(path) dir := filepath.Dir(path)
err := os.MkdirAll(dir, 0755) err := os.MkdirAll(dir, 0755)
Check(err) Check(err)
@ -480,21 +493,21 @@ func ReadOrCreateConfig(path string) *Config {
Check(err) Check(err)
} }
log.Println("Loading config from", path) LogInfo("Loading config from", path)
metadata, err := toml.DecodeFile(path, &config) metadata, err := toml.DecodeFile(path, &config)
Check(err) Check(err)
for _, key := range metadata.Undecoded() { for _, key := range metadata.Undecoded() {
log.Println("Warning: unknown config option", strings.Join(key, ".")) LogInfo("Warning: unknown config option", strings.Join(key, "."))
} }
HandleDeprecations(config, &metadata) deprecations := HandleDeprecations(config, &metadata)
err = CleanConfig(&config) err = CleanConfig(&config)
if err != nil { if err != nil {
log.Fatal(fmt.Errorf("Error in config: %s", err)) return Config{}, nil, err
} }
return &config return config, deprecations, nil
} }
func ReadOrCreateKey(config *Config) *rsa.PrivateKey { func ReadOrCreateKey(config *Config) *rsa.PrivateKey {

View File

@ -61,10 +61,6 @@ func TestConfig(t *testing.T) {
config.DefaultMaxPlayerCount = Constants.MaxPlayerCountUnlimited config.DefaultMaxPlayerCount = Constants.MaxPlayerCountUnlimited
assert.Nil(t, CleanConfig(config)) assert.Nil(t, CleanConfig(config))
config = configTestConfig(sd)
config.DataDirectory = "/tmp/DraslInvalidDataDirectoryNothingHere"
assert.NotNil(t, CleanConfig(config))
// Missing state directory should be ignored // Missing state directory should be ignored
config = configTestConfig(sd) config = configTestConfig(sd)
config.StateDirectory = "/tmp/DraslInvalidStateDirectoryNothingHere" config.StateDirectory = "/tmp/DraslInvalidStateDirectoryNothingHere"
@ -154,4 +150,21 @@ func TestConfig(t *testing.T) {
var templateConfig Config var templateConfig Config
_, err := toml.Decode(TEMPLATE_CONFIG_FILE, &templateConfig) _, err := toml.Decode(TEMPLATE_CONFIG_FILE, &templateConfig)
assert.Nil(t, err) assert.Nil(t, err)
// Test that the example configs are valid
_, deprecations, err := ReadConfig("example/config-example.toml", false)
assert.Empty(t, deprecations)
assert.Nil(t, err)
// The example configs should all be the same
correctBytes, err := os.ReadFile("example/config-example.toml")
assert.Nil(t, err)
configBytes, err := os.ReadFile("example/docker/config/config.toml")
assert.Nil(t, err)
assert.Equal(t, correctBytes, configBytes)
configBytes, err = os.ReadFile("example/docker-caddy/config/config.toml")
assert.Nil(t, err)
assert.Equal(t, correctBytes, configBytes)
} }

View File

@ -1,15 +1,17 @@
# Drasl default config file # Drasl default config file
# Example: drasl.example.com # Example: drasl.example.com
Domain = "" Domain = "CHANGEME"
# Example: https://drasl.example.com # Example: https://drasl.example.com
BaseURL = "" BaseURL = "https://CHANGEME"
# List of usernames who automatically become admins of the Drasl instance # List of usernames who automatically become admins of the Drasl instance
DefaultAdmins = [""] DefaultAdmins = [""]
[CreateNewPlayer]
AllowChoosingUUID = true
[RegistrationNewPlayer] [RegistrationNewPlayer]
Allow = true Allow = true
AllowChoosingUUID = true
RequireInvite = true RequireInvite = true

View File

@ -1,15 +1,17 @@
# Drasl default config file # Drasl default config file
# Example: drasl.example.com # Example: drasl.example.com
Domain = "" Domain = "CHANGEME"
# Example: https://drasl.example.com # Example: https://drasl.example.com
BaseURL = "" BaseURL = "https://CHANGEME"
# List of usernames who automatically become admins of the Drasl instance # List of usernames who automatically become admins of the Drasl instance
DefaultAdmins = [""] DefaultAdmins = [""]
[CreateNewPlayer]
AllowChoosingUUID = true
[RegistrationNewPlayer] [RegistrationNewPlayer]
Allow = true Allow = true
AllowChoosingUUID = true
RequireInvite = true RequireInvite = true

View File

@ -1,15 +1,17 @@
# Drasl default config file # Drasl default config file
# Example: drasl.example.com # Example: drasl.example.com
Domain = "" Domain = "CHANGEME"
# Example: https://drasl.example.com # Example: https://drasl.example.com
BaseURL = "" BaseURL = "https://CHANGEME"
# List of usernames who automatically become admins of the Drasl instance # List of usernames who automatically become admins of the Drasl instance
DefaultAdmins = [""] DefaultAdmins = [""]
[CreateNewPlayer]
AllowChoosingUUID = true
[RegistrationNewPlayer] [RegistrationNewPlayer]
Allow = true Allow = true
AllowChoosingUUID = true
RequireInvite = true RequireInvite = true

View File

@ -171,7 +171,7 @@ func (app *App) HandleWebError(err error, c *echo.Context) error {
} }
} }
app.LogError(err, c) LogError(err, c)
safeMethods := []string{ safeMethods := []string{
"GET", "GET",

36
main.go
View File

@ -30,7 +30,13 @@ import (
"time" "time"
) )
var DEBUG = os.Getenv("DRASL_DEBUG") != "" func DRASL_DEBUG() bool {
return os.Getenv("DRASL_DEBUG") != ""
}
func DRASL_TEST() bool {
return os.Getenv("DRASL_TEST") != ""
}
var bodyDump = middleware.BodyDump(func(c echo.Context, reqBody, resBody []byte) { var bodyDump = middleware.BodyDump(func(c echo.Context, reqBody, resBody []byte) {
fmt.Printf("%s\n", reqBody) fmt.Printf("%s\n", reqBody)
@ -65,8 +71,14 @@ type App struct {
OIDCProvidersByIssuer map[string]*OIDCProvider OIDCProvidersByIssuer map[string]*OIDCProvider
} }
func (app *App) LogError(err error, c *echo.Context) { func LogInfo(args ...interface{}) {
if err != nil && !app.Config.TestMode { if !DRASL_TEST() {
log.Println(args...)
}
}
func LogError(err error, c *echo.Context) {
if err != nil && !DRASL_TEST() {
log.Println("Unexpected error in "+(*c).Request().Method+" "+(*c).Path()+":", err) log.Println("Unexpected error in "+(*c).Request().Method+" "+(*c).Path()+":", err)
} }
} }
@ -83,7 +95,7 @@ func (app *App) HandleError(err error, c echo.Context) {
additionalErr = app.HandleYggdrasilError(err, &c) additionalErr = app.HandleYggdrasilError(err, &c)
} }
if additionalErr != nil { if additionalErr != nil {
app.LogError(fmt.Errorf("Additional error while handling an error: %w", additionalErr), &c) LogError(fmt.Errorf("Additional error while handling an error: %w", additionalErr), &c)
} }
} }
@ -134,7 +146,7 @@ func makeRateLimiter(app *App) echo.MiddlewareFunc {
func (app *App) MakeServer() *echo.Echo { func (app *App) MakeServer() *echo.Echo {
e := echo.New() e := echo.New()
e.HideBanner = true e.HideBanner = true
e.HidePort = app.Config.TestMode e.HidePort = DRASL_TEST()
e.HTTPErrorHandler = app.HandleError e.HTTPErrorHandler = app.HandleError
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
@ -146,7 +158,7 @@ func (app *App) MakeServer() *echo.Echo {
if app.Config.LogRequests { if app.Config.LogRequests {
e.Use(middleware.Logger()) e.Use(middleware.Logger())
} }
if DEBUG { if DRASL_DEBUG() {
e.Use(bodyDump) e.Use(bodyDump)
} }
if app.Config.RateLimit.Enable { if app.Config.RateLimit.Enable {
@ -400,6 +412,9 @@ func setup(config *Config) *App {
log.Fatalf("Couldn't access StateDirectory %s: %s", config.StateDirectory, err) log.Fatalf("Couldn't access StateDirectory %s: %s", config.StateDirectory, err)
} }
} }
if _, err := os.Open(config.DataDirectory); err != nil {
log.Fatalf("Couldn't access DataDirectory: %s", err)
}
// Crypto // Crypto
key := ReadOrCreateKey(config) key := ReadOrCreateKey(config)
@ -550,7 +565,7 @@ func setup(config *Config) *App {
Check(err) Check(err)
// Print an initial invite link if necessary // Print an initial invite link if necessary
if !app.Config.TestMode { if !DRASL_TEST() {
newPlayerInvite := app.Config.RegistrationNewPlayer.Allow && config.RegistrationNewPlayer.RequireInvite newPlayerInvite := app.Config.RegistrationNewPlayer.Allow && config.RegistrationNewPlayer.RequireInvite
existingPlayerInvite := app.Config.RegistrationExistingPlayer.Allow && config.RegistrationExistingPlayer.RequireInvite existingPlayerInvite := app.Config.RegistrationExistingPlayer.Allow && config.RegistrationExistingPlayer.RequireInvite
if newPlayerInvite || existingPlayerInvite { if newPlayerInvite || existingPlayerInvite {
@ -595,8 +610,11 @@ func main() {
os.Exit(0) os.Exit(0)
} }
config := ReadOrCreateConfig(*configPath) config, _, err := ReadConfig(*configPath, true)
app := setup(config) if err != nil {
log.Fatalf("Error in config: %s", err)
}
app := setup(&config)
Check(app.MakeServer().Start(app.Config.ListenAddress)) Check(app.MakeServer().Start(app.Config.ListenAddress))
} }

View File

@ -24,6 +24,8 @@ import (
"time" "time"
) )
var _ = os.Setenv("DRASL_TEST", "1")
const TEST_USERNAME = "Username" const TEST_USERNAME = "Username"
const TEST_USERNAME_UPPERCASE = "USERNAME" const TEST_USERNAME_UPPERCASE = "USERNAME"
const TEST_PLAYER_NAME = "Username" const TEST_PLAYER_NAME = "Username"
@ -324,7 +326,6 @@ func testConfig() *Config {
config.RateLimit = noRateLimit config.RateLimit = noRateLimit
config.FallbackAPIServers = []FallbackAPIServer{} config.FallbackAPIServers = []FallbackAPIServer{}
config.LogRequests = false config.LogRequests = false
config.TestMode = true
return &config return &config
} }