package com.laytonsmith.core.federation;
import com.laytonsmith.PureUtilities.Common.StreamUtils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A FederationServer represents a server that is listening for remote connections.
* Once a connection is established, a "sub socket" will be created, and that represents
* a single connection to the client.
*/
public class FederationServer {
private final String serverName;
private final String password;
private final File authorizedKeys;
private final String allowFrom;
private final int masterPort;
private final Map<Socket, Long> subSockets = new HashMap<>();
private ServerSocket serverSocket;
private boolean closed = false;
public FederationServer(String serverName, String password, File authorizedKeys, String allowFrom, int masterPort) {
this.serverName = serverName;
this.password = password;
this.authorizedKeys = authorizedKeys;
this.allowFrom = allowFrom;
this.masterPort = masterPort;
}
/**
* Sets the ServerSocket that this server is listening on.
* @param serverSocket
*/
public void setServerSocket(ServerSocket serverSocket) {
this.serverSocket = serverSocket;
}
/**
* Gets the ServerSocket that this server is listening on.
* @return
*/
public ServerSocket getServerSocket() {
return this.serverSocket;
}
/**
* Returns the name of this server.
* @return
*/
public String getServerName() {
return serverName;
}
/**
* Returns the server password. If null, the server has no password.
* @return
*/
public String getPassword() {
return password;
}
/**
* Returns a link to the authorized keys file. This is a list of client
* keys that are allowed to connect to this server.
* @return
*/
public File getAuthorizedKeys() {
return authorizedKeys;
}
/**
* Returns the allow from string. This is a comma separated list of the IP
* or hostnames that are allowed.
* @return
*/
public String getAllowFrom() {
return allowFrom;
}
/**
* Returns the master port. This is NOT the same port as the one the server
* is currently listening on however, this is simply the master port for this
* server's federation.
* @return
*/
public int getMasterPort() {
return masterPort;
}
/**
* A sub socket is a list of sockets that are currently connected to this
* Server Socket.
*
* @param s
*/
public void addSubSocket(Socket s) {
subSockets.put(s, System.currentTimeMillis());
}
/**
* Removes the sub socket from this server.
*
* @param s
*/
public void removeSubSocket(Socket s) {
subSockets.remove(s);
}
/**
* Listens for connections. Each connection spawns a new thread. This method
* blocks until the server socket is closed.
* @throws java.io.IOException
*/
public void listenForConnections() throws IOException{
while(!serverSocket.isClosed()){
final Socket s = serverSocket.accept();
addSubSocket(s);
new Thread(new Runnable() {
@Override
public void run() {
try {
FederationCommunication communicator = new FederationCommunication(
new BufferedInputStream(s.getInputStream()),
new BufferedOutputStream(s.getOutputStream()));
// Before we verify that the connection is allowed, we want
// to limit the amount of resources this can take. The
// HELLO needs to complete relatively quickly, or it will
// forcibly close the socket. This will prevent bad connections
// from piling up.
Thread connectionWatcher = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
// We weren't interrupted within the grace
// period, which means the connection is taking
// too long. Forcibly terminate it.
try {
if(s.isConnected()){
s.close();
}
} catch (IOException ex) {
Logger.getLogger(FederationServer.class.getName()).log(Level.SEVERE, null, ex);
}
} catch (InterruptedException ex) {
// Ok, it's good. The connection may now
// idle without risk of being killed.
}
}
}, "FederationServerConnectionWatcher-" + s.hashCode());
connectionWatcher.start();
String hello = communicator.readUnencryptedLine();
if(!"HELLO".equals(hello)){
// Bad message. Close the socket immediately, and return.
s.close();
connectionWatcher.interrupt();
return;
}
// Ohhai
// Now get the version...
FederationVersion version;
String sVersion = communicator.readUnencryptedLine();
try {
version = FederationVersion.fromVersion(sVersion);
communicator.writeUnencryptedLine("VERSION OK");
} catch(IllegalArgumentException ex){
// The version is unsupported. The client is newer than this server knows how
// to deal with. So, write out the version error data, then close the socket and
// continue.
communicator.writeUnencryptedLine("VERSION BAD");
byte [] errorMsg = ("The server does not support the version of this client (" + sVersion + ")!").getBytes("UTF-8");
communicator.writeUnencryptedLine(Integer.toString(errorMsg.length));
communicator.writeUnencrypted(errorMsg);
s.close();
connectionWatcher.interrupt();
return;
}
// The rest of the code may vary based on the version.
if(version == FederationVersion.V1_0_0){
// Are we encrypted?
// TODO: This is currently unused
boolean isEncrypted = "1".equals(communicator.readUnencryptedLine());
// The rest of the data is sent possibly encrypted, so we use
// the normal form of the rest of these.
String clientPassword = communicator.readLine();
if(password != null){
// If the password is required by the server, we need to
// verify they got it correct. If it's not required, they
// were going to send a line anyways.
if(!password.equals(clientPassword)){
// Oops, wrong guess, try again...
communicator.writeLine("ERROR");
byte [] errorMsg = ("Wrong password").getBytes("UTF-8");
communicator.writeLine(Integer.toString(errorMsg.length));
communicator.writeBytes(errorMsg);
s.close();
connectionWatcher.interrupt();
return;
}
}
// Alright, we're connected!
communicator.writeLine("OK");
// We now allow the connection to idle.
connectionWatcher.interrupt();
}
} catch (IOException ex) {
Logger.getLogger(FederationServer.class.getName()).log(Level.SEVERE, null, ex);
}
}
}, "FederationServer-" + serverName + "-Connection " + subSockets.size()).start();
}
}
/**
* Closes and removes all sub sockets that have been inactive for X minutes.
*
* @param inactiveMS
*/
public void checkSocketTimeouts(long inactiveMS) {
Iterator<Socket> it = subSockets.keySet().iterator();
while (it.hasNext()) {
Socket s = it.next();
if (subSockets.get(s) < System.currentTimeMillis() - inactiveMS) {
try {
s.close();
} catch (IOException ex) {
Logger.getLogger(com.laytonsmith.core.functions.Federation.class.getName()).log(Level.SEVERE, null, ex);
}
it.remove();
}
}
}
/**
* Updates the activity of this socket.
*
* @param s
*/
public void updateSocketActivity(Socket s) {
if (subSockets.containsKey(s)) {
subSockets.put(s, System.currentTimeMillis());
}
}
/**
* Closes all the sockets this server is currently using. This includes the
* sub sockets that have been spawned off, as well as the server socket. Once
* this is called, the reference to the FederationServer object should be
* lost, as the object becomes useless at that point.
*/
public void closeAllSockets(){
try {
serverSocket.close();
} catch (IOException ex) {
Logger.getLogger(FederationServer.class.getName()).log(Level.SEVERE, null, ex);
}
for(Socket sub : subSockets.keySet()){
try {
sub.close();
} catch (IOException ex) {
Logger.getLogger(FederationServer.class.getName()).log(Level.SEVERE, null, ex);
}
}
closed = true;
}
@Override
@SuppressWarnings("FinalizeDeclaration")
protected void finalize() throws Throwable {
super.finalize();
if(!closed){
StreamUtils.GetSystemErr().println("FederationServer was not closed properly, and cleanup is having to be done in the finalize method!");
closeAllSockets();
}
}
}