From 01cd524abe298d33a9ba34451a0b5b561837e485 Mon Sep 17 00:00:00 2001 From: dalbodeule <11470513+dalbodeule@users.noreply.github.com> Date: Mon, 8 Dec 2025 00:13:30 +0900 Subject: [PATCH 1/4] [feat](server, client, build): integrate dotenv for environment variable management (by @ryu31847) - Added `github.com/joho/godotenv` for loading `.env` files in server and client. - Implemented environment variable validation and logging in both main programs. - Updated Makefile with `.env` export and validation steps for required variables. - Simplified error handling in `writeErrorPage` rendering logic. --- Makefile | 20 +++++++++++-- cmd/client/main.go | 42 ++++++++++++++++++++++++++ cmd/server/main.go | 74 +++++++++++++++++++++++++++++----------------- go.mod | 1 + go.sum | 2 ++ 5 files changed, 110 insertions(+), 29 deletions(-) diff --git a/Makefile b/Makefile index f9afeea..7d0bfd6 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,10 @@ CLIENT_BIN := $(BIN_DIR)/hop-gate-client VERSION ?= $(shell git describe --tags --dirty --always 2>/dev/null || echo dev) +# .env 파일 로드 +include .env +export $(shell sed 's/=.*//' .env) + .PHONY: all server client clean docker-server run-server run-client errors-css all: server client @@ -38,13 +42,13 @@ errors-css: echo "package.json not found; skipping errors-css build"; \ fi -server: errors-css +server: errors-css check-env-server @echo "Building server..." @mkdir -p $(BIN_DIR) $(GO) build -ldflags "-X main.version=$(VERSION)" -o $(SERVER_BIN) $(SERVER_PKG) @echo "Server binary: $(SERVER_BIN)" -client: +client: check-env-client @echo "Building client..." @mkdir -p $(BIN_DIR) $(GO) build -ldflags "-X main.version=$(VERSION)" -o $(CLIENT_BIN) $(CLIENT_PKG) @@ -66,3 +70,15 @@ docker-server: @echo "Building server Docker image..." docker build -f Dockerfile.server -t hop-gate-server:$(VERSION) . +check-env-server: + @if [ -z "$$HOP_SERVER_HTTP_LISTEN" ]; then echo "필수 환경 변수 HOP_SERVER_HTTP_LISTEN가 설정되지 않았습니다."; exit 1; fi + @if [ -z "$$HOP_SERVER_HTTPS_LISTEN" ]; then echo "필수 환경 변수 HOP_SERVER_HTTPS_LISTEN가 설정되지 않았습니다."; exit 1; fi + @if [ -z "$$HOP_SERVER_DTLS_LISTEN" ]; then echo "필수 환경 변수 HOP_SERVER_DTLS_LISTEN가 설정되지 않았습니다."; exit 1; fi + @if [ -z "$$HOP_SERVER_DOMAIN" ]; then echo "필수 환경 변수 HOP_SERVER_DOMAIN가 설정되지 않았습니다."; exit 1; fi + +check-env-client: + @if [ -z "$$HOP_CLIENT_SERVER_ADDR" ]; then echo "필수 환경 변수 HOP_CLIENT_SERVER_ADDR가 설정되지 않았습니다."; exit 1; fi + @if [ -z "$$HOP_CLIENT_DOMAIN" ]; then echo "필수 환경 변수 HOP_CLIENT_DOMAIN가 설정되지 않았습니다."; exit 1; fi + @if [ -z "$$HOP_CLIENT_API_KEY" ]; then echo "필수 환경 변수 HOP_CLIENT_API_KEY가 설정되지 않았습니다."; exit 1; fi + @if [ -z "$$HOP_CLIENT_LOCAL_TARGET" ]; then echo "필수 환경 변수 HOP_CLIENT_LOCAL_TARGET가 설정되지 않았습니다."; exit 1; fi + @if [ -z "$$HOP_CLIENT_DEBUG" ]; then echo "필수 환경 변수 HOP_CLIENT_DEBUG가 설정되지 않았습니다."; exit 1; fi \ No newline at end of file diff --git a/cmd/client/main.go b/cmd/client/main.go index 86e2b2a..0f9003d 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "crypto/x509" "flag" + "log" "net" "os" "strings" @@ -13,8 +14,25 @@ import ( "github.com/dalbodeule/hop-gate/internal/dtls" "github.com/dalbodeule/hop-gate/internal/logging" "github.com/dalbodeule/hop-gate/internal/proxy" + + "github.com/joho/godotenv" ) +func init() { + // .env 파일 로드 + if err := godotenv.Overload(); err != nil { + log.Fatalf(".env 파일을 로드할 수 없습니다: %v", err) + } +} + +func getEnvOrPanic(key string) string { + value, exists := os.LookupEnv(key) + if !exists || value == "" { + log.Fatalf("필수 환경 변수 %s가 설정되지 않았습니다.", key) + } + return value +} + // maskAPIKey 는 로그에 노출할 때 클라이언트 API Key 를 일부만 보여주기 위한 헬퍼입니다. func maskAPIKey(key string) string { if len(key) <= 8 { @@ -36,6 +54,30 @@ func firstNonEmpty(values ...string) string { func main() { logger := logging.NewStdJSONLogger("client") + // .env 파일 로드 + if err := godotenv.Load(); err != nil { + log.Println("Failed to load .env file. Using default environment variables.") + } + + // 필수 환경 변수 유효성 검사 + serverAddr := getEnvOrPanic("HOP_CLIENT_SERVER_ADDR") + clientDomain := getEnvOrPanic("HOP_CLIENT_DOMAIN") + apiKey := getEnvOrPanic("HOP_CLIENT_API_KEY") + localTarget := getEnvOrPanic("HOP_CLIENT_LOCAL_TARGET") + debug := getEnvOrPanic("HOP_CLIENT_DEBUG") + + // 디버깅 플래그 확인 + if debug != "true" && debug != "false" { + log.Fatalf("Invalid value for HOP_CLIENT_DEBUG. It must be set to 'true' or 'false'.") + } + + // 유효성 검사 결과 출력 + log.Printf("SERVER_ADDR: %s", serverAddr) + log.Printf("CLIENT_DOMAIN: %s", clientDomain) + log.Printf("API_KEY: %s", maskAPIKey(apiKey)) + log.Printf("LOCAL_TARGET: %s", localTarget) + log.Printf("DEBUG: %s", debug) + // CLI 인자 정의 (env 보다 우선 적용됨) serverAddrFlag := flag.String("server-addr", "", "DTLS server address (host:port)") domainFlag := flag.String("domain", "", "registered domain (e.g. api.example.com)") diff --git a/cmd/server/main.go b/cmd/server/main.go index 356cbeb..0357fec 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -8,6 +8,7 @@ import ( "fmt" "io" stdfs "io/fs" + "log" "net" "net/http" "os" @@ -28,6 +29,8 @@ import ( "github.com/dalbodeule/hop-gate/internal/observability" "github.com/dalbodeule/hop-gate/internal/protocol" "github.com/dalbodeule/hop-gate/internal/store" + + "github.com/joho/godotenv" ) type dtlsSessionWrapper struct { @@ -35,6 +38,21 @@ type dtlsSessionWrapper struct { mu sync.Mutex } +func init() { + // .env 파일 로드 + if err := godotenv.Overload(); err != nil { + log.Fatalf(".env 파일을 로드할 수 없습니다: %v", err) + } +} + +func getEnvOrPanic(key string) string { + value, exists := os.LookupEnv(key) + if !exists || value == "" { + log.Fatalf("필수 환경 변수 %s가 설정되지 않았습니다.", key) + } + return value +} + // canonicalizeDomainForDNS 는 DTLS 핸드셰이크에서 전달된 도메인 문자열을 // DNS 조회 및 DB 조회에 사용할 수 있는 정규화된 호스트명으로 변환합니다. (ko) // canonicalizeDomainForDNS normalizes the domain string from the DTLS handshake @@ -277,8 +295,8 @@ var hopGateOwnedHeaders = map[string]struct{}{ "Referrer-Policy": {}, } -// writeErrorPage 는 주요 HTTP 에러 코드(400/404/500/502/504/525)에 대해 정적 HTML 에러 페이지를 렌더링합니다. (ko) -// writeErrorPage renders static HTML error pages for key HTTP error codes (400/404/500/502/504/525). (en) +// writeErrorPage 는 주요 HTTP 에러 코드(400/404/500/525)에 대해 정적 HTML 에러 페이지를 렌더링합니다. (ko) +// writeErrorPage renders static HTML error pages for key HTTP error codes (400/404/500/525). (en) // // 템플릿 로딩 우선순위: (ko) // 1. HOP_ERROR_PAGES_DIR/.html (또는 ./errors/.html) (ko) @@ -294,31 +312,9 @@ func writeErrorPage(w http.ResponseWriter, r *http.Request, status int) { setSecurityAndIdentityHeaders(w, r) } - // 4xx / 5xx 대역에 대한 템플릿 매핑 규칙: (ko) - // - 400 series: 400.html 로 렌더링 (단, 404 는 404.html 사용) (ko) - // - 500 series: 500.html 로 렌더링 (단, 502/504/525 는 개별 템플릿 사용) (ko) - // - // Mapping rules for 4xx / 5xx ranges: (en) - // - 400 series: render using 400.html (except 404 uses 404.html). (en) - // - 500 series: render using 500.html (except 502/504/525 which have dedicated templates). (en) - mapped := status - switch { - case status >= 400 && status < 500: - if status != http.StatusBadRequest && status != http.StatusNotFound { - mapped = http.StatusBadRequest - } - case status >= 500 && status < 600: - if status != http.StatusInternalServerError && - status != http.StatusBadGateway && - status != errorpages.StatusGatewayTimeout && - status != errorpages.StatusTLSHandshakeFailed { - mapped = http.StatusInternalServerError - } - } - - // Delegates actual HTML rendering to internal/errorpages with mapped status. (en) - // 실제 HTML 렌더링은 매핑된 상태 코드로 internal/errorpages 패키지에 위임합니다. (ko) - errorpages.Render(w, r, mapped) + // Delegates actual HTML rendering to internal/errorpages. (en) + // 실제 HTML 렌더링은 internal/errorpages 패키지에 위임합니다. (ko) + errorpages.Render(w, r, status) } // setSecurityAndIdentityHeaders 는 HopGate 에서 공통으로 추가하는 보안/식별 헤더를 설정합니다. (ko) @@ -652,6 +648,30 @@ func newHTTPHandler(logger logging.Logger, proxyTimeout time.Duration) http.Hand func main() { logger := logging.NewStdJSONLogger("server") + // .env 파일 로드 + if err := godotenv.Load(); err != nil { + log.Println(".env 파일을 로드할 수 없습니다. 기본 환경 변수를 사용합니다.") + } + + // 필수 환경 변수 유효성 검사 + httpListen := getEnvOrPanic("HOP_SERVER_HTTP_LISTEN") + httpsListen := getEnvOrPanic("HOP_SERVER_HTTPS_LISTEN") + dtlsListen := getEnvOrPanic("HOP_SERVER_DTLS_LISTEN") + domain := getEnvOrPanic("HOP_SERVER_DOMAIN") + debug := getEnvOrPanic("HOP_SERVER_DEBUG") + + // 디버깅 플래그 확인 + if debug != "true" && debug != "false" { + log.Fatalf("HOP_SERVER_DEBUG 값이 잘못되었습니다. true 또는 false로 설정해야 합니다.") + } + + // 유효성 검사 결과 출력 + log.Printf("HTTP_LISTEN: %s", httpListen) + log.Printf("HTTPS_LISTEN: %s", httpsListen) + log.Printf("DTLS_LISTEN: %s", dtlsListen) + log.Printf("DOMAIN: %s", domain) + log.Printf("DEBUG: %s", debug) + // Prometheus 메트릭 등록 observability.MustRegister() diff --git a/go.mod b/go.mod index d6ee597..9223dea 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( entgo.io/ent v0.14.5 github.com/go-acme/lego/v4 v4.28.1 github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 github.com/pion/dtls/v3 v3.0.7 github.com/prometheus/client_golang v1.19.0 diff --git a/go.sum b/go.sum index cfe1a69..2c9b84c 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo= github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= From 00b47fda8e73f66335722aa92f15c9af2bb0c78d Mon Sep 17 00:00:00 2001 From: dalbodeule <11470513+dalbodeule@users.noreply.github.com> Date: Mon, 8 Dec 2025 00:34:34 +0900 Subject: [PATCH 2/4] [refactor](server, client, config): remove `godotenv` dependency and enhance env var handling - Replaced `godotenv` with a custom `.env` loader that respects OS-level environment variables. - Updated server and client initialization to prioritize OS environment variables over `.env` values. - Improved environment variable validation and logging with structured logs. - Applied cleaner error handling and removed redundant `log` package usage. --- cmd/client/main.go | 75 ++++++++++++++++++--------------------- cmd/server/main.go | 75 ++++++++++++++++++--------------------- internal/config/config.go | 12 +++++-- 3 files changed, 79 insertions(+), 83 deletions(-) diff --git a/cmd/client/main.go b/cmd/client/main.go index 0f9003d..daa5ce5 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -5,7 +5,6 @@ import ( "crypto/tls" "crypto/x509" "flag" - "log" "net" "os" "strings" @@ -14,21 +13,15 @@ import ( "github.com/dalbodeule/hop-gate/internal/dtls" "github.com/dalbodeule/hop-gate/internal/logging" "github.com/dalbodeule/hop-gate/internal/proxy" - - "github.com/joho/godotenv" ) -func init() { - // .env 파일 로드 - if err := godotenv.Overload(); err != nil { - log.Fatalf(".env 파일을 로드할 수 없습니다: %v", err) - } -} - -func getEnvOrPanic(key string) string { +func getEnvOrPanic(logger logging.Logger, key string) string { value, exists := os.LookupEnv(key) - if !exists || value == "" { - log.Fatalf("필수 환경 변수 %s가 설정되지 않았습니다.", key) + if !exists || strings.TrimSpace(value) == "" { + logger.Error("missing required environment variable", logging.Fields{ + "env": key, + }) + os.Exit(1) } return value } @@ -54,29 +47,40 @@ func firstNonEmpty(values ...string) string { func main() { logger := logging.NewStdJSONLogger("client") - // .env 파일 로드 - if err := godotenv.Load(); err != nil { - log.Println("Failed to load .env file. Using default environment variables.") + // 1. 환경변수(.env 포함)에서 클라이언트 설정 로드 + // internal/config 패키지가 .env 를 먼저 읽고, 이미 설정된 OS 환경변수를 우선시합니다. + envCfg, err := config.LoadClientConfigFromEnv() + if err != nil { + logger.Error("failed to load client config from env", logging.Fields{ + "error": err.Error(), + }) + os.Exit(1) } - // 필수 환경 변수 유효성 검사 - serverAddr := getEnvOrPanic("HOP_CLIENT_SERVER_ADDR") - clientDomain := getEnvOrPanic("HOP_CLIENT_DOMAIN") - apiKey := getEnvOrPanic("HOP_CLIENT_API_KEY") - localTarget := getEnvOrPanic("HOP_CLIENT_LOCAL_TARGET") - debug := getEnvOrPanic("HOP_CLIENT_DEBUG") + // 2. 필수 환경 변수 유효성 검사 (.env 포함; OS 환경변수가 우선) + serverAddrEnv := getEnvOrPanic(logger, "HOP_CLIENT_SERVER_ADDR") + clientDomainEnv := getEnvOrPanic(logger, "HOP_CLIENT_DOMAIN") + apiKeyEnv := getEnvOrPanic(logger, "HOP_CLIENT_API_KEY") + localTargetEnv := getEnvOrPanic(logger, "HOP_CLIENT_LOCAL_TARGET") + debugEnv := getEnvOrPanic(logger, "HOP_CLIENT_DEBUG") - // 디버깅 플래그 확인 - if debug != "true" && debug != "false" { - log.Fatalf("Invalid value for HOP_CLIENT_DEBUG. It must be set to 'true' or 'false'.") + // 디버깅 플래그 형식 확인 + if debugEnv != "true" && debugEnv != "false" { + logger.Error("invalid value for HOP_CLIENT_DEBUG; must be 'true' or 'false'", logging.Fields{ + "env": "HOP_CLIENT_DEBUG", + "value": debugEnv, + }) + os.Exit(1) } - // 유효성 검사 결과 출력 - log.Printf("SERVER_ADDR: %s", serverAddr) - log.Printf("CLIENT_DOMAIN: %s", clientDomain) - log.Printf("API_KEY: %s", maskAPIKey(apiKey)) - log.Printf("LOCAL_TARGET: %s", localTarget) - log.Printf("DEBUG: %s", debug) + // 유효성 검사 결과를 구조화 로그로 출력 + logger.Info("validated client env vars", logging.Fields{ + "HOP_CLIENT_SERVER_ADDR": serverAddrEnv, + "HOP_CLIENT_DOMAIN": clientDomainEnv, + "HOP_CLIENT_API_KEY_MASK": maskAPIKey(apiKeyEnv), + "HOP_CLIENT_LOCAL_TARGET": localTargetEnv, + "HOP_CLIENT_DEBUG": debugEnv, + }) // CLI 인자 정의 (env 보다 우선 적용됨) serverAddrFlag := flag.String("server-addr", "", "DTLS server address (host:port)") @@ -86,15 +90,6 @@ func main() { flag.Parse() - // 1. 환경변수(.env 포함)에서 클라이언트 설정 로드 - envCfg, err := config.LoadClientConfigFromEnv() - if err != nil { - logger.Error("failed to load client config from env", logging.Fields{ - "error": err.Error(), - }) - os.Exit(1) - } - // 2. CLI 인자 우선, env 후순위로 최종 설정 구성 finalCfg := &config.ClientConfig{ ServerAddr: firstNonEmpty(strings.TrimSpace(*serverAddrFlag), strings.TrimSpace(envCfg.ServerAddr)), diff --git a/cmd/server/main.go b/cmd/server/main.go index 0357fec..e43af50 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -8,7 +8,6 @@ import ( "fmt" "io" stdfs "io/fs" - "log" "net" "net/http" "os" @@ -29,8 +28,6 @@ import ( "github.com/dalbodeule/hop-gate/internal/observability" "github.com/dalbodeule/hop-gate/internal/protocol" "github.com/dalbodeule/hop-gate/internal/store" - - "github.com/joho/godotenv" ) type dtlsSessionWrapper struct { @@ -38,17 +35,13 @@ type dtlsSessionWrapper struct { mu sync.Mutex } -func init() { - // .env 파일 로드 - if err := godotenv.Overload(); err != nil { - log.Fatalf(".env 파일을 로드할 수 없습니다: %v", err) - } -} - -func getEnvOrPanic(key string) string { +func getEnvOrPanic(logger logging.Logger, key string) string { value, exists := os.LookupEnv(key) - if !exists || value == "" { - log.Fatalf("필수 환경 변수 %s가 설정되지 않았습니다.", key) + if !exists || strings.TrimSpace(value) == "" { + logger.Error("missing required environment variable", logging.Fields{ + "env": key, + }) + os.Exit(1) } return value } @@ -648,34 +641,8 @@ func newHTTPHandler(logger logging.Logger, proxyTimeout time.Duration) http.Hand func main() { logger := logging.NewStdJSONLogger("server") - // .env 파일 로드 - if err := godotenv.Load(); err != nil { - log.Println(".env 파일을 로드할 수 없습니다. 기본 환경 변수를 사용합니다.") - } - - // 필수 환경 변수 유효성 검사 - httpListen := getEnvOrPanic("HOP_SERVER_HTTP_LISTEN") - httpsListen := getEnvOrPanic("HOP_SERVER_HTTPS_LISTEN") - dtlsListen := getEnvOrPanic("HOP_SERVER_DTLS_LISTEN") - domain := getEnvOrPanic("HOP_SERVER_DOMAIN") - debug := getEnvOrPanic("HOP_SERVER_DEBUG") - - // 디버깅 플래그 확인 - if debug != "true" && debug != "false" { - log.Fatalf("HOP_SERVER_DEBUG 값이 잘못되었습니다. true 또는 false로 설정해야 합니다.") - } - - // 유효성 검사 결과 출력 - log.Printf("HTTP_LISTEN: %s", httpListen) - log.Printf("HTTPS_LISTEN: %s", httpsListen) - log.Printf("DTLS_LISTEN: %s", dtlsListen) - log.Printf("DOMAIN: %s", domain) - log.Printf("DEBUG: %s", debug) - - // Prometheus 메트릭 등록 - observability.MustRegister() - // 1. 서버 설정 로드 (.env + 환경변수) + // internal/config 패키지가 .env 를 먼저 읽고, 이미 설정된 OS 환경변수를 우선시합니다. cfg, err := config.LoadServerConfigFromEnv() if err != nil { logger.Error("failed to load server config from env", logging.Fields{ @@ -684,6 +651,34 @@ func main() { os.Exit(1) } + // 2. 필수 환경 변수 유효성 검사 (.env 포함; OS 환경변수가 우선) + httpListenEnv := getEnvOrPanic(logger, "HOP_SERVER_HTTP_LISTEN") + httpsListenEnv := getEnvOrPanic(logger, "HOP_SERVER_HTTPS_LISTEN") + dtlsListenEnv := getEnvOrPanic(logger, "HOP_SERVER_DTLS_LISTEN") + domainEnv := getEnvOrPanic(logger, "HOP_SERVER_DOMAIN") + debugEnv := getEnvOrPanic(logger, "HOP_SERVER_DEBUG") + + // 디버깅 플래그 형식 확인 + if debugEnv != "true" && debugEnv != "false" { + logger.Error("invalid value for HOP_SERVER_DEBUG; must be 'true' or 'false'", logging.Fields{ + "env": "HOP_SERVER_DEBUG", + "value": debugEnv, + }) + os.Exit(1) + } + + // 유효성 검사 결과를 구조화 로그로 출력 + logger.Info("validated server env vars", logging.Fields{ + "HOP_SERVER_HTTP_LISTEN": httpListenEnv, + "HOP_SERVER_HTTPS_LISTEN": httpsListenEnv, + "HOP_SERVER_DTLS_LISTEN": dtlsListenEnv, + "HOP_SERVER_DOMAIN": domainEnv, + "HOP_SERVER_DEBUG": debugEnv, + }) + + // Prometheus 메트릭 등록 + observability.MustRegister() + logger.Info("hop-gate server starting", logging.Fields{ "stack": "prometheus-loki-grafana", "http_listen": cfg.HTTPListen, diff --git a/internal/config/config.go b/internal/config/config.go index bba8ec9..7cc776e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -107,7 +107,11 @@ func loadDotEnvOnce() { val = strings.Trim(val, `"'`) if key != "" { - _ = os.Setenv(key, val) + // 이미 OS 환경변수에 설정된 값이 있는 경우 이를 우선시하고, + // 비어 있는 키에 대해서만 .env 값을 주입합니다. + if _, exists := os.LookupEnv(key); !exists { + _ = os.Setenv(key, val) + } } } if err := scanner.Err(); err != nil { @@ -209,7 +213,8 @@ func loadLoggingFromEnv() LoggingConfig { } } -// LoadServerConfigFromEnv 는 .env 를 우선 읽고, 이후 환경 변수를 기반으로 서버 설정을 구성합니다. +// LoadServerConfigFromEnv 는 .env 를 한 번 읽어 현재 환경변수를 보완한 뒤 +// "환경변수 > .env" 우선순위로 서버 설정을 구성합니다. func LoadServerConfigFromEnv() (*ServerConfig, error) { loadDotEnvOnce() if dotenvErr != nil { @@ -228,7 +233,8 @@ func LoadServerConfigFromEnv() (*ServerConfig, error) { return cfg, nil } -// LoadClientConfigFromEnv 는 .env 를 우선 읽고, 이후 환경 변수를 기반으로 클라이언트 설정을 구성합니다. +// LoadClientConfigFromEnv 는 .env 를 한 번 읽어 현재 환경변수를 보완한 뒤 +// "환경변수 > .env" 우선순위로 클라이언트 설정을 구성합니다. // 실제 런타임에서 사용되는 필드는 ServerAddr, Domain, ClientAPIKey, LocalTarget 입니다. func LoadClientConfigFromEnv() (*ClientConfig, error) { loadDotEnvOnce() From 302acb640d6cb7e9a0b15da7a859a94f6cf91f71 Mon Sep 17 00:00:00 2001 From: dalbodeule <11470513+dalbodeule@users.noreply.github.com> Date: Mon, 8 Dec 2025 00:41:58 +0900 Subject: [PATCH 3/4] [docs](README): add detailed documentation for `.env` and environment variable handling - Documented the custom `.env` loader behavior, prioritization of OS-level environment variables, and validation stages. - Explained server and client-specific configuration loading process. - Added best practices for environment variable usage in development and production environments. --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8db0e04..04b5775 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ go mod tidy ### 3.2 Makefile 사용 (Using Makefile) -서버/클라이언트 빌드를 위해 상위 [`Makefile`](Makefile)을 제공합니다. +서버/클라이언트 빌드를 위해 상위 [`Makefile`](Makefile)을 제공합니다. A top-level [`Makefile`](Makefile) is provided for server/client builds. ```bash @@ -69,11 +69,54 @@ make server make client ``` -빌드 결과는 `./bin/hop-gate-server`, `./bin/hop-gate-client` 로 생성됩니다. +빌드 결과는 `./bin/hop-gate-server`, `./bin/hop-gate-client` 로 생성됩니다. Build artifacts are created as `./bin/hop-gate-server` and `./bin/hop-gate-client`. --- +### 3.3 환경변수와 .env 처리 (Environment variables and .env handling) + +HopGate 는 공통 설정을 [`internal/config/config.go`](internal/config/config.go) 에서 로드하며, +**운영체제 환경변수(OS env)가 `.env` 파일보다 우선**하도록 설계되어 있습니다. +HopGate loads shared configuration from [`internal/config/config.go`](internal/config/config.go) and is designed so that **OS-level environment variables take precedence over `.env`**. + +- `.env` 로더: [`loadDotEnvOnce`](internal/config/config.go) + - 현재 작업 디렉터리의 `.env` 파일을 한 번만 읽습니다. + - 이미 OS 환경변수에 설정된 키는 **덮어쓰지 않고 그대로 유지**하고, 비어 있는 키에 대해서만 `.env` 값을 주입합니다. + - `.env` 파일이 존재하지 않으면 조용히 무시합니다 (에러가 아닙니다). + The loader reads the `.env` file once, **does not override existing OS env values**, and only fills missing keys. If `.env` is missing, it is silently ignored. + +- 서버 설정 로더 (Server config loader): [`LoadServerConfigFromEnv`](internal/config/config.go) + - `.env` 로더를 먼저 호출한 뒤, `HOP_SERVER_*` 환경변수에서 서버 설정을 구성합니다. + - 실제 실행 시점에는 서버 엔트리포인트 [`cmd/server/main.go`](cmd/server/main.go) 에서 필수 환경변수가 모두 설정되었는지 한 번 더 검증합니다. + It calls the `.env` loader first, then builds server config from `HOP_SERVER_*` env vars, and finally the server entrypoint [`cmd/server/main.go`](cmd/server/main.go) validates required variables. + +- 클라이언트 설정 로더 (Client config loader): [`LoadClientConfigFromEnv`](internal/config/config.go) + - `.env` 로더를 동일하게 사용하며, `HOP_CLIENT_*` 환경변수에서 클라이언트 설정을 구성합니다. + - 이후 CLI 인자(예: `--server-addr`, `--domain`)가 있을 경우 env 값보다 우선 적용됩니다. + The same loader is used for `HOP_CLIENT_*` env vars, and CLI flags override env values when provided. + +빌드/실행 시 필수 환경변수는 다음 두 단계에서 검증됩니다. +Required environment variables are validated in two stages: + +1. **빌드 단계 (Build-time) – Makefile 체크 (optional guard)** + - [`Makefile`](Makefile) 에서 `.env` 를 `include` 한 뒤, `check-env-server` / `check-env-client` 타깃으로 최소한의 필수 env 를 확인합니다. + - 예) 서버 빌드 시: `make server` → `errors-css` → `check-env-server` → `go build` 순으로 실행됩니다. + The [`Makefile`](Makefile) includes `.env` and uses `check-env-server` / `check-env-client` targets to guard required variables before build. + +2. **실행 단계 (Runtime) – 엔트리포인트에서 엄격 검증 (strict runtime validation)** + - 서버: [`cmd/server/main.go`](cmd/server/main.go) + - 헬퍼 `getEnvOrPanic(logger, key)` 를 사용해 `HOP_SERVER_HTTP_LISTEN`, `HOP_SERVER_HTTPS_LISTEN`, `HOP_SERVER_DTLS_LISTEN`, `HOP_SERVER_DOMAIN`, `HOP_SERVER_DEBUG` 가 비어 있지 않은지 확인합니다. + - 누락되었거나 공백인 경우, 구조화 에러 로그(JSON)와 함께 프로세스를 종료합니다. + - 클라이언트: [`cmd/client/main.go`](cmd/client/main.go) + - `HOP_CLIENT_SERVER_ADDR`, `HOP_CLIENT_DOMAIN`, `HOP_CLIENT_API_KEY`, `HOP_CLIENT_LOCAL_TARGET`, `HOP_CLIENT_DEBUG` 를 동일한 방식으로 검증합니다. + - 두 경우 모두 `HOP_*_DEBUG` 값은 문자열 `"true"` 또는 `"false"` 만 허용합니다. + Both server and client use a helper (`getEnvOrPanic`) to enforce non-empty required env vars at startup and log structured JSON errors on failure. The debug flags must be the strings `"true"` or `"false"`. + +실제 배포 환경에서는 `.env` 보다는 시스템 환경변수(Kubernetes `env`, Docker `-e`, systemd `Environment=` 등)를 사용하는 것을 권장하며, +로컬 개발에서는 `.env.example` 을 복사한 `.env` 파일을 사용해 빠르게 설정을 구성할 수 있습니다. +For production deployments, prefer OS-level env (Kubernetes `env`, Docker `-e`, systemd `Environment=`, etc.), and use a local `.env` (copied from `.env.example`) mainly for development. + ## 4. DTLS 핸드셰이크 테스트 (Testing DTLS Handshake) HopGate는 DTLS 위에서 **도메인 + 클라이언트 API Key** 기반의 애플리케이션 레벨 핸드셰이크를 수행합니다. From c5b3c11df0687196142ee4afc072dc95caf3b5a9 Mon Sep 17 00:00:00 2001 From: dalbodeule <11470513+dalbodeule@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:26:08 +0900 Subject: [PATCH 4/4] [refactor](build, Makefile): drop `godotenv` dependency and fix Korean grammar in env checks - Removed `godotenv` dependency from `go.mod` as it's no longer used. - Corrected Korean grammar in Makefile environment variable validation messages. --- Makefile | 2 +- go.mod | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7d0bfd6..6bebed3 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,7 @@ docker-server: docker build -f Dockerfile.server -t hop-gate-server:$(VERSION) . check-env-server: - @if [ -z "$$HOP_SERVER_HTTP_LISTEN" ]; then echo "필수 환경 변수 HOP_SERVER_HTTP_LISTEN가 설정되지 않았습니다."; exit 1; fi + @if [ -z "$$HOP_SERVER_HTTP_LISTEN" ]; then echo "필수 환경 변수 HOP_SERVER_HTTP_LISTEN이 설정되지 않았습니다."; exit 1; fi @if [ -z "$$HOP_SERVER_HTTPS_LISTEN" ]; then echo "필수 환경 변수 HOP_SERVER_HTTPS_LISTEN가 설정되지 않았습니다."; exit 1; fi @if [ -z "$$HOP_SERVER_DTLS_LISTEN" ]; then echo "필수 환경 변수 HOP_SERVER_DTLS_LISTEN가 설정되지 않았습니다."; exit 1; fi @if [ -z "$$HOP_SERVER_DOMAIN" ]; then echo "필수 환경 변수 HOP_SERVER_DOMAIN가 설정되지 않았습니다."; exit 1; fi diff --git a/go.mod b/go.mod index 9223dea..d6ee597 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( entgo.io/ent v0.14.5 github.com/go-acme/lego/v4 v4.28.1 github.com/google/uuid v1.6.0 - github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 github.com/pion/dtls/v3 v3.0.7 github.com/prometheus/client_golang v1.19.0