HIGH (3건): - 런처 파일 업로드 시 PE 헤더 검증 + 500MB 크기 제한 추가 - 체인 노드 URL 파싱 시 scheme/host 유효성 검증 - Dockerfile 비루트 사용자(app:1000) 실행 MEDIUM (7건): - SSAFY username 충돌 시 랜덤 suffix로 최대 3회 재시도 - 내부 API username 검증 validID(256자) → validUsername(3~50자) 분리 - 동시 업로드 경합 방지 sync.Mutex 추가 - 프로덕션 환경변수 검증 강화 (DB_PASSWORD, OPERATOR_KEY_HEX, INTERNAL_API_KEY) - Redis 에러 시 멱등성 요청 통과 → 503 거부로 변경 - CORS AllowOrigins 환경변수화 (CORS_ALLOW_ORIGINS) - Refresh 엔드포인트 rate limiting 추가 (IP당 5 req/min) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
package config
|
|
|
|
import (
|
|
"log"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/joho/godotenv"
|
|
)
|
|
|
|
type Config struct {
|
|
AppPort string
|
|
DBHost string
|
|
DBPort string
|
|
DBUser string
|
|
DBPassword string
|
|
DBName string
|
|
RedisAddr string
|
|
RedisPassword string
|
|
JWTSecret string
|
|
RefreshSecret string
|
|
JWTExpiryHours int
|
|
AdminUsername string
|
|
AdminPassword string
|
|
BaseURL string
|
|
GameDir string
|
|
|
|
// Chain integration
|
|
// ChainNodeURL은 단일 노드 설정용 (하위 호환).
|
|
// ChainNodeURLs는 CHAIN_NODE_URLS(쉼표 구분) 또는 ChainNodeURL에서 파생.
|
|
ChainNodeURL string
|
|
ChainNodeURLs []string
|
|
ChainID string
|
|
OperatorKeyHex string
|
|
WalletEncryptionKey string
|
|
|
|
// CORS
|
|
CORSAllowOrigins string
|
|
|
|
// Server-to-server auth
|
|
InternalAPIKey string
|
|
|
|
// SSAFY OAuth 2.0
|
|
SSAFYClientID string
|
|
SSAFYClientSecret string
|
|
SSAFYRedirectURI string
|
|
}
|
|
|
|
var C Config
|
|
|
|
func Load() {
|
|
_ = godotenv.Load()
|
|
|
|
hours, _ := strconv.Atoi(getEnv("JWT_EXPIRY_HOURS", "24"))
|
|
C = Config{
|
|
AppPort: getEnv("APP_PORT", "8080"),
|
|
DBHost: getEnv("DB_HOST", "localhost"),
|
|
DBPort: getEnv("DB_PORT", "3306"),
|
|
DBUser: getEnv("DB_USER", "root"),
|
|
DBPassword: getEnv("DB_PASSWORD", ""),
|
|
DBName: getEnv("DB_NAME", "a301"),
|
|
RedisAddr: getEnv("REDIS_ADDR", "localhost:6379"),
|
|
RedisPassword: getEnv("REDIS_PASSWORD", ""),
|
|
JWTSecret: getEnv("JWT_SECRET", "secret"),
|
|
RefreshSecret: getEnv("REFRESH_SECRET", "refresh-secret"),
|
|
JWTExpiryHours: hours,
|
|
AdminUsername: getEnv("ADMIN_USERNAME", "admin"),
|
|
AdminPassword: getEnv("ADMIN_PASSWORD", "admin1234"),
|
|
BaseURL: getEnv("BASE_URL", "http://localhost:8080"),
|
|
GameDir: getEnv("GAME_DIR", "/data/game"),
|
|
|
|
ChainNodeURL: getEnv("CHAIN_NODE_URL", "http://localhost:8545"),
|
|
ChainID: getEnv("CHAIN_ID", "tolchain-dev"),
|
|
OperatorKeyHex: getEnv("OPERATOR_KEY_HEX", ""),
|
|
WalletEncryptionKey: getEnv("WALLET_ENCRYPTION_KEY", ""),
|
|
|
|
CORSAllowOrigins: getEnv("CORS_ALLOW_ORIGINS", "https://a301.tolelom.xyz"),
|
|
|
|
InternalAPIKey: getEnv("INTERNAL_API_KEY", ""),
|
|
|
|
SSAFYClientID: getEnv("SSAFY_CLIENT_ID", ""),
|
|
SSAFYClientSecret: getEnv("SSAFY_CLIENT_SECRET", ""),
|
|
SSAFYRedirectURI: getEnv("SSAFY_REDIRECT_URI", ""),
|
|
}
|
|
|
|
// CHAIN_NODE_URLS (쉼표 구분) 우선, 없으면 CHAIN_NODE_URL 단일값 사용
|
|
if raw := getEnv("CHAIN_NODE_URLS", ""); raw != "" {
|
|
for _, u := range strings.Split(raw, ",") {
|
|
if u = strings.TrimSpace(u); u != "" {
|
|
if parsed, err := url.Parse(u); err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
log.Fatalf("FATAL: invalid CHAIN_NODE_URL: %q (must be http:// or https://)", u)
|
|
}
|
|
C.ChainNodeURLs = append(C.ChainNodeURLs, u)
|
|
}
|
|
}
|
|
}
|
|
if len(C.ChainNodeURLs) == 0 {
|
|
C.ChainNodeURLs = []string{C.ChainNodeURL}
|
|
}
|
|
}
|
|
|
|
// WarnInsecureDefaults logs warnings for security-sensitive settings left at defaults.
|
|
// In production mode (APP_ENV=production), insecure defaults cause a fatal exit.
|
|
func WarnInsecureDefaults() {
|
|
isProd := getEnv("APP_ENV", "") == "production"
|
|
|
|
insecure := false
|
|
if C.JWTSecret == "secret" {
|
|
log.Println("WARNING: JWT_SECRET is using the default value — set a strong secret for production")
|
|
insecure = true
|
|
}
|
|
if C.RefreshSecret == "refresh-secret" {
|
|
log.Println("WARNING: REFRESH_SECRET is using the default value — set a strong secret for production")
|
|
insecure = true
|
|
}
|
|
if C.AdminPassword == "admin1234" {
|
|
log.Println("WARNING: ADMIN_PASSWORD is using the default value — change it for production")
|
|
insecure = true
|
|
}
|
|
if C.WalletEncryptionKey == "" {
|
|
log.Println("WARNING: WALLET_ENCRYPTION_KEY is empty — blockchain wallet features will fail")
|
|
}
|
|
|
|
if isProd {
|
|
if C.DBPassword == "" {
|
|
log.Println("FATAL: DB_PASSWORD must be set in production")
|
|
insecure = true
|
|
}
|
|
if C.OperatorKeyHex == "" {
|
|
log.Println("FATAL: OPERATOR_KEY_HEX must be set in production")
|
|
insecure = true
|
|
}
|
|
if C.InternalAPIKey == "" {
|
|
log.Println("FATAL: INTERNAL_API_KEY must be set in production")
|
|
insecure = true
|
|
}
|
|
}
|
|
|
|
if isProd && insecure {
|
|
log.Fatal("FATAL: insecure defaults detected in production — check warnings above")
|
|
}
|
|
}
|
|
|
|
// getEnv returns the environment variable value, or fallback if unset or empty.
|
|
// Note: explicitly setting a variable to "" is treated as unset.
|
|
func getEnv(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|