Compare commits

...

45 Commits

Author SHA1 Message Date
github-actions[bot] fdf0e5d806 🆙 Bump version to 4.1.13 [skip ci] 2026-05-04 13:38:38 +00:00
DuckieTM c64d3b7b8d Merge pull request #98 from duckietm/dev
Dev
2026-05-04 15:37:38 +02:00
duckietm c2b85c0c8c 🆙 Redone Background profiles 2026-05-04 15:15:41 +02:00
duckietm f8a651b059 🆙 Security update Info stand background 2026-05-04 13:18:06 +02:00
github-actions[bot] 00f9feab14 🆙 Bump version to 4.1.12 [skip ci] 2026-05-04 08:54:02 +00:00
DuckieTM 0b37705b65 Merge pull request #97 from duckietm/dev
Dev
2026-05-04 10:53:08 +02:00
duckietm 9b77ca1016 🆙 Cleanup 2026-05-04 10:52:48 +02:00
duckietm 39941cd496 🆕 Added extra packet for the pets 2026-05-04 10:08:37 +02:00
duckietm 7095dfad43 🆙 Fix Pickall 2026-05-04 08:20:58 +02:00
DuckieTM 750b172304 Merge pull request #96 from simoleo89/feat/full-box-background
Feat/full box background
2026-05-04 08:03:55 +02:00
simoleo89 5afa1f274c feat(profile): add background_card_id for full-box card backgrounds
Introduces a 4th profile-style id (cardBg) alongside the existing
background/stand/overlay triplet. The new id is meant to render a
background that fills the entire user info card on the client.

- HabboInfo: new InfostandCardBg field, loaded/saved with the
  existing background ids; users.background_card_id column added
  via sqlupdates/add_users_background_card_id.sql.
- ChangeInfostandBgEvent: reads a 4th int with bytesAvailable
  guard to remain compatible with older clients.
- RoomUserDataComposer, RoomUsersComposer, UserProfileComposer:
  append the cardBg int after the existing trio. Bot sections in
  RoomUsersComposer pad an extra zero to keep field count consistent.
2026-05-03 22:09:53 +02:00
duckietm 8f59eb652f 🆙 As NAcho wants it, add effect on disconnected user & small security update 2026-05-01 16:59:34 +02:00
duckietm 8a8cd1121e 🆕 Create Custom Bage & Security update 2026-05-01 15:58:48 +02:00
github-actions[bot] 60e5ba3a6a 🆙 Bump version to 4.1.11 [skip ci] 2026-05-01 05:49:08 +00:00
DuckieTM 9fa3fad70c Merge pull request #95 from duckietm/dev
🆕 News API
2026-05-01 07:48:08 +02:00
duckietm 860f61f765 🆕 News API 2026-04-30 17:21:33 +02:00
github-actions[bot] c5137bf3dc 🆙 Bump version to 4.1.10 [skip ci] 2026-04-29 15:11:05 +00:00
DuckieTM 5150418796 Merge pull request #94 from duckietm/dev
Dev
2026-04-29 17:10:02 +02:00
duckietm 5c71b318fb 🆙 Latest compiled version 2026-04-29 17:09:43 +02:00
duckietm 1cac407c45 🆕 Effect selection in user dropdown 2026-04-29 13:20:53 +02:00
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
github-actions[bot] 6d203c1267 🆙 Bump version to 4.1.6 [skip ci] 2026-04-24 14:35:01 +00:00
DuckieTM a8bcb27d27 Merge pull request #90 from duckietm/dev
🆙 CryptoV2 - please red the how_things_work on DC !!!
2026-04-24 16:33:57 +02:00
duckietm b18d65bd79 🆙 CryptoV2 - please red the how_things_work on DC !!! 2026-04-24 15:54:37 +02:00
github-actions[bot] 13958cb11e 🆙 Bump version to 4.1.5 [skip ci] 2026-04-24 09:19:58 +00:00
DuckieTM 7414bc2589 Merge pull request #89 from duckietm/dev
Dev
2026-04-24 11:19:14 +02:00
duckietm da2307f3b5 🆙 Updated Tokens to use JWT rotational tokens 2026-04-24 11:18:46 +02:00
duckietm 030b5ec174 🆕 Handshake on connect - ECDH key exchange (P-256 so it works in every browser's crypto.subtle) 2026-04-23 15:53:30 +02:00
github-actions[bot] ec54dc5c85 🆙 Bump version to 4.1.4 [skip ci] 2026-04-23 08:20:27 +00:00
DuckieTM 50acf6217e Merge pull request #88 from duckietm/dev
Dev
2026-04-23 10:19:32 +02:00
duckietm dd06f2b15c 🆙 Token login added 2026-04-23 10:19:06 +02:00
duckietm d5497e49ad 🆙 Update API and added Copy to Template room command 2026-04-22 16:03:40 +02:00
github-actions[bot] 1916c6c785 🆙 Bump version to 4.1.3 [skip ci] 2026-04-22 05:38:10 +00:00
DuckieTM 4c46c0cb00 Merge pull request #87 from duckietm/dev
Dev
2026-04-22 07:37:10 +02:00
DuckieTM fdcc33212f Merge branch 'main' into dev 2026-04-22 07:37:02 +02:00
duckietm bcee750ff8 🆙 Bump to version 4.1.2 2026-04-22 07:36:19 +02:00
duckietm 872dd11bd2 🆕 API installed
Api has been enabled over the websocket address :

/api/auth/login
/api/auth/register
/api/auth/forgot-password
/api/auth/logout
/api/auth/check-email
/api/health
2026-04-22 07:35:06 +02:00
47 changed files with 3988 additions and 397 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;
+97
View File
@@ -36,3 +36,100 @@ INSERT INTO `emulator_settings` (`key`, `value`) VALUES
('smtp.use_tls', '1'),
('smtp.use_ssl', '0')
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
INSERT INTO emulator_settings (`key`, `value`, `comment`) VALUES
('new_user_credits', '0' , 'This is the default setting for habbo credits when creating an account for the NitroV3 Login'),
('new_user_duckets', '0' , 'This is the default setting for habbo duckets when creating an account for the NitroV3 Login'),
('new_user_diamonds', '0' , 'This is the default setting for habbo diamonds when creating an account for the NitroV3 Login')
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
-- Grant to rank 7 only (adjust rank_7 if your rank id differs)
INSERT INTO `permission_definitions` (`permission_key`, `rank_7`, `comment`) VALUES
('cmd_setroom_template', '1', 'Use the setroom_template to copy the room into the template')
ON DUPLICATE KEY UPDATE `rank_7` = VALUES(`rank_7`);
INSERT INTO `emulator_texts` (`key`, `value`) VALUES
('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'),
('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'),
('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'),
('commands.error.cmd_setroom_template', 'Could not save room as template. Check the server log for details.'),
('commands.error.cmd_setroom_template.no_room', 'You must be inside a room to use this command.')
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
CREATE TABLE IF NOT EXISTS `room_templates` (
`template_id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(128) NOT NULL DEFAULT '',
`description` varchar(256) NOT NULL DEFAULT '',
`thumbnail` varchar(512) NOT NULL DEFAULT '',
`sort_order` int(11) NOT NULL DEFAULT 0,
`enabled` enum('0','1') NOT NULL DEFAULT '1',
`name` varchar(50) NOT NULL DEFAULT '',
`room_description` varchar(250) NOT NULL DEFAULT '',
`model` varchar(100) NOT NULL,
`password` varchar(50) NOT NULL DEFAULT '',
`state` enum('open','locked','password','invisible') NOT NULL DEFAULT 'open',
`users_max` int(11) NOT NULL DEFAULT 25,
`category` int(11) NOT NULL DEFAULT 0,
`paper_floor` varchar(50) NOT NULL DEFAULT '0.0',
`paper_wall` varchar(50) NOT NULL DEFAULT '0.0',
`paper_landscape` varchar(50) NOT NULL DEFAULT '0.0',
`thickness_wall` int(11) NOT NULL DEFAULT 0,
`thickness_floor` int(11) NOT NULL DEFAULT 0,
`moodlight_data` varchar(2048) NOT NULL DEFAULT '',
`override_model` enum('0','1') NOT NULL DEFAULT '0',
`trade_mode` int(2) NOT NULL DEFAULT 2,
`heightmap` mediumtext NOT NULL DEFAULT '',
`door_x` int(11) NOT NULL DEFAULT 0,
`door_y` int(11) NOT NULL DEFAULT 0,
`door_dir` int(4) NOT NULL DEFAULT 2,
PRIMARY KEY (`template_id`),
KEY `enabled_sort` (`enabled`, `sort_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- --------------------------------------------------------
-- Items belonging to a template. Clone target is `items`.
-- `template_id` replaces `room_id`; `user_id` is absent because items
-- are re-owned by the new user at clone time.
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS `room_templates_items` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`template_id` int(11) NOT NULL,
`item_id` int(11) unsigned NOT NULL,
`wall_pos` varchar(20) NOT NULL DEFAULT '',
`x` int(11) NOT NULL DEFAULT 0,
`y` int(11) NOT NULL DEFAULT 0,
`z` double(10,6) NOT NULL DEFAULT 0.000000,
`rot` int(11) NOT NULL DEFAULT 0,
`extra_data` varchar(2096) NOT NULL DEFAULT '',
`wired_data` varchar(4096) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `template_id` (`template_id`),
CONSTRAINT `fk_rt_items_template`
FOREIGN KEY (`template_id`) REFERENCES `room_templates` (`template_id`) ON DELETE CASCADE,
CONSTRAINT `fk_rt_items_item_base`
FOREIGN KEY (`item_id`) REFERENCES `items_base` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `users_remember_families` (
`family_id` char(36) NOT NULL,
`user_id` int(11) NOT NULL,
`current_version` int(11) NOT NULL DEFAULT 1,
`created_at` int(11) NOT NULL,
`expires_at` int(11) NOT NULL,
`revoked` tinyint(1) NOT NULL DEFAULT 0,
`last_ip` varchar(45) NOT NULL DEFAULT '',
PRIMARY KEY (`family_id`),
KEY `user_id` (`user_id`),
KEY `expires_at` (`expires_at`),
CONSTRAINT `fk_remember_family_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
DROP TABLE IF EXISTS `users_remember_tokens`;
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
('login.remember.duration.days', '30'),
('login.remember.rotate.interval.minutes', '15'),
('login.remember.jwt.secret', '')
ON DUPLICATE KEY UPDATE `value` = `value`;
+8
View File
@@ -0,0 +1,8 @@
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
('crypto.ws.enabled', '0'),
('crypto.ws.signing.enabled', '0'),
('crypto.ws.signing.public_key', ''),
('crypto.ws.signing.private_key', '')
ON DUPLICATE KEY UPDATE `value` = `value`;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,28 @@
-- Make sure that the emulator has write access to the badge_path folder !!!!!
CREATE TABLE IF NOT EXISTS `users_custom_badge_settings` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`badge_path` varchar(255) NOT NULL DEFAULT '/var/www/gamedata/c_images/album1584',
`badge_url` varchar(255) NOT NULL DEFAULT '/gamedata/c_images/album1584',
`price_badge` int(11) NOT NULL DEFAULT 0,
`currency_type` int(11) NOT NULL DEFAULT -1,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
INSERT INTO `users_custom_badge_settings` (`id`, `badge_path`, `badge_url`, `price_badge`, `currency_type`)
SELECT 1, '/var/www/gamedata/c_images/album1584', '/gamedata/c_images/album1584', 50, 5
WHERE NOT EXISTS (SELECT 1 FROM `users_custom_badge_settings` WHERE `id` = 1);
CREATE TABLE IF NOT EXISTS `user_custom_badge` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`badge_id` varchar(64) NOT NULL,
`badge_name` varchar(64) NOT NULL DEFAULT '',
`badge_description` varchar(255) NOT NULL DEFAULT '',
`date_created` int(11) NOT NULL DEFAULT 0,
`date_edit` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `badge_id` (`badge_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `fk_user_custom_badge_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
@@ -0,0 +1 @@
ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `background_card_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_overlay_id`;
@@ -1,8 +1,3 @@
-- ============================================================
-- Custom Prefix System - Complete Setup
-- ============================================================
-- 1. Main user prefixes table
CREATE TABLE IF NOT EXISTS `user_prefixes` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
@@ -46,34 +41,6 @@ INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES
('mod'),
('owner');
-- 4. Add effect column (if table already exists without it)
-- ALTER TABLE `user_prefixes` ADD COLUMN IF NOT EXISTS `effect` VARCHAR(50) NOT NULL DEFAULT '' AFTER `icon`;
-- ============================================================
-- Catalog page for custom prefixes
-- ============================================================
-- NOTE: Adjust parent_id to match your catalog parent category ID.
-- Example: parent_id = -1 for root, or the ID of your "Extra" / "Specials" category
INSERT INTO `catalog_pages` (
`parent_id`, `caption`, `caption_save`, `icon_image`, `visible`, `enabled`,
`min_rank`, `page_layout`, `page_strings_1`, `page_strings_2`
) VALUES (
-1,
'Custom Prefix',
'custom_prefix',
1,
1,
1,
1,
'custom_prefix',
'Create your own custom prefix!\rChoose text, colors, icon and effects to stand out in chat.',
''
);
-- ============================================================
-- Command texts (insert into emulator_texts if not present)
-- ============================================================
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
-- GivePrefix command
('commands.keys.cmd_give_prefix', 'giveprefix'),
@@ -105,11 +72,11 @@ INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
('commands.succes.cmd_prefix_blacklist.added', 'Word "%word%" added to prefix blacklist.'),
('commands.succes.cmd_prefix_blacklist.removed', 'Word "%word%" removed from prefix blacklist.');
-- ============================================================
-- Permissions for prefix commands (add to permissions table)
-- ============================================================
INSERT IGNORE INTO `permissions` (`id`, `rank_id`, `permission_name`, `setting_type`) VALUES
(NULL, 7, 'cmd_give_prefix', '1'),
(NULL, 7, 'cmd_list_prefixes', '1'),
(NULL, 7, 'cmd_remove_prefix', '1'),
(NULL, 7, 'cmd_prefix_blacklist', '1');
INSERT IGNORE INTO permission_definitions
(permission_key, max_value, rank_1, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7)
VALUES
('cmd_give_prefix', '1', '0', '0', '0', '0', '0', '0', '1'),
('cmd_list_prefixes', '1', '0', '0', '0', '0', '0', '0', '1'),
('cmd_remove_prefix', '1', '0', '0', '0', '0', '0', '0', '1'),
('cmd_prefix_blacklist', '1', '0', '0', '0', '0', '0', '0', '1');
@@ -0,0 +1,244 @@
CREATE TABLE IF NOT EXISTS `infostand_backgrounds` (
`id` int(11) NOT NULL,
`category` enum('background','stand','overlay','card') NOT NULL,
`min_rank` int(11) NOT NULL DEFAULT 0,
`is_hc_only` tinyint(1) NOT NULL DEFAULT 0,
`is_ambassador_only` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`,`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT INTO `infostand_backgrounds` (`id`, `category`, `min_rank`, `is_hc_only`, `is_ambassador_only`) VALUES
(0, 'background', 0, 0, 0),
(1, 'background', 0, 0, 0),
(2, 'background', 0, 0, 0),
(3, 'background', 0, 0, 0),
(4, 'background', 0, 0, 0),
(5, 'background', 0, 0, 0),
(6, 'background', 0, 0, 0),
(7, 'background', 0, 0, 0),
(8, 'background', 0, 0, 0),
(9, 'background', 0, 0, 0),
(10, 'background', 0, 0, 0),
(11, 'background', 0, 0, 0),
(12, 'background', 0, 0, 0),
(13, 'background', 0, 0, 0),
(14, 'background', 0, 0, 0),
(15, 'background', 0, 0, 0),
(16, 'background', 0, 0, 0),
(17, 'background', 0, 0, 0),
(18, 'background', 0, 0, 0),
(19, 'background', 0, 0, 0),
(20, 'background', 0, 0, 0),
(21, 'background', 0, 0, 0),
(22, 'background', 0, 0, 0),
(23, 'background', 0, 0, 0),
(24, 'background', 0, 0, 0),
(25, 'background', 0, 0, 0),
(26, 'background', 0, 0, 0),
(27, 'background', 0, 0, 0),
(28, 'background', 0, 0, 0),
(29, 'background', 0, 0, 0),
(30, 'background', 0, 0, 0),
(31, 'background', 0, 0, 0),
(32, 'background', 0, 0, 0),
(33, 'background', 0, 0, 0),
(34, 'background', 0, 0, 0),
(35, 'background', 0, 0, 0),
(36, 'background', 0, 0, 0),
(37, 'background', 0, 0, 0),
(38, 'background', 0, 0, 0),
(39, 'background', 0, 0, 0),
(40, 'background', 0, 0, 0),
(41, 'background', 0, 0, 0),
(42, 'background', 0, 1, 0),
(43, 'background', 0, 1, 0),
(44, 'background', 0, 1, 0),
(45, 'background', 0, 1, 0),
(46, 'background', 0, 1, 0),
(47, 'background', 0, 1, 0),
(48, 'background', 0, 1, 0),
(49, 'background', 0, 1, 0),
(50, 'background', 0, 1, 0),
(51, 'background', 0, 1, 0),
(52, 'background', 0, 1, 0),
(53, 'background', 0, 1, 0),
(54, 'background', 0, 1, 0),
(55, 'background', 0, 1, 0),
(56, 'background', 0, 1, 0),
(57, 'background', 0, 1, 0),
(58, 'background', 0, 1, 0),
(59, 'background', 0, 1, 0),
(60, 'background', 0, 1, 0),
(61, 'background', 0, 1, 0),
(62, 'background', 0, 1, 0),
(63, 'background', 0, 1, 0),
(64, 'background', 0, 1, 0),
(65, 'background', 0, 1, 0),
(66, 'background', 0, 1, 0),
(67, 'background', 0, 1, 0),
(68, 'background', 0, 1, 0),
(69, 'background', 0, 1, 0),
(70, 'background', 0, 1, 0),
(71, 'background', 0, 1, 0),
(72, 'background', 0, 1, 0),
(73, 'background', 0, 1, 0),
(74, 'background', 0, 1, 0),
(75, 'background', 0, 1, 0),
(76, 'background', 0, 1, 0),
(77, 'background', 0, 1, 0),
(78, 'background', 0, 1, 0),
(79, 'background', 0, 1, 0),
(80, 'background', 0, 1, 0),
(81, 'background', 0, 1, 0),
(82, 'background', 0, 1, 0),
(83, 'background', 0, 1, 0),
(84, 'background', 0, 1, 0),
(85, 'background', 0, 1, 0),
(86, 'background', 0, 1, 0),
(87, 'background', 0, 1, 0),
(88, 'background', 0, 1, 0),
(89, 'background', 0, 1, 0),
(90, 'background', 0, 1, 0),
(91, 'background', 0, 1, 0),
(92, 'background', 0, 1, 0),
(93, 'background', 0, 1, 0),
(94, 'background', 0, 1, 0),
(95, 'background', 0, 1, 0),
(96, 'background', 0, 1, 0),
(97, 'background', 0, 1, 0),
(98, 'background', 0, 1, 0),
(99, 'background', 0, 1, 0),
(100, 'background', 0, 1, 0),
(101, 'background', 2, 0, 0),
(102, 'background', 0, 1, 0),
(103, 'background', 0, 1, 0),
(104, 'background', 0, 1, 0),
(105, 'background', 0, 1, 0),
(106, 'background', 0, 1, 0),
(107, 'background', 0, 1, 0),
(108, 'background', 0, 1, 0),
(109, 'background', 0, 1, 0),
(110, 'background', 0, 1, 0),
(111, 'background', 0, 1, 0),
(112, 'background', 0, 1, 0),
(113, 'background', 0, 1, 0),
(114, 'background', 0, 1, 0),
(115, 'background', 0, 1, 0),
(116, 'background', 0, 1, 0),
(117, 'background', 0, 1, 0),
(118, 'background', 0, 1, 0),
(119, 'background', 0, 1, 0),
(120, 'background', 0, 1, 0),
(121, 'background', 0, 1, 0),
(122, 'background', 0, 1, 0),
(123, 'background', 0, 1, 0),
(124, 'background', 0, 1, 0),
(125, 'background', 0, 1, 0),
(126, 'background', 0, 1, 0),
(127, 'background', 0, 1, 0),
(128, 'background', 0, 1, 0),
(129, 'background', 0, 1, 0),
(130, 'background', 0, 1, 0),
(131, 'background', 0, 1, 0),
(132, 'background', 0, 1, 0),
(133, 'background', 0, 1, 0),
(134, 'background', 0, 1, 0),
(135, 'background', 0, 1, 0),
(136, 'background', 0, 1, 0),
(137, 'background', 0, 1, 0),
(138, 'background', 0, 1, 0),
(139, 'background', 0, 1, 0),
(140, 'background', 0, 1, 0),
(141, 'background', 0, 1, 0),
(142, 'background', 0, 1, 0),
(143, 'background', 0, 1, 0),
(144, 'background', 0, 1, 0),
(145, 'background', 0, 1, 0),
(146, 'background', 0, 1, 0),
(147, 'background', 0, 1, 0),
(148, 'background', 0, 1, 0),
(149, 'background', 0, 1, 0),
(150, 'background', 0, 1, 0),
(151, 'background', 0, 1, 0),
(152, 'background', 0, 1, 0),
(153, 'background', 0, 1, 0),
(154, 'background', 0, 1, 0),
(155, 'background', 0, 1, 0),
(156, 'background', 0, 1, 0),
(157, 'background', 0, 1, 0),
(158, 'background', 0, 1, 0),
(159, 'background', 0, 1, 0),
(160, 'background', 0, 1, 0),
(161, 'background', 0, 1, 0),
(162, 'background', 0, 1, 0),
(163, 'background', 0, 1, 0),
(164, 'background', 0, 1, 0),
(165, 'background', 0, 1, 0),
(166, 'background', 0, 1, 0),
(167, 'background', 0, 1, 0),
(168, 'background', 0, 1, 0),
(169, 'background', 0, 1, 0),
(170, 'background', 0, 1, 0),
(171, 'background', 0, 1, 0),
(172, 'background', 0, 1, 0),
(173, 'background', 0, 1, 0),
(174, 'background', 0, 1, 0),
(175, 'background', 0, 1, 0),
(176, 'background', 0, 1, 0),
(177, 'background', 0, 1, 0),
(178, 'background', 0, 1, 0),
(179, 'background', 0, 1, 0),
(180, 'background', 0, 1, 0),
(181, 'background', 0, 1, 0),
(182, 'background', 0, 1, 0),
(183, 'background', 0, 1, 0),
(184, 'background', 0, 1, 0),
(185, 'background', 0, 1, 0),
(186, 'background', 0, 1, 0),
(187, 'background', 0, 1, 0),
(0, 'stand', 0, 0, 0),
(1, 'stand', 0, 0, 0),
(2, 'stand', 0, 0, 0),
(3, 'stand', 0, 0, 0),
(4, 'stand', 0, 0, 0),
(5, 'stand', 0, 0, 0),
(6, 'stand', 0, 0, 0),
(7, 'stand', 0, 0, 0),
(8, 'stand', 0, 0, 0),
(9, 'stand', 0, 0, 0),
(10, 'stand', 0, 0, 0),
(11, 'stand', 0, 0, 0),
(12, 'stand', 0, 0, 0),
(13, 'stand', 0, 0, 0),
(14, 'stand', 0, 0, 0),
(15, 'stand', 0, 0, 0),
(16, 'stand', 0, 1, 0),
(17, 'stand', 0, 1, 0),
(18, 'stand', 0, 1, 0),
(19, 'stand', 0, 1, 0),
(20, 'stand', 0, 1, 0),
(21, 'stand', 0, 1, 0),
(0, 'overlay', 0, 0, 0),
(1, 'overlay', 0, 0, 0),
(2, 'overlay', 0, 1, 0),
(3, 'overlay', 0, 1, 0),
(4, 'overlay', 0, 1, 0),
(5, 'overlay', 0, 1, 0),
(6, 'overlay', 0, 1, 0),
(7, 'overlay', 0, 1, 0),
(8, 'overlay', 0, 1, 0),
(1, 'card', 0, 0, 0),
(2, 'card', 0, 0, 0),
(3, 'card', 0, 0, 0),
(4, 'card', 0, 0, 0),
(5, 'card', 0, 0, 0),
(6, 'card', 0, 0, 0),
(7, 'card', 0, 0, 0),
(8, 'card', 0, 0, 0),
(9, 'card', 0, 0, 0),
(10, 'card', 0, 0, 0),
(11, 'card', 0, 0, 0),
(12, 'card', 0, 0, 0),
(13, 'card', 0, 0, 0),
(14, 'card', 0, 0, 0),
(15, 'card', 0, 0, 0);
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.1.5</version>
<version>4.1.13</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -1,17 +0,0 @@
-- ============================================================
-- Catalog & Furni Admin Permission
-- Adds acc_catalogfurni permission to the permissions table
-- Required by: CatalogAdmin packet handlers (10050-10059)
-- ============================================================
-- 1. Add the column to the permissions table
ALTER TABLE `permissions`
ADD COLUMN `acc_catalogfurni` ENUM('0','1') NOT NULL DEFAULT '0'
AFTER `acc_catalog_ids`;
-- 2. Enable for Administrator (rank 7) by default
UPDATE `permissions` SET `acc_catalogfurni` = '1' WHERE `id` = 7;
-- Optional: enable for other ranks as needed
-- UPDATE `permissions` SET `acc_catalogfurni` = '1' WHERE `id` = 6; -- Super Mod
-- UPDATE `permissions` SET `acc_catalogfurni` = '1' WHERE `id` = 5; -- Moderator
@@ -28,6 +28,7 @@ public class RoomUserPetComposer extends MessageComposer {
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendString(this.petType + " " + this.race + " " + this.color + " 2 2 -1 0 3 -1 0");
this.response.appendInt(this.habbo.getRoomUnit().getId());
this.response.appendInt(this.habbo.getRoomUnit().getX());
@@ -22,6 +22,8 @@ import com.eu.habbo.habbohotel.polls.PollManager;
import com.eu.habbo.habbohotel.rooms.RoomChatBubbleManager;
import com.eu.habbo.habbohotel.rooms.RoomManager;
import com.eu.habbo.habbohotel.users.HabboManager;
import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager;
import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager;
import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionManager;
import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionScheduler;
import org.slf4j.Logger;
@@ -58,6 +60,8 @@ public class GameEnvironment {
private SubscriptionManager subscriptionManager;
private CalendarManager calendarManager;
private RoomChatBubbleManager roomChatBubbleManager;
private CustomBadgeManager customBadgeManager;
private InfostandBackgroundManager infostandBackgroundManager;
public void load() throws Exception {
LOGGER.info("GameEnvironment -> Loading...");
@@ -84,6 +88,8 @@ public class GameEnvironment {
this.pollManager = new PollManager();
this.calendarManager = new CalendarManager();
this.roomChatBubbleManager = new RoomChatBubbleManager();
this.customBadgeManager = new CustomBadgeManager();
this.infostandBackgroundManager = new InfostandBackgroundManager();
this.roomManager.loadPublicRooms();
this.navigatorManager.loadNavigator();
@@ -219,4 +225,12 @@ public class GameEnvironment {
public RoomChatBubbleManager getRoomChatBubbleManager() {
return roomChatBubbleManager;
}
public CustomBadgeManager getCustomBadgeManager() {
return this.customBadgeManager;
}
public InfostandBackgroundManager getInfostandBackgroundManager() {
return this.infostandBackgroundManager;
}
}
@@ -190,7 +190,7 @@ public class CommandHandler {
addCommand(new ControlCommand());
addCommand(new CoordsCommand());
addCommand(new CreditsCommand());
addCommand(new DanceCommand());
addCommand(new DanceCommand());
addCommand(new DiagonalCommand());
addCommand(new DisconnectCommand());
addCommand(new EjectAllCommand());
@@ -230,7 +230,7 @@ public class CommandHandler {
addCommand(new MutePetsCommand());
addCommand(new PetInfoCommand());
addCommand(new PickallCommand());
addCommand(new PingCommand());
addCommand(new PingCommand());
addCommand(new PixelCommand());
addCommand(new PluginsCommand());
addCommand(new PointsCommand());
@@ -253,6 +253,7 @@ public class CommandHandler {
addCommand(new SayCommand());
addCommand(new SetMaxCommand());
addCommand(new SetPollCommand());
addCommand(new SetRoomTemplateCommand());
addCommand(new SetSpeedCommand());
addCommand(new ShoutAllCommand());
addCommand(new ShoutCommand());
@@ -0,0 +1,116 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
public class SetRoomTemplateCommand extends Command {
private static final Logger LOGGER = LoggerFactory.getLogger(SetRoomTemplateCommand.class);
public SetRoomTemplateCommand() {
super("cmd_setroom_template", Emulator.getTexts().getValue("commands.keys.cmd_setroom_template").split(";"));
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
Room room = gameClient.getHabbo().getHabboInfo().getCurrentRoom();
if (room == null) {
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.error.cmd_setroom_template.no_room"),
RoomChatMessageBubbles.ALERT);
return true;
}
String yes = Emulator.getTexts().getValue("generic.yes");
if (params.length < 2 || !params[1].equalsIgnoreCase(yes)) {
gameClient.getHabbo().alert(
Emulator.getTexts().getValue("commands.succes.cmd_setroom_template.verify")
.replace("%generic.yes%", yes)
.replace("%roomname%", room.getName()));
return true;
}
int newTemplateId = 0;
int itemsCopied = 0;
int itemsSkipped = 0;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement insTemplate = connection.prepareStatement(
"INSERT INTO room_templates (title, description, thumbnail, sort_order, enabled, " +
"name, room_description, model, password, state, users_max, category, " +
"paper_floor, paper_wall, paper_landscape, thickness_wall, thickness_floor, " +
"moodlight_data, override_model, trade_mode) " +
"(SELECT name, description, '', 0, '1', " +
"name, description, model, password, state, users_max, category, " +
"paper_floor, paper_wall, paper_landscape, thickness_wall, thickness_floor, " +
"moodlight_data, override_model, trade_mode " +
"FROM rooms WHERE id = ?)",
Statement.RETURN_GENERATED_KEYS)) {
insTemplate.setInt(1, room.getId());
insTemplate.executeUpdate();
try (ResultSet keys = insTemplate.getGeneratedKeys()) {
if (keys.next()) newTemplateId = keys.getInt(1);
}
}
if (newTemplateId <= 0) {
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.error.cmd_setroom_template"),
RoomChatMessageBubbles.ALERT);
return true;
}
if (room.hasCustomLayout()) {
try (PreparedStatement updLayout = connection.prepareStatement(
"UPDATE room_templates t " +
"JOIN room_models_custom c ON c.id = ? " +
"SET t.heightmap = c.heightmap, t.door_x = c.door_x, " +
" t.door_y = c.door_y, t.door_dir = c.door_dir " +
"WHERE t.template_id = ?")) {
updLayout.setInt(1, room.getId());
updLayout.setInt(2, newTemplateId);
updLayout.executeUpdate();
}
}
try (PreparedStatement insItems = connection.prepareStatement(
"INSERT INTO room_templates_items (template_id, item_id, wall_pos, x, y, z, rot, extra_data, wired_data) " +
"SELECT ?, i.item_id, i.wall_pos, i.x, i.y, i.z, i.rot, i.extra_data, i.wired_data " +
"FROM items i JOIN items_base ib ON ib.id = i.item_id " +
"WHERE i.room_id = ?")) {
insItems.setInt(1, newTemplateId);
insItems.setInt(2, room.getId());
itemsCopied = insItems.executeUpdate();
}
try (PreparedStatement countTotal = connection.prepareStatement(
"SELECT COUNT(*) FROM items WHERE room_id = ?")) {
countTotal.setInt(1, room.getId());
try (ResultSet rs = countTotal.executeQuery()) {
if (rs.next()) itemsSkipped = Math.max(0, rs.getInt(1) - itemsCopied);
}
}
} catch (SQLException e) {
LOGGER.error("cmd_setroom_template failed for roomId=" + room.getId(), e);
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.error.cmd_setroom_template"),
RoomChatMessageBubbles.ALERT);
return true;
}
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_setroom_template")
.replace("%id%", Integer.toString(newTemplateId))
.replace("%items%", Integer.toString(itemsCopied))
.replace("%skipped%", Integer.toString(itemsSkipped)),
RoomChatMessageBubbles.ALERT);
return true;
}
}
@@ -1,23 +1,16 @@
package com.eu.habbo.habbohotel.gameclients;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomUnit;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserEffectComposer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
/**
* Manages a grace period for disconnected users. Instead of immediately
* disposing a Habbo when their WebSocket drops, the Habbo is held in
* a "ghost" state for a configurable number of seconds. If the same
* user reconnects (via SSO ticket) within the grace window, their
* existing Habbo object is resumed on the new connection — keeping
* them in their room, preserving inventory state, etc.
*
* Config key: session.reconnect.grace.seconds (default: 30)
*/
public class SessionResumeManager {
private static final Logger LOGGER = LoggerFactory.getLogger(SessionResumeManager.class);
@@ -37,12 +30,10 @@ public class SessionResumeManager {
return Emulator.getConfig().getInt("session.reconnect.grace.seconds", 30);
}
/**
* Park a disconnected Habbo in ghost mode. Their room presence is
* preserved, but the old GameClient channel is closed.
*
* @return true if the habbo was parked (grace period > 0), false if immediate dispose should happen
*/
public int getPausedEffectId() {
return Emulator.getConfig().getInt("session.reconnect.effect.id", 170);
}
public boolean parkHabbo(Habbo habbo, String ssoTicket) {
int graceSeconds = getGracePeriodSeconds();
if (graceSeconds <= 0) {
@@ -51,7 +42,6 @@ public class SessionResumeManager {
int userId = habbo.getHabboInfo().getId();
// Cancel any existing ghost session for this user
GhostSession existing = ghostSessions.remove(userId);
if (existing != null && existing.disposeFuture != null) {
existing.disposeFuture.cancel(false);
@@ -60,12 +50,18 @@ public class SessionResumeManager {
LOGGER.info("[SessionResume] Parking {} (id={}) for {}s grace period",
habbo.getHabboInfo().getUsername(), userId, graceSeconds);
// Restore the SSO ticket so the client can reconnect with the same ticket
if (ssoTicket != null && !ssoTicket.isEmpty()) {
restoreSsoTicket(userId, ssoTicket);
}
// Schedule the final disconnect after the grace period
int previousEffectId = 0;
int previousEffectEnd = 0;
RoomUnit unit = habbo.getRoomUnit();
if (unit != null) {
previousEffectId = unit.getEffectId();
previousEffectEnd = unit.getEffectEndTimestamp();
}
ScheduledFuture<?> future = Emulator.getThreading().run(() -> {
GhostSession ghost = ghostSessions.remove(userId);
if (ghost != null) {
@@ -75,22 +71,19 @@ public class SessionResumeManager {
}
}, graceSeconds * 1000);
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future));
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd));
applyPausedEffect(habbo);
return true;
}
/**
* Try to resume a ghost session for the given user ID.
*
* @return the parked Habbo if found within grace period, null otherwise
*/
public Habbo resumeSession(int userId) {
GhostSession ghost = ghostSessions.remove(userId);
if (ghost == null) {
return null;
}
// Cancel the scheduled dispose
if (ghost.disposeFuture != null) {
ghost.disposeFuture.cancel(false);
}
@@ -98,19 +91,15 @@ public class SessionResumeManager {
LOGGER.info("[SessionResume] Resuming session for {} (id={})",
ghost.habbo.getHabboInfo().getUsername(), userId);
restorePausedEffect(ghost);
return ghost.habbo;
}
/**
* Check if a user has a ghost session (is in grace period).
*/
public boolean hasGhostSession(int userId) {
return ghostSessions.containsKey(userId);
}
/**
* Immediately expire all ghost sessions (e.g. on emulator shutdown).
*/
public void disposeAll() {
for (GhostSession ghost : ghostSessions.values()) {
if (ghost.disposeFuture != null) {
@@ -121,9 +110,6 @@ public class SessionResumeManager {
ghostSessions.clear();
}
/**
* Perform the actual full disconnect that normally happens in Habbo.disconnect().
*/
private void performFullDisconnect(Habbo habbo) {
try {
habbo.getHabboInfo().setOnline(false);
@@ -132,7 +118,6 @@ public class SessionResumeManager {
LOGGER.error("[SessionResume] Error during deferred disconnect", e);
}
// Clear the SSO ticket now that the grace period is truly over
clearSsoTicket(habbo.getHabboInfo().getId());
}
@@ -148,6 +133,38 @@ public class SessionResumeManager {
}
}
private void applyPausedEffect(Habbo habbo) {
int effectId = getPausedEffectId();
if (effectId <= 0) return;
try {
RoomUnit unit = habbo.getRoomUnit();
Room room = habbo.getHabboInfo() == null ? null : habbo.getHabboInfo().getCurrentRoom();
if (unit == null || room == null) return;
int endTimestamp = Emulator.getIntUnixTimestamp() + getGracePeriodSeconds() + 10;
unit.setEffectId(effectId, endTimestamp);
room.sendComposer(new RoomUserEffectComposer(unit).compose());
} catch (Exception e) {
LOGGER.error("[SessionResume] Failed to apply paused effect", e);
}
}
private void restorePausedEffect(GhostSession ghost) {
try {
Habbo habbo = ghost.habbo;
RoomUnit unit = habbo.getRoomUnit();
Room room = habbo.getHabboInfo() == null ? null : habbo.getHabboInfo().getCurrentRoom();
if (unit == null || room == null) return;
int pausedEffectId = getPausedEffectId();
if (unit.getEffectId() == pausedEffectId) {
unit.setEffectId(ghost.previousEffectId, ghost.previousEffectEnd);
room.sendComposer(new RoomUserEffectComposer(unit).compose());
}
} catch (Exception e) {
LOGGER.error("[SessionResume] Failed to restore previous effect", e);
}
}
private void clearSsoTicket(int userId) {
try (var connection = Emulator.getDatabase().getDataSource().getConnection();
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
@@ -163,11 +180,16 @@ public class SessionResumeManager {
final Habbo habbo;
final String ssoTicket;
final ScheduledFuture<?> disposeFuture;
final int previousEffectId;
final int previousEffectEnd;
GhostSession(Habbo habbo, String ssoTicket, ScheduledFuture<?> disposeFuture) {
GhostSession(Habbo habbo, String ssoTicket, ScheduledFuture<?> disposeFuture,
int previousEffectId, int previousEffectEnd) {
this.habbo = habbo;
this.ssoTicket = ssoTicket;
this.disposeFuture = disposeFuture;
this.previousEffectId = previousEffectId;
this.previousEffectEnd = previousEffectEnd;
}
}
}
@@ -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
}
}
@@ -15,25 +15,19 @@ import com.eu.habbo.habbohotel.items.interactions.games.tag.InteractionTagField;
import com.eu.habbo.habbohotel.items.interactions.games.tag.InteractionTagPole;
import com.eu.habbo.habbohotel.items.interactions.pets.*;
import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectSendSignal;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredBlob;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFurniVariable;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRoomVariable;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUserVariable;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableEcho;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableReference;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableTextConnector;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraContextVariable;
import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport;
import com.eu.habbo.habbohotel.items.interactions.wired.extra.*;
import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerReceiveSignal;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.habbohotel.users.HabboManager;
import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredMovementPhysics;
import com.eu.habbo.habbohotel.wired.tick.WiredTickable;
import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer;
import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer;
import com.eu.habbo.messages.outgoing.rooms.items.*;
import com.eu.habbo.plugin.Event;
import com.eu.habbo.plugin.events.furniture.*;
@@ -94,7 +88,7 @@ public class RoomItemManager {
}
try (PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM items WHERE room_id = ?")) {
"SELECT * FROM items WHERE room_id = ?")) {
statement.setInt(1, this.room.getId());
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
@@ -106,8 +100,8 @@ public class RoomItemManager {
}
if (this.itemCount() > Room.MAXIMUM_FURNI) {
LOGGER.error("Room ID: {} has exceeded the furniture limit ({} > {}).",
this.room.getId(), this.itemCount(), Room.MAXIMUM_FURNI);
LOGGER.error("Room ID: {} has exceeded the furniture limit ({} > {}).",
this.room.getId(), this.itemCount(), Room.MAXIMUM_FURNI);
}
}
@@ -116,7 +110,7 @@ public class RoomItemManager {
*/
public void loadWiredData(Connection connection) {
try (PreparedStatement statement = connection.prepareStatement(
"SELECT id, wired_data FROM items WHERE room_id = ? AND wired_data<>''")) {
"SELECT id, wired_data FROM items WHERE room_id = ? AND wired_data<>''")) {
statement.setInt(1, this.room.getId());
try (ResultSet set = statement.executeQuery()) {
@@ -274,7 +268,7 @@ public class RoomItemManager {
}
if (iterator.value().getBaseItem().getInteractionType().getType()
== InteractionPostIt.class) {
== InteractionPostIt.class) {
items.add(iterator.value());
}
}
@@ -359,7 +353,7 @@ public class RoomItemManager {
}
if (!(tile.x >= item.getX() && tile.x <= item.getX() + width - 1 && tile.y >= item.getY()
&& tile.y <= item.getY() + length - 1)) {
&& tile.y <= item.getY() + length - 1)) {
continue;
}
@@ -447,7 +441,7 @@ public class RoomItemManager {
}
if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem)
> item.getZ() + Item.getCurrentHeight(item)) {
> item.getZ() + Item.getCurrentHeight(item)) {
continue;
}
@@ -516,7 +510,7 @@ public class RoomItemManager {
}
if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem)
> item.getZ() + Item.getCurrentHeight(item)) {
> item.getZ() + Item.getCurrentHeight(item)) {
continue;
}
@@ -598,7 +592,7 @@ public class RoomItemManager {
}
if (lowestChair != null && lowestChair.getZ() + Item.getCurrentHeight(lowestChair)
> item.getZ() + Item.getCurrentHeight(item)) {
> item.getZ() + Item.getCurrentHeight(item)) {
continue;
}
@@ -647,7 +641,7 @@ public class RoomItemManager {
this.furniOwnerNames.put(item.getUserId(), habbo.getUsername());
} else {
LOGGER.error("Failed to find username for item (ID: {}, UserID: {})",
item.getId(), item.getUserId());
item.getId(), item.getUserId());
}
}
}
@@ -665,7 +659,7 @@ public class RoomItemManager {
if (specialTypes == null) {
return;
}
boolean isWiredItem = false;
synchronized (specialTypes) {
@@ -714,29 +708,29 @@ public class RoomItemManager {
} else if (item instanceof InteractionPetTree) {
specialTypes.addPetTree((InteractionPetTree) item);
} else if (item instanceof InteractionMoodLight ||
item instanceof InteractionPyramid ||
item instanceof InteractionMusicDisc ||
item instanceof InteractionBattleBanzaiSphere ||
item instanceof InteractionTalkingFurniture ||
item instanceof InteractionWater ||
item instanceof InteractionWaterItem ||
item instanceof InteractionMuteArea ||
item instanceof InteractionBuildArea ||
item instanceof InteractionTagPole ||
item instanceof InteractionTagField ||
item instanceof InteractionJukeBox ||
item instanceof InteractionPetBreedingNest ||
item instanceof InteractionBlackHole ||
item instanceof InteractionWiredHighscore ||
item instanceof InteractionStickyPole ||
item instanceof WiredBlob ||
item instanceof InteractionTent ||
item instanceof InteractionSnowboardSlope ||
item instanceof InteractionFireworks) {
item instanceof InteractionPyramid ||
item instanceof InteractionMusicDisc ||
item instanceof InteractionBattleBanzaiSphere ||
item instanceof InteractionTalkingFurniture ||
item instanceof InteractionWater ||
item instanceof InteractionWaterItem ||
item instanceof InteractionMuteArea ||
item instanceof InteractionBuildArea ||
item instanceof InteractionTagPole ||
item instanceof InteractionTagField ||
item instanceof InteractionJukeBox ||
item instanceof InteractionPetBreedingNest ||
item instanceof InteractionBlackHole ||
item instanceof InteractionWiredHighscore ||
item instanceof InteractionStickyPole ||
item instanceof WiredBlob ||
item instanceof InteractionTent ||
item instanceof InteractionSnowboardSlope ||
item instanceof InteractionFireworks) {
specialTypes.addUndefined(item);
}
}
// Invalidate wired cache when wired items are added
if (isWiredItem) {
WiredManager.invalidateRoom(this.room);
@@ -810,7 +804,7 @@ public class RoomItemManager {
}
this.room.getFurniVariableManager().removeAssignmentsForFurni(item.getId());
boolean isWiredItem = false;
// Unregister from tick service for time-based wired triggers (new 50ms tick system)
@@ -822,53 +816,53 @@ public class RoomItemManager {
specialTypes.removeCycleTask((ICycleable) item);
}
if (item instanceof InteractionBattleBanzaiTeleporter) {
specialTypes.removeBanzaiTeleporter((InteractionBattleBanzaiTeleporter) item);
} else if (item instanceof InteractionWiredTrigger) {
specialTypes.removeTrigger((InteractionWiredTrigger) item);
isWiredItem = true;
} else if (item instanceof InteractionWiredEffect) {
specialTypes.removeEffect((InteractionWiredEffect) item);
isWiredItem = true;
} else if (item instanceof InteractionWiredCondition) {
specialTypes.removeCondition((InteractionWiredCondition) item);
isWiredItem = true;
} else if (item instanceof InteractionWiredExtra) {
boolean removedContextDefinition = false;
boolean removedVariableTextConnector = false;
if (item instanceof WiredExtraUserVariable) {
this.room.getUserVariableManager().removeDefinition(item.getId());
} else if (item instanceof WiredExtraFurniVariable) {
this.room.getFurniVariableManager().removeDefinition(item.getId());
} else if (item instanceof WiredExtraRoomVariable) {
this.room.getRoomVariableManager().removeDefinition(item.getId());
} else if (item instanceof WiredExtraContextVariable) {
removedContextDefinition = true;
} else if (item instanceof WiredExtraVariableTextConnector) {
removedVariableTextConnector = true;
} else if (item instanceof WiredExtraVariableReference) {
if (((WiredExtraVariableReference) item).isRoomReference()) {
this.room.getRoomVariableManager().removeDefinition(item.getId());
} else {
this.room.getUserVariableManager().removeDefinition(item.getId());
}
} else if (item instanceof WiredExtraVariableEcho) {
WiredExtraVariableEcho echo = (WiredExtraVariableEcho) item;
if (item instanceof InteractionBattleBanzaiTeleporter) {
specialTypes.removeBanzaiTeleporter((InteractionBattleBanzaiTeleporter) item);
} else if (item instanceof InteractionWiredTrigger) {
specialTypes.removeTrigger((InteractionWiredTrigger) item);
isWiredItem = true;
} else if (item instanceof InteractionWiredEffect) {
specialTypes.removeEffect((InteractionWiredEffect) item);
isWiredItem = true;
} else if (item instanceof InteractionWiredCondition) {
specialTypes.removeCondition((InteractionWiredCondition) item);
isWiredItem = true;
} else if (item instanceof InteractionWiredExtra) {
boolean removedContextDefinition = false;
boolean removedVariableTextConnector = false;
if (item instanceof WiredExtraUserVariable) {
this.room.getUserVariableManager().removeDefinition(item.getId());
} else if (item instanceof WiredExtraFurniVariable) {
this.room.getFurniVariableManager().removeDefinition(item.getId());
} else if (item instanceof WiredExtraRoomVariable) {
this.room.getRoomVariableManager().removeDefinition(item.getId());
} else if (item instanceof WiredExtraContextVariable) {
removedContextDefinition = true;
} else if (item instanceof WiredExtraVariableTextConnector) {
removedVariableTextConnector = true;
} else if (item instanceof WiredExtraVariableReference) {
if (((WiredExtraVariableReference) item).isRoomReference()) {
this.room.getRoomVariableManager().removeDefinition(item.getId());
} else {
this.room.getUserVariableManager().removeDefinition(item.getId());
}
} else if (item instanceof WiredExtraVariableEcho) {
WiredExtraVariableEcho echo = (WiredExtraVariableEcho) item;
if (echo.isRoomEcho()) {
this.room.getRoomVariableManager().removeDefinition(item.getId());
} else if (echo.isFurniEcho()) {
this.room.getFurniVariableManager().removeDefinition(item.getId());
} else {
this.room.getUserVariableManager().removeDefinition(item.getId());
}
}
specialTypes.removeExtra((InteractionWiredExtra) item);
if (removedContextDefinition || removedVariableTextConnector) {
WiredContextVariableSupport.broadcastDefinitions(this.room);
}
isWiredItem = true;
} else if (item instanceof InteractionRoller) {
if (echo.isRoomEcho()) {
this.room.getRoomVariableManager().removeDefinition(item.getId());
} else if (echo.isFurniEcho()) {
this.room.getFurniVariableManager().removeDefinition(item.getId());
} else {
this.room.getUserVariableManager().removeDefinition(item.getId());
}
}
specialTypes.removeExtra((InteractionWiredExtra) item);
if (removedContextDefinition || removedVariableTextConnector) {
WiredContextVariableSupport.broadcastDefinitions(this.room);
}
isWiredItem = true;
} else if (item instanceof InteractionRoller) {
specialTypes.removeRoller((InteractionRoller) item);
} else if (item instanceof InteractionGameScoreboard) {
specialTypes.removeScoreboard((InteractionGameScoreboard) item);
@@ -889,26 +883,26 @@ public class RoomItemManager {
} else if (item instanceof InteractionPetTree) {
specialTypes.removePetTree((InteractionPetTree) item);
} else if (item instanceof InteractionMoodLight ||
item instanceof InteractionPyramid ||
item instanceof InteractionMusicDisc ||
item instanceof InteractionBattleBanzaiSphere ||
item instanceof InteractionTalkingFurniture ||
item instanceof InteractionWaterItem ||
item instanceof InteractionWater ||
item instanceof InteractionMuteArea ||
item instanceof InteractionTagPole ||
item instanceof InteractionTagField ||
item instanceof InteractionJukeBox ||
item instanceof InteractionPetBreedingNest ||
item instanceof InteractionBlackHole ||
item instanceof InteractionWiredHighscore ||
item instanceof InteractionStickyPole ||
item instanceof WiredBlob ||
item instanceof InteractionTent ||
item instanceof InteractionSnowboardSlope) {
item instanceof InteractionPyramid ||
item instanceof InteractionMusicDisc ||
item instanceof InteractionBattleBanzaiSphere ||
item instanceof InteractionTalkingFurniture ||
item instanceof InteractionWaterItem ||
item instanceof InteractionWater ||
item instanceof InteractionMuteArea ||
item instanceof InteractionTagPole ||
item instanceof InteractionTagField ||
item instanceof InteractionJukeBox ||
item instanceof InteractionPetBreedingNest ||
item instanceof InteractionBlackHole ||
item instanceof InteractionWiredHighscore ||
item instanceof InteractionStickyPole ||
item instanceof WiredBlob ||
item instanceof InteractionTent ||
item instanceof InteractionSnowboardSlope) {
specialTypes.removeUndefined(item);
}
// Invalidate wired cache when wired items are removed
if (isWiredItem || cleanedSignalAntennaReferences) {
WiredManager.invalidateRoom(this.room);
@@ -936,9 +930,9 @@ public class RoomItemManager {
if (item.getBaseItem().getType() == FurnitureType.FLOOR) {
this.room.sendComposer(new FloorItemUpdateComposer(item).compose());
this.room.updateTiles(this.room.getLayout()
.getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()),
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
item.getRotation()));
.getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()),
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
item.getRotation()));
} else if (item.getBaseItem().getType() == FurnitureType.WALL) {
this.room.sendComposer(new WallItemUpdateComposer(item).compose());
}
@@ -963,9 +957,9 @@ public class RoomItemManager {
}
this.room.updateTiles(this.room.getLayout()
.getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()),
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
item.getRotation()));
.getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()),
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
item.getRotation()));
if (item instanceof InteractionMultiHeight) {
((InteractionMultiHeight) item).updateUnitsOnItem(this.room);
@@ -1032,7 +1026,7 @@ public class RoomItemManager {
if (Emulator.getPluginManager().isRegistered(FurniturePickedUpEvent.class, true)) {
FurniturePickedUpEvent event = Emulator.getPluginManager()
.fireEvent(new FurniturePickedUpEvent(item, picker));
.fireEvent(new FurniturePickedUpEvent(item, picker));
if (event.isCancelled()) {
return;
@@ -1060,10 +1054,10 @@ public class RoomItemManager {
}
THashSet<RoomTile> updatedTiles = this.room.getLayout().getTilesAt(
this.room.getLayout().getTile(item.getX(), item.getY()),
item.getBaseItem().getWidth(),
item.getBaseItem().getLength(),
item.getRotation());
this.room.getLayout().getTile(item.getX(), item.getY()),
item.getBaseItem().getWidth(),
item.getBaseItem().getLength(),
item.getRotation());
this.room.updateTiles(updatedTiles);
for (RoomTile tile : updatedTiles) {
@@ -1114,6 +1108,7 @@ public class RoomItemManager {
if (habbo != null && !inventoryItems.isEmpty()) {
habbo.getInventory().getItemsComponent().addItems(inventoryItems);
habbo.getClient().sendResponse(new AddHabboItemComposer(inventoryItems));
habbo.getClient().sendResponse(new InventoryRefreshComposer());
}
for (HabboItem i : items) {
@@ -1160,7 +1155,7 @@ public class RoomItemManager {
}
userItemsMap.computeIfAbsent(iterator.value().getUserId(), k -> new THashSet<>())
.add(iterator.value());
.add(iterator.value());
}
}
@@ -1182,6 +1177,7 @@ public class RoomItemManager {
if (user != null && !inventoryItems.isEmpty()) {
user.getInventory().getItemsComponent().addItems(inventoryItems);
user.getClient().sendResponse(new AddHabboItemComposer(inventoryItems));
user.getClient().sendResponse(new InventoryRefreshComposer());
}
}
}
@@ -1222,7 +1218,7 @@ public class RoomItemManager {
for (short y = 0; y < item.getBaseItem().getLength(); y++) {
for (short x = 0; x < item.getBaseItem().getWidth(); x++) {
RoomTile tile = this.room.getLayout().getTile(
(short) (item.getX() + x), (short) (item.getY() + y));
(short) (item.getX() + x), (short) (item.getY() + y));
if (tile != null) {
lockedTiles.add(tile);
@@ -1233,7 +1229,7 @@ public class RoomItemManager {
for (short y = 0; y < item.getBaseItem().getWidth(); y++) {
for (short x = 0; x < item.getBaseItem().getLength(); x++) {
RoomTile tile = this.room.getLayout().getTile(
(short) (item.getX() + x), (short) (item.getY() + y));
(short) (item.getX() + x), (short) (item.getY() + y));
if (tile != null) {
lockedTiles.add(tile);
@@ -1324,8 +1320,8 @@ public class RoomItemManager {
rotation %= 8;
if (this.room.hasRights(habbo) || this.room.getGuildRightLevel(habbo)
.isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || habbo.hasPermission(
Permission.ACC_MOVEROTATE) || BuildersClubRoomSupport.canPlaceInRoom(habbo, this.room)) {
.isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || habbo.hasPermission(
Permission.ACC_MOVEROTATE) || BuildersClubRoomSupport.canPlaceInRoom(habbo, this.room)) {
return FurnitureMovementError.NONE;
}
@@ -1334,10 +1330,10 @@ public class RoomItemManager {
if (rentSpace != null) {
if (!RoomLayout.squareInSquare(RoomLayout.getRectangle(rentSpace.getX(), rentSpace.getY(),
rentSpace.getBaseItem().getWidth(), rentSpace.getBaseItem().getLength(),
rentSpace.getRotation()),
RoomLayout.getRectangle(tile.x, tile.y, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation))) {
rentSpace.getBaseItem().getWidth(), rentSpace.getBaseItem().getLength(),
rentSpace.getRotation()),
RoomLayout.getRectangle(tile.x, tile.y, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation))) {
return FurnitureMovementError.NO_RIGHTS;
} else {
return FurnitureMovementError.NONE;
@@ -1347,7 +1343,7 @@ public class RoomItemManager {
for (HabboItem area : this.room.getRoomSpecialTypes().getItemsOfType(InteractionBuildArea.class)) {
if (((InteractionBuildArea) area).inSquare(tile) && ((InteractionBuildArea) area).isBuilder(
habbo.getHabboInfo().getUsername())) {
habbo.getHabboInfo().getUsername())) {
return FurnitureMovementError.NONE;
}
}
@@ -1438,14 +1434,14 @@ public class RoomItemManager {
}
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation);
item.getBaseItem().getLength(), rotation);
for (RoomTile t : occupiedTiles) {
if (t.state == RoomTileState.INVALID) {
return FurnitureMovementError.INVALID_MOVE;
}
if (!Emulator.getConfig().getBoolean("wired.place.under", false) || (
Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable()
&& !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) {
Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable()
&& !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) {
if (checkForUnits && this.room.hasHabbosAt(t.x, t.y)) {
return FurnitureMovementError.TILE_HAS_HABBOS;
}
@@ -1490,7 +1486,7 @@ public class RoomItemManager {
}
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation);
item.getBaseItem().getLength(), rotation);
for (RoomTile t : occupiedTiles) {
if (t.state == RoomTileState.INVALID) {
return FurnitureMovementError.INVALID_MOVE;
@@ -1542,7 +1538,7 @@ public class RoomItemManager {
boolean pluginHelper = false;
if (Emulator.getPluginManager().isRegistered(FurniturePlacedEvent.class, true)) {
FurniturePlacedEvent event = Emulator.getPluginManager()
.fireEvent(new FurniturePlacedEvent(item, owner, tile));
.fireEvent(new FurniturePlacedEvent(item, owner, tile));
if (event.isCancelled()) {
return FurnitureMovementError.CANCEL_PLUGIN_PLACE;
@@ -1553,7 +1549,7 @@ public class RoomItemManager {
RoomLayout layout = this.room.getLayout();
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation);
item.getBaseItem().getLength(), rotation);
FurnitureMovementError fits = furnitureFitsAt(tile, item, rotation);
@@ -1572,7 +1568,7 @@ public class RoomItemManager {
if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) {
FurnitureBuildheightEvent event = Emulator.getPluginManager()
.fireEvent(new FurnitureBuildheightEvent(item, owner, 0.00, height));
.fireEvent(new FurnitureBuildheightEvent(item, owner, 0.00, height));
if (event.hasChangedHeight()) {
height = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight();
}
@@ -1592,7 +1588,7 @@ public class RoomItemManager {
item.onPlace(this.room);
this.room.updateTiles(occupiedTiles);
this.room.sendComposer(
new AddFloorItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose());
new AddFloorItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose());
if (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item)) {
RoomConfInvisSupport.sendState(this.room);
@@ -1620,7 +1616,7 @@ public class RoomItemManager {
*/
public FurnitureMovementError placeWallFurniAt(HabboItem item, String wallPosition, Habbo owner) {
if (!(this.room.hasRights(owner) || this.room.getGuildRightLevel(owner)
.isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || BuildersClubRoomSupport.canPlaceInRoom(owner, this.room))) {
.isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || BuildersClubRoomSupport.canPlaceInRoom(owner, this.room))) {
return FurnitureMovementError.NO_RIGHTS;
}
@@ -1638,7 +1634,7 @@ public class RoomItemManager {
this.furniOwnerNames.put(item.getUserId(), this.resolveOwnerName(item, owner));
}
this.room.sendComposer(
new AddWallItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose());
new AddWallItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose());
item.needsUpdate(true);
this.addHabboItem(item);
item.setRoomId(this.room.getId());
@@ -1989,7 +1985,7 @@ public class RoomItemManager {
boolean pluginHelper = false;
if (Emulator.getPluginManager().isRegistered(FurnitureMovedEvent.class, true)) {
FurnitureMovedEvent event = Emulator.getPluginManager()
.fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile));
.fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile));
if (event.isCancelled()) {
return FurnitureMovementError.CANCEL_PLUGIN_MOVE;
}
@@ -2002,9 +1998,9 @@ public class RoomItemManager {
// Check if can be placed at new position
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation);
item.getBaseItem().getLength(), rotation);
THashSet<RoomTile> newOccupiedTiles = layout.getTilesAt(tile,
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation);
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation);
HabboItem topItem = this.getTopItemAt(occupiedTiles, null);
@@ -2013,15 +2009,15 @@ public class RoomItemManager {
for (RoomTile t : occupiedTiles) {
HabboItem tileTopItem = this.getTopItemAt(t.x, t.y);
if (!magicTile && ((tileTopItem != null && tileTopItem != item ? (
t.state.equals(RoomTileState.INVALID) || !t.getAllowStack()
t.state.equals(RoomTileState.INVALID) || !t.getAllowStack()
|| !tileTopItem.getBaseItem().allowStack())
: this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) {
: this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) {
return FurnitureMovementError.CANT_STACK;
}
if (!Emulator.getConfig().getBoolean("wired.place.under", false) || (
Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable()
&& !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) {
Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable()
&& !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) {
if (checkForUnits) {
if (!magicTile && this.room.hasHabbosAt(t.x, t.y)) {
return FurnitureMovementError.TILE_HAS_HABBOS;
@@ -2048,8 +2044,8 @@ public class RoomItemManager {
}
THashSet<RoomTile> oldOccupiedTiles = layout.getTilesAt(
layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), item.getRotation());
layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), item.getRotation());
int oldRotation = item.getRotation();
@@ -2066,9 +2062,9 @@ public class RoomItemManager {
}
if ((stackHelper == null && topItem != null && topItem != item && !topItem.getBaseItem()
.allowStack()) || (topItem != null && topItem != item
&& topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item)
> Room.MAXIMUM_FURNI_HEIGHT)) {
.allowStack()) || (topItem != null && topItem != item
&& topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item)
> Room.MAXIMUM_FURNI_HEIGHT)) {
item.setRotation(oldRotation);
return FurnitureMovementError.CANT_STACK;
}
@@ -2117,7 +2113,7 @@ public class RoomItemManager {
if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) {
FurnitureBuildheightEvent event = Emulator.getPluginManager()
.fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height));
.fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height));
if (event.hasChangedHeight()) {
height = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight();
pluginHeight = true;
@@ -2138,7 +2134,7 @@ public class RoomItemManager {
if (item.getZ() > Room.MAXIMUM_FURNI_HEIGHT) {
item.setZ(Room.MAXIMUM_FURNI_HEIGHT);
}
// Update wired spatial index and invalidate cache when wired items are moved
if (item instanceof InteractionWiredTrigger) {
this.room.getRoomSpecialTypes().updateTriggerLocation((InteractionWiredTrigger) item, oldLocation.x, oldLocation.y);
@@ -2198,7 +2194,7 @@ public class RoomItemManager {
boolean pluginHelper = false;
if (Emulator.getPluginManager().isRegistered(FurnitureMovedEvent.class, true)) {
FurnitureMovedEvent event = Emulator.getPluginManager()
.fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile));
.fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile));
if (event.isCancelled()) {
return FurnitureMovementError.CANCEL_PLUGIN_MOVE;
}
@@ -2210,9 +2206,9 @@ public class RoomItemManager {
HabboItem stackHelper = this.findStackHeightHelperAt(tile, item);
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation);
item.getBaseItem().getLength(), rotation);
THashSet<RoomTile> newOccupiedTiles = layout.getTilesAt(tile,
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation);
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation);
HabboItem topItem = this.getTopPhysicsItemAt(occupiedTiles, null, physics);
@@ -2221,9 +2217,9 @@ public class RoomItemManager {
for (RoomTile t : occupiedTiles) {
HabboItem tileTopItem = this.getTopPhysicsItemAt(t.x, t.y, item, physics);
if (!magicTile && ((tileTopItem != null && tileTopItem != item ? (
t.state.equals(RoomTileState.INVALID) || !t.getAllowStack()
t.state.equals(RoomTileState.INVALID) || !t.getAllowStack()
|| !tileTopItem.getBaseItem().allowStack())
: this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) {
: this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) {
return FurnitureMovementError.CANT_STACK;
}
@@ -2251,8 +2247,8 @@ public class RoomItemManager {
}
THashSet<RoomTile> oldOccupiedTiles = layout.getTilesAt(
layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), item.getRotation());
layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), item.getRotation());
int oldRotation = item.getRotation();
@@ -2269,9 +2265,9 @@ public class RoomItemManager {
}
if ((stackHelper == null && topItem != null && topItem != item && !topItem.getBaseItem()
.allowStack()) || (topItem != null && topItem != item
&& topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item)
> Room.MAXIMUM_FURNI_HEIGHT)) {
.allowStack()) || (topItem != null && topItem != item
&& topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item)
> Room.MAXIMUM_FURNI_HEIGHT)) {
item.setRotation(oldRotation);
return FurnitureMovementError.CANT_STACK;
}
@@ -2319,7 +2315,7 @@ public class RoomItemManager {
if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) {
FurnitureBuildheightEvent event = Emulator.getPluginManager()
.fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height));
.fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height));
if (event.hasChangedHeight()) {
height = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight();
pluginHeight = true;
@@ -2391,10 +2387,10 @@ public class RoomItemManager {
boolean magicTile = this.isStackPlacementBypassItem(item);
RoomLayout layout = this.room.getLayout();
// Check if can be placed at new position
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), rotation);
item.getBaseItem().getLength(), rotation);
java.util.List<Pair<RoomTile, THashSet<HabboItem>>> tileFurniList = new java.util.ArrayList<>();
for (RoomTile t : occupiedTiles) {
@@ -2438,8 +2434,8 @@ public class RoomItemManager {
}
return !item.isWalkable()
&& !item.getBaseItem().allowSit()
&& !item.getBaseItem().allowLay();
&& !item.getBaseItem().allowSit()
&& !item.getBaseItem().allowLay();
}
private FurnitureMovementError getPhysicsUnitCollision(RoomTile tile, WiredMovementPhysics physics) {
@@ -2515,7 +2511,7 @@ public class RoomItemManager {
for (HabboItem item : this.getPhysicsItemsAt(tile, exclude, physics)) {
if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem)
> item.getZ() + Item.getCurrentHeight(item)) {
> item.getZ() + Item.getCurrentHeight(item)) {
continue;
}
@@ -2539,7 +2535,7 @@ public class RoomItemManager {
}
if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem)
> topItem.getZ() + Item.getCurrentHeight(topItem)) {
> topItem.getZ() + Item.getCurrentHeight(topItem)) {
continue;
}
@@ -45,6 +45,7 @@ public class HabboInfo implements Runnable {
private int InfostandBg;
private int InfostandStand;
private int InfostandOverlay;
private int InfostandCardBg;
private int loadingRoom;
private Room currentRoom;
private String roomEntryMethod = "door";
@@ -91,6 +92,7 @@ public class HabboInfo implements Runnable {
this.InfostandBg = set.getInt("background_id");
this.InfostandStand = set.getInt("background_stand_id");
this.InfostandOverlay = set.getInt("background_overlay_id");
this.InfostandCardBg = set.getInt("background_card_id");
this.currentRoom = null;
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
@@ -290,6 +292,14 @@ public class HabboInfo implements Runnable {
public void setInfostandOverlay(int infostandOverlay) {
InfostandOverlay = infostandOverlay;
}
public int getInfostandCardBg() {
return InfostandCardBg;
}
public void setInfostandCardBg(int infostandCardBg) {
InfostandCardBg = infostandCardBg;
}
public Rank getRank() {
return this.rank;
}
@@ -577,7 +587,7 @@ public class HabboInfo implements Runnable {
try {
SqlQueries.update(
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ? WHERE id = ?",
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ?, background_card_id = ? WHERE id = ?",
this.motto,
this.online ? "1" : "0",
this.look,
@@ -593,6 +603,7 @@ public class HabboInfo implements Runnable {
this.InfostandBg,
this.InfostandStand,
this.InfostandOverlay,
this.InfostandCardBg,
this.id);
} catch (SqlQueries.DataAccessException e) {
LOGGER.error("Caught SQL exception", e);
@@ -0,0 +1,75 @@
package com.eu.habbo.habbohotel.users.custombadge;
import java.sql.ResultSet;
import java.sql.SQLException;
public class CustomBadge {
private final int id;
private final int userId;
private final String badgeId;
private String badgeName;
private String badgeDescription;
private final int dateCreated;
private int dateEdit;
public CustomBadge(ResultSet set) throws SQLException {
this.id = set.getInt("id");
this.userId = set.getInt("user_id");
this.badgeId = set.getString("badge_id");
this.badgeName = set.getString("badge_name");
this.badgeDescription = set.getString("badge_description");
this.dateCreated = set.getInt("date_created");
this.dateEdit = set.getInt("date_edit");
}
public CustomBadge(int id, int userId, String badgeId, String badgeName, String badgeDescription, int dateCreated, int dateEdit) {
this.id = id;
this.userId = userId;
this.badgeId = badgeId;
this.badgeName = badgeName;
this.badgeDescription = badgeDescription;
this.dateCreated = dateCreated;
this.dateEdit = dateEdit;
}
public int getId() {
return this.id;
}
public int getUserId() {
return this.userId;
}
public String getBadgeId() {
return this.badgeId;
}
public String getBadgeName() {
return this.badgeName;
}
public String getBadgeDescription() {
return this.badgeDescription;
}
public int getDateCreated() {
return this.dateCreated;
}
public int getDateEdit() {
return this.dateEdit;
}
public void setBadgeName(String badgeName) {
this.badgeName = badgeName;
}
public void setBadgeDescription(String badgeDescription) {
this.badgeDescription = badgeDescription;
}
public void setDateEdit(int dateEdit) {
this.dateEdit = dateEdit;
}
}
@@ -0,0 +1,15 @@
package com.eu.habbo.habbohotel.users.custombadge;
public class CustomBadgeException extends Exception {
private final String code;
public CustomBadgeException(String code, String message) {
super(message);
this.code = code;
}
public String getCode() {
return this.code;
}
}
@@ -0,0 +1,588 @@
package com.eu.habbo.habbohotel.users.custombadge;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboBadge;
import com.eu.habbo.habbohotel.users.inventory.BadgesComponent;
import com.eu.habbo.messages.outgoing.inventory.InventoryBadgesComposer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
public class CustomBadgeManager {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomBadgeManager.class);
public static final int MAX_PER_USER = 5;
public static final int BADGE_WIDTH = 40;
public static final int BADGE_HEIGHT = 40;
public static final int MAX_BADGE_SIZE_BYTES = 40960;
private static final int RANDOM_SUFFIX_LENGTH = 5;
private static final char[] RANDOM_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
private static final Pattern BADGE_ID_PATTERN = Pattern.compile("^CUST[A-Z0-9]{" + RANDOM_SUFFIX_LENGTH + "}-\\d+$");
private static final byte[] PNG_MAGIC = { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
private static final int RATE_LIMIT_OPS = 5;
private static final long RATE_LIMIT_WINDOW_MS = 60_000L;
private final SecureRandom random = new SecureRandom();
private final Map<Integer, long[]> rateBuckets = new ConcurrentHashMap<>();
private final Map<String, BadgeText> textCache = new ConcurrentHashMap<>();
private final java.util.concurrent.atomic.AtomicLong textCacheVersion = new java.util.concurrent.atomic.AtomicLong();
private volatile CustomBadgeSettings settings;
public CustomBadgeManager() {
this.reload();
}
public static final class BadgeText {
public final String name;
public final String description;
public BadgeText(String name, String description) {
this.name = name == null ? "" : name;
this.description = description == null ? "" : description;
}
}
public Map<String, BadgeText> getTextCache() {
return java.util.Collections.unmodifiableMap(this.textCache);
}
public long getTextCacheVersion() {
return this.textCacheVersion.get();
}
private void loadTextCache() {
Map<String, BadgeText> next = new java.util.HashMap<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT `badge_id`, `badge_name`, `badge_description` FROM `user_custom_badge`")) {
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
next.put(resultSet.getString("badge_id"),
new BadgeText(
resultSet.getString("badge_name"),
resultSet.getString("badge_description")));
}
}
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to load badge text cache.", e);
return;
}
this.textCache.clear();
this.textCache.putAll(next);
this.textCacheVersion.incrementAndGet();
LOGGER.info("CustomBadgeManager -> loaded {} custom badge texts into memory.", next.size());
}
public void reload() {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT `badge_path`, `badge_url`, `price_badge`, `currency_type` FROM `users_custom_badge_settings` ORDER BY `id` ASC LIMIT 1")) {
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
this.settings = new CustomBadgeSettings(
resultSet.getString("badge_path"),
resultSet.getString("badge_url"),
resultSet.getInt("price_badge"),
resultSet.getInt("currency_type"));
} else {
this.settings = new CustomBadgeSettings(
"/var/www/gamedata/c_images/album1584",
"/gamedata/c_images/album1584",
0, -1);
LOGGER.warn("CustomBadgeManager -> No row found in users_custom_badge_settings, falling back to defaults.");
}
}
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to load settings.", e);
}
loadTextCache();
}
public CustomBadgeSettings getSettings() {
return this.settings;
}
public List<CustomBadge> listForUser(int userId) {
List<CustomBadge> result = new ArrayList<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM `user_custom_badge` WHERE `user_id` = ? ORDER BY `date_created` ASC")) {
statement.setInt(1, userId);
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
result.add(new CustomBadge(resultSet));
}
}
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to list badges for user " + userId, e);
}
return result;
}
public CustomBadge getByBadgeId(String badgeId) {
if (badgeId == null || badgeId.isEmpty()) return null;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT * FROM `user_custom_badge` WHERE `badge_id` = ? LIMIT 1")) {
statement.setString(1, badgeId);
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
return new CustomBadge(resultSet);
}
}
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to load badge " + badgeId, e);
}
return null;
}
public int countForUser(int userId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT COUNT(*) FROM `user_custom_badge` WHERE `user_id` = ?")) {
statement.setInt(1, userId);
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
return resultSet.getInt(1);
}
}
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to count badges for user " + userId, e);
}
return 0;
}
public CustomBadge create(int userId, String name, String description, byte[] pngBytes) throws CustomBadgeException {
enforceRateLimit(userId);
if (this.countForUser(userId) >= MAX_PER_USER) {
throw new CustomBadgeException("limit_reached", "Maximum of " + MAX_PER_USER + " custom badges reached.");
}
BufferedImage image = validatePng(pngBytes);
chargeForCreate(userId);
String badgeId = generateBadgeId();
int now = (int) (System.currentTimeMillis() / 1000L);
try {
writeBadgeFile(badgeId, image);
} catch (CustomBadgeException e) {
refundForCreate(userId);
throw e;
}
String safeName = sanitize(name, 64);
String safeDesc = sanitize(description, 255);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO `user_custom_badge` (`user_id`, `badge_id`, `badge_name`, `badge_description`, `date_created`, `date_edit`) VALUES (?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, userId);
statement.setString(2, badgeId);
statement.setString(3, safeName);
statement.setString(4, safeDesc);
statement.setInt(5, now);
statement.setInt(6, now);
statement.executeUpdate();
int generatedId = 0;
try (ResultSet keys = statement.getGeneratedKeys()) {
if (keys.next()) generatedId = keys.getInt(1);
}
this.textCache.put(badgeId, new BadgeText(safeName, safeDesc));
this.textCacheVersion.incrementAndGet();
issueBadgeToInventory(userId, badgeId);
return new CustomBadge(generatedId, userId, badgeId, safeName, safeDesc, now, now);
} catch (SQLException e) {
deleteBadgeFileQuietly(badgeId);
refundForCreate(userId);
LOGGER.error("CustomBadgeManager -> Failed to insert badge for user " + userId, e);
throw new CustomBadgeException("db_error", "Could not save the badge.");
}
}
public CustomBadge update(int userId, String oldBadgeId, String name, String description, byte[] pngBytes) throws CustomBadgeException {
enforceRateLimit(userId);
CustomBadge existing = getByBadgeId(oldBadgeId);
if (existing == null || existing.getUserId() != userId) {
throw new CustomBadgeException("not_found", "Badge not found.");
}
BufferedImage image = validatePng(pngBytes);
String newBadgeId = generateBadgeId();
int now = (int) (System.currentTimeMillis() / 1000L);
writeBadgeFile(newBadgeId, image);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE `user_custom_badge` SET `badge_id` = ?, `badge_name` = ?, `badge_description` = ?, `date_edit` = ? WHERE `id` = ?")) {
statement.setString(1, newBadgeId);
statement.setString(2, sanitize(name, 64));
statement.setString(3, sanitize(description, 255));
statement.setInt(4, now);
statement.setInt(5, existing.getId());
statement.executeUpdate();
} catch (SQLException e) {
deleteBadgeFileQuietly(newBadgeId);
LOGGER.error("CustomBadgeManager -> Failed to update badge " + oldBadgeId, e);
throw new CustomBadgeException("db_error", "Could not update the badge.");
}
String safeName = sanitize(name, 64);
String safeDesc = sanitize(description, 255);
this.textCache.remove(oldBadgeId);
this.textCache.put(newBadgeId, new BadgeText(safeName, safeDesc));
this.textCacheVersion.incrementAndGet();
renameBadgeInInventory(userId, oldBadgeId, newBadgeId);
deleteBadgeFileQuietly(oldBadgeId);
return new CustomBadge(existing.getId(), userId, newBadgeId, safeName, safeDesc, existing.getDateCreated(), now);
}
public void delete(int userId, String badgeId) throws CustomBadgeException {
enforceRateLimit(userId);
CustomBadge existing = getByBadgeId(badgeId);
if (existing == null || existing.getUserId() != userId) {
throw new CustomBadgeException("not_found", "Badge not found.");
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"DELETE FROM `user_custom_badge` WHERE `id` = ?")) {
statement.setInt(1, existing.getId());
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to delete badge " + badgeId, e);
throw new CustomBadgeException("db_error", "Could not delete the badge.");
}
this.textCache.remove(badgeId);
this.textCacheVersion.incrementAndGet();
revokeBadgeFromInventory(userId, badgeId);
deleteBadgeFileQuietly(badgeId);
}
public boolean isCustomBadgeId(String badgeId) {
return badgeId != null && BADGE_ID_PATTERN.matcher(badgeId).matches();
}
public String generateBadgeId() {
long timestamp = System.currentTimeMillis() / 1000L;
for (int attempt = 0; attempt < 8; attempt++) {
StringBuilder suffix = new StringBuilder(RANDOM_SUFFIX_LENGTH);
for (int i = 0; i < RANDOM_SUFFIX_LENGTH; i++) {
suffix.append(RANDOM_ALPHABET[this.random.nextInt(RANDOM_ALPHABET.length)]);
}
String candidate = "CUST" + suffix + "-" + timestamp;
if (getByBadgeId(candidate) == null) return candidate;
timestamp++;
}
throw new IllegalStateException("Could not allocate a unique custom badge id after 8 attempts.");
}
public String publicUrlFor(String badgeId) {
CustomBadgeSettings current = this.settings;
if (current == null) return "";
String base = current.getBadgeUrl();
if (base == null || base.isEmpty()) return "";
if (base.endsWith("/")) return base + badgeId + ".gif";
return base + "/" + badgeId + ".gif";
}
private void chargeForCreate(int userId) throws CustomBadgeException {
CustomBadgeSettings current = this.settings;
if (current == null) return;
int price = current.getPriceBadge();
if (price <= 0) return;
Habbo habbo = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (habbo == null) {
throw new CustomBadgeException("must_be_online",
"You must be online in the hotel to create a paid badge.");
}
int currencyType = current.getCurrencyType();
if (currencyType == -1) {
if (habbo.getHabboInfo().getCredits() < price) {
throw new CustomBadgeException("insufficient_funds",
"You don't have enough credits (need " + price + ").");
}
habbo.giveCredits(-price);
} else {
if (habbo.getHabboInfo().getCurrencyAmount(currencyType) < price) {
throw new CustomBadgeException("insufficient_funds",
"You don't have enough of that currency (need " + price + ").");
}
habbo.givePoints(currencyType, -price);
}
}
private void issueBadgeToInventory(int userId, String badgeId) {
Habbo online = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (online != null) {
BadgesComponent.createBadge(badgeId, online);
if (online.getClient() != null) {
online.getClient().sendResponse(new InventoryBadgesComposer(online));
}
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO `users_badges` (`user_id`, `slot_id`, `badge_code`) VALUES (?, 0, ?)")) {
statement.setInt(1, userId);
statement.setString(2, badgeId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to issue offline badge " + badgeId + " to user " + userId, e);
}
}
private void renameBadgeInInventory(int userId, String oldBadgeId, String newBadgeId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE `users_badges` SET `badge_code` = ? WHERE `user_id` = ? AND `badge_code` = ?")) {
statement.setString(1, newBadgeId);
statement.setInt(2, userId);
statement.setString(3, oldBadgeId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("CustomBadgeManager -> Failed to rename badge in users_badges " + oldBadgeId + " -> " + newBadgeId, e);
}
Habbo online = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (online == null) return;
HabboBadge existing = online.getInventory().getBadgesComponent().getBadge(oldBadgeId);
if (existing != null) existing.setCode(newBadgeId);
if (online.getClient() != null) {
online.getClient().sendResponse(new InventoryBadgesComposer(online));
}
}
private void revokeBadgeFromInventory(int userId, String badgeId) {
BadgesComponent.deleteBadge(userId, badgeId);
Habbo online = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (online == null) return;
online.getInventory().getBadgesComponent().removeBadge(badgeId);
if (online.getClient() != null) {
online.getClient().sendResponse(new InventoryBadgesComposer(online));
}
}
private BufferedImage validatePng(byte[] data) throws CustomBadgeException {
if (data == null || data.length == 0) {
throw new CustomBadgeException("empty", "Badge image is empty.");
}
if (data.length > MAX_BADGE_SIZE_BYTES) {
throw new CustomBadgeException("too_large", "Badge image exceeds " + MAX_BADGE_SIZE_BYTES + " bytes.");
}
if (data.length < PNG_MAGIC.length) {
throw new CustomBadgeException("invalid_image", "Badge image must be a PNG.");
}
for (int i = 0; i < PNG_MAGIC.length; i++) {
if (data[i] != PNG_MAGIC[i]) {
throw new CustomBadgeException("invalid_image", "Badge image must be a PNG.");
}
}
try (ImageInputStream peek = ImageIO.createImageInputStream(new ByteArrayInputStream(data))) {
if (peek == null) throw new IOException("no input stream");
Iterator<ImageReader> readers = ImageIO.getImageReaders(peek);
if (!readers.hasNext()) {
throw new CustomBadgeException("invalid_image", "Badge image format not recognised.");
}
ImageReader reader = readers.next();
try {
reader.setInput(peek, true, true);
int w = reader.getWidth(0);
int h = reader.getHeight(0);
if (w != BADGE_WIDTH || h != BADGE_HEIGHT) {
throw new CustomBadgeException("wrong_dimensions",
"Badge image must be " + BADGE_WIDTH + "x" + BADGE_HEIGHT + " pixels.");
}
} finally {
reader.dispose();
}
} catch (IOException e) {
throw new CustomBadgeException("invalid_image", "Badge image header could not be read.");
}
BufferedImage image;
try {
image = ImageIO.read(new ByteArrayInputStream(data));
} catch (IOException e) {
throw new CustomBadgeException("invalid_image", "Badge image could not be decoded.");
}
if (image == null
|| image.getWidth() != BADGE_WIDTH
|| image.getHeight() != BADGE_HEIGHT) {
throw new CustomBadgeException("invalid_image", "Badge image could not be decoded.");
}
return image;
}
private void enforceRateLimit(int userId) throws CustomBadgeException {
long now = System.currentTimeMillis();
long[] bucket = this.rateBuckets.computeIfAbsent(userId, id -> new long[RATE_LIMIT_OPS]);
synchronized (bucket) {
long oldest = Long.MAX_VALUE;
int oldestIdx = 0;
for (int i = 0; i < bucket.length; i++) {
if (bucket[i] < oldest) { oldest = bucket[i]; oldestIdx = i; }
}
if (oldest > now - RATE_LIMIT_WINDOW_MS) {
throw new CustomBadgeException("rate_limited",
"Too many badge operations. Try again in a moment.");
}
bucket[oldestIdx] = now;
}
}
private void refundForCreate(int userId) {
CustomBadgeSettings current = this.settings;
if (current == null) return;
int price = current.getPriceBadge();
if (price <= 0) return;
Habbo habbo = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (habbo == null) {
LOGGER.warn("CustomBadgeManager -> Could not refund {} (price {}): user offline", userId, price);
return;
}
int currencyType = current.getCurrencyType();
if (currencyType == -1) habbo.giveCredits(price);
else habbo.givePoints(currencyType, price);
}
private void writeBadgeFile(String badgeId, BufferedImage source) throws CustomBadgeException {
CustomBadgeSettings current = this.settings;
if (current == null || current.getBadgePath() == null || current.getBadgePath().isEmpty()) {
throw new CustomBadgeException("not_configured", "Custom badge storage path is not configured.");
}
try {
Path dir = Paths.get(current.getBadgePath()).toAbsolutePath();
Files.createDirectories(dir);
Path target = dir.resolve(badgeId + ".gif");
BufferedImage indexed = toIndexedGifImage(source);
if (!ImageIO.write(indexed, "gif", target.toFile())) {
throw new IOException("No GIF ImageWriter available.");
}
LOGGER.info("CustomBadgeManager -> wrote badge {} ({} bytes) to {}",
badgeId, Files.size(target), target);
} catch (IOException e) {
LOGGER.error("CustomBadgeManager -> Failed to write badge " + badgeId
+ " to " + current.getBadgePath(), e);
throw new CustomBadgeException("write_failed", "Could not save the badge file.");
}
}
private static BufferedImage toIndexedGifImage(BufferedImage source) {
int w = source.getWidth();
int h = source.getHeight();
int[] pixels = source.getRGB(0, 0, w, h, null, 0, w);
Map<Integer, Integer> indexByColor = new LinkedHashMap<>();
indexByColor.put(0, 0);
for (int p : pixels) {
int alpha = (p >>> 24) & 0xff;
int key = (alpha < 128) ? 0 : (p | 0xFF000000);
if (key == 0) continue;
if (indexByColor.size() >= 256) break;
indexByColor.computeIfAbsent(key, k -> indexByColor.size());
}
int n = indexByColor.size();
byte[] r = new byte[n];
byte[] g = new byte[n];
byte[] b = new byte[n];
int i = 0;
for (Integer color : indexByColor.keySet()) {
r[i] = (byte) ((color >>> 16) & 0xff);
g[i] = (byte) ((color >>> 8) & 0xff);
b[i] = (byte) (color & 0xff);
i++;
}
IndexColorModel colorModel = new IndexColorModel(8, n, r, g, b, 0);
BufferedImage out = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int p = pixels[y * w + x];
int alpha = (p >>> 24) & 0xff;
int key = (alpha < 128) ? 0 : (p | 0xFF000000);
Integer idx = indexByColor.get(key);
out.getRaster().setSample(x, y, 0, idx == null ? 0 : idx);
}
}
return out;
}
private void deleteBadgeFileQuietly(String badgeId) {
CustomBadgeSettings current = this.settings;
if (current == null || current.getBadgePath() == null) return;
File file = new File(current.getBadgePath(), badgeId + ".gif");
if (file.exists() && !file.delete()) {
LOGGER.warn("CustomBadgeManager -> Could not delete stale badge file: {}", file.getAbsolutePath());
}
}
private static String sanitize(String value, int maxLength) {
if (value == null) return "";
StringBuilder out = new StringBuilder(Math.min(value.length(), maxLength));
for (int i = 0; i < value.length() && out.length() < maxLength; i++) {
char c = value.charAt(i);
if (c < 0x20 || c == 0x7F) continue;
out.append(c);
}
return out.toString().trim();
}
}
@@ -0,0 +1,32 @@
package com.eu.habbo.habbohotel.users.custombadge;
public class CustomBadgeSettings {
private final String badgePath;
private final String badgeUrl;
private final int priceBadge;
private final int currencyType;
public CustomBadgeSettings(String badgePath, String badgeUrl, int priceBadge, int currencyType) {
this.badgePath = badgePath;
this.badgeUrl = badgeUrl;
this.priceBadge = priceBadge;
this.currencyType = currencyType;
}
public String getBadgePath() {
return this.badgePath;
}
public String getBadgeUrl() {
return this.badgeUrl;
}
public int getPriceBadge() {
return this.priceBadge;
}
public int getCurrencyType() {
return this.currencyType;
}
}
@@ -0,0 +1,136 @@
package com.eu.habbo.habbohotel.users.infostand;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboStats;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
public class InfostandBackgroundManager {
private static final Logger LOGGER = LoggerFactory.getLogger(InfostandBackgroundManager.class);
public enum Category {
BACKGROUND("background"),
STAND("stand"),
OVERLAY("overlay"),
CARD("card");
public final String dbValue;
Category(String dbValue) {
this.dbValue = dbValue;
}
public static Category fromDbValue(String value) {
for (Category category : values()) {
if (category.dbValue.equalsIgnoreCase(value)) return category;
}
return null;
}
}
private final Map<Category, Map<Integer, Entry>> entries = new EnumMap<>(Category.class);
private boolean enforce = false;
public InfostandBackgroundManager() {
for (Category category : Category.values()) {
this.entries.put(category, Collections.emptyMap());
}
this.reload();
}
public void reload() {
Map<Category, Map<Integer, Entry>> next = new EnumMap<>(Category.class);
for (Category category : Category.values()) {
next.put(category, new HashMap<>());
}
int loaded = 0;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT id, category, min_rank, is_hc_only, is_ambassador_only FROM infostand_backgrounds");
ResultSet set = statement.executeQuery()) {
while (set.next()) {
Category category = Category.fromDbValue(set.getString("category"));
if (category == null) continue;
int id = set.getInt("id");
int minRank = set.getInt("min_rank");
boolean isHcOnly = set.getBoolean("is_hc_only");
boolean isAmbassadorOnly = set.getBoolean("is_ambassador_only");
next.get(category).put(id, new Entry(minRank, isHcOnly, isAmbassadorOnly));
loaded++;
}
} catch (SQLException e) {
this.enforce = false;
for (Category category : Category.values()) {
this.entries.put(category, Collections.emptyMap());
}
LOGGER.error("InfostandBackgroundManager -> Failed to load infostand_backgrounds, server-side validation disabled.", e);
return;
}
for (Category category : Category.values()) {
this.entries.put(category, next.get(category));
}
this.enforce = loaded > 0;
if (this.enforce) {
LOGGER.info("InfostandBackgroundManager -> Loaded {} backgrounds, {} stands, {} overlays, {} cards from infostand_backgrounds.",
this.entries.get(Category.BACKGROUND).size(),
this.entries.get(Category.STAND).size(),
this.entries.get(Category.OVERLAY).size(),
this.entries.get(Category.CARD).size());
} else {
LOGGER.info("InfostandBackgroundManager -> infostand_backgrounds is empty, server-side validation disabled (only range clamp will apply).");
}
}
public boolean canUse(Habbo habbo, Category category, int id) {
if (id == 0) return true;
if (!this.enforce) return true;
if (habbo == null) return false;
Map<Integer, Entry> categoryEntries = this.entries.get(category);
if (categoryEntries == null) return false;
Entry entry = categoryEntries.get(id);
if (entry == null) return false;
HabboInfo info = habbo.getHabboInfo();
int rankId = (info != null && info.getRank() != null) ? info.getRank().getId() : 0;
HabboStats stats = habbo.getHabboStats();
boolean hasClub = stats != null && stats.hasActiveClub();
if (entry.isHcOnly && !hasClub) return false;
if (entry.isAmbassadorOnly && !habbo.hasPermission(Permission.ACC_AMBASSADOR)) return false;
if (rankId < entry.minRank) return false;
return true;
}
public static final class Entry {
public final int minRank;
public final boolean isHcOnly;
public final boolean isAmbassadorOnly;
public Entry(int minRank, boolean isHcOnly, boolean isAmbassadorOnly) {
this.minRank = minRank;
this.isHcOnly = isHcOnly;
this.isAmbassadorOnly = isAmbassadorOnly;
}
}
}
@@ -1,14 +1,30 @@
package com.eu.habbo.messages.incoming.users;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
public class ActivateEffectEvent extends MessageHandler {
@Override
public void handle() throws Exception {
int effectId = this.packet.readInt();
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
if (this.client.getHabbo().getInventory().getEffectsComponent().ownsEffect(effectId)) {
this.client.getHabbo().getInventory().getEffectsComponent().activateEffect(effectId);
if (habbo.getInventory().getEffectsComponent().ownsEffect(effectId)) {
habbo.getInventory().getEffectsComponent().activateEffect(effectId);
return;
}
int rankId = habbo.getHabboInfo().getRank().getId();
if (Emulator.getGameEnvironment().getPermissionsManager().isEffectBlocked(effectId, rankId)) {
return;
}
Room room = habbo.getHabboInfo().getCurrentRoom();
if (room == null || habbo.getHabboInfo().getRiding() != null) return;
room.giveEffect(habbo, effectId, -1);
}
}
@@ -1,24 +1,77 @@
package com.eu.habbo.messages.incoming.users;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboStats;
import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager;
import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager.Category;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer;
public class ChangeInfostandBgEvent extends MessageHandler {
private static final String COOLDOWN_KEY = "infostand_bg_cooldown";
private static final long COOLDOWN_MS = 500L;
private static final int MIN_ID = 0;
private static final int MAX_ID = 9999;
@Override
public void handle() throws Exception {
int backgroundImage = this.packet.readInt();
int backgroundStand = this.packet.readInt();
int backgroundOverlay = this.packet.readInt();
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
this.client.getHabbo().getHabboInfo().setInfostandBg(backgroundImage);
this.client.getHabbo().getHabboInfo().setInfostandStand(backgroundStand);
this.client.getHabbo().getHabboInfo().setInfostandOverlay(backgroundOverlay);
this.client.getHabbo().getHabboInfo().run();
HabboInfo info = habbo.getHabboInfo();
if (info == null) return;
if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) {
this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose());
HabboStats stats = habbo.getHabboStats();
if (stats != null) {
long now = System.currentTimeMillis();
Object last = stats.cache.get(COOLDOWN_KEY);
if (last instanceof Long && (now - (Long) last) < COOLDOWN_MS) {
return;
}
stats.cache.put(COOLDOWN_KEY, now);
}
int requestedBg = sanitize(this.packet.readInt());
int requestedStand = sanitize(this.packet.readInt());
int requestedOverlay = sanitize(this.packet.readInt());
int requestedCard = this.packet.bytesAvailable() >= 4 ? sanitize(this.packet.readInt()) : 0;
InfostandBackgroundManager manager = Emulator.getGameEnvironment() != null ? Emulator.getGameEnvironment().getInfostandBackgroundManager() : null;
int backgroundImage = resolve(manager, habbo, Category.BACKGROUND, requestedBg, info.getInfostandBg());
int backgroundStand = resolve(manager, habbo, Category.STAND, requestedStand, info.getInfostandStand());
int backgroundOverlay = resolve(manager, habbo, Category.OVERLAY, requestedOverlay, info.getInfostandOverlay());
int backgroundCard = resolve(manager, habbo, Category.CARD, requestedCard, info.getInfostandCardBg());
if (info.getInfostandBg() == backgroundImage
&& info.getInfostandStand() == backgroundStand
&& info.getInfostandOverlay() == backgroundOverlay
&& info.getInfostandCardBg() == backgroundCard) {
return;
}
info.setInfostandBg(backgroundImage);
info.setInfostandStand(backgroundStand);
info.setInfostandOverlay(backgroundOverlay);
info.setInfostandCardBg(backgroundCard);
info.run();
if (info.getCurrentRoom() != null) {
info.getCurrentRoom().sendComposer(new RoomUserDataComposer(habbo).compose());
} else {
this.client.sendResponse(new RoomUserDataComposer(this.client.getHabbo()));
this.client.sendResponse(new RoomUserDataComposer(habbo));
}
}
}
private static int sanitize(int value) {
if (value < MIN_ID || value > MAX_ID) return 0;
return value;
}
private static int resolve(InfostandBackgroundManager manager, Habbo habbo, Category category, int requested, int current) {
if (manager == null) return requested;
return manager.canUse(habbo, category, requested) ? requested : current;
}
}
@@ -36,6 +36,7 @@ public class RoomPetComposer extends MessageComposer implements TIntObjectProced
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
if (pet instanceof IPetLook) {
this.response.appendString(((IPetLook) pet).getLook());
} else {
@@ -23,6 +23,7 @@ public class RoomUserDataComposer extends MessageComposer {
this.response.appendInt(this.habbo.getHabboInfo().getInfostandBg());
this.response.appendInt(this.habbo.getHabboInfo().getInfostandStand());
this.response.appendInt(this.habbo.getHabboInfo().getInfostandOverlay());
this.response.appendInt(this.habbo.getHabboInfo().getInfostandCardBg());
return this.response;
}
@@ -43,6 +43,7 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendInt(this.habbo.getHabboInfo().getInfostandBg());
this.response.appendInt(this.habbo.getHabboInfo().getInfostandStand());
this.response.appendInt(this.habbo.getHabboInfo().getInfostandOverlay());
this.response.appendInt(this.habbo.getHabboInfo().getInfostandCardBg());
this.response.appendString(this.habbo.getHabboInfo().getLook());
this.response.appendInt(this.habbo.getRoomUnit().getId()); //Room Unit ID
this.response.appendInt(this.habbo.getRoomUnit().getX());
@@ -78,6 +79,7 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendInt(habbo.getHabboInfo().getInfostandBg());
this.response.appendInt(habbo.getHabboInfo().getInfostandStand());
this.response.appendInt(habbo.getHabboInfo().getInfostandOverlay());
this.response.appendInt(habbo.getHabboInfo().getInfostandCardBg());
this.response.appendString(habbo.getHabboInfo().getLook());
this.response.appendInt(habbo.getRoomUnit().getId()); //Room Unit ID
this.response.appendInt(habbo.getRoomUnit().getX());
@@ -111,6 +113,7 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendString(this.bot.getFigure());
this.response.appendInt(this.bot.getRoomUnit().getId());
this.response.appendInt(this.bot.getRoomUnit().getX());
@@ -143,6 +146,7 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendString(bot.getFigure());
this.response.appendInt(bot.getRoomUnit().getId());
this.response.appendInt(bot.getRoomUnit().getX());
@@ -115,6 +115,7 @@ public class UserProfileComposer extends MessageComposer {
this.response.appendInt(this.habboInfo.getInfostandBg());
this.response.appendInt(this.habboInfo.getInfostandStand());
this.response.appendInt(this.habboInfo.getInfostandOverlay());
this.response.appendInt(this.habboInfo.getInfostandCardBg());
return this.response;
}
@@ -96,6 +96,16 @@ public class GameServer extends Server {
LOGGER.error("Failed to start WebSocket server on {}:{}", wsHost, wsPort);
} else {
LOGGER.info("WebSocket server started on {}:{} (SSL: {})", wsHost, wsPort, wsInitializer.isSslEnabled());
if (com.eu.habbo.Emulator.getConfig().getBoolean("crypto.ws.signing.enabled", false)) {
try {
com.eu.habbo.networking.gameserver.crypto.CryptoSigningKeyManager.get();
LOGGER.info("[ws-crypto] signing public key ready: {}",
com.eu.habbo.networking.gameserver.crypto.CryptoSigningKeyManager.publicKeyBase64());
} catch (Exception e) {
LOGGER.error("[ws-crypto] failed to warm signing keypair", e);
}
}
}
}
@@ -10,4 +10,6 @@ public class GameServerAttributes {
public static final AttributeKey<HabboRC4> CRYPTO_CLIENT = AttributeKey.valueOf("CryptoClient");
public static final AttributeKey<HabboRC4> CRYPTO_SERVER = AttributeKey.valueOf("CryptoServer");
public static final AttributeKey<String> WS_IP = AttributeKey.valueOf("WebSocketIP");
public static final AttributeKey<byte[]> WS_AES_KEY = AttributeKey.valueOf("WsAesKey");
}
@@ -1,8 +1,11 @@
package com.eu.habbo.networking.gameserver;
import com.eu.habbo.Emulator;
import com.eu.habbo.messages.PacketManager;
import com.eu.habbo.networking.gameserver.auth.AuthHttpHandler;
import com.eu.habbo.networking.gameserver.badges.BadgeHttpHandler;
import com.eu.habbo.networking.gameserver.codec.WebSocketCodec;
import com.eu.habbo.networking.gameserver.crypto.WsHandshakeHandler;
import com.eu.habbo.networking.gameserver.decoders.*;
import com.eu.habbo.networking.gameserver.encoders.GameServerMessageEncoder;
import com.eu.habbo.networking.gameserver.encoders.GameServerMessageLogger;
@@ -51,8 +54,14 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
ch.pipeline().addLast("httpAggregator", new HttpObjectAggregator(MAX_FRAME_SIZE));
ch.pipeline().addLast("wsHttpHandler", new WebSocketHttpHandler());
ch.pipeline().addLast("authHttpHandler", new AuthHttpHandler());
ch.pipeline().addLast("badgeHttpHandler", new BadgeHttpHandler());
ch.pipeline().addLast("wsProtocolHandler", new WebSocketServerProtocolHandler(this.wsConfig));
ch.pipeline().addLast("wsCodec", new WebSocketCodec());
if (Emulator.getConfig().getBoolean("crypto.ws.enabled", false)) {
ch.pipeline().addLast(WsHandshakeHandler.HANDLER_NAME, new WsHandshakeHandler());
}
ch.pipeline().addLast(new GamePolicyDecoder());
ch.pipeline().addLast(new GameByteFrameDecoder());
ch.pipeline().addLast(new GameByteDecoder());
@@ -0,0 +1,140 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Base64;
public final class AccessTokenService {
private static final Logger LOGGER = LoggerFactory.getLogger(AccessTokenService.class);
private static final SecureRandom RNG = new SecureRandom();
private static final Base64.Encoder URL_ENC = Base64.getUrlEncoder().withoutPadding();
private static final Base64.Decoder URL_DEC = Base64.getUrlDecoder();
private static volatile String cachedSecret = null;
private AccessTokenService() {}
public static final class Issued {
public final String token;
public final long expiresAt;
Issued(String token, long expiresAt) {
this.token = token;
this.expiresAt = expiresAt;
}
}
public static long ttlSeconds() {
return Math.max(60L, Emulator.getConfig().getInt("login.access.jwt.ttl.seconds", 86400));
}
public static Issued issue(int userId) {
long now = Emulator.getIntUnixTimestamp();
long exp = now + ttlSeconds();
JsonObject header = new JsonObject();
header.addProperty("alg", "HS256");
header.addProperty("typ", "JWT");
JsonObject payload = new JsonObject();
payload.addProperty("sub", userId);
payload.addProperty("iat", now);
payload.addProperty("exp", exp);
payload.addProperty("typ", "access");
String h = URL_ENC.encodeToString(header.toString().getBytes(StandardCharsets.UTF_8));
String p = URL_ENC.encodeToString(payload.toString().getBytes(StandardCharsets.UTF_8));
String signingInput = h + "." + p;
String sig = URL_ENC.encodeToString(hmacSha256(secret().getBytes(StandardCharsets.UTF_8),
signingInput.getBytes(StandardCharsets.UTF_8)));
return new Issued(signingInput + "." + sig, exp);
}
public static int verify(String token) {
if (token == null || token.isEmpty()) return 0;
String[] parts = token.split("\\.");
if (parts.length != 3) return 0;
try {
String signingInput = parts[0] + "." + parts[1];
byte[] expected = hmacSha256(secret().getBytes(StandardCharsets.UTF_8),
signingInput.getBytes(StandardCharsets.UTF_8));
byte[] provided = URL_DEC.decode(parts[2]);
if (!constantTimeEquals(expected, provided)) return 0;
byte[] payloadBytes = URL_DEC.decode(parts[1]);
JsonObject payload = JsonParser.parseString(new String(payloadBytes, StandardCharsets.UTF_8)).getAsJsonObject();
if (!payload.has("typ") || !"access".equals(payload.get("typ").getAsString())) return 0;
long exp = payload.get("exp").getAsLong();
if (exp <= Emulator.getIntUnixTimestamp()) return 0;
return payload.get("sub").getAsInt();
} catch (Exception e) {
return 0;
}
}
private static String secret() {
String s = cachedSecret;
if (s != null && !s.isEmpty()) return s;
synchronized (AccessTokenService.class) {
if (cachedSecret != null && !cachedSecret.isEmpty()) return cachedSecret;
String configured = Emulator.getConfig().getValue("login.access.jwt.secret", "");
if (configured != null && !configured.isEmpty()) {
cachedSecret = configured;
return configured;
}
byte[] buf = new byte[48];
RNG.nextBytes(buf);
String generated = Base64.getEncoder().withoutPadding().encodeToString(buf);
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO emulator_settings (`key`, `value`) VALUES ('login.access.jwt.secret', ?) "
+ "ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)")) {
stmt.setString(1, generated);
stmt.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Could not persist generated login.access.jwt.secret; using in-memory only", e);
}
Emulator.getConfig().update("login.access.jwt.secret", generated);
cachedSecret = generated;
LOGGER.info("[auth/access] generated new access token signing secret (persisted to emulator_settings)");
return generated;
}
}
private static byte[] hmacSha256(byte[] key, byte[] data) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(data);
} catch (Exception e) {
throw new IllegalStateException("HmacSHA256 unavailable", e);
}
}
private static boolean constantTimeEquals(byte[] a, byte[] b) {
if (a == null || b == null || a.length != b.length) return false;
int r = 0;
for (int i = 0; i < a.length; i++) r |= a[i] ^ b[i];
return r == 0;
}
}
@@ -2,6 +2,7 @@ package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import com.eu.habbo.networking.gameserver.GameServerAttributes;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.netty.buffer.Unpooled;
@@ -25,14 +26,25 @@ import java.util.regex.Pattern;
public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthHttpHandler.class);
private static final String LOGIN_PATH = "/api/auth/login";
private static final String REGISTER_PATH = "/api/auth/register";
private static final String FORGOT_PATH = "/api/auth/forgot-password";
private static final String LOGOUT_PATH = "/api/auth/logout";
private static final String LOGIN_PATH = "/api/auth/login";
private static final String REGISTER_PATH = "/api/auth/register";
private static final String FORGOT_PATH = "/api/auth/forgot-password";
private static final String LOGOUT_PATH = "/api/auth/logout";
private static final String CHECK_EMAIL_PATH = "/api/auth/check-email";
private static final String CHECK_USERNAME_PATH = "/api/auth/check-username";
private static final String ROOM_TEMPLATES_PATH = "/api/auth/room-templates";
private static final String NEWS_PATH = "/api/auth/news";
private static final String REMEMBER_PATH = "/api/auth/remember";
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 HEALTH_PATH = "/api/health";
private static final Pattern USERNAME_RE = Pattern.compile("^[A-Za-z0-9._-]{3,32}$");
private static final Pattern EMAIL_RE = Pattern.compile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$");
private static final Pattern FIGURE_RE = Pattern.compile("^[A-Za-z0-9.\\-]{1,200}$");
private static final SecureRandom RNG = new SecureRandom();
private static final int MAX_BODY_BYTES = 8 * 1024;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
@@ -44,7 +56,15 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
String path = new QueryStringDecoder(req.uri()).path();
if (!path.equals(LOGIN_PATH) && !path.equals(REGISTER_PATH)
&& !path.equals(FORGOT_PATH) && !path.equals(LOGOUT_PATH)) {
&& !path.equals(FORGOT_PATH) && !path.equals(LOGOUT_PATH)
&& !path.equals(CHECK_EMAIL_PATH) && !path.equals(CHECK_USERNAME_PATH)
&& !path.equals(ROOM_TEMPLATES_PATH)
&& !path.equals(NEWS_PATH)
&& !path.equals(REMEMBER_PATH)
&& !path.equals(REFRESH_PATH)
&& !path.equals(SERVER_KEY_PATH)
&& !path.equals(SSO_TOKEN_PATH)
&& !path.equals(HEALTH_PATH)) {
super.channelRead(ctx, msg);
return;
}
@@ -62,6 +82,51 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
return;
}
if (path.equals(HEALTH_PATH)) {
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, errorPayload("Use GET."));
return;
}
JsonObject ok = new JsonObject();
ok.addProperty("status", "ok");
sendJson(ctx, req, HttpResponseStatus.OK, ok);
return;
}
if (path.equals(ROOM_TEMPLATES_PATH)) {
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, errorPayload("Use GET."));
return;
}
handleRoomTemplates(ctx, req);
return;
}
if (path.equals(NEWS_PATH)) {
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, errorPayload("Use GET."));
return;
}
String ip = resolveClientIp(ctx, req);
if (!AuthRateLimiter.tryProbe(ip)) {
long secs = AuthRateLimiter.secondsUntilProbeReset(ip);
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
errorPayload("Too many requests. Try again in " + secs + "s."));
return;
}
handleNews(ctx, req);
return;
}
if (path.equals(SERVER_KEY_PATH)) {
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, errorPayload("Use GET."));
return;
}
handleServerKey(ctx, req);
return;
}
if (req.method() != HttpMethod.POST) {
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, errorPayload("Use POST."));
return;
@@ -76,6 +141,11 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
return;
}
if (req.content().readableBytes() > MAX_BODY_BYTES) {
sendJson(ctx, req, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, errorPayload("Payload too large."));
return;
}
JsonObject body;
try {
String text = req.content().toString(StandardCharsets.UTF_8);
@@ -90,6 +160,27 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
return;
}
if (path.equals(CHECK_EMAIL_PATH)) {
handleCheckEmail(ctx, req, body, ip);
return;
}
if (path.equals(CHECK_USERNAME_PATH)) {
handleCheckUsername(ctx, req, body, ip);
return;
}
if (path.equals(REMEMBER_PATH)) {
handleRemember(ctx, req, body, ip);
return;
}
if (path.equals(REFRESH_PATH)) {
handleRefresh(ctx, req, body, ip);
return;
}
if (path.equals(SSO_TOKEN_PATH)) {
handleSsoToken(ctx, req, body, ip);
return;
}
String turnstileToken = readString(body, "turnstileToken");
if (!TurnstileVerifier.verify(turnstileToken, ip)) {
AuthRateLimiter.recordFailure(ip);
@@ -104,105 +195,320 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
}
}
/* ─── Logout ────────────────────────────────────────────────────────── */
private void handleLogout(ChannelHandlerContext ctx, FullHttpRequest req, com.google.gson.JsonObject body) {
String ssoTicket = readString(body, "ssoTicket");
JsonObject ok = new JsonObject();
ok.addProperty("message", "Logged out.");
if (ssoTicket == null || ssoTicket.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.OK, ok);
private void handleCheckEmail(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
if (!AuthRateLimiter.tryProbe(ip)) {
long secs = AuthRateLimiter.secondsUntilProbeReset(ip);
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
errorPayload("Too many requests. Try again in " + secs + "s."));
return;
}
String email = readString(body, "email").trim();
if (email.isEmpty() || email.length() > 254 || !EMAIL_RE.matcher(email).matches()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Invalid email address."));
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement lookup = conn.prepareStatement(
"SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
lookup.setString(1, ssoTicket);
int userId = 0;
try (ResultSet rs = lookup.executeQuery()) {
if (rs.next()) userId = rs.getInt("id");
Boolean cached = AvailabilityCache.lookupEmail(email);
boolean taken;
if (cached != null) {
taken = !cached;
} else {
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT 1 FROM users WHERE mail = ? LIMIT 1")) {
stmt.setString(1, email);
try (ResultSet rs = stmt.executeQuery()) {
taken = rs.next();
}
} catch (Exception e) {
LOGGER.error("check-email failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
return;
}
AvailabilityCache.storeEmail(email, !taken);
}
if (userId > 0) {
try (PreparedStatement clear = conn.prepareStatement(
"UPDATE users SET auth_ticket = '', online = '0' WHERE id = ? LIMIT 1")) {
clear.setInt(1, userId);
clear.executeUpdate();
JsonObject res = new JsonObject();
res.addProperty("available", !taken);
if (taken) res.addProperty("error", "This email is already in use.");
sendJson(ctx, req, HttpResponseStatus.OK, res);
}
private void handleCheckUsername(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
if (!AuthRateLimiter.tryProbe(ip)) {
long secs = AuthRateLimiter.secondsUntilProbeReset(ip);
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
errorPayload("Too many requests. Try again in " + secs + "s."));
return;
}
String username = readString(body, "username").trim();
if (!USERNAME_RE.matcher(username).matches()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("Username must be 3-32 chars (letters, numbers, . _ -)."));
return;
}
Boolean cached = AvailabilityCache.lookupUsername(username);
boolean taken;
if (cached != null) {
taken = !cached;
} else {
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT 1 FROM users WHERE username = ? LIMIT 1")) {
stmt.setString(1, username);
try (ResultSet rs = stmt.executeQuery()) {
taken = rs.next();
}
} catch (Exception e) {
LOGGER.error("check-username failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
return;
}
AvailabilityCache.storeUsername(username, !taken);
}
JsonObject res = new JsonObject();
res.addProperty("available", !taken);
if (taken) res.addProperty("error", "This Habbo name is already taken.");
sendJson(ctx, req, HttpResponseStatus.OK, res);
}
private void handleLogout(ChannelHandlerContext ctx, FullHttpRequest req, com.google.gson.JsonObject body) {
String ssoTicket = readString(body, "ssoTicket");
String rememberToken = readString(body, "rememberToken").trim();
JsonObject ok = new JsonObject();
ok.addProperty("message", "Logged out.");
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
int userId = 0;
if (ssoTicket != null && !ssoTicket.isEmpty()) {
try (PreparedStatement lookup = conn.prepareStatement(
"SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
lookup.setString(1, ssoTicket);
try (ResultSet rs = lookup.executeQuery()) {
if (rs.next()) userId = rs.getInt("id");
}
}
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());
if (userId > 0) {
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());
}
}
}
}
if (!rememberToken.isEmpty()) {
RememberJwtService.revokeFromToken(conn, rememberToken);
}
} catch (Exception e) {
LOGGER.error("Logout cleanup failed for ticket", e);
LOGGER.error("Logout cleanup failed", e);
}
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
/* ─── Login ─────────────────────────────────────────────────────────── */
private void handleRemember(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String jwt = readString(body, "rememberToken").trim();
if (jwt.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing rememberToken."));
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
RememberJwtService.RotationResult rot = RememberJwtService.rotate(conn, jwt, ip);
if (rot == null) {
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Remember token invalid or expired."));
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, rot.userId);
upd.executeUpdate();
}
JsonObject ok = new JsonObject();
ok.addProperty("ssoTicket", ssoTicket);
ok.addProperty("username", rot.username);
ok.addProperty("rememberToken", rot.jwt);
ok.addProperty("expiresAt", rot.expiresAt);
AccessTokenService.Issued access = AccessTokenService.issue(rot.userId);
ok.addProperty("accessToken", access.token);
ok.addProperty("accessTokenExpiresAt", access.expiresAt);
sendJson(ctx, req, HttpResponseStatus.OK, ok);
} catch (Exception e) {
LOGGER.error("Remember login failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
private void handleSsoToken(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String ssoTicket = readString(body, "ssoTicket").trim();
if (ssoTicket.isEmpty() || ssoTicket.length() > 128) {
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing or invalid ssoTicket."));
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement lookup = conn.prepareStatement(
"SELECT id, username FROM users WHERE auth_ticket = ? LIMIT 1")) {
lookup.setString(1, ssoTicket);
try (ResultSet rs = lookup.executeQuery()) {
if (!rs.next()) {
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("SSO ticket not recognised."));
return;
}
int userId = rs.getInt("id");
String username = rs.getString("username");
AuthRateLimiter.recordSuccess(ip);
AccessTokenService.Issued access = AccessTokenService.issue(userId);
JsonObject ok = new JsonObject();
ok.addProperty("username", username);
ok.addProperty("accessToken", access.token);
ok.addProperty("accessTokenExpiresAt", access.expiresAt);
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
} catch (Exception e) {
LOGGER.error("[auth/sso-token] lookup failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
private void handleRefresh(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String jwt = readString(body, "rememberToken").trim();
if (jwt.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing rememberToken."));
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
RememberJwtService.RotationResult rot = RememberJwtService.rotate(conn, jwt, ip);
if (rot == null) {
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Remember token invalid or expired."));
return;
}
JsonObject ok = new JsonObject();
ok.addProperty("rememberToken", rot.jwt);
ok.addProperty("expiresAt", rot.expiresAt);
AccessTokenService.Issued access = AccessTokenService.issue(rot.userId);
ok.addProperty("accessToken", access.token);
ok.addProperty("accessTokenExpiresAt", access.expiresAt);
sendJson(ctx, req, HttpResponseStatus.OK, ok);
} catch (Exception e) {
LOGGER.error("Refresh failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
private void handleLogin(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String username = readString(body, "username").trim();
String password = readString(body, "password");
boolean rememberMe = readBoolean(body, "remember", false);
if (username.isEmpty() || password.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing credentials."));
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)");
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;
}
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;
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);
AccessTokenService.Issued access = AccessTokenService.issue(userId);
ok.addProperty("accessToken", access.token);
ok.addProperty("accessTokenExpiresAt", access.expiresAt);
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
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();
}
AuthRateLimiter.recordSuccess(ip);
JsonObject ok = new JsonObject();
ok.addProperty("ssoTicket", ssoTicket);
ok.addProperty("username", rs.getString("username"));
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
} catch (Exception e) {
LOGGER.error("Login query failed for username=" + username, e);
@@ -210,8 +516,6 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
}
}
/* ─── Register ──────────────────────────────────────────────────────── */
private void handleRegister(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
if (!Emulator.getConfig().getBoolean("login.register.enabled", true)) {
sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, errorPayload("Registration is closed."));
@@ -221,6 +525,9 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
String username = readString(body, "username").trim();
String email = readString(body, "email").trim();
String password = readString(body, "password");
String figure = readString(body, "figure").trim();
String gender = readString(body, "gender").trim().toUpperCase();
int templateId = readInt(body, "templateId", 0);
if (!USERNAME_RE.matcher(username).matches()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
@@ -280,11 +587,19 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
String defaultMotto = Emulator.getConfig().getValue("register.default.motto", "I love Habbo!");
int now = Emulator.getIntUnixTimestamp();
String finalLook = (figure.isEmpty() || !FIGURE_RE.matcher(figure).matches()) ? defaultLook : figure;
String finalGender = (gender.equals("M") || gender.equals("F")) ? gender : "M";
int startingCredits = Math.max(0, Emulator.getConfig().getInt("new_user_credits", 0));
int startingDuckets = Math.max(0, Emulator.getConfig().getInt("new_user_duckets", 0));
int startingDiamonds = Math.max(0, Emulator.getConfig().getInt("new_user_diamonds", 0));
int newUserId = 0;
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO users (username, password, mail, account_created, " +
"ip_register, ip_current, last_online, last_login, motto, look, gender, " +
"credits, `rank`, home_room, machine_id, auth_ticket, online) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'M', 0, 1, 0, '', '', '0')",
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, '', '', '0')",
Statement.RETURN_GENERATED_KEYS)) {
ins.setString(1, username);
ins.setString(2, hashed);
@@ -295,10 +610,31 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
ins.setInt(7, now);
ins.setInt(8, now);
ins.setString(9, defaultMotto);
ins.setString(10, defaultLook);
ins.setString(10, finalLook);
ins.setString(11, finalGender);
ins.setInt(12, startingCredits);
ins.executeUpdate();
try (ResultSet keys = ins.getGeneratedKeys()) {
if (keys.next()) newUserId = keys.getInt(1);
}
}
if (newUserId > 0 && (startingDuckets > 0 || startingDiamonds > 0)) {
seedUserCurrencies(conn, newUserId, startingDuckets, startingDiamonds);
}
LOGGER.info("[auth/register] user created id={} username='{}' templateId={} credits={} duckets={} diamonds={}",
newUserId, username, templateId, startingCredits, startingDuckets, startingDiamonds);
if (newUserId > 0 && templateId > 0) {
cloneTemplateForUser(conn, templateId, newUserId, username);
} else if (templateId > 0) {
LOGGER.warn("[auth/register] skipping template clone: user insert did not return an id (username='{}')", username);
}
AvailabilityCache.invalidateEmail(email);
AvailabilityCache.invalidateUsername(username);
JsonObject ok = new JsonObject();
ok.addProperty("message", "Welcome aboard, " + username + "! Your account is ready — log in below with the password you just chose.");
sendJson(ctx, req, HttpResponseStatus.OK, ok);
@@ -308,7 +644,271 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
}
}
/* ─── Forgot password ───────────────────────────────────────────────── */
private static void materializeCustomLayout(Connection conn, int templateId, int newRoomId) {
String overrideModel = "0";
String heightmap = "";
int doorX = 0, doorY = 0, doorDir = 2;
try (PreparedStatement sel = conn.prepareStatement(
"SELECT override_model, heightmap, door_x, door_y, door_dir " +
"FROM room_templates WHERE template_id = ? LIMIT 1")) {
sel.setInt(1, templateId);
try (ResultSet rs = sel.executeQuery()) {
if (rs.next()) {
overrideModel = rs.getString("override_model");
heightmap = rs.getString("heightmap");
doorX = rs.getInt("door_x");
doorY = rs.getInt("door_y");
doorDir = rs.getInt("door_dir");
}
}
} catch (SQLException e) {
LOGGER.error("[auth/register] reading template layout failed templateId=" + templateId, e);
return;
}
if (!"1".equals(overrideModel) || heightmap == null || heightmap.isEmpty()) {
return;
}
String customName = "custom_" + newRoomId;
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO room_models_custom (id, name, door_x, door_y, door_dir, heightmap) " +
"VALUES (?, ?, ?, ?, ?, ?) " +
"ON DUPLICATE KEY UPDATE name = VALUES(name), door_x = VALUES(door_x), " +
"door_y = VALUES(door_y), door_dir = VALUES(door_dir), heightmap = VALUES(heightmap)")) {
ins.setInt(1, newRoomId);
ins.setString(2, customName);
ins.setInt(3, doorX);
ins.setInt(4, doorY);
ins.setInt(5, doorDir);
ins.setString(6, heightmap);
ins.executeUpdate();
} catch (SQLException e) {
LOGGER.error("[auth/register] room_models_custom insert failed roomId=" + newRoomId, e);
return;
}
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE rooms SET model = ? WHERE id = ? LIMIT 1")) {
upd.setString(1, customName);
upd.setInt(2, newRoomId);
upd.executeUpdate();
} catch (SQLException e) {
LOGGER.error("[auth/register] rooms.model rename failed roomId=" + newRoomId, e);
}
LOGGER.info("[auth/register] materialized custom layout '{}' for roomId={}", customName, newRoomId);
}
private static void seedUserCurrencies(Connection conn, int userId, int duckets, int diamonds) {
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO users_currency (user_id, type, amount) VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE amount = VALUES(amount)")) {
if (duckets > 0) {
ins.setInt(1, userId);
ins.setInt(2, 0);
ins.setInt(3, duckets);
ins.addBatch();
}
if (diamonds > 0) {
ins.setInt(1, userId);
ins.setInt(2, 5);
ins.setInt(3, diamonds);
ins.addBatch();
}
ins.executeBatch();
} catch (SQLException e) {
LOGGER.error("[auth/register] seeding users_currency failed userId=" + userId
+ " duckets=" + duckets + " diamonds=" + diamonds, e);
}
}
private void handleRoomTemplates(ChannelHandlerContext ctx, FullHttpRequest req) {
JsonArray templates = new JsonArray();
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT template_id, title, description, thumbnail " +
"FROM room_templates WHERE enabled = '1' " +
"ORDER BY sort_order ASC, template_id ASC")) {
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
JsonObject t = new JsonObject();
t.addProperty("templateId", rs.getInt("template_id"));
t.addProperty("title", rs.getString("title"));
t.addProperty("description", rs.getString("description"));
t.addProperty("thumbnail", rs.getString("thumbnail"));
templates.add(t);
}
}
} catch (Exception e) {
LOGGER.error("room-templates list failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
return;
}
JsonObject res = new JsonObject();
res.add("templates", templates);
sendJson(ctx, req, HttpResponseStatus.OK, res);
}
private static final long NEWS_CACHE_TTL_MS = 30_000L;
private static final int NEWS_IMAGE_MAX_BYTES = 512 * 1024;
private static volatile NewsCacheEntry NEWS_CACHE = null;
private static final class NewsCacheEntry {
final byte[] jsonBytes;
final long expiresAt;
NewsCacheEntry(byte[] j, long e) { jsonBytes = j; expiresAt = e; }
}
private void handleNews(ChannelHandlerContext ctx, FullHttpRequest req) {
long now = System.currentTimeMillis();
NewsCacheEntry cached = NEWS_CACHE;
if (cached == null || cached.expiresAt < now) {
JsonArray items = new JsonArray();
int limit = Math.max(1, Math.min(20, Emulator.getConfig().getInt("login.news.limit", 5)));
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT id, title, body, image, link_text, link_url " +
"FROM ui_news WHERE enabled = 1 " +
"ORDER BY sort_order ASC, id DESC LIMIT ?")) {
stmt.setInt(1, limit);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
int id = rs.getInt("id");
JsonObject n = new JsonObject();
n.addProperty("id", id);
n.addProperty("title", rs.getString("title"));
n.addProperty("body", rs.getString("body"));
String image = rs.getString("image");
if (image != null && image.length() > NEWS_IMAGE_MAX_BYTES) {
LOGGER.warn("ui_news id={} image is {} bytes (>{}KB cap), omitting in response",
id, image.length(), NEWS_IMAGE_MAX_BYTES / 1024);
image = null;
}
n.addProperty("image", image); // gson encodes null as JSON null
n.addProperty("linkText", rs.getString("link_text"));
n.addProperty("linkUrl", rs.getString("link_url"));
items.add(n);
}
}
} catch (Exception e) {
LOGGER.error("ui_news list failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
return;
}
JsonObject res = new JsonObject();
res.add("news", items);
byte[] bytes = res.toString().getBytes(StandardCharsets.UTF_8);
cached = new NewsCacheEntry(bytes, now + NEWS_CACHE_TTL_MS);
NEWS_CACHE = cached;
}
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
Unpooled.wrappedBuffer(cached.jsonBytes));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8");
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, cached.jsonBytes.length);
response.headers().set(HttpHeaderNames.CACHE_CONTROL, "public, max-age=30");
applyCors(req, response);
boolean keepAlive = isKeepAlive(req);
if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
var future = ctx.writeAndFlush(response);
if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE);
}
private void handleServerKey(ChannelHandlerContext ctx, FullHttpRequest req) {
try {
JsonObject ok = new JsonObject();
ok.addProperty("publicKey", com.eu.habbo.networking.gameserver.crypto.CryptoSigningKeyManager.publicKeyBase64());
ok.addProperty("algorithm", "ECDSA-P256-SHA256");
sendJson(ctx, req, HttpResponseStatus.OK, ok);
} catch (Exception e) {
LOGGER.error("server-key fetch failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
private static void cloneTemplateForUser(Connection conn, int templateId, int userId, String userName) {
LOGGER.info("[auth/register] cloning template id={} for user id={} name='{}'", templateId, userId, userName);
try (PreparedStatement check = conn.prepareStatement(
"SELECT 1 FROM room_templates WHERE template_id = ? AND enabled = '1' LIMIT 1")) {
check.setInt(1, templateId);
try (ResultSet rs = check.executeQuery()) {
if (!rs.next()) {
LOGGER.warn("[auth/register] unknown/disabled room template id={} for user id={}", templateId, userId);
return;
}
}
} catch (SQLException e) {
LOGGER.error("[auth/register] template lookup failed for templateId=" + templateId, e);
return;
}
int newRoomId = 0;
int roomsInserted = 0;
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO rooms (owner_id, owner_name, name, description, model, password, state, " +
"users_max, category, paper_floor, paper_wall, paper_landscape, thickness_wall, " +
"thickness_floor, moodlight_data, override_model, trade_mode) " +
"(SELECT ?, ?, name, room_description, model, password, state, " +
"users_max, category, paper_floor, paper_wall, paper_landscape, thickness_wall, " +
"thickness_floor, moodlight_data, override_model, trade_mode " +
"FROM room_templates WHERE template_id = ?)",
Statement.RETURN_GENERATED_KEYS)) {
ins.setInt(1, userId);
ins.setString(2, userName);
ins.setInt(3, templateId);
roomsInserted = ins.executeUpdate();
try (ResultSet keys = ins.getGeneratedKeys()) {
if (keys.next()) newRoomId = keys.getInt(1);
}
} catch (SQLException e) {
LOGGER.error("[auth/register] clone rooms failed templateId=" + templateId + " userId=" + userId, e);
return;
}
LOGGER.info("[auth/register] rooms insert: rowsAffected={} newRoomId={}", roomsInserted, newRoomId);
if (newRoomId <= 0) {
LOGGER.warn("[auth/register] clone aborted - no roomId returned (templateId={}, userId={})", templateId, userId);
return;
}
materializeCustomLayout(conn, templateId, newRoomId);
int itemsInserted = 0;
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO items (user_id, room_id, item_id, wall_pos, x, y, z, rot, " +
"extra_data, wired_data, limited_data, guild_id) " +
"(SELECT ?, ?, item_id, wall_pos, x, y, z, rot, extra_data, wired_data, '0:0', 0 " +
"FROM room_templates_items WHERE template_id = ?)")) {
ins.setInt(1, userId);
ins.setInt(2, newRoomId);
ins.setInt(3, templateId);
itemsInserted = ins.executeUpdate();
} catch (SQLException e) {
LOGGER.error("[auth/register] clone items failed templateId=" + templateId
+ " roomId=" + newRoomId + " userId=" + userId, e);
}
LOGGER.info("[auth/register] items insert: rowsAffected={} roomId={}", itemsInserted, newRoomId);
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE users SET home_room = ? WHERE id = ? LIMIT 1")) {
upd.setInt(1, newRoomId);
upd.setInt(2, userId);
int rows = upd.executeUpdate();
LOGGER.info("[auth/register] home_room update: rowsAffected={} userId={} roomId={}", rows, userId, newRoomId);
} catch (SQLException e) {
LOGGER.error("[auth/register] setting home_room failed userId=" + userId + " roomId=" + newRoomId, e);
}
}
private void handleForgot(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String email = readString(body, "email").trim();
@@ -363,7 +963,73 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
/* ─── Helpers ───────────────────────────────────────────────────────── */
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;
@@ -395,6 +1061,27 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
}
}
private static int readInt(JsonObject obj, String key, int defaultValue) {
if (obj == null || !obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
try {
return obj.get(key).getAsInt();
} catch (Exception e) {
return defaultValue;
}
}
private static boolean readBoolean(JsonObject obj, String key, boolean defaultValue) {
if (obj == null || !obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
try {
com.google.gson.JsonElement el = obj.get(key);
if (el.getAsJsonPrimitive().isBoolean()) return el.getAsBoolean();
String s = el.getAsString();
return "1".equals(s) || "true".equalsIgnoreCase(s);
} catch (Exception e) {
return defaultValue;
}
}
private static String resolveClientIp(ChannelHandlerContext ctx, FullHttpRequest req) {
String ipHeader = Emulator.getConfig() != null
? Emulator.getConfig().getValue("ws.ip.header", "")
@@ -449,7 +1136,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
response.headers().set("Vary", "Origin");
response.headers().set("Access-Control-Allow-Credentials", "true");
}
response.headers().set("Access-Control-Allow-Methods", "POST, OPTIONS");
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS");
response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With");
}
@@ -9,6 +9,7 @@ import java.util.concurrent.atomic.AtomicReference;
public final class AuthRateLimiter {
private static final Map<String, AtomicReference<State>> STATE = new ConcurrentHashMap<>();
private static final Map<String, AtomicReference<ProbeState>> PROBE_STATE = new ConcurrentHashMap<>();
private AuthRateLimiter() {}
@@ -58,6 +59,35 @@ public final class AuthRateLimiter {
STATE.remove(ip);
}
public static boolean tryProbe(String ip) {
if (!isEnabled() || ip == null || ip.isEmpty()) return true;
if (isLocked(ip)) return false;
long now = System.currentTimeMillis();
long windowMs = configInt("login.probe.window_sec", 60) * 1000L;
int maxAttempts = configInt("login.probe.max_attempts", 20);
ProbeState next = PROBE_STATE.computeIfAbsent(ip, k -> new AtomicReference<>(new ProbeState(0, now)))
.updateAndGet(prev -> {
if (prev == null || (now - prev.windowStartMillis) > windowMs) {
return new ProbeState(1, now);
}
return new ProbeState(prev.count + 1, prev.windowStartMillis);
});
return next.count <= maxAttempts;
}
public static long secondsUntilProbeReset(String ip) {
AtomicReference<ProbeState> ref = PROBE_STATE.get(ip);
if (ref == null) return 0;
ProbeState current = ref.get();
if (current == null) return 0;
long windowMs = configInt("login.probe.window_sec", 60) * 1000L;
long remainingMs = (current.windowStartMillis + windowMs) - System.currentTimeMillis();
return remainingMs > 0 ? (remainingMs / 1000L) + 1L : 0L;
}
private static boolean isEnabled() {
return Emulator.getConfig() != null
&& Emulator.getConfig().getBoolean("login.ratelimit.enabled", true);
@@ -68,4 +98,5 @@ public final class AuthRateLimiter {
}
private record State(int attempts, long windowStartMillis, long lockedUntilMillis) {}
private record ProbeState(int count, long windowStartMillis) {}
}
@@ -0,0 +1,91 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class AvailabilityCache {
private static final Map<String, Entry> EMAIL_CACHE = new ConcurrentHashMap<>();
private static final Map<String, Entry> USERNAME_CACHE = new ConcurrentHashMap<>();
private AvailabilityCache() {}
public static Boolean lookupEmail(String email) {
return read(EMAIL_CACHE, key(email));
}
public static Boolean lookupUsername(String username) {
return read(USERNAME_CACHE, key(username));
}
public static void storeEmail(String email, boolean available) {
write(EMAIL_CACHE, key(email), available);
}
public static void storeUsername(String username, boolean available) {
write(USERNAME_CACHE, key(username), available);
}
public static void invalidateEmail(String email) {
EMAIL_CACHE.remove(key(email));
}
public static void invalidateUsername(String username) {
USERNAME_CACHE.remove(key(username));
}
private static String key(String value) {
return value == null ? "" : value.trim().toLowerCase(Locale.ROOT);
}
private static Boolean read(Map<String, Entry> cache, String key) {
if (!isEnabled() || key.isEmpty()) return null;
Entry entry = cache.get(key);
if (entry == null) return null;
if (entry.expiresAt < System.currentTimeMillis()) {
cache.remove(key, entry);
return null;
}
return entry.available;
}
private static void write(Map<String, Entry> cache, String key, boolean available) {
if (!isEnabled() || key.isEmpty()) return;
int maxEntries = configInt("login.probe.cache_max_entries", 10_000);
if (cache.size() >= maxEntries) evict(cache, maxEntries);
long ttlMs = configInt("login.probe.cache_ttl_sec", 60) * 1000L;
cache.put(key, new Entry(available, System.currentTimeMillis() + ttlMs));
}
private static void evict(Map<String, Entry> cache, int maxEntries) {
long now = System.currentTimeMillis();
cache.values().removeIf(e -> e.expiresAt < now);
if (cache.size() < maxEntries) return;
int overflow = cache.size() - maxEntries + 1;
Iterator<String> it = cache.keySet().iterator();
while (overflow > 0 && it.hasNext()) {
it.next();
it.remove();
overflow--;
}
}
private static boolean isEnabled() {
return Emulator.getConfig() == null
|| Emulator.getConfig().getBoolean("login.probe.cache_enabled", true);
}
private static int configInt(String key, int fallback) {
return Emulator.getConfig() != null ? Emulator.getConfig().getInt(key, fallback) : fallback;
}
private record Entry(boolean available, long expiresAt) {}
}
@@ -0,0 +1,277 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Base64;
import java.util.UUID;
public final class RememberJwtService {
private static final Logger LOGGER = LoggerFactory.getLogger(RememberJwtService.class);
private static final SecureRandom RNG = new SecureRandom();
private static final Base64.Encoder URL_ENC = Base64.getUrlEncoder().withoutPadding();
private static final Base64.Decoder URL_DEC = Base64.getUrlDecoder();
private static volatile String cachedSecret = null;
private RememberJwtService() {}
public static final class RotationResult {
public final String jwt;
public final int userId;
public final String username;
public final long expiresAt;
RotationResult(String jwt, int userId, String username, long expiresAt) {
this.jwt = jwt;
this.userId = userId;
this.username = username;
this.expiresAt = expiresAt;
}
}
private static int familyTtlDays() {
return Math.max(1, Emulator.getConfig().getInt("login.remember.duration.days", 30));
}
private static long familyTtlSeconds() {
return familyTtlDays() * 86400L;
}
private static String secret() {
String s = cachedSecret;
if (s != null && !s.isEmpty()) return s;
synchronized (RememberJwtService.class) {
if (cachedSecret != null && !cachedSecret.isEmpty()) return cachedSecret;
String configured = Emulator.getConfig().getValue("login.remember.jwt.secret", "");
if (configured != null && !configured.isEmpty()) {
cachedSecret = configured;
return configured;
}
byte[] buf = new byte[48];
RNG.nextBytes(buf);
String generated = Base64.getEncoder().withoutPadding().encodeToString(buf);
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO emulator_settings (`key`, `value`) VALUES ('login.remember.jwt.secret', ?) "
+ "ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)")) {
stmt.setString(1, generated);
stmt.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Could not persist generated login.remember.jwt.secret; using in-memory only", e);
}
Emulator.getConfig().update("login.remember.jwt.secret", generated);
cachedSecret = generated;
LOGGER.info("[auth/remember] generated new JWT signing secret (persisted to emulator_settings)");
return generated;
}
}
public static RotationResult issueForNewFamily(Connection conn, int userId, String username, String ip) throws SQLException {
String familyId = UUID.randomUUID().toString();
long now = Emulator.getIntUnixTimestamp();
long expiresAt = now + familyTtlSeconds();
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO users_remember_families (family_id, user_id, current_version, created_at, expires_at, revoked, last_ip) "
+ "VALUES (?, ?, 1, ?, ?, 0, ?)")) {
ins.setString(1, familyId);
ins.setInt(2, userId);
ins.setLong(3, now);
ins.setLong(4, expiresAt);
ins.setString(5, ip == null ? "" : ip);
ins.executeUpdate();
}
String jwt = buildJwt(userId, familyId, 1, now, expiresAt);
return new RotationResult(jwt, userId, username, expiresAt);
}
public static RotationResult rotate(Connection conn, String jwt, String ip) {
ParsedJwt parsed;
try {
parsed = verifyAndParse(jwt);
} catch (Exception e) {
LOGGER.debug("[auth/remember] invalid JWT: {}", e.getMessage());
return null;
}
long now = Emulator.getIntUnixTimestamp();
if (parsed.exp <= now) return null;
int familyVersion = 0;
boolean revoked = false;
long familyExpiresAt = 0;
try (PreparedStatement sel = conn.prepareStatement(
"SELECT current_version, revoked, expires_at FROM users_remember_families WHERE family_id = ? AND user_id = ? LIMIT 1")) {
sel.setString(1, parsed.familyId);
sel.setInt(2, parsed.userId);
try (ResultSet rs = sel.executeQuery()) {
if (!rs.next()) return null;
familyVersion = rs.getInt("current_version");
revoked = rs.getInt("revoked") != 0;
familyExpiresAt = rs.getLong("expires_at");
}
} catch (SQLException e) {
LOGGER.error("[auth/remember] family lookup failed", e);
return null;
}
if (revoked || familyExpiresAt <= now) return null;
if (parsed.version < familyVersion) {
LOGGER.warn("[auth/remember] replay detected: familyId={} presented v={} but current is v={}, revoking family",
parsed.familyId, parsed.version, familyVersion);
revokeFamilyById(conn, parsed.familyId);
return null;
}
if (parsed.version > familyVersion) {
LOGGER.warn("[auth/remember] future version: familyId={} presented v={} but current is v={}",
parsed.familyId, parsed.version, familyVersion);
return null;
}
int newVersion = familyVersion + 1;
long newExpiresAt = now + familyTtlSeconds();
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE users_remember_families SET current_version = ?, expires_at = ?, last_ip = ? "
+ "WHERE family_id = ? AND current_version = ? AND revoked = 0")) {
upd.setInt(1, newVersion);
upd.setLong(2, newExpiresAt);
upd.setString(3, ip == null ? "" : ip);
upd.setString(4, parsed.familyId);
upd.setInt(5, familyVersion);
int rows = upd.executeUpdate();
if (rows == 0) return null;
} catch (SQLException e) {
LOGGER.error("[auth/remember] rotation update failed", e);
return null;
}
String username = null;
try (PreparedStatement usr = conn.prepareStatement("SELECT username FROM users WHERE id = ? LIMIT 1")) {
usr.setInt(1, parsed.userId);
try (ResultSet rs = usr.executeQuery()) {
if (rs.next()) username = rs.getString("username");
}
} catch (SQLException e) {
LOGGER.error("[auth/remember] username lookup failed", e);
}
if (username == null) return null;
String newJwt = buildJwt(parsed.userId, parsed.familyId, newVersion, now, newExpiresAt);
return new RotationResult(newJwt, parsed.userId, username, newExpiresAt);
}
public static void revokeFromToken(Connection conn, String jwt) {
try {
ParsedJwt p = verifyAndParse(jwt);
revokeFamilyById(conn, p.familyId);
} catch (Exception ignored) { }
}
private static void revokeFamilyById(Connection conn, String familyId) {
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE users_remember_families SET revoked = 1 WHERE family_id = ?")) {
upd.setString(1, familyId);
upd.executeUpdate();
} catch (SQLException e) {
LOGGER.error("[auth/remember] revoke failed for familyId=" + familyId, e);
}
}
private static String buildJwt(int userId, String familyId, int version, long iat, long exp) {
JsonObject header = new JsonObject();
header.addProperty("alg", "HS256");
header.addProperty("typ", "JWT");
JsonObject payload = new JsonObject();
payload.addProperty("sub", userId);
payload.addProperty("fid", familyId);
payload.addProperty("v", version);
payload.addProperty("iat", iat);
payload.addProperty("exp", exp);
payload.addProperty("typ", "refresh");
String h = URL_ENC.encodeToString(header.toString().getBytes(StandardCharsets.UTF_8));
String p = URL_ENC.encodeToString(payload.toString().getBytes(StandardCharsets.UTF_8));
String signingInput = h + "." + p;
String sig = URL_ENC.encodeToString(hmacSha256(secret().getBytes(StandardCharsets.UTF_8),
signingInput.getBytes(StandardCharsets.UTF_8)));
return signingInput + "." + sig;
}
private static final class ParsedJwt {
final int userId;
final String familyId;
final int version;
final long exp;
ParsedJwt(int userId, String familyId, int version, long exp) {
this.userId = userId;
this.familyId = familyId;
this.version = version;
this.exp = exp;
}
}
private static ParsedJwt verifyAndParse(String jwt) throws Exception {
if (jwt == null || jwt.isEmpty()) throw new IllegalArgumentException("empty");
String[] parts = jwt.split("\\.");
if (parts.length != 3) throw new IllegalArgumentException("not 3 segments");
String signingInput = parts[0] + "." + parts[1];
byte[] expected = hmacSha256(secret().getBytes(StandardCharsets.UTF_8), signingInput.getBytes(StandardCharsets.UTF_8));
byte[] provided = URL_DEC.decode(parts[2]);
if (!constantTimeEquals(expected, provided)) throw new SecurityException("bad signature");
byte[] payloadBytes = URL_DEC.decode(parts[1]);
JsonObject payload = JsonParser.parseString(new String(payloadBytes, StandardCharsets.UTF_8)).getAsJsonObject();
if (!payload.has("typ") || !"refresh".equals(payload.get("typ").getAsString())) throw new IllegalArgumentException("wrong typ");
int userId = payload.get("sub").getAsInt();
String fid = payload.get("fid").getAsString();
int version = payload.get("v").getAsInt();
long exp = payload.get("exp").getAsLong();
return new ParsedJwt(userId, fid, version, exp);
}
private static byte[] hmacSha256(byte[] key, byte[] data) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(data);
} catch (Exception e) {
throw new IllegalStateException("HmacSHA256 unavailable", e);
}
}
private static boolean constantTimeEquals(byte[] a, byte[] b) {
if (a == null || b == null || a.length != b.length) return false;
int r = 0;
for (int i = 0; i < a.length; i++) r |= a[i] ^ b[i];
return r == 0;
}
}
@@ -0,0 +1,371 @@
package com.eu.habbo.networking.gameserver.badges;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.custombadge.CustomBadge;
import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeException;
import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager;
import com.eu.habbo.networking.gameserver.GameServerAttributes;
import com.eu.habbo.networking.gameserver.auth.AccessTokenService;
import com.eu.habbo.networking.gameserver.auth.AuthRateLimiter;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.*;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
public class BadgeHttpHandler extends ChannelInboundHandlerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(BadgeHttpHandler.class);
private static final String BASE_PATH = "/api/badges/custom";
private static final int MAX_BODY_BYTES = 128 * 1024;
private static volatile JsonObject cachedTextsResponse = null;
private static volatile long cachedTextsVersion = -1L;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (!(msg instanceof FullHttpRequest req)) {
super.channelRead(ctx, msg);
return;
}
String path = new QueryStringDecoder(req.uri()).path();
if (!path.equals(BASE_PATH) && !path.startsWith(BASE_PATH + "/")) {
super.channelRead(ctx, msg);
return;
}
try {
handle(ctx, req, path);
} finally {
ReferenceCountUtil.release(req);
}
}
private void handle(ChannelHandlerContext ctx, FullHttpRequest req, String path) {
if (req.method() == HttpMethod.OPTIONS) {
sendCors(ctx, req);
return;
}
if (path.equals(BASE_PATH + "/texts")) {
if (req.method() == HttpMethod.GET || req.method() == HttpMethod.HEAD) {
String ip = resolveClientIp(ctx, req);
if (!AuthRateLimiter.tryProbe(ip)) {
long secs = AuthRateLimiter.secondsUntilProbeReset(ip);
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
error("Too many requests. Try again in " + secs + "s."));
return;
}
handleTexts(ctx, req);
return;
}
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, error("Use GET."));
return;
}
int userId = authenticate(req);
if (userId == 0) {
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, error("Authentication required."));
return;
}
if (req.content().readableBytes() > MAX_BODY_BYTES) {
sendJson(ctx, req, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, error("Payload too large."));
return;
}
String trailing = path.length() > BASE_PATH.length() ? path.substring(BASE_PATH.length() + 1) : "";
try {
if (trailing.isEmpty()) {
if (req.method() == HttpMethod.GET || req.method() == HttpMethod.HEAD) {
handleList(ctx, req, userId);
return;
}
if (req.method() == HttpMethod.POST) {
handleCreate(ctx, req, userId);
return;
}
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, error("Use GET or POST."));
return;
}
String badgeId = trailing;
CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager();
if (!manager.isCustomBadgeId(badgeId)) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Invalid badge id."));
return;
}
if (req.method() == HttpMethod.PUT || req.method() == HttpMethod.POST) {
handleUpdate(ctx, req, userId, badgeId);
return;
}
if (req.method() == HttpMethod.DELETE) {
handleDelete(ctx, req, userId, badgeId);
return;
}
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, error("Use PUT or DELETE."));
} catch (Exception e) {
LOGGER.error("[badges/custom] unexpected error path=" + path, e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, error("Server error."));
}
}
private void handleTexts(ChannelHandlerContext ctx, FullHttpRequest req) {
CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager();
long version = manager.getTextCacheVersion();
JsonObject ok = cachedTextsResponse;
if (ok == null || cachedTextsVersion != version) {
java.util.Map<String, CustomBadgeManager.BadgeText> cache = manager.getTextCache();
JsonObject texts = new JsonObject();
for (java.util.Map.Entry<String, CustomBadgeManager.BadgeText> entry : cache.entrySet()) {
String badgeId = entry.getKey();
CustomBadgeManager.BadgeText value = entry.getValue();
texts.addProperty("badge_name_" + badgeId, value.name);
texts.addProperty("badge_desc_" + badgeId, value.description);
}
JsonObject built = new JsonObject();
built.add("texts", texts);
built.addProperty("count", cache.size());
built.addProperty("version", version);
cachedTextsResponse = built;
cachedTextsVersion = version;
ok = built;
}
sendJsonCached(ctx, req, HttpResponseStatus.OK, ok);
}
private void handleList(ChannelHandlerContext ctx, FullHttpRequest req, int userId) {
CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager();
List<CustomBadge> badges = manager.listForUser(userId);
JsonArray arr = new JsonArray();
for (CustomBadge b : badges) arr.add(toJson(b, manager));
JsonObject ok = new JsonObject();
ok.add("badges", arr);
ok.addProperty("max", CustomBadgeManager.MAX_PER_USER);
ok.addProperty("badgeWidth", CustomBadgeManager.BADGE_WIDTH);
ok.addProperty("badgeHeight", CustomBadgeManager.BADGE_HEIGHT);
ok.addProperty("maxBadgeSizeBytes", CustomBadgeManager.MAX_BADGE_SIZE_BYTES);
if (manager.getSettings() != null) {
ok.addProperty("priceBadge", manager.getSettings().getPriceBadge());
ok.addProperty("currencyType", manager.getSettings().getCurrencyType());
}
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
private void handleCreate(ChannelHandlerContext ctx, FullHttpRequest req, int userId) {
JsonObject body = readJsonBody(req);
if (body == null) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Invalid JSON body."));
return;
}
byte[] png = decodeImage(body);
if (png == null) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Missing or invalid image."));
return;
}
String name = optString(body, "name");
String description = optString(body, "description");
CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager();
try {
CustomBadge created = manager.create(userId, name, description, png);
sendJson(ctx, req, HttpResponseStatus.CREATED, toJson(created, manager));
} catch (CustomBadgeException e) {
sendJson(ctx, req, statusFor(e), error(e.getMessage(), e.getCode()));
}
}
private void handleUpdate(ChannelHandlerContext ctx, FullHttpRequest req, int userId, String badgeId) {
JsonObject body = readJsonBody(req);
if (body == null) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Invalid JSON body."));
return;
}
byte[] png = decodeImage(body);
if (png == null) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Missing or invalid image."));
return;
}
String name = optString(body, "name");
String description = optString(body, "description");
CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager();
try {
CustomBadge updated = manager.update(userId, badgeId, name, description, png);
sendJson(ctx, req, HttpResponseStatus.OK, toJson(updated, manager));
} catch (CustomBadgeException e) {
sendJson(ctx, req, statusFor(e), error(e.getMessage(), e.getCode()));
}
}
private void handleDelete(ChannelHandlerContext ctx, FullHttpRequest req, int userId, String badgeId) {
CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager();
try {
manager.delete(userId, badgeId);
JsonObject ok = new JsonObject();
ok.addProperty("deleted", badgeId);
sendJson(ctx, req, HttpResponseStatus.OK, ok);
} catch (CustomBadgeException e) {
sendJson(ctx, req, statusFor(e), error(e.getMessage(), e.getCode()));
}
}
private static byte[] decodeImage(JsonObject body) {
if (!body.has("image")) return null;
try {
String raw = body.get("image").getAsString();
if (raw == null || raw.isEmpty()) return null;
int comma = raw.indexOf(',');
String b64 = raw.startsWith("data:") && comma >= 0 ? raw.substring(comma + 1) : raw;
return Base64.getDecoder().decode(b64.replaceAll("\\s+", ""));
} catch (Exception e) {
return null;
}
}
private static JsonObject readJsonBody(FullHttpRequest req) {
try {
String text = req.content().toString(StandardCharsets.UTF_8);
if (text.isEmpty()) return new JsonObject();
return JsonParser.parseString(text).getAsJsonObject();
} catch (Exception e) {
return null;
}
}
private static String optString(JsonObject body, String key) {
if (body == null || !body.has(key) || body.get(key).isJsonNull()) return "";
try { return body.get(key).getAsString(); }
catch (Exception e) { return ""; }
}
private static int authenticate(FullHttpRequest req) {
String header = req.headers().get(HttpHeaderNames.AUTHORIZATION);
if (header == null || header.isEmpty()) return 0;
String token;
if (header.startsWith("Bearer ")) token = header.substring(7).trim();
else token = header.trim();
return AccessTokenService.verify(token);
}
private static HttpResponseStatus statusFor(CustomBadgeException e) {
return switch (e.getCode()) {
case "not_found" -> HttpResponseStatus.NOT_FOUND;
case "insufficient_funds" -> HttpResponseStatus.PAYMENT_REQUIRED;
case "must_be_online" -> HttpResponseStatus.CONFLICT;
case "rate_limited" -> HttpResponseStatus.TOO_MANY_REQUESTS;
case "limit_reached", "wrong_dimensions", "too_large", "empty", "invalid_image", "not_configured" ->
HttpResponseStatus.BAD_REQUEST;
default -> HttpResponseStatus.INTERNAL_SERVER_ERROR;
};
}
private static JsonObject toJson(CustomBadge badge, CustomBadgeManager manager) {
JsonObject obj = new JsonObject();
obj.addProperty("badgeId", badge.getBadgeId());
obj.addProperty("badgeCode", badge.getBadgeId());
obj.addProperty("name", badge.getBadgeName());
obj.addProperty("description", badge.getBadgeDescription());
obj.addProperty("dateCreated", badge.getDateCreated());
obj.addProperty("dateEdit", badge.getDateEdit());
obj.addProperty("url", manager.publicUrlFor(badge.getBadgeId()));
return obj;
}
private static JsonObject error(String message) {
return error(message, null);
}
private static JsonObject error(String message, String code) {
JsonObject obj = new JsonObject();
obj.addProperty("error", message);
if (code != null) obj.addProperty("code", code);
return obj;
}
private static void sendJsonCached(ChannelHandlerContext ctx, FullHttpRequest req,
HttpResponseStatus status, JsonObject body) {
byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8");
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
response.headers().set(HttpHeaderNames.CACHE_CONTROL, "public, max-age=30");
applyCors(req, response);
boolean keepAlive = isKeepAlive(req);
if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
var future = ctx.writeAndFlush(response);
if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE);
}
private static void sendJson(ChannelHandlerContext ctx, FullHttpRequest req,
HttpResponseStatus status, JsonObject body) {
byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8");
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
applyCors(req, response);
boolean keepAlive = isKeepAlive(req);
if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
var future = ctx.writeAndFlush(response);
if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE);
}
private static void sendCors(ChannelHandlerContext ctx, FullHttpRequest req) {
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
applyCors(req, response);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void applyCors(FullHttpRequest req, FullHttpResponse response) {
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, PUT, DELETE, OPTIONS");
response.headers().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
}
private static boolean isKeepAlive(FullHttpRequest req) {
String connection = req.headers().get(HttpHeaderNames.CONNECTION);
if (connection != null && connection.equalsIgnoreCase("close")) return false;
if (connection != null && connection.equalsIgnoreCase("keep-alive")) return true;
return req.protocolVersion().isKeepAliveDefault();
}
@SuppressWarnings("unused")
private static String resolveClientIp(ChannelHandlerContext ctx, FullHttpRequest req) {
if (ctx.channel().attr(GameServerAttributes.WS_IP).get() != null) {
return ctx.channel().attr(GameServerAttributes.WS_IP).get();
}
if (ctx.channel().remoteAddress() instanceof InetSocketAddress addr) {
return addr.getAddress().getHostAddress();
}
return "";
}
}
@@ -0,0 +1,90 @@
package com.eu.habbo.networking.gameserver.crypto;
import com.eu.habbo.Emulator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.Base64;
public final class CryptoSigningKeyManager {
private static final Logger LOGGER = LoggerFactory.getLogger(CryptoSigningKeyManager.class);
private static final String KEY_PUBLIC = "crypto.ws.signing.public_key";
private static final String KEY_PRIVATE = "crypto.ws.signing.private_key";
private static volatile KeyPair cached;
private static volatile String cachedPublicB64;
private CryptoSigningKeyManager() {}
public static KeyPair get() {
KeyPair kp = cached;
if (kp != null) return kp;
synchronized (CryptoSigningKeyManager.class) {
if (cached != null) return cached;
String pubB64 = Emulator.getConfig().getValue(KEY_PUBLIC, "");
String privB64 = Emulator.getConfig().getValue(KEY_PRIVATE, "");
if (pubB64 != null && !pubB64.isEmpty() && privB64 != null && !privB64.isEmpty()) {
try {
byte[] pubDer = Base64.getDecoder().decode(pubB64);
byte[] privDer = Base64.getDecoder().decode(privB64);
KeyFactory kf = KeyFactory.getInstance("EC");
PublicKey pub = kf.generatePublic(new X509EncodedKeySpec(pubDer));
PrivateKey priv = kf.generatePrivate(new PKCS8EncodedKeySpec(privDer));
cached = new KeyPair(pub, priv);
cachedPublicB64 = pubB64;
return cached;
} catch (Exception e) {
LOGGER.error("[ws-crypto] persisted signing key is corrupt, generating a new pair", e);
}
}
try {
KeyPair generated = WsSessionCrypto.generateSigningKeyPair();
byte[] pubDer = WsSessionCrypto.encodePublicKeySpki(generated.getPublic());
byte[] privDer = WsSessionCrypto.encodePrivateKeyPkcs8(generated.getPrivate());
String newPubB64 = Base64.getEncoder().withoutPadding().encodeToString(pubDer);
String newPrivB64 = Base64.getEncoder().withoutPadding().encodeToString(privDer);
persist(KEY_PUBLIC, newPubB64);
persist(KEY_PRIVATE, newPrivB64);
Emulator.getConfig().update(KEY_PUBLIC, newPubB64);
Emulator.getConfig().update(KEY_PRIVATE, newPrivB64);
cached = generated;
cachedPublicB64 = newPubB64;
LOGGER.info("[ws-crypto] generated a new ECDSA P-256 signing keypair (persisted to emulator_settings)");
return cached;
} catch (Exception e) {
throw new IllegalStateException("Cannot generate signing keypair", e);
}
}
}
public static String publicKeyBase64() {
if (cachedPublicB64 == null) get();
return cachedPublicB64;
}
private static void persist(String key, String value) {
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO emulator_settings (`key`, `value`) VALUES (?, ?) "
+ "ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)")) {
stmt.setString(1, key);
stmt.setString(2, value);
stmt.executeUpdate();
} catch (Exception e) {
LOGGER.error("[ws-crypto] failed to persist " + key + " to emulator_settings (key stays in-memory only)", e);
}
}
}
@@ -0,0 +1,46 @@
package com.eu.habbo.networking.gameserver.crypto;
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.handler.codec.MessageToMessageDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class WsAesDecoder extends MessageToMessageDecoder<ByteBuf> {
private static final Logger LOGGER = LoggerFactory.getLogger(WsAesDecoder.class);
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
byte[] key = ctx.channel().attr(GameServerAttributes.WS_AES_KEY).get();
if (key == null) {
LOGGER.warn("[ws-crypto] inbound frame with no session key, closing");
ctx.close();
return;
}
int readable = in.readableBytes();
if (readable < WsSessionCrypto.NONCE_LEN + 16) {
LOGGER.warn("[ws-crypto] inbound frame too short ({} bytes)", readable);
ctx.close();
return;
}
byte[] nonce = new byte[WsSessionCrypto.NONCE_LEN];
in.readBytes(nonce);
byte[] ct = new byte[in.readableBytes()];
in.readBytes(ct);
try {
byte[] plain = WsSessionCrypto.aesGcmDecrypt(key, nonce, ct);
out.add(Unpooled.wrappedBuffer(plain));
} catch (Exception e) {
LOGGER.warn("[ws-crypto] AES-GCM decrypt failed ({}), closing channel", e.getClass().getSimpleName());
ctx.close();
}
}
}
@@ -0,0 +1,35 @@
package com.eu.habbo.networking.gameserver.crypto;
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.handler.codec.MessageToMessageEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class WsAesEncoder extends MessageToMessageEncoder<ByteBuf> {
private static final Logger LOGGER = LoggerFactory.getLogger(WsAesEncoder.class);
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
byte[] key = ctx.channel().attr(GameServerAttributes.WS_AES_KEY).get();
if (key == null) {
LOGGER.warn("[ws-crypto] outbound frame with no session key, dropping");
return;
}
byte[] plain = new byte[in.readableBytes()];
in.readBytes(plain);
byte[] nonce = WsSessionCrypto.randomNonce();
byte[] ct = WsSessionCrypto.aesGcmEncrypt(key, nonce, plain);
ByteBuf framed = ctx.alloc().buffer(nonce.length + ct.length);
framed.writeBytes(nonce);
framed.writeBytes(ct);
out.add(framed);
}
}
@@ -0,0 +1,152 @@
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;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
public class WsHandshakeHandler extends ChannelInboundHandlerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(WsHandshakeHandler.class);
public static final String HANDLER_NAME = "wsCryptoHandshake";
private static final boolean SIGN_ENABLED = Emulator.getConfig().getBoolean("crypto.ws.signing.enabled", false);
private KeyPair serverKeyPair;
private boolean helloSent = false;
private boolean handshakeComplete = false;
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
sendServerHello(ctx);
}
super.userEventTriggered(ctx, evt);
}
private void sendServerHello(ChannelHandlerContext ctx) {
if (helloSent) return;
try {
this.serverKeyPair = WsSessionCrypto.generateEphemeralKeyPair();
byte[] spki = WsSessionCrypto.encodePublicKeySpki(serverKeyPair.getPublic());
byte[] sigIeee = null;
if (SIGN_ENABLED) {
KeyPair signingKp = CryptoSigningKeyManager.get();
byte[] sigDer = WsSessionCrypto.signEcdsaSha256(signingKp.getPrivate(), spki);
sigIeee = WsSessionCrypto.derToIeee1363(sigDer);
}
int frameLen = 4 + 1 + 2 + spki.length + (sigIeee != null ? 2 + sigIeee.length : 0);
ByteBuf buf = ctx.alloc().buffer(frameLen);
buf.writeInt(WsSessionCrypto.HANDSHAKE_MAGIC);
buf.writeByte(WsSessionCrypto.TYPE_SERVER_HELLO);
buf.writeShort(spki.length);
buf.writeBytes(spki);
if (sigIeee != null) {
buf.writeShort(sigIeee.length);
buf.writeBytes(sigIeee);
}
ctx.writeAndFlush(buf);
helloSent = true;
} catch (Exception e) {
LOGGER.error("[ws-crypto] failed to send server_hello", e);
ctx.close();
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (handshakeComplete) {
ctx.fireChannelRead(msg);
return;
}
if (!(msg instanceof ByteBuf)) {
ctx.fireChannelRead(msg);
return;
}
ByteBuf in = (ByteBuf) msg;
try {
if (in.readableBytes() < 7) {
LOGGER.warn("[ws-crypto] handshake frame too short ({} bytes) from {}", in.readableBytes(), clientAddress(ctx));
ctx.close();
return;
}
int magic = in.readInt();
if (magic != WsSessionCrypto.HANDSHAKE_MAGIC) {
LOGGER.warn("[ws-crypto] handshake magic mismatch: 0x{} from {}", Integer.toHexString(magic), clientAddress(ctx));
ctx.close();
return;
}
byte type = in.readByte();
if (type != WsSessionCrypto.TYPE_CLIENT_HELLO) {
LOGGER.warn("[ws-crypto] expected client_hello, got type=0x{} from {}", Integer.toHexString(type & 0xff), clientAddress(ctx));
ctx.close();
return;
}
int keyLen = in.readUnsignedShort();
if (keyLen <= 0 || keyLen > in.readableBytes() || keyLen > 2048) {
LOGGER.warn("[ws-crypto] invalid client key length {} from {}", keyLen, clientAddress(ctx));
ctx.close();
return;
}
byte[] clientSpki = new byte[keyLen];
in.readBytes(clientSpki);
PublicKey clientPub = WsSessionCrypto.decodePublicKeySpki(clientSpki);
PrivateKey ourPriv = serverKeyPair.getPrivate();
byte[] shared = WsSessionCrypto.deriveSharedSecret(ourPriv, clientPub);
byte[] aesKey = WsSessionCrypto.deriveAesKey(shared);
ctx.channel().attr(GameServerAttributes.WS_AES_KEY).set(aesKey);
ChannelPipeline p = ctx.pipeline();
p.addAfter(HANDLER_NAME, "wsAesDecoder", new WsAesDecoder());
p.addAfter(HANDLER_NAME, "wsAesEncoder", new WsAesEncoder());
handshakeComplete = true;
p.remove(this);
LOGGER.debug("[ws-crypto] handshake complete for {}", clientAddress(ctx));
} catch (Exception e) {
LOGGER.warn("[ws-crypto] handshake failed from {} : {}", clientAddress(ctx), friendlyReason(e));
ctx.close();
} finally {
in.release();
}
}
private static String clientAddress(ChannelHandlerContext ctx) {
String wsIp = ctx.channel().attr(GameServerAttributes.WS_IP).get();
if (wsIp != null && !wsIp.isEmpty()) return wsIp;
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) {
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();
}
}
@@ -0,0 +1,163 @@
package com.eu.habbo.networking.gameserver.crypto;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.Mac;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
public final class WsSessionCrypto {
public static final int HANDSHAKE_MAGIC = 0xC0DEC0DE;
public static final byte TYPE_SERVER_HELLO = 0x01;
public static final byte TYPE_CLIENT_HELLO = 0x02;
public static final String HKDF_INFO = "nitro-ws-v1";
public static final int AES_KEY_LEN = 32;
public static final int NONCE_LEN = 12;
public static final int GCM_TAG_BITS = 128;
private static final SecureRandom RNG = new SecureRandom();
private WsSessionCrypto() {}
public static KeyPair generateEphemeralKeyPair() throws GeneralSecurityException {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
kpg.initialize(new java.security.spec.ECGenParameterSpec("secp256r1"), RNG);
return kpg.generateKeyPair();
}
public static byte[] encodePublicKeySpki(PublicKey publicKey) {
return publicKey.getEncoded();
}
public static PublicKey decodePublicKeySpki(byte[] spki) throws GeneralSecurityException {
KeyFactory kf = KeyFactory.getInstance("EC");
return kf.generatePublic(new X509EncodedKeySpec(spki));
}
public static byte[] deriveSharedSecret(PrivateKey ourPrivate, PublicKey theirPublic) throws GeneralSecurityException {
KeyAgreement ka = KeyAgreement.getInstance("ECDH");
ka.init(ourPrivate);
ka.doPhase(theirPublic, true);
return ka.generateSecret();
}
public static byte[] hkdfSha256(byte[] ikm, byte[] salt, byte[] info, int outLen) throws GeneralSecurityException {
if (salt == null || salt.length == 0) salt = new byte[32];
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(salt, "HmacSHA256"));
byte[] prk = mac.doFinal(ikm);
int hashLen = 32;
int n = (outLen + hashLen - 1) / hashLen;
if (n > 255) throw new GeneralSecurityException("HKDF output too long");
ByteArrayOutputStream okm = new ByteArrayOutputStream();
byte[] t = new byte[0];
for (int i = 1; i <= n; i++) {
mac.init(new SecretKeySpec(prk, "HmacSHA256"));
mac.update(t);
if (info != null) mac.update(info);
mac.update((byte) i);
t = mac.doFinal();
okm.write(t, 0, t.length);
}
byte[] result = okm.toByteArray();
return (result.length == outLen) ? result : Arrays.copyOf(result, outLen);
}
public static byte[] deriveAesKey(byte[] sharedSecret) throws GeneralSecurityException {
return hkdfSha256(sharedSecret, null, HKDF_INFO.getBytes(StandardCharsets.UTF_8), AES_KEY_LEN);
}
public static byte[] aesGcmEncrypt(byte[] key, byte[] nonce, byte[] plaintext) throws GeneralSecurityException {
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(GCM_TAG_BITS, nonce));
return c.doFinal(plaintext);
}
public static byte[] aesGcmDecrypt(byte[] key, byte[] nonce, byte[] ciphertextWithTag) throws GeneralSecurityException {
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(GCM_TAG_BITS, nonce));
return c.doFinal(ciphertextWithTag);
}
public static byte[] randomNonce() {
byte[] n = new byte[NONCE_LEN];
RNG.nextBytes(n);
return n;
}
public static KeyPair generateSigningKeyPair() throws GeneralSecurityException {
return generateEphemeralKeyPair();
}
public static PrivateKey decodePrivateKeyPkcs8(byte[] pkcs8) throws GeneralSecurityException {
KeyFactory kf = KeyFactory.getInstance("EC");
return kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8));
}
public static byte[] encodePrivateKeyPkcs8(PrivateKey privateKey) {
return privateKey.getEncoded();
}
public static byte[] signEcdsaSha256(PrivateKey signingKey, byte[] message) throws GeneralSecurityException {
Signature sig = Signature.getInstance("SHA256withECDSA");
sig.initSign(signingKey);
sig.update(message);
return sig.sign();
}
public static byte[] derToIeee1363(byte[] der) throws GeneralSecurityException {
if (der == null || der.length < 8 || der[0] != 0x30) {
throw new GeneralSecurityException("Malformed DER signature");
}
int seqLen;
int idx;
if ((der[1] & 0x80) == 0) {
seqLen = der[1] & 0xff;
idx = 2;
} else {
int lenBytes = der[1] & 0x7f;
if (lenBytes > 2) throw new GeneralSecurityException("DER length too big");
seqLen = 0;
for (int i = 0; i < lenBytes; i++) seqLen = (seqLen << 8) | (der[2 + i] & 0xff);
idx = 2 + lenBytes;
}
if (idx + seqLen > der.length) throw new GeneralSecurityException("DER truncated");
if (der[idx] != 0x02) throw new GeneralSecurityException("Expected INTEGER r");
int rLen = der[idx + 1] & 0xff;
int rStart = idx + 2;
int sHeader = rStart + rLen;
if (der[sHeader] != 0x02) throw new GeneralSecurityException("Expected INTEGER s");
int sLen = der[sHeader + 1] & 0xff;
int sStart = sHeader + 2;
byte[] r = stripLeadingZero(Arrays.copyOfRange(der, rStart, rStart + rLen));
byte[] s = stripLeadingZero(Arrays.copyOfRange(der, sStart, sStart + sLen));
byte[] out = new byte[64];
System.arraycopy(r, 0, out, 32 - r.length, r.length);
System.arraycopy(s, 0, out, 64 - s.length, s.length);
return out;
}
private static byte[] stripLeadingZero(byte[] v) {
int i = 0;
while (i < v.length - 1 && v[i] == 0x00) i++;
return Arrays.copyOfRange(v, i, v.length);
}
}
@@ -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));
}
}
@@ -8,6 +8,9 @@ db.params=
db.pool.minsize=25
db.pool.maxsize=100
# Encrypt your traffic
crypto.ws.enabled=0
#Game Configuration.
#Host IP. Most likely just 0.0.0.0 Use 127.0.0.1 if you want to play on LAN.
game.host=0.0.0.0