新增用于生成无符号uuid的工具

密码哈西改为数据库混淆 不在前端处理

修正了Yag元数据中因为使用不同json库导致报错的问题

修正了profile数据库查询重复query导致死循环的问题

新增了邮箱唯一性验证
This commit is contained in:
张宇衡 2022-12-03 15:39:33 +08:00
parent 5e2e5a5b27
commit 052e2b8d11
18 changed files with 123 additions and 103 deletions

View File

@ -6,14 +6,13 @@ import site.deercloud.identityverification.IdentityVerification;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
public class AFKTracker {
public static final long IGNORES_AFK = -1L;
private final Set<UUID> usedAFKCommand;
private final Set<String> usedAFKCommand;
private Long afkThresholdMs;
private final IdentityVerification plugin;
@ -30,26 +29,26 @@ public class AFKTracker {
return afkThresholdMs;
}
public void hasIgnorePermission(UUID playerUUID) {
public void hasIgnorePermission(String playerUUID) {
storeLastMovement(playerUUID, IGNORES_AFK);
}
private void storeLastMovement(UUID playerUUID, long time) {
private void storeLastMovement(String playerUUID, long time) {
GameSessionCache.getCacheSession(playerUUID)
.ifPresent(gameSession -> gameSession.setLastMovement(time));
}
private long getLastMovement(UUID playerUUID, long time) {
private long getLastMovement(String playerUUID, long time) {
return getLastMovement(playerUUID)
.orElse(time);
}
private Optional<Long> getLastMovement(UUID playerUUID) {
private Optional<Long> getLastMovement(String playerUUID) {
return GameSessionCache.getCacheSession(playerUUID)
.map(GameSession::getLastMovement);
}
public void usedAfkCommand(UUID playerUUID, long time) {
public void usedAfkCommand(String playerUUID, long time) {
long lastMoved = getLastMovement(playerUUID, time);
if (lastMoved == IGNORES_AFK) {
return;
@ -58,7 +57,7 @@ public class AFKTracker {
storeLastMovement(playerUUID, time - getAfkThreshold());
}
public long performedAction(UUID playerUUID, long time) {
public long performedAction(String playerUUID, long time) {
long lastMoved = getLastMovement(playerUUID, time);
// Ignore afk permission
if (lastMoved == IGNORES_AFK) {
@ -83,13 +82,13 @@ public class AFKTracker {
}
}
public long loggedOut(UUID uuid, long time) {
public long loggedOut(String uuid, long time) {
long timeAFK = performedAction(uuid, time);
usedAFKCommand.remove(uuid);
return timeAFK;
}
public boolean isAfk(UUID playerUUID) {
public boolean isAfk(String playerUUID) {
long time = System.currentTimeMillis();
Optional<Long> lastMoved = getLastMovement(playerUUID);

View File

@ -8,17 +8,17 @@ import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class GameSessionCache {
private static final Map<UUID, GameSession> ACTIVE_SESSIONS = new ConcurrentHashMap<>();
private static final Map<String, GameSession> ACTIVE_SESSIONS = new ConcurrentHashMap<>();
public static Optional<GameSession> getCacheSession(UUID playerUUID) {
public static Optional<GameSession> getCacheSession(String playerUUID) {
return Optional.ofNullable(ACTIVE_SESSIONS.get(playerUUID));
}
public static void addSession(UUID playerUUID) {
public static void addSession(String playerUUID) {
ACTIVE_SESSIONS.put(playerUUID, new GameSession());
}
public static void removeSession(UUID playerUUID) {
public static void removeSession(String playerUUID) {
// TODO: save to database
ACTIVE_SESSIONS.remove(playerUUID);

View File

@ -5,14 +5,12 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import site.deercloud.identityverification.Controller.AFKTracker;
import site.deercloud.identityverification.Controller.GameSessionCache;
import site.deercloud.identityverification.SQLite.BanListDAO;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.SQLite.WhiteListDAO;
import site.deercloud.identityverification.Controller.ConfigManager;
import site.deercloud.identityverification.Utils.UnsignedUUID;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
@ -31,18 +29,18 @@ public class Events implements Listener {
String ban_time = sdf.format(BanListDAO.getBanTimeById(ban_record_id));
player.kickPlayer("你已被封禁,请联系管理员。原因:[ " + ban_reason + " ]" + " 至:" + ban_time);
}
GameSessionCache.addSession(event.getPlayer().getUniqueId());
GameSessionCache.addSession(UnsignedUUID.UnUUIDof(event.getPlayer()));
}
@EventHandler
public void onPlayerQuit(PlayerJoinEvent event) throws SQLException {
GameSessionCache.removeSession(event.getPlayer().getUniqueId());
GameSessionCache.removeSession(UnsignedUUID.UnUUIDof(event.getPlayer()));
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) throws SQLException {
Player player = event.getPlayer();
IdentityVerification.getAfkTracker().performedAction(player.getUniqueId(), System.currentTimeMillis());
IdentityVerification.getAfkTracker().performedAction(UnsignedUUID.UnUUIDof(event.getPlayer()), System.currentTimeMillis());
}
ConfigManager configManager = IdentityVerification.getInstance().getConfigManager();

View File

@ -4,11 +4,13 @@ import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.Controller.EmailCodeCache;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.Utils.EmailSender;
import site.deercloud.identityverification.Utils.MyLogger;
import site.deercloud.identityverification.Utils.RandomCode;
import java.io.IOException;
import java.sql.SQLException;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.getBody;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.jsonResponse;
@ -25,8 +27,11 @@ public class GetEmailCode implements HttpHandler {
JSONObject request = getBody(exchange);
String email = request.getString("email");
// TODO: 验证邮箱唯一性
// 验证邮箱唯一性
if (UserDAO.selectByEmail(email) != null) {
jsonResponse(exchange, 400, "此邮箱已被注册。", null);
return;
}
if (!EmailCodeCache.isEmailCodeExpired(email)) {
jsonResponse(exchange, 500, "禁止频繁操作!", null);
return;
@ -40,7 +45,7 @@ public class GetEmailCode implements HttpHandler {
EmailCodeCache.addEmailCode(email, code);
jsonResponse(exchange, 200, "发送成功,请在五分钟内完成注册。", null);
} catch (IOException e) {
} catch (IOException | SQLException e) {
exchange.close();
MyLogger.debug(e);
}

View File

@ -12,8 +12,8 @@ import site.deercloud.identityverification.SQLite.InviteRelationDAO;
import site.deercloud.identityverification.SQLite.ProfileDAO;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import site.deercloud.identityverification.Utils.UnsignedUUID;
import java.util.UUID;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.getBody;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.jsonResponse;
@ -40,8 +40,11 @@ public class Registration implements HttpHandler {
String profile_name = jsonObject.getString("profile_name");
// TODO: 验证邮箱唯一性
// 验证邮箱唯一性
if (UserDAO.selectByEmail(email) != null) {
jsonResponse(exchange, 400, "此邮箱已被注册。", null);
return;
}
// 验证邮箱验证码
if (EmailCodeCache.isEmailCodeExpired(email)) {
jsonResponse(exchange, 500, "邮箱验证码无效,请重新获取。", null);
@ -61,7 +64,7 @@ public class Registration implements HttpHandler {
jsonResponse(exchange, 400, "邀请码不存在或已被使用!", null);
return;
}
String new_uuid = UUID.randomUUID().toString();
String new_uuid = UnsignedUUID.GenerateUUID();
String inviteCodeOwner = InviteCodeDAO.getInviterUUID(inviteCode);
// 创建邀请关系
InviteRelationDAO.insert(new_uuid, inviteCodeOwner, System.currentTimeMillis());
@ -81,9 +84,9 @@ public class Registration implements HttpHandler {
// 创建一个默认角色
Profile profile = new Profile();
profile.name = profile_name;
profile.uuid = UUID.randomUUID().toString();
profile.uuid = UnsignedUUID.GenerateUUID();
profile.belongTo = user.uuid;
Texture texture = Texture.newDefault(profile.uuid);
Texture texture = Texture.newDefault(profile.name);
profile.textures = texture.serialWithBase64();
profile.textures_signature = texture.sign();
ProfileDAO.insert(profile);

View File

@ -1,5 +1,6 @@
package site.deercloud.identityverification.HttpServer.Yggdrasil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.gson.JsonArray;
import com.sun.net.httpserver.HttpExchange;
@ -7,35 +8,40 @@ import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.HttpServer.model.Response;
import site.deercloud.identityverification.IdentityVerification;
import site.deercloud.identityverification.Controller.ConfigManager;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
public class MetaData implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("GET")) {
Response.err_method_not_allowed(exchange);
return;
public void handle(HttpExchange exchange){
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("GET")) {
Response.err_method_not_allowed(exchange);
return;
}
ConfigManager configManager = IdentityVerification.getInstance().getConfigManager();
JSONObject meta_json = new JSONObject();
meta_json.put("serverName", configManager.getServerName());
meta_json.put("implementationName", configManager.getImplementationName());
meta_json.put("implementationVersion", configManager.getImplementationVersion());
JSONObject links_json = new JSONObject();
links_json.put("homepage", configManager.getHomePageUrl());
links_json.put("register", configManager.getRegisterUrl());
meta_json.put("links", links_json);
JSONArray skinDomains_json = new JSONArray();
skinDomains_json.addAll(configManager.getSkinDomains());
JSONObject response_json = new JSONObject();
response_json.put("meta", meta_json);
response_json.put("skinDomains", skinDomains_json);
response_json.put("signaturePublickey", configManager.getSignaturePublicKey());
Response.success_200(exchange, response_json);
} catch (Exception e) {
exchange.close();
MyLogger.debug(e);
}
ConfigManager configManager = IdentityVerification.getInstance().getConfigManager();
JSONObject meta_json = new JSONObject();
meta_json.put("serverName", configManager.getServerName());
meta_json.put("implementationName", configManager.getImplementationName());
meta_json.put("implementationVersion", configManager.getImplementationVersion());
JSONObject links_json = new JSONObject();
links_json.put("homepage", configManager.getHomePageUrl());
links_json.put("register", configManager.getRegisterUrl());
meta_json.put("links", links_json);
JsonArray skinDomains_json = new JsonArray();
for (String domain : configManager.getSkinDomains()) {
skinDomains_json.add(domain);
}
JSONObject response_json = new JSONObject();
response_json.put("meta", meta_json);
response_json.put("skinDomains", skinDomains_json);
response_json.put("signaturePublickey", configManager.getSignaturePublicKey());
Response.success_200(exchange, response_json);
}
}

View File

@ -12,12 +12,9 @@ import site.deercloud.identityverification.SQLite.ProfileDAO;
import site.deercloud.identityverification.SQLite.TokenDAO;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import site.deercloud.identityverification.Utils.UnsignedUUID;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.UUID;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.getBody;
import static site.deercloud.identityverification.HttpServer.model.Response.*;
@ -45,12 +42,13 @@ public class Authenticate implements HttpHandler {
if (!UserDAO.checkPassword(username, password)) {
err_password_wrong(exchange, "密码错误,或短时间内多次登录失败而被暂时禁止登录");
}
MyLogger.debug("密码验证通过" + username);
User user = UserDAO.selectByEmail(username);
MyLogger.debug(user.serialToJSONObject().toString());
if (clientToken == null) {
clientToken = UUID.randomUUID().toString();
clientToken = UnsignedUUID.GenerateUUID();
}
String accessToken = UUID.randomUUID().toString();
String accessToken = UnsignedUUID.GenerateUUID();
// 颁发令牌
Token token = new Token();
@ -88,7 +86,7 @@ public class Authenticate implements HttpHandler {
response.put("user", user.serialToJSONObject());
}
Response.success_200(exchange, response);
} catch (IOException | SQLException e) {
} catch (Exception e) {
exchange.close();
MyLogger.debug(e);
}

View File

@ -10,12 +10,12 @@ import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.SQLite.TokenDAO;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import site.deercloud.identityverification.Utils.UnsignedUUID;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Objects;
import java.util.UUID;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.getBody;
@ -50,7 +50,7 @@ public class Refresh implements HttpHandler {
// 删除原先的令牌 并生成新的令牌
TokenDAO.deleteByAccessToken(accessToken);
new_token.accessToken = UUID.randomUUID().toString();
new_token.accessToken = UnsignedUUID.GenerateUUID();
new_token.expiresAt = System.currentTimeMillis() + 432000000;
// 选择角色的操作 要求原令牌所绑定的角色为空
if (!body.getJSONObject("selectedProfile").isEmpty()) {

View File

@ -56,8 +56,7 @@ public class Response {
}
public static void success_no_content(HttpExchange exchange) throws IOException {
exchange.sendResponseHeaders(204, "No Content".getBytes().length);
exchange.getResponseBody().write("No Content".getBytes());
exchange.sendResponseHeaders(204, -1);
exchange.getResponseBody().close();
}

View File

@ -3,9 +3,9 @@ package site.deercloud.identityverification.HttpServer.model;
import com.alibaba.fastjson.JSONObject;
import site.deercloud.identityverification.IdentityVerification;
import site.deercloud.identityverification.Utils.SignatureUtil;
import site.deercloud.identityverification.Utils.UnsignedUUID;
import java.util.Base64;
import java.util.UUID;
public class Texture {
public String profileId;
@ -17,7 +17,7 @@ public class Texture {
public static Texture newDefault(String Name) {
Texture texture = new Texture();
texture.profileName = Name;
texture.profileId = UUID.randomUUID().toString();
texture.profileId = UnsignedUUID.GenerateUUID();
texture.timestamp = System.currentTimeMillis();
texture.skinUrl = "https://textures.minecraft.net/texture/60a5bd016b3c9a1b9272e4929e30827a67be4ebb219017adbbc4a4d22ebd5b1";
texture.capeUrl = null;

View File

@ -12,11 +12,9 @@ import site.deercloud.identityverification.Controller.ConfigManager;
import site.deercloud.identityverification.Utils.FileToString;
import site.deercloud.identityverification.Utils.MyLogger;
import site.deercloud.identityverification.Controller.GameSessionCache;
import site.deercloud.identityverification.Utils.UnsignedUUID;
import java.io.FileReader;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.UUID;
public final class IdentityVerification extends JavaPlugin {
@ -36,8 +34,8 @@ public final class IdentityVerification extends JavaPlugin {
// 生成控制台用户
User consoleUser = new User();
consoleUser.email = "console@mc.com";
consoleUser.uuid = UUID.randomUUID().toString();
consoleUser.password = String.valueOf("123456".hashCode());
consoleUser.uuid = UnsignedUUID.GenerateUUID();
consoleUser.password = "123456";
UserDAO.insert(consoleUser);
}
} catch (SQLException e) {

View File

@ -1,6 +1,7 @@
package site.deercloud.identityverification.SQLite;
import site.deercloud.identityverification.HttpServer.model.InviteCode;
import site.deercloud.identityverification.Utils.MyLogger;
import java.sql.*;
import java.util.HashSet;

View File

@ -1,9 +1,11 @@
package site.deercloud.identityverification.SQLite;
import site.deercloud.identityverification.HttpServer.model.Profile;
import site.deercloud.identityverification.Utils.MyLogger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
@ -54,15 +56,16 @@ public class ProfileDAO {
String sql = "SELECT * FROM profile WHERE uuid = ?";
PreparedStatement preparedStatement = SqlManager.session.prepareStatement(sql);
preparedStatement.setString(1, uuid);
ResultSet resultSet = preparedStatement.executeQuery();
Profile profile = new Profile();
if (preparedStatement.executeQuery().next()) {
profile.uuid = preparedStatement.getResultSet().getString("uuid");
profile.belongTo = preparedStatement.getResultSet().getString("belong_to");
profile.name = preparedStatement.getResultSet().getString("name");
profile.textures = preparedStatement.getResultSet().getString("textures");
profile.textures_signature = preparedStatement.getResultSet().getString("textures_signature");
profile.uploadableTextures = preparedStatement.getResultSet().getString("uploadableTextures");
profile.uploadableTextures_signature = preparedStatement.getResultSet().getString("uploadableTextures_signature");
if (resultSet.next()) {
profile.uuid = resultSet.getString("uuid");
profile.belongTo = resultSet.getString("belong_to");
profile.name = resultSet.getString("name");
profile.textures = resultSet.getString("textures");
profile.textures_signature = resultSet.getString("textures_signature");
profile.uploadableTextures = resultSet.getString("uploadableTextures");
profile.uploadableTextures_signature = resultSet.getString("uploadableTextures_signature");
return profile;
}
return null;
@ -73,16 +76,18 @@ public class ProfileDAO {
PreparedStatement preparedStatement = SqlManager.session.prepareStatement(sql);
preparedStatement.setString(1, belongTo);
ArrayList<Profile> profiles = new ArrayList<>();
while (preparedStatement.executeQuery().next()) {
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
Profile profile = new Profile();
profile.uuid = preparedStatement.executeQuery().getString("uuid");
profile.uuid = resultSet.getString("uuid");
profile.belongTo = belongTo;
profile.name = preparedStatement.executeQuery().getString("name");
profile.textures = preparedStatement.executeQuery().getString("textures");
profile.textures_signature = preparedStatement.executeQuery().getString("textures_signature");
profile.uploadableTextures = preparedStatement.executeQuery().getString("uploadableTextures");
profile.uploadableTextures_signature = preparedStatement.executeQuery().getString("uploadableTextures_signature");
profile.name = resultSet.getString("name");
profile.textures = resultSet.getString("textures");
profile.textures_signature = resultSet.getString("textures_signature");
profile.uploadableTextures = resultSet.getString("uploadableTextures");
profile.uploadableTextures_signature = resultSet.getString("uploadableTextures_signature");
profiles.add(profile);
MyLogger.debug(profile.serialToJSONObject(true, true).toString());
}
return profiles;
}

View File

@ -2,17 +2,14 @@ package site.deercloud.identityverification.SQLite;
import site.deercloud.identityverification.HttpServer.model.User;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.*;
public class UserDAO {
public static void createTable() throws SQLException {
String sql = "CREATE TABLE IF NOT EXISTS user (\n"
+ " uuid text PRIMARY KEY,\n" // UUID
+ " email text NOT NULL,\n" // 邮箱
+ " password text NOT NULL,\n" // 密码
+ " password integer NOT NULL,\n" // 密码
+ " create_time integer NOT NULL,\n" // 数据创建时间
+ " update_time integer NOT NULL\n" // 数据更新时间
+ ");";
@ -26,7 +23,7 @@ public class UserDAO {
PreparedStatement prep = SqlManager.session.prepareStatement(sql);
prep.setString(1, user.uuid);
prep.setString(2, user.email);
prep.setString(3, user.password);
prep.setLong(3, user.password.hashCode());
prep.setLong(4, System.currentTimeMillis());
prep.setLong(5, System.currentTimeMillis());
prep.executeUpdate();
@ -36,7 +33,7 @@ public class UserDAO {
String sql = "UPDATE user SET email = ?, password = ?, update_time = ? WHERE uuid = ?";
PreparedStatement prep = SqlManager.session.prepareStatement(sql);
prep.setString(1, user.email);
prep.setString(2, user.password);
prep.setLong(2, user.password.hashCode());
prep.setLong(3, System.currentTimeMillis());
prep.setString(4, user.uuid);
prep.executeUpdate();
@ -56,7 +53,6 @@ public class UserDAO {
User user = new User();
user.uuid = uuid;
user.email = prep.executeQuery().getString("email");
user.password = prep.executeQuery().getString("password");
user.createTime = prep.executeQuery().getLong("create_time");
user.updateTime = prep.executeQuery().getLong("update_time");
return user;
@ -66,13 +62,13 @@ public class UserDAO {
String sql = "SELECT * FROM user WHERE email = ?";
PreparedStatement prep = SqlManager.session.prepareStatement(sql);
prep.setString(1, email);
if (prep.executeQuery().next()) {
ResultSet rs = prep.executeQuery();
if (rs.next()) {
User user = new User();
user.uuid = prep.executeQuery().getString("uuid");
user.uuid = rs.getString("uuid");
user.email = email;
user.password = prep.executeQuery().getString("password");
user.createTime = prep.executeQuery().getLong("create_time");
user.updateTime = prep.executeQuery().getLong("update_time");
user.createTime = rs.getLong("create_time");
user.updateTime = rs.getLong("update_time");
return user;
} else {
return null;
@ -82,8 +78,9 @@ public class UserDAO {
public static Boolean checkPassword(String email, String password) throws SQLException {
String sql = "SELECT * FROM user WHERE email = ?";
PreparedStatement prep = SqlManager.session.prepareStatement(sql);
prep.setString(1, email);
if (prep.executeQuery().next()) {
return prep.executeQuery().getString("password").equals(password);
return prep.executeQuery().getLong("password") == password.hashCode();
} else {
return false;
}

View File

@ -1,7 +1,6 @@
package site.deercloud.identityverification.Utils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
public class FileToString {

View File

@ -8,6 +8,7 @@ import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import org.apache.commons.codec.binary.Base64;
// 作者: https://www.jianshu.com/p/c7d09ed656b1
public class SignatureUtil {
private final static String SIGN_TYPE_RSA = "RSA";
private final static String SIGN_ALGORITHMS = "SHA1WithRSA";

View File

@ -0,0 +1,13 @@
package site.deercloud.identityverification.Utils;
import org.bukkit.entity.Player;
public class UnsignedUUID {
public static String GenerateUUID() {
return java.util.UUID.randomUUID().toString().replace("-", "");
}
public static String UnUUIDof(Player player) {
return player.getUniqueId().toString().replace("-", "");
}
}

View File

@ -5,10 +5,8 @@ import com.alibaba.fastjson.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
public class Utils {