// Package sdkserver is the Go port of the Rust sdkserver: HTTP dispatch + // auth + SRTools save upload, using the shared runtime configuration. package sdkserver import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "os" "strings" "sync" "gac-hkrpg-go/internal/common" google "google.golang.org/protobuf/proto" goproto "gac-hkrpg-go/protocol/proto" ) // VersionConfig is one entry of versions.json. type VersionConfig struct { AssetBundleURL string `json:"asset_bundle_url"` ExResourceURL string `json:"ex_resource_url"` LuaURL string `json:"lua_url"` IfixURL string `json:"ifix_url"` } type appState struct { runtime common.RuntimeConfig mu sync.Mutex hotfixMap map[string]VersionConfig } func loadHotfix() map[string]VersionConfig { out := map[string]VersionConfig{} if raw, err := os.ReadFile("hotfix.json"); err == nil { _ = json.Unmarshal(raw, &out) } if raw, err := os.ReadFile("versions.json"); err == nil { _ = json.Unmarshal(raw, &out) } return out } // getOrInsertHotfix mirrors AppState::get_or_insert_hotfix — unknown versions // are fetched from the official gateway via a proxy, then persisted. func (a *appState) getOrInsertHotfix(version, dispatchSeed string) VersionConfig { a.mu.Lock() defer a.mu.Unlock() if cfg, ok := a.hotfixMap[version]; ok { return cfg } if cfg, ok := a.hotfixMap["CNBETAWin"+version]; ok { return cfg } if cfg, ok := a.hotfixMap["OSBETAWin"+version]; ok { return cfg } Log(fmt.Sprintf("trying to fetch hotfix for version %s with dispatch seed %s", version, dispatchSeed)) hotfix, err := fetchHotfix(version, dispatchSeed) if err != nil { Log("failed to fetch hotfix. reason: " + err.Error()) hotfix = VersionConfig{} } a.hotfixMap[version] = hotfix if out, err := json.MarshalIndent(a.hotfixMap, "", " "); err == nil { _ = os.WriteFile("versions.json", out, 0o644) } return a.hotfixMap[version] } // fetchHotfix queries the official query_gateway endpoint through a proxy. func fetchHotfix(version, dispatchSeed string) (VersionConfig, error) { const ( proxyHost = "proxy1.neonteam.dev" osBeta = "beta-release01-asia.starrails.com" osProd = "prod-official-asia-dp01.starrails.com" cnBeta = "beta-release01-cn.bhsr.com" cnProd = "prod-gf-cn-dp01.bhsr.com" ) region, branch := parseVersionString(version) host := osBeta switch { case region == "OS" && branch == "PROD": host = osProd case region == "CN" && branch == "BETA": host = cnBeta case region == "CN" && branch == "PROD": host = cnProd } url := fmt.Sprintf("https://%s/%s/query_gateway?version=%s&platform_type=1&language_type=3&dispatch_seed=%s&channel_id=1&sub_channel_id=1&is_need_url=1", proxyHost, host, version, dispatchSeed) Log("fetching hotfix: " + url) resp, err := http.Get(url) if err != nil { return VersionConfig{}, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return VersionConfig{}, err } Log("raw gateway response: " + string(body)) bin, err := base64.StdEncoding.DecodeString(string(body)) if err != nil { return VersionConfig{}, err } decoded := &goproto.Gateserver{} if err := google.Unmarshal(bin, decoded); err != nil { return VersionConfig{}, err } if decoded.AssetBundleUrl == "" && decoded.ExResourceUrl == "" { return VersionConfig{}, fmt.Errorf("asset_bundle_url and ex_resource_url are empty! gateway response: %+v", decoded) } return VersionConfig{ AssetBundleURL: decoded.AssetBundleUrl, ExResourceURL: decoded.ExResourceUrl, LuaURL: decoded.LuaUrl, IfixURL: decoded.IfixUrl, }, nil } // parseVersionString extracts region/branch, e.g. a version with the OSBETAWin prefix -> OS,BETA. func parseVersionString(s string) (string, string) { branches := []string{"PREbeta", "BETA", "PROD", "DEV", "PRE", "GM", "CECREATION"} if len(s) < 2 { return "OS", "BETA" } region := s[:2] rest := s[2:] for _, b := range branches { if len(rest) >= len(b) && rest[:len(b)] == b { return region, b } } return region, "BETA" } // --------------------------------------------------------------------------- // handlers // --------------------------------------------------------------------------- func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(v) } func queryDispatch(w http.ResponseWriter, _ *http.Request, runtime common.RuntimeConfig) { rsp := &goproto.Dispatch{ Retcode: 0, RegionList: []*goproto.RegionInfo{{ Name: "GaC hkrpg GO", Title: "GaC hkrpg GO", EnvType: "9", DispatchUrl: runtime.PublicSDKURL + "/query_gateway", }}, } bin, err := google.Marshal(rsp) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } _, _ = w.Write([]byte(base64.StdEncoding.EncodeToString(bin))) } func queryGateway(state *appState) http.HandlerFunc { type params struct { Version string `json:"-"` DispatchSeed string `json:"-"` } return func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() version := q.Get("version") seed := q.Get("dispatch_seed") _ = params{} cfg := state.getOrInsertHotfix(version, seed) if state.runtime.LuaOverlay.Enabled { cfg.LuaURL = state.runtime.PublicSDKURL + luaOverlayRoute } rsp := &goproto.Gateserver{ Ip: state.runtime.PublicGameHost, Port: uint32(state.runtime.GamePort), AssetBundleUrl: cfg.AssetBundleURL, AssetBundleUrlB: cfg.AssetBundleURL, ExResourceUrl: cfg.ExResourceURL, LuaUrl: cfg.LuaURL, IfixVersion: "0", Unk1: true, Unk2: true, Unk3: true, Unk4: true, Unk5: true, Unk6: true, Unk7: true, } bin, err := google.Marshal(rsp) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } _, _ = w.Write([]byte(base64.StdEncoding.EncodeToString(bin))) } } type authRequestPayload struct { Account string `json:"account"` Password string `json:"password"` UID string `json:"uid"` Token string `json:"token"` } func parseAuthPayload(r *http.Request) authRequestPayload { var payload authRequestPayload if r.Body != nil { body, err := io.ReadAll(r.Body) if err == nil && len(body) > 0 { _ = json.Unmarshal(body, &payload) r.Body = io.NopCloser(bytes.NewBuffer(body)) } } return payload } func (a *appState) standardAccount(accountName string) map[string]any { if accountName == "" { accountName = a.runtime.AccountName } return map[string]any{ "name": accountName, "account_name": accountName, "area_code": "**", "email": accountName, "country": "US", "is_email_verify": "1", "token": "mostsecuretokenever", "uid": fmt.Sprint(a.runtime.PlayerUID), } } func (a *appState) loginReply(w http.ResponseWriter, r *http.Request) { req := parseAuthPayload(r) writeJSON(w, map[string]any{ "data": map[string]any{ "account": a.standardAccount(req.Account), "device_grant_required": false, "reactivate_required": false, "realperson_required": false, "safe_mobile_required": false, }, "message": "OK", "retcode": 0, }) } func (a *appState) granterLoginReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "data": map[string]any{ "account_type": 1, "combo_id": fmt.Sprint(a.runtime.PlayerUID), "combo_token": "9065ad8507d5a1991cb6fddacac5999b780bbd92", "data": `{"guest":false}`, "heartbeat": false, "open_id": fmt.Sprint(a.runtime.PlayerUID), }, "message": "OK", "retcode": 0, }) } func riskyCheckReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "data": map[string]any{ "id": "06611ed14c3131a676b19c0d34c0644b", "action": "ACTION_NONE", "geetest": nil, }, "message": "OK", "retcode": 0, }) } func (a *appState) apnLoginReply(w http.ResponseWriter, r *http.Request) { req := parseAuthPayload(r) accName := req.Account if accName == "" { accName = a.runtime.AccountName } uidStr := fmt.Sprint(a.runtime.PlayerUID) writeJSON(w, map[string]any{ "data": map[string]any{ "bind_email_action_ticket": "", "reactivate_action_token": "", "token": map[string]any{ "token": "mostsecuretokenever", "token_type": 1, }, "tokens": []any{ map[string]any{ "token": "mostsecuretokenever", "token_type": 1, }, }, "user_info": map[string]any{ "name": accName, "account_name": accName, "aid": uidStr, "mid": uidStr, "is_email_verify": 1, "area_code": "**", "country": "US", "is_adult": 1, "email": accName, "mobile": "", "safe_area_code": "", "safe_mobile": "", "realname": "", "identity_code": "", "rebind_area_code": "", "rebind_mobile": "", "rebind_mobile_time": "1", "links": []any{}, "password_time": "1", "unmasked_email": accName, "unmasked_email_type": 1, }, "ext_user_info": map[string]any{ "guardian_email": "", "birth": "0", }, }, "message": "OK", "retcode": 0, }) } func (a *appState) apnVerifyReply(w http.ResponseWriter, r *http.Request) { req := parseAuthPayload(r) accName := req.Account if accName == "" { accName = a.runtime.AccountName } uidStr := fmt.Sprint(a.runtime.PlayerUID) if req.UID != "" { uidStr = req.UID } writeJSON(w, map[string]any{ "data": map[string]any{ "bind_email_action_ticket": "", "reactivate_action_token": "", "token": map[string]any{ "token": "mostsecuretokenever", "token_type": 1, }, "tokens": []any{ map[string]any{ "token": "mostsecuretokenever", "token_type": 1, }, }, "user_info": map[string]any{ "name": accName, "account_name": accName, "aid": uidStr, "mid": uidStr, "is_email_verify": 1, "area_code": "**", "country": "US", "is_adult": 1, "email": accName, "mobile": "", "safe_area_code": "", "safe_mobile": "", "realname": "", "identity_code": "", "rebind_area_code": "", "rebind_mobile": "", "rebind_mobile_time": "1", "links": []any{}, "password_time": "1", "unmasked_email": accName, "unmasked_email_type": 1, }, "ext_user_info": map[string]any{ "guardian_email": "", "birth": "0", }, }, "message": "OK", "retcode": 0, }) } func shieldLoadConfigReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "retcode": 0, "message": "OK", "data": map[string]any{ "id": 24, "game_key": "hkrpg_global", "client": "PC", "identity": "I_IDENTITY", "guest": false, "ignore_versions": "", "scene": "S_NORMAL", "name": "Honkai: Star Rail", "disable_regist": true, "enable_email_captcha": false, "thirdparty": []string{"fb", "tw", "gl", "ap"}, "disable_mmt": false, "server_guest": false, "thirdparty_ignore": map[string]any{}, "enable_ps_bind_account": false, "thirdparty_login_configs": map[string]any{ "tw": map[string]any{"token_type": "TK_GAME_TOKEN", "game_token_expires_in": 2592000}, "ap": map[string]any{"token_type": "TK_GAME_TOKEN", "game_token_expires_in": 2592000}, "fb": map[string]any{"token_type": "TK_GAME_TOKEN", "game_token_expires_in": 2592000}, "gl": map[string]any{"token_type": "TK_GAME_TOKEN", "game_token_expires_in": 2592000}, }, "initialize_firebase": false, "bbs_auth_login": false, "bbs_auth_login_ignore": []any{}, "fetch_instance_id": false, "enable_flash_login": false, }, }) } func comboGetConfigReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "retcode": 0, "message": "OK", "data": map[string]any{ "protocol": true, "qr_enabled": false, "log_level": "INFO", "announce_url": "", "push_alias_type": 0, "disable_ysdk_guard": true, "enable_announce_pic_popup": false, "app_name": "Honkai: Star Rail", "qr_enabled_apps": map[string]bool{"bbs": false, "cloud": false}, "qr_app_icons": map[string]string{"app": "", "bbs": "", "cloud": ""}, "qr_cloud_display_name": "", "enable_user_center": true, "functional_switch_configs": map[string]any{}, }, }) } func compareProtocolVersionReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "retcode": 0, "message": "OK", "data": map[string]any{ "modified": false, "protocol": nil, }, }) } func agreementInfosReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "retcode": 0, "message": "OK", "data": map[string]any{ "marketing_agreements": []any{}, }, }) } func comboConfigReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "retcode": 0, "message": "OK", "data": map[string]any{ "vals": map[string]any{ "kibana_pc_config": `{"enable":0,"level":"Info","modules":["download"]}`, "network_report_config": `{"enable":0,"status_codes":[206],"url_paths":["dataUpload","red_dot"]}`, "list_price_tierv2_enable": "false", "enable_web_dpi": "true", }, }, }) } func deviceFpReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "code": 200, "device_fp": "20261030233000", "msg": "ok", }) } func announcementReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "retcode": 0, "message": "OK", "data": map[string]any{ "alert": false, "alert_id": 0, "remind": false, "extra_remind": false, "total": 0, "list": []any{}, "infos": []any{}, }, }) } func notFound(w http.ResponseWriter, _ *http.Request) { writeJSON(w, map[string]any{"message": "OK", "retcode": 0}) } // srToolsSave is the /srtools upload endpoint (SRTools writes freesr-data.json). func srToolsSave(w http.ResponseWriter, r *http.Request) { var payload struct { Data json.RawMessage `json:"data"` } body, err := io.ReadAll(r.Body) if err != nil || len(body) == 0 { writeJSON(w, map[string]any{"message": "empty body", "status": 400}) return } toWrite := body if err := json.Unmarshal(body, &payload); err == nil && len(payload.Data) > 0 { toWrite = payload.Data } // pretty-print and store; freesr-data.json uses the same layout the // gameserver parses, so pass through unchanged var pretty map[string]any if err := json.Unmarshal(toWrite, &pretty); err != nil { writeJSON(w, map[string]any{"message": "malformed json: " + err.Error(), "status": 500}) return } out, _ := json.MarshalIndent(pretty, "", " ") if err := os.WriteFile("freesr-data.json", out, 0o644); err != nil { writeJSON(w, map[string]any{"message": "failed to write freesr-data.json: " + err.Error(), "status": 500}) return } // Reload and sync persistent if d, err := common.LoadFreesrData(); err == nil { d.SavePersistent() } writeJSON(w, map[string]any{"message": "OK", "status": 200}) } func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { query := "" if r.URL.RawQuery != "" { query = "?" + r.URL.RawQuery } Log(fmt.Sprintf("[%s] %s%s", r.Method, r.URL.Path, query)) next.ServeHTTP(w, r) }) } // Listen starts the HTTP sdkserver. func Listen() error { runtime, err := common.LoadRuntimeConfig() if err != nil { return err } state := &appState{hotfixMap: loadHotfix(), runtime: runtime} Log(fmt.Sprintf("loaded %d hotfix versions", len(state.hotfixMap))) mux := http.NewServeMux() mux.HandleFunc("/query_dispatch", func(w http.ResponseWriter, r *http.Request) { queryDispatch(w, r, runtime) }) mux.HandleFunc("/query_gateway", queryGateway(state)) // HoYoverse / Star Rail Global & CN Authentication Routes mux.HandleFunc("/{product_name}/account/ma-passport/api/appLoginByPassword", func(w http.ResponseWriter, r *http.Request) { state.apnLoginReply(w, r) }) mux.HandleFunc("/account/ma-passport/api/appLoginByPassword", func(w http.ResponseWriter, r *http.Request) { state.apnLoginReply(w, r) }) mux.HandleFunc("/account/ma-cn-passport/app/loginByPassword", func(w http.ResponseWriter, r *http.Request) { state.apnLoginReply(w, r) }) mux.HandleFunc("/{product_name}/account/ma-passport/token/verify", func(w http.ResponseWriter, r *http.Request) { state.apnVerifyReply(w, r) }) mux.HandleFunc("/account/ma-passport/token/verify", func(w http.ResponseWriter, r *http.Request) { state.apnVerifyReply(w, r) }) mux.HandleFunc("/account/ma-cn-session/app/verify", func(w http.ResponseWriter, r *http.Request) { state.apnVerifyReply(w, r) }) mux.HandleFunc("/{product_name}/mdk/shield/api/login", func(w http.ResponseWriter, r *http.Request) { state.loginReply(w, r) }) mux.HandleFunc("/{product_name}/mdk/shield/api/verify", func(w http.ResponseWriter, r *http.Request) { state.loginReply(w, r) }) mux.HandleFunc("/{product_name}/mdk/shield/api/loadConfig", func(w http.ResponseWriter, _ *http.Request) { shieldLoadConfigReply(w) }) mux.HandleFunc("/{product_name}/combo/granter/api/getConfig", func(w http.ResponseWriter, _ *http.Request) { comboGetConfigReply(w) }) mux.HandleFunc("/{product_name}/combo/granter/api/compareProtocolVersion", func(w http.ResponseWriter, _ *http.Request) { compareProtocolVersionReply(w) }) mux.HandleFunc("/{product_name}/combo/granter/login/v2/login", func(w http.ResponseWriter, _ *http.Request) { state.granterLoginReply(w) }) mux.HandleFunc("/{product_name}/mdk/agreement/api/getAgreementInfos", func(w http.ResponseWriter, _ *http.Request) { agreementInfosReply(w) }) mux.HandleFunc("/combo/box/api/config/sdk/combo", func(w http.ResponseWriter, _ *http.Request) { comboConfigReply(w) }) mux.HandleFunc("/combo/box/api/config/sw/precache", func(w http.ResponseWriter, _ *http.Request) { comboConfigReply(w) }) mux.HandleFunc("/device-fp/api/getFp", func(w http.ResponseWriter, _ *http.Request) { deviceFpReply(w) }) mux.HandleFunc("/device-fp/api/getExtList", func(w http.ResponseWriter, _ *http.Request) { deviceFpReply(w) }) mux.HandleFunc("/account/risky/api/check", func(w http.ResponseWriter, _ *http.Request) { riskyCheckReply(w) }) mux.HandleFunc("/{product_name}/combo/red_dot/list", func(w http.ResponseWriter, _ *http.Request) { announcementReply(w) }) mux.HandleFunc("/common/{product_name}/announcement/api/getAlertAnn", func(w http.ResponseWriter, _ *http.Request) { announcementReply(w) }) mux.HandleFunc("/common/{product_name}/announcement/api/getAlertPic", func(w http.ResponseWriter, _ *http.Request) { announcementReply(w) }) mux.HandleFunc("/srtools", corsWrapper(srToolsSave)) registerAPIRoutes(mux) // Fallback Router: ensures any unmapped path variations from various client versions are caught safely mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path if path == "/" || path == "/dashboard" { apiRoot(w, r) return } Log(fmt.Sprintf("Fallback matching for path: %s", path)) switch { case strings.Contains(path, "loginByPassword") || strings.Contains(path, "appLoginByPassword"): state.apnLoginReply(w, r) case strings.Contains(path, "ma-passport/token/verify") || strings.Contains(path, "ma-cn-session/app/verify") || strings.HasSuffix(path, "/token/verify"): state.apnVerifyReply(w, r) case strings.Contains(path, "mdk/shield/api/login"): state.loginReply(w, r) case strings.Contains(path, "mdk/shield/api/verify"): state.loginReply(w, r) case strings.Contains(path, "combo/granter/login/v2/login"): state.granterLoginReply(w) case strings.Contains(path, "mdk/shield/api/loadConfig"): shieldLoadConfigReply(w) case strings.Contains(path, "combo/granter/api/getConfig"): comboGetConfigReply(w) case strings.Contains(path, "combo/granter/api/compareProtocolVersion"): compareProtocolVersionReply(w) case strings.Contains(path, "mdk/agreement/api/getAgreementInfos"): agreementInfosReply(w) case strings.Contains(path, "combo/box/api/config"): comboConfigReply(w) case strings.Contains(path, "risky/api/check"): riskyCheckReply(w) case strings.Contains(path, "device-fp"): deviceFpReply(w) case strings.Contains(path, "announcement") || strings.Contains(path, "red_dot"): announcementReply(w) default: notFound(w, r) } }) addr := runtime.SDKAddress() Log("sdkserver is listening at " + addr) Log(fmt.Sprintf("REST API Gateway is live at %s/", runtime.PublicSDKURL)) return http.ListenAndServe(addr, loggingMiddleware(withLuaOverlay(mux, runtime.LuaOverlay))) } func Log(msg string) { fmt.Println("[sdkserver]", msg) }