Initial Commit

This commit is contained in:
Melledy
2025-10-27 02:02:26 -07:00
commit f58951fe2a
378 changed files with 315914 additions and 0 deletions

View File

@@ -0,0 +1,212 @@
package emu.nebula.server.routes;
import java.util.Set;
import org.reflections.Reflections;
import emu.nebula.Nebula;
import emu.nebula.game.GameContext;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgIdUtils;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.util.AeadHelper;
import emu.nebula.util.Utils;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
@Getter
public class AgentZoneHandler implements Handler {
private final Int2ObjectMap<NetHandler> handlers;
private final static byte[] EMPTY_BYTES = new byte[0];
public AgentZoneHandler() {
this.handlers = new Int2ObjectOpenHashMap<>();
this.registerHandlers();
}
protected GameContext getGameContext() {
return Nebula.getGameContext();
}
@Override
public void handle(Context ctx) throws Exception {
// Setup session
GameSession session = null;
byte[] sessionKey = AeadHelper.serverGarbleKey;
boolean hasKey3 = false;
// Get token
String token = ctx.header("X-Token");
// Set headers
ctx.res().setHeader("Server", "agent");
// Check if we have a token
if (token != null) {
// Get session
session = getGameContext().getSessionByToken(token);
// Uh oh - session not found
if (session == null || session.getKey() == null) {
ctx.status(500);
ctx.result("");
return;
}
// Set key
sessionKey = session.getKey();
hasKey3 = true;
}
// Parse request
byte[] data = null;
int msgId = 0;
try {
// Get message
byte[] message = ctx.bodyAsBytes();
int offset = 0;
// Sanity for malformed packets
if (message.length <= 12) {
ctx.status(500);
ctx.result("");
return;
}
// Decrypt message
if (hasKey3) {
message = AeadHelper.decryptChaCha(message, sessionKey);
offset = 10;
} else {
message = AeadHelper.decryptBasic(message, sessionKey);
message = AeadHelper.decryptGCM(message, sessionKey);
}
// Get message id
msgId = (message[offset++] << 8) | (message[offset++] & 0xff);
// Set data
data = new byte[message.length - offset];
System.arraycopy(message, offset, data, 0, data.length);
// Log
if (Nebula.getConfig().getLogOptions().packets) {
this.logRecv(msgId, data);
}
} catch (Exception e) {
// Decrypt error
e.printStackTrace();
ctx.status(500);
ctx.result("");
return;
}
// Update last active time for session
if (session != null) {
session.updateLastActiveTime();
}
// Handle packet
NetHandler handler = this.handlers.get(msgId);
byte[] result = null;
try {
if (handler == null) {
Nebula.getLogger().warn("Unhandled request: " + msgId);
return;
}
// Check handler requirements
if (session == null) {
if (handler.requireSession()) {
return;
}
} else if (session.getPlayer() == null && handler.requirePlayer()) {
return;
}
// Handle data
result = handler.handle(session, data);
} catch (Exception e) {
// Handler error
e.printStackTrace();
} finally {
// Send result
if (result == null) {
ctx.status(500);
ctx.result(EMPTY_BYTES);
return;
}
// Log
if (Nebula.getConfig().getLogOptions().packets) {
this.logSend(result);
}
// Encrypt
if (hasKey3) {
result = AeadHelper.encryptChaCha(result, sessionKey);
} else {
result = AeadHelper.encryptGCM(result, sessionKey);
result = AeadHelper.encryptBasic(result, sessionKey);
}
// Send to client
ctx.status(200);
ctx.result(result);
ctx.res().setHeader("Content-Type", null);
}
}
// Loggers
private void logRecv(int msgId, byte[] data) {
Nebula.getLogger().info("RECV: " + NetMsgIdUtils.getMsgIdName(msgId) + " (" + msgId + ")");
System.out.println(Utils.bytesToHex(data));
}
private void logSend(byte[] data) {
int sendMsgId = (data[0] << 8) | (data[1] & 0xff);
Nebula.getLogger().info("SEND: " + NetMsgIdUtils.getMsgIdName(sendMsgId) + " (" + sendMsgId + ")");
System.out.println(Utils.bytesToHex(data, 2));
}
// Register handlers
private void registerHandlers() {
// Setup handlers
Reflections reflections = new Reflections(Nebula.class.getPackageName());
Set<Class<?>> handlers = reflections.getTypesAnnotatedWith(HandlerId.class);
for (Class<?> cls : handlers) {
// Make sure class is a handler
if (!NetHandler.class.isAssignableFrom(cls)) {
continue;
}
try {
NetHandler handler = (NetHandler) cls.getConstructor().newInstance();
HandlerId def = cls.getAnnotation(HandlerId.class);
int opcode = def.value();
if (opcode != 0) {
// Put in handler map
this.handlers.put(opcode, handler);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// Log registration
Nebula.getLogger().info("Registered " + handlers.size() + " handlers");
}
}

View File

@@ -0,0 +1,30 @@
package emu.nebula.server.routes;
import org.jetbrains.annotations.NotNull;
import emu.nebula.server.HttpServer;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import lombok.Getter;
@Getter
public class CommonConfigHandler implements Handler {
private HttpServer server;
private String json;
public CommonConfigHandler(HttpServer server) {
this.server = server;
this.json = "{\"Code\":200,\"Data\":{\"AppConfig\":{\"ACCOUNT_RETRIEVAL\":{\"FIRST_LOGIN_POPUP\":false,\"LOGIN_POPUP\":false,\"PAGE_URL\":\"\"},\"AGREEMENT_POPUP_TYPE\":\"Browser\",\"APPLE_CURRENCY_BLOCK_LIST\":null,\"APPLE_TYPE_KEY\":\"apple_hk\",\"APP_CLIENT_LANG\":[\"en\"],\"APP_DEBUG\":0,\"APP_GL\":\"en\",\"BIND_METHOD\":[\"google\",\"apple_hk\",\"facebook\"],\"CAPTCHA_ENABLED\":false,\"CLIENT_LOG_REPORTING\":{\"ENABLE\":true},\"CREDIT_INVESTIGATION\":\"0.0\",\"DESTROY_USER_DAYS\":15,\"DESTROY_USER_ENABLE\":1,\"DETECTION_ADDRESS\":{\"AUTO\":{\"DNS\":[\"${url}\",\"${url}\",\"${url}/meta/serverlist.html\"],\"HTTP\":[\"${url}\",\"${url}\",\"${url}\"],\"MTR\":[\"${url}\",\"${url}\",\"${url}/meta/serverlist.html\"],\"PING\":[\"${url}\",\"${url}\",\"${url}/meta/serverlist.html\"],\"TCP\":[\"${url}\",\"${url}\",\"${url}/meta/serverlist.html\"]},\"ENABLE\":true,\"ENABLE_MANUAL\":true,\"INTERNET\":\"https://www.google.com\",\"INTERNET_ADDRESS\":\"https://www.google.com\",\"NETWORK_ENDPORINT\":\"\",\"NETWORK_PROJECT\":\"\",\"NETWORK_SECRET_KEY\":\"\"},\"ENABLE_AGREEMENT\":true,\"ENABLE_MULTI_LANG_AGREEMENT\":false,\"ENABLE_TEXT_REVIEW\":true,\"ERROR_CODE\":\"4.4\",\"FILE_DOMAIN\":\"\",\"GEETEST_ENABLE\":false,\"GEETEST_ID\":\"\",\"GOOGLE_ANALYTICS_MEASUREMENT_ID\":\"\",\"MIGRATE_POPUP\":true,\"NICKNAME_REG\":\"^[A-Za-z0-9]{2,20}$\",\"POPUP\":{\"Data\":[{\"Lang\":\"ja\",\"Text\":\"YostarIDを作成\"},{\"Lang\":\"en\",\"Text\":\"CreateaYostaraccount\"},{\"Lang\":\"kr\",\"Text\":\"YOSTAR계정가입하기\"},{\"Lang\":\"fr\",\"Text\":\"CréezvotrecompteYostar\"},{\"Lang\":\"de\",\"Text\":\"EinenYostar-Accounterstellen\"}],\"Enable\":true},\"PRIVACY_AGREEMENT\":\"0.1\",\"RECHARGE_LIMIT\":{\"Enable\":false,\"IsOneLimit\":false,\"Items\":[],\"OneLimitAmount\":0},\"SHARE\":{\"CaptureScreen\":{\"AutoCloseDelay\":0,\"Enabled\":false},\"Facebook\":{\"AppID\":\"\",\"Enabled\":false},\"Instagram\":{\"Enabled\":false},\"Kakao\":{\"AppKey\":\"\",\"Enabled\":false},\"Naver\":{\"Enabled\":false},\"Twitter\":{\"Enabled\":false}},\"SLS\":{\"ACCESS_KEY_ID\":\"7b5d0ffd0943f26704fc547a871c68b1b5d56b5c9caeb354205b81f445d7af59\",\"ACCESS_KEY_SECRET\":\"4a5e9cc8a50819290c9bfa1fedc79da7c50e85189a05eb462a3d28a7688eabb0\",\"ENABLE\":false},\"SURVEY_POPUP_TYPE\":\"Browser\",\"UDATA\":{\"Enable\":false,\"URL\":\"${url}\"},\"USER_AGREEMENT\":\"0.1\",\"YOSTAR_PREFIX\":\"yoyo\"},\"EuropeUnion\":false,\"StoreConfig\":{\"ADJUST_APPID\":\"\",\"ADJUST_CHARGEEVENTTOKEN\":\"\",\"ADJUST_ENABLED\":0,\"ADJUST_EVENTTOKENS\":null,\"ADJUST_ISDEBUG\":0,\"AIRWALLEX_ENABLED\":false,\"AI_HELP\":{\"AihelpAppID\":\"yostar1_platform_2db52a57068b1ee3fe3652c8b53d581b\",\"AihelpAppKey\":\"YOSTAR1_app_bc226f4419a7447c9de95711f8a2d3d9\",\"AihelpDomain\":\"yostar1.aihelp.net\",\"CustomerServiceURL\":\"\",\"CustomerWay\":1,\"DisplayType\":\"Browser\",\"Enable\":1,\"Mode\":\"robot\"},\"APPLEID\":\"\",\"CODA_ENABLED\":false,\"ENABLED_PAY\":{\"AIRWALLEX_ENABLED\":false,\"CODA_ENABLED\":false,\"GMOAlipay\":false,\"GMOAu\":false,\"GMOCreditcard\":false,\"GMOCvs\":false,\"GMODocomo\":false,\"GMOPaypal\":false,\"GMOPaypay\":false,\"GMOSoftbank\":false,\"MYCARD_ENABLED\":false,\"PAYPAL_ENABLED\":true,\"RAZER_ENABLED\":false,\"STEAM_ENABLED\":false,\"STRIPE_ENABLED\":true,\"TOSS_ENABLED\":false,\"WEBMONEY_ENABLED\":false},\"FACEBOOK_APPID\":\"\",\"FACEBOOK_CLIENT_TOKEN\":\"\",\"FACEBOOK_SECRET\":\"\",\"FIREBASE_ENABLED\":0,\"GMO_CC_JS\":\"https://\",\"GMO_CC_KEY\":\"\",\"GMO_CC_SHOPID\":\"\",\"GMO_PAY_CHANNEL\":{\"GMOAlipay\":false,\"GMOAu\":false,\"GMOCreditcard\":false,\"GMOCvs\":false,\"GMODocomo\":false,\"GMOPaypal\":false,\"GMOPaypay\":false,\"GMOSoftbank\":false},\"GMO_PAY_ENABLED\":false,\"GOOGLE_CLIENT_ID\":\"\",\"GOOGLE_CLIENT_SECRET\":\"\",\"GUEST_CREATE_METHOD\":0,\"GUIDE_POPUP\":{\"DATA\":null,\"ENABLE\":0},\"LOGIN\":{\"DEFAULT\":\"yostar\",\"ICON_SIZE\":\"big\",\"SORT\":[\"google\",\"apple\",\"device\"]},\"MYCARD_ENABLED\":false,\"ONE_STORE_LICENSE_KEY\":\"\",\"PAYPAL_ENABLED\":false,\"RAZER_ENABLED\":false,\"REMOTE_CONFIG\":[],\"SAMSUNG_SANDBOX_MODE\":false,\"STEAM_APPID\":\"\",\"STEAM_ENABLED\":false,\"STEAM_PAY_APPID\":\"\",\"STRIPE_ENABLED\":false,\"TOSS_ENABLED\":false,\"TWITTER_KEY\":\"\",\"TWITTER_SECRET\":\"\",\"WEBMONEY_ENABLED\":false}},\"Msg\":\"OK\"}";
String address = server.getServerConfig().getDisplayAddress();
this.json = this.json.replaceAll("\\$\\{url}", address);
}
@Override
public void handle(@NotNull Context ctx) throws Exception {
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result(this.json);
}
}

View File

@@ -0,0 +1,83 @@
package emu.nebula.server.routes;
import org.jetbrains.annotations.NotNull;
import emu.nebula.Nebula;
import emu.nebula.game.account.Account;
import emu.nebula.game.account.AccountHelper;
import emu.nebula.util.JsonUtils;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import lombok.Getter;
@Getter
public class GetAuthHandler implements Handler {
@Override
public void handle(@NotNull Context ctx) throws Exception {
// Parse request
var req = JsonUtils.decode(ctx.body(), GetAuthRequestJson.class);
if (req == null || req.Account == null || req.Account.isEmpty() || req.Code == null) {
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result("{\"Code\":100600,\"Data\":{},\"Msg\":\"Error\"}"); // PARAM_IS_EMPTY
return;
}
// Get account
Account account = AccountHelper.getAccountByEmail(req.Account);
if (account == null) {
// Create an account if were allowed to
if (Nebula.getConfig().getServerOptions().isAutoCreateAccount()) {
account = AccountHelper.createAccount(req.Account, null, 0);
}
} else {
// Check passcode sent by email
if (!account.verifyCode(req.Code)) {
account = null;
}
}
// Sanity
if (account == null) {
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result("{\"Code\":100403,\"Data\":{},\"Msg\":\"Error\"}"); // TOKEN_AUTH_FAILED
return;
}
// Build request
var response = new GetAuthResponseJson();
response.Code = 200;
response.Msg = "OK";
response.Data = new GetAuthResponseJson.GetAuthDataJson();
response.Data.UID = account.getEmail();
response.Data.Token = account.generateLoginToken();
response.Data.Account = account.getEmail();
// Result
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result(JsonUtils.encode(response));
}
private static class GetAuthRequestJson {
public String Account;
public String Code;
}
@SuppressWarnings("unused")
private static class GetAuthResponseJson {
public int Code;
public GetAuthDataJson Data;
public String Msg;
private static class GetAuthDataJson {
public String UID;
public String Token;
public String Account;
}
}
}

View File

@@ -0,0 +1,22 @@
package emu.nebula.server.routes;
import org.jetbrains.annotations.NotNull;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class HttpJsonResponse implements Handler {
private final String json;
public HttpJsonResponse(String jsonString) {
this.json = jsonString;
}
@Override
public void handle(@NotNull Context ctx) throws Exception {
ctx.status(200);
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result(json);
}
}

View File

@@ -0,0 +1,60 @@
package emu.nebula.server.routes;
import org.jetbrains.annotations.NotNull;
import emu.nebula.proto.Pb.ServerAgent;
import emu.nebula.proto.Pb.ServerListMeta;
import emu.nebula.server.HttpServer;
import emu.nebula.util.AeadHelper;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import lombok.AccessLevel;
import lombok.Getter;
@Getter(AccessLevel.PRIVATE)
public class MetaServerlistHandler implements Handler {
private HttpServer server;
private ServerListMeta list;
private byte[] proto;
public MetaServerlistHandler(HttpServer server) {
this.server = server;
// Create server list
this.list = ServerListMeta.newInstance()
.setVersion(22)
.setReportEndpoint(server.getServerConfig().getDisplayAddress() + "/report");
var agent = ServerAgent.newInstance()
.setName("Nebula") // TODO allow change in config
.setAddr(server.getServerConfig().getDisplayAddress() + "/agent-zone-1/")
.setStatus(1)
.setZone(1);
this.list.addAgent(agent);
var agent2 = ServerAgent.newInstance()
.setName("Test") // TODO allow change in config
.setAddr(server.getServerConfig().getDisplayAddress() + "/agent-zone-1/")
.setStatus(1)
.setZone(1);
this.list.addAgent(agent2);
// Cache proto
this.proto = list.toByteArray();
}
@Override
public void handle(@NotNull Context ctx) throws Exception {
// Result
try {
ctx.contentType(ContentType.APPLICATION_OCTET_STREAM);
ctx.result(AeadHelper.encryptCBC(this.getProto()));
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,35 @@
package emu.nebula.server.routes;
import org.jetbrains.annotations.NotNull;
import emu.nebula.proto.Pb.ClientDiff;
import emu.nebula.util.AeadHelper;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import lombok.AccessLevel;
import lombok.Getter;
@Getter(AccessLevel.PRIVATE)
public class MetaWinHandler implements Handler {
private ClientDiff list;
private byte[] proto;
public MetaWinHandler() {
// Create client diff
this.list = ClientDiff.newInstance();
// TODO load from json or something
// Cache proto
this.proto = list.toByteArray();
}
@Override
public void handle(@NotNull Context ctx) throws Exception {
// Result
ctx.contentType(ContentType.APPLICATION_OCTET_STREAM);
ctx.result(AeadHelper.encryptCBC(this.getProto()));
}
}

View File

@@ -0,0 +1,51 @@
package emu.nebula.server.routes;
import java.util.List;
public class UserLoginEntity {
public int Code;
public UserDetailJson Data;
public String Msg;
public static class UserDetailJson {
public long AgeVerifyMethod;
public Object Destroy;
public boolean IsTestAccount;
public List<UserKeyJson> Keys;
public long ServerNowAt;
public UserInfoJson UserInfo;
public LoginYostarJson Yostar;
public Object YostarDestroy;
}
public static class UserKeyJson {
public String ID;
public String Type;
public String Key;
public String NickName;
public long CreatedAt;
}
public static class UserInfoJson {
public String ID;
public int UID2;
public String PID;
public String Token;
public String Birthday;
public String RegChannel;
public String TransCode;
public int State;
public String DeviceID;
public long CreatedAt;
}
public static class LoginYostarJson {
public String ID;
public String Country;
public String Nickname;
public String Picture;
public int State;
public int AgreeAd;
public long CreatedAt;
}
}

View File

@@ -0,0 +1,127 @@
package emu.nebula.server.routes;
import java.util.ArrayList;
import org.jetbrains.annotations.NotNull;
import emu.nebula.game.account.Account;
import emu.nebula.game.account.AccountHelper;
import emu.nebula.server.routes.UserLoginEntity.LoginYostarJson;
import emu.nebula.server.routes.UserLoginEntity.UserDetailJson;
import emu.nebula.server.routes.UserLoginEntity.UserInfoJson;
import emu.nebula.server.routes.UserLoginEntity.UserKeyJson;
import emu.nebula.util.JsonUtils;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import lombok.Getter;
@Getter
public class UserLoginHandler implements Handler {
@Override
public void handle(@NotNull Context ctx) throws Exception {
// Get account from header first
Account account = this.getAccountFromHeader(ctx);
// Check req body for account details
if (account == null) {
account = this.getAccountFromBody(ctx);
}
// Check
if (account == null) {
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result("{\"Code\":100403,\"Data\":{},\"Msg\":\"Error\"}"); // TOKEN_AUTH_FAILED
return;
}
// Create response
var response = new UserLoginEntity();
response.Code = 200;
response.Msg = "OK";
response.Data = new UserDetailJson();
response.Data.Keys = new ArrayList<>();
response.Data.UserInfo = new UserInfoJson();
response.Data.Yostar = new LoginYostarJson();
response.Data.UserInfo.ID = account.getUid();
response.Data.UserInfo.UID2 = 0;
response.Data.UserInfo.PID = "NEBULA";
response.Data.UserInfo.Token = account.getLoginToken();
response.Data.UserInfo.Birthday = "";
response.Data.UserInfo.RegChannel = "pc";
response.Data.UserInfo.TransCode = "";
response.Data.UserInfo.State = 1;
response.Data.UserInfo.DeviceID = "";
response.Data.UserInfo.CreatedAt = account.getCreatedAt();
response.Data.Yostar.ID = account.getUid();
response.Data.Yostar.Country = "US";
response.Data.Yostar.Nickname = account.getNickname();
response.Data.Yostar.Picture = account.getPicture();
response.Data.Yostar.State = 1;
response.Data.Yostar.AgreeAd = 0;
response.Data.Yostar.CreatedAt = account.getCreatedAt();
var key = new UserKeyJson();
key.ID = account.getUid();
key.Type = "yostar";
key.Key = account.getEmail();
key.NickName = account.getEmail();
key.CreatedAt = account.getCreatedAt();
response.Data.Keys.add(key);
// Result
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result(JsonUtils.encode(response, true));
}
protected Account getAccountFromBody(Context ctx) {
// Parse request
var req = JsonUtils.decode(ctx.body(), UserLoginRequestJson.class);
if (req == null || req.OpenID == null || req.Token == null) {
return null;
}
// Get account
return AccountHelper.getAccountByLoginToken(req.Token);
}
protected Account getAccountFromHeader(Context ctx) {
// Parse request
var req = JsonUtils.decode(ctx.header("Authorization"), UserAuthDataJson.class);
if (req == null || req.Head == null || req.Head.Token == null) {
return null;
}
// Get account
return AccountHelper.getAccountByLoginToken(req.Head.Token);
}
@SuppressWarnings("unused")
private static class UserLoginRequestJson {
public String OpenID;
public String Token;
public String Type;
public String UserName;
public String Secret;
public int CheckAccount;
}
@SuppressWarnings("unused")
private static class UserAuthDataJson {
public UserAuthHeadJson Head;
public String Sign;
protected static class UserAuthHeadJson {
public String UID;
public String Token;
}
}
}

View File

@@ -0,0 +1,51 @@
package emu.nebula.server.routes;
import org.jetbrains.annotations.NotNull;
import emu.nebula.game.account.Account;
import emu.nebula.util.JsonUtils;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import lombok.Getter;
@Getter
public class UserSetDataHandler extends UserLoginHandler {
@Override
public void handle(@NotNull Context ctx) throws Exception {
// Get account from header first
Account account = this.getAccountFromHeader(ctx);
// Check
if (account == null) {
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result("{\"Code\":100403,\"Data\":{},\"Msg\":\"Error\"}"); // TOKEN_AUTH_FAILED
return;
}
// Parse request
var req = JsonUtils.decode(ctx.body(), UserSetDataReqJson.class);
if (req.Key == null || req.Value == null) {
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result("{\"Code\":100110,\"Data\":{},\"Msg\":\"Error\"}"); // VALID_FAIL
return;
}
if (req.Key.equals("Nickname")) {
account.setNickname(req.Value);
account.save();
}
// Result
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result("{\"Code\":200,\"Data\":{},\"Msg\":\"OK\"}");
}
@SuppressWarnings("unused")
private static class UserSetDataReqJson {
public String Key;
public String Value;
}
}