diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/Habbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/Habbo.java index 61bc7135..0bc6a604 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/Habbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/Habbo.java @@ -14,6 +14,7 @@ import com.eu.habbo.messages.outgoing.rooms.FloodCounterComposer; import com.eu.habbo.messages.outgoing.rooms.ForwardToRoomComposer; import com.eu.habbo.messages.outgoing.rooms.users.*; import com.eu.habbo.messages.outgoing.users.*; +import com.eu.habbo.networking.gameserver.GameServerAttributes; import com.eu.habbo.plugin.events.users.UserCreditsEvent; import com.eu.habbo.plugin.events.users.UserDisconnectEvent; import com.eu.habbo.plugin.events.users.UserGetIPAddressEvent; @@ -118,15 +119,20 @@ public class Habbo implements Runnable { public boolean connect() { String ip = ""; - String ProxyIP = ""; + String proxyInfo = ""; - if (!Emulator.getConfig().getBoolean("networking.tcp.proxy") && this.client.getChannel().remoteAddress() != null) { + String wsIp = this.client.getChannel().attr(GameServerAttributes.WS_IP).get(); + if (wsIp != null && !wsIp.isEmpty()) { + ip = wsIp; + SocketAddress address = this.client.getChannel().remoteAddress(); + proxyInfo = ((InetSocketAddress) address).getAddress().getHostAddress(); + } else if (!Emulator.getConfig().getBoolean("networking.tcp.proxy") && this.client.getChannel().remoteAddress() != null) { SocketAddress address = this.client.getChannel().remoteAddress(); ip = ((InetSocketAddress) address).getAddress().getHostAddress(); - ProxyIP = "- no proxy server used"; + proxyInfo = "- no proxy server used"; } else { SocketAddress address = this.client.getChannel().remoteAddress(); - ProxyIP = ((InetSocketAddress) address).getAddress().getHostAddress(); + proxyInfo = ((InetSocketAddress) address).getAddress().getHostAddress(); } if (Emulator.getPluginManager().isRegistered(UserGetIPAddressEvent.class, true)) { @@ -170,7 +176,7 @@ public class Habbo implements Runnable { this.messenger.connectionChanged(this, true, false); Emulator.getGameEnvironment().getRoomManager().loadRoomsForHabbo(this); - LOGGER.info("{} logged in from IP {} using proxyserver {}", this.habboInfo.getUsername(), this.habboInfo.getIpLogin(), ProxyIP); + LOGGER.info("{} logged in from IP {} using proxyserver {}", this.habboInfo.getUsername(), this.habboInfo.getIpLogin(), proxyInfo); LOGGER.info("{} client MachineId = {}", this.habboInfo.getUsername(), this.client.getMachineId()); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RequestRoomSettingsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RequestRoomSettingsEvent.java index 20cfb22f..ee1f793f 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RequestRoomSettingsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RequestRoomSettingsEvent.java @@ -13,7 +13,7 @@ public class RequestRoomSettingsEvent extends MessageHandler { Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId); if (room == null) return; - if (!room.hasRights(this.client.getHabbo()) || !this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) return; + if (!room.hasRights(this.client.getHabbo()) && !this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) return; this.client.sendResponse(new RoomSettingsComposer(room)); } diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServer.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServer.java index 12cc52c0..eebc86b5 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServer.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServer.java @@ -8,13 +8,24 @@ import com.eu.habbo.networking.gameserver.decoders.*; import com.eu.habbo.networking.gameserver.encoders.GameServerMessageEncoder; import com.eu.habbo.networking.gameserver.encoders.GameServerMessageLogger; import com.eu.habbo.networking.gameserver.handlers.IdleTimeoutHandler; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.FixedRecvByteBufAllocator; import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.logging.LoggingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class GameServer extends Server { + private static final Logger LOGGER = LoggerFactory.getLogger(GameServer.class); + private final PacketManager packetManager; private final GameClientManager gameClientManager; + private ServerBootstrap webSocketBootstrap; public GameServer(String host, int port) throws Exception { super("Game Server", host, port, Emulator.getConfig().getInt("io.bossgroup.threads"), Emulator.getConfig().getInt("io.workergroup.threads")); @@ -51,6 +62,41 @@ public class GameServer extends Server { } } }); + + initializeWebSocketServer(); + } + + private void initializeWebSocketServer() { + if (!Emulator.getConfig().getBoolean("ws.enabled", false)) { + return; + } + + String wsHost = Emulator.getConfig().getValue("ws.host", "0.0.0.0"); + int wsPort = Emulator.getConfig().getInt("ws.port", 2096); + + WebSocketChannelInitializer wsInitializer = new WebSocketChannelInitializer(); + + this.webSocketBootstrap = new ServerBootstrap(); + this.webSocketBootstrap.group(this.getBossGroup(), this.getWorkerGroup()); + this.webSocketBootstrap.channel(NioServerSocketChannel.class); + this.webSocketBootstrap.childOption(ChannelOption.TCP_NODELAY, true); + this.webSocketBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true); + this.webSocketBootstrap.childOption(ChannelOption.SO_REUSEADDR, true); + this.webSocketBootstrap.childOption(ChannelOption.SO_RCVBUF, 4096); + this.webSocketBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(4096)); + this.webSocketBootstrap.childOption(ChannelOption.ALLOCATOR, new UnpooledByteBufAllocator(false)); + this.webSocketBootstrap.childHandler(wsInitializer); + + ChannelFuture wsFuture = this.webSocketBootstrap.bind(wsHost, wsPort); + + while (!wsFuture.isDone()) { + } + + if (!wsFuture.isSuccess()) { + LOGGER.error("Failed to start WebSocket server on {}:{}", wsHost, wsPort); + } else { + LOGGER.info("WebSocket server started on {}:{} (SSL: {})", wsHost, wsPort, wsInitializer.isSslEnabled()); + } } public PacketManager getPacketManager() { diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServerAttributes.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServerAttributes.java index fca00731..5de9d67f 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServerAttributes.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/GameServerAttributes.java @@ -9,5 +9,5 @@ public class GameServerAttributes { public static final AttributeKey CLIENT = AttributeKey.valueOf("GameClient"); public static final AttributeKey CRYPTO_CLIENT = AttributeKey.valueOf("CryptoClient"); public static final AttributeKey CRYPTO_SERVER = AttributeKey.valueOf("CryptoServer"); - + public static final AttributeKey WS_IP = AttributeKey.valueOf("WebSocketIP"); } diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java new file mode 100644 index 00000000..54234640 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java @@ -0,0 +1,79 @@ +package com.eu.habbo.networking.gameserver; + +import com.eu.habbo.messages.PacketManager; +import com.eu.habbo.networking.gameserver.codec.WebSocketCodec; +import com.eu.habbo.networking.gameserver.decoders.*; +import com.eu.habbo.networking.gameserver.encoders.GameServerMessageEncoder; +import com.eu.habbo.networking.gameserver.encoders.GameServerMessageLogger; +import com.eu.habbo.networking.gameserver.handlers.IdleTimeoutHandler; +import com.eu.habbo.networking.gameserver.handlers.WebSocketHttpHandler; +import com.eu.habbo.networking.gameserver.ssl.SSLCertificateLoader; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; + +import javax.net.ssl.SSLEngine; + +public class WebSocketChannelInitializer extends ChannelInitializer { + private static final int MAX_FRAME_SIZE = 500000; + + private final SslContext sslContext; + private final boolean sslEnabled; + private final WebSocketServerProtocolConfig wsConfig; + + public WebSocketChannelInitializer() { + this.sslContext = SSLCertificateLoader.getContext(); + this.sslEnabled = this.sslContext != null; + this.wsConfig = WebSocketServerProtocolConfig.newBuilder() + .websocketPath("/") + .checkStartsWith(true) + .maxFramePayloadLength(MAX_FRAME_SIZE) + .build(); + } + + @Override + protected void initChannel(SocketChannel ch) { + ch.pipeline().addLast("logger", new LoggingHandler()); + + if (this.sslEnabled) { + SSLEngine engine = this.sslContext.newEngine(ch.alloc()); + ch.pipeline().addLast(new SslHandler(engine)); + } + + ch.pipeline().addLast("httpCodec", new HttpServerCodec()); + ch.pipeline().addLast("httpAggregator", new HttpObjectAggregator(MAX_FRAME_SIZE)); + ch.pipeline().addLast("wsHttpHandler", new WebSocketHttpHandler()); + ch.pipeline().addLast("wsProtocolHandler", new WebSocketServerProtocolHandler(this.wsConfig)); + ch.pipeline().addLast("wsCodec", new WebSocketCodec()); + + // Standard game decoders + ch.pipeline().addLast(new GamePolicyDecoder()); + ch.pipeline().addLast(new GameByteFrameDecoder()); + ch.pipeline().addLast(new GameByteDecoder()); + + if (PacketManager.DEBUG_SHOW_PACKETS) { + ch.pipeline().addLast(new GameClientMessageLogger()); + } + + ch.pipeline().addLast("idleEventHandler", new IdleTimeoutHandler(30, 60)); + ch.pipeline().addLast(new GameMessageRateLimit()); + ch.pipeline().addLast(new GameMessageHandler()); + + // Encoders + ch.pipeline().addLast("messageEncoder", new GameServerMessageEncoder()); + + if (PacketManager.DEBUG_SHOW_PACKETS) { + ch.pipeline().addLast(new GameServerMessageLogger()); + } + } + + public boolean isSslEnabled() { + return this.sslEnabled; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/codec/WebSocketCodec.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/codec/WebSocketCodec.java new file mode 100644 index 00000000..f5dd7f01 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/codec/WebSocketCodec.java @@ -0,0 +1,21 @@ +package com.eu.habbo.networking.gameserver.codec; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageCodec; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; + +import java.util.List; + +public class WebSocketCodec extends MessageToMessageCodec { + @Override + protected void encode(ChannelHandlerContext ctx, ByteBuf in, List out) { + out.add(new BinaryWebSocketFrame(in).retain()); + } + + @Override + protected void decode(ChannelHandlerContext ctx, WebSocketFrame in, List out) { + out.add(in.content().retain()); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/handlers/WebSocketHttpHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/handlers/WebSocketHttpHandler.java new file mode 100644 index 00000000..d4753f6a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/handlers/WebSocketHttpHandler.java @@ -0,0 +1,84 @@ +package com.eu.habbo.networking.gameserver.handlers; + +import com.eu.habbo.Emulator; +import com.eu.habbo.networking.gameserver.GameServerAttributes; +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 java.net.URI; + +public class WebSocketHttpHandler extends ChannelInboundHandlerAdapter { + private static final String ORIGIN_HEADER = "Origin"; + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof HttpMessage) { + if (!handleHttpRequest(ctx, (HttpMessage) msg)) { + ReferenceCountUtil.release(msg); + return; + } + } + super.channelRead(ctx, msg); + ctx.pipeline().remove(this); + } + + private boolean handleHttpRequest(ChannelHandlerContext ctx, HttpMessage req) { + String origin = "error"; + + try { + if (req.headers().contains(ORIGIN_HEADER)) { + origin = getDomainNameFromUrl(req.headers().get(ORIGIN_HEADER)); + } + } catch (Exception ignored) { + } + + String whitelist = Emulator.getConfig().getValue("ws.whitelist", "localhost"); + if (!isWhitelisted(origin, whitelist.split(","))) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.FORBIDDEN, + Unpooled.wrappedBuffer("Origin forbidden".getBytes()) + ); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + return false; + } + + String ipHeader = Emulator.getConfig().getValue("ws.ip.header", ""); + if (!ipHeader.isEmpty() && req.headers().contains(ipHeader)) { + String ip = req.headers().get(ipHeader); + ctx.channel().attr(GameServerAttributes.WS_IP).set(ip); + } + + return true; + } + + private static String getDomainNameFromUrl(String url) throws Exception { + URI uri = new URI(url); + String domain = uri.getHost(); + return domain.startsWith("www.") ? domain.substring(4) : domain; + } + + private static boolean isWhitelisted(String toCheck, String[] whitelist) { + for (String entry : whitelist) { + String trimmed = entry.trim(); + if (trimmed.equals("*")) { + return true; + } + if (trimmed.startsWith("*")) { + String suffix = trimmed.substring(1); + if (toCheck.endsWith(suffix) || ("." + toCheck).equals(suffix)) { + return true; + } + } else { + if (toCheck.equals(trimmed)) { + return true; + } + } + } + return false; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/ssl/SSLCertificateLoader.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/ssl/SSLCertificateLoader.java new file mode 100644 index 00000000..f653bd76 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/ssl/SSLCertificateLoader.java @@ -0,0 +1,32 @@ +package com.eu.habbo.networking.gameserver.ssl; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; + +public class SSLCertificateLoader { + private static final String SSL_PATH = "ssl"; + private static final Logger LOGGER = LoggerFactory.getLogger(SSLCertificateLoader.class); + + public static SslContext getContext() { + try { + File certFile = new File(SSL_PATH + File.separator + "cert.pem"); + File keyFile = new File(SSL_PATH + File.separator + "privkey.pem"); + + if (!certFile.exists() || !keyFile.exists()) { + LOGGER.debug("SSL certificates not found in '{}' directory, WSS disabled", SSL_PATH); + return null; + } + + SslContext context = SslContextBuilder.forServer(certFile, keyFile).build(); + LOGGER.info("SSL certificates loaded successfully, WSS enabled"); + return context; + } catch (Exception e) { + LOGGER.warn("Failed to load SSL certificates: {}", e.getMessage()); + return null; + } + } +} diff --git a/Latest_Compiled_Version/config.ini.example b/Latest_Compiled_Version/config.ini.example new file mode 100644 index 00000000..1d05c91c --- /dev/null +++ b/Latest_Compiled_Version/config.ini.example @@ -0,0 +1,35 @@ +#Database Configuration. +db.hostname=localhost +db.port=3306 +db.database=ms +db.username=root +db.password=root +db.params= +db.pool.minsize=25 +db.pool.maxsize=100 + +#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 +game.port=3000 + +#RCON Configuration. +#RCON Host IP. Leave this at 127.0.0.1 if you're running your website on the same server as the emulator. +rcon.host=127.0.0.1 +rcon.port=3001 +rcon.allowed=127.0.0.1;127.0.0.2 + +#WebSocket Configuration (for Nitro) +#Set ws.enabled to true to enable WebSocket connections. +ws.enabled=false +ws.host=0.0.0.0 +ws.port=2096 +#Comma-separated whitelist of allowed origins. Supports wildcards: *.example.com, * (allow all) +ws.whitelist=localhost +#Header name for real client IP when behind a proxy (e.g., X-Forwarded-For, CF-Connecting-IP). Leave empty if not using a proxy. +ws.ip.header= + +enc.enabled=false +enc.e=3 +enc.n=86851dd364d5c5cece3c883171cc6ddc5760779b992482bd1e20dd296888df91b33b936a7b93f06d29e8870f703a216257dec7c81de0058fea4cc5116f75e6efc4e9113513e45357dc3fd43d4efab5963ef178b78bd61e81a14c603b24c8bcce0a12230b320045498edc29282ff0603bc7b7dae8fc1b05b52b2f301a9dc783b7 +enc.d=59ae13e243392e89ded305764bdd9e92e4eafa67bb6dac7e1415e8c645b0950bccd26246fd0d4af37145af5fa026c0ec3a94853013eaae5ff1888360f4f9449ee023762ec195dff3f30ca0b08b8c947e3859877b5d7dced5c8715c58b53740b84e11fbc71349a27c31745fcefeeea57cff291099205e230e0c7c27e8e1c0512b \ No newline at end of file