feat(auth): implement user and device authentication

This commit is contained in:
QiuSW
2026-07-26 15:18:48 +08:00
parent c5d3b215ff
commit 49db5b8305
66 changed files with 6216 additions and 271 deletions
+69
View File
@@ -13,6 +13,8 @@ const (
HTTPAddressEnvironment = "CMROUBAO_HTTP_ADDR"
DatabasePathEnvironment = "CMROUBAO_DATABASE_PATH"
AssetDirectoryEnvironment = "CMROUBAO_ASSET_DIR"
TLSCertificateEnvironment = "CMROUBAO_TLS_CERT_FILE"
TLSPrivateKeyEnvironment = "CMROUBAO_TLS_KEY_FILE"
defaultHTTPAddress = "127.0.0.1:8080"
defaultDatabasePath = "var/cmroubao.db"
@@ -25,6 +27,8 @@ type Config struct {
HTTPAddress string
DatabasePath string
AssetDirectory string
TLSCertificate string
TLSPrivateKey string
ReadHeaderTimeout time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
@@ -68,11 +72,39 @@ func Load(lookup LookupEnvironment) (Config, error) {
if err != nil {
return Config{}, err
}
tlsCertificate, certificateSet, err := optionalEnvironmentValue(
lookup,
TLSCertificateEnvironment,
)
if err != nil {
return Config{}, err
}
tlsPrivateKey, privateKeySet, err := optionalEnvironmentValue(
lookup,
TLSPrivateKeyEnvironment,
)
if err != nil {
return Config{}, err
}
if certificateSet != privateKeySet {
return Config{}, errors.New(
TLSCertificateEnvironment + " and " +
TLSPrivateKeyEnvironment + " must be set together",
)
}
if !certificateSet && !isLoopbackAddress(httpAddress) {
return Config{}, errors.New(
HTTPAddressEnvironment +
" must use loopback unless TLS is configured",
)
}
return Config{
HTTPAddress: httpAddress,
DatabasePath: filepath.Clean(databasePath),
AssetDirectory: assetDirectory,
TLSCertificate: cleanOptionalPath(tlsCertificate),
TLSPrivateKey: cleanOptionalPath(tlsPrivateKey),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
@@ -82,6 +114,43 @@ func Load(lookup LookupEnvironment) (Config, error) {
}, nil
}
func cleanOptionalPath(value string) string {
if value == "" {
return ""
}
return filepath.Clean(value)
}
func optionalEnvironmentValue(
lookup LookupEnvironment,
name string,
) (string, bool, error) {
value, exists := lookup(name)
if !exists {
return "", false, nil
}
value = strings.TrimSpace(value)
if value == "" {
return "", false, errors.New(name + " must not be blank")
}
if strings.ContainsRune(value, '\x00') {
return "", false, errors.New(name + " contains an invalid character")
}
return value, true, nil
}
func isLoopbackAddress(address string) bool {
host, _, err := net.SplitHostPort(address)
if err != nil {
return false
}
if strings.EqualFold(host, "localhost") {
return true
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}
func validatedAssetDirectory(path string) (string, error) {
if strings.ContainsRune(path, '\x00') {
return "", errors.New(
@@ -39,6 +39,8 @@ func TestLoadAcceptsExplicitConfiguration(t *testing.T) {
HTTPAddressEnvironment: "192.0.2.10:9090",
DatabasePathEnvironment: "tmp/test.db",
AssetDirectoryEnvironment: "tmp/assets",
TLSCertificateEnvironment: "tmp/server.crt",
TLSPrivateKeyEnvironment: "tmp/server.key",
}
cfg, err := Load(mapEnvironment(values))
@@ -55,6 +57,14 @@ func TestLoadAcceptsExplicitConfiguration(t *testing.T) {
if cfg.AssetDirectory != filepath.Clean(values[AssetDirectoryEnvironment]) {
t.Fatalf("AssetDirectory = %q", cfg.AssetDirectory)
}
if cfg.TLSCertificate != filepath.Clean(values[TLSCertificateEnvironment]) ||
cfg.TLSPrivateKey != filepath.Clean(values[TLSPrivateKeyEnvironment]) {
t.Fatalf(
"TLS files = %q / %q",
cfg.TLSCertificate,
cfg.TLSPrivateKey,
)
}
}
func TestLoadRejectsUnsafeOrInvalidValues(t *testing.T) {
@@ -116,6 +126,25 @@ func TestLoadRejectsUnsafeOrInvalidValues(t *testing.T) {
AssetDirectoryEnvironment: ".",
},
},
{
name: "non loopback without TLS",
values: map[string]string{
HTTPAddressEnvironment: "0.0.0.0:8080",
},
},
{
name: "TLS certificate without key",
values: map[string]string{
TLSCertificateEnvironment: "tmp/server.crt",
},
},
{
name: "blank TLS key",
values: map[string]string{
TLSCertificateEnvironment: "tmp/server.crt",
TLSPrivateKeyEnvironment: " ",
},
},
}
for _, test := range tests {