aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--command.c95
-rw-r--r--command.h5
-rw-r--r--config.c115
-rw-r--r--config.h25
-rw-r--r--main.c3
6 files changed, 219 insertions, 25 deletions
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;i<members.count;i++){
i64 nfds;
const int *fds=userdata_online(members.list[i].id,&nfds);
@@ -381,6 +381,52 @@ static struct cmd_retval cmd_send(struct conn_data *data,const char *tag,const c
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, "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, &timestamp) || 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 <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#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 <stdbool.h>
+
+/* 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();