Merge pull request #1 from renaissanceorg/user_record

User record
This commit is contained in:
Tristan B. Velloza Kildaire 2023-11-22 10:47:05 +02:00 committed by GitHub
commit b698576bce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 733 additions and 159 deletions

View File

@ -6,6 +6,19 @@ 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 class Connection : Thread
{
@ -104,6 +117,11 @@ public class Connection : Thread
// ... 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
@ -128,6 +146,7 @@ public class Connection : Thread
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);
@ -135,136 +154,221 @@ public class Connection : Thread
MessageType mType;
Command responseCommand;
CommandType responseType;
Status responseStatus;
if(incomingCommandType == CommandType.NOP_COMMAND)
/**
* Perform validation before continueing
*/
if(cast(Validatable)incomingCommand)
{
import davinci.c2s.test;
logger.dbg("We got a NOP");
NopMessage nopMessage = cast(NopMessage)baseMessage.getCommand();
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.NOP_COMMAND;
responseCommand = nopMessage;
}
// Handle authentication request
else if(incomingCommandType == CommandType.AUTH_COMMAND)
{
import davinci.c2s.auth : AuthMessage, AuthResponse;
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
this.myUsername = authMessage.getUsername();
AuthResponse authResp = new AuthResponse();
if(status)
Validatable validtabaleCommand = cast(Validatable)incomingCommand;
string reason;
if(!validtabaleCommand.validate(reason))
{
authResp.good();
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;
}
else
}
/**
* Handle the different types of commands
*/
switch(incomingCommandType)
{
/**
* Handle NOP commands
*/
case CommandType.NOP_COMMAND:
{
authResp.bad();
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());
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.AUTH_RESPONSE;
responseCommand = authResp;
}
// Handle channel list requests
else if(incomingCommandType == CommandType.CHANNELS_ENUMERATE_REQ)
{
import davinci.c2s.channels : ChannelEnumerateRequest, ChannelEnumerateReply;
// TODO: This is just for testing now - i intend to have a nice auth manager
AuthResponse authResp = new AuthResponse();
if(status)
{
authResp.good();
ChannelEnumerateRequest chanEnumReq = cast(ChannelEnumerateRequest)baseMessage.getCommand();
ubyte limit = chanEnumReq.getLimit();
ulong offset = chanEnumReq.getOffset();
// Save username
this.myUsername = authMessage.getUsername();
}
else
{
authResp.bad();
}
string[] channelNames = this.associatedServer.getChannelNames(offset, limit);
ChannelEnumerateReply chanEnumRep = new ChannelEnumerateReply(channelNames);
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.AUTH_RESPONSE;
responseCommand = authResp;
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.CHANNELS_ENUMERATE_REP;
responseCommand = chanEnumRep;
}
// Handle channel joins
else if(incomingCommandType == CommandType.MEMBERSHIP_JOIN)
{
import davinci.c2s.channels : ChannelMembership;
import renaissance.server.channelmanager : ChannelManager, Channel;
break;
}
/**
* Handle channel list requests
*/
case CommandType.CHANNELS_ENUMERATE_REQ:
{
// FIXME: Figure out how we want to do auth checks
if(!isAuthd())
{
ChannelMembership chanMemReq = cast(ChannelMembership)baseMessage.getCommand();
string channel = chanMemReq.getChannel();
}
ChannelEnumerateRequest chanEnumReq = cast(ChannelEnumerateRequest)baseMessage.getCommand();
ubyte limit = chanEnumReq.getLimit();
ulong offset = chanEnumReq.getOffset();
// Join the channel
ChannelManager chanMan = this.associatedServer.getChannelManager();
bool status = chanMan.membershipJoin(channel, this.myUsername); // TODO: Handle return value
chanMemReq.replyGood();
string[] channelNames = this.associatedServer.getChannelNames(offset, limit);
ChannelEnumerateReply chanEnumRep = new ChannelEnumerateReply(channelNames);
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.MEMBERSHIP_JOIN_REP;
responseCommand = chanMemReq;
}
// Handle channel membership requests
else if(incomingCommandType == CommandType.MEMBERSHIP_LIST)
{
import davinci.c2s.channels : ChannelMembership;
import renaissance.server.channelmanager : ChannelManager, Channel;
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.CHANNELS_ENUMERATE_REP;
responseCommand = chanEnumRep;
logger.error("HALO");
break;
}
/**
* Handle channel joins
*/
case CommandType.MEMBERSHIP_JOIN:
{
ChannelMembership chanMemReq = cast(ChannelMembership)baseMessage.getCommand();
string channel = chanMemReq.getChannel();
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();
// Obtain the current members
ChannelManager chanMan = this.associatedServer.getChannelManager();
string[] currentMembers;
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: Handle return value
bool status = chanMan.membershipList(channel, currentMembers);
logger.dbg("Current members of '"~channel~"': ", currentMembers);
chanMemReq.listReplyGood(currentMembers);
// TODO: Get channel, lookup and do permission checks
mType = MessageType.CLIENT_TO_SERVER;
responseType = CommandType.MEMBERSHIP_LIST_REP;
responseCommand = chanMemReq;
// 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;
}
}
// Handle channel leaves
else if(incomingCommandType == CommandType.MEMBERSHIP_LEAVE)
{
import davinci.c2s.channels : ChannelMembership;
import renaissance.server.channelmanager : ChannelManager, Channel;
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;
}
// Unsupported type for server
else
{
import davinci.c2s.generic : UnknownCommandReply;
import std.conv : to;
logger.warn("Received unsupported message type", baseMessage);
// TODO: Generate error here
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);
}
encode_n_send:
// Generate response
response = new BaseMessage(mType, responseType, responseCommand);

View File

@ -0,0 +1,113 @@
module renaissance.server.messagemanager;
import renaissance.server.server : Server;
import std.container.slist : SList;
import core.sync.mutex : Mutex;
import renaissance.logging;
public struct Message
{
private string destination;
private string message;
private string from;
this(string destination, string from, string message)
{
this.destination = destination;
this.from = from;
this.message = message;
}
public void setBody(string message)
{
this.message = message;
}
public void setFrom(string from)
{
this.from = from;
}
public void setDestination(string destination)
{
this.destination = destination;
}
}
public enum QUEUE_DEFAULT_SIZE = 100;
public class Queue
{
private size_t maxSize;
private SList!(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();
}
// Enqueue
this.queue.insertAfter(this.queue[], message);
}
}
public interface MessageDeliveryTransport
{
// On incoming message
public bool onIncoming(Message latest, Queue from);
// On message that must be egressed
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);
}
public void recvq(Message message)
{
}
public static MessageManager create(MessageDeliveryTransport transport)
{
MessageManager manager = new MessageManager();
manager.transport = transport;
return manager;
}
}

View File

@ -8,13 +8,15 @@ 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;
@ -32,6 +34,10 @@ public class Server
// TODO: Some sendq/recq mechanism with messages or something
// ... should be placed here
private AuthManager authManager;
private MessageManager messageManager;
/**
* Constructs a new server
*/
@ -43,6 +49,12 @@ public class Server
/* 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);
}
@ -190,9 +202,7 @@ public class Server
{
logger.dbg("Attempting auth with user '", username, "' and password '", password, "'");
// TODO: Implement me
return true;
return this.authManager.authenticate(username, password);
}
public string[] getChannelNames(ulong offset, ubyte limit)
@ -205,6 +215,29 @@ public class Server
{
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);
return true;
}
}
version(unittest)
@ -222,57 +255,57 @@ version(unittest)
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.start();
client.nopRequest();
client.nopRequest();
// client.nopRequest();
// client.nopRequest();
// while(true)
// {
// Thread.sleep(dur!("seconds")(20));
// }
}
// // while(true)
// // {
// // Thread.sleep(dur!("seconds")(20));
// // }
// }

View File

@ -0,0 +1,324 @@
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();
}
}
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)
{
User* foundUser;
// Lock
this.usersLock.lock();
foundUser = this.users[username];
// Unlock
this.usersLock.unlock();
return foundUser;
}
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;
}
}