You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
🆙 As NAcho wants it, add effect on disconnected user & small security update
This commit is contained in:
+59
-37
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -54,6 +54,7 @@ public class CustomBadgeManager {
|
||||
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;
|
||||
|
||||
@@ -74,6 +75,10 @@ public class CustomBadgeManager {
|
||||
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();
|
||||
@@ -93,6 +98,7 @@ public class CustomBadgeManager {
|
||||
}
|
||||
this.textCache.clear();
|
||||
this.textCache.putAll(next);
|
||||
this.textCacheVersion.incrementAndGet();
|
||||
LOGGER.info("CustomBadgeManager -> loaded {} custom badge texts into memory.", next.size());
|
||||
}
|
||||
|
||||
@@ -219,6 +225,7 @@ public class CustomBadgeManager {
|
||||
}
|
||||
|
||||
this.textCache.put(badgeId, new BadgeText(safeName, safeDesc));
|
||||
this.textCacheVersion.incrementAndGet();
|
||||
issueBadgeToInventory(userId, badgeId);
|
||||
|
||||
return new CustomBadge(generatedId, userId, badgeId, safeName, safeDesc, now, now);
|
||||
@@ -264,6 +271,7 @@ public class CustomBadgeManager {
|
||||
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);
|
||||
@@ -288,6 +296,7 @@ public class CustomBadgeManager {
|
||||
}
|
||||
|
||||
this.textCache.remove(badgeId);
|
||||
this.textCacheVersion.incrementAndGet();
|
||||
revokeBadgeFromInventory(userId, badgeId);
|
||||
deleteBadgeFileQuietly(badgeId);
|
||||
}
|
||||
|
||||
+45
-13
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -30,6 +31,9 @@ public class BadgeHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
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)) {
|
||||
@@ -58,6 +62,13 @@ public class BadgeHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -116,20 +127,26 @@ public class BadgeHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private void handleTexts(ChannelHandlerContext ctx, FullHttpRequest req) {
|
||||
CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager();
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
JsonObject ok = new JsonObject();
|
||||
ok.add("texts", texts);
|
||||
ok.addProperty("count", cache.size());
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
sendJsonCached(ctx, req, HttpResponseStatus.OK, ok);
|
||||
}
|
||||
|
||||
private void handleList(ChannelHandlerContext ctx, FullHttpRequest req, int userId) {
|
||||
@@ -287,6 +304,21 @@ public class BadgeHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user