diff --git a/Database Updates/018_Last_Username_Change.sql b/Database Updates/018_Last_Username_Change.sql new file mode 100644 index 00000000..76409ecb --- /dev/null +++ b/Database Updates/018_Last_Username_Change.sql @@ -0,0 +1,5 @@ +ALTER TABLE users +ADD COLUMN `last_username_change` INT(11) NOT NULL; + +INSERT INTO emulator_settings (`key`, `value`, `comment`) +VALUES ('rename.cooldown_days', '30', 'Days between username changes'); \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/ChangeNameCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/ChangeNameCommand.java deleted file mode 100644 index 0fa9bbc9..00000000 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/ChangeNameCommand.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.eu.habbo.habbohotel.commands; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.messages.outgoing.users.UserDataComposer; - -public class ChangeNameCommand extends Command { - public ChangeNameCommand() { - super("cmd_changename", Emulator.getTexts().getValue("commands.keys.cmd_changename").split(";")); - } - - @Override - public boolean handle(GameClient gameClient, String[] params) throws Exception { - gameClient.getHabbo().getHabboStats().allowNameChange = !gameClient.getHabbo().getHabboStats().allowNameChange; - gameClient.sendResponse(new UserDataComposer(gameClient.getHabbo())); - return true; - } -} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandHandler.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandHandler.java index f85d83f2..eb3979fa 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandHandler.java @@ -184,7 +184,6 @@ public class CommandHandler { addCommand(new BlockAlertCommand()); addCommand(new BotsCommand()); addCommand(new CalendarCommand()); - addCommand(new ChangeNameCommand()); addCommand(new ChatTypeCommand()); addCommand(new CommandsCommand()); addCommand(new ControlCommand()); diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java index de1a12c0..b7830d8e 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java @@ -38,6 +38,9 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { private static final String REFRESH_PATH = "/api/auth/refresh"; private static final String SERVER_KEY_PATH = "/api/auth/server-key"; private static final String SSO_TOKEN_PATH = "/api/auth/sso-token"; + private static final String CHANGE_PASSWORD_PATH = "/api/auth/change-password"; + private static final String CHANGE_EMAIL_PATH = "/api/auth/change-email"; + private static final String CHANGE_USERNAME_PATH = "/api/auth/change-username"; private static final String HEALTH_PATH = "/api/health"; private static final Pattern USERNAME_RE = Pattern.compile("^[A-Za-z0-9._-]{3,32}$"); @@ -64,6 +67,9 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { && !path.equals(REFRESH_PATH) && !path.equals(SERVER_KEY_PATH) && !path.equals(SSO_TOKEN_PATH) + && !path.equals(CHANGE_PASSWORD_PATH) + && !path.equals(CHANGE_EMAIL_PATH) + && !path.equals(CHANGE_USERNAME_PATH) && !path.equals(HEALTH_PATH)) { super.channelRead(ctx, msg); return; @@ -180,6 +186,18 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { handleSsoToken(ctx, req, body, ip); return; } + if (path.equals(CHANGE_PASSWORD_PATH)) { + handleChangePassword(ctx, req, body, ip); + return; + } + if (path.equals(CHANGE_EMAIL_PATH)) { + handleChangeEmail(ctx, req, body, ip); + return; + } + if (path.equals(CHANGE_USERNAME_PATH)) { + handleChangeUsername(ctx, req, body, ip); + return; + } String turnstileToken = readString(body, "turnstileToken"); if (!TurnstileVerifier.verify(turnstileToken, ip)) { @@ -274,6 +292,517 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { sendJson(ctx, req, HttpResponseStatus.OK, res); } + private void handleChangePassword(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) { + String authHeader = req.headers().get(HttpHeaderNames.AUTHORIZATION); + String bearer = ""; + if (authHeader != null && authHeader.regionMatches(true, 0, "Bearer ", 0, 7)) { + bearer = authHeader.substring(7).trim(); + } + + int userId = AccessTokenService.verify(bearer); + if (userId <= 0) { + AuthRateLimiter.recordFailure(ip); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Not authenticated.")); + return; + } + + String currentPassword = readString(body, "currentPassword"); + String newPassword = readString(body, "newPassword"); + String confirmPassword = readString(body, "confirmPassword"); + + if (currentPassword.isEmpty() || newPassword.isEmpty() || confirmPassword.isEmpty()) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("All fields are required.")); + return; + } + + if (currentPassword.length() > 256 || newPassword.length() > 256) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("Password too long.")); + return; + } + + if (!newPassword.equals(confirmPassword)) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("New passwords do not match.")); + return; + } + + if (newPassword.length() < 8) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("Password must be at least 8 characters.")); + return; + } + + if (newPassword.equals(currentPassword)) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("New password must be different from the current password.")); + return; + } + + try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) { + String storedHash = null; + String username = null; + try (PreparedStatement lookup = conn.prepareStatement( + "SELECT username, password FROM users WHERE id = ? LIMIT 1")) { + lookup.setInt(1, userId); + try (ResultSet rs = lookup.executeQuery()) { + if (rs.next()) { + username = rs.getString("username"); + storedHash = rs.getString("password"); + } + } + } + + if (storedHash == null || storedHash.isEmpty()) { + AuthRateLimiter.recordFailure(ip); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Account not found.")); + return; + } + + if (!checkPassword(currentPassword, storedHash)) { + AuthRateLimiter.recordFailure(ip); + LOGGER.info("[auth/change-password] current password mismatch for user id={} username='{}'", userId, username); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, + errorPayload("Current password is incorrect.")); + return; + } + + String hashed = BCrypt.hashpw(newPassword, BCrypt.gensalt(12)); + try (PreparedStatement upd = conn.prepareStatement( + "UPDATE users SET password = ? WHERE id = ? LIMIT 1")) { + upd.setString(1, hashed); + upd.setInt(2, userId); + upd.executeUpdate(); + } + + AuthRateLimiter.recordSuccess(ip); + LOGGER.info("[auth/change-password] password updated for user id={} username='{}' ip='{}'", userId, username, ip); + + JsonObject ok = new JsonObject(); + ok.addProperty("message", "Password updated successfully."); + sendJson(ctx, req, HttpResponseStatus.OK, ok); + } catch (Exception e) { + LOGGER.error("[auth/change-password] failed for user id=" + userId, e); + sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error.")); + } + } + + private void handleChangeEmail(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) { + String authHeader = req.headers().get(HttpHeaderNames.AUTHORIZATION); + String bearer = ""; + if (authHeader != null && authHeader.regionMatches(true, 0, "Bearer ", 0, 7)) { + bearer = authHeader.substring(7).trim(); + } + + int userId = AccessTokenService.verify(bearer); + if (userId <= 0) { + AuthRateLimiter.recordFailure(ip); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Not authenticated.")); + return; + } + + String currentPassword = readString(body, "currentPassword"); + String newEmail = readString(body, "newEmail").trim(); + + if (currentPassword.isEmpty() || newEmail.isEmpty()) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("All fields are required.")); + return; + } + + if (currentPassword.length() > 256 || newEmail.length() > 254) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("Field too long.")); + return; + } + + if (!EMAIL_RE.matcher(newEmail).matches()) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("Invalid email address.")); + return; + } + + try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) { + String storedHash = null; + String username = null; + String currentEmail = null; + try (PreparedStatement lookup = conn.prepareStatement( + "SELECT username, password, mail FROM users WHERE id = ? LIMIT 1")) { + lookup.setInt(1, userId); + try (ResultSet rs = lookup.executeQuery()) { + if (rs.next()) { + username = rs.getString("username"); + storedHash = rs.getString("password"); + currentEmail = rs.getString("mail"); + } + } + } + + if (storedHash == null || storedHash.isEmpty()) { + AuthRateLimiter.recordFailure(ip); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Account not found.")); + return; + } + + if (!checkPassword(currentPassword, storedHash)) { + AuthRateLimiter.recordFailure(ip); + LOGGER.info("[auth/change-email] password mismatch for user id={} username='{}'", userId, username); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, + errorPayload("Current password is incorrect.")); + return; + } + + if (currentEmail != null && currentEmail.equalsIgnoreCase(newEmail)) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("New email must be different from the current email.")); + return; + } + + try (PreparedStatement check = conn.prepareStatement( + "SELECT id FROM users WHERE mail = ? AND id <> ? LIMIT 1")) { + check.setString(1, newEmail); + check.setInt(2, userId); + try (ResultSet rs = check.executeQuery()) { + if (rs.next()) { + sendJson(ctx, req, HttpResponseStatus.CONFLICT, + errorPayload("That email address is already in use.")); + return; + } + } + } + + try (PreparedStatement upd = conn.prepareStatement( + "UPDATE users SET mail = ? WHERE id = ? LIMIT 1")) { + upd.setString(1, newEmail); + upd.setInt(2, userId); + upd.executeUpdate(); + } + + if (currentEmail != null && !currentEmail.isEmpty()) AvailabilityCache.invalidateEmail(currentEmail); + AvailabilityCache.invalidateEmail(newEmail); + + AuthRateLimiter.recordSuccess(ip); + LOGGER.info("[auth/change-email] email updated for user id={} username='{}' ip='{}'", userId, username, ip); + + JsonObject ok = new JsonObject(); + ok.addProperty("message", "Email updated successfully."); + ok.addProperty("email", newEmail); + sendJson(ctx, req, HttpResponseStatus.OK, ok); + } catch (Exception e) { + LOGGER.error("[auth/change-email] failed for user id=" + userId, e); + sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error.")); + } + } + + private void handleChangeUsername(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) { + String authHeader = req.headers().get(HttpHeaderNames.AUTHORIZATION); + String bearer = ""; + if (authHeader != null && authHeader.regionMatches(true, 0, "Bearer ", 0, 7)) { + bearer = authHeader.substring(7).trim(); + } + + int userId = AccessTokenService.verify(bearer); + if (userId <= 0) { + AuthRateLimiter.recordFailure(ip); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Not authenticated.")); + return; + } + + String currentPassword = readString(body, "currentPassword"); + String newUsername = readString(body, "newUsername").trim(); + + if (currentPassword.isEmpty() || newUsername.isEmpty()) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("All fields are required.")); + return; + } + + if (currentPassword.length() > 256) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("Field too long.")); + return; + } + + if (newUsername.length() > 25) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("Username can be at most 25 characters.")); + return; + } + + if (!USERNAME_RE.matcher(newUsername).matches()) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("Username must be 3-25 characters (letters, numbers, . _ -).")); + return; + } + + long cooldownDays = Math.max(0, Emulator.getConfig().getInt("rename.cooldown_days", 30)); + long cooldownSeconds = cooldownDays * 86400L; + + try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) { + String storedHash = null; + String currentUsername = null; + int lastChange = 0; + boolean cooldownColumnExists = true; + + try (PreparedStatement lookup = conn.prepareStatement( + "SELECT username, password, last_username_change FROM users WHERE id = ? LIMIT 1")) { + lookup.setInt(1, userId); + try (ResultSet rs = lookup.executeQuery()) { + if (rs.next()) { + currentUsername = rs.getString("username"); + storedHash = rs.getString("password"); + lastChange = rs.getInt("last_username_change"); + } + } + } catch (SQLException missingColumn) { + cooldownColumnExists = false; + LOGGER.warn("[auth/change-username] users.last_username_change column missing — cooldown disabled. Run the migration in config/Database.sql."); + try (PreparedStatement lookup = conn.prepareStatement( + "SELECT username, password FROM users WHERE id = ? LIMIT 1")) { + lookup.setInt(1, userId); + try (ResultSet rs = lookup.executeQuery()) { + if (rs.next()) { + currentUsername = rs.getString("username"); + storedHash = rs.getString("password"); + } + } + } + } + + if (storedHash == null || storedHash.isEmpty()) { + AuthRateLimiter.recordFailure(ip); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Account not found.")); + return; + } + + if (!checkPassword(currentPassword, storedHash)) { + AuthRateLimiter.recordFailure(ip); + LOGGER.info("[auth/change-username] password mismatch for user id={} username='{}'", userId, currentUsername); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, + errorPayload("Current password is incorrect.")); + return; + } + + // Case-only changes (e.g. "bob" -> "Bob") are allowed; identical strings are not. + if (currentUsername != null && currentUsername.equals(newUsername)) { + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, + errorPayload("New username must be different from the current username.")); + return; + } + + int now = Emulator.getIntUnixTimestamp(); + if (cooldownColumnExists && cooldownSeconds > 0 && lastChange > 0) { + long allowedAt = (long) lastChange + cooldownSeconds; + if (now < allowedAt) { + long remaining = allowedAt - now; + long days = remaining / 86400L; + long hours = (remaining % 86400L) / 3600L; + String wait = days > 0 ? (days + " day" + (days == 1 ? "" : "s")) : (hours + " hour" + (hours == 1 ? "" : "s")); + sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS, + errorPayload("You can rename again in " + wait + ".")); + return; + } + } + + try (PreparedStatement banned = conn.prepareStatement( + "SELECT 1 FROM banned_usernames WHERE LOWER(username) = LOWER(?) LIMIT 1")) { + banned.setString(1, newUsername); + try (ResultSet rs = banned.executeQuery()) { + if (rs.next()) { + sendJson(ctx, req, HttpResponseStatus.CONFLICT, + errorPayload("That username is not allowed.")); + return; + } + } + } catch (SQLException bannedTableError) { + // Only swallow "table doesn't exist" — banned_usernames is optional in some installs. + // Any other DB error (timeout, syntax, permission, connection drop) must fail the + // request so a transient outage can't silently bypass the blocklist. + if (bannedTableError.getErrorCode() != 1146 + && !"42S02".equals(bannedTableError.getSQLState())) { + throw bannedTableError; + } + } + + try (PreparedStatement check = conn.prepareStatement( + "SELECT id FROM users WHERE LOWER(username) = LOWER(?) AND id <> ? LIMIT 1")) { + check.setString(1, newUsername); + check.setInt(2, userId); + try (ResultSet rs = check.executeQuery()) { + if (rs.next()) { + sendJson(ctx, req, HttpResponseStatus.CONFLICT, + errorPayload("That username is already taken.")); + return; + } + } + } + + boolean previousAutoCommit = conn.getAutoCommit(); + conn.setAutoCommit(false); + + boolean cooldownRace = false; + boolean duplicateName = false; + + try { + int rowsUpdated = 0; + + // Folding the cooldown into the WHERE clause makes the rename atomic against + // a second concurrent request with the same token: only one of them will see + // rowsUpdated == 1, the other gets 0 and we surface it as 429 instead of letting + // both succeed and writing last_username_change twice. + try (PreparedStatement upd = conn.prepareStatement( + cooldownColumnExists + ? "UPDATE users SET username = ?, last_username_change = ? " + + "WHERE id = ? " + + " AND (last_username_change = 0 OR last_username_change + ? <= ?) " + + "LIMIT 1" + : "UPDATE users SET username = ? WHERE id = ? LIMIT 1")) { + upd.setString(1, newUsername); + if (cooldownColumnExists) { + upd.setInt(2, now); + upd.setInt(3, userId); + upd.setLong(4, cooldownSeconds); + upd.setInt(5, now); + } else { + upd.setInt(2, userId); + } + try { + rowsUpdated = upd.executeUpdate(); + } catch (SQLException dup) { + // ER_DUP_ENTRY (1062) / SQLState 23000 — the UNIQUE KEY on users.username + // raced us between the pre-check and the UPDATE. Surface as 409 instead of + // a generic 500 so the UI can show "username taken". + if (dup.getErrorCode() == 1062 || "23000".equals(dup.getSQLState())) { + duplicateName = true; + } else { + throw dup; + } + } + } + + if (duplicateName || (cooldownColumnExists && rowsUpdated == 0)) { + if (!duplicateName) cooldownRace = true; + conn.rollback(); + } else { + try (PreparedStatement upd = conn.prepareStatement( + "UPDATE rooms SET owner_name = ? WHERE owner_id = ?")) { + upd.setString(1, newUsername); + upd.setInt(2, userId); + upd.executeUpdate(); + } + + try (PreparedStatement upd = conn.prepareStatement( + "UPDATE rooms_for_sale SET owner_name = ? WHERE user_id = ?")) { + upd.setString(1, newUsername); + upd.setInt(2, userId); + upd.executeUpdate(); + } catch (SQLException roomsForSale) { + // rooms_for_sale is optional — only swallow "table doesn't exist". + if (roomsForSale.getErrorCode() != 1146 + && !"42S02".equals(roomsForSale.getSQLState())) { + throw roomsForSale; + } + } + + conn.commit(); + } + } catch (SQLException txError) { + try { conn.rollback(); } catch (SQLException ignore) {} + throw txError; + } finally { + conn.setAutoCommit(previousAutoCommit); + } + + if (duplicateName) { + LOGGER.info("[auth/change-username] dup-entry race for user id={} wanted='{}'", userId, newUsername); + sendJson(ctx, req, HttpResponseStatus.CONFLICT, + errorPayload("That username is already taken.")); + return; + } + + if (cooldownRace) { + LOGGER.info("[auth/change-username] cooldown race for user id={} (concurrent rename rejected)", userId); + sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS, + errorPayload("Rename already in progress — please wait.")); + return; + } + + // Refresh in-memory caches BEFORE we drop the client. On disconnect, Habbo.disconnect() + // calls HabboInfo.run() which persists every field of HabboInfo back to the users row + // (including username) — so if the in-memory object still holds the old name it will + // overwrite the DB update we just committed. We also need to re-key the + // HabboManager.onlineHabbosByName map and update any loaded Room.ownerName fields so + // navigator / room cards stop showing the old name without a server restart. + try { + if (Emulator.getGameServer() != null && Emulator.getGameServer().getGameClientManager() != null + && Emulator.getGameEnvironment() != null && Emulator.getGameEnvironment().getHabboManager() != null) { + com.eu.habbo.habbohotel.users.Habbo habbo = + Emulator.getGameServer().getGameClientManager().getHabbo(userId); + if (habbo != null) { + // Re-key the username map AND update the in-memory username so the + // disconnect-time HabboInfo.run() persists the new name, not the old one. + Emulator.getGameEnvironment().getHabboManager().removeHabbo(habbo); + habbo.getHabboInfo().setUsername(newUsername); + Emulator.getGameEnvironment().getHabboManager().addHabbo(habbo); + } + } + } catch (Exception cacheError) { + LOGGER.warn("[auth/change-username] failed to refresh HabboManager cache", cacheError); + } + + try { + if (Emulator.getGameEnvironment() != null && Emulator.getGameEnvironment().getRoomManager() != null) { + for (com.eu.habbo.habbohotel.rooms.Room room : Emulator.getGameEnvironment().getRoomManager().getActiveRooms()) { + if (room.getOwnerId() == userId) { + room.setOwnerName(newUsername); + } + } + } + } catch (Exception cacheError) { + LOGGER.warn("[auth/change-username] failed to refresh Room.ownerName cache", cacheError); + } + + // Marketplace offer-list pages bake the seller name into the serialized payload, + // so invalidate it; the next /offers request will rebuild it with the new name. + try { + com.eu.habbo.messages.incoming.catalog.marketplace.RequestOffersEvent.cachedResults.clear(); + } catch (Exception cacheError) { + LOGGER.warn("[auth/change-username] failed to clear marketplace cache", cacheError); + } + + // Drop any active session so the user reconnects with the fresh identity + // (friend list, navigator, messenger threads all refresh on relogin). + try (PreparedStatement clear = conn.prepareStatement( + "UPDATE users SET auth_ticket = '', online = '0' WHERE id = ? LIMIT 1")) { + clear.setInt(1, userId); + clear.executeUpdate(); + } + + if (Emulator.getGameServer() != null + && Emulator.getGameServer().getGameClientManager() != null) { + com.eu.habbo.habbohotel.users.Habbo habbo = + Emulator.getGameServer().getGameClientManager().getHabbo(userId); + if (habbo != null && habbo.getClient() != null) { + Emulator.getGameServer().getGameClientManager().disposeClient(habbo.getClient()); + } + } + + AuthRateLimiter.recordSuccess(ip); + LOGGER.info("[auth/change-username] '{}' -> '{}' (user id={}, ip='{}')", + currentUsername, newUsername, userId, ip); + + JsonObject ok = new JsonObject(); + ok.addProperty("message", "Username updated. Please log in again with your new name."); + ok.addProperty("username", newUsername); + ok.addProperty("relogin", true); + sendJson(ctx, req, HttpResponseStatus.OK, ok); + } catch (Exception e) { + LOGGER.error("[auth/change-username] failed for user id=" + userId, e); + sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error.")); + } + } + private void handleLogout(ChannelHandlerContext ctx, FullHttpRequest req, com.google.gson.JsonObject body) { String ssoTicket = readString(body, "ssoTicket"); String rememberToken = readString(body, "rememberToken").trim(); @@ -1135,11 +1664,20 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { String origin = req.headers().get(HttpHeaderNames.ORIGIN); if (origin != null && !origin.isEmpty()) { response.headers().set("Access-Control-Allow-Origin", origin); - response.headers().set("Vary", "Origin"); response.headers().set("Access-Control-Allow-Credentials", "true"); } response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS"); - response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api"); + + String requestedHeaders = req.headers().get("Access-Control-Request-Headers"); + if (requestedHeaders != null && !requestedHeaders.isEmpty()) { + response.headers().set("Access-Control-Allow-Headers", requestedHeaders); + } else { + response.headers().set("Access-Control-Allow-Headers", + "Authorization, Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api"); + } + + response.headers().set("Vary", "Origin, Access-Control-Request-Headers, Access-Control-Request-Method"); + response.headers().set("Access-Control-Max-Age", "600"); response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp"); } diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java index cfeab3bd..85934953 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java @@ -267,11 +267,20 @@ public class NitroSecureApiHandler extends ChannelDuplexHandler { String origin = req.headers().get(HttpHeaderNames.ORIGIN); if (origin != null && !origin.isEmpty()) { response.headers().set("Access-Control-Allow-Origin", origin); - response.headers().set("Vary", "Origin"); response.headers().set("Access-Control-Allow-Credentials", "true"); } response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS"); - response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api"); + + String requestedHeaders = req.headers().get("Access-Control-Request-Headers"); + if (requestedHeaders != null && !requestedHeaders.isEmpty()) { + response.headers().set("Access-Control-Allow-Headers", requestedHeaders); + } else { + response.headers().set("Access-Control-Allow-Headers", + "Authorization, Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api"); + } + + response.headers().set("Vary", "Origin, Access-Control-Request-Headers, Access-Control-Request-Method"); + response.headers().set("Access-Control-Max-Age", "600"); response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp"); } diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java index a2da7b4e..73c07e69 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java @@ -299,10 +299,18 @@ public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter { String origin = req.headers().get(HttpHeaderNames.ORIGIN); if (origin != null && !origin.isEmpty()) { response.headers().set("Access-Control-Allow-Origin", origin); - response.headers().set("Vary", "Origin"); } response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS"); - response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Nitro-Key"); + + String requestedHeaders = req.headers().get("Access-Control-Request-Headers"); + if (requestedHeaders != null && !requestedHeaders.isEmpty()) { + response.headers().set("Access-Control-Allow-Headers", requestedHeaders); + } else { + response.headers().set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Nitro-Key"); + } + + response.headers().set("Vary", "Origin, Access-Control-Request-Headers, Access-Control-Request-Method"); + response.headers().set("Access-Control-Max-Age", "600"); response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp"); } diff --git a/Latest_Compiled_Version/Habbo-Latest.jar b/Latest_Compiled_Version/Habbo-Latest.jar deleted file mode 100644 index 6c6bf754..00000000 Binary files a/Latest_Compiled_Version/Habbo-Latest.jar and /dev/null differ