void http_request(http_t *ctx, const char *verb, const char *host, uint16_t port, const char *uri) { http_log(ctx, "%s: host=\"%s\" uri=\"%s\"", __func__, host, uri); http_send_str(ctx, verb); http_send_str(ctx, " "); http_send_str(ctx, uri); http_send_line(ctx, " HTTP/1.1"); http_send_str(ctx, "Host: "); http_send_str(ctx, host); if (port != HTTP_DEFAULT_PORT) { char buf[32], *p = buf; size_t size = sizeof buf; p = append_char(p, &size, ':'); p = append_uint(p, &size, port); http_send_str(ctx, buf); } http_send_str(ctx, "\r\n"); http_send_str(ctx, http_useragent_header); http_send_line(ctx, http_connection_header(ctx)); http_send_extra_headers(ctx); }
int http_send_head_reply(http_t *ctx) { http_send_status_line(ctx, 200, "OK"); http_send_line(ctx, "Content-Type: text/plain"); http_send_line(ctx, http_connection_header(ctx)); http_terminate_headers(ctx); return 0; }
int http_send_empty_reply(http_t *ctx) { RUNTIME_ASSERT(ctx); ACCLOG_SET(ctx->acclog, size, 0); http_send_status_line(ctx, ctx->status_code, ctx->status_msg); http_send_line(ctx, http_connection_header(ctx)); http_send_line(ctx, "Content-Length: 0"); http_send_extra_headers(ctx); return http_terminate_headers(ctx); }
void http_send_extra_headers(http_t *ctx) { const snode_t *sn; ctx->extra_headers = snode_reverse(ctx->extra_headers); for (sn = ctx->extra_headers; NULL != sn; sn = sn->next) { http_send_line(ctx, sn->ptr); } }
int http_redirect(http_t *ctx, const char *url, bool permanent) { static const char location[] = "Location: "; RUNTIME_ASSERT(url && strlen(url) > 0); if (permanent) http_send_status_line(ctx, 301, "Moved Permanently"); else http_send_status_line(ctx, 307, "Temporary Redirect"); http_send_line(ctx, http_connection_header(ctx)); http_send_data(ctx, location, sizeof location - 1); http_send_line(ctx, url); http_send_extra_headers(ctx); http_terminate_headers(ctx); return 0; }
int http_send_content_length_header(http_t *ctx) { if ((uint64_t) -1 != ctx->content_length) { static const char length_hdr[] = "Content-Length: "; char buf[UINT64_DEC_BUFLEN]; http_send_data(ctx, length_hdr, sizeof length_hdr - 1); if (ctx->content_length > (uint32_t) -1) { print_uint64(buf, sizeof buf, ctx->content_length); } else { print_uint32(buf, sizeof buf, ctx->content_length); } return http_send_line(ctx, buf); } return 0; }
int http_send_response(http_t *ctx) { RUNTIME_ASSERT(ctx != NULL); if (!ctx->status_code) { ctx->status_code = 500; ctx->status_msg = "Internal Server Error"; } http_send_status_line(ctx, ctx->status_code, ctx->status_msg); http_send_content_length_header(ctx); http_send_line(ctx, http_connection_header(ctx)); http_send_extra_headers(ctx); http_terminate_headers(ctx); return 0; }
int http_send_document(http_t *ctx, const char *data, size_t size) { RUNTIME_ASSERT(ctx != NULL); if (!ctx->status_code) { ctx->status_code = 200; ctx->status_msg = "OK"; } http_send_status_line(ctx, ctx->status_code, ctx->status_msg); http_set_content_length(ctx, size); http_send_content_length_header(ctx); http_send_line(ctx, http_connection_header(ctx)); http_send_extra_headers(ctx); ACCLOG_SET(ctx->acclog, size, size); http_terminate_headers(ctx); if (size > 0) http_send_data(ctx, data, size); return 0; }
enum host_status host_recv_file(struct host *h, struct http_info *req, FILE *file) { boolean mid_inflate = false, mid_chunk = false, deflated = false; unsigned int content_length = 0; const char *host_name = h->name; unsigned long len = 0, pos = 0; char line[LINE_BUF_LEN]; z_stream stream; ssize_t line_len; enum { NONE, NORMAL, CHUNKED, } transfer_type = NONE; // Tell the server that we support pipelining snprintf(line, LINE_BUF_LEN, "GET %s HTTP/1.1", req->url); line[LINE_BUF_LEN - 1] = 0; if(http_send_line(h, line) < 0) return -HOST_SEND_FAILED; // For vhost resolution if (h->proxied) host_name = h->endpoint; snprintf(line, LINE_BUF_LEN, "Host: %s", host_name); line[LINE_BUF_LEN - 1] = 0; if(http_send_line(h, line) < 0) return -HOST_SEND_FAILED; // We support DEFLATE/GZIP payloads if(http_send_line(h, "Accept-Encoding: gzip") < 0) return -HOST_SEND_FAILED; // Black line tells server we are done if(http_send_line(h, "") < 0) return -HOST_SEND_FAILED; // Read in the HTTP status line line_len = http_recv_line(h, line, LINE_BUF_LEN); if(line_len < 0) { warn("No response for url '%s': %d!\n", req->url, (int)line_len); return line_len; } if(!http_read_status(req, line, line_len)) { warn("Invalid status: %s\nFailed for url '%s'\n", line, req->url); return -HOST_HTTP_INVALID_STATUS; } // Unhandled status categories switch(req->status_type) { case 1: return -HOST_HTTP_INFO; case 3: return -HOST_HTTP_REDIRECT; case 4: return -HOST_HTTP_CLIENT_ERROR; case 5: return -HOST_HTTP_SERVER_ERROR; } // Now parse the HTTP headers, extracting only the pertinent fields while(true) { int len = http_recv_line(h, line, LINE_BUF_LEN); char *key, *value, *buf = line; if(len < 0) return -HOST_HTTP_INVALID_HEADER; else if(len == 0) break; key = strsep(&buf, ":"); value = strsep(&buf, ":"); if(!key || !value) return -HOST_HTTP_INVALID_HEADER; // Skip common prefix space if present if(value[0] == ' ') value++; /* Parse pertinent headers. These are: * * Content-Length Necessary to determine payload length * Transfer-Encoding Instead of Content-Length, can only be "chunked" * Content-Type Text or binary; also used for sanity checks * Content-Encoding Present and set to 'gzip' if deflated */ if(strcmp(key, "Content-Length") == 0) { char *endptr; content_length = (unsigned int)strtoul(value, &endptr, 10); if(endptr[0]) return -HOST_HTTP_INVALID_CONTENT_LENGTH; transfer_type = NORMAL; } else if(strcmp(key, "Transfer-Encoding") == 0) { if(strcmp(value, "chunked") != 0) return -HOST_HTTP_INVALID_TRANSFER_ENCODING; transfer_type = CHUNKED; } else if(strcmp(key, "Content-Type") == 0) { strncpy(req->content_type, value, 63); if(strcmp(value, req->expected_type) != 0) return -HOST_HTTP_INVALID_CONTENT_TYPE; } else if(strcmp(key, "Content-Encoding") == 0) { if(strcmp(value, "gzip") != 0) return -HOST_HTTP_INVALID_CONTENT_ENCODING; deflated = true; } } if(transfer_type != NORMAL && transfer_type != CHUNKED) return -HOST_HTTP_INVALID_TRANSFER_ENCODING; while(true) { unsigned long block_size; char block[BLOCK_SIZE]; /* Both transfer mechanisms need preambles. For NORMAL, this will * happen only once, because we have a predetermined length for * transfer. However, for CHUNKED we don't know the total payload * size, so this will be invoked each time we exhaust a chunk. * * The CHUNKED handling basically involves chopping away the * headers and determining the next chunk size. */ if(!mid_chunk) { if(transfer_type == NORMAL) len = content_length; else if(transfer_type == CHUNKED) { char *endptr, *length, *buf = line; // Get a chunk_length;parameters formatted line (CRLF terminated) if(http_recv_line(h, line, LINE_BUF_LEN) <= 0) return -HOST_HTTP_INVALID_CHUNK_LENGTH; // HTTP 1.1 says we can ignore trailing parameters length = strsep(&buf, ";"); if(!length) return -HOST_HTTP_INVALID_CHUNK_LENGTH; // Convert hex length to unsigned long; check for conversion errors len = strtoul(length, &endptr, 16); if(endptr[0]) return -HOST_HTTP_INVALID_CHUNK_LENGTH; } mid_chunk = true; pos = 0; } /* For NORMAL transfers, this indicates that there was a zero byte * payload. This is unusual but we can handle it safely by aborting. * * For CHUNKED transfers, zero indicates that there are no more chunks * to process, and that final footer handling should occur. We then * abort as with NORMAL. */ if(len == 0) { if(transfer_type == CHUNKED) if(!http_skip_headers(h)) return -HOST_HTTP_INVALID_HEADER; break; } /* For a NORMAL transfer, the block_size computation should yield * BLOCK_SIZE until the final block, which will be len % BLOCK_SIZE. * * For CHUNKED, this block_size can be more volatile. In most cases it * will be BLOCK_SIZE if chunk size > BLOCK_SIZE, until the final block. * * However for very small chunks (which are unlikely) this will always * be shorter than BLOCK_SIZE. */ block_size = MIN(BLOCK_SIZE, len - pos); /* In either case, all headers and block computation has now been done, * and the buffer can be streamed to disk. */ if(!__recv(h, block, block_size)) return -HOST_RECV_FAILED; if(deflated) { /* This is the first block requiring inflation. In this case, we must * parse the GZIP header in order to compute an offset to the DEFLATE * formatted data. */ if(!mid_inflate) { ssize_t deflate_offset = 0; /* Compute the offset within this block to begin the inflation * process. For all but the first block, deflate_offset will be * zero. */ deflate_offset = zlib_skip_gzip_header(block, block_size); if(deflate_offset < 0) return deflate_offset; /* Now we can initialize the decompressor. Pass along the block * without the GZIP header (and for a GZIP, this is also without * the DEFLATE header too, which is what the -MAX_WBITS trick is for). */ stream.avail_in = block_size - (unsigned long)deflate_offset; stream.next_in = (Bytef *)&block[deflate_offset]; stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.opaque = Z_NULL; if(inflateInit2(&stream, -MAX_WBITS) != Z_OK) return -HOST_ZLIB_INFLATE_FAILED; mid_inflate = true; } else { stream.avail_in = block_size; stream.next_in = (Bytef *)block; } while(true) { char outbuf[BLOCK_SIZE]; int ret; // Each pass, only decompress a maximum of BLOCK_SIZE stream.avail_out = BLOCK_SIZE; stream.next_out = (Bytef *)outbuf; /* Perform the inflation (this will modify avail_in and * next_in automatically. */ ret = inflate(&stream, Z_NO_FLUSH); if(ret != Z_OK && ret != Z_STREAM_END) return -HOST_ZLIB_INFLATE_FAILED; // Push the block to disk if(fwrite(outbuf, BLOCK_SIZE - stream.avail_out, 1, file) != 1) return -HOST_FWRITE_FAILED; // If the stream has terminated, flag it and break out if(ret == Z_STREAM_END) { mid_inflate = false; break; } /* The stream hasn't terminated but we've exhausted input * data for this pass. */ if(stream.avail_in == 0) break; } // The stream terminated, so we should free associated data-structures if(!mid_inflate) inflateEnd(&stream); } else { /* If the transfer is not deflated, we can simply write out * block_size bytes to the file now. */ if(fwrite(block, block_size, 1, file) != 1) return -HOST_FWRITE_FAILED; } pos += block_size; if(h->recv_cb) h->recv_cb(ftell(file)); /* For NORMAL transfers we can now abort since we have reached the end * of our payload. * * For CHUNKED transfers, we remove the trailing newline and flag that * a new set of chunk headers should be read. */ if(len == pos) { if(transfer_type == NORMAL) break; else if(transfer_type == CHUNKED) { if(http_recv_line(h, line, LINE_BUF_LEN) != 0) return -HOST_HTTP_INVALID_HEADER; mid_chunk = false; } } } return HOST_SUCCESS; }
enum host_status host_send_file(struct host *h, FILE *file, const char *mime_type) { boolean mid_deflate = false; char line[LINE_BUF_LEN]; uint32_t crc, uSize; z_stream stream; long size; // Tell the client that we're going to use HTTP/1.1 features if(http_send_line(h, "HTTP/1.1 200 OK") < 0) return -HOST_SEND_FAILED; /* To bring ourselves into complete HTTP 1.1 compliance, send * some headers that we know our client doesn't actually need. */ if(http_send_line(h, "Accept-Ranges: bytes") < 0) return -HOST_SEND_FAILED; if(http_send_line(h, "Vary: Accept-Encoding") < 0) return -HOST_SEND_FAILED; // Always zlib deflate content; keeps code simple if(http_send_line(h, "Content-Encoding: gzip") < 0) return -HOST_SEND_FAILED; // We'll just send everything chunked, unconditionally if(http_send_line(h, "Transfer-Encoding: chunked") < 0) return -HOST_SEND_FAILED; // Pass along a type hint for the client (mandatory sanity check for MZX) snprintf(line, LINE_BUF_LEN, "Content-Type: %s", mime_type); line[LINE_BUF_LEN - 1] = 0; if(http_send_line(h, line) < 0) return -HOST_SEND_FAILED; // Terminate the headers with a blank line if(http_send_line(h, "") < 0) return -HOST_SEND_FAILED; // Initialize CRC for GZIP footer crc = crc32(0L, Z_NULL, 0); // Record uncompressed size for GZIP footer size = ftell_and_rewind(file); uSize = (uint32_t)size; if(size < 0) return -HOST_FREAD_FAILED; while(true) { int deflate_offset = 0, ret = Z_OK, deflate_flag = Z_SYNC_FLUSH; char block[BLOCK_SIZE], zblock[BLOCK_SIZE]; size_t block_size; /* Read a block from the disk source and compute a CRC32 on the * fly. This CRC will be dumped at the end of the deflated data * and is required for RFC 1952 compliancy. */ block_size = fread(block, 1, BLOCK_SIZE, file); crc = crc32(crc, (Bytef *)block, block_size); /* The fread() above pretty much guarantees that block_size will * be BLOCK_SIZE up to the final block. However, fread() also * returns a short count if there was an I/O error, and we must * detect this. If it's legitimately the final block, give the * compressor this information. */ if(block_size != BLOCK_SIZE) { if(!feof(file)) return -HOST_FREAD_FAILED; deflate_flag = Z_FINISH; } /* We exhausted input at a block boundary. This is unlikely, * but in the event that it happens we can simply ignore * the deflate stage and write out the terminal chunk signature. */ if(block_size == 0) break; /* Regardless of whether we are initializing the compressor in * the next section or not, we always have BLOCK_SIZE aligned * input data available. */ stream.avail_in = block_size; stream.next_in = (Bytef *)block; if(!mid_deflate) { deflate_offset = zlib_forge_gzip_header(zblock); stream.avail_out = BLOCK_SIZE - (unsigned long)deflate_offset; stream.next_out = (Bytef *)&zblock[deflate_offset]; stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.opaque = Z_NULL; ret = deflateInit2(&stream, Z_BEST_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY); if(ret != Z_OK) return -HOST_ZLIB_DEFLATE_FAILED; mid_deflate = true; } else { stream.avail_out = BLOCK_SIZE; stream.next_out = (Bytef *)zblock; } while(true) { unsigned long chunk_size; // Deflate a chunk of the input (partially or fully) ret = deflate(&stream, deflate_flag); if(ret != Z_OK && ret != Z_STREAM_END) return -HOST_ZLIB_INFLATE_FAILED; // Compute chunk length (final chunk includes GZIP footer) chunk_size = BLOCK_SIZE - stream.avail_out; if(ret == Z_STREAM_END) chunk_size += 2 * sizeof(uint32_t); // Dump compressed chunk length snprintf(line, LINE_BUF_LEN, "%lx", chunk_size); if(http_send_line(h, line) < 0) return -HOST_SEND_FAILED; // Send the compressed output block over the socket if(!__send(h, zblock, BLOCK_SIZE - stream.avail_out)) return -HOST_SEND_FAILED; /* We might not have finished the entire stream, but the * available input is likely to have been exhausted. With * Z_SYNC_FLUSH this will commonly result in zero bytes * remaining in the input source. Additionally, if this is * the final chunk (and Z_FINISH was flagged), Z_STREAM_END * will be set. In either case, we must break out. */ if((ret == Z_OK && stream.avail_in == 0) || ret == Z_STREAM_END) break; // Output has been flushed; start over for the remaining input (if any) stream.avail_out = BLOCK_SIZE; stream.next_out = (Bytef *)zblock; } /* Z_FINISH was flagged, stream ended * Terminate compression */ if(ret == Z_STREAM_END) { // Free any zlib allocated resources deflateEnd(&stream); // Write out GZIP `CRC32' footer if(!__send(h, &crc, sizeof(uint32_t))) return -HOST_SEND_FAILED; // Write out GZIP `ISIZE' footer if(!__send(h, &uSize, sizeof(uint32_t))) return -HOST_SEND_FAILED; mid_deflate = false; } // Newline after chunk's data if(http_send_line(h, "") < 0) return -HOST_SEND_FAILED; // Final block; can break out if(block_size != BLOCK_SIZE) break; } // Terminal chunk signature, so called "trailer" if(http_send_line(h, "0") < 0) return -HOST_SEND_FAILED; // Post-trailer newline if(http_send_line(h, "") < 0) return -HOST_SEND_FAILED; return HOST_SUCCESS; }