besterd/source/connection/connection.d

535 lines
13 KiB
D

module connection.connection;
import utils.debugging : debugPrint; /* TODO: Stephen */
import std.conv : to;
import std.socket : Socket, AddressFamily, SocketType, ProtocolType, parseAddress, SocketFlags, Address;
import core.thread : Thread;
import std.stdio : writeln, File;
import std.json : JSONValue, parseJSON, JSONException, JSONType, toJSON;
import std.string : cmp;
import handlers.handler : MessageHandler;
import server.server : BesterServer;
import handlers.response : ResponseError, HandlerResponse;
import utils.message : receiveMessage, sendMessage;
import base.net : NetworkException;
import base.types : BesterException;
public final class BesterConnection : Thread
{
/* The socket to the client */
private Socket clientConnection;
/* The server backend */
public BesterServer server;
/* The client's credentials */
private string username;
private string password;
/* If the connection is active */
private bool isActive = true;
/* The connection scope */
public enum Scope
{
CLIENT,
SERVER,
UNKNOWN
}
/* The type of this connection */
private Scope connectionType = Scope.UNKNOWN;
/* Get the type of the connection */
public Scope getType()
{
return connectionType;
}
/* Get the socket */
public Socket getSocket()
{
return clientConnection;
}
this(Socket clientConnection, BesterServer server)
{
/* Save socket and set thread worker function pointer */
super(&run);
this.clientConnection = clientConnection;
this.server = server;
debugPrint("New client handler spawned for " ~ clientConnection.remoteAddress().toAddrString());
}
/**
* Shutdown the BesterConnection by stopping
* the read-write loop and closing the socket.
*/
public void shutdown()
{
/* TODO: Send message posssibly, think about this for listeners and informers (etc.) too */
isActive = false;
}
override public string toString()
{
return username ~ "@" ~ clientConnection.remoteAddress().toAddrString();
}
/**
* Returns an array of the username and password.
*/
public string[] getCredentials()
{
return [username, password];
}
/* Read/send loop */
private void run()
{
debugPrint("<<< Begin read/send loop >>>");
while(isActive)
{
/* Received JSON message */
JSONValue receivedMessage;
/* Attempt to receive a message */
try
{
/* Receive a message */
receiveMessage(clientConnection, receivedMessage);
/**
* If the message was received successfully then
* process the message. */
processMessage(receivedMessage);
/* Check if this is a server connection, if so, end the connection */
if(connectionType == Scope.SERVER)
{
debugPrint("Server connection done, closing BesterConnection.");
shutdown();
}
}
catch(BesterException exception)
{
debugPrint("Error in read/write loop: " ~ exception.toString());
break;
}
}
debugPrint("<<< End read/send loop >>>");
/* Close the socket */
clientConnection.close();
/* TODO: Remove myself from the connections array */
}
/**
* Destructor for BesterConnection
*
* Upon the client disconnecting, the only reference to
* this object should be through the `connections` array
* in the instance of BesterServer but because that will
* be removed in the end of the `run` function call.
*
* And because the thread ends thereafter, there will be
* no reference there either.
*
* Only then will this function be called by the garbage-
* collector, this will provide the remaining clean ups.
*/
~this()
{
debugPrint("Destructor for \"" ~ this.toString() ~ "\" running...");
/* Close the socket to the client */
clientConnection.close();
debugPrint("Closed socket to client");
debugPrint("Destructor finished");
}
/* TODO: Comment [], rename [] */
/**
* Dispatches the message to the correct message handler.
*
* Returns `true` on success or partial success, `false`
* on fatal protocol error.
*/
private bool dispatchMessage(Scope scopeField, JSONValue payloadBlock)
{
/* The payload type */
string payloadType;
/* The payload data */
JSONValue payloadData;
/* The payload tag */
string payloadTag;
/* Attempt to parse protocol-critical fields */
try
{
/* Get the payload type */
payloadType = payloadBlock["type"].str;
debugPrint("Payload type is \"" ~ payloadType ~ "\"");
/* Get the payload data */
payloadData = payloadBlock["data"];
/* Get the payload tag */
payloadTag = payloadBlock["id"].str();
}
catch(JSONException e)
{
debugPrint("Fatal error when processing packet, missing fields");
return false;
}
/* Lookup the payloadType handler */
MessageHandler chosenHandler = server.findHandler(payloadType);
/* Check if it is a dummy type */
if(cmp(payloadType, "dummy") == 0)
{
/* Construct a dummy response */
JSONValue dummyMessage;
/* Construct a header block */
JSONValue headerBlock;
headerBlock["status"] = "0";
/* Attach the header block */
dummyMessage["header"] = headerBlock;
/* Construct the payload block */
JSONValue dummyPayloadBlock;
dummyPayloadBlock["data"] = null;
dummyPayloadBlock["type"] = payloadType;
dummyPayloadBlock["id"] = payloadTag;
/* Attach the payload block */
dummyMessage["payload"] = dummyPayloadBlock;
try
{
/* Send the message */
sendMessage(clientConnection, dummyMessage);
}
catch(NetworkException e)
{
debugPrint("Error sending status message, fatal closing connection");
/* TODO: We should deactivate the connection when this happens */
return false;
}
}
/* Check if the payload is a built-in command */
else if(cmp(payloadType, "builtin") == 0)
{
/* TODO: Implement me */
debugPrint("Built-in payload type");
/**
* Built-in commands follow the structure of
* "command" : {"type" : "cmdType", "command" : ...}
*/
JSONValue commandBlock = payloadData["command"];
string commandType = commandBlock["type"].str;
//JSONValue command = commandBlock["args"];
/* If the command is `close` */
if(cmp(commandType, "close") == 0)
{
debugPrint("Closing socket...");
isActive = false;
// sendStatus(0, JSONValue());
}
else
{
debugPrint("Invalid built-in command type");
/* TODO: Generate error response */
// dispatchStatus = false;
/* TODO: Send a response as the "builtin" message handler */
}
}
/* If an external handler is found (i.e. not a built-in command) */
else if(chosenHandler)
{
/* TODO: Implement me */
debugPrint("Chosen handler for payload type \"" ~ payloadType ~ "\" is " ~ chosenHandler.getPluginName());
try
{
/* Provide the handler the message and let it process it and send us a reply */
HandlerResponse handlerResponse = chosenHandler.handleMessage(payloadData);
/* TODO: Continue here, we will make all error handling do on construction as to make this all more compact */
debugPrint("<<< Message Handler [" ~ chosenHandler.getPluginName() ~ "] response >>>\n\n" ~ handlerResponse.toString());
/* Execute the message handler's command (as per its reply) and pass in the tag */
handlerResponse.execute(this, payloadTag);
}
catch(ResponseError e)
{
/* In the case of an error with the message handler, send an error to the client/server */
/* TODO: Clean up comments */
/* Send error message to client */
sendStatusReport(StatusType.FAILURE, payloadTag);
}
/* TODO: Be more specific with errors and reporting in the future */
catch(Exception e)
{
/* TODO: Remove me */
debugPrint("fhjhfsdjhfdjhgsdkjh UUUUH:" ~e.toString());
/* Send error message to client */
sendStatusReport(StatusType.FAILURE, payloadTag);
}
debugPrint("Handler section done (for client)");
}
/* If no message handler for the specified type could be found */
else
{
/* TODO: Implement error handling */
debugPrint("No handler available for payload type \"" ~ payloadType ~ "\"");
/* Send error message to client */
sendStatusReport(StatusType.FAILURE, payloadTag);
}
return true;
}
/**
* Type of the status report.
* Either 0 (for success) or 1 (for failure).
*/
public enum StatusType
{
SUCCESS,
FAILURE
}
/**
* Send a status report for the message with id
* `id` of type `StatusType`.
*/
public void sendStatusReport(StatusType statusType, string id)
{
/* Construct the response */
JSONValue statusMessage;
/* Construct the header block */
JSONValue headerBlock;
headerBlock["status"] = statusType == 0 ? "good" : "bad";
headerBlock["messageType"] = "statusReport";
/* Attach the header block */
statusMessage["header"] = headerBlock;
/* Create the payload block */
JSONValue payloadBlock;
payloadBlock["id"] = id;
/* Attach the payload block */
statusMessage["payload"] = payloadBlock;
try
{
/* Send the message */
sendMessage(clientConnection, statusMessage);
}
catch(NetworkException e)
{
debugPrint("Error sending status message");
}
}
/**
* Given the headerBlock, this returns the requested scope
* of the connection.
*/
private Scope getConnectionScope(JSONValue headerBlock)
{
/* TODO: Type checking and bounds checking */
/* Get the scope block */
JSONValue scopeBlock = headerBlock["scope"];
string scopeString = scopeBlock.str();
if(cmp(scopeString, "client") == 0)
{
return Scope.CLIENT;
}
else if(cmp(scopeString, "server") == 0)
{
return Scope.SERVER;
}
return Scope.UNKNOWN;
}
/**
* Sends an error message on fatal error.
* Used before client shutdown on such
* an error.
*/
private void sendFatalMessage()
{
/* TODO: Implement me */
}
/* Process the received message */
private void processMessage(JSONValue jsonMessage)
{
/* Attempt to convert the message to JSON */
try
{
/* Convert message to JSON */
debugPrint("<<< Received JSON >>>\n\n" ~ jsonMessage.toPrettyString());
/* Get the header */
JSONValue headerBlock = jsonMessage["header"];
/**
* Check to see if this connection is currently "untyped".
*
* If it is then we set the type.
*/
if(connectionType == Scope.UNKNOWN)
{
/* Get the scope of the message */
Scope scopeField = getConnectionScope(headerBlock);
/* TODO: Authenticate if client, else do ntohing for server */
/* Decide what action to take depending on the scope */
if(scopeField == Scope.UNKNOWN)
{
/* If the host-provided `scope` field was invalid */
debugPrint("Host provided scope was UNKNOWN");
/* TODO: Send message back about an invalid scope */
/* Send fatal message */
sendFatalMessage();
/* Stop the read/write loop */
shutdown();
return;
}
else if(scopeField == Scope.CLIENT)
{
/**
* If the host-provided `scope` field is `Scope.CLIENT`
* then we must attempt authentication, if it fails
* send the client a message back and then close the
* connection.
*/
debugPrint("Client scope enabled");
bool authenticationStatus;
/* The `authentication` block */
JSONValue authenticationBlock = headerBlock["authentication"];
/* Get the username and password */
string username = authenticationBlock["username"].str(), password = authenticationBlock["password"].str();
/* Attempt authentication */
authenticationStatus = server.authenticate(username, password);
/* Check if the authentication was successful or not */
if(authenticationStatus)
{
/**
* If the authentication was successful then store the
* client's credentials.
*/
this.username = username;
this.password = password;
/* Send error message to client */
// sendStatus(5, JSONValue());
/* TODO: Send authentication success */
sendStatusReport(StatusType.SUCCESS, "auth_special");
}
/* If authentication failed due to malformed message or incorrect details */
else
{
/**
* If the authentication was unsuccessful then send a
* message to the client stating so and close the connection.
*/
debugPrint("Authenticating the user failed, sending error and closing connection.");
/* Send fatal message */
sendFatalMessage();
/* Stop the read/write loop */
shutdown();
return;
}
}
else if(scopeField == Scope.SERVER)
{
debugPrint("Server scope enabled");
}
/* Set the connection type to `scopeField` */
connectionType = scopeField;
if(connectionType == Scope.CLIENT)
{
return;
}
}
/* Get the `payload` block */
JSONValue payloadBlock = jsonMessage["payload"];
debugPrint("<<< Payload is >>>\n\n" ~ payloadBlock.toPrettyString());
/**
* Dispatch the message. If a fatal failure is
* detected then the connection will be shutdown.
*/
if(dispatchMessage(connectionType, payloadBlock))
{
debugPrint("Dispatch succeeded");
}
else
{
debugPrint("Dispatch failed, deactivating connection...");
/* Send fatal message */
sendFatalMessage();
/* Shutdown the connection */
shutdown();
}
}
/* If the attempt to convert the message to JSON fails */
catch(JSONException exception)
{
debugPrint("Fatal format error, deactivating connection...");
/* Send fatal message */
sendFatalMessage();
/* Shutdown the connection */
shutdown();
}
}
}