From: git Date: Sat, 23 May 2026 14:29:39 +0000 (-0400) Subject: initital public commit X-Git-Url: https://git.datadissipation.net/?a=commitdiff_plain;h=bbe1c82f5e636335f93fb7f921dfe44afc1e3189;p=mage.git initital public commit --- bbe1c82f5e636335f93fb7f921dfe44afc1e3189 diff --git a/Makefile b/Makefile new file mode 100644 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 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 index 0000000..60f9617 --- /dev/null +++ b/config.def.h @@ -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 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 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 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 index 0000000..defced0 --- /dev/null +++ b/example/LICENSE.txt @@ -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 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 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 index 0000000..6de62b8 --- /dev/null +++ b/mage.c @@ -0,0 +1,584 @@ +#ifdef __linux__ + #include +#else + #include +#endif +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#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" + "

404 Not Found

"; + + 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; +}