diff --git a/LaunchServer/source/auth/handler/AuthHandler.java b/LaunchServer/source/auth/handler/AuthHandler.java index bd0dd51..0ad6b6e 100644 --- a/LaunchServer/source/auth/handler/AuthHandler.java +++ b/LaunchServer/source/auth/handler/AuthHandler.java @@ -57,6 +57,9 @@ static { registerHandler("null", NullAuthHandler::new); + registerHandler("memory", MemoryAuthHandler::new); + + // Auth handler that doesn't do nothing :D registerHandler("binaryFile", BinaryFileAuthHandler::new); registerHandler("textFile", TextFileAuthHandler::new); registerHandler("mysql", MySQLAuthHandler::new); diff --git a/LaunchServer/source/auth/handler/BinaryFileAuthHandler.java b/LaunchServer/source/auth/handler/BinaryFileAuthHandler.java index 7b2ec15..c6813f4 100644 --- a/LaunchServer/source/auth/handler/BinaryFileAuthHandler.java +++ b/LaunchServer/source/auth/handler/BinaryFileAuthHandler.java @@ -21,18 +21,18 @@ int count = input.readLength(0); for (int i = 0; i < count; i++) { UUID uuid = input.readUUID(); - Auth auth = new Auth(input); - addAuth(uuid, auth); + Entry entry = new Entry(input); + addAuth(uuid, entry); } } } @Override protected void writeAuthFile() throws IOException { - Set> entrySet = entrySet(); + Set> entrySet = entrySet(); try (HOutput output = new HOutput(IOHelper.newOutput(file))) { output.writeLength(entrySet.size(), 0); - for (Map.Entry entry : entrySet) { + for (Map.Entry entry : entrySet) { output.writeUUID(entry.getKey()); entry.getValue().write(output); } diff --git a/LaunchServer/source/auth/handler/CachedAuthHandler.java b/LaunchServer/source/auth/handler/CachedAuthHandler.java index 7d425dd..2f5ffba 100644 --- a/LaunchServer/source/auth/handler/CachedAuthHandler.java +++ b/LaunchServer/source/auth/handler/CachedAuthHandler.java @@ -69,7 +69,10 @@ @LauncherAPI protected void addEntry(Entry entry) { - entryCache.put(entry.uuid, entry); + Entry previous = entryCache.put(entry.uuid, entry); + if(previous != null) { // In case of username changing + usernamesCache.remove(CommonHelper.low(previous.username)); + } usernamesCache.put(CommonHelper.low(entry.username), entry.uuid); } diff --git a/LaunchServer/source/auth/handler/FileAuthHandler.java b/LaunchServer/source/auth/handler/FileAuthHandler.java index 36d0b80..0e951ee 100644 --- a/LaunchServer/source/auth/handler/FileAuthHandler.java +++ b/LaunchServer/source/auth/handler/FileAuthHandler.java @@ -34,7 +34,7 @@ private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); // Storage - private final Map authsMap = new HashMap<>(IOHelper.BUFFER_SIZE); + private final Map authsMap = new HashMap<>(IOHelper.BUFFER_SIZE); private final Map usernamesMap = new HashMap<>(IOHelper.BUFFER_SIZE); @LauncherAPI @@ -59,20 +59,20 @@ lock.writeLock().lock(); try { UUID uuid = usernameToUUID(username); - Auth auth = authsMap.get(uuid); + Entry entry = authsMap.get(uuid); // Not registered? Fix it! - if (auth == null) { - auth = new Auth(username); + if (entry == null) { + entry = new Entry(username); // Generate UUID uuid = genUUIDFor(username); - authsMap.put(uuid, auth); + authsMap.put(uuid, entry); usernamesMap.put(CommonHelper.low(username), uuid); } // Authenticate - auth.auth(username, accessToken); + entry.auth(username, accessToken); return uuid; } finally { lock.writeLock().unlock(); @@ -84,10 +84,10 @@ lock.readLock().lock(); try { UUID uuid = usernameToUUID(username); - Auth auth = authsMap.get(uuid); + Entry entry = authsMap.get(uuid); // Check server (if has such account of course) - return auth != null && auth.checkServer(username, serverID) ? uuid : null; + return entry != null && entry.checkServer(username, serverID) ? uuid : null; } finally { lock.readLock().unlock(); } @@ -108,8 +108,8 @@ public final boolean joinServer(String username, String accessToken, String serverID) { lock.writeLock().lock(); try { - Auth auth = authsMap.get(usernameToUUID(username)); - return auth != null && auth.joinServer(username, accessToken, serverID); + Entry entry = authsMap.get(usernameToUUID(username)); + return entry != null && entry.joinServer(username, accessToken, serverID); } finally { lock.writeLock().unlock(); } @@ -129,23 +129,26 @@ public final String uuidToUsername(UUID uuid) { lock.readLock().lock(); try { - Auth auth = authsMap.get(uuid); - return auth == null ? null : auth.username; + Entry entry = authsMap.get(uuid); + return entry == null ? null : entry.username; } finally { lock.readLock().unlock(); } } @LauncherAPI - public final Set> entrySet() { + public final Set> entrySet() { return Collections.unmodifiableMap(authsMap).entrySet(); } @LauncherAPI - protected final void addAuth(UUID uuid, Auth entry) throws IOException { + protected final void addAuth(UUID uuid, Entry entry) throws IOException { lock.writeLock().lock(); try { - authsMap.put(uuid, entry); + Entry previous = authsMap.put(uuid, entry); + if (previous != null) { // In case of username changing + usernamesMap.remove(CommonHelper.low(previous.username)); + } usernamesMap.put(CommonHelper.low(entry.username), uuid); } finally { lock.writeLock().unlock(); @@ -160,7 +163,7 @@ private UUID genUUIDFor(String username) { if (offlineUUIDs) { - UUID md5UUID = PlayerProfile.md5UUID(username); + UUID md5UUID = PlayerProfile.offlineUUID(username); if (!authsMap.containsKey(md5UUID)) { return md5UUID; } @@ -175,18 +178,18 @@ return uuid; } - public static final class Auth extends StreamObject { + public static final class Entry extends StreamObject { private String username; private String accessToken; private String serverID; @LauncherAPI - public Auth(String username) { + public Entry(String username) { this.username = VerifyHelper.verifyUsername(username); } @LauncherAPI - public Auth(String username, String accessToken, String serverID) { + public Entry(String username, String accessToken, String serverID) { this(username); if (accessToken == null && serverID != null) { throw new IllegalArgumentException("Can't set access token while server ID is null"); @@ -198,7 +201,7 @@ } @LauncherAPI - public Auth(HInput input) throws IOException { + public Entry(HInput input) throws IOException { username = VerifyHelper.verifyUsername(input.readASCII(16)); if (input.readBoolean()) { accessToken = SecurityHelper.verifyToken(input.readASCII(-SecurityHelper.TOKEN_STRING_LENGTH)); diff --git a/LaunchServer/source/auth/handler/MemoryAuthHandler.java b/LaunchServer/source/auth/handler/MemoryAuthHandler.java new file mode 100644 index 0000000..c15b43a --- /dev/null +++ b/LaunchServer/source/auth/handler/MemoryAuthHandler.java @@ -0,0 +1,63 @@ +package launchserver.auth.handler; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.UUID; + +import launcher.helper.IOHelper; +import launcher.helper.LogHelper; +import launcher.helper.VerifyHelper; +import launcher.serialize.config.entry.BlockConfigEntry; + +public final class MemoryAuthHandler extends CachedAuthHandler { + public MemoryAuthHandler(BlockConfigEntry block) { + super(block); + LogHelper.warning("Usage of MemoryAuthHandler isn't recommended!"); + } + + @Override + public void flush() throws IOException { + // Do nothing + } + + @Override + protected Entry fetchEntry(UUID uuid) throws IOException { + return new Entry(uuid, toUsername(uuid), null, null); + } + + @Override + protected Entry fetchEntry(String username) throws IOException { + return new Entry(toUUID(username), username, null, null); + } + + @Override + protected boolean updateAuth(UUID uuid, String username, String accessToken) throws IOException { + return true; // Do nothing + } + + @Override + protected boolean updateServerID(UUID uuid, String serverID) throws IOException { + return true; // Do nothing + } + + private static UUID toUUID(String username) { + ByteBuffer buffer = ByteBuffer.wrap(Arrays.copyOf(IOHelper.encodeASCII(username), 16)); + return new UUID(buffer.getLong(), buffer.getLong()); // MOST, LEAST + } + + private static String toUsername(UUID uuid) { + byte[] bytes = ByteBuffer.allocate(16). + putLong(uuid.getMostSignificantBits()). + putLong(uuid.getLeastSignificantBits()).array(); + + // Find username end + int length = 0; + while(length < bytes.length && bytes[length] != 0) { + length++; + } + + // Decode and verify + return VerifyHelper.verifyUsername(new String(bytes, 0, length, IOHelper.ASCII_CHARSET)); + } +} diff --git a/LaunchServer/source/auth/handler/TextFileAuthHandler.java b/LaunchServer/source/auth/handler/TextFileAuthHandler.java index 97f6849..191bd43 100644 --- a/LaunchServer/source/auth/handler/TextFileAuthHandler.java +++ b/LaunchServer/source/auth/handler/TextFileAuthHandler.java @@ -44,7 +44,7 @@ authBlock.getEntryValue("serverID", StringConfigEntry.class) : null; // Add auth entry - addAuth(uuid, new Auth(username, accessToken, serverID)); + addAuth(uuid, new Entry(username, accessToken, serverID)); } } @@ -53,11 +53,11 @@ boolean next = false; // Write auth blocks to map - Set> entrySet = entrySet(); + Set> entrySet = entrySet(); Map> map = new LinkedHashMap<>(entrySet.size()); - for (Map.Entry entry : entrySet) { + for (Map.Entry entry : entrySet) { UUID uuid = entry.getKey(); - Auth auth = entry.getValue(); + Entry auth = entry.getValue(); // Set auth entry data Map> authMap = new LinkedHashMap<>(entrySet.size()); diff --git a/LaunchServer/source/auth/provider/AuthProvider.java b/LaunchServer/source/auth/provider/AuthProvider.java index 26dbedf..30f80db 100644 --- a/LaunchServer/source/auth/provider/AuthProvider.java +++ b/LaunchServer/source/auth/provider/AuthProvider.java @@ -45,8 +45,10 @@ registerProvider("null", NullAuthProvider::new); registerProvider("accept", AcceptAuthProvider::new); registerProvider("reject", RejectAuthProvider::new); + + // Auth providers that doesn't do nothing :D registerProvider("mysql", MySQLAuthProvider::new); - registerProvider("http", HTTPAuthProvider::new); registerProvider("file", FileAuthProvider::new); + registerProvider("url", URLAuthProvider::new); } } diff --git a/LaunchServer/source/auth/provider/HTTPAuthProvider.java b/LaunchServer/source/auth/provider/HTTPAuthProvider.java deleted file mode 100644 index 60ac89c..0000000 --- a/LaunchServer/source/auth/provider/HTTPAuthProvider.java +++ /dev/null @@ -1,44 +0,0 @@ -package launchserver.auth.provider; - -import java.io.IOException; -import java.net.URL; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import launcher.helper.CommonHelper; -import launcher.helper.IOHelper; -import launcher.serialize.config.entry.BlockConfigEntry; -import launcher.serialize.config.entry.StringConfigEntry; - -public final class HTTPAuthProvider extends AuthProvider { - private final String url; - private final Pattern response; - - public HTTPAuthProvider(BlockConfigEntry block) { - super(block); - url = block.getEntryValue("url", StringConfigEntry.class); - response = Pattern.compile(block.getEntryValue("response", StringConfigEntry.class)); - - // Verify is valid URL - IOHelper.verifyURL(getFormattedURL("httpAuthLogin", "httpAuthPassword")); - } - - @Override - public String auth(String login, String password) throws IOException { - String currentResponse = IOHelper.request(new URL(getFormattedURL(login, password))); - - // Match username - Matcher matcher = response.matcher(currentResponse); - return matcher.matches() && matcher.groupCount() >= 1 ? - matcher.group("username") : authError(currentResponse); - } - - @Override - public void flush() { - // Do nothing - } - - private String getFormattedURL(String login, String password) { - return CommonHelper.replace(url, "login", login, "password", password); - } -} diff --git a/LaunchServer/source/auth/provider/URLAuthProvider.java b/LaunchServer/source/auth/provider/URLAuthProvider.java new file mode 100644 index 0000000..e0d3b94 --- /dev/null +++ b/LaunchServer/source/auth/provider/URLAuthProvider.java @@ -0,0 +1,44 @@ +package launchserver.auth.provider; + +import java.io.IOException; +import java.net.URL; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import launcher.helper.CommonHelper; +import launcher.helper.IOHelper; +import launcher.serialize.config.entry.BlockConfigEntry; +import launcher.serialize.config.entry.StringConfigEntry; + +public final class URLAuthProvider extends AuthProvider { + private final String url; + private final Pattern response; + + public URLAuthProvider(BlockConfigEntry block) { + super(block); + url = block.getEntryValue("url", StringConfigEntry.class); + response = Pattern.compile(block.getEntryValue("response", StringConfigEntry.class)); + + // Verify is valid URL + IOHelper.verifyURL(getFormattedURL("urlAuthLogin", "urlAuthPassword")); + } + + @Override + public String auth(String login, String password) throws IOException { + String currentResponse = IOHelper.request(new URL(getFormattedURL(login, password))); + + // Match username + Matcher matcher = response.matcher(currentResponse); + return matcher.matches() && matcher.groupCount() >= 1 ? + matcher.group("username") : authError(currentResponse); + } + + @Override + public void flush() { + // Do nothing + } + + private String getFormattedURL(String login, String password) { + return CommonHelper.replace(url, "login", login, "password", password); + } +} diff --git a/Launcher/source/client/PlayerProfile.java b/Launcher/source/client/PlayerProfile.java index a8d6bd9..af44f7f 100644 --- a/Launcher/source/client/PlayerProfile.java +++ b/Launcher/source/client/PlayerProfile.java @@ -55,12 +55,12 @@ } @LauncherAPI - public static UUID md5UUID(String username) { - return UUID.nameUUIDFromBytes(IOHelper.encodeASCII("OfflinePlayer:" + username)); + public static PlayerProfile newOfflineProfile(String username) { + return new PlayerProfile(offlineUUID(username), username, null, null); } @LauncherAPI - public static PlayerProfile newOfflineProfile(String username) { - return new PlayerProfile(md5UUID(username), username, null, null); + public static UUID offlineUUID(String username) { + return UUID.nameUUIDFromBytes(IOHelper.encodeASCII("OfflinePlayer:" + username)); } } diff --git a/Launcher/source/helper/IOHelper.java b/Launcher/source/helper/IOHelper.java index f772e05..13d3881 100644 --- a/Launcher/source/helper/IOHelper.java +++ b/Launcher/source/helper/IOHelper.java @@ -1,5 +1,8 @@ package launcher.helper; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; @@ -201,6 +204,11 @@ } @LauncherAPI + public static boolean isValidTextureBounds(int width, int height) { + return width % 64 == 0 && height * 2 == width && width <= 1024; + } + + @LauncherAPI public static void move(Path source, Path target) throws IOException { createParentDirs(target); Files.move(source, target, COPY_OPTIONS); @@ -372,6 +380,26 @@ } @LauncherAPI + public static BufferedImage readTexture(Object input) throws IOException { + ImageReader reader = ImageIO.getImageReadersByMIMEType("image/png").next(); + try { + reader.setInput(ImageIO.createImageInputStream(input), false, false); + + // Verify texture bounds + int width = reader.getWidth(0); + int height = reader.getHeight(0); + if (!isValidTextureBounds(width, height)) { + throw new IOException(String.format("Invalid texture bounds: %dx%d", width, height)); + } + + // Read image + return reader.read(0); + } finally { + reader.dispose(); + } + } + + @LauncherAPI public static String request(URL url) throws IOException { return decode(read(url)).trim(); } @@ -537,6 +565,12 @@ } @LauncherAPI + public static BufferedImage verifyTexture(BufferedImage skin) { + return VerifyHelper.verify(skin, i -> isValidTextureBounds(i.getWidth(), i.getHeight()), + String.format("Invalid texture bounds: %dx%d", skin.getWidth(), skin.getHeight())); + } + + @LauncherAPI public static String verifyURL(String url) { try { new URL(url).toURI();