// xk-hy-forward-go:内网双通道透明转发服务。 // // 部署在内网(可访问政务云 59.202.52.129),外网 xk-hy-transit-go 只访问本服务,不直连政务云。 // // 通道一 — 业务监管数据(28212): // // POST /province/supervise/data → SUPERVISE_TARGET_URL // 请求体为 transit 加密后的 JSON,Header 原样透传。 // // 通道二 — 处方 PDF 文件上传(28211): // // POST /mng/file/auth/upload → FILE_TARGET_URL // multipart 表单与 X-Authorization(uploadToken)原样透传。 // // 环境变量见 .env.example;日志默认 ../log/forward/app-YYYY-MM-DD.log(LOG_DIR 可覆盖)。 package main import ( "crypto/tls" "fmt" "io" "log" "net" "net/http" "net/http/httputil" "net/url" "os" "path/filepath" "strings" "sync" "time" "github.com/joho/godotenv" ) //const ( // defaultSuperviseTarget = "https://59.202.52.129:28212/province/supervise/data" // defaultFileTarget = "https://59.202.52.129:28211/mng/file/auth/upload" //) const ( defaultSuperviseTarget = "http://127.0.0.1:18001/api/" defaultFileTarget = "http://127.0.0.1:18001/api/u" ) var ( logMu sync.Mutex appLogW io.Writer ) func main() { _ = godotenv.Load() if err := initAppLog(); err != nil { log.Printf("应用日志初始化失败: %v", err) } listen := env("LISTEN_ADDR", ":16001") allowIPs := env("ALLOW_IPS", "") // SUPERVISE_TARGET_URL 优先;未配置时兼容旧变量 TARGET_URL superviseTarget := env("SUPERVISE_TARGET_URL", "") if superviseTarget == "" { superviseTarget = env("TARGET_URL", defaultSuperviseTarget) } fileTarget := env("FILE_TARGET_URL", defaultFileTarget) superviseProxy, err := newReverseProxy(superviseTarget) if err != nil { log.Fatalf("SUPERVISE_TARGET_URL 无效: %v", err) } fileProxy, err := newReverseProxy(fileTarget) if err != nil { log.Fatalf("FILE_TARGET_URL 无效: %v", err) } mux := http.NewServeMux() // 健康检查:供运维与 transit 部署前探测 forward 是否存活 mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) // 通道一:监管业务上报(加密 JSON) mux.HandleFunc("/province/supervise/data", func(w http.ResponseWriter, r *http.Request) { handleForward(w, r, "supervise", superviseProxy, superviseTarget, allowIPs) }) // 通道二:处方 PDF 上传(multipart + uploadToken) mux.HandleFunc("/mng/file/auth/upload", func(w http.ResponseWriter, r *http.Request) { handleForward(w, r, "file", fileProxy, fileTarget, allowIPs) }) msg := fmt.Sprintf("xk-hy-forward-go 监听 %s | supervise=%s | file=%s", listen, superviseTarget, fileTarget) appLogf(msg) log.Print(msg) if err := http.ListenAndServe(listen, mux); err != nil { log.Fatal(err) } } // newReverseProxy 创建指向政务云目标的反向代理:仅替换 Scheme/Host/Path,不修改 body 与业务 Header。 func newReverseProxy(target string) (*httputil.ReverseProxy, error) { targetURL, err := url.Parse(target) if err != nil { return nil, err } proxy := httputil.NewSingleHostReverseProxy(targetURL) proxy.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // 政务网自签证书 } proxy.Director = func(req *http.Request) { req.URL.Scheme = targetURL.Scheme req.URL.Host = targetURL.Host req.URL.Path = targetURL.Path req.URL.RawPath = targetURL.RawPath req.URL.RawQuery = targetURL.RawQuery req.Host = targetURL.Host } return proxy, nil } // handleForward 统一处理 POST 转发:白名单校验 → 反向代理 → 记录耗时与 HTTP 状态码。 func handleForward(w http.ResponseWriter, r *http.Request, kind string, proxy *httputil.ReverseProxy, target, allowIPs string) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } clientIP := clientIP(r) if allowIPs != "" && !ipAllowed(clientIP, allowIPs) { appLogf("%s | %s | %s | forbidden | not in ALLOW_IPS", time.Now().Format(time.RFC3339), clientIP, kind) http.Error(w, "forbidden", http.StatusForbidden) return } start := time.Now() sw := &statusRecorder{ResponseWriter: w, status: http.StatusOK} proxy.ServeHTTP(sw, r) ms := time.Since(start).Milliseconds() line := fmt.Sprintf("%s | %s | %s | http=%d | %dms | %s", time.Now().Format(time.RFC3339), clientIP, kind, sw.status, ms, target) appLogf(line) log.Printf("forward %s %s http=%d %dms", clientIP, kind, sw.status, ms) } // statusRecorder 包装 ResponseWriter,用于在代理完成后记录实际 HTTP 状态码。 type statusRecorder struct { http.ResponseWriter status int } func (w *statusRecorder) WriteHeader(code int) { w.status = code w.ResponseWriter.WriteHeader(code) } func initAppLog() error { root := resolveLogRoot("forward") if err := os.MkdirAll(root, 0o755); err != nil { return err } day := time.Now().Format("2006-01-02") f, err := os.OpenFile(filepath.Join(root, "app-"+day+".log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { return err } appLogW = io.MultiWriter(os.Stdout, f) return nil } func resolveLogRoot(service string) string { if v := strings.TrimSpace(os.Getenv("LOG_DIR")); v != "" { return filepath.Join(v, service) } base := programDir() return filepath.Join(base, "..", "log", service) } func programDir() string { if exe, err := os.Executable(); err == nil { dir := filepath.Dir(exe) if strings.Contains(dir, "go-build") { if wd, err := os.Getwd(); err == nil && wd != "" { return wd } } return dir } if wd, err := os.Getwd(); err == nil && wd != "" { return wd } return "." } func appLogf(format string, args ...any) { logMu.Lock() w := appLogW logMu.Unlock() if w == nil { w = os.Stdout } log.New(w, "[forward] ", log.LstdFlags).Printf(format, args...) } func env(key, def string) string { if v := strings.TrimSpace(os.Getenv(key)); v != "" { return v } return def } func clientIP(r *http.Request) string { ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return r.RemoteAddr } return ip } // ipAllowed 判断来源 IP 是否在 ALLOW_IPS 白名单(逗号分隔);空表示不限制。 func ipAllowed(ip, allow string) bool { for _, part := range strings.Split(allow, ",") { if strings.TrimSpace(part) == ip { return true } } return false }