summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar shtrophic2024-12-23 13:42:45 +0100
committerGravatar shtrophic2024-12-23 13:42:45 +0100
commita7ca4007f2a55a8becab1e4595d2696dd6e7bfd1 (patch)
tree3128196bd7eb298be5a37edac5922009ec5fcac1
parentMerge tag '2.66' (diff)
parentVersion 2.67 RELEASED. (diff)
downloadpenes-snac2-a7ca4007f2a55a8becab1e4595d2696dd6e7bfd1.tar.gz
penes-snac2-a7ca4007f2a55a8becab1e4595d2696dd6e7bfd1.tar.xz
penes-snac2-a7ca4007f2a55a8becab1e4595d2696dd6e7bfd1.zip
Merge tag '2.67'
Version 2.67 RELEASED.
-rw-r--r--Makefile8
-rw-r--r--RELEASE_NOTES.md14
-rw-r--r--activitypub.c33
-rw-r--r--data.c190
-rw-r--r--doc/snac.829
-rw-r--r--examples/static-linking-with-musl.md77
-rw-r--r--format.c52
-rw-r--r--html.c90
-rw-r--r--httpd.c36
-rw-r--r--mastoapi.c259
-rw-r--r--snac.h10
-rw-r--r--webfinger.c6
-rw-r--r--xs_fcgi.h2
13 files changed, 692 insertions, 114 deletions
diff --git a/Makefile b/Makefile
index 2b89cfa..2977cd4 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
1PREFIX=/usr/local 1PREFIX?=/usr/local
2PREFIX_MAN=$(PREFIX)/man 2PREFIX_MAN=$(PREFIX)/man
3CFLAGS?=-g -Wall -Wextra -pedantic 3CFLAGS?=-g -Wall -Wextra -pedantic
4 4
@@ -6,16 +6,16 @@ all: snac
6 6
7snac: snac.o main.o sandbox.o data.o http.o httpd.o webfinger.o \ 7snac: snac.o main.o sandbox.o data.o http.o httpd.o webfinger.o \
8 activitypub.o html.o utils.o format.o upgrade.o mastoapi.o 8 activitypub.o html.o utils.o format.o upgrade.o mastoapi.o
9 $(CC) $(CFLAGS) -L/usr/local/lib *.o -lcurl -lcrypto $(LDFLAGS) -pthread -o $@ 9 $(CC) $(CFLAGS) -L$(PREFIX)/lib *.o -lcurl -lcrypto $(LDFLAGS) -pthread -o $@
10 10
11.c.o: 11.c.o:
12 $(CC) $(CFLAGS) $(CPPFLAGS) -I/usr/local/include -c $< 12 $(CC) $(CFLAGS) $(CPPFLAGS) -I$(PREFIX)/include -c $<
13 13
14clean: 14clean:
15 rm -rf *.o *.core snac makefile.depend 15 rm -rf *.o *.core snac makefile.depend
16 16
17dep: 17dep:
18 $(CC) -I/usr/local/include -MM *.c > makefile.depend 18 $(CC) -I$(PREFIX)/include -MM *.c > makefile.depend
19 19
20install: 20install:
21 mkdir -p -m 755 $(PREFIX)/bin 21 mkdir -p -m 755 $(PREFIX)/bin
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 4630352..073985b 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,19 @@
1# Release Notes 1# Release Notes
2 2
3## 2.67
4
5The search box also accepts post URLs; the post is requested and, if it's found, can be interacted with (liked, boosted, replied to, etc.).
6
7IP addresses for failed logins are tracked and throttled to mitigate brute force attacks (see `snac(8)` for more information).
8
9Fixed a bug regarding repeated attachments when editing a post.
10
11Mastodon 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).
12
13When running in server mode, the pidfile is locked to avoid concurrent running of the same server instance.
14
15Added documentation and some tweaks to enable static compilation with musl (contributed by Shamar).
16
3## 2.66 17## 2.66
4 18
5As 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. 19As 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.
diff --git a/activitypub.c b/activitypub.c
index 773df78..34cc32f 100644
--- a/activitypub.c
+++ b/activitypub.c
@@ -258,6 +258,10 @@ xs_list *get_attachments(const xs_dict *msg)
258 d = xs_dict_append(d, "href", href); 258 d = xs_dict_append(d, "href", href);
259 d = xs_dict_append(d, "name", name); 259 d = xs_dict_append(d, "name", name);
260 260
261 const xs_dict *icon = xs_dict_get(v, "icon");
262 if (xs_type(icon) == XSTYPE_DICT)
263 d = xs_dict_append(d, "icon", icon);
264
261 l = xs_list_append(l, d); 265 l = xs_list_append(l, d);
262 } 266 }
263 } 267 }
@@ -1476,20 +1480,31 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
1476 1480
1477 /* create the attachment list, if there are any */ 1481 /* create the attachment list, if there are any */
1478 if (!xs_is_null(attach)) { 1482 if (!xs_is_null(attach)) {
1479 int c = 0; 1483 xs_list_foreach(attach, v) {
1480 while (xs_list_next(attach, &v, &c)) {
1481 xs *d = xs_dict_new();
1482 const char *url = xs_list_get(v, 0); 1484 const char *url = xs_list_get(v, 0);
1483 const char *alt = xs_list_get(v, 1); 1485 const char *alt = xs_list_get(v, 1);
1484 const char *mime = xs_mime_by_ext(url); 1486 const char *mime = xs_mime_by_ext(url);
1487 int add = 1;
1488
1489 /* check if it's already here */
1490 const xs_dict *ad;
1491 xs_list_foreach(atls, ad) {
1492 if (strcmp(xs_dict_get_def(ad, "url", ""), url) == 0) {
1493 add = 0;
1494 break;
1495 }
1496 }
1485 1497
1486 d = xs_dict_append(d, "mediaType", mime); 1498 if (add) {
1487 d = xs_dict_append(d, "url", url); 1499 xs *d = xs_dict_new();
1488 d = xs_dict_append(d, "name", alt); 1500 d = xs_dict_append(d, "mediaType", mime);
1489 d = xs_dict_append(d, "type", 1501 d = xs_dict_append(d, "url", url);
1490 xs_startswith(mime, "image/") ? "Image" : "Document"); 1502 d = xs_dict_append(d, "name", alt);
1503 d = xs_dict_append(d, "type",
1504 xs_startswith(mime, "image/") ? "Image" : "Document");
1491 1505
1492 atls = xs_list_append(atls, d); 1506 atls = xs_list_append(atls, d);
1507 }
1493 } 1508 }
1494 } 1509 }
1495 1510
diff --git a/data.c b/data.c
index 8a3fe2d..eb4c9d5 100644
--- a/data.c
+++ b/data.c
@@ -2705,6 +2705,23 @@ xs_list *content_search(snac *user, const char *regex,
2705 if (id == NULL || is_hidden(user, id)) 2705 if (id == NULL || is_hidden(user, id))
2706 continue; 2706 continue;
2707 2707
2708 /* test for the post URL */
2709 if (strcmp(id, regex) == 0) {
2710 if (xs_set_add(&seen, md5) == 1)
2711 show--;
2712
2713 continue;
2714 }
2715
2716 /* test for the alternate post id */
2717 const char *url = xs_dict_get(post, "url");
2718 if (xs_type(url) == XSTYPE_STRING && strcmp(url, regex) == 0) {
2719 if (xs_set_add(&seen, md5) == 1)
2720 show--;
2721
2722 continue;
2723 }
2724
2708 xs *c = xs_str_new(NULL); 2725 xs *c = xs_str_new(NULL);
2709 const char *content = xs_dict_get(post, "content"); 2726 const char *content = xs_dict_get(post, "content");
2710 const char *name = xs_dict_get(post, "name"); 2727 const char *name = xs_dict_get(post, "name");
@@ -2786,6 +2803,74 @@ xs_str *notify_check_time(snac *snac, int reset)
2786 return t; 2803 return t;
2787} 2804}
2788 2805
2806xs_dict *markers_get(snac *snac, const xs_list *markers)
2807{
2808 xs *data = NULL;
2809 xs_dict *returns = xs_dict_new();
2810 xs *fn = xs_fmt("%s/markers.json", snac->basedir);
2811 const xs_str *v = NULL;
2812 FILE *f;
2813
2814 if ((f = fopen(fn, "r")) != NULL) {
2815 data = xs_json_load(f);
2816 fclose(f);
2817 }
2818
2819 if (xs_is_null(data))
2820 data = xs_dict_new();
2821
2822 xs_list_foreach(markers, v) {
2823 const xs_dict *mark = xs_dict_get(data, v);
2824 if (!xs_is_null(mark)) {
2825 returns = xs_dict_append(returns, v, mark);
2826 }
2827 }
2828 return returns;
2829}
2830
2831xs_dict *markers_set(snac *snac, const char *home_marker, const char *notify_marker)
2832/* gets or sets notification marker */
2833{
2834 xs *data = NULL;
2835 xs_dict *written = xs_dict_new();
2836 xs *fn = xs_fmt("%s/markers.json", snac->basedir);
2837 FILE *f;
2838
2839 if ((f = fopen(fn, "r")) != NULL) {
2840 data = xs_json_load(f);
2841 fclose(f);
2842 }
2843
2844 if (xs_is_null(data))
2845 data = xs_dict_new();
2846
2847 if (!xs_is_null(home_marker)) {
2848 xs *home = xs_dict_new();
2849 xs *s_tid = tid(0);
2850 home = xs_dict_append(home, "last_read_id", home_marker);
2851 home = xs_dict_append(home, "version", xs_stock(0));
2852 home = xs_dict_append(home, "updated_at", s_tid);
2853 data = xs_dict_set(data, "home", home);
2854 written = xs_dict_append(written, "home", home);
2855 }
2856
2857 if (!xs_is_null(notify_marker)) {
2858 xs *notify = xs_dict_new();
2859 xs *s_tid = tid(0);
2860 notify = xs_dict_append(notify, "last_read_id", notify_marker);
2861 notify = xs_dict_append(notify, "version", xs_stock(0));
2862 notify = xs_dict_append(notify, "updated_at", s_tid);
2863 data = xs_dict_set(data, "notifications", notify);
2864 written = xs_dict_append(written, "notifications", notify);
2865 }
2866
2867 if ((f = fopen(fn, "w")) != NULL) {
2868 xs_json_dump(data, 4, f);
2869 fclose(f);
2870 }
2871
2872 return written;
2873}
2789 2874
2790void notify_add(snac *snac, const char *type, const char *utype, 2875void notify_add(snac *snac, const char *type, const char *utype,
2791 const char *actor, const char *objid, const xs_dict *msg) 2876 const char *actor, const char *objid, const xs_dict *msg)
@@ -3767,3 +3852,108 @@ xs_str *make_url(const char *href, const char *proxy, int by_token)
3767 3852
3768 return url; 3853 return url;
3769} 3854}
3855
3856
3857/** bad login throttle **/
3858
3859xs_str *_badlogin_fn(const char *addr)
3860{
3861 xs *md5 = xs_md5_hex(addr, strlen(addr));
3862 xs *dir = xs_fmt("%s/badlogin", srv_basedir);
3863
3864 mkdirx(dir);
3865
3866 return xs_fmt("%s/%s", dir, md5);
3867}
3868
3869
3870int _badlogin_read(const char *fn, int *failures)
3871/* reads a badlogin file */
3872{
3873 int ok = 0;
3874 FILE *f;
3875
3876 pthread_mutex_lock(&data_mutex);
3877
3878 if ((f = fopen(fn, "r")) != NULL) {
3879 xs *l = xs_readline(f);
3880 fclose(f);
3881
3882 if (sscanf(l, "%d", failures) == 1)
3883 ok = 1;
3884 }
3885
3886 pthread_mutex_unlock(&data_mutex);
3887
3888 return ok;
3889}
3890
3891
3892int badlogin_check(const char *user, const char *addr)
3893/* checks if this address is authorized to try a login */
3894{
3895 int valid = 1;
3896
3897 if (xs_type(addr) == XSTYPE_STRING) {
3898 xs *fn = _badlogin_fn(addr);
3899 double mt = mtime(fn);
3900
3901 if (mt > 0) {
3902 int badlogin_expire = xs_number_get(xs_dict_get_def(srv_config,
3903 "badlogin_expire", "300"));
3904
3905 mt += badlogin_expire;
3906
3907 /* if file is expired, delete and give pass */
3908 if (mt < time(NULL)) {
3909 srv_debug(1, xs_fmt("Login from %s for %s allowed again", addr, user));
3910 unlink(fn);
3911 }
3912 else {
3913 int failures;
3914
3915 if (_badlogin_read(fn, &failures)) {
3916 int badlogin_max = xs_number_get(xs_dict_get_def(srv_config,
3917 "badlogin_retries", "5"));
3918
3919 if (failures >= badlogin_max) {
3920 valid = 0;
3921
3922 xs *d = xs_str_iso_date((time_t) mt);
3923
3924 srv_debug(1,
3925 xs_fmt("Login from %s for %s forbidden until %s", addr, user, d));
3926 }
3927 }
3928 }
3929 }
3930 }
3931
3932 return valid;
3933}
3934
3935
3936void badlogin_inc(const char *user, const char *addr)
3937/* increments a bad login from this address */
3938{
3939 if (xs_type(addr) == XSTYPE_STRING) {
3940 int failures = 0;
3941 xs *fn = _badlogin_fn(addr);
3942 FILE *f;
3943
3944 _badlogin_read(fn, &failures);
3945
3946 pthread_mutex_lock(&data_mutex);
3947
3948 if ((f = fopen(fn, "w")) != NULL) {
3949 failures++;
3950
3951 fprintf(f, "%d %s %s\n", failures, addr, user);
3952 fclose(f);
3953
3954 srv_log(xs_fmt("Registered %d login failure(s) from %s for %s", failures, addr, user));
3955 }
3956
3957 pthread_mutex_unlock(&data_mutex);
3958 }
3959}
diff --git a/doc/snac.8 b/doc/snac.8
index 54ae744..f5e4bd5 100644
--- a/doc/snac.8
+++ b/doc/snac.8
@@ -242,6 +242,12 @@ posts will not be direct ones, but proxied by
242This way, remote media servers will not see the user's IP, but the server one, 242This way, remote media servers will not see the user's IP, but the server one,
243improving privacy. Please take note that this will increase the server's incoming 243improving privacy. Please take note that this will increase the server's incoming
244and outgoing traffic. 244and outgoing traffic.
245.It Ic badlogin_retries
246If incorrect logins from a given IP address reach this count, subsequent attempts
247from it are rejected until the lock expires (default: 5 retries).
248.It Ic badlogin_expire
249The number of seconds a blocked IP address is ignored in login attempts
250(default: 300 seconds).
245.El 251.El
246.Pp 252.Pp
247You must restart the server to make effective these changes. 253You must restart the server to make effective these changes.
@@ -546,6 +552,22 @@ heavily on how all the servers involved behave. Just cross your fingers and hope
546Full instances can be blocked. This operation must be done from 552Full instances can be blocked. This operation must be done from
547the command-line tool. See 553the command-line tool. See
548.Xr snac 1 . 554.Xr snac 1 .
555.Pp
556.Ss Bad login throttling
557Since version 2.67, a simple logic to avoid brute force attacks against user passwords
558has been implemented: if, from a given IP address, the number of failed logins reaches
559a given threshold, further tries from that IP address are never successful until a timer
560expires. The maximum number of retries can be configured in the
561.Pa server.json
562file by setting the
563.Ic badlogin_retries
564variable, and the number of seconds the IP address unlock timer expires, in
565.Ic badlogin_expire .
566Please take note that, for this system to work, you must setup your web server proxy
567to pass the remote connection address in the
568.Ic X-Forwarded-For
569HTTP header (unless you use the FastCGI interface; if that's the case, you don't have
570to do anything).
549.Sh ENVIRONMENT 571.Sh ENVIRONMENT
550.Bl -tag -width Ds 572.Bl -tag -width Ds
551.It Ev DEBUG 573.It Ev DEBUG
@@ -603,35 +625,42 @@ example.com server section:
603location /fedi { 625location /fedi {
604 proxy_pass http://localhost:8001; 626 proxy_pass http://localhost:8001;
605 proxy_set_header Host $http_host; 627 proxy_set_header Host $http_host;
628 proxy_set_header X-Forwarded-For $remote_addr;
606} 629}
607# webfinger 630# webfinger
608location /.well-known/webfinger { 631location /.well-known/webfinger {
609 proxy_pass http://localhost:8001; 632 proxy_pass http://localhost:8001;
610 proxy_set_header Host $http_host; 633 proxy_set_header Host $http_host;
634 proxy_set_header X-Forwarded-For $remote_addr;
611} 635}
612# Mastodon API (entry points) 636# Mastodon API (entry points)
613location /api/v1/ { 637location /api/v1/ {
614 proxy_pass http://localhost:8001; 638 proxy_pass http://localhost:8001;
615 proxy_set_header Host $http_host; 639 proxy_set_header Host $http_host;
640 proxy_set_header X-Forwarded-For $remote_addr;
616} 641}
617location /api/v2/ { 642location /api/v2/ {
618 proxy_pass http://localhost:8001; 643 proxy_pass http://localhost:8001;
619 proxy_set_header Host $http_host; 644 proxy_set_header Host $http_host;
645 proxy_set_header X-Forwarded-For $remote_addr;
620} 646}
621# Mastodon API (OAuth support) 647# Mastodon API (OAuth support)
622location /oauth { 648location /oauth {
623 proxy_pass http://localhost:8001; 649 proxy_pass http://localhost:8001;
624 proxy_set_header Host $http_host; 650 proxy_set_header Host $http_host;
651 proxy_set_header X-Forwarded-For $remote_addr;
625} 652}
626# optional 653# optional
627location /.well-known/nodeinfo { 654location /.well-known/nodeinfo {
628 proxy_pass http://localhost:8001; 655 proxy_pass http://localhost:8001;
629 proxy_set_header Host $http_host; 656 proxy_set_header Host $http_host;
657 proxy_set_header X-Forwarded-For $remote_addr;
630} 658}
631# optional (needed by some Mastodon API clients) 659# optional (needed by some Mastodon API clients)
632location /.well-known/host-meta { 660location /.well-known/host-meta {
633 proxy_pass http://localhost:8001; 661 proxy_pass http://localhost:8001;
634 proxy_set_header Host $http_host; 662 proxy_set_header Host $http_host;
663 proxy_set_header X-Forwarded-For $remote_addr;
635} 664}
636.Ed 665.Ed
637.Pp 666.Pp
diff --git a/examples/static-linking-with-musl.md b/examples/static-linking-with-musl.md
new file mode 100644
index 0000000..b14132c
--- /dev/null
+++ b/examples/static-linking-with-musl.md
@@ -0,0 +1,77 @@
1# How to build a statically linked Snac with musl
2
3Prepare the environment
4```
5mkdir build
6cd build
7export BUILD_TARGET=$PWD
8export CC="musl-gcc"
9```
10
11Download and build latest zlib
12```
13wget http://zlib.net/current/zlib.tar.gz
14tar xvf zlib.tar.gz
15cd zlib-1.3.1/
16./configure --prefix=$BUILD_TARGET --static
17make
18make install
19cd ..
20```
21
22Download and build latest openssl
23```
24wget https://github.com/openssl/openssl/releases/download/openssl-3.4.0/openssl-3.4.0.tar.gz
25tar xvf openssl-3.4.0.tar.gz
26cd openssl-3.4.0
27CC="musl-gcc -fPIE -pie -static -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/" \
28 ./Configure no-shared no-async --prefix=$BUILD_TARGET --openssldir=$BUILD_TARGET/ssl linux-x86_64
29make depend
30make
31make install
32cd ..
33```
34
35Download and build latest curl
36```
37wget https://curl.se/download/curl-7.88.1.tar.gz
38tar xvf curl-7.88.1.tar.gz
39cd curl-7.88.1
40./configure --disable-shared --enable-static --disable-silent-rules \
41 --disable-debug --disable-warnings --disable-werror \
42 --disable-curldebug --disable-symbol-hiding --disable-ares \
43 --disable-rt --disable-ech --disable-dependency-tracking \
44 --disable-libtool-lock --enable-http --disable-ftp \
45 --disable-file --disable-ldap --disable-ldaps \
46 --disable-rtsp --disable-proxy --disable-dict \
47 --disable-telnet --disable-tftp --disable-pop3 \
48 --disable-imap --disable-smb --disable-smtp --disable-gopher \
49 --disable-mqtt --disable-manual --disable-libcurl-option --disable-ipv6 \
50 --disable-openssl-auto-load-config --disable-versioned-symbols
51 --disable-verbose --disable-sspi --disable-crypto-auth \
52 --disable-ntlm --disable-ntlm-wb --disable-tls-srp \
53 --disable-unix-sockets --disable-cookies --disable-socketpair \
54 --disable-http-auth --disable-doh --disable-mime --disable-dateparse \
55 --disable-netrc --disable-progress-meter --disable-dnsshuffle \
56 --disable-get-easy-options --disable-alt-svc --disable-websockets \
57 --without-brotli --without-zstd --without-libpsl --without-libgsasl \
58 --without-librtmp --without-winidn --disable-threaded-resolver \
59 --with-openssl=$BUILD_TARGET/ --with-zlib=$BUILD_TARGET/ \
60 --prefix=$BUILD_TARGET/
61make
62make install
63cd ..
64```
65
66Download and build latest snac2
67```
68git clone https://codeberg.org/grunfink/snac2.git # or cd to your existing repo
69cd snac2
70make CFLAGS="-g -Wall -Wextra -pedantic -static -DWITHOUT_SHM" \
71 LDFLAGS="-L$BUILD_TARGET/lib64 -lcurl -lssl -lcrypto -lz" \
72 PREFIX=$BUILD_TARGET
73make install PREFIX=$BUILD_TARGET
74cd ..
75```
76
77Finally a statically linked snac is ready at $BUILD_TARGET/bin
diff --git a/format.c b/format.c
index d0b535d..12783ae 100644
--- a/format.c
+++ b/format.c
@@ -163,14 +163,26 @@ static xs_str *format_line(const char *line, xs_list **attach)
163 const char *mime = xs_mime_by_ext(img_url); 163 const char *mime = xs_mime_by_ext(img_url);
164 164
165 if (attach != NULL && xs_startswith(mime, "image/")) { 165 if (attach != NULL && xs_startswith(mime, "image/")) {
166 xs *d = xs_dict_new(); 166 const xs_dict *ad;
167 167 int add = 1;
168 d = xs_dict_append(d, "mediaType", mime); 168
169 d = xs_dict_append(d, "url", img_url); 169 xs_list_foreach(*attach, ad) {
170 d = xs_dict_append(d, "name", alt_text); 170 if (strcmp(xs_dict_get_def(ad, "url", ""), img_url) == 0) {
171 d = xs_dict_append(d, "type", "Image"); 171 add = 0;
172 172 break;
173 *attach = xs_list_append(*attach, d); 173 }
174 }
175
176 if (add) {
177 xs *d = xs_dict_new();
178
179 d = xs_dict_append(d, "mediaType", mime);
180 d = xs_dict_append(d, "url", img_url);
181 d = xs_dict_append(d, "name", alt_text);
182 d = xs_dict_append(d, "type", "Image");
183
184 *attach = xs_list_append(*attach, d);
185 }
174 } 186 }
175 else { 187 else {
176 xs *link = xs_fmt("<a href=\"%s\">%s</a>", img_url, alt_text); 188 xs *link = xs_fmt("<a href=\"%s\">%s</a>", img_url, alt_text);
@@ -191,14 +203,26 @@ static xs_str *format_line(const char *line, xs_list **attach)
191 203
192 if (attach != NULL && xs_startswith(mime, "image/")) { 204 if (attach != NULL && xs_startswith(mime, "image/")) {
193 /* if it's a link to an image, insert it as an attachment */ 205 /* if it's a link to an image, insert it as an attachment */
194 xs *d = xs_dict_new(); 206 const xs_dict *ad;
207 int add = 1;
208
209 xs_list_foreach(*attach, ad) {
210 if (strcmp(xs_dict_get_def(ad, "url", ""), v2) == 0) {
211 add = 0;
212 break;
213 }
214 }
195 215
196 d = xs_dict_append(d, "mediaType", mime); 216 if (add) {
197 d = xs_dict_append(d, "url", v2); 217 xs *d = xs_dict_new();
198 d = xs_dict_append(d, "name", ""); 218
199 d = xs_dict_append(d, "type", "Image"); 219 d = xs_dict_append(d, "mediaType", mime);
220 d = xs_dict_append(d, "url", v2);
221 d = xs_dict_append(d, "name", "");
222 d = xs_dict_append(d, "type", "Image");
200 223
201 *attach = xs_list_append(*attach, d); 224 *attach = xs_list_append(*attach, d);
225 }
202 } 226 }
203 else { 227 else {
204 xs *s1 = xs_fmt("<a href=\"%s\" target=\"_blank\">%s</a>", v2, u); 228 xs *s1 = xs_fmt("<a href=\"%s\" target=\"_blank\">%s</a>", v2, u);
diff --git a/html.c b/html.c
index 2c353d2..2c0a823 100644
--- a/html.c
+++ b/html.c
@@ -29,9 +29,18 @@ int login(snac *snac, const xs_dict *headers)
29 xs *l1 = xs_split_n(s2, ":", 1); 29 xs *l1 = xs_split_n(s2, ":", 1);
30 30
31 if (xs_list_len(l1) == 2) { 31 if (xs_list_len(l1) == 2) {
32 logged_in = check_password( 32 const char *user = xs_list_get(l1, 0);
33 xs_list_get(l1, 0), xs_list_get(l1, 1), 33 const char *pwd = xs_list_get(l1, 1);
34 xs_dict_get(snac->config, "passwd")); 34 const char *addr = xs_or(xs_dict_get(headers, "remote-addr"),
35 xs_dict_get(headers, "x-forwarded-for"));
36
37 if (badlogin_check(user, addr)) {
38 logged_in = check_password(user, pwd,
39 xs_dict_get(snac->config, "passwd"));
40
41 if (!logged_in)
42 badlogin_inc(user, addr);
43 }
35 } 44 }
36 } 45 }
37 46
@@ -633,6 +642,17 @@ xs_html *html_user_head(snac *user, const char *desc, const char *url)
633 else 642 else
634 s_desc = xs_dup(desc); 643 s_desc = xs_dup(desc);
635 644
645 /* show metrics in og:description? */
646 if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
647 xs *fwers = follower_list(user);
648 xs *fwing = following_list(user);
649
650 xs *s1 = xs_fmt(L("%d following, %d followers · "),
651 xs_list_len(fwing), xs_list_len(fwers));
652
653 s_desc = xs_str_prepend_i(s_desc, s1);
654 }
655
636 /* shorten desc to a reasonable size */ 656 /* shorten desc to a reasonable size */
637 for (n = 0; s_desc[n]; n++) { 657 for (n = 0; s_desc[n]; n++) {
638 if (n > 512 && (s_desc[n] == ' ' || s_desc[n] == '\n')) 658 if (n > 512 && (s_desc[n] == ' ' || s_desc[n] == '\n'))
@@ -2041,6 +2061,23 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2041 if (content && xs_str_in(content, o_href) != -1) 2061 if (content && xs_str_in(content, o_href) != -1)
2042 continue; 2062 continue;
2043 2063
2064 /* do this attachment include an icon? */
2065 const xs_dict *icon = xs_dict_get(a, "icon");
2066 if (xs_type(icon) == XSTYPE_DICT) {
2067 const char *icon_mtype = xs_dict_get(icon, "mediaType");
2068 const char *icon_url = xs_dict_get(icon, "url");
2069
2070 if (icon_mtype && icon_url && xs_startswith(icon_mtype, "image/")) {
2071 xs_html_add(content_attachments,
2072 xs_html_tag("a",
2073 xs_html_attr("href", icon_url),
2074 xs_html_attr("target", "_blank"),
2075 xs_html_sctag("img",
2076 xs_html_attr("loading", "lazy"),
2077 xs_html_attr("src", icon_url))));
2078 }
2079 }
2080
2044 xs *href = make_url(o_href, proxy, 0); 2081 xs *href = make_url(o_href, proxy, 0);
2045 2082
2046 if (xs_startswith(type, "image/") || strcmp(type, "Image") == 0) { 2083 if (xs_startswith(type, "image/") || strcmp(type, "Image") == 0) {
@@ -2996,9 +3033,54 @@ int html_get_handler(const xs_dict *req, const char *q_path,
2996 } 3033 }
2997 else { 3034 else {
2998 const char *q = xs_dict_get(q_vars, "q"); 3035 const char *q = xs_dict_get(q_vars, "q");
3036 xs *url_acct = NULL;
3037
3038 /* searching for an URL? */
3039 if (q && xs_match(q, "https://*|http://*")) {
3040 /* may by an actor; try a webfinger */
3041 xs *actor_obj = NULL;
3042
3043 if (valid_status(webfinger_request(q, &actor_obj, &url_acct))) {
3044 /* it's an actor; do the dirty trick of changing q to the account name */
3045 q = url_acct;
3046 }
3047 else {
3048 /* if it's not already here, try to bring it to the user's timeline */
3049 xs *md5 = xs_md5_hex(q, strlen(q));
3050
3051 if (!timeline_here(&snac, md5)) {
3052 xs *object = NULL;
3053 int status;
3054
3055 status = activitypub_request(&snac, q, &object);
3056 snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status));
3057
3058 if (valid_status(status)) {
3059 /* got it; also request the actor */
3060 const char *attr_to = get_atto(object);
3061
3062 if (!xs_is_null(attr_to)) {
3063 status = actor_request(&snac, attr_to, &actor_obj);
3064
3065 snac_debug(&snac, 1, xs_fmt("Request author %s of %s %d", attr_to, q, status));
3066
3067 if (valid_status(status)) {
3068 /* add the actor */
3069 actor_add(attr_to, actor_obj);
3070
3071 /* add the post to the timeline */
3072 timeline_add(&snac, q, object);
3073 }
3074 }
3075 }
3076 }
3077 }
3078
3079 /* fall through */
3080 }
2999 3081
3000 if (q && *q) { 3082 if (q && *q) {
3001 if (xs_regex_match(q, "^@?[a-zA-Z0-9_]+@[a-zA-Z0-9-]+\\.")) { 3083 if (xs_regex_match(q, "^@?[a-zA-Z0-9._]+@[a-zA-Z0-9-]+\\.")) {
3002 /** search account **/ 3084 /** search account **/
3003 xs *actor = NULL; 3085 xs *actor = NULL;
3004 xs *acct = NULL; 3086 xs *acct = NULL;
diff --git a/httpd.c b/httpd.c
index 81d2f9e..11e4d17 100644
--- a/httpd.c
+++ b/httpd.c
@@ -279,6 +279,7 @@ void httpd_connection(FILE *f)
279 xs *payload = NULL; 279 xs *payload = NULL;
280 xs *etag = NULL; 280 xs *etag = NULL;
281 xs *last_modified = NULL; 281 xs *last_modified = NULL;
282 xs *link = NULL;
282 int p_size = 0; 283 int p_size = 0;
283 const char *p; 284 const char *p;
284 int fcgi_id; 285 int fcgi_id;
@@ -326,7 +327,7 @@ void httpd_connection(FILE *f)
326 status = oauth_get_handler(req, q_path, &body, &b_size, &ctype); 327 status = oauth_get_handler(req, q_path, &body, &b_size, &ctype);
327 328
328 if (status == 0) 329 if (status == 0)
329 status = mastoapi_get_handler(req, q_path, &body, &b_size, &ctype); 330 status = mastoapi_get_handler(req, q_path, &body, &b_size, &ctype, &link);
330#endif /* NO_MASTODON_API */ 331#endif /* NO_MASTODON_API */
331 332
332 if (status == 0) 333 if (status == 0)
@@ -426,6 +427,8 @@ void httpd_connection(FILE *f)
426 headers = xs_dict_append(headers, "etag", etag); 427 headers = xs_dict_append(headers, "etag", etag);
427 if (!xs_is_null(last_modified)) 428 if (!xs_is_null(last_modified))
428 headers = xs_dict_append(headers, "last-modified", last_modified); 429 headers = xs_dict_append(headers, "last-modified", last_modified);
430 if (!xs_is_null(link))
431 headers = xs_dict_append(headers, "Link", link);
429 432
430 /* if there are any additional headers, add them */ 433 /* if there are any additional headers, add them */
431 const xs_dict *more_headers = xs_dict_get(srv_config, "http_headers"); 434 const xs_dict *more_headers = xs_dict_get(srv_config, "http_headers");
@@ -775,6 +778,26 @@ void httpd(void)
775 xs *shm_name = NULL; 778 xs *shm_name = NULL;
776 sem_t anon_job_sem; 779 sem_t anon_job_sem;
777 xs *pidfile = xs_fmt("%s/server.pid", srv_basedir); 780 xs *pidfile = xs_fmt("%s/server.pid", srv_basedir);
781 int pidfd;
782
783 {
784 /* do some pidfile locking acrobatics */
785 if ((pidfd = open(pidfile, O_RDWR | O_CREAT, 0660)) == -1) {
786 srv_log(xs_fmt("Cannot create pidfile %s -- cannot continue", pidfile));
787 return;
788 }
789
790 if (lockf(pidfd, F_TLOCK, 1) == -1) {
791 srv_log(xs_fmt("Cannot lock pidfile %s -- server already running?", pidfile));
792 close(pidfd);
793 return;
794 }
795
796 ftruncate(pidfd, 0);
797
798 xs *s = xs_fmt("%d\n", (int)getpid());
799 write(pidfd, s, strlen(s));
800 }
778 801
779 address = xs_dict_get(srv_config, "address"); 802 address = xs_dict_get(srv_config, "address");
780 803
@@ -810,17 +833,6 @@ void httpd(void)
810 srv_log(xs_fmt("httpd%s start %s %s", p_state->use_fcgi ? " (FastCGI)" : "", 833 srv_log(xs_fmt("httpd%s start %s %s", p_state->use_fcgi ? " (FastCGI)" : "",
811 full_address, USER_AGENT)); 834 full_address, USER_AGENT));
812 835
813 {
814 FILE *f;
815
816 if ((f = fopen(pidfile, "w")) != NULL) {
817 fprintf(f, "%d\n", getpid());
818 fclose(f);
819 }
820 else
821 srv_log(xs_fmt("Cannot create %s: %s", pidfile, strerror(errno)));
822 }
823
824 /* show the number of usable file descriptors */ 836 /* show the number of usable file descriptors */
825 struct rlimit r; 837 struct rlimit r;
826 getrlimit(RLIMIT_NOFILE, &r); 838 getrlimit(RLIMIT_NOFILE, &r);
diff --git a/mastoapi.c b/mastoapi.c
index 990898b..09e18a1 100644
--- a/mastoapi.c
+++ b/mastoapi.c
@@ -293,47 +293,54 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
293 snac snac; 293 snac snac;
294 294
295 if (user_open(&snac, login)) { 295 if (user_open(&snac, login)) {
296 /* check the login + password */ 296 const char *addr = xs_or(xs_dict_get(req, "remote-addr"),
297 if (check_password(login, passwd, xs_dict_get(snac.config, "passwd"))) { 297 xs_dict_get(req, "x-forwarded-for"));
298 /* success! redirect to the desired uri */
299 xs *code = random_str();
300 298
301 xs_free(*body); 299 if (badlogin_check(login, addr)) {
300 /* check the login + password */
301 if (check_password(login, passwd, xs_dict_get(snac.config, "passwd"))) {
302 /* success! redirect to the desired uri */
303 xs *code = random_str();
302 304
303 if (strcmp(redir, "urn:ietf:wg:oauth:2.0:oob") == 0) { 305 xs_free(*body);
304 *body = xs_dup(code);
305 }
306 else {
307 if (xs_str_in(redir, "?") != -1)
308 *body = xs_fmt("%s&code=%s", redir, code);
309 else
310 *body = xs_fmt("%s?code=%s", redir, code);
311 306
312 status = HTTP_STATUS_SEE_OTHER; 307 if (strcmp(redir, "urn:ietf:wg:oauth:2.0:oob") == 0) {
313 } 308 *body = xs_dup(code);
309 }
310 else {
311 if (xs_str_in(redir, "?") != -1)
312 *body = xs_fmt("%s&code=%s", redir, code);
313 else
314 *body = xs_fmt("%s?code=%s", redir, code);
314 315
315 /* if there is a state, add it */ 316 status = HTTP_STATUS_SEE_OTHER;
316 if (!xs_is_null(state) && *state) { 317 }
317 *body = xs_str_cat(*body, "&state=");
318 *body = xs_str_cat(*body, state);
319 }
320 318
321 srv_log(xs_fmt("oauth x-snac-login: '%s' success, redirect to %s", 319 /* if there is a state, add it */
320 if (!xs_is_null(state) && *state) {
321 *body = xs_str_cat(*body, "&state=");
322 *body = xs_str_cat(*body, state);
323 }
324
325 srv_log(xs_fmt("oauth x-snac-login: '%s' success, redirect to %s",
322 login, *body)); 326 login, *body));
323 327
324 /* assign the login to the app */ 328 /* assign the login to the app */
325 xs *app = app_get(cid); 329 xs *app = app_get(cid);
326 330
327 if (app != NULL) { 331 if (app != NULL) {
328 app = xs_dict_set(app, "uid", login); 332 app = xs_dict_set(app, "uid", login);
329 app = xs_dict_set(app, "code", code); 333 app = xs_dict_set(app, "code", code);
330 app_add(cid, app); 334 app_add(cid, app);
335 }
336 else
337 srv_log(xs_fmt("oauth x-snac-login: error getting app %s", cid));
338 }
339 else {
340 srv_debug(1, xs_fmt("oauth x-snac-login: login '%s' incorrect", login));
341 badlogin_inc(login, addr);
331 } 342 }
332 else
333 srv_log(xs_fmt("oauth x-snac-login: error getting app %s", cid));
334 } 343 }
335 else
336 srv_debug(1, xs_fmt("oauth x-snac-login: login '%s' incorrect", login));
337 344
338 user_free(&snac); 345 user_free(&snac);
339 } 346 }
@@ -474,29 +481,36 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
474 snac user; 481 snac user;
475 482
476 if (user_open(&user, login)) { 483 if (user_open(&user, login)) {
477 /* check the login + password */ 484 const char *addr = xs_or(xs_dict_get(req, "remote-addr"),
478 if (check_password(login, passwd, xs_dict_get(user.config, "passwd"))) { 485 xs_dict_get(req, "x-forwarded-for"));
479 /* success! create a new token */
480 xs *tokid = random_str();
481 486
482 srv_debug(1, xs_fmt("x-snac-new-token: " 487 if (badlogin_check(login, addr)) {
488 /* check the login + password */
489 if (check_password(login, passwd, xs_dict_get(user.config, "passwd"))) {
490 /* success! create a new token */
491 xs *tokid = random_str();
492
493 srv_debug(1, xs_fmt("x-snac-new-token: "
483 "successful login for %s, new token %s", login, tokid)); 494 "successful login for %s, new token %s", login, tokid));
484 495
485 xs *token = xs_dict_new(); 496 xs *token = xs_dict_new();
486 token = xs_dict_append(token, "token", tokid); 497 token = xs_dict_append(token, "token", tokid);
487 token = xs_dict_append(token, "client_id", "snac-client"); 498 token = xs_dict_append(token, "client_id", "snac-client");
488 token = xs_dict_append(token, "client_secret", ""); 499 token = xs_dict_append(token, "client_secret", "");
489 token = xs_dict_append(token, "uid", login); 500 token = xs_dict_append(token, "uid", login);
490 token = xs_dict_append(token, "code", ""); 501 token = xs_dict_append(token, "code", "");
491 502
492 token_add(tokid, token); 503 token_add(tokid, token);
493 504
494 *ctype = "text/plain"; 505 *ctype = "text/plain";
495 xs_free(*body); 506 xs_free(*body);
496 *body = xs_dup(tokid); 507 *body = xs_dup(tokid);
497 } 508 }
509 else
510 badlogin_inc(login, addr);
498 511
499 user_free(&user); 512 user_free(&user);
513 }
500 } 514 }
501 } 515 }
502 } 516 }
@@ -898,7 +912,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
898 const char *o_href = xs_dict_get(v, "href"); 912 const char *o_href = xs_dict_get(v, "href");
899 const char *name = xs_dict_get(v, "name"); 913 const char *name = xs_dict_get(v, "name");
900 914
901 if (xs_match(type, "image/*|video/*|Image|Video")) { /* */ 915 if (xs_match(type, "image/*|video/*|audio/*|Image|Video")) { /* */
902 xs *matteid = xs_fmt("%s_%d", id, xs_list_len(matt)); 916 xs *matteid = xs_fmt("%s_%d", id, xs_list_len(matt));
903 xs *href = make_url(o_href, proxy, 1); 917 xs *href = make_url(o_href, proxy, 1);
904 918
@@ -910,7 +924,8 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
910 d = xs_dict_append(d, "remote_url", href); 924 d = xs_dict_append(d, "remote_url", href);
911 d = xs_dict_append(d, "description", name); 925 d = xs_dict_append(d, "description", name);
912 926
913 d = xs_dict_append(d, "type", (*type == 'v' || *type == 'V') ? "video" : "image"); 927 d = xs_dict_append(d, "type", (*type == 'v' || *type == 'V') ? "video" :
928 (*type == 'a' || *type == 'A') ? "audio" : "image");
914 929
915 matt = xs_list_append(matt, d); 930 matt = xs_list_append(matt, d);
916 } 931 }
@@ -990,7 +1005,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
990 const char *o_url = xs_dict_get(icon, "url"); 1005 const char *o_url = xs_dict_get(icon, "url");
991 1006
992 if (!xs_is_null(o_url)) { 1007 if (!xs_is_null(o_url)) {
993 xs *url = make_url(o_url, snac->actor, 1); 1008 xs *url = make_url(o_url, snac ? snac->actor : NULL, 1);
994 xs *nm = xs_strip_chars_i(xs_dup(name), ":"); 1009 xs *nm = xs_strip_chars_i(xs_dup(name), ":");
995 1010
996 d1 = xs_dict_append(d1, "shortcode", nm); 1011 d1 = xs_dict_append(d1, "shortcode", nm);
@@ -1193,10 +1208,13 @@ int process_auth_token(snac *snac, const xs_dict *req)
1193 return logged_in; 1208 return logged_in;
1194} 1209}
1195 1210
1211
1196void credentials_get(char **body, char **ctype, int *status, snac snac) 1212void credentials_get(char **body, char **ctype, int *status, snac snac)
1197{ 1213{
1198 xs *acct = xs_dict_new(); 1214 xs *acct = xs_dict_new();
1199 1215
1216 const xs_val *bot = xs_dict_get(snac.config, "bot");
1217
1200 acct = xs_dict_append(acct, "id", snac.md5); 1218 acct = xs_dict_append(acct, "id", snac.md5);
1201 acct = xs_dict_append(acct, "username", xs_dict_get(snac.config, "uid")); 1219 acct = xs_dict_append(acct, "username", xs_dict_get(snac.config, "uid"));
1202 acct = xs_dict_append(acct, "acct", xs_dict_get(snac.config, "uid")); 1220 acct = xs_dict_append(acct, "acct", xs_dict_get(snac.config, "uid"));
@@ -1206,7 +1224,7 @@ void credentials_get(char **body, char **ctype, int *status, snac snac)
1206 acct = xs_dict_append(acct, "note", xs_dict_get(snac.config, "bio")); 1224 acct = xs_dict_append(acct, "note", xs_dict_get(snac.config, "bio"));
1207 acct = xs_dict_append(acct, "url", snac.actor); 1225 acct = xs_dict_append(acct, "url", snac.actor);
1208 acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE)); 1226 acct = xs_dict_append(acct, "locked", xs_stock(XSTYPE_FALSE));
1209 acct = xs_dict_append(acct, "bot", xs_dict_get(snac.config, "bot")); 1227 acct = xs_dict_append(acct, "bot", xs_stock(xs_is_true(bot) ? XSTYPE_TRUE : XSTYPE_FALSE));
1210 acct = xs_dict_append(acct, "emojis", xs_stock(XSTYPE_LIST)); 1228 acct = xs_dict_append(acct, "emojis", xs_stock(XSTYPE_LIST));
1211 1229
1212 xs *src = xs_json_loads("{\"privacy\":\"public\", \"language\":\"en\"," 1230 xs *src = xs_json_loads("{\"privacy\":\"public\", \"language\":\"en\","
@@ -1220,7 +1238,7 @@ void credentials_get(char **body, char **ctype, int *status, snac snac)
1220 src = xs_dict_set(src, "sensitive", 1238 src = xs_dict_set(src, "sensitive",
1221 strcmp(cw, "open") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE)); 1239 strcmp(cw, "open") == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE));
1222 1240
1223 src = xs_dict_set(src, "bot", xs_dict_get(snac.config, "bot")); 1241 src = xs_dict_set(src, "bot", xs_stock(xs_is_true(bot) ? XSTYPE_TRUE : XSTYPE_FALSE));
1224 1242
1225 xs *avatar = NULL; 1243 xs *avatar = NULL;
1226 const char *av = xs_dict_get(snac.config, "avatar"); 1244 const char *av = xs_dict_get(snac.config, "avatar");
@@ -1319,7 +1337,7 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
1319 1337
1320 const char *max_id = xs_dict_get(args, "max_id"); 1338 const char *max_id = xs_dict_get(args, "max_id");
1321 const char *since_id = xs_dict_get(args, "since_id"); 1339 const char *since_id = xs_dict_get(args, "since_id");
1322 const char *min_id = xs_dict_get(args, "min_id"); 1340 const char *min_id = xs_dict_get(args, "min_id"); /* unsupported old-to-new navigation */
1323 const char *limit_s = xs_dict_get(args, "limit"); 1341 const char *limit_s = xs_dict_get(args, "limit");
1324 int limit = 0; 1342 int limit = 0;
1325 int cnt = 0; 1343 int cnt = 0;
@@ -1330,7 +1348,7 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
1330 if (limit == 0) 1348 if (limit == 0)
1331 limit = 20; 1349 limit = 20;
1332 1350
1333 if (index_desc_first(f, md5, 0)) { 1351 if (min_id == NULL && index_desc_first(f, md5, 0)) {
1334 do { 1352 do {
1335 xs *msg = NULL; 1353 xs *msg = NULL;
1336 1354
@@ -1348,13 +1366,6 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
1348 break; 1366 break;
1349 } 1367 }
1350 1368
1351 /* only returns entries newer than min_id */
1352 /* what does really "Return results immediately newer than ID" mean? */
1353 if (min_id) {
1354 if (strcmp(md5, MID_TO_MD5(min_id)) == 0)
1355 break;
1356 }
1357
1358 /* get the entry */ 1369 /* get the entry */
1359 if (user) { 1370 if (user) {
1360 if (!valid_status(timeline_get_by_md5(user, md5, &msg))) 1371 if (!valid_status(timeline_get_by_md5(user, md5, &msg)))
@@ -1438,8 +1449,35 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
1438} 1449}
1439 1450
1440 1451
1452xs_str *timeline_link_header(const char *endpoint, xs_list *timeline)
1453/* returns a Link header with paging information */
1454{
1455 xs_str *s = NULL;
1456
1457 if (xs_list_len(timeline) == 0)
1458 return NULL;
1459
1460 const xs_dict *first_st = xs_list_get(timeline, 0);
1461 const xs_dict *last_st = xs_list_get(timeline, -1);
1462 const char *first_id = xs_dict_get(first_st, "id");
1463 const char *last_id = xs_dict_get(last_st, "id");
1464 const char *host = xs_dict_get(srv_config, "host");
1465 const char *protocol = xs_dict_get_def(srv_config, "protocol", "https");
1466
1467 s = xs_fmt(
1468 "<%s:/" "/%s%s?max_id=%s>; rel=\"next\", "
1469 "<%s:/" "/%s%s?since_id=%s>; rel=\"prev\"",
1470 protocol, host, endpoint, last_id,
1471 protocol, host, endpoint, first_id);
1472
1473 srv_debug(1, xs_fmt("timeline_link_header %s", s));
1474
1475 return s;
1476}
1477
1478
1441int mastoapi_get_handler(const xs_dict *req, const char *q_path, 1479int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1442 char **body, int *b_size, char **ctype) 1480 char **body, int *b_size, char **ctype, xs_str **link)
1443{ 1481{
1444 (void)b_size; 1482 (void)b_size;
1445 1483
@@ -1699,6 +1737,8 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1699 xs *ifn = user_index_fn(&snac1, "private"); 1737 xs *ifn = user_index_fn(&snac1, "private");
1700 xs *out = mastoapi_timeline(&snac1, args, ifn); 1738 xs *out = mastoapi_timeline(&snac1, args, ifn);
1701 1739
1740 *link = timeline_link_header("/api/v1/timelines/home", out);
1741
1702 *body = xs_json_dumps(out, 4); 1742 *body = xs_json_dumps(out, 4);
1703 *ctype = "application/json"; 1743 *ctype = "application/json";
1704 status = HTTP_STATUS_OK; 1744 status = HTTP_STATUS_OK;
@@ -1763,8 +1803,14 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1763 xs *out = xs_list_new(); 1803 xs *out = xs_list_new();
1764 const xs_dict *v; 1804 const xs_dict *v;
1765 const xs_list *excl = xs_dict_get(args, "exclude_types[]"); 1805 const xs_list *excl = xs_dict_get(args, "exclude_types[]");
1806 const char *min_id = xs_dict_get(args, "min_id");
1766 const char *max_id = xs_dict_get(args, "max_id"); 1807 const char *max_id = xs_dict_get(args, "max_id");
1767 1808
1809 if (dbglevel) {
1810 xs *js = xs_json_dumps(args, 0);
1811 srv_debug(1, xs_fmt("mastoapi_notifications args %s", js));
1812 }
1813
1768 xs_list_foreach(l, v) { 1814 xs_list_foreach(l, v) {
1769 xs *noti = notify_get(&snac1, v); 1815 xs *noti = notify_get(&snac1, v);
1770 1816
@@ -1795,6 +1841,12 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1795 continue; 1841 continue;
1796 } 1842 }
1797 1843
1844 if (min_id) {
1845 if (strcmp(fid, min_id) <= 0) {
1846 continue;
1847 }
1848 }
1849
1798 /* convert the type */ 1850 /* convert the type */
1799 if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0) 1851 if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0)
1800 type = "favourite"; 1852 type = "favourite";
@@ -1842,6 +1894,8 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
1842 out = xs_list_append(out, mn); 1894 out = xs_list_append(out, mn);
1843 } 1895 }
1844 1896
1897 srv_debug(1, xs_fmt("mastoapi_notifications count %d", xs_list_len(out)));
1898
1845 *body = xs_json_dumps(out, 4); 1899 *body = xs_json_dumps(out, 4);
1846 *ctype = "application/json"; 1900 *ctype = "application/json";
1847 status = HTTP_STATUS_OK; 1901 status = HTTP_STATUS_OK;
@@ -2273,9 +2327,22 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
2273 } 2327 }
2274 else 2328 else
2275 if (strcmp(cmd, "/v1/markers") == 0) { /** **/ 2329 if (strcmp(cmd, "/v1/markers") == 0) { /** **/
2276 *body = xs_dup("{}"); 2330 if (logged_in) {
2277 *ctype = "application/json"; 2331 const xs_list *timeline = xs_dict_get(args, "timeline[]");
2278 status = HTTP_STATUS_OK; 2332 xs_str *json = NULL;
2333 if (!xs_is_null(timeline))
2334 json = xs_json_dumps(markers_get(&snac1, timeline), 4);
2335
2336 if (!xs_is_null(json))
2337 *body = json;
2338 else
2339 *body = xs_dup("{}");
2340
2341 *ctype = "application/json";
2342 status = HTTP_STATUS_OK;
2343 }
2344 else
2345 status = HTTP_STATUS_UNAUTHORIZED;
2279 } 2346 }
2280 else 2347 else
2281 if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/ 2348 if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/
@@ -2310,6 +2377,37 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
2310 if (xs_is_null(offset) || strcmp(offset, "0") == 0) { 2377 if (xs_is_null(offset) || strcmp(offset, "0") == 0) {
2311 /* reply something only for offset 0; otherwise, 2378 /* reply something only for offset 0; otherwise,
2312 apps like Tusky keep asking again and again */ 2379 apps like Tusky keep asking again and again */
2380 if (xs_startswith(q, "https://")) {
2381 xs *md5 = xs_md5_hex(q, strlen(q));
2382
2383 if (!timeline_here(&snac1, md5)) {
2384 xs *object = NULL;
2385 int status;
2386
2387 status = activitypub_request(&snac1, q, &object);
2388 snac_debug(&snac1, 1, xs_fmt("Request searched URL %s %d", q, status));
2389
2390 if (valid_status(status)) {
2391 /* got it; also request the actor */
2392 const char *attr_to = get_atto(object);
2393 xs *actor_obj = NULL;
2394
2395 if (!xs_is_null(attr_to)) {
2396 status = actor_request(&snac1, attr_to, &actor_obj);
2397
2398 snac_debug(&snac1, 1, xs_fmt("Request author %s of %s %d", attr_to, q, status));
2399
2400 if (valid_status(status)) {
2401 /* add the actor */
2402 actor_add(attr_to, actor_obj);
2403
2404 /* add the post to the timeline */
2405 timeline_add(&snac1, q, object);
2406 }
2407 }
2408 }
2409 }
2410 }
2313 2411
2314 if (!xs_is_null(q)) { 2412 if (!xs_is_null(q)) {
2315 if (xs_is_null(type) || strcmp(type, "accounts") == 0) { 2413 if (xs_is_null(type) || strcmp(type, "accounts") == 0) {
@@ -2945,6 +3043,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
2945 status = HTTP_STATUS_UNPROCESSABLE_CONTENT; 3043 status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
2946 } 3044 }
2947 } 3045 }
3046 else
2948 if (xs_startswith(cmd, "/v1/lists/")) { /** list maintenance **/ 3047 if (xs_startswith(cmd, "/v1/lists/")) { /** list maintenance **/
2949 if (logged_in) { 3048 if (logged_in) {
2950 xs *l = xs_split(cmd, "/"); 3049 xs *l = xs_split(cmd, "/");
@@ -2972,9 +3071,35 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
2972 } 3071 }
2973 } 3072 }
2974 } 3073 }
3074 }
3075 else if (strcmp(cmd, "/v1/markers") == 0) { /** **/
3076 xs_str *json = NULL;
3077 if (logged_in) {
3078 const xs_str *home_marker = xs_dict_get(args, "home[last_read_id]");
3079 if (xs_is_null(home_marker)) {
3080 const xs_dict *home = xs_dict_get(args, "home");
3081 if (!xs_is_null(home))
3082 home_marker = xs_dict_get(home, "last_read_id");
3083 }
3084
3085 const xs_str *notify_marker = xs_dict_get(args, "notifications[last_read_id]");
3086 if (xs_is_null(notify_marker)) {
3087 const xs_dict *notify = xs_dict_get(args, "notifications");
3088 if (!xs_is_null(notify))
3089 notify_marker = xs_dict_get(notify, "last_read_id");
3090 }
3091 json = xs_json_dumps(markers_set(&snac, home_marker, notify_marker), 4);
3092 }
3093 if (!xs_is_null(json))
3094 *body = json;
2975 else 3095 else
2976 status = HTTP_STATUS_UNPROCESSABLE_CONTENT; 3096 *body = xs_dup("{}");
3097
3098 *ctype = "application/json";
3099 status = HTTP_STATUS_OK;
2977 } 3100 }
3101 else
3102 status = HTTP_STATUS_UNPROCESSABLE_CONTENT;
2978 3103
2979 /* user cleanup */ 3104 /* user cleanup */
2980 if (logged_in) 3105 if (logged_in)
diff --git a/snac.h b/snac.h
index 583610d..96916d1 100644
--- a/snac.h
+++ b/snac.h
@@ -1,7 +1,7 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
3 3
4#define VERSION "2.66" 4#define VERSION "2.67"
5 5
6#define USER_AGENT "snac/" VERSION 6#define USER_AGENT "snac/" VERSION
7 7
@@ -238,6 +238,9 @@ int notify_new_num(snac *snac);
238xs_list *notify_list(snac *snac, int skip, int show); 238xs_list *notify_list(snac *snac, int skip, int show);
239void notify_clear(snac *snac); 239void notify_clear(snac *snac);
240 240
241xs_dict *markers_get(snac *snac, const xs_list *markers);
242xs_dict *markers_set(snac *snac, const char *home_marker, const char *notify_marker);
243
241void inbox_add(const char *inbox); 244void inbox_add(const char *inbox);
242void inbox_add_by_actor(const xs_dict *actor); 245void inbox_add_by_actor(const xs_dict *actor);
243xs_list *inbox_list(void); 246xs_list *inbox_list(void);
@@ -386,7 +389,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
386 const char *payload, int p_size, 389 const char *payload, int p_size,
387 char **body, int *b_size, char **ctype); 390 char **body, int *b_size, char **ctype);
388int mastoapi_get_handler(const xs_dict *req, const char *q_path, 391int mastoapi_get_handler(const xs_dict *req, const char *q_path,
389 char **body, int *b_size, char **ctype); 392 char **body, int *b_size, char **ctype, xs_str **link);
390int mastoapi_post_handler(const xs_dict *req, const char *q_path, 393int mastoapi_post_handler(const xs_dict *req, const char *q_path,
391 const char *payload, int p_size, 394 const char *payload, int p_size,
392 char **body, int *b_size, char **ctype); 395 char **body, int *b_size, char **ctype);
@@ -427,3 +430,6 @@ typedef struct {
427t_announcement *announcement(double after); 430t_announcement *announcement(double after);
428 431
429xs_str *make_url(const char *href, const char *proxy, int by_token); 432xs_str *make_url(const char *href, const char *proxy, int by_token);
433
434int badlogin_check(const char *user, const char *addr);
435void badlogin_inc(const char *user, const char *addr);
diff --git a/webfinger.c b/webfinger.c
index 893d777..85123bc 100644
--- a/webfinger.c
+++ b/webfinger.c
@@ -82,7 +82,11 @@ int webfinger_request_signed(snac *snac, const char *qs, xs_str **actor, xs_str
82 82
83 if (obj == NULL && valid_status(status) && payload) { 83 if (obj == NULL && valid_status(status) && payload) {
84 obj = xs_json_loads(payload); 84 obj = xs_json_loads(payload);
85 object_add(cached_qs, obj); 85
86 if (obj)
87 object_add(cached_qs, obj);
88 else
89 status = HTTP_STATUS_BAD_REQUEST;
86 } 90 }
87 91
88 if (obj) { 92 if (obj) {
diff --git a/xs_fcgi.h b/xs_fcgi.h
index c79121f..c6ffe1f 100644
--- a/xs_fcgi.h
+++ b/xs_fcgi.h
@@ -190,7 +190,7 @@ xs_dict *xs_fcgi_request(FILE *f, xs_str **payload, int *p_size, int *fcgi_id)
190 q_vars = xs_url_vars(xs_list_get(pnv, 1)); 190 q_vars = xs_url_vars(xs_list_get(pnv, 1));
191 } 191 }
192 else 192 else
193 if (xs_match(k, "CONTENT_TYPE|CONTENT_LENGTH|HTTP_*")) { 193 if (xs_match(k, "CONTENT_TYPE|CONTENT_LENGTH|REMOTE_ADDR|HTTP_*")) {
194 if (xs_startswith(k, "HTTP_")) 194 if (xs_startswith(k, "HTTP_"))
195 k = xs_crop_i(k, 5, 0); 195 k = xs_crop_i(k, 5, 0);
196 196