From c008243f11c2faa6852ae633af026e9e74a4cb75 Mon Sep 17 00:00:00 2001 From: CAtlas0784 Date: Fri, 30 Oct 2026 23:43:34 +0700 Subject: [PATCH] feat(sdkserver): add HoYoverse OS passport login routes and universal fallback --- .../gameserver/handlers_equipment_test.go | 2 +- internal/sdkserver/server.go | 365 +++++++++++++++--- 2 files changed, 321 insertions(+), 46 deletions(-) diff --git a/internal/gameserver/handlers_equipment_test.go b/internal/gameserver/handlers_equipment_test.go index 43c9ddf..57db6f3 100644 --- a/internal/gameserver/handlers_equipment_test.go +++ b/internal/gameserver/handlers_equipment_test.go @@ -139,7 +139,7 @@ func TestEquipmentPickerAuxiliaryRequestsHaveTypedReplies(t *testing.T) { } } if recommendation.EquipAvatar != avatarID || recommendation.BigDataRecommendType != goproto.BigDataRecommendType_BIG_DATA_RECOMMEND_TYPE_AVATAR_RELIC { - t.Fatalf("recommendation response lost request context: %+v", recommendation) + t.Fatalf("recommendation response lost request context: %+v", &recommendation) } } diff --git a/internal/sdkserver/server.go b/internal/sdkserver/server.go index 6abeaef..f03e019 100644 --- a/internal/sdkserver/server.go +++ b/internal/sdkserver/server.go @@ -3,15 +3,18 @@ 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" @@ -213,31 +216,46 @@ func queryGateway(state *appState) http.HandlerFunc { } } -type accountPayload struct { - Data struct { - Account map[string]any `json:"account"` - } `json:"data"` - Message string `json:"message"` - Retcode int `json:"retcode"` +type authRequestPayload struct { + Account string `json:"account"` + Password string `json:"password"` + UID string `json:"uid"` + Token string `json:"token"` } -func (a *appState) standardAccount() map[string]any { +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": a.runtime.AccountName, - "account_name": a.runtime.AccountName, + "name": accountName, + "account_name": accountName, "area_code": "**", - "email": a.runtime.AccountName, - "country": "RU", + "email": accountName, + "country": "US", "is_email_verify": "1", "token": "mostsecuretokenever", "uid": fmt.Sprint(a.runtime.PlayerUID), } } -func (a *appState) loginReply(w http.ResponseWriter) { +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(), + "account": a.standardAccount(req.Account), "device_grant_required": false, "reactivate_required": false, "realperson_required": false, @@ -275,20 +293,54 @@ func riskyCheckReply(w http.ResponseWriter) { }) } -func (a *appState) apnLoginReply(w http.ResponseWriter) { +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{ - "token": map[string]any{"token": "mostsecuretokenever", "token_type": 1}, + "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": a.runtime.AccountName, - "account_name": a.runtime.AccountName, - "aid": fmt.Sprint(a.runtime.PlayerUID), - "mid": fmt.Sprint(a.runtime.PlayerUID), - "is_email_verify": 1, - "area_code": "**", - "country": "US", - "is_adult": 1, - "email": a.runtime.AccountName, + "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", @@ -296,20 +348,57 @@ func (a *appState) apnLoginReply(w http.ResponseWriter) { }) } -func (a *appState) apnVerifyReply(w http.ResponseWriter) { +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{ - "tokens": []any{map[string]any{"token": "mostsecuretokenever", "token_type": 1}}, + "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": a.runtime.AccountName, - "account_name": a.runtime.AccountName, - "aid": fmt.Sprint(a.runtime.PlayerUID), - "mid": fmt.Sprint(a.runtime.PlayerUID), - "is_email_verify": 1, - "area_code": "**", - "country": "US", - "is_adult": 1, - "email": a.runtime.AccountName, + "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", @@ -317,6 +406,123 @@ func (a *appState) apnVerifyReply(w http.ResponseWriter) { }) } +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}) } @@ -358,6 +564,17 @@ func srToolsSave(w http.ResponseWriter, r *http.Request) { 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() @@ -370,26 +587,84 @@ func Listen() error { mux := http.NewServeMux() mux.HandleFunc("/query_dispatch", func(w http.ResponseWriter, r *http.Request) { queryDispatch(w, r, runtime) }) mux.HandleFunc("/query_gateway", queryGateway(state)) - mux.HandleFunc("/{product_name}/mdk/shield/api/login", func(w http.ResponseWriter, r *http.Request) { state.loginReply(w) }) - mux.HandleFunc("/{product_name}/mdk/shield/api/verify", func(w http.ResponseWriter, r *http.Request) { state.loginReply(w) }) - mux.HandleFunc("/{product_name}/combo/granter/login/v2/login", func(w http.ResponseWriter, r *http.Request) { state.granterLoginReply(w) }) - mux.HandleFunc("/account/risky/api/check", func(w http.ResponseWriter, r *http.Request) { riskyCheckReply(w) }) - mux.HandleFunc("/account/ma-cn-passport/app/loginByPassword", func(w http.ResponseWriter, r *http.Request) { state.apnLoginReply(w) }) - mux.HandleFunc("/account/ma-cn-session/app/verify", func(w http.ResponseWriter, r *http.Request) { state.apnVerifyReply(w) }) + + // 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) { - if r.URL.Path == "/" || r.URL.Path == "/dashboard" { + path := r.URL.Path + if path == "/" || path == "/dashboard" { apiRoot(w, r) return } - notFound(w, r) + + 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/ (for GaC-Srtool)", runtime.PublicSDKURL)) - return http.ListenAndServe(addr, withLuaOverlay(mux, runtime.LuaOverlay)) + 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) }