/* * 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 #include #include #include #include #include #include #include #include #include #include "common.h" #define CLOSE(fd) do { assert(fd >= 0); if (close((fd)) != 0) efmt("close failed"); } while (0); #define REALLOC(p, size) do { p = realloc(p, size); if (!p) efmt("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 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) { efmt("host %s not found", host); goto failure; } fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { efmt("cannot create socket"); goto failure; } if (connect(fd, addr->ai_addr, addr->ai_addrlen) != 0) { efmt("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; efmt("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; efmt("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; efmt("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; efmt("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; efmt("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) { efmt("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; efmt("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; efmt("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) { efmt("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) { efmt("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) { efmt("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 { efmt("child has negative exit code %d", exitcode); } } else if (WIFSIGNALED(status)) { dbgprintf("child %d killed by signal %d\n", (int) pid, WTERMSIG(status)); efmt("child killed by signal %d", WTERMSIG(status)); } else { dbgprintf("child %d gone with status 0x%x\n", (int) pid, status); efmt("child gone, but neither exit nor signal"); } child_count--; } errno = 0; if (waitpid(-1, &status, 0) != -1 || errno != ECHILD) { efmt("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; efmt("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; efmt("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; }