DispatchServer.java 22.7 KB
Newer Older
Melledy's avatar
Melledy committed
1
2
3
4
5
6
package emu.grasscutter.server.dispatch;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.protobuf.ByteString;

7
import emu.grasscutter.Config;
Melledy's avatar
Melledy committed
8
9
10
11
12
13
14
import emu.grasscutter.Grasscutter;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account;
import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp;
import emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.QueryRegionListHttpRsp;
import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo;
import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo;
Jaida Wu's avatar
Jaida Wu committed
15
import emu.grasscutter.server.dispatch.json.*;
Melledy's avatar
Melledy committed
16
import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData;
KingRainbow44's avatar
KingRainbow44 committed
17
18
import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent;
import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent;
Melledy's avatar
Melledy committed
19
import emu.grasscutter.utils.FileUtils;
20
21
22
23
24
import express.Express;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
Melledy's avatar
Melledy committed
25

Jaida Wu's avatar
Jaida Wu committed
26
27
import java.io.*;
import java.net.URLDecoder;
28
import java.util.*;
Melledy's avatar
Melledy committed
29

KingRainbow44's avatar
KingRainbow44 committed
30
public final class DispatchServer {
Jaida Wu's avatar
Jaida Wu committed
31
32
	public static String query_region_list = "";
	public static String query_cur_region = "";
33

Melledy's avatar
Melledy committed
34
	private final Gson gson;
35
	private final String defaultServerName = "os_usa";
36

Melledy's avatar
Melledy committed
37
	public String regionListBase64;
38
	public Map<String, RegionData> regions;
39
	private Express httpServer;
40

Melledy's avatar
Melledy committed
41
	public DispatchServer() {
42
		this.regions = new HashMap<>();
Melledy's avatar
Melledy committed
43
		this.gson = new GsonBuilder().create();
44

Melledy's avatar
Melledy committed
45
46
47
		this.loadQueries();
		this.initRegion();
	}
48

49
50
	public Express getServer() {
		return httpServer;
Melledy's avatar
Melledy committed
51
	}
52

53
54
55
56
57
58
	public void setHttpServer(Express httpServer) {
		this.httpServer.stop();
		this.httpServer = httpServer;
		this.httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port);
	}

Melledy's avatar
Melledy committed
59
60
61
62
	public Gson getGsonFactory() {
		return gson;
	}

63
64
	public QueryCurrRegionHttpRsp getCurrRegion() {
		// Needs to be fixed by having the game servers connect to the dispatch server.
65
		if (Grasscutter.getConfig().RunMode.equalsIgnoreCase("HYBRID")) {
66
			return regions.get(defaultServerName).parsedRegionQuery;
67
68
		}

69
		Grasscutter.getLogger().warn("[Dispatch] Unsupported run mode for getCurrRegion()");
70
		return null;
Melledy's avatar
Melledy committed
71
	}
72

Melledy's avatar
Melledy committed
73
74
	public void loadQueries() {
		File file;
75

Melledy's avatar
Melledy committed
76
77
78
79
		file = new File(Grasscutter.getConfig().DATA_FOLDER + "query_region_list.txt");
		if (file.exists()) {
			query_region_list = new String(FileUtils.read(file));
		} else {
Jaida Wu's avatar
Jaida Wu committed
80
			Grasscutter.getLogger().warn("[Dispatch] query_region_list not found! Using default region list.");
Melledy's avatar
Melledy committed
81
		}
82

Melledy's avatar
Melledy committed
83
84
85
86
		file = new File(Grasscutter.getConfig().DATA_FOLDER + "query_cur_region.txt");
		if (file.exists()) {
			query_cur_region = new String(FileUtils.read(file));
		} else {
Jaida Wu's avatar
Jaida Wu committed
87
			Grasscutter.getLogger().warn("[Dispatch] query_cur_region not found! Using default current region.");
Melledy's avatar
Melledy committed
88
89
90
91
92
93
94
		}
	}

	private void initRegion() {
		try {
			byte[] decoded = Base64.getDecoder().decode(query_region_list);
			QueryRegionListHttpRsp rl = QueryRegionListHttpRsp.parseFrom(decoded);
95

Melledy's avatar
Melledy committed
96
97
			byte[] decoded2 = Base64.getDecoder().decode(query_cur_region);
			QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(decoded2);
98

KingRainbow44's avatar
KingRainbow44 committed
99
100
			List<RegionSimpleInfo> servers = new ArrayList<>();
			List<String> usedNames = new ArrayList<>(); // List to check for potential naming conflicts
101
102
			if (Grasscutter.getConfig().RunMode.equalsIgnoreCase("HYBRID")) { // Automatically add the game server if in
																				// hybrid mode
103
104
105
106
				RegionSimpleInfo server = RegionSimpleInfo.newBuilder()
						.setName("os_usa")
						.setTitle(Grasscutter.getConfig().getGameServerOptions().Name)
						.setType("DEV_PUBLIC")
107
108
109
110
111
112
113
114
115
						.setDispatchUrl(
								"http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://"
										+ (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty()
												? Grasscutter.getConfig().getDispatchOptions().Ip
												: Grasscutter.getConfig().getDispatchOptions().PublicIp)
										+ ":"
										+ (Grasscutter.getConfig().getDispatchOptions().PublicPort != 0
												? Grasscutter.getConfig().getDispatchOptions().PublicPort
												: Grasscutter.getConfig().getDispatchOptions().Port)
116
										+ "/query_cur_region/" + defaultServerName)
117
118
119
120
121
						.build();
				usedNames.add(defaultServerName);
				servers.add(server);

				RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder()
122
						.setGateserverIp((Grasscutter.getConfig().getGameServerOptions().PublicIp.isEmpty()
123
124
								? Grasscutter.getConfig().getGameServerOptions().Ip
								: Grasscutter.getConfig().getGameServerOptions().PublicIp))
125
						.setGateserverPort(Grasscutter.getConfig().getGameServerOptions().PublicPort != 0
126
127
128
129
								? Grasscutter.getConfig().getGameServerOptions().PublicPort
								: Grasscutter.getConfig().getGameServerOptions().Port)
						.setSecretKey(ByteString
								.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin")))
130
131
132
						.build();

				QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(serverRegion).build();
133
134
				regions.put(defaultServerName, new RegionData(parsedRegionQuery,
						Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray())));
135
136

			} else {
137
138
139
				if (Grasscutter.getConfig().getDispatchOptions().getGameServers().length == 0) {
					Grasscutter.getLogger()
							.error("[Dispatch] There are no game servers available. Exiting due to unplayable state.");
140
141
142
143
					System.exit(1);
				}
			}

144
145
146
			for (Config.DispatchServerOptions.RegionInfo regionInfo : Grasscutter.getConfig().getDispatchOptions()
					.getGameServers()) {
				if (usedNames.contains(regionInfo.Name)) {
147
148
149
150
151
152
153
					Grasscutter.getLogger().error("Region name already in use.");
					continue;
				}
				RegionSimpleInfo server = RegionSimpleInfo.newBuilder()
						.setName(regionInfo.Name)
						.setTitle(regionInfo.Title)
						.setType("DEV_PUBLIC")
154
155
156
157
158
						.setDispatchUrl(
								"http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://"
										+ (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty()
												? Grasscutter.getConfig().getDispatchOptions().Ip
												: Grasscutter.getConfig().getDispatchOptions().PublicIp)
159
160
161
										+ ":" + (Grasscutter.getConfig().getDispatchOptions().PublicPort != 0
										? Grasscutter.getConfig().getDispatchOptions().PublicPort
										: Grasscutter.getConfig().getDispatchOptions().Port) + "/query_cur_region/" + regionInfo.Name)
162
163
164
165
166
						.build();
				usedNames.add(regionInfo.Name);
				servers.add(server);

				RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder()
167
168
						.setGateserverIp(regionInfo.Ip)
						.setGateserverPort(regionInfo.Port)
169
170
						.setSecretKey(ByteString
								.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin")))
171
172
173
						.build();

				QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(serverRegion).build();
174
175
				regions.put(regionInfo.Name, new RegionData(parsedRegionQuery,
						Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray())));
176
177
			}

Melledy's avatar
Melledy committed
178
			QueryRegionListHttpRsp regionList = QueryRegionListHttpRsp.newBuilder()
179
					.addAllRegionList(servers)
180
181
182
183
					.setClientSecretKey(rl.getClientSecretKey())
					.setClientCustomConfigEncrypted(rl.getClientCustomConfigEncrypted())
					.setEnableLoginPc(true)
					.build();
Melledy's avatar
Melledy committed
184
185
186

			this.regionListBase64 = Base64.getEncoder().encodeToString(regionList.toByteString().toByteArray());
		} catch (Exception e) {
187
			Grasscutter.getLogger().error("[Dispatch] Error while initializing region info!", e);
Melledy's avatar
Melledy committed
188
189
190
191
		}
	}

	public void start() throws Exception {
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
		httpServer = new Express(config -> {
			config.server(() -> {
				Server server = new Server();
				ServerConnector serverConnector;

				if(Grasscutter.getConfig().getDispatchOptions().UseSSL) {
					SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
					File keystoreFile = new File(Grasscutter.getConfig().getDispatchOptions().KeystorePath);

					if(keystoreFile.exists()) {
						try {
							sslContextFactory.setKeyStorePath(keystoreFile.getPath());
							sslContextFactory.setKeyStorePassword(Grasscutter.getConfig().getDispatchOptions().KeystorePassword);
						} catch (Exception e) {
							e.printStackTrace();
							Grasscutter.getLogger().warn("[Dispatch] Unable to load keystore. Trying default keystore password...");

							try {
								sslContextFactory.setKeyStorePath(keystoreFile.getPath());
								sslContextFactory.setKeyStorePassword("123456");
								Grasscutter.getLogger().warn("[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json.");
							} catch (Exception e2) {
								Grasscutter.getLogger().warn("[Dispatch] Error while loading keystore!");
								e2.printStackTrace();
							}
						}

						serverConnector = new ServerConnector(server, sslContextFactory);
					} else {
						Grasscutter.getLogger().warn("[Dispatch] No SSL cert found! Falling back to HTTP server.");
						Grasscutter.getConfig().getDispatchOptions().UseSSL = false;

						serverConnector = new ServerConnector(server);
225
					}
226
227
				} else {
					serverConnector = new ServerConnector(server);
228
				}
229
230
231
232
233
234
235
236
237

				serverConnector.setPort(Grasscutter.getConfig().getDispatchOptions().Port);
				server.setConnectors(new Connector[]{serverConnector});
				return server;
			});

			config.enforceSsl = Grasscutter.getConfig().getDispatchOptions().UseSSL;
			if(Grasscutter.getConfig().DebugMode.equalsIgnoreCase("ALL")) {
				config.enableDevLogging();
238
			}
239
		});
Jaida Wu's avatar
Jaida Wu committed
240

241
		httpServer.get("/", (req, res) -> res.send("Welcome to Grasscutter"));
Jaida Wu's avatar
Jaida Wu committed
242

243
244
		httpServer.raw().error(404, ctx -> {
			if(Grasscutter.getConfig().DebugMode.equalsIgnoreCase("MISSING")) {
Benjamin Elsdon's avatar
Benjamin Elsdon committed
245
				Grasscutter.getLogger().info(String.format("[Dispatch] Potential unhandled %s request: %s", ctx.method(), ctx.url()));
246
247
248
249
			}
			ctx.contentType("text/html");
			ctx.result("<!doctype html><html lang=\"en\"><body><img src=\"https://http.cat/404\" /></body></html>"); // I'm like 70% sure this won't break anything.
		});
250

Melledy's avatar
Melledy committed
251
		// Dispatch
252
		httpServer.get("/query_region_list", (req, res) -> {
KingRainbow44's avatar
KingRainbow44 committed
253
			// Log
254
			Grasscutter.getLogger().info(String.format("[Dispatch] Client %s request: query_region_list", req.ip()));
Jaida Wu's avatar
Jaida Wu committed
255

256
257
258
			// Invoke event.
			QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListBase64); event.call();
			// Respond with event result.
259
			res.send(event.getRegionList());
Melledy's avatar
Melledy committed
260
		});
261

262
263
264
265
266
267
268
269
270
271
		httpServer.get("/query_cur_region/:id", (req, res) -> {
			String regionName = req.params("id");
			// Log
			Grasscutter.getLogger().info(
					String.format("Client %s request: query_cur_region/%s", req.ip(), regionName));
			// Create a response form the request query parameters
			String response = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw==";
			if (req.query().values().size() > 0) {
				response = regions.get(regionName).Base64;
			}
272

273
274
275
276
277
278
279
			// Invoke event.
			QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(response); event.call();
			// Respond with event result.
			res.send(event.getRegionInfo());
		});

		// Login
280

281
		httpServer.post("/hk4e_global/mdk/shield/api/login", (req, res) -> {
KingRainbow44's avatar
KingRainbow44 committed
282
283
284
			// Get post data
			LoginAccountRequestJson requestData = null;
			try {
285
				String body = req.ctx().body();
KingRainbow44's avatar
KingRainbow44 committed
286
				requestData = getGsonFactory().fromJson(body, LoginAccountRequestJson.class);
287
288
			} catch (Exception ignored) {
			}
289

KingRainbow44's avatar
KingRainbow44 committed
290
291
292
293
294
			// Create response json
			if (requestData == null) {
				return;
			}
			LoginResultJson responseData = new LoginResultJson();
Jaida Wu's avatar
Jaida Wu committed
295

296
			Grasscutter.getLogger()
297
					.info(String.format("[Dispatch] Client %s is trying to log in", req.ip()));
298

KingRainbow44's avatar
KingRainbow44 committed
299
300
			// Login
			Account account = DatabaseHelper.getAccountByName(requestData.account);
301

302
			// Check if account exists, else create a new one.
303
			if (account == null) {
304
305
				// Account doesnt exist, so we can either auto create it if the config value is
				// set
306
				if (Grasscutter.getConfig().getDispatchOptions().AutomaticallyCreateAccounts) {
307
308
					// This account has been created AUTOMATICALLY. There will be no permissions
					// added.
309
					account = DatabaseHelper.createAccountWithId(requestData.account, 0);
Jaida Wu's avatar
Jaida Wu committed
310

311
312
313
314
					for (String permission : Grasscutter.getConfig().getDispatchOptions().defaultPermissions) {
						account.addPermission(permission);
					}

Jaida Wu's avatar
Jaida Wu committed
315
316
317
318
319
					if (account != null) {
						responseData.message = "OK";
						responseData.data.account.uid = account.getId();
						responseData.data.account.token = account.generateSessionKey();
						responseData.data.account.email = account.getEmail();
Jaida Wu's avatar
Jaida Wu committed
320

321
322
						Grasscutter.getLogger()
								.info(String.format("[Dispatch] Client %s failed to log in: Account %s created",
323
										req.ip(), responseData.data.account.uid));
Jaida Wu's avatar
Jaida Wu committed
324
325
326
					} else {
						responseData.retcode = -201;
						responseData.message = "Username not found, create failed.";
Jaida Wu's avatar
Jaida Wu committed
327

328
						Grasscutter.getLogger().info(String.format(
329
								"[Dispatch] Client %s failed to log in: Account create failed", req.ip()));
Jaida Wu's avatar
Jaida Wu committed
330
					}
331
332
333
				} else {
					responseData.retcode = -201;
					responseData.message = "Username not found.";
Jaida Wu's avatar
Jaida Wu committed
334

335
					Grasscutter.getLogger().info(String
336
							.format("[Dispatch] Client %s failed to log in: Account no found", req.ip()));
337
				}
KingRainbow44's avatar
KingRainbow44 committed
338
			} else {
339
				// Account was found, log the player in
KingRainbow44's avatar
KingRainbow44 committed
340
341
342
343
				responseData.message = "OK";
				responseData.data.account.uid = account.getId();
				responseData.data.account.token = account.generateSessionKey();
				responseData.data.account.email = account.getEmail();
Jaida Wu's avatar
Jaida Wu committed
344

345
				Grasscutter.getLogger().info(String.format("[Dispatch] Client %s logged in as %s", req.ip(),
346
						responseData.data.account.uid));
KingRainbow44's avatar
KingRainbow44 committed
347
			}
Jaida Wu's avatar
Jaida Wu committed
348

349
			res.send(responseData);
Melledy's avatar
Melledy committed
350
		});
351

Melledy's avatar
Melledy committed
352
		// Login via token
353
		httpServer.post("/hk4e_global/mdk/shield/api/verify", (req, res) -> {
KingRainbow44's avatar
KingRainbow44 committed
354
355
356
			// Get post data
			LoginTokenRequestJson requestData = null;
			try {
357
				String body = req.ctx().body();
KingRainbow44's avatar
KingRainbow44 committed
358
				requestData = getGsonFactory().fromJson(body, LoginTokenRequestJson.class);
359
360
			} catch (Exception ignored) {
			}
361

KingRainbow44's avatar
KingRainbow44 committed
362
363
364
365
366
			// Create response json
			if (requestData == null) {
				return;
			}
			LoginResultJson responseData = new LoginResultJson();
367
			Grasscutter.getLogger().info(String.format("[Dispatch] Client %s is trying to log in via token", req.ip()));
KingRainbow44's avatar
KingRainbow44 committed
368
369
370

			// Login
			Account account = DatabaseHelper.getAccountById(requestData.uid);
371

KingRainbow44's avatar
KingRainbow44 committed
372
373
374
375
			// Test
			if (account == null || !account.getSessionKey().equals(requestData.token)) {
				responseData.retcode = -111;
				responseData.message = "Game account cache information error";
Jaida Wu's avatar
Jaida Wu committed
376

377
				Grasscutter.getLogger()
378
						.info(String.format("[Dispatch] Client %s failed to log in via token", req.ip()));
KingRainbow44's avatar
KingRainbow44 committed
379
380
381
382
383
			} else {
				responseData.message = "OK";
				responseData.data.account.uid = requestData.uid;
				responseData.data.account.token = requestData.token;
				responseData.data.account.email = account.getEmail();
mingjun97's avatar
mingjun97 committed
384
385
386
				if (responseData.data.account.email == null) { // null will trigger crash in some client
					responseData.data.account.email = "";
				}
Jaida Wu's avatar
Jaida Wu committed
387

388
				Grasscutter.getLogger().info(String.format("[Dispatch] Client %s logged in via token as %s",
389
						req.ip(), responseData.data.account.uid));
KingRainbow44's avatar
KingRainbow44 committed
390
			}
Jaida Wu's avatar
Jaida Wu committed
391

392
			res.send(responseData);
Melledy's avatar
Melledy committed
393
		});
394

Melledy's avatar
Melledy committed
395
		// Exchange for combo token
396
		httpServer.post("/hk4e_global/combo/granter/login/v2/login", (req, res) -> {
KingRainbow44's avatar
KingRainbow44 committed
397
398
399
			// Get post data
			ComboTokenReqJson requestData = null;
			try {
400
				String body = req.ctx().body();
KingRainbow44's avatar
KingRainbow44 committed
401
				requestData = getGsonFactory().fromJson(body, ComboTokenReqJson.class);
402
403
			} catch (Exception ignored) {
			}
404

KingRainbow44's avatar
KingRainbow44 committed
405
406
407
408
			// Create response json
			if (requestData == null || requestData.data == null) {
				return;
			}
409
			LoginTokenData loginData = getGsonFactory().fromJson(requestData.data, LoginTokenData.class); // Get login
410
			// data
KingRainbow44's avatar
KingRainbow44 committed
411
412
413
414
			ComboTokenResJson responseData = new ComboTokenResJson();

			// Login
			Account account = DatabaseHelper.getAccountById(loginData.uid);
415

KingRainbow44's avatar
KingRainbow44 committed
416
417
418
419
			// Test
			if (account == null || !account.getSessionKey().equals(loginData.token)) {
				responseData.retcode = -201;
				responseData.message = "Wrong session key.";
Jaida Wu's avatar
Jaida Wu committed
420

421
				Grasscutter.getLogger().info(
422
						String.format("[Dispatch] Client %s failed to exchange combo token", req.ip()));
KingRainbow44's avatar
KingRainbow44 committed
423
424
425
426
427
			} else {
				responseData.message = "OK";
				responseData.data.open_id = loginData.uid;
				responseData.data.combo_id = "157795300";
				responseData.data.combo_token = account.generateLoginToken();
Jaida Wu's avatar
Jaida Wu committed
428

429
				Grasscutter.getLogger().info(
430
						String.format("[Dispatch] Client %s succeed to exchange combo token", req.ip()));
KingRainbow44's avatar
KingRainbow44 committed
431
			}
Jaida Wu's avatar
Jaida Wu committed
432

433
			res.send(responseData);
Melledy's avatar
Melledy committed
434
		});
435
436
437
438

		// TODO: There are some missing route request types here (You can tell if they are missing if they are .all and not anything else)
		//  When http requests for theses routes are found please remove it from the list in DispatchHttpJsonHandler and update the route request types here

Melledy's avatar
Melledy committed
439
		// Agreement and Protocol
440
441
442
		// hk4e-sdk-os.hoyoverse.com
		httpServer.get("/hk4e_global/mdk/agreement/api/getAgreementInfos", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}"));
		// hk4e-sdk-os.hoyoverse.com
mingjun97's avatar
mingjun97 committed
443
		httpServer.get("/hk4e_global/combo/granter/api/compareProtocolVersion", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":true,\"protocol\":{\"id\":0,\"app_id\":4,\"language\":\"en\",\"user_proto\":\"\",\"priv_proto\":\"\",\"major\":7,\"minimum\":0,\"create_time\":\"0\",\"teenager_proto\":\"\",\"third_proto\":\"\"}}}"));
444

Melledy's avatar
Melledy committed
445
		// Game data
446
447
448
449
450
451
452
453
454
455
456
		// hk4e-api-os.hoyoverse.com
		httpServer.all("/common/hk4e_global/announcement/api/getAlertPic", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}"));
		// hk4e-api-os.hoyoverse.com
		httpServer.all("/common/hk4e_global/announcement/api/getAlertAnn", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}"));
		// hk4e-api-os.hoyoverse.com
		httpServer.all("/common/hk4e_global/announcement/api/getAnnList", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"list\":[],\"total\":0,\"type_list\":[],\"alert\":false,\"alert_id\":0,\"timezone\":0,\"t\":\"" + System.currentTimeMillis() + "\"}}"));
		// hk4e-api-os-static.hoyoverse.com
		httpServer.all("/common/hk4e_global/announcement/api/getAnnContent", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"list\":[],\"total\":0}}"));
		// hk4e-sdk-os.hoyoverse.com
		httpServer.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}"));

Melledy's avatar
Melledy committed
457
		// Captcha
458
459
460
		// api-account-os.hoyoverse.com
		httpServer.post("/account/risky/api/check", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}"));

461
		// Config
462
463
464
465
466
		// sdk-os-static.hoyoverse.com
		httpServer.get("/combo/box/api/config/sdk/combo", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"vals\":{\"disable_email_bind_skip\":\"false\",\"email_bind_remind_interval\":\"7\",\"email_bind_remind\":\"true\"}}}"));
		// hk4e-sdk-os-static.hoyoverse.com
		httpServer.get("/hk4e_global/combo/granter/api/getConfig", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"protocol\":true,\"qr_enabled\":false,\"log_level\":\"INFO\",\"announce_url\":\"https://webstatic-sea.hoyoverse.com/hk4e/announcement/index.html?sdk_presentation_style=fullscreen\\u0026sdk_screen_transparent=true\\u0026game_biz=hk4e_global\\u0026auth_appid=announcement\\u0026game=hk4e#/\",\"push_alias_type\":2,\"disable_ysdk_guard\":false,\"enable_announce_pic_popup\":true}}"));
		// hk4e-sdk-os-static.hoyoverse.com
Magix's avatar
Magix committed
467
		httpServer.get("/hk4e_global/mdk/shield/api/loadConfig", new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":6,\"game_key\":\"hk4e_global\",\"client\":\"PC\",\"identity\":\"I_IDENTITY\",\"guest\":false,\"ignore_versions\":\"\",\"scene\":\"S_NORMAL\",\"name\":\"原神海外\",\"disable_regist\":false,\"enable_email_captcha\":false,\"thirdparty\":[\"fb\",\"tw\"],\"disable_mmt\":false,\"server_guest\":false,\"thirdparty_ignore\":{\"tw\":\"\",\"fb\":\"\"},\"enable_ps_bind_account\":false,\"thirdparty_login_configs\":{\"tw\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800},\"fb\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800}}}}"));
Melledy's avatar
Melledy committed
468
		// Test api?
469
		// abtest-api-data-sg.hoyoverse.com
Benjamin Elsdon's avatar
Benjamin Elsdon committed
470
		httpServer.post("/data_abtest_api/config/experiment/list", new DispatchHttpJsonHandler("{\"retcode\":0,\"success\":true,\"message\":\"\",\"data\":[{\"code\":1000,\"type\":2,\"config_id\":\"14\",\"period_id\":\"6036_99\",\"version\":\"1\",\"configs\":{\"cardType\":\"old\"}}]}"));
471
472
473
474

		// log-upload-os.mihoyo.com
		httpServer.all("/log/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}"));
		httpServer.all("/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}"));
mingjun97's avatar
mingjun97 committed
475
		httpServer.post("/sdk/dataUpload", (req, res) -> res.send("Hello"));
476
477
		// /perf/config/verify?device_id=xxx&platform=x&name=xxx
		httpServer.all("/perf/config/verify", new DispatchHttpJsonHandler("{\"code\":0}"));
478

Melledy's avatar
Melledy committed
479
		// Logging servers
480
		// overseauspider.yuanshen.com
481
		httpServer.all("/log", new ClientLogHandler());
482
		// log-upload-os.mihoyo.com
483
		httpServer.all("/crash/dataUpload", new ClientLogHandler());
Jaida Wu's avatar
Jaida Wu committed
484

mingjun97's avatar
mingjun97 committed
485
486
487
		// webstatic-sea.hoyoverse.com
		httpServer.get("/admin/mi18n/plat_oversea/m202003048/m202003048-version.json", new DispatchHttpJsonHandler("{\"version\":51}"));

488
		httpServer.get("/gacha", (req, res) -> res.send("<!doctype html><html lang=\"en\"><head><title>Gacha</title></head><body></body></html>"));
Jaida Wu's avatar
Jaida Wu committed
489

490
491
		httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port);
		Grasscutter.getLogger().info("[Dispatch] Dispatch server started on port " + httpServer.raw().port());
Melledy's avatar
Melledy committed
492
	}
493

Melledy's avatar
Melledy committed
494
	private Map<String, String> parseQueryString(String qs) {
Jaida Wu's avatar
Jaida Wu committed
495
		Map<String, String> result = new HashMap<>();
Jaida Wu's avatar
Jaida Wu committed
496
		if (qs == null) {
Jaida Wu's avatar
Jaida Wu committed
497
			return result;
Jaida Wu's avatar
Jaida Wu committed
498
		}
Jaida Wu's avatar
Jaida Wu committed
499
500
501
502

		int last = 0, next, l = qs.length();
		while (last < l) {
			next = qs.indexOf('&', last);
Jaida Wu's avatar
Jaida Wu committed
503
			if (next == -1) {
Jaida Wu's avatar
Jaida Wu committed
504
				next = l;
Jaida Wu's avatar
Jaida Wu committed
505
			}
Jaida Wu's avatar
Jaida Wu committed
506
507
508

			if (next > last) {
				int eqPos = qs.indexOf('=', last);
509
510
511
512
513
514
515
516
517
				try {
					if (eqPos < 0 || eqPos > next) {
						result.put(URLDecoder.decode(qs.substring(last, next), "utf-8"), "");
					} else {
						result.put(URLDecoder.decode(qs.substring(last, eqPos), "utf-8"),
								URLDecoder.decode(qs.substring(eqPos + 1, next), "utf-8"));
					}
				} catch (UnsupportedEncodingException e) {
					throw new RuntimeException(e); // will never happen, utf-8 support is mandatory for java
Jaida Wu's avatar
Jaida Wu committed
518
519
520
521
522
				}
			}
			last = next + 1;
		}
		return result;
Melledy's avatar
Melledy committed
523
	}
524
525
526
527
528
529
530
531
532

	public static class RegionData {
		QueryCurrRegionHttpRsp parsedRegionQuery;
		String Base64;

		public RegionData(QueryCurrRegionHttpRsp prq, String b64) {
			this.parsedRegionQuery = prq;
			this.Base64 = b64;
		}
533
534
535
536
537
538
539
540

		public QueryCurrRegionHttpRsp getParsedRegionQuery() {
			return parsedRegionQuery;
		}

		public String getBase64() {
			return Base64;
		}
541
	}
Melledy's avatar
Melledy committed
542
}