First commit
This commit is contained in:
commit
914557babb
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@ -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
|
64
pom.xml
Normal file
64
pom.xml
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.alterdekim.xcraft.auth</groupId>
|
||||||
|
<artifactId>xcraft-auth</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>1.8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>1.8</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>spigot-repo</id>
|
||||||
|
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.spigotmc</groupId>
|
||||||
|
<artifactId>spigot-api</artifactId>
|
||||||
|
<version>1.12.2-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.nanohttpd</groupId>
|
||||||
|
<artifactId>nanohttpd</artifactId>
|
||||||
|
<version>2.3.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mindrot</groupId>
|
||||||
|
<artifactId>jbcrypt</artifactId>
|
||||||
|
<version>0.4</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>single</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<descriptorRefs>
|
||||||
|
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||||
|
</descriptorRefs>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
13
src/main/java/com/alterdekim/xcraft/auth/PasswordHasher.java
Normal file
13
src/main/java/com/alterdekim/xcraft/auth/PasswordHasher.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
121
src/main/java/com/alterdekim/xcraft/auth/SaltNic.java
Normal file
121
src/main/java/com/alterdekim/xcraft/auth/SaltNic.java
Normal file
@ -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<String, Boolean> 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<String, String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
src/main/java/com/alterdekim/xcraft/auth/UserStorage.java
Normal file
43
src/main/java/com/alterdekim/xcraft/auth/UserStorage.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
src/main/java/com/alterdekim/xcraft/auth/XCraft.java
Normal file
64
src/main/java/com/alterdekim/xcraft/auth/XCraft.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
src/main/resources/config.yml
Normal file
0
src/main/resources/config.yml
Normal file
6
src/main/resources/plugin.yml
Normal file
6
src/main/resources/plugin.yml
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user