6de62b8859fcd41dd234f7b96f911cd2a9c928d0
[mage.git] / mage.c
1 #ifdef __linux__
2 #include <sys/epoll.h>
3 #else
4 #include <sys/event.h>
5 #endif
6 #include <sys/stat.h>
7 #include <sys/socket.h>
8
9 #include <stdint.h>
10 #include <stdio.h>
11 #include <stdlib.h>
12 #include <string.h>
13 #include <unistd.h>
14
15 #include <ctype.h>
16 #include <err.h>
17 #include <fcntl.h>
18 #include <pthread.h>
19
20 #include <arpa/inet.h>
21 #include <netinet/in.h>
22 #include <MagickWand/MagickWand.h>
23
24 #include "config.h"
25
26 #define NUM_IMAGES (sizeof(imgpaths) / sizeof(imgpaths[0]))
27
28 typedef struct {
29 struct sockaddr_in addr;
30 int sock;
31 } ThreadArgs;
32
33 typedef struct {
34 ThreadArgs clients[MAX_QUEUE];
35 int head;
36 int tail;
37 int count;
38 pthread_mutex_t lock;
39 pthread_cond_t note;
40 } ClientQueue;
41
42 typedef struct {
43 unsigned char *data;
44 size_t size;
45 } ImageBuffer;
46
47 static void seedinit(void);
48 static int imgloadbuff(const char *, ImageBuffer *);
49 static int imgpreload(void);
50 static char *b64e(const unsigned char *, size_t, size_t *);
51 static char *fontpreload(void);
52 static double randrange(double, double);
53 static unsigned long randnext(void);
54 static unsigned char *imggen(const unsigned char *, size_t,
55 const char *, size_t *,
56 double, double, double);
57 static void terminatepath(char *, size_t);
58 static char *getheader(const char *, const char *);
59 static void handleclient(ThreadArgs *);
60 static void *worker(void *);
61
62 static ClientQueue queue;
63 static ImageBuffer imgbuffs[NUM_IMAGES];
64 static char *fonturi;
65 /* TODO: remove compiler-specific __thread */
66 static __thread char buffer[BUFFER_SIZE];
67 static __thread char method[16], path[256], header[512];
68 static unsigned long randstate = 0;
69 static pthread_mutex_t random_mutex = PTHREAD_MUTEX_INITIALIZER;
70
71 void
72 seedinit(void)
73 {
74 int fd;
75
76 /* Try urandom first, random as a fallback */
77 if ((fd = open("/dev/urandom", O_RDONLY)) < 0) {
78 fd = open("/dev/random", O_RDONLY);
79 }
80
81 if (fd < 0) {
82 warn("open random device");
83 randstate = 12345; /* Fallback seed */
84 return;
85 }
86
87 if (read(fd, &randstate, sizeof(randstate)) <
88 (ssize_t)sizeof(randstate)) {
89 warn("read random device");
90 randstate = 12345; /* Fallback seed */
91 }
92
93 close(fd);
94 printf("Random seed initialized: %lu\n", randstate);
95
96 return;
97 }
98
99 int
100 imgloadbuff(const char *path, ImageBuffer *ibuffer)
101 {
102 int fd;
103 ssize_t bytesr;
104 struct stat st;
105
106 fd = open(path, O_RDONLY);
107 if (fd < 0) {
108 warn("open image file");
109 return -1;
110 }
111
112 if (fstat(fd, &st) < 0) {
113 warn("fstat");
114 close(fd);
115 return -1;
116 }
117
118 ibuffer->size = st.st_size;
119 ibuffer->data = malloc(ibuffer->size);
120 if (!ibuffer->data) {
121 warnx("failed to allocate image buffer");
122 close(fd);
123 return -1;
124 }
125
126 bytesr = read(fd, ibuffer->data, ibuffer->size);
127 if (bytesr != (ssize_t)ibuffer->size) {
128 warn("read image file");
129 free(ibuffer->data);
130 ibuffer->data = NULL;
131 ibuffer->size = 0;
132 close(fd);
133 return -1;
134 }
135
136 close(fd);
137 printf("Loaded image: %s (%zu bytes)\n", path, ibuffer->size);
138
139 return 0;
140 }
141
142
143 int
144 imgpreload(void)
145 {
146 for (int i = 0; i < (int)NUM_IMAGES; i++) {
147 if (imgloadbuff(imgpaths[i], &imgbuffs[i]) < 0)
148 {
149 warnx("failed to load image %d: %s", i,
150 imgpaths[i]);
151 return -1;
152 }
153 }
154
155 return 0;
156 }
157
158 char *
159 b64e(const unsigned char *data, size_t ilen, size_t *olen)
160 {
161 char *edata;
162 uint32_t octet_a, octet_b, octet_c, triple;
163 static const char etable[] =
164 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
165 *olen = 4 * ((ilen + 2) / 3);
166
167 edata = malloc(*olen + 1);
168 if (!edata) return NULL;
169
170 for (size_t i = 0, j = 0; i < ilen;) {
171 octet_a = i < ilen ? data[i++] : 0;
172 octet_b = i < ilen ? data[i++] : 0;
173 octet_c = i < ilen ? data[i++] : 0;
174
175 triple = (octet_a << 0x10) + (octet_b << 0x08) +
176 octet_c;
177
178 edata[j++] = etable[(triple >> 3 * 6) & 0x3F];
179 edata[j++] = etable[(triple >> 2 * 6) & 0x3F];
180 edata[j++] = (i > ilen + 1) ? '=' : etable[(triple >> 1 * 6) &
181 0x3F];
182 edata[j++] = (i > ilen) ? '=' : etable[triple & 0x3F];
183 }
184
185 edata[*olen] = '\0';
186
187 return edata;
188 }
189
190 char *
191 fontpreload(void)
192 {
193 size_t fontsz, b64sz, readsz;
194 FILE *file;
195 char *b64font, *inlineuri;
196 unsigned char *fontbuffer;
197
198 file = fopen(fontpath, "rb");
199 if (!file)
200 err(1, "open font file");
201
202 fseek(file, 0, SEEK_END);
203 fontsz = ftell(file);
204 fseek(file, 0, SEEK_SET);
205
206 fontbuffer = malloc(fontsz);
207 if (!fontbuffer)
208 errx(1, "failed to allocate raw font");
209
210 readsz = fread(fontbuffer, 1, fontsz, file);
211 if (readsz < fontsz || ferror(file))
212 err(1, "read font file");
213
214 fclose(file);
215
216 b64font = b64e(fontbuffer, fontsz, &b64sz);
217 free(fontbuffer);
218
219 inlineuri = malloc(b64sz + 8);
220 if (!inlineuri)
221 errx(1, "failed to allocate base64 font");
222
223 sprintf(inlineuri, "inline:%s", b64font);
224 printf("Loaded font from: %s\n", fontpath);
225
226 return inlineuri;
227 }
228
229 /* Linear congruential generator */
230 unsigned long
231 randnext(void)
232 {
233 unsigned long result;
234
235 pthread_mutex_lock(&random_mutex);
236 randstate = (randstate * 1103515245 + 12345) & 0x7fffffff;
237 result = randstate;
238 pthread_mutex_unlock(&random_mutex);
239
240 return result;
241 }
242
243 double
244 randrange(double min, double max)
245 {
246 unsigned long rand_val;
247 double normalized;
248
249 rand_val = randnext();
250 normalized = (double)rand_val / 0x7fffffff;
251
252 return min + (normalized * (max - min));
253 }
254
255 unsigned char *
256 imggen(const unsigned char *imgdata, size_t imgsz,
257 const char *msg, size_t *output_size,
258 double hue, double saturation, double brightness)
259 {
260 size_t data_size = 0;
261 unsigned char *result = NULL;
262
263 MagickWand *wand = NewMagickWand();
264 DrawingWand *dw = NewDrawingWand();
265 PixelWand *pw = NewPixelWand();
266
267 if (!MagickReadImageBlob(wand, imgdata, imgsz)) {
268 fprintf(stderr, "failed to read image from buffer");
269 goto cleanup;
270 }
271
272 MagickModulateImage(wand, brightness, saturation, hue);
273
274 PixelSetRed(pw, randrange(0.0, 1.0));
275 PixelSetGreen(pw, randrange(0.0, 1.0));
276 PixelSetBlue(pw, randrange(0.0, 1.0));
277 PixelSetAlpha(pw, 1.0); /* Fully opaque */
278
279 DrawSetFillColor(dw, pw);
280 DrawSetFont(dw, fonturi);
281 DrawSetFontSize(dw, (int)randrange(8.0, 200.0));
282 DrawSetGravity(dw, CenterGravity);
283
284 MagickAnnotateImage(wand, dw, saturation / 4, hue / 4, hue, msg);
285 result = MagickGetImageBlob(wand, &data_size);
286
287 cleanup:
288 *output_size = data_size;
289 DestroyMagickWand(wand);
290 DestroyDrawingWand(dw);
291 DestroyPixelWand(pw);
292
293 return result;
294 }
295
296 void
297 terminatepath(char *path, size_t len)
298 {
299 char *current = path;
300
301 while (*current && *current != '\r' && current < path + len)
302 current++;
303
304 *current = '\0';
305
306 return;
307 }
308
309 char *
310 getheader(const char *headerbuff, const char *find)
311 {
312 if (!headerbuff) {
313 return NULL;
314 }
315
316 const size_t tlen = strlen(find);
317 char *current = (char *)headerbuff;
318
319 while (*current) {
320 if (strncasecmp(current, find, tlen) == 0) {
321 char *vptr, *endptr;
322
323 vptr = current + tlen;
324 while (*vptr == ' ' || *vptr == '\t') {
325 vptr++;
326 }
327
328 endptr = strchr(vptr, '\r');
329 if (endptr) *endptr = '\0';
330
331 return vptr;
332 }
333
334 current = strchr(current, '\n');
335 if (!current) {
336 break;
337 }
338 current++;
339 }
340
341 return NULL;
342 }
343
344 void
345 handleclient(ThreadArgs *args)
346 {
347 int clientsock;
348 ssize_t recbytes;
349 static const char response_404[] = "HTTP/1.1 404 Not Found\r\n"
350 "Content-Type: text/html\r\n"
351 "Content-Length: 48\r\n\r\n"
352 "<html><body><h1>404 Not Found</h1></body></html>";
353
354 clientsock = args->sock;
355
356 recbytes = recv(clientsock, buffer, BUFFER_SIZE - 1, 0);
357 if (recbytes <= 0) {
358 close(clientsock);
359 return;
360 }
361
362 buffer[recbytes] = '\0';
363
364 /* Parse HTTP request */
365 /* method is unused, for now */
366 terminatepath(path, 256);
367
368 sscanf(buffer, "%15s %255s", method, path);
369
370 if (strncmp(path, PATH, sizeof(PATH) - 1) == 0) {
371 int imgid = (int)randrange(0.0, NUM_IMAGES);
372 size_t imgsz = 0;
373 double hue, saturation, brightness;
374 char hip[INET_ADDRSTRLEN];
375 char *hhip;
376 unsigned char *imgdata;
377
378 hue = randrange(10.0, 150.0);
379 saturation = randrange(10.0, 200.0);
380 brightness = randrange(30.0, 150.0);
381
382 inet_ntop(AF_INET, &(args->addr.sin_addr), hip, sizeof(hip));
383 hhip = getheader(buffer, "X-Forwarded-For:");
384 if (!hhip) hhip = hip;
385
386 imgdata = imggen(imgbuffs[imgid].data,
387 imgbuffs[imgid].size,
388 path + sizeof(PATH) - 1, &imgsz,
389 hue, saturation, brightness);
390
391 printf("{ \"forwarded-for\": \"%s\", \"host\": \"%s\", \"image\": \"%d\", "
392 "\"path\": \"%s\", \"size\": \"%zu\", \"hue\": \"%.2f\", "
393 "\"saturation\": \"%.2f\", \"brightness\": \"%.2f\" }\n",
394 hhip, hip, imgid, path + sizeof(PATH) - 1, imgsz, hue, saturation, brightness);
395
396 if (imgdata) {
397 snprintf(header, sizeof(header),
398 "HTTP/1.1 200 OK\r\n"
399 "Content-Type: image/jpeg\r\n"
400 "Content-Length: %zu\r\n"
401 "Connection: close\r\n\r\n",
402 imgsz);
403
404 send(clientsock, header, strlen(header), 0);
405 send(clientsock, imgdata, imgsz, 0);
406 MagickRelinquishMemory(imgdata);
407 } else {
408 fprintf(stderr, "failed to process image %d\n", imgid);
409 send(clientsock, response_404, sizeof(response_404), 0);
410 }
411 } else {
412 send(clientsock, response_404, sizeof(response_404), 0);
413 }
414
415 close(clientsock);
416 return;
417 }
418
419 void *
420 worker(void *unused)
421 {
422 (void)unused;
423 ThreadArgs args;
424
425 for (;;) {
426 pthread_mutex_lock(&queue.lock);
427
428 while (queue.count == 0) {
429 pthread_cond_wait(&queue.note, &queue.lock);
430 }
431
432 args.sock = queue.clients[queue.head].sock;
433 args.addr = queue.clients[queue.head].addr;
434 queue.head = (queue.head + 1) % MAX_QUEUE;
435 queue.count--;
436
437 pthread_mutex_unlock(&queue.lock);
438
439 handleclient(&args);
440 }
441
442 return NULL;
443 }
444
445 int
446 main(int argc, char *argv[])
447 {
448 int c, efd, opt, serversock;
449 const char *progname = argv[0];
450 pthread_t threads[NUM_THREADS];
451 struct sockaddr_in server_addr = { 0 };
452
453 #ifdef __linux__
454 struct epoll_event event, events[1];
455 #else
456 struct kevent kev, kevs[10];
457 #endif
458
459 while ((c = getopt(argc, argv, "d")) >= 0) {
460 switch (c) {
461 case 'd':
462 if (daemon(0, 0) < 0) err(1, "daemon");
463 break;
464 default:
465 fprintf(stderr, "usage: %s [-d]\n", progname);
466 exit(2);
467 }
468 }
469
470 MagickWandGenesis();
471 seedinit();
472
473 serversock = socket(AF_INET, SOCK_STREAM, 0);
474 if (serversock < 0)
475 err(1, "socket");
476
477 if (imgpreload() < 0)
478 errx(1, "failed to preload images");
479
480 fonturi = fontpreload();
481 if (!fonturi)
482 errx(1, "failed to preload the font");
483
484 opt = 1;
485 setsockopt(serversock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
486
487 server_addr.sin_family = AF_INET;
488 server_addr.sin_addr.s_addr = INADDR_ANY;
489 server_addr.sin_port = htons(PORT);
490
491 if (bind(serversock, (struct sockaddr *) &server_addr,
492 sizeof(server_addr)) < 0)
493 err(1, "bind");
494
495 if (listen(serversock, 1024) < 0)
496 err(1, "listen");
497
498 printf("Server listening on port %d\n", PORT);
499 printf("Access images at: http://localhost:%d" PATH "\n", PORT);
500
501 pthread_mutex_init(&queue.lock, NULL);
502 pthread_cond_init(&queue.note, NULL);
503
504 for (int i = 0; i < NUM_THREADS; i++) {
505 pthread_create(&threads[i], NULL, worker, NULL);
506 pthread_detach(threads[i]);
507 }
508
509 #ifdef __linux__
510 if ((efd = epoll_create1(0)) < 0)
511 err(1, "epoll_create1");
512
513 event.events = EPOLLIN;
514 event.data.fd = serversock;
515 epoll_ctl(efd, EPOLL_CTL_ADD, serversock, &event);
516 #else
517 if ((efd = kqueue()) < 0)
518 err(1, "kqueue");
519
520 EV_SET(&kev, serversock, EVFILT_READ, EV_ADD, 0, 0, NULL);
521 if (kevent(efd, &kev, 1, NULL, 0, NULL) < 0)
522 err(1, "kevent");
523 #endif
524
525
526 for (;;) {
527 int nfds;
528
529 #ifdef __linux__
530 if ((nfds = epoll_wait(efd, events, 1 , -1)) < 0) {
531 perror("epoll_wait");
532 continue;
533 }
534
535
536 if (events[0].events & EPOLLIN) {
537 #else
538 if ((nfds = kevent(efd, NULL, 0, kevs, 10, NULL)) < 0) {
539 perror("kevent");
540 continue;
541 }
542
543 for (int i = 0; i < nfds; i++) {
544 if(kevs[i].ident == (uintptr_t)serversock) {
545 #endif
546 /*
547 * This goes inside the for loop above in case of kqueue/kevent
548 */
549 ThreadArgs args;
550 socklen_t clientaddrlen = sizeof(args.addr);
551
552 args.sock = accept(serversock, (struct sockaddr *)&args.addr,
553 &clientaddrlen);
554
555 if (args.sock < 0) {
556 perror("accept");
557 continue;
558 }
559
560 pthread_mutex_lock(&queue.lock);
561
562 if (queue.count >= MAX_QUEUE) {
563 close(args.sock);
564 fprintf(stderr, "Client queue full, rejecting\n");
565 } else {
566 queue.clients[queue.tail].sock = args.sock;
567 queue.clients[queue.tail].addr = args.addr;
568 queue.tail = (queue.tail + 1) % MAX_QUEUE;
569 queue.count++;
570 pthread_cond_signal(&queue.note);
571 }
572
573 pthread_mutex_unlock(&queue.lock);
574 }
575 #ifndef __linux__
576 } /* closing the kevent for loop */
577 #endif
578 }
579 close(efd);
580 close(serversock);
581 MagickWandTerminus();
582
583 return 0;
584 }