DispatchServer.java 22.3 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 HashMap<String, RegionData> regions;
39
	private Express httpServer;
40

Melledy's avatar
Melledy committed
41
	public DispatchServer() {
42
		this.regions = new HashMap<String, RegionData>();
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();
Jaida Wu's avatar
Jaida Wu committed
384

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

389
			res.send(responseData);
Melledy's avatar
Melledy committed
390
		});
391

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

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

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

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

418
				Grasscutter.getLogger().info(
419
						String.format("[Dispatch] Client %s failed to exchange combo token", req.ip()));
KingRainbow44's avatar
KingRainbow44 committed
420
421
422
423
424
			} 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
425

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

430
			res.send(responseData);
Melledy's avatar
Melledy committed
431
		});
432
433
434
435

		// 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
436
		// Agreement and Protocol
437
438
439
440
441
		// 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
		httpServer.post("/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\":\"\"}}}"));

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

458
		// Config
459
460
461
462
463
		// 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
464
		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
465
		// Test api?
466
		// abtest-api-data-sg.hoyoverse.com
Benjamin Elsdon's avatar
Benjamin Elsdon committed
467
		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\"}}]}"));
468
469
470
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}"));
		httpServer.post("/sdk/dataUpload", new DispatchHttpJsonHandler("{\"code\":0}"));
		// /perf/config/verify?device_id=xxx&platform=x&name=xxx
		httpServer.all("/perf/config/verify", new DispatchHttpJsonHandler("{\"code\":0}"));
475

Melledy's avatar
Melledy committed
476
		// Logging servers
477
478
479
480
		// overseauspider.yuanshen.com
		httpServer.all("/log", new DispatchHttpJsonHandler("{\"code\":0}"));
		// log-upload-os.mihoyo.com
		httpServer.all("/crash/dataUpload", new DispatchHttpJsonHandler("{\"code\":0}"));
Jaida Wu's avatar
Jaida Wu committed
481

482
		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
483

484
485
		httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port);
		Grasscutter.getLogger().info("[Dispatch] Dispatch server started on port " + httpServer.raw().port());
Melledy's avatar
Melledy committed
486
	}
487

Melledy's avatar
Melledy committed
488
	private Map<String, String> parseQueryString(String qs) {
Jaida Wu's avatar
Jaida Wu committed
489
		Map<String, String> result = new HashMap<>();
Jaida Wu's avatar
Jaida Wu committed
490
		if (qs == null) {
Jaida Wu's avatar
Jaida Wu committed
491
			return result;
Jaida Wu's avatar
Jaida Wu committed
492
		}
Jaida Wu's avatar
Jaida Wu committed
493
494
495
496

		int last = 0, next, l = qs.length();
		while (last < l) {
			next = qs.indexOf('&', last);
Jaida Wu's avatar
Jaida Wu committed
497
			if (next == -1) {
Jaida Wu's avatar
Jaida Wu committed
498
				next = l;
Jaida Wu's avatar
Jaida Wu committed
499
			}
Jaida Wu's avatar
Jaida Wu committed
500
501
502

			if (next > last) {
				int eqPos = qs.indexOf('=', last);
503
504
505
506
507
508
509
510
511
				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
512
513
514
515
516
				}
			}
			last = next + 1;
		}
		return result;
Melledy's avatar
Melledy committed
517
	}
518
519
520
521
522
523
524
525
526
527

	public static class RegionData {
		QueryCurrRegionHttpRsp parsedRegionQuery;
		String Base64;

		public RegionData(QueryCurrRegionHttpRsp prq, String b64) {
			this.parsedRegionQuery = prq;
			this.Base64 = b64;
		}
	}
Melledy's avatar
Melledy committed
528
}