Compare commits

...

86 Commits

Author SHA1 Message Date
Tristan B. Velloza Kildaire 24aaf05dd5 README
- Added coverage badge
2023-11-26 14:17:56 +02:00
Tristan B. Velloza Kildaire 313ff9b557 Connection
- Removed now-completed TODO
- Use a boolean to supress unreachable errors
- Added a TODO to implement the above
- On exiting loop call the `Server`'s `onConnectionDisconnect(Connection)` method
2023-11-26 14:15:59 +02:00
Tristan B. Velloza Kildaire 416d583658 Connection
- Added TODO (backported)
2023-11-26 14:13:12 +02:00
Tristan B. Velloza Kildaire d44cc03437 Server
- Added initial implementation of `onConnectionDisconnect(Connection connection)`
2023-11-26 14:12:04 +02:00
Tristan B. Velloza Kildaire bbcf9b1413 LinkType
- Added first member (so `LinkType.init`) `UNSET`

Connection

- Added `linkType` and getter
2023-11-26 14:09:25 +02:00
Tristan B. Velloza Kildaire 8df9c09f43 LinkType
- Added new enum
- With members `USER` and `SERVER`
2023-11-26 14:08:05 +02:00
Tristan B. Velloza Kildaire 8dbd906b7d MessageDeliveryTransport
- Documented methods
2023-11-23 09:45:59 +02:00
Tristan B. Velloza Kildaire 8a572978af MessageDeliveryTransport
- Documented the interface
2023-11-23 09:44:52 +02:00
Tristan B. Velloza Kildaire f11c81d599 Message
- Documented
2023-11-23 09:40:39 +02:00
Tristan B. Velloza Kildaire 9e137e4301 Server
- Print out the found `User*` (tostring it) when found in `onOutgoing(Message latest, Queue from)`
2023-11-23 09:26:50 +02:00
Tristan B. Velloza Kildaire 1d8c0044a8 USer
- Implemented `toString()`
2023-11-23 09:26:20 +02:00
Tristan B. Velloza Kildaire d9b95dac5f AuthManager
- Fixed null pointer dereference error in `getUser(string)` which would occur when a user entry was not found in the hashmap
2023-11-22 21:54:38 +02:00
Tristan B. Velloza Kildaire e54f7f2d8a Server
_ Print warnings when either `fromUser` or/and `toUser` lookups fail
2023-11-22 21:50:29 +02:00
Tristan B. Velloza Kildaire dd90aa7a5e Server
- Added stubs for null checking for the `User*`'s
2023-11-22 21:49:03 +02:00
Tristan B. Velloza Kildaire 9205c5cd06 AuthManager
- `getUser(string username)` will now return `null` if the user could not be found, else the `User*` is returned
2023-11-22 21:48:40 +02:00
Tristan B. Velloza Kildaire cef970944a Server
- When `onOutgoing(Message, QUeue)` is called lookup the `User*`s for the source and destination from the provided `Message`
2023-11-22 21:30:38 +02:00
Tristan B. Velloza Kildaire dff66decae Message
- Added `getFrom()`, `getDestination()` and `getMessage()`
2023-11-22 21:29:39 +02:00
Tristan B. Velloza Kildaire fe5f05d532
Update d.yml 2023-11-22 16:20:23 +02:00
Tristan B. Velloza Kildaire 4a3aa28357
Update d.yml 2023-11-22 16:19:50 +02:00
Tristan B. Velloza Kildaire 6f2f542580 Dub
- Require at minimum `gogga` version `2.2.1`

Logging

- Switched logger mode to a clearer one
2023-11-22 12:01:07 +02:00
Tristan B. Velloza Kildaire f5721bfc3c Queue
- Added some notes
2023-11-22 11:04:28 +02:00
Tristan B. Velloza Kildaire 0e479307a2 PolicyDecision
- Added new enum type

Queue

- Switched to using `DList` which maintains a head AND (most importantly) a TAIL which we can therefore reach in O(1) time obviously
- This above is useful for the `PolicyDecision` of `DROP_TAIL`
- The `enqueue(Message)` method now respects the result of `policyCheck()`
- Implemented `policyCheck()`
2023-11-22 11:01:52 +02:00
Tristan B. Velloza Kildaire 62c35787e3 MessageManager
- Enqueue messages for send-q and receive-q respectively
2023-11-22 10:54:07 +02:00
Tristan B. Velloza Kildaire b698576bce
Merge pull request #1 from renaissanceorg/user_record
User record
2023-11-22 10:47:05 +02:00
Tristan B. Velloza Kildaire 05345b8ce6 Connection
- Re-enabled validation checking
2023-11-22 10:42:23 +02:00
Tristan B. Velloza Kildaire 56d48d2645 User
- Now contains a  `Option*` map mapped to by a `string`
- Implemented `addOption(Option*)`

Option

- Added new type

AuthManager

- Added TODO
2023-11-22 10:33:20 +02:00
Tristan B. Velloza Kildaire 24534b7ae9 Connection
- Added comment
2023-11-22 10:32:21 +02:00
Tristan B. Velloza Kildaire d9f7ef7c2a Connection
- Disabled validation for now
2023-11-22 10:30:21 +02:00
Tristan B. Velloza Kildaire c36223ac08 Connection
- Cleaned up imports
- Switched to using a switch statement for handling the different `CommandType`s
2023-11-22 10:30:04 +02:00
Tristan B. Velloza Kildaire b120ba19e4 Connection
- Handle message sending
- Set the status to delivered
2023-11-21 14:11:17 +02:00
Tristan B. Velloza Kildaire 0096b7d4ac Server
- Added a `MessageManager`
- Implemented `getMessageManager()`
- Implemented the `MessageDeliveryTransport` interface by implementing `onIncoming(...)` and `onOugoing(...)`
2023-11-21 13:46:14 +02:00
Tristan B. Velloza Kildaire 0b252c07ba MessageManager
- Log calls to `sendq(Message)`
2023-11-21 13:45:28 +02:00
Tristan B. Velloza Kildaire 334a013104 Connection
- Set the `Message`'s from field to the current user
- Log message send
2023-11-21 13:45:00 +02:00
Tristan B. Velloza Kildaire b6c003d808 Connection
- Implemented `isAuthd()`
- Added `responseStatus` (not sure if I am going to be using it though)
- Added TODOs regarding `AUTH_COMMAND`
- Initial support for `CHANNEL_SEND_MESSAGE`; it now will discover all recipients, construct the respective `Message` and place it on the `MessageManager`'s send-queue
2023-11-20 16:16:24 +02:00
Tristan B. Velloza Kildaire e075e1f7e0 Message
- Implemented `setBody(string)`, `setFrom(string)` and `setDestination(string)`

MessageManager

- Migrated to using the `MessageDeliveryTransport` interface
2023-11-20 16:16:15 +02:00
Tristan B. Velloza Kildaire 386b86950f Queue
- Added the `enqueue(Message)` method
- Initialize a lock for the queue itself

MessageDeliveryTransport

- Added new interface

MessageManager

- Added stub `sendq(Message)`
- Added stub `recvq(Message)`
2023-11-20 15:37:42 +02:00
Tristan B. Velloza Kildaire cd6c07d209 Message
- Added message structure
2023-11-19 21:13:05 +02:00
Tristan B. Velloza Kildaire 6a493eb29c Message manager
- Added `QUEUE_DEFAULT_SIZE` with a value of `100`

Queue

- Added new type

MEssageManager

- Added basic message manager sub-system
- This is still a work-in-progress
2023-11-19 20:20:13 +02:00
Tristan B. Velloza Kildaire 6343121ba5 AuthManager
- Made `getUser(string)` public
- Added note to it
2023-11-19 16:12:05 +02:00
Tristan B. Velloza Kildaire e2d378f5a5 AuthManager
- Implemented `removeUser(string username)`
- Implemented `addUser(String)`
2023-11-19 16:07:27 +02:00
Tristan B. Velloza Kildaire 8781354616 DummyProvider
- Fixed
2023-11-19 15:09:30 +02:00
Tristan B. Velloza Kildaire 3d6e1da659 AuthManager
- FIxed API usage
2023-11-19 15:09:01 +02:00
Tristan B. Velloza Kildaire 04b7a87f78 AuthProvider
- No longer provide a `User`

DummyProvider

- API update
2023-11-19 15:08:32 +02:00
Tristan B. Velloza Kildaire 8a7e70e2a9 AuthManager
- Changed the `User[string]` to `User*[string]`
- Updated `getUser(string)` to use the new `User*[string]`
2023-11-19 14:50:07 +02:00
Tristan B. Velloza Kildaire ebd2df7af7 Server
- Added an instance of the `AuthManager`
- `attemptAuth(string, string)` now uses the authentication manager

Server (unittests)

- Disabled a unit test for now
2023-11-19 14:38:18 +02:00
Tristan B. Velloza Kildaire bfc5467f28 AuthProvider
- Added provider interface

DummyProvider

- Added a dummy authentication provider

AuthManager

- Implemented an initial authentication manager
2023-11-19 14:37:00 +02:00
Tristan B. Velloza Kildaire 8415fd7c9f User (unittests)
- Added more testing

User

- Updated names
- Implemented `getUsername()`
2023-11-18 13:58:52 +02:00
Tristan B. Velloza Kildaire 6e2e680b44 User
- Constructor now sets username
2023-11-18 13:54:07 +02:00
Tristan B. Velloza Kildaire dac50c74c6 User
- Implemented `setUsername(string username)`
- Implemented `getStatus()`
- Added status field

Status

- Added new enum
2023-11-18 13:53:48 +02:00
Tristan B. Velloza Kildaire b14f338695 User
- Added new type
- No default constructor as I need to actually initialize somethings

Users (unittests)

- Added a rudimentary unittest
2023-11-18 13:20:46 +02:00
Tristan B. Velloza Kildaire ad5eed9836 Server (unittests)
- Disabled import of `tristanable.queue` as it failed
2023-11-18 13:16:34 +02:00
Tristan B. Velloza Kildaire c7059f5a91 Connection
- Changed testing username from `tristan` to `bababooey`
- Set username based on the `AUTH_MESSAGE`'s value
- Print out current members of a channel when enumerating them
- Added support for `MEMBERSHIP_LEAVE`
2023-11-18 13:07:30 +02:00
Tristan B. Velloza Kildaire 4b436b21ee Connection
- Fixed handling of `MEMBERSHIP_JOIN` which was not joining but enumerating and generating an enumeration reply
- Added support for joining then and proper support for `MEMBERSHIP_LIST` (enumeration)
2023-11-18 12:30:38 +02:00
Tristan B. Velloza Kildaire c07b2185d5 Connection
- Added future TODO (maybe) for the `handle(TaggedMessage)` function
- Extract the `CommandType` at the top and use that instead of using a function call each time
- Added support for `MEMBERSHIP_JOIN` command type
- Added handling for unknown commands
2023-11-18 11:30:39 +02:00
Tristan B. Velloza Kildaire 111174d8fb Server
- Added `getChannelManager()`
2023-11-17 23:33:03 +02:00
Tristan B. Velloza Kildaire 5983bbb56b Channel
- Implemented `getMembers()`

ChannelManager

- Implemented `membershipList(string channel, ref string[] membersList)`
2023-11-17 23:32:47 +02:00
Tristan B. Velloza Kildaire 822959f324 ChannelManager
- Create a duplicate of channel list returned for safety
- This updates `getChannelNames(ulong offset, ubyte limit)`
2023-11-16 15:59:59 +02:00
Tristan B. Velloza Kildaire 44d9d53d02 ChannelManager
- Moved locking for `getChannelNames(ulong offset, ubyte limit)`
2023-11-16 15:59:14 +02:00
Tristan B. Velloza Kildaire e9fddfc55c ChannelManager
- Added some testing channels

Server

- We now construct a `ChannelManager` on construction
- `getChannelNames(ulong, ubyte)` now uses the `ChannelManager` to enumerate the channels
2023-11-16 14:54:08 +02:00
Tristan B. Velloza Kildaire 938ca897bd ChannelManager
- Fixed upper bound calculation when we overshot the length
2023-11-16 14:52:10 +02:00
Tristan B. Velloza Kildaire 3d813ee197 ChannelManager
- Fixed offset, it must be `<` and NOT `<=` as that would include the length of the array
2023-11-16 14:50:44 +02:00
Tristan B. Velloza Kildaire 9ec3020ee2 Channel
- Added a `Mutex` which is initialized on struct construction
- Added methods `hasMember(string username)`, `removeMember(string username)` and `addMember(string username)`

ChannelManager

- Use `addMember(string)` and `removeMember(string)` for `membershipJoin(string, string)` and `membershipLeave` respectively
2023-11-16 14:46:22 +02:00
Tristan B. Velloza Kildaire df517090a0 Channelmanager
- Added a logger
- Implemented `getChannelNames(ulong offset, ubyte limit)`
- Implemented `membershipLeave(string channel, string username)`
2023-11-16 08:38:46 +02:00
Tristan B. Velloza Kildaire 6a1e7bce72 Channelmanager
- Implemented the ability to join a channel
- This was added in the method `membershipJoin(string channel, string username)`
2023-11-15 23:58:07 +02:00
Tristan B. Velloza Kildaire 5fbe43bcea Channelmanager
- Calling `channelCreate(string channel)` now will only add a channel if it did not exist already
2023-11-15 23:32:28 +02:00
Tristan B. Velloza Kildaire 3859b75047 ChannelManager
- Added initial channel management code
2023-11-15 23:24:08 +02:00
Tristan B. Velloza Kildaire cb60e76937 Connection
- `handle(TaggedMessage)` now returns a `TaggedMessage`
- Only send a response if `handle(...)` returns a non-`null` entity
2023-11-15 22:30:56 +02:00
Tristan B. Velloza Kildaire d15645263f Connection
- Added channel enumeration support
2023-11-10 15:26:15 +02:00
Tristan B. Velloza Kildaire 355f89e83d Server
- Added `getChanelNames(ulong, ubyte)`
2023-11-10 13:40:43 +02:00
Tristan B. Velloza Kildaire d415a071c2 Connection
- Added proper handling of responses by setting the correct reply types, commands and so forth
- Re-use the tagged message, update payload and send response
2023-11-10 12:57:36 +02:00
Tristan B. Velloza Kildaire c917a33eb6 Dub
- Use at least version `0.2.2` for `dante`
- Use at least version `0.1.11` for `davinci`
2023-11-10 12:50:13 +02:00
Tristan B. Velloza Kildaire db1d0c2c2a Connection
- Added handling for `AUTH_COMMAND`

Server

- Added stub `attemptAuth(string, string)` method
2023-11-10 00:14:35 +02:00
Tristan B. Velloza Kildaire 322057e8e1 Dub
- Use `~master`
2023-11-09 23:03:18 +02:00
Tristan B. Velloza Kildaire 753924c631 Revert "Dub"
This reverts commit 32fc2915d2.
2023-11-09 21:31:43 +02:00
Tristan B. Velloza Kildaire 32fc2915d2 Dub
- Use `~master`
2023-11-09 21:23:17 +02:00
Tristan B. Velloza Kildaire 13b727fe80 Connection
- Fxed type

Dub

- Upgraded `davinci` and `dante`
2023-10-03 17:27:57 +02:00
Tristan B. Velloza Kildaire b8d739f4d3 Connection
- Removed already-completed TODO
- Added documentation for the `handle(TaggedMessage)`
2023-10-02 20:11:09 +02:00
Tristan B. Velloza Kildaire a608b685ff Connection
- When receiving a `NOP_COMMAND` echo it back
2023-10-01 22:41:43 +02:00
Tristan B. Velloza Kildaire 36a76e3a5d Connection
- Added TODO noting we need to wait on ANY QUEUE
2023-10-01 22:33:24 +02:00
Tristan B. Velloza Kildaire 17ccf9a4db Daemon
- When running (for now) listen on `/tmp/renaissance.sock`
2023-10-01 22:23:50 +02:00
Tristan B. Velloza Kildaire 33b2fe3fbd Server
- Made lekker
2023-10-01 22:23:13 +02:00
Tristan B. Velloza Kildaire da67dc3902 Dub
- Adjusted versions required
2023-10-01 21:27:27 +02:00
Tristan B. Velloza Kildaire 26de09c9e0 Connection
- Print out the `toString()` of the `Command` object
2023-05-06 13:45:08 +02:00
Tristan B. Velloza Kildaire a89fc1c7ef Server (unit tests)
- Create a new `DanteClient` to connect to the `Server` instance and send two `NopRequest`s
2023-05-06 13:38:54 +02:00
Tristan B. Velloza Kildaire 968604e0c8 - Working on `handle(TaggedMessage)`, now decodes using davinci 2023-05-06 13:38:26 +02:00
Tristan B. Velloza Kildaire 62e3ab8acd - Upgraded dependencies 2023-05-06 13:38:12 +02:00
10 changed files with 1407 additions and 57 deletions

View File

@ -6,9 +6,9 @@ name: D
on:
push:
branches: [ "master" ]
branches: [ "**" ]
pull_request:
branches: [ "master" ]
branches: [ "**" ]
permissions:
contents: read
@ -22,6 +22,12 @@ jobs:
- uses: actions/checkout@v3
- uses: dlang-community/setup-dlang@4c99aa991ce7d19dd3064de0a4f2f6b2f152e2d7
- name: Install Doveralls (code coverage tool)
run: |
dub fetch doveralls
sudo apt update
sudo apt install libcurl4-openssl-dev
- name: 'Build & Test'
run: |
# Build the project, with its main file included, without unittests
@ -29,4 +35,8 @@ jobs:
# Build and run tests, as defined by `unittest` configuration
# In this mode, `mainSourceFile` is excluded and `version (unittest)` are included
# See https://dub.pm/package-format-json.html#configurations
dub test --compiler=$DC
dub test --compiler=$DC --coverage
- name: Coverage upload
run: |
dub run doveralls -- -t ${{secrets.COVERALLS_REPO_TOKEN}}

View File

@ -13,4 +13,4 @@
<br>
<br>
[![D](https://github.com/renaissanceorg/renaissance/actions/workflows/d.yml/badge.svg)](https://github.com/renaissanceorg/renaissance/actions/workflows/d.yml)
[![D](https://github.com/renaissanceorg/renaissance/actions/workflows/d.yml/badge.svg)](https://github.com/renaissanceorg/renaissance/actions/workflows/d.yml) [![Coverage Status](https://coveralls.io/repos/github/renaissanceorg/renaissance/badge.svg?branch=feature/queueing)](https://coveralls.io/github/renaissanceorg/renaissance?branch=feature/queueing)

View File

@ -4,12 +4,12 @@
],
"copyright": "Copyright © 2023, Tristan B. Kildaire",
"dependencies": {
"dante": "0.1.1",
"davinci": "0.0.3",
"gogga": "2.1.18",
"dante": ">=0.2.2",
"davinci": ">=0.1.11",
"gogga": ">=2.2.1",
"lumars": "~>1.11.0",
"river": "~>0.3.7",
"tristanable": "3.2.0-beta"
"river": ">=0.3.7",
"tristanable": ">=4.0.0-beta"
},
"description": "Reference implementation of the DNET server protocol",
"license": "AGPL 3.0",

View File

@ -6,6 +6,26 @@ import renaissance.server;
import river.core;
import tristanable;
import renaissance.logging;
import renaissance.server.messagemanager : MessageManager, Message;
import davinci.base.components : Validatable;
import davinci.c2s.auth : AuthMessage, AuthResponse;
import davinci.c2s.generic : UnknownCommandReply;
import davinci.c2s.channels : ChannelEnumerateRequest, ChannelEnumerateReply, ChannelMembership, ChannelMessage;
import davinci.c2s.test : NopMessage;
import renaissance.server.channelmanager : ChannelManager, Channel;
import std.conv : to;
public enum LinkType
{
UNSET,
USER,
SERVER
}
public class Connection : Thread
{
@ -24,6 +44,12 @@ public class Connection : Thread
private Manager tManager;
private Queue incomingQueue;
/**
* Whether this is a user connection
* or a server link
*/
private LinkType linkType;
private this(Server associatedServer, Stream clientStream)
{
this.associatedServer = associatedServer;
@ -53,6 +79,11 @@ public class Connection : Thread
this.tManager.setDefaultQueue(this.incomingQueue);
}
public LinkType getLinkType()
{
return this.linkType;
}
private void worker()
{
// TODO: Start tristanable manager here
@ -65,8 +96,9 @@ public class Connection : Thread
// TODO: Well, we'd tasky I guess so I'd need to use it there I guess
// TODO: Add worker function here
while(true)
// TODO: Imp,ent nthe loop condition status (exit on error)
bool isGood = true;
while(isGood)
{
// TODO: Addn a tasky/tristanable queue managing thing with
// ... socket here (probably just the latter)
@ -87,13 +119,291 @@ public class Connection : Thread
logger.dbg("Awoken? after dequeue()");
// Process the message
handle(incomingMessage);
TaggedMessage response = handle(incomingMessage);
if(response !is null)
{
logger.dbg("There was a response, sending: ", response);
this.tManager.sendMessage(incomingMessage);
}
else
{
logger.dbg("There was no response, not sending anything.");
}
}
// Clean up (TODO: Shutdown the TManager)
// Clean up - notify disconnection
this.associatedServer.onConnectionDisconnect(this);
}
private void handle(TaggedMessage incomingMessage)
// FIXME: These should be part of the auth details
// ... associated with this user
string myUsername = "bababooey";
private bool isAuthd()
{
return myUsername.length != 0;
}
/**
* Given a `TaggedMessage` this method will decode
* it into a Davinci `BaseMessage`, determine the
* payload type via this header and then handle
* the message/command accordingly
*
* Params:
* incomingMessage = the `TaggedMessage`
* Returns: the response `TaggedMessage`, or
* `null` if no response is to be sent
*/
private TaggedMessage handle(TaggedMessage incomingMessage)
{
// TODO: In future this decoder, surely, should be idk
// ... in davinci as in stateful encoder/decoder
// ... reply-generator
logger.dbg("Examining message '"~incomingMessage.toString()~"' ...");
byte[] payload = incomingMessage.getPayload();
import davinci;
BaseMessage baseMessage = BaseMessage.decode(payload);
logger.dbg("Incoming message: "~baseMessage.getCommand().toString());
logger.dbg("BaseMessage type: ", baseMessage.getMessageType());
Command incomingCommand = baseMessage.getCommand();
CommandType incomingCommandType = baseMessage.getCommandType();
logger.dbg("Incoming CommandType: ", incomingCommandType);
BaseMessage response;
MessageType mType;
Command responseCommand;
CommandType responseType;
Status responseStatus;
/**
* Perform validation before continueing
*/
if(cast(Validatable)incomingCommand)
{
Validatable validtabaleCommand = cast(Validatable)incomingCommand;
string reason;
if(!validtabaleCommand.validate(reason))
{
logger.error("Validation failed with reason: '", reason, "'");
UnknownCommandReply unknownCmdReply = new UnknownCommandReply(reason);
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.UNKNOWN_COMMAND;
responseCommand = unknownCmdReply;
// TODO: Can we do this without gotos?
goto encode_n_send;
}
}
/**
* Handle the different types of commands
*/
switch(incomingCommandType)
{
/**
* Handle NOP commands
*/
case CommandType.NOP_COMMAND:
{
logger.dbg("We got a NOP");
NopMessage nopMessage = cast(NopMessage)baseMessage.getCommand();
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.NOP_COMMAND;
responseCommand = nopMessage;
break;
}
/**
* Handle authentication request
*/
case CommandType.AUTH_COMMAND:
{
AuthMessage authMessage = cast(AuthMessage)baseMessage.getCommand();
bool status = this.associatedServer.attemptAuth(authMessage.getUsername(), authMessage.getPassword());
// TODO: This is just for testing now - i intend to have a nice auth manager
AuthResponse authResp = new AuthResponse();
if(status)
{
authResp.good();
// Save username
this.myUsername = authMessage.getUsername();
}
else
{
authResp.bad();
}
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.AUTH_RESPONSE;
responseCommand = authResp;
break;
}
/**
* Handle channel list requests
*/
case CommandType.CHANNELS_ENUMERATE_REQ:
{
// FIXME: Figure out how we want to do auth checks
if(!isAuthd())
{
}
ChannelEnumerateRequest chanEnumReq = cast(ChannelEnumerateRequest)baseMessage.getCommand();
ubyte limit = chanEnumReq.getLimit();
ulong offset = chanEnumReq.getOffset();
string[] channelNames = this.associatedServer.getChannelNames(offset, limit);
ChannelEnumerateReply chanEnumRep = new ChannelEnumerateReply(channelNames);
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.CHANNELS_ENUMERATE_REP;
responseCommand = chanEnumRep;
break;
}
/**
* Handle channel joins
*/
case CommandType.MEMBERSHIP_JOIN:
{
ChannelMembership chanMemReq = cast(ChannelMembership)baseMessage.getCommand();
string channel = chanMemReq.getChannel();
// Join the channel
ChannelManager chanMan = this.associatedServer.getChannelManager();
bool status = chanMan.membershipJoin(channel, this.myUsername); // TODO: Handle return value
chanMemReq.replyGood();
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.MEMBERSHIP_JOIN_REP;
responseCommand = chanMemReq;
break;
}
/**
* Handle channel membership requests
*/
case CommandType.MEMBERSHIP_LIST:
{
ChannelMembership chanMemReq = cast(ChannelMembership)baseMessage.getCommand();
string channel = chanMemReq.getChannel();
// Obtain the current members
ChannelManager chanMan = this.associatedServer.getChannelManager();
string[] currentMembers;
// TODO: Handle return value
bool status = chanMan.membershipList(channel, currentMembers);
logger.dbg("Current members of '"~channel~"': ", currentMembers);
chanMemReq.listReplyGood(currentMembers);
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.MEMBERSHIP_LIST_REP;
responseCommand = chanMemReq;
break;
}
/**
* Handle channel leaves
*/
case CommandType.MEMBERSHIP_LEAVE:
{
ChannelMembership chanMemReq = cast(ChannelMembership)baseMessage.getCommand();
string channel = chanMemReq.getChannel();
// Join the channel
ChannelManager chanMan = this.associatedServer.getChannelManager();
bool status = chanMan.membershipLeave(channel, this.myUsername); // TODO: Handle return value
chanMemReq.replyGood();
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.MEMBERSHIP_LEAVE_REP;
responseCommand = chanMemReq;
break;
}
/**
* Handle message sending
*/
case CommandType.CHANNEL_SEND_MESSAGE:
{
ChannelMessage chanMesg = cast(ChannelMessage)baseMessage.getCommand();
// TODO: Get channel, lookup and do permission checks
// TODO: Use a messagemanager thing here
MessageManager mesgMan = this.associatedServer.getMessageManager();
// TODO: Check multiple recipients
string[] recipients = chanMesg.getRecipients();
foreach(string to; recipients)
{
Message message;
message.setBody(chanMesg.getMessage());
message.setFrom(this.myUsername);
message.setDestination(to);
logger.dbg("Sending message: ", message);
mesgMan.sendq(message);
}
// TODO: Set this ONLY if we succeeeded in delivery
chanMesg.messageDelivered();
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.SEND_CHANNEL_MESG_REP;
responseCommand = chanMesg;
break;
}
/**
* Anything else is an unknown
* command, therefore generate
* an error reply
*/
default:
{
logger.warn("Received unsupported message type", baseMessage);
UnknownCommandReply unknownCmdReply = new UnknownCommandReply("Command with type number: "~to!(string)(cast(ulong)incomingCommandType));
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.UNKNOWN_COMMAND;
responseCommand = unknownCmdReply;
logger.warn("We have generated err: ", responseCommand);
break;
}
}
encode_n_send:
// Generate response
response = new BaseMessage(mType, responseType, responseCommand);
// Construct a response using the same tag
// (for matching) but a new payload (the
// response message)
incomingMessage.setPayload(response.encode());
return incomingMessage;
}
/**

View File

@ -19,7 +19,8 @@ void main()
import renaissance.listeners;
import std.socket;
Address listenAddr = parseAddress("::1", 9091);
// Address listenAddr = parseAddress("::1", 9091);
Address listenAddr = new UnixAddress("/tmp/renaissance.sock");
StreamListener streamListener = StreamListener.create(server, listenAddr);
server.start();

View File

@ -8,4 +8,5 @@ __gshared static this()
{
logger = new GoggaLogger();
logger.enableDebug();
logger.mode(GoggaMode.RUSTACEAN_SIMPLE);
}

View File

@ -0,0 +1,315 @@
module renaissance.server.channelmanager;
import renaissance.server.server : Server;
import std.container.slist : SList;
import core.sync.mutex : Mutex;
import renaissance.logging;
public struct Channel
{
private SList!(string) members;
private string name;
// TODO: Actually use this
private Mutex channelLock;
this(string channelName)
{
this.channelLock = new Mutex();
this.name = channelName;
}
public bool hasMember(string username)
{
// Lock the channel
this.channelLock.lock();
// On exit
scope(exit)
{
this.channelLock.unlock();
}
// Search for membership
foreach(string member; this.members)
{
if(member == username)
{
return true;
}
}
return false;
}
public bool removeMember(string username)
{
// Lock the channel
this.channelLock.lock();
// On exit
scope(exit)
{
this.channelLock.unlock();
}
// Only remove if we have the member
if(hasMember(username))
{
// Remove ourselves from the channel
this.members.linearRemoveElement(username);
return true;
}
// Error if not present
else
{
return false;
}
}
public bool addMember(string username)
{
// Lock the channel
this.channelLock.lock();
// On exit
scope(exit)
{
this.channelLock.unlock();
}
// Only add if we are not yet present
if(!hasMember(username))
{
// Remove ourselves from the channel
this.members.insertAfter(this.members[], username);
return true;
}
// Error if present already
else
{
return false;
}
}
public string[] getMembers()
{
string[] arrCopy;
foreach(string member; this.members)
{
arrCopy ~= member;
}
return arrCopy;
}
}
public final class ChannelManager
{
private Server server;
/**
* Map of channel names to
* the respective channel
* descriptors
*/
private Channel[string] channels;
private Mutex channelsLock;
private this()
{
this.channelsLock = new Mutex();
// TODO: Disable later, this just adds some testing channels
// return ["#general", "#tomfoolery"];
channelCreate("#general");
channelCreate("#tomfoolery");
}
public static ChannelManager create(Server server)
{
ChannelManager manager = new ChannelManager();
manager.server = server;
return manager;
}
private Channel* getChannel(string channel)
{
// Lock channels map
this.channelsLock.lock();
// On exit
scope(exit)
{
// Unlock channels map
this.channelsLock.unlock();
}
// Return a Channel* IF
// a value for that key exists
return channel in this.channels;
}
private Channel* channelGet(string channel)
{
return getChannel(channel);
}
public bool channelExists(string channel)
{
return getChannel(channel) !is null;
}
public bool channelCreate(string channel)
{
// Lock channels map
this.channelsLock.lock();
// On exit
scope(exit)
{
// Unlock channels map
this.channelsLock.unlock();
}
if(channelExists(channel))
{
return false;
}
// Add a new channel descriptor
Channel channelDesc = Channel(channel);
this.channels[channel] = channelDesc;
return true;
}
public string[] getChannelNames(ulong offset, ubyte limit)
{
// Lock channels map
this.channelsLock.lock();
// TODO: Implement offset and limit
// Adjust offset if it overshoots available
// items
if(!(offset < this.channels.length))
{
offset = 0;
}
ulong upperBound = offset+limit;
logger.dbg("Upper bound (before): ", upperBound);
if(upperBound >= this.channels.keys().length)
{
upperBound = this.channels.keys().length;
}
logger.dbg("Upper bound (after): ", upperBound);
logger.dbg("Limit: ", limit);
// Get the channels
string[] channels = this.channels.keys()[offset..upperBound].dup;
// Unlock channels map
this.channelsLock.unlock();
return channels;
}
// NOTE: In future we could lock just the channel entry?
// (once it has been found)
public bool membershipJoin(string channel, string username)
{
// Lock channels map
this.channelsLock.lock();
// TODO: Move lock
// On exit
scope(exit)
{
// Unlock channels map
this.channelsLock.unlock();
}
// Get the channel, check for our own membership
Channel* channelDesc = channelGet(channel);
// If not found, then that's an error
if(channelDesc is null)
{
return false;
}
// TODO: Run any policies here
// If not, then add user and it is fine
//
// Return value is whether or not
// adding succeeded
return channelDesc.addMember(username);
// TODO: Run notification hooks here on the server
}
public bool membershipList(string channel, ref string[] membersList)
{
// Get the channel, check for our own membership
Channel* channelDesc = channelGet(channel);
// If not found, then that's an error
if(channelDesc is null)
{
return false;
}
// If found, get the members
membersList = channelDesc.getMembers();
return true;
}
public bool membershipLeave(string channel, string username)
{
// Lock channels map
this.channelsLock.lock();
// TODO: Move lock
// On exit
scope(exit)
{
// Unlock channels map
this.channelsLock.unlock();
}
// Get the channel, check for our own membership
Channel* channelDesc = channelGet(channel);
// If not found, then that's an error
if(channelDesc is null)
{
return false;
}
// TODO: Run any policies here
// If present, then leave
//
// Return value is whether or not
// removal succeeded
return channelDesc.removeMember(username);
// TODO: Run notification hooks here on the server
}
}
unittest
{
ChannelManager chanMan = new ChannelManager();
// TODO: Add testing here
}

View File

@ -0,0 +1,250 @@
module renaissance.server.messagemanager;
import renaissance.server.server : Server;
import std.container.dlist : DList;
import core.sync.mutex : Mutex;
import renaissance.logging;
/**
* An in-memory representation of
* a message
*/
public struct Message
{
private string destination;
private string message;
private string from;
/**
* Constructs a new message
*
* Params:
* destination = the destination
* from = the from user
* message = the message itself
*/
this(string destination, string from, string message)
{
this.destination = destination;
this.from = from;
this.message = message;
}
/**
* Sets the message's body
*
* Params:
* message = the contents
*/
public void setBody(string message)
{
this.message = message;
}
/**
* Sets the from paramneter
*
* Params:
* from = the username
*/
public void setFrom(string from)
{
this.from = from;
}
/**
* Sets the destination of this message
*
* Params:
* destination = the username
*/
public void setDestination(string destination)
{
this.destination = destination;
}
/**
* Returns the contents of this
* message
*
* Returns: the contents
*/
public string getBody()
{
return this.message;
}
/**
* Returns the from parameter
*
* Returns: the username
*/
public string getFrom()
{
return this.from;
}
/**
* Returns the destination
*
* Returns: the username
*/
public string getDestination()
{
return this.destination;
}
}
public enum QUEUE_DEFAULT_SIZE = 100;
public enum PolicyDecision
{
DROP_INCOMING,
DROP_TAIL,
ACCEPT
}
// TODO: Templatize in the future on the T element type
public class Queue
{
private size_t maxSize;
private DList!(Message) queue;
private Mutex lock;
public this(size_t maxSize = QUEUE_DEFAULT_SIZE)
{
this.lock = new Mutex();
}
public void enqueue(Message message)
{
// Lock the queue
this.lock.lock();
// On exit
scope(exit)
{
// Unlock the queue
this.lock.unlock();
}
// Apply queuing policy
PolicyDecision decision = policyCheck();
logger.dbg("Queue decision: ", decision);
// If we should tail-drop
if(decision == PolicyDecision.DROP_TAIL)
{
// Drop tail
this.queue.removeBack();
}
// If we should drop the incoming
else if(decision == PolicyDecision.DROP_INCOMING)
{
// Do not insert
return;
}
// Accept
else if(decision == PolicyDecision.ACCEPT)
{
// Fall through
}
// Enqueue
this.queue.insertAfter(this.queue[], message);
}
private PolicyDecision policyCheck() // NOTE: In future must use lock if decision requires anlysing internal queue
{
// TODO: Implement me
return PolicyDecision.ACCEPT;
}
}
/**
* Defines an interface of methods
* which are to be called whenever
* new messages are enqueued onto
* a so-called "incoming" (recv-q)
* and "outgoing" (send-q) queues
*
* The `MessageManager` will use
* these as the hooks it applies
* to its send/recv queues.
*
* An example usage of this is
* to allow `Server` to get notified
* whenever a new item appears.
*/
public interface MessageDeliveryTransport
{
/**
* Called when a message has just been
* enqueued to the incoming queue
*
* Params:
* latest = the latest message
* from = the queue
* Returns: `true` if you handled
* this without error, `false`
* otherwise
*/
public bool onIncoming(Message latest, Queue from);
/**
* Called when a message has just been
* enqueued to the outgoing queue
*
* Params:
* latest = the latest message
* from = the queue
* Returns: `true` if you handled
* this without error, `false`
* otherwise
*/
public bool onOutgoing(Message latest, Queue from);
}
// TODO: Should have a thread that manages
// ... message delivery by just calling something
// ... in server (it must handle encoding and
// ... so forth)
public class MessageManager
{
private MessageDeliveryTransport transport;
private Queue sendQueue;
private Queue receiveQueue;
private this()
{
// Initialize the queues (send+receive)
this.sendQueue = new Queue();
this.receiveQueue = new Queue();
}
public void sendq(Message message)
{
logger.info("Received message for sending: ", message);
// Enqueue to send-q
this.sendQueue.enqueue(message);
}
public void recvq(Message message)
{
logger.info("Received message for reception: ", message);
// Enqueue to recv-q
this.receiveQueue.enqueue(message);
}
public static MessageManager create(MessageDeliveryTransport transport)
{
MessageManager manager = new MessageManager();
manager.transport = transport;
return manager;
}
}

View File

@ -7,13 +7,16 @@ import std.algorithm : canFind;
import renaissance.exceptions;
import renaissance.connection;
import renaissance.logging;
import renaissance.server.channelmanager;
import renaissance.server.users;
import renaissance.server.messagemanager;
/**
* Represents an instance of the daemon which manages
* all listeners attached to it, server state and
* message processing
*/
public class Server
public class Server : MessageDeliveryTransport
{
// TODO: array of listeners
private SList!(Listener) listenerQ;
@ -26,12 +29,32 @@ public class Server
// TODO: volatility
private bool isRunning = false;
// TODO: Add constructor
private ChannelManager channelManager;
// TODO: Some sendq/recq mechanism with messages or something
// ... should be placed here
private AuthManager authManager;
private MessageManager messageManager;
/**
* Constructs a new server
*/
this()
{
/* Initialize all mutexes */
this.listenerQLock = new Mutex();
this.connectionQLock = new Mutex();
/* Initialize the channel management sub-system */
this.channelManager = ChannelManager.create(this);
/* Initialize the authentication management sub-system */
this.authManager = AuthManager.create(this); // TODO: Set custo provder here based on argument to this constructor
/* Initialize the message management sub-system */
this.messageManager = MessageManager.create(this);
}
@ -174,6 +197,99 @@ public class Server
/* Unlock the connection queue */
connectionQLock.unlock();
}
public bool attemptAuth(string username, string password)
{
logger.dbg("Attempting auth with user '", username, "' and password '", password, "'");
return this.authManager.authenticate(username, password);
}
public string[] getChannelNames(ulong offset, ubyte limit)
{
// TODO: Implement me
return this.channelManager.getChannelNames(offset, limit);
}
public ChannelManager getChannelManager()
{
return this.channelManager;
}
public MessageManager getMessageManager()
{
return this.messageManager;
}
// On incoming message
public bool onIncoming(Message latest, Queue from)
{
// TODO: Implement me
logger.info("Incoming stub with latest ", latest, "from queue ", from);
return true;
}
// On message that must be egressed
public bool onOutgoing(Message latest, Queue from)
{
// TODO: Implement me
logger.info("Outgoing stub with latest ", latest, "from queue ", from);
// Lookup the user (source)
User* fromUser = this.authManager.getUser(latest.getFrom());
// Lookup the user (destination)
User* toUser = this.authManager.getUser(latest.getDestination());
if(fromUser == null)
{
// TODO: Handle this
logger.warn("Could not find fromUser (User* was null)");
}
else
{
logger.dbg("Found fromUser (User*)", fromUser.toString());
}
if(toUser == null)
{
// TODO: Handle this
logger.warn("Could not find toUser (User* was null)");
}
else
{
logger.dbg("Found toUser (User*)", toUser.toString());
}
return true;
}
// On connection disconnecting
public void onConnectionDisconnect(Connection connection)
{
// TODO: Decide whether it is a user link or a server de-link
import renaissance.connection.connection : LinkType;
LinkType type = connection.getLinkType();
logger.dbg("Disconnecting link ", connection, " of type ", type);
switch(type)
{
case LinkType.UNSET:
logger.warn("Not doing anything because this link's type was never set");
break;
case LinkType.USER:
// TODO: Implement me
break;
case LinkType.SERVER:
// TODO: Implement me
break;
default:
break;
}
}
}
version(unittest)
@ -184,56 +300,64 @@ version(unittest)
// TODO: Building a testing client with the imports below
import std.socket;
import tristanable.manager;
import tristanable.queue;
// import tristanable.queue;
import tristanable.encoding;
import core.thread;
import dante;
}
unittest
{
/**
* Setup a `Server` instance followed by
* creating a single listener, after this
* start the server
*/
Server server = new Server();
// Address listenAddr = parseAddress("::1", 9091);
Address listenAddr = new UnixAddress("/tmp/renaissance2.sock");
StreamListener streamListener = StreamListener.create(server, listenAddr);
server.start();
// unittest
// {
// /**
// * Setup a `Server` instance followed by
// * creating a single listener, after this
// * start the server
// */
// Server server = new Server();
// // Address listenAddr = parseAddress("::1", 9091);
// Address listenAddr = new UnixAddress("/tmp/renaissance2.sock");
// StreamListener streamListener = StreamListener.create(server, listenAddr);
// server.start();
scope(exit)
{
import std.stdio;
remove((cast(UnixAddress)listenAddr).path().ptr);
}
// scope(exit)
// {
// import std.stdio;
// remove((cast(UnixAddress)listenAddr).path().ptr);
// }
// /**
// * Create a few clients here (TODO: We'd need the client code)
// */
// for(ulong idx = 0; idx < 10; idx++)
// {
// Socket clientSocket = new Socket(listenAddr.addressFamily(), SocketType.STREAM);
// clientSocket.connect(listenAddr);
// Manager manager = new Manager(clientSocket);
// Queue myQueue = new Queue(69);
// manager.registerQueue(myQueue);
// manager.start();
// // /**
// // * Create a few clients here (TODO: We'd need the client code)
// // */
// // for(ulong idx = 0; idx < 10; idx++)
// // {
// // Socket clientSocket = new Socket(listenAddr.addressFamily(), SocketType.STREAM);
// // clientSocket.connect(listenAddr);
// // Manager manager = new Manager(clientSocket);
// // Queue myQueue = new Queue(69);
// // manager.registerQueue(myQueue);
// // manager.start();
// // Thread.sleep(dur!("seconds")(2));
// TaggedMessage myMessage = new TaggedMessage(69, cast(byte[])"ABBA");
// manager.sendMessage(myMessage);
// manager.sendMessage(myMessage);
// // Thread.sleep(dur!("seconds")(2));
// manager.sendMessage(myMessage);
// manager.sendMessage(myMessage);
// }
// // // Thread.sleep(dur!("seconds")(2));
// // TaggedMessage myMessage = new TaggedMessage(69, cast(byte[])"ABBA");
// // manager.sendMessage(myMessage);
// // manager.sendMessage(myMessage);
// // // Thread.sleep(dur!("seconds")(2));
// // manager.sendMessage(myMessage);
// // manager.sendMessage(myMessage);
// // }
DanteClient client = new DanteClient(new UnixAddress("/tmp/renaissance2.sock"));
// DanteClient client = new DanteClient(new UnixAddress("/tmp/renaissance2.sock"));
// client.start();
// client.nopRequest();
// client.nopRequest();
// while(true)
// Thread.sleep(dur!("seconds")(20));
}
// // while(true)
// // {
// // Thread.sleep(dur!("seconds")(20));
// // }
// }

View File

@ -0,0 +1,339 @@
module renaissance.server.users;
import core.sync.mutex : Mutex;
public struct Option
{
// TODO: Implement me
private string name;
private ubyte[] value;
private string description;
private Mutex lock;
@disable
private this();
public this(string name)
{
this.lock = new Mutex();
this.name = name;
}
public string getName()
{
// Lock
this.lock.lock();
// On exit
scope(exit)
{
// Unlock
this.lock.unlock();
}
// NOTE: String is immutable but incase of cast
return name.dup;
}
public void setName(string name)
{
// Lock
this.lock.lock();
// On exit
scope(exit)
{
// Unlock
this.lock.unlock();
}
// Copy the argument
this.name = name.dup;
}
public ubyte[] getValue()
{
// Lock
this.lock.lock();
// On exit
scope(exit)
{
// Unlock
this.lock.unlock();
}
return this.value.dup;
}
public void setValue(ubyte[] value)
{
// Lock
this.lock.lock();
// On exit
scope(exit)
{
// Unlock
this.lock.unlock();
}
// Copy the argument
this.value = value.dup;
}
}
public enum Status
{
ONLINE,
OFFLINE,
INVISIBLE,
AWAY
}
public struct User
{
private string username;
private Status status;
private Option*[string] options; // Profile key-value
private Mutex lock;
@disable
private this();
this(string username)
{
this.lock = new Mutex();
setUsername(username);
}
// TODO: Disallow parameter less construction?
public bool setUsername(string username)
{
// Username cannot be empty (TODO: Have a regex check)
if(username.length == 0)
{
return false;
}
// Lock
this.lock.lock();
// Set the username
this.username = username;
// Unlock
this.lock.unlock();
return true;
}
public string getUsername()
{
string usernameCpy;
// Lock
this.lock.lock();
// Get the username
usernameCpy = this.username;
// Unlock
this.lock.unlock();
return usernameCpy;
}
public Status getStatus()
{
Status statusCpy;
// Lock
this.lock.lock();
// Get the status
statusCpy = this.status;
// Unlock
this.lock.unlock();
return statusCpy;
}
public void addOption(Option* option)
{
// Lock
this.lock.lock();
// Insert the option
this.options[option.getName()] = option;
// Unlock
this.lock.unlock();
}
public string toString()
{
import std.conv : to;
return "User [username: "~getUsername()~", status: "~to!(string)(getStatus())~"]";
}
}
unittest
{
User u = User("deavmi");
assert(u.getUsername(), "deavmi");
// Change the username
u.setUsername("gustav");
assert(u.getUsername(), "gustav");
}
public interface AuthProvider
{
public bool authenticate(string username, string password);
}
public class DummyProvider : AuthProvider
{
public bool authenticate(string username, string password)
{
return true;
}
}
import renaissance.server.server : Server;
import renaissance.logging;
// Should handle all users authenticated and
// act as an information base for the current
// users
public class AuthManager
{
private Server server;
// TODO: Need an AuthProvider here
private AuthProvider provider;
/**
* TODO: We need to find a way to easily
* manage User* mapped to by a string (username)
* and how updating the username (key) would
* work (including the allocated value)
* then
*
* Update: We won't expose this User*
* to the public API as that means
* you can manipulate the user
* there (that is fine) but ALSO
* replace the entire user there
*
* Nah, forget the above we should discern
* between username (never changing)
* and nick
*
* UPDATE2: We will STILL need to index (somehow)
* on that then, perhaps a seperate
*
* What is the point of usernames? for
* auth but then nick is what people _should_
* see when you `membershipList()`.
*
* So we would need to update
* ChannelManager code to do that
*
*/
private User*[string] users;
private Mutex usersLock;
private this(AuthProvider provider)
{
this.usersLock = new Mutex();
this.provider = provider;
}
// NOTE: Don't try de-allocate it, smart ass
public User* getUser(string username)
{
// Lock
this.usersLock.lock();
// On exit
scope(exit)
{
// Unlock
this.usersLock.unlock();
}
// Check if such a user exists
User** potentialUserPtrPtr = username in this.users;
if(potentialUserPtrPtr == null)
{
return null;
}
else
{
return *potentialUserPtrPtr;
}
}
private void addUser(string username)
{
// Lock
this.usersLock.lock();
// Create the user and insert it
User* newUser = new User(username);
this.users[username] = newUser;
// Unlock
this.usersLock.unlock();
}
private void removeUser(string username)
{
// Lock
this.usersLock.lock();
// Remove the user
this.users.remove(username);
// Unlock
this.usersLock.unlock();
}
public bool authenticate(string username, string password)
{
logger.dbg("Authentication request for user '"~username~"' with password '"~password~"'");
bool status;
// TODO: Disallow the username from being empty
status = this.provider.authenticate(username, password);
if(status)
{
addUser(username);
logger.info("Authenticated user '"~username~"'");
}
else
{
logger.error("Authentication failed for user '"~username~"'");
}
return status;
}
public static AuthManager create(Server server, AuthProvider provider = new DummyProvider())
{
AuthManager manager = new AuthManager(provider);
manager.server = server;
return manager;
}
}