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
import emu.grasscutter.Grasscutter;
9
10
import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.Grasscutter.ServerRunMode;
Melledy's avatar
Melledy committed
11
12
13
14
15
16
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
17
import emu.grasscutter.server.dispatch.json.*;
Melledy's avatar
Melledy committed
18
import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData;
KingRainbow44's avatar
KingRainbow44 committed
19
20
import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent;
import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent;
21
import emu.grasscutter.server.http.gacha.GachaRecordHandler;
Melledy's avatar
Melledy committed
22
import emu.grasscutter.utils.FileUtils;
23
24
25
26
27
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
28

Jaida Wu's avatar
Jaida Wu committed
29
30
import java.io.*;
import java.net.URLDecoder;
31
import java.util.*;
Melledy's avatar
Melledy committed
32

KingRainbow44's avatar
KingRainbow44 committed
33
public final class DispatchServer {
Jaida Wu's avatar
Jaida Wu committed
34
35
	public static String query_region_list = "";
	public static String query_cur_region = "";
36

Melledy's avatar
Melledy committed
37
	private final Gson gson;
38
	private final String defaultServerName = "os_usa";
39

Melledy's avatar
Melledy committed
40
	public String regionListBase64;
41
	public Map<String, RegionData> regions;
42
	private Express httpServer;
43

Melledy's avatar
Melledy committed
44
	public DispatchServer() {
45
		this.regions = new HashMap<>();
Melledy's avatar
Melledy committed
46
		this.gson = new GsonBuilder().create();
47

Melledy's avatar
Melledy committed
48
49
50
		this.loadQueries();
		this.initRegion();
	}
51

52
53
	public Express getServer() {
		return httpServer;
Melledy's avatar
Melledy committed
54
	}
55

56
57
58
59
60
61
	public void setHttpServer(Express httpServer) {
		this.httpServer.stop();
		this.httpServer = httpServer;
		this.httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port);
	}

Melledy's avatar
Melledy committed
62
63
64
65
	public Gson getGsonFactory() {
		return gson;
	}

66
67
	public QueryCurrRegionHttpRsp getCurrRegion() {
		// Needs to be fixed by having the game servers connect to the dispatch server.
68
		if (Grasscutter.getConfig().RunMode == ServerRunMode.HYBRID) {
69
			return regions.get(defaultServerName).parsedRegionQuery;
70
71
		}

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

Melledy's avatar
Melledy committed
76
77
	public void loadQueries() {
		File file;
78

Melledy's avatar
Melledy committed
79
80
81
82
		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
83
			Grasscutter.getLogger().warn("[Dispatch] query_region_list not found! Using default region list.");
Melledy's avatar
Melledy committed
84
		}
85

Melledy's avatar
Melledy committed
86
87
88
89
		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
90
			Grasscutter.getLogger().warn("[Dispatch] query_cur_region not found! Using default current region.");
Melledy's avatar
Melledy committed
91
92
93
94
95
96
97
		}
	}

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

Melledy's avatar
Melledy committed
99
100
			byte[] decoded2 = Base64.getDecoder().decode(query_cur_region);
			QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(decoded2);
101

KingRainbow44's avatar
KingRainbow44 committed
102
103
			List<RegionSimpleInfo> servers = new ArrayList<>();
			List<String> usedNames = new ArrayList<>(); // List to check for potential naming conflicts
104
			if (Grasscutter.getConfig().RunMode == ServerRunMode.HYBRID) { // Automatically add the game server if in
105
																				// hybrid mode
106
107
108
109
				RegionSimpleInfo server = RegionSimpleInfo.newBuilder()
						.setName("os_usa")
						.setTitle(Grasscutter.getConfig().getGameServerOptions().Name)
						.setType("DEV_PUBLIC")
110
111
112
113
114
115
116
117
118
						.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)
119
										+ "/query_cur_region/" + defaultServerName)
120
121
122
123
124
						.build();
				usedNames.add(defaultServerName);
				servers.add(server);

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

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

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

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

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

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

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

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

	public void start() throws Exception {
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
225
226
227
		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);
228
					}
229
230
				} else {
					serverConnector = new ServerConnector(server);
231
				}
232
233
234
235
236
237
238

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

			config.enforceSsl = Grasscutter.getConfig().getDispatchOptions().UseSSL;
239
			if(Grasscutter.getConfig().DebugMode == ServerDebugMode.ALL) {
240
				config.enableDevLogging();
241
			}
242
		});
Jaida Wu's avatar
Jaida Wu committed
243

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

246
		httpServer.raw().error(404, ctx -> {
247
			if(Grasscutter.getConfig().DebugMode == ServerDebugMode.MISSING) {
Benjamin Elsdon's avatar
Benjamin Elsdon committed
248
				Grasscutter.getLogger().info(String.format("[Dispatch] Potential unhandled %s request: %s", ctx.method(), ctx.url()));
249
250
251
252
			}
			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.
		});
253

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

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

265
266
267
268
269
270
271
272
273
274
		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;
			}
275

276
277
278
279
280
281
282
			// Invoke event.
			QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(response); event.call();
			// Respond with event result.
			res.send(event.getRegionInfo());
		});

		// Login
283

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

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

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

KingRainbow44's avatar
KingRainbow44 committed
302
303
			// Login
			Account account = DatabaseHelper.getAccountByName(requestData.account);
304

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

314
315
316
317
					for (String permission : Grasscutter.getConfig().getDispatchOptions().defaultPermissions) {
						account.addPermission(permission);
					}

Jaida Wu's avatar
Jaida Wu committed
318
319
320
321
322
					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
323

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

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

338
					Grasscutter.getLogger().info(String
339
							.format("[Dispatch] Client %s failed to log in: Account no found", req.ip()));
340
				}
KingRainbow44's avatar
KingRainbow44 committed
341
			} else {
342
				// Account was found, log the player in
KingRainbow44's avatar
KingRainbow44 committed
343
344
345
346
				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
347

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

352
			res.send(responseData);
Melledy's avatar
Melledy committed
353
		});
354

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

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

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

KingRainbow44's avatar
KingRainbow44 committed
375
376
377
378
			// 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
379

380
				Grasscutter.getLogger()
381
						.info(String.format("[Dispatch] Client %s failed to log in via token", req.ip()));
KingRainbow44's avatar
KingRainbow44 committed
382
383
384
385
386
			} else {
				responseData.message = "OK";
				responseData.data.account.uid = requestData.uid;
				responseData.data.account.token = requestData.token;
				responseData.data.account.email = account.getEmail();
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
444
		// this could be either GET or POST based on the observation of different clients
		httpServer.all("/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\":\"\"}}}"));
445

Melledy's avatar
Melledy committed
446
		// Game data
447
448
449
450
451
452
453
454
455
456
457
		// 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
458
		// Captcha
459
460
461
		// 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}}"));

462
		// Config
463
464
465
466
467
		// 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
468
		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
469
		// Test api?
470
		// abtest-api-data-sg.hoyoverse.com
Benjamin Elsdon's avatar
Benjamin Elsdon committed
471
		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\"}}]}"));
472
473
474
475

		// log-upload-os.mihoyo.com
		httpServer.all("/log/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}"));
		httpServer.all("/sdk/upload", new DispatchHttpJsonHandler("{\"code\":0}"));
476
		httpServer.post("/sdk/dataUpload", new DispatchHttpJsonHandler("{\"code\":0}"));
477
478
		// /perf/config/verify?device_id=xxx&platform=x&name=xxx
		httpServer.all("/perf/config/verify", new DispatchHttpJsonHandler("{\"code\":0}"));
479

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

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

489
		httpServer.get("/gacha", new GachaRecordHandler());
Jaida Wu's avatar
Jaida Wu committed
490

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

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

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

			if (next > last) {
				int eqPos = qs.indexOf('=', last);
510
511
512
513
514
515
516
517
518
				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
519
520
521
522
523
				}
			}
			last = next + 1;
		}
		return result;
Melledy's avatar
Melledy committed
524
	}
525
526
527
528
529
530
531
532
533

	public static class RegionData {
		QueryCurrRegionHttpRsp parsedRegionQuery;
		String Base64;

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

		public QueryCurrRegionHttpRsp getParsedRegionQuery() {
			return parsedRegionQuery;
		}

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