added pinging and nearby LAN world query (untested)
This commit is contained in:
parent
0de75b537d
commit
84772d4172
|
@ -3,6 +3,7 @@ package net.lax1dude.eaglercraft.sp.relay;
|
||||||
public class Constants {
|
public class Constants {
|
||||||
|
|
||||||
public static final String versionName = "0.1a";
|
public static final String versionName = "0.1a";
|
||||||
|
public static final String versionBrand = "lax1dude";
|
||||||
public static final int protocolVersion = 1;
|
public static final int protocolVersion = 1;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,13 +33,13 @@ public class EaglerSPClient {
|
||||||
try {
|
try {
|
||||||
this.socket.send(IPacket.writePacket(packet));
|
this.socket.send(IPacket.writePacket(packet));
|
||||||
}catch(IOException ex) {
|
}catch(IOException ex) {
|
||||||
EaglerSPRelay.logger.debug("Error sending data to {}", this.socket.getRemoteSocketAddress());
|
EaglerSPRelay.logger.debug("Error sending data to {}", this.socket.getAttachment());
|
||||||
EaglerSPRelay.logger.debug(ex);
|
EaglerSPRelay.logger.debug(ex);
|
||||||
disconnect(IPacketFEDisconnectClient.TYPE_INTERNAL_ERROR, "Internal Server Error");
|
disconnect(IPacketFEDisconnectClient.TYPE_INTERNAL_ERROR, "Internal Server Error");
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
EaglerSPRelay.logger.debug("WARNING: Tried to send data to {} after the connection closed.", this.socket.getRemoteSocketAddress());
|
EaglerSPRelay.logger.debug("WARNING: Tried to send data to {} after the connection closed.", this.socket.getAttachment());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,21 +48,21 @@ public class EaglerSPClient {
|
||||||
if(LoginState.assertEquals(this, LoginState.INIT)) {
|
if(LoginState.assertEquals(this, LoginState.INIT)) {
|
||||||
state = LoginState.SENT_ICE_CANDIDATE;
|
state = LoginState.SENT_ICE_CANDIDATE;
|
||||||
server.handleClientICECandidate(this, (IPacket03ICECandidate)packet);
|
server.handleClientICECandidate(this, (IPacket03ICECandidate)packet);
|
||||||
EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x03: ICECandidate", socket.getRemoteSocketAddress());
|
EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x03: ICECandidate", socket.getAttachment());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}else if(packet instanceof IPacket04Description) {
|
}else if(packet instanceof IPacket04Description) {
|
||||||
if(LoginState.assertEquals(this, LoginState.RECIEVED_ICE_CANIDATE)) {
|
if(LoginState.assertEquals(this, LoginState.RECIEVED_ICE_CANIDATE)) {
|
||||||
state = LoginState.SENT_DESCRIPTION;
|
state = LoginState.SENT_DESCRIPTION;
|
||||||
server.handleClientDescription(this, (IPacket04Description)packet);
|
server.handleClientDescription(this, (IPacket04Description)packet);
|
||||||
EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x04: Description", socket.getRemoteSocketAddress());
|
EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x04: Description", socket.getAttachment());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}else if(packet instanceof IPacket05ClientSuccess) {
|
}else if(packet instanceof IPacket05ClientSuccess) {
|
||||||
if(LoginState.assertEquals(this, LoginState.RECIEVED_DESCRIPTION)) {
|
if(LoginState.assertEquals(this, LoginState.RECIEVED_DESCRIPTION)) {
|
||||||
state = LoginState.FINISHED;
|
state = LoginState.FINISHED;
|
||||||
server.handleClientSuccess(this, (IPacket05ClientSuccess)packet);
|
server.handleClientSuccess(this, (IPacket05ClientSuccess)packet);
|
||||||
EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x05: ClientSuccess", socket.getRemoteSocketAddress());
|
EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x05: ClientSuccess", socket.getAttachment());
|
||||||
disconnect(IPacketFEDisconnectClient.TYPE_FINISHED_SUCCESS, "Successful connection");
|
disconnect(IPacketFEDisconnectClient.TYPE_FINISHED_SUCCESS, "Successful connection");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -70,7 +70,7 @@ public class EaglerSPClient {
|
||||||
if(LoginState.assertEquals(this, LoginState.RECIEVED_DESCRIPTION)) {
|
if(LoginState.assertEquals(this, LoginState.RECIEVED_DESCRIPTION)) {
|
||||||
state = LoginState.FINISHED;
|
state = LoginState.FINISHED;
|
||||||
server.handleClientFailure(this, (IPacket06ClientFailure)packet);
|
server.handleClientFailure(this, (IPacket06ClientFailure)packet);
|
||||||
EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x05: ClientFailure", socket.getRemoteSocketAddress());
|
EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x05: ClientFailure", socket.getAttachment());
|
||||||
disconnect(IPacketFEDisconnectClient.TYPE_FINISHED_FAILED, "Failed connection");
|
disconnect(IPacketFEDisconnectClient.TYPE_FINISHED_FAILED, "Failed connection");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -101,7 +101,7 @@ public class EaglerSPClient {
|
||||||
send(pkt);
|
send(pkt);
|
||||||
socket.close();
|
socket.close();
|
||||||
}
|
}
|
||||||
EaglerSPRelay.logger.debug("[{}][Relay -> Client] PKT 0xFE: #{} {}", socket.getRemoteSocketAddress(), code, reason);
|
EaglerSPRelay.logger.debug("[{}][Relay -> Client] PKT 0xFE: #{} {}", socket.getAttachment(), code, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final int clientCodeLength = 16;
|
public static final int clientCodeLength = 16;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import java.nio.ByteBuffer;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ import org.java_websocket.handshake.ClientHandshake;
|
||||||
import org.java_websocket.server.WebSocketServer;
|
import org.java_websocket.server.WebSocketServer;
|
||||||
|
|
||||||
import net.lax1dude.eaglercraft.sp.relay.pkt.*;
|
import net.lax1dude.eaglercraft.sp.relay.pkt.*;
|
||||||
|
import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket07LocalWorlds.LocalWorld;
|
||||||
|
|
||||||
public class EaglerSPRelay extends WebSocketServer {
|
public class EaglerSPRelay extends WebSocketServer {
|
||||||
|
|
||||||
|
@ -47,10 +49,10 @@ public class EaglerSPRelay extends WebSocketServer {
|
||||||
try {
|
try {
|
||||||
long millis = System.currentTimeMillis();
|
long millis = System.currentTimeMillis();
|
||||||
synchronized(pendingConnections) {
|
synchronized(pendingConnections) {
|
||||||
Iterator<Entry<WebSocket,Long>> itr = pendingConnections.entrySet().iterator();
|
Iterator<Entry<WebSocket,PendingConnection>> itr = pendingConnections.entrySet().iterator();
|
||||||
while(itr.hasNext()) {
|
while(itr.hasNext()) {
|
||||||
Entry<WebSocket,Long> etr = itr.next();
|
Entry<WebSocket,PendingConnection> etr = itr.next();
|
||||||
if(millis - etr.getValue().longValue() > 1000l) {
|
if(millis - etr.getValue().openTime > 1000l) {
|
||||||
etr.getKey().close();
|
etr.getKey().close();
|
||||||
itr.remove();
|
itr.remove();
|
||||||
}
|
}
|
||||||
|
@ -60,7 +62,7 @@ public class EaglerSPRelay extends WebSocketServer {
|
||||||
Iterator<EaglerSPClient> itr = clientConnections.values().iterator();
|
Iterator<EaglerSPClient> itr = clientConnections.values().iterator();
|
||||||
while(itr.hasNext()) {
|
while(itr.hasNext()) {
|
||||||
EaglerSPClient cl = itr.next();
|
EaglerSPClient cl = itr.next();
|
||||||
if(millis - cl.createdOn > 6000l) {
|
if(millis - cl.createdOn > 500l) {
|
||||||
cl.disconnect(IPacketFEDisconnectClient.TYPE_TIMEOUT, "Took too long to connect!");
|
cl.disconnect(IPacketFEDisconnectClient.TYPE_TIMEOUT, "Took too long to connect!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,11 +97,24 @@ public class EaglerSPRelay extends WebSocketServer {
|
||||||
super(addr);
|
super(addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Map<WebSocket,Long> pendingConnections = new HashMap();
|
private static class PendingConnection {
|
||||||
|
|
||||||
|
private final long openTime;
|
||||||
|
private final String address;
|
||||||
|
|
||||||
|
public PendingConnection(long openTime, String address) {
|
||||||
|
this.openTime = openTime;
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Map<WebSocket,PendingConnection> pendingConnections = new HashMap();
|
||||||
private static final Map<String,EaglerSPClient> clientIds = new HashMap();
|
private static final Map<String,EaglerSPClient> clientIds = new HashMap();
|
||||||
private static final Map<WebSocket,EaglerSPClient> clientConnections = new HashMap();
|
private static final Map<WebSocket,EaglerSPClient> clientConnections = new HashMap();
|
||||||
private static final Map<String,EaglerSPServer> serverCodes = new HashMap();
|
private static final Map<String,EaglerSPServer> serverCodes = new HashMap();
|
||||||
private static final Map<WebSocket,EaglerSPServer> serverConnections = new HashMap();
|
private static final Map<WebSocket,EaglerSPServer> serverConnections = new HashMap();
|
||||||
|
private static final Map<String,List<EaglerSPServer>> serverAddressSets = new HashMap();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
|
@ -109,27 +124,36 @@ public class EaglerSPRelay extends WebSocketServer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOpen(WebSocket arg0, ClientHandshake arg1) {
|
public void onOpen(WebSocket arg0, ClientHandshake arg1) {
|
||||||
synchronized(pendingConnections) {
|
String addr;
|
||||||
|
long millis = System.currentTimeMillis();
|
||||||
|
if(config.isEnableRealIpHeader() && arg1.hasFieldValue("X-Real-IP")) {
|
||||||
|
addr = arg1.getFieldValue("X-Real-IP").toLowerCase();
|
||||||
|
}else {
|
||||||
|
addr = arg0.getRemoteSocketAddress().getAddress().getHostAddress().toLowerCase();
|
||||||
|
}
|
||||||
|
arg0.setAttachment(addr);
|
||||||
|
PendingConnection waiting = new PendingConnection(millis, addr);
|
||||||
logger.debug("[{}]: Connection opened", arg0.getRemoteSocketAddress());
|
logger.debug("[{}]: Connection opened", arg0.getRemoteSocketAddress());
|
||||||
pendingConnections.put(arg0, System.currentTimeMillis());
|
synchronized(pendingConnections) {
|
||||||
|
pendingConnections.put(arg0, waiting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(WebSocket arg0, ByteBuffer arg1) {
|
public void onMessage(WebSocket arg0, ByteBuffer arg1) {
|
||||||
DataInputStream sid = new DataInputStream(new ByteBufferInputStream(arg1));
|
DataInputStream sid = new DataInputStream(new ByteBufferInputStream(arg1));
|
||||||
boolean waiting;
|
PendingConnection waiting;
|
||||||
synchronized(pendingConnections) {
|
synchronized(pendingConnections) {
|
||||||
waiting = pendingConnections.remove(arg0) != null;
|
waiting = pendingConnections.remove(arg0);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
IPacket pkt = IPacket.readPacket(sid);
|
IPacket pkt = IPacket.readPacket(sid);
|
||||||
if(waiting) {
|
if(waiting != null) {
|
||||||
if(pkt instanceof IPacket00Handshake) {
|
if(pkt instanceof IPacket00Handshake) {
|
||||||
IPacket00Handshake ipkt = (IPacket00Handshake)pkt;
|
IPacket00Handshake ipkt = (IPacket00Handshake)pkt;
|
||||||
if(ipkt.connectionVersion != Constants.protocolVersion) {
|
if(ipkt.connectionVersion != Constants.protocolVersion) {
|
||||||
logger.debug("[{}]: Connected with unsupported protocol version: {} (supported "
|
logger.debug("[{}]: Connected with unsupported protocol version: {} (supported "
|
||||||
+ "version: {})", arg0.getRemoteSocketAddress(), Constants.protocolVersion);
|
+ "version: {})", arg0.getAttachment(), Constants.protocolVersion);
|
||||||
if(ipkt.connectionVersion < Constants.protocolVersion) {
|
if(ipkt.connectionVersion < Constants.protocolVersion) {
|
||||||
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_PROTOCOL_VERSION,
|
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_PROTOCOL_VERSION,
|
||||||
"Outdated Client! (v" + Constants.protocolVersion + " req)")));
|
"Outdated Client! (v" + Constants.protocolVersion + " req)")));
|
||||||
|
@ -141,7 +165,7 @@ public class EaglerSPRelay extends WebSocketServer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(ipkt.connectionType == 0x01) {
|
if(ipkt.connectionType == 0x01) {
|
||||||
logger.debug("[{}]: Connected as a server", arg0.getRemoteSocketAddress());
|
logger.debug("[{}]: Connected as a server", arg0.getAttachment());
|
||||||
EaglerSPServer srv;
|
EaglerSPServer srv;
|
||||||
synchronized(serverCodes) {
|
synchronized(serverCodes) {
|
||||||
int j = 0;
|
int j = 0;
|
||||||
|
@ -149,7 +173,7 @@ public class EaglerSPRelay extends WebSocketServer {
|
||||||
do {
|
do {
|
||||||
if(++j > 100) {
|
if(++j > 100) {
|
||||||
logger.error("Error: relay is running out of codes!");
|
logger.error("Error: relay is running out of codes!");
|
||||||
logger.error("Closing connection to {}", arg0.getRemoteSocketAddress());
|
logger.error("Closing connection to {}", arg0.getAttachment());
|
||||||
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INTERNAL_ERROR,
|
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INTERNAL_ERROR,
|
||||||
"Internal Server Error")));
|
"Internal Server Error")));
|
||||||
arg0.close();
|
arg0.close();
|
||||||
|
@ -157,20 +181,28 @@ public class EaglerSPRelay extends WebSocketServer {
|
||||||
}
|
}
|
||||||
code = config.generateCode();
|
code = config.generateCode();
|
||||||
}while(serverCodes.containsKey(code));
|
}while(serverCodes.containsKey(code));
|
||||||
srv = new EaglerSPServer(arg0, code);
|
srv = new EaglerSPServer(arg0, code, ipkt.connectionCode, waiting.address);
|
||||||
serverCodes.put(code, srv);
|
serverCodes.put(code, srv);
|
||||||
ipkt.connectionCode = code;
|
ipkt.connectionCode = code;
|
||||||
arg0.send(IPacket.writePacket(ipkt));
|
arg0.send(IPacket.writePacket(ipkt));
|
||||||
logger.debug("[{}][Relay -> Server] PKT 0x00: Assign join code: {}", arg0.getRemoteSocketAddress(), code);
|
logger.debug("[{}][Relay -> Server] PKT 0x00: Assign join code: {}", arg0.getAttachment(), code);
|
||||||
}
|
}
|
||||||
synchronized(serverConnections) {
|
synchronized(serverConnections) {
|
||||||
serverConnections.put(arg0, srv);
|
serverConnections.put(arg0, srv);
|
||||||
}
|
}
|
||||||
|
synchronized(serverAddressSets) {
|
||||||
|
List<EaglerSPServer> lst = serverAddressSets.get(srv.serverAddress);
|
||||||
|
if(lst == null) {
|
||||||
|
lst = new ArrayList();
|
||||||
|
serverAddressSets.put(srv.serverAddress, lst);
|
||||||
|
}
|
||||||
|
lst.add(srv);
|
||||||
|
}
|
||||||
srv.send(new IPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers));
|
srv.send(new IPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers));
|
||||||
logger.debug("[{}][Relay -> Server] PKT 0x01: Send ICE server list to server", arg0.getRemoteSocketAddress());
|
logger.debug("[{}][Relay -> Server] PKT 0x01: Send ICE server list to server", arg0.getAttachment());
|
||||||
}else if(ipkt.connectionType == 0x02) {
|
}else if(ipkt.connectionType == 0x02) {
|
||||||
String code = ipkt.connectionCode;
|
String code = ipkt.connectionCode;
|
||||||
logger.debug("[{}]: Connected as a client, requested server code: {}", arg0.getRemoteSocketAddress(), code);
|
logger.debug("[{}]: Connected as a client, requested server code: {}", arg0.getAttachment(), code);
|
||||||
if(code.length() != config.getCodeLength()) {
|
if(code.length() != config.getCodeLength()) {
|
||||||
logger.debug("The code '{}' is invalid because it's the wrong length, disconnecting");
|
logger.debug("The code '{}' is invalid because it's the wrong length, disconnecting");
|
||||||
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_CODE_LENGTH,
|
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_CODE_LENGTH,
|
||||||
|
@ -204,17 +236,25 @@ public class EaglerSPRelay extends WebSocketServer {
|
||||||
clientConnections.put(arg0, cl);
|
clientConnections.put(arg0, cl);
|
||||||
}
|
}
|
||||||
cl.send(new IPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers));
|
cl.send(new IPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers));
|
||||||
logger.debug("[{}][Relay -> Client] PKT 0x01: Send ICE server list to client", arg0.getRemoteSocketAddress());
|
logger.debug("[{}][Relay -> Client] PKT 0x01: Send ICE server list to client", arg0.getAttachment());
|
||||||
}
|
}
|
||||||
|
}else if(ipkt.connectionType == 0x02) {
|
||||||
|
logger.debug("[{}]: Pinging the server", arg0.getAttachment());
|
||||||
|
arg0.send(IPacket.writePacket(new IPacket69Pong(Constants.protocolVersion, config.getComment(), Constants.versionBrand)));
|
||||||
|
arg0.close();
|
||||||
|
}else if(ipkt.connectionType == 0x03) {
|
||||||
|
logger.debug("[{}]: Polling the server for other worlds", arg0.getAttachment());
|
||||||
|
arg0.send(IPacket.writePacket(new IPacket07LocalWorlds(getLocalWorlds(waiting.address))));
|
||||||
|
arg0.close();
|
||||||
}else {
|
}else {
|
||||||
logger.debug("[{}]: Unknown connection type: {}", arg0.getRemoteSocketAddress(), ipkt.connectionType);
|
logger.debug("[{}]: Unknown connection type: {}", arg0.getAttachment(), ipkt.connectionType);
|
||||||
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_ILLEGAL_OPERATION,
|
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_ILLEGAL_OPERATION,
|
||||||
"Unexpected Init Packet")));
|
"Unexpected Init Packet")));
|
||||||
arg0.close();
|
arg0.close();
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
logger.debug("[{}]: Pending connection did not send a 0x00 packet to identify "
|
logger.debug("[{}]: Pending connection did not send a 0x00 packet to identify "
|
||||||
+ "as a client or server", arg0.getRemoteSocketAddress());
|
+ "as a client or server", arg0.getAttachment());
|
||||||
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_ILLEGAL_OPERATION,
|
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_ILLEGAL_OPERATION,
|
||||||
"Unexpected Init Packet")));
|
"Unexpected Init Packet")));
|
||||||
arg0.close();
|
arg0.close();
|
||||||
|
@ -226,7 +266,7 @@ public class EaglerSPRelay extends WebSocketServer {
|
||||||
}
|
}
|
||||||
if(srv != null) {
|
if(srv != null) {
|
||||||
if(!srv.handle(pkt)) {
|
if(!srv.handle(pkt)) {
|
||||||
logger.debug("[{}]: Server sent invalid packet: {}", arg0.getRemoteSocketAddress(), pkt.getClass().getSimpleName());
|
logger.debug("[{}]: Server sent invalid packet: {}", arg0.getAttachment(), pkt.getClass().getSimpleName());
|
||||||
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INVALID_PACKET,
|
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INVALID_PACKET,
|
||||||
"Invalid Packet Recieved")));
|
"Invalid Packet Recieved")));
|
||||||
arg0.close();
|
arg0.close();
|
||||||
|
@ -238,13 +278,13 @@ public class EaglerSPRelay extends WebSocketServer {
|
||||||
}
|
}
|
||||||
if(cl != null) {
|
if(cl != null) {
|
||||||
if(cl.handle(pkt)) {
|
if(cl.handle(pkt)) {
|
||||||
logger.debug("[{}]: Client sent invalid packet: {}", arg0.getRemoteSocketAddress(), pkt.getClass().getSimpleName());
|
logger.debug("[{}]: Client sent invalid packet: {}", arg0.getAttachment(), pkt.getClass().getSimpleName());
|
||||||
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INVALID_PACKET,
|
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INVALID_PACKET,
|
||||||
"Invalid Packet Recieved")));
|
"Invalid Packet Recieved")));
|
||||||
arg0.close();
|
arg0.close();
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
logger.debug("[{}]: Connection has no client/server attached to it!", arg0.getRemoteSocketAddress());
|
logger.debug("[{}]: Connection has no client/server attached to it!", arg0.getAttachment());
|
||||||
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_ILLEGAL_OPERATION,
|
arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_ILLEGAL_OPERATION,
|
||||||
"Internal Server Error")));
|
"Internal Server Error")));
|
||||||
arg0.close();
|
arg0.close();
|
||||||
|
@ -252,14 +292,14 @@ public class EaglerSPRelay extends WebSocketServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}catch(Throwable t) {
|
}catch(Throwable t) {
|
||||||
logger.error("[{}]: Failed to handle binary frame: {}", arg0.getRemoteSocketAddress(), t);
|
logger.error("[{}]: Failed to handle binary frame: {}", arg0.getAttachment(), t);
|
||||||
arg0.close();
|
arg0.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(WebSocket arg0, String arg1) {
|
public void onMessage(WebSocket arg0, String arg1) {
|
||||||
logger.debug("[{}]: Sent a text frame, disconnecting", arg0.getRemoteSocketAddress());
|
logger.debug("[{}]: Sent a text frame, disconnecting", arg0.getAttachment());
|
||||||
arg0.close();
|
arg0.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,7 +310,7 @@ public class EaglerSPRelay extends WebSocketServer {
|
||||||
srv = serverConnections.remove(arg0);
|
srv = serverConnections.remove(arg0);
|
||||||
}
|
}
|
||||||
if(srv != null) {
|
if(srv != null) {
|
||||||
logger.debug("[{}]: Server closed, code: {}", arg0.getRemoteSocketAddress(), srv.code);
|
logger.debug("[{}]: Server closed, code: {}", arg0.getAttachment(), srv.code);
|
||||||
synchronized(serverCodes) {
|
synchronized(serverCodes) {
|
||||||
serverCodes.remove(srv.code);
|
serverCodes.remove(srv.code);
|
||||||
}
|
}
|
||||||
|
@ -282,32 +322,58 @@ public class EaglerSPRelay extends WebSocketServer {
|
||||||
while(itr.hasNext()) {
|
while(itr.hasNext()) {
|
||||||
EaglerSPClient cl = itr.next();
|
EaglerSPClient cl = itr.next();
|
||||||
if(cl.server == srv) {
|
if(cl.server == srv) {
|
||||||
logger.debug("[{}]: Disconnecting client: {} (id: ", cl.socket.getRemoteSocketAddress(), cl.id);
|
logger.debug("[{}]: Disconnecting client: {} (id: ", cl.socket.getAttachment(), cl.id);
|
||||||
cl.socket.close();
|
cl.socket.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
synchronized(serverAddressSets) {
|
||||||
|
List<EaglerSPServer> lst = serverAddressSets.get(srv.serverAddress);
|
||||||
|
lst.remove(srv);
|
||||||
|
if(lst.size() == 0) {
|
||||||
|
serverAddressSets.remove(srv.serverAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
}else {
|
}else {
|
||||||
EaglerSPClient cl;
|
EaglerSPClient cl;
|
||||||
synchronized(clientConnections) {
|
synchronized(clientConnections) {
|
||||||
cl = clientConnections.remove(arg0);
|
cl = clientConnections.remove(arg0);
|
||||||
}
|
}
|
||||||
if(cl != null) {
|
if(cl != null) {
|
||||||
logger.debug("[{}]: Client closed, id: {}", arg0.getRemoteSocketAddress(), cl.id);
|
logger.debug("[{}]: Client closed, id: {}", arg0.getAttachment(), cl.id);
|
||||||
synchronized(clientIds) {
|
synchronized(clientIds) {
|
||||||
clientIds.remove(cl.id);
|
clientIds.remove(cl.id);
|
||||||
}
|
}
|
||||||
cl.server.handleClientDisconnect(cl);
|
cl.server.handleClientDisconnect(cl);
|
||||||
}else {
|
}else {
|
||||||
logger.debug("[{}]: Connection Closed: {} ", arg0.getRemoteSocketAddress());
|
logger.debug("[{}]: Connection Closed: {} ", arg0.getAttachment());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(WebSocket arg0, Exception arg1) {
|
public void onError(WebSocket arg0, Exception arg1) {
|
||||||
logger.error("[{}]: Exception thrown: {}", arg0.getRemoteSocketAddress(), arg1.toString());
|
logger.error("[{}]: Exception thrown: {}", arg0.getAttachment(), arg1.toString());
|
||||||
logger.debug(arg1);
|
logger.debug(arg1);
|
||||||
arg0.close();
|
arg0.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<IPacket07LocalWorlds.LocalWorld> getLocalWorlds(String addr) {
|
||||||
|
List<IPacket07LocalWorlds.LocalWorld> lst = new ArrayList();
|
||||||
|
synchronized(serverAddressSets) {
|
||||||
|
List<EaglerSPServer> srvs = serverAddressSets.get(addr);
|
||||||
|
if(srvs != null) {
|
||||||
|
if(srvs.size() == 0) {
|
||||||
|
serverAddressSets.remove(addr);
|
||||||
|
}else {
|
||||||
|
for(EaglerSPServer s : srvs) {
|
||||||
|
if(!s.serverHidden) {
|
||||||
|
lst.add(new LocalWorld(s.serverName, s.code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lst;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ public class EaglerSPRelayConfig {
|
||||||
private String originWhitelist = "";
|
private String originWhitelist = "";
|
||||||
private String[] originWhitelistArray = new String[0];
|
private String[] originWhitelistArray = new String[0];
|
||||||
private boolean enableRealIpHeader = false;
|
private boolean enableRealIpHeader = false;
|
||||||
|
private String serverComment = "Eags. Public LAN Relay";
|
||||||
|
|
||||||
public void load(File conf) {
|
public void load(File conf) {
|
||||||
if(!conf.isFile()) {
|
if(!conf.isFile()) {
|
||||||
|
@ -38,7 +39,7 @@ public class EaglerSPRelayConfig {
|
||||||
boolean gotRateLimitEnable = false, gotRateLimitPeriod = false;
|
boolean gotRateLimitEnable = false, gotRateLimitPeriod = false;
|
||||||
boolean gotRateLimitLimit = false, gotRateLimitLockoutLimit = false;
|
boolean gotRateLimitLimit = false, gotRateLimitLockoutLimit = false;
|
||||||
boolean gotRateLimitLockoutDuration = false, gotOriginWhitelist = false;
|
boolean gotRateLimitLockoutDuration = false, gotOriginWhitelist = false;
|
||||||
boolean gotEnableRealIpHeader = false, gotAddress = false;
|
boolean gotEnableRealIpHeader = false, gotAddress = false, gotComment = false;
|
||||||
Throwable t2 = null;
|
Throwable t2 = null;
|
||||||
try(BufferedReader reader = new BufferedReader(new FileReader(conf))) {
|
try(BufferedReader reader = new BufferedReader(new FileReader(conf))) {
|
||||||
String s;
|
String s;
|
||||||
|
@ -158,6 +159,9 @@ public class EaglerSPRelayConfig {
|
||||||
t2 = t;
|
t2 = t;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}else if(ss[0].equalsIgnoreCase("server-comment")) {
|
||||||
|
serverComment = ss[1];
|
||||||
|
gotComment = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,7 +177,7 @@ public class EaglerSPRelayConfig {
|
||||||
!gotCodeMixCase || !gotConnectionsPerIP || !gotRateLimitEnable ||
|
!gotCodeMixCase || !gotConnectionsPerIP || !gotRateLimitEnable ||
|
||||||
!gotRateLimitPeriod || !gotRateLimitLimit || !gotRateLimitLockoutLimit ||
|
!gotRateLimitPeriod || !gotRateLimitLimit || !gotRateLimitLockoutLimit ||
|
||||||
!gotRateLimitLockoutDuration || !gotOriginWhitelist ||
|
!gotRateLimitLockoutDuration || !gotOriginWhitelist ||
|
||||||
!gotEnableRealIpHeader || !gotAddress) {
|
!gotEnableRealIpHeader || !gotAddress || !gotComment) {
|
||||||
EaglerSPRelay.logger.warn("Updating config file: {}", conf.getAbsoluteFile());
|
EaglerSPRelay.logger.warn("Updating config file: {}", conf.getAbsoluteFile());
|
||||||
save(conf);
|
save(conf);
|
||||||
}
|
}
|
||||||
|
@ -206,7 +210,8 @@ public class EaglerSPRelayConfig {
|
||||||
w.println("ratelimit-lockout-limit=" + rateLimitLockoutLimit);
|
w.println("ratelimit-lockout-limit=" + rateLimitLockoutLimit);
|
||||||
w.println("ratelimit-lockout-duration=" + rateLimitLockoutDuration);
|
w.println("ratelimit-lockout-duration=" + rateLimitLockoutDuration);
|
||||||
w.println("origin-whitelist=" + originWhitelist);
|
w.println("origin-whitelist=" + originWhitelist);
|
||||||
w.print("enable-real-ip-header=" + enableRealIpHeader);
|
w.println("enable-real-ip-header=" + enableRealIpHeader);
|
||||||
|
w.print("server-comment=" + serverComment);
|
||||||
}catch(IOException t) {
|
}catch(IOException t) {
|
||||||
EaglerSPRelay.logger.error("Failed to write config file: {}", conf.getAbsoluteFile());
|
EaglerSPRelay.logger.error("Failed to write config file: {}", conf.getAbsoluteFile());
|
||||||
EaglerSPRelay.logger.error(t);
|
EaglerSPRelay.logger.error(t);
|
||||||
|
@ -279,6 +284,10 @@ public class EaglerSPRelayConfig {
|
||||||
return enableRealIpHeader;
|
return enableRealIpHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getComment() {
|
||||||
|
return serverComment;
|
||||||
|
}
|
||||||
|
|
||||||
public String generateCode() {
|
public String generateCode() {
|
||||||
Random r = new Random();
|
Random r = new Random();
|
||||||
char[] ret = new char[codeLength];
|
char[] ret = new char[codeLength];
|
||||||
|
|
|
@ -20,11 +20,27 @@ public class EaglerSPServer {
|
||||||
public final WebSocket socket;
|
public final WebSocket socket;
|
||||||
public final String code;
|
public final String code;
|
||||||
public final Map<String,EaglerSPClient> clients;
|
public final Map<String,EaglerSPClient> clients;
|
||||||
|
public final String serverName;
|
||||||
|
public final String serverAddress;
|
||||||
|
public final boolean serverHidden;
|
||||||
|
|
||||||
EaglerSPServer(WebSocket sock, String code) {
|
EaglerSPServer(WebSocket sock, String code, String serverName, String serverAddress) {
|
||||||
this.socket = sock;
|
this.socket = sock;
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.clients = new HashMap();
|
this.clients = new HashMap();
|
||||||
|
|
||||||
|
if(serverName.endsWith(";1")) {
|
||||||
|
this.serverHidden = true;
|
||||||
|
serverName = serverName.substring(0, serverName.length() - 2);
|
||||||
|
}else if(serverName.endsWith(";0")) {
|
||||||
|
this.serverHidden = false;
|
||||||
|
serverName = serverName.substring(0, serverName.length() - 2);
|
||||||
|
}else {
|
||||||
|
this.serverHidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.serverName = serverName;
|
||||||
|
this.serverAddress = serverAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void send(IPacket packet) {
|
public void send(IPacket packet) {
|
||||||
|
@ -32,7 +48,7 @@ public class EaglerSPServer {
|
||||||
try {
|
try {
|
||||||
this.socket.send(IPacket.writePacket(packet));
|
this.socket.send(IPacket.writePacket(packet));
|
||||||
}catch(IOException ex) {
|
}catch(IOException ex) {
|
||||||
EaglerSPRelay.logger.debug("Error sending data to {}", this.socket.getRemoteSocketAddress());
|
EaglerSPRelay.logger.debug("Error sending data to {}", this.serverAddress);
|
||||||
EaglerSPRelay.logger.debug(ex);
|
EaglerSPRelay.logger.debug(ex);
|
||||||
try {
|
try {
|
||||||
this.socket.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INTERNAL_ERROR,
|
this.socket.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INTERNAL_ERROR,
|
||||||
|
@ -42,7 +58,7 @@ public class EaglerSPServer {
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
EaglerSPRelay.logger.debug("WARNING: Tried to send data to {} after the connection closed.", this.socket.getRemoteSocketAddress());
|
EaglerSPRelay.logger.debug("WARNING: Tried to send data to {} after the connection closed.", this.serverAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +70,7 @@ public class EaglerSPServer {
|
||||||
if(LoginState.assertEquals(cl, LoginState.SENT_ICE_CANDIDATE)) {
|
if(LoginState.assertEquals(cl, LoginState.SENT_ICE_CANDIDATE)) {
|
||||||
cl.state = LoginState.RECIEVED_ICE_CANIDATE;
|
cl.state = LoginState.RECIEVED_ICE_CANIDATE;
|
||||||
cl.handleServerICECandidate(packet);
|
cl.handleServerICECandidate(packet);
|
||||||
EaglerSPRelay.logger.debug("[{}][Server -> Relay -> Client] PKT 0x03: ICECandidate", cl.socket.getRemoteSocketAddress());
|
EaglerSPRelay.logger.debug("[{}][Server -> Relay -> Client] PKT 0x03: ICECandidate", cl.socket.getAttachment());
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
this.socket.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_UNKNOWN_CLIENT,
|
this.socket.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_UNKNOWN_CLIENT,
|
||||||
|
@ -68,7 +84,7 @@ public class EaglerSPServer {
|
||||||
if(LoginState.assertEquals(cl, LoginState.SENT_DESCRIPTION)) {
|
if(LoginState.assertEquals(cl, LoginState.SENT_DESCRIPTION)) {
|
||||||
cl.state = LoginState.RECIEVED_DESCRIPTION;
|
cl.state = LoginState.RECIEVED_DESCRIPTION;
|
||||||
cl.handleServerDescription(packet);
|
cl.handleServerDescription(packet);
|
||||||
EaglerSPRelay.logger.debug("[{}][Server -> Relay -> Client] PKT 0x04: Description", cl.socket.getRemoteSocketAddress());
|
EaglerSPRelay.logger.debug("[{}][Server -> Relay -> Client] PKT 0x04: Description", cl.socket.getAttachment());
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
this.socket.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_UNKNOWN_CLIENT,
|
this.socket.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_UNKNOWN_CLIENT,
|
||||||
|
@ -80,7 +96,7 @@ public class EaglerSPServer {
|
||||||
EaglerSPClient cl = clients.get(packet.clientId);
|
EaglerSPClient cl = clients.get(packet.clientId);
|
||||||
if(cl != null) {
|
if(cl != null) {
|
||||||
cl.handleServerDisconnectClient(packet);
|
cl.handleServerDisconnectClient(packet);
|
||||||
EaglerSPRelay.logger.debug("[{}][Server -> Relay -> Client] PKT 0xFE: Disconnect: {}: {}", cl.socket.getRemoteSocketAddress(),
|
EaglerSPRelay.logger.debug("[{}][Server -> Relay -> Client] PKT 0xFE: Disconnect: {}: {}", cl.socket.getAttachment(),
|
||||||
packet.code, packet.reason);
|
packet.code, packet.reason);
|
||||||
}else {
|
}else {
|
||||||
this.socket.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_UNKNOWN_CLIENT,
|
this.socket.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_UNKNOWN_CLIENT,
|
||||||
|
@ -96,7 +112,7 @@ public class EaglerSPServer {
|
||||||
synchronized(clients) {
|
synchronized(clients) {
|
||||||
clients.put(client.id, client);
|
clients.put(client.id, client);
|
||||||
send(new IPacket02NewClient(client.id));
|
send(new IPacket02NewClient(client.id));
|
||||||
EaglerSPRelay.logger.debug("[{}][Relay -> Server] PKT 0x02: Notify server of the client, id: {}", socket.getRemoteSocketAddress(), client.id);
|
EaglerSPRelay.logger.debug("[{}][Relay -> Server] PKT 0x02: Notify server of the client, id: {}", serverAddress, client.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ public enum LoginState {
|
||||||
if(client.state != state) {
|
if(client.state != state) {
|
||||||
String msg = "client is in state " + client.state.name() + " when it was supposed to be " + state.name();
|
String msg = "client is in state " + client.state.name() + " when it was supposed to be " + state.name();
|
||||||
client.disconnect(IPacketFEDisconnectClient.TYPE_INVALID_OPERATION, msg);
|
client.disconnect(IPacketFEDisconnectClient.TYPE_INVALID_OPERATION, msg);
|
||||||
EaglerSPRelay.logger.debug("[{}][Relay -> Client] PKT 0xFE: TYPE_INVALID_OPERATION: {}", client.socket.getRemoteSocketAddress(), msg);
|
EaglerSPRelay.logger.debug("[{}][Relay -> Client] PKT 0xFE: TYPE_INVALID_OPERATION: {}", client.socket.getAttachment(), msg);
|
||||||
return false;
|
return false;
|
||||||
}else {
|
}else {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -12,7 +12,7 @@ public class Util {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String sock2String(InetSocketAddress sock) {
|
public static String sock2String(InetSocketAddress sock) {
|
||||||
return sock.getAddress().getHostAddress() + sock.getPort();
|
return sock.getAddress().getHostAddress() + ":" + sock.getPort();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ public class IPacket {
|
||||||
register(0x04, IPacket04Description.class);
|
register(0x04, IPacket04Description.class);
|
||||||
register(0x05, IPacket05ClientSuccess.class);
|
register(0x05, IPacket05ClientSuccess.class);
|
||||||
register(0x06, IPacket06ClientFailure.class);
|
register(0x06, IPacket06ClientFailure.class);
|
||||||
|
register(0x07, IPacket07LocalWorlds.class);
|
||||||
|
register(0x69, IPacket69Pong.class);
|
||||||
register(0xFE, IPacketFEDisconnectClient.class);
|
register(0xFE, IPacketFEDisconnectClient.class);
|
||||||
register(0xFF, IPacketFFErrorCode.class);
|
register(0xFF, IPacketFFErrorCode.class);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package net.lax1dude.eaglercraft.sp.relay.pkt;
|
||||||
|
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class IPacket07LocalWorlds extends IPacket {
|
||||||
|
|
||||||
|
public static class LocalWorld {
|
||||||
|
|
||||||
|
public final String worldName;
|
||||||
|
public final String worldCode;
|
||||||
|
|
||||||
|
public LocalWorld(String worldName, String worldCode) {
|
||||||
|
this.worldName = worldName;
|
||||||
|
this.worldCode = worldCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public final List<LocalWorld> worldsList;
|
||||||
|
|
||||||
|
public IPacket07LocalWorlds(List<LocalWorld> worldsList) {
|
||||||
|
this.worldsList = worldsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(DataOutputStream output) throws IOException {
|
||||||
|
int i = worldsList.size();
|
||||||
|
if(i > 255) {
|
||||||
|
i = 255;
|
||||||
|
}
|
||||||
|
output.write(i);
|
||||||
|
for(int j = 0; j < i; ++i) {
|
||||||
|
LocalWorld w = worldsList.get(j);
|
||||||
|
writeASCII8(output, w.worldName);
|
||||||
|
writeASCII8(output, w.worldCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package net.lax1dude.eaglercraft.sp.relay.pkt;
|
||||||
|
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class IPacket69Pong extends IPacket {
|
||||||
|
|
||||||
|
public int protcolVersion;
|
||||||
|
public String comment;
|
||||||
|
public String brand;
|
||||||
|
|
||||||
|
public IPacket69Pong(int protcolVersion, String comment, String brand) {
|
||||||
|
if(comment.length() > 255) {
|
||||||
|
comment = comment.substring(0, 256);
|
||||||
|
}
|
||||||
|
this.protcolVersion = protcolVersion;
|
||||||
|
this.comment = comment;
|
||||||
|
this.brand = brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(DataOutputStream output) throws IOException {
|
||||||
|
output.write(protcolVersion);
|
||||||
|
writeASCII8(output, comment);
|
||||||
|
writeASCII8(output, brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int packetLength() {
|
||||||
|
return 3 + comment.length() + brand.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user