Merge pull request 'Mastodon PATCH API for user profile updates' (#169) from louis77/snac2:master into master

Reviewed-on: https://codeberg.org/grunfink/snac2/pulls/169
This commit is contained in:
grunfink 2024-05-30 08:44:54 +00:00
commit d56d4beb90
14 changed files with 641 additions and 337 deletions

View File

@ -52,7 +52,7 @@ mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \
snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode.h xs_json.h xs_curl.h \
xs_openssl.h xs_socket.h xs_url.h xs_httpd.h xs_mime.h xs_regex.h \
xs_set.h xs_time.h xs_glob.h xs_random.h xs_match.h xs_fcgi.h xs_html.h \
snac.h
snac.h http_codes.h
upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h
utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \
xs_random.h xs_glob.h xs_curl.h xs_regex.h snac.h

View File

@ -96,19 +96,19 @@ int activitypub_request(snac *user, const char *url, xs_dict **data)
ctype = xs_dict_get(response, "content-type");
if (xs_is_null(ctype))
status = 400;
status = HTTP_STATUS_BAD_REQUEST;
else
if (xs_str_in(ctype, "application/activity+json") != -1 ||
xs_str_in(ctype, "application/ld+json") != -1) {
/* if there is no payload, fail */
if (xs_is_null(payload))
status = 400;
status = HTTP_STATUS_BAD_REQUEST;
else
*data = xs_json_loads(payload);
}
else
status = 500;
status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
}
return status;
@ -443,7 +443,7 @@ int send_to_actor(snac *snac, const char *actor, const xs_dict *msg,
xs_val **payload, int *p_size, int timeout)
/* sends a message to an actor */
{
int status = 400;
int status = HTTP_STATUS_BAD_REQUEST;
xs *inbox = get_actor_inbox(actor);
if (!xs_is_null(inbox))
@ -1762,7 +1762,9 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
a_status = actor_request(snac, actor, &actor_o);
/* do not retry permanent failures */
if (a_status == 404 || a_status == 410 || a_status < 0) {
if (a_status == HTTP_STATUS_NOT_FOUND
|| a_status == HTTP_STATUS_GONE
|| a_status < 0) {
srv_debug(1, xs_fmt("dropping message due to actor error %s %d", actor, a_status));
return -1;
}
@ -1905,7 +1907,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
}
else
if (strcmp(utype, "Announce") == 0) { /** **/
int status = 200;
int status = HTTP_STATUS_OK;
/* commented out: if a followed user boosts something that
is requested and then unboosts, the post remains here,
@ -2015,7 +2017,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
if (xs_type(object) == XSTYPE_DICT)
object = xs_dict_get(object, "id");
if (timeline_admire(snac, object, actor, 1) == 201)
if (timeline_admire(snac, object, actor, 1) == HTTP_STATUS_CREATED)
snac_log(snac, xs_fmt("new 'Like' %s %s", actor, object));
else
snac_log(snac, xs_fmt("repeated 'Like' from %s to %s", actor, object));
@ -2046,7 +2048,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
xs *who_o = NULL;
if (valid_status(actor_request(snac, who, &who_o))) {
if (timeline_admire(snac, object, actor, 0) == 201)
if (timeline_admire(snac, object, actor, 0) == HTTP_STATUS_CREATED)
snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
else
snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
@ -2383,11 +2385,15 @@ void process_queue_item(xs_dict *q_item)
/* if it's not the first time it fails with a timeout,
penalize the server by skipping one retry */
if (p_status == status && status == 499)
if (p_status == status && status == HTTP_STATUS_CLIENT_CLOSED_REQUEST)
retries++;
/* error sending; requeue? */
if (status == 400 || status == 404 || status == 405 || status == 410 || status < 0)
if (status == HTTP_STATUS_BAD_REQUEST
|| status == HTTP_STATUS_NOT_FOUND
|| status == HTTP_STATUS_METHOD_NOT_ALLOWED
|| status == HTTP_STATUS_GONE
|| status < 0)
/* explicit error: discard */
srv_log(xs_fmt("output message: fatal error %s %d", inbox, status));
else
@ -2574,7 +2580,7 @@ int process_queue(void)
int activitypub_get_handler(const xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype)
{
int status = 200;
int status = HTTP_STATUS_OK;
const char *accept = xs_dict_get(req, "accept");
snac snac;
xs *msg = NULL;
@ -2594,7 +2600,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
if (!user_open(&snac, uid)) {
/* invalid user */
srv_debug(1, xs_fmt("activitypub_get_handler bad user %s", uid));
return 404;
return HTTP_STATUS_NOT_FOUND;
}
p_path = xs_list_get(l, 2);
@ -2652,12 +2658,12 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
/* don't return non-public objects */
if (valid_status(status) && !is_msg_public(msg))
status = 404;
status = HTTP_STATUS_NOT_FOUND;
}
else
status = 404;
status = HTTP_STATUS_NOT_FOUND;
if (status == 200 && msg != NULL) {
if (status == HTTP_STATUS_OK && msg != NULL) {
*body = xs_json_dumps(msg, 4);
*b_size = strlen(*body);
}
@ -2677,7 +2683,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
{
(void)b_size;
int status = 202; /* accepted */
int status = HTTP_STATUS_ACCEPTED;
const char *i_ctype = xs_dict_get(req, "content-type");
snac snac;
const char *v;
@ -2685,13 +2691,13 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
if (i_ctype == NULL) {
*body = xs_str_new("no content-type");
*ctype = "text/plain";
return 400;
return HTTP_STATUS_BAD_REQUEST;
}
if (xs_is_null(payload)) {
*body = xs_str_new("no payload");
*ctype = "text/plain";
return 400;
return HTTP_STATUS_BAD_REQUEST;
}
if (xs_str_in(i_ctype, "application/activity+json") == -1 &&
@ -2709,7 +2715,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
*body = xs_str_new("JSON error");
*ctype = "text/plain";
return 400;
return HTTP_STATUS_BAD_REQUEST;
}
if (id && is_instance_blocked(id)) {
@ -2717,7 +2723,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
*body = xs_str_new("blocked");
*ctype = "text/plain";
return 403;
return HTTP_STATUS_FORBIDDEN;
}
/* get the user and path */
@ -2725,20 +2731,20 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
if (xs_list_len(l) == 2 && strcmp(xs_list_get(l, 1), "shared-inbox") == 0) {
enqueue_shared_input(msg, req, 0);
return 202;
return HTTP_STATUS_ACCEPTED;
}
if (xs_list_len(l) != 3 || strcmp(xs_list_get(l, 2), "inbox") != 0) {
/* strange q_path */
srv_debug(1, xs_fmt("activitypub_post_handler unsupported path %s", q_path));
return 404;
return HTTP_STATUS_NOT_FOUND;
}
const char *uid = xs_list_get(l, 1);
if (!user_open(&snac, uid)) {
/* invalid user */
srv_debug(1, xs_fmt("activitypub_post_handler bad user %s", uid));
return 404;
return HTTP_STATUS_NOT_FOUND;
}
/* if it has a digest, check it now, because
@ -2752,7 +2758,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
*body = xs_str_new("bad digest");
*ctype = "text/plain";
status = 400;
status = HTTP_STATUS_BAD_REQUEST;
}
}
@ -2763,7 +2769,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path,
*body = xs_str_new("rejected");
*ctype = "text/plain";
status = 403;
status = HTTP_STATUS_FORBIDDEN;
}
}

95
data.c
View File

@ -303,6 +303,33 @@ int user_open_by_md5(snac *snac, const char *md5)
return 0;
}
int user_persist(snac *snac)
/* store user */
{
xs *fn = xs_fmt("%s/user.json", snac->basedir);
xs *bfn = xs_fmt("%s.bak", fn);
FILE *f;
rename(fn, bfn);
if ((f = fopen(fn, "w")) != NULL) {
xs_json_dump(snac->config, 4, f);
fclose(f);
}
else
rename(bfn, fn);
history_del(snac, "timeline.html_");
xs *a_msg = msg_actor(snac);
xs *u_msg = msg_update(snac, a_msg);
enqueue_message(snac, u_msg);
enqueue_verify_links(snac);
return 0;
}
double mtime_nl(const char *fn, int *n_link)
/* returns the mtime and number of links of a file or directory, or 0.0 */
@ -355,12 +382,12 @@ int is_md5_hex(const char *md5)
int index_add_md5(const char *fn, const char *md5)
/* adds an md5 to an index */
{
int status = 201; /* Created */
int status = HTTP_STATUS_CREATED;
FILE *f;
if (!is_md5_hex(md5)) {
srv_log(xs_fmt("index_add_md5: bad md5 %s %s", fn, md5));
return 400;
return HTTP_STATUS_BAD_REQUEST;
}
pthread_mutex_lock(&data_mutex);
@ -375,7 +402,7 @@ int index_add_md5(const char *fn, const char *md5)
fclose(f);
}
else
status = 500;
status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
pthread_mutex_unlock(&data_mutex);
@ -394,7 +421,7 @@ int index_add(const char *fn, const char *id)
int index_del_md5(const char *fn, const char *md5)
/* deletes an md5 from an index */
{
int status = 404;
int status = HTTP_STATUS_NOT_FOUND;
FILE *f;
pthread_mutex_lock(&data_mutex);
@ -411,7 +438,7 @@ int index_del_md5(const char *fn, const char *md5)
[yes: this breaks index_len()] */
fseek(f, -33, SEEK_CUR);
fwrite("-", 1, 1, f);
status = 200;
status = HTTP_STATUS_OK;
break;
}
@ -420,7 +447,7 @@ int index_del_md5(const char *fn, const char *md5)
fclose(f);
}
else
status = 410;
status = HTTP_STATUS_GONE;
pthread_mutex_unlock(&data_mutex);
@ -660,7 +687,7 @@ int object_here(const char *id)
int object_get_by_md5(const char *md5, xs_dict **obj)
/* returns a stored object, optionally of the requested type */
{
int status = 404;
int status = HTTP_STATUS_NOT_FOUND;
xs *fn = _object_fn_by_md5(md5, "object_get_by_md5");
FILE *f;
@ -669,7 +696,7 @@ int object_get_by_md5(const char *md5, xs_dict **obj)
fclose(f);
if (*obj)
status = 200;
status = HTTP_STATUS_OK;
}
else
*obj = NULL;
@ -689,7 +716,7 @@ int object_get(const char *id, xs_dict **obj)
int _object_add(const char *id, const xs_dict *obj, int ow)
/* stores an object */
{
int status = 201; /* Created */
int status = HTTP_STATUS_CREATED; /* Created */
xs *fn = _object_fn(id);
FILE *f;
@ -697,10 +724,10 @@ int _object_add(const char *id, const xs_dict *obj, int ow)
if (!ow) {
/* object already here */
srv_debug(1, xs_fmt("object_add object already here %s", id));
return 204; /* No content */
return HTTP_STATUS_NO_CONTENT;
}
else
status = 200;
status = HTTP_STATUS_OK;
}
if ((f = fopen(fn, "w")) != NULL) {
@ -736,7 +763,7 @@ int _object_add(const char *id, const xs_dict *obj, int ow)
}
else {
srv_log(xs_fmt("object_add error writing %s (errno: %d)", fn, errno));
status = 500;
status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
}
srv_debug(1, xs_fmt("object_add %s %s %d", id, fn, status));
@ -762,11 +789,11 @@ int object_add_ow(const char *id, const xs_dict *obj)
int object_del_by_md5(const char *md5)
/* deletes an object by its md5 */
{
int status = 404;
int status = HTTP_STATUS_NOT_FOUND;
xs *fn = _object_fn_by_md5(md5, "object_del_by_md5");
if (unlink(fn) != -1) {
status = 200;
status = HTTP_STATUS_OK;
/* also delete associated indexes */
xs *spec = xs_dup(fn);
@ -907,7 +934,7 @@ int object_parent(const char *md5, char *buf, int size)
int object_admire(const char *id, const char *actor, int like)
/* actor likes or announces this object */
{
int status = 200;
int status = HTTP_STATUS_OK;
xs *fn = _object_fn(id);
fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
@ -1007,7 +1034,7 @@ int follower_add(snac *snac, const char *actor)
snac_debug(snac, 2, xs_fmt("follower_add %s", actor));
return ret == -1 ? 500 : 200;
return ret == -1 ? HTTP_STATUS_INTERNAL_SERVER_ERROR : HTTP_STATUS_OK;
}
@ -1018,7 +1045,7 @@ int follower_del(snac *snac, const char *actor)
snac_debug(snac, 2, xs_fmt("follower_del %s", actor));
return ret == -1 ? 404 : 200;
return ret == -1 ? HTTP_STATUS_NOT_FOUND : HTTP_STATUS_OK;
}
@ -1109,7 +1136,7 @@ int timeline_here(snac *snac, const char *md5)
int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg)
/* gets a message from the timeline */
{
int status = 404;
int status = HTTP_STATUS_NOT_FOUND;
FILE *f = NULL;
xs *fn = timeline_fn_by_md5(snac, md5);
@ -1119,7 +1146,7 @@ int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg)
fclose(f);
if (*msg != NULL)
status = 200;
status = HTTP_STATUS_OK;
}
return status;
@ -1282,7 +1309,7 @@ xs_str *_following_fn(snac *snac, const char *actor)
int following_add(snac *snac, const char *actor, const xs_dict *msg)
/* adds to the following list */
{
int ret = 201; /* created */
int ret = HTTP_STATUS_CREATED;
xs *fn = _following_fn(snac, actor);
FILE *f;
xs *p_object = NULL;
@ -1295,7 +1322,7 @@ int following_add(snac *snac, const char *actor, const xs_dict *msg)
if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
snac_debug(snac, 1, xs_fmt("following_add actor already confirmed %s", actor));
return 200;
return HTTP_STATUS_OK;
}
}
@ -1311,7 +1338,7 @@ int following_add(snac *snac, const char *actor, const xs_dict *msg)
link(actor_fn, fn);
}
else
ret = 500;
ret = HTTP_STATUS_INTERNAL_SERVER_ERROR;
snac_debug(snac, 2, xs_fmt("following_add %s %s", actor, fn));
@ -1332,7 +1359,7 @@ int following_del(snac *snac, const char *actor)
fn = xs_replace_i(fn, ".json", "_a.json");
unlink(fn);
return 200;
return HTTP_STATUS_OK;
}
@ -1350,14 +1377,14 @@ int following_get(snac *snac, const char *actor, xs_dict **data)
{
xs *fn = _following_fn(snac, actor);
FILE *f;
int status = 200;
int status = HTTP_STATUS_OK;
if ((f = fopen(fn, "r")) != NULL) {
*data = xs_json_load(f);
fclose(f);
}
else
status = 404;
status = HTTP_STATUS_NOT_FOUND;
return status;
}
@ -1576,7 +1603,7 @@ int actor_add(const char *actor, const xs_dict *msg)
int actor_get(const char *actor, xs_dict **data)
/* returns an already downloaded actor */
{
int status = 200;
int status = HTTP_STATUS_OK;
xs_dict *d = NULL;
if (xs_startswith(actor, srv_baseurl)) {
@ -1590,10 +1617,10 @@ int actor_get(const char *actor, xs_dict **data)
*data = msg_actor(&user);
user_free(&user);
return 200;
return HTTP_STATUS_OK;
}
else
return 404;
return HTTP_STATUS_NOT_FOUND;
}
/* read the object */
@ -1606,7 +1633,7 @@ int actor_get(const char *actor, xs_dict **data)
if (xs_is_null(xs_dict_get(d, "id")) || xs_is_null(xs_dict_get(d, "type"))) {
srv_debug(1, xs_fmt("corrupted actor object %s", actor));
d = xs_free(d);
return 404;
return HTTP_STATUS_NOT_FOUND;
}
if (data)
@ -1622,7 +1649,7 @@ int actor_get(const char *actor, xs_dict **data)
if (mtime(fn) + max_time < (double) time(NULL)) {
/* actor data exists but also stinks */
status = 205; /* "205: Reset Content" "110: Response Is Stale" */
status = HTTP_STATUS_RESET_CONTENT; /* "110: Response Is Stale" */
}
return status;
@ -1634,7 +1661,7 @@ int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
{
int status = actor_get(actor, data);
if (status == 205 && user && !xs_startswith(actor, srv_baseurl))
if (status == HTTP_STATUS_RESET_CONTENT && user && !xs_startswith(actor, srv_baseurl))
enqueue_actor_refresh(user, actor, 0);
return status;
@ -1953,7 +1980,7 @@ static int _load_raw_file(const char *fn, xs_val **data, int *size,
const char *inm, xs_str **etag)
/* loads a cached file */
{
int status = 404;
int status = HTTP_STATUS_NOT_FOUND;
if (fn) {
double tm = mtime(fn);
@ -1965,7 +1992,7 @@ static int _load_raw_file(const char *fn, xs_val **data, int *size,
/* if if-none-match is set, check if it's the same */
if (!xs_is_null(inm) && strcmp(e, inm) == 0) {
/* client has the newest version */
status = 304;
status = HTTP_STATUS_NOT_MODIFIED;
}
else {
/* newer or never downloaded; read the full file */
@ -1976,7 +2003,7 @@ static int _load_raw_file(const char *fn, xs_val **data, int *size,
*data = xs_read(f, size);
fclose(f);
status = 200;
status = HTTP_STATUS_OK;
}
}

98
html.c
View File

@ -2540,7 +2540,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype, xs_str **etag)
{
const char *accept = xs_dict_get(req, "accept");
int status = 404;
int status = HTTP_STATUS_NOT_FOUND;
snac snac;
xs *uid = NULL;
const char *p_path;
@ -2553,7 +2553,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
if (xs_is_null(v)) {
srv_log(xs_fmt("html_get_handler bad query '%s'", q_path));
return 404;
return HTTP_STATUS_NOT_FOUND;
}
uid = xs_dup(v);
@ -2569,7 +2569,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
if (!uid || !user_open(&snac, uid)) {
/* invalid user */
srv_debug(1, xs_fmt("html_get_handler bad user %s", uid));
return 404;
return HTTP_STATUS_NOT_FOUND;
}
/* return the RSS if requested by Accept header */
@ -2598,7 +2598,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
/** empty public timeline for private users **/
*body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1);
*b_size = strlen(*body);
status = 200;
status = HTTP_STATUS_OK;
}
else
if (cache && history_mtime(&snac, h) > timeline_mtime(&snac)) {
@ -2617,7 +2617,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
*body = html_timeline(&snac, pins, 1, skip, show, xs_list_len(next), NULL, "", 1);
*b_size = strlen(*body);
status = 200;
status = HTTP_STATUS_OK;
if (save)
history_add(&snac, h, *body, *b_size, etag);
@ -2627,7 +2627,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
if (strcmp(p_path, "admin") == 0) { /** private timeline **/
if (!login(&snac, req)) {
*body = xs_dup(uid);
status = 401;
status = HTTP_STATUS_UNAUTHORIZED;
}
else {
const char *q = xs_dict_get(q_vars, "q");
@ -2649,7 +2649,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
*body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0);
*b_size = strlen(*body);
status = 200;
status = HTTP_STATUS_OK;
}
else {
/** search by content **/
@ -2670,7 +2670,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
*body = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show, title, page, 0);
*b_size = strlen(*body);
status = 200;
status = HTTP_STATUS_OK;
}
}
else {
@ -2699,7 +2699,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
xs_list_len(next), NULL, "/admin", 1);
*b_size = strlen(*body);
status = 200;
status = HTTP_STATUS_OK;
if (save)
history_add(&snac, "timeline.html_", *body, *b_size, etag);
@ -2711,7 +2711,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
if (xs_startswith(p_path, "admin/p/")) { /** unique post by md5 **/
if (!login(&snac, req)) {
*body = xs_dup(uid);
status = 401;
status = HTTP_STATUS_UNAUTHORIZED;
}
else {
xs *l = xs_split(p_path, "/");
@ -2722,7 +2722,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
*body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1);
*b_size = strlen(*body);
status = 200;
status = HTTP_STATUS_OK;
}
}
}
@ -2730,31 +2730,31 @@ int html_get_handler(const xs_dict *req, const char *q_path,
if (strcmp(p_path, "people") == 0) { /** the list of people **/
if (!login(&snac, req)) {
*body = xs_dup(uid);
status = 401;
status = HTTP_STATUS_UNAUTHORIZED;
}
else {
*body = html_people(&snac);
*b_size = strlen(*body);
status = 200;
status = HTTP_STATUS_OK;
}
}
else
if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
if (!login(&snac, req)) {
*body = xs_dup(uid);
status = 401;
status = HTTP_STATUS_UNAUTHORIZED;
}
else {
*body = html_notifications(&snac, skip, show);
*b_size = strlen(*body);
status = 200;
status = HTTP_STATUS_OK;
}
}
else
if (strcmp(p_path, "instance") == 0) { /** instance timeline **/
if (!login(&snac, req)) {
*body = xs_dup(uid);
status = 401;
status = HTTP_STATUS_UNAUTHORIZED;
}
else {
xs *list = timeline_instance_list(skip, show);
@ -2763,14 +2763,14 @@ int html_get_handler(const xs_dict *req, const char *q_path,
*body = html_timeline(&snac, list, 0, skip, show,
xs_list_len(next), L("Showing instance timeline"), "/instance", 0);
*b_size = strlen(*body);
status = 200;
status = HTTP_STATUS_OK;
}
}
else
if (xs_startswith(p_path, "list/")) { /** list timelines **/
if (!login(&snac, req)) {
*body = xs_dup(uid);
status = 401;
status = HTTP_STATUS_UNAUTHORIZED;
}
else {
xs *l = xs_split(p_path, "/");
@ -2787,14 +2787,14 @@ int html_get_handler(const xs_dict *req, const char *q_path,
*body = html_timeline(&snac, list, 0, skip, show,
xs_list_len(next), title, base, 1);
*b_size = strlen(*body);
status = 200;
status = HTTP_STATUS_OK;
}
}
}
else
if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/
if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
return 403;
return HTTP_STATUS_FORBIDDEN;
xs *id = xs_fmt("%s/%s", snac.actor, p_path);
xs *msg = NULL;
@ -2807,7 +2807,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
*body = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1);
*b_size = strlen(*body);
status = 200;
status = HTTP_STATUS_OK;
}
}
else
@ -2829,10 +2829,10 @@ int html_get_handler(const xs_dict *req, const char *q_path,
else
if (xs_startswith(p_path, "h/")) { /** an entry from the history **/
if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
return 403;
return HTTP_STATUS_FORBIDDEN;
if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE)
return 403;
return HTTP_STATUS_FORBIDDEN;
xs *l = xs_split(p_path, "/");
const char *id = xs_list_get(l, 1);
@ -2841,7 +2841,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
if (xs_endswith(id, "timeline.html_")) {
/* Don't let them in */
*b_size = 0;
status = 404;
status = HTTP_STATUS_NOT_FOUND;
}
else
status = history_get(&snac, id, body, b_size,
@ -2851,7 +2851,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
else
if (strcmp(p_path, ".rss") == 0) { /** public timeline in RSS format **/
if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
return 403;
return HTTP_STATUS_FORBIDDEN;
xs *elems = timeline_simple_list(&snac, "public", 0, 20);
xs *bio = not_really_markdown(xs_dict_get(snac.config, "bio"), NULL, NULL);
@ -2865,12 +2865,12 @@ int html_get_handler(const xs_dict *req, const char *q_path,
*body = timeline_to_rss(&snac, elems, rss_title, rss_link, bio);
*b_size = strlen(*body);
*ctype = "application/rss+xml; charset=utf-8";
status = 200;
status = HTTP_STATUS_OK;
snac_debug(&snac, 1, xs_fmt("serving RSS"));
}
else
status = 404;
status = HTTP_STATUS_NOT_FOUND;
user_free(&snac);
@ -2901,7 +2901,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
if (!uid || !user_open(&snac, uid)) {
/* invalid user */
srv_debug(1, xs_fmt("html_post_handler bad user %s", uid));
return 404;
return HTTP_STATUS_NOT_FOUND;
}
p_path = xs_list_get(l, 2);
@ -2910,7 +2910,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
if (!login(&snac, req)) {
user_free(&snac);
*body = xs_dup(uid);
return 401;
return HTTP_STATUS_UNAUTHORIZED;
}
p_vars = xs_dict_get(req, "p_vars");
@ -3049,7 +3049,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
history_del(&snac, "timeline.html_");
}
status = 303;
status = HTTP_STATUS_SEE_OTHER;
}
else
if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/
@ -3060,11 +3060,11 @@ int html_post_handler(const xs_dict *req, const char *q_path,
const char *group = xs_dict_get(p_vars, "group");
if (action == NULL)
return 404;
return HTTP_STATUS_NOT_FOUND;
snac_debug(&snac, 1, xs_fmt("web action '%s' received", action));
status = 303;
status = HTTP_STATUS_SEE_OTHER;
if (strcmp(action, L("Like")) == 0) { /** **/
xs *msg = msg_admiration(&snac, id, "Like");
@ -3216,10 +3216,10 @@ int html_post_handler(const xs_dict *req, const char *q_path,
timeline_touch(&snac);
}
else
status = 404;
status = HTTP_STATUS_NOT_FOUND;
/* delete the cached timeline */
if (status == 303)
if (status == HTTP_STATUS_SEE_OTHER)
history_del(&snac, "timeline.html_");
}
else
@ -3334,36 +3334,16 @@ int html_post_handler(const xs_dict *req, const char *q_path,
snac.config = xs_dict_set(snac.config, "passwd", pw);
}
xs *fn = xs_fmt("%s/user.json", snac.basedir);
xs *bfn = xs_fmt("%s.bak", fn);
FILE *f;
user_persist(&snac);
rename(fn, bfn);
if ((f = fopen(fn, "w")) != NULL) {
xs_json_dump(snac.config, 4, f);
fclose(f);
}
else
rename(bfn, fn);
history_del(&snac, "timeline.html_");
xs *a_msg = msg_actor(&snac);
xs *u_msg = msg_update(&snac, a_msg);
enqueue_message(&snac, u_msg);
enqueue_verify_links(&snac);
status = 303;
status = HTTP_STATUS_SEE_OTHER;
}
else
if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
notify_clear(&snac);
timeline_touch(&snac);
status = 303;
status = HTTP_STATUS_SEE_OTHER;
}
else
if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/
@ -3416,10 +3396,10 @@ int html_post_handler(const xs_dict *req, const char *q_path,
}
}
status = 303;
status = HTTP_STATUS_SEE_OTHER;
}
if (status == 303) {
if (status == HTTP_STATUS_SEE_OTHER) {
const char *redir = xs_dict_get(p_vars, "redir");
if (xs_is_null(redir))

45
http_codes.h Normal file
View File

@ -0,0 +1,45 @@
HTTP_STATUS(100, CONTINUE, Continue)
HTTP_STATUS(101, SWITCHING_PROTOCOLS, Switching Protocols)
HTTP_STATUS(102, PROCESSING, Processing)
HTTP_STATUS(103, EARLY_HINTS, Early Hints)
HTTP_STATUS(200, OK, OK)
HTTP_STATUS(201, CREATED, Created)
HTTP_STATUS(202, ACCEPTED, Accepted)
HTTP_STATUS(203, NON_AUTHORITATIVE_INFORMATION, Non Authoritative Information)
HTTP_STATUS(204, NO_CONTENT, No Content)
HTTP_STATUS(205, RESET_CONTENT, Reset Content)
HTTP_STATUS(206, PARTIAL_CONTENT, Partial Content)
HTTP_STATUS(207, MULTI_STATUS, Multi Status)
HTTP_STATUS(208, ALREADY_REPORTED, Already Reported)
HTTP_STATUS(218, THIS_IS_FINE, This Is Fine)
HTTP_STATUS(226, IM_USED, IM Used)
HTTP_STATUS(300, MULTIPLE_CHOICES, Multiple Choices)
HTTP_STATUS(301, MOVED_PERMANENTLY, Moved Permanently)
HTTP_STATUS(302, FOUND, Found)
HTTP_STATUS(303, SEE_OTHER, See Other)
HTTP_STATUS(304, NOT_MODIFIED, Not Modified)
HTTP_STATUS(305, USE_PROXY, Use Proxy)
HTTP_STATUS(306, SWITCH_PROXY, Switch Proxy)
HTTP_STATUS(307, TEMPORARY_REDIRECT, Temporary Redirect)
HTTP_STATUS(308, PERMANENT_REDIRECT, Permanent Redirect)
HTTP_STATUS(400, BAD_REQUEST, Bad Request)
HTTP_STATUS(401, UNAUTHORIZED, Unauthorized)
HTTP_STATUS(402, PAYMENT_REQUIRED, Payment Required)
HTTP_STATUS(403, FORBIDDEN, Forbidden)
HTTP_STATUS(404, NOT_FOUND, Not Found)
HTTP_STATUS(405, METHOD_NOT_ALLOWED, Method Not Allowed)
HTTP_STATUS(406, NOT_ACCEPTABLE, Not Acceptable)
HTTP_STATUS(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required)
HTTP_STATUS(408, REQUEST_TIMEOUT, Request Timeout)
HTTP_STATUS(409, CONFLICT, Conflict)
HTTP_STATUS(410, GONE, Gone)
HTTP_STATUS(421, MISDIRECTED_REQUEST, Misdirected Request)
HTTP_STATUS(422, UNPROCESSABLE_CONTENT, Unprocessable Content)
HTTP_STATUS(499, CLIENT_CLOSED_REQUEST, Client Closed Request)
HTTP_STATUS(500, INTERNAL_SERVER_ERROR, Internal Server Error)
HTTP_STATUS(501, NOT_IMPLEMENTED, Not Implemented)
HTTP_STATUS(502, BAD_GATEWAY, Bad Gateway)
HTTP_STATUS(503, SERVICE_UNAVAILABLE, Service Unavailable)
HTTP_STATUS(504, GATEWAY_TIMEOUT, Gateway Timeout)
HTTP_STATUS(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported)
HTTP_STATUS(507, INSUFFICIENT_STORAGE, Insufficient Storage)

38
httpd.c
View File

@ -217,17 +217,17 @@ int server_get_handler(xs_dict *req, const char *q_path,
*body = greeting_html();
if (*body)
status = 200;
status = HTTP_STATUS_OK;
}
else
if (strcmp(q_path, "/susie.png") == 0 || strcmp(q_path, "/favicon.ico") == 0 ) {
status = 200;
status = HTTP_STATUS_OK;
*body = xs_base64_dec(default_avatar_base64(), b_size);
*ctype = "image/png";
}
else
if (strcmp(q_path, "/.well-known/nodeinfo") == 0) {
status = 200;
status = HTTP_STATUS_OK;
*ctype = "application/json; charset=utf-8";
*body = xs_fmt("{\"links\":["
"{\"rel\":\"http:/" "/nodeinfo.diaspora.software/ns/schema/2.0\","
@ -236,7 +236,7 @@ int server_get_handler(xs_dict *req, const char *q_path,
}
else
if (strcmp(q_path, "/.well-known/host-meta") == 0) {
status = 200;
status = HTTP_STATUS_OK;
*ctype = "application/xrd+xml";
*body = xs_fmt("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<XRD>"
@ -245,13 +245,13 @@ int server_get_handler(xs_dict *req, const char *q_path,
}
else
if (strcmp(q_path, "/nodeinfo_2_0") == 0) {
status = 200;
status = HTTP_STATUS_OK;
*ctype = "application/json; charset=utf-8";
*body = nodeinfo_2_0();
}
else
if (strcmp(q_path, "/robots.txt") == 0) {
status = 200;
status = HTTP_STATUS_OK;
*ctype = "text/plain";
*body = xs_str_new("User-agent: *\n"
"Disallow: /\n");
@ -360,10 +360,20 @@ void httpd_connection(FILE *f)
payload, p_size, &body, &b_size, &ctype);
#endif
}
else
if (strcmp(method, "PATCH") == 0) {
#ifndef NO_MASTODON_API
if (status == 0)
status = mastoapi_patch_handler(req, q_path,
payload, p_size, &body, &b_size, &ctype);
#endif
}
else
if (strcmp(method, "OPTIONS") == 0) {
status = 200;
status = HTTP_STATUS_OK;
}
else
if (strcmp(method, "DELETE") == 0) {
@ -378,22 +388,22 @@ void httpd_connection(FILE *f)
if (status == 0) {
srv_archive_error("unattended_method", "unattended method", req, payload);
srv_debug(1, xs_fmt("httpd_connection unattended %s %s", method, q_path));
status = 404;
status = HTTP_STATUS_NOT_FOUND;
}
if (status == 403)
if (status == HTTP_STATUS_FORBIDDEN)
body = xs_str_new("<h1>403 Forbidden</h1>");
if (status == 404)
if (status == HTTP_STATUS_NOT_FOUND)
body = xs_str_new("<h1>404 Not Found</h1>");
if (status == 400 && body != NULL)
if (status == HTTP_STATUS_BAD_REQUEST && body != NULL)
body = xs_str_new("<h1>400 Bad Request</h1>");
if (status == 303)
if (status == HTTP_STATUS_SEE_OTHER)
headers = xs_dict_append(headers, "location", body);
if (status == 401) {
if (status == HTTP_STATUS_UNAUTHORIZED) {
xs *www_auth = xs_fmt("Basic realm=\"@%s@%s snac login\"",
body, xs_dict_get(srv_config, "host"));
@ -432,7 +442,7 @@ void httpd_connection(FILE *f)
if (p_state->use_fcgi)
xs_fcgi_response(f, status, headers, body, b_size, fcgi_id);
else
xs_httpd_response(f, status, headers, body, b_size);
xs_httpd_response(f, status, http_status_text(status), headers, body, b_size);
fclose(f);

File diff suppressed because it is too large Load Diff

12
snac.c
View File

@ -170,3 +170,15 @@ int check_password(const char *uid, const char *passwd, const char *hash)
return ret;
}
const char *http_status_text(int status)
/* translate status codes to canonical status texts */
{
switch (status) {
#define HTTP_STATUS(code, name, text) case HTTP_STATUS_ ## name: return #text;
#include "http_codes.h"
#undef HTTP_STATUS
default: return "Unknown";
}
}

14
snac.h
View File

@ -76,6 +76,7 @@ int user_open(snac *snac, const char *uid);
void user_free(snac *snac);
xs_list *user_list(void);
int user_open_by_md5(snac *snac, const char *md5);
int user_persist(snac *snac);
int validate_uid(const char *uid);
@ -358,6 +359,19 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
int mastoapi_put_handler(const xs_dict *req, const char *q_path,
const char *payload, int p_size,
char **body, int *b_size, char **ctype);
void persist_image(const char *key, const xs_val *data, const char *payload, snac *snac);
int mastoapi_patch_handler(const xs_dict *req, const char *q_path,
const char *payload, int p_size,
char **body, int *b_size, char **ctype);
void mastoapi_purge(void);
void verify_links(snac *user);
typedef enum {
#define HTTP_STATUS(code, name, text) HTTP_STATUS_ ## name = code,
#include "http_codes.h"
#undef HTTP_STATUS
} http_status;
const char *http_status_text(int status);

View File

@ -42,7 +42,7 @@ int webfinger_request_signed(snac *snac, const char *qs, char **actor, char **us
}
if (host == NULL || resource == NULL)
return 400;
return HTTP_STATUS_BAD_REQUEST;
headers = xs_dict_append(headers, "accept", "application/json");
headers = xs_dict_append(headers, "user-agent", USER_AGENT);
@ -139,7 +139,7 @@ int webfinger_get_handler(xs_dict *req, char *q_path,
const char *resource = xs_dict_get(q_vars, "resource");
if (resource == NULL)
return 400;
return HTTP_STATUS_BAD_REQUEST;
snac snac;
int found = 0;
@ -220,12 +220,12 @@ int webfinger_get_handler(xs_dict *req, char *q_path,
user_free(&snac);
status = 200;
status = HTTP_STATUS_OK;
*body = j;
*ctype = "application/jrd+json";
}
else
status = 404;
status = HTTP_STATUS_NOT_FOUND;
srv_debug(1, xs_fmt("webfinger_get_handler resource=%s %d", resource, status));

2
xs.h
View File

@ -52,7 +52,7 @@ typedef char xs_data;
void *xs_free(void *ptr);
void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func);
#define xs_realloc(ptr, size) _xs_realloc(ptr, size, __FILE__, __LINE__, __FUNCTION__)
#define xs_realloc(ptr, size) _xs_realloc(ptr, size, __FILE__, __LINE__, __func__)
int _xs_blk_size(int sz);
void _xs_destroy(char **var);
#define xs_debug() raise(SIGTRAP)

View File

@ -5,7 +5,7 @@
#define _XS_HTTPD_H
xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size);
void xs_httpd_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size);
void xs_httpd_response(FILE *f, int status, const char *status_text, xs_dict *headers, xs_str *body, int b_size);
#ifdef XS_IMPLEMENTATION
@ -95,14 +95,14 @@ xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size)
}
void xs_httpd_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size)
void xs_httpd_response(FILE *f, int status, const char *status_text, xs_dict *headers, xs_str *body, int b_size)
/* sends an httpd response */
{
xs *proto;
const xs_str *k;
const xs_val *v;
proto = xs_fmt("HTTP/1.1 %d %s", status, status / 100 == 2 ? "OK" : "ERROR");
proto = xs_fmt("HTTP/1.1 %d %s", status, status_text);
fprintf(f, "%s\r\n", proto);
int c = 0;

View File

@ -111,4 +111,4 @@ int xs_set_add(xs_set *s, const xs_val *data)
#endif /* XS_IMPLEMENTATION */
#endif /* XS_SET_H */
#endif /* XS_SET_H */

View File

@ -8,7 +8,6 @@ xs_str *xs_url_dec(const char *str);
xs_dict *xs_url_vars(const char *str);
xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *header);
#ifdef XS_IMPLEMENTATION
xs_str *xs_url_dec(const char *str)
@ -107,7 +106,13 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
if (xs_list_len(l1) != 2)
return NULL;
boundary = xs_fmt("--%s", xs_list_get(l1, 1));
boundary = xs_dup(xs_list_get(l1, 1));
/* Tokodon sends the boundary header with double quotes surrounded */
if (xs_starts_and_ends("\"", boundary, "\"") != 0)
boundary = xs_strip_chars_i(boundary, "\"");
boundary = xs_fmt("--%s", boundary);
}
bsz = strlen(boundary);
@ -120,6 +125,7 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
xs *l1 = NULL;
const char *vn = NULL;
const char *fn = NULL;
const char *ct = NULL;
char *q;
int po, ps;
@ -132,32 +138,47 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
/* skip the \r\n */
p += 2;
/* now on a Content-Disposition... line; get it */
q = strchr(p, '\r');
s1 = xs_realloc(NULL, q - p + 1);
memcpy(s1, p, q - p);
s1[q - p] = '\0';
/* Tokodon sends also a Content-Type headers,
let's use it to determine the file type */
do {
if (p[0] == 13 && p[1] == 10)
break;
q = strchr(p, '\r');
s1 = xs_realloc(NULL, q - p + 1);
memcpy(s1, p, q - p);
s1[q - p] = '\0';
/* move on (over a \r\n) */
p = q;
if (xs_startswith(s1, "Content-Disposition")) {
/* split by " like a primitive man */
l1 = xs_split(s1, "\"");
/* split by " like a primitive man */
l1 = xs_split(s1, "\"");
/* get the variable name */
vn = xs_list_get(l1, 1);
/* get the variable name */
vn = xs_list_get(l1, 1);
/* is it an attached file? */
if (xs_list_len(l1) >= 4 && strcmp(xs_list_get(l1, 2), "; filename=") == 0) {
/* get the file name */
fn = xs_list_get(l1, 3);
}
}
else
if (xs_startswith(s1, "Content-Type")) {
l1 = xs_split(s1, ":");
/* is it an attached file? */
if (xs_list_len(l1) >= 4 && strcmp(xs_list_get(l1, 2), "; filename=") == 0) {
/* get the file name */
fn = xs_list_get(l1, 3);
}
if (xs_list_len(l1) >= 2) {
ct = xs_lstrip_chars_i(xs_dup(xs_list_get(l1, 1)), " ");
}
}
p += (q - p);
p += 2; // Skip /r/n
} while (1);
/* find the start of the part content */
if ((p = xs_memmem(p, p_size - (p - payload), "\r\n\r\n", 4)) == NULL)
if ((p = xs_memmem(p, p_size - (p - payload), "\r\n", 2)) == NULL)
break;
p += 4;
p += 2; // Skip empty line
/* find the next boundary */
if ((q = xs_memmem(p, p_size - (p - payload), boundary, bsz)) == NULL)
@ -169,6 +190,13 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
/* is it a filename? */
if (fn != NULL) {
/* p_var value is a list */
/* if filename has no extension and content-type is image, attach extension to the filename */
if (strchr(fn, '.') == NULL && xs_startswith(ct, "image/")) {
char *ext = strchr(ct, '/');
ext++;
fn = xs_str_cat(xs_str_new(""), fn, ".", ext);
}
xs *l1 = xs_list_new();
xs *vpo = xs_number_new(po);
xs *vps = xs_number_new(ps);