initital public commit
authorgit <redacted>
Sat, 23 May 2026 14:29:39 +0000 (10:29 -0400)
committergit <redacted>
Sat, 23 May 2026 14:29:39 +0000 (10:29 -0400)
Makefile [new file with mode: 0644]
README.md [new file with mode: 0644]
config.def.h [new file with mode: 0644]
config.mk [new file with mode: 0644]
example/1.png [new file with mode: 0644]
example/2.png [new file with mode: 0644]
example/LICENSE.txt [new file with mode: 0644]
example/Tuffy_Bold.ttf [new file with mode: 0644]
mage.c [new file with mode: 0644]

diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..1171f79
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,38 @@
+include config.mk
+
+TARGET = mage
+SRC = mage.c
+
+all: ${SRC}
+       ${CC} ${SRC} ${CFLAGS} ${LDFLAGS} -o ${TARGET}
+
+${SRC}: config.h
+
+config.h:
+       cp config.def.h $@
+
+clean:
+       rm -f *.[oa] ${TARGET}
+
+distclean: clean
+       rm -f mage.tar.gz
+
+dist: distclean
+       mkdir -p mage-dist
+       cp -R ${SRC} config.def.h config.mk Makefile example mage-dist/
+       tar -cf - mage-dist | gzip > mage.tar.gz
+       rm -rf mage-dist
+
+install:
+       mkdir -p ${PREFIX}/bin
+       cp -f ${TARGET} ${PREFIX}/bin
+       chmod 755 ${PREFIX}/bin/${TARGET}
+
+uninstall:
+       rm -f ${PREFIX}/bin/${TARGET}
+
+update:
+       cp -fp ${TARGET} ${PREFIX}/bin
+       ${RESTARTCMD}
+
+.PHONY: all clean distclean dist install uninstall update
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..aa30343
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+mage - tarpit assist image generating server
+
+Loads a set of images (and a single font) into memory and serves them via HTTP, modifying their hue, saturation and brightness.
+Log (in JSON) is printed to stdout, stderr is reserved for errors - launching mage with `-d` (daemonize) disables all output.
+
+Use this behind a rate-limiting reverse-proxy, as the server is build to provide maximum throughput. Using uncompressed and small images also helps with CPU loads.
+
+Access images at `http://host:{PORT}{PATH}/*`, any string in place of the wildcard will be overlaid on the image.
diff --git a/config.def.h b/config.def.h
new file mode 100644 (file)
index 0000000..60f9617
--- /dev/null
@@ -0,0 +1,28 @@
+/* can also be changed by adding -D"$MACRO_NAME"=# to CFLAGS */
+#ifndef PORT
+ #define PORT            8080
+#endif
+#ifndef PATH
+ #define PATH           "/mage/"
+#endif
+#ifndef NUM_THREADS
+ #define NUM_THREADS    2
+#endif
+#ifndef MAX_QUEUE
+ #define MAX_QUEUE     128
+#endif
+#ifndef BUFFER_SIZE
+ #define BUFFER_SIZE     4096
+#endif
+
+/*
+ * remember to use absolute paths when deploying
+ * daemons change their current directory to root (/)
+ */
+
+static const char fontpath[] = "./example/Tuffy_Bold.ttf";
+
+static const char *imgpaths[] = {
+       "./example/1.png",
+       "./example/2.png"
+};
diff --git a/config.mk b/config.mk
new file mode 100644 (file)
index 0000000..2844881
--- /dev/null
+++ b/config.mk
@@ -0,0 +1,10 @@
+PREFIX = /usr/local
+
+# the command to exectute on `make update`, eg.,
+# `systemctl restart mage.service`
+RESTARTCMD = echo "no command set"
+
+MAGICKFLAGS != pkg-config --cflags --libs MagickWand
+
+CFLAGS = -Wall -Wextra -Werror -Wpedantic -O2 -fstack-protector-strong -fstack-clash-protection -D_FORTIFY_SOURCE=2 -Wformat -Wformat-security -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack -fPIE -pie ${MAGICKFLAGS}
+LDFLAGS = -lpthread
diff --git a/example/1.png b/example/1.png
new file mode 100644 (file)
index 0000000..7b9aa89
Binary files /dev/null and b/example/1.png differ
diff --git a/example/2.png b/example/2.png
new file mode 100644 (file)
index 0000000..8223753
Binary files /dev/null and b/example/2.png differ
diff --git a/example/LICENSE.txt b/example/LICENSE.txt
new file mode 100644 (file)
index 0000000..defced0
--- /dev/null
@@ -0,0 +1,11 @@
+We, the copyright holders of this work, hereby release it into the
+public domain. This applies worldwide.
+
+In case this is not legally possible,
+
+We grant any entity the right to use this work for any purpose, without
+any conditions, unless such conditions are required by law.
+
+Thatcher Ulrich <tu@tulrich.com> http://tulrich.com
+Karoly Barta bartakarcsi@gmail.com
+Michael Evans http://www.evertype.com
diff --git a/example/Tuffy_Bold.ttf b/example/Tuffy_Bold.ttf
new file mode 100644 (file)
index 0000000..33fcc8a
Binary files /dev/null and b/example/Tuffy_Bold.ttf differ
diff --git a/mage.c b/mage.c
new file mode 100644 (file)
index 0000000..6de62b8
--- /dev/null
+++ b/mage.c
@@ -0,0 +1,584 @@
+#ifdef __linux__
+ #include <sys/epoll.h>
+#else
+ #include <sys/event.h>
+#endif
+#include <sys/stat.h>
+#include <sys/socket.h>
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <fcntl.h>
+#include <pthread.h>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <MagickWand/MagickWand.h>
+
+#include "config.h"
+
+#define NUM_IMAGES      (sizeof(imgpaths) / sizeof(imgpaths[0]))
+
+typedef struct {
+       struct sockaddr_in addr;
+       int sock;
+} ThreadArgs;
+
+typedef struct {
+       ThreadArgs clients[MAX_QUEUE];
+       int head;
+       int tail;
+       int count;
+       pthread_mutex_t lock;
+       pthread_cond_t  note;
+} ClientQueue;
+
+typedef struct {
+       unsigned char *data;
+       size_t size;
+} ImageBuffer;
+
+static void seedinit(void);
+static int imgloadbuff(const char *, ImageBuffer *);
+static int imgpreload(void);
+static char *b64e(const unsigned char *, size_t, size_t *);
+static char *fontpreload(void);
+static double randrange(double, double);
+static unsigned long randnext(void);
+static unsigned char *imggen(const unsigned char *, size_t,
+                             const char *, size_t *,
+                            double, double, double);
+static void terminatepath(char *, size_t);
+static char *getheader(const char *, const char *);
+static void handleclient(ThreadArgs *);
+static void *worker(void *);
+
+static ClientQueue queue;
+static ImageBuffer imgbuffs[NUM_IMAGES];
+static char *fonturi;
+/* TODO: remove compiler-specific __thread */
+static __thread char buffer[BUFFER_SIZE];
+static __thread char method[16], path[256], header[512];
+static unsigned long randstate = 0;
+static pthread_mutex_t random_mutex = PTHREAD_MUTEX_INITIALIZER;
+
+void
+seedinit(void)
+{
+       int fd;
+
+       /* Try urandom first, random as a fallback */
+       if ((fd = open("/dev/urandom", O_RDONLY)) < 0) {
+               fd = open("/dev/random", O_RDONLY);
+       }
+
+       if (fd < 0) {
+               warn("open random device");
+               randstate = 12345;   /* Fallback seed */
+               return;
+       }
+
+       if (read(fd, &randstate, sizeof(randstate)) <
+                (ssize_t)sizeof(randstate)) {
+               warn("read random device");
+               randstate = 12345;   /* Fallback seed */
+       }
+
+       close(fd);
+       printf("Random seed initialized: %lu\n", randstate);
+
+       return;
+}
+
+int
+imgloadbuff(const char *path, ImageBuffer *ibuffer)
+{
+       int fd;
+       ssize_t bytesr;
+       struct stat st;
+
+       fd = open(path, O_RDONLY);
+       if (fd < 0) {
+               warn("open image file");
+               return -1;
+       }
+
+       if (fstat(fd, &st) < 0) {
+               warn("fstat");
+               close(fd);
+               return -1;
+       }
+
+       ibuffer->size = st.st_size;
+       ibuffer->data = malloc(ibuffer->size);
+       if (!ibuffer->data) {
+               warnx("failed to allocate image buffer");
+               close(fd);
+               return -1;
+       }
+
+       bytesr = read(fd, ibuffer->data, ibuffer->size);
+       if (bytesr != (ssize_t)ibuffer->size) {
+               warn("read image file");
+               free(ibuffer->data);
+               ibuffer->data = NULL;
+               ibuffer->size = 0;
+               close(fd);
+               return -1;
+       }
+
+       close(fd);
+       printf("Loaded image: %s (%zu bytes)\n", path, ibuffer->size);
+
+       return 0;
+}
+
+
+int
+imgpreload(void)
+{
+       for (int i = 0; i < (int)NUM_IMAGES; i++) {
+               if (imgloadbuff(imgpaths[i], &imgbuffs[i]) < 0)
+               {
+                       warnx("failed to load image %d: %s", i,
+                             imgpaths[i]);
+                       return -1;
+               }
+       }
+
+       return 0;
+}
+
+char *
+b64e(const unsigned char *data, size_t ilen, size_t *olen)
+{
+       char *edata;
+       uint32_t octet_a, octet_b, octet_c, triple;
+       static const char etable[] =
+               "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+       *olen = 4 * ((ilen + 2) / 3);
+
+       edata = malloc(*olen + 1);
+       if (!edata) return NULL;
+
+       for (size_t i = 0, j = 0; i < ilen;) {
+               octet_a = i < ilen ? data[i++] : 0;
+               octet_b = i < ilen ? data[i++] : 0;
+               octet_c = i < ilen ? data[i++] : 0;
+
+               triple = (octet_a << 0x10) + (octet_b << 0x08) +
+                                 octet_c;
+
+               edata[j++] = etable[(triple >> 3 * 6) & 0x3F];
+               edata[j++] = etable[(triple >> 2 * 6) & 0x3F];
+               edata[j++] = (i > ilen + 1) ? '=' : etable[(triple >> 1 * 6) &
+                                                          0x3F];
+               edata[j++] = (i > ilen) ? '=' : etable[triple & 0x3F];
+       }
+
+       edata[*olen] = '\0';
+
+       return edata;
+}
+
+char *
+fontpreload(void)
+{
+       size_t fontsz, b64sz, readsz;
+       FILE *file;
+       char *b64font, *inlineuri;
+       unsigned char *fontbuffer;
+
+       file = fopen(fontpath, "rb");
+       if (!file)
+               err(1, "open font file");
+
+       fseek(file, 0, SEEK_END);
+       fontsz = ftell(file);
+       fseek(file, 0, SEEK_SET);
+
+       fontbuffer = malloc(fontsz);
+       if (!fontbuffer)
+               errx(1, "failed to allocate raw font");
+
+       readsz = fread(fontbuffer, 1, fontsz, file);
+       if (readsz < fontsz || ferror(file))
+               err(1, "read font file");
+
+       fclose(file);
+
+       b64font = b64e(fontbuffer, fontsz, &b64sz);
+       free(fontbuffer);
+
+       inlineuri = malloc(b64sz + 8);
+       if (!inlineuri)
+               errx(1, "failed to allocate base64 font");
+
+       sprintf(inlineuri, "inline:%s", b64font);
+       printf("Loaded font from: %s\n", fontpath);
+
+       return inlineuri;
+}
+
+/* Linear congruential generator */
+unsigned long
+randnext(void)
+{
+       unsigned long result;
+
+       pthread_mutex_lock(&random_mutex);
+       randstate = (randstate * 1103515245 + 12345) & 0x7fffffff;
+       result = randstate;
+       pthread_mutex_unlock(&random_mutex);
+
+       return result;
+}
+
+double
+randrange(double min, double max)
+{
+       unsigned long rand_val;
+       double normalized;
+
+       rand_val = randnext();
+       normalized = (double)rand_val / 0x7fffffff;
+
+       return min + (normalized * (max - min));
+}
+
+unsigned char *
+imggen(const unsigned char *imgdata, size_t imgsz,
+       const char *msg, size_t *output_size,
+       double hue, double saturation, double brightness)
+{
+       size_t data_size = 0;
+       unsigned char *result = NULL;
+
+       MagickWand *wand = NewMagickWand();
+       DrawingWand *dw = NewDrawingWand();
+       PixelWand *pw = NewPixelWand();
+
+       if (!MagickReadImageBlob(wand, imgdata, imgsz)) {
+               fprintf(stderr, "failed to read image from buffer");
+               goto cleanup;
+       }
+
+       MagickModulateImage(wand, brightness, saturation, hue);
+
+       PixelSetRed(pw, randrange(0.0, 1.0));
+       PixelSetGreen(pw, randrange(0.0, 1.0));
+       PixelSetBlue(pw, randrange(0.0, 1.0));
+       PixelSetAlpha(pw, 1.0); /* Fully opaque */
+
+       DrawSetFillColor(dw, pw);
+       DrawSetFont(dw, fonturi);
+       DrawSetFontSize(dw, (int)randrange(8.0, 200.0));
+       DrawSetGravity(dw, CenterGravity);
+
+       MagickAnnotateImage(wand, dw, saturation / 4, hue / 4, hue, msg);
+       result = MagickGetImageBlob(wand, &data_size);
+
+cleanup:
+       *output_size = data_size;
+       DestroyMagickWand(wand);
+       DestroyDrawingWand(dw);
+       DestroyPixelWand(pw);
+
+       return result;
+}
+
+void
+terminatepath(char *path, size_t len)
+{
+       char *current = path;
+
+       while (*current && *current != '\r' && current < path + len)
+               current++;
+
+       *current = '\0';
+
+       return;
+}
+
+char *
+getheader(const char *headerbuff, const char *find)
+{
+       if (!headerbuff) {
+               return NULL;
+       }
+
+       const size_t tlen = strlen(find);
+       char *current = (char *)headerbuff;
+
+       while (*current) {
+               if (strncasecmp(current, find, tlen) == 0) {
+                       char *vptr, *endptr;
+
+                       vptr = current + tlen;
+                       while (*vptr == ' ' || *vptr == '\t') {
+                               vptr++;
+                       }
+
+                       endptr = strchr(vptr, '\r');
+                       if (endptr) *endptr = '\0';
+
+                       return vptr;
+               }
+
+               current = strchr(current, '\n');
+               if (!current) {
+                       break;
+               }
+               current++;
+       }
+
+       return NULL;
+}
+
+void
+handleclient(ThreadArgs *args)
+{
+       int clientsock;
+       ssize_t recbytes;
+       static const char response_404[] = "HTTP/1.1 404 Not Found\r\n"
+                                  "Content-Type: text/html\r\n"
+                                  "Content-Length: 48\r\n\r\n"
+                                  "<html><body><h1>404 Not Found</h1></body></html>";
+
+       clientsock = args->sock;
+
+       recbytes = recv(clientsock, buffer, BUFFER_SIZE - 1, 0);
+       if (recbytes <= 0) {
+               close(clientsock);
+               return;
+       }
+
+       buffer[recbytes] = '\0';
+
+       /* Parse HTTP request */
+       /* method is unused, for now */
+       terminatepath(path, 256);
+
+       sscanf(buffer, "%15s %255s", method, path);
+
+       if (strncmp(path, PATH, sizeof(PATH) - 1) == 0) {
+               int imgid = (int)randrange(0.0, NUM_IMAGES);
+               size_t imgsz = 0;
+               double hue, saturation, brightness;
+               char hip[INET_ADDRSTRLEN];
+               char *hhip;
+               unsigned char *imgdata;
+
+               hue = randrange(10.0, 150.0);
+               saturation = randrange(10.0, 200.0);
+               brightness = randrange(30.0, 150.0);
+
+               inet_ntop(AF_INET, &(args->addr.sin_addr), hip, sizeof(hip));
+               hhip = getheader(buffer, "X-Forwarded-For:");
+               if (!hhip) hhip = hip;
+
+               imgdata = imggen(imgbuffs[imgid].data,
+                                imgbuffs[imgid].size,
+                                path + sizeof(PATH) - 1, &imgsz,
+                                hue, saturation, brightness);
+
+               printf("{ \"forwarded-for\": \"%s\", \"host\": \"%s\", \"image\": \"%d\", "
+                      "\"path\": \"%s\", \"size\": \"%zu\", \"hue\": \"%.2f\", "
+                      "\"saturation\": \"%.2f\", \"brightness\": \"%.2f\" }\n",
+                      hhip, hip, imgid, path + sizeof(PATH) - 1, imgsz, hue, saturation, brightness);
+
+               if (imgdata) {
+                       snprintf(header, sizeof(header),
+                                "HTTP/1.1 200 OK\r\n"
+                                "Content-Type: image/jpeg\r\n"
+                                "Content-Length: %zu\r\n"
+                                "Connection: close\r\n\r\n",
+                                imgsz);
+
+                       send(clientsock, header, strlen(header), 0);
+                       send(clientsock, imgdata, imgsz, 0);
+                       MagickRelinquishMemory(imgdata);
+               } else {
+                       fprintf(stderr, "failed to process image %d\n", imgid);
+                       send(clientsock, response_404, sizeof(response_404), 0);
+               }
+       } else {
+               send(clientsock, response_404, sizeof(response_404), 0);
+       }
+
+       close(clientsock);
+       return;
+}
+
+void *
+worker(void *unused)
+{
+       (void)unused;
+       ThreadArgs args;
+
+       for (;;) {
+               pthread_mutex_lock(&queue.lock);
+
+               while (queue.count == 0) {
+                       pthread_cond_wait(&queue.note, &queue.lock);
+               }
+
+               args.sock = queue.clients[queue.head].sock;
+               args.addr = queue.clients[queue.head].addr;
+               queue.head = (queue.head + 1) % MAX_QUEUE;
+               queue.count--;
+
+               pthread_mutex_unlock(&queue.lock);
+
+               handleclient(&args);
+       }
+
+       return NULL;
+}
+
+int
+main(int argc, char *argv[])
+{
+       int c, efd, opt, serversock;
+       const char *progname = argv[0];
+       pthread_t threads[NUM_THREADS];
+       struct sockaddr_in server_addr = { 0 };
+
+#ifdef __linux__
+       struct epoll_event event, events[1];
+#else
+       struct kevent kev, kevs[10];
+#endif
+
+       while ((c = getopt(argc, argv, "d")) >= 0) {
+               switch (c) {
+               case 'd':
+                       if (daemon(0, 0) < 0) err(1, "daemon");
+                       break;
+               default:
+                       fprintf(stderr, "usage: %s [-d]\n", progname);
+                       exit(2);
+               }
+       }
+
+       MagickWandGenesis();
+       seedinit();
+
+       serversock = socket(AF_INET, SOCK_STREAM, 0);
+       if (serversock < 0)
+               err(1, "socket");
+
+       if (imgpreload() < 0)
+               errx(1, "failed to preload images");
+
+       fonturi = fontpreload();
+       if (!fonturi)
+               errx(1, "failed to preload the font");
+
+       opt = 1;
+       setsockopt(serversock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+
+       server_addr.sin_family = AF_INET;
+       server_addr.sin_addr.s_addr = INADDR_ANY;
+       server_addr.sin_port = htons(PORT);
+
+       if (bind(serversock, (struct sockaddr *) &server_addr,
+                sizeof(server_addr)) < 0)
+               err(1, "bind");
+
+       if (listen(serversock, 1024) < 0)
+               err(1, "listen");
+
+       printf("Server listening on port %d\n", PORT);
+       printf("Access images at: http://localhost:%d" PATH "\n", PORT);
+
+       pthread_mutex_init(&queue.lock, NULL);
+       pthread_cond_init(&queue.note, NULL);
+
+       for (int i = 0; i < NUM_THREADS; i++) {
+               pthread_create(&threads[i], NULL, worker, NULL);
+               pthread_detach(threads[i]);
+       }
+
+#ifdef __linux__
+       if ((efd = epoll_create1(0)) < 0)
+               err(1, "epoll_create1");
+
+       event.events = EPOLLIN;
+       event.data.fd = serversock;
+       epoll_ctl(efd, EPOLL_CTL_ADD, serversock, &event);
+#else
+       if ((efd = kqueue()) < 0)
+               err(1, "kqueue");
+
+       EV_SET(&kev, serversock, EVFILT_READ, EV_ADD, 0, 0, NULL);
+       if (kevent(efd, &kev, 1, NULL, 0, NULL) < 0)
+               err(1, "kevent");
+#endif
+
+
+       for (;;) {
+               int nfds;
+
+#ifdef __linux__
+               if ((nfds = epoll_wait(efd, events, 1 , -1)) < 0) {
+                       perror("epoll_wait");
+                       continue;
+               }
+
+
+               if (events[0].events & EPOLLIN) {
+#else
+               if ((nfds = kevent(efd, NULL, 0, kevs, 10, NULL)) < 0) {
+                       perror("kevent");
+                       continue;
+               }
+
+               for (int i = 0; i < nfds; i++) {
+                       if(kevs[i].ident == (uintptr_t)serversock) {
+#endif
+               /*
+                * This goes inside the for loop above in case of kqueue/kevent
+                */
+                       ThreadArgs args;
+                       socklen_t clientaddrlen = sizeof(args.addr);
+
+                       args.sock = accept(serversock, (struct sockaddr *)&args.addr,
+                                          &clientaddrlen);
+
+                       if (args.sock < 0) {
+                               perror("accept");
+                               continue;
+                       }
+
+                       pthread_mutex_lock(&queue.lock);
+
+                       if (queue.count >= MAX_QUEUE) {
+                               close(args.sock);
+                               fprintf(stderr, "Client queue full, rejecting\n");
+                       } else {
+                               queue.clients[queue.tail].sock = args.sock;
+                               queue.clients[queue.tail].addr = args.addr;
+                               queue.tail = (queue.tail + 1) % MAX_QUEUE;
+                               queue.count++;
+                               pthread_cond_signal(&queue.note);
+                       }
+
+                       pthread_mutex_unlock(&queue.lock);
+               }
+#ifndef __linux__
+               } /* closing the kevent for loop */
+#endif
+       }
+       close(efd);
+       close(serversock);
+       MagickWandTerminus();
+
+       return 0;
+}