Compare commits

..

9 Commits

Author SHA1 Message Date
github-actions[bot] d85eecd624 🆙 Bump version to 4.1.9 [skip ci] 2026-04-28 11:52:58 +00:00
DuckieTM c50098a945 Merge pull request #93 from duckietm/dev
🆕 Added Staffchat to the Emu
2026-04-28 13:52:02 +02:00
duckietm 0224f3f416 🆕 Added Staffchat to the Emu
!!! Do not run the Staffchat plugin anymore !!!!

- execute the sql:

INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`)
VALUES ( 'acc_staff_chat', 1, 'Grants access to the in-game Staff Chat group buddy: receives broadcasts from other staff and can broadcast to anyone holding this permission.' )
ON DUPLICATE KEY UPDATE `max_value` = VALUES(`max_value`), `comment`   = VALUES(`comment`);
2026-04-28 13:51:04 +02:00
github-actions[bot] 03d37650a0 🆙 Bump version to 4.1.8 [skip ci] 2026-04-28 09:32:12 +00:00
DuckieTM f4e5449443 Merge pull request #92 from duckietm/dev
🆙 Added Ban to the API
2026-04-28 11:31:15 +02:00
duckietm 1ebc8314a8 🆙 Added Ban to the API 2026-04-28 11:30:54 +02:00
github-actions[bot] 85a60cf591 🆙 Bump version to 4.1.7 [skip ci] 2026-04-24 20:10:07 +00:00
DuckieTM 41d7420251 Merge pull request #91 from duckietm/dev
🆙 Added some btter logging and fix pre-existing leak in GameByteDecoder
2026-04-24 22:09:13 +02:00
DuckieTM 5dd602ebab 🆙 Added some btter logging and fix pre-existing leak in GameByteDecoder 2026-04-24 22:08:27 +02:00
9 changed files with 224 additions and 93 deletions
+6 -35
View File
@@ -1,37 +1,3 @@
-- =============================================================================
-- Consolidated Database Updates - All-in-One
-- =============================================================================
-- This file combines ALL individual update scripts from SQL/Database Updates/
-- into a single idempotent migration. Every statement is safe to re-run:
-- - ALTER TABLE ADD COLUMN IF NOT EXISTS (MariaDB 10.0+)
-- - ALTER TABLE CHANGE/MODIFY COLUMN IF EXISTS
-- - CREATE TABLE IF NOT EXISTS
-- - INSERT IGNORE / ON DUPLICATE KEY UPDATE for settings
-- - TRUNCATE + re-insert for reference data (breeding)
--
-- Run order: This file FIRST, then 001_optimize_gameserver.sql
--
-- Source files (in applied order):
-- 1. UpdateDatabase_Allow_diagonale.sql
-- 2. UpdateDatabase_BOT.sql
-- 3. UpdateDatabase_Banners.sql
-- 4. UpdateDatabase_DanceCMD.sql
-- 5. UpdateDatabase_Happiness.sql
-- 6. UpdateDatabase_Websocket.sql
-- 7. UpdateDatabase_unignorable.sql
-- 8. Default_Camera.sql
-- 9. 07012026_UpdateDatabase_to_4-0-1.sql
-- 10. 09012026_UpdateDatabase_to_4-0-2.sql
-- 11. 12012026_Battle Banzai.sql (same as #10, deduplicated)
-- 12. 12012026_Breeding Fixes.sql
-- 13. 12012026_ChatBubbles.sql
-- 14. 16032026_updateall_command.sql
-- 15. 17032026_allow_underpass.sql
-- 16. 19032026_hotel_timezone.sql
-- 17. 21022026_user_prefixes.sql
-- 18. 06042026_builders_club_catalog_offers.sql
-- =============================================================================
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
SET @OLD_SQL_MODE = @@SQL_MODE;
@@ -512,8 +478,13 @@ ALTER TABLE `users_settings`
ADD COLUMN IF NOT EXISTS `builders_club_bonus_furni` INT(11) NOT NULL DEFAULT 0 AFTER `hc_gifts_claimed`;
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`)
VALUES ( 'acc_staff_chat', 1, 'Grants access to the in-game Staff Chat group buddy: receives broadcasts from other staff and can broadcast to anyone holding this permission.' )
ON DUPLICATE KEY UPDATE `max_value` = VALUES(`max_value`), `comment` = VALUES(`comment`);
-- =============================================================================
-- Done
-- Done.
-- =============================================================================
SET FOREIGN_KEY_CHECKS = 1;
SET SQL_MODE = @OLD_SQL_MODE;
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.1.6</version>
<version>4.1.9</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -219,6 +219,10 @@ public class Messenger {
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
if (habbo.hasPermission(StaffChatBuddy.PERMISSION_KEY)) {
this.friends.putIfAbsent(StaffChatBuddy.BUDDY_ID, new StaffChatBuddy(habbo.getHabboInfo().getId()));
}
}
public MessengerBuddy loadFriend(Habbo habbo, int userId) {
@@ -0,0 +1,57 @@
package com.eu.habbo.habbohotel.messenger;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.commands.CommandHandler;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboGender;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.friends.FriendChatMessageComposer;
public class StaffChatBuddy extends MessengerBuddy {
public static final int BUDDY_ID = -1;
public static final String PERMISSION_KEY = "acc_staff_chat";
public static final String DISPLAY_NAME = "Staff Chat";
public static final String DEFAULT_LOOK = "ADM";
public StaffChatBuddy(int userOne) {
super(BUDDY_ID, DISPLAY_NAME, DEFAULT_LOOK, (short) 0, userOne);
this.setOnline(true);
}
@Override
public void onMessageReceived(Habbo from, String message) {
if (from == null || message == null || message.isEmpty()) return;
// Re-check permission so a staff member who was demoted mid-session
// can no longer broadcast to the staff channel.
if (!from.hasPermission(PERMISSION_KEY)) return;
if (message.charAt(0) == ':') {
CommandHandler.handleCommand(from.getClient(), message);
return;
}
Message chatMessage = new Message(from.getHabboInfo().getId(), BUDDY_ID, message);
Emulator.getGameServer().getGameClientManager().sendBroadcastResponse(
new FriendChatMessageComposer(chatMessage, BUDDY_ID, from.getHabboInfo().getId()).compose(),
PERMISSION_KEY,
from.getClient());
}
@Override
public void serialize(ServerMessage message) {
message.appendInt(this.getId());
message.appendString(this.getUsername());
message.appendInt(this.getGender().equals(HabboGender.M) ? 0 : 1);
message.appendBoolean(true); // online
message.appendBoolean(false); // not in room
message.appendString(this.getLook());
message.appendInt(0); // category
message.appendString(""); // motto
message.appendString(""); // last seen
message.appendString(""); // realname
message.appendBoolean(true); // offline messaging supported
message.appendBoolean(false);
message.appendBoolean(false);
message.appendShort(0); // relation
}
}
@@ -364,62 +364,82 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT id, username, password FROM users WHERE username = ? LIMIT 1")) {
stmt.setString(1, username);
try (ResultSet rs = stmt.executeQuery()) {
if (!rs.next()) {
LOGGER.info("[auth/login] user not found username='{}' ip={}", username, ip);
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
errorPayload("Invalid Habbo name or password."));
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
if (ip != null && !ip.isEmpty()) {
BanInfo ipBan = lookupIpBan(conn, ip);
if (ipBan != null) {
LOGGER.info("[auth/login] ip ban hit ip={} type={} expires={}",
ip, ipBan.type, ipBan.expiresAt);
sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, bannedPayload(ipBan));
return;
}
}
int userId = rs.getInt("id");
String stored = rs.getString("password");
String storedPreview = stored == null
? "<null>"
: (stored.isEmpty() ? "<empty>" : stored.substring(0, Math.min(7, stored.length())) + "…(" + stored.length() + " chars)");
if (stored == null || stored.isEmpty() || !checkPassword(password, stored)) {
LOGGER.info("[auth/login] password mismatch for user id={} username='{}' stored='{}'",
userId, username, storedPreview);
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
errorPayload("Invalid Habbo name or password."));
return;
}
String ssoTicket = mintSsoTicket();
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE users SET auth_ticket = ?, ip_current = ? WHERE id = ? LIMIT 1")) {
upd.setString(1, ssoTicket);
upd.setString(2, ip == null ? "" : ip);
upd.setInt(3, userId);
upd.executeUpdate();
}
String rememberToken = null;
if (rememberMe) {
try {
RememberJwtService.RotationResult issued = RememberJwtService.issueForNewFamily(
conn, userId, rs.getString("username"), ip);
rememberToken = issued.jwt;
} catch (SQLException e) {
LOGGER.error("Failed to issue remember-me JWT for userId=" + userId, e);
try (PreparedStatement stmt = conn.prepareStatement(
"SELECT id, username, password FROM users WHERE username = ? LIMIT 1")) {
stmt.setString(1, username);
try (ResultSet rs = stmt.executeQuery()) {
if (!rs.next()) {
LOGGER.info("[auth/login] user not found username='{}' ip={}", username, ip);
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
errorPayload("Invalid Habbo name or password."));
return;
}
int userId = rs.getInt("id");
String stored = rs.getString("password");
String storedPreview = stored == null
? "<null>"
: (stored.isEmpty() ? "<empty>" : stored.substring(0, Math.min(7, stored.length())) + "…(" + stored.length() + " chars)");
if (stored == null || stored.isEmpty() || !checkPassword(password, stored)) {
LOGGER.info("[auth/login] password mismatch for user id={} username='{}' stored='{}'",
userId, username, storedPreview);
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
errorPayload("Invalid Habbo name or password."));
return;
}
BanInfo accountBan = lookupAccountBan(conn, userId);
if (accountBan != null) {
LOGGER.info("[auth/login] account ban hit userId={} type={} expires={}",
userId, accountBan.type, accountBan.expiresAt);
AuthRateLimiter.recordSuccess(ip);
sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, bannedPayload(accountBan));
return;
}
String ssoTicket = mintSsoTicket();
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE users SET auth_ticket = ?, ip_current = ? WHERE id = ? LIMIT 1")) {
upd.setString(1, ssoTicket);
upd.setString(2, ip == null ? "" : ip);
upd.setInt(3, userId);
upd.executeUpdate();
}
String rememberToken = null;
if (rememberMe) {
try {
RememberJwtService.RotationResult issued = RememberJwtService.issueForNewFamily(
conn, userId, rs.getString("username"), ip);
rememberToken = issued.jwt;
} catch (SQLException e) {
LOGGER.error("Failed to issue remember-me JWT for userId=" + userId, e);
}
}
AuthRateLimiter.recordSuccess(ip);
JsonObject ok = new JsonObject();
ok.addProperty("ssoTicket", ssoTicket);
ok.addProperty("username", rs.getString("username"));
if (rememberToken != null) ok.addProperty("rememberToken", rememberToken);
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
AuthRateLimiter.recordSuccess(ip);
JsonObject ok = new JsonObject();
ok.addProperty("ssoTicket", ssoTicket);
ok.addProperty("username", rs.getString("username"));
if (rememberToken != null) ok.addProperty("rememberToken", rememberToken);
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
} catch (Exception e) {
LOGGER.error("Login query failed for username=" + username, e);
@@ -804,6 +824,74 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
private static final long PERMANENT_BAN_THRESHOLD_SECONDS = 30L * 365L * 24L * 60L * 60L;
private static final class BanInfo {
final String type;
final String reason;
final int expiresAt;
BanInfo(String type, String reason, int expiresAt) {
this.type = type == null ? "account" : type;
this.reason = reason == null ? "" : reason;
this.expiresAt = expiresAt;
}
boolean isPermanent() {
return (long) expiresAt - Emulator.getIntUnixTimestamp() > PERMANENT_BAN_THRESHOLD_SECONDS;
}
}
private static BanInfo lookupAccountBan(Connection conn, int userId) throws SQLException {
try (PreparedStatement stmt = conn.prepareStatement(
"SELECT ban_expire, ban_reason, type FROM bans " +
"WHERE user_id = ? AND ban_expire >= ? AND (type = 'account' OR type = 'super') " +
"ORDER BY ban_expire DESC LIMIT 1")) {
stmt.setInt(1, userId);
stmt.setInt(2, Emulator.getIntUnixTimestamp());
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return new BanInfo(rs.getString("type"), rs.getString("ban_reason"), rs.getInt("ban_expire"));
}
}
}
return null;
}
private static BanInfo lookupIpBan(Connection conn, String ip) throws SQLException {
try (PreparedStatement stmt = conn.prepareStatement(
"SELECT ban_expire, ban_reason, type FROM bans " +
"WHERE ip = ? AND ban_expire >= ? AND (type = 'ip' OR type = 'super') " +
"ORDER BY ban_expire DESC LIMIT 1")) {
stmt.setString(1, ip);
stmt.setInt(2, Emulator.getIntUnixTimestamp());
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return new BanInfo(rs.getString("type"), rs.getString("ban_reason"), rs.getInt("ban_expire"));
}
}
}
return null;
}
private static JsonObject bannedPayload(BanInfo ban) {
boolean permanent = ban.isPermanent();
String message = permanent
? "Your account has been permanently banned."
: "Your account is temporarily banned.";
JsonObject details = new JsonObject();
details.addProperty("type", ban.type);
details.addProperty("reason", ban.reason);
details.addProperty("permanent", permanent);
if (!permanent) details.addProperty("expiresAt", ban.expiresAt);
JsonObject obj = new JsonObject();
obj.addProperty("error", message);
obj.add("ban", details);
return obj;
}
private static boolean checkPassword(String plain, String stored) {
String compatible = stored.startsWith("$2y$") ? "$2a$" + stored.substring(4) : stored;
try {
@@ -39,7 +39,7 @@ public class WsAesDecoder extends MessageToMessageDecoder<ByteBuf> {
byte[] plain = WsSessionCrypto.aesGcmDecrypt(key, nonce, ct);
out.add(Unpooled.wrappedBuffer(plain));
} catch (Exception e) {
LOGGER.warn("[ws-crypto] AES-GCM decrypt failed", e);
LOGGER.warn("[ws-crypto] AES-GCM decrypt failed ({}), closing channel", e.getClass().getSimpleName());
ctx.close();
}
}
@@ -3,6 +3,7 @@ package com.eu.habbo.networking.gameserver.crypto;
import com.eu.habbo.Emulator;
import com.eu.habbo.networking.gameserver.GameServerAttributes;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
@@ -118,7 +119,7 @@ public class WsHandshakeHandler extends ChannelInboundHandlerAdapter {
LOGGER.debug("[ws-crypto] handshake complete for {}", clientAddress(ctx));
} catch (Exception e) {
LOGGER.error("[ws-crypto] handshake failed from " + clientAddress(ctx), e);
LOGGER.warn("[ws-crypto] handshake failed from {} : {}", clientAddress(ctx), friendlyReason(e));
ctx.close();
} finally {
in.release();
@@ -131,9 +132,21 @@ public class WsHandshakeHandler extends ChannelInboundHandlerAdapter {
return String.valueOf(ctx.channel().remoteAddress());
}
private static String friendlyReason(Throwable t) {
if (t == null) return "unknown";
String name = t.getClass().getSimpleName();
String msg = t.getMessage();
return (msg == null || msg.isEmpty()) ? name : name + ": " + msg;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
LOGGER.error("[ws-crypto] handshake handler error", cause);
if (cause instanceof java.io.IOException) {
LOGGER.debug("[ws-crypto] client disconnected during handshake ({}): {}",
clientAddress(ctx), friendlyReason(cause));
} else {
LOGGER.error("[ws-crypto] handshake handler error from " + clientAddress(ctx), cause);
}
ctx.close();
}
}
@@ -2,7 +2,6 @@ package com.eu.habbo.networking.gameserver.decoders;
import com.eu.habbo.messages.ClientMessage;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
@@ -12,8 +11,7 @@ public class GameByteDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
short header = in.readShort();
ByteBuf body = Unpooled.copiedBuffer(in.readBytes(in.readableBytes()));
ByteBuf body = in.readBytes(in.readableBytes());
out.add(new ClientMessage(header, body));
}
}