wrote a signaling server, untested, needs ratelimiting
This commit is contained in:
parent
e93e53541e
commit
0b2a351278
BIN
sp-relay/Java-WebSocket-1.5.1-with-dependencies.jar
Normal file
BIN
sp-relay/Java-WebSocket-1.5.1-with-dependencies.jar
Normal file
Binary file not shown.
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
13
sp-relay/src/main/resources/relays.txt
Normal file
13
sp-relay/src/main/resources/relays.txt
Normal 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
|
Loading…
Reference in New Issue
Block a user