commit e65760a
Merge pull request 'master' (#1) from grunfink/snac2:master into master Reviewed-on: https://codeberg.org/poesty/snac2/pulls/1
16 files changed,
+610,
-147
M
Makefile
+2,
-2
1@@ -1,6 +1,6 @@
2 PREFIX=/usr/local
3 PREFIX_MAN=$(PREFIX)/man
4-CFLAGS?=-g -Wall
5+CFLAGS?=-g -Wall -Wextra
6
7 all: snac
8
9@@ -40,7 +40,7 @@ httpd.o: httpd.c xs.h xs_io.h xs_encdec.h xs_json.h xs_socket.h \
10 xs_httpd.h xs_mime.h snac.h
11 main.o: main.c xs.h xs_io.h xs_encdec.h xs_json.h snac.h
12 mastoapi.o: mastoapi.c xs.h xs_encdec.h xs_openssl.h xs_json.h xs_io.h \
13- xs_time.h snac.h
14+ xs_time.h xs_glob.h snac.h
15 snac.o: snac.c xs.h xs_io.h xs_encdec.h xs_json.h xs_curl.h xs_openssl.h \
16 xs_socket.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h \
17 snac.h
+10,
-0
1@@ -1,5 +1,15 @@
2 # Release Notes
3
4+## 2.30
5+
6+Fixed a bug that made some notifications to be missed.
7+
8+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.
9+
10+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).
11+
12+The build process now includes the `-Wextra` flag.
13+
14 ## 2.29
15
16 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).
+47,
-9
1@@ -109,7 +109,7 @@ int actor_request(snac *snac, const char *actor, xs_dict **data)
2
3 if (valid_status(status2)) {
4 /* renew data */
5- status = actor_add(snac, actor, payload);
6+ status = actor_add(actor, payload);
7
8 if (data != NULL) {
9 *data = payload;
10@@ -437,7 +437,8 @@ void process_tags(snac *snac, const char *content, d_char **n_content, d_char **
11
12 /** messages **/
13
14-d_char *msg_base(snac *snac, char *type, char *id, char *actor, char *date, char *object)
15+xs_dict *msg_base(snac *snac, const char *type, const char *id,
16+ const char *actor, const char *date, const char *object)
17 /* creates a base ActivityPub message */
18 {
19 xs *did = NULL;
20@@ -467,7 +468,7 @@ d_char *msg_base(snac *snac, char *type, char *id, char *actor, char *date, char
21 }
22 }
23
24- d_char *msg = xs_dict_new();
25+ xs_dict *msg = xs_dict_new();
26
27 msg = xs_dict_append(msg, "@context", "https:/" "/www.w3.org/ns/activitystreams");
28 msg = xs_dict_append(msg, "type", type);
29@@ -845,6 +846,28 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
30 }
31
32
33+xs_dict *msg_ping(snac *user, const char *rcpt)
34+/* creates a Ping message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */
35+{
36+ xs_dict *msg = msg_base(user, "Ping", "@dummy", user->actor, NULL, NULL);
37+
38+ msg = xs_dict_append(msg, "to", rcpt);
39+
40+ return msg;
41+}
42+
43+
44+xs_dict *msg_pong(snac *user, const char *rcpt, const char *object)
45+/* creates a Pong message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */
46+{
47+ xs_dict *msg = msg_base(user, "Pong", "@dummy", user->actor, NULL, object);
48+
49+ msg = xs_dict_append(msg, "to", rcpt);
50+
51+ return msg;
52+}
53+
54+
55 void notify(snac *snac, xs_str *type, xs_str *utype, xs_str *actor, xs_dict *msg)
56 /* notifies the user of relevant events */
57 {
58@@ -1121,7 +1144,7 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
59 else
60 if (strcmp(type, "Update") == 0) {
61 if (strcmp(utype, "Person") == 0) {
62- actor_add(snac, actor, xs_dict_get(msg, "object"));
63+ actor_add(actor, xs_dict_get(msg, "object"));
64
65 snac_log(snac, xs_fmt("updated actor %s", actor));
66 }
67@@ -1147,7 +1170,19 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
68 snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object));
69 }
70 else
71- snac_debug(snac, 1, xs_fmt("process_message type '%s' ignored", type));
72+ if (strcmp(type, "Pong") == 0) {
73+ snac_log(snac, xs_fmt("'Pong' received from %s", actor));
74+ }
75+ else
76+ if (strcmp(type, "Ping") == 0) {
77+ snac_log(snac, xs_fmt("'Ping' requested from %s", actor));
78+
79+ xs *rsp = msg_pong(snac, actor, xs_dict_get(msg, "id"));
80+
81+ enqueue_output_by_actor(snac, rsp, actor, 0);
82+ }
83+ else
84+ snac_debug(snac, 1, xs_fmt("process_input_message type '%s' ignored", type));
85
86 if (do_notify) {
87 notify(snac, type, utype, actor, msg);
88@@ -1384,8 +1419,9 @@ void process_queue_item(xs_dict *q_item)
89 xs *headers = xs_dict_new();
90 headers = xs_dict_append(headers, "content-type", "application/json");
91
92- xs *rsp = xs_http_request("POST", url, headers,
93+ xs *rsp = xs_http_request("POST", url, headers,
94 body, strlen(body), &status, NULL, NULL, 0);
95+ rsp = xs_free(rsp);
96
97 srv_debug(0, xs_fmt("telegram post %d", status));
98 }
99@@ -1426,7 +1462,7 @@ int process_queue(void)
100
101 /** HTTP handlers */
102
103-int activitypub_get_handler(d_char *req, char *q_path,
104+int activitypub_get_handler(const xs_dict *req, const char *q_path,
105 char **body, int *b_size, char **ctype)
106 {
107 int status = 200;
108@@ -1519,11 +1555,13 @@ int activitypub_get_handler(d_char *req, char *q_path,
109 }
110
111
112-int activitypub_post_handler(d_char *req, char *q_path,
113- d_char *payload, int p_size,
114+int activitypub_post_handler(const xs_dict *req, const char *q_path,
115+ char *payload, int p_size,
116 char **body, int *b_size, char **ctype)
117 /* processes an input message */
118 {
119+ (void)b_size;
120+
121 int status = 202; /* accepted */
122 char *i_ctype = xs_dict_get(req, "content-type");
123 snac snac;
M
data.c
+110,
-11
1@@ -328,6 +328,51 @@ int index_add(const char *fn, const char *id)
2 }
3
4
5+int index_del_md5(const char *fn, const char *md5)
6+/* deletes an md5 from an index */
7+{
8+ int status = 404;
9+ FILE *f;
10+
11+ pthread_mutex_lock(&data_mutex);
12+
13+ if ((f = fopen(fn, "r+")) != NULL) {
14+ char line[256];
15+
16+ while (fgets(line, sizeof(line), f) != NULL) {
17+ line[32] = '\0';
18+
19+ if (strcmp(line, md5) == 0) {
20+ /* found! just rewind, overwrite it with garbage
21+ and an eventual call to index_gc() will clean it
22+ [yes: this breaks index_len()] */
23+ fseek(f, -33, SEEK_CUR);
24+ fwrite("-", 1, 1, f);
25+ status = 200;
26+
27+ break;
28+ }
29+ }
30+
31+ fclose(f);
32+ }
33+ else
34+ status = 500;
35+
36+ pthread_mutex_unlock(&data_mutex);
37+
38+ return status;
39+}
40+
41+
42+int index_del(const char *fn, const char *id)
43+/* deletes an id from an index */
44+{
45+ xs *md5 = xs_md5_hex(id, strlen(id));
46+ return index_del_md5(fn, md5);
47+}
48+
49+
50 int index_gc(const char *fn)
51 /* garbage-collects an index, deleting objects that are not here */
52 {
53@@ -772,6 +817,23 @@ int object_admire(const char *id, const char *actor, int like)
54 }
55
56
57+int object_unadmire(const char *id, const char *actor, int like)
58+/* actor no longer likes or announces this object */
59+{
60+ int status;
61+ xs *fn = _object_fn(id);
62+
63+ fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
64+
65+ status = index_del(fn, actor);
66+
67+ srv_debug(0,
68+ xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status));
69+
70+ return status;
71+}
72+
73+
74 int _object_user_cache(snac *snac, const char *id, const char *cachedir, int del)
75 /* adds or deletes from a user cache */
76 {
77@@ -969,8 +1031,13 @@ void timeline_update_indexes(snac *snac, const char *id)
78
79 if (valid_status(object_get(id, &msg))) {
80 /* if its ours and is public, also store in public */
81- if (is_msg_public(snac, msg))
82+ if (is_msg_public(snac, msg)) {
83 object_user_cache_add(snac, id, "public");
84+
85+ /* also add it to the instance public timeline */
86+ xs *ipt = xs_fmt("%s/public.idx", srv_basedir);
87+ index_add(ipt, id);
88+ }
89 }
90 }
91 }
92@@ -1041,7 +1108,7 @@ xs_list *timeline_top_level(snac *snac, xs_list *list)
93 }
94
95
96-d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show)
97+xs_list *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show)
98 /* returns a timeline (with all entries) */
99 {
100 int c_max;
101@@ -1059,7 +1126,7 @@ d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int sho
102 }
103
104
105-d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show)
106+xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show)
107 /* returns a timeline (only top level entries) */
108 {
109 xs *list = timeline_simple_list(snac, idx_name, skip, show);
110@@ -1068,6 +1135,15 @@ d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show)
111 }
112
113
114+xs_list *timeline_instance_list(int skip, int show)
115+/* returns the timeline for the full instance */
116+{
117+ xs *idx = xs_fmt("%s/public.idx", srv_basedir);
118+
119+ return index_list_desc(idx, skip, show);
120+}
121+
122+
123 /** following **/
124
125 /* this needs special treatment and cannot use the object db as is,
126@@ -1273,30 +1349,49 @@ int is_hidden(snac *snac, const char *id)
127 }
128
129
130-int actor_add(snac *snac, const char *actor, d_char *msg)
131+int actor_add(const char *actor, xs_dict *msg)
132 /* adds an actor */
133 {
134 return object_add_ow(actor, msg);
135 }
136
137
138-int actor_get(snac *snac, const char *actor, d_char **data)
139+int actor_get(snac *snac1, const char *actor, xs_dict **data)
140 /* returns an already downloaded actor */
141 {
142 int status = 200;
143- d_char *d;
144+ xs_dict *d = NULL;
145
146- if (strcmp(actor, snac->actor) == 0) {
147+ if (strcmp(actor, snac1->actor) == 0) {
148 /* this actor */
149 if (data)
150- *data = msg_actor(snac);
151+ *data = msg_actor(snac1);
152
153 return status;
154 }
155
156+ if (xs_startswith(actor, srv_baseurl)) {
157+ /* it's a (possible) local user */
158+ xs *l = xs_split(actor, "/");
159+ const char *uid = xs_list_get(l, -1);
160+ snac user;
161+
162+ if (!xs_is_null(uid) && user_open(&user, uid)) {
163+ if (data)
164+ *data = msg_actor(&user);
165+
166+ user_free(&user);
167+ return 200;
168+ }
169+ else
170+ return 404;
171+ }
172+
173 /* read the object */
174- if (!valid_status(status = object_get(actor, &d)))
175+ if (!valid_status(status = object_get(actor, &d))) {
176+ d = xs_free(d);
177 return status;
178+ }
179
180 if (data)
181 *data = d;
182@@ -1719,7 +1814,7 @@ static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries)
183 }
184
185
186-void enqueue_input(snac *snac, xs_dict *msg, xs_dict *req, int retries)
187+void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries)
188 /* enqueues an input message */
189 {
190 xs *qmsg = _new_qmsg("input", msg, retries);
191@@ -2020,7 +2115,11 @@ void purge_server(void)
192 xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
193 _purge_dir(ib_dir, 7);
194
195- srv_debug(1, xs_fmt("purge: global (obj: %d, idx: %d)", cnt, icnt));
196+ /* purge the instance timeline */
197+ xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir);
198+ int itl_gc = index_gc(itl_fn);
199+
200+ srv_debug(1, xs_fmt("purge: global (obj: %d, idx: %d, itl: %d)", cnt, icnt, itl_gc));
201 }
202
203
+30,
-0
1@@ -220,6 +220,36 @@ Please take note that they will show your timeline in a 'Mastodon fashion'
2 post display with the most active threads at the top that the web interface of
3 .Nm
4 provides.
5+.Ss Implementing post bots
6+.Nm
7+makes very easy to post messages in a non-interactive manner. This example
8+posts a string:
9+.Bd -literal -offset indent
10+uptime | snac note $SNAC_BASEDIR $SNAC_USER -
11+.Ed
12+.Pp
13+You can setup a line like this from a
14+.Xr crontab 5
15+or similar. Take note that you need a) command-line access to the same machine
16+that hosts the
17+.Nm
18+instance, and b) write permissions to the storage directories and files.
19+.Pp
20+You can also post non-interactively using the Mastodon API and a command-line
21+http tool like
22+.Xr curl 1
23+or similar. This has the advantage that you can do it remotely from any host,
24+anywhere; the only thing you need is an API Token. This is an example:
25+.Bd -literal -offset indent
26+curl -X POST https://$SNAC_HOST/api/v1/statuses \\
27+--header "Authorization: Bearer ${TOKEN}" -d "status=$(uptime)"
28+.Ed
29+.Pp
30+You can obtain an API Token by connecting to the following URL:
31+.Bd -literal -offset indent
32+https://$SNAC_HOST/oauth/x-snac-get-token
33+.Ed
34+.Pp
35 .Sh ENVIRONMENT
36 .Bl -tag -width Ds
37 .It Ev DEBUG
+1,
-1
1@@ -1,7 +1,7 @@
2 #! /bin/sh
3 if [ ! -e /data/data/server.json ]
4 then
5- echo -ne "0.0.0.0\r\n8001\r\nlocalhost\r\n\r\n" | snac init /data/data
6+ echo -ne "0.0.0.0\r\n8001\r\nlocalhost\r\n\r\n\r\n" | snac init /data/data
7 snac adduser /data/data testuser
8 fi
9 SSLKEYLOGFILE=/data/key snac httpd /data/data
M
html.c
+16,
-11
1@@ -79,7 +79,7 @@ xs_str *actor_name(xs_dict *actor)
2 }
3
4
5-d_char *html_actor_icon(snac *snac, d_char *os, char *actor,
6+xs_str *html_actor_icon(xs_str *os, char *actor,
7 const char *date, const char *udate, const char *url, int priv)
8 {
9 xs *s = xs_str_new(NULL);
10@@ -168,7 +168,7 @@ d_char *html_msg_icon(snac *snac, d_char *os, char *msg)
11 date = xs_dict_get(msg, "published");
12 udate = xs_dict_get(msg, "updated");
13
14- os = html_actor_icon(snac, os, actor, date, udate, url, priv);
15+ os = html_actor_icon(os, actor, date, udate, url, priv);
16 }
17
18 return os;
19@@ -983,7 +983,7 @@ d_char *html_entry(snac *snac, d_char *os, char *msg, int local,
20 }
21
22
23-d_char *html_user_footer(snac *snac, d_char *s)
24+xs_str *html_user_footer(xs_str *s)
25 {
26 xs *s1 = xs_fmt(
27 "<div class=\"snac-footer\">\n"
28@@ -1064,7 +1064,7 @@ d_char *html_timeline(snac *snac, char *list, int local, int skip, int show, int
29 s = xs_str_cat(s, s1);
30 }
31
32- s = html_user_footer(snac, s);
33+ s = html_user_footer(s);
34
35 s = xs_str_cat(s, "</body>\n</html>\n");
36
37@@ -1088,8 +1088,7 @@ d_char *html_people_list(snac *snac, d_char *os, d_char *list, const char *heade
38 if (valid_status(actor_get(snac, actor_id, &actor))) {
39 s = xs_str_cat(s, "<div class=\"snac-post\">\n");
40
41- s = html_actor_icon(snac, s, actor, xs_dict_get(actor, "published"), NULL, NULL, 0);
42-
43+ s = html_actor_icon(s, actor, xs_dict_get(actor, "published"), NULL, NULL, 0);
44
45 /* content (user bio) */
46 char *c = xs_dict_get(actor, "summary");
47@@ -1182,7 +1181,7 @@ d_char *html_people(snac *snac)
48
49 s = html_people_list(snac, s, wers, L("People that follows you"), "e");
50
51- s = html_user_footer(snac, s);
52+ s = html_user_footer(s);
53
54 s = xs_str_cat(s, "</body>\n</html>\n");
55
56@@ -1226,7 +1225,7 @@ xs_str *html_notifications(snac *snac)
57 const char *actor_id = xs_dict_get(noti, "actor");
58 xs *actor = NULL;
59
60- if (!valid_status(object_get(actor_id, &actor)))
61+ if (!valid_status(actor_get(snac, actor_id, &actor)))
62 continue;
63
64 xs *a_name = actor_name(actor);
65@@ -1277,12 +1276,13 @@ xs_str *html_notifications(snac *snac)
66 s = xs_str_cat(s, s1);
67 }
68
69- s = html_user_footer(snac, s);
70+ s = html_user_footer(s);
71
72 s = xs_str_cat(s, "</body>\n</html>\n");
73
74 /* set the check time to now */
75 xs *dummy = notify_check_time(snac, 1);
76+ dummy = xs_free(dummy);
77
78 timeline_touch(snac);
79
80@@ -1290,7 +1290,8 @@ xs_str *html_notifications(snac *snac)
81 }
82
83
84-int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char **ctype)
85+int html_get_handler(const xs_dict *req, const char *q_path,
86+ char **body, int *b_size, char **ctype)
87 {
88 char *accept = xs_dict_get(req, "accept");
89 int status = 404;
90@@ -1547,9 +1548,13 @@ int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char *
91 }
92
93
94-int html_post_handler(d_char *req, char *q_path, d_char *payload, int p_size,
95+int html_post_handler(const xs_dict *req, const char *q_path,
96+ char *payload, int p_size,
97 char **body, int *b_size, char **ctype)
98 {
99+ (void)p_size;
100+ (void)ctype;
101+
102 int status = 0;
103 snac snac;
104 char *uid, *p_path;
M
http.c
+1,
-1
1@@ -33,7 +33,7 @@ xs_dict *http_signed_request_raw(const char *keyid, const char *seckey,
2 date = xs_str_utctime(0, "%a, %d %b %Y %H:%M:%S GMT");
3
4 {
5- xs *s = xs_replace(url, "https:/" "/", "");
6+ xs *s = xs_replace_n(url, "https:/" "/", "", 1);
7 l1 = xs_split_n(s, "/", 1);
8 }
9
M
httpd.c
+7,
-1
1@@ -43,12 +43,14 @@ d_char *nodeinfo_2_0(void)
2 }
3
4
5-int server_get_handler(d_char *req, char *q_path,
6+int server_get_handler(xs_dict *req, char *q_path,
7 char **body, int *b_size, char **ctype)
8 /* basic server services */
9 {
10 int status = 0;
11
12+ (void)req;
13+
14 /* is it the server root? */
15 if (*q_path == '\0') {
16 /* try to open greeting.html */
17@@ -285,6 +287,8 @@ static jmp_buf on_break;
18
19 void term_handler(int s)
20 {
21+ (void)s;
22+
23 longjmp(on_break, 1);
24 }
25
26@@ -401,6 +405,8 @@ static void *background_thread(void *arg)
27 {
28 time_t purge_time;
29
30+ (void)arg;
31+
32 /* first purge time */
33 purge_time = time(NULL) + 10 * 60;
34
M
main.c
+22,
-0
1@@ -30,6 +30,7 @@ int usage(void)
2 printf("actor {basedir} {uid} {url} Requests an actor\n");
3 printf("note {basedir} {uid} {'text'} Sends a note to followers\n");
4 printf("resetpwd {basedir} {uid} Resets the password of a user\n");
5+ printf("ping {basedir} {uid} {actor} Pings an actor\n");
6
7 return 1;
8 }
9@@ -228,6 +229,27 @@ int main(int argc, char *argv[])
10 return 0;
11 }
12
13+ if (strcmp(cmd, "ping") == 0) {
14+ xs *actor_o = NULL;
15+
16+ if (valid_status(actor_request(&snac, url, &actor_o))) {
17+ xs *msg = msg_ping(&snac, url);
18+
19+ enqueue_output_by_actor(&snac, msg, url, 0);
20+
21+ if (dbglevel) {
22+ xs *j = xs_json_dumps_pp(msg, 4);
23+ printf("%s\n", j);
24+ }
25+ }
26+ else {
27+ srv_log(xs_fmt("Error getting actor %s", url));
28+ return 1;
29+ }
30+
31+ return 0;
32+ }
33+
34 if (strcmp(cmd, "request") == 0) {
35 int status;
36 xs *data = NULL;
+121,
-80
1@@ -162,7 +162,7 @@ const char *login_page = ""
2 "<!DOCTYPE html>\n"
3 "<body><h1>%s OAuth identify</h1>\n"
4 "<div style=\"background-color: red; color: white\">%s</div>\n"
5-"<form method=\"post\" action=\"https:/" "/%s/oauth/x-snac-login\">\n"
6+"<form method=\"post\" action=\"https:/" "/%s/%s\">\n"
7 "<p>Login: <input type=\"text\" name=\"login\"></p>\n"
8 "<p>Password: <input type=\"password\" name=\"passwd\"></p>\n"
9 "<input type=\"hidden\" name=\"redir\" value=\"%s\">\n"
10@@ -175,6 +175,8 @@ const char *login_page = ""
11 int oauth_get_handler(const xs_dict *req, const char *q_path,
12 char **body, int *b_size, char **ctype)
13 {
14+ (void)b_size;
15+
16 if (!xs_startswith(q_path, "/oauth/"))
17 return 0;
18
19@@ -185,7 +187,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
20
21 int status = 404;
22 xs_dict *msg = xs_dict_get(req, "q_vars");
23- xs *cmd = xs_replace(q_path, "/oauth", "");
24+ xs *cmd = xs_replace_n(q_path, "/oauth", "", 1);
25
26 srv_debug(1, xs_fmt("oauth_get_handler %s", q_path));
27
28@@ -206,17 +208,28 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
29 if (xs_is_null(state))
30 state = "";
31
32- *body = xs_fmt(login_page, host, "", host, ruri, cid, state, USER_AGENT);
33+ *body = xs_fmt(login_page, host, "", host, "oauth/x-snac-login",
34+ ruri, cid, state, USER_AGENT);
35 *ctype = "text/html";
36 status = 200;
37
38- srv_debug(0, xs_fmt("oauth authorize: generating login page"));
39+ srv_debug(1, xs_fmt("oauth authorize: generating login page"));
40 }
41 else
42- srv_debug(0, xs_fmt("oauth authorize: bad client_id %s", cid));
43+ srv_debug(1, xs_fmt("oauth authorize: bad client_id %s", cid));
44 }
45 else
46- srv_debug(0, xs_fmt("oauth authorize: invalid or unset arguments"));
47+ srv_debug(1, xs_fmt("oauth authorize: invalid or unset arguments"));
48+ }
49+ else
50+ if (strcmp(cmd, "/x-snac-get-token") == 0) {
51+ const char *host = xs_dict_get(srv_config, "host");
52+
53+ *body = xs_fmt(login_page, host, "", host, "oauth/x-snac-get-token",
54+ "", "", "", USER_AGENT);
55+ *ctype = "text/html";
56+ status = 200;
57+
58 }
59
60 return status;
61@@ -227,6 +240,9 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
62 const char *payload, int p_size,
63 char **body, int *b_size, char **ctype)
64 {
65+ (void)p_size;
66+ (void)b_size;
67+
68 if (!xs_startswith(q_path, "/oauth/"))
69 return 0;
70
71@@ -245,7 +261,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
72 else
73 args = xs_dup(xs_dict_get(req, "p_vars"));
74
75- xs *cmd = xs_replace(q_path, "/oauth", "");
76+ xs *cmd = xs_replace_n(q_path, "/oauth", "", 1);
77
78 srv_debug(1, xs_fmt("oauth_post_handler %s", q_path));
79
80@@ -259,7 +275,8 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
81 const char *host = xs_dict_get(srv_config, "host");
82
83 /* by default, generate another login form with an error */
84- *body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, redir, cid, state, USER_AGENT);
85+ *body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, "oauth/x-snac-login",
86+ redir, cid, state, USER_AGENT);
87 *ctype = "text/html";
88 status = 200;
89
90@@ -268,8 +285,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
91
92 if (user_open(&snac, login)) {
93 /* check the login + password */
94- if (check_password(login, passwd,
95- xs_dict_get(snac.config, "passwd"))) {
96+ if (check_password(login, passwd, xs_dict_get(snac.config, "passwd"))) {
97 /* success! redirect to the desired uri */
98 xs *code = random_str();
99
100@@ -328,7 +344,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
101 const char *auhdr = xs_dict_get(req, "authorization");
102
103 if (!xs_is_null(auhdr) && xs_startswith(auhdr, "Basic ")) {
104- xs *s1 = xs_replace(auhdr, "Basic ", "");
105+ xs *s1 = xs_replace_n(auhdr, "Basic ", "", 1);
106 int size;
107 xs *s2 = xs_base64_dec(s1, &size);
108
109@@ -373,7 +389,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
110
111 const char *uid = xs_dict_get(app, "uid");
112
113- srv_debug(0, xs_fmt("oauth token: "
114+ srv_debug(1, xs_fmt("oauth token: "
115 "successful login for %s, new token %s", uid, tokid));
116
117 xs *token = xs_dict_new();
118@@ -387,7 +403,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
119 }
120 }
121 else {
122- srv_debug(0, xs_fmt("oauth token: invalid or unset arguments"));
123+ srv_debug(1, xs_fmt("oauth token: invalid or unset arguments"));
124 status = 400;
125 }
126 }
127@@ -409,7 +425,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
128 }
129 else {
130 token_del(tokid);
131- srv_debug(0, xs_fmt("oauth revoke: revoked token %s", tokid));
132+ srv_debug(1, xs_fmt("oauth revoke: revoked token %s", tokid));
133 status = 200;
134
135 /* also delete the app, as it serves no purpose from now on */
136@@ -417,10 +433,52 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
137 }
138 }
139 else {
140- srv_debug(0, xs_fmt("oauth revoke: invalid or unset arguments"));
141+ srv_debug(1, xs_fmt("oauth revoke: invalid or unset arguments"));
142 status = 403;
143 }
144 }
145+ if (strcmp(cmd, "/x-snac-get-token") == 0) {
146+ const char *login = xs_dict_get(args, "login");
147+ const char *passwd = xs_dict_get(args, "passwd");
148+
149+ const char *host = xs_dict_get(srv_config, "host");
150+
151+ /* by default, generate another login form with an error */
152+ *body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, "oauth/x-snac-get-token",
153+ "", "", "", USER_AGENT);
154+ *ctype = "text/html";
155+ status = 200;
156+
157+ if (login && passwd) {
158+ snac user;
159+
160+ if (user_open(&user, login)) {
161+ /* check the login + password */
162+ if (check_password(login, passwd, xs_dict_get(user.config, "passwd"))) {
163+ /* success! create a new token */
164+ xs *tokid = random_str();
165+
166+ srv_debug(1, xs_fmt("x-snac-new-token: "
167+ "successful login for %s, new token %s", login, tokid));
168+
169+ xs *token = xs_dict_new();
170+ token = xs_dict_append(token, "token", tokid);
171+ token = xs_dict_append(token, "client_id", "snac-client");
172+ token = xs_dict_append(token, "client_secret", "");
173+ token = xs_dict_append(token, "uid", login);
174+ token = xs_dict_append(token, "code", "");
175+
176+ token_add(tokid, token);
177+
178+ *ctype = "text/plain";
179+ xs_free(*body);
180+ *body = xs_dup(tokid);
181+ }
182+
183+ user_free(&user);
184+ }
185+ }
186+ }
187
188 return status;
189 }
190@@ -537,7 +595,6 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
191 xs *f = xs_val_new(XSTYPE_FALSE);
192 xs *t = xs_val_new(XSTYPE_TRUE);
193 xs *n = xs_val_new(XSTYPE_NULL);
194- xs *el = xs_list_new();
195 xs *idx = NULL;
196 xs *ixc = NULL;
197
198@@ -787,7 +844,7 @@ int process_auth_token(snac *snac, const xs_dict *req)
199
200 /* if there is an authorization field, try to validate it */
201 if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) {
202- xs *tokid = xs_replace(v, "Bearer ", "");
203+ xs *tokid = xs_replace_n(v, "Bearer ", "", 1);
204 xs *token = token_get(tokid);
205
206 if (token != NULL) {
207@@ -815,6 +872,8 @@ int process_auth_token(snac *snac, const xs_dict *req)
208 int mastoapi_get_handler(const xs_dict *req, const char *q_path,
209 char **body, int *b_size, char **ctype)
210 {
211+ (void)b_size;
212+
213 if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
214 return 0;
215
216@@ -826,7 +885,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
217
218 int status = 404;
219 xs_dict *args = xs_dict_get(req, "q_vars");
220- xs *cmd = xs_replace(q_path, "/api", "");
221+ xs *cmd = xs_replace_n(q_path, "/api", "", 1);
222
223 snac snac1 = {0};
224 int logged_in = process_auth_token(&snac1, req);
225@@ -1036,11 +1095,14 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
226 }
227 else
228 if (strcmp(cmd, "/v1/timelines/public") == 0) {
229- /* the public timeline (public timelines for all users) */
230+ /* the instance public timeline (public timelines for all users) */
231
232- /* this is an ugly kludge: first users in the list get all the fame */
233+ /* NOTE: this api call needs no authorization; but,
234+ I need a logged-in user in mastoapi_status() for
235+ is_msg_public() and the liked/boosted flags,
236+ so it will silently fail for pure public access */
237
238- const char *limit_s = xs_dict_get(args, "limit");
239+ const char *limit_s = xs_dict_get(args, "limit");
240 int limit = 0;
241 int cnt = 0;
242
243@@ -1050,44 +1112,28 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
244 if (limit == 0)
245 limit = 20;
246
247- xs *out = xs_list_new();
248- xs *users = user_list();
249- xs_list *p = users;
250- xs_str *uid;
251-
252- while (xs_list_iter(&p, &uid) && cnt < limit) {
253- snac user;
254-
255- if (user_open(&user, uid)) {
256- xs *timeline = timeline_simple_list(&user, "public", 0, 4);
257- xs_list *p2 = timeline;
258- xs_str *v;
259-
260- while (xs_list_iter(&p2, &v) && cnt < limit) {
261- xs *msg = NULL;
262+ xs *timeline = timeline_instance_list(0, limit);
263+ xs *out = xs_list_new();
264+ xs_list *p = timeline;
265+ xs_str *md5;
266
267- /* get the entry */
268- if (!valid_status(timeline_get_by_md5(&user, v, &msg)))
269- continue;
270-
271- /* discard non-Notes */
272- if (strcmp(xs_dict_get(msg, "type"), "Note") != 0)
273- continue;
274+ while (logged_in && xs_list_iter(&p, &md5) && cnt < limit) {
275+ xs *msg = NULL;
276
277- /* discard entries not by this user */
278- if (!xs_startswith(xs_dict_get(msg, "id"), user.actor))
279- continue;
280+ /* get the entry */
281+ if (!valid_status(object_get_by_md5(md5, &msg)))
282+ continue;
283
284- /* convert the Note into a Mastodon status */
285- xs *st = mastoapi_status(&user, msg);
286+ /* discard non-Notes */
287+ if (strcmp(xs_dict_get(msg, "type"), "Note") != 0)
288+ continue;
289
290- if (st != NULL) {
291- out = xs_list_append(out, st);
292- cnt++;
293- }
294- }
295+ /* convert the Note into a Mastodon status */
296+ xs *st = mastoapi_status(&snac1, msg);
297
298- user_free(&user);
299+ if (st != NULL) {
300+ out = xs_list_append(out, st);
301+ cnt++;
302 }
303 }
304
305@@ -1121,7 +1167,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
306 xs *actor = NULL;
307 xs *entry = NULL;
308
309- if (!valid_status(object_get(xs_dict_get(noti, "actor"), &actor)))
310+ if (!valid_status(actor_get(&snac1, xs_dict_get(noti, "actor"), &actor)))
311 continue;
312
313 if (objid != NULL && !valid_status(object_get(objid, &entry)))
314@@ -1287,25 +1333,6 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
315 cfg = xs_dict_append(cfg, "statuses", d11);
316 }
317
318- {
319- xs *d11 = xs_dict_new();
320- xs *mt = xs_list_new();
321-
322- mt = xs_list_append(mt, "image/jpeg");
323- mt = xs_list_append(mt, "image/png");
324- mt = xs_list_append(mt, "image/gif");
325-
326- d11 = xs_dict_append(d11, "supported_mime_types", mt);
327-
328- d11 = xs_dict_append(d11, "image_size_limit", z);
329- d11 = xs_dict_append(d11, "image_matrix_limit", z);
330- d11 = xs_dict_append(d11, "video_size_limit", z);
331- d11 = xs_dict_append(d11, "video_matrix_limit", z);
332- d11 = xs_dict_append(d11, "video_frame_rate_limit", z);
333-
334- cfg = xs_dict_append(cfg, "media_attachments", d11);
335- }
336-
337 ins = xs_dict_append(ins, "configuration", cfg);
338
339 *body = xs_json_dumps_pp(ins, 4);
340@@ -1326,7 +1353,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
341 /* skip the 'fake' part of the id */
342 id = MID_TO_MD5(id);
343
344- if (valid_status(timeline_get_by_md5(&snac1, id, &msg))) {
345+ if (valid_status(object_get_by_md5(id, &msg))) {
346 if (op == NULL) {
347 if (!is_muted(&snac1, xs_dict_get(msg, "attributedTo"))) {
348 /* return the status itself */
349@@ -1487,6 +1514,9 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
350 const char *payload, int p_size,
351 char **body, int *b_size, char **ctype)
352 {
353+ (void)p_size;
354+ (void)b_size;
355+
356 if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
357 return 0;
358
359@@ -1513,7 +1543,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
360 printf("%s\n", j);
361 }*/
362
363- xs *cmd = xs_replace(q_path, "/api", "");
364+ xs *cmd = xs_replace_n(q_path, "/api", "", 1);
365
366 snac snac = {0};
367 int logged_in = process_auth_token(&snac, req);
368@@ -1562,7 +1592,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
369
370 app_add(cid, app);
371
372- srv_debug(0, xs_fmt("mastoapi apps: new app %s", cid));
373+ srv_debug(1, xs_fmt("mastoapi apps: new app %s", cid));
374 }
375 }
376 else
377@@ -1582,6 +1612,9 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
378 if (xs_is_null(media_ids))
379 media_ids = xs_dict_get(args, "media_ids[]");
380
381+ if (xs_is_null(visibility))
382+ visibility = "public";
383+
384 xs *attach_list = xs_list_new();
385 xs *irt = NULL;
386
387@@ -1683,7 +1716,11 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
388 }
389 else
390 if (strcmp(op, "unfavourite") == 0) {
391- /* snac does not support Undo+Like */
392+ /* partial support: as the original Like message
393+ is not stored anywhere here, it's not possible
394+ to send an Undo + Like; the only thing done here
395+ is to delete the actor from the list of likes */
396+ object_unadmire(id, snac.actor, 1);
397 }
398 else
399 if (strcmp(op, "reblog") == 0) {
400@@ -1698,7 +1735,8 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
401 }
402 else
403 if (strcmp(op, "unreblog") == 0) {
404- /* snac does not support Undo+Announce */
405+ /* partial support: see comment in 'unfavourite' */
406+ object_unadmire(id, snac.actor, 0);
407 }
408 else
409 if (strcmp(op, "bookmark") == 0) {
410@@ -1903,6 +1941,9 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
411 const char *payload, int p_size,
412 char **body, int *b_size, char **ctype)
413 {
414+ (void)p_size;
415+ (void)b_size;
416+
417 if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
418 return 0;
419
420@@ -1924,7 +1965,7 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
421 if (args == NULL)
422 return 400;
423
424- xs *cmd = xs_replace(q_path, "/api", "");
425+ xs *cmd = xs_replace_n(q_path, "/api", "", 1);
426
427 snac snac = {0};
428 int logged_in = process_auth_token(&snac, req);
M
snac.h
+18,
-13
1@@ -1,7 +1,7 @@
2 /* snac - A simple, minimalistic ActivityPub instance */
3 /* copyright (c) 2022 - 2023 grunfink / MIT license */
4
5-#define VERSION "2.29"
6+#define VERSION "2.30"
7
8 #define USER_AGENT "snac/" VERSION
9
10@@ -83,6 +83,7 @@ int object_del_if_unref(const char *id);
11 double object_ctime_by_md5(const char *md5);
12 double object_ctime(const char *id);
13 int object_admire(const char *id, const char *actor, int like);
14+int object_unadmire(const char *id, const char *actor, int like);
15
16 int object_likes_len(const char *id);
17 int object_announces_len(const char *id);
18@@ -105,14 +106,14 @@ int timeline_touch(snac *snac);
19 int timeline_here(snac *snac, const char *md5);
20 int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg);
21 int timeline_del(snac *snac, char *id);
22-d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show);
23-d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show);
24+xs_list *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show);
25+xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show);
26 int timeline_add(snac *snac, char *id, char *o_msg);
27 void timeline_admire(snac *snac, char *id, char *admirer, int like);
28
29 xs_list *timeline_top_level(snac *snac, xs_list *list);
30-
31-d_char *local_list(snac *snac, int max);
32+xs_list *local_list(snac *snac, int max);
33+xs_list *timeline_instance_list(int skip, int show);
34
35 int following_add(snac *snac, const char *actor, const xs_dict *msg);
36 int following_del(snac *snac, const char *actor);
37@@ -127,8 +128,8 @@ int is_muted(snac *snac, const char *actor);
38 void hide(snac *snac, const char *id);
39 int is_hidden(snac *snac, const char *id);
40
41-int actor_add(snac *snac, const char *actor, d_char *msg);
42-int actor_get(snac *snac, const char *actor, d_char **data);
43+int actor_add(const char *actor, xs_dict *msg);
44+int actor_get(snac *snac, const char *actor, xs_dict **data);
45
46 int static_get(snac *snac, const char *id, d_char **data, int *size);
47 void static_put(snac *snac, const char *id, const char *data, int size);
48@@ -154,7 +155,7 @@ void inbox_add(const char *inbox);
49 void inbox_add_by_actor(const xs_dict *actor);
50 xs_list *inbox_list(void);
51
52-void enqueue_input(snac *snac, xs_dict *msg, xs_dict *req, int retries);
53+void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries);
54 void enqueue_output_raw(const char *keyid, const char *seckey,
55 xs_dict *msg, xs_str *inbox, int retries);
56 void enqueue_output(snac *snac, xs_dict *msg, xs_str *inbox, int retries);
57@@ -186,7 +187,7 @@ int check_signature(snac *snac, xs_dict *req, xs_str **err);
58 void httpd(void);
59
60 int webfinger_request(const char *qs, char **actor, char **user);
61-int webfinger_get_handler(d_char *req, char *q_path,
62+int webfinger_get_handler(xs_dict *req, char *q_path,
63 char **body, int *b_size, char **ctype);
64
65 const char *default_avatar_base64(void);
66@@ -202,6 +203,8 @@ d_char *msg_undo(snac *snac, char *object);
67 d_char *msg_delete(snac *snac, char *id);
68 d_char *msg_actor(snac *snac);
69 xs_dict *msg_update(snac *snac, xs_dict *object);
70+xs_dict *msg_ping(snac *user, const char *rcpt);
71+xs_dict *msg_pong(snac *user, const char *rcpt, const char *object);
72
73 int activitypub_request(snac *snac, const char *url, xs_dict **data);
74 int actor_request(snac *snac, const char *actor, xs_dict **data);
75@@ -219,17 +222,19 @@ int process_user_queue(snac *snac);
76 void process_queue_item(xs_dict *q_item);
77 int process_queue(void);
78
79-int activitypub_get_handler(d_char *req, char *q_path,
80+int activitypub_get_handler(const xs_dict *req, const char *q_path,
81 char **body, int *b_size, char **ctype);
82-int activitypub_post_handler(d_char *req, char *q_path,
83+int activitypub_post_handler(const xs_dict *req, const char *q_path,
84 char *payload, int p_size,
85 char **body, int *b_size, char **ctype);
86
87 d_char *not_really_markdown(const char *content);
88 d_char *sanitize(const char *str);
89
90-int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char **ctype);
91-int html_post_handler(d_char *req, char *q_path, d_char *payload, int p_size,
92+int html_get_handler(const xs_dict *req, const char *q_path,
93+ char **body, int *b_size, char **ctype);
94+int html_post_handler(const xs_dict *req, const char *q_path,
95+ char *payload, int p_size,
96 char **body, int *b_size, char **ctype);
97
98 int snac_init(const char *_basedir);
+5,
-3
1@@ -21,7 +21,7 @@ int webfinger_request(const char *qs, char **actor, char **user)
2
3 if (xs_startswith(qs, "https:/" "/")) {
4 /* actor query: pick the host */
5- xs *s = xs_replace(qs, "https:/" "/", "");
6+ xs *s = xs_replace_n(qs, "https:/" "/", "", 1);
7
8 l = xs_split_n(s, "/", 1);
9
10@@ -74,7 +74,7 @@ int webfinger_request(const char *qs, char **actor, char **user)
11 char *subject = xs_dict_get(obj, "subject");
12
13 if (subject)
14- *user = xs_replace(subject, "acct:", "");
15+ *user = xs_replace_n(subject, "acct:", "", 1);
16 }
17
18 if (actor != NULL) {
19@@ -105,6 +105,8 @@ int webfinger_get_handler(d_char *req, char *q_path,
20 {
21 int status;
22
23+ (void)b_size;
24+
25 if (strcmp(q_path, "/.well-known/webfinger") != 0)
26 return 0;
27
28@@ -137,7 +139,7 @@ int webfinger_get_handler(d_char *req, char *q_path,
29 else
30 if (xs_startswith(resource, "acct:")) {
31 /* it's an account name */
32- xs *an = xs_replace(resource, "acct:", "");
33+ xs *an = xs_replace_n(resource, "acct:", "", 1);
34 xs *l = NULL;
35
36 /* strip a possible leading @ */
M
xs.h
+8,
-4
1@@ -65,8 +65,10 @@ xs_str *xs_str_new(const char *str);
2 xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix);
3 #define xs_str_prepend_i(str, prefix) xs_str_wrap_i(prefix, str, NULL)
4 #define xs_str_cat(str, suffix) xs_str_wrap_i(NULL, str, suffix)
5-xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto);
6-#define xs_replace(str, sfrom, sto) xs_replace_i(xs_dup(str), sfrom, sto)
7+xs_str *xs_replace_in(xs_str *str, const char *sfrom, const char *sto, int times);
8+#define xs_replace_i(str, sfrom, sto) xs_replace_in(str, sfrom, sto, XS_ALL)
9+#define xs_replace(str, sfrom, sto) xs_replace_in(xs_dup(str), sfrom, sto, XS_ALL)
10+#define xs_replace_n(str, sfrom, sto, times) xs_replace_in(xs_dup(str), sfrom, sto, times)
11 xs_str *xs_fmt(const char *fmt, ...);
12 int xs_str_in(const char *haystack, const char *needle);
13 int _xs_startsorends(const char *str, const char *xfix, int ends);
14@@ -416,7 +418,7 @@ xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix)
15 }
16
17
18-xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto)
19+xs_str *xs_replace_in(xs_str *str, const char *sfrom, const char *sto, int times)
20 /* replaces inline all sfrom with sto */
21 {
22 XS_ASSERT_TYPE(str, XSTYPE_STRING);
23@@ -426,7 +428,7 @@ xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto)
24 char *ss;
25 int offset = 0;
26
27- while ((ss = strstr(str + offset, sfrom)) != NULL) {
28+ while (times > 0 && (ss = strstr(str + offset, sfrom)) != NULL) {
29 int n_offset = ss - str;
30
31 str = xs_collapse(str, n_offset, sfsz);
32@@ -434,6 +436,8 @@ xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto)
33 memcpy(str + n_offset, sto, stsz);
34
35 offset = n_offset + stsz;
36+
37+ times--;
38 }
39
40 return str;
+211,
-10
1@@ -7,13 +7,20 @@
2 xs_str *xs_hex_enc(const xs_val *data, int size);
3 xs_val *xs_hex_dec(const xs_str *hex, int *size);
4 int xs_is_hex(const char *str);
5+ xs_str *xs_base32_enc(const xs_val *data, int sz);
6+ xs_str *xs_base32hex_enc(const xs_val *data, int sz);
7+ xs_val *xs_base32_dec(const xs_str *data, int *size);
8+ xs_val *xs_base32hex_dec(const xs_str *data, int *size);
9 xs_str *xs_base64_enc(const xs_val *data, int sz);
10 xs_val *xs_base64_dec(const xs_str *data, int *size);
11+ int xs_is_base64(const char *str);
12 xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint);
13
14
15 #ifdef XS_IMPLEMENTATION
16
17+/** hex **/
18+
19 xs_str *xs_hex_enc(const xs_val *data, int size)
20 /* returns an hexdump of data */
21 {
22@@ -78,16 +85,178 @@ int xs_is_hex(const char *str)
23 }
24
25
26-xs_str *xs_base64_enc(const xs_val *data, int sz)
27-/* encodes data to base64 */
28+/** base32 */
29+
30+static char *xs_b32_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
31+ "234567=";
32+
33+static char *xs_b32hex_tbl = "0123456789"
34+ "ABCDEFGHIJKLMNOPQRSTUV=";
35+
36+/*
37+ 00000|00011|11111|12222|22223|33333|33444|44444
38+*/
39+
40+xs_str *xs_base32_enc_tbl(const xs_val *data, int sz, const char *b32_tbl)
41+/* encodes data to base32 using a table */
42+{
43+ xs_str *s = xs_str_new(NULL);
44+ unsigned char *p;
45+ int n;
46+
47+ p = (unsigned char *)data;
48+
49+ for (n = 0; n < sz; n += 5) {
50+ int l = sz - n;
51+ char enc[9] = "========";
52+
53+ enc[0] = b32_tbl[(p[n] >> 3) & 0x1f];
54+
55+ if (l > 1) {
56+ enc[1] = b32_tbl[(p[n] << 2 | p[n + 1] >> 6) & 0x1f];
57+ enc[2] = b32_tbl[(p[n + 1] >> 1) & 0x1f];
58+
59+ if (l > 2) {
60+ enc[3] = b32_tbl[(p[n + 1] << 4 | p[n + 2] >> 4) & 0x1f];
61+
62+ if (l > 3) {
63+ enc[4] = b32_tbl[(p[n + 2] << 1 | p[n + 3] >> 7) & 0x1f];
64+ enc[5] = b32_tbl[(p[n + 3] >> 2) & 0x1f];
65+
66+ if (l > 4) {
67+ enc[6] = b32_tbl[(p[n + 3] << 3 | p[n + 4] >> 5) & 0x1f];
68+ enc[7] = b32_tbl[(p[n + 4]) & 0x1f];
69+ }
70+ else
71+ enc[6] = b32_tbl[(p[n + 3] << 3) & 0x1f];
72+ }
73+ else
74+ enc[4] = b32_tbl[(p[n + 2] << 1) & 0x1f];
75+ }
76+ else
77+ enc[3] = b32_tbl[(p[n + 1] << 4) & 0x1f];
78+ }
79+ else
80+ enc[1] = b32_tbl[(p[n] << 2) & 0x1f];
81+
82+ s = xs_str_cat(s, enc);
83+ }
84+
85+ return s;
86+}
87+
88+
89+xs_str *xs_base32_enc(const xs_val *data, int sz)
90+/* encodes data to base32 */
91+{
92+ return xs_base32_enc_tbl(data, sz, xs_b32_tbl);
93+}
94+
95+
96+xs_str *xs_base32hex_enc(const xs_val *data, int sz)
97+/* encodes data to base32 with HEX alphabet (RFC4648) */
98+{
99+ return xs_base32_enc_tbl(data, sz, xs_b32hex_tbl);
100+}
101+
102+
103+xs_val *xs_base32_dec_tbl(const xs_str *data, int *size, const char *b32_tbl)
104+/* decodes data from base32 using a table */
105+{
106+ xs_val *s = NULL;
107+ int sz = 0;
108+ char *p;
109+
110+ p = (char *)data;
111+
112+ /* size of data must be a multiple of 8 */
113+ if (strlen(p) % 8)
114+ return NULL;
115+
116+ for (p = (char *)data; *p; p += 8) {
117+ int cs[8];
118+ int n;
119+ unsigned char tmp[5];
120+
121+ for (n = 0; n < 8; n++) {
122+ char *ss = strchr(b32_tbl, p[n]);
123+
124+ if (ss == NULL) {
125+ /* not a base32 char */
126+ return xs_free(s);
127+ }
128+
129+ cs[n] = ss - b32_tbl;
130+ }
131+
132+ n = 0;
133+
134+ /* #0 byte */
135+ tmp[n++] = cs[0] << 3 | cs[1] >> 2;
136+
137+ if (cs[2] != 32) {
138+ /* #1 byte */
139+ tmp[n++] = (cs[1] & 0x3) << 6 | cs[2] << 1 | (cs[3] & 0x10) >> 4;
140+
141+ if (cs[4] != 32) {
142+ /* #2 byte */
143+ tmp[n++] = (cs[3] & 0xf) << 4 | cs[4] >> 1;
144+
145+ if (cs[5] != 32) {
146+ /* #3 byte */
147+ tmp[n++] = (cs[4] & 0x1) << 7 | cs[5] << 2 | cs[6] >> 3;
148+
149+ if (cs[7] != 32) {
150+ /* #4 byte */
151+ tmp[n++] = (cs[6] & 0x7) << 5 | cs[7];
152+ }
153+ }
154+ }
155+ }
156+
157+ /* must be done manually because data can be pure binary */
158+ s = xs_realloc(s, _xs_blk_size(sz + n));
159+ memcpy(s + sz, tmp, n);
160+ sz += n;
161+ }
162+
163+ /* asciiz it to use it as a string */
164+ s = xs_realloc(s, _xs_blk_size(sz + 1));
165+ s[sz] = '\0';
166+
167+ *size = sz;
168+
169+ return s;
170+}
171+
172+
173+xs_val *xs_base32_dec(const xs_str *data, int *size)
174+/* decodes data from base32 */
175+{
176+ return xs_base32_dec_tbl(data, size, xs_b32_tbl);
177+}
178+
179+
180+xs_val *xs_base32hex_dec(const xs_str *data, int *size)
181+/* decodes data from base32 with HEX alphabet (RFC4648) */
182+{
183+ return xs_base32_dec_tbl(data, size, xs_b32hex_tbl);
184+}
185+
186+
187+/** base64 */
188+
189+static char *xs_b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
190+ "abcdefghijklmnopqrstuvwxyz"
191+ "0123456789+/=";
192+
193+xs_str *xs_base64_enc_tbl(const xs_val *data, int sz, const char *b64_tbl)
194+/* encodes data to base64 using a table */
195 {
196 xs_str *s;
197 unsigned char *p;
198 char *i;
199 int bsz, n;
200- static char *b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
201- "abcdefghijklmnopqrstuvwxyz"
202- "0123456789+/";
203
204 bsz = ((sz + 3 - 1) / 3) * 4;
205 i = s = xs_realloc(NULL, _xs_blk_size(bsz + 1));
206@@ -123,15 +292,19 @@ xs_str *xs_base64_enc(const xs_val *data, int sz)
207 }
208
209
210-xs_val *xs_base64_dec(const xs_str *data, int *size)
211-/* decodes data from base64 */
212+xs_str *xs_base64_enc(const xs_val *data, int sz)
213+/* encodes data to base64 */
214+{
215+ return xs_base64_enc_tbl(data, sz, xs_b64_tbl);
216+}
217+
218+
219+xs_val *xs_base64_dec_tbl(const xs_str *data, int *size, const char *b64_tbl)
220+/* decodes data from base64 using a table */
221 {
222 xs_val *s = NULL;
223 int sz = 0;
224 char *p;
225- static char *b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
226- "abcdefghijklmnopqrstuvwxyz"
227- "0123456789+/=";
228
229 p = (char *)data;
230
231@@ -184,6 +357,34 @@ xs_val *xs_base64_dec(const xs_str *data, int *size)
232 }
233
234
235+xs_val *xs_base64_dec(const xs_str *data, int *size)
236+/* decodes data from base64 */
237+{
238+ return xs_base64_dec_tbl(data, size, xs_b64_tbl);
239+}
240+
241+
242+int xs_is_base64_tbl(const char *str, const char *b64_tbl)
243+/* returns 1 if str is a base64 string, with table */
244+{
245+ while (*str) {
246+ if (strchr(b64_tbl, *str++) == NULL)
247+ return 0;
248+ }
249+
250+ return 1;
251+}
252+
253+
254+int xs_is_base64(const char *str)
255+/* returns 1 if str is a base64 string */
256+{
257+ return xs_is_base64_tbl(str, xs_b64_tbl);
258+}
259+
260+
261+/** utf-8 **/
262+
263 xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint)
264 /* encodes an Unicode codepoint to utf8 */
265 {
+1,
-1
1@@ -1 +1 @@
2-/* a885c7cc4c8e6384ae23125ed05f434471ccc6fb */
3+/* dfdd729248d7169b80cb6a7462fe6c0ba6efeb16 */