diff --git a/pom.xml b/pom.xml
index b1930c8..66abcc9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,11 +38,42 @@
jbcrypt
0.4
-
+
+ com.jsoniter
+ jsoniter
+ 0.9.23
+
+
+ org.projectlombok
+ lombok
+ 1.18.36
+ provided
+
+
+ org.mapdb
+ mapdb
+ 3.0.0
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 1.8
+ 1.8
+
+
+ org.projectlombok
+ lombok
+ 1.18.36
+
+
+
+
maven-assembly-plugin
diff --git a/src/main/java/com/alterdekim/xcraft/auth/SaltNic.java b/src/main/java/com/alterdekim/xcraft/auth/SaltNic.java
index 77d808d..b77a720 100644
--- a/src/main/java/com/alterdekim/xcraft/auth/SaltNic.java
+++ b/src/main/java/com/alterdekim/xcraft/auth/SaltNic.java
@@ -1,29 +1,47 @@
package com.alterdekim.xcraft.auth;
+import com.alterdekim.xcraft.auth.database.User;
+import com.alterdekim.xcraft.auth.database.UserStorage;
+import com.alterdekim.xcraft.auth.request.JoinMinecraftServerRequest;
+import com.alterdekim.xcraft.auth.request.SignUpRequest;
+import com.alterdekim.xcraft.auth.response.*;
+import com.jsoniter.JsonIterator;
+import com.jsoniter.output.JsonStream;
import fi.iki.elonen.NanoHTTPD;
-import org.json.simple.JSONObject;
-import org.json.simple.parser.JSONParser;
-import org.json.simple.parser.ParseException;
-import org.mindrot.jbcrypt.BCrypt;
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
import java.security.NoSuchAlgorithmException;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import java.util.logging.Logger;
+import static com.alterdekim.xcraft.auth.XCraft.PUBLIC_DOMAIN;
import static com.alterdekim.xcraft.auth.XCraft.SERVER_PORT;
public class SaltNic extends NanoHTTPD {
private final Logger logger;
+ private final ConcurrentMap sessions;
+ private final UserStorage storage;
- private final Map sessions;
+ private final Response invalidSession = newFixedLengthResponse(Response.Status.FORBIDDEN, "application/json", "{\"error\":\"ForbiddenOperationException\", \"errorMessage\":\"Invalid session token\", \"cause\": \"ForbiddenOperationException\"}");
+
+ private static final String SKIN_DIRECTORY = "plugins/XCraftAuth/skins";
+ private static final String CAPE_DIRECTORY = "plugins/XCraftAuth/capes";
+ private static final int MAX_FILE_SIZE = 1024 * 1024;
public SaltNic(Logger logger) throws IOException {
super(SERVER_PORT);
this.logger = logger;
- this.sessions = new HashMap<>();
+ this.storage = new UserStorage();
+ this.sessions = new ConcurrentHashMap<>();
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
logger.info("SaltNic session server started on http://localhost:"+SERVER_PORT);
}
@@ -42,23 +60,192 @@ public class SaltNic extends NanoHTTPD {
return handleProfileRequest(session, uri);
} else if (uri.startsWith("/api/register") && method == Method.POST) {
return handleProfileRegistration(session);
+ } else if (Method.POST == method && "/api/set_model".equals(uri)) {
+ return handleSetModel(session);
+ } else if (Method.POST == method && "/api/upload_cape".equals(uri)) {
+ return handleCapeUpload(session);
+ } else if (Method.POST == method && "/api/upload".equals(uri)) {
+ return handleSkinUpload(session);
+ } else if (Method.GET == method && uri.startsWith("/api/skin/s")) {
+ return serveSkinImage(uri.substring(11));
+ } else if (Method.GET == method && uri.startsWith("/api/cape/a")) {
+ return serveCapeImage(uri.substring(11));
}
return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "404 Not Found");
}
+ private Response handleSkinUpload(IHTTPSession session) {
+ try {
+ // Parse multipart form data
+ Map files = new java.util.HashMap<>();
+ session.parseBody(files);
+ String playerUUID = session.getParameters().get("uuid").get(0);
+ String password = session.getParameters().get("password").get(0);
+
+ if (playerUUID == null || password == null || !files.containsKey("skin")) {
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Missing player credentials or skin file");
+ }
+
+ if( this.storage.getUserPassword(playerUUID) == null || !PasswordHasher.checkPassword(password, this.storage.getUserPassword(playerUUID)) ) {
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Invalid credentials");
+ }
+
+ new File(SKIN_DIRECTORY).mkdirs();
+
+ File tempFile = new File(files.get("skin"));
+
+ long fileSize = tempFile.length();
+ if (fileSize > MAX_FILE_SIZE) {
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "File is too large. Max size: 1MB");
+ }
+
+ BufferedImage image = ImageIO.read(tempFile);
+ if (image == null) {
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Invalid image file");
+ }
+
+ int width = image.getWidth();
+ int height = image.getHeight();
+
+ if (!((width == 64 && height == 64) || (width == 64 && height == 32))) {
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Invalid dimensions. Only 64x64 or 64x32 allowed");
+ }
+
+ File skinFile = new File(SKIN_DIRECTORY, playerUUID + ".png");
+
+ Files.copy(tempFile.toPath(), skinFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Skin uploaded successfully for " + playerUUID);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Upload failed: " + e.getMessage());
+ }
+ }
+
+ private Response handleCapeUpload(IHTTPSession session) {
+ try {
+ // Parse multipart form data
+ Map files = new java.util.HashMap<>();
+ session.parseBody(files);
+ String playerUUID = session.getParameters().get("uuid").get(0);
+ String password = session.getParameters().get("password").get(0);
+
+ if (playerUUID == null || password == null || !files.containsKey("cape")) {
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Missing player credentials or skin file");
+ }
+
+ if( this.storage.getUserPassword(playerUUID) == null || !PasswordHasher.checkPassword(password, this.storage.getUserPassword(playerUUID)) ) {
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Invalid credentials");
+ }
+
+ new File(CAPE_DIRECTORY).mkdirs();
+
+ File tempFile = new File(files.get("cape"));
+
+ long fileSize = tempFile.length();
+ if (fileSize > MAX_FILE_SIZE) {
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "File is too large. Max size: 1MB");
+ }
+
+ BufferedImage image = ImageIO.read(tempFile);
+ if (image == null) {
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Invalid image file");
+ }
+
+ int width = image.getWidth();
+ int height = image.getHeight();
+
+ if (!(width == 64 && height == 32)) {
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Invalid dimensions. Only 64x32 allowed");
+ }
+
+ File capeFile = new File(CAPE_DIRECTORY, playerUUID + ".png");
+
+ Files.copy(tempFile.toPath(), capeFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Cape uploaded successfully for " + playerUUID);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Upload failed: " + e.getMessage());
+ }
+ }
+
+ private Response handleSetModel(IHTTPSession session) {
+ try {
+ String playerUUID = session.getParameters().get("uuid").get(0);
+ String password = session.getParameters().get("password").get(0);
+ Boolean model = Boolean.parseBoolean(session.getParameters().get("model").get(0));
+
+ if (playerUUID == null || password == null) {
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Missing player credentials");
+ }
+
+ if( this.storage.getUserPassword(playerUUID) == null || !PasswordHasher.checkPassword(password, this.storage.getUserPassword(playerUUID)) ) {
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Invalid credentials");
+ }
+
+ this.storage.setSkinModel(playerUUID, model ? User.SkinModel.Alex : User.SkinModel.Steve);
+
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Skin model was set successfully for " + playerUUID);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", "Setting failed: " + e.getMessage());
+ }
+ }
+
+ private Response serveCapeImage(String playerName) {
+ File skinFile = new File(CAPE_DIRECTORY, playerName + ".png");
+
+ if (!skinFile.exists()) {
+ return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Cape not found for " + playerName);
+ }
+ try {
+ FileInputStream fis = new FileInputStream(skinFile);
+ return newChunkedResponse(Response.Status.OK, "image/png", fis);
+ } catch (IOException e) {
+ return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Failed to load skin");
+ }
+ }
+
+ private Response serveSkinImage(String playerName) {
+ File skinFile = new File(SKIN_DIRECTORY, playerName + ".png");
+
+ if (!skinFile.exists()) {
+ return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Skin not found for " + playerName);
+ }
+ try {
+ FileInputStream fis = new FileInputStream(skinFile);
+ return newChunkedResponse(Response.Status.OK, "image/png", fis);
+ } catch (IOException e) {
+ return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Failed to load skin");
+ }
+ }
+
+ private MinecraftProperty getTextures(String uuid) {
+ Map textures = new HashMap<>();
+
+ if( new File(SKIN_DIRECTORY, uuid + ".png").exists() ) {
+ MinecraftProfileTexture texture = new MinecraftProfileTexture("http://"+PUBLIC_DOMAIN+":"+SERVER_PORT+"/api/skin/s" + uuid, new HashMap<>());
+ if( storage.getSkinModel(uuid) == User.SkinModel.Alex ) {
+ texture.getMetadata().put("model", "slim");
+ }
+ textures.put(MinecraftProfileTexture.Type.SKIN, texture);
+ }
+
+ if( new File(CAPE_DIRECTORY, uuid + ".png").exists() ) {
+ textures.put(MinecraftProfileTexture.Type.CAPE, new MinecraftProfileTexture("http://"+PUBLIC_DOMAIN+":"+SERVER_PORT+"/api/cape/a" + uuid, new HashMap<>()));
+ }
+
+ MinecraftTexturesPayload minecraftTexturesPayload = new MinecraftTexturesPayload(System.currentTimeMillis(), uuid, this.storage.getUsername(uuid), textures);
+ return new MinecraftProperty("textures", Base64.getEncoder().encodeToString(JsonStream.serialize(minecraftTexturesPayload).getBytes()));
+ }
+
private Response handleProfileRequest(IHTTPSession session, String uri) {
if( uri.length() != 45 ) return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Server error");
String uuid = uri.substring(13);
- if( UserStorage.getUserPassword(uuid) == null ) return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Server error");
- return newFixedLengthResponse(Response.Status.OK, "application/json", "{\n" +
- " \"id\" : \""+uuid+"\",\n" +
- " \"name\" : \"Notch\",\n" +
- " \"properties\" : [ {\n" +
- " \"name\" : \"textures\",\n" +
- " \"value\" : \"ewogICJ0aW1lc3RhbXAiIDogMTc0MjA1ODQ1MDI1MywKICAicHJvZmlsZUlkIiA6ICJmYzE0MzZmZmQ3MDA0NWFmOWMxODNkZjhjODMwMmU5ZiIsCiAgInByb2ZpbGVOYW1lIiA6ICJEYXJ0SmV2ZGVyIiwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzVlMTM1Y2ZkYTgwM2U3ZDQ4NTNhN2M5YjQ5N2JhZjM3YWNlNmZkZGYyYjYyNDI1MWY3YjkwNmYyOTAwZWRiMyIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9LAogICAgIkNBUEUiIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2EyZThkOTdlYzc5MTAwZTkwYTc1ZDM2OWQxYjNiYTgxMjczYzRmODJiYzFiNzM3ZTkzNGVlZDRhODU0YmUxYjYiCiAgICB9CiAgfQp9\"\n" +
- " } ],\n" +
- " \"profileActions\" : [ ]\n" +
- "}");
+ if( this.storage.getUserPassword(uuid) == null ) return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Server error");
+ List properties = Collections.singletonList(getTextures(uuid));
+ String username = this.storage.getUsername(uuid);
+ MinecraftProfilePropertiesResponse profile = new MinecraftProfilePropertiesResponse(uuid, username, properties);
+ return newFixedLengthResponse(Response.Status.OK, "application/json", JsonStream.serialize(profile));
}
private Response handleHasJoinedRequest(IHTTPSession session) {
@@ -66,34 +253,26 @@ public class SaltNic extends NanoHTTPD {
String uuid = UserId.generateUserId(session.getParameters().get("username").get(0));
if (this.sessions.containsKey(uuid) && this.sessions.get(uuid)) {
this.sessions.remove(uuid);
- return newFixedLengthResponse(Response.Status.OK, "application/json", "{\n" +
- " \"id\" : \"" + uuid + "\",\n" +
- " \"name\" : \"" + session.getParameters().get("username").get(0) + "\",\n" +
- " \"properties\" : [ {\n" +
- " \"name\" : \"textures\",\n" +
- " \"value\" : \"ewogICJ0aW1lc3RhbXAiIDogMTc0MjA1ODQ1MDI1MywKICAicHJvZmlsZUlkIiA6ICJmYzE0MzZmZmQ3MDA0NWFmOWMxODNkZjhjODMwMmU5ZiIsCiAgInByb2ZpbGVOYW1lIiA6ICJEYXJ0SmV2ZGVyIiwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzVlMTM1Y2ZkYTgwM2U3ZDQ4NTNhN2M5YjQ5N2JhZjM3YWNlNmZkZGYyYjYyNDI1MWY3YjkwNmYyOTAwZWRiMyIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9LAogICAgIkNBUEUiIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2EyZThkOTdlYzc5MTAwZTkwYTc1ZDM2OWQxYjNiYTgxMjczYzRmODJiYzFiNzM3ZTkzNGVlZDRhODU0YmUxYjYiCiAgICB9CiAgfQp9\"\n" +
- " } ],\n" +
- " \"profileActions\" : [ ]\n" +
- "}");
+ return handleProfileRequest(session, "/api/profile/"+uuid);
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
- return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Server error");
+ return invalidSession;
}
private Response handleProfileRegistration(IHTTPSession session) {
try {
Map files = new HashMap<>();
session.parseBody(files);
- JSONObject json = parseJSON(files.get("postData"));
+ SignUpRequest request = JsonIterator.deserialize(files.get("postData"), SignUpRequest.class);
- if (json == null) {
+ if (request == null) {
return newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/plain", "Invalid JSON format");
}
- String username = (String) json.get("username");
- String password = (String) json.get("password");
+ String username = request.getUsername();
+ String password = request.getPassword();
if (username == null || password == null || password.length() < 3 || username.length() < 3) {
return newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/plain", "Missing username or password");
@@ -101,17 +280,15 @@ public class SaltNic extends NanoHTTPD {
String uuid = UserId.generateUserId(username);
- if( UserStorage.getUserPassword(uuid) != null ) {
+ if( this.storage.getUserPassword(uuid) != null ) {
return newFixedLengthResponse(Response.Status.CONFLICT, "text/plain", "User already exists");
}
- UserStorage.saveUser(uuid, PasswordHasher.hashPassword(password));
+ this.storage.saveUser(uuid, new User(username, PasswordHasher.hashPassword(password), User.SkinModel.Steve));
- JSONObject response = new JSONObject();
- response.put("uuid", uuid);
- return newFixedLengthResponse(Response.Status.OK, "text/plain", response.toJSONString());
+ return newFixedLengthResponse(Response.Status.OK, "text/plain", JsonStream.serialize(new SignUpResponse(uuid)));
} catch (Exception e) {
- logger.warning("Error while processing sign up request from client: " + e.getMessage());
+ logger.info("Error while processing sign up request from client: " + e.getMessage());
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Server error: " + e.getMessage());
}
}
@@ -120,41 +297,37 @@ public class SaltNic extends NanoHTTPD {
try {
Map files = new HashMap<>();
session.parseBody(files);
- JSONObject json = parseJSON(files.get("postData"));
+ JoinMinecraftServerRequest joinRequest = JsonIterator.deserialize(files.get("postData"), JoinMinecraftServerRequest.class);
- if (json == null) {
- return newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/plain", "Invalid JSON format");
+ if (joinRequest == null) {
+ return invalidSession;
}
- String username = (String) json.get("selectedProfile");
- String sessionToken = (String) json.get("accessToken");
+ String username = joinRequest.getSelectedProfile();
+ String sessionToken = joinRequest.getAccessToken();
if (username == null || sessionToken == null) {
- return newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/plain", "Missing selectedProfile or accessToken");
+ return invalidSession;
}
- boolean validSession = PasswordHasher.checkPassword(sessionToken, UserStorage.getUserPassword(username));
+ boolean validSession = PasswordHasher.checkPassword(sessionToken, this.storage.getUserPassword(username));
if (validSession) {
this.sessions.put(username, true);
return newFixedLengthResponse(Response.Status.OK, "application/json", "{}");
} else {
this.sessions.put(username, false);
- return newFixedLengthResponse(Response.Status.UNAUTHORIZED, "application/json","{\"status\":\"error\", \"message\":\"Invalid session token\"}");
+ return invalidSession;
}
} catch (Exception e) {
- logger.warning("Error while processing join request from client: " + e.getMessage());
- return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Server error: " + e.getMessage());
+ logger.info("Error while processing join request from client: " + e.getMessage());
+ return invalidSession;
}
}
- private JSONObject parseJSON(String jsonData) {
- try {
- JSONParser parser = new JSONParser();
- return (JSONObject) parser.parse(jsonData);
- } catch (ParseException e) {
- e.printStackTrace();
- return null;
- }
+ @Override
+ public void stop() {
+ super.stop();
+ this.storage.close();
}
}
diff --git a/src/main/java/com/alterdekim/xcraft/auth/UserStorage.java b/src/main/java/com/alterdekim/xcraft/auth/UserStorage.java
deleted file mode 100644
index 5ec0528..0000000
--- a/src/main/java/com/alterdekim/xcraft/auth/UserStorage.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.alterdekim.xcraft.auth;
-
-import org.json.simple.JSONObject;
-import org.json.simple.parser.JSONParser;
-import org.json.simple.parser.ParseException;
-
-import java.io.File;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
-
-public class UserStorage {
- private static final File USER_FILE = new File("plugins/XCraftAuth/users.json");
-
- public static void saveUser(String username, String hashedPassword) {
- JSONObject users = loadUsers();
- users.put(username, hashedPassword);
-
- try (FileWriter writer = new FileWriter(USER_FILE)) {
- writer.write(users.toJSONString());
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- public static String getUserPassword(String username) {
- JSONObject users = loadUsers();
- return (String) users.get(username);
- }
-
- private static JSONObject loadUsers() {
- if (!USER_FILE.exists()) {
- return new JSONObject();
- }
-
- try (FileReader reader = new FileReader(USER_FILE)) {
- JSONParser parser = new JSONParser();
- return (JSONObject) parser.parse(reader);
- } catch (IOException | ParseException e) {
- return new JSONObject();
- }
- }
-}
diff --git a/src/main/java/com/alterdekim/xcraft/auth/XCraft.java b/src/main/java/com/alterdekim/xcraft/auth/XCraft.java
index 3f40a43..9e9dfc1 100644
--- a/src/main/java/com/alterdekim/xcraft/auth/XCraft.java
+++ b/src/main/java/com/alterdekim/xcraft/auth/XCraft.java
@@ -12,7 +12,9 @@ public class XCraft extends JavaPlugin {
private static SaltNic server = null;
- public static final int SERVER_PORT = 8999;
+ public static int SERVER_PORT = 8999;
+ public static String PUBLIC_DOMAIN = "localhost";
+ public static Boolean USE_HTTPS = false;
@Override
public void onEnable() {
@@ -20,17 +22,24 @@ public class XCraft extends JavaPlugin {
if( server == null ) {
try {
getLogger().info("Starting SaltNic server...");
+ SERVER_PORT = getConfig().getInt("public_port");
+ PUBLIC_DOMAIN = getConfig().getString("public_domain");
+ USE_HTTPS = getConfig().getBoolean("use_https");
server = new SaltNic(getLogger());
} catch (IOException e) {
getLogger().severe("Failed to start SaltNic server: " + e.getMessage());
}
}
getLogger().info("Patching AuthLib URLs...");
- try {
- patchAuthLib();
- getLogger().info("AuthLib URLs patched successfully!");
- } catch (Exception e) {
- getLogger().severe("Failed to patch AuthLib: " + e.getMessage());
+ while(true) {
+ try {
+ patchAuthLib();
+ getLogger().info("AuthLib URLs patched successfully!");
+ return;
+ } catch (Exception e) {
+ e.printStackTrace();
+ getLogger().severe("Failed to patch AuthLib: " + e.getMessage());
+ }
}
}
diff --git a/src/main/java/com/alterdekim/xcraft/auth/database/User.java b/src/main/java/com/alterdekim/xcraft/auth/database/User.java
new file mode 100644
index 0000000..de9aba5
--- /dev/null
+++ b/src/main/java/com/alterdekim/xcraft/auth/database/User.java
@@ -0,0 +1,20 @@
+package com.alterdekim.xcraft.auth.database;
+
+import lombok.*;
+
+@Setter
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class User {
+ private String username;
+ private String password;
+ private SkinModel model;
+
+ @Getter
+ @RequiredArgsConstructor
+ public enum SkinModel {
+ Steve,
+ Alex
+ }
+}
diff --git a/src/main/java/com/alterdekim/xcraft/auth/database/UserStorage.java b/src/main/java/com/alterdekim/xcraft/auth/database/UserStorage.java
new file mode 100644
index 0000000..dc43a64
--- /dev/null
+++ b/src/main/java/com/alterdekim/xcraft/auth/database/UserStorage.java
@@ -0,0 +1,56 @@
+package com.alterdekim.xcraft.auth.database;
+
+import com.jsoniter.JsonIterator;
+import com.jsoniter.output.JsonStream;
+import org.mapdb.DB;
+import org.mapdb.DBMaker;
+import org.mapdb.Serializer;
+
+import java.io.File;
+import java.util.concurrent.ConcurrentMap;
+
+public class UserStorage {
+ private static final File DB_FILE = new File("plugins/XCraftAuth/users.db");
+ private final DB db = DBMaker.fileDB(DB_FILE).fileMmapEnable().make();
+ private final ConcurrentMap users = db.hashMap("users", Serializer.STRING, Serializer.STRING).createOrOpen();
+
+ public void saveUser(String uuid, User user) {
+ this.users.put(uuid, JsonStream.serialize(user));
+ }
+
+ public User.SkinModel getSkinModel(String uuid) {
+ if( this.users.containsKey(uuid) ) {
+ User user = JsonIterator.deserialize(this.users.get(uuid), User.class);
+ return user.getModel();
+ }
+ return User.SkinModel.Steve;
+ }
+
+ public String getUsername(String uuid) {
+ if( this.users.containsKey(uuid) ) {
+ User user = JsonIterator.deserialize(this.users.get(uuid), User.class);
+ return user.getUsername();
+ }
+ return null;
+ }
+
+ public String getUserPassword(String uuid) {
+ if (this.users.containsKey(uuid)) {
+ User user = JsonIterator.deserialize(this.users.get(uuid), User.class);
+ return user.getPassword();
+ }
+ return null;
+ }
+
+ public void close() {
+ db.close();
+ }
+
+ public void setSkinModel(String uuid, User.SkinModel skinModel) {
+ if (this.users.containsKey(uuid)) {
+ User user = JsonIterator.deserialize(this.users.get(uuid), User.class);
+ user.setModel(skinModel);
+ this.saveUser(uuid, user);
+ }
+ }
+}
diff --git a/src/main/java/com/alterdekim/xcraft/auth/request/JoinMinecraftServerRequest.java b/src/main/java/com/alterdekim/xcraft/auth/request/JoinMinecraftServerRequest.java
new file mode 100644
index 0000000..79b48f8
--- /dev/null
+++ b/src/main/java/com/alterdekim/xcraft/auth/request/JoinMinecraftServerRequest.java
@@ -0,0 +1,14 @@
+package com.alterdekim.xcraft.auth.request;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor
+@Getter
+@NoArgsConstructor
+public class JoinMinecraftServerRequest {
+ public String accessToken;
+ public String selectedProfile;
+ public String serverId;
+}
diff --git a/src/main/java/com/alterdekim/xcraft/auth/request/SignUpRequest.java b/src/main/java/com/alterdekim/xcraft/auth/request/SignUpRequest.java
new file mode 100644
index 0000000..565b936
--- /dev/null
+++ b/src/main/java/com/alterdekim/xcraft/auth/request/SignUpRequest.java
@@ -0,0 +1,13 @@
+package com.alterdekim.xcraft.auth.request;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@AllArgsConstructor
+@NoArgsConstructor
+public class SignUpRequest {
+ private String username;
+ private String password;
+}
diff --git a/src/main/java/com/alterdekim/xcraft/auth/response/MinecraftProfilePropertiesResponse.java b/src/main/java/com/alterdekim/xcraft/auth/response/MinecraftProfilePropertiesResponse.java
new file mode 100644
index 0000000..2cfdde0
--- /dev/null
+++ b/src/main/java/com/alterdekim/xcraft/auth/response/MinecraftProfilePropertiesResponse.java
@@ -0,0 +1,14 @@
+package com.alterdekim.xcraft.auth.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.List;
+
+@AllArgsConstructor
+@Getter
+public class MinecraftProfilePropertiesResponse {
+ private String id;
+ private String name;
+ private List properties;
+}
\ No newline at end of file
diff --git a/src/main/java/com/alterdekim/xcraft/auth/response/MinecraftProfileTexture.java b/src/main/java/com/alterdekim/xcraft/auth/response/MinecraftProfileTexture.java
new file mode 100644
index 0000000..46fa00e
--- /dev/null
+++ b/src/main/java/com/alterdekim/xcraft/auth/response/MinecraftProfileTexture.java
@@ -0,0 +1,20 @@
+package com.alterdekim.xcraft.auth.response;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Map;
+
+
+@Getter
+@RequiredArgsConstructor
+public class MinecraftProfileTexture {
+ private final String url;
+ private final Map metadata;
+
+ public enum Type {
+ SKIN,
+ CAPE;
+ }
+}
+
diff --git a/src/main/java/com/alterdekim/xcraft/auth/response/MinecraftProperty.java b/src/main/java/com/alterdekim/xcraft/auth/response/MinecraftProperty.java
new file mode 100644
index 0000000..ea03a89
--- /dev/null
+++ b/src/main/java/com/alterdekim/xcraft/auth/response/MinecraftProperty.java
@@ -0,0 +1,11 @@
+package com.alterdekim.xcraft.auth.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public class MinecraftProperty {
+ private String name;
+ private String value;
+}
diff --git a/src/main/java/com/alterdekim/xcraft/auth/response/MinecraftTexturesPayload.java b/src/main/java/com/alterdekim/xcraft/auth/response/MinecraftTexturesPayload.java
new file mode 100644
index 0000000..21f54dd
--- /dev/null
+++ b/src/main/java/com/alterdekim/xcraft/auth/response/MinecraftTexturesPayload.java
@@ -0,0 +1,15 @@
+package com.alterdekim.xcraft.auth.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Map;
+
+@AllArgsConstructor
+@Getter
+public class MinecraftTexturesPayload {
+ private long timestamp;
+ private String profileId;
+ private String profileName;
+ private Map textures;
+}
\ No newline at end of file
diff --git a/src/main/java/com/alterdekim/xcraft/auth/response/SignUpResponse.java b/src/main/java/com/alterdekim/xcraft/auth/response/SignUpResponse.java
new file mode 100644
index 0000000..972734f
--- /dev/null
+++ b/src/main/java/com/alterdekim/xcraft/auth/response/SignUpResponse.java
@@ -0,0 +1,10 @@
+package com.alterdekim.xcraft.auth.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public class SignUpResponse {
+ private String uuid;
+}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index e69de29..f08c3d2 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -0,0 +1,3 @@
+public_domain: "localhost"
+public_port: 8999
+use_https: false
\ No newline at end of file