package com.laytonsmith.core.federation;
import com.laytonsmith.PureUtilities.Common.StreamUtils;
import com.laytonsmith.persistence.DataSourceException;
import com.laytonsmith.persistence.PersistenceNetwork;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This class manages the master socket connection. If there are multiple server
* names within the same process, there only needs to be one master socket, and
* then, there only needs to be a master socket if no other process on the
* machine has the master socket for a given port. This class manages having a
* master socket running for each listening port, but only if needed.
*/
public class FederationMasterSocket {
private static FederationMasterSocket defaultInstance;
/**
* Creates, if needed, the default instance of the master socket.
*
* @return
*/
public static FederationMasterSocket getFederationMasterSocket() {
if (defaultInstance == null) {
defaultInstance = new FederationMasterSocket();
}
return defaultInstance;
}
/**
* Clears the default instance of the master socket, and shuts down the old
* one if it was already created, and running.
*/
public static void clearFederationMasterSocket() {
if (defaultInstance != null) {
defaultInstance.closeAll();
defaultInstance = null;
}
}
private FederationMasterSocket() {
//
}
private Map<Integer, ServerSocket> servers;
private boolean closed = false;
/**
* Closes all the master sockets, for all open ports.
*/
public void closeAll() {
for (ServerSocket socket : servers.values()) {
try {
socket.close();
closed = true;
} catch (IOException ex) {
Logger.getLogger(FederationMasterSocket.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
/**
* Closes the master socket for the given port, but only that one. If there
* is no master socket on that port, nothing happens, and false is returned.
* If it was closed successfully, true is returned.
*
* @param port
* @return
* @throws java.io.IOException
*/
public boolean close(int port) throws IOException {
if (servers.containsKey(port)) {
servers.get(port).close();
servers.remove(port);
return true;
} else {
return false;
}
}
/**
* Ensures the master socket is open for the given port. If it is already up
* and running, nothing happens.
*
* @param pn The persistence network, for finding the registration of a particular server.
* @param port The port the master socket should be listening on.
* @throws java.io.IOException
*/
public void ensureMasterSocketOpen(final PersistenceNetwork pn, int port) throws IOException {
if (!servers.containsKey(port) || servers.get(port).isClosed()) {
if (Federation.available(port)) {
// We're the first server to register on this port, so
// we need to actually start it up.
final ServerSocket masterSocket = new ServerSocket(port);
servers.put(port, masterSocket);
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
final Socket s = masterSocket.accept();
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.
}
}
}, "FederationMasterSocketConnectionWatcher-" + s.hashCode());
connectionWatcher.start();
FederationCommunication communicator = new FederationCommunication(new BufferedInputStream(s.getInputStream()),
new BufferedOutputStream(s.getOutputStream()));
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) {
String command = communicator.readUnencryptedLine();
if ("GET PORT".equals(command)) {
String serverName = communicator.readUnencryptedLine();
String value = pn.get(new String[]{"federation", serverName});
if(value != null){
FederationRegistration reg = FederationRegistration.fromJSON(value);
if(reg.updatedSince(Federation.DEAD_SERVER_TIMEOUT)){
int port = reg.getPort();
communicator.writeLine("OK");
communicator.writeLine(Integer.toString(port));
s.close();
connectionWatcher.interrupt();
return;
}
}
byte[] errorMsg = ("The server \"" + serverName + "\" could not be found on this host.").getBytes("UTF-8");
communicator.writeUnencryptedLine("ERROR");
communicator.writeUnencryptedLine(Integer.toString(errorMsg.length));
communicator.writeUnencrypted(errorMsg);
s.close();
connectionWatcher.interrupt();
} else {
// Programming error.
s.close();
connectionWatcher.interrupt();
}
}
} catch (IOException | DataSourceException ex) {
Logger.getLogger(FederationMasterSocket.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}, "FederationMasterSocket-Port " + port).start();
}
}
}
@Override
@SuppressWarnings("FinalizeDeclaration")
protected void finalize() throws Throwable {
super.finalize();
if (!closed) {
StreamUtils.GetSystemErr().println("FederationMasterSocket was not closed properly, and cleanup is having to be done in the finalize method!");
closeAll();
}
}
}