rust: prevent dead_code warning
[nbdkit.git] / tests / web-server.c
blob799b1b2c04dcee5f12171c9543a58f32d7e20318
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);
291 fprintf (stderr, "web server: completed request\n");
294 fprintf (stderr, "web server: closing socket\n");
295 close (s);
298 static void
299 handle_file_request (int s, enum method method)
301 const bool headers_only = method == HEAD;
302 uint64_t offset, length, end;
303 const char *p;
304 const char response1_ok[] = "HTTP/1.1 200 OK\r\n";
305 const char response1_partial[] = "HTTP/1.1 206 Partial Content\r\n";
306 const char response2[] =
307 "Accept-rANGES: bytes\r\n" /* See RHBZ#1837337 */
308 "Connection: close\r\n"
309 "Content-Type: application/octet-stream\r\n";
310 char response3[64];
311 const char response4[] = "\r\n";
312 char *data;
314 /* If there's no Range request header then send the full size as the
315 * content-length.
317 p = strcasestr (request, "\r\nRange: bytes=");
318 if (p == NULL) {
319 offset = 0;
320 length = statbuf.st_size;
321 xwrite (s, response1_ok, strlen (response1_ok));
323 else {
324 p += 15;
325 if (sscanf (p, "%" SCNu64 "-%" SCNu64, &offset, &end) != 2) {
326 fprintf (stderr, "web server: could not parse "
327 "range request from curl client\n");
328 exit (EXIT_FAILURE);
330 /* Unclear but "Range: bytes=0-4" means bytes 0-3. '4' is the
331 * byte beyond the end of the range.
333 length = end - offset;
334 xwrite (s, response1_partial, strlen (response1_partial));
337 xwrite (s, response2, strlen (response2));
338 snprintf (response3, sizeof response3,
339 "Content-Length: %" PRIu64 "\r\n", length);
340 xwrite (s, response3, strlen (response3));
341 xwrite (s, response4, strlen (response4));
343 if (headers_only)
344 return;
346 /* Send the file content. */
347 data = malloc (length);
348 if (data == NULL) {
349 perror ("malloc");
350 exit (EXIT_FAILURE);
353 xpread (data, length, offset);
354 xwrite (s, data, length);
356 free (data);
359 /* Request for /mirror */
360 static void
361 handle_mirror_redirect_request (int s)
363 static char rr = '1'; /* round robin '1', '2', '3' */
364 /* Note we send 302 (temporary redirect), same as Fedora's mirrorservice. */
365 const char found[] = "HTTP/1.1 302 Found\r\nContent-Length: 0\r\n";
366 char location[] = "Location: /mirrorX\r\n";
367 char close_[] = "Connection: close\r\n";
368 const char eol[] = "\r\n";
370 location[17] = rr;
371 rr++;
372 if (rr == '4')
373 rr = '1';
375 xwrite (s, found, strlen (found));
376 xwrite (s, location, strlen (location));
377 xwrite (s, close_, strlen (close_));
378 xwrite (s, eol, strlen (eol));
381 static void
382 handle_mirror_data_request (int s, enum method method, char byte)
384 const bool headers_only = method == HEAD;
385 uint64_t offset, length, end;
386 const char *p;
387 const char response1_ok[] = "HTTP/1.1 200 OK\r\n";
388 const char response1_partial[] = "HTTP/1.1 206 Partial Content\r\n";
389 const char response2[] =
390 "Accept-rANGES: bytes\r\n" /* See RHBZ#1837337 */
391 "Connection: close\r\n"
392 "Content-Type: application/octet-stream\r\n";
393 char response3[64];
394 const char response4[] = "\r\n";
395 char *data;
397 /* If there's no Range request header then send the full size as the
398 * content-length.
400 p = strcasestr (request, "\r\nRange: bytes=");
401 if (p == NULL) {
402 offset = 0;
403 length = statbuf.st_size;
404 xwrite (s, response1_ok, strlen (response1_ok));
406 else {
407 p += 15;
408 if (sscanf (p, "%" SCNu64 "-%" SCNu64, &offset, &end) != 2) {
409 fprintf (stderr, "web server: could not parse "
410 "range request from curl client\n");
411 exit (EXIT_FAILURE);
413 /* Unclear but "Range: bytes=0-4" means bytes 0-3. '4' is the
414 * byte beyond the end of the range.
416 length = end - offset;
417 xwrite (s, response1_partial, strlen (response1_partial));
420 xwrite (s, response2, strlen (response2));
421 snprintf (response3, sizeof response3,
422 "Content-Length: %" PRIu64 "\r\n", length);
423 xwrite (s, response3, strlen (response3));
424 xwrite (s, response4, strlen (response4));
426 if (headers_only)
427 return;
429 /* Send the file content. */
430 data = malloc (length);
431 if (data == NULL) {
432 perror ("malloc");
433 exit (EXIT_FAILURE);
436 memset (data, byte, length);
437 xwrite (s, data, length);
439 free (data);
442 static void
443 send_404_not_found (int s)
445 const char response[] =
446 "HTTP/1.1 404 Not Found\r\n"
447 "Content-Length: 0\r\n"
448 "Connection: close\r\n"
449 "\r\n";
450 xwrite (s, response, strlen (response));
453 static void
454 send_405_method_not_allowed (int s)
456 const char response[] =
457 "HTTP/1.1 405 Method Not Allowed\r\n"
458 "Content-Length: 0\r\n"
459 "Connection: close\r\n"
460 "\r\n";
461 xwrite (s, response, strlen (response));
464 static void
465 send_500_internal_server_error (int s)
467 const char response[] =
468 "HTTP/1.1 500 Internal Server Error\r\n"
469 "Content-Length: 0\r\n"
470 "Connection: close\r\n"
471 "\r\n";
472 xwrite (s, response, strlen (response));
475 static void
476 xwrite (int s, const char *buf, size_t len)
478 ssize_t r;
480 while (len > 0) {
481 r = write (s, buf, len);
482 if (r == -1) {
483 perror ("write");
484 exit (EXIT_FAILURE);
486 buf += r;
487 len -= r;
491 static void
492 xpread (char *buf, size_t count, off_t offset)
494 ssize_t r;
496 while (count > 0) {
497 r = pread (fd, buf, count, offset);
498 if (r == -1) {
499 perror ("read");
500 exit (EXIT_FAILURE);
502 if (r == 0) {
503 fprintf (stderr, "pread: unexpected end of file\n");
504 exit (EXIT_FAILURE);
506 buf += r;
507 count -= r;
508 offset += r;