Merge pull request #108 from Lorenzune/merge-duckie-main-2026-05-06

Add badge leaderboard API endpoint
This commit is contained in:
DuckieTM
2026-05-20 08:00:20 +02:00
committed by GitHub
4 changed files with 938 additions and 1 deletions
@@ -0,0 +1,477 @@
-- ============================================================
-- Live required schema
-- ============================================================
-- Consolidated schema for the currently used Nitro/Arcturus live
-- additions. This file intentionally excludes old/unused migration
-- artifacts and dump-only data.
--
-- Scope:
-- - tables/columns currently referenced by Java code
-- - runtime settings required by secure assets/API, login, wired, and UI
-- - safe CREATE IF NOT EXISTS / ADD COLUMN IF NOT EXISTS statements
--
-- Assumes the base Arcturus database already exists.
-- Tested for MariaDB-style syntax used by this project.
-- ============================================================
SET NAMES utf8mb4;
-- ------------------------------------------------------------
-- Core settings support
-- ------------------------------------------------------------
ALTER TABLE `emulator_settings`
ADD COLUMN IF NOT EXISTS `comment` TEXT NULL DEFAULT '' AFTER `value`;
CREATE TABLE IF NOT EXISTS `wired_emulator_settings` (
`key` VARCHAR(255) NOT NULL,
`value` TEXT NOT NULL,
`comment` TEXT NULL DEFAULT '',
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
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', ''),
('login.access.jwt.secret', ''),
('login.remember.duration.days', '30'),
('login.remember.rotate.interval.minutes', '15'),
('login.remember.jwt.secret', ''),
('login.turnstile.enabled', '0'),
('login.turnstile.sitekey', ''),
('login.turnstile.secretkey', ''),
('login.ratelimit.enabled', '1'),
('login.ratelimit.max_attempts', '5'),
('login.ratelimit.window_sec', '60'),
('login.ratelimit.lockout_sec', '120'),
('login.register.enabled', '1'),
('register.max_per_ip', '5'),
('register.default.look', 'hr-100-7.hd-180-1.ch-210-66.lg-270-82.sh-290-80'),
('register.default.motto', 'I love Habbo!'),
('password.reset.url', 'http://localhost/reset-password'),
('smtp.provider', 'own'),
('smtp.host', 'localhost'),
('smtp.port', '587'),
('smtp.username', ''),
('smtp.password', ''),
('smtp.from_address', 'no-reply@example.com'),
('smtp.from_name', 'Habbo Hotel'),
('smtp.use_tls', '1'),
('smtp.use_ssl', '0'),
('new_user_credits', '0'),
('new_user_duckets', '0'),
('new_user_diamonds', '0')
ON DUPLICATE KEY UPDATE `value` = `value`;
INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`) VALUES
('wired.engine.enabled', '1', 'Compatibility flag. The runtime uses the new wired engine.'),
('wired.engine.exclusive', '1', 'Compatibility flag. The runtime uses exclusive wired engine execution.'),
('wired.engine.maxStepsPerStack', '100', 'Maximum internal processing steps allowed for a single wired stack execution.'),
('wired.engine.debug', '0', 'Enable verbose debug logging for the wired engine.'),
('wired.custom.enabled', '0', 'Enable custom legacy wired compatibility behavior.'),
('hotel.wired.furni.selection.count', '5', 'Maximum number of furni that a wired box can store or select.'),
('hotel.wired.max_delay', '20', 'Maximum delay value accepted by wired effects that support delayed execution.'),
('hotel.wired.message.max_length', '512', 'Maximum length of wired message text fields.'),
('wired.effect.teleport.delay', '500', 'Delay in milliseconds used by wired teleport movement.'),
('wired.place.under', '0', 'Allow placing wired furniture underneath other items when room rules permit it.'),
('wired.tick.interval.ms', '50', 'Global wired tick interval in milliseconds.'),
('wired.tick.resolution', '100', 'Legacy wired tick resolution value.'),
('wired.tick.debug', '0', 'Enable verbose logging for the wired tick service.'),
('wired.tick.thread.priority', '6', 'Java thread priority used by the wired tick service.'),
('wired.highscores.displaycount', '25', 'Maximum number of wired highscore entries shown to users.'),
('wired.abuse.max.recursion.depth', '10', 'Maximum recursive wired depth before execution is stopped.'),
('wired.abuse.max.events.per.window', '100', 'Maximum identical wired events allowed inside the abuse rate-limit window.'),
('wired.abuse.rate.limit.window.ms', '10000', 'Wired abuse rate-limit window in milliseconds.'),
('wired.abuse.ban.duration.ms', '600000', 'Temporary wired ban duration after abuse detection.'),
('wired.monitor.usage.window.ms', '1000', 'Rolling window size for wired usage monitoring.'),
('wired.monitor.usage.limit', '1000', 'Maximum wired usage budget in one monitor window.'),
('wired.monitor.delayed.events.limit', '100', 'Maximum delayed wired events queued in one room.'),
('wired.monitor.overload.average.ms', '50', 'Average execution time threshold for overload tracking.'),
('wired.monitor.overload.peak.ms', '150', 'Peak execution time threshold for overload tracking.'),
('wired.monitor.overload.consecutive.windows', '2', 'Consecutive overloaded windows required before logging overload.'),
('wired.monitor.heavy.usage.percent', '70', 'Usage percentage threshold for heavy-room tracking.'),
('wired.monitor.heavy.consecutive.windows', '5', 'Consecutive windows above heavy usage threshold.'),
('wired.monitor.heavy.delayed.percent', '60', 'Delayed queue percentage threshold for heavy-room tracking.')
ON DUPLICATE KEY UPDATE
`value` = VALUES(`value`),
`comment` = VALUES(`comment`);
-- ------------------------------------------------------------
-- Login API, room templates, remember-me, and news
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `password_resets` (
`user_id` INT(11) NOT NULL,
`token` VARCHAR(128) NOT NULL,
`expires_at` TIMESTAMP NOT NULL,
`created_ip` VARCHAR(64) NOT NULL DEFAULT '',
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_password_resets_token` (`token`),
CONSTRAINT `fk_password_resets_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
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 `idx_users_remember_families_user_id` (`user_id`),
KEY `idx_users_remember_families_expires_at` (`expires_at`),
CONSTRAINT `fk_users_remember_families_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
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,
`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 `idx_room_templates_enabled_sort` (`enabled`, `sort_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
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 `idx_room_templates_items_template_id` (`template_id`),
KEY `idx_room_templates_items_item_id` (`item_id`),
CONSTRAINT `fk_room_templates_items_template`
FOREIGN KEY (`template_id`) REFERENCES `room_templates` (`template_id`) ON DELETE CASCADE,
CONSTRAINT `fk_room_templates_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 `ui_news` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`title` VARCHAR(150) NOT NULL,
`body` TEXT NOT NULL,
`image` MEDIUMTEXT DEFAULT NULL,
`link_text` VARCHAR(80) NOT NULL DEFAULT '',
`link_url` VARCHAR(255) NOT NULL DEFAULT '',
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_ui_news_enabled_sort` (`enabled`, `sort_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
INSERT INTO `ui_news` (`title`, `body`, `image`, `link_text`, `link_url`, `enabled`, `sort_order`)
SELECT 'Welcome to the Hotel!', 'Catch up on the latest events, updates and competitions happening right now in the hotel.', '', '', '', 1, 0
WHERE NOT EXISTS (SELECT 1 FROM `ui_news`);
-- ------------------------------------------------------------
-- Wired runtime data
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `room_wired_settings` (
`room_id` INT(11) NOT NULL,
`inspect_mask` INT(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can open and inspect Wired in the room. 1=everyone, 2=users with rights, 4=group members, 8=group admins.',
`modify_mask` INT(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can modify Wired in the room. 2=users with rights, 4=group members, 8=group admins.',
PRIMARY KEY (`room_id`),
CONSTRAINT `fk_room_wired_settings_room`
FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `room_wired_variables` (
`room_id` INT(11) NOT NULL,
`variable_item_id` INT(11) NOT NULL,
`value` INT(11) DEFAULT NULL,
`created_at` INT(11) NOT NULL DEFAULT 0,
`updated_at` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`room_id`, `variable_item_id`),
KEY `idx_room_wired_variables_room_item` (`room_id`, `variable_item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `room_user_wired_variables` (
`room_id` INT(11) NOT NULL,
`user_id` INT(11) NOT NULL,
`variable_item_id` INT(11) NOT NULL,
`value` INT(11) DEFAULT NULL,
`created_at` INT(11) NOT NULL DEFAULT 0,
`updated_at` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`room_id`, `user_id`, `variable_item_id`),
KEY `idx_room_user_wired_variables_room_item` (`room_id`, `variable_item_id`),
KEY `idx_room_user_wired_variables_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `room_furni_wired_variables` (
`room_id` INT(11) NOT NULL,
`furni_id` INT(11) NOT NULL,
`variable_item_id` INT(11) NOT NULL,
`value` INT(11) DEFAULT NULL,
`created_at` INT(11) NOT NULL DEFAULT 0,
`updated_at` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`room_id`, `furni_id`, `variable_item_id`),
KEY `idx_room_furni_wired_variables_room_item` (`room_id`, `variable_item_id`),
KEY `idx_room_furni_wired_variables_furni` (`furni_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ------------------------------------------------------------
-- User customization: prefixes, nick icons, profile backgrounds
-- ------------------------------------------------------------
ALTER TABLE `users`
ADD COLUMN IF NOT EXISTS `background_id` INT(11) NOT NULL DEFAULT 0 AFTER `machine_id`,
ADD COLUMN IF NOT EXISTS `background_stand_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_id`,
ADD COLUMN IF NOT EXISTS `background_overlay_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_stand_id`,
ADD COLUMN IF NOT EXISTS `background_card_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_overlay_id`;
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=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO `infostand_backgrounds` (`id`, `category`, `min_rank`, `is_hc_only`, `is_ambassador_only`) VALUES
(0, 'background', 0, 0, 0),
(0, 'stand', 0, 0, 0),
(0, 'overlay', 0, 0, 0),
(0, 'card', 0, 0, 0);
CREATE TABLE IF NOT EXISTS `user_prefixes` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`text` VARCHAR(50) NOT NULL,
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
`icon` VARCHAR(50) NOT NULL DEFAULT '',
`effect` VARCHAR(50) NOT NULL DEFAULT '',
`font` VARCHAR(50) NOT NULL DEFAULT '',
`catalog_prefix_id` INT(11) NOT NULL DEFAULT 0,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`is_custom` TINYINT(1) NOT NULL DEFAULT 1,
`active` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_user_prefixes_user_id` (`user_id`),
KEY `idx_user_prefixes_user_active` (`user_id`, `active`),
CONSTRAINT `fk_user_prefixes_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `custom_prefixes_catalog` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`text` VARCHAR(50) NOT NULL,
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
`icon` VARCHAR(50) NOT NULL DEFAULT '',
`effect` VARCHAR(50) NOT NULL DEFAULT '',
`font` VARCHAR(50) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `user_visual_settings` (
`user_id` INT(11) NOT NULL,
`display_order` VARCHAR(50) NOT NULL DEFAULT 'icon-prefix-name',
PRIMARY KEY (`user_id`),
CONSTRAINT `fk_user_visual_settings_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
`key_name` VARCHAR(100) NOT NULL,
`value` VARCHAR(255) NOT NULL,
PRIMARY KEY (`key_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`word` VARCHAR(100) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_custom_prefix_blacklist_word` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES
('max_length', '15'),
('min_rank_to_buy', '1'),
('price_credits', '5'),
('price_points', '0'),
('points_type', '0'),
('font_price_credits', '10'),
('font_price_points', '0'),
('font_points_type', '0');
INSERT IGNORE INTO `custom_prefixes_catalog`
(`id`, `display_name`, `text`, `color`, `icon`, `effect`, `font`, `points`, `points_type`, `enabled`, `sort_order`)
VALUES
(1, 'VIP', 'VIP', '#FFD700', '', 'glow', '', 10, 0, 1, 1),
(2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2),
(3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3);
CREATE TABLE IF NOT EXISTS `custom_nick_icons_catalog` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`icon_key` VARCHAR(50) NOT NULL,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_custom_nick_icons_catalog_icon_key` (`icon_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `user_nick_icons` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`icon_key` VARCHAR(50) NOT NULL,
`active` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_nick_icons_user_icon` (`user_id`, `icon_key`),
KEY `idx_user_nick_icons_user_id` (`user_id`),
KEY `idx_user_nick_icons_user_active` (`user_id`, `active`),
CONSTRAINT `fk_user_nick_icons_user`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO `custom_nick_icons_catalog` (`icon_key`, `display_name`, `points`, `points_type`, `enabled`, `sort_order`) VALUES
('1', 'Icon 1', 10, 0, 1, 1),
('2', 'Icon 2', 10, 0, 1, 2),
('3', 'Icon 3', 10, 0, 1, 3),
('4', 'Icon 4', 10, 0, 1, 4),
('5', 'Icon 5', 10, 0, 1, 5),
('6', 'Icon 6', 10, 0, 1, 6);
-- ------------------------------------------------------------
-- Custom badge maker
-- ------------------------------------------------------------
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 `uk_user_custom_badge_badge_id` (`badge_id`),
KEY `idx_user_custom_badge_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;
-- ------------------------------------------------------------
-- UI/catalog compatibility values used by the current client
-- ------------------------------------------------------------
INSERT INTO `chat_bubbles` (`type`, `name`, `permission`, `overridable`, `triggers_talking_furniture`) VALUES
(200, 'SHOW_MESSAGE_RED', '', 1, 0),
(201, 'SHOW_MESSAGE_GREEN', '', 1, 0),
(202, 'SHOW_MESSAGE_BLUE', '', 1, 0),
(210, 'SHOW_MESSAGE_ALERT', '', 1, 0),
(211, 'SHOW_MESSAGE_INFO', '', 1, 0),
(212, 'SHOW_MESSAGE_WARNING', '', 1, 0),
(220, 'SHOW_MESSAGE_WRONG', '', 1, 0),
(221, 'SHOW_MESSAGE_WRONG_CIRCLED', '', 1, 0),
(222, 'SHOW_MESSAGE_CORRECT', '', 1, 0),
(223, 'SHOW_MESSAGE_CORRECT_CIRCLED', '', 1, 0),
(224, 'SHOW_MESSAGE_QUESTION', '', 1, 0),
(225, 'SHOW_MESSAGE_QUESTION_CIRCLED', '', 1, 0),
(226, 'SHOW_MESSAGE_ARROW_UP', '', 1, 0),
(227, 'SHOW_MESSAGE_ARROW_UP_CIRCLED', '', 1, 0),
(228, 'SHOW_MESSAGE_ARROW_DOWN', '', 1, 0),
(229, 'SHOW_MESSAGE_ARROW_DOWN_CIRCLED', '', 1, 0),
(250, 'SHOW_MESSAGE_SKULL', '', 1, 0),
(251, 'SHOW_MESSAGE_SKULL_ALT', '', 1, 0),
(252, 'SHOW_MESSAGE_MAGNIFIER', '', 1, 0)
ON DUPLICATE KEY UPDATE
`name` = VALUES(`name`),
`permission` = VALUES(`permission`),
`overridable` = VALUES(`overridable`),
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
INSERT IGNORE 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.'),
('commands.keys.cmd_give_prefix', 'giveprefix'),
('commands.keys.cmd_list_prefixes', 'listprefixes'),
('commands.keys.cmd_remove_prefix', 'removeprefix'),
('commands.keys.cmd_prefix_blacklist', 'prefixblacklist'),
('wiredfurni.badgereceived.body', 'You have just received a new Badge! Check your Inventory!'),
('wiredfurni.badgereceived.title', 'Badge received!');
-- Optional permission metadata for normalized permission schemas.
-- Actual rank values still belong in the permissions/permission_ranks setup.
CREATE TABLE IF NOT EXISTS `permission_definitions` (
`permission_key` VARCHAR(64) NOT NULL,
`max_value` TINYINT(3) UNSIGNED NOT NULL DEFAULT 1,
`comment` TEXT NOT NULL,
PRIMARY KEY (`permission_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`) VALUES
('cmd_setroom_template', 1, 'Allows using :setroom_template to copy a room into the login room-template table.'),
('cmd_give_prefix', 1, 'Allows granting custom prefixes to users.'),
('cmd_list_prefixes', 1, 'Allows listing custom prefixes assigned to users.'),
('cmd_remove_prefix', 1, 'Allows removing custom prefixes from users.'),
('cmd_prefix_blacklist', 1, 'Allows managing the custom prefix blacklist.')
ON DUPLICATE KEY UPDATE
`max_value` = VALUES(`max_value`),
`comment` = VALUES(`comment`);
-- ------------------------------------------------------------
-- Explicitly obsolete table from older remember-me attempts.
-- The current Java uses users_remember_families only.
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `users_remember_tokens`;
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.2.8</version>
<version>4.2.9</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -5,6 +5,7 @@ import com.eu.habbo.messages.PacketManager;
import com.eu.habbo.networking.gameserver.auth.AuthHttpHandler;
import com.eu.habbo.networking.gameserver.auth.NitroSecureApiHandler;
import com.eu.habbo.networking.gameserver.auth.NitroSecureAssetHandler;
import com.eu.habbo.networking.gameserver.badges.BadgeLeaderboardHttpHandler;
import com.eu.habbo.networking.gameserver.badges.BadgeHttpHandler;
import com.eu.habbo.networking.gameserver.codec.WebSocketCodec;
import com.eu.habbo.networking.gameserver.crypto.WsHandshakeHandler;
@@ -60,6 +61,7 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
ch.pipeline().addLast("nitroSecureApiHandler", new NitroSecureApiHandler());
ch.pipeline().addLast("authHttpHandler", new AuthHttpHandler());
ch.pipeline().addLast("badgeHttpHandler", new BadgeHttpHandler());
ch.pipeline().addLast("badgeLeaderboardHttpHandler", new BadgeLeaderboardHttpHandler());
ch.pipeline().addLast("wsProtocolHandler", new WebSocketServerProtocolHandler(this.wsConfig));
ch.pipeline().addLast("wsFrameAggregator", new WebSocketFrameAggregator(MAX_FRAME_SIZE));
ch.pipeline().addLast("wsCodec", new WebSocketCodec());
@@ -0,0 +1,458 @@
package com.eu.habbo.networking.gameserver.badges;
import com.eu.habbo.Emulator;
import com.eu.habbo.networking.gameserver.auth.AccessTokenService;
import com.eu.habbo.networking.gameserver.auth.CorsOriginGate;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
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.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.*;
public class BadgeLeaderboardHttpHandler extends ChannelInboundHandlerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(BadgeLeaderboardHttpHandler.class);
private static final String BASE_PATH = "/api/badges/leaderboard";
private static final long CACHE_TTL_MS = 15_000L;
private static final int MAX_BOARD_USERS = 100;
private static volatile Snapshot cache = null;
private static final class Snapshot {
final List<UserBadgeAggregate> badgeUsers;
final List<UserAchievementAggregate> achievementUsers;
final JsonArray badgeStats;
final long expiresAt;
Snapshot(List<UserBadgeAggregate> badgeUsers, List<UserAchievementAggregate> achievementUsers, JsonArray badgeStats, long expiresAt) {
this.badgeUsers = badgeUsers;
this.achievementUsers = achievementUsers;
this.badgeStats = badgeStats;
this.expiresAt = expiresAt;
}
}
private static final class UserBadgeAggregate {
final int userId;
final String username;
final String figure;
final int totalBadges;
final EnumMap<Rarity, Integer> counts;
UserBadgeAggregate(int userId, String username, String figure, int totalBadges, EnumMap<Rarity, Integer> counts) {
this.userId = userId;
this.username = username;
this.figure = figure;
this.totalBadges = totalBadges;
this.counts = counts;
}
}
private static final class UserAchievementAggregate {
final int userId;
final String username;
final String figure;
final int achievementScore;
UserAchievementAggregate(int userId, String username, String figure, int achievementScore) {
this.userId = userId;
this.username = username;
this.figure = figure;
this.achievementScore = achievementScore;
}
}
private static final class ViewerProfile {
final int userId;
final String username;
final String figure;
ViewerProfile(int userId, String username, String figure) {
this.userId = userId;
this.username = username;
this.figure = figure;
}
}
private enum Rarity {
COMMON("common"),
RARE("rare"),
EPIC("epic"),
LEGENDARY("legendary"),
MYTHICAL("mythical"),
UNIQUE("unique");
final String key;
Rarity(String key) {
this.key = key;
}
}
@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);
} finally {
ReferenceCountUtil.release(req);
}
}
private void handle(ChannelHandlerContext ctx, FullHttpRequest req) {
if (req.method() == HttpMethod.OPTIONS) {
sendCors(ctx, req);
return;
}
if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) {
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, error("Use GET."));
return;
}
try {
Snapshot snapshot = loadSnapshot();
int viewerUserId = authenticateOptional(req);
ViewerProfile viewerProfile = loadViewerProfile(viewerUserId);
JsonObject payload = new JsonObject();
payload.addProperty("viewerUserId", viewerUserId);
payload.add("badgeStats", cloneArray(snapshot.badgeStats));
payload.add("thresholds", buildThresholdsPayload());
JsonObject boards = new JsonObject();
boards.add("totalBadges", buildBadgeBoard(snapshot.badgeUsers, viewerUserId, viewerProfile, null));
boards.add("achievementLevel", buildAchievementBoard(snapshot.achievementUsers, viewerUserId, viewerProfile));
JsonObject rarityBoards = new JsonObject();
for (Rarity rarity : Rarity.values()) {
rarityBoards.add(rarity.key, buildBadgeBoard(snapshot.badgeUsers, viewerUserId, viewerProfile, rarity));
}
boards.add("rarity", rarityBoards);
payload.add("leaderboards", boards);
sendJson(ctx, req, HttpResponseStatus.OK, payload);
} catch (Exception e) {
LOGGER.error("[badges/leaderboard] unexpected error", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, error("Server error."));
}
}
private Snapshot loadSnapshot() throws Exception {
long now = System.currentTimeMillis();
Snapshot current = cache;
if (current != null && current.expiresAt >= now) return current;
synchronized (BadgeLeaderboardHttpHandler.class) {
current = cache;
if (current != null && current.expiresAt >= now) return current;
JsonArray badgeStats = new JsonArray();
List<UserBadgeAggregate> badgeUsers = new ArrayList<>();
List<UserAchievementAggregate> achievementUsers = new ArrayList<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
loadBadgeStats(connection, badgeStats);
loadBadgeUsers(connection, badgeUsers);
loadAchievementUsers(connection, achievementUsers);
}
Snapshot built = new Snapshot(badgeUsers, achievementUsers, badgeStats, now + CACHE_TTL_MS);
cache = built;
return built;
}
}
private void loadBadgeStats(Connection connection, JsonArray badgeStats) throws Exception {
try (PreparedStatement statement = connection.prepareStatement(
"SELECT badge_code, COUNT(DISTINCT user_id) AS owner_count " +
"FROM users_badges GROUP BY badge_code ORDER BY owner_count ASC, badge_code ASC")) {
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
String badgeCode = set.getString("badge_code");
int ownerCount = set.getInt("owner_count");
JsonObject entry = new JsonObject();
entry.addProperty("badgeCode", badgeCode);
entry.addProperty("ownerCount", ownerCount);
entry.addProperty("rarity", classify(ownerCount).key);
badgeStats.add(entry);
}
}
}
}
private void loadBadgeUsers(Connection connection, List<UserBadgeAggregate> badgeUsers) throws Exception {
String sql =
"SELECT u.id AS user_id, u.username, u.look, " +
"COUNT(DISTINCT ub.badge_code) AS total_badges, " +
"COUNT(DISTINCT CASE WHEN counts.owner_count > 50 THEN ub.badge_code END) AS common_count, " +
"COUNT(DISTINCT CASE WHEN counts.owner_count > 10 AND counts.owner_count <= 50 THEN ub.badge_code END) AS rare_count, " +
"COUNT(DISTINCT CASE WHEN counts.owner_count > 6 AND counts.owner_count <= 10 THEN ub.badge_code END) AS epic_count, " +
"COUNT(DISTINCT CASE WHEN counts.owner_count > 3 AND counts.owner_count <= 6 THEN ub.badge_code END) AS legendary_count, " +
"COUNT(DISTINCT CASE WHEN counts.owner_count > 1 AND counts.owner_count <= 3 THEN ub.badge_code END) AS mythical_count, " +
"COUNT(DISTINCT CASE WHEN counts.owner_count = 1 THEN ub.badge_code END) AS unique_count " +
"FROM users_badges ub " +
"INNER JOIN users u ON u.id = ub.user_id " +
"INNER JOIN (SELECT badge_code, COUNT(DISTINCT user_id) AS owner_count FROM users_badges GROUP BY badge_code) counts ON counts.badge_code = ub.badge_code " +
"GROUP BY u.id, u.username, u.look";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
EnumMap<Rarity, Integer> counts = new EnumMap<>(Rarity.class);
counts.put(Rarity.COMMON, set.getInt("common_count"));
counts.put(Rarity.RARE, set.getInt("rare_count"));
counts.put(Rarity.EPIC, set.getInt("epic_count"));
counts.put(Rarity.LEGENDARY, set.getInt("legendary_count"));
counts.put(Rarity.MYTHICAL, set.getInt("mythical_count"));
counts.put(Rarity.UNIQUE, set.getInt("unique_count"));
badgeUsers.add(new UserBadgeAggregate(
set.getInt("user_id"),
safe(set.getString("username")),
safe(set.getString("look")),
set.getInt("total_badges"),
counts
));
}
}
}
}
private void loadAchievementUsers(Connection connection, List<UserAchievementAggregate> achievementUsers) throws Exception {
try (PreparedStatement statement = connection.prepareStatement(
"SELECT u.id AS user_id, u.username, u.look, COALESCE(us.achievement_score, 0) AS achievement_score " +
"FROM users u INNER JOIN users_settings us ON us.user_id = u.id " +
"WHERE COALESCE(us.achievement_score, 0) > 0")) {
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
achievementUsers.add(new UserAchievementAggregate(
set.getInt("user_id"),
safe(set.getString("username")),
safe(set.getString("look")),
set.getInt("achievement_score")
));
}
}
}
}
private ViewerProfile loadViewerProfile(int viewerUserId) throws Exception {
if (viewerUserId <= 0) return null;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT id, username, look FROM users WHERE id = ? LIMIT 1")) {
statement.setInt(1, viewerUserId);
try (ResultSet set = statement.executeQuery()) {
if (!set.next()) return null;
return new ViewerProfile(
set.getInt("id"),
safe(set.getString("username")),
safe(set.getString("look"))
);
}
}
}
private JsonObject buildBadgeBoard(List<UserBadgeAggregate> users, int viewerUserId, ViewerProfile viewerProfile, Rarity rarity) {
List<JsonObject> ranked = new ArrayList<>();
for (UserBadgeAggregate user : users) {
int score = (rarity == null) ? user.totalBadges : user.counts.getOrDefault(rarity, 0);
if (score <= 0) continue;
ranked.add(toEntry(user.userId, user.username, user.figure, score));
}
ranked.sort((a, b) -> {
int scoreCompare = Integer.compare(b.get("score").getAsInt(), a.get("score").getAsInt());
if (scoreCompare != 0) return scoreCompare;
return Integer.compare(a.get("userId").getAsInt(), b.get("userId").getAsInt());
});
return finalizeBoard(ranked, viewerUserId, viewerProfile);
}
private JsonObject buildAchievementBoard(List<UserAchievementAggregate> users, int viewerUserId, ViewerProfile viewerProfile) {
List<JsonObject> ranked = new ArrayList<>();
for (UserAchievementAggregate user : users) {
if (user.achievementScore <= 0) continue;
ranked.add(toEntry(user.userId, user.username, user.figure, user.achievementScore));
}
ranked.sort((a, b) -> {
int scoreCompare = Integer.compare(b.get("score").getAsInt(), a.get("score").getAsInt());
if (scoreCompare != 0) return scoreCompare;
return Integer.compare(a.get("userId").getAsInt(), b.get("userId").getAsInt());
});
return finalizeBoard(ranked, viewerUserId, viewerProfile);
}
private JsonObject finalizeBoard(List<JsonObject> ranked, int viewerUserId, ViewerProfile viewerProfile) {
JsonArray entries = new JsonArray();
JsonObject viewerEntry = null;
int cappedSize = Math.min(ranked.size(), MAX_BOARD_USERS);
for (int index = 0; index < cappedSize; index++) {
JsonObject entry = ranked.get(index).deepCopy();
int rank = index + 1;
entry.addProperty("rank", rank);
entries.add(entry);
if (viewerUserId > 0 && entry.get("userId").getAsInt() == viewerUserId) viewerEntry = entry;
}
if (viewerEntry == null && viewerUserId > 0) {
for (int index = 0; index < ranked.size(); index++) {
JsonObject entry = ranked.get(index);
if (entry.get("userId").getAsInt() != viewerUserId) continue;
viewerEntry = entry.deepCopy();
viewerEntry.addProperty("rank", index + 1);
break;
}
}
if (viewerEntry == null && viewerProfile != null) {
viewerEntry = toEntry(viewerProfile.userId, viewerProfile.username, viewerProfile.figure, 0);
viewerEntry.addProperty("rank", 0);
}
JsonObject board = new JsonObject();
board.add("entries", entries);
board.addProperty("totalPlayers", cappedSize);
board.add("viewerEntry", viewerEntry != null ? viewerEntry : new JsonObject());
return board;
}
private JsonObject toEntry(int userId, String username, String figure, int score) {
JsonObject entry = new JsonObject();
entry.addProperty("userId", userId);
entry.addProperty("username", username);
entry.addProperty("figure", figure);
entry.addProperty("score", score);
return entry;
}
private JsonObject buildThresholdsPayload() {
JsonObject thresholds = new JsonObject();
thresholds.addProperty("commonMinOwners", 51);
thresholds.addProperty("rareMinOwners", 11);
thresholds.addProperty("epicMinOwners", 7);
thresholds.addProperty("legendaryMinOwners", 4);
thresholds.addProperty("mythicalMinOwners", 2);
thresholds.addProperty("uniqueOwners", 1);
return thresholds;
}
private static Rarity classify(int ownerCount) {
if (ownerCount > 50) return Rarity.COMMON;
if (ownerCount > 10) return Rarity.RARE;
if (ownerCount > 6) return Rarity.EPIC;
if (ownerCount > 3) return Rarity.LEGENDARY;
if (ownerCount > 1) return Rarity.MYTHICAL;
if (ownerCount > 0) return Rarity.UNIQUE;
return Rarity.COMMON;
}
private static int authenticateOptional(FullHttpRequest req) {
String header = req.headers().get(HttpHeaderNames.AUTHORIZATION);
if (header == null || header.isEmpty()) return 0;
String token = header.startsWith("Bearer ") ? header.substring(7).trim() : header.trim();
return AccessTokenService.verify(token);
}
private static JsonArray cloneArray(JsonArray source) {
JsonArray copy = new JsonArray();
source.forEach(element -> copy.add(element.deepCopy()));
return copy;
}
private static String safe(String value) {
return value == null ? "" : value;
}
private static JsonObject error(String message) {
JsonObject obj = new JsonObject();
obj.addProperty("error", message);
return obj;
}
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);
response.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-store, no-cache, must-revalidate");
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() && CorsOriginGate.isAllowed(req)) {
response.headers().set("Access-Control-Allow-Origin", origin);
response.headers().set("Access-Control-Allow-Credentials", "true");
}
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
String requestedHeaders = req.headers().get("Access-Control-Request-Headers");
if (requestedHeaders != null && !requestedHeaders.isEmpty()) {
response.headers().set("Access-Control-Allow-Headers", requestedHeaders);
} else {
response.headers().set("Access-Control-Allow-Headers",
"Authorization, Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api");
}
response.headers().set("Vary", "Origin, Access-Control-Request-Headers, Access-Control-Request-Method");
response.headers().set("Access-Control-Max-Age", "600");
response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp");
}
private static boolean isKeepAlive(FullHttpRequest req) {
String connection = req.headers().get(HttpHeaderNames.CONNECTION);
return connection == null || !"close".equalsIgnoreCase(connection);
}
}