diff --git a/dub.json b/dub.json index a2f3e25..f77c423 100644 --- a/dub.json +++ b/dub.json @@ -7,7 +7,7 @@ "dependencies": { "dlog": "0.3.19", "eventy": "0.4.0", - "libsnooze": "0.3.3" + "libsnooze": "1.0.0-beta" }, "description": "A sane IRC framework for the D language", "license": "LGPL-3.0", diff --git a/source/birchwood/client/client.d b/source/birchwood/client/client.d index 8b312a0..5e05c72 100644 --- a/source/birchwood/client/client.d +++ b/source/birchwood/client/client.d @@ -8,7 +8,7 @@ import core.sync.mutex : Mutex; import core.thread : Thread, dur; import std.string; import eventy : EventyEvent = Event, Engine, EventType, Signal; -import birchwood.config : ConnectionInfo; +import birchwood.config; import birchwood.client.exceptions : BirchwoodException, ErrorType; import birchwood.protocol.messages : Message, encodeMessage, decodeMessage, isValidText; @@ -80,6 +80,11 @@ public class Client : Thread */ this.receiver = new ReceiverThread(this); this.sender = new SenderThread(this); + + /** + * Set defaults in db + */ + setDefaults(this.connInfo); } /** @@ -161,7 +166,7 @@ public class Client : Thread import birchwood.protocol.constants : ReplyType; - if(commandReply.getReplyType() == ReplyType.RPL_BOUNCE) + if(commandReply.getReplyType() == ReplyType.RPL_ISUPPORT) { // TODO: Testing code was here // logger.log(); @@ -183,7 +188,18 @@ public class Client : Thread // logger.log("<<<>>>"); // logger.log(); + import std.stdio; + writeln("Support stuff: ", commandReply.getKVPairs()); + /* Fetch and parse the received key-value pairs */ + string[string] receivedKV = commandReply.getKVPairs(); + foreach(string key; receivedKV.keys()) + { + /* Update the db */ + string value = receivedKV[key]; + connInfo.updateDB(key, value); + logger.log("Updated key in db '"~key~"' with value '"~value~"'"); + } } } @@ -201,9 +217,21 @@ public class Client : Thread /* Ensure no illegal characters in nick name */ if(isValidText(nickname)) { - /* Set the nick */ - Message nickMessage = new Message("", "NICK", nickname); - sendMessage(nickMessage); + // TODO: We could investigate this later if we want to be safer + ulong maxNickLen = connInfo.getDB!(ulong)("MAXNICKLEN"); + + /* If the username's lenght is within the allowed bounds */ + if(nickname.length <= maxNickLen) + { + /* Set the nick */ + Message nickMessage = new Message("", "NICK", nickname); + sendMessage(nickMessage); + } + /* If not */ + else + { + throw new BirchwoodException(ErrorType.NICKNAME_TOO_LONG); + } } else { @@ -785,6 +813,9 @@ public class Client : Thread /* Start the socket read-decode loop */ this.start(); + + /* Do the /NICK and /USER handshake */ + doAuth(); } catch(SocketOSException e) { @@ -798,6 +829,44 @@ public class Client : Thread } } + /** + * Performs the /NICK and /USER handshake. + * + * This method will set the hostname to be equal to the chosen + * username in the ConnectionInfo struct + * + * Params: + * servername = the servername to use (default: bogus.net) + */ + private void doAuth(string servername = "bogus.net") + { + Thread.sleep(dur!("seconds")(2)); + nick(connInfo.nickname); + + Thread.sleep(dur!("seconds")(2)); + // TODO: Note I am making hostname the same as username always (is this okay?) + // TODO: Note I am making the servername always bogus.net + user(connInfo.username, connInfo.username, servername, connInfo.realname); + } + + /** + * Performs user identification + * + * Params: + * username = the username to identify with + * hostname = the hostname to use + * servername = the servername to use + * realname = your realname + */ + public void user(string username, string hostname, string servername, string realname) + { + // TODO: Implement me properly with all required checks + + /* User message */ + Message userMessage = new Message("", "USER", username~" "~hostname~" "~servername~" "~":"~realname); + sendMessage(userMessage); + } + /** * Adds a given message onto the receieve queue for @@ -943,9 +1012,21 @@ public class Client : Thread /* Receieve at most 512 bytes (as per RFC) */ ptrdiff_t bytesRead = socket.receive(currentData, SocketFlags.PEEK); - import std.stdio; - // writeln(bytesRead); - // writeln(currentData); + version(unittest) + { + import std.stdio; + writeln("(peek) bytesRead: '", bytesRead, "' (status var or count)"); + writeln("(peek) currentData: '", currentData, "'"); + + // On remote end closing connection + if(bytesRead == 0) + { + writeln("About to do the panic!"); + *cast(byte*)0 = 2; + } + } + + /* FIXME: CHECK BYTES READ FOR SOCKET ERRORS! */ @@ -1019,7 +1100,7 @@ public class Client : Thread scratch.length = bytesRead; this.socket.receive(scratch); - + /* TODO: Yield here and in other places before continue */ @@ -1034,12 +1115,11 @@ public class Client : Thread unittest { - /* FIXME: Get domaina name resolution support */ // ConnectionInfo connInfo = ConnectionInfo.newConnection("irc.freenode.net", 6667, "testBirchwood"); //freenode: 149.28.246.185 //snootnet: 178.62.125.123 //bonobonet: fd08:8441:e254::5 - ConnectionInfo connInfo = ConnectionInfo.newConnection("worcester.community.networks.deavmi.assigned.network", 6667, "testBirchwood"); + ConnectionInfo connInfo = ConnectionInfo.newConnection("worcester.community.networks.deavmi.assigned.network", 6667, "birchwood", "doggie", "Tristan B. Kildaire"); // // Set the fakelag to 1 second // connInfo.setFakeLag(1); @@ -1047,17 +1127,22 @@ public class Client : Thread // Create a new Client Client client = new Client(connInfo); + // Authenticate client.connect(); // TODO: The below should all be automatic, maybe once IRCV3 is done // ... we should automate sending in NICK and USER stuff - Thread.sleep(dur!("seconds")(2)); - // client.command(new Message("", "NICK", "birchwood")); // TODO: add nickcommand - client.nick("birchwood"); + // Thread.sleep(dur!("seconds")(2)); + // client.nick("birchwood"); + + // Thread.sleep(dur!("seconds")(2)); + // client.command(new Message("", "USER", "doggie doggie irc.frdeenode.net :Tristan B. Kildaire")); + // client.user("doggie", "doggie", "irc.frdeenode.net", "Tristan B. Kildaire"); + + + - Thread.sleep(dur!("seconds")(2)); - client.command(new Message("", "USER", "doggie doggie irc.frdeenode.net :Tristan B. Kildaire")); Thread.sleep(dur!("seconds")(4)); // client.command(new Message("", "JOIN", "#birchwood")); @@ -1165,8 +1250,29 @@ public class Client : Thread */ client.leaveChannel(["#birchwoodLeave2"]); + + /** + * Definately by now we would have learnt the new MAXNICLEN + * which on BonoboNET is 30, hence the below should work + */ + try + { + client.nick("birchwood123456789123456789123"); + assert(true); + } + catch(BirchwoodException e) + { + assert(false); + } + // TODO: Don't forget to re-enable this when done testing! - Thread.sleep(dur!("seconds")(15)); - client.quit(); + Thread.sleep(dur!("seconds")(4)); + client.joinChannel("#birchwood"); + while(true) + { + Thread.sleep(dur!("seconds")(15)); + } + + // client.quit(); } } \ No newline at end of file diff --git a/source/birchwood/client/exceptions.d b/source/birchwood/client/exceptions.d index db01916..7258c38 100644 --- a/source/birchwood/client/exceptions.d +++ b/source/birchwood/client/exceptions.d @@ -66,7 +66,18 @@ public enum ErrorType * If invalid parameters are passed * to any of the text formatting functions */ - INVALID_FORMATTING + INVALID_FORMATTING, + + /** + * If a key-lookup in the ConnInfo failed + */ + DB_KEY_NOT_FOUND, + + /** + * If the requested nickname (via /NICK) is + * too long + */ + NICKNAME_TOO_LONG } /** diff --git a/source/birchwood/client/receiver.d b/source/birchwood/client/receiver.d index 3b964ab..a847660 100644 --- a/source/birchwood/client/receiver.d +++ b/source/birchwood/client/receiver.d @@ -20,6 +20,11 @@ import std.string : indexOf; import birchwood.client.events : PongEvent, IRCEvent; import std.string : cmp; +version(unittest) +{ + import std.stdio : writeln; +} + /** * Manages the receive queue and performs * message parsing and event triggering @@ -104,10 +109,6 @@ public final class ReceiverThread : Thread * an event depending on the type of message * * Handles PINGs along with normal messages - * - * TODO: Our high load average is from here - * ... it is getting lock a lot and spinning here - * ... we should use libsnooze to avoid this */ private void recvHandlerFunc() { @@ -130,7 +131,28 @@ public final class ReceiverThread : Thread // TODO: See above notes about libsnooze behaviour due // ... to usage in our context - receiveEvent.wait(); // TODO: Catch any exceptions from libsnooze + try + { + receiveEvent.wait(); + } + catch(InterruptedException e) + { + version(unittest) + { + writeln("wait() interrupted"); + } + continue; + } + catch(FatalException e) + { + // TODO: This should crash and end + version(unittest) + { + writeln("wait() had a FATAL error!!!!!!!!!!!"); + } + continue; + } + diff --git a/source/birchwood/client/sender.d b/source/birchwood/client/sender.d index c51c423..0de1df0 100644 --- a/source/birchwood/client/sender.d +++ b/source/birchwood/client/sender.d @@ -14,6 +14,11 @@ import libsnooze; import birchwood.client; +version(unittest) +{ + import std.stdio : writeln; +} + /** * Manages the send queue */ @@ -90,9 +95,6 @@ public final class SenderThread : Thread /** * The send queue worker function - * - * TODO: Same issue as recvHandlerFunc - * ... we should I/O wait (sleep) here */ private void sendHandlerFunc() { @@ -116,7 +118,30 @@ public final class SenderThread : Thread // TODO: See above notes about libsnooze behaviour due // ... to usage in our context - sendEvent.wait(); // TODO: Catch any exceptions from libsnooze + try + { + sendEvent.wait(); + } + catch(InterruptedException e) + { + version(unittest) + { + writeln("wait() interrupted"); + } + continue; + } + catch(FatalException e) + { + // TODO: This should crash and end + version(unittest) + { + writeln("wait() had a FATAL error!!!!!!!!!!!"); + } + continue; + } + + + // TODO: After the above call have a once-off call to `ensure()` here // ... which then only runs once and sets a `ready` flag for the Client diff --git a/source/birchwood/config/conninfo.d b/source/birchwood/config/conninfo.d index c9d01f0..8960a54 100644 --- a/source/birchwood/config/conninfo.d +++ b/source/birchwood/config/conninfo.d @@ -5,6 +5,7 @@ module birchwood.config.conninfo; import std.socket : SocketException, Address, getAddress; import birchwood.client.exceptions; +import std.conv : to, ConvException; /** * Represents the connection details for a server @@ -20,7 +21,17 @@ public shared struct ConnectionInfo /** * Nickname to use */ - private string nickname; + public string nickname; + + /** + * Username + */ + public string username; + + /** + * Real name + */ + public string realname; /** * Size to use to dequeue bytes @@ -40,6 +51,12 @@ public shared struct ConnectionInfo */ public const string quitMessage; + /** + * Key-value pairs learnt from the + * server + */ + private string[string] db; + /* TODO: before publishing change this bulk size */ /** @@ -52,11 +69,13 @@ public shared struct ConnectionInfo * bulkReadSize = the dequeue read size * quitMessage = the message to use when quitting */ - private this(Address addrInfo, string nickname, ulong bulkReadSize = 20, string quitMessage = "birchwood client disconnecting...") + private this(Address addrInfo, string nickname, string username, string realname, ulong bulkReadSize = 20, string quitMessage = "birchwood client disconnecting...") { // NOTE: Not sure if much mutable in Address anyways this.addrInfo = cast(shared Address)addrInfo; this.nickname = nickname; + this.username = username; + this.realname = realname; this.bulkReadSize = bulkReadSize; this.quitMessage = quitMessage; @@ -116,6 +135,52 @@ public shared struct ConnectionInfo this.fakeLag = fakeLag; } + /** + * Update a value in the key-value pair database + * + * Params: + * key = the key to set + * value = the value to set to + */ + public void updateDB(string key, string value) + { + db[key] = value; + } + + /** + * Retrieve a value from the key-value pair database + * + * Params: + * key = the key to lookup + * Returns: the value as type T, if not able to convert then T.init + * Throws: + * BirchwoodException if the key is not found + */ + public T getDB(T)(string key) + { + if(key in db) + { + /* Attempt conversion into T */ + try + { + /* Fetch and convert */ + T value = to!(T)(db[key]); + return value; + } + /* If conversion to type T fails */ + catch(ConvException e) + { + /* Return the initial value for such a paremeter */ + return T.init; + } + } + else + { + throw new BirchwoodException(ErrorType.DB_KEY_NOT_FOUND, "Could not find key '"~key~"'"); + } + } + + /** * Creates a ConnectionInfo struct representing a client configuration which * can be provided to the Client class to create a new connection based on its @@ -128,7 +193,7 @@ public shared struct ConnectionInfo * * Returns: ConnectionInfo for this server */ - public static ConnectionInfo newConnection(string hostname, ushort port, string nickname) + public static ConnectionInfo newConnection(string hostname, ushort port, string nickname, string username, string realname) { try { @@ -144,7 +209,7 @@ public shared struct ConnectionInfo /* TODO: Add feature to choose which address to use, prefer v4 or v6 type of thing */ Address chosenAddress = addrInfo[0]; - return ConnectionInfo(chosenAddress, nickname); + return ConnectionInfo(chosenAddress, nickname, username, realname); } catch(SocketException e) { @@ -162,7 +227,7 @@ public shared struct ConnectionInfo { try { - newConnection("1.", 21, "deavmi"); + newConnection("1.", 21, "deavmi", "thedeavmi", "Tristan Brice Birchwood Kildaire"); assert(false); } catch(BirchwoodException e) @@ -172,7 +237,7 @@ public shared struct ConnectionInfo try { - newConnection("1.1.1.1", 21, ""); + newConnection("1.1.1.1", 21, "", "thedeavmi", "Tristan Brice Birchwood Kildaire"); assert(false); } catch(BirchwoodException e) @@ -181,4 +246,17 @@ public shared struct ConnectionInfo } } +} + +/** + * Sets the default values as per rfc1459 in the + * key-value pair DB + * + * Params: + * connInfo = a reference to the ConnectionInfo struct to update + */ +public void setDefaults(ref ConnectionInfo connInfo) +{ + /* Set the `MAXNICKLEN` to a default of 9 */ + connInfo.updateDB("MAXNICKLEN", "9"); } \ No newline at end of file diff --git a/source/birchwood/config/package.d b/source/birchwood/config/package.d index 1bb8dfd..82b1e2d 100644 --- a/source/birchwood/config/package.d +++ b/source/birchwood/config/package.d @@ -6,4 +6,4 @@ module birchwood.config; /** * Connection information */ -public import birchwood.config.conninfo : ConnectionInfo; \ No newline at end of file +public import birchwood.config.conninfo : ConnectionInfo, setDefaults; \ No newline at end of file diff --git a/source/birchwood/protocol/constants.d b/source/birchwood/protocol/constants.d index b1d6d3e..0863eec 100644 --- a/source/birchwood/protocol/constants.d +++ b/source/birchwood/protocol/constants.d @@ -149,7 +149,6 @@ public enum ReplyType : ulong RPL_MYPORTIS = 384, ERR_BADCHANMASK = 476, - /** * rfc 2812 */ @@ -157,9 +156,17 @@ public enum ReplyType : ulong RPL_YOURHOST = 2, RPL_CREATED = 3, RPL_MYINFO = 4, - RPL_BOUNCE = 5, // TODO: We care about the key-value pairs here in RPL_BOUNCE + RPL_BOUNCE = 10, // In ircv3 this changed from 005 to 010 ERR_NOCHANMODES = 477, + /** + * ircv3 + */ + RPL_LOCALUSERS = 265, + RPL_GLOBALUSERS = 266, + RPL_WHOISCERTFP = 276, + RPL_ISUPPORT = 5, // This overrides the old rfc2812 RPL_BOUNCE code (we can only support one of these) + /** diff --git a/source/birchwood/protocol/messages.d b/source/birchwood/protocol/messages.d index 62f7488..47e195c 100644 --- a/source/birchwood/protocol/messages.d +++ b/source/birchwood/protocol/messages.d @@ -428,7 +428,7 @@ public final class Message bool hasTrailer; string[] paramsSplit = splitting(params, hasTrailer); - logger.debug_("ParamsSPlit direct:", paramsSplit); + // logger.debug_("ParamsSPlit direct:", paramsSplit); @@ -440,7 +440,7 @@ public final class Message /* Remove it from the parameters */ paramsSplit = paramsSplit[0..$-1]; - logger.debug_("GOt railer ", trailing); + // logger.debug_("GOt railer ", trailing); } ppPairs = paramsSplit; @@ -461,7 +461,7 @@ public final class Message /* Save the trailing */ ppTrailing = trailing; - logger.debug_("ppTrailing: ", ppTrailing); + // logger.debug_("ppTrailing: ", ppTrailing); } }