Commit 7925d1cd authored by Melledy's avatar Melledy
Browse files

Initial commit

parents
package emu.grasscutter.net.packet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class PacketWriter {
// Little endian
private final ByteArrayOutputStream baos;
public PacketWriter() {
this.baos = new ByteArrayOutputStream(128);
}
public byte[] build() {
return baos.toByteArray();
}
// Writers
public void writeEmpty(int i) {
while (i > 0) {
baos.write(0);
i--;
}
}
public void writeMax(int i) {
while (i > 0) {
baos.write(0xFF);
i--;
}
}
public void writeInt8(byte b) {
baos.write(b);
}
public void writeInt8(int i) {
baos.write((byte) i);
}
public void writeBoolean(boolean b) {
baos.write(b ? 1 : 0);
}
public void writeUint8(byte b) {
// Unsigned byte
baos.write(b & 0xFF);
}
public void writeUint8(int i) {
baos.write((byte) i & 0xFF);
}
public void writeUint16(int i) {
// Unsigned short
baos.write((byte) (i & 0xFF));
baos.write((byte) ((i >>> 8) & 0xFF));
}
public void writeUint24(int i) {
// 24 bit integer
baos.write((byte) (i & 0xFF));
baos.write((byte) ((i >>> 8) & 0xFF));
baos.write((byte) ((i >>> 16) & 0xFF));
}
public void writeInt16(int i) {
// Signed short
baos.write((byte) i);
baos.write((byte) (i >>> 8));
}
public void writeUint32(int i) {
// Unsigned int
baos.write((byte) (i & 0xFF));
baos.write((byte) ((i >>> 8) & 0xFF));
baos.write((byte) ((i >>> 16) & 0xFF));
baos.write((byte) ((i >>> 24) & 0xFF));
}
public void writeInt32(int i) {
// Signed int
baos.write((byte) i);
baos.write((byte) (i >>> 8));
baos.write((byte) (i >>> 16));
baos.write((byte) (i >>> 24));
}
public void writeUint32(long i) {
// Unsigned int (long)
baos.write((byte) (i & 0xFF));
baos.write((byte) ((i >>> 8) & 0xFF));
baos.write((byte) ((i >>> 16) & 0xFF));
baos.write((byte) ((i >>> 24) & 0xFF));
}
public void writeFloat(float f){
this.writeUint32(Float.floatToRawIntBits(f));
}
public void writeUint64(long l) {
baos.write((byte) (l & 0xFF));
baos.write((byte) ((l >>> 8) & 0xFF));
baos.write((byte) ((l >>> 16) & 0xFF));
baos.write((byte) ((l >>> 24) & 0xFF));
baos.write((byte) ((l >>> 32) & 0xFF));
baos.write((byte) ((l >>> 40) & 0xFF));
baos.write((byte) ((l >>> 48) & 0xFF));
baos.write((byte) ((l >>> 56) & 0xFF));
}
public void writeDouble(double d){
long l = Double.doubleToLongBits(d);
this.writeUint64(l);
}
public void writeString16(String s) {
if (s == null) {
this.writeUint16(0);
return;
}
this.writeUint16(s.length() * 2);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
this.writeUint16((short) c);
}
}
public void writeString8(String s) {
if (s == null) {
this.writeUint16(0);
return;
}
this.writeUint16(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
this.writeUint8((byte) c);
}
}
public void writeDirectString8(String s, int expectedSize) {
if (s == null) {
return;
}
for (int i = 0; i < expectedSize; i++) {
char c = i < s.length() ? s.charAt(i) : 0;
this.writeUint8((byte) c);
}
}
public void writeBytes(byte[] bytes) {
try {
baos.write(bytes);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void writeBytes(int[] bytes) {
byte[] b = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++)
b[i] = (byte)bytes[i];
try {
baos.write(b);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
package emu.grasscutter.net.packet;
public class Retcode {
public static final int SUCCESS = 0;
public static final int FAIL = 1;
}
package emu.grasscutter.netty;
import emu.grasscutter.Grasscutter;
import io.jpower.kcp.netty.UkcpChannel;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public abstract class MihoyoKcpChannel extends ChannelInboundHandlerAdapter {
private UkcpChannel kcpChannel;
private ChannelHandlerContext ctx;
private boolean isActive;
public UkcpChannel getChannel() {
return kcpChannel;
}
public boolean isActive() {
return this.isActive;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
this.kcpChannel = (UkcpChannel) ctx.channel();
this.ctx = ctx;
this.isActive = true;
this.onConnect();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
this.isActive = false;
this.onDisconnect();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf data = (ByteBuf) msg;
onMessage(ctx, data);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
close();
}
protected void send(byte[] data) {
if (!isActive()) {
return;
}
ByteBuf packet = Unpooled.wrappedBuffer(data);
kcpChannel.writeAndFlush(packet);
}
public void close() {
if (getChannel() != null) {
getChannel().close();
}
}
/*
protected void logPacket(ByteBuffer buf) {
ByteBuf b = Unpooled.wrappedBuffer(buf.array());
logPacket(b);
}
*/
protected void logPacket(ByteBuf buf) {
Grasscutter.getLogger().info("Received: \n" + ByteBufUtil.prettyHexDump(buf));
}
// Events
protected abstract void onConnect();
protected abstract void onDisconnect();
public abstract void onMessage(ChannelHandlerContext ctx, ByteBuf data);
}
package emu.grasscutter.netty;
import java.net.SocketAddress;
import java.nio.channels.SelectableChannel;
import java.util.List;
import io.netty.channel.Channel;
import io.netty.channel.ChannelConfig;
import io.netty.channel.ChannelMetadata;
import io.netty.channel.ChannelOutboundBuffer;
import io.netty.channel.nio.AbstractNioMessageChannel;
public class MihoyoKcpHandshaker extends AbstractNioMessageChannel {
protected MihoyoKcpHandshaker(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent, ch, readInterestOp);
}
@Override
public ChannelConfig config() {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean isActive() {
// TODO Auto-generated method stub
return false;
}
@Override
public ChannelMetadata metadata() {
// TODO Auto-generated method stub
return null;
}
@Override
protected int doReadMessages(List<Object> buf) throws Exception {
// TODO Auto-generated method stub
return 0;
}
@Override
protected boolean doWriteMessage(Object msg, ChannelOutboundBuffer in) throws Exception {
// TODO Auto-generated method stub
return false;
}
@Override
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
// TODO Auto-generated method stub
return false;
}
@Override
protected void doFinishConnect() throws Exception {
// TODO Auto-generated method stub
}
@Override
protected SocketAddress localAddress0() {
// TODO Auto-generated method stub
return null;
}
@Override
protected SocketAddress remoteAddress0() {
// TODO Auto-generated method stub
return null;
}
@Override
protected void doBind(SocketAddress localAddress) throws Exception {
// TODO Auto-generated method stub
}
@Override
protected void doDisconnect() throws Exception {
// TODO Auto-generated method stub
}
}
package emu.grasscutter.netty;
import java.net.InetSocketAddress;
import emu.grasscutter.Grasscutter;
import io.jpower.kcp.netty.ChannelOptionHelper;
import io.jpower.kcp.netty.UkcpChannelOption;
import io.jpower.kcp.netty.UkcpServerChannel;
import io.netty.bootstrap.UkcpServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
@SuppressWarnings("rawtypes")
public class MihoyoKcpServer extends Thread {
private EventLoopGroup group;
private UkcpServerBootstrap bootstrap;
private ChannelInitializer serverInitializer;
private InetSocketAddress address;
public MihoyoKcpServer(InetSocketAddress address) {
this.address = address;
this.setName("Netty Server Thread");
}
public InetSocketAddress getAddress() {
return this.address;
}
public ChannelInitializer getServerInitializer() {
return serverInitializer;
}
public void setServerInitializer(ChannelInitializer serverInitializer) {
this.serverInitializer = serverInitializer;
}
@Override
public void run() {
if (getServerInitializer() == null) {
this.setServerInitializer(new MihoyoKcpServerInitializer());
}
try {
group = new NioEventLoopGroup();
bootstrap = new UkcpServerBootstrap();
bootstrap.group(group)
.channel(UkcpServerChannel.class)
.childHandler(this.getServerInitializer());
ChannelOptionHelper
.nodelay(bootstrap, true, 20, 2, true)
.childOption(UkcpChannelOption.UKCP_MTU, 1200);
// Start handler
this.onStart();
// Start the server.
ChannelFuture f = bootstrap.bind(getAddress()).sync();
// Start finish handler
this.onStartFinish();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
// Close
finish();
}
}
public void onStart() {
}
public void onStartFinish() {
}
private void finish() {
try {
group.shutdownGracefully();
} catch (Exception e) {
}
Grasscutter.getLogger().info("Game Server closed");
}
}
package emu.grasscutter.netty;
import io.jpower.kcp.netty.UkcpChannel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
@SuppressWarnings("unused")
public class MihoyoKcpServerInitializer extends ChannelInitializer<UkcpChannel> {
@Override
protected void initChannel(UkcpChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
}
}
package emu.grasscutter.server.dispatch;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
public class DispatchHttpJsonHandler implements HttpHandler {
private final String response;
public DispatchHttpJsonHandler(String response) {
this.response = response;
}
@Override
public void handle(HttpExchange t) throws IOException {
// Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("application/json"));
t.sendResponseHeaders(200, response.getBytes().length);
// Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
}
package emu.grasscutter.server.dispatch;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.security.KeyStore;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.protobuf.ByteString;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsServer;
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;
import emu.grasscutter.server.dispatch.json.ComboTokenReqJson;
import emu.grasscutter.server.dispatch.json.ComboTokenResJson;
import emu.grasscutter.server.dispatch.json.LoginAccountRequestJson;
import emu.grasscutter.server.dispatch.json.LoginResultJson;
import emu.grasscutter.server.dispatch.json.LoginTokenRequestJson;
import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import com.sun.net.httpserver.HttpServer;
public class DispatchServer {
private HttpsServer server;
private final InetSocketAddress address;
private final Gson gson;
private QueryCurrRegionHttpRsp currRegion;
public String regionListBase64;
public String regionCurrentBase64;
public static String query_region_list = "";
public static String query_cur_region = "";
public DispatchServer() {
this.address = new InetSocketAddress(Grasscutter.getConfig().DispatchServerIp, Grasscutter.getConfig().DispatchServerPort);
this.gson = new GsonBuilder().create();
this.loadQueries();
this.initRegion();
}
public InetSocketAddress getAddress() {
return address;
}
public Gson getGsonFactory() {
return gson;
}
public QueryCurrRegionHttpRsp getCurrRegion() {
return currRegion;
}
public void loadQueries() {
File file;
file = new File(Grasscutter.getConfig().DATA_FOLDER + "query_region_list.txt");
if (file.exists()) {
query_region_list = new String(FileUtils.read(file));
} else {
Grasscutter.getLogger().warn("query_region_list not found! Using default region list.");
}
file = new File(Grasscutter.getConfig().DATA_FOLDER + "query_cur_region.txt");
if (file.exists()) {
query_cur_region = new String(FileUtils.read(file));
} else {
Grasscutter.getLogger().warn("query_cur_region not found! Using default current region.");
}
}
private void initRegion() {
try {
byte[] decoded = Base64.getDecoder().decode(query_region_list);
QueryRegionListHttpRsp rl = QueryRegionListHttpRsp.parseFrom(decoded);
byte[] decoded2 = Base64.getDecoder().decode(query_cur_region);
QueryCurrRegionHttpRsp regionQuery = QueryCurrRegionHttpRsp.parseFrom(decoded2);
RegionSimpleInfo server = RegionSimpleInfo.newBuilder()
.setName("os_usa")
.setTitle(Grasscutter.getConfig().GameServerName)
.setType("DEV_PUBLIC")
.setDispatchUrl("https://" + Grasscutter.getConfig().DispatchServerIp + ":" + getAddress().getPort() + "/query_cur_region")
.build();
RegionSimpleInfo serverTest2 = RegionSimpleInfo.newBuilder()
.setName("os_euro")
.setTitle("Grasscutter")
.setType("DEV_PUBLIC")
.setDispatchUrl("https://" + Grasscutter.getConfig().DispatchServerIp + ":" + getAddress().getPort() + "/query_cur_region")
.build();
QueryRegionListHttpRsp regionList = QueryRegionListHttpRsp.newBuilder()
.addServers(server)
.addServers(serverTest2)
.setClientSecretKey(rl.getClientSecretKey())
.setClientCustomConfigEncrypted(rl.getClientCustomConfigEncrypted())
.setEnableLoginPc(true)
.build();
RegionInfo currentRegion = regionQuery.getRegionInfo().toBuilder()
.setIp(Grasscutter.getConfig().GameServerIp)
.setPort(Grasscutter.getConfig().GameServerPort)
.setSecretKey(ByteString.copyFrom(FileUtils.read(Grasscutter.getConfig().KEY_FOLDER + "dispatchSeed.bin")))
.build();
QueryCurrRegionHttpRsp parsedRegionQuery = regionQuery.toBuilder().setRegionInfo(currentRegion).build();
this.regionListBase64 = Base64.getEncoder().encodeToString(regionList.toByteString().toByteArray());
this.regionCurrentBase64 = Base64.getEncoder().encodeToString(parsedRegionQuery.toByteString().toByteArray());
this.currRegion = parsedRegionQuery;
} catch (Exception e) {
e.printStackTrace();
}
}
public void start() throws Exception {
server = HttpsServer.create(getAddress(), 0);
SSLContext sslContext = SSLContext.getInstance("TLS");
try (FileInputStream fis = new FileInputStream(Grasscutter.getConfig().DispatchServerKeystorePath)) {
char[] keystorePassword = Grasscutter.getConfig().DispatchServerKeystorePassword.toCharArray();
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(fis, keystorePassword);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, keystorePassword);
sslContext.init(kmf.getKeyManagers(), null, null);
server.setHttpsConfigurator(new HttpsConfigurator(sslContext));
} catch (Exception e) {
Grasscutter.getLogger().error("No SSL cert found!");
return;
}
server.createContext("/", new HttpHandler() {
@Override
public void handle(HttpExchange t) throws IOException {
//Create a response form the request query parameters
String response = "Hello";
//Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8"));
t.sendResponseHeaders(200, response.getBytes().length);
//Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
});
// Dispatch
server.createContext("/query_region_list", new HttpHandler() {
@Override
public void handle(HttpExchange t) throws IOException {
// Log
Grasscutter.getLogger().info("Client request: query_region_list");
// Create a response form the request query parameters
String response = regionListBase64;
// Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8"));
t.sendResponseHeaders(200, response.getBytes().length);
// Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
});
server.createContext("/query_cur_region", new HttpHandler() {
@Override
public void handle(HttpExchange t) throws IOException {
// Log
Grasscutter.getLogger().info("Client request: query_cur_region");
// Create a response form the request query parameters
URI uri = t.getRequestURI();
String response = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw==";
if (uri.getQuery() != null && uri.getQuery().length() > 0) {
response = regionCurrentBase64;
}
// Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8"));
t.sendResponseHeaders(200, response.getBytes().length);
// Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
});
// Login via account
server.createContext("/hk4e_global/mdk/shield/api/login", new HttpHandler() {
@Override
public void handle(HttpExchange t) throws IOException {
// Get post data
LoginAccountRequestJson requestData = null;
try {
String body = Utils.toString(t.getRequestBody());
requestData = getGsonFactory().fromJson(body, LoginAccountRequestJson.class);
} catch (Exception e) {
}
// Create response json
if (requestData == null) {
return;
}
LoginResultJson responseData = new LoginResultJson();
// Login
Account account = DatabaseHelper.getAccountByName(requestData.account);
// Test
if (account == null) {
responseData.retcode = -201;
responseData.message = "Username not found.";
} else {
responseData.message = "OK";
responseData.data.account.uid = account.getId();
responseData.data.account.token = account.generateSessionKey();
responseData.data.account.email = account.getEmail();
}
// Create a response
String response = getGsonFactory().toJson(responseData);
// Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("application/json"));
t.sendResponseHeaders(200, response.getBytes().length);
// Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
});
// Login via token
server.createContext("/hk4e_global/mdk/shield/api/verify", new HttpHandler() {
@Override
public void handle(HttpExchange t) throws IOException {
// Get post data
LoginTokenRequestJson requestData = null;
try {
String body = Utils.toString(t.getRequestBody());
requestData = getGsonFactory().fromJson(body, LoginTokenRequestJson.class);
} catch (Exception e) {
}
// Create response json
if (requestData == null) {
return;
}
LoginResultJson responseData = new LoginResultJson();
// Login
Account account = DatabaseHelper.getAccountById(requestData.uid);
// Test
if (account == null || !account.getSessionKey().equals(requestData.token)) {
responseData.retcode = -111;
responseData.message = "Game account cache information error";
} else {
responseData.message = "OK";
responseData.data.account.uid = requestData.uid;
responseData.data.account.token = requestData.token;
responseData.data.account.email = account.getEmail();
}
// Create a response
String response = getGsonFactory().toJson(responseData);
// Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("application/json"));
t.sendResponseHeaders(200, response.getBytes().length);
// Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
});
// Exchange for combo token
server.createContext("/hk4e_global/combo/granter/login/v2/login", new HttpHandler() {
@Override
public void handle(HttpExchange t) throws IOException {
// Get post data
ComboTokenReqJson requestData = null;
try {
String body = Utils.toString(t.getRequestBody());
requestData = getGsonFactory().fromJson(body, ComboTokenReqJson.class);
} catch (Exception e) {
}
// Create response json
if (requestData == null || requestData.data == null) {
return;
}
LoginTokenData loginData = getGsonFactory().fromJson(requestData.data, LoginTokenData.class); // Get login data
ComboTokenResJson responseData = new ComboTokenResJson();
// Login
Account account = DatabaseHelper.getAccountById(loginData.uid);
// Test
if (account == null || !account.getSessionKey().equals(loginData.token)) {
responseData.retcode = -201;
responseData.message = "Wrong session key.";
} else {
responseData.message = "OK";
responseData.data.open_id = loginData.uid;
responseData.data.combo_id = "157795300";
responseData.data.combo_token = account.generateLoginToken();
}
// Create a response
String response = getGsonFactory().toJson(responseData);
// Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("application/json"));
t.sendResponseHeaders(200, response.getBytes().length);
// Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
});
// Agreement and Protocol
server.createContext( // hk4e-sdk-os.hoyoverse.com
"/hk4e_global/mdk/agreement/api/getAgreementInfos",
new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}")
);
server.createContext( // hk4e-sdk-os.hoyoverse.com
"/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\":\"\"}}}")
);
// Game data
server.createContext( // hk4e-api-os.hoyoverse.com
"/common/hk4e_global/announcement/api/getAlertPic",
new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"total\":0,\"list\":[]}}")
);
server.createContext( // hk4e-api-os.hoyoverse.com
"/common/hk4e_global/announcement/api/getAlertAnn",
new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"alert\":false,\"alert_id\":0,\"remind\":true}}")
);
server.createContext( // hk4e-api-os.hoyoverse.com
"/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() + "\"}}")
);
server.createContext( // hk4e-api-os-static.hoyoverse.com
"/common/hk4e_global/announcement/api/getAnnContent",
new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"list\":[],\"total\":0}}")
);
server.createContext( // hk4e-sdk-os.hoyoverse.com
"/hk4e_global/mdk/shopwindow/shopwindow/listPriceTier",
new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"suggest_currency\":\"USD\",\"tiers\":[]}}")
);
// Captcha
server.createContext( // api-account-os.hoyoverse.com
"/account/risky/api/check",
new DispatchHttpJsonHandler("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"c8820f246a5241ab9973f71df3ddd791\",\"action\":\"\",\"geetest\":{\"challenge\":\"\",\"gt\":\"\",\"new_captcha\":0,\"success\":1}}}")
);
// Config
server.createContext( // sdk-os-static.hoyoverse.com
"/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\"}}}")
);
server.createContext( // hk4e-sdk-os-static.hoyoverse.com
"/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}}")
);
server.createContext( // hk4e-sdk-os-static.hoyoverse.com
"/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}}}}")
);
// Test api?
server.createContext( // abtest-api-data-sg.hoyoverse.com
"/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\"}}]}")
);
// Log Server
server.createContext( // log-upload-os.mihoyo.com
"/log/sdk/upload",
new DispatchHttpJsonHandler("{\"code\":0}")
);
server.createContext( // log-upload-os.mihoyo.com
"/sdk/upload",
new DispatchHttpJsonHandler("{\"code\":0}")
);
// Start server
server.start();
Grasscutter.getLogger().info("Dispatch server started on port " + getAddress().getPort());
// Logging servers
HttpServer overseaLogServer = HttpServer.create(new InetSocketAddress(Grasscutter.getConfig().DispatchServerIp, 8888), 0);
overseaLogServer.createContext( // overseauspider.yuanshen.com
"/log",
new DispatchHttpJsonHandler("{\"code\":0}")
);
overseaLogServer.start();
Grasscutter.getLogger().info("Log server (overseauspider) started on port " + 8888);
HttpServer uploadLogServer = HttpServer.create(new InetSocketAddress(Grasscutter.getConfig().DispatchServerIp, 80), 0);
uploadLogServer.createContext( // log-upload-os.mihoyo.com
"/crash/dataUpload",
new DispatchHttpJsonHandler("{\"code\":0}")
);
uploadLogServer.createContext("/gacha", new HttpHandler() {
@Override
public void handle(HttpExchange t) throws IOException {
//Create a response form the request query parameters
String response = "<!doctype html><html lang=\"en\"><head><title>Gacha</title></head><body></body></html>";
//Set the response header status and length
t.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html; charset=UTF-8"));
t.sendResponseHeaders(200, response.getBytes().length);
//Write the response string
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
});
uploadLogServer.start();
Grasscutter.getLogger().info("Log server (log-upload-os) started on port " + 80);
}
private Map<String, String> parseQueryString(String qs) {
Map<String, String> result = new HashMap<>();
if (qs == null)
return result;
int last = 0, next, l = qs.length();
while (last < l) {
next = qs.indexOf('&', last);
if (next == -1)
next = l;
if (next > last) {
int eqPos = qs.indexOf('=', last);
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
}
}
last = next + 1;
}
return result;
}
}
package emu.grasscutter.server.dispatch.json;
public class ComboTokenReqJson {
public int app_id;
public int channel_id;
public String data;
public String device;
public String sign;
public class LoginTokenData {
public String uid;
public String token;
public boolean guest;
}
}
package emu.grasscutter.server.dispatch.json;
public class ComboTokenResJson {
public String message;
public int retcode;
public LoginData data = new LoginData();
public class LoginData {
public int account_type = 1;
public boolean heartbeat;
public String combo_id;
public String combo_token;
public String open_id;
public String data = "{\"guest\":false}";
public String fatigue_remind = null; // ?
}
}
package emu.grasscutter.server.dispatch.json;
public class LoginAccountRequestJson {
public String account;
public String password;
public boolean is_crypto;
}
package emu.grasscutter.server.dispatch.json;
public class LoginResultJson {
public String message;
public int retcode;
public VerifyData data = new VerifyData();
public class VerifyData {
public VerifyAccountData account = new VerifyAccountData();
public boolean device_grant_required = false;
public String realname_operation = "NONE";
public boolean realperson_required = false;
public boolean safe_mobile_required = false;
}
public class VerifyAccountData {
public String uid;
public String name = "";
public String email;
public String mobile = "";
public String is_email_verify = "0";
public String realname = "";
public String identity_card = "";
public String token;
public String safe_mobile = "";
public String facebook_name = "";
public String twitter_name = "";
public String game_center_name = "";
public String google_name = "";
public String apple_name = "";
public String sony_name = "";
public String tap_name = "";
public String country = "US";
public String reactivate_ticket = "";
public String area_code = "**";
public String device_grant_ticket = "";
}
}
package emu.grasscutter.server.dispatch.json;
public class LoginTokenRequestJson {
public String uid;
public String token;
}
package emu.grasscutter.server.game;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import emu.grasscutter.GenshinConstants;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.GenshinPlayer;
import emu.grasscutter.game.dungeons.DungeonManager;
import emu.grasscutter.game.gacha.GachaManager;
import emu.grasscutter.game.managers.ChatManager;
import emu.grasscutter.game.managers.InventoryManager;
import emu.grasscutter.game.managers.MultiplayerManager;
import emu.grasscutter.game.shop.ShopManager;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
import emu.grasscutter.netty.MihoyoKcpServer;
public class GameServer extends MihoyoKcpServer {
private final InetSocketAddress address;
private final GameServerPacketHandler packetHandler;
private final Timer gameLoop;
private final Map<Integer, GenshinPlayer> players;
private final ChatManager chatManager;
private final InventoryManager inventoryManager;
private final GachaManager gachaManager;
private final ShopManager shopManager;
private final MultiplayerManager multiplayerManager;
private final DungeonManager dungeonManager;
public GameServer(InetSocketAddress address) {
super(address);
this.setServerInitializer(new GameServerInitializer(this));
this.address = address;
this.packetHandler = new GameServerPacketHandler(PacketHandler.class);
this.players = new ConcurrentHashMap<>();
this.chatManager = new ChatManager(this);
this.inventoryManager = new InventoryManager(this);
this.gachaManager = new GachaManager(this);
this.shopManager = new ShopManager(this);
this.multiplayerManager = new MultiplayerManager(this);
this.dungeonManager = new DungeonManager(this);
// Ticker
this.gameLoop = new Timer();
this.gameLoop.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
onTick();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}, new Date(), 1000L);
// Shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown));
}
public GameServerPacketHandler getPacketHandler() {
return packetHandler;
}
public Map<Integer, GenshinPlayer> getPlayers() {
return players;
}
public ChatManager getChatManager() {
return chatManager;
}
public InventoryManager getInventoryManager() {
return inventoryManager;
}
public GachaManager getGachaManager() {
return gachaManager;
}
public ShopManager getShopManager() {
return shopManager;
}
public MultiplayerManager getMultiplayerManager() {
return multiplayerManager;
}
public DungeonManager getDungeonManager() {
return dungeonManager;
}
public void registerPlayer(GenshinPlayer player) {
getPlayers().put(player.getId(), player);
}
public GenshinPlayer getPlayerById(int id) {
return this.getPlayers().get(id);
}
public GenshinPlayer forceGetPlayerById(int id) {
// Console check
if (id == GenshinConstants.SERVER_CONSOLE_UID) {
return null;
}
// Get from online players
GenshinPlayer player = this.getPlayerById(id);
// Check database if character isnt here
if (player == null) {
player = DatabaseHelper.getPlayerById(id);
}
return player;
}
public SocialDetail.Builder getSocialDetailById(int id) {
// Get from online players
GenshinPlayer player = this.forceGetPlayerById(id);
if (player == null) {
return null;
}
return player.getSocialDetail();
}
public void onTick() throws Exception {
for (GenshinPlayer player : this.getPlayers().values()) {
player.onTick();
}
}
@Override
public void onStartFinish() {
Grasscutter.getLogger().info("Game Server started on port " + address.getPort());
}
public void onServerShutdown() {
// Kick and save all players
List<GenshinPlayer> list = new ArrayList<>(this.getPlayers().size());
list.addAll(this.getPlayers().values());
for (GenshinPlayer player : list) {
player.getSession().close();
}
}
}
package emu.grasscutter.server.game;
import emu.grasscutter.netty.MihoyoKcpServerInitializer;
import io.jpower.kcp.netty.UkcpChannel;
import io.netty.channel.ChannelPipeline;
public class GameServerInitializer extends MihoyoKcpServerInitializer {
private GameServer server;
public GameServerInitializer(GameServer server) {
this.server = server;
}
@Override
protected void initChannel(UkcpChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
GameSession session = new GameSession(server);
pipeline.addLast(session);
}
}
package emu.grasscutter.server.game;
import java.util.Set;
import org.reflections.Reflections;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.server.game.GameSession.SessionState;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public class GameServerPacketHandler {
private final Int2ObjectMap<PacketHandler> handlers;
public GameServerPacketHandler(Class<? extends PacketHandler> handlerClass) {
this.handlers = new Int2ObjectOpenHashMap<>();
this.registerHandlers(handlerClass);
}
public void registerHandlers(Class<? extends PacketHandler> handlerClass) {
Reflections reflections = new Reflections("emu.grasscutter.server.packet");
Set<?> handlerClasses = reflections.getSubTypesOf(handlerClass);
for (Object obj : handlerClasses) {
Class<?> c = (Class<?>) obj;
try {
Opcodes opcode = c.getAnnotation(Opcodes.class);
if (opcode == null || opcode.disabled() || opcode.value() <= 0) {
continue;
}
PacketHandler packetHandler = (PacketHandler) c.newInstance();
this.handlers.put(opcode.value(), packetHandler);
} catch (Exception e) {
e.printStackTrace();
}
}
// Debug
Grasscutter.getLogger().info("Registered " + this.handlers.size() + " " + handlerClass.getSimpleName() + "s");
}
public void handle(GameSession session, int opcode, byte[] header, byte[] payload) {
PacketHandler handler = null;
handler = this.handlers.get(opcode);
if (handler != null) {
try {
// Make sure session is ready for packets
SessionState state = session.getState();
if (opcode == PacketOpcodes.PingReq) {
// Always continue if packet is ping request
} else if (opcode == PacketOpcodes.GetPlayerTokenReq) {
if (state != SessionState.WAITING_FOR_TOKEN) {
return;
}
} else if (opcode == PacketOpcodes.PlayerLoginReq) {
if (state != SessionState.WAITING_FOR_LOGIN) {
return;
}
} else if (opcode == PacketOpcodes.SetPlayerBornDataReq) {
if (state != SessionState.PICKING_CHARACTER) {
return;
}
} else {
if (state != SessionState.ACTIVE) {
return;
}
}
// Handle
handler.handle(session, header, payload);
} catch (Exception ex) {
// TODO Remove this when no more needed
ex.printStackTrace();
}
return; // Packet successfully handled
}
// Log unhandled packets
if (Grasscutter.getConfig().LOG_PACKETS) {
//Grasscutter.getLogger().info("Unhandled packet (" + opcode + "): " + PacketOpcodesUtil.getOpcodeName(opcode));
}
}
}
package emu.grasscutter.server.game;
import java.io.File;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.GenshinPlayer;
import emu.grasscutter.net.packet.GenshinPacket;
import emu.grasscutter.net.packet.PacketOpcodesUtil;
import emu.grasscutter.netty.MihoyoKcpChannel;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
public class GameSession extends MihoyoKcpChannel {
private GameServer server;
private Account account;
private GenshinPlayer player;
private boolean useSecretKey;
private SessionState state;
private int clientTime;
private long lastPingTime;
private int lastClientSeq = 10;
public GameSession(GameServer server) {
this.server = server;
this.state = SessionState.WAITING_FOR_TOKEN;
this.lastPingTime = System.currentTimeMillis();
}
public GameServer getServer() {
return server;
}
public InetSocketAddress getAddress() {
if (this.getChannel() == null) {
return null;
}
return this.getChannel().remoteAddress();
}
public boolean useSecretKey() {
return useSecretKey;
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
public String getAccountId() {
return this.getAccount().getId();
}
public GenshinPlayer getPlayer() {
return player;
}
public synchronized void setPlayer(GenshinPlayer player) {
this.player = player;
this.player.setSession(this);
this.player.setAccount(this.getAccount());
}
public SessionState getState() {
return state;
}
public void setState(SessionState state) {
this.state = state;
}
public boolean isLoggedIn() {
return this.getPlayer() != null;
}
public void setUseSecretKey(boolean useSecretKey) {
this.useSecretKey = useSecretKey;
}
public int getClientTime() {
return this.clientTime;
}
public long getLastPingTime() {
return lastPingTime;
}
public void updateLastPingTime(int clientTime) {
this.clientTime = clientTime;
this.lastPingTime = System.currentTimeMillis();
}
public int getNextClientSequence() {
return ++lastClientSeq;
}
@Override
protected void onConnect() {
Grasscutter.getLogger().info("Client connected from " + getAddress().getHostString().toLowerCase());
}
@Override
protected synchronized void onDisconnect() { // Synchronize so we dont add character at the same time
Grasscutter.getLogger().info("Client disconnected from " + getAddress().getHostString().toLowerCase());
// Set state so no more packets can be handled
this.setState(SessionState.INACTIVE);
// Save after disconnecting
if (this.isLoggedIn()) {
// Save
getPlayer().onLogout();
// Remove from gameserver
getServer().getPlayers().remove(getPlayer().getId());
}
}
protected void logPacket(ByteBuffer buf) {
ByteBuf b = Unpooled.wrappedBuffer(buf.array());
logPacket(b);
}
public void replayPacket(int opcode, String name) {
String filePath = Grasscutter.getConfig().PACKETS_FOLDER + name;
File p = new File(filePath);
if (!p.exists()) return;
byte[] packet = FileUtils.read(p);
GenshinPacket genshinPacket = new GenshinPacket(opcode);
genshinPacket.setData(packet);
// Log
logPacket(genshinPacket.getOpcode());
send(genshinPacket);
}
public void send(GenshinPacket genshinPacket) {
// Test
if (genshinPacket.getOpcode() <= 0) {
Grasscutter.getLogger().warn("Tried to send packet with missing cmd id!");
return;
}
// Header
if (genshinPacket.shouldBuildHeader()) {
genshinPacket.buildHeader(this.getNextClientSequence());
}
// Build packet
byte[] data = genshinPacket.build();
// Log
if (Grasscutter.getConfig().LOG_PACKETS) {
logPacket(genshinPacket);
}
// Send
send(data);
}
private void logPacket(int opcode) {
//Grasscutter.getLogger().info("SEND: " + PacketOpcodesUtil.getOpcodeName(opcode));
//System.out.println(Utils.bytesToHex(genshinPacket.getData()));
}
private void logPacket(GenshinPacket genshinPacket) {
Grasscutter.getLogger().info("SEND: " + PacketOpcodesUtil.getOpcodeName(genshinPacket.getOpcode()) + " (" + genshinPacket.getOpcode() + ")");
System.out.println(Utils.bytesToHex(genshinPacket.getData()));
}
@Override
public void onMessage(ChannelHandlerContext ctx, ByteBuf data) {
// Decrypt and turn back into a packet
byte[] byteData = Utils.byteBufToArray(data);
Crypto.xor(byteData, useSecretKey() ? Crypto.ENCRYPT_KEY : Crypto.DISPATCH_KEY);
ByteBuf packet = Unpooled.wrappedBuffer(byteData);
// Log
//logPacket(packet);
// Handle
try {
while (packet.readableBytes() > 0) {
// Length
if (packet.readableBytes() < 12) {
return;
}
// Packet sanity check
int const1 = packet.readShort();
if (const1 != 17767) {
return; // Bad packet
}
// Data
int opcode = packet.readShort();
int headerLength = packet.readShort();
int payloadLength = packet.readInt();
byte[] header = new byte[headerLength];
byte[] payload = new byte[payloadLength];
packet.readBytes(header);
packet.readBytes(payload);
// Sanity check #2
int const2 = packet.readShort();
if (const2 != -30293) {
return; // Bad packet
}
// Log packet
if (Grasscutter.getConfig().LOG_PACKETS) {
Grasscutter.getLogger().info("RECV: " + PacketOpcodesUtil.getOpcodeName(opcode) + " (" + opcode + ")");
System.out.println(Utils.bytesToHex(payload));
}
// Handle
getServer().getPacketHandler().handle(this, opcode, header, payload);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
packet.release();
}
}
public enum SessionState {
INACTIVE,
WAITING_FOR_TOKEN,
WAITING_FOR_LOGIN,
PICKING_CHARACTER,
ACTIVE;
}
}
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.server.game.GameSession;
@Opcodes(PacketOpcodes.NONE)
public class Handler extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
// Auto template
}
}
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.AbilityInvocationsNotifyOuterClass.AbilityInvocationsNotify;
import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.server.game.GameSession;
@Opcodes(PacketOpcodes.AbilityInvocationsNotify)
public class HandlerAbilityInvocationsNotify extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
AbilityInvocationsNotify notif = AbilityInvocationsNotify.parseFrom(payload);
for (AbilityInvokeEntry entry : notif.getInvokesList()) {
//System.out.println(entry.getArgumentType() + ": " + Utils.bytesToHex(entry.getAbilityData().toByteArray()));
session.getPlayer().getAbilityInvokeHandler().addEntry(entry.getForwardType(), entry);
}
if (notif.getInvokesList().size() > 0) {
session.getPlayer().getAbilityInvokeHandler().update(session.getPlayer());
}
}
}
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.AskAddFriendReqOuterClass.AskAddFriendReq;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.server.game.GameSession;
@Opcodes(PacketOpcodes.AskAddFriendReq)
public class HandlerAskAddFriendReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
AskAddFriendReq req = AskAddFriendReq.parseFrom(payload);
session.getPlayer().getFriendsList().sendFriendRequest(req.getTargetUid());
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment