XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFESpaceNinjaServer

feat: android support (#3161)

Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3161 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: Jānis <janisslsm@janisslsm.id.lv> Co-committed-by: Jānis <janisslsm@janisslsm.id.lv>

eb272a98
Jānis <janisslsm@janisslsm.id.lv>
提交于

代码差异

11 个文件 +852 -22
Modified package.json +1 -0
@@ -29,6 +29,7 @@
29 29 "chokidar": "^4.0.3",
30 30 "crc-32": "^1.2.2",
31 31 "express": "^5",
32 "google-auth-library": "^10.5.0",
32 33 "json-with-bigint": "^3.4.4",
33 34 "mongoose": "^8.11.0",
34 35 "morgan": "^1.10.0",
Modified src/controllers/api/loginController.ts +35 -5
@@ -15,10 +15,28 @@ import { fromStoreItem } from "../../services/itemDataService.ts";
15 15 import { getTokenForClient, getTunablesForClient } from "../../services/tunablesService.ts";
16 16 import type { AddressInfo } from "node:net";
17 17 import gameToBuildVersion from "../../constants/gameToBuildVersion.ts";
18 import { getGoogleAccountData } from "../../helpers/customHelpers/customHelpers.ts";
18 19
19 20 export const loginController: RequestHandler = async (request, response) => {
20 21 const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
21 22
23 const isAndroid = loginRequest.ClientType === "Android";
24 if (isAndroid) {
25 try {
26 const { userId, email } = await getGoogleAccountData(loginRequest.GoogleTokenId);
27 if (email === undefined || email === "") {
28 response.status(400).json({ error: "incorrect login data" });
29 return;
30 }
31 loginRequest.email = email;
32 loginRequest.GoogleTokenId = userId;
33 } catch (error: unknown) {
34 response.status(400).json({ error: "incorrect login data" });
35 return;
36 }
37 loginRequest.password = "android"; // edit in mongodb if you want to access it via webui
38 }
39
22 40 if (config.tunables?.useLoginToken) {
23 41 if (request.query.token !== getTokenForClient((request.socket.address() as AddressInfo).address)) {
24 42 response.status(400).json({ error: "missing or incorrect token" });
@@ -33,7 +51,7 @@ export const loginController: RequestHandler = async (request, response) => {
33 51 ? request.query.buildLabel.split(" ").join("+")
34 52 : buildConfig.buildLabel;
35 53
36 if (version_compare(buildLabel, "2025.12.10.16.35") > 0) {
54 if (!isAndroid && version_compare(buildLabel, "2025.12.10.16.35") > 0) {
37 55 response.status(400).json({ error: "do you want me to change your diapers, too?" });
38 56 return;
39 57 }
@@ -51,12 +69,17 @@ export const loginController: RequestHandler = async (request, response) => {
51 69 DisplayName: name,
52 70 CountryCode: loginRequest.lang?.toUpperCase() ?? "EN",
53 71 ClientType: loginRequest.ClientType,
72 GoogleTokenId: loginRequest.GoogleTokenId,
54 73 Nonce: createNonce(),
55 74 BuildLabel: buildLabel,
56 75 LastLogin: new Date()
57 76 });
58 77 logger.debug("created new account");
59 response.send(createLoginResponse(request, newAccount, buildLabel)).end();
78 if (isAndroid) {
79 response.status(400).json({ error: `noAndroidAccount;countryCode=US` });
80 } else {
81 response.send(createLoginResponse(request, newAccount, buildLabel)).end();
82 }
60 83 return;
61 84 } catch (error: unknown) {
62 85 if (error instanceof Error) {
@@ -70,9 +93,16 @@ export const loginController: RequestHandler = async (request, response) => {
70 93 return;
71 94 }
72 95
73 if (!isCorrectPassword(loginRequest.password, account.password)) {
74 response.status(400).json({ error: "incorrect login data" });
75 return;
96 if (isAndroid) {
97 if (loginRequest.GoogleTokenId !== account.GoogleTokenId) {
98 response.status(400).json({ error: "incorrect login data" });
99 return;
100 }
101 } else {
102 if (!isCorrectPassword(loginRequest.password, account.password)) {
103 response.status(400).json({ error: "incorrect login data" });
104 return;
105 }
76 106 }
77 107
78 108 if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
Added src/controllers/api/signupAndroidController.ts +16 -0
@@ -0,0 +1,16 @@
1 import type { RequestHandler } from "express";
2 import { Account } from "../../models/loginModel.ts";
3 import { getGoogleAccountData } from "../../helpers/customHelpers/customHelpers.ts";
4
5 export const signupAndroidController: RequestHandler = async (request, res) => {
6 const googleTokenId = request.query.googleTokenId as string | undefined;
7 const { userId } = await getGoogleAccountData(googleTokenId);
8 const account = await Account.findOne({ GoogleTokenId: userId });
9 if (!account) {
10 res.status(400).json({ error: "account not found" });
11 return;
12 }
13 account.DisplayName = request.query.accountName as string;
14 await account.save();
15 res.status(200).end();
16 };
Added src/controllers/api/versionController.ts +11 -0
@@ -0,0 +1,11 @@
1 import type { RequestHandler } from "express";
2 import { buildConfig } from "../../services/buildConfigService.ts";
3
4 export const versionController: RequestHandler = (request, res) => {
5 const buildLabel: string =
6 typeof request.query.buildLabel == "string"
7 ? request.query.buildLabel.split(" ").join("+")
8 : buildConfig.buildLabel;
9
10 res.send(buildLabel);
11 };
Modified src/helpers/customHelpers/customHelpers.ts +20 -2
@@ -1,7 +1,8 @@
1 import type { IAccountCreation } from "../../types/customTypes.ts";
1 import type { IAccountCreation, IAndroidAccount } from "../../types/customTypes.ts";
2 2 import type { IDatabaseAccountRequiredFields } from "../../types/loginTypes.ts";
3 3 import crypto from "crypto";
4 4 import { isString, parseEmail, parseString } from "../general.ts";
5 import { OAuth2Client } from "google-auth-library";
5 6
6 7 const getWhirlpoolHash = (rawPassword: string): string => {
7 8 const whirlpool = crypto.createHash("whirlpool");
@@ -10,6 +11,23 @@ const getWhirlpoolHash = (rawPassword: string): string => {
10 11 return hash;
11 12 };
12 13
14 const getGoogleAccountData = async (googleTokenId: string | undefined): Promise<IAndroidAccount> => {
15 if (!googleTokenId) {
16 throw new Error("google token is missing");
17 }
18 const client = new OAuth2Client();
19 const ticket = await client.verifyIdToken({
20 idToken: googleTokenId
21 });
22
23 const payload = ticket.getPayload();
24 if (!payload) {
25 throw new Error("payload missing, perhaps invalid google token");
26 }
27
28 return { userId: payload["sub"], email: payload["email"] };
29 };
30
13 31 const parsePassword = (passwordCandidate: unknown): string => {
14 32 // a different function could be called that checks whether the password has a certain shape
15 33 if (!isString(passwordCandidate)) {
@@ -53,4 +71,4 @@ const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccountReq
53 71 } satisfies IDatabaseAccountRequiredFields;
54 72 };
55 73
56 export { toDatabaseAccount, toAccountCreation as toCreateAccount };
74 export { toDatabaseAccount, toAccountCreation as toCreateAccount, getGoogleAccountData };
Modified src/models/loginModel.ts +1 -0
@@ -20,6 +20,7 @@ const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
20 20 ForceLogoutVersion: { type: Number, default: 0 },
21 21 AmazonAuthToken: { type: String },
22 22 AmazonRefreshToken: { type: String },
23 GoogleTokenId: { type: String },
23 24 ConsentNeeded: { type: Boolean, default: false },
24 25 TrackedSettings: { type: [String], default: [] },
25 26 Nonce: { type: Number, default: 0 },
Modified src/routes/api.ts +5 -1
@@ -151,6 +151,7 @@ import { setSuitInfectionController } from "../controllers/api/setSuitInfectionC
151 151 import { setSupportedSyndicateController } from "../controllers/api/setSupportedSyndicateController.ts";
152 152 import { setWeaponSkillTreeController } from "../controllers/api/setWeaponSkillTreeController.ts";
153 153 import { shipDecorationsController } from "../controllers/api/shipDecorationsController.ts";
154 import { signupAndroidController } from "../controllers/api/signupAndroidController.ts";
154 155 import { startCollectibleEntryController } from "../controllers/api/startCollectibleEntryController.ts";
155 156 import { startDojoRecipeController } from "../controllers/api/startDojoRecipeController.ts";
156 157 import { startLibraryDailyTaskController } from "../controllers/api/startLibraryDailyTaskController.ts";
@@ -174,6 +175,7 @@ import { updateThemeController } from "../controllers/api/updateThemeController.
174 175 import { upgradeOperatorController } from "../controllers/api/upgradeOperatorController.ts";
175 176 import { upgradesController } from "../controllers/api/upgradesController.ts";
176 177 import { valenceSwapController } from "../controllers/api/valenceSwapController.ts";
178 import { versionController } from "../controllers/api/versionController.ts";
177 179 import { wishlistController } from "../controllers/api/wishlistController.ts";
178 180 import { worldStateController } from "../controllers/dynamic/worldStateController.ts";
179 181
@@ -240,6 +242,7 @@ apiRouter.get("/setBootLocation.php", setBootLocationController);
240 242 apiRouter.get("/setDojoURL", setDojoURLController);
241 243 apiRouter.get("/setGuildMotd.php", setGuildMotdController);
242 244 apiRouter.get("/setSupportedSyndicate.php", setSupportedSyndicateController);
245 apiRouter.get("/signupAndroid.php", signupAndroidController);
243 246 apiRouter.get("/startLibraryDailyTask.php", startLibraryDailyTaskController);
244 247 apiRouter.get("/startLibraryPersonalTarget.php", startLibraryPersonalTargetController);
245 248 apiRouter.get("/surveys.php", surveysController);
@@ -247,6 +250,7 @@ apiRouter.get("/trading.php", tradingController);
247 250 apiRouter.get("/trainingResult.php", trainingResultGetController);
248 251 apiRouter.get("/updateSession.php", updateSessionGetController);
249 252 apiRouter.get("/upgradeOperator.php", upgradeOperatorController);
253 apiRouter.get("/version.php", versionController);
250 254 apiRouter.get("/worldState.php", worldStateController); // U8
251 255
252 256 // post
@@ -313,7 +317,7 @@ apiRouter.post("/giveStartingGear.php", giveStartingGearPostController);
313 317 apiRouter.post("/guildTech.php", guildTechController);
314 318 apiRouter.post("/hostSession.php", hostSessionController);
315 319 apiRouter.post("/hubBlessing.php", hubBlessingController);
316 apiRouter.post("/inbox.php", inboxController); // from ~U15, don't know when they changed it to GET
320 apiRouter.post("/inbox.php", inboxController); // from ~U15, don't know when they changed it to GET
317 321 apiRouter.post("/infestedFoundry.php", infestedFoundryController);
318 322 apiRouter.post("/instantCompleteRecipe.php", claimCompletedRecipeController); // U8
319 323 apiRouter.post("/inventory.php", inventoryController); // used by companion app
Modified src/routes/cache.ts +28 -0
@@ -28,4 +28,32 @@ cacheRouter.get(/^\/0\/.+!.+$/, async (req, res) => {
28 28 }
29 29 });
30 30
31 // routes for android and possibly other non-PC platforms
32 cacheRouter.get(/^\/origin\/[a-zA-Z0-9]+\/index\.txt\.lzma.*$/, (_, res) => {
33 res.sendFile(`static/data/content/index.txt.lzma`, { root: "./" });
34 });
35
36 cacheRouter.get(/^\/[0-9]+\/H\.Cache\.bin.*$/, (req, res) => {
37 if (typeof req.query.version == "string" && req.query.version.match(/^\d\d\d\d\.\d\d\.\d\d\.\d\d\.\d\d$/)) {
38 res.sendFile(`static/data/H.Cache_${req.query.version}.bin`, { root: "./" });
39 } else {
40 res.sendFile(`static/data/H.Cache_${buildConfig.version}.bin`, { root: "./" });
41 }
42 });
43
44 cacheRouter.get(/^(\/\/|\/)(SplitCaches|Lotus|Tools|7|7_en)\/.*$/, async (req, res) => {
45 try {
46 const dir = req.path.replaceAll("//", "/").substring(0, req.path.lastIndexOf("/"));
47 const file = req.path.substring(dir.length + 1);
48 const filePath = `static/data/content${dir}/${file}`;
49 // Return file if we have it
50 await fs.access(filePath);
51 const data = await fs.readFile(filePath, null);
52 res.send(data);
53 } catch (err) {
54 // 404 if we don't
55 res.status(404).end();
56 }
57 });
58
31 59 export { cacheRouter };
Modified src/types/customTypes.ts +5 -0
@@ -4,3 +4,8 @@ export interface IAccountCreation {
4 4 DisplayName: string;
5 5 CountryCode: string;
6 6 }
7
8 export interface IAndroidAccount {
9 email?: string;
10 userId: string;
11 }
Modified src/types/loginTypes.ts +2 -0
@@ -6,6 +6,7 @@ export interface IAccountAndLoginResponseCommons {
6 6 ClientType?: string;
7 7 CrossPlatformAllowed?: boolean;
8 8 ForceLogoutVersion?: number;
9 GoogleTokenId?: string;
9 10 AmazonAuthToken?: string;
10 11 AmazonRefreshToken?: string;
11 12 ConsentNeeded?: boolean;
@@ -42,6 +43,7 @@ export interface ILoginRequest {
42 43 date: number;
43 44 ClientType?: string;
44 45 PS?: string;
46 GoogleTokenId?: string;
45 47 kick?: boolean;
46 48 }
47 49