mirror of
https://github.com/unmojang/drasl.git
synced 2025-08-03 19:06:04 -04:00
646 lines
19 KiB
Go
646 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/BurntSushi/toml"
|
|
mapset "github.com/deckarep/golang-set/v2"
|
|
"github.com/dgraph-io/ristretto"
|
|
"github.com/samber/mo"
|
|
"golang.org/x/net/idna"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
type rateLimitConfig struct {
|
|
Enable bool
|
|
RequestsPerSecond float64
|
|
}
|
|
|
|
type bodyLimitConfig struct {
|
|
Enable bool
|
|
SizeLimitKiB int
|
|
}
|
|
|
|
type rawFallbackAPIServerConfig struct {
|
|
Nickname *string
|
|
SessionURL *string
|
|
AccountURL *string
|
|
ServicesURL *string
|
|
SkinDomains *[]string
|
|
CacheTTLSeconds *int
|
|
DenyUnknownUsers *bool
|
|
EnableAuthentication *bool
|
|
}
|
|
|
|
type FallbackAPIServerConfig struct {
|
|
Nickname string
|
|
SessionURL string
|
|
AccountURL string
|
|
ServicesURL string
|
|
SkinDomains []string
|
|
CacheTTLSeconds int
|
|
DenyUnknownUsers bool
|
|
EnableAuthentication bool
|
|
}
|
|
|
|
type rawRegistrationOIDCConfig struct {
|
|
Name *string
|
|
Issuer *string
|
|
ClientID *string
|
|
ClientSecret *string
|
|
PKCE *bool
|
|
RequireInvite *bool
|
|
AllowChoosingPlayerName *bool
|
|
}
|
|
|
|
type RegistrationOIDCConfig struct {
|
|
Name string
|
|
Issuer string
|
|
ClientID string
|
|
ClientSecret string
|
|
PKCE bool
|
|
RequireInvite bool
|
|
AllowChoosingPlayerName bool
|
|
}
|
|
|
|
type transientUsersConfig struct {
|
|
Allow bool
|
|
UsernameRegex string
|
|
Password string
|
|
}
|
|
|
|
type v2RegistrationNewPlayerConfig struct {
|
|
AllowChoosingUUID bool
|
|
}
|
|
|
|
type registrationNewPlayerConfig struct {
|
|
v2RegistrationNewPlayerConfig
|
|
Allow bool
|
|
RequireInvite bool
|
|
}
|
|
|
|
type v2RegistrationExistingPlayerConfig struct {
|
|
Nickname string
|
|
SessionURL string
|
|
AccountURL string
|
|
SetSkinURL string
|
|
RequireSkinVerification bool
|
|
}
|
|
|
|
type registrationExistingPlayerConfig struct {
|
|
v2RegistrationExistingPlayerConfig
|
|
Allow bool
|
|
RequireInvite bool
|
|
}
|
|
|
|
type createNewPlayerConfig struct {
|
|
Allow bool
|
|
AllowChoosingUUID bool
|
|
}
|
|
|
|
type importExistingPlayerConfig struct {
|
|
Allow bool
|
|
Nickname string
|
|
SessionURL string
|
|
AccountURL string
|
|
SetSkinURL string
|
|
RequireSkinVerification bool
|
|
}
|
|
|
|
type BaseConfig struct {
|
|
AllowCapes bool
|
|
AllowChangingPlayerName bool
|
|
AllowMultipleAccessTokens bool
|
|
AllowPasswordLogin bool
|
|
AllowSkins bool
|
|
AllowTextureFromURL bool
|
|
AllowAddingDeletingPlayers bool
|
|
ApplicationOwner string
|
|
ApplicationName string
|
|
BaseURL string
|
|
BodyLimit bodyLimitConfig
|
|
CORSAllowOrigins []string
|
|
CreateNewPlayer createNewPlayerConfig
|
|
DataDirectory string
|
|
DefaultAdmins []string
|
|
DefaultPreferredLanguage string
|
|
DefaultMaxPlayerCount int
|
|
Domain string
|
|
EnableBackgroundEffect bool
|
|
EnableFooter bool
|
|
EnableWebFrontEnd bool
|
|
ForwardSkins bool
|
|
InstanceName string
|
|
ImportExistingPlayer importExistingPlayerConfig
|
|
ListenAddress string
|
|
LogRequests bool
|
|
MinPasswordLength int
|
|
PlayerUUIDGeneration string
|
|
PreMigrationBackups bool
|
|
RateLimit rateLimitConfig
|
|
RegistrationExistingPlayer registrationExistingPlayerConfig
|
|
RegistrationNewPlayer registrationNewPlayerConfig
|
|
RequestCache ristretto.Config
|
|
SignPublicKeys bool
|
|
SkinSizeLimit int
|
|
OfflineSkins bool
|
|
StateDirectory string
|
|
TokenExpireSec int
|
|
TokenStaleSec int
|
|
TransientUsers transientUsersConfig
|
|
ValidPlayerNameRegex string
|
|
}
|
|
|
|
type Config struct {
|
|
BaseConfig
|
|
FallbackAPIServers []FallbackAPIServerConfig
|
|
RegistrationOIDC []RegistrationOIDCConfig
|
|
}
|
|
|
|
type RawConfig struct {
|
|
BaseConfig
|
|
FallbackAPIServers []rawFallbackAPIServerConfig
|
|
RegistrationOIDC []rawRegistrationOIDCConfig
|
|
}
|
|
|
|
var defaultRateLimitConfig = rateLimitConfig{
|
|
Enable: true,
|
|
RequestsPerSecond: 5,
|
|
}
|
|
var defaultBodyLimitConfig = bodyLimitConfig{
|
|
Enable: true,
|
|
SizeLimitKiB: 8192,
|
|
}
|
|
|
|
var DefaultRistrettoConfig = &ristretto.Config{
|
|
// Defaults from https://pkg.go.dev/github.com/dgraph-io/ristretto#readme-config
|
|
NumCounters: 1e7,
|
|
MaxCost: 1 << 30, // 1 GiB
|
|
BufferItems: 64,
|
|
}
|
|
|
|
func DefaultRawConfig() RawConfig {
|
|
return RawConfig{
|
|
BaseConfig: BaseConfig{
|
|
AllowCapes: true,
|
|
AllowChangingPlayerName: true,
|
|
AllowPasswordLogin: true,
|
|
AllowSkins: true,
|
|
AllowTextureFromURL: false,
|
|
AllowAddingDeletingPlayers: false,
|
|
ApplicationName: "Drasl",
|
|
ApplicationOwner: "Anonymous",
|
|
BaseURL: "",
|
|
BodyLimit: defaultBodyLimitConfig,
|
|
CORSAllowOrigins: []string{},
|
|
CreateNewPlayer: createNewPlayerConfig{
|
|
Allow: true,
|
|
AllowChoosingUUID: false,
|
|
},
|
|
DataDirectory: GetDefaultDataDirectory(),
|
|
DefaultAdmins: []string{},
|
|
DefaultPreferredLanguage: "en",
|
|
DefaultMaxPlayerCount: 1,
|
|
Domain: "",
|
|
EnableBackgroundEffect: true,
|
|
EnableFooter: true,
|
|
EnableWebFrontEnd: true,
|
|
ForwardSkins: true,
|
|
ImportExistingPlayer: importExistingPlayerConfig{
|
|
Allow: false,
|
|
},
|
|
InstanceName: "Drasl",
|
|
ListenAddress: "0.0.0.0:25585",
|
|
LogRequests: true,
|
|
MinPasswordLength: 8,
|
|
OfflineSkins: true,
|
|
PlayerUUIDGeneration: "random",
|
|
PreMigrationBackups: true,
|
|
RateLimit: defaultRateLimitConfig,
|
|
RegistrationExistingPlayer: registrationExistingPlayerConfig{
|
|
Allow: false,
|
|
},
|
|
RegistrationNewPlayer: registrationNewPlayerConfig{
|
|
Allow: true,
|
|
RequireInvite: false,
|
|
},
|
|
RequestCache: *DefaultRistrettoConfig,
|
|
SignPublicKeys: true,
|
|
SkinSizeLimit: 64,
|
|
StateDirectory: GetDefaultStateDirectory(),
|
|
TokenExpireSec: 0,
|
|
TokenStaleSec: 0,
|
|
TransientUsers: transientUsersConfig{
|
|
Allow: false,
|
|
},
|
|
ValidPlayerNameRegex: "^[a-zA-Z0-9_]+$",
|
|
},
|
|
FallbackAPIServers: []rawFallbackAPIServerConfig{},
|
|
RegistrationOIDC: []rawRegistrationOIDCConfig{},
|
|
}
|
|
}
|
|
|
|
func DefaultConfig() Config {
|
|
return Config{
|
|
BaseConfig: DefaultRawConfig().BaseConfig,
|
|
}
|
|
}
|
|
|
|
func DefaultFallbackAPIServer() FallbackAPIServerConfig {
|
|
return FallbackAPIServerConfig{
|
|
CacheTTLSeconds: 600,
|
|
DenyUnknownUsers: false,
|
|
EnableAuthentication: true,
|
|
SkinDomains: []string{},
|
|
}
|
|
}
|
|
|
|
func DefaultRegistrationOIDC() RegistrationOIDCConfig {
|
|
return RegistrationOIDCConfig{
|
|
AllowChoosingPlayerName: true,
|
|
PKCE: true,
|
|
RequireInvite: false,
|
|
}
|
|
}
|
|
|
|
func AssignConfig[Res, Raw any](defaults Res, raw Raw) Res {
|
|
configType := reflect.TypeOf(defaults)
|
|
|
|
rawValue := reflect.ValueOf(raw)
|
|
defaultsValue := reflect.ValueOf(defaults)
|
|
|
|
out := new(Res)
|
|
outValue := reflect.ValueOf(out).Elem()
|
|
|
|
for i := 0; i < configType.NumField(); i += 1 {
|
|
key := configType.Field(i).Name
|
|
|
|
rawField := rawValue.FieldByName(key)
|
|
if rawField == (reflect.Value{}) {
|
|
continue
|
|
}
|
|
|
|
outField := outValue.FieldByName(key)
|
|
if rawField.IsNil() {
|
|
outField.Set(defaultsValue.FieldByName(key))
|
|
} else {
|
|
rawField := rawValue.FieldByName(key).Elem()
|
|
outField.Set(rawField)
|
|
}
|
|
}
|
|
|
|
return *out
|
|
}
|
|
|
|
func cleanURL(key string, required mo.Option[string], urlString string, trimTrailingSlash bool) (string, error) {
|
|
if urlString == "" {
|
|
if example, ok := required.Get(); ok {
|
|
return "", fmt.Errorf("%s must be set. Example: %s", key, example)
|
|
}
|
|
return urlString, nil
|
|
}
|
|
|
|
parsedURL, err := url.Parse(urlString)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Invalid %s: %s", key, err)
|
|
}
|
|
|
|
punycodeHost, err := idna.ToASCII(parsedURL.Host)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Invalid %s: %s", key, err)
|
|
}
|
|
parsedURL.Host = punycodeHost
|
|
|
|
if trimTrailingSlash {
|
|
parsedURL.Path = strings.TrimSuffix(parsedURL.Path, "/")
|
|
}
|
|
return parsedURL.String(), nil
|
|
}
|
|
|
|
func cleanDomain(key string, required mo.Option[string], domain string) (string, error) {
|
|
if domain == "" {
|
|
if example, ok := required.Get(); ok {
|
|
return "", fmt.Errorf("%s must be set. Example: %s", key, example)
|
|
}
|
|
return domain, nil
|
|
}
|
|
|
|
punycoded, err := idna.ToASCII(domain)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Invalid %s: %s", key, err)
|
|
}
|
|
return punycoded, nil
|
|
}
|
|
|
|
func CleanConfig(rawConfig *RawConfig) (Config, error) {
|
|
config := Config{}
|
|
config.BaseConfig = rawConfig.BaseConfig
|
|
|
|
var err error
|
|
config.BaseURL, err = cleanURL("BaseURL", mo.Some("https://drasl.example.com"), config.BaseURL, true)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
if !IsValidPreferredLanguage(config.DefaultPreferredLanguage) {
|
|
return Config{}, fmt.Errorf("Invalid DefaultPreferredLanguage %s", config.DefaultPreferredLanguage)
|
|
}
|
|
|
|
if config.Domain == "" {
|
|
return Config{}, errors.New("Domain must be set to a valid fully qualified domain name")
|
|
}
|
|
|
|
config.Domain, err = cleanDomain(
|
|
"Domain",
|
|
mo.Some("drasl.example.com"),
|
|
config.Domain,
|
|
)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
if config.InstanceName == "" {
|
|
return Config{}, errors.New("InstanceName must be set")
|
|
}
|
|
if config.ListenAddress == "" {
|
|
return Config{}, errors.New("ListenAddress must be set. Example: 0.0.0.0:25585")
|
|
}
|
|
if config.DefaultMaxPlayerCount < 0 && config.DefaultMaxPlayerCount != Constants.MaxPlayerCountUnlimited {
|
|
return Config{}, fmt.Errorf("DefaultMaxPlayerCount must be >= 0, or %d to indicate unlimited players", Constants.MaxPlayerCountUnlimited)
|
|
}
|
|
if config.RegistrationNewPlayer.Allow {
|
|
if !config.CreateNewPlayer.Allow {
|
|
return Config{}, errors.New("If RegisterNewPlayer is allowed, CreateNewPlayer must be allowed.")
|
|
}
|
|
}
|
|
switch config.PlayerUUIDGeneration {
|
|
case PlayerUUIDGenerationRandom:
|
|
case PlayerUUIDGenerationOffline:
|
|
default:
|
|
return Config{}, errors.New(`PlayerUUIDGeneration must be either "random" or "offline".`)
|
|
}
|
|
if config.RegistrationExistingPlayer.Allow {
|
|
if !config.ImportExistingPlayer.Allow {
|
|
return Config{}, errors.New("If RegistrationExistingPlayer is allowed, ImportExistingPlayer must be allowed.")
|
|
}
|
|
if config.ImportExistingPlayer.Nickname == "" {
|
|
return Config{}, errors.New("If RegistrationExistingPlayer is allowed, ImportExistingPlayer.Nickname must be set")
|
|
}
|
|
if config.ImportExistingPlayer.SessionURL == "" {
|
|
return Config{}, errors.New("If RegistrationExistingPlayer is allowed, ImportExistingPlayer.SessionURL must be set. Example: https://sessionserver.mojang.com")
|
|
}
|
|
if config.ImportExistingPlayer.AccountURL == "" {
|
|
return Config{}, errors.New("If RegistrationExistingPlayer is allowed, ImportExistingPlayer.AccountURL must be set. Example: https://api.mojang.com")
|
|
}
|
|
}
|
|
if config.ImportExistingPlayer.Allow {
|
|
if config.ImportExistingPlayer.Nickname == "" {
|
|
return Config{}, errors.New("ImportExistingPlayer.Nickname must be set")
|
|
}
|
|
|
|
config.ImportExistingPlayer.SessionURL, err = cleanURL(
|
|
"ImportExistingPlayer.SessionURL",
|
|
mo.Some("https://sessionserver.mojang.com"),
|
|
config.ImportExistingPlayer.SessionURL, true,
|
|
)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
config.ImportExistingPlayer.AccountURL, err = cleanURL(
|
|
"ImportExistingPlayer.AccountURL",
|
|
mo.Some("https://api.mojang.com"),
|
|
config.ImportExistingPlayer.AccountURL, true,
|
|
)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
config.ImportExistingPlayer.SetSkinURL, err = cleanURL(
|
|
"ImportExistingPlayer.SetSkinURL",
|
|
mo.None[string](),
|
|
config.ImportExistingPlayer.SetSkinURL, true,
|
|
)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
}
|
|
|
|
fallbackAPIServerNames := mapset.NewSet[string]()
|
|
for _, rawFallbackAPIServer := range PtrSlice(rawConfig.FallbackAPIServers) {
|
|
fallbackAPIServerConfig := AssignConfig(DefaultFallbackAPIServer(), *rawFallbackAPIServer)
|
|
|
|
if fallbackAPIServerConfig.Nickname == "" {
|
|
return Config{}, errors.New("FallbackAPIServer Nickname must be set")
|
|
}
|
|
if fallbackAPIServerNames.Contains(fallbackAPIServerConfig.Nickname) {
|
|
return Config{}, fmt.Errorf("Duplicate FallbackAPIServer Nickname: %s", fallbackAPIServerConfig.Nickname)
|
|
}
|
|
fallbackAPIServerNames.Add(fallbackAPIServerConfig.Nickname)
|
|
|
|
fallbackAPIServerConfig.SessionURL, err = cleanURL(
|
|
fmt.Sprintf("FallbackAPIServer %s SessionURL", fallbackAPIServerConfig.Nickname),
|
|
mo.Some("https://sessionserver.mojang.com"),
|
|
fallbackAPIServerConfig.SessionURL, true,
|
|
)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
fallbackAPIServerConfig.AccountURL, err = cleanURL(
|
|
fmt.Sprintf("FallbackAPIServer %s AccountURL", fallbackAPIServerConfig.Nickname),
|
|
mo.Some("https://api.mojang.com"),
|
|
fallbackAPIServerConfig.AccountURL, true,
|
|
)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
fallbackAPIServerConfig.ServicesURL, err = cleanURL(
|
|
fmt.Sprintf("FallbackAPIServer %s ServicesURL", fallbackAPIServerConfig.Nickname),
|
|
mo.Some("https://api.minecraftservices.com"),
|
|
fallbackAPIServerConfig.ServicesURL, true,
|
|
)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
for _, skinDomain := range PtrSlice(fallbackAPIServerConfig.SkinDomains) {
|
|
*skinDomain, err = cleanDomain(
|
|
fmt.Sprintf("FallbackAPIServer %s SkinDomain", fallbackAPIServerConfig.Nickname),
|
|
mo.Some("textures.minecraft.net"),
|
|
*skinDomain,
|
|
)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
}
|
|
|
|
config.FallbackAPIServers = append(config.FallbackAPIServers, fallbackAPIServerConfig)
|
|
}
|
|
|
|
oidcNames := mapset.NewSet[string]()
|
|
for _, rawOIDCConfig := range PtrSlice(rawConfig.RegistrationOIDC) {
|
|
oidcConfig := AssignConfig(DefaultRegistrationOIDC(), *rawOIDCConfig)
|
|
|
|
if oidcConfig.Name == "" {
|
|
return Config{}, errors.New("RegistrationOIDC Name must be set")
|
|
}
|
|
if oidcNames.Contains(oidcConfig.Name) {
|
|
return Config{}, fmt.Errorf("Duplicate RegistrationOIDC Name: %s", oidcConfig.Name)
|
|
}
|
|
oidcNames.Add(oidcConfig.Name)
|
|
oidcConfig.Issuer, err = cleanURL(
|
|
fmt.Sprintf("RegistrationOIDC %s Issuer", oidcConfig.Name),
|
|
mo.Some("https://idm.example.com/oauth2/openid/drasl"),
|
|
oidcConfig.Issuer,
|
|
false,
|
|
)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
config.RegistrationOIDC = append(config.RegistrationOIDC, oidcConfig)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
const TEMPLATE_CONFIG_FILE = `# Drasl default config file
|
|
|
|
# Example: drasl.example.com
|
|
Domain = ""
|
|
|
|
# Example: https://drasl.example.com
|
|
BaseURL = ""
|
|
|
|
# List of usernames who automatically become admins of the Drasl instance
|
|
DefaultAdmins = [""]
|
|
|
|
[RegistrationNewPlayer]
|
|
Allow = true
|
|
RequireInvite = true
|
|
`
|
|
|
|
func HandleDeprecations(oldRawConfig *RawConfig, metadata *toml.MetaData) (RawConfig, [][]string) {
|
|
rawConfig := *oldRawConfig
|
|
deprecatedPaths := make([][]string, 0)
|
|
|
|
warningTemplate := "Warning: config option %s is deprecated and will be removed in a future version. Use %s instead."
|
|
|
|
path_ := []string{"RegistrationNewPlayer", "AllowChoosingUUID"}
|
|
if metadata.IsDefined(path_...) {
|
|
LogInfo(fmt.Sprintf(warningTemplate, strings.Join(path_, "."), "CreateNewPlayer.AllowChoosingUUID"))
|
|
deprecatedPaths = append(deprecatedPaths, path_)
|
|
if !metadata.IsDefined("CreateNewPlayer", "AllowChoosingUUID") {
|
|
rawConfig.CreateNewPlayer.AllowChoosingUUID = rawConfig.RegistrationNewPlayer.AllowChoosingUUID
|
|
}
|
|
}
|
|
path_ = []string{"RegistrationExistingPlayer", "Nickname"}
|
|
if metadata.IsDefined(path_...) {
|
|
LogInfo(fmt.Sprintf(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.Nickname"))
|
|
deprecatedPaths = append(deprecatedPaths, path_)
|
|
if !metadata.IsDefined("ImportExistingPlayer", "Nickname") {
|
|
rawConfig.ImportExistingPlayer.Nickname = rawConfig.RegistrationExistingPlayer.Nickname
|
|
}
|
|
}
|
|
path_ = []string{"RegistrationExistingPlayer", "SessionURL"}
|
|
if metadata.IsDefined(path_...) {
|
|
LogInfo(fmt.Sprintf(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.SessionURL"))
|
|
deprecatedPaths = append(deprecatedPaths, path_)
|
|
if !metadata.IsDefined("ImportExistingPlayer", "SessionURL") {
|
|
rawConfig.ImportExistingPlayer.SessionURL = rawConfig.RegistrationExistingPlayer.SessionURL
|
|
}
|
|
}
|
|
path_ = []string{"RegistrationExistingPlayer", "AccountURL"}
|
|
if metadata.IsDefined(path_...) {
|
|
LogInfo(fmt.Sprintf(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.AccountURL"))
|
|
deprecatedPaths = append(deprecatedPaths, path_)
|
|
if !metadata.IsDefined("ImportExistingPlayer", "AccountURL") {
|
|
rawConfig.ImportExistingPlayer.AccountURL = rawConfig.RegistrationExistingPlayer.AccountURL
|
|
}
|
|
}
|
|
path_ = []string{"RegistrationExistingPlayer", "SetSkinURL"}
|
|
if metadata.IsDefined(path_...) {
|
|
LogInfo(fmt.Sprintf(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.SetSkinURL"))
|
|
deprecatedPaths = append(deprecatedPaths, path_)
|
|
if !metadata.IsDefined("ImportExistingPlayer", "SetSkinURL") {
|
|
rawConfig.ImportExistingPlayer.SetSkinURL = rawConfig.RegistrationExistingPlayer.SetSkinURL
|
|
}
|
|
}
|
|
path_ = []string{"RegistrationExistingPlayer", "RequireSkinVerification"}
|
|
if metadata.IsDefined(path_...) {
|
|
LogInfo(fmt.Sprintf(warningTemplate, strings.Join(path_, "."), "ImportExistingPlayer.RequireSkinVerification"))
|
|
deprecatedPaths = append(deprecatedPaths, path_)
|
|
if !metadata.IsDefined("ImportExistingPlayer", "RequireSkinVerification") {
|
|
rawConfig.ImportExistingPlayer.RequireSkinVerification = rawConfig.RegistrationExistingPlayer.RequireSkinVerification
|
|
}
|
|
}
|
|
|
|
return rawConfig, deprecatedPaths
|
|
}
|
|
|
|
func ReadConfig(path string, createIfNotExists bool) (Config, [][]string, error) {
|
|
rawConfig := DefaultRawConfig()
|
|
|
|
_, err := os.Stat(path)
|
|
if err != nil {
|
|
if !createIfNotExists {
|
|
return Config{}, nil, err
|
|
}
|
|
|
|
LogInfo("Config file at", path, "doesn't exist, creating it with template values.")
|
|
dir := filepath.Dir(path)
|
|
err := os.MkdirAll(dir, 0755)
|
|
Check(err)
|
|
|
|
f := Unwrap(os.Create(path))
|
|
defer f.Close()
|
|
|
|
_, err = f.Write([]byte(TEMPLATE_CONFIG_FILE))
|
|
Check(err)
|
|
}
|
|
|
|
LogInfo("Loading config from", path)
|
|
metadata, err := toml.DecodeFile(path, &rawConfig)
|
|
Check(err)
|
|
|
|
for _, key := range metadata.Undecoded() {
|
|
LogInfo("Warning: unknown config option", strings.Join(key, "."))
|
|
}
|
|
|
|
rawConfig, deprecations := HandleDeprecations(&rawConfig, &metadata)
|
|
config, err := CleanConfig(&rawConfig)
|
|
if err != nil {
|
|
return Config{}, nil, err
|
|
}
|
|
|
|
return config, deprecations, nil
|
|
}
|
|
|
|
func ReadOrCreateKey(config *Config) *rsa.PrivateKey {
|
|
path := path.Join(config.StateDirectory, "key.pkcs8")
|
|
|
|
der, err := os.ReadFile(path)
|
|
if err == nil {
|
|
key := Unwrap(x509.ParsePKCS8PrivateKey(der))
|
|
|
|
return key.(*rsa.PrivateKey)
|
|
} else {
|
|
key := Unwrap(rsa.GenerateKey(rand.Reader, 4096))
|
|
|
|
der := Unwrap(x509.MarshalPKCS8PrivateKey(key))
|
|
err = os.WriteFile(path, der, 0600)
|
|
Check(err)
|
|
|
|
return key
|
|
}
|
|
}
|