commit a7ca400
Merge tag '2.67' Version 2.67 RELEASED.
M
Makefile
+4,
-4
1@@ -1,4 +1,4 @@
2-PREFIX=/usr/local
3+PREFIX?=/usr/local
4 PREFIX_MAN=$(PREFIX)/man
5 CFLAGS?=-g -Wall -Wextra -pedantic
6
7@@ -6,16 +6,16 @@ all: snac
8
9 snac: snac.o main.o sandbox.o data.o http.o httpd.o webfinger.o \
10 activitypub.o html.o utils.o format.o upgrade.o mastoapi.o
11- $(CC) $(CFLAGS) -L/usr/local/lib *.o -lcurl -lcrypto $(LDFLAGS) -pthread -o $@
12+ $(CC) $(CFLAGS) -L$(PREFIX)/lib *.o -lcurl -lcrypto $(LDFLAGS) -pthread -o $@
13
14 .c.o:
15- $(CC) $(CFLAGS) $(CPPFLAGS) -I/usr/local/include -c $<
16+ $(CC) $(CFLAGS) $(CPPFLAGS) -I$(PREFIX)/include -c $<
17
18 clean:
19 rm -rf *.o *.core snac makefile.depend
20
21 dep:
22- $(CC) -I/usr/local/include -MM *.c > makefile.depend
23+ $(CC) -I$(PREFIX)/include -MM *.c > makefile.depend
24
25 install:
26 mkdir -p -m 755 $(PREFIX)/bin
+14,
-0
1@@ -1,5 +1,19 @@
2 # Release Notes
3
4+## 2.67
5+
6+The search box also accepts post URLs; the post is requested and, if it's found, can be interacted with (liked, boosted, replied to, etc.).
7+
8+IP addresses for failed logins are tracked and throttled to mitigate brute force attacks (see `snac(8)` for more information).
9+
10+Fixed a bug regarding repeated attachments when editing a post.
11+
12+Mastodon API: Improved timeline pagination in some clients (details: an HTTP `Link` header has been added), fixed missing audio attachments, fixed an incorrect value for the `bot` field in newly created accounts, fixed a crash, implemented markers (contributed by nowster).
13+
14+When running in server mode, the pidfile is locked to avoid concurrent running of the same server instance.
15+
16+Added documentation and some tweaks to enable static compilation with musl (contributed by Shamar).
17+
18 ## 2.66
19
20 As many users have asked for it, there is now an option to make the number of followed and following accounts public (still disabled by default). These are only the numbers; the lists themselves are never published.
+24,
-9
1@@ -258,6 +258,10 @@ xs_list *get_attachments(const xs_dict *msg)
2 d = xs_dict_append(d, "href", href);
3 d = xs_dict_append(d, "name", name);
4
5+ const xs_dict *icon = xs_dict_get(v, "icon");
6+ if (xs_type(icon) == XSTYPE_DICT)
7+ d = xs_dict_append(d, "icon", icon);
8+
9 l = xs_list_append(l, d);
10 }
11 }
12@@ -1476,20 +1480,31 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
13
14 /* create the attachment list, if there are any */
15 if (!xs_is_null(attach)) {
16- int c = 0;
17- while (xs_list_next(attach, &v, &c)) {
18- xs *d = xs_dict_new();
19+ xs_list_foreach(attach, v) {
20 const char *url = xs_list_get(v, 0);
21 const char *alt = xs_list_get(v, 1);
22 const char *mime = xs_mime_by_ext(url);
23+ int add = 1;
24+
25+ /* check if it's already here */
26+ const xs_dict *ad;
27+ xs_list_foreach(atls, ad) {
28+ if (strcmp(xs_dict_get_def(ad, "url", ""), url) == 0) {
29+ add = 0;
30+ break;
31+ }
32+ }
33
34- d = xs_dict_append(d, "mediaType", mime);
35- d = xs_dict_append(d, "url", url);
36- d = xs_dict_append(d, "name", alt);
37- d = xs_dict_append(d, "type",
38- xs_startswith(mime, "image/") ? "Image" : "Document");
39+ if (add) {
40+ xs *d = xs_dict_new();
41+ d = xs_dict_append(d, "mediaType", mime);
42+ d = xs_dict_append(d, "url", url);
43+ d = xs_dict_append(d, "name", alt);
44+ d = xs_dict_append(d, "type",
45+ xs_startswith(mime, "image/") ? "Image" : "Document");
46
47- atls = xs_list_append(atls, d);
48+ atls = xs_list_append(atls, d);
49+ }
50 }
51 }
52
M
data.c
+190,
-0
1@@ -2705,6 +2705,23 @@ xs_list *content_search(snac *user, const char *regex,
2 if (id == NULL || is_hidden(user, id))
3 continue;
4
5+ /* test for the post URL */
6+ if (strcmp(id, regex) == 0) {
7+ if (xs_set_add(&seen, md5) == 1)
8+ show--;
9+
10+ continue;
11+ }
12+
13+ /* test for the alternate post id */
14+ const char *url = xs_dict_get(post, "url");
15+ if (xs_type(url) == XSTYPE_STRING && strcmp(url, regex) == 0) {
16+ if (xs_set_add(&seen, md5) == 1)
17+ show--;
18+
19+ continue;
20+ }
21+
22 xs *c = xs_str_new(NULL);
23 const char *content = xs_dict_get(post, "content");
24 const char *name = xs_dict_get(post, "name");
25@@ -2786,6 +2803,74 @@ xs_str *notify_check_time(snac *snac, int reset)
26 return t;
27 }
28
29+xs_dict *markers_get(snac *snac, const xs_list *markers)
30+{
31+ xs *data = NULL;
32+ xs_dict *returns = xs_dict_new();
33+ xs *fn = xs_fmt("%s/markers.json", snac->basedir);
34+ const xs_str *v = NULL;
35+ FILE *f;
36+
37+ if ((f = fopen(fn, "r")) != NULL) {
38+ data = xs_json_load(f);
39+ fclose(f);
40+ }
41+
42+ if (xs_is_null(data))
43+ data = xs_dict_new();
44+
45+ xs_list_foreach(markers, v) {
46+ const xs_dict *mark = xs_dict_get(data, v);
47+ if (!xs_is_null(mark)) {
48+ returns = xs_dict_append(returns, v, mark);
49+ }
50+ }
51+ return returns;
52+}
53+
54+xs_dict *markers_set(snac *snac, const char *home_marker, const char *notify_marker)
55+/* gets or sets notification marker */
56+{
57+ xs *data = NULL;
58+ xs_dict *written = xs_dict_new();
59+ xs *fn = xs_fmt("%s/markers.json", snac->basedir);
60+ FILE *f;
61+
62+ if ((f = fopen(fn, "r")) != NULL) {
63+ data = xs_json_load(f);
64+ fclose(f);
65+ }
66+
67+ if (xs_is_null(data))
68+ data = xs_dict_new();
69+
70+ if (!xs_is_null(home_marker)) {
71+ xs *home = xs_dict_new();
72+ xs *s_tid = tid(0);
73+ home = xs_dict_append(home, "last_read_id", home_marker);
74+ home = xs_dict_append(home, "version", xs_stock(0));
75+ home = xs_dict_append(home, "updated_at", s_tid);
76+ data = xs_dict_set(data, "home", home);
77+ written = xs_dict_append(written, "home", home);
78+ }
79+
80+ if (!xs_is_null(notify_marker)) {
81+ xs *notify = xs_dict_new();
82+ xs *s_tid = tid(0);
83+ notify = xs_dict_append(notify, "last_read_id", notify_marker);
84+ notify = xs_dict_append(notify, "version", xs_stock(0));
85+ notify = xs_dict_append(notify, "updated_at", s_tid);
86+ data = xs_dict_set(data, "notifications", notify);
87+ written = xs_dict_append(written, "notifications", notify);
88+ }
89+
90+ if ((f = fopen(fn, "w")) != NULL) {
91+ xs_json_dump(data, 4, f);
92+ fclose(f);
93+ }
94+
95+ return written;
96+}
97
98 void notify_add(snac *snac, const char *type, const char *utype,
99 const char *actor, const char *objid, const xs_dict *msg)
100@@ -3767,3 +3852,108 @@ xs_str *make_url(const char *href, const char *proxy, int by_token)
101
102 return url;
103 }
104+
105+
106+/** bad login throttle **/
107+
108+xs_str *_badlogin_fn(const char *addr)
109+{
110+ xs *md5 = xs_md5_hex(addr, strlen(addr));
111+ xs *dir = xs_fmt("%s/badlogin", srv_basedir);
112+
113+ mkdirx(dir);
114+
115+ return xs_fmt("%s/%s", dir, md5);
116+}
117+
118+
119+int _badlogin_read(const char *fn, int *failures)
120+/* reads a badlogin file */
121+{
122+ int ok = 0;
123+ FILE *f;
124+
125+ pthread_mutex_lock(&data_mutex);
126+
127+ if ((f = fopen(fn, "r")) != NULL) {
128+ xs *l = xs_readline(f);
129+ fclose(f);
130+
131+ if (sscanf(l, "%d", failures) == 1)
132+ ok = 1;
133+ }
134+
135+ pthread_mutex_unlock(&data_mutex);
136+
137+ return ok;
138+}
139+
140+
141+int badlogin_check(const char *user, const char *addr)
142+/* checks if this address is authorized to try a login */
143+{
144+ int valid = 1;
145+
146+ if (xs_type(addr) == XSTYPE_STRING) {
147+ xs *fn = _badlogin_fn(addr);
148+ double mt = mtime(fn);
149+
150+ if (mt > 0) {
151+ int badlogin_expire = xs_number_get(xs_dict_get_def(srv_config,
152+ "badlogin_expire", "300"));
153+
154+ mt += badlogin_expire;
155+
156+ /* if file is expired, delete and give pass */
157+ if (mt < time(NULL)) {
158+ srv_debug(1, xs_fmt("Login from %s for %s allowed again", addr, user));
159+ unlink(fn);
160+ }
161+ else {
162+ int failures;
163+
164+ if (_badlogin_read(fn, &failures)) {
165+ int badlogin_max = xs_number_get(xs_dict_get_def(srv_config,
166+ "badlogin_retries", "5"));
167+
168+ if (failures >= badlogin_max) {
169+ valid = 0;
170+
171+ xs *d = xs_str_iso_date((time_t) mt);
172+
173+ srv_debug(1,
174+ xs_fmt("Login from %s for %s forbidden until %s", addr, user, d));
175+ }
176+ }
177+ }
178+ }
179+ }
180+
181+ return valid;
182+}
183+
184+
185+void badlogin_inc(const char *user, const char *addr)
186+/* increments a bad login from this address */
187+{
188+ if (xs_type(addr) == XSTYPE_STRING) {
189+ int failures = 0;
190+ xs *fn = _badlogin_fn(addr);
191+ FILE *f;
192+
193+ _badlogin_read(fn, &failures);
194+
195+ pthread_mutex_lock(&data_mutex);
196+
197+ if ((f = fopen(fn, "w")) != NULL) {
198+ failures++;
199+
200+ fprintf(f, "%d %s %s\n", failures, addr, user);
201+ fclose(f);
202+
203+ srv_log(xs_fmt("Registered %d login failure(s) from %s for %s", failures, addr, user));
204+ }
205+
206+ pthread_mutex_unlock(&data_mutex);
207+ }
208+}
+29,
-0
1@@ -242,6 +242,12 @@ posts will not be direct ones, but proxied by
2 This way, remote media servers will not see the user's IP, but the server one,
3 improving privacy. Please take note that this will increase the server's incoming
4 and outgoing traffic.
5+.It Ic badlogin_retries
6+If incorrect logins from a given IP address reach this count, subsequent attempts
7+from it are rejected until the lock expires (default: 5 retries).
8+.It Ic badlogin_expire
9+The number of seconds a blocked IP address is ignored in login attempts
10+(default: 300 seconds).
11 .El
12 .Pp
13 You must restart the server to make effective these changes.
14@@ -546,6 +552,22 @@ heavily on how all the servers involved behave. Just cross your fingers and hope
15 Full instances can be blocked. This operation must be done from
16 the command-line tool. See
17 .Xr snac 1 .
18+.Pp
19+.Ss Bad login throttling
20+Since version 2.67, a simple logic to avoid brute force attacks against user passwords
21+has been implemented: if, from a given IP address, the number of failed logins reaches
22+a given threshold, further tries from that IP address are never successful until a timer
23+expires. The maximum number of retries can be configured in the
24+.Pa server.json
25+file by setting the
26+.Ic badlogin_retries
27+variable, and the number of seconds the IP address unlock timer expires, in
28+.Ic badlogin_expire .
29+Please take note that, for this system to work, you must setup your web server proxy
30+to pass the remote connection address in the
31+.Ic X-Forwarded-For
32+HTTP header (unless you use the FastCGI interface; if that's the case, you don't have
33+to do anything).
34 .Sh ENVIRONMENT
35 .Bl -tag -width Ds
36 .It Ev DEBUG
37@@ -603,35 +625,42 @@ example.com server section:
38 location /fedi {
39 proxy_pass http://localhost:8001;
40 proxy_set_header Host $http_host;
41+ proxy_set_header X-Forwarded-For $remote_addr;
42 }
43 # webfinger
44 location /.well-known/webfinger {
45 proxy_pass http://localhost:8001;
46 proxy_set_header Host $http_host;
47+ proxy_set_header X-Forwarded-For $remote_addr;
48 }
49 # Mastodon API (entry points)
50 location /api/v1/ {
51 proxy_pass http://localhost:8001;
52 proxy_set_header Host $http_host;
53+ proxy_set_header X-Forwarded-For $remote_addr;
54 }
55 location /api/v2/ {
56 proxy_pass http://localhost:8001;
57 proxy_set_header Host $http_host;
58+ proxy_set_header X-Forwarded-For $remote_addr;
59 }
60 # Mastodon API (OAuth support)
61 location /oauth {
62 proxy_pass http://localhost:8001;
63 proxy_set_header Host $http_host;
64+ proxy_set_header X-Forwarded-For $remote_addr;
65 }
66 # optional
67 location /.well-known/nodeinfo {
68 proxy_pass http://localhost:8001;
69 proxy_set_header Host $http_host;
70+ proxy_set_header X-Forwarded-For $remote_addr;
71 }
72 # optional (needed by some Mastodon API clients)
73 location /.well-known/host-meta {
74 proxy_pass http://localhost:8001;
75 proxy_set_header Host $http_host;
76+ proxy_set_header X-Forwarded-For $remote_addr;
77 }
78 .Ed
79 .Pp
+77,
-0
1@@ -0,0 +1,77 @@
2+# How to build a statically linked Snac with musl
3+
4+Prepare the environment
5+```
6+mkdir build
7+cd build
8+export BUILD_TARGET=$PWD
9+export CC="musl-gcc"
10+```
11+
12+Download and build latest zlib
13+```
14+wget http://zlib.net/current/zlib.tar.gz
15+tar xvf zlib.tar.gz
16+cd zlib-1.3.1/
17+./configure --prefix=$BUILD_TARGET --static
18+make
19+make install
20+cd ..
21+```
22+
23+Download and build latest openssl
24+```
25+wget https://github.com/openssl/openssl/releases/download/openssl-3.4.0/openssl-3.4.0.tar.gz
26+tar xvf openssl-3.4.0.tar.gz
27+cd openssl-3.4.0
28+CC="musl-gcc -fPIE -pie -static -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/" \
29+ ./Configure no-shared no-async --prefix=$BUILD_TARGET --openssldir=$BUILD_TARGET/ssl linux-x86_64
30+make depend
31+make
32+make install
33+cd ..
34+```
35+
36+Download and build latest curl
37+```
38+wget https://curl.se/download/curl-7.88.1.tar.gz
39+tar xvf curl-7.88.1.tar.gz
40+cd curl-7.88.1
41+./configure --disable-shared --enable-static --disable-silent-rules \
42+ --disable-debug --disable-warnings --disable-werror \
43+ --disable-curldebug --disable-symbol-hiding --disable-ares \
44+ --disable-rt --disable-ech --disable-dependency-tracking \
45+ --disable-libtool-lock --enable-http --disable-ftp \
46+ --disable-file --disable-ldap --disable-ldaps \
47+ --disable-rtsp --disable-proxy --disable-dict \
48+ --disable-telnet --disable-tftp --disable-pop3 \
49+ --disable-imap --disable-smb --disable-smtp --disable-gopher \
50+ --disable-mqtt --disable-manual --disable-libcurl-option --disable-ipv6 \
51+ --disable-openssl-auto-load-config --disable-versioned-symbols
52+ --disable-verbose --disable-sspi --disable-crypto-auth \
53+ --disable-ntlm --disable-ntlm-wb --disable-tls-srp \
54+ --disable-unix-sockets --disable-cookies --disable-socketpair \
55+ --disable-http-auth --disable-doh --disable-mime --disable-dateparse \
56+ --disable-netrc --disable-progress-meter --disable-dnsshuffle \
57+ --disable-get-easy-options --disable-alt-svc --disable-websockets \
58+ --without-brotli --without-zstd --without-libpsl --without-libgsasl \
59+ --without-librtmp --without-winidn --disable-threaded-resolver \
60+ --with-openssl=$BUILD_TARGET/ --with-zlib=$BUILD_TARGET/ \
61+ --prefix=$BUILD_TARGET/
62+make
63+make install
64+cd ..
65+```
66+
67+Download and build latest snac2
68+```
69+git clone https://codeberg.org/grunfink/snac2.git # or cd to your existing repo
70+cd snac2
71+make CFLAGS="-g -Wall -Wextra -pedantic -static -DWITHOUT_SHM" \
72+ LDFLAGS="-L$BUILD_TARGET/lib64 -lcurl -lssl -lcrypto -lz" \
73+ PREFIX=$BUILD_TARGET
74+make install PREFIX=$BUILD_TARGET
75+cd ..
76+```
77+
78+Finally a statically linked snac is ready at $BUILD_TARGET/bin
M
format.c
+38,
-14
1@@ -163,14 +163,26 @@ static xs_str *format_line(const char *line, xs_list **attach)
2 const char *mime = xs_mime_by_ext(img_url);
3
4 if (attach != NULL && xs_startswith(mime, "image/")) {
5- xs *d = xs_dict_new();
6-
7- d = xs_dict_append(d, "mediaType", mime);
8- d = xs_dict_append(d, "url", img_url);
9- d = xs_dict_append(d, "name", alt_text);
10- d = xs_dict_append(d, "type", "Image");
11-
12- *attach = xs_list_append(*attach, d);
13+ const xs_dict *ad;
14+ int add = 1;
15+
16+ xs_list_foreach(*attach, ad) {
17+ if (strcmp(xs_dict_get_def(ad, "url", ""), img_url) == 0) {
18+ add = 0;
19+ break;
20+ }
21+ }
22+
23+ if (add) {
24+ xs *d = xs_dict_new();
25+
26+ d = xs_dict_append(d, "mediaType", mime);
27+ d = xs_dict_append(d, "url", img_url);
28+ d = xs_dict_append(d, "name", alt_text);
29+ d = xs_dict_append(d, "type", "Image");
30+
31+ *attach = xs_list_append(*attach, d);
32+ }
33 }
34 else {
35 xs *link = xs_fmt("<a href=\"%s\">%s</a>", img_url, alt_text);
36@@ -191,14 +203,26 @@ static xs_str *format_line(const char *line, xs_list **attach)
37
38 if (attach != NULL && xs_startswith(mime, "image/")) {
39 /* if it's a link to an image, insert it as an attachment */
40- xs *d = xs_dict_new();
41+ const xs_dict *ad;
42+ int add = 1;
43+
44+ xs_list_foreach(*attach, ad) {
45+ if (strcmp(xs_dict_get_def(ad, "url", ""), v2) == 0) {
46+ add = 0;
47+ break;
48+ }
49+ }
50
51- d = xs_dict_append(d, "mediaType", mime);
52- d = xs_dict_append(d, "url", v2);
53- d = xs_dict_append(d, "name", "");
54- d = xs_dict_append(d, "type", "Image");
55+ if (add) {
56+ xs *d = xs_dict_new();
57+
58+ d = xs_dict_append(d, "mediaType", mime);
59+ d = xs_dict_append(d, "url", v2);
60+ d = xs_dict_append(d, "name", "");
61+ d = xs_dict_append(d, "type", "Image");
62
63- *attach = xs_list_append(*attach, d);
64+ *attach = xs_list_append(*attach, d);
65+ }
66 }
67 else {
68 xs *s1 = xs_fmt("<a href=\"%s\" target=\"_blank\">%s</a>", v2, u);
M
html.c
+86,
-4
1@@ -29,9 +29,18 @@ int login(snac *snac, const xs_dict *headers)
2 xs *l1 = xs_split_n(s2, ":", 1);
3
4 if (xs_list_len(l1) == 2) {
5- logged_in = check_password(
6- xs_list_get(l1, 0), xs_list_get(l1, 1),
7- xs_dict_get(snac->config, "passwd"));
8+ const char *user = xs_list_get(l1, 0);
9+ const char *pwd = xs_list_get(l1, 1);
10+ const char *addr = xs_or(xs_dict_get(headers, "remote-addr"),
11+ xs_dict_get(headers, "x-forwarded-for"));
12+
13+ if (badlogin_check(user, addr)) {
14+ logged_in = check_password(user, pwd,
15+ xs_dict_get(snac->config, "passwd"));
16+
17+ if (!logged_in)
18+ badlogin_inc(user, addr);
19+ }
20 }
21 }
22
23@@ -633,6 +642,17 @@ xs_html *html_user_head(snac *user, const char *desc, const char *url)
24 else
25 s_desc = xs_dup(desc);
26
27+ /* show metrics in og:description? */
28+ if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
29+ xs *fwers = follower_list(user);
30+ xs *fwing = following_list(user);
31+
32+ xs *s1 = xs_fmt(L("%d following, %d followers · "),
33+ xs_list_len(fwing), xs_list_len(fwers));
34+
35+ s_desc = xs_str_prepend_i(s_desc, s1);
36+ }
37+
38 /* shorten desc to a reasonable size */
39 for (n = 0; s_desc[n]; n++) {
40 if (n > 512 && (s_desc[n] == ' ' || s_desc[n] == '\n'))
41@@ -2041,6 +2061,23 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
42 if (content && xs_str_in(content, o_href) != -1)
43 continue;
44
45+ /* do this attachment include an icon? */
46+ const xs_dict *icon = xs_dict_get(a, "icon");
47+ if (xs_type(icon) == XSTYPE_DICT) {
48+ const char *icon_mtype = xs_dict_get(icon, "mediaType");
49+ const char *icon_url = xs_dict_get(icon, "url");
50+
51+ if (icon_mtype && icon_url && xs_startswith(icon_mtype, "image/")) {
52+ xs_html_add(content_attachments,
53+ xs_html_tag("a",
54+ xs_html_attr("href", icon_url),
55+ xs_html_attr("target", "_blank"),
56+ xs_html_sctag("img",
57+ xs_html_attr("loading", "lazy"),
58+ xs_html_attr("src", icon_url))));
59+ }
60+ }
61+
62 xs *href = make_url(o_href, proxy, 0);
63
64 if (xs_startswith(type, "image/") || strcmp(type, "Image") == 0) {
65@@ -2996,9 +3033,54 @@ int html_get_handler(const xs_dict *req, const char *q_path,
66 }
67 else {
68 const char *q = xs_dict_get(q_vars, "q");
69+ xs *url_acct = NULL;
70+
71+ /* searching for an URL? */
72+ if (q && xs_match(q, "https://*|http://*")) {
73+ /* may by an actor; try a webfinger */
74+ xs *actor_obj = NULL;
75+
76+ if (valid_status(webfinger_request(q, &actor_obj, &url_acct))) {
77+ /* it's an actor; do the dirty trick of changing q to the account name */
78+ q = url_acct;
79+ }
80+ else {
81+ /* if it's not already here, try to bring it to the user's timeline */
82+ xs *md5 = xs_md5_hex(q, strlen(q));
83+
84+ if (!timeline_here(&snac, md5)) {
85+ xs *object = NULL;
86+ int status;
87+
88+ status = activitypub_request(&snac, q, &object);
89+ snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status));
90+
91+ if (valid_status(status)) {
92+ /* got it; also request the actor */
93+ const char *attr_to = get_atto(object);
94+
95+ if (!xs_is_null(attr_to)) {
96+ status = actor_request(&snac, attr_to, &actor_obj);
97+
98+ snac_debug(&snac, 1, xs_fmt("Request author %s of %s %d", attr_to, q, status));
99+
100+ if (valid_status(status)) {
101+ /* add the actor */
102+ actor_add(attr_to, actor_obj);
103+
104+ /* add the post to the timeline */
105+ timeline_add(&snac, q, object);
106+ }
107+ }
108+ }
109+ }
110+ }
111+
112+ /* fall through */
113+ }
114
115 if (q && *q) {
116- if (xs_regex_match(q, "^@?[a-zA-Z0-9_]+@[a-zA-Z0-9-]+\\.")) {
117+ if (xs_regex_match(q, "^@?[a-zA-Z0-9._]+@[a-zA-Z0-9-]+\\.")) {
118 /** search account **/
119 xs *actor = NULL;
120 xs *acct = NULL;
M
httpd.c
+24,
-12
1@@ -279,6 +279,7 @@ void httpd_connection(FILE *f)
2 xs *payload = NULL;
3 xs *etag = NULL;
4 xs *last_modified = NULL;
5+ xs *link = NULL;
6 int p_size = 0;
7 const char *p;
8 int fcgi_id;
9@@ -326,7 +327,7 @@ void httpd_connection(FILE *f)
10 status = oauth_get_handler(req, q_path, &body, &b_size, &ctype);
11
12 if (status == 0)
13- status = mastoapi_get_handler(req, q_path, &body, &b_size, &ctype);
14+ status = mastoapi_get_handler(req, q_path, &body, &b_size, &ctype, &link);
15 #endif /* NO_MASTODON_API */
16
17 if (status == 0)
18@@ -426,6 +427,8 @@ void httpd_connection(FILE *f)
19 headers = xs_dict_append(headers, "etag", etag);
20 if (!xs_is_null(last_modified))
21 headers = xs_dict_append(headers, "last-modified", last_modified);
22+ if (!xs_is_null(link))
23+ headers = xs_dict_append(headers, "Link", link);
24
25 /* if there are any additional headers, add them */
26 const xs_dict *more_headers = xs_dict_get(srv_config, "http_headers");
27@@ -775,6 +778,26 @@ void httpd(void)
28 xs *shm_name = NULL;
29 sem_t anon_job_sem;
30 xs *pidfile = xs_fmt("%s/server.pid", srv_basedir);
31+ int pidfd;
32+
33+ {
34+ /* do some pidfile locking acrobatics */
35+ if ((pidfd = open(pidfile, O_RDWR | O_CREAT, 0660)) == -1) {
36+ srv_log(xs_fmt("Cannot create pidfile %s -- cannot continue", pidfile));
37+ return;
38+ }
39+
40+ if (lockf(pidfd, F_TLOCK, 1) == -1) {
41+ srv_log(xs_fmt("Cannot lock pidfile %s -- server already running?", pidfile));
42+ close(pidfd);
43+ return;
44+ }
45+
46+ ftruncate(pidfd, 0);
47+
48+ xs *s = xs_fmt("%d\n", (int)getpid());
49+ write(pidfd, s, strlen(s));
50+ }
51
52 address = xs_dict_get(srv_config, "address");
53
54@@ -810,17 +833,6 @@ void httpd(void)
55 srv_log(xs_fmt("httpd%s start %s %s", p_state->use_fcgi ? " (FastCGI)" : "",
56 full_address, USER_AGENT));
57
58- {
59- FILE *f;
60-
61- if ((f = fopen(pidfile, "w")) != NULL) {
62- fprintf(f, "%d\n", getpid());
63- fclose(f);
64- }
65- else
66- srv_log(xs_fmt("Cannot create %s: %s", pidfile, strerror(errno)));
67- }
68-
69 /* show the number of usable file descriptors */
70 struct rlimit r;
71 getrlimit(RLIMIT_NOFILE, &r);
+192,
-67
1@@ -293,47 +293,54 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
2 snac snac;
3
4 if (user_open(&snac, login)) {
5- /* check the login + password */
6- if (check_password(login, passwd, xs_dict_get(snac.config, "passwd"))) {
7- /* success! redirect to the desired uri */
8- xs *code = random_str();
9+ const char *addr = xs_or(xs_dict_get(req, "remote-addr"),
10+ xs_dict_get(req, "x-forwarded-for"));
11
12- xs_free(*body);
13+ if (badlogin_check(login, addr)) {
14+ /* check the login + password */
15+ if (check_password(login, passwd, xs_dict_get(snac.config, "passwd"))) {
16+ /* success! redirect to the desired uri */
17+ xs *code = random_str();
18
19- if (strcmp(redir, "urn:ietf:wg:oauth:2.0:oob") == 0) {
20- *body = xs_dup(code);
21- }
22- else {
23- if (xs_str_in(redir, "?") != -1)
24- *body = xs_fmt("%s&code=%s", redir, code);
25- else
26- *body = xs_fmt("%s?code=%s", redir, code);
27+ xs_free(*body);
28
29- status = HTTP_STATUS_SEE_OTHER;
30- }
31+ if (strcmp(redir, "urn:ietf:wg:oauth:2.0:oob") == 0) {
32+ *body = xs_dup(code);
33+ }
34+ else {
35+ if (xs_str_in(redir, "?") != -1)
36+ *body = xs_fmt("%s&code=%s", redir, code);
37+ else
38+ *body = xs_fmt("%s?code=%s", redir, code);
39
40- /* if there is a state, add it */
41- if (!xs_is_null(state) && *state) {
42- *body = xs_str_cat(*body, "&state=");
43- *body = xs_str_cat(*body, state);
44- }
45+ status = HTTP_STATUS_SEE_OTHER;
46+ }
47
48- srv_log(xs_fmt("oauth x-snac-login: '%s' success, redirect to %s",
49+ /* if there is a state, add it */
50+ if (!xs_is_null(state) && *state) {
51+ *body = xs_str_cat(*body, "&state=");
52+ *body = xs_str_cat(*body, state);
53+ }
54+
55+ srv_log(xs_fmt("oauth x-snac-login: '%s' success, redirect to %s",
56 login, *body));
57
58- /* assign the login to the app */
59- xs *app = app_get(cid);
60+ /* assign the login to the app */
61+ xs *app = app_get(cid);
62
63- if (app != NULL) {
64- app = xs_dict_set(app, "uid", login);
65- app = xs_dict_set(app, "code", code);
66- app_add(cid, app);
67+ if (app != NULL) {
68+ app = xs_dict_set(app, "uid", login);
69+ app = xs_dict_set(app, "code", code);
70+ app_add(cid, app);
71+ }
72+ else
73+ srv_log(xs_fmt("oauth x-snac-login: error getting app %s", cid));
74+ }
75+ else {
76+ srv_debug(1, xs_fmt("oauth x-snac-login: login '%s' incorrect", login));
77+ badlogin_inc(login, addr);
78 }
79- else
80- srv_log(xs_fmt("oauth x-snac-login: error getting app %s", cid));
81 }
82- else
83- srv_debug(1, xs_fmt("oauth x-snac-login: login '%s' incorrect", login));
84
85 user_free(&snac);
86 }
87@@ -474,29 +481,36 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
88 snac user;
89
90 if (user_open(&user, login)) {
91- /* check the login + password */
92- if (check_password(login, passwd, xs_dict_get(user.config, "passwd"))) {
93- /* success! create a new token */
94- xs *tokid = random_str();
95+ const char *addr = xs_or(xs_dict_get(req, "remote-addr"),
96+ xs_dict_get(req, "x-forwarded-for"));
97
98- srv_debug(1, xs_fmt("x-snac-new-token: "
99+ if (badlogin_check(login, addr)) {
100+ /* check the login + password */
101+ if (check_password(login, passwd, xs_dict_get(user.config, "passwd"))) {
102+ /* success! create a new token */
103+ xs *tokid = random_str();
104+
105+ srv_debug(1, xs_fmt("x-snac-new-token: "
106 "successful login for %s, new token %s", login, tokid));
107
108- xs *token = xs_dict_new();
109- token = xs_dict_append(token, "token", tokid);
110- token = xs_dict_append(token, "client_id", "snac-client");
111- token = xs_dict_append(token, "client_secret", "");
112- token = xs_dict_append(token, "uid", login);
113- token = xs_dict_append(token, "code", "");
114+ xs *token = xs_dict_new();
115+ token = xs_dict_append(token, "token", tokid);
116+ token = xs_dict_append(token, "client_id", "snac-client");
117+ token = xs_dict_append(token, "client_secret", "");
118+ token = xs_dict_append(token, "uid", login);
119+ token = xs_dict_append(token, "code", "");
120
121- token_add(tokid, token);
122+ token_add(tokid, token);
123
124- *ctype = "text/plain";
125- xs_free(*body);
126- *body = xs_dup(tokid);
127- }
128+ *ctype = "text/plain";
129+ xs_free(*body);
130+ *body = xs_dup(tokid);
131+ }
132+ else
133+ badlogin_inc(login, addr);
134
135- user_free(&user);
136+ user_free(&user);
137+ }
138 }
139 }
140 }
141@@ -898,7 +912,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
142 const char *o_href = xs_dict_get(v, "href");
143 const char *name = xs_dict_get(v, "name");
144
145- if (xs_match(type, "image/*|video/*|Image|Video")) { /* */
146+ if (xs_match(type, "image/*|video/*|audio/*|Image|Video")) { /* */
147 xs *matteid = xs_fmt("%s_%d", id, xs_list_len(matt));
148 xs *href = make_url(o_href, proxy, 1);
149
150@@ -910,7 +924,8 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
151 d = xs_dict_append(d, "remote_url", href);
152 d = xs_dict_append(d, "description", name);
153
154- d = xs_dict_append(d, "type", (*type == 'v' || *type == 'V') ? "video" : "image");
155+ d = xs_dict_append(d, "type", (*type == 'v' || *type == 'V') ? "video" :
156+ (*type == 'a' || *type == 'A') ? "audio" : "image");
157
158 matt = xs_list_append(matt, d);
159 }
160@@ -990,7 +1005,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
161 const char *o_url = xs_dict_get(icon, "url");
162
163 if (!xs_is_null(o_url)) {
164- xs *url = make_url(o_url, snac->actor, 1);
165+ xs *url = make_url(o_url, snac ? snac->actor : NULL, 1);
166 xs *nm = xs_strip_chars_i(xs_dup(name), ":");
167
168 d1 = xs_dict_append(d1, "shortcode", nm);
169@@ -1193,10 +1208,13 @@ int process_auth_token(snac *snac, const xs_dict *req)
170 return logged_in;
171 }
172
173+
174 void credentials_get(char **body, char **ctype, int *status, snac snac)
175 {
176 xs *acct = xs_dict_new();
177
178+ const xs_val *bot = xs_dict_get(snac.config, "bot");
179+
180 acct = xs_dict_append(acct, "id", snac.md5);
181 acct = xs_dict_append(acct, "username", xs_dict_get(snac.config, "uid"));
182 acct = xs_dict_append(acct, "acct", xs_dict_get(snac.config, "uid"));
183@@ -1206,7 +1224,7 @@ void credentials_get(char **body, char **ctype, int *status, snac snac)
184 acct = xs_dict_append(acct, "note", xs_dict_get(snac.config, "bio"));
185 acct = xs_dict_append(acct, "url", snac.actor);
186 acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
187- acct = xs_dict_append(acct, "bot", xs_dict_get(snac.config, "bot"));
188+ acct = xs_dict_append(acct, "bot", xs_stock(xs_is_true(bot) ? XSTYPE_TRUE : XSTYPE_FALSE));
189 acct = xs_dict_append(acct, "emojis", xs_stock(XSTYPE_LIST));
190
191 xs *src = xs_json_loads("{\"privacy\":\"public\", \"language\":\"en\","
192@@ -1220,7 +1238,7 @@ void credentials_get(char **body, char **ctype, int *status, snac snac)
193 src = xs_dict_set(src, "sensitive",
194 strcmp(cw, "open") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
195
196- src = xs_dict_set(src, "bot", xs_dict_get(snac.config, "bot"));
197+ src = xs_dict_set(src, "bot", xs_stock(xs_is_true(bot) ? XSTYPE_TRUE : XSTYPE_FALSE));
198
199 xs *avatar = NULL;
200 const char *av = xs_dict_get(snac.config, "avatar");
201@@ -1319,7 +1337,7 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
202
203 const char *max_id = xs_dict_get(args, "max_id");
204 const char *since_id = xs_dict_get(args, "since_id");
205- const char *min_id = xs_dict_get(args, "min_id");
206+ const char *min_id = xs_dict_get(args, "min_id"); /* unsupported old-to-new navigation */
207 const char *limit_s = xs_dict_get(args, "limit");
208 int limit = 0;
209 int cnt = 0;
210@@ -1330,7 +1348,7 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
211 if (limit == 0)
212 limit = 20;
213
214- if (index_desc_first(f, md5, 0)) {
215+ if (min_id == NULL && index_desc_first(f, md5, 0)) {
216 do {
217 xs *msg = NULL;
218
219@@ -1348,13 +1366,6 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
220 break;
221 }
222
223- /* only returns entries newer than min_id */
224- /* what does really "Return results immediately newer than ID" mean? */
225- if (min_id) {
226- if (strcmp(md5, MID_TO_MD5(min_id)) == 0)
227- break;
228- }
229-
230 /* get the entry */
231 if (user) {
232 if (!valid_status(timeline_get_by_md5(user, md5, &msg)))
233@@ -1438,8 +1449,35 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
234 }
235
236
237+xs_str *timeline_link_header(const char *endpoint, xs_list *timeline)
238+/* returns a Link header with paging information */
239+{
240+ xs_str *s = NULL;
241+
242+ if (xs_list_len(timeline) == 0)
243+ return NULL;
244+
245+ const xs_dict *first_st = xs_list_get(timeline, 0);
246+ const xs_dict *last_st = xs_list_get(timeline, -1);
247+ const char *first_id = xs_dict_get(first_st, "id");
248+ const char *last_id = xs_dict_get(last_st, "id");
249+ const char *host = xs_dict_get(srv_config, "host");
250+ const char *protocol = xs_dict_get_def(srv_config, "protocol", "https");
251+
252+ s = xs_fmt(
253+ "<%s:/" "/%s%s?max_id=%s>; rel=\"next\", "
254+ "<%s:/" "/%s%s?since_id=%s>; rel=\"prev\"",
255+ protocol, host, endpoint, last_id,
256+ protocol, host, endpoint, first_id);
257+
258+ srv_debug(1, xs_fmt("timeline_link_header %s", s));
259+
260+ return s;
261+}
262+
263+
264 int mastoapi_get_handler(const xs_dict *req, const char *q_path,
265- char **body, int *b_size, char **ctype)
266+ char **body, int *b_size, char **ctype, xs_str **link)
267 {
268 (void)b_size;
269
270@@ -1699,6 +1737,8 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
271 xs *ifn = user_index_fn(&snac1, "private");
272 xs *out = mastoapi_timeline(&snac1, args, ifn);
273
274+ *link = timeline_link_header("/api/v1/timelines/home", out);
275+
276 *body = xs_json_dumps(out, 4);
277 *ctype = "application/json";
278 status = HTTP_STATUS_OK;
279@@ -1763,8 +1803,14 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
280 xs *out = xs_list_new();
281 const xs_dict *v;
282 const xs_list *excl = xs_dict_get(args, "exclude_types[]");
283+ const char *min_id = xs_dict_get(args, "min_id");
284 const char *max_id = xs_dict_get(args, "max_id");
285
286+ if (dbglevel) {
287+ xs *js = xs_json_dumps(args, 0);
288+ srv_debug(1, xs_fmt("mastoapi_notifications args %s", js));
289+ }
290+
291 xs_list_foreach(l, v) {
292 xs *noti = notify_get(&snac1, v);
293
294@@ -1795,6 +1841,12 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
295 continue;
296 }
297
298+ if (min_id) {
299+ if (strcmp(fid, min_id) <= 0) {
300+ continue;
301+ }
302+ }
303+
304 /* convert the type */
305 if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0)
306 type = "favourite";
307@@ -1842,6 +1894,8 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
308 out = xs_list_append(out, mn);
309 }
310
311+ srv_debug(1, xs_fmt("mastoapi_notifications count %d", xs_list_len(out)));
312+
313 *body = xs_json_dumps(out, 4);
314 *ctype = "application/json";
315 status = HTTP_STATUS_OK;
316@@ -2273,9 +2327,22 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
317 }
318 else
319 if (strcmp(cmd, "/v1/markers") == 0) { /** **/
320- *body = xs_dup("{}");
321- *ctype = "application/json";
322- status = HTTP_STATUS_OK;
323+ if (logged_in) {
324+ const xs_list *timeline = xs_dict_get(args, "timeline[]");
325+ xs_str *json = NULL;
326+ if (!xs_is_null(timeline))
327+ json = xs_json_dumps(markers_get(&snac1, timeline), 4);
328+
329+ if (!xs_is_null(json))
330+ *body = json;
331+ else
332+ *body = xs_dup("{}");
333+
334+ *ctype = "application/json";
335+ status = HTTP_STATUS_OK;
336+ }
337+ else
338+ status = HTTP_STATUS_UNAUTHORIZED;
339 }
340 else
341 if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/
342@@ -2310,6 +2377,37 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
343 if (xs_is_null(offset) || strcmp(offset, "0") == 0) {
344 /* reply something only for offset 0; otherwise,
345 apps like Tusky keep asking again and again */
346+ if (xs_startswith(q, "https://")) {
347+ xs *md5 = xs_md5_hex(q, strlen(q));
348+
349+ if (!timeline_here(&snac1, md5)) {
350+ xs *object = NULL;
351+ int status;
352+
353+ status = activitypub_request(&snac1, q, &object);
354+ snac_debug(&snac1, 1, xs_fmt("Request searched URL %s %d", q, status));
355+
356+ if (valid_status(status)) {
357+ /* got it; also request the actor */
358+ const char *attr_to = get_atto(object);
359+ xs *actor_obj = NULL;
360+
361+ if (!xs_is_null(attr_to)) {
362+ status = actor_request(&snac1, attr_to, &actor_obj);
363+
364+ snac_debug(&snac1, 1, xs_fmt("Request author %s of %s %d", attr_to, q, status));
365+
366+ if (valid_status(status)) {
367+ /* add the actor */
368+ actor_add(attr_to, actor_obj);
369+
370+ /* add the post to the timeline */
371+ timeline_add(&snac1, q, object);
372+ }
373+ }
374+ }
375+ }
376+ }
377
378 if (!xs_is_null(q)) {
379 if (xs_is_null(type) || strcmp(type, "accounts") == 0) {
380@@ -2945,6 +3043,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
381 status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
382 }
383 }
384+ else
385 if (xs_startswith(cmd, "/v1/lists/")) { /** list maintenance **/
386 if (logged_in) {
387 xs *l = xs_split(cmd, "/");
388@@ -2972,9 +3071,35 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
389 }
390 }
391 }
392+ }
393+ else if (strcmp(cmd, "/v1/markers") == 0) { /** **/
394+ xs_str *json = NULL;
395+ if (logged_in) {
396+ const xs_str *home_marker = xs_dict_get(args, "home[last_read_id]");
397+ if (xs_is_null(home_marker)) {
398+ const xs_dict *home = xs_dict_get(args, "home");
399+ if (!xs_is_null(home))
400+ home_marker = xs_dict_get(home, "last_read_id");
401+ }
402+
403+ const xs_str *notify_marker = xs_dict_get(args, "notifications[last_read_id]");
404+ if (xs_is_null(notify_marker)) {
405+ const xs_dict *notify = xs_dict_get(args, "notifications");
406+ if (!xs_is_null(notify))
407+ notify_marker = xs_dict_get(notify, "last_read_id");
408+ }
409+ json = xs_json_dumps(markers_set(&snac, home_marker, notify_marker), 4);
410+ }
411+ if (!xs_is_null(json))
412+ *body = json;
413 else
414- status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
415+ *body = xs_dup("{}");
416+
417+ *ctype = "application/json";
418+ status = HTTP_STATUS_OK;
419 }
420+ else
421+ status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
422
423 /* user cleanup */
424 if (logged_in)
M
snac.h
+8,
-2
1@@ -1,7 +1,7 @@
2 /* snac - A simple, minimalistic ActivityPub instance */
3 /* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
4
5-#define VERSION "2.66"
6+#define VERSION "2.67"
7
8 #define USER_AGENT "snac/" VERSION
9
10@@ -238,6 +238,9 @@ int notify_new_num(snac *snac);
11 xs_list *notify_list(snac *snac, int skip, int show);
12 void notify_clear(snac *snac);
13
14+xs_dict *markers_get(snac *snac, const xs_list *markers);
15+xs_dict *markers_set(snac *snac, const char *home_marker, const char *notify_marker);
16+
17 void inbox_add(const char *inbox);
18 void inbox_add_by_actor(const xs_dict *actor);
19 xs_list *inbox_list(void);
20@@ -386,7 +389,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
21 const char *payload, int p_size,
22 char **body, int *b_size, char **ctype);
23 int mastoapi_get_handler(const xs_dict *req, const char *q_path,
24- char **body, int *b_size, char **ctype);
25+ char **body, int *b_size, char **ctype, xs_str **link);
26 int mastoapi_post_handler(const xs_dict *req, const char *q_path,
27 const char *payload, int p_size,
28 char **body, int *b_size, char **ctype);
29@@ -427,3 +430,6 @@ typedef struct {
30 t_announcement *announcement(double after);
31
32 xs_str *make_url(const char *href, const char *proxy, int by_token);
33+
34+int badlogin_check(const char *user, const char *addr);
35+void badlogin_inc(const char *user, const char *addr);
+5,
-1
1@@ -82,7 +82,11 @@ int webfinger_request_signed(snac *snac, const char *qs, xs_str **actor, xs_str
2
3 if (obj == NULL && valid_status(status) && payload) {
4 obj = xs_json_loads(payload);
5- object_add(cached_qs, obj);
6+
7+ if (obj)
8+ object_add(cached_qs, obj);
9+ else
10+ status = HTTP_STATUS_BAD_REQUEST;
11 }
12
13 if (obj) {
+1,
-1
1@@ -190,7 +190,7 @@ xs_dict *xs_fcgi_request(FILE *f, xs_str **payload, int *p_size, int *fcgi_id)
2 q_vars = xs_url_vars(xs_list_get(pnv, 1));
3 }
4 else
5- if (xs_match(k, "CONTENT_TYPE|CONTENT_LENGTH|HTTP_*")) {
6+ if (xs_match(k, "CONTENT_TYPE|CONTENT_LENGTH|REMOTE_ADDR|HTTP_*")) {
7 if (xs_startswith(k, "HTTP_"))
8 k = xs_crop_i(k, 5, 0);
9