From 6a69d512e4615e01b5f35e7b68af307969fc6c17 Mon Sep 17 00:00:00 2001 From: Tom Smeding Date: Wed, 10 Feb 2021 12:07:00 +0100 Subject: server: Support sendat, protocol version 4 --- .gitignore | 1 + command.c | 95 +++++++++++++++++++++++++++++++++++++------------- command.h | 5 ++- config.c | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ config.h | 25 ++++++++++++++ main.c | 3 ++ 6 files changed, 219 insertions(+), 25 deletions(-) create mode 100644 config.c create mode 100644 config.h diff --git a/.gitignore b/.gitignore index a33cbb5..596282e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ compile_commands.json CommandHash CommandHash.hi protocol.html +config.txt diff --git a/command.c b/command.c index f69593c..ac6ff09 100644 --- a/command.c +++ b/command.c @@ -14,6 +14,7 @@ #include "firebase.h" #include "user_data.h" #include "util.h" +#include "config.h" #define MAX_MESSAGE_LEN 10000 @@ -309,29 +310,23 @@ static struct cmd_retval cmd_invite(struct conn_data *data,const char *tag,const 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"); +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,"Message too long"); return RET_OK; } - userdata_mark_active(data->userid,data->fd,true); - - const char *roomname=args[0]; i64 replyid; - if(!parse_i64(args[1],&replyid)){ + if(!parse_i64(replyidstr,&replyid)){ debug("Connection fd=%d sent an invalid number for 'send': '%s'", - data->fd,args[1]); + data->fd,replyidstr); return RET_CLOSE(true); } - const char *message=args[2]; - - if(strlen(message)>MAX_MESSAGE_LEN){ - net_send_error(data->fd,tag,"Message too long"); - return RET_OK; - } - i64 roomid=db_find_room(roomname); + const i64 roomid=db_find_room(roomname); if(roomid==-1){ net_send_error(data->fd,tag,"Room not found"); return RET_OK; @@ -340,21 +335,26 @@ static struct cmd_retval cmd_send(struct conn_data *data,const char *tag,const c net_send_error(data->fd,tag,"Not in that room"); return RET_OK; } + if(replyid>=0){ - struct db_message msg=db_get_message(replyid); + const struct db_message msg=db_get_message(replyid); + bool error_sent=false; if(msg.msgid==-1){ net_send_error(data->fd,tag,"Replied-to message not found"); - return RET_OK; + error_sent=true; + } else if(check_reply_earlier&&msg.timestamp>=timestamp){ + net_send_error(data->fd,tag,"Replied-to message later than target timestamp"); + error_sent=true; } db_nullify_message(msg); + if(error_sent)return RET_OK; } - i64 timestamp=make_timestamp(); - i64 msgid=db_create_message(roomid,data->userid,make_timestamp(),replyid,message); - bool closed=net_send_number(data->fd,tag,msgid); + 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 *username=db_get_username(data->userid); + 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); @@ -363,7 +363,7 @@ static struct cmd_retval cmd_send(struct conn_data *data,const char *tag,const c firebase_send_message(roomname,roomid,username,message); free(username); - struct db_user_list members=db_list_members(roomid); + const struct db_user_list members=db_list_members(roomid); for(i64 i=0;iuserid == -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 *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, "Not logged in"); + return RET_OK; + } + + if (data->protversion < 4) { + net_send_error(data->fd, tag, "sendat unavailable in protocol version 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, "sendat not allowed"); + 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){ @@ -548,8 +594,8 @@ struct cmd_info{ // 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, cmd1, len) ((9 * cmd0 + 1 * cmd1 + 3 * len) % COMMAND_HASH_MODULUS) +#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} @@ -568,6 +614,7 @@ static const struct cmd_info commands[COMMAND_HASH_MODULUS] = { 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), diff --git a/command.h b/command.h index 68ec014..23f38ff 100644 --- a/command.h +++ b/command.h @@ -4,7 +4,10 @@ #include "conn_data.h" -#define PROTOCOL_VERSION 3 +// Protocol version 4 adds the 'sendat' command; if the client reports not to +// support sendat, we just disable that command. In this manner, we continue to +// support protocol version 3. +#define PROTOCOL_VERSION 4 #define MIN_SUPPORTED_PROTOCOL_VERSION 3 diff --git a/config.c b/config.c new file mode 100644 index 0000000..bd7f822 --- /dev/null +++ b/config.c @@ -0,0 +1,115 @@ +#include +#include +#include +#include +#include "config.h" +#include "global.h" + + +struct config_data { + size_t num_apikeys; + char **apikeys; + struct apikey_perm *apikey_perms; +}; + +static struct config_data config_data; + +static void add_apikey(char *key, struct apikey_perm perm) { + // This is slow, but optimisation is not worth it + config_data.num_apikeys++; + config_data.apikeys = realloc(config_data.apikeys, config_data.num_apikeys, char*); + config_data.apikey_perms = + realloc(config_data.apikey_perms, config_data.num_apikeys, struct apikey_perm); + + config_data.apikeys[config_data.num_apikeys - 1] = key; + config_data.apikey_perms[config_data.num_apikeys - 1] = perm; +} + +struct slice { + const char *s; + size_t len; +}; + +static struct slice next_word(struct slice *line) { + size_t cursor = 0; + while (cursor < line->len && isspace(line->s[cursor])) cursor++; + const size_t start = cursor; + while (cursor < line->len && !isspace(line->s[cursor])) cursor++; + const struct slice word = (struct slice){line->s + start, cursor - start}; + line->s += cursor; + line->len -= cursor; + return word; +} + +static void apply_config_line(const char *linebuf, size_t linelen) { + struct slice line = (struct slice){linebuf, linelen}; + struct slice cmd = next_word(&line); + if (cmd.len == 0) return; + + if (cmd.len == 6 && memcmp(cmd.s, "apikey", 6) == 0) { + struct slice key = next_word(&line); + struct slice pbits = next_word(&line); + if (line.len > 0) die("Too many fields on 'apikey' line"); + + for (size_t i = 0; i < pbits.len; i++) { + if (pbits.s[i] != '0' && pbits.s[i] != '1') { + die("Invalid permission bit '%c' on 'apikey' line", pbits.s[i]); + } + } + + if (pbits.len != 1) die("Incorrect permission bit vector length on 'apikey' line"); + struct apikey_perm perm; + perm.sendat = pbits.s[0] == '1'; + + for (size_t i = 0; i < key.len; i++) { + if (key.s[i] == '\0') die("Invalid null byte in key on 'apikey' line"); + } + + char *keystr = malloc(key.len + 1, char); + memcpy(keystr, key.s, key.len); + keystr[key.len] = '\0'; + + add_apikey(keystr, perm); + } else { + char *cmdstr = malloc(cmd.len + 1, char); + memcpy(cmdstr, cmd.s, cmd.len); + cmdstr[cmd.len] = '\0'; + die("Unknown command '%s' in config file", cmdstr); + } +} + +void config_init(const char *filename) { + FILE *f = fopen(filename, "r"); + if (!f) die("Cannot open config file '%s'", filename); + + char *line = NULL; + size_t linecap = 0; + ssize_t nr; + while ((nr = getline(&line, &linecap, f)) >= 0) { + if (line[0] == '#') continue; + if (nr > 0 && line[nr - 1] == '\n') nr--; + if (nr > 0 && line[nr - 1] == '\r') nr--; + apply_config_line(line, nr); + } + free(line); + + if (!feof(f) && ferror(f)) { + die("Error reading config file '%s': %s", filename, strerror(errno)); + } + + fprintf(stderr, "config: Registered %zu API key%s\n", + config_data.num_apikeys, config_data.num_apikeys == 1 ? "" : "s"); +} + +struct apikey_perm config_check_apikey(const char *apikey) { + for (size_t i = 0; i < config_data.num_apikeys; i++) { + if (strcmp(config_data.apikeys[i], apikey) == 0) { + return config_data.apikey_perms[i]; + } + } + + // Unknown key means no permissions + struct apikey_perm perm; + memset(&perm, 0, sizeof perm); + return perm; +} diff --git a/config.h b/config.h new file mode 100644 index 0000000..d1f31a8 --- /dev/null +++ b/config.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +/* Config file format + * + * Each line is a command that modifies some part of the configuration. A line + * starting with a '#' is a comment and is ignored. The available commands are + * as follows: + * - 'apikey': the line contains three space-separated fields: 'apikey', the + * key itself, and a permission bit vector (string of 0/1 characters). The + * permission bit vector has the following elements: + * 1. Whether 'sendat' is allowed. + */ + + +// Writes to stderr and exits on failure. +void config_init(const char *filename); + +// Permissions for a particular API key +struct apikey_perm { + bool sendat; +}; + +struct apikey_perm config_check_apikey(const char *apikey); diff --git a/main.c b/main.c index a2ea8fc..5643b86 100644 --- a/main.c +++ b/main.c @@ -20,6 +20,7 @@ #include "runloop.h" #include "user_data.h" #include "hashtable.h" +#include "config.h" #define PORT (29536) // python: int("msg",36) @@ -162,6 +163,8 @@ int main(int argc,char **argv){ return 1; } + config_init("config.txt"); + signal(SIGPIPE,signal_handler); conn_hash_init(); -- cgit v1.2.3-54-g00ecf