Merge branch 'dev' into 'master'

Add native SSL and method for getting real IP

See merge request nitro/ms-websockets!2
This commit is contained in:
skeletor 2021-01-29 07:27:32 +00:00
commit 584e9be0de
9 changed files with 156 additions and 82 deletions

View File

@ -1,15 +1,23 @@
# NitroWebsockets
## What is NitroWebsockets? ##
NitroWebsockets is a plugin that adds Nitro HTML5 Client compatibility to any hotel running Arcturus MS 2.5.0 and above.
NitroWebsockets is a plugin that adds Nitro HTML5 Client compatibility to any hotel running Arcturus MS 3.0.0 and above.
## How do I configure the plugin?
Startup the plugin so it generates the required entries under your `emulator_settings` table. The following fields will be generated:
- `websockets.whitelist` - a comma-delimited list containing all permitted Origin headers. You should write the domain name of your hotel here, since the Websocket connection will be initiated there. Wildcards are also supported, so you can whitelist all subdomains by adding for example: `*.example.com`, or even whitelist all origins by adding `*` (not recommended)
- `ws.nitro.host` - host ip, should leave it as 0.0.0.0
- `ws.nitro.port` - host port, can be any port but if you want to proxy wss traffic with Cloudflare read the following section
- `ws.nitro.ip.header` - header that will be used for obtaining the user's real ip address if server is behind a proxy. Will most likely be needed to be set to `X-Forwarded-For` or `CF-Connecting-IP` if behind Cloudflare.
## How do I connect to my emulator using Secure Websockets (wss)? ##
You have several options to add WSS support to your websocket server.
- You can add your certificate and key file to the path `/ssl/cert.pem` and `/ssl/privkey.pem` to add WSS support directly to the server **Note**:The client will not accept self-signed certificates, you must use a certificate signed by a CA (you can get one for free from letsencrypt.org)
- or you can proxy WSS with either cloudflare or nginx. **Note**: Adding a proxy means that you will have to configure `ws.nitro.ip.header` so that the plugin is able to get the player's real ip address, and not the IP address of the proxy.
### Proxying WSS with Cloudflare
You can easily proxy wss traffic using Cloudflare. However, you should first make sure that your `ws.nitro.port` is set to one that is listed as HTTPS Cloudflare Compatible in the following link:
https://support.cloudflare.com/hc/en-us/articles/200169156-Which-ports-will-Cloudflare-work-with-

View File

@ -24,7 +24,7 @@
<dependency>
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>2.5.0</version>
<version>3.0.0</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,22 @@
package org.krews.plugin.nitro;
import java.net.URI;
public class Utils {
public static String getDomainNameFromUrl(String url) throws Exception {
URI uri = new URI(url);
String domain = uri.getHost();
return domain.startsWith("www.") ? domain.substring(4) : domain;
}
public static boolean isWhitelisted(String toCheck, String[] whitelist) {
for(String whitelistEntry : whitelist) {
if(whitelistEntry.startsWith("*")) {
if(toCheck.endsWith(whitelistEntry.substring(1)) || ("." + toCheck).equals(whitelistEntry.substring(1))) return true;
} else {
if(toCheck.equals(whitelistEntry)) return true;
}
}
return false;
}
}

View File

@ -6,12 +6,16 @@ import com.eu.habbo.plugin.EventHandler;
import com.eu.habbo.plugin.EventListener;
import com.eu.habbo.plugin.HabboPlugin;
import com.eu.habbo.plugin.events.emulator.EmulatorLoadedEvent;
import com.eu.habbo.plugin.events.users.UserGetIPAddressEvent;
import io.netty.channel.Channel;
import io.netty.util.AttributeKey;
import org.krews.plugin.nitro.websockets.NetworkChannelInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class main extends HabboPlugin implements EventListener {
private static final Logger LOGGER = LoggerFactory.getLogger(main.class);
public static final AttributeKey<String> WS_IP = AttributeKey.valueOf("WS_IP");
public void onEnable() throws Exception {
Emulator.getPluginManager().registerEvents(this, this);
@ -34,12 +38,25 @@ public class main extends HabboPlugin implements EventListener {
Emulator.getConfig().register("websockets.whitelist", "localhost");
Emulator.getConfig().register("ws.nitro.host", "0.0.0.0");
Emulator.getConfig().register("ws.nitro.port", "2096");
Emulator.getConfig().register("ws.nitro.ip.header", "");
Emulator.getGameServer().getServerBootstrap().childHandler(new NetworkChannelInitializer());
NetworkChannelInitializer wsChannelHandler = new NetworkChannelInitializer();
Emulator.getGameServer().getServerBootstrap().childHandler(wsChannelHandler);
Emulator.getGameServer().getServerBootstrap().bind(Emulator.getConfig().getValue("ws.nitro.host", "0.0.0.0"), Emulator.getConfig().getInt("ws.nitro.port", 2096)).sync();
LOGGER.info("OFFICIAL PLUGIN - Nitro Websockets has started!");
LOGGER.info("Nitro Websockets Listening on " + Emulator.getConfig().getValue("ws.nitro.host", "0.0.0.0") + ":" + Emulator.getConfig().getInt("ws.nitro.port", 2096));
LOGGER.info("Nitro Websockets Listening on " + (wsChannelHandler.isSSL() ? "wss://" : "ws://") + Emulator.getConfig().getValue("ws.nitro.host", "0.0.0.0") + ":" + Emulator.getConfig().getInt("ws.nitro.port", 2096));
}
@EventHandler
public void onUserGetIPEvent(UserGetIPAddressEvent e) {
Channel channel = e.habbo.getClient().getChannel();
if(channel != null && channel.hasAttr(main.WS_IP)) {
String ip = channel.attr(main.WS_IP).get();
if(!ip.isEmpty()) {
e.setUpdatedIp(ip);
}
}
}
}

View File

@ -5,13 +5,26 @@ 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 org.krews.plugin.nitro.websockets.handlers.MessageInterceptorHandler;
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.WebSocketServerProtocolHandler;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.timeout.IdleStateHandler;
import org.krews.plugin.nitro.websockets.codec.WebSocketCodec;
import org.krews.plugin.nitro.websockets.handlers.CustomHTTPHandler;
import org.krews.plugin.nitro.websockets.ssl.SSLCertificateLoader;
public class NetworkChannelInitializer extends ChannelInitializer<SocketChannel> {
private final SslContext context;
private final boolean isSSL;
public NetworkChannelInitializer() {
context = SSLCertificateLoader.getContext();
isSSL = context != null;
}
@Override
public void initChannel(SocketChannel ch) {
@ -20,7 +33,14 @@ public class NetworkChannelInitializer extends ChannelInitializer<SocketChannel>
ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(60, 30, 0));
ch.pipeline().addAfter("idleStateHandler", "idleEventHandler", new IdleTimeoutHandler());
ch.pipeline().addLast("messageInterceptor", new MessageInterceptorHandler());
if(isSSL) {
ch.pipeline().addLast(context.newHandler(ch.alloc()));
}
ch.pipeline().addLast("httpCodec", new HttpServerCodec());
ch.pipeline().addLast("objectAggregator", new HttpObjectAggregator(65536));
ch.pipeline().addLast("customHttpHandler", new CustomHTTPHandler());
ch.pipeline().addLast("protocolHandler", new WebSocketServerProtocolHandler("/", true));
ch.pipeline().addLast("websocketCodec", new WebSocketCodec());
// Decoders.
ch.pipeline().addLast(new GamePolicyDecoder());
@ -41,4 +61,8 @@ public class NetworkChannelInitializer extends ChannelInitializer<SocketChannel>
ch.pipeline().addLast(new GameServerMessageLogger());
}
}
public boolean isSSL() {
return isSSL;
}
}

View File

@ -1,23 +1,14 @@
package org.krews.plugin.nitro.websockets.codec;
import com.eu.habbo.Emulator;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import io.netty.handler.codec.http.websocketx.*;
import org.krews.plugin.nitro.main;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
public class WebSocketCodec extends MessageToMessageCodec<WebSocketFrame, ByteBuf> {
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketCodec.class);
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
out.add(new BinaryWebSocketFrame(in).retain());
@ -27,43 +18,5 @@ public class WebSocketCodec extends MessageToMessageCodec<WebSocketFrame, ByteBu
protected void decode(ChannelHandlerContext ctx, WebSocketFrame in, List<Object> out) {
out.add(in.content().retain());
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// only allow websockets connections from the whitelist
if(evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
WebSocketServerProtocolHandler.HandshakeComplete handshake = (WebSocketServerProtocolHandler.HandshakeComplete)evt;
String origin = getDomainName(handshake.requestHeaders().get("Origin"));
if(!isWhitelisted(origin)) {
ctx.channel().writeAndFlush(new CloseWebSocketFrame(403, "Origin forbidden")).addListener(ChannelFutureListener.CLOSE);
}
}
else {
super.userEventTriggered(ctx, evt);
}
}
public static String getDomainName(String url) throws URISyntaxException {
URI uri = new URI(url);
String domain = uri.getHost();
return domain.startsWith("www.") ? domain.substring(4) : domain;
}
public static boolean isWhitelisted(String origin) {
String[] allowedOrigins = Emulator.getConfig().getValue("websockets.whitelist", "localhost").split(",");
for(String entry : allowedOrigins) {
if(entry.startsWith("*")) {
if(origin.endsWith(entry.substring(1)) || ("." + origin).equals(entry.substring(1))) return true;
} else {
if(origin.equals(entry)) return true;
}
}
if(Emulator.getConfig().getBoolean("debug.mode")) {
LOGGER.info("Origin not allowed: " + origin);
}
return false;
}
}

View File

@ -0,0 +1,53 @@
package org.krews.plugin.nitro.websockets.handlers;
import com.eu.habbo.Emulator;
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.krews.plugin.nitro.Utils;
import org.krews.plugin.nitro.main;
public class CustomHTTPHandler extends ChannelInboundHandlerAdapter {
private static final String ORIGIN_HEADER = "Origin";
//private static final String FORWARD_PROXY_REALIP = "X-Forwarded-For";
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof HttpMessage) {
if(!handleHttpRequest(ctx, (HttpMessage) msg))
{
ReferenceCountUtil.release(msg);//discard message
return;
}
}
super.channelRead(ctx, msg);
ctx.pipeline().remove(this);
}
public boolean handleHttpRequest(ChannelHandlerContext ctx, HttpMessage req) {
String origin = "error";
try {
if(req.headers().contains(ORIGIN_HEADER)) {
origin = Utils.getDomainNameFromUrl(req.headers().get(ORIGIN_HEADER));
}
} catch (Exception ignored) { }
if(!Utils.isWhitelisted(origin, Emulator.getConfig().getValue("websockets.whitelist", "localhost").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 header = Emulator.getConfig().getValue("ws.nitro.ip.header", "");
if(!header.isEmpty() && req.headers().contains(header)) {
String ip = req.headers().get(header);
ctx.channel().attr(main.WS_IP).set(ip);
}
return true;
}
}

View File

@ -1,27 +0,0 @@
package org.krews.plugin.nitro.websockets.handlers;
import org.krews.plugin.nitro.websockets.codec.WebSocketCodec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.util.CharsetUtil;
import java.util.List;
public class MessageInterceptorHandler extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if(in.toString(CharsetUtil.UTF_8).startsWith("GET")) {
// this is a websocket upgrade request, so add the appropriate decoders/encoders
ctx.pipeline().addAfter("messageInterceptor", "websocketCodec", new WebSocketCodec());
ctx.pipeline().addAfter("messageInterceptor", "protocolHandler", new WebSocketServerProtocolHandler("/", true));
ctx.pipeline().addAfter("messageInterceptor", "objectAggregator", new HttpObjectAggregator(65536));
ctx.pipeline().addAfter("messageInterceptor", "httpCodec", new HttpServerCodec());
}
// Remove ourselves
ctx.pipeline().remove(this);
}
}

View File

@ -0,0 +1,24 @@
package org.krews.plugin.nitro.websockets.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 filePath = "ssl";
private static final Logger LOGGER = LoggerFactory.getLogger(SSLCertificateLoader.class);
public static SslContext getContext() {
SslContext context;
try {
context = SslContextBuilder.forServer(new File( filePath + File.separator + "cert.pem" ), new File( filePath + File.separator + "privkey.pem" )).build();
} catch ( Exception e ) {
LOGGER.info("Unable to load ssl: " + e.getMessage());
context = null;
}
return context;
}
}