--- /dev/null
+/*
+ * test82: test HTTP with a remote server (is $USENETWORK="yes")
+ */
+
+#define DEBUG 0
+
+#if DEBUG
+#define dbgprintf(...) do { \
+ fprintf(stderr, "[%s:%s:%d %d] ", \
+ __FILE__, __FUNCTION__, \
+ __LINE__, getpid()); \
+ fprintf(stderr, __VA_ARGS__); \
+ fflush(stderr); \
+ } while (0)
+#else
+#define dbgprintf(...)
+#endif
+
+#include <arpa/inet.h>
+#include <assert.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+
+#include "common.h"
+
+#define FAIL(...) fail(__FILE__, __FUNCTION__, __LINE__, __VA_ARGS__)
+#define CLOSE(fd) do { assert(fd >= 0); if (close((fd)) != 0) FAIL("close failed"); } while (0);
+#define REALLOC(p, size) do { p = realloc(p, size); if (!p) FAIL("realloc of %zu bytes failed", size); } while (0);
+
+#define HOST "test82.minix3.org"
+#define PORT 80
+#define PATH1 "/test1.txt"
+#define PATH1_DATA "Hello world\n"
+#define PATH2 "/test2.bin"
+
+static void callback_verify_path1(const void *data, size_t size);
+static void callback_verify_path2(const void *data, size_t size);
+
+#define URL_COUNT 2
+
+struct url {
+ const char *host;
+ int port;
+ const char *path;
+ void (* callback_verify)(const void *data, size_t size);
+};
+
+static const struct url urls[URL_COUNT] = {
+ { HOST, PORT, PATH1, callback_verify_path1 },
+ { HOST, PORT, PATH2, callback_verify_path2 },
+};
+
+static void fail(const char *file, const char *func, int line,
+ const char *fmt, ...) __attribute__ ((format(printf, 4, 5)));
+
+static void fail(const char *file, const char *func, int line,
+ const char *fmt, ...) {
+ va_list ap;
+ char buf[1024];
+ size_t len;
+
+ assert(file);
+ assert(func);
+ assert(fmt);
+
+ len = snprintf(buf, sizeof(buf), "[%s:%s:%d] ", file, func, line);
+
+ va_start(ap, fmt);
+ len += vsnprintf(buf + len, sizeof(buf) - len, fmt, ap);
+ va_end(ap);
+
+ snprintf(buf + len, sizeof(buf) - len, " errno=%d error=%s",
+ errno, strerror(errno));
+
+ em(line, buf);
+}
+
+static int http_connect(const char *host, int port) {
+ struct addrinfo *addr = NULL;
+ int fd = -1;
+ struct addrinfo hints = {
+ .ai_family = PF_INET,
+ .ai_socktype = SOCK_STREAM,
+ };
+ char serv[12];
+
+ assert(host);
+
+ snprintf(serv, sizeof(serv), "%d", port);
+
+ errno = 0;
+ if (getaddrinfo(host, serv, &hints, &addr) != 0 || !addr) {
+ FAIL("host %s not found", host);
+ goto failure;
+ }
+
+ fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (fd < 0) {
+ FAIL("cannot create socket");
+ goto failure;
+ }
+
+ if (connect(fd, addr->ai_addr, addr->ai_addrlen) != 0) {
+ FAIL("cannot connect to %s:%d", host, port);
+ goto failure;
+ }
+
+ freeaddrinfo(addr);
+ return fd;
+
+failure:
+ if (fd >= 0) CLOSE(fd);
+ if (addr) freeaddrinfo(addr);
+ return -1;
+}
+
+static void write_chunked(
+ int fd,
+ const char *data,
+ size_t size,
+ size_t chunksize) {
+ ssize_t r;
+ size_t s;
+
+ assert(fd >= 0);
+ assert(data);
+ assert(chunksize > 0);
+
+ while (size > 0) {
+ s = chunksize;
+ if (s > size) s = size;
+
+ errno = 0;
+ r = write(fd, data, s);
+ if (r <= 0 || (size_t) r > s) {
+ errno = 0;
+ FAIL("write of %zu bytes failed with result %zd", s, r);
+ break;
+ }
+
+ data += r;
+ size -= r;
+ }
+}
+
+static void http_send_request(
+ int fd,
+ const char *host,
+ const char *path,
+ size_t chunksize,
+ int bigrequest) {
+ char buf[8192];
+ size_t len;
+ int lineno;
+
+ assert(fd >= 0);
+ assert(host);
+ assert(path);
+ assert(chunksize > 0);
+
+ /* http://tools.ietf.org/html/rfc2616#section-5 */
+ len = snprintf(buf, sizeof(buf),
+ "GET %s HTTP/1.1\r\n"
+ "Host: %s\r\n",
+ path, host);
+ if (bigrequest) {
+ lineno = 0;
+ while (len + 24 < sizeof(buf)) {
+ len += snprintf(buf + len, sizeof(buf) - len,
+ "X-Padding%d: %d\r\n",
+ lineno, lineno);
+ lineno++;
+ }
+ }
+ len += snprintf(buf + len, sizeof(buf) - len, "\r\n");
+
+ dbgprintf("sending request:\n%.*s", (int) len, buf);
+ write_chunked(fd, buf, len, chunksize);
+}
+
+static int is_whitespace(char c) {
+ return c == ' ' || c == '\t';
+}
+
+static int is_whitespace_or_linebreak(char c) {
+ return is_whitespace(c) || c == '\r' || c == '\n';
+}
+
+static int is_numeric(char c) {
+ return c >= '0' && c <= '9';
+}
+
+static int http_get_header_line(
+ const char *data,
+ size_t len,
+ size_t *index_p,
+ size_t *linelen_p) {
+ int has_cr;
+ size_t index;
+ size_t linelen;
+
+ assert(data);
+ assert(index_p);
+ assert(*index_p <= len);
+ assert(linelen_p);
+
+ /* starting the next line with whitespace means the line is continued */
+ index = *index_p;
+ do {
+ while (index < len && data[index] != '\n') index++;
+ if (index >= len) goto notfound;
+ index++;
+ } while (index < len && is_whitespace(data[index]));
+
+ /* exclude LF or CR+LF from line length */
+ assert(index - 1 >= *index_p && data[index - 1] == '\n');
+ has_cr = (index - 2 >= *index_p) && data[index - 2] == '\r';
+ linelen = index - *index_p - (has_cr ? 2 : 1);
+
+ /* if LF is the last character in the buffer, the line may be continued
+ * when more data is retrieved unless we reached the end of the headers
+ */
+ if (index >= len && linelen > 0) goto notfound;
+
+ *linelen_p = linelen;
+ *index_p = index;
+ return 1;
+
+notfound:
+ *linelen_p = 0;
+ *index_p = index;
+ return 0;
+}
+
+static int http_get_status_line(
+ const char *data,
+ size_t len,
+ size_t *index_p,
+ int *error_p,
+ int *code_p) {
+ int code, i;
+ size_t index;
+
+ assert(data);
+ assert(index_p);
+ assert(*index_p <= len);
+ assert(error_p);
+ assert(*error_p == 0);
+ assert(code_p);
+
+ /* skip leading whitespace/blank lines */
+ index = *index_p;
+ while (index < len && is_whitespace_or_linebreak(data[index])) index++;
+
+ /* parse version */
+ while (index < len && !is_whitespace(data[index])) index++;
+
+ /* skip separator */
+ while (index < len && is_whitespace(data[index])) index++;
+
+ /* parse status code */
+ code = 0;
+ for (i = 0; i < 3; i++) {
+ if (index >= len) goto notfound;
+ if (!is_numeric(data[index])) {
+ errno = 0;
+ FAIL("HTTP error: bad status line: \"%.*s\"",
+ (int) (index - *index_p), data + *index_p);
+ *error_p = 1;
+ goto notfound;
+ }
+ code = code * 10 + (data[index++] - '0');
+ }
+
+ /* skip separator */
+ while (index < len && is_whitespace(data[index])) index++;
+
+ /* parse reason phrase */
+ while (index < len && data[index] != '\n') index++;
+ if (index >= len) goto notfound;
+ index++;
+
+ *code_p = code;
+ *index_p = index;
+ return 1;
+
+notfound:
+ *code_p = 0;
+ *index_p = index;
+ return 0;
+}
+
+static int http_header_is(
+ const char *data,
+ size_t len,
+ size_t index,
+ const char *name,
+ size_t *index_value_p) {
+ size_t namelen;
+
+ assert(data);
+ assert(index <= len);
+ assert(name);
+ assert(index_value_p);
+
+ namelen = strlen(name);
+ if (index + namelen > len) goto notfound;
+ if (strncasecmp(data + index, name, namelen) != 0) goto notfound;
+ index += namelen;
+ while (index < len && is_whitespace(data[index])) index++;
+ if (index >= len || data[index] != ':') goto notfound;
+ index++;
+
+ while (index < len && is_whitespace(data[index])) index++;
+ *index_value_p = index;
+ return 1;
+
+notfound:
+ *index_value_p = 0;
+ return 0;
+}
+
+static int http_parse_int_header(
+ const char *data,
+ size_t index,
+ size_t index_end,
+ int *value_p,
+ int *error_p) {
+ int value = 0;
+
+ assert(data);
+ assert(index <= index_end);
+ assert(value_p);
+ assert(error_p);
+ assert(!*error_p);
+
+ while (index < index_end && is_numeric(data[index])) {
+ value = value * 10 + (data[index++] - '0');
+ }
+
+ while (index < index_end && is_whitespace_or_linebreak(data[index])) {
+ index++;
+ }
+
+ if (index < index_end) {
+ errno = 0;
+ FAIL("HTTP error: bad numeric header value: \"%.*s\"",
+ (int) (index_end - index), data + index);
+ *error_p = 1;
+ return 0;
+ }
+
+ *value_p = value;
+ return 1;
+}
+
+static int http_response_complete(
+ const char *data,
+ size_t len,
+ int *error_p,
+ int *code_p,
+ size_t *index_body_p) {
+ int content_length = -1;
+ size_t index = 0, index_line;
+ size_t index_value;
+ size_t linelen;
+
+ assert(data);
+ assert(error_p);
+ assert(!*error_p);
+ assert(code_p);
+ assert(index_body_p);
+
+ /* parse status line */
+ if (!http_get_status_line(data, len, &index, error_p, code_p)) {
+ return 0;
+ }
+
+ /* parse headers */
+ for (;;) {
+ index_line = index;
+ if (!http_get_header_line(data, len, &index, &linelen)) {
+ return 0;
+ }
+ if (linelen == 0) break;
+ if (http_header_is(data, len, index_line,
+ "Content-Length", &index_value)) {
+ if (!http_parse_int_header(data, index_value,
+ index_line + linelen, &content_length,
+ error_p)) {
+ return 0;
+ }
+ }
+ }
+
+ /* do we know how long the response will be? */
+ if (content_length < 0) {
+ errno = 0;
+ FAIL("HTTP error: missing Content-Length header "
+ "(maybe Transfer-Encoding is specified instead "
+ "but this is currently unsupported)");
+ goto error;
+ }
+
+ /* check whether the amount of data is correct */
+ if (len > index + content_length) {
+ errno = 0;
+ FAIL("HTTP error: more data received than expected");
+ goto error;
+ }
+
+ *index_body_p = index;
+ return len == index + content_length;
+
+error:
+ *error_p = 1;
+ *code_p = 0;
+ *index_body_p = 0;
+ return 0;
+}
+
+static void http_recv_response(
+ int fd,
+ void (* callback_verify)(const void *data, size_t size),
+ size_t chunksize) {
+ int code;
+ char *data;
+ size_t datalen = 0, datasize = 0;
+ int error = 0;
+ size_t index_body;
+ ssize_t r;
+
+ assert(fd >= 0);
+ assert(callback_verify);
+ assert(chunksize > 0);
+
+ data = NULL;
+ for (;;) {
+ /* make room for another chunk in the buffer if needed */
+ if (datasize < datalen + chunksize) {
+ datasize = (datalen + chunksize) * 2;
+ REALLOC(data, datasize);
+ }
+
+ /* read a chunk of data */
+ errno = 0;
+ r = read(fd, data + datalen, chunksize);
+ if (r < 0 || (size_t) r > chunksize) {
+ FAIL("read of %zu bytes failed with result %zd",
+ chunksize, r);
+ goto cleanup;
+ }
+ datalen += r;
+
+ /* if we received all headers+data, we are done */
+ if (http_response_complete(data, datalen, &error, &code,
+ &index_body)) {
+ break;
+ }
+ if (error) goto cleanup;
+
+ /* check for premature disconnection */
+ if (r == 0) {
+ errno = 0;
+ FAIL("server disconnected even though the response "
+ "seems to be incomplete");
+ goto cleanup;
+ }
+ }
+
+ dbgprintf("received response:\n%.*s", (int) datalen, data);
+
+ assert(index_body <= datalen);
+ if (code == 200) {
+ callback_verify(data + index_body, datalen - index_body);
+ } else {
+ errno = 0;
+ FAIL("unexpected HTTP status code %d", code);
+ }
+
+cleanup:
+ if (data) free(data);
+}
+
+static void http_test(
+ const struct url *url,
+ size_t chunksize,
+ int bigrequest,
+ int delay,
+ int withshutdown) {
+ int fd;
+
+ assert(url);
+ assert(chunksize > 0);
+
+ dbgprintf("attempting download from http://%s:%d%s, "
+ "chunksize=%zu, bigrequest=%d, delay=%d, withshutdown=%d\n",
+ url->host, url->port, url->path, chunksize, bigrequest,
+ delay, withshutdown);
+
+ fd = http_connect(url->host, url->port);
+ if (fd < 0) return;
+
+ http_send_request(fd, url->host, url->path, chunksize, bigrequest);
+
+ errno = 0;
+ if (withshutdown && shutdown(fd, SHUT_WR) != 0) {
+ FAIL("shutdown failed");
+ }
+
+ if (delay) sleep(1);
+ http_recv_response(fd, url->callback_verify, chunksize);
+
+ CLOSE(fd);
+
+ dbgprintf("download attempt completed\n");
+}
+
+static int child_count;
+
+static void http_test_fork(
+ const struct url *url,
+ size_t chunksize,
+ int bigrequest,
+ int delay,
+ int withshutdown) {
+ int errctold;
+ pid_t pid;
+
+ assert(url);
+ assert(chunksize > 0);
+
+ errno = 0;
+ pid = fork();
+ if (pid < 0) {
+ FAIL("fork failed");
+ return;
+ }
+
+ if (pid > 0) {
+ child_count++;
+ return;
+ }
+
+ errctold = errct;
+ http_test(
+ url,
+ chunksize,
+ bigrequest,
+ delay,
+ withshutdown);
+ assert(errct >= errctold);
+ exit(errct - errctold);
+}
+
+static void wait_all(void) {
+ int exitcode, status;
+ pid_t pid;
+
+ while (child_count > 0) {
+ errno = 0;
+ pid = waitpid(-1, &status, 0);
+ if (pid <= 0) {
+ FAIL("waitpid failed");
+ return;
+ }
+ if (WIFEXITED(status)) {
+ exitcode = WEXITSTATUS(status);
+ dbgprintf("child %d completed with exit code %d\n",
+ (int) pid, exitcode);
+ if (exitcode >= 0) {
+ errct += exitcode;
+ } else {
+ FAIL("child has negative exit code %d",
+ exitcode);
+ }
+ } else if (WIFSIGNALED(status)) {
+ dbgprintf("child %d killed by signal %d\n",
+ (int) pid, WTERMSIG(status));
+ FAIL("child killed by signal %d", WTERMSIG(status));
+ } else {
+ dbgprintf("child %d gone with status 0x%x\n",
+ (int) pid, status);
+ FAIL("child gone, but neither exit nor signal");
+ }
+ child_count--;
+ }
+
+ errno = 0;
+ if (waitpid(-1, &status, 0) != -1 || errno != ECHILD) {
+ FAIL("waitpid should have returned ECHILD");
+ }
+}
+
+#define OPTION_BIGREQUEST (1 << 0)
+#define OPTION_DELAY (1 << 1)
+#define OPTION_SHUTDOWN (1 << 2)
+
+static void http_test_all(int multiproc) {
+ static const size_t chunksizes[] = { 1, 1024, 65536 };
+ static const int optionsets[] = {
+ 0,
+ OPTION_BIGREQUEST,
+ OPTION_DELAY,
+ OPTION_SHUTDOWN,
+ OPTION_BIGREQUEST | OPTION_DELAY | OPTION_SHUTDOWN,
+ };
+ int chunksizeindex;
+ int options;
+ int optionindex;
+ int urlindex;
+
+ for (urlindex = 0; urlindex < URL_COUNT; urlindex++) {
+ for (chunksizeindex = 0; chunksizeindex < 3; chunksizeindex++) {
+ for (optionindex = 0; optionindex < 3; optionindex++) {
+ options = optionsets[optionindex];
+ (multiproc ? http_test_fork : http_test)(
+ &urls[urlindex],
+ chunksizes[chunksizeindex],
+ options & OPTION_BIGREQUEST,
+ options & OPTION_DELAY,
+ options & OPTION_SHUTDOWN);
+ }
+ }
+ }
+
+ wait_all();
+}
+
+static void verify_data(
+ const void *httpdata, size_t httpsize,
+ const void *refdata, size_t refsize,
+ const char *path) {
+
+ assert(httpdata);
+ assert(refdata);
+ assert(path);
+
+ if (httpsize != refsize) {
+ errno = 0;
+ FAIL("download from http://%s:%d%s returned wrong number "
+ "of bytes: %zd (expected %zd)",
+ HOST, PORT, path, httpsize, refsize);
+ } else if (memcmp(httpdata, refdata, refsize) != 0) {
+ errno = 0;
+ FAIL("download from http://%s:%d%s returned wrong data",
+ HOST, PORT, path);
+ }
+}
+
+static void callback_verify_path1(const void *data, size_t size) {
+ verify_data(data, size, PATH1_DATA, strlen(PATH1_DATA), PATH1);
+}
+
+static void callback_verify_path2(const void *data, size_t size) {
+ unsigned short buf[65536];
+ int i;
+
+ for (i = 0; i < 65536; i++) buf[i] = htons(i);
+
+ verify_data(data, size, buf, sizeof(buf), PATH2);
+}
+
+int main(int argc, char **argv)
+{
+ int use_network;
+
+ start(82);
+
+ use_network = get_setting_use_network();
+ if (use_network) {
+ http_test_all(0 /* multiproc */);
+ http_test_all(1 /* multiproc */);
+ } else {
+ dbgprintf("test disabled, set USENETWORK=yes to enable\n");
+ }
+
+ quit();
+ return 0;
+}