diff --git a/api.go b/api.go index 330c869..c9bce6e 100644 --- a/api.go +++ b/api.go @@ -72,7 +72,7 @@ func (app *App) HandleAPIError(err error, c *echo.Context) error { } if log { - app.LogError(err, c) + LogError(err, c) } return (*c).JSON(code, APIError{Message: message}) diff --git a/common.go b/common.go index 22198ac..3e364b8 100644 --- a/common.go +++ b/common.go @@ -294,7 +294,7 @@ func (app *App) HandleYggdrasilError(err error, c *echo.Context) error { 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")}) } diff --git a/config.go b/config.go index 2ac4f8d..e82b027 100644 --- a/config.go +++ b/config.go @@ -11,7 +11,6 @@ import ( "github.com/dgraph-io/ristretto" "github.com/samber/mo" "golang.org/x/net/idna" - "log" "net/url" "os" "path" @@ -131,7 +130,6 @@ type Config struct { SkinSizeLimit int OfflineSkins bool StateDirectory string - TestMode bool TokenExpireSec int TokenStaleSec int TransientUsers transientUsersConfig @@ -199,7 +197,6 @@ func DefaultConfig() Config { SignPublicKeys: true, SkinSizeLimit: 128, StateDirectory: GetDefaultStateDirectory(), - TestMode: false, TokenExpireSec: 0, TokenStaleSec: 0, TransientUsers: transientUsersConfig{ @@ -279,9 +276,6 @@ func CleanConfig(config *Config) error { if config.ListenAddress == "" { 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 { return fmt.Errorf("DefaultMaxPlayerCount must be >= 0, or %d to indicate unlimited players", Constants.MaxPlayerCountUnlimited) } @@ -421,54 +415,73 @@ Allow = 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." - 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") { config.CreateNewPlayer.AllowChoosingUUID = config.RegistrationNewPlayer.AllowChoosingUUID } } - if metadata.IsDefined("RegistrationExistingPlayer", "Nickname") { - log.Printf(warningTemplate, "RegistrationExistingPlayer.Nickname", "ImportExistingPlayer.Nickname") + path_ = []string{"RegistrationExistingPlayer", "Nickname"} + if metadata.IsDefined(path_...) { + LogInfo(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.Nickname") + deprecatedPaths = append(deprecatedPaths, path_) if !metadata.IsDefined("ImportExistingPlayer", "Nickname") { config.ImportExistingPlayer.Nickname = config.RegistrationExistingPlayer.Nickname } } - if metadata.IsDefined("RegistrationExistingPlayer", "SessionURL") { - log.Printf(warningTemplate, "RegistrationExistingPlayer.SessionURL", "ImportExistingPlayer.SessionURL") + path_ = []string{"RegistrationExistingPlayer", "SessionURL"} + if metadata.IsDefined(path_...) { + LogInfo(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.SessionURL") + deprecatedPaths = append(deprecatedPaths, path_) if !metadata.IsDefined("ImportExistingPlayer", "SessionURL") { config.ImportExistingPlayer.SessionURL = config.RegistrationExistingPlayer.SessionURL } } - if metadata.IsDefined("RegistrationExistingPlayer", "AccountURL") { - log.Printf(warningTemplate, "RegistrationExistingPlayer.AccountURL", "ImportExistingPlayer.AccountURL") + path_ = []string{"RegistrationExistingPlayer", "AccountURL"} + if metadata.IsDefined(path_...) { + LogInfo(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.AccountURL") + deprecatedPaths = append(deprecatedPaths, path_) if !metadata.IsDefined("ImportExistingPlayer", "AccountURL") { config.ImportExistingPlayer.AccountURL = config.RegistrationExistingPlayer.AccountURL } } - if metadata.IsDefined("RegistrationExistingPlayer", "SetSkinURL") { - log.Printf(warningTemplate, "RegistrationExistingPlayer.SetSkinURL", "ImportExistingPlayer.SetSkinURL") + path_ = []string{"RegistrationExistingPlayer", "SetSkinURL"} + if metadata.IsDefined(path_...) { + LogInfo(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.SetSkinURL") + deprecatedPaths = append(deprecatedPaths, path_) if !metadata.IsDefined("ImportExistingPlayer", "SetSkinURL") { config.ImportExistingPlayer.SetSkinURL = config.RegistrationExistingPlayer.SetSkinURL } } - if metadata.IsDefined("RegistrationExistingPlayer", "RequireSkinVerification") { - log.Printf(warningTemplate, "RegistrationExistingPlayer.RequireSkinVerification", "ImportExistingPlayer.RequireSkinVerification") + path_ = []string{"RegistrationExistingPlayer", "RequireSkinVerification"} + if metadata.IsDefined(path_...) { + LogInfo(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.RequireSkinVerification") + deprecatedPaths = append(deprecatedPaths, path_) if !metadata.IsDefined("ImportExistingPlayer", "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() _, err := os.Stat(path) 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) err := os.MkdirAll(dir, 0755) Check(err) @@ -480,21 +493,21 @@ func ReadOrCreateConfig(path string) *Config { Check(err) } - log.Println("Loading config from", path) + LogInfo("Loading config from", path) metadata, err := toml.DecodeFile(path, &config) Check(err) 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) 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 { diff --git a/config_test.go b/config_test.go index 9020d4e..48e1d20 100644 --- a/config_test.go +++ b/config_test.go @@ -61,10 +61,6 @@ func TestConfig(t *testing.T) { config.DefaultMaxPlayerCount = Constants.MaxPlayerCountUnlimited assert.Nil(t, CleanConfig(config)) - config = configTestConfig(sd) - config.DataDirectory = "/tmp/DraslInvalidDataDirectoryNothingHere" - assert.NotNil(t, CleanConfig(config)) - // Missing state directory should be ignored config = configTestConfig(sd) config.StateDirectory = "/tmp/DraslInvalidStateDirectoryNothingHere" @@ -154,4 +150,21 @@ func TestConfig(t *testing.T) { var templateConfig Config _, err := toml.Decode(TEMPLATE_CONFIG_FILE, &templateConfig) 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) } diff --git a/example/config-example.toml b/example/config-example.toml index 544b387..f4bf27e 100644 --- a/example/config-example.toml +++ b/example/config-example.toml @@ -1,15 +1,17 @@ # Drasl default config file # Example: drasl.example.com -Domain = "" +Domain = "CHANGEME" # Example: https://drasl.example.com -BaseURL = "" +BaseURL = "https://CHANGEME" # List of usernames who automatically become admins of the Drasl instance DefaultAdmins = [""] +[CreateNewPlayer] +AllowChoosingUUID = true + [RegistrationNewPlayer] Allow = true -AllowChoosingUUID = true RequireInvite = true diff --git a/example/docker-caddy/config/config.toml b/example/docker-caddy/config/config.toml index 544b387..f4bf27e 100644 --- a/example/docker-caddy/config/config.toml +++ b/example/docker-caddy/config/config.toml @@ -1,15 +1,17 @@ # Drasl default config file # Example: drasl.example.com -Domain = "" +Domain = "CHANGEME" # Example: https://drasl.example.com -BaseURL = "" +BaseURL = "https://CHANGEME" # List of usernames who automatically become admins of the Drasl instance DefaultAdmins = [""] +[CreateNewPlayer] +AllowChoosingUUID = true + [RegistrationNewPlayer] Allow = true -AllowChoosingUUID = true RequireInvite = true diff --git a/example/docker/config/config.toml b/example/docker/config/config.toml index 544b387..f4bf27e 100644 --- a/example/docker/config/config.toml +++ b/example/docker/config/config.toml @@ -1,15 +1,17 @@ # Drasl default config file # Example: drasl.example.com -Domain = "" +Domain = "CHANGEME" # Example: https://drasl.example.com -BaseURL = "" +BaseURL = "https://CHANGEME" # List of usernames who automatically become admins of the Drasl instance DefaultAdmins = [""] +[CreateNewPlayer] +AllowChoosingUUID = true + [RegistrationNewPlayer] Allow = true -AllowChoosingUUID = true RequireInvite = true diff --git a/front.go b/front.go index 976d6ec..fc6ab39 100644 --- a/front.go +++ b/front.go @@ -171,7 +171,7 @@ func (app *App) HandleWebError(err error, c *echo.Context) error { } } - app.LogError(err, c) + LogError(err, c) safeMethods := []string{ "GET", diff --git a/main.go b/main.go index 049d512..41e9540 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,13 @@ import ( "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) { fmt.Printf("%s\n", reqBody) @@ -65,8 +71,14 @@ type App struct { OIDCProvidersByIssuer map[string]*OIDCProvider } -func (app *App) LogError(err error, c *echo.Context) { - if err != nil && !app.Config.TestMode { +func LogInfo(args ...interface{}) { + 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) } } @@ -83,7 +95,7 @@ func (app *App) HandleError(err error, c echo.Context) { additionalErr = app.HandleYggdrasilError(err, &c) } 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 { e := echo.New() e.HideBanner = true - e.HidePort = app.Config.TestMode + e.HidePort = DRASL_TEST() e.HTTPErrorHandler = app.HandleError e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { @@ -146,7 +158,7 @@ func (app *App) MakeServer() *echo.Echo { if app.Config.LogRequests { e.Use(middleware.Logger()) } - if DEBUG { + if DRASL_DEBUG() { e.Use(bodyDump) } 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) } } + if _, err := os.Open(config.DataDirectory); err != nil { + log.Fatalf("Couldn't access DataDirectory: %s", err) + } // Crypto key := ReadOrCreateKey(config) @@ -550,7 +565,7 @@ func setup(config *Config) *App { Check(err) // Print an initial invite link if necessary - if !app.Config.TestMode { + if !DRASL_TEST() { newPlayerInvite := app.Config.RegistrationNewPlayer.Allow && config.RegistrationNewPlayer.RequireInvite existingPlayerInvite := app.Config.RegistrationExistingPlayer.Allow && config.RegistrationExistingPlayer.RequireInvite if newPlayerInvite || existingPlayerInvite { @@ -595,8 +610,11 @@ func main() { os.Exit(0) } - config := ReadOrCreateConfig(*configPath) - app := setup(config) + config, _, err := ReadConfig(*configPath, true) + if err != nil { + log.Fatalf("Error in config: %s", err) + } + app := setup(&config) Check(app.MakeServer().Start(app.Config.ListenAddress)) } diff --git a/test_suite_test.go b/test_suite_test.go index 88b4cc4..ef424f5 100644 --- a/test_suite_test.go +++ b/test_suite_test.go @@ -24,6 +24,8 @@ import ( "time" ) +var _ = os.Setenv("DRASL_TEST", "1") + const TEST_USERNAME = "Username" const TEST_USERNAME_UPPERCASE = "USERNAME" const TEST_PLAYER_NAME = "Username" @@ -324,7 +326,6 @@ func testConfig() *Config { config.RateLimit = noRateLimit config.FallbackAPIServers = []FallbackAPIServer{} config.LogRequests = false - config.TestMode = true return &config }