#define _GNU_SOURCE #include #include #include #include #include #include #include #include "broadcast.h" #include "command.h" #include "db.h" #include "event.h" #include "net.h" #include "firebase.h" #include "user_data.h" #include "util.h" #include "config.h" #define MAX_MESSAGE_LEN 10000 struct cmd_retval{ bool socket_close; bool memzero; }; #define RET_OK ((struct cmd_retval){.socket_close=false,.memzero=false}) #define RET_CLOSE(close_) ((struct cmd_retval){.socket_close=(close_),.memzero=false}) #define RET_MEMZERO ((struct cmd_retval){.socket_close=false,.memzero=true}) #define RET_MEMZERO_CLOSE(close_) ((struct cmd_retval){.socket_close=(close_),.memzero=true}) #define ERR_VERSION_UNSUPP "Version not supported" #define ERR_USER_ALREADY_EXISTS "Username already exists" #define ERR_INVALID_CREDS "Invalid credentials" #define ERR_NOLOGIN "Not logged in" #define ERR_NOROOM "Room not found" #define ERR_NOUSER "User not found" #define ERR_USER_ALREADY_IN_ROOM "User already in that room" #define ERR_MSG_TOO_LONG "Message too long" #define ERR_NOMSG "Message not found" #define ERR_NOREPLYMSG "Replied-to message not found" #define ERR_REPLYMSG_TIMETRAVEL "Replied-to message later than target timestamp" #define ERR_SENDAT_UNAVAIL_3 "sendat unavailable in protocol version 3" #define ERR_SENDAT_FORBIDDEN "sendat not allowed" static struct cmd_retval cmd_version(struct conn_data *data,const char *tag,const char **args){ i64 version; if (!parse_i64(args[0], &version) || version < MIN_SUPPORTED_PROTOCOL_VERSION || version > PROTOCOL_VERSION) { data->protversion = -1; net_send_error(data->fd, tag, ERR_VERSION_UNSUPP); return RET_OK; } data->protversion = version; net_send_ok(data->fd, tag); return RET_OK; } static struct cmd_retval cmd_register(struct conn_data *data,const char *tag,const char **args){ i64 userid=db_find_user(args[0]); if(userid!=-1){ net_send_error(data->fd,tag,ERR_USER_ALREADY_EXISTS); return RET_OK; } db_create_user(args[0],args[1]); return RET_MEMZERO_CLOSE(net_send_ok(data->fd,tag)); } static struct cmd_retval cmd_login(struct conn_data *data,const char *tag,const char **args){ // TODO: use sodium_mlock correctly for the password // Note: this function has exactly ONE return point, so that it is easier // to see that we indeed return MEMZERO. i64 userid=db_find_user(args[0]); if(userid==-1){ net_send_error(data->fd,tag,ERR_INVALID_CREDS); if(data->userid!=-1){ userdata_unregister(data->userid,data->fd); broadcast_online_change(data->userid); data->userid=-1; } } else { bool success = db_check_pass(userid, args[1]); if(data->userid!=-1){ userdata_unregister(data->userid,data->fd); broadcast_online_change(data->userid); } if(success){ data->userid=userid; userdata_register(userid,data->fd); net_send_ok(data->fd,tag); broadcast_online_change(userid); } else { data->userid=-1; net_send_error(data->fd,tag,ERR_INVALID_CREDS); } } return RET_MEMZERO; } static struct cmd_retval cmd_logout(struct conn_data *data,const char *tag,const char **args){ (void)args; if(data->userid!=-1){ userdata_unregister(data->userid,data->fd); broadcast_online_change(data->userid); data->userid=-1; } net_send_ok(data->fd,tag); return RET_OK; } static struct cmd_retval cmd_change_password(struct conn_data *data, const char *tag, const char **args) { // TODO: use sodium_mlock correctly for the password // Note: this function has exactly ONE return point, so that it is easier // to see that we indeed return MEMZERO. if (data->userid == -1) { net_send_error(data->fd, tag, ERR_NOLOGIN); } else { db_set_pass(data->userid, args[0]); net_send_ok(data->fd, tag); } return RET_MEMZERO; } static struct cmd_retval cmd_list_rooms(struct conn_data *data,const char *tag,const char **args){ (void)args; if(data->userid==-1){ net_send_error(data->fd,tag,ERR_NOLOGIN); return RET_OK; } struct db_room_list rl=db_list_rooms(data->userid); if(rl.count<=0){ db_nullify_room_list(rl); return RET_CLOSE(net_send_list(data->fd,tag,0,NULL)); } const char *names[rl.count]; for(i64 i=0;ifd,tag,rl.count,names); db_nullify_room_list(rl); return RET_CLOSE(closed); } static struct cmd_retval cmd_list_members(struct conn_data *data,const char *tag,const char **args){ if(data->userid==-1){ net_send_error(data->fd,tag,ERR_NOLOGIN); return RET_OK; } i64 roomid=db_find_room(args[0]); if(roomid==-1||!db_is_member(roomid,data->userid)){ net_send_error(data->fd,tag,ERR_NOROOM); return RET_OK; } struct db_user_list ul=db_list_members(roomid); if(ul.count<=0){ db_nullify_user_list(ul); return RET_CLOSE(net_send_list(data->fd,tag,0,NULL)); } const char *names[ul.count]; for(i64 i=0;ifd,tag,ul.count,names); db_nullify_user_list(ul); return RET_CLOSE(closed); } static struct cmd_retval cmd_create_room(struct conn_data *data,const char *tag,const char **args){ (void)args; if (data->userid == -1) { net_send_error(data->fd, tag, ERR_NOLOGIN); return RET_OK; } userdata_mark_active(data->userid, data->fd, true); struct db_name_id room = db_create_room(); db_add_member(room.id, data->userid); char *username = db_get_username(data->userid); char *invitebuf = NULL; i64 invitebuflen = asprintf(&invitebuf, "_push invite %s %s\n", room.name, username); free(username); i64 nfds; const int *fds = userdata_online(data->userid, &nfds); for (i64 j = 0; j < nfds; j++) { if (fds[j] != data->fd) { net_send_raw_text(fds[j], invitebuf, invitebuflen); } } free(invitebuf); bool closed = net_send_name(data->fd, tag, room.name); db_nullify_name_id(room); return RET_CLOSE(closed); } static struct cmd_retval cmd_leave_room(struct conn_data *data,const char *tag,const char **args){ (void)args; if (data->userid == -1) { net_send_error(data->fd, tag, ERR_NOLOGIN); return RET_OK; } userdata_mark_active(data->userid, data->fd, true); const char *roomname = args[0]; i64 roomid = db_find_room(roomname); if (roomid == -1) { net_send_error(data->fd, tag, ERR_NOROOM); return RET_OK; } if (!db_remove_member(roomid, data->userid)) { net_send_error(data->fd, tag, ERR_NOROOM); return RET_OK; } char *username = db_get_username(data->userid); char *msgbuf = NULL; i64 msgbuflen = asprintf(&msgbuf, "_push leave %s %s\n", roomname, username); free(username); // First send the leave push message to all remaining room members struct db_user_list members = db_list_members(roomid); for (i64 i = 0; i < members.count; i++) { i64 nfds; const int *fds = userdata_online(members.list[i].id, &nfds); for (i64 j = 0; j < nfds; j++) { net_send_raw_text(fds[j], msgbuf, msgbuflen); } } db_nullify_user_list(members); // Then send the leave push message to the other sessions of the current user i64 nfds; const int *fds = userdata_online(data->userid, &nfds); for (i64 j = 0; j < nfds; j++) { if (fds[j] != data->fd) { net_send_raw_text(fds[j], msgbuf, msgbuflen); } } free(msgbuf); return RET_CLOSE(net_send_ok(data->fd,tag)); } static struct cmd_retval cmd_invite(struct conn_data *data,const char *tag,const char **args){ if(data->userid==-1){ net_send_error(data->fd,tag,ERR_NOLOGIN); return RET_OK; } userdata_mark_active(data->userid,data->fd,true); const char *roomname=args[0]; i64 roomid=db_find_room(roomname); if(roomid==-1||!db_is_member(roomid,data->userid)){ net_send_error(data->fd,tag,ERR_NOROOM); return RET_OK; } i64 user2=db_find_user(args[1]); if(user2==-1){ net_send_error(data->fd,tag,ERR_NOUSER); return RET_OK; } if(db_is_member(roomid,user2)){ net_send_error(data->fd,tag,ERR_USER_ALREADY_IN_ROOM); return RET_OK; } db_add_member(roomid,user2); char *inviter_username=db_get_username(data->userid); const char *username=args[1]; char *joinbuf=NULL; i64 joinbuflen=asprintf(&joinbuf,"_push join %s %s\n",roomname,username); char *invitebuf=NULL; i64 invitebuflen=asprintf(&invitebuf,"_push invite %s %s\n",roomname,inviter_username); free(inviter_username); event_emit_join(make_timestamp(),username,roomname); struct db_user_list members=db_list_members(roomid); for(i64 i=0;ifd){ if(members.list[i].id==user2){ net_send_raw_text(fds[j],invitebuf,invitebuflen); } else { net_send_raw_text(fds[j],joinbuf,joinbuflen); } } } } db_nullify_user_list(members); free(joinbuf); free(invitebuf); return RET_CLOSE(net_send_ok(data->fd,tag)); } static struct cmd_retval send_impl( struct conn_data *data,const char *tag, const char *roomname,const char *replyidstr,const char *message, i64 timestamp,bool check_reply_earlier){ if(strlen(message)>MAX_MESSAGE_LEN){ net_send_error(data->fd,tag,ERR_MSG_TOO_LONG); return RET_OK; } i64 replyid; if(!parse_i64(replyidstr,&replyid)){ debug("Connection fd=%d sent an invalid number for 'send': '%s'", data->fd,replyidstr); return RET_CLOSE(true); } const i64 roomid=db_find_room(roomname); if(roomid==-1||!db_is_member(roomid,data->userid)){ net_send_error(data->fd,tag,ERR_NOROOM); return RET_OK; } if(replyid>=0){ const struct db_message msg=db_get_message(replyid); bool error_sent=false; if(msg.msgid==-1){ net_send_error(data->fd,tag,ERR_NOREPLYMSG); error_sent=true; } else if(check_reply_earlier&&msg.timestamp>=timestamp){ net_send_error(data->fd,tag,ERR_REPLYMSG_TIMETRAVEL); error_sent=true; } db_nullify_message(msg); if(error_sent)return RET_OK; } const i64 msgid=db_create_message(roomid,data->userid,timestamp,replyid,message); const bool closed=net_send_number(data->fd,tag,msgid); char *pushbuf=NULL; char *const username=db_get_username(data->userid); i64 pushbuflen=asprintf(&pushbuf, "_push message %s %s %" PRIi64 " %" PRIi64 " %" PRIi64 " %s\n", roomname,username,timestamp,msgid,replyid,message); event_emit_message(timestamp,message,username,roomname,replyid); firebase_send_message(roomname,roomid,username,message); free(username); const struct db_user_list members=db_list_members(roomid); for(i64 i=0;ifd){ net_send_raw_text(fds[j],pushbuf,pushbuflen); } } } db_nullify_user_list(members); free(pushbuf); return RET_CLOSE(closed); } static struct cmd_retval cmd_send(struct conn_data *data, const char *tag, const char **args){ if (data->userid == -1) { net_send_error(data->fd, tag, ERR_NOLOGIN); return RET_OK; } userdata_mark_active(data->userid, data->fd, true); const char *roomname = args[0]; const char *replyidstr = args[1]; const char *message = args[2]; return send_impl(data, tag, roomname, replyidstr, message, make_timestamp(), false); } static struct cmd_retval cmd_sendat(struct conn_data *data, const char *tag, const char **args){ if (data->userid == -1) { net_send_error(data->fd, tag, ERR_NOLOGIN); return RET_OK; } if (data->protversion < 4) { net_send_error(data->fd, tag, ERR_SENDAT_UNAVAIL_3); return RET_OK; } const char *apikey = args[0]; const char *roomname = args[1]; const char *replyidstr = args[2]; const char *timestampstr = args[3]; const char *message = args[4]; if (!config_check_apikey(apikey).sendat) { net_send_error(data->fd, tag, ERR_SENDAT_FORBIDDEN); return RET_OK; } i64 timestamp; if (!parse_i64(timestampstr, ×tamp) || timestamp < 0){ debug("Connection fd=%d (apikey=%s) sent an invalid timestamp for 'sendat': '%s'", data->fd, apikey, args[2]); return RET_CLOSE(true); } return send_impl(data, tag, roomname, replyidstr, message, timestamp, true); } static struct cmd_retval history_cmd_helper( struct conn_data *data,const char *tag,const char **args, const char *cmdname,i64 beforeid){ i64 nrequested; if(!parse_i64(args[1],&nrequested)||nrequested<0){ debug("Connection fd=%d sent an invalid number for '%s': '%s'", data->fd,cmdname,args[1]); return RET_CLOSE(true); } if(data->userid==-1){ net_send_error(data->fd,tag,ERR_NOLOGIN); return RET_OK; } const char *roomname=args[0]; i64 roomid=db_find_room(roomname); if(roomid==-1||!db_is_member(roomid,data->userid)){ net_send_error(data->fd,tag,ERR_NOROOM); return RET_OK; } struct db_message_list ml=db_get_messages_before(roomid,nrequested,beforeid); char *buf=NULL; i64 len=asprintf(&buf,"%s history %" PRIi64 "\n",tag,ml.count); bool closed=net_send_raw_text(data->fd,buf,len); free(buf); if(closed){ db_nullify_message_list(ml); return RET_CLOSE(true); } for(i64 i=ml.count-1;i>=0;i--){ char *username=db_get_username(ml.list[i].userid); len=asprintf(&buf,"%s history_message %" PRIi64 " %s %s %" PRIi64 " %" PRIi64 " %" PRIi64 " %s\n", tag,ml.count-1-i,roomname,username,ml.list[i].timestamp, ml.list[i].msgid,ml.list[i].replyid,ml.list[i].message); free(username); closed=net_send_raw_text(data->fd,buf,len); free(buf); if(closed)break; } db_nullify_message_list(ml); return RET_CLOSE(closed); } static struct cmd_retval cmd_history(struct conn_data *data,const char *tag,const char **args){ return history_cmd_helper(data,tag,args,"history",-1); } static struct cmd_retval cmd_history_before(struct conn_data *data,const char *tag,const char **args){ i64 beforeid; if(!parse_i64(args[2],&beforeid)){ debug("Connection fd=%d sent an invalid id for 'history_before': '%s'", data->fd,args[2]); return RET_CLOSE(true); } if(beforeid<0)beforeid=INT64_MAX; return history_cmd_helper(data,tag,args,"history_before",beforeid); } static struct cmd_retval cmd_get_message(struct conn_data *data, const char *tag, const char **args) { i64 msgid; if (!parse_i64(args[0], &msgid)) { debug("Connection fd=%d sent an invalid number for 'get_message': '%s'", data->fd, args[0]); return RET_CLOSE(true); } if (data->userid == -1) { net_send_error(data->fd, tag, ERR_NOLOGIN); return RET_OK; } struct db_message msg = db_get_message(msgid); if (msg.msgid == -1) { net_send_error(data->fd, tag, ERR_NOMSG); return RET_OK; } if (!db_is_member(msg.roomid, data->userid)) { db_nullify_message(msg); // Don't disclose that the message actually exists elsewhere net_send_error(data->fd, tag, ERR_NOMSG); return RET_OK; } char *roomname = db_get_roomname(msg.roomid); char *username = db_get_username(msg.userid); char *buf = NULL; i64 len = asprintf(&buf, "%s message %s %s %" PRIi64 " %" PRIi64 " %" PRIi64 " %s\n", tag, roomname, username, msg.timestamp, msg.msgid, msg.replyid, msg.message); free(roomname); free(username); bool closed = net_send_raw_text(data->fd, buf, len); free(buf); db_nullify_message(msg); return RET_CLOSE(closed); } static struct cmd_retval cmd_ping(struct conn_data *data,const char *tag,const char **args){ (void)args; return RET_CLOSE(net_send_pong(data->fd,tag)); } static struct cmd_retval cmd_is_online(struct conn_data *data,const char *tag,const char **args){ if (data->userid == -1) { net_send_error(data->fd, tag, ERR_NOLOGIN); return RET_OK; } i64 userid2 = db_find_user(args[0]); if (userid2 == -1 || !db_user_knows_user(data->userid, userid2)) { net_send_error(data->fd, tag, ERR_NOUSER); return RET_OK; } i64 nfds; (void)userdata_online(userid2, &nfds); return RET_CLOSE(net_send_number(data->fd, tag, nfds)); } static struct cmd_retval cmd_firebase_token(struct conn_data *data,const char *tag,const char **args){ if(data->userid==-1){ net_send_error(data->fd,tag,ERR_NOLOGIN); return RET_OK; } db_add_token(data->userid,args[0]); return RET_CLOSE(net_send_ok(data->fd,tag)); } static struct cmd_retval cmd_delete_firebase_token(struct conn_data *data,const char *tag,const char **args){ if(data->userid==-1){ net_send_error(data->fd,tag,ERR_NOLOGIN); return RET_OK; } db_delete_token(data->userid,args[0]); return RET_CLOSE(net_send_ok(data->fd,tag)); } static struct cmd_retval cmd_user_active(struct conn_data *data,const char *tag,const char **args){ if(data->userid==-1){ net_send_error(data->fd,tag,ERR_NOLOGIN); return RET_OK; } i64 active; if(!parse_i64(args[0],&active)||active<0){ debug("Connection fd=%d sent an invalid number for 'user_active': '%s'",data->fd,args[0]); return RET_CLOSE(true); } userdata_mark_active(data->userid,data->fd,active>0); return RET_CLOSE(net_send_ok(data->fd,tag)); } struct cmd_info{ const char *cmdname; int nargs; bool longlast; // whether the last argument should span the rest of the input line struct cmd_retval (*handler)(struct conn_data *data,const char *tag,const char **args); }; // Use CommandHash.hs to re-generate this perfect hash function for a different // list of commands. #define COMMAND_HASH_MODULUS 32 #define COMMAND_HASH(cmd0, cmd1, len) ((5 * cmd0 + 9 * cmd1 + 1 * len) % COMMAND_HASH_MODULUS) #define COMMAND_ENTRY(cmd0, cmd1, cmd, nargs, longlast, handler) \ [COMMAND_HASH(cmd0, cmd1, strlen(cmd))] = {cmd, nargs, longlast, handler} // First two arguments to COMMAND_ENTRY must be command[0] and [1]. This is because // apparently, "abc"[0] is not a constant expression, while strlen("abc") is. static const struct cmd_info commands[COMMAND_HASH_MODULUS] = { COMMAND_ENTRY('v','e', "version", 1, false, cmd_version), COMMAND_ENTRY('r','e', "register", 2, true, cmd_register), COMMAND_ENTRY('l','o', "login", 2, true, cmd_login), COMMAND_ENTRY('l','o', "logout", 0, false, cmd_logout), COMMAND_ENTRY('c','h', "change_password", 1, true, cmd_change_password), COMMAND_ENTRY('l','i', "list_rooms", 0, false, cmd_list_rooms), COMMAND_ENTRY('l','i', "list_members", 1, false, cmd_list_members), COMMAND_ENTRY('c','r', "create_room", 0, false, cmd_create_room), COMMAND_ENTRY('l','e', "leave_room", 1, false, cmd_leave_room), COMMAND_ENTRY('i','n', "invite", 2, false, cmd_invite), COMMAND_ENTRY('s','e', "send", 3, true, cmd_send), COMMAND_ENTRY('s','e', "sendat", 5, true, cmd_sendat), COMMAND_ENTRY('h','i', "history", 2, false, cmd_history), COMMAND_ENTRY('h','i', "history_before", 3, false, cmd_history_before), COMMAND_ENTRY('g','e', "get_message", 1, false, cmd_get_message), COMMAND_ENTRY('p','i', "ping", 0, false, cmd_ping), COMMAND_ENTRY('i','s', "is_online", 1, false, cmd_is_online), COMMAND_ENTRY('f','i', "firebase_token", 1, false, cmd_firebase_token), COMMAND_ENTRY('d','e', "delete_firebase_token", 1, false, cmd_delete_firebase_token), COMMAND_ENTRY('u','s', "user_active", 1, false, cmd_user_active), }; bool handle_input_line(struct conn_data *data,char *line,size_t linelen){ line[linelen]='\0'; char *sepp=memchr(line,' ',linelen); if(sepp==NULL){ debug("No space in input line from connection %d",data->fd); return true; } const char *tag=line; const size_t taglen=sepp-tag; *sepp='\0'; line+=taglen+1; linelen-=taglen+1; sepp=memchr(line,' ',linelen); if(sepp==NULL)sepp=line+linelen; const size_t cmdlen=sepp-line; const struct cmd_info *command=NULL; if(cmdlen>=2)command=&commands[COMMAND_HASH(line[0],line[1],cmdlen)]; if(!command->cmdname ||cmdlen!=strlen(command->cmdname) ||memcmp(line,command->cmdname,cmdlen)!=0){ debug("Unknown command %s on connection %d",line,data->fd); return true; } // Ensure first command is 'version' if(data->protversion==-1&&command->handler!=cmd_version){ debug("Command %s before version negotiation on connection %d", command->cmdname,data->fd); return true; } const int nargs=command->nargs; const char *args[nargs]; size_t cursor=cmdlen+1; for(int i=0;ilinelen){ debug("Connection %d sent too few parameters to command %s",data->fd,command->cmdname); return true; } if(i==nargs-1&&command->longlast){ sepp=line+linelen; } else { sepp=memchr(line+cursor,' ',linelen-cursor); if(sepp==NULL)sepp=line+linelen; } *sepp='\0'; args[i]=line+cursor; cursor=sepp-line+1; } if(sepp-line<(i64)linelen){ debug("Connection %d sent too many parameters to command %s",data->fd,command->cmdname); return true; } struct cmd_retval retval=command->handler(data,tag,(const char**)args); if(retval.memzero)sodium_memzero(line,linelen); return retval.socket_close; }