Files
xk-hy-forward-go/main.go

227 lines
6.3 KiB
Go
Raw Normal View History

2026-05-22 08:05:05 +08:00
// xk-hy-forward-go内网双通道透明转发服务。
//
// 部署在内网(可访问政务云 59.202.52.129),外网 xk-hy-transit-go 只访问本服务,不直连政务云。
//
// 通道一 — 业务监管数据28212
//
// POST /province/supervise/data → SUPERVISE_TARGET_URL
// 请求体为 transit 加密后的 JSONHeader 原样透传。
//
// 通道二 — 处方 PDF 文件上传28211
//
// POST /mng/file/auth/upload → FILE_TARGET_URL
// multipart 表单与 X-AuthorizationuploadToken原样透传。
//
// 环境变量见 .env.example日志默认 ../log/forward/app-YYYY-MM-DD.logLOG_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
}