Merge development into plugin-auth

This commit is contained in:
KingRainbow44
2022-05-14 12:08:33 -04:00
111 changed files with 4188 additions and 1219 deletions

View File

@@ -5,10 +5,13 @@ import com.google.protobuf.InvalidProtocolBufferException;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerRunMode;
import emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.*;
import emu.grasscutter.net.proto.RegionInfoOuterClass;
import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo;
import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo;
import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent;
import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import express.Express;
@@ -30,45 +33,24 @@ import static emu.grasscutter.net.proto.QueryRegionListHttpRspOuterClass.*;
* Handles requests related to region queries.
*/
public final class RegionHandler implements Router {
private String regionQuery = "";
private String regionList = "";
private static final Map<String, RegionData> regions = new ConcurrentHashMap<>();
private static String regionListResponse;
public RegionHandler() {
try { // Read & initialize region data.
this.readRegionData();
this.initialize();
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to initialize region data.", exception);
}
}
/**
* Loads initial region data.
*/
private void readRegionData() {
File file;
file = new File(DATA("query_region_list.txt"));
if (file.exists())
this.regionList = new String(FileUtils.read(file));
else Grasscutter.getLogger().error("[Dispatch] 'query_region_list' not found!");
file = new File(DATA("query_cur_region.txt"));
if (file.exists())
regionQuery = new String(FileUtils.read(file));
else Grasscutter.getLogger().warn("[Dispatch] 'query_cur_region' not found!");
}
/**
* Configures region data according to configuration.
*/
private void initialize() throws InvalidProtocolBufferException {
// Decode the initial region query.
byte[] queryBase64 = Base64.getDecoder().decode(this.regionQuery);
QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(queryBase64);
private void initialize() {
String dispatchDomain = "http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://"
+ lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":"
+ lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort);
// Create regions.
List<RegionSimpleInfo> servers = new ArrayList<>();
@@ -87,37 +69,33 @@ public final class RegionHandler implements Router {
Grasscutter.getLogger().error("Region name already in use.");
return;
}
// Create a region identifier.
var identifier = RegionSimpleInfo.newBuilder()
.setName(region.Name).setTitle(region.Title)
.setType("DEV_PUBLIC").setDispatchUrl(
"http" + (HTTP_ENCRYPTION.useInRouting ? "s" : "") + "://"
+ lr(HTTP_INFO.accessAddress, HTTP_INFO.bindAddress) + ":"
+ lr(HTTP_INFO.accessPort, HTTP_INFO.bindPort)
+ "/query_cur_region/" + region.Name)
.setName(region.Name).setTitle(region.Title).setType("DEV_PUBLIC")
.setDispatchUrl(dispatchDomain + "/query_cur_region/" + region.Name)
.build();
usedNames.add(region.Name); servers.add(identifier);
// Create a region info object.
var regionInfo = regionQuery.getRegionInfo().toBuilder()
var regionInfo = RegionInfo.newBuilder()
.setGateserverIp(region.Ip).setGateserverPort(region.Port)
.setSecretKey(ByteString.copyFrom(FileUtils.read(KEYS_FOLDER + "/dispatchSeed.bin")))
.setSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
.build();
// Create an updated region query.
var updatedQuery = regionQuery.toBuilder().setRegionInfo(regionInfo).build();
var updatedQuery = QueryCurrRegionHttpRsp.newBuilder().setRegionInfo(regionInfo).build();
regions.put(region.Name, new RegionData(updatedQuery, Utils.base64Encode(updatedQuery.toByteString().toByteArray())));
});
// Decode the initial region list.
byte[] listBase64 = Base64.getDecoder().decode(this.regionList);
QueryRegionListHttpRsp regionList = QueryRegionListHttpRsp.parseFrom(listBase64);
// Create a config object.
byte[] customConfig = "{\"sdkenv\":\"2\",\"checkdevice\":\"false\",\"loadPatch\":\"false\",\"showexception\":\"false\",\"regionConfig\":\"pm|fk|add\",\"downloadMode\":\"0\"}".getBytes();
Crypto.xor(customConfig, Crypto.DISPATCH_KEY); // XOR the config with the key.
// Create an updated region list.
QueryRegionListHttpRsp updatedRegionList = QueryRegionListHttpRsp.newBuilder()
.addAllRegionList(servers)
.setClientSecretKey(regionList.getClientSecretKey())
.setClientCustomConfigEncrypted(regionList.getClientCustomConfigEncrypted())
.setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
.setClientCustomConfigEncrypted(ByteString.copyFrom(customConfig))
.setEnableLoginPc(true).build();
// Set the region list response.

View File

@@ -3,6 +3,8 @@ package emu.grasscutter.server.http.handlers;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.http.objects.HttpJsonResponse;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import express.Express;
import express.http.Request;
import express.http.Response;
@@ -11,6 +13,7 @@ import io.javalin.Javalin;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Objects;
import static emu.grasscutter.Configuration.DATA;
@@ -19,6 +22,18 @@ import static emu.grasscutter.Configuration.DATA;
* Handles requests related to the announcements page.
*/
public final class AnnouncementsHandler implements Router {
private static String template, swjs, vue;
public AnnouncementsHandler() {
var templateFile = new File(Utils.toFilePath(DATA("/hk4e/announcement/index.html")));
var swjsFile = new File(Utils.toFilePath(DATA("/hk4e/announcement/sw.js")));
var vueFile = new File(Utils.toFilePath(DATA("/hk4e/announcement/vue.min.js")));
template = templateFile.exists() ? new String(FileUtils.read(template)) : null;
swjs = swjsFile.exists() ? new String(FileUtils.read(swjs)) : null;
vue = vueFile.exists() ? new String(FileUtils.read(vueFile)) : null;
}
@Override public void applyRoutes(Express express, Javalin handle) {
// hk4e-api-os.hoyoverse.com
express.all("/common/hk4e_global/announcement/api/getAlertPic", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}"));
@@ -30,14 +45,45 @@ public final class AnnouncementsHandler implements Router {
express.all("/common/hk4e_global/announcement/api/getAnnContent", AnnouncementsHandler::getAnnouncement);
// hk4e-sdk-os.hoyoverse.com
express.all("/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}"));
express.get("/hk4e/announcement/*", AnnouncementsHandler::getPageResources);
express.get("/sw.js", AnnouncementsHandler::getPageResources);
express.get("/dora/lib/vue/2.6.11/vue.min.js", AnnouncementsHandler::getPageResources);
}
private static void getAnnouncement(Request request, Response response) {
if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnContent")) {
response.send("{\"retcode\":0,\"message\":\"OK\",\"data\":" + readToString(new File(DATA("GameAnnouncement.json"))) +"}");
String data = readToString(Paths.get(DATA("GameAnnouncement.json")).toFile());
response.send("{\"retcode\":0,\"message\":\"OK\",\"data\":" + data + "}");
} else if (Objects.equals(request.baseUrl(), "/common/hk4e_global/announcement/api/getAnnList")) {
String data = readToString(new File(DATA("GameAnnouncementList.json"))).replace("System.currentTimeMillis()",String.valueOf(System.currentTimeMillis()));
response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": "+data +"}");
String data = readToString(Paths.get(DATA("GameAnnouncementList.json")).toFile())
.replace("System.currentTimeMillis()", String.valueOf(System.currentTimeMillis()));
response.send("{\"retcode\":0,\"message\":\"OK\",\"data\": " + data + "}");
}
}
private static void getPageResources(Request request, Response response) {
var path = request.path();
switch(path) {
case "/sw.js" -> response.send(swjs);
case "/hk4e/announcement/index.html" -> response.send(template);
case "/dora/lib/vue/2.6.11/vue.min.js" -> response.send(vue);
default -> {
File renderFile = new File(Utils.toFilePath(DATA(path)));
if(!renderFile.exists()) {
Grasscutter.getLogger().info("File not exist: " + path);
return;
}
String ext = path.substring(path.lastIndexOf(".") + 1);
if ("css".equals(ext)) {
response.type("text/css");
response.send(FileUtils.read(renderFile));
} else {
response.send(FileUtils.read(renderFile));
}
}
}
}

View File

@@ -2,6 +2,10 @@ package emu.grasscutter.server.http.handlers;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.gacha.GachaBanner;
import emu.grasscutter.game.gacha.GachaManager;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.FileUtils;
@@ -13,8 +17,12 @@ import io.javalin.Javalin;
import io.javalin.http.staticfiles.Location;
import java.io.File;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import static emu.grasscutter.Configuration.DATA;
import static emu.grasscutter.utils.Language.translate;
/**
* Handles all gacha-related HTTP requests.
@@ -22,7 +30,8 @@ import static emu.grasscutter.Configuration.DATA;
public final class GachaHandler implements Router {
private final String gachaMappings;
private static String frontendTemplate = "{{REPLACE_RECORD}}";
private static String recordsTemplate = "";
private static String detailsTemplate = "";
public GachaHandler() {
this.gachaMappings = Utils.toFilePath(DATA("/gacha_mappings.js"));
@@ -35,12 +44,15 @@ public final class GachaHandler implements Router {
}
var templateFile = new File(DATA("/gacha_records.html"));
if(templateFile.exists())
frontendTemplate = new String(FileUtils.read(templateFile));
recordsTemplate = templateFile.exists() ? new String(FileUtils.read(templateFile)) : "{{REPLACE_RECORD}}";
templateFile = new File(Utils.toFilePath(DATA("/gacha_details.html")));
detailsTemplate = templateFile.exists() ? new String(FileUtils.read(templateFile)) : null;
}
@Override public void applyRoutes(Express express, Javalin handle) {
express.get("/gacha", GachaHandler::gachaRecords);
express.get("/gacha/details", GachaHandler::gachaDetails);
express.useStaticFallback("/gacha/mappings", this.gachaMappings, Location.EXTERNAL);
}
@@ -63,9 +75,62 @@ public final class GachaHandler implements Router {
String records = DatabaseHelper.getGachaRecords(account.getPlayerUid(), gachaType, page).toString();
long maxPage = DatabaseHelper.getGachaRecordsMaxPage(account.getPlayerUid(), page, gachaType);
response.send(frontendTemplate
response.send(recordsTemplate
.replace("{{REPLACE_RECORD}}", records)
.replace("{{REPLACE_MAXPAGE}}", String.valueOf(maxPage)));
}
}
private static void gachaDetails(Request request, Response response) {
String template = detailsTemplate;
// Get player info (for langauge).
String sessionKey = request.query("s");
Account account = DatabaseHelper.getAccountBySessionKey(sessionKey);
Player player = Grasscutter.getGameServer().getPlayerByUid(account.getPlayerUid());
// If the template was not loaded, return an error.
if (detailsTemplate == null) {
response.send(translate(player, "gacha.details.template_missing"));
return;
}
// Add translated title etc. to the page.
template = template.replace("{{TITLE}}", translate(player, "gacha.details.title"))
.replace("{{AVAILABLE_FIVE_STARS}}", translate(player, "gacha.details.available_five_stars"))
.replace("{{AVAILABLE_FOUR_STARS}}", translate(player, "gacha.details.available_four_stars"))
.replace("{{AVAILABLE_THREE_STARS}}", translate(player, "gacha.details.available_three_stars"))
.replace("{{LANGUAGE}}", Utils.getLanguageCode(account.getLocale()));
// Get the banner info for the banner we want.
int gachaType = Integer.parseInt(request.query("gachaType"));
GachaManager manager = Grasscutter.getGameServer().getGachaManager();
GachaBanner banner = manager.getGachaBanners().get(gachaType);
// Add 5-star items.
Set<String> fiveStarItems = new LinkedHashSet<>();
Arrays.stream(banner.getRateUpItems5()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
Arrays.stream(banner.getFallbackItems5Pool1()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
Arrays.stream(banner.getFallbackItems5Pool2()).forEach(i -> fiveStarItems.add(Integer.toString(i)));
template = template.replace("{{FIVE_STARS}}", "[" + String.join(",", fiveStarItems) + "]");
// Add 4-star items.
Set<String> fourStarItems = new LinkedHashSet<>();
Arrays.stream(banner.getRateUpItems4()).forEach(i -> fourStarItems.add(Integer.toString(i)));
Arrays.stream(banner.getFallbackItems4Pool1()).forEach(i -> fourStarItems.add(Integer.toString(i)));
Arrays.stream(banner.getFallbackItems4Pool2()).forEach(i -> fourStarItems.add(Integer.toString(i)));
template = template.replace("{{FOUR_STARS}}", "[" + String.join(",", fourStarItems) + "]");
// Add 3-star items.
Set<String> threeStarItems = new LinkedHashSet<>();
Arrays.stream(banner.getFallbackItems3()).forEach(i -> threeStarItems.add(Integer.toString(i)));
template = template.replace("{{THREE_STARS}}", "[" + String.join(",", threeStarItems) + "]");
// Done.
response.send(template);
}
}