Merge pull request 'master' (#1) from grunfink/snac2:master into master

Reviewed-on: https://codeberg.org/poesty/snac2/pulls/1
This commit is contained in:
poesty 2023-05-07 07:27:24 +00:00
commit e65760a349
16 changed files with 610 additions and 147 deletions

View file

@ -1,6 +1,6 @@
PREFIX=/usr/local PREFIX=/usr/local
PREFIX_MAN=$(PREFIX)/man PREFIX_MAN=$(PREFIX)/man
CFLAGS?=-g -Wall CFLAGS?=-g -Wall -Wextra
all: snac all: snac
@ -40,7 +40,7 @@ httpd.o: httpd.c xs.h xs_io.h xs_encdec.h xs_json.h xs_socket.h \
xs_httpd.h xs_mime.h snac.h xs_httpd.h xs_mime.h snac.h
main.o: main.c xs.h xs_io.h xs_encdec.h xs_json.h snac.h main.o: main.c xs.h xs_io.h xs_encdec.h xs_json.h snac.h
mastoapi.o: mastoapi.c xs.h xs_encdec.h xs_openssl.h xs_json.h xs_io.h \ mastoapi.o: mastoapi.c xs.h xs_encdec.h xs_openssl.h xs_json.h xs_io.h \
xs_time.h snac.h xs_time.h xs_glob.h snac.h
snac.o: snac.c xs.h xs_io.h xs_encdec.h xs_json.h xs_curl.h xs_openssl.h \ snac.o: snac.c xs.h xs_io.h xs_encdec.h xs_json.h xs_curl.h xs_openssl.h \
xs_socket.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h \ xs_socket.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h \
snac.h snac.h

View file

@ -1,5 +1,15 @@
# Release Notes # Release Notes
## 2.30
Fixed a bug that made some notifications to be missed.
New Mastodon API features: the instance public timeline is now a real one, unfavourite / unreblog is supported (somewhat). Some regression bugs regarding image posting were also fixed.
The non-standard `Ping` and `Pong` #ActivityPub activities have been implemented as proposed by @tedu@honk.tedunangst.com in the https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt document (with a minor diversion: retries are managed in the same way as the rest of #snac messages).
The build process now includes the `-Wextra` flag.
## 2.29 ## 2.29
New Mastodon API features: account search, relationships (so the Follow/Unfollow buttons now appear for each account), follow and unfollow accounts, an instance-level timeline (very kludgy), custom emojis for accounts and statuses, many bug fixes (sadly, the Mastodon official app still does not work). New Mastodon API features: account search, relationships (so the Follow/Unfollow buttons now appear for each account), follow and unfollow accounts, an instance-level timeline (very kludgy), custom emojis for accounts and statuses, many bug fixes (sadly, the Mastodon official app still does not work).

View file

@ -109,7 +109,7 @@ int actor_request(snac *snac, const char *actor, xs_dict **data)
if (valid_status(status2)) { if (valid_status(status2)) {
/* renew data */ /* renew data */
status = actor_add(snac, actor, payload); status = actor_add(actor, payload);
if (data != NULL) { if (data != NULL) {
*data = payload; *data = payload;
@ -437,7 +437,8 @@ void process_tags(snac *snac, const char *content, d_char **n_content, d_char **
/** messages **/ /** messages **/
d_char *msg_base(snac *snac, char *type, char *id, char *actor, char *date, char *object) xs_dict *msg_base(snac *snac, const char *type, const char *id,
const char *actor, const char *date, const char *object)
/* creates a base ActivityPub message */ /* creates a base ActivityPub message */
{ {
xs *did = NULL; xs *did = NULL;
@ -467,7 +468,7 @@ d_char *msg_base(snac *snac, char *type, char *id, char *actor, char *date, char
} }
} }
d_char *msg = xs_dict_new(); xs_dict *msg = xs_dict_new();
msg = xs_dict_append(msg, "@context", "https:/" "/www.w3.org/ns/activitystreams"); msg = xs_dict_append(msg, "@context", "https:/" "/www.w3.org/ns/activitystreams");
msg = xs_dict_append(msg, "type", type); msg = xs_dict_append(msg, "type", type);
@ -845,6 +846,28 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
} }
xs_dict *msg_ping(snac *user, const char *rcpt)
/* creates a Ping message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */
{
xs_dict *msg = msg_base(user, "Ping", "@dummy", user->actor, NULL, NULL);
msg = xs_dict_append(msg, "to", rcpt);
return msg;
}
xs_dict *msg_pong(snac *user, const char *rcpt, const char *object)
/* creates a Pong message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */
{
xs_dict *msg = msg_base(user, "Pong", "@dummy", user->actor, NULL, object);
msg = xs_dict_append(msg, "to", rcpt);
return msg;
}
void notify(snac *snac, xs_str *type, xs_str *utype, xs_str *actor, xs_dict *msg) void notify(snac *snac, xs_str *type, xs_str *utype, xs_str *actor, xs_dict *msg)
/* notifies the user of relevant events */ /* notifies the user of relevant events */
{ {
@ -1121,7 +1144,7 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
else else
if (strcmp(type, "Update") == 0) { if (strcmp(type, "Update") == 0) {
if (strcmp(utype, "Person") == 0) { if (strcmp(utype, "Person") == 0) {
actor_add(snac, actor, xs_dict_get(msg, "object")); actor_add(actor, xs_dict_get(msg, "object"));
snac_log(snac, xs_fmt("updated actor %s", actor)); snac_log(snac, xs_fmt("updated actor %s", actor));
} }
@ -1147,7 +1170,19 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object)); snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object));
} }
else else
snac_debug(snac, 1, xs_fmt("process_message type '%s' ignored", type)); if (strcmp(type, "Pong") == 0) {
snac_log(snac, xs_fmt("'Pong' received from %s", actor));
}
else
if (strcmp(type, "Ping") == 0) {
snac_log(snac, xs_fmt("'Ping' requested from %s", actor));
xs *rsp = msg_pong(snac, actor, xs_dict_get(msg, "id"));
enqueue_output_by_actor(snac, rsp, actor, 0);
}
else
snac_debug(snac, 1, xs_fmt("process_input_message type '%s' ignored", type));
if (do_notify) { if (do_notify) {
notify(snac, type, utype, actor, msg); notify(snac, type, utype, actor, msg);
@ -1386,6 +1421,7 @@ void process_queue_item(xs_dict *q_item)
xs *rsp = xs_http_request("POST", url, headers, xs *rsp = xs_http_request("POST", url, headers,
body, strlen(body), &status, NULL, NULL, 0); body, strlen(body), &status, NULL, NULL, 0);
rsp = xs_free(rsp);
srv_debug(0, xs_fmt("telegram post %d", status)); srv_debug(0, xs_fmt("telegram post %d", status));
} }
@ -1426,7 +1462,7 @@ int process_queue(void)
/** HTTP handlers */ /** HTTP handlers */
int activitypub_get_handler(d_char *req, char *q_path, int activitypub_get_handler(const xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype) char **body, int *b_size, char **ctype)
{ {
int status = 200; int status = 200;
@ -1519,11 +1555,13 @@ int activitypub_get_handler(d_char *req, char *q_path,
} }
int activitypub_post_handler(d_char *req, char *q_path, int activitypub_post_handler(const xs_dict *req, const char *q_path,
d_char *payload, int p_size, char *payload, int p_size,
char **body, int *b_size, char **ctype) char **body, int *b_size, char **ctype)
/* processes an input message */ /* processes an input message */
{ {
(void)b_size;
int status = 202; /* accepted */ int status = 202; /* accepted */
char *i_ctype = xs_dict_get(req, "content-type"); char *i_ctype = xs_dict_get(req, "content-type");
snac snac; snac snac;

121
data.c
View file

@ -328,6 +328,51 @@ 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;
FILE *f;
pthread_mutex_lock(&data_mutex);
if ((f = fopen(fn, "r+")) != NULL) {
char line[256];
while (fgets(line, sizeof(line), f) != NULL) {
line[32] = '\0';
if (strcmp(line, md5) == 0) {
/* found! just rewind, overwrite it with garbage
and an eventual call to index_gc() will clean it
[yes: this breaks index_len()] */
fseek(f, -33, SEEK_CUR);
fwrite("-", 1, 1, f);
status = 200;
break;
}
}
fclose(f);
}
else
status = 500;
pthread_mutex_unlock(&data_mutex);
return status;
}
int index_del(const char *fn, const char *id)
/* deletes an id from an index */
{
xs *md5 = xs_md5_hex(id, strlen(id));
return index_del_md5(fn, md5);
}
int index_gc(const char *fn) int index_gc(const char *fn)
/* garbage-collects an index, deleting objects that are not here */ /* garbage-collects an index, deleting objects that are not here */
{ {
@ -772,6 +817,23 @@ int object_admire(const char *id, const char *actor, int like)
} }
int object_unadmire(const char *id, const char *actor, int like)
/* actor no longer likes or announces this object */
{
int status;
xs *fn = _object_fn(id);
fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
status = index_del(fn, actor);
srv_debug(0,
xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status));
return status;
}
int _object_user_cache(snac *snac, const char *id, const char *cachedir, int del) int _object_user_cache(snac *snac, const char *id, const char *cachedir, int del)
/* adds or deletes from a user cache */ /* adds or deletes from a user cache */
{ {
@ -969,8 +1031,13 @@ void timeline_update_indexes(snac *snac, const char *id)
if (valid_status(object_get(id, &msg))) { if (valid_status(object_get(id, &msg))) {
/* if its ours and is public, also store in public */ /* if its ours and is public, also store in public */
if (is_msg_public(snac, msg)) if (is_msg_public(snac, msg)) {
object_user_cache_add(snac, id, "public"); object_user_cache_add(snac, id, "public");
/* also add it to the instance public timeline */
xs *ipt = xs_fmt("%s/public.idx", srv_basedir);
index_add(ipt, id);
}
} }
} }
} }
@ -1041,7 +1108,7 @@ xs_list *timeline_top_level(snac *snac, xs_list *list)
} }
d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show) xs_list *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show)
/* returns a timeline (with all entries) */ /* returns a timeline (with all entries) */
{ {
int c_max; int c_max;
@ -1059,7 +1126,7 @@ d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int sho
} }
d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show) xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show)
/* returns a timeline (only top level entries) */ /* returns a timeline (only top level entries) */
{ {
xs *list = timeline_simple_list(snac, idx_name, skip, show); xs *list = timeline_simple_list(snac, idx_name, skip, show);
@ -1068,6 +1135,15 @@ d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show)
} }
xs_list *timeline_instance_list(int skip, int show)
/* returns the timeline for the full instance */
{
xs *idx = xs_fmt("%s/public.idx", srv_basedir);
return index_list_desc(idx, skip, show);
}
/** following **/ /** following **/
/* this needs special treatment and cannot use the object db as is, /* this needs special treatment and cannot use the object db as is,
@ -1273,30 +1349,49 @@ int is_hidden(snac *snac, const char *id)
} }
int actor_add(snac *snac, const char *actor, d_char *msg) int actor_add(const char *actor, xs_dict *msg)
/* adds an actor */ /* adds an actor */
{ {
return object_add_ow(actor, msg); return object_add_ow(actor, msg);
} }
int actor_get(snac *snac, const char *actor, d_char **data) int actor_get(snac *snac1, const char *actor, xs_dict **data)
/* returns an already downloaded actor */ /* returns an already downloaded actor */
{ {
int status = 200; int status = 200;
d_char *d; xs_dict *d = NULL;
if (strcmp(actor, snac->actor) == 0) { if (strcmp(actor, snac1->actor) == 0) {
/* this actor */ /* this actor */
if (data) if (data)
*data = msg_actor(snac); *data = msg_actor(snac1);
return status; return status;
} }
if (xs_startswith(actor, srv_baseurl)) {
/* it's a (possible) local user */
xs *l = xs_split(actor, "/");
const char *uid = xs_list_get(l, -1);
snac user;
if (!xs_is_null(uid) && user_open(&user, uid)) {
if (data)
*data = msg_actor(&user);
user_free(&user);
return 200;
}
else
return 404;
}
/* read the object */ /* read the object */
if (!valid_status(status = object_get(actor, &d))) if (!valid_status(status = object_get(actor, &d))) {
d = xs_free(d);
return status; return status;
}
if (data) if (data)
*data = d; *data = d;
@ -1719,7 +1814,7 @@ static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries)
} }
void enqueue_input(snac *snac, xs_dict *msg, xs_dict *req, int retries) void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries)
/* enqueues an input message */ /* enqueues an input message */
{ {
xs *qmsg = _new_qmsg("input", msg, retries); xs *qmsg = _new_qmsg("input", msg, retries);
@ -2020,7 +2115,11 @@ void purge_server(void)
xs *ib_dir = xs_fmt("%s/inbox", srv_basedir); xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
_purge_dir(ib_dir, 7); _purge_dir(ib_dir, 7);
srv_debug(1, xs_fmt("purge: global (obj: %d, idx: %d)", cnt, icnt)); /* purge the instance timeline */
xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir);
int itl_gc = index_gc(itl_fn);
srv_debug(1, xs_fmt("purge: global (obj: %d, idx: %d, itl: %d)", cnt, icnt, itl_gc));
} }

View file

@ -220,6 +220,36 @@ Please take note that they will show your timeline in a 'Mastodon fashion'
post display with the most active threads at the top that the web interface of post display with the most active threads at the top that the web interface of
.Nm .Nm
provides. provides.
.Ss Implementing post bots
.Nm
makes very easy to post messages in a non-interactive manner. This example
posts a string:
.Bd -literal -offset indent
uptime | snac note $SNAC_BASEDIR $SNAC_USER -
.Ed
.Pp
You can setup a line like this from a
.Xr crontab 5
or similar. Take note that you need a) command-line access to the same machine
that hosts the
.Nm
instance, and b) write permissions to the storage directories and files.
.Pp
You can also post non-interactively using the Mastodon API and a command-line
http tool like
.Xr curl 1
or similar. This has the advantage that you can do it remotely from any host,
anywhere; the only thing you need is an API Token. This is an example:
.Bd -literal -offset indent
curl -X POST https://$SNAC_HOST/api/v1/statuses \\
--header "Authorization: Bearer ${TOKEN}" -d "status=$(uptime)"
.Ed
.Pp
You can obtain an API Token by connecting to the following URL:
.Bd -literal -offset indent
https://$SNAC_HOST/oauth/x-snac-get-token
.Ed
.Pp
.Sh ENVIRONMENT .Sh ENVIRONMENT
.Bl -tag -width Ds .Bl -tag -width Ds
.It Ev DEBUG .It Ev DEBUG

View file

@ -1,7 +1,7 @@
#! /bin/sh #! /bin/sh
if [ ! -e /data/data/server.json ] if [ ! -e /data/data/server.json ]
then then
echo -ne "0.0.0.0\r\n8001\r\nlocalhost\r\n\r\n" | snac init /data/data echo -ne "0.0.0.0\r\n8001\r\nlocalhost\r\n\r\n\r\n" | snac init /data/data
snac adduser /data/data testuser snac adduser /data/data testuser
fi fi
SSLKEYLOGFILE=/data/key snac httpd /data/data SSLKEYLOGFILE=/data/key snac httpd /data/data

27
html.c
View file

@ -79,7 +79,7 @@ xs_str *actor_name(xs_dict *actor)
} }
d_char *html_actor_icon(snac *snac, d_char *os, char *actor, xs_str *html_actor_icon(xs_str *os, char *actor,
const char *date, const char *udate, const char *url, int priv) const char *date, const char *udate, const char *url, int priv)
{ {
xs *s = xs_str_new(NULL); xs *s = xs_str_new(NULL);
@ -168,7 +168,7 @@ d_char *html_msg_icon(snac *snac, d_char *os, char *msg)
date = xs_dict_get(msg, "published"); date = xs_dict_get(msg, "published");
udate = xs_dict_get(msg, "updated"); udate = xs_dict_get(msg, "updated");
os = html_actor_icon(snac, os, actor, date, udate, url, priv); os = html_actor_icon(os, actor, date, udate, url, priv);
} }
return os; return os;
@ -983,7 +983,7 @@ d_char *html_entry(snac *snac, d_char *os, char *msg, int local,
} }
d_char *html_user_footer(snac *snac, d_char *s) xs_str *html_user_footer(xs_str *s)
{ {
xs *s1 = xs_fmt( xs *s1 = xs_fmt(
"<div class=\"snac-footer\">\n" "<div class=\"snac-footer\">\n"
@ -1064,7 +1064,7 @@ d_char *html_timeline(snac *snac, char *list, int local, int skip, int show, int
s = xs_str_cat(s, s1); s = xs_str_cat(s, s1);
} }
s = html_user_footer(snac, s); s = html_user_footer(s);
s = xs_str_cat(s, "</body>\n</html>\n"); s = xs_str_cat(s, "</body>\n</html>\n");
@ -1088,8 +1088,7 @@ d_char *html_people_list(snac *snac, d_char *os, d_char *list, const char *heade
if (valid_status(actor_get(snac, actor_id, &actor))) { if (valid_status(actor_get(snac, actor_id, &actor))) {
s = xs_str_cat(s, "<div class=\"snac-post\">\n"); s = xs_str_cat(s, "<div class=\"snac-post\">\n");
s = html_actor_icon(snac, s, actor, xs_dict_get(actor, "published"), NULL, NULL, 0); s = html_actor_icon(s, actor, xs_dict_get(actor, "published"), NULL, NULL, 0);
/* content (user bio) */ /* content (user bio) */
char *c = xs_dict_get(actor, "summary"); char *c = xs_dict_get(actor, "summary");
@ -1182,7 +1181,7 @@ d_char *html_people(snac *snac)
s = html_people_list(snac, s, wers, L("People that follows you"), "e"); s = html_people_list(snac, s, wers, L("People that follows you"), "e");
s = html_user_footer(snac, s); s = html_user_footer(s);
s = xs_str_cat(s, "</body>\n</html>\n"); s = xs_str_cat(s, "</body>\n</html>\n");
@ -1226,7 +1225,7 @@ xs_str *html_notifications(snac *snac)
const char *actor_id = xs_dict_get(noti, "actor"); const char *actor_id = xs_dict_get(noti, "actor");
xs *actor = NULL; xs *actor = NULL;
if (!valid_status(object_get(actor_id, &actor))) if (!valid_status(actor_get(snac, actor_id, &actor)))
continue; continue;
xs *a_name = actor_name(actor); xs *a_name = actor_name(actor);
@ -1277,12 +1276,13 @@ xs_str *html_notifications(snac *snac)
s = xs_str_cat(s, s1); s = xs_str_cat(s, s1);
} }
s = html_user_footer(snac, s); s = html_user_footer(s);
s = xs_str_cat(s, "</body>\n</html>\n"); s = xs_str_cat(s, "</body>\n</html>\n");
/* set the check time to now */ /* set the check time to now */
xs *dummy = notify_check_time(snac, 1); xs *dummy = notify_check_time(snac, 1);
dummy = xs_free(dummy);
timeline_touch(snac); timeline_touch(snac);
@ -1290,7 +1290,8 @@ xs_str *html_notifications(snac *snac)
} }
int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char **ctype) int html_get_handler(const xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype)
{ {
char *accept = xs_dict_get(req, "accept"); char *accept = xs_dict_get(req, "accept");
int status = 404; int status = 404;
@ -1547,9 +1548,13 @@ int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char *
} }
int html_post_handler(d_char *req, char *q_path, d_char *payload, int p_size, int html_post_handler(const xs_dict *req, const char *q_path,
char *payload, int p_size,
char **body, int *b_size, char **ctype) char **body, int *b_size, char **ctype)
{ {
(void)p_size;
(void)ctype;
int status = 0; int status = 0;
snac snac; snac snac;
char *uid, *p_path; char *uid, *p_path;

2
http.c
View file

@ -33,7 +33,7 @@ xs_dict *http_signed_request_raw(const char *keyid, const char *seckey,
date = xs_str_utctime(0, "%a, %d %b %Y %H:%M:%S GMT"); date = xs_str_utctime(0, "%a, %d %b %Y %H:%M:%S GMT");
{ {
xs *s = xs_replace(url, "https:/" "/", ""); xs *s = xs_replace_n(url, "https:/" "/", "", 1);
l1 = xs_split_n(s, "/", 1); l1 = xs_split_n(s, "/", 1);
} }

View file

@ -43,12 +43,14 @@ d_char *nodeinfo_2_0(void)
} }
int server_get_handler(d_char *req, char *q_path, int server_get_handler(xs_dict *req, char *q_path,
char **body, int *b_size, char **ctype) char **body, int *b_size, char **ctype)
/* basic server services */ /* basic server services */
{ {
int status = 0; int status = 0;
(void)req;
/* is it the server root? */ /* is it the server root? */
if (*q_path == '\0') { if (*q_path == '\0') {
/* try to open greeting.html */ /* try to open greeting.html */
@ -285,6 +287,8 @@ static jmp_buf on_break;
void term_handler(int s) void term_handler(int s)
{ {
(void)s;
longjmp(on_break, 1); longjmp(on_break, 1);
} }
@ -401,6 +405,8 @@ static void *background_thread(void *arg)
{ {
time_t purge_time; time_t purge_time;
(void)arg;
/* first purge time */ /* first purge time */
purge_time = time(NULL) + 10 * 60; purge_time = time(NULL) + 10 * 60;

22
main.c
View file

@ -30,6 +30,7 @@ int usage(void)
printf("actor {basedir} {uid} {url} Requests an actor\n"); printf("actor {basedir} {uid} {url} Requests an actor\n");
printf("note {basedir} {uid} {'text'} Sends a note to followers\n"); printf("note {basedir} {uid} {'text'} Sends a note to followers\n");
printf("resetpwd {basedir} {uid} Resets the password of a user\n"); printf("resetpwd {basedir} {uid} Resets the password of a user\n");
printf("ping {basedir} {uid} {actor} Pings an actor\n");
return 1; return 1;
} }
@ -228,6 +229,27 @@ int main(int argc, char *argv[])
return 0; return 0;
} }
if (strcmp(cmd, "ping") == 0) {
xs *actor_o = NULL;
if (valid_status(actor_request(&snac, url, &actor_o))) {
xs *msg = msg_ping(&snac, url);
enqueue_output_by_actor(&snac, msg, url, 0);
if (dbglevel) {
xs *j = xs_json_dumps_pp(msg, 4);
printf("%s\n", j);
}
}
else {
srv_log(xs_fmt("Error getting actor %s", url));
return 1;
}
return 0;
}
if (strcmp(cmd, "request") == 0) { if (strcmp(cmd, "request") == 0) {
int status; int status;
xs *data = NULL; xs *data = NULL;

View file

@ -162,7 +162,7 @@ const char *login_page = ""
"<!DOCTYPE html>\n" "<!DOCTYPE html>\n"
"<body><h1>%s OAuth identify</h1>\n" "<body><h1>%s OAuth identify</h1>\n"
"<div style=\"background-color: red; color: white\">%s</div>\n" "<div style=\"background-color: red; color: white\">%s</div>\n"
"<form method=\"post\" action=\"https:/" "/%s/oauth/x-snac-login\">\n" "<form method=\"post\" action=\"https:/" "/%s/%s\">\n"
"<p>Login: <input type=\"text\" name=\"login\"></p>\n" "<p>Login: <input type=\"text\" name=\"login\"></p>\n"
"<p>Password: <input type=\"password\" name=\"passwd\"></p>\n" "<p>Password: <input type=\"password\" name=\"passwd\"></p>\n"
"<input type=\"hidden\" name=\"redir\" value=\"%s\">\n" "<input type=\"hidden\" name=\"redir\" value=\"%s\">\n"
@ -175,6 +175,8 @@ const char *login_page = ""
int oauth_get_handler(const xs_dict *req, const char *q_path, int oauth_get_handler(const xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype) char **body, int *b_size, char **ctype)
{ {
(void)b_size;
if (!xs_startswith(q_path, "/oauth/")) if (!xs_startswith(q_path, "/oauth/"))
return 0; return 0;
@ -185,7 +187,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
int status = 404; int status = 404;
xs_dict *msg = xs_dict_get(req, "q_vars"); xs_dict *msg = xs_dict_get(req, "q_vars");
xs *cmd = xs_replace(q_path, "/oauth", ""); xs *cmd = xs_replace_n(q_path, "/oauth", "", 1);
srv_debug(1, xs_fmt("oauth_get_handler %s", q_path)); srv_debug(1, xs_fmt("oauth_get_handler %s", q_path));
@ -206,17 +208,28 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
if (xs_is_null(state)) if (xs_is_null(state))
state = ""; state = "";
*body = xs_fmt(login_page, host, "", host, ruri, cid, state, USER_AGENT); *body = xs_fmt(login_page, host, "", host, "oauth/x-snac-login",
ruri, cid, state, USER_AGENT);
*ctype = "text/html"; *ctype = "text/html";
status = 200; status = 200;
srv_debug(0, xs_fmt("oauth authorize: generating login page")); srv_debug(1, xs_fmt("oauth authorize: generating login page"));
} }
else else
srv_debug(0, xs_fmt("oauth authorize: bad client_id %s", cid)); srv_debug(1, xs_fmt("oauth authorize: bad client_id %s", cid));
} }
else else
srv_debug(0, xs_fmt("oauth authorize: invalid or unset arguments")); srv_debug(1, xs_fmt("oauth authorize: invalid or unset arguments"));
}
else
if (strcmp(cmd, "/x-snac-get-token") == 0) {
const char *host = xs_dict_get(srv_config, "host");
*body = xs_fmt(login_page, host, "", host, "oauth/x-snac-get-token",
"", "", "", USER_AGENT);
*ctype = "text/html";
status = 200;
} }
return status; return status;
@ -227,6 +240,9 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
const char *payload, int p_size, const char *payload, int p_size,
char **body, int *b_size, char **ctype) char **body, int *b_size, char **ctype)
{ {
(void)p_size;
(void)b_size;
if (!xs_startswith(q_path, "/oauth/")) if (!xs_startswith(q_path, "/oauth/"))
return 0; return 0;
@ -245,7 +261,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
else else
args = xs_dup(xs_dict_get(req, "p_vars")); args = xs_dup(xs_dict_get(req, "p_vars"));
xs *cmd = xs_replace(q_path, "/oauth", ""); xs *cmd = xs_replace_n(q_path, "/oauth", "", 1);
srv_debug(1, xs_fmt("oauth_post_handler %s", q_path)); srv_debug(1, xs_fmt("oauth_post_handler %s", q_path));
@ -259,7 +275,8 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
const char *host = xs_dict_get(srv_config, "host"); const char *host = xs_dict_get(srv_config, "host");
/* by default, generate another login form with an error */ /* by default, generate another login form with an error */
*body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, redir, cid, state, USER_AGENT); *body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, "oauth/x-snac-login",
redir, cid, state, USER_AGENT);
*ctype = "text/html"; *ctype = "text/html";
status = 200; status = 200;
@ -268,8 +285,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
if (user_open(&snac, login)) { if (user_open(&snac, login)) {
/* check the login + password */ /* check the login + password */
if (check_password(login, passwd, if (check_password(login, passwd, xs_dict_get(snac.config, "passwd"))) {
xs_dict_get(snac.config, "passwd"))) {
/* success! redirect to the desired uri */ /* success! redirect to the desired uri */
xs *code = random_str(); xs *code = random_str();
@ -328,7 +344,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
const char *auhdr = xs_dict_get(req, "authorization"); const char *auhdr = xs_dict_get(req, "authorization");
if (!xs_is_null(auhdr) && xs_startswith(auhdr, "Basic ")) { if (!xs_is_null(auhdr) && xs_startswith(auhdr, "Basic ")) {
xs *s1 = xs_replace(auhdr, "Basic ", ""); xs *s1 = xs_replace_n(auhdr, "Basic ", "", 1);
int size; int size;
xs *s2 = xs_base64_dec(s1, &size); xs *s2 = xs_base64_dec(s1, &size);
@ -373,7 +389,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
const char *uid = xs_dict_get(app, "uid"); const char *uid = xs_dict_get(app, "uid");
srv_debug(0, xs_fmt("oauth token: " srv_debug(1, xs_fmt("oauth token: "
"successful login for %s, new token %s", uid, tokid)); "successful login for %s, new token %s", uid, tokid));
xs *token = xs_dict_new(); xs *token = xs_dict_new();
@ -387,7 +403,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
} }
} }
else { else {
srv_debug(0, xs_fmt("oauth token: invalid or unset arguments")); srv_debug(1, xs_fmt("oauth token: invalid or unset arguments"));
status = 400; status = 400;
} }
} }
@ -409,7 +425,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
} }
else { else {
token_del(tokid); token_del(tokid);
srv_debug(0, xs_fmt("oauth revoke: revoked token %s", tokid)); srv_debug(1, xs_fmt("oauth revoke: revoked token %s", tokid));
status = 200; status = 200;
/* also delete the app, as it serves no purpose from now on */ /* also delete the app, as it serves no purpose from now on */
@ -417,10 +433,52 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
} }
} }
else { else {
srv_debug(0, xs_fmt("oauth revoke: invalid or unset arguments")); srv_debug(1, xs_fmt("oauth revoke: invalid or unset arguments"));
status = 403; status = 403;
} }
} }
if (strcmp(cmd, "/x-snac-get-token") == 0) {
const char *login = xs_dict_get(args, "login");
const char *passwd = xs_dict_get(args, "passwd");
const char *host = xs_dict_get(srv_config, "host");
/* by default, generate another login form with an error */
*body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, "oauth/x-snac-get-token",
"", "", "", USER_AGENT);
*ctype = "text/html";
status = 200;
if (login && passwd) {
snac user;
if (user_open(&user, login)) {
/* check the login + password */
if (check_password(login, passwd, xs_dict_get(user.config, "passwd"))) {
/* success! create a new token */
xs *tokid = random_str();
srv_debug(1, xs_fmt("x-snac-new-token: "
"successful login for %s, new token %s", login, tokid));
xs *token = xs_dict_new();
token = xs_dict_append(token, "token", tokid);
token = xs_dict_append(token, "client_id", "snac-client");
token = xs_dict_append(token, "client_secret", "");
token = xs_dict_append(token, "uid", login);
token = xs_dict_append(token, "code", "");
token_add(tokid, token);
*ctype = "text/plain";
xs_free(*body);
*body = xs_dup(tokid);
}
user_free(&user);
}
}
}
return status; return status;
} }
@ -537,7 +595,6 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
xs *f = xs_val_new(XSTYPE_FALSE); xs *f = xs_val_new(XSTYPE_FALSE);
xs *t = xs_val_new(XSTYPE_TRUE); xs *t = xs_val_new(XSTYPE_TRUE);
xs *n = xs_val_new(XSTYPE_NULL); xs *n = xs_val_new(XSTYPE_NULL);
xs *el = xs_list_new();
xs *idx = NULL; xs *idx = NULL;
xs *ixc = NULL; xs *ixc = NULL;
@ -787,7 +844,7 @@ int process_auth_token(snac *snac, const xs_dict *req)
/* if there is an authorization field, try to validate it */ /* if there is an authorization field, try to validate it */
if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) { if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) {
xs *tokid = xs_replace(v, "Bearer ", ""); xs *tokid = xs_replace_n(v, "Bearer ", "", 1);
xs *token = token_get(tokid); xs *token = token_get(tokid);
if (token != NULL) { if (token != NULL) {
@ -815,6 +872,8 @@ int process_auth_token(snac *snac, const xs_dict *req)
int mastoapi_get_handler(const xs_dict *req, const char *q_path, int mastoapi_get_handler(const xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype) char **body, int *b_size, char **ctype)
{ {
(void)b_size;
if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/")) if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
return 0; return 0;
@ -826,7 +885,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
int status = 404; int status = 404;
xs_dict *args = xs_dict_get(req, "q_vars"); xs_dict *args = xs_dict_get(req, "q_vars");
xs *cmd = xs_replace(q_path, "/api", ""); xs *cmd = xs_replace_n(q_path, "/api", "", 1);
snac snac1 = {0}; snac snac1 = {0};
int logged_in = process_auth_token(&snac1, req); int logged_in = process_auth_token(&snac1, req);
@ -1036,9 +1095,12 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
} }
else else
if (strcmp(cmd, "/v1/timelines/public") == 0) { if (strcmp(cmd, "/v1/timelines/public") == 0) {
/* the public timeline (public timelines for all users) */ /* the instance public timeline (public timelines for all users) */
/* this is an ugly kludge: first users in the list get all the fame */ /* NOTE: this api call needs no authorization; but,
I need a logged-in user in mastoapi_status() for
is_msg_public() and the liked/boosted flags,
so it will silently fail for pure public access */
const char *limit_s = xs_dict_get(args, "limit"); const char *limit_s = xs_dict_get(args, "limit");
int limit = 0; int limit = 0;
@ -1050,36 +1112,24 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
if (limit == 0) if (limit == 0)
limit = 20; limit = 20;
xs *timeline = timeline_instance_list(0, limit);
xs *out = xs_list_new(); xs *out = xs_list_new();
xs *users = user_list(); xs_list *p = timeline;
xs_list *p = users; xs_str *md5;
xs_str *uid;
while (xs_list_iter(&p, &uid) && cnt < limit) { while (logged_in && xs_list_iter(&p, &md5) && cnt < limit) {
snac user;
if (user_open(&user, uid)) {
xs *timeline = timeline_simple_list(&user, "public", 0, 4);
xs_list *p2 = timeline;
xs_str *v;
while (xs_list_iter(&p2, &v) && cnt < limit) {
xs *msg = NULL; xs *msg = NULL;
/* get the entry */ /* get the entry */
if (!valid_status(timeline_get_by_md5(&user, v, &msg))) if (!valid_status(object_get_by_md5(md5, &msg)))
continue; continue;
/* discard non-Notes */ /* discard non-Notes */
if (strcmp(xs_dict_get(msg, "type"), "Note") != 0) if (strcmp(xs_dict_get(msg, "type"), "Note") != 0)
continue; continue;
/* discard entries not by this user */
if (!xs_startswith(xs_dict_get(msg, "id"), user.actor))
continue;
/* convert the Note into a Mastodon status */ /* convert the Note into a Mastodon status */
xs *st = mastoapi_status(&user, msg); xs *st = mastoapi_status(&snac1, msg);
if (st != NULL) { if (st != NULL) {
out = xs_list_append(out, st); out = xs_list_append(out, st);
@ -1087,10 +1137,6 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
} }
} }
user_free(&user);
}
}
*body = xs_json_dumps_pp(out, 4); *body = xs_json_dumps_pp(out, 4);
*ctype = "application/json"; *ctype = "application/json";
status = 200; status = 200;
@ -1121,7 +1167,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
xs *actor = NULL; xs *actor = NULL;
xs *entry = NULL; xs *entry = NULL;
if (!valid_status(object_get(xs_dict_get(noti, "actor"), &actor))) if (!valid_status(actor_get(&snac1, xs_dict_get(noti, "actor"), &actor)))
continue; continue;
if (objid != NULL && !valid_status(object_get(objid, &entry))) if (objid != NULL && !valid_status(object_get(objid, &entry)))
@ -1287,25 +1333,6 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
cfg = xs_dict_append(cfg, "statuses", d11); cfg = xs_dict_append(cfg, "statuses", d11);
} }
{
xs *d11 = xs_dict_new();
xs *mt = xs_list_new();
mt = xs_list_append(mt, "image/jpeg");
mt = xs_list_append(mt, "image/png");
mt = xs_list_append(mt, "image/gif");
d11 = xs_dict_append(d11, "supported_mime_types", mt);
d11 = xs_dict_append(d11, "image_size_limit", z);
d11 = xs_dict_append(d11, "image_matrix_limit", z);
d11 = xs_dict_append(d11, "video_size_limit", z);
d11 = xs_dict_append(d11, "video_matrix_limit", z);
d11 = xs_dict_append(d11, "video_frame_rate_limit", z);
cfg = xs_dict_append(cfg, "media_attachments", d11);
}
ins = xs_dict_append(ins, "configuration", cfg); ins = xs_dict_append(ins, "configuration", cfg);
*body = xs_json_dumps_pp(ins, 4); *body = xs_json_dumps_pp(ins, 4);
@ -1326,7 +1353,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
/* skip the 'fake' part of the id */ /* skip the 'fake' part of the id */
id = MID_TO_MD5(id); id = MID_TO_MD5(id);
if (valid_status(timeline_get_by_md5(&snac1, id, &msg))) { if (valid_status(object_get_by_md5(id, &msg))) {
if (op == NULL) { if (op == NULL) {
if (!is_muted(&snac1, xs_dict_get(msg, "attributedTo"))) { if (!is_muted(&snac1, xs_dict_get(msg, "attributedTo"))) {
/* return the status itself */ /* return the status itself */
@ -1487,6 +1514,9 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
const char *payload, int p_size, const char *payload, int p_size,
char **body, int *b_size, char **ctype) char **body, int *b_size, char **ctype)
{ {
(void)p_size;
(void)b_size;
if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/")) if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
return 0; return 0;
@ -1513,7 +1543,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
printf("%s\n", j); printf("%s\n", j);
}*/ }*/
xs *cmd = xs_replace(q_path, "/api", ""); xs *cmd = xs_replace_n(q_path, "/api", "", 1);
snac snac = {0}; snac snac = {0};
int logged_in = process_auth_token(&snac, req); int logged_in = process_auth_token(&snac, req);
@ -1562,7 +1592,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
app_add(cid, app); app_add(cid, app);
srv_debug(0, xs_fmt("mastoapi apps: new app %s", cid)); srv_debug(1, xs_fmt("mastoapi apps: new app %s", cid));
} }
} }
else else
@ -1582,6 +1612,9 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
if (xs_is_null(media_ids)) if (xs_is_null(media_ids))
media_ids = xs_dict_get(args, "media_ids[]"); media_ids = xs_dict_get(args, "media_ids[]");
if (xs_is_null(visibility))
visibility = "public";
xs *attach_list = xs_list_new(); xs *attach_list = xs_list_new();
xs *irt = NULL; xs *irt = NULL;
@ -1683,7 +1716,11 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
} }
else else
if (strcmp(op, "unfavourite") == 0) { if (strcmp(op, "unfavourite") == 0) {
/* snac does not support Undo+Like */ /* partial support: as the original Like message
is not stored anywhere here, it's not possible
to send an Undo + Like; the only thing done here
is to delete the actor from the list of likes */
object_unadmire(id, snac.actor, 1);
} }
else else
if (strcmp(op, "reblog") == 0) { if (strcmp(op, "reblog") == 0) {
@ -1698,7 +1735,8 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
} }
else else
if (strcmp(op, "unreblog") == 0) { if (strcmp(op, "unreblog") == 0) {
/* snac does not support Undo+Announce */ /* partial support: see comment in 'unfavourite' */
object_unadmire(id, snac.actor, 0);
} }
else else
if (strcmp(op, "bookmark") == 0) { if (strcmp(op, "bookmark") == 0) {
@ -1903,6 +1941,9 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
const char *payload, int p_size, const char *payload, int p_size,
char **body, int *b_size, char **ctype) char **body, int *b_size, char **ctype)
{ {
(void)p_size;
(void)b_size;
if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/")) if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
return 0; return 0;
@ -1924,7 +1965,7 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
if (args == NULL) if (args == NULL)
return 400; return 400;
xs *cmd = xs_replace(q_path, "/api", ""); xs *cmd = xs_replace_n(q_path, "/api", "", 1);
snac snac = {0}; snac snac = {0};
int logged_in = process_auth_token(&snac, req); int logged_in = process_auth_token(&snac, req);

31
snac.h
View file

@ -1,7 +1,7 @@
/* snac - A simple, minimalistic ActivityPub instance */ /* snac - A simple, minimalistic ActivityPub instance */
/* copyright (c) 2022 - 2023 grunfink / MIT license */ /* copyright (c) 2022 - 2023 grunfink / MIT license */
#define VERSION "2.29" #define VERSION "2.30"
#define USER_AGENT "snac/" VERSION #define USER_AGENT "snac/" VERSION
@ -83,6 +83,7 @@ int object_del_if_unref(const char *id);
double object_ctime_by_md5(const char *md5); double object_ctime_by_md5(const char *md5);
double object_ctime(const char *id); double object_ctime(const char *id);
int object_admire(const char *id, const char *actor, int like); int object_admire(const char *id, const char *actor, int like);
int object_unadmire(const char *id, const char *actor, int like);
int object_likes_len(const char *id); int object_likes_len(const char *id);
int object_announces_len(const char *id); int object_announces_len(const char *id);
@ -105,14 +106,14 @@ int timeline_touch(snac *snac);
int timeline_here(snac *snac, const char *md5); int timeline_here(snac *snac, const char *md5);
int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg); int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg);
int timeline_del(snac *snac, char *id); int timeline_del(snac *snac, char *id);
d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show); xs_list *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show);
d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show); xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show);
int timeline_add(snac *snac, char *id, char *o_msg); int timeline_add(snac *snac, char *id, char *o_msg);
void timeline_admire(snac *snac, char *id, char *admirer, int like); void timeline_admire(snac *snac, char *id, char *admirer, int like);
xs_list *timeline_top_level(snac *snac, xs_list *list); xs_list *timeline_top_level(snac *snac, xs_list *list);
xs_list *local_list(snac *snac, int max);
d_char *local_list(snac *snac, int max); xs_list *timeline_instance_list(int skip, int show);
int following_add(snac *snac, const char *actor, const xs_dict *msg); int following_add(snac *snac, const char *actor, const xs_dict *msg);
int following_del(snac *snac, const char *actor); int following_del(snac *snac, const char *actor);
@ -127,8 +128,8 @@ int is_muted(snac *snac, const char *actor);
void hide(snac *snac, const char *id); void hide(snac *snac, const char *id);
int is_hidden(snac *snac, const char *id); int is_hidden(snac *snac, const char *id);
int actor_add(snac *snac, const char *actor, d_char *msg); int actor_add(const char *actor, xs_dict *msg);
int actor_get(snac *snac, const char *actor, d_char **data); int actor_get(snac *snac, const char *actor, xs_dict **data);
int static_get(snac *snac, const char *id, d_char **data, int *size); int static_get(snac *snac, const char *id, d_char **data, int *size);
void static_put(snac *snac, const char *id, const char *data, int size); void static_put(snac *snac, const char *id, const char *data, int size);
@ -154,7 +155,7 @@ void inbox_add(const char *inbox);
void inbox_add_by_actor(const xs_dict *actor); void inbox_add_by_actor(const xs_dict *actor);
xs_list *inbox_list(void); xs_list *inbox_list(void);
void enqueue_input(snac *snac, xs_dict *msg, xs_dict *req, int retries); void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries);
void enqueue_output_raw(const char *keyid, const char *seckey, void enqueue_output_raw(const char *keyid, const char *seckey,
xs_dict *msg, xs_str *inbox, int retries); xs_dict *msg, xs_str *inbox, int retries);
void enqueue_output(snac *snac, xs_dict *msg, xs_str *inbox, int retries); void enqueue_output(snac *snac, xs_dict *msg, xs_str *inbox, int retries);
@ -186,7 +187,7 @@ int check_signature(snac *snac, xs_dict *req, xs_str **err);
void httpd(void); void httpd(void);
int webfinger_request(const char *qs, char **actor, char **user); int webfinger_request(const char *qs, char **actor, char **user);
int webfinger_get_handler(d_char *req, char *q_path, int webfinger_get_handler(xs_dict *req, char *q_path,
char **body, int *b_size, char **ctype); char **body, int *b_size, char **ctype);
const char *default_avatar_base64(void); const char *default_avatar_base64(void);
@ -202,6 +203,8 @@ d_char *msg_undo(snac *snac, char *object);
d_char *msg_delete(snac *snac, char *id); d_char *msg_delete(snac *snac, char *id);
d_char *msg_actor(snac *snac); d_char *msg_actor(snac *snac);
xs_dict *msg_update(snac *snac, xs_dict *object); xs_dict *msg_update(snac *snac, xs_dict *object);
xs_dict *msg_ping(snac *user, const char *rcpt);
xs_dict *msg_pong(snac *user, const char *rcpt, const char *object);
int activitypub_request(snac *snac, const char *url, xs_dict **data); int activitypub_request(snac *snac, const char *url, xs_dict **data);
int actor_request(snac *snac, const char *actor, xs_dict **data); int actor_request(snac *snac, const char *actor, xs_dict **data);
@ -219,17 +222,19 @@ int process_user_queue(snac *snac);
void process_queue_item(xs_dict *q_item); void process_queue_item(xs_dict *q_item);
int process_queue(void); int process_queue(void);
int activitypub_get_handler(d_char *req, char *q_path, int activitypub_get_handler(const xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype); char **body, int *b_size, char **ctype);
int activitypub_post_handler(d_char *req, char *q_path, int activitypub_post_handler(const xs_dict *req, const char *q_path,
char *payload, int p_size, char *payload, int p_size,
char **body, int *b_size, char **ctype); char **body, int *b_size, char **ctype);
d_char *not_really_markdown(const char *content); d_char *not_really_markdown(const char *content);
d_char *sanitize(const char *str); d_char *sanitize(const char *str);
int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char **ctype); int html_get_handler(const xs_dict *req, const char *q_path,
int html_post_handler(d_char *req, char *q_path, d_char *payload, int p_size, char **body, int *b_size, char **ctype);
int html_post_handler(const xs_dict *req, const char *q_path,
char *payload, int p_size,
char **body, int *b_size, char **ctype); char **body, int *b_size, char **ctype);
int snac_init(const char *_basedir); int snac_init(const char *_basedir);

View file

@ -21,7 +21,7 @@ int webfinger_request(const char *qs, char **actor, char **user)
if (xs_startswith(qs, "https:/" "/")) { if (xs_startswith(qs, "https:/" "/")) {
/* actor query: pick the host */ /* actor query: pick the host */
xs *s = xs_replace(qs, "https:/" "/", ""); xs *s = xs_replace_n(qs, "https:/" "/", "", 1);
l = xs_split_n(s, "/", 1); l = xs_split_n(s, "/", 1);
@ -74,7 +74,7 @@ int webfinger_request(const char *qs, char **actor, char **user)
char *subject = xs_dict_get(obj, "subject"); char *subject = xs_dict_get(obj, "subject");
if (subject) if (subject)
*user = xs_replace(subject, "acct:", ""); *user = xs_replace_n(subject, "acct:", "", 1);
} }
if (actor != NULL) { if (actor != NULL) {
@ -105,6 +105,8 @@ int webfinger_get_handler(d_char *req, char *q_path,
{ {
int status; int status;
(void)b_size;
if (strcmp(q_path, "/.well-known/webfinger") != 0) if (strcmp(q_path, "/.well-known/webfinger") != 0)
return 0; return 0;
@ -137,7 +139,7 @@ int webfinger_get_handler(d_char *req, char *q_path,
else else
if (xs_startswith(resource, "acct:")) { if (xs_startswith(resource, "acct:")) {
/* it's an account name */ /* it's an account name */
xs *an = xs_replace(resource, "acct:", ""); xs *an = xs_replace_n(resource, "acct:", "", 1);
xs *l = NULL; xs *l = NULL;
/* strip a possible leading @ */ /* strip a possible leading @ */

12
xs.h
View file

@ -65,8 +65,10 @@ xs_str *xs_str_new(const char *str);
xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix); xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix);
#define xs_str_prepend_i(str, prefix) xs_str_wrap_i(prefix, str, NULL) #define xs_str_prepend_i(str, prefix) xs_str_wrap_i(prefix, str, NULL)
#define xs_str_cat(str, suffix) xs_str_wrap_i(NULL, str, suffix) #define xs_str_cat(str, suffix) xs_str_wrap_i(NULL, str, suffix)
xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto); xs_str *xs_replace_in(xs_str *str, const char *sfrom, const char *sto, int times);
#define xs_replace(str, sfrom, sto) xs_replace_i(xs_dup(str), sfrom, sto) #define xs_replace_i(str, sfrom, sto) xs_replace_in(str, sfrom, sto, XS_ALL)
#define xs_replace(str, sfrom, sto) xs_replace_in(xs_dup(str), sfrom, sto, XS_ALL)
#define xs_replace_n(str, sfrom, sto, times) xs_replace_in(xs_dup(str), sfrom, sto, times)
xs_str *xs_fmt(const char *fmt, ...); xs_str *xs_fmt(const char *fmt, ...);
int xs_str_in(const char *haystack, const char *needle); int xs_str_in(const char *haystack, const char *needle);
int _xs_startsorends(const char *str, const char *xfix, int ends); int _xs_startsorends(const char *str, const char *xfix, int ends);
@ -416,7 +418,7 @@ xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix)
} }
xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto) xs_str *xs_replace_in(xs_str *str, const char *sfrom, const char *sto, int times)
/* replaces inline all sfrom with sto */ /* replaces inline all sfrom with sto */
{ {
XS_ASSERT_TYPE(str, XSTYPE_STRING); XS_ASSERT_TYPE(str, XSTYPE_STRING);
@ -426,7 +428,7 @@ xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto)
char *ss; char *ss;
int offset = 0; int offset = 0;
while ((ss = strstr(str + offset, sfrom)) != NULL) { while (times > 0 && (ss = strstr(str + offset, sfrom)) != NULL) {
int n_offset = ss - str; int n_offset = ss - str;
str = xs_collapse(str, n_offset, sfsz); str = xs_collapse(str, n_offset, sfsz);
@ -434,6 +436,8 @@ xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto)
memcpy(str + n_offset, sto, stsz); memcpy(str + n_offset, sto, stsz);
offset = n_offset + stsz; offset = n_offset + stsz;
times--;
} }
return str; return str;

View file

@ -7,13 +7,20 @@
xs_str *xs_hex_enc(const xs_val *data, int size); xs_str *xs_hex_enc(const xs_val *data, int size);
xs_val *xs_hex_dec(const xs_str *hex, int *size); xs_val *xs_hex_dec(const xs_str *hex, int *size);
int xs_is_hex(const char *str); int xs_is_hex(const char *str);
xs_str *xs_base32_enc(const xs_val *data, int sz);
xs_str *xs_base32hex_enc(const xs_val *data, int sz);
xs_val *xs_base32_dec(const xs_str *data, int *size);
xs_val *xs_base32hex_dec(const xs_str *data, int *size);
xs_str *xs_base64_enc(const xs_val *data, int sz); xs_str *xs_base64_enc(const xs_val *data, int sz);
xs_val *xs_base64_dec(const xs_str *data, int *size); xs_val *xs_base64_dec(const xs_str *data, int *size);
int xs_is_base64(const char *str);
xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint); xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint);
#ifdef XS_IMPLEMENTATION #ifdef XS_IMPLEMENTATION
/** hex **/
xs_str *xs_hex_enc(const xs_val *data, int size) xs_str *xs_hex_enc(const xs_val *data, int size)
/* returns an hexdump of data */ /* returns an hexdump of data */
{ {
@ -78,16 +85,178 @@ int xs_is_hex(const char *str)
} }
xs_str *xs_base64_enc(const xs_val *data, int sz) /** base32 */
/* encodes data to base64 */
static char *xs_b32_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"234567=";
static char *xs_b32hex_tbl = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUV=";
/*
00000|00011|11111|12222|22223|33333|33444|44444
*/
xs_str *xs_base32_enc_tbl(const xs_val *data, int sz, const char *b32_tbl)
/* encodes data to base32 using a table */
{
xs_str *s = xs_str_new(NULL);
unsigned char *p;
int n;
p = (unsigned char *)data;
for (n = 0; n < sz; n += 5) {
int l = sz - n;
char enc[9] = "========";
enc[0] = b32_tbl[(p[n] >> 3) & 0x1f];
if (l > 1) {
enc[1] = b32_tbl[(p[n] << 2 | p[n + 1] >> 6) & 0x1f];
enc[2] = b32_tbl[(p[n + 1] >> 1) & 0x1f];
if (l > 2) {
enc[3] = b32_tbl[(p[n + 1] << 4 | p[n + 2] >> 4) & 0x1f];
if (l > 3) {
enc[4] = b32_tbl[(p[n + 2] << 1 | p[n + 3] >> 7) & 0x1f];
enc[5] = b32_tbl[(p[n + 3] >> 2) & 0x1f];
if (l > 4) {
enc[6] = b32_tbl[(p[n + 3] << 3 | p[n + 4] >> 5) & 0x1f];
enc[7] = b32_tbl[(p[n + 4]) & 0x1f];
}
else
enc[6] = b32_tbl[(p[n + 3] << 3) & 0x1f];
}
else
enc[4] = b32_tbl[(p[n + 2] << 1) & 0x1f];
}
else
enc[3] = b32_tbl[(p[n + 1] << 4) & 0x1f];
}
else
enc[1] = b32_tbl[(p[n] << 2) & 0x1f];
s = xs_str_cat(s, enc);
}
return s;
}
xs_str *xs_base32_enc(const xs_val *data, int sz)
/* encodes data to base32 */
{
return xs_base32_enc_tbl(data, sz, xs_b32_tbl);
}
xs_str *xs_base32hex_enc(const xs_val *data, int sz)
/* encodes data to base32 with HEX alphabet (RFC4648) */
{
return xs_base32_enc_tbl(data, sz, xs_b32hex_tbl);
}
xs_val *xs_base32_dec_tbl(const xs_str *data, int *size, const char *b32_tbl)
/* decodes data from base32 using a table */
{
xs_val *s = NULL;
int sz = 0;
char *p;
p = (char *)data;
/* size of data must be a multiple of 8 */
if (strlen(p) % 8)
return NULL;
for (p = (char *)data; *p; p += 8) {
int cs[8];
int n;
unsigned char tmp[5];
for (n = 0; n < 8; n++) {
char *ss = strchr(b32_tbl, p[n]);
if (ss == NULL) {
/* not a base32 char */
return xs_free(s);
}
cs[n] = ss - b32_tbl;
}
n = 0;
/* #0 byte */
tmp[n++] = cs[0] << 3 | cs[1] >> 2;
if (cs[2] != 32) {
/* #1 byte */
tmp[n++] = (cs[1] & 0x3) << 6 | cs[2] << 1 | (cs[3] & 0x10) >> 4;
if (cs[4] != 32) {
/* #2 byte */
tmp[n++] = (cs[3] & 0xf) << 4 | cs[4] >> 1;
if (cs[5] != 32) {
/* #3 byte */
tmp[n++] = (cs[4] & 0x1) << 7 | cs[5] << 2 | cs[6] >> 3;
if (cs[7] != 32) {
/* #4 byte */
tmp[n++] = (cs[6] & 0x7) << 5 | cs[7];
}
}
}
}
/* must be done manually because data can be pure binary */
s = xs_realloc(s, _xs_blk_size(sz + n));
memcpy(s + sz, tmp, n);
sz += n;
}
/* asciiz it to use it as a string */
s = xs_realloc(s, _xs_blk_size(sz + 1));
s[sz] = '\0';
*size = sz;
return s;
}
xs_val *xs_base32_dec(const xs_str *data, int *size)
/* decodes data from base32 */
{
return xs_base32_dec_tbl(data, size, xs_b32_tbl);
}
xs_val *xs_base32hex_dec(const xs_str *data, int *size)
/* decodes data from base32 with HEX alphabet (RFC4648) */
{
return xs_base32_dec_tbl(data, size, xs_b32hex_tbl);
}
/** base64 */
static char *xs_b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/=";
xs_str *xs_base64_enc_tbl(const xs_val *data, int sz, const char *b64_tbl)
/* encodes data to base64 using a table */
{ {
xs_str *s; xs_str *s;
unsigned char *p; unsigned char *p;
char *i; char *i;
int bsz, n; int bsz, n;
static char *b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
bsz = ((sz + 3 - 1) / 3) * 4; bsz = ((sz + 3 - 1) / 3) * 4;
i = s = xs_realloc(NULL, _xs_blk_size(bsz + 1)); i = s = xs_realloc(NULL, _xs_blk_size(bsz + 1));
@ -123,15 +292,19 @@ xs_str *xs_base64_enc(const xs_val *data, int sz)
} }
xs_val *xs_base64_dec(const xs_str *data, int *size) xs_str *xs_base64_enc(const xs_val *data, int sz)
/* decodes data from base64 */ /* encodes data to base64 */
{
return xs_base64_enc_tbl(data, sz, xs_b64_tbl);
}
xs_val *xs_base64_dec_tbl(const xs_str *data, int *size, const char *b64_tbl)
/* decodes data from base64 using a table */
{ {
xs_val *s = NULL; xs_val *s = NULL;
int sz = 0; int sz = 0;
char *p; char *p;
static char *b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/=";
p = (char *)data; p = (char *)data;
@ -184,6 +357,34 @@ xs_val *xs_base64_dec(const xs_str *data, int *size)
} }
xs_val *xs_base64_dec(const xs_str *data, int *size)
/* decodes data from base64 */
{
return xs_base64_dec_tbl(data, size, xs_b64_tbl);
}
int xs_is_base64_tbl(const char *str, const char *b64_tbl)
/* returns 1 if str is a base64 string, with table */
{
while (*str) {
if (strchr(b64_tbl, *str++) == NULL)
return 0;
}
return 1;
}
int xs_is_base64(const char *str)
/* returns 1 if str is a base64 string */
{
return xs_is_base64_tbl(str, xs_b64_tbl);
}
/** utf-8 **/
xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint) xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint)
/* encodes an Unicode codepoint to utf8 */ /* encodes an Unicode codepoint to utf8 */
{ {

View file

@ -1 +1 @@
/* a885c7cc4c8e6384ae23125ed05f434471ccc6fb */ /* dfdd729248d7169b80cb6a7462fe6c0ba6efeb16 */