commit a7ca400

shtrophic  ·  2024-12-23 12:42:45 +0000 UTC
parents af98f4e, 89a8c2e
Merge tag '2.67'

Version 2.67 RELEASED.
13 files changed,  +692, -114
M data.c
M html.c
M snac.h
+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
+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;
+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