Files
xk-hy-forward-go/main.go
2026-05-22 08:05:05 +08:00

227 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}