返回提交历史
Modified
package-lock.json
+728
-14
Modified
package.json
+1
-0
Modified
src/controllers/api/loginController.ts
+35
-5
Added
src/controllers/api/signupAndroidController.ts
+16
-0
Added
src/controllers/api/versionController.ts
+11
-0
Modified
src/helpers/customHelpers/customHelpers.ts
+20
-2
Modified
src/models/loginModel.ts
+1
-0
Modified
src/routes/api.ts
+5
-1
Modified
src/routes/cache.ts
+28
-0
Modified
src/types/customTypes.ts
+5
-0
Modified
src/types/loginTypes.ts
+2
-0
XFEstudio/SpaceNinjaServer
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
代码差异
11 个文件
+852
-22
@@ -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",
@@ -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) {
@@ -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
};
@@ -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
};
@@ -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 };
@@ -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 },
@@ -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
@@ -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 };
@@ -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
}
@@ -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