commit 914557babb6c103a9d9a01ea9bd1d1c30d773adb Author: alterwain Date: Sun Mar 16 17:27:30 2025 +0300 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..b1930c8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + com.alterdekim.xcraft.auth + xcraft-auth + 1.0-SNAPSHOT + + + 1.8 + 1.8 + UTF-8 + + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + + + + org.spigotmc + spigot-api + 1.12.2-R0.1-SNAPSHOT + provided + + + org.nanohttpd + nanohttpd + 2.3.1 + + + org.mindrot + jbcrypt + 0.4 + + + + + + + + maven-assembly-plugin + + + package + + single + + + + + + jar-with-dependencies + + + + + + \ No newline at end of file diff --git a/src/main/java/com/alterdekim/xcraft/auth/PasswordHasher.java b/src/main/java/com/alterdekim/xcraft/auth/PasswordHasher.java new file mode 100644 index 0000000..7871f50 --- /dev/null +++ b/src/main/java/com/alterdekim/xcraft/auth/PasswordHasher.java @@ -0,0 +1,13 @@ +package com.alterdekim.xcraft.auth; + +import org.mindrot.jbcrypt.BCrypt; + +public class PasswordHasher { + public static String hashPassword(String plainPassword) { + return BCrypt.hashpw(plainPassword, BCrypt.gensalt(12)); + } + + public static boolean checkPassword(String plainPassword, String hashedPassword) { + return BCrypt.checkpw(plainPassword, hashedPassword); + } +} diff --git a/src/main/java/com/alterdekim/xcraft/auth/SaltNic.java b/src/main/java/com/alterdekim/xcraft/auth/SaltNic.java new file mode 100644 index 0000000..fd79cf4 --- /dev/null +++ b/src/main/java/com/alterdekim/xcraft/auth/SaltNic.java @@ -0,0 +1,121 @@ +package com.alterdekim.xcraft.auth; + +import fi.iki.elonen.NanoHTTPD; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Logger; + +import static com.alterdekim.xcraft.auth.XCraft.SERVER_PORT; + +public class SaltNic extends NanoHTTPD { + + private final Logger logger; + + private final Map sessions; + + public SaltNic(Logger logger) throws IOException { + super(SERVER_PORT); + this.logger = logger; + this.sessions = new HashMap<>(); + start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); + logger.info("SaltNic session server started on http://localhost:"+SERVER_PORT); + } + + @Override + public Response serve(IHTTPSession session) { + String uri = session.getUri(); + Method method = session.getMethod(); + logger.info("Attempted to reach url: " + uri + " | method: " + method); + + if ("/api/join".equals(uri) && method == Method.POST) { + return handleJoinRequest(session); + } else if ("/api/hasJoined".equals(uri) && method == Method.GET) { + return handleHasJoinedRequest(session); + } else if (uri.startsWith("/api/profile/") && method == Method.GET) { + return handleProfileRequest(session, uri); + } + return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "404 Not Found"); + } + + 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); + logger.info("Substr success " + uuid); + if( UserStorage.getUserPassword(uuid) == null ) return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Server error"); + logger.info("Success response"); + 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" + + "}"); + } + + private Response handleHasJoinedRequest(IHTTPSession session) { + String uuid = UUID.nameUUIDFromBytes(session.getParameters().get("username").get(0).getBytes()).toString().replace("-", ""); + logger.info("hasJoined params: " + uuid); + if( this.sessions.containsKey(uuid) && this.sessions.get(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 newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Server error"); + } + + private Response handleJoinRequest(IHTTPSession session) { + try { + Map files = new HashMap<>(); + session.parseBody(files); + JSONObject json = parseJSON(files.get("postData")); + + if (json == null) { + return newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/plain", "Invalid JSON format"); + } + + String username = (String) json.get("selectedProfile"); + String sessionToken = (String) json.get("accessToken"); + + if (username == null || sessionToken == null) { + return newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/plain", "Missing selectedProfile or accessToken"); + } + + boolean validSession = PasswordHasher.checkPassword(sessionToken, UserStorage.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\"}"); + } + } 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()); + } + } + + private JSONObject parseJSON(String jsonData) { + try { + JSONParser parser = new JSONParser(); + return (JSONObject) parser.parse(jsonData); + } catch (ParseException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/com/alterdekim/xcraft/auth/UserStorage.java b/src/main/java/com/alterdekim/xcraft/auth/UserStorage.java new file mode 100644 index 0000000..5ec0528 --- /dev/null +++ b/src/main/java/com/alterdekim/xcraft/auth/UserStorage.java @@ -0,0 +1,43 @@ +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 new file mode 100644 index 0000000..3f40a43 --- /dev/null +++ b/src/main/java/com/alterdekim/xcraft/auth/XCraft.java @@ -0,0 +1,64 @@ +package com.alterdekim.xcraft.auth; + + +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.URL; + +public class XCraft extends JavaPlugin { + + private static SaltNic server = null; + + public static final int SERVER_PORT = 8999; + + @Override + public void onEnable() { + this.saveDefaultConfig(); + if( server == null ) { + try { + getLogger().info("Starting SaltNic server..."); + 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()); + } + } + + private void patchAuthLib() throws Exception { + Class clazz = Class.forName("com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService"); + modifyFinalField(clazz, "BASE_URL", "http://localhost:"+SERVER_PORT+"/api/"); + modifyFinalField(clazz, "JOIN_URL", new URL("http://localhost:"+SERVER_PORT+"/api/join")); + modifyFinalField(clazz, "CHECK_URL", new URL("http://localhost:"+SERVER_PORT+"/api/hasJoined")); + } + + private void modifyFinalField(Class clazz, String fieldName, Object newValue) throws Exception { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + + field.set(null, newValue); + getLogger().info(fieldName + " patched to: " + newValue); + } + + @Override + public void onDisable() { + if (server != null) { + server.stop(); + getLogger().info("SaltNic session server stopped."); + server = null; + } + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..1ed9e45 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,6 @@ +name: XCraftAuth +main: com.alterdekim.xcraft.auth.XCraft +version: 1.0 +author: Michael Wain +api-version: 1.12.2 +description: XCraft authentication system for Spigot \ No newline at end of file