Files
Ananta-ps/internal/gateway/gateway.go
T

393 lines
10 KiB
Go

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 {
cert, err := tls.LoadX509KeyPair(filepath.Join(g.certDir, "cert.pem"), filepath.Join(g.certDir, "key.pem"))
if err != nil {
return fmt.Errorf("load gateway TLS certificate: %w", err)
}
mux := http.NewServeMux()
mux.HandleFunc("/", g.handleDispatch)
addr := func(port int) string { return net.JoinHostPort(g.cfg.Network.BindHost, fmt.Sprint(port)) }
g.listSrv = &http.Server{Addr: addr(g.cfg.Network.Proxy.LoginListPort), Handler: mux}
g.httpSrv = &http.Server{Addr: addr(g.cfg.Network.Proxy.HttpPort), Handler: mux}
g.httpsSrv = &http.Server{Addr: addr(g.cfg.Network.Proxy.HttpsPort), Handler: mux,
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12}}
servers := []*http.Server{g.listSrv, g.httpSrv, g.httpsSrv}
listeners := make([]net.Listener, 0, len(servers))
for _, srv := range servers {
ln, err := net.Listen("tcp", srv.Addr)
if err != nil {
for _, opened := range listeners {
opened.Close()
}
return fmt.Errorf("listen gateway %s: %w", srv.Addr, err)
}
listeners = append(listeners, ln)
}
for i, srv := range servers {
ln := listeners[i]
if srv == g.httpsSrv {
ln = tls.NewListener(ln, srv.TLSConfig)
}
log.Printf("[GATEWAY] Listening on %s", ln.Addr())
go func() {
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
log.Printf("[GATEWAY] Serve error: %v", err)
}
}()
}
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(`<!doctype html>
<html>
<head><meta charset="utf-8"><title>MPay Login Bypass</title></head>
<body>
<script>
const user = {
account: "%s",
user_id: "%s",
uid: "%s",
aid: "%d",
token: "%s",
access_token: "%s",
login_ticket: "%s",
device_id: "ananta-pc-client",
deviceid: "ananta-pc-client",
udid: "ananta-pc-client",
server_id: "%d",
code: 200,
subcode: 0,
msg: "ok"
};
const payload = {
methodId: "ngwebview_notify_native",
reqData: {
methodId: "onUserLogin",
device_id: user.device_id,
deviceid: user.deviceid,
udid: user.udid,
user: user
}
};
function sendLogin() {
try {
if (window.NeteaseMpayJSBridge && window.NeteaseMpayJSBridge.Common && window.NeteaseMpayJSBridge.Common.onUserLogin) {
window.NeteaseMpayJSBridge.Common.onUserLogin(user);
}
} catch (e) {}
try {
if (window.UniSDKJSBridge && window.UniSDKJSBridge.postMsgToNative) {
window.UniSDKJSBridge.postMsgToNative(payload);
}
} catch (e) {}
}
window.addEventListener("NeteaseMpayJSBridgeReady", sendLogin);
setTimeout(sendLogin, 100);
setTimeout(sendLogin, 300);
setTimeout(sendLogin, 1000);
</script>
<h3>Authenticating Ananta...</h3>
</body>
</html>`, 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)
}