#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" 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}) 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, "Version not supported"); 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,"Username 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,"User not found"); 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,"Incorrect password"); } } 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_list_rooms(struct conn_data *data,const char *tag,const char **args){ (void)args; if(data->userid==-1){ net_send_error(data->fd,tag,"Not logged in"); 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,"Not logged in"); return RET_OK; } i64 roomid=db_find_room(args[0]); if(roomid==-1){ net_send_error(data->fd,tag,"Room not found"); return RET_OK; } if(!db_is_member(roomid,data->userid)){ net_send_error(data->fd,tag,"Not in that room"); 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,"Not logged in"); 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); bool closed=net_send_name(data->fd,tag,room.name); db_nullify_name_id(room); return RET_CLOSE(closed); } 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,"Not logged in"); 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,"Room not found"); return RET_OK; } i64 user2=db_find_user(args[1]); if(user2==-1){ net_send_error(data->fd,tag,"User not found"); return RET_OK; } if(!db_is_member(roomid,data->userid)){ net_send_error(data->fd,tag,"Not in that room"); return RET_OK; } if(db_is_member(roomid,user2)){ net_send_error(data->fd,tag,"User already in that room"); return RET_OK; } db_add_member(roomid,user2); const 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); 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 cmd_send(struct conn_data *data,const char *tag,const char **args){ if(data->userid==-1){ net_send_error(data->fd,tag,"Not logged in"); return RET_OK; } userdata_mark_active(data->userid,data->fd,true); const char *roomname=args[0]; const char *message=args[1]; i64 roomid=db_find_room(roomname); if(roomid==-1){ net_send_error(data->fd,tag,"Room not found"); return RET_OK; } if(!db_is_member(roomid,data->userid)){ net_send_error(data->fd,tag,"Not in that room"); return RET_OK; } i64 timestamp=make_timestamp(); i64 msgid=db_create_message(roomid,data->userid,make_timestamp(),message); bool closed=net_send_number(data->fd,tag,msgid); char *pushbuf=NULL; char *username=db_get_username(data->userid); i64 pushbuflen=asprintf(&pushbuf,"_push message %s %s %" PRIi64 " %" PRIi64 " %s\n", roomname,username,timestamp,msgid,message); event_emit_message(timestamp,message,username,roomname); firebase_send_message(roomname,roomid,username,message); free(username); 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 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,"Not logged in"); return RET_OK; } const char *roomname=args[0]; i64 roomid=db_find_room(roomname); if(roomid==-1){ net_send_error(data->fd,tag,"Room not found"); return RET_OK; } if(!db_is_member(roomid,data->userid)){ net_send_error(data->fd,tag,"Not in that room"); 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 " %s\n", tag,ml.count-1-i,roomname,username,ml.list[i].timestamp, ml.list[i].msgid,ml.list[i].message); 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_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){ i64 userid=db_find_user(args[0]); if(userid==-1){ net_send_error(data->fd,tag,"User not found"); return RET_OK; } i64 nfds; (void)userdata_online(userid,&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,"Not logged in"); 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,"Not logged in"); 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,"Not logged in"); 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 31 #define COMMAND_HASH(cmd0, len) ((cmd0 + 6 * len) % COMMAND_HASH_MODULUS) #define COMMAND_ENTRY(cmd0, cmd, nargs, longlast, handler) \ [COMMAND_HASH(cmd0, strlen(cmd))] = {cmd, nargs, longlast, handler} // First argument to COMMAND_ENTRY must be command[0]. 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', "version", 1, false, cmd_version), COMMAND_ENTRY('r', "register", 2, true, cmd_register), COMMAND_ENTRY('l', "login", 2, true, cmd_login), COMMAND_ENTRY('l', "logout", 0, false, cmd_logout), COMMAND_ENTRY('l', "list_rooms", 0, false, cmd_list_rooms), COMMAND_ENTRY('l', "list_members", 1, false, cmd_list_members), COMMAND_ENTRY('c', "create_room", 0, false, cmd_create_room), COMMAND_ENTRY('i', "invite", 2, false, cmd_invite), COMMAND_ENTRY('s', "send", 2, true, cmd_send), COMMAND_ENTRY('h', "history", 2, false, cmd_history), COMMAND_ENTRY('h', "history_before", 3, false, cmd_history_before), COMMAND_ENTRY('p', "ping", 0, false, cmd_ping), COMMAND_ENTRY('i', "is_online", 1, false, cmd_is_online), COMMAND_ENTRY('f', "firebase_token", 1, false, cmd_firebase_token), COMMAND_ENTRY('d', "delete_firebase_token", 1, false, cmd_delete_firebase_token), COMMAND_ENTRY('u', "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=&commands[COMMAND_HASH(line[0],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; }