DispatchServer.java 22.2 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;
17
18
import emu.grasscutter.server.dispatch.authentication.AuthenticationHandler;
import emu.grasscutter.server.dispatch.authentication.DefaultAuthenticationHandler;
Jaida Wu's avatar
Jaida Wu committed
19
import emu.grasscutter.server.dispatch.json.*;
Melledy's avatar
Melledy committed
20
import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData;
KingRainbow44's avatar
KingRainbow44 committed
21
22
import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent;
import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent;
23
import emu.grasscutter.server.http.gacha.GachaRecordHandler;
24
import emu.grasscutter.server.http.gcstatic.StaticFileHandler;
Melledy's avatar
Melledy committed
25
import emu.grasscutter.utils.FileUtils;
26
27
28
29
30
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
31

Jaida Wu's avatar
Jaida Wu committed
32
33
import java.io.*;
import java.net.URLDecoder;
34
import java.util.*;
Melledy's avatar
Melledy committed
35

KingRainbow44's avatar
KingRainbow44 committed
36
public final class DispatchServer {
Jaida Wu's avatar
Jaida Wu committed
37
38
	public static String query_region_list = "";
	public static String query_cur_region = "";
39

Melledy's avatar
Melledy committed
40
	private final Gson gson;
41
	private final String defaultServerName = "os_usa";
42

Melledy's avatar
Melledy committed
43
	public String regionListBase64;
44
	public Map<String, RegionData> regions;
45
	private AuthenticationHandler authHandler;
46
	private Express httpServer;
47

Melledy's avatar
Melledy committed
48
	public DispatchServer() {
49
		this.regions = new HashMap<>();
Melledy's avatar
Melledy committed
50
		this.gson = new GsonBuilder().create();
51

Melledy's avatar
Melledy committed
52
53
54
		this.loadQueries();
		this.initRegion();
	}
55

56
57
	public Express getServer() {
		return httpServer;
Melledy's avatar
Melledy committed
58
	}
59

60
61
62
63
64
65
	public void setHttpServer(Express httpServer) {
		this.httpServer.stop();
		this.httpServer = httpServer;
		this.httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port);
	}

Melledy's avatar
Melledy committed
66
67
68
69
	public Gson getGsonFactory() {
		return gson;
	}

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

76
		Grasscutter.getLogger().warn("[Dispatch] Unsupported run mode for getCurrRegion()");
77
		return null;
Melledy's avatar
Melledy committed
78
	}
79

Melledy's avatar
Melledy committed
80
81
	public void loadQueries() {
		File file;
82

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

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

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

Melledy's avatar
Melledy committed
103
104
			byte[] decoded2 = Base64.getDecoder().decode(query_cur_region);
			QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(decoded2);
105

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

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

				QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(serverRegion).build();
140
141
				regions.put(defaultServerName, new RegionData(parsedRegionQuery,
						Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray())));
142
143

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

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

				RegionInfo serverRegion = regionQuery.getRegionInfo().toBuilder()
174
175
						.setGateserverIp(regionInfo.Ip)
						.setGateserverPort(regionInfo.Port)
176
177
						.setSecretKey(ByteString
								.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin")))
178
179
180
						.build();

				QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(serverRegion).build();
181
182
				regions.put(regionInfo.Name, new RegionData(parsedRegionQuery,
						Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray())));
183
184
			}

Melledy's avatar
Melledy committed
185
			QueryRegionListHttpRsp regionList = QueryRegionListHttpRsp.newBuilder()
186
					.addAllRegionList(servers)
187
188
189
190
					.setClientSecretKey(rl.getClientSecretKey())
					.setClientCustomConfigEncrypted(rl.getClientCustomConfigEncrypted())
					.setEnableLoginPc(true)
					.build();
Melledy's avatar
Melledy committed
191
192
193

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

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

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

			config.enforceSsl = Grasscutter.getConfig().getDispatchOptions().UseSSL;
243
			if(Grasscutter.getConfig().DebugMode == ServerDebugMode.ALL) {
244
				config.enableDevLogging();
245
			}
246
		});
Jaida Wu's avatar
Jaida Wu committed
247

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

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

258
259
260
261
262
263
264
265
266
267
		// Authentication Handler
		// These routes are so that authentication routes are always the same no matter what auth system is used.
		httpServer.get("/authentication/type", (req, res) -> {
			res.send(this.getAuthHandler().getClass().getName());
		});

		httpServer.post("/authentication/login", (req, res) -> this.getAuthHandler().handleLogin(req, res));
		httpServer.post("/authentication/register", (req, res) -> this.getAuthHandler().handleRegister(req, res));
		httpServer.post("/authentication/change_password", (req, res) -> this.getAuthHandler().handleChangePassword(req, res));

Melledy's avatar
Melledy committed
268
		// Dispatch
269
		httpServer.get("/query_region_list", (req, res) -> {
KingRainbow44's avatar
KingRainbow44 committed
270
			// Log
271
			Grasscutter.getLogger().info(String.format("[Dispatch] Client %s request: query_region_list", req.ip()));
Jaida Wu's avatar
Jaida Wu committed
272

273
274
275
			// Invoke event.
			QueryAllRegionsEvent event = new QueryAllRegionsEvent(regionListBase64); event.call();
			// Respond with event result.
276
			res.send(event.getRegionList());
Melledy's avatar
Melledy committed
277
		});
278

279
280
281
282
283
284
285
286
287
288
		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;
			}
289

290
291
292
293
294
295
296
			// Invoke event.
			QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(response); event.call();
			// Respond with event result.
			res.send(event.getRegionInfo());
		});

		// Login
297

298
		httpServer.post("/hk4e_global/mdk/shield/api/login", (req, res) -> {
KingRainbow44's avatar
KingRainbow44 committed
299
300
301
			// Get post data
			LoginAccountRequestJson requestData = null;
			try {
302
				String body = req.ctx().body();
KingRainbow44's avatar
KingRainbow44 committed
303
				requestData = getGsonFactory().fromJson(body, LoginAccountRequestJson.class);
304
			} catch (Exception ignored) { }
305

KingRainbow44's avatar
KingRainbow44 committed
306
307
308
309
			// Create response json
			if (requestData == null) {
				return;
			}
310
			Grasscutter.getLogger().info(String.format("[Dispatch] Client %s is trying to log in", req.ip()));
311

312
			res.send(authHandler.handleGameLogin(req, requestData));
Melledy's avatar
Melledy committed
313
		});
314

Melledy's avatar
Melledy committed
315
		// Login via token
316
		httpServer.post("/hk4e_global/mdk/shield/api/verify", (req, res) -> {
KingRainbow44's avatar
KingRainbow44 committed
317
318
319
			// Get post data
			LoginTokenRequestJson requestData = null;
			try {
320
				String body = req.ctx().body();
KingRainbow44's avatar
KingRainbow44 committed
321
				requestData = getGsonFactory().fromJson(body, LoginTokenRequestJson.class);
322
323
			} catch (Exception ignored) {
			}
324

KingRainbow44's avatar
KingRainbow44 committed
325
326
327
328
329
			// Create response json
			if (requestData == null) {
				return;
			}
			LoginResultJson responseData = new LoginResultJson();
330
			Grasscutter.getLogger().info(String.format("[Dispatch] Client %s is trying to log in via token", req.ip()));
KingRainbow44's avatar
KingRainbow44 committed
331
332
333

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

KingRainbow44's avatar
KingRainbow44 committed
335
336
337
338
			// 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
339

340
				Grasscutter.getLogger()
341
						.info(String.format("[Dispatch] Client %s failed to log in via token", req.ip()));
KingRainbow44's avatar
KingRainbow44 committed
342
343
344
345
346
			} 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
347

348
				Grasscutter.getLogger().info(String.format("[Dispatch] Client %s logged in via token as %s",
349
						req.ip(), 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
		// Exchange for combo token
356
		httpServer.post("/hk4e_global/combo/granter/login/v2/login", (req, res) -> {
KingRainbow44's avatar
KingRainbow44 committed
357
358
359
			// Get post data
			ComboTokenReqJson requestData = null;
			try {
360
				String body = req.ctx().body();
KingRainbow44's avatar
KingRainbow44 committed
361
				requestData = getGsonFactory().fromJson(body, ComboTokenReqJson.class);
362
363
			} catch (Exception ignored) {
			}
364

KingRainbow44's avatar
KingRainbow44 committed
365
366
367
368
			// Create response json
			if (requestData == null || requestData.data == null) {
				return;
			}
369
			LoginTokenData loginData = getGsonFactory().fromJson(requestData.data, LoginTokenData.class); // Get login
370
			// data
KingRainbow44's avatar
KingRainbow44 committed
371
372
373
374
			ComboTokenResJson responseData = new ComboTokenResJson();

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

KingRainbow44's avatar
KingRainbow44 committed
376
377
378
379
			// Test
			if (account == null || !account.getSessionKey().equals(loginData.token)) {
				responseData.retcode = -201;
				responseData.message = "Wrong session key.";
Jaida Wu's avatar
Jaida Wu committed
380

381
				Grasscutter.getLogger().info(
382
						String.format("[Dispatch] Client %s failed to exchange combo token", req.ip()));
KingRainbow44's avatar
KingRainbow44 committed
383
384
385
386
387
			} 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
388

389
				Grasscutter.getLogger().info(
390
						String.format("[Dispatch] Client %s succeed to exchange combo token", req.ip()));
KingRainbow44's avatar
KingRainbow44 committed
391
			}
Jaida Wu's avatar
Jaida Wu committed
392

393
			res.send(responseData);
Melledy's avatar
Melledy committed
394
		});
395
396
397
398

		// 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
399
		// Agreement and Protocol
400
401
402
		// 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
403
404
		// 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\":\"\"}}}"));
405

Melledy's avatar
Melledy committed
406
		// Game data
407
408
409
410
411
		// 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
BaiSugar's avatar
BaiSugar committed
412
		httpServer.all("/common/hk4e_global/announcement/api/getAnnList", new AnnouncementHandler());
413
		// hk4e-api-os-static.hoyoverse.com
BaiSugar's avatar
BaiSugar committed
414
		httpServer.all("/common/hk4e_global/announcement/api/getAnnContent", new AnnouncementHandler());
415
416
417
		// 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
418
		// Captcha
419
420
421
		// 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}}"));

422
		// Config
423
424
425
426
427
		// 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
428
		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
429
		// Test api?
430
		// abtest-api-data-sg.hoyoverse.com
Benjamin Elsdon's avatar
Benjamin Elsdon committed
431
		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\"}}]}"));
432
433
434
435

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

Melledy's avatar
Melledy committed
440
		// Logging servers
441
		// overseauspider.yuanshen.com
442
		httpServer.all("/log", new ClientLogHandler());
443
		// log-upload-os.mihoyo.com
444
		httpServer.all("/crash/dataUpload", new ClientLogHandler());
Jaida Wu's avatar
Jaida Wu committed
445

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

449
		// gacha record
450
		httpServer.get("/gacha", new GachaRecordHandler());
Jaida Wu's avatar
Jaida Wu committed
451

452
453
454
		// static file provider
		httpServer.get("/gcstatic/*", new StaticFileHandler());

455
456
		httpServer.listen(Grasscutter.getConfig().getDispatchOptions().Port);
		Grasscutter.getLogger().info("[Dispatch] Dispatch server started on port " + httpServer.raw().port());
Melledy's avatar
Melledy committed
457
	}
458

Melledy's avatar
Melledy committed
459
	private Map<String, String> parseQueryString(String qs) {
Jaida Wu's avatar
Jaida Wu committed
460
		Map<String, String> result = new HashMap<>();
Jaida Wu's avatar
Jaida Wu committed
461
		if (qs == null) {
Jaida Wu's avatar
Jaida Wu committed
462
			return result;
Jaida Wu's avatar
Jaida Wu committed
463
		}
Jaida Wu's avatar
Jaida Wu committed
464
465
466
467

		int last = 0, next, l = qs.length();
		while (last < l) {
			next = qs.indexOf('&', last);
Jaida Wu's avatar
Jaida Wu committed
468
			if (next == -1) {
Jaida Wu's avatar
Jaida Wu committed
469
				next = l;
Jaida Wu's avatar
Jaida Wu committed
470
			}
Jaida Wu's avatar
Jaida Wu committed
471
472
473

			if (next > last) {
				int eqPos = qs.indexOf('=', last);
474
475
476
477
478
479
480
481
482
				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
483
484
485
486
487
				}
			}
			last = next + 1;
		}
		return result;
Melledy's avatar
Melledy committed
488
	}
489

490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
	public AuthenticationHandler getAuthHandler() {
		if(authHandler == null) {
			return new DefaultAuthenticationHandler();
		}
		Grasscutter.getLogger().info(authHandler.getClass().getName());

		return authHandler;
	}

	public boolean registerAuthHandler(AuthenticationHandler authHandler) {
		if(this.authHandler != null) {
			Grasscutter.getLogger().error(String.format("[Dispatch] Unable to register '%s' authentication handler. \n" +
					"The '%s' authentication handler has already been registered", authHandler.getClass().getName(), this.authHandler.getClass().getName()));
			return false;
		}
		this.authHandler = authHandler;
		return true;
	}

	public void resetAuthHandler() {
		this.authHandler = null;
	}

513
514
515
516
517
518
519
520
	public static class RegionData {
		QueryCurrRegionHttpRsp parsedRegionQuery;
		String Base64;

		public RegionData(QueryCurrRegionHttpRsp prq, String b64) {
			this.parsedRegionQuery = prq;
			this.Base64 = b64;
		}
521
522
523
524
525
526
527
528

		public QueryCurrRegionHttpRsp getParsedRegionQuery() {
			return parsedRegionQuery;
		}

		public String getBase64() {
			return Base64;
		}
529
	}
Melledy's avatar
Melledy committed
530
}