feat(go): Port full Ananta private server to Go architecture
This commit is contained in:
1 parent
ca2e80f9b7
commit
c73d744649
28 files changed
+3585
-57
No files matched your search
@@ -0,0 +1,433 @@
|
||||
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{
|
||||
"Ananta.Proxy/proxy/public",
|
||||
"proxy/public",
|
||||
"../Ananta.Proxy/proxy/public",
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if _, err := os.Stat(c); err == nil {
|
||||
publicDir = c
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if certDir == "" {
|
||||
candidates := []string{
|
||||
"Ananta.Proxy/proxy/certs",
|
||||
"proxy/certs",
|
||||
"../Ananta.Proxy/proxy/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(`<!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)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"ananta-ps/internal/config"
|
||||
)
|
||||
|
||||
func TestGatewayDispatch(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Client: config.ClientConfig{
|
||||
ServerID: 801,
|
||||
},
|
||||
Network: config.NetworkConfig{
|
||||
AdvertisedHost: "127.0.0.1",
|
||||
LoginPorts: []int{5200, 5201},
|
||||
Proxy: config.ProxyConfig{
|
||||
LoginListPort: 5801,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gw := New(cfg, "../../Ananta.Proxy/proxy/public", "../../Ananta.Proxy/proxy/certs")
|
||||
|
||||
// Test LoginList
|
||||
req := httptest.NewRequest("GET", "/LoginList", nil)
|
||||
w := httptest.NewRecorder()
|
||||
gw.handleDispatch(w, req)
|
||||
resp := w.Result()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, "127.0.0.1:5200") {
|
||||
t.Fatalf("expected 127.0.0.1:5200 in LoginList, got %s", body)
|
||||
}
|
||||
|
||||
// Test ServerList
|
||||
req = httptest.NewRequest("GET", "/OuterTest1/serverlist.txt", nil)
|
||||
w = httptest.NewRecorder()
|
||||
gw.handleDispatch(w, req)
|
||||
if w.Result().StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200 for serverlist.txt, got %d", w.Result().StatusCode)
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "QA101,Outer,http://127.0.0.1:5801/LoginList,1,14") {
|
||||
t.Fatalf("unexpected serverlist body: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user