diff options
| author | Denys Vlasenko <vda.linux@googlemail.com> | 2026-01-21 18:43:41 +0100 |
|---|---|---|
| committer | Denys Vlasenko <vda.linux@googlemail.com> | 2026-01-21 18:47:56 +0100 |
| commit | 58b46b7d67c4063aafb94bf82f4e2f3d6e0e3878 (patch) | |
| tree | e1e781dddbfb6ecdee77154c745935de500cb1bb | |
| parent | 38721685af680f8ffd8b5b70ed74a0c305519eac (diff) | |
| download | busybox-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-x | networking/httpd_helpers.sh | 7 | ||||
| -rw-r--r-- | networking/httpd_ratelimit_cgi.c | 242 |
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 | ||
| 3 | PREFIX="i486-linux-uclibc-" | 3 | PREFIX="i686-linux-musl-" |
| 4 | OPTS="-static -static-libgcc \ | 4 | OPTS="-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 \ |
| 24 | httpd_ssi.c -o httpd_ssi && strip httpd_ssi | 24 | httpd_ssi.c -o httpd_ssi && strip httpd_ssi |
| 25 | |||
| 26 | ${PREFIX}gcc \ | ||
| 27 | ${OPTS} \ | ||
| 28 | -Wl,-Map -Wl,httpd_ssi.map \ | ||
| 29 | httpd_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 | ||
| 20 | i486-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 \ | ||
| 32 | httpd_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 | |||
| 42 | static 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 | |||
| 69 | static 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 | |||
| 77 | static void write_and_die(int fd, const char *msg) | ||
| 78 | { | ||
| 79 | full_write(fd, msg, strlen(msg)); | ||
| 80 | exit(0); | ||
| 81 | } | ||
| 82 | |||
| 83 | static void write_and_die2(int fd, const char *msg, const char *msg2) | ||
| 84 | { | ||
| 85 | full_write2(fd, msg, msg2); | ||
| 86 | exit(0); | ||
| 87 | } | ||
| 88 | |||
| 89 | static 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 | |||
| 103 | static 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 | |||
| 114 | int 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 | |||
| 235 | exec: | ||
| 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 | ||
