mirror of
https://github.com/dalbodeule/hop-gate.git
synced 2025-12-07 20:35:44 +09:00
Implement initial DTLS handshake flow for server and client using pion/dtls. Load server and client configuration from .env/environment, including new debug flags and logging config. On the server: - load ServerConfig from env, including DTLS listen addr and debug flag - create DTLS listener with optional self-signed localhost cert in debug - accept DTLS sessions and run PerformServerHandshake with a dummy domain validator On the client: - load ClientConfig from env, then override with CLI flags where given - validate required fields: server_addr, domain, api_key, local_target - create DTLS client and run PerformClientHandshake - support debug mode to skip server certificate verification Also: - update go.mod/go.sum with pion/dtls and related dependencies - extend .env.example with new ports, client config, and debug flags - ignore built binaries via bin/ in .gitignore BREAKING CHANGE: client environment variables have changed. The former HOP_CLIENT_ID, HOP_CLIENT_AUTH_TOKEN and HOP_CLIENT_SERVICE_PORTS are replaced by HOP_CLIENT_DOMAIN, HOP_CLIENT_API_KEY, HOP_CLIENT_LOCAL_TARGET and HOP_CLIENT_DEBUG. Client startup now requires server_addr, domain, api_key and local_target to be provided (via env or CLI).
266 lines
7.6 KiB
Go
266 lines
7.6 KiB
Go
package config
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// LoggingConfig 는 공통 로그 설정을 담습니다.
|
|
// Loki push 에 필요한 엔드포인트/인증/정적 라벨 등을 포함합니다.
|
|
type LoggingConfig struct {
|
|
Level string // 예: "debug", "info", "warn", "error"
|
|
Loki LokiConfig // Loki 관련 설정
|
|
}
|
|
|
|
// LokiConfig 는 Loki HTTP push 설정을 담습니다.
|
|
type LokiConfig struct {
|
|
Enable bool // true 인 경우 Loki 로도 push
|
|
Endpoint string // 예: "http://loki:3100/loki/api/v1/push"
|
|
TenantID string // multi-tenant Loki 사용 시 X-Scope-OrgID 등에 사용
|
|
Username string // basic auth 사용자명(선택)
|
|
Password string // basic auth 비밀번호(선택)
|
|
StaticLabels map[string]string // 모든 로그에 공통으로 붙일 라벨 (app=hop-gate,env=dev 등)
|
|
}
|
|
|
|
// ServerConfig 는 서버 프로세스 설정을 담습니다.
|
|
type ServerConfig struct {
|
|
HTTPListen string // 예: ":80"
|
|
HTTPSListen string // 예: ":443"
|
|
DTLSListen string // 예: ":443"
|
|
Domain string // 메인 도메인
|
|
ProxyDomains []string // 프록시 서브도메인 또는 별도 도메인
|
|
Debug bool // true 이면 디버그 모드 (예: self-signed 인증서 신뢰, 검증 스킵 등)
|
|
|
|
Logging LoggingConfig // 서버용 로그 설정
|
|
}
|
|
|
|
// ClientConfig 는 클라이언트 프로세스 설정을 담습니다.
|
|
// 현재 클라이언트는 다음 4가지 설정만 사용합니다.
|
|
// - ServerAddr : DTLS 서버 주소 (host:port)
|
|
// - Domain : 서버에서 등록된 도메인 (예: api.example.com)
|
|
// - ClientAPIKey : 도메인에 매핑된 64자 클라이언트 API Key
|
|
// - LocalTarget : 로컬에서 요청할 서버 주소 (예: 127.0.0.1:8080)
|
|
//
|
|
// 값은 .env/환경변수와 CLI 인자를 조합해 구성하며,
|
|
// CLI 인자가 우선, env 가 후순위로 적용됩니다.
|
|
type ClientConfig struct {
|
|
ServerAddr string // DTLS 서버 주소 (host:port)
|
|
Domain string // 서버에서 등록된 도메인 (예: api.example.com)
|
|
ClientAPIKey string // 도메인에 매핑된 64자 클라이언트 API Key
|
|
LocalTarget string // 로컬에서 요청할 서버 주소 (예: 127.0.0.1:8080)
|
|
Debug bool // true 이면 디버그 모드 (예: 서버 인증서 검증 스킵 등)
|
|
|
|
Logging LoggingConfig // 클라이언트용 로그 설정
|
|
}
|
|
|
|
var (
|
|
dotenvOnce sync.Once
|
|
dotenvErr error
|
|
)
|
|
|
|
// loadDotEnvOnce 는 현재 작업 디렉터리의 .env 파일을 한 번만 읽어서 os.Environ 에 주입합니다.
|
|
// - KEY=VALUE, export KEY=VALUE 형식을 지원
|
|
// - # 으로 시작하는 줄은 주석으로 간주합니다.
|
|
func loadDotEnvOnce() {
|
|
dotenvOnce.Do(func() {
|
|
fi, err := os.Stat(".env")
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
// .env 가 없으면 조용히 무시
|
|
return
|
|
}
|
|
dotenvErr = err
|
|
return
|
|
}
|
|
if fi.IsDir() {
|
|
// 디렉터리이면 무시
|
|
return
|
|
}
|
|
|
|
f, err := os.Open(".env")
|
|
if err != nil {
|
|
dotenvErr = err
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "export ") {
|
|
line = strings.TrimSpace(strings.TrimPrefix(line, "export "))
|
|
}
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
key := strings.TrimSpace(parts[0])
|
|
val := strings.TrimSpace(parts[1])
|
|
// 양 끝의 작은/큰따옴표 제거
|
|
val = strings.Trim(val, `"'`)
|
|
|
|
if key != "" {
|
|
_ = os.Setenv(key, val)
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
dotenvErr = err
|
|
return
|
|
}
|
|
})
|
|
}
|
|
|
|
func getEnvOrDefault(key, def string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
func getEnvBool(key string, def bool) bool {
|
|
v := strings.ToLower(strings.TrimSpace(os.Getenv(key)))
|
|
if v == "" {
|
|
return def
|
|
}
|
|
switch v {
|
|
case "1", "true", "yes", "y", "on":
|
|
return true
|
|
case "0", "false", "no", "n", "off":
|
|
return false
|
|
default:
|
|
return def
|
|
}
|
|
}
|
|
|
|
func parseCSVEnv(key string) []string {
|
|
raw := os.Getenv(key)
|
|
if raw == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(raw, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// parseKeyValueCSV 는 "k1=v1,k2=v2" 형태의 문자열을 map 으로 변환합니다.
|
|
func parseKeyValueCSV(raw string) map[string]string {
|
|
if raw == "" {
|
|
return nil
|
|
}
|
|
m := make(map[string]string)
|
|
for _, part := range strings.Split(raw, ",") {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
kv := strings.SplitN(part, "=", 2)
|
|
if len(kv) != 2 {
|
|
continue
|
|
}
|
|
k := strings.TrimSpace(kv[0])
|
|
v := strings.TrimSpace(kv[1])
|
|
if k != "" {
|
|
m[k] = v
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// parseServicePortsEnv 는 "name1=127.0.0.1:8080,name2=127.0.0.1:9000" 형식을 파싱합니다.
|
|
func parseServicePortsEnv(key string) map[string]string {
|
|
raw := os.Getenv(key)
|
|
return parseKeyValueCSV(raw)
|
|
}
|
|
|
|
// loadLoggingFromEnv 는 공통 로그 설정을 .env/환경변수에서 읽어옵니다.
|
|
func loadLoggingFromEnv() LoggingConfig {
|
|
level := getEnvOrDefault("HOP_LOG_LEVEL", "info")
|
|
|
|
lokiEnable := getEnvBool("HOP_LOKI_ENABLE", false)
|
|
lokiEndpoint := os.Getenv("HOP_LOKI_ENDPOINT")
|
|
lokiTenantID := os.Getenv("HOP_LOKI_TENANT_ID")
|
|
lokiUsername := os.Getenv("HOP_LOKI_USERNAME")
|
|
lokiPassword := os.Getenv("HOP_LOKI_PASSWORD")
|
|
lokiStaticLabels := parseKeyValueCSV(os.Getenv("HOP_LOKI_STATIC_LABELS"))
|
|
|
|
return LoggingConfig{
|
|
Level: level,
|
|
Loki: LokiConfig{
|
|
Enable: lokiEnable,
|
|
Endpoint: lokiEndpoint,
|
|
TenantID: lokiTenantID,
|
|
Username: lokiUsername,
|
|
Password: lokiPassword,
|
|
StaticLabels: lokiStaticLabels,
|
|
},
|
|
}
|
|
}
|
|
|
|
// LoadServerConfigFromEnv 는 .env 를 우선 읽고, 이후 환경 변수를 기반으로 서버 설정을 구성합니다.
|
|
func LoadServerConfigFromEnv() (*ServerConfig, error) {
|
|
loadDotEnvOnce()
|
|
if dotenvErr != nil {
|
|
return nil, dotenvErr
|
|
}
|
|
|
|
cfg := &ServerConfig{
|
|
HTTPListen: getEnvOrDefault("HOP_SERVER_HTTP_LISTEN", ":80"),
|
|
HTTPSListen: getEnvOrDefault("HOP_SERVER_HTTPS_LISTEN", ":443"),
|
|
DTLSListen: getEnvOrDefault("HOP_SERVER_DTLS_LISTEN", ":443"),
|
|
Domain: os.Getenv("HOP_SERVER_DOMAIN"),
|
|
ProxyDomains: parseCSVEnv("HOP_SERVER_PROXY_DOMAINS"),
|
|
Debug: getEnvBool("HOP_SERVER_DEBUG", false),
|
|
Logging: loadLoggingFromEnv(),
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
// LoadClientConfigFromEnv 는 .env 를 우선 읽고, 이후 환경 변수를 기반으로 클라이언트 설정을 구성합니다.
|
|
// 실제 런타임에서 사용되는 필드는 ServerAddr, Domain, ClientAPIKey, LocalTarget 입니다.
|
|
func LoadClientConfigFromEnv() (*ClientConfig, error) {
|
|
loadDotEnvOnce()
|
|
if dotenvErr != nil {
|
|
return nil, dotenvErr
|
|
}
|
|
|
|
cfg := &ClientConfig{
|
|
ServerAddr: os.Getenv("HOP_CLIENT_SERVER_ADDR"),
|
|
Domain: os.Getenv("HOP_CLIENT_DOMAIN"),
|
|
ClientAPIKey: os.Getenv("HOP_CLIENT_API_KEY"),
|
|
LocalTarget: os.Getenv("HOP_CLIENT_LOCAL_TARGET"),
|
|
Debug: getEnvBool("HOP_CLIENT_DEBUG", false),
|
|
Logging: loadLoggingFromEnv(),
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
// Optional: 숫자 포트만 지정하고 싶을 경우를 위한 헬퍼 (예: "80" -> ":80").
|
|
// 현재는 사용하지 않지만, 향후 유효성 검사/정규화에 사용할 수 있습니다.
|
|
func normalizePort(p string, def string) string {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
return def
|
|
}
|
|
if strings.HasPrefix(p, ":") {
|
|
return p
|
|
}
|
|
// 숫자로만 구성된 경우 ":" prefix 를 붙입니다.
|
|
if _, err := strconv.Atoi(p); err == nil {
|
|
return ":" + p
|
|
}
|
|
return p
|
|
}
|