aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDenys Vlasenko <vda.linux@googlemail.com>2026-01-21 18:43:41 +0100
committerDenys Vlasenko <vda.linux@googlemail.com>2026-01-21 18:47:56 +0100
commit58b46b7d67c4063aafb94bf82f4e2f3d6e0e3878 (patch)
treee1e781dddbfb6ecdee77154c745935de500cb1bb
parent38721685af680f8ffd8b5b70ed74a0c305519eac (diff)
downloadbusybox-w32-58b46b7d67c4063aafb94bf82f4e2f3d6e0e3878.tar.gz
busybox-w32-58b46b7d67c4063aafb94bf82f4e2f3d6e0e3878.tar.bz2
busybox-w32-58b46b7d67c4063aafb94bf82f4e2f3d6e0e3878.zip
networking/httpd_ratelimit_cgi.c: new example CGI handler
text data bss dec hex filename 4003 40 352 4395 112b httpd_ratelimit_cgi Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
-rwxr-xr-xnetworking/httpd_helpers.sh7
-rw-r--r--networking/httpd_ratelimit_cgi.c242
2 files changed, 248 insertions, 1 deletions
diff --git a/networking/httpd_helpers.sh b/networking/httpd_helpers.sh
index 8eaa2d456..d2c639840 100755
--- a/networking/httpd_helpers.sh
+++ b/networking/httpd_helpers.sh
@@ -1,6 +1,6 @@
1#!/bin/sh 1#!/bin/sh
2 2
3PREFIX="i486-linux-uclibc-" 3PREFIX="i686-linux-musl-"
4OPTS="-static -static-libgcc \ 4OPTS="-static -static-libgcc \
5-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 \ 5-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 \
6-Wall -Wshadow -Wwrite-strings -Wundef -Wstrict-prototypes -Werror \ 6-Wall -Wshadow -Wwrite-strings -Wundef -Wstrict-prototypes -Werror \
@@ -22,3 +22,8 @@ ${PREFIX}gcc \
22${OPTS} \ 22${OPTS} \
23-Wl,-Map -Wl,httpd_ssi.map \ 23-Wl,-Map -Wl,httpd_ssi.map \
24httpd_ssi.c -o httpd_ssi && strip httpd_ssi 24httpd_ssi.c -o httpd_ssi && strip httpd_ssi
25
26${PREFIX}gcc \
27${OPTS} \
28-Wl,-Map -Wl,httpd_ssi.map \
29httpd_ratelimit_cgi.c -o httpd_ratelimit_cgi && strip httpd_ratelimit_cgi
diff --git a/networking/httpd_ratelimit_cgi.c b/networking/httpd_ratelimit_cgi.c
new file mode 100644
index 000000000..713274873
--- /dev/null
+++ b/networking/httpd_ratelimit_cgi.c
@@ -0,0 +1,242 @@
1/*
2 * Copyright (c) 2026 Denys Vlasenko <vda.linux@googlemail.com>
3 *
4 * Licensed under GPLv2, see file LICENSE in this source tree.
5 */
6
7/*
8 * This program is a CGI application. It is intended to rate-limit
9 * invocations of another, presumably resource-intensive CGI
10 * which you want to only allow less than N instances at any one time.
11 *
12 * Any extra clients who try to run the CGI will get the
13 * "429 Too Many Requests" HTTP response.
14 *
15 * The most efficient way to do so is to use a shebang-style executable file:
16 * #!/path/to/httpd_ratelimit_cgi /tmp/lockdir 99 /path/to/expensive_cgi
17 */
18
19/* Build a-la
20i486-linux-uclibc-gcc \
21-static -static-libgcc \
22-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 \
23-Wall -Wshadow -Wwrite-strings -Wundef -Wstrict-prototypes -Werror \
24-Wold-style-definition -Wdeclaration-after-statement -Wno-pointer-sign \
25-Wmissing-prototypes -Wmissing-declarations \
26-Os -fno-builtin-strlen -finline-limit=0 -fomit-frame-pointer \
27-ffunction-sections -fdata-sections -fno-guess-branch-probability \
28-funsigned-char \
29-falign-functions=1 -falign-jumps=1 -falign-labels=1 -falign-loops=1 \
30-march=i386 -mpreferred-stack-boundary=2 \
31-Wl,-Map -Wl,link.map -Wl,--warn-common -Wl,--sort-common -Wl,--gc-sections \
32httpd_ratelimit_cgi.c -o httpd_ratelimit_cgi
33*/
34#include <stdlib.h>
35#include <string.h>
36#include <unistd.h>
37#include <errno.h>
38#include <signal.h>
39#include <sys/stat.h> /* mkdir */
40#include <limits.h>
41
42static ssize_t full_write(int fd, const void *buf, size_t len)
43{
44 ssize_t cc;
45 ssize_t total;
46
47 total = 0;
48
49 while (len) {
50 cc = write(fd, buf, len);
51
52 if (cc < 0) {
53 if (total) {
54 /* we already wrote some! */
55 /* user can do another write to know the error code */
56 return total;
57 }
58 return cc; /* write() returns -1 on failure. */
59 }
60
61 total += cc;
62 buf = ((const char *)buf) + cc;
63 len -= cc;
64 }
65
66 return total;
67}
68
69static void full_write2(int fd, const char *msg, const char *msg2)
70{
71 full_write(fd, msg, strlen(msg));
72 full_write(fd, " '", 2);
73 full_write(fd, msg2, strlen(msg2));
74 full_write(fd, "'\n", 2);
75}
76
77static void write_and_die(int fd, const char *msg)
78{
79 full_write(fd, msg, strlen(msg));
80 exit(0);
81}
82
83static void write_and_die2(int fd, const char *msg, const char *msg2)
84{
85 full_write2(fd, msg, msg2);
86 exit(0);
87}
88
89static void fmt_ul(char *dst, unsigned long n)
90{
91 char buf[sizeof(n)*3 + 2];
92 char *p;
93
94 p = buf + sizeof(buf) - 1;
95 *p = '\0';
96 do {
97 *--p = (n % 10) + '0';
98 n /= 10;
99 } while (n);
100 strcpy(dst, p);
101}
102
103static long get_no(const char *s)
104{
105 const char *start = s;
106 long v = 0;
107 while (*s >= '0' && *s <= '9')
108 v = v * 10 + (*s++ - '0');
109 if (start == s || *s != '\0' /*|| v < 0*/)
110 return -1;
111 return v;
112}
113
114int main(int argc, char **argv)
115{
116 const char *lock_dir = ".";
117 unsigned long max_slots;
118 char *sp;
119 char *symno;
120 unsigned slot_num;
121 pid_t my_pid;
122 char my_pid_str[sizeof(long)*3];
123
124 argv++;
125 if (!argv[0] || !argv[1])
126 write_and_die(2, "Usage: ratelimit [LOCKDIR] MAX_PROCS PROG [ARGS]\n");
127
128 /* ratelimit "[LOCKDIR] MAX_PROCS PROG" SHEBANG [ARGS] syntax?
129 * This happens if we are running as shebang file
130 * of the form "!#/path/to/ratelimit [/tmp/cgit] 10 CGI_BINARY"
131 * (in this case argv[1] is the shebang's filename) */
132 sp = strchr(argv[0], ' ');
133 if (sp) {
134 *sp++ = '\0';
135 /* convert to ratelimit "SOME\0THING" SHEBANG [ARGS] form */
136 /* argv1 ^ */
137 argv[1] = sp;
138 sp = strchr(sp, ' ');
139 if (sp) { /* "THING" also has a space? There is a LOCKDIR! */
140 *sp++ = '\0';
141 /* convert to ratelimit "SOME\0THI\0G" SHEBANG [ARGS] form */
142 /* argv0^ ^argv1 */
143 lock_dir = argv[0];
144 argv[0] = argv[1];
145 argv[1] = sp;
146 goto get_max;
147 }
148 }
149
150 max_slots = get_no(argv[0]);
151 if (max_slots > 9999) {
152 /* ratelimit LOCKDIR MAX_PROCS PROG [ARGS] */
153 lock_dir = argv[0];
154 if (!lock_dir[0])
155 write_and_die2(2, "Bad LOCKDIR", argv[0]);
156 argv++;
157 get_max:
158 max_slots = get_no(argv[0]);
159 if (max_slots > 9999)
160 write_and_die2(2, "Bad MAX_PROCS", argv[0]);
161 }
162 argv++; /* points to PROG [ARGS] */
163
164 {
165 char slot_path[strlen(lock_dir) + 16];
166 symno = stpcpy(stpcpy(slot_path, lock_dir), "/lock.");
167
168 my_pid = getpid();
169 fmt_ul(my_pid_str, my_pid);
170
171 /* Ensure lock directory exists (idempotent, ignores errors) */
172 if (lock_dir[0] != '.' || lock_dir[1]) /* Don't bother with "." */
173 mkdir(lock_dir, 0755);
174
175 /* Starting slot varies per process */
176 slot_num = my_pid;
177
178 /* max_slots = 0 is allowed for testing */
179 if (max_slots != 0) for (int i = 0; i < max_slots; i++) {
180 slot_num = (slot_num + 1) % max_slots;
181 fmt_ul(symno, slot_num);
182
183 while (1) {
184 char buf[32];
185 ssize_t len;
186 long old_pid;
187
188 /* Try to claim atomically */
189 if (symlink(my_pid_str, slot_path) == 0)
190 goto exec;
191
192 /* Only handle EEXIST - other errors skip to next slot */
193 if (errno != EEXIST)
194 break;
195
196 /* Read existing target PID */
197 len = readlink(slot_path, buf, sizeof(buf) - 1);
198 if (len < 1) {
199 /* Broken/empty - clean up and retry */
200 unlink(slot_path);
201 continue;
202 }
203 buf[len] = '\0';
204
205 /* Parse PID */
206 old_pid = get_no(buf);
207 if (old_pid <= 0 || old_pid > INT_MAX) {
208 /* Invalid PID string - clean up and retry */
209 unlink(slot_path);
210 continue;
211 }
212
213 /* Check if old process is alive */
214 if (kill(old_pid, 0) == 0 || errno != ESRCH) {
215 /* Alive (or unexpected error): slot in use, try next */
216 break;
217 }
218
219 /* Dead: clean up and retry this slot */
220 unlink(slot_path);
221 /* Loop continues to retry symlink() */
222 }
223 }
224
225 /* No slot available, return 429 */
226 write_and_die(1, "HTTP/1.1 429 Too Many Requests\r\n"
227 "Content-Type: text/plain\r\n"
228 "Retry-After: 60\r\n"
229 "Connection: close\r\n\r\n"
230 "Too many concurrent requests\n"
231 );
232 return 0;
233 }
234
235exec:
236 execv(argv[0], argv);
237 full_write2(2, "can't execute", argv[0]);
238 write_and_die(1, "HTTP/1.1 500 Internal Server Error\r\n"
239 "Content-Type: text/plain\r\n\r\n"
240 "Failed to execute binary\n");
241 return 1;
242} \ No newline at end of file