wrote a signaling server, untested, needs ratelimiting

This commit is contained in:
LAX1DUDE 2022-08-07 23:46:13 -07:00
parent e93e53541e
commit 0b2a351278
22 changed files with 1888 additions and 0 deletions

Binary file not shown.

View File

@ -0,0 +1,63 @@
package net.lax1dude.eaglercraft.sp.relay;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
public class ByteBufferInputStream extends InputStream {
private final ByteBuffer buffer;
public ByteBufferInputStream(ByteBuffer buf) {
buffer = buf;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int max = buffer.remaining();
if(len > max) {
len = max;
}
buffer.get(b, off, len);
return len;
}
@Override
public int read() throws IOException {
if(buffer.remaining() == 0) {
return -1;
}else {
return (int)buffer.get() & 0xFF;
}
}
@Override
public long skip(long n) throws IOException {
int max = buffer.remaining();
if(n > max) {
n = (int)max;
}
return max;
}
@Override
public int available() throws IOException {
return buffer.remaining();
}
@Override
public synchronized void mark(int readlimit) {
buffer.mark();
}
@Override
public synchronized void reset() throws IOException {
buffer.reset();
}
@Override
public boolean markSupported() {
return true;
}
}

View File

@ -0,0 +1,8 @@
package net.lax1dude.eaglercraft.sp.relay;
public class Constants {
public static final String versionName = "0.1a";
public static final int protocolVersion = 1;
}

View File

@ -0,0 +1,275 @@
/*
* Copyright (c) 2022 Calder Young. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
package net.lax1dude.eaglercraft.sp.relay;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class DebugLogger {
private static Level debugLoggingLevel = Level.INFO;
public static void enableDebugLogging(Level level) {
if(level == null) {
level = Level.NONE;
}
debugLoggingLevel = level;
}
public static boolean debugLoggingEnabled() {
return debugLoggingLevel != Level.NONE;
}
private static final Map<String,DebugLogger> loggers = new HashMap();
public static DebugLogger getLogger(String name) {
DebugLogger ret = loggers.get(name);
if(ret == null) {
ret = new DebugLogger(name);
loggers.put(name, ret);
}
return ret;
}
private final String name;
private DebugLogger(String name) {
this.name = name;
}
public static enum Level {
NONE("NONE", 0, System.out), DEBUG("DEBUG", 4, System.out), INFO("INFO", 3, System.out),
WARN("WARN", 2, System.err), ERROR("ERROR", 1, System.err);
public final String label;
public final int level;
public final PrintStream output;
private Level(String label, int level, PrintStream output) {
this.label = label;
this.level = level;
this.output = output;
}
}
private class LogStream extends OutputStream {
private final Level logLevel;
private final ByteArrayOutputStream lineBuffer = new ByteArrayOutputStream();
private LogStream(Level logLevel) {
this.logLevel = logLevel;
}
@Override
public void write(int b) throws IOException {
if(b == (int)'\r') {
return;
}else if(b == (int)'\n') {
byte[] line = lineBuffer.toByteArray();
lineBuffer.reset();
log(logLevel, new String(line, StandardCharsets.UTF_8));
}else {
lineBuffer.write(b);
}
}
}
private OutputStream infoOutputStream = null;
private PrintStream infoPrintStream = null;
private OutputStream warnOutputStream = null;
private PrintStream warnPrintStream = null;
private OutputStream errorOutputStream = null;
private PrintStream errorPrintStream = null;
public OutputStream getOutputStream(Level lvl) {
switch(lvl) {
case INFO:
default:
if(infoOutputStream == null) {
infoOutputStream = new LogStream(Level.INFO);
}
return infoOutputStream;
case WARN:
if(warnOutputStream == null) {
warnOutputStream = new LogStream(Level.WARN);
}
return warnOutputStream;
case ERROR:
if(errorOutputStream == null) {
errorOutputStream = new LogStream(Level.ERROR);
}
return errorOutputStream;
}
}
public PrintStream getPrintStream(Level lvl) {
switch(lvl) {
case INFO:
default:
if(infoPrintStream == null) {
infoPrintStream = new PrintStream(getOutputStream(Level.INFO));
}
return infoPrintStream;
case WARN:
if(warnPrintStream == null) {
warnPrintStream = new PrintStream(getOutputStream(Level.WARN));
}
return warnPrintStream;
case ERROR:
if(errorPrintStream == null) {
errorPrintStream = new PrintStream(getOutputStream(Level.ERROR));
}
return errorPrintStream;
}
}
private static final SimpleDateFormat fmt = new SimpleDateFormat("hh:mm:ss+SSS");
private final Date dateInstance = new Date();
public static String formatParams(String msg, Object... params) {
if(params.length > 0) {
StringBuilder builtString = new StringBuilder();
for(int i = 0; i < params.length; ++i) {
int idx = msg.indexOf("{}");
if(idx != -1) {
builtString.append(msg.substring(0, idx));
if(params[i] instanceof InetSocketAddress) {
params[i] = Util.sock2String((InetSocketAddress)params[i]);
}
builtString.append(params[i]);
msg = msg.substring(idx + 2);
}else {
break;
}
}
builtString.append(msg);
return builtString.toString();
}else {
return msg;
}
}
public void log(Level lvl, String msg, Object... params) {
if(debugLoggingLevel.level >= lvl.level) {
synchronized(this) {
dateInstance.setTime(System.currentTimeMillis());
System.out.println("[" + fmt.format(dateInstance) + "][" + Thread.currentThread().getName() + "/" + lvl.label + "][" + name + "]: " +
(params.length == 0 ? msg : formatParams(msg, params)));
}
}
}
public void log(Level lvl, Throwable stackTrace) {
stackTrace.printStackTrace(getPrintStream(lvl));
}
public void debug(String msg) {
if(debugLoggingLevel.level >= Level.DEBUG.level) {
log(Level.DEBUG, msg);
}
}
public void debug(String msg, Object... params) {
if(debugLoggingLevel.level >= Level.DEBUG.level) {
log(Level.DEBUG, msg, params);
}
}
public void debug(Throwable t) {
if(debugLoggingLevel.level >= Level.DEBUG.level) {
log(Level.DEBUG, t);
}
}
public void info(String msg) {
if(debugLoggingLevel.level >= Level.INFO.level) {
log(Level.INFO, msg);
}
}
public void info(String msg, Object... params) {
if(debugLoggingLevel.level >= Level.INFO.level) {
log(Level.INFO, msg, params);
}
}
public void info(Throwable t) {
if(debugLoggingLevel.level >= Level.INFO.level) {
log(Level.INFO, t);
}
}
public void warn(String msg) {
if(debugLoggingLevel.level >= Level.WARN.level) {
log(Level.WARN, msg);
}
}
public void warn(String msg, Object... params) {
if(debugLoggingLevel.level >= Level.WARN.level) {
log(Level.WARN, msg, params);
}
}
public void warn(Throwable t) {
if(debugLoggingLevel.level >= Level.WARN.level) {
log(Level.WARN, t);
}
}
public void error(String msg) {
if(debugLoggingLevel.level >= Level.ERROR.level) {
log(Level.ERROR, msg);
}
}
public void error(String msg, Object... params) {
if(debugLoggingLevel.level >= Level.ERROR.level) {
log(Level.ERROR, msg, params);
}
}
public void error(Throwable t) {
if(debugLoggingLevel.level >= Level.ERROR.level) {
log(Level.ERROR, t);
}
}
}

View File

@ -0,0 +1,119 @@
package net.lax1dude.eaglercraft.sp.relay;
import java.io.IOException;
import java.util.Random;
import org.java_websocket.WebSocket;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket04Description;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket03ICECandidate;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket05ClientSuccess;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket06ClientFailure;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacketFEDisconnectClient;
public class EaglerSPClient {
public final WebSocket socket;
public final EaglerSPServer server;
public final String id;
public final long createdOn;
public boolean serverNotifiedOfClose = false;
public LoginState state = LoginState.INIT;
EaglerSPClient(WebSocket sock, EaglerSPServer srv, String id) {
this.socket = sock;
this.server = srv;
this.id = id;
this.createdOn = System.currentTimeMillis();
}
public void send(IPacket packet) {
if(this.socket.isOpen()) {
try {
this.socket.send(IPacket.writePacket(packet));
}catch(IOException ex) {
EaglerSPRelay.logger.debug("Error sending data to {}", this.socket.getRemoteSocketAddress());
EaglerSPRelay.logger.debug(ex);
disconnect(IPacketFEDisconnectClient.TYPE_INTERNAL_ERROR, "Internal Server Error");
this.socket.close();
}
}else {
EaglerSPRelay.logger.debug("WARNING: Tried to send data to {} after the connection closed.", this.socket.getRemoteSocketAddress());
}
}
public boolean handle(IPacket packet) throws IOException {
if(packet instanceof IPacket03ICECandidate) {
if(LoginState.assertEquals(this, LoginState.INIT)) {
state = LoginState.SENT_ICE_CANDIDATE;
server.handleClientICECandidate(this, (IPacket03ICECandidate)packet);
EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x03: ICECandidate", socket.getRemoteSocketAddress());
}
return true;
}else if(packet instanceof IPacket04Description) {
if(LoginState.assertEquals(this, LoginState.RECIEVED_ICE_CANIDATE)) {
state = LoginState.SENT_DESCRIPTION;
server.handleClientDescription(this, (IPacket04Description)packet);
EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x04: Description", socket.getRemoteSocketAddress());
}
return true;
}else if(packet instanceof IPacket05ClientSuccess) {
if(LoginState.assertEquals(this, LoginState.RECIEVED_DESCRIPTION)) {
state = LoginState.FINISHED;
server.handleClientSuccess(this, (IPacket05ClientSuccess)packet);
EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x05: ClientSuccess", socket.getRemoteSocketAddress());
disconnect(IPacketFEDisconnectClient.TYPE_FINISHED_SUCCESS, "Successful connection");
}
return true;
}else if(packet instanceof IPacket06ClientFailure) {
if(LoginState.assertEquals(this, LoginState.RECIEVED_DESCRIPTION)) {
state = LoginState.FINISHED;
server.handleClientFailure(this, (IPacket06ClientFailure)packet);
EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x05: ClientFailure", socket.getRemoteSocketAddress());
disconnect(IPacketFEDisconnectClient.TYPE_FINISHED_FAILED, "Failed connection");
}
return true;
}else {
return false;
}
}
public void handleServerICECandidate(IPacket03ICECandidate desc) {
send(new IPacket03ICECandidate("", desc.candidate));
}
public void handleServerDescription(IPacket04Description desc) {
send(new IPacket04Description("", desc.description));
}
public void handleServerDisconnectClient(IPacketFEDisconnectClient packet) {
disconnect(packet.code, packet.reason);
}
public void disconnect(int code, String reason) {
IPacket pkt = new IPacketFEDisconnectClient(id, code, reason);
if(!serverNotifiedOfClose) {
server.send(pkt);
serverNotifiedOfClose = true;
}
if(this.socket.isOpen()) {
send(pkt);
socket.close();
}
EaglerSPRelay.logger.debug("[{}][Relay -> Client] PKT 0xFE: #{} {}", socket.getRemoteSocketAddress(), code, reason);
}
public static final int clientCodeLength = 16;
private static final String clientCodeChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static String generateClientId() {
Random r = new Random();
char[] ret = new char[clientCodeLength];
for(int i = 0; i < ret.length; ++i) {
ret[i] = clientCodeChars.charAt(r.nextInt(clientCodeChars.length()));
}
return new String(ret);
}
}

View File

@ -0,0 +1,313 @@
package net.lax1dude.eaglercraft.sp.relay;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
import net.lax1dude.eaglercraft.sp.relay.pkt.*;
public class EaglerSPRelay extends WebSocketServer {
public static EaglerSPRelay instance;
public static final EaglerSPRelayConfig config = new EaglerSPRelayConfig();
public static final DebugLogger logger = DebugLogger.getLogger("EaglerSPRelay");
public static void main(String[] args) throws IOException, InterruptedException {
for(int i = 0; i < args.length; ++i) {
if(args[i].equalsIgnoreCase("--debug")) {
DebugLogger.enableDebugLogging(DebugLogger.Level.DEBUG);
logger.debug("Debug logging enabled");
}
}
logger.info("Starting EaglerSPRelay version {}...", Constants.versionName);
config.load(new File("relayConfig.ini"));
EaglerSPRelayConfigRelayList.loadRelays(new File("relays.txt"));
logger.info("Starting WebSocket Server...");
instance = new EaglerSPRelay(new InetSocketAddress(config.getAddress(), config.getPort()));
instance.setConnectionLostTimeout(20);
instance.setReuseAddr(true);
instance.start();
Thread tickThread = new Thread((() -> {
while(true) {
try {
long millis = System.currentTimeMillis();
synchronized(pendingConnections) {
Iterator<Entry<WebSocket,Long>> itr = pendingConnections.entrySet().iterator();
while(itr.hasNext()) {
Entry<WebSocket,Long> etr = itr.next();
if(millis - etr.getValue().longValue() > 1000l) {
etr.getKey().close();
itr.remove();
}
}
}
synchronized(clientConnections) {
Iterator<EaglerSPClient> itr = clientConnections.values().iterator();
while(itr.hasNext()) {
EaglerSPClient cl = itr.next();
if(millis - cl.createdOn > 6000l) {
cl.disconnect(IPacketFEDisconnectClient.TYPE_TIMEOUT, "Took too long to connect!");
}
}
}
}catch(Throwable t) {
logger.error("Error in update loop!");
logger.error(t);
}
Util.sleep(50l);
}
}), "Relay Tick");
tickThread.setDaemon(true);
tickThread.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String s;
while((s = reader.readLine()) != null) {
s = s.trim();
if(s.equalsIgnoreCase("stop") || s.equalsIgnoreCase("end")) {
logger.info("Shutting down...");
instance.stop();
System.exit(0);
}else {
logger.info("Unknown command: {}", s);
logger.info("Type 'stop' to exit, 'reload' to reload config");
}
}
}
private EaglerSPRelay(InetSocketAddress addr) {
super(addr);
}
private static final Map<WebSocket,Long> pendingConnections = new HashMap();
private static final Map<String,EaglerSPClient> clientIds = new HashMap();
private static final Map<WebSocket,EaglerSPClient> clientConnections = new HashMap();
private static final Map<String,EaglerSPServer> serverCodes = new HashMap();
private static final Map<WebSocket,EaglerSPServer> serverConnections = new HashMap();
@Override
public void onStart() {
logger.info("Listening on {}", getAddress());
logger.info("Type 'stop' to exit");
}
@Override
public void onOpen(WebSocket arg0, ClientHandshake arg1) {
synchronized(pendingConnections) {
logger.debug("[{}]: Connection opened", arg0.getRemoteSocketAddress());
pendingConnections.put(arg0, System.currentTimeMillis());
}
}
@Override
public void onMessage(WebSocket arg0, ByteBuffer arg1) {
DataInputStream sid = new DataInputStream(new ByteBufferInputStream(arg1));
boolean waiting;
synchronized(pendingConnections) {
waiting = pendingConnections.remove(arg0) != null;
}
try {
IPacket pkt = IPacket.readPacket(sid);
if(waiting) {
if(pkt instanceof IPacket00Handshake) {
IPacket00Handshake ipkt = (IPacket00Handshake)pkt;
if(ipkt.connectionVersion != Constants.protocolVersion) {
logger.debug("[{}]: Connected with unsupported protocol version: {} (supported "
+ "version: {})", arg0.getRemoteSocketAddress(), Constants.protocolVersion);
if(ipkt.connectionVersion < Constants.protocolVersion) {
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_PROTOCOL_VERSION,
"Outdated Client! (v" + Constants.protocolVersion + " req)")));
}else {
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_PROTOCOL_VERSION,
"Outdated Server! (still on v" + Constants.protocolVersion + ")")));
}
arg0.close();
return;
}
if(ipkt.connectionType == 0x01) {
logger.debug("[{}]: Connected as a server", arg0.getRemoteSocketAddress());
EaglerSPServer srv;
synchronized(serverCodes) {
int j = 0;
String code;
do {
if(++j > 100) {
logger.error("Error: relay is running out of codes!");
logger.error("Closing connection to {}", arg0.getRemoteSocketAddress());
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INTERNAL_ERROR,
"Internal Server Error")));
arg0.close();
return;
}
code = config.generateCode();
}while(serverCodes.containsKey(code));
srv = new EaglerSPServer(arg0, code);
serverCodes.put(code, srv);
ipkt.connectionCode = code;
arg0.send(IPacket.writePacket(ipkt));
logger.debug("[{}][Relay -> Server] PKT 0x00: Assign join code: {}", arg0.getRemoteSocketAddress(), code);
}
synchronized(serverConnections) {
serverConnections.put(arg0, srv);
}
srv.send(new IPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers));
logger.debug("[{}][Relay -> Server] PKT 0x01: Send ICE server list to server", arg0.getRemoteSocketAddress());
}else if(ipkt.connectionType == 0x02) {
String code = ipkt.connectionCode;
logger.debug("[{}]: Connected as a client, requested server code: {}", arg0.getRemoteSocketAddress(), code);
if(code.length() != config.getCodeLength()) {
logger.debug("The code '{}' is invalid because it's the wrong length, disconnecting");
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_CODE_LENGTH,
"The join code is the wrong length, it should be " + config.getCodeLength() + " chars long")));
arg0.close();
}else {
EaglerSPServer srv;
synchronized(serverCodes) {
srv = serverCodes.get(code);
}
if(srv == null) {
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INCORRECT_CODE,
"Invalid code, no LAN world found!")));
arg0.close();
return;
}
String id;
EaglerSPClient cl;
synchronized(clientIds) {
int j = 0;
do {
id = EaglerSPClient.generateClientId();
}while(clientIds.containsKey(id));
cl = new EaglerSPClient(arg0, srv, id);
clientIds.put(id, cl);
ipkt.connectionCode = id;
arg0.send(IPacket.writePacket(ipkt));
srv.handleNewClient(cl);
}
synchronized(clientConnections) {
clientConnections.put(arg0, cl);
}
cl.send(new IPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers));
logger.debug("[{}][Relay -> Client] PKT 0x01: Send ICE server list to client", arg0.getRemoteSocketAddress());
}
}else {
logger.debug("[{}]: Unknown connection type: {}", arg0.getRemoteSocketAddress(), ipkt.connectionType);
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_ILLEGAL_OPERATION,
"Unexpected Init Packet")));
arg0.close();
}
}else {
logger.debug("[{}]: Pending connection did not send a 0x00 packet to identify "
+ "as a client or server", arg0.getRemoteSocketAddress());
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_ILLEGAL_OPERATION,
"Unexpected Init Packet")));
arg0.close();
}
}else {
EaglerSPServer srv;
synchronized(serverConnections) {
srv = serverConnections.get(arg0);
}
if(srv != null) {
if(!srv.handle(pkt)) {
logger.debug("[{}]: Server sent invalid packet: {}", arg0.getRemoteSocketAddress(), pkt.getClass().getSimpleName());
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INVALID_PACKET,
"Invalid Packet Recieved")));
arg0.close();
}
}else {
EaglerSPClient cl;
synchronized(clientConnections) {
cl = clientConnections.get(arg0);
}
if(cl != null) {
if(cl.handle(pkt)) {
logger.debug("[{}]: Client sent invalid packet: {}", arg0.getRemoteSocketAddress(), pkt.getClass().getSimpleName());
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INVALID_PACKET,
"Invalid Packet Recieved")));
arg0.close();
}
}else {
logger.debug("[{}]: Connection has no client/server attached to it!", arg0.getRemoteSocketAddress());
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_ILLEGAL_OPERATION,
"Internal Server Error")));
arg0.close();
}
}
}
}catch(Throwable t) {
logger.error("[{}]: Failed to handle binary frame: {}", arg0.getRemoteSocketAddress(), t);
arg0.close();
}
}
@Override
public void onMessage(WebSocket arg0, String arg1) {
logger.debug("[{}]: Sent a text frame, disconnecting", arg0.getRemoteSocketAddress());
arg0.close();
}
@Override
public void onClose(WebSocket arg0, int arg1, String arg2, boolean arg3) {
EaglerSPServer srv;
synchronized(serverConnections) {
srv = serverConnections.remove(arg0);
}
if(srv != null) {
logger.debug("[{}]: Server closed, code: {}", arg0.getRemoteSocketAddress(), srv.code);
synchronized(serverCodes) {
serverCodes.remove(srv.code);
}
ArrayList<EaglerSPClient> clientList;
synchronized(clientConnections) {
clientList = new ArrayList(clientConnections.values());
}
Iterator<EaglerSPClient> itr = clientList.iterator();
while(itr.hasNext()) {
EaglerSPClient cl = itr.next();
if(cl.server == srv) {
logger.debug("[{}]: Disconnecting client: {} (id: ", cl.socket.getRemoteSocketAddress(), cl.id);
cl.socket.close();
}
}
}else {
EaglerSPClient cl;
synchronized(clientConnections) {
cl = clientConnections.remove(arg0);
}
if(cl != null) {
logger.debug("[{}]: Client closed, id: {}", arg0.getRemoteSocketAddress(), cl.id);
synchronized(clientIds) {
clientIds.remove(cl.id);
}
cl.server.handleClientDisconnect(cl);
}else {
logger.debug("[{}]: Connection Closed: {} ", arg0.getRemoteSocketAddress());
}
}
}
@Override
public void onError(WebSocket arg0, Exception arg1) {
logger.error("[{}]: Exception thrown: {}", arg0.getRemoteSocketAddress(), arg1.toString());
logger.debug(arg1);
arg0.close();
}
}

View File

@ -0,0 +1,298 @@
package net.lax1dude.eaglercraft.sp.relay;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class EaglerSPRelayConfig {
private String address = "0.0.0.0";
private int port = 6699;
private int codeLength = 5;
private String codeChars = "abcdefghijklmnopqrstuvwxyz0123456789$%&*+?!";
private boolean codeMixCase = false;
private int connectionsPerIP = 256;
private boolean rateLimitEnable = false;
private int rateLimitPeriod = 128;
private int rateLimitLimit = 48;
private int rateLimitLockoutLimit = 64;
private int rateLimitLockoutDuration = 600;
private String originWhitelist = "";
private String[] originWhitelistArray = new String[0];
private boolean enableRealIpHeader = false;
public void load(File conf) {
if(!conf.isFile()) {
EaglerSPRelay.logger.info("Creating config file: {}", conf.getAbsoluteFile());
save(conf);
}else {
EaglerSPRelay.logger.info("Loading config file: {}", conf.getAbsoluteFile());
boolean gotPort = false, gotCodeLength = false, gotCodeChars = false;
boolean gotCodeMixCase = false, gotConnectionsPerIP = false;
boolean gotRateLimitEnable = false, gotRateLimitPeriod = false;
boolean gotRateLimitLimit = false, gotRateLimitLockoutLimit = false;
boolean gotRateLimitLockoutDuration = false, gotOriginWhitelist = false;
boolean gotEnableRealIpHeader = false, gotAddress = false;
Throwable t2 = null;
try(BufferedReader reader = new BufferedReader(new FileReader(conf))) {
String s;
while((s = reader.readLine()) != null) {
String[] ss = s.trim().split("=", 2);
if(ss.length == 2) {
if(ss[0].equalsIgnoreCase("port")) {
try {
port = Integer.parseInt(ss[1]);
gotPort = true;
}catch(Throwable t) {
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("address")) {
address = ss[1];
gotAddress = true;
}else if(ss[0].equalsIgnoreCase("code-length")) {
try {
codeLength = Integer.parseInt(ss[1]);
gotCodeLength = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid code-length {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("code-chars")) {
if(ss[1].length() < 2) {
t2 = new IllegalArgumentException("not enough chars");
EaglerSPRelay.logger.warn("Invalid code-chars {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t2);
}else {
codeChars = ss[1];
gotCodeChars = true;
}
}else if(ss[0].equalsIgnoreCase("code-mix-case")) {
try {
codeMixCase = getBooleanValue(ss[1]);
gotCodeMixCase = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid code-mix-case {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("connections-per-ip")) {
try {
connectionsPerIP = Integer.parseInt(ss[1]);
gotConnectionsPerIP = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid connections-per-ip {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("ratelimit-enable")) {
try {
rateLimitEnable = getBooleanValue(ss[1]);
gotRateLimitEnable = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid rate-limit-enable {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("ratelimit-period")) {
try {
rateLimitPeriod = Integer.parseInt(ss[1]);
gotRateLimitPeriod = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid ratelimit-period {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("ratelimit-limit")) {
try {
rateLimitLimit = Integer.parseInt(ss[1]);
gotRateLimitLimit = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid ratelimit-limit {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("ratelimit-lockout-limit")) {
try {
rateLimitLockoutLimit = Integer.parseInt(ss[1]);
gotRateLimitLockoutLimit = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid ratelimit-lockout-limit {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("ratelimit-lockout-duration")) {
try {
rateLimitLockoutDuration = Integer.parseInt(ss[1]);
gotRateLimitLockoutDuration = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid ratelimit-lockout-duration {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}else if(ss[0].equalsIgnoreCase("origin-whitelist")) {
originWhitelist = ss[1];
gotOriginWhitelist = true;
}else if(ss[0].equalsIgnoreCase("enable-real-ip-header")) {
try {
enableRealIpHeader = getBooleanValue(ss[1]);
gotEnableRealIpHeader = true;
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid enable-real-ip-header {} in conf {}", ss[1], conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
break;
}
}
}
}
}catch(IOException t) {
EaglerSPRelay.logger.error("Failed to load config file: {}", conf.getAbsoluteFile());
EaglerSPRelay.logger.error(t);
}catch(Throwable t) {
EaglerSPRelay.logger.warn("Invalid config file: {}", conf.getAbsoluteFile());
EaglerSPRelay.logger.warn(t);
t2 = t;
}
if(t2 != null || !gotPort || !gotCodeLength || !gotCodeChars ||
!gotCodeMixCase || !gotConnectionsPerIP || !gotRateLimitEnable ||
!gotRateLimitPeriod || !gotRateLimitLimit || !gotRateLimitLockoutLimit ||
!gotRateLimitLockoutDuration || !gotOriginWhitelist ||
!gotEnableRealIpHeader || !gotAddress) {
EaglerSPRelay.logger.warn("Updating config file: {}", conf.getAbsoluteFile());
save(conf);
}
String[] splitted = originWhitelist.split(";");
List<String> splittedList = new ArrayList();
for(int i = 0; i < splitted.length; ++i) {
splitted[i] = splitted[i].trim();
if(splitted[i].length() > 0) {
splittedList.add(splitted[i]);
}
}
originWhitelistArray = new String[splittedList.size()];
for(int i = 0; i < originWhitelistArray.length; ++i) {
originWhitelistArray[i] = splittedList.get(i);
}
}
}
public void save(File conf) {
try(PrintWriter w = new PrintWriter(new FileOutputStream(conf))) {
w.println("address=" + address);
w.println("port=" + port);
w.println("code-length=" + codeLength);
w.println("code-chars=" + codeChars);
w.println("code-mix-case=" + codeMixCase);
w.println("connections-per-ip=" + connectionsPerIP);
w.println("ratelimit-enable=" + rateLimitEnable);
w.println("ratelimit-period=" + rateLimitPeriod);
w.println("ratelimit-limit=" + rateLimitLimit);
w.println("ratelimit-lockout-limit=" + rateLimitLockoutLimit);
w.println("ratelimit-lockout-duration=" + rateLimitLockoutDuration);
w.println("origin-whitelist=" + originWhitelist);
w.print("enable-real-ip-header=" + enableRealIpHeader);
}catch(IOException t) {
EaglerSPRelay.logger.error("Failed to write config file: {}", conf.getAbsoluteFile());
EaglerSPRelay.logger.error(t);
}
}
private static boolean getBooleanValue(String str) {
if(str.equalsIgnoreCase("true") || str.equals("1")) {
return true;
}else if(str.equalsIgnoreCase("false") || str.equals("0")) {
return false;
}else {
throw new IllegalArgumentException("Not a boolean: " + str);
}
}
public String getAddress() {
return address;
}
public int getPort() {
return port;
}
public int getCodeLength() {
return codeLength;
}
public String getCodeChars() {
return codeChars;
}
public boolean isCodeMixCase() {
return codeMixCase;
}
public int getConnectionsPerIP() {
return connectionsPerIP;
}
public boolean isRateLimitEnable() {
return rateLimitEnable;
}
public int getRateLimitPeriod() {
return rateLimitPeriod;
}
public int getRateLimitLimit() {
return rateLimitLimit;
}
public int getRateLimitLockoutLimit() {
return rateLimitLockoutLimit;
}
public int getRateLimitLockoutDuration() {
return rateLimitLockoutDuration;
}
public String getOriginWhitelist() {
return originWhitelist;
}
public String[] getOriginWhitelistArray() {
return originWhitelistArray;
}
public boolean isEnableRealIpHeader() {
return enableRealIpHeader;
}
public String generateCode() {
Random r = new Random();
char[] ret = new char[codeLength];
for(int i = 0; i < codeLength; ++i) {
ret[i] = codeChars.charAt(r.nextInt(codeChars.length()));
if(codeMixCase) {
if(r.nextBoolean()) {
ret[i] = Character.toLowerCase(ret[i]);
}else {
ret[i] = Character.toUpperCase(ret[i]);
}
}
}
return new String(ret);
}
}

View File

@ -0,0 +1,161 @@
package net.lax1dude.eaglercraft.sp.relay;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
public class EaglerSPRelayConfigRelayList {
public static enum RelayType {
STUN, TURN;
}
public static class RelayServer {
public final RelayType type;
public final String address;
public final String username;
public final String password;
protected RelayServer(RelayType type, String address, String username, String password) {
this.type = type;
this.address = address;
this.username = username;
this.password = password;
}
protected RelayServer(RelayType type, String address) {
this.type = type;
this.address = address;
this.username = null;
this.password = null;
}
}
public static final Collection<RelayServer> relayServers = new ArrayList();
public static void loadRelays(File list) throws IOException {
ArrayList<RelayServer> loading = new ArrayList();
if(!list.isFile()) {
EaglerSPRelay.logger.info("Creating new {}...", list.getName());
try(InputStream is = EaglerSPRelayConfigRelayList.class.getResourceAsStream("/relays.txt");
FileOutputStream os = new FileOutputStream(list)) {
byte[] buffer = new byte[4096];
int i;
while((i = is.read(buffer)) != -1) {
os.write(buffer, 0, i);
}
}
}
EaglerSPRelay.logger.info("Loading STUN/TURN relays from: {}", list.getName());
RelayType addType = null;
String addAddress = null;
String addUsername = null;
String addPassword = null;
try(BufferedReader reader = new BufferedReader(new FileReader(list))) {
String line;
while((line = reader.readLine()) != null) {
line = line.trim();
if(line.length() == 0) {
continue;
}
boolean isSTUNHead = line.equals("[STUN]");
boolean isTURNHead = line.equals("[TURN]");
if(isSTUNHead || isTURNHead) {
if(addType != null) {
add(list.getName(), loading, addType, addAddress, addUsername, addPassword);
}
addAddress = null;
addUsername = null;
addPassword = null;
addType = null;
}
if(isSTUNHead) {
addType = RelayType.STUN;
}else if(isTURNHead) {
addType = RelayType.TURN;
}else if(line.startsWith("url")) {
int spidx = line.indexOf('=');
if(spidx < 3) {
EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line);
}else {
line = line.substring(spidx).trim();
if(line.length() < 1) {
EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line);
}else {
addAddress = line;
}
}
}else if(line.startsWith("username")) {
int spidx = line.indexOf('=');
if(spidx < 8) {
EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line);
}else {
line = line.substring(spidx).trim();
if(line.length() < 1) {
EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line);
}else {
addUsername = line;
}
}
}else if(line.startsWith("password")) {
int spidx = line.indexOf('=');
if(spidx < 8) {
EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line);
}else {
line = line.substring(spidx).trim();
if(line.length() < 1) {
EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line);
}else {
addPassword = line;
}
}
}else {
EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line);
}
}
}
if(addType != null) {
add(list.getName(), loading, addType, addAddress, addUsername, addPassword);
}
if(loading.size() == 0) {
throw new IOException(list.getName() + ": no servers loaded");
}else {
relayServers.clear();
relayServers.addAll(loading);
}
}
private static void add(String filename, Collection<RelayServer> loading, RelayType type, String url, String user, String pass) {
if(type == RelayType.STUN) {
if(url == null) {
EaglerSPRelay.logger.error("Error: Invalid [STUN] in {}, missing 'url'", filename);
}else {
loading.add(new RelayServer(RelayType.STUN, url));
}
}else if(type == RelayType.TURN) {
if(url == null) {
EaglerSPRelay.logger.error("Error: Invalid [TURN] in {}, missing 'url'", filename);
}else if(user == null) {
EaglerSPRelay.logger.error("Error: Invalid [TURN] in {}, missing 'user' for {}", filename, url);
}else if(pass == null) {
EaglerSPRelay.logger.error("Error: Invalid [TURN] in {}, missing 'pass' for {}", filename, url);
}else {
loading.add(new RelayServer(RelayType.TURN, url, user, pass));
}
}
}
}

View File

@ -0,0 +1,129 @@
package net.lax1dude.eaglercraft.sp.relay;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.java_websocket.WebSocket;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket02NewClient;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket04Description;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket03ICECandidate;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket05ClientSuccess;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket06ClientFailure;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacketFEDisconnectClient;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacketFFErrorCode;
public class EaglerSPServer {
public final WebSocket socket;
public final String code;
public final Map<String,EaglerSPClient> clients;
EaglerSPServer(WebSocket sock, String code) {
this.socket = sock;
this.code = code;
this.clients = new HashMap();
}
public void send(IPacket packet) {
if(this.socket.isOpen()) {
try {
this.socket.send(IPacket.writePacket(packet));
}catch(IOException ex) {
EaglerSPRelay.logger.debug("Error sending data to {}", this.socket.getRemoteSocketAddress());
EaglerSPRelay.logger.debug(ex);
try {
this.socket.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INTERNAL_ERROR,
"Internal Server Error")));
}catch(IOException ex2) {
}
this.socket.close();
}
}else {
EaglerSPRelay.logger.debug("WARNING: Tried to send data to {} after the connection closed.", this.socket.getRemoteSocketAddress());
}
}
public boolean handle(IPacket _packet) throws IOException {
if(_packet instanceof IPacket03ICECandidate) {
IPacket03ICECandidate packet = (IPacket03ICECandidate)_packet;
EaglerSPClient cl = clients.get(packet.peerId);
if(cl != null) {
if(LoginState.assertEquals(cl, LoginState.SENT_ICE_CANDIDATE)) {
cl.state = LoginState.RECIEVED_ICE_CANIDATE;
cl.handleServerICECandidate(packet);
EaglerSPRelay.logger.debug("[{}][Server -> Relay -> Client] PKT 0x03: ICECandidate", cl.socket.getRemoteSocketAddress());
}
}else {
this.socket.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_UNKNOWN_CLIENT,
"Unknown Client ID: " + packet.peerId)));
}
return true;
}else if(_packet instanceof IPacket04Description) {
IPacket04Description packet = (IPacket04Description)_packet;
EaglerSPClient cl = clients.get(packet.peerId);
if(cl != null) {
if(LoginState.assertEquals(cl, LoginState.SENT_DESCRIPTION)) {
cl.state = LoginState.RECIEVED_DESCRIPTION;
cl.handleServerDescription(packet);
EaglerSPRelay.logger.debug("[{}][Server -> Relay -> Client] PKT 0x04: Description", cl.socket.getRemoteSocketAddress());
}
}else {
this.socket.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_UNKNOWN_CLIENT,
"Unknown Client ID: " + packet.peerId)));
}
return true;
}else if(_packet instanceof IPacketFEDisconnectClient) {
IPacketFEDisconnectClient packet = (IPacketFEDisconnectClient)_packet;
EaglerSPClient cl = clients.get(packet.clientId);
if(cl != null) {
cl.handleServerDisconnectClient(packet);
EaglerSPRelay.logger.debug("[{}][Server -> Relay -> Client] PKT 0xFE: Disconnect: {}: {}", cl.socket.getRemoteSocketAddress(),
packet.code, packet.reason);
}else {
this.socket.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_UNKNOWN_CLIENT,
"Unknown Client ID: " + packet.clientId)));
}
return true;
}else {
return false;
}
}
public void handleNewClient(EaglerSPClient client) {
synchronized(clients) {
clients.put(client.id, client);
send(new IPacket02NewClient(client.id));
EaglerSPRelay.logger.debug("[{}][Relay -> Server] PKT 0x02: Notify server of the client, id: {}", socket.getRemoteSocketAddress(), client.id);
}
}
public void handleClientDisconnect(EaglerSPClient client) {
synchronized(clients) {
clients.remove(client.id);
}
if(!client.serverNotifiedOfClose) {
send(new IPacketFEDisconnectClient(client.id, IPacketFEDisconnectClient.TYPE_UNKNOWN, "End of stream"));
client.serverNotifiedOfClose = true;
}
}
public void handleClientICECandidate(EaglerSPClient client, IPacket03ICECandidate packet) {
send(new IPacket03ICECandidate(client.id, packet.candidate));
}
public void handleClientDescription(EaglerSPClient client, IPacket04Description packet) {
send(new IPacket04Description(client.id, packet.description));
}
public void handleClientSuccess(EaglerSPClient client, IPacket05ClientSuccess packet) {
send(new IPacket05ClientSuccess(client.id));
}
public void handleClientFailure(EaglerSPClient client, IPacket06ClientFailure packet) {
send(new IPacket06ClientFailure(client.id));
}
}

View File

@ -0,0 +1,24 @@
package net.lax1dude.eaglercraft.sp.relay;
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacketFEDisconnectClient;
/**
* SENT = Client has sent something to the server<br>
* RECIEVED = Server has sent something to the client
*/
public enum LoginState {
INIT, SENT_ICE_CANDIDATE, RECIEVED_ICE_CANIDATE, SENT_DESCRIPTION, RECIEVED_DESCRIPTION, FINISHED;
public static boolean assertEquals(EaglerSPClient client, LoginState state) {
if(client.state != state) {
String msg = "client is in state " + client.state.name() + " when it was supposed to be " + state.name();
client.disconnect(IPacketFEDisconnectClient.TYPE_INVALID_OPERATION, msg);
EaglerSPRelay.logger.debug("[{}][Relay -> Client] PKT 0xFE: TYPE_INVALID_OPERATION: {}", client.socket.getRemoteSocketAddress(), msg);
return false;
}else {
return true;
}
}
}

View File

@ -0,0 +1,18 @@
package net.lax1dude.eaglercraft.sp.relay;
import java.net.InetSocketAddress;
public class Util {
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
}
}
public static String sock2String(InetSocketAddress sock) {
return sock.getAddress().getHostAddress() + sock.getPort();
}
}

View File

@ -0,0 +1,146 @@
package net.lax1dude.eaglercraft.sp.relay.pkt;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import net.lax1dude.eaglercraft.sp.relay.EaglerSPRelay;
public class IPacket {
private static final Map<Integer,Class<? extends IPacket>> definedPacketClasses = new HashMap();
private static final Map<Class<? extends IPacket>,Integer> definedPacketIds = new HashMap();
private static void register(int id, Class<? extends IPacket> clazz) {
definedPacketClasses.put(id, clazz);
definedPacketIds.put(clazz, id);
}
static {
register(0x00, IPacket00Handshake.class);
register(0x01, IPacket01ICEServers.class);
register(0x02, IPacket02NewClient.class);
register(0x03, IPacket03ICECandidate.class);
register(0x04, IPacket04Description.class);
register(0x05, IPacket05ClientSuccess.class);
register(0x06, IPacket06ClientFailure.class);
register(0xFE, IPacketFEDisconnectClient.class);
register(0xFF, IPacketFFErrorCode.class);
}
public static IPacket readPacket(DataInputStream input) throws IOException {
int i = input.read();
try {
Class<? extends IPacket> clazz = definedPacketClasses.get(i);
if(clazz == null) {
throw new IOException("Unknown packet type: " + i);
}
IPacket pkt = clazz.newInstance();
pkt.read(input);
return pkt;
} catch (InstantiationException | IllegalAccessException e) {
EaglerSPRelay.logger.error("Could not instanciate packet {}", i);
EaglerSPRelay.logger.error(e);
throw new IOException("Unknown packet type: " + i);
}
}
public static byte[] writePacket(IPacket packet) throws IOException {
Integer i = definedPacketIds.get(packet.getClass());
if(i != null) {
int len = packet.packetLength();
ByteArrayOutputStream bao = len == -1 ? new ByteArrayOutputStream() :
new ByteArrayOutputStream(len + 1);
bao.write(i);
packet.write(new DataOutputStream(bao));
byte[] ret = bao.toByteArray();
if(len != -1 && ret.length != len + 1) {
EaglerSPRelay.logger.debug("writePacket buffer for packet {} {} by {} bytes",
packet.getClass().getSimpleName(), len + 1 < ret.length ? "overflowed" :
"underflowed", len + 1 < ret.length ? ret.length - len - 1 : len + 1 - ret.length);
}
return ret;
}else {
throw new IOException("Unknown packet type: " + packet.getClass().getSimpleName());
}
}
public void read(DataInputStream input) throws IOException {
}
public void write(DataOutputStream output) throws IOException {
}
public int packetLength() {
return -1;
}
public static String readASCII(InputStream is, int len) throws IOException {
char[] ret = new char[len];
for(int i = 0; i < len; ++i) {
int j = is.read();
if(j < 0) {
return null;
}
ret[i] = (char)is.read();
}
return new String(ret);
}
public static void writeASCII(OutputStream is, String txt) throws IOException {
for(int i = 0, l = txt.length(); i < l; ++i) {
is.write((int)txt.charAt(i));
}
}
public static String readASCII8(InputStream is) throws IOException {
int i = is.read();
if(i < 0) {
return null;
}else {
return readASCII(is, i);
}
}
public static void writeASCII8(OutputStream is, String txt) throws IOException {
if(txt == null) {
is.write(0);
}else {
int l = txt.length();
is.write(l);
for(int i = 0; i < l; ++i) {
is.write((int)txt.charAt(i));
}
}
}
public static String readASCII16(InputStream is) throws IOException {
int hi = is.read();
int lo = is.read();
if(hi < 0 || lo < 0) {
return null;
}else {
return readASCII(is, (hi << 8) | lo);
}
}
public static void writeASCII16(OutputStream is, String txt) throws IOException {
if(txt == null) {
is.write(0);
is.write(0);
}else {
int l = txt.length();
is.write((l >> 8) & 0xFF);
is.write(l & 0xFF);
for(int i = 0; i < l; ++i) {
is.write((int)txt.charAt(i));
}
}
}
}

View File

@ -0,0 +1,41 @@
package net.lax1dude.eaglercraft.sp.relay.pkt;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public class IPacket00Handshake extends IPacket {
public int connectionType = 0;
public int connectionVersion = 1;
public String connectionCode = null;
public IPacket00Handshake() {
}
public IPacket00Handshake(int connectionType, int connectionVersion,
String connectionCode) {
this.connectionType = connectionType;
this.connectionVersion = connectionVersion;
this.connectionCode = connectionCode;
}
@Override
public void read(DataInputStream input) throws IOException {
connectionType = input.read();
connectionVersion = input.read();
connectionCode = IPacket.readASCII8(input);
}
@Override
public void write(DataOutputStream output) throws IOException {
output.write(connectionType);
IPacket.writeASCII8(output, connectionCode);
}
@Override
public int packetLength() {
return 1 + 1 + (connectionCode != null ? 1 + connectionCode.length() : 0);
}
}

View File

@ -0,0 +1,39 @@
package net.lax1dude.eaglercraft.sp.relay.pkt;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import net.lax1dude.eaglercraft.sp.relay.EaglerSPRelayConfigRelayList.RelayServer;
import net.lax1dude.eaglercraft.sp.relay.EaglerSPRelayConfigRelayList.RelayType;
public class IPacket01ICEServers extends IPacket {
public final Collection<RelayServer> servers;
public IPacket01ICEServers(Collection<RelayServer> servers) {
this.servers = servers;
}
public void write(DataOutputStream output) throws IOException {
int l = servers.size();
output.writeShort(l);
Iterator<RelayServer> itr = servers.iterator();
while(itr.hasNext()) {
RelayServer srv = itr.next();
if(srv.type == RelayType.STUN) {
output.write('S');
writeASCII16(output, srv.address);
}else if(srv.type == RelayType.TURN) {
output.write('T');
writeASCII16(output, srv.address);
writeASCII8(output, srv.username);
writeASCII8(output, srv.password);
}else {
throw new IOException("Unknown/Unsupported Relay Type: " + srv.type.name());
}
}
}
}

View File

@ -0,0 +1,22 @@
package net.lax1dude.eaglercraft.sp.relay.pkt;
import java.io.DataOutputStream;
import java.io.IOException;
public class IPacket02NewClient extends IPacket {
public String clientId;
public IPacket02NewClient(String clientId) {
this.clientId = clientId;
}
public void write(DataOutputStream output) throws IOException {
writeASCII8(output, clientId);
}
public int packetLength() {
return 1 + clientId.length();
}
}

View File

@ -0,0 +1,34 @@
package net.lax1dude.eaglercraft.sp.relay.pkt;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public class IPacket03ICECandidate extends IPacket {
public String peerId;
public String candidate;
public IPacket03ICECandidate(String peerId, String desc) {
this.peerId = peerId;
this.candidate = desc;
}
public IPacket03ICECandidate() {
}
public void read(DataInputStream input) throws IOException {
peerId = readASCII8(input);
candidate = readASCII16(input);
}
public void write(DataOutputStream output) throws IOException {
writeASCII8(output, peerId);
writeASCII16(output, candidate);
}
public int packetLength() {
return 1 + peerId.length() + 2 + candidate.length();
}
}

View File

@ -0,0 +1,34 @@
package net.lax1dude.eaglercraft.sp.relay.pkt;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public class IPacket04Description extends IPacket {
public String peerId;
public String description;
public IPacket04Description(String peerId, String desc) {
this.peerId = peerId;
this.description = desc;
}
public IPacket04Description() {
}
public void read(DataInputStream input) throws IOException {
peerId = readASCII8(input);
description = readASCII16(input);
}
public void write(DataOutputStream output) throws IOException {
writeASCII8(output, peerId);
writeASCII16(output, description);
}
public int packetLength() {
return 1 + peerId.length() + 2 + description.length();
}
}

View File

@ -0,0 +1,30 @@
package net.lax1dude.eaglercraft.sp.relay.pkt;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public class IPacket05ClientSuccess extends IPacket {
public String clientId;
public IPacket05ClientSuccess() {
}
public IPacket05ClientSuccess(String clientId) {
this.clientId = clientId;
}
public void read(DataInputStream input) throws IOException {
clientId = readASCII8(input);
}
public void write(DataOutputStream output) throws IOException {
writeASCII8(output, clientId);
}
public int packetLength() {
return 1 + clientId.length();
}
}

View File

@ -0,0 +1,30 @@
package net.lax1dude.eaglercraft.sp.relay.pkt;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public class IPacket06ClientFailure extends IPacket {
public String clientId;
public IPacket06ClientFailure() {
}
public IPacket06ClientFailure(String clientId) {
this.clientId = clientId;
}
public void read(DataInputStream input) throws IOException {
clientId = readASCII8(input);
}
public void write(DataOutputStream output) throws IOException {
writeASCII8(output, clientId);
}
public int packetLength() {
return 1 + clientId.length();
}
}

View File

@ -0,0 +1,45 @@
package net.lax1dude.eaglercraft.sp.relay.pkt;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public class IPacketFEDisconnectClient extends IPacket {
public static final int TYPE_FINISHED_SUCCESS = 0x00;
public static final int TYPE_FINISHED_FAILED = 0x01;
public static final int TYPE_TIMEOUT = 0x02;
public static final int TYPE_INVALID_OPERATION = 0x03;
public static final int TYPE_INTERNAL_ERROR = 0x04;
public static final int TYPE_UNKNOWN = 0xFF;
public String clientId;
public int code;
public String reason;
public IPacketFEDisconnectClient() {
}
public IPacketFEDisconnectClient(String clientId, int code, String reason) {
this.clientId = clientId;
this.code = code;
this.reason = reason;
}
public void read(DataInputStream input) throws IOException {
clientId = readASCII8(input);
code = input.read();
reason = readASCII16(input);
}
public void write(DataOutputStream output) throws IOException {
writeASCII8(output, clientId);
output.write(code);
writeASCII16(output, reason);
}
public int packetLength() {
return -1;
}
}

View File

@ -0,0 +1,46 @@
package net.lax1dude.eaglercraft.sp.relay.pkt;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public class IPacketFFErrorCode extends IPacket {
public static final int TYPE_INTERNAL_ERROR = 0x00;
public static final int TYPE_PROTOCOL_VERSION = 0x01;
public static final int TYPE_INVALID_PACKET = 0x02;
public static final int TYPE_ILLEGAL_OPERATION = 0x03;
public static final int TYPE_CODE_LENGTH = 0x04;
public static final int TYPE_INCORRECT_CODE = 0x05;
public static final int TYPE_SERVER_DISCONNECTED = 0x06;
public static final int TYPE_UNKNOWN_CLIENT = 0x07;
public int code;
public String desc;
public IPacketFFErrorCode() {
}
public IPacketFFErrorCode(int code, String desc) {
this.code = code;
this.desc = desc;
}
@Override
public void read(DataInputStream input) throws IOException {
code = input.read();
desc = readASCII16(input);
}
@Override
public void write(DataOutputStream input) throws IOException {
input.write(code);
writeASCII16(input, desc);
}
@Override
public int packetLength() {
return 1 + 2 + desc.length();
}
}

View File

@ -0,0 +1,13 @@
[STUN]
url=stun:openrelay.metered.ca:80
[TURN]
url=turn:openrelay.metered.ca:443
username=openrelayproject
password=openrelayproject
[TURN]
url=turn:openrelay.metered.ca:443?transport=tcp
username=openrelayproject
password=openrelayproject