ci: Update .gitlab-ci.yml
[nbdkit.git] / tests / web-server.c
blob5a9484c0e93ceda9f529e423dfe86878673facd0
1 /* nbdkit
2 * Copyright (C) 2018-2020 Red Hat Inc.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
11 * * Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
15 * * Neither the name of Red Hat nor the names of its contributors may be
16 * used to endorse or promote products derived from this software without
17 * specific prior written permission.
19 * THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
21 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
23 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
26 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
29 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30 * SUCH DAMAGE.
33 /* See web-server.h */
35 #include <config.h>
37 #include <stdio.h>
38 #include <stdlib.h>
39 #include <stdbool.h>
40 #include <stdint.h>
41 #include <inttypes.h>
42 #include <string.h>
43 #include <fcntl.h>
44 #include <unistd.h>
45 #include <errno.h>
46 #include <sys/types.h>
47 #include <sys/stat.h>
48 #include <sys/socket.h>
49 #include <sys/un.h>
51 #include <pthread.h>
53 #include "web-server.h"
55 #ifndef SOCK_CLOEXEC
56 /* For this file, we don't care if fds are marked cloexec; leaking is okay. */
57 #define SOCK_CLOEXEC 0
58 #endif
60 enum method { HEAD, GET };
62 static char tmpdir[] = "/tmp/wsXXXXXX";
63 static char sockpath[] = "............./sock";
64 static int listen_sock = -1;
65 static int fd = -1;
66 static struct stat statbuf;
67 static char request[16384];
68 static check_request_t check_request;
70 static void *start_web_server (void *arg);
71 static void handle_requests (int s);
72 static void handle_file_request (int s, enum method method);
73 static void handle_mirror_redirect_request (int s);
74 static void handle_mirror_data_request (int s, enum method method, char byte);
75 static void send_404_not_found (int s);
76 static void send_405_method_not_allowed (int s);
77 static void send_500_internal_server_error (int s);
78 static void xwrite (int s, const char *buf, size_t len);
79 static void xpread (char *buf, size_t count, off_t offset);
81 static void
82 cleanup (void)
84 if (fd >= 0)
85 close (fd);
86 if (listen_sock >= 0)
87 close (listen_sock);
88 listen_sock = -1;
89 unlink (sockpath);
90 rmdir (tmpdir);
93 const char *
94 web_server (const char *filename, check_request_t _check_request)
96 struct sockaddr_un addr;
97 pthread_t thread;
98 int err;
100 check_request = _check_request;
102 /* Open the file. */
103 fd = open (filename, O_RDONLY|O_CLOEXEC);
104 if (fd == -1) {
105 perror (filename);
106 return NULL;
108 if (fstat (fd, &statbuf) == -1) {
109 perror ("stat");
110 goto err1;
113 /* Create the temporary directory for the socket. */
114 if (mkdtemp (tmpdir) == NULL) {
115 perror ("mkdtemp");
116 goto err1;
119 /* Create the listening socket for the web server. */
120 memcpy (sockpath, tmpdir, strlen (tmpdir));
121 listen_sock = socket (AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0);
122 if (listen_sock == -1) {
123 perror ("socket");
124 goto err2;
127 addr.sun_family = AF_UNIX;
128 memcpy (addr.sun_path, sockpath, strlen (sockpath) + 1);
129 if (bind (listen_sock, (struct sockaddr *) &addr, sizeof addr) == -1) {
130 perror (sockpath);
131 goto err3;
134 if (listen (listen_sock, SOMAXCONN) == -1) {
135 perror ("listen");
136 goto err4;
139 /* Run the web server in a separate thread. */
140 err = pthread_create (&thread, NULL, start_web_server, NULL);
141 if (err) {
142 errno = err;
143 perror ("pthread_create");
144 goto err4;
146 err = pthread_detach (thread);
147 if (err) {
148 errno = err;
149 perror ("pthread_detach");
150 goto err4;
153 atexit (cleanup);
155 return sockpath;
157 err4:
158 unlink (sockpath);
159 err3:
160 close (listen_sock);
161 err2:
162 rmdir (tmpdir);
163 err1:
164 close (fd);
165 return NULL;
168 static void *
169 start_web_server (void *arg)
171 fprintf (stderr, "web server: listening on %s\n", sockpath);
173 for (;;) {
174 int s = accept (listen_sock, NULL, NULL);
175 if (s == -1) {
176 /* This is not an error: The server has closed the socket in
177 * cleanup() because it is exiting, resulting in accept(2) above
178 * returning EBADF, so just exit the thread.
180 if (errno == EBADF)
181 return NULL;
182 perror ("web server: accept");
183 exit (EXIT_FAILURE);
185 handle_requests (s);
189 static void
190 handle_requests (int s)
192 bool eof = false;
194 fprintf (stderr, "web server: accepted connection\n");
196 while (!eof) {
197 size_t r, n, sz;
198 enum method method;
199 char path[128];
201 /* Read request until we see "\r\n\r\n" (end of headers) or EOF. */
202 n = 0;
203 for (;;) {
204 if (n >= sizeof request - 1 /* allow one byte for \0 */) {
205 fprintf (stderr, "web server: request too long\n");
206 exit (EXIT_FAILURE);
208 sz = sizeof request - n - 1;
209 r = read (s, &request[n], sz);
210 if (r == -1) {
211 perror ("read");
212 exit (EXIT_FAILURE);
214 if (r == 0) {
215 eof = true;
216 break;
218 n += r;
219 request[n] = '\0';
220 if (strstr (request, "\r\n\r\n"))
221 break;
224 if (n == 0)
225 continue;
227 fprintf (stderr, "web server: request:\n%s", request);
229 /* Call the optional user function to check the request. */
230 if (check_request) check_request (request);
232 /* Get the method and path fields from the first line. */
233 if (strncmp (request, "HEAD ", 5) == 0) {
234 method = HEAD;
235 n = strcspn (&request[5], " \n\t");
236 if (n >= sizeof path) {
237 send_500_internal_server_error (s);
238 eof = true;
239 break;
241 memcpy (path, &request[5], n);
242 path[n] = '\0';
244 else if (strncmp (request, "GET ", 4) == 0) {
245 method = GET;
246 n = strcspn (&request[4], " \n\t");
247 if (n >= sizeof path) {
248 send_500_internal_server_error (s);
249 eof = true;
250 break;
252 memcpy (path, &request[4], n);
253 path[n] = '\0';
255 else {
256 send_405_method_not_allowed (s);
257 eof = true;
258 break;
261 fprintf (stderr, "web server: requested path: %s\n", path);
263 /* For testing retry-request + curl:
264 * /mirror redirects round-robin to /mirror1, /mirror2, /mirror3
265 * /mirror1 returns a file of \x01 bytes
266 * /mirror2 returns a file of \x02 bytes
267 * /mirror3 returns 404 errors
268 * Anything else returns a 500 error
270 if (strcmp (path, "/mirror") == 0)
271 handle_mirror_redirect_request (s);
272 else if (strcmp (path, "/mirror1") == 0)
273 handle_mirror_data_request (s, method, 1);
274 else if (strcmp (path, "/mirror2") == 0)
275 handle_mirror_data_request (s, method, 2);
276 else if (strcmp (path, "/mirror3") == 0) {
277 send_404_not_found (s);
278 eof = true;
280 else if (strncmp (path, "/mirror", 7) == 0) {
281 send_500_internal_server_error (s);
282 eof = true;
285 /* Otherwise it's a regular file request. 'path' is ignored, we
286 * only serve a single file passed to web_server().
288 else
289 handle_file_request (s, method);
292 close (s);
295 static void
296 handle_file_request (int s, enum method method)
298 const bool headers_only = method == HEAD;
299 uint64_t offset, length, end;
300 const char *p;
301 const char response1_ok[] = "HTTP/1.1 200 OK\r\n";
302 const char response1_partial[] = "HTTP/1.1 206 Partial Content\r\n";
303 const char response2[] =
304 "Accept-rANGES: bytes\r\n" /* See RHBZ#1837337 */
305 "Connection: keep-alive\r\n"
306 "Content-Type: application/octet-stream\r\n";
307 char response3[64];
308 const char response4[] = "\r\n";
309 char *data;
311 /* If there's no Range request header then send the full size as the
312 * content-length.
314 p = strcasestr (request, "\r\nRange: bytes=");
315 if (p == NULL) {
316 offset = 0;
317 length = statbuf.st_size;
318 xwrite (s, response1_ok, strlen (response1_ok));
320 else {
321 p += 15;
322 if (sscanf (p, "%" SCNu64 "-%" SCNu64, &offset, &end) != 2) {
323 fprintf (stderr, "web server: could not parse "
324 "range request from curl client\n");
325 exit (EXIT_FAILURE);
327 /* Unclear but "Range: bytes=0-4" means bytes 0-3. '4' is the
328 * byte beyond the end of the range.
330 length = end - offset;
331 xwrite (s, response1_partial, strlen (response1_partial));
334 xwrite (s, response2, strlen (response2));
335 snprintf (response3, sizeof response3,
336 "Content-Length: %" PRIu64 "\r\n", length);
337 xwrite (s, response3, strlen (response3));
338 xwrite (s, response4, strlen (response4));
340 if (headers_only)
341 return;
343 /* Send the file content. */
344 data = malloc (length);
345 if (data == NULL) {
346 perror ("malloc");
347 exit (EXIT_FAILURE);
350 xpread (data, length, offset);
351 xwrite (s, data, length);
353 free (data);
356 /* Request for /mirror */
357 static void
358 handle_mirror_redirect_request (int s)
360 static char rr = '1'; /* round robin '1', '2', '3' */
361 /* Note we send 302 (temporary redirect), same as Fedora's mirrorservice. */
362 const char found[] = "HTTP/1.1 302 Found\r\nContent-Length: 0\r\n";
363 char location[] = "Location: /mirrorX\r\n";
364 const char eol[] = "\r\n";
366 location[17] = rr;
367 rr++;
368 if (rr == '4')
369 rr = '1';
371 xwrite (s, found, strlen (found));
372 xwrite (s, location, strlen (location));
373 xwrite (s, eol, strlen (eol));
376 static void
377 handle_mirror_data_request (int s, enum method method, char byte)
379 const bool headers_only = method == HEAD;
380 uint64_t offset, length, end;
381 const char *p;
382 const char response1_ok[] = "HTTP/1.1 200 OK\r\n";
383 const char response1_partial[] = "HTTP/1.1 206 Partial Content\r\n";
384 const char response2[] =
385 "Accept-rANGES: bytes\r\n" /* See RHBZ#1837337 */
386 "Connection: keep-alive\r\n"
387 "Content-Type: application/octet-stream\r\n";
388 char response3[64];
389 const char response4[] = "\r\n";
390 char *data;
392 /* If there's no Range request header then send the full size as the
393 * content-length.
395 p = strcasestr (request, "\r\nRange: bytes=");
396 if (p == NULL) {
397 offset = 0;
398 length = statbuf.st_size;
399 xwrite (s, response1_ok, strlen (response1_ok));
401 else {
402 p += 15;
403 if (sscanf (p, "%" SCNu64 "-%" SCNu64, &offset, &end) != 2) {
404 fprintf (stderr, "web server: could not parse "
405 "range request from curl client\n");
406 exit (EXIT_FAILURE);
408 /* Unclear but "Range: bytes=0-4" means bytes 0-3. '4' is the
409 * byte beyond the end of the range.
411 length = end - offset;
412 xwrite (s, response1_partial, strlen (response1_partial));
415 xwrite (s, response2, strlen (response2));
416 snprintf (response3, sizeof response3,
417 "Content-Length: %" PRIu64 "\r\n", length);
418 xwrite (s, response3, strlen (response3));
419 xwrite (s, response4, strlen (response4));
421 if (headers_only)
422 return;
424 /* Send the file content. */
425 data = malloc (length);
426 if (data == NULL) {
427 perror ("malloc");
428 exit (EXIT_FAILURE);
431 memset (data, byte, length);
432 xwrite (s, data, length);
434 free (data);
437 static void
438 send_404_not_found (int s)
440 const char response[] =
441 "HTTP/1.1 404 Not Found\r\n"
442 "Content-Length: 0\r\n"
443 "Connection: close\r\n"
444 "\r\n";
445 xwrite (s, response, strlen (response));
448 static void
449 send_405_method_not_allowed (int s)
451 const char response[] =
452 "HTTP/1.1 405 Method Not Allowed\r\n"
453 "Content-Length: 0\r\n"
454 "Connection: close\r\n"
455 "\r\n";
456 xwrite (s, response, strlen (response));
459 static void
460 send_500_internal_server_error (int s)
462 const char response[] =
463 "HTTP/1.1 500 Internal Server Error\r\n"
464 "Content-Length: 0\r\n"
465 "Connection: close\r\n"
466 "\r\n";
467 xwrite (s, response, strlen (response));
470 static void
471 xwrite (int s, const char *buf, size_t len)
473 ssize_t r;
475 while (len > 0) {
476 r = write (s, buf, len);
477 if (r == -1) {
478 perror ("write");
479 exit (EXIT_FAILURE);
481 buf += r;
482 len -= r;
486 static void
487 xpread (char *buf, size_t count, off_t offset)
489 ssize_t r;
491 while (count > 0) {
492 r = pread (fd, buf, count, offset);
493 if (r == -1) {
494 perror ("read");
495 exit (EXIT_FAILURE);
497 if (r == 0) {
498 fprintf (stderr, "pread: unexpected end of file\n");
499 exit (EXIT_FAILURE);
501 buf += r;
502 count -= r;
503 offset += r;