package gateway import ( "context" "crypto/tls" "encoding/json" "fmt" "log" "net" "net/http" "os" "path/filepath" "strings" "time" "ananta-ps/internal/config" ) type Gateway struct { cfg *config.Config publicDir string certDir string httpSrv *http.Server httpsSrv *http.Server listSrv *http.Server cancelFunc context.CancelFunc } func New(cfg *config.Config, publicDir, certDir string) *Gateway { if publicDir == "" { candidates := []string{ "public", "../public", } for _, c := range candidates { if _, err := os.Stat(c); err == nil { publicDir = c break } } } if certDir == "" { candidates := []string{ "certs", "../certs", } for _, c := range candidates { if _, err := os.Stat(c); err == nil { certDir = c break } } } return &Gateway{ cfg: cfg, publicDir: publicDir, certDir: certDir, } } func (g *Gateway) Start() error { ctx, cancel := context.WithCancel(context.Background()) g.cancelFunc = cancel mux := http.NewServeMux() mux.HandleFunc("/", g.handleDispatch) // 1. Port 5801: LoginList & Mobile Dispatch Server listAddr := fmt.Sprintf("%s:%d", g.cfg.Network.BindHost, g.cfg.Network.Proxy.LoginListPort) g.listSrv = &http.Server{ Addr: listAddr, Handler: mux, } go func() { log.Printf("[GATEWAY] LoginList & Mobile Dispatch listening on http://%s (Port %d)", g.cfg.Network.AdvertisedHost, g.cfg.Network.Proxy.LoginListPort) if err := g.listSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Printf("[GATEWAY] LoginList server error: %v", err) } }() // 2. Port 80: HTTP Gateway httpAddr := fmt.Sprintf("%s:%d", g.cfg.Network.BindHost, g.cfg.Network.Proxy.HttpPort) g.httpSrv = &http.Server{ Addr: httpAddr, Handler: mux, } go func() { log.Printf("[GATEWAY] HTTP Proxy listening on port %d", g.cfg.Network.Proxy.HttpPort) if err := g.httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Printf("[GATEWAY] HTTP Proxy error (might need Admin privileges for port 80): %v", err) } }() // 3. Port 443: HTTPS Proxy with TLS SNI certFile := filepath.Join(g.certDir, "cert.pem") keyFile := filepath.Join(g.certDir, "key.pem") if _, err := os.Stat(certFile); err == nil { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err == nil { tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { return &cert, nil }, } httpsAddr := fmt.Sprintf("%s:%d", g.cfg.Network.BindHost, g.cfg.Network.Proxy.HttpsPort) g.httpsSrv = &http.Server{ Addr: httpsAddr, Handler: mux, TLSConfig: tlsConfig, } go func() { log.Printf("[GATEWAY] HTTPS SNI Proxy listening on port %d", g.cfg.Network.Proxy.HttpsPort) ln, err := net.Listen("tcp", httpsAddr) if err != nil { log.Printf("[GATEWAY] HTTPS Proxy listen error (port 443): %v", err) return } tlsLn := tls.NewListener(ln, tlsConfig) if err := g.httpsSrv.Serve(tlsLn); err != nil && err != http.ErrServerClosed { log.Printf("[GATEWAY] HTTPS Proxy error: %v", err) } }() } else { log.Printf("[GATEWAY] Warning: failed to load TLS certificate %s: %v", certFile, err) } } else { log.Printf("[GATEWAY] Warning: cert.pem not found in %s; HTTPS port 443 disabled", g.certDir) } _ = ctx return nil } func (g *Gateway) Stop() error { if g.cancelFunc != nil { g.cancelFunc() } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if g.listSrv != nil { _ = g.listSrv.Shutdown(ctx) } if g.httpSrv != nil { _ = g.httpSrv.Shutdown(ctx) } if g.httpsSrv != nil { _ = g.httpsSrv.Shutdown(ctx) } return nil } func (g *Gateway) handleDispatch(w http.ResponseWriter, r *http.Request) { urlPath := strings.ToLower(r.URL.Path) host := strings.ToLower(r.Host) // Clean up double slashes for strings.Contains(urlPath, "//") { urlPath = strings.ReplaceAll(urlPath, "//", "/") } // 1. LoginList (Port 5801 or via Host /LoginList) if strings.HasSuffix(urlPath, "/loginlist") || urlPath == "/loginlist" { g.serveLoginList(w, r) return } // 2. ServerList (Contract MD5 7-column bypass) if strings.HasSuffix(urlPath, "/serverlist.txt") || strings.HasSuffix(urlPath, "/serverlist_review.txt") { g.serveServerList(w) return } // 3. MPay / UniSDK WebView Auth Bypass if urlPath == "/local-mpay-login" { g.serveMpayLogin(w) return } if urlPath == "/local-empty" { g.serveJSON(w, map[string]interface{}{ "code": 0, "ret": 0, "result": 0, "errno": 0, "msg": "ok", }) return } // 4. UniSDK Check Enter & Gray Release if strings.Contains(urlPath, "/sdk/check_enter") { g.serveJSON(w, map[string]interface{}{ "code": 200, "status": 0, "success": true, "message": "ok", "result": 0, "ret": 0, "subcode": 0, }) return } if strings.Contains(urlPath, "/sdk/get_gray_release_info") { g.serveJSON(w, map[string]interface{}{ "code": 200, "status": 0, "success": true, "message": "ok", "result": 0, "ret": 0, "subcode": 0, }) return } if strings.Contains(urlPath, "/sdk/uni_sauth") { g.serveJSON(w, map[string]interface{}{ "code": 200, "status": 0, "success": true, "message": "ok", "result": 0, "ret": 0, "subcode": 0, "data": map[string]interface{}{ "uid": g.cfg.Player.AccountId, "token": g.cfg.Player.LoginToken, }, }) return } // 5. ConstConfig.cfg if strings.HasSuffix(urlPath, "/constconfig.cfg") { g.serveFile(w, filepath.Join(g.publicDir, "OuterTest1_ConstConfig.cfg"), "text/plain") return } // 6. pc_boot.config (Disable shader warmup) if strings.HasSuffix(urlPath, "/pc_boot.config") { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write([]byte("enable-cs-warmup=0\nenable-ecs-shader-warmup=0\n")) return } // 7. pc_netease_version.json (Disable triggerWarmupBelowVersion) if strings.HasSuffix(urlPath, "/pc_netease_version.json") { g.serveFile(w, filepath.Join(g.publicDir, "pc_netease_version.json"), "application/json") return } // 8. device_check_pc.json if strings.HasSuffix(urlPath, "/device_check_pc.json") { g.serveJSON(w, map[string]interface{}{}) return } // 9. Startup Patch Info if strings.HasSuffix(urlPath, "/trunk-client_startup_patch_info.txt") { g.serveFile(w, filepath.Join(g.publicDir, "trunk-client_startup_patch_info.txt"), "text/plain") return } // 10. FastPatch ZIP if strings.HasPrefix(urlPath, "/fastpatch/") && strings.HasSuffix(urlPath, "/fastpatch.zip") { g.serveFile(w, filepath.Join(g.publicDir, "fastpatch_4229938.zip"), "application/zip") return } // 11. Resource Patch Info if strings.HasSuffix(urlPath, "/patch.info") { // Prefer latest Patch.info patchFile := filepath.Join(g.publicDir, "Patch_20260805061659654p0.info") if _, err := os.Stat(patchFile); err != nil { patchFile = filepath.Join(g.publicDir, "Patch_20260707064825384p0.info") } g.serveFile(w, patchFile, "application/octet-stream") return } // 12. vfs_res_version if strings.Contains(urlPath, "vfs_res_version") { g.serveFile(w, filepath.Join(g.publicDir, "vfs_res_version"), "application/octet-stream") return } // 13. download_header.ehd if strings.Contains(urlPath, "download_header.ehd") { g.serveFile(w, filepath.Join(g.publicDir, "Blocks", "download_header.ehd"), "application/octet-stream") return } // 14. stage0 patch if strings.HasSuffix(urlPath, ".stage0.patch.bytes.standalone") { g.serveFile(w, filepath.Join(g.publicDir, "stage0_patch.bytes.standalone"), "application/octet-stream") return } // 15. Audit & Login.bin if urlPath == "/audit.txt" { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write([]byte("8000000\nlocal private server compatibility stub\nok\n\n")) return } if urlPath == "/game_notice/login.bin" { loginBin := filepath.Join(g.publicDir, "Login.bin") if _, err := os.Stat(loginBin); err == nil { g.serveFile(w, loginBin, "application/octet-stream") return } w.WriteHeader(http.StatusOK) return } // Generic handler for all NetEase matrix / uniSDK subdomains if strings.Contains(host, "netease.com") || strings.Contains(host, "163.com") { g.serveJSON(w, map[string]interface{}{ "code": 200, "subcode": 0, "ret": 0, "result": 0, "errno": 0, "status": 0, "success": true, "msg": "ok", "message": "ok", }) return } // Default fallback w.WriteHeader(http.StatusOK) w.Write([]byte("Ananta Private Server Gateway Running")) } func (g *Gateway) serveLoginList(w http.ResponseWriter, r *http.Request) { login0 := g.cfg.Network.LoginPorts[0] login1 := login0 if len(g.cfg.Network.LoginPorts) > 1 { login1 = g.cfg.Network.LoginPorts[1] } host := g.cfg.Network.AdvertisedHost body := fmt.Sprintf("%s:%d\n%s:%d\n", host, login0, host, login1) w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write([]byte(body)) } func (g *Gateway) serveServerList(w http.ResponseWriter) { loginListUrl := fmt.Sprintf("http://%s:%d/LoginList", g.cfg.Network.AdvertisedHost, g.cfg.Network.Proxy.LoginListPort) // Exactly 7 basic columns: // *,801,QA101,Outer,http://127.0.0.1:5801/LoginList,1,14 body := fmt.Sprintf("*,%d,QA101,Outer,%s,1,14\n", g.cfg.Client.ServerID, loginListUrl) w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write([]byte(body)) } func (g *Gateway) serveMpayLogin(w http.ResponseWriter) { html := fmt.Sprintf(` MPay Login Bypass

Authenticating Ananta...

`, g.cfg.Player.AccountId, g.cfg.Player.AccountId, g.cfg.Player.AccountId, g.cfg.Client.Aid, g.cfg.Player.LoginToken, g.cfg.Player.LoginToken, g.cfg.Player.LoginToken, g.cfg.Client.ServerID) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write([]byte(html)) } func (g *Gateway) serveJSON(w http.ResponseWriter, v interface{}) { data, _ := json.Marshal(v) w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write(data) } func (g *Gateway) serveFile(w http.ResponseWriter, filePath, contentType string) { data, err := os.ReadFile(filePath) if err != nil { w.WriteHeader(http.StatusNotFound) return } if contentType != "" { w.Header().Set("Content-Type", contentType) } w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data))) w.WriteHeader(http.StatusOK) w.Write(data) }