X-Git-Url: https://gerrit.opnfv.org/gerrit/gitweb?a=blobdiff_plain;f=qemu%2Froms%2Fipxe%2Fsrc%2Fnet%2Ftcp%2Fhttpcore.c;fp=qemu%2Froms%2Fipxe%2Fsrc%2Fnet%2Ftcp%2Fhttpcore.c;h=1d1953e6191872434cfc5530122f22730994ed34;hb=e44e3482bdb4d0ebde2d8b41830ac2cdb07948fb;hp=0000000000000000000000000000000000000000;hpb=9ca8dbcc65cfc63d6f5ef3312a33184e1d726e00;p=kvmfornfv.git diff --git a/qemu/roms/ipxe/src/net/tcp/httpcore.c b/qemu/roms/ipxe/src/net/tcp/httpcore.c new file mode 100644 index 000000000..1d1953e61 --- /dev/null +++ b/qemu/roms/ipxe/src/net/tcp/httpcore.c @@ -0,0 +1,1574 @@ +/* + * Copyright (C) 2007 Michael Brown . + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +FILE_LICENCE ( GPL2_OR_LATER ); + +/** + * @file + * + * Hyper Text Transfer Protocol (HTTP) core functionality + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Disambiguate the various error causes */ +#define EACCES_401 __einfo_error ( EINFO_EACCES_401 ) +#define EINFO_EACCES_401 \ + __einfo_uniqify ( EINFO_EACCES, 0x01, "HTTP 401 Unauthorized" ) +#define EIO_OTHER __einfo_error ( EINFO_EIO_OTHER ) +#define EINFO_EIO_OTHER \ + __einfo_uniqify ( EINFO_EIO, 0x01, "Unrecognised HTTP response code" ) +#define EIO_CONTENT_LENGTH __einfo_error ( EINFO_EIO_CONTENT_LENGTH ) +#define EINFO_EIO_CONTENT_LENGTH \ + __einfo_uniqify ( EINFO_EIO, 0x02, "Content length mismatch" ) +#define EINVAL_RESPONSE __einfo_error ( EINFO_EINVAL_RESPONSE ) +#define EINFO_EINVAL_RESPONSE \ + __einfo_uniqify ( EINFO_EINVAL, 0x01, "Invalid content length" ) +#define EINVAL_HEADER __einfo_error ( EINFO_EINVAL_HEADER ) +#define EINFO_EINVAL_HEADER \ + __einfo_uniqify ( EINFO_EINVAL, 0x02, "Invalid header" ) +#define EINVAL_CONTENT_LENGTH __einfo_error ( EINFO_EINVAL_CONTENT_LENGTH ) +#define EINFO_EINVAL_CONTENT_LENGTH \ + __einfo_uniqify ( EINFO_EINVAL, 0x03, "Invalid content length" ) +#define EINVAL_CHUNK_LENGTH __einfo_error ( EINFO_EINVAL_CHUNK_LENGTH ) +#define EINFO_EINVAL_CHUNK_LENGTH \ + __einfo_uniqify ( EINFO_EINVAL, 0x04, "Invalid chunk length" ) +#define ENOENT_404 __einfo_error ( EINFO_ENOENT_404 ) +#define EINFO_ENOENT_404 \ + __einfo_uniqify ( EINFO_ENOENT, 0x01, "HTTP 404 Not Found" ) +#define EPERM_403 __einfo_error ( EINFO_EPERM_403 ) +#define EINFO_EPERM_403 \ + __einfo_uniqify ( EINFO_EPERM, 0x01, "HTTP 403 Forbidden" ) +#define EPROTO_UNSOLICITED __einfo_error ( EINFO_EPROTO_UNSOLICITED ) +#define EINFO_EPROTO_UNSOLICITED \ + __einfo_uniqify ( EINFO_EPROTO, 0x01, "Unsolicited data" ) + +/** Block size used for HTTP block device request */ +#define HTTP_BLKSIZE 512 + +/** Retry delay used when we cannot understand the Retry-After header */ +#define HTTP_RETRY_SECONDS 5 + +/** Receive profiler */ +static struct profiler http_rx_profiler __profiler = { .name = "http.rx" }; + +/** Data transfer profiler */ +static struct profiler http_xfer_profiler __profiler = { .name = "http.xfer" }; + +/** HTTP flags */ +enum http_flags { + /** Request is waiting to be transmitted */ + HTTP_TX_PENDING = 0x0001, + /** Fetch header only */ + HTTP_HEAD_ONLY = 0x0002, + /** Client would like to keep connection alive */ + HTTP_CLIENT_KEEPALIVE = 0x0004, + /** Server will keep connection alive */ + HTTP_SERVER_KEEPALIVE = 0x0008, + /** Discard the current request and try again */ + HTTP_TRY_AGAIN = 0x0010, + /** Provide Basic authentication details */ + HTTP_BASIC_AUTH = 0x0020, + /** Provide Digest authentication details */ + HTTP_DIGEST_AUTH = 0x0040, + /** Socket must be reopened */ + HTTP_REOPEN_SOCKET = 0x0080, +}; + +/** HTTP receive state */ +enum http_rx_state { + HTTP_RX_RESPONSE = 0, + HTTP_RX_HEADER, + HTTP_RX_CHUNK_LEN, + /* In HTTP_RX_DATA, it is acceptable for the server to close + * the connection (unless we are in the middle of a chunked + * transfer). + */ + HTTP_RX_DATA, + /* In the following states, it is acceptable for the server to + * close the connection. + */ + HTTP_RX_TRAILER, + HTTP_RX_IDLE, + HTTP_RX_DEAD, +}; + +/** + * An HTTP request + * + */ +struct http_request { + /** Reference count */ + struct refcnt refcnt; + /** Data transfer interface */ + struct interface xfer; + /** Partial transfer interface */ + struct interface partial; + + /** URI being fetched */ + struct uri *uri; + /** Default port */ + unsigned int default_port; + /** Filter (if any) */ + int ( * filter ) ( struct interface *xfer, + const char *name, + struct interface **next ); + /** Transport layer interface */ + struct interface socket; + + /** Flags */ + unsigned int flags; + /** Starting offset of partial transfer (if applicable) */ + size_t partial_start; + /** Length of partial transfer (if applicable) */ + size_t partial_len; + + /** TX process */ + struct process process; + + /** RX state */ + enum http_rx_state rx_state; + /** Response code */ + unsigned int code; + /** Received length */ + size_t rx_len; + /** Length remaining (or 0 if unknown) */ + size_t remaining; + /** HTTP is using Transfer-Encoding: chunked */ + int chunked; + /** Current chunk length remaining (if applicable) */ + size_t chunk_remaining; + /** Line buffer for received header lines */ + struct line_buffer linebuf; + /** Receive data buffer (if applicable) */ + userptr_t rx_buffer; + + /** Authentication realm (if any) */ + char *auth_realm; + /** Authentication nonce (if any) */ + char *auth_nonce; + /** Authentication opaque string (if any) */ + char *auth_opaque; + + /** Request retry timer */ + struct retry_timer timer; + /** Retry delay (in timer ticks) */ + unsigned long retry_delay; +}; + +/** + * Free HTTP request + * + * @v refcnt Reference counter + */ +static void http_free ( struct refcnt *refcnt ) { + struct http_request *http = + container_of ( refcnt, struct http_request, refcnt ); + + uri_put ( http->uri ); + empty_line_buffer ( &http->linebuf ); + free ( http->auth_realm ); + free ( http->auth_nonce ); + free ( http->auth_opaque ); + free ( http ); +}; + +/** + * Close HTTP request + * + * @v http HTTP request + * @v rc Return status code + */ +static void http_close ( struct http_request *http, int rc ) { + + /* Prevent further processing of any current packet */ + http->rx_state = HTTP_RX_DEAD; + + /* Prevent reconnection */ + http->flags &= ~HTTP_CLIENT_KEEPALIVE; + + /* Remove process */ + process_del ( &http->process ); + + /* Close all data transfer interfaces */ + intf_shutdown ( &http->socket, rc ); + intf_shutdown ( &http->partial, rc ); + intf_shutdown ( &http->xfer, rc ); +} + +/** + * Open HTTP socket + * + * @v http HTTP request + * @ret rc Return status code + */ +static int http_socket_open ( struct http_request *http ) { + struct uri *uri = http->uri; + struct sockaddr_tcpip server; + struct interface *socket; + int rc; + + /* Open socket */ + memset ( &server, 0, sizeof ( server ) ); + server.st_port = htons ( uri_port ( uri, http->default_port ) ); + socket = &http->socket; + if ( http->filter ) { + if ( ( rc = http->filter ( socket, uri->host, &socket ) ) != 0 ) + return rc; + } + if ( ( rc = xfer_open_named_socket ( socket, SOCK_STREAM, + ( struct sockaddr * ) &server, + uri->host, NULL ) ) != 0 ) + return rc; + + return 0; +} + +/** + * Retry HTTP request + * + * @v timer Retry timer + * @v fail Failure indicator + */ +static void http_retry ( struct retry_timer *timer, int fail __unused ) { + struct http_request *http = + container_of ( timer, struct http_request, timer ); + int rc; + + /* Reopen socket if required */ + if ( http->flags & HTTP_REOPEN_SOCKET ) { + http->flags &= ~HTTP_REOPEN_SOCKET; + DBGC ( http, "HTTP %p reopening connection\n", http ); + if ( ( rc = http_socket_open ( http ) ) != 0 ) { + http_close ( http, rc ); + return; + } + } + + /* Retry the request if applicable */ + if ( http->flags & HTTP_TRY_AGAIN ) { + http->flags &= ~HTTP_TRY_AGAIN; + DBGC ( http, "HTTP %p retrying request\n", http ); + http->flags |= HTTP_TX_PENDING; + http->rx_state = HTTP_RX_RESPONSE; + process_add ( &http->process ); + } +} + +/** + * Mark HTTP request as completed successfully + * + * @v http HTTP request + */ +static void http_done ( struct http_request *http ) { + + /* If we are not at an appropriate stage of the protocol + * (including being in the middle of a chunked transfer), + * force an error. + */ + if ( ( http->rx_state < HTTP_RX_DATA ) || ( http->chunked != 0 ) ) { + DBGC ( http, "HTTP %p connection closed unexpectedly in state " + "%d\n", http, http->rx_state ); + http_close ( http, -ECONNRESET ); + return; + } + + /* If we had a Content-Length, and the received content length + * isn't correct, force an error + */ + if ( http->remaining != 0 ) { + DBGC ( http, "HTTP %p incorrect length %zd, should be %zd\n", + http, http->rx_len, ( http->rx_len + http->remaining ) ); + http_close ( http, -EIO_CONTENT_LENGTH ); + return; + } + + /* Enter idle state */ + http->rx_state = HTTP_RX_IDLE; + http->rx_len = 0; + assert ( http->remaining == 0 ); + assert ( http->chunked == 0 ); + assert ( http->chunk_remaining == 0 ); + + /* Close partial transfer interface */ + if ( ! ( http->flags & HTTP_TRY_AGAIN ) ) + intf_restart ( &http->partial, 0 ); + + /* Close everything unless we want to keep the connection alive */ + if ( ! ( http->flags & ( HTTP_CLIENT_KEEPALIVE | HTTP_TRY_AGAIN ) ) ) { + http_close ( http, 0 ); + return; + } + + /* If the server is not intending to keep the connection + * alive, then close the socket and mark it as requiring + * reopening. + */ + if ( ! ( http->flags & HTTP_SERVER_KEEPALIVE ) ) { + intf_restart ( &http->socket, 0 ); + http->flags &= ~HTTP_SERVER_KEEPALIVE; + http->flags |= HTTP_REOPEN_SOCKET; + } + + /* Start request retry timer */ + start_timer_fixed ( &http->timer, http->retry_delay ); + http->retry_delay = 0; +} + +/** + * Convert HTTP response code to return status code + * + * @v response HTTP response code + * @ret rc Return status code + */ +static int http_response_to_rc ( unsigned int response ) { + switch ( response ) { + case 200: + case 206: + case 301: + case 302: + case 303: + return 0; + case 404: + return -ENOENT_404; + case 403: + return -EPERM_403; + case 401: + return -EACCES_401; + default: + return -EIO_OTHER; + } +} + +/** + * Handle HTTP response + * + * @v http HTTP request + * @v response HTTP response + * @ret rc Return status code + */ +static int http_rx_response ( struct http_request *http, char *response ) { + char *spc; + + DBGC ( http, "HTTP %p response \"%s\"\n", http, response ); + + /* Check response starts with "HTTP/" */ + if ( strncmp ( response, "HTTP/", 5 ) != 0 ) + return -EINVAL_RESPONSE; + + /* Locate and store response code */ + spc = strchr ( response, ' ' ); + if ( ! spc ) + return -EINVAL_RESPONSE; + http->code = strtoul ( spc, NULL, 10 ); + + /* Move to receive headers */ + http->rx_state = ( ( http->flags & HTTP_HEAD_ONLY ) ? + HTTP_RX_TRAILER : HTTP_RX_HEADER ); + return 0; +} + +/** + * Handle HTTP Location header + * + * @v http HTTP request + * @v value HTTP header value + * @ret rc Return status code + */ +static int http_rx_location ( struct http_request *http, char *value ) { + int rc; + + /* Redirect to new location */ + DBGC ( http, "HTTP %p redirecting to %s\n", http, value ); + if ( ( rc = xfer_redirect ( &http->xfer, LOCATION_URI_STRING, + value ) ) != 0 ) { + DBGC ( http, "HTTP %p could not redirect: %s\n", + http, strerror ( rc ) ); + return rc; + } + + return 0; +} + +/** + * Handle HTTP Content-Length header + * + * @v http HTTP request + * @v value HTTP header value + * @ret rc Return status code + */ +static int http_rx_content_length ( struct http_request *http, char *value ) { + struct block_device_capacity capacity; + size_t content_len; + char *endp; + + /* Parse content length */ + content_len = strtoul ( value, &endp, 10 ); + if ( ! ( ( *endp == '\0' ) || isspace ( *endp ) ) ) { + DBGC ( http, "HTTP %p invalid Content-Length \"%s\"\n", + http, value ); + return -EINVAL_CONTENT_LENGTH; + } + + /* If we already have an expected content length, and this + * isn't it, then complain + */ + if ( http->remaining && ( http->remaining != content_len ) ) { + DBGC ( http, "HTTP %p incorrect Content-Length %zd (expected " + "%zd)\n", http, content_len, http->remaining ); + return -EIO_CONTENT_LENGTH; + } + if ( ! ( http->flags & HTTP_HEAD_ONLY ) ) + http->remaining = content_len; + + /* Do nothing more if we are retrying the request */ + if ( http->flags & HTTP_TRY_AGAIN ) + return 0; + + /* Use seek() to notify recipient of filesize */ + xfer_seek ( &http->xfer, http->remaining ); + xfer_seek ( &http->xfer, 0 ); + + /* Report block device capacity if applicable */ + if ( http->flags & HTTP_HEAD_ONLY ) { + capacity.blocks = ( content_len / HTTP_BLKSIZE ); + capacity.blksize = HTTP_BLKSIZE; + capacity.max_count = -1U; + block_capacity ( &http->partial, &capacity ); + } + return 0; +} + +/** + * Handle HTTP Transfer-Encoding header + * + * @v http HTTP request + * @v value HTTP header value + * @ret rc Return status code + */ +static int http_rx_transfer_encoding ( struct http_request *http, char *value ){ + + if ( strcasecmp ( value, "chunked" ) == 0 ) { + /* Mark connection as using chunked transfer encoding */ + http->chunked = 1; + } + + return 0; +} + +/** + * Handle HTTP Connection header + * + * @v http HTTP request + * @v value HTTP header value + * @ret rc Return status code + */ +static int http_rx_connection ( struct http_request *http, char *value ) { + + if ( strcasecmp ( value, "keep-alive" ) == 0 ) { + /* Mark connection as being kept alive by the server */ + http->flags |= HTTP_SERVER_KEEPALIVE; + } + + return 0; +} + +/** + * Handle WWW-Authenticate Basic header + * + * @v http HTTP request + * @v params Parameters + * @ret rc Return status code + */ +static int http_rx_basic_auth ( struct http_request *http, char *params ) { + + DBGC ( http, "HTTP %p Basic authentication required (%s)\n", + http, params ); + + /* If we received a 401 Unauthorized response, then retry + * using Basic authentication + */ + if ( ( http->code == 401 ) && + ( ! ( http->flags & HTTP_BASIC_AUTH ) ) && + ( http->uri->user != NULL ) ) { + http->flags |= ( HTTP_TRY_AGAIN | HTTP_BASIC_AUTH ); + } + + return 0; +} + +/** + * Parse Digest authentication parameter + * + * @v params Parameters + * @v name Parameter name (including trailing "=\"") + * @ret value Parameter value, or NULL + */ +static char * http_digest_param ( char *params, const char *name ) { + char *key; + char *value; + char *terminator; + + /* Locate parameter */ + key = strstr ( params, name ); + if ( ! key ) + return NULL; + + /* Extract value */ + value = ( key + strlen ( name ) ); + terminator = strchr ( value, '"' ); + if ( ! terminator ) + return NULL; + return strndup ( value, ( terminator - value ) ); +} + +/** + * Handle WWW-Authenticate Digest header + * + * @v http HTTP request + * @v params Parameters + * @ret rc Return status code + */ +static int http_rx_digest_auth ( struct http_request *http, char *params ) { + + DBGC ( http, "HTTP %p Digest authentication required (%s)\n", + http, params ); + + /* If we received a 401 Unauthorized response, then retry + * using Digest authentication + */ + if ( ( http->code == 401 ) && + ( ! ( http->flags & HTTP_DIGEST_AUTH ) ) && + ( http->uri->user != NULL ) ) { + + /* Extract realm */ + free ( http->auth_realm ); + http->auth_realm = http_digest_param ( params, "realm=\"" ); + if ( ! http->auth_realm ) { + DBGC ( http, "HTTP %p Digest prompt missing realm\n", + http ); + return -EINVAL_HEADER; + } + + /* Extract nonce */ + free ( http->auth_nonce ); + http->auth_nonce = http_digest_param ( params, "nonce=\"" ); + if ( ! http->auth_nonce ) { + DBGC ( http, "HTTP %p Digest prompt missing nonce\n", + http ); + return -EINVAL_HEADER; + } + + /* Extract opaque */ + free ( http->auth_opaque ); + http->auth_opaque = http_digest_param ( params, "opaque=\"" ); + if ( ! http->auth_opaque ) { + /* Not an error; "opaque" is optional */ + } + + http->flags |= ( HTTP_TRY_AGAIN | HTTP_DIGEST_AUTH ); + } + + return 0; +} + +/** An HTTP WWW-Authenticate header handler */ +struct http_auth_header_handler { + /** Scheme (e.g. "Basic") */ + const char *scheme; + /** Handle received parameters + * + * @v http HTTP request + * @v params Parameters + * @ret rc Return status code + */ + int ( * rx ) ( struct http_request *http, char *params ); +}; + +/** List of HTTP WWW-Authenticate header handlers */ +static struct http_auth_header_handler http_auth_header_handlers[] = { + { + .scheme = "Basic", + .rx = http_rx_basic_auth, + }, + { + .scheme = "Digest", + .rx = http_rx_digest_auth, + }, + { NULL, NULL }, +}; + +/** + * Handle HTTP WWW-Authenticate header + * + * @v http HTTP request + * @v value HTTP header value + * @ret rc Return status code + */ +static int http_rx_www_authenticate ( struct http_request *http, char *value ) { + struct http_auth_header_handler *handler; + char *separator; + char *scheme; + char *params; + int rc; + + /* Extract scheme */ + separator = strchr ( value, ' ' ); + if ( ! separator ) { + DBGC ( http, "HTTP %p malformed WWW-Authenticate header\n", + http ); + return -EINVAL_HEADER; + } + *separator = '\0'; + scheme = value; + params = ( separator + 1 ); + + /* Hand off to header handler, if one exists */ + for ( handler = http_auth_header_handlers; handler->scheme; handler++ ){ + if ( strcasecmp ( scheme, handler->scheme ) == 0 ) { + if ( ( rc = handler->rx ( http, params ) ) != 0 ) + return rc; + break; + } + } + return 0; +} + +/** + * Handle HTTP Retry-After header + * + * @v http HTTP request + * @v value HTTP header value + * @ret rc Return status code + */ +static int http_rx_retry_after ( struct http_request *http, char *value ) { + unsigned long seconds; + char *endp; + + DBGC ( http, "HTTP %p retry requested (%s)\n", http, value ); + + /* If we received a 503 Service Unavailable response, then + * retry after the specified number of seconds. If the value + * is not a simple number of seconds (e.g. a full HTTP date), + * then retry after a fixed delay, since we don't have code + * able to parse full HTTP dates. + */ + if ( http->code == 503 ) { + seconds = strtoul ( value, &endp, 10 ); + if ( *endp != '\0' ) { + seconds = HTTP_RETRY_SECONDS; + DBGC ( http, "HTTP %p cannot understand \"%s\"; " + "using %ld seconds\n", http, value, seconds ); + } + http->flags |= HTTP_TRY_AGAIN; + http->retry_delay = ( seconds * TICKS_PER_SEC ); + } + + return 0; +} + +/** An HTTP header handler */ +struct http_header_handler { + /** Name (e.g. "Content-Length") */ + const char *header; + /** Handle received header + * + * @v http HTTP request + * @v value HTTP header value + * @ret rc Return status code + * + * If an error is returned, the download will be aborted. + */ + int ( * rx ) ( struct http_request *http, char *value ); +}; + +/** List of HTTP header handlers */ +static struct http_header_handler http_header_handlers[] = { + { + .header = "Location", + .rx = http_rx_location, + }, + { + .header = "Content-Length", + .rx = http_rx_content_length, + }, + { + .header = "Transfer-Encoding", + .rx = http_rx_transfer_encoding, + }, + { + .header = "Connection", + .rx = http_rx_connection, + }, + { + .header = "WWW-Authenticate", + .rx = http_rx_www_authenticate, + }, + { + .header = "Retry-After", + .rx = http_rx_retry_after, + }, + { NULL, NULL } +}; + +/** + * Handle HTTP header + * + * @v http HTTP request + * @v header HTTP header + * @ret rc Return status code + */ +static int http_rx_header ( struct http_request *http, char *header ) { + struct http_header_handler *handler; + char *separator; + char *value; + int rc; + + /* An empty header line marks the end of this phase */ + if ( ! header[0] ) { + empty_line_buffer ( &http->linebuf ); + + /* Handle response code */ + if ( ! ( http->flags & HTTP_TRY_AGAIN ) ) { + if ( ( rc = http_response_to_rc ( http->code ) ) != 0 ) + return rc; + } + + /* Move to next state */ + if ( http->rx_state == HTTP_RX_HEADER ) { + DBGC ( http, "HTTP %p start of data\n", http ); + http->rx_state = ( http->chunked ? + HTTP_RX_CHUNK_LEN : HTTP_RX_DATA ); + if ( ( http->partial_len != 0 ) && + ( ! ( http->flags & HTTP_TRY_AGAIN ) ) ) { + http->remaining = http->partial_len; + } + return 0; + } else { + DBGC ( http, "HTTP %p end of trailer\n", http ); + http_done ( http ); + return 0; + } + } + + DBGC ( http, "HTTP %p header \"%s\"\n", http, header ); + + /* Split header at the ": " */ + separator = strstr ( header, ": " ); + if ( ! separator ) { + DBGC ( http, "HTTP %p malformed header\n", http ); + return -EINVAL_HEADER; + } + *separator = '\0'; + value = ( separator + 2 ); + + /* Hand off to header handler, if one exists */ + for ( handler = http_header_handlers ; handler->header ; handler++ ) { + if ( strcasecmp ( header, handler->header ) == 0 ) { + if ( ( rc = handler->rx ( http, value ) ) != 0 ) + return rc; + break; + } + } + return 0; +} + +/** + * Handle HTTP chunk length + * + * @v http HTTP request + * @v length HTTP chunk length + * @ret rc Return status code + */ +static int http_rx_chunk_len ( struct http_request *http, char *length ) { + char *endp; + + /* Skip blank lines between chunks */ + if ( length[0] == '\0' ) + return 0; + + /* Parse chunk length */ + http->chunk_remaining = strtoul ( length, &endp, 16 ); + if ( *endp != '\0' ) { + DBGC ( http, "HTTP %p invalid chunk length \"%s\"\n", + http, length ); + return -EINVAL_CHUNK_LENGTH; + } + + /* Terminate chunked encoding if applicable */ + if ( http->chunk_remaining == 0 ) { + DBGC ( http, "HTTP %p end of chunks\n", http ); + http->chunked = 0; + http->rx_state = HTTP_RX_TRAILER; + return 0; + } + + /* Use seek() to notify recipient of new filesize */ + DBGC ( http, "HTTP %p start of chunk of length %zd\n", + http, http->chunk_remaining ); + if ( ! ( http->flags & HTTP_TRY_AGAIN ) ) { + xfer_seek ( &http->xfer, + ( http->rx_len + http->chunk_remaining ) ); + xfer_seek ( &http->xfer, http->rx_len ); + } + + /* Start receiving data */ + http->rx_state = HTTP_RX_DATA; + + return 0; +} + +/** An HTTP line-based data handler */ +struct http_line_handler { + /** Handle line + * + * @v http HTTP request + * @v line Line to handle + * @ret rc Return status code + */ + int ( * rx ) ( struct http_request *http, char *line ); +}; + +/** List of HTTP line-based data handlers */ +static struct http_line_handler http_line_handlers[] = { + [HTTP_RX_RESPONSE] = { .rx = http_rx_response }, + [HTTP_RX_HEADER] = { .rx = http_rx_header }, + [HTTP_RX_CHUNK_LEN] = { .rx = http_rx_chunk_len }, + [HTTP_RX_TRAILER] = { .rx = http_rx_header }, +}; + +/** + * Handle new data arriving via HTTP connection + * + * @v http HTTP request + * @v iobuf I/O buffer + * @v meta Data transfer metadata + * @ret rc Return status code + */ +static int http_socket_deliver ( struct http_request *http, + struct io_buffer *iobuf, + struct xfer_metadata *meta __unused ) { + struct http_line_handler *lh; + char *line; + size_t data_len; + ssize_t line_len; + int rc = 0; + + profile_start ( &http_rx_profiler ); + while ( iobuf && iob_len ( iobuf ) ) { + + switch ( http->rx_state ) { + case HTTP_RX_IDLE: + /* Receiving any data in this state is an error */ + DBGC ( http, "HTTP %p received %zd bytes while %s\n", + http, iob_len ( iobuf ), + ( ( http->rx_state == HTTP_RX_IDLE ) ? + "idle" : "dead" ) ); + rc = -EPROTO_UNSOLICITED; + goto done; + case HTTP_RX_DEAD: + /* Do no further processing */ + goto done; + case HTTP_RX_DATA: + /* Pass received data to caller */ + data_len = iob_len ( iobuf ); + if ( http->chunk_remaining && + ( http->chunk_remaining < data_len ) ) { + data_len = http->chunk_remaining; + } + if ( http->remaining && + ( http->remaining < data_len ) ) { + data_len = http->remaining; + } + if ( http->flags & HTTP_TRY_AGAIN ) { + /* Discard all received data */ + iob_pull ( iobuf, data_len ); + } else if ( http->rx_buffer != UNULL ) { + /* Copy to partial transfer buffer */ + copy_to_user ( http->rx_buffer, http->rx_len, + iobuf->data, data_len ); + iob_pull ( iobuf, data_len ); + } else if ( data_len < iob_len ( iobuf ) ) { + /* Deliver partial buffer as raw data */ + profile_start ( &http_xfer_profiler ); + rc = xfer_deliver_raw ( &http->xfer, + iobuf->data, data_len ); + iob_pull ( iobuf, data_len ); + if ( rc != 0 ) + goto done; + profile_stop ( &http_xfer_profiler ); + } else { + /* Deliver whole I/O buffer */ + profile_start ( &http_xfer_profiler ); + if ( ( rc = xfer_deliver_iob ( &http->xfer, + iob_disown ( iobuf ) ) ) != 0 ) + goto done; + profile_stop ( &http_xfer_profiler ); + } + http->rx_len += data_len; + if ( http->chunk_remaining ) { + http->chunk_remaining -= data_len; + if ( http->chunk_remaining == 0 ) + http->rx_state = HTTP_RX_CHUNK_LEN; + } + if ( http->remaining ) { + http->remaining -= data_len; + if ( ( http->remaining == 0 ) && + ( http->rx_state == HTTP_RX_DATA ) ) { + http_done ( http ); + } + } + break; + case HTTP_RX_RESPONSE: + case HTTP_RX_HEADER: + case HTTP_RX_CHUNK_LEN: + case HTTP_RX_TRAILER: + /* In the other phases, buffer and process a + * line at a time + */ + line_len = line_buffer ( &http->linebuf, iobuf->data, + iob_len ( iobuf ) ); + if ( line_len < 0 ) { + rc = line_len; + DBGC ( http, "HTTP %p could not buffer line: " + "%s\n", http, strerror ( rc ) ); + goto done; + } + iob_pull ( iobuf, line_len ); + line = buffered_line ( &http->linebuf ); + if ( line ) { + lh = &http_line_handlers[http->rx_state]; + if ( ( rc = lh->rx ( http, line ) ) != 0 ) + goto done; + } + break; + default: + assert ( 0 ); + break; + } + } + + done: + if ( rc ) + http_close ( http, rc ); + free_iob ( iobuf ); + profile_stop ( &http_rx_profiler ); + return rc; +} + +/** + * Check HTTP socket flow control window + * + * @v http HTTP request + * @ret len Length of window + */ +static size_t http_socket_window ( struct http_request *http __unused ) { + + /* Window is always open. This is to prevent TCP from + * stalling if our parent window is not currently open. + */ + return ( ~( ( size_t ) 0 ) ); +} + +/** + * Close HTTP socket + * + * @v http HTTP request + * @v rc Reason for close + */ +static void http_socket_close ( struct http_request *http, int rc ) { + + /* If we have an error, terminate */ + if ( rc != 0 ) { + http_close ( http, rc ); + return; + } + + /* Mark HTTP request as complete */ + http_done ( http ); +} + +/** + * Generate HTTP Basic authorisation string + * + * @v http HTTP request + * @ret auth Authorisation string, or NULL on error + * + * The authorisation string is dynamically allocated, and must be + * freed by the caller. + */ +static char * http_basic_auth ( struct http_request *http ) { + const char *user = http->uri->user; + const char *password = ( http->uri->password ? + http->uri->password : "" ); + size_t user_pw_len = + ( strlen ( user ) + 1 /* ":" */ + strlen ( password ) ); + char user_pw[ user_pw_len + 1 /* NUL */ ]; + size_t user_pw_base64_len = base64_encoded_len ( user_pw_len ); + char user_pw_base64[ user_pw_base64_len + 1 /* NUL */ ]; + char *auth; + int len; + + /* Sanity check */ + assert ( user != NULL ); + + /* Make "user:password" string from decoded fields */ + snprintf ( user_pw, sizeof ( user_pw ), "%s:%s", user, password ); + + /* Base64-encode the "user:password" string */ + base64_encode ( ( void * ) user_pw, user_pw_len, user_pw_base64 ); + + /* Generate the authorisation string */ + len = asprintf ( &auth, "Authorization: Basic %s\r\n", + user_pw_base64 ); + if ( len < 0 ) + return NULL; + + return auth; +} + +/** + * Generate HTTP Digest authorisation string + * + * @v http HTTP request + * @v method HTTP method (e.g. "GET") + * @v uri HTTP request URI (e.g. "/index.html") + * @ret auth Authorisation string, or NULL on error + * + * The authorisation string is dynamically allocated, and must be + * freed by the caller. + */ +static char * http_digest_auth ( struct http_request *http, + const char *method, const char *uri ) { + const char *user = http->uri->user; + const char *password = ( http->uri->password ? + http->uri->password : "" ); + const char *realm = http->auth_realm; + const char *nonce = http->auth_nonce; + const char *opaque = http->auth_opaque; + static const char colon = ':'; + uint8_t ctx[MD5_CTX_SIZE]; + uint8_t digest[MD5_DIGEST_SIZE]; + char ha1[ base16_encoded_len ( sizeof ( digest ) ) + 1 /* NUL */ ]; + char ha2[ base16_encoded_len ( sizeof ( digest ) ) + 1 /* NUL */ ]; + char response[ base16_encoded_len ( sizeof ( digest ) ) + 1 /* NUL */ ]; + char *auth; + int len; + + /* Sanity checks */ + assert ( user != NULL ); + assert ( realm != NULL ); + assert ( nonce != NULL ); + + /* Generate HA1 */ + digest_init ( &md5_algorithm, ctx ); + digest_update ( &md5_algorithm, ctx, user, strlen ( user ) ); + digest_update ( &md5_algorithm, ctx, &colon, sizeof ( colon ) ); + digest_update ( &md5_algorithm, ctx, realm, strlen ( realm ) ); + digest_update ( &md5_algorithm, ctx, &colon, sizeof ( colon ) ); + digest_update ( &md5_algorithm, ctx, password, strlen ( password ) ); + digest_final ( &md5_algorithm, ctx, digest ); + base16_encode ( digest, sizeof ( digest ), ha1 ); + + /* Generate HA2 */ + digest_init ( &md5_algorithm, ctx ); + digest_update ( &md5_algorithm, ctx, method, strlen ( method ) ); + digest_update ( &md5_algorithm, ctx, &colon, sizeof ( colon ) ); + digest_update ( &md5_algorithm, ctx, uri, strlen ( uri ) ); + digest_final ( &md5_algorithm, ctx, digest ); + base16_encode ( digest, sizeof ( digest ), ha2 ); + + /* Generate response */ + digest_init ( &md5_algorithm, ctx ); + digest_update ( &md5_algorithm, ctx, ha1, strlen ( ha1 ) ); + digest_update ( &md5_algorithm, ctx, &colon, sizeof ( colon ) ); + digest_update ( &md5_algorithm, ctx, nonce, strlen ( nonce ) ); + digest_update ( &md5_algorithm, ctx, &colon, sizeof ( colon ) ); + digest_update ( &md5_algorithm, ctx, ha2, strlen ( ha2 ) ); + digest_final ( &md5_algorithm, ctx, digest ); + base16_encode ( digest, sizeof ( digest ), response ); + + /* Generate the authorisation string */ + len = asprintf ( &auth, "Authorization: Digest username=\"%s\", " + "realm=\"%s\", nonce=\"%s\", uri=\"%s\", " + "%s%s%sresponse=\"%s\"\r\n", user, realm, nonce, uri, + ( opaque ? "opaque=\"" : "" ), + ( opaque ? opaque : "" ), + ( opaque ? "\", " : "" ), response ); + if ( len < 0 ) + return NULL; + + return auth; +} + +/** + * Generate HTTP POST parameter list + * + * @v http HTTP request + * @v buf Buffer to contain HTTP POST parameters + * @v len Length of buffer + * @ret len Length of parameter list (excluding terminating NUL) + */ +static size_t http_post_params ( struct http_request *http, + char *buf, size_t len ) { + struct parameter *param; + ssize_t remaining = len; + size_t frag_len; + + /* Add each parameter in the form "key=value", joined with "&" */ + len = 0; + for_each_param ( param, http->uri->params ) { + + /* Add the "&", if applicable */ + if ( len ) { + if ( remaining > 0 ) + *buf = '&'; + buf++; + len++; + remaining--; + } + + /* URI-encode the key */ + frag_len = uri_encode ( param->key, 0, buf, remaining ); + buf += frag_len; + len += frag_len; + remaining -= frag_len; + + /* Add the "=" */ + if ( remaining > 0 ) + *buf = '='; + buf++; + len++; + remaining--; + + /* URI-encode the value */ + frag_len = uri_encode ( param->value, 0, buf, remaining ); + buf += frag_len; + len += frag_len; + remaining -= frag_len; + } + + /* Ensure string is NUL-terminated even if no parameters are present */ + if ( remaining > 0 ) + *buf = '\0'; + + return len; +} + +/** + * Generate HTTP POST body + * + * @v http HTTP request + * @ret post I/O buffer containing POST body, or NULL on error + */ +static struct io_buffer * http_post ( struct http_request *http ) { + struct io_buffer *post; + size_t len; + size_t check_len; + + /* Calculate length of parameter list */ + len = http_post_params ( http, NULL, 0 ); + + /* Allocate parameter list */ + post = alloc_iob ( len + 1 /* NUL */ ); + if ( ! post ) + return NULL; + + /* Fill parameter list */ + check_len = http_post_params ( http, iob_put ( post, len ), + ( len + 1 /* NUL */ ) ); + assert ( len == check_len ); + DBGC ( http, "HTTP %p POST %s\n", http, ( ( char * ) post->data ) ); + + return post; +} + +/** + * HTTP process + * + * @v http HTTP request + */ +static void http_step ( struct http_request *http ) { + struct io_buffer *post; + struct uri host_uri; + struct uri path_uri; + char *host_uri_string; + char *path_uri_string; + char *method; + char *range; + char *auth; + char *content; + int len; + int rc; + + /* Do nothing if we have already transmitted the request */ + if ( ! ( http->flags & HTTP_TX_PENDING ) ) + return; + + /* Do nothing until socket is ready */ + if ( ! xfer_window ( &http->socket ) ) + return; + + /* Force a HEAD request if we have nowhere to send any received data */ + if ( ( xfer_window ( &http->xfer ) == 0 ) && + ( http->rx_buffer == UNULL ) ) { + http->flags |= ( HTTP_HEAD_ONLY | HTTP_CLIENT_KEEPALIVE ); + } + + /* Determine method */ + method = ( ( http->flags & HTTP_HEAD_ONLY ) ? "HEAD" : + ( http->uri->params ? "POST" : "GET" ) ); + + /* Construct host URI */ + memset ( &host_uri, 0, sizeof ( host_uri ) ); + host_uri.host = http->uri->host; + host_uri.port = http->uri->port; + host_uri_string = format_uri_alloc ( &host_uri ); + if ( ! host_uri_string ) { + rc = -ENOMEM; + goto err_host_uri; + } + + /* Construct path URI */ + memset ( &path_uri, 0, sizeof ( path_uri ) ); + path_uri.path = ( http->uri->path ? http->uri->path : "/" ); + path_uri.query = http->uri->query; + path_uri_string = format_uri_alloc ( &path_uri ); + if ( ! path_uri_string ) { + rc = -ENOMEM; + goto err_path_uri; + } + + /* Calculate range request parameters if applicable */ + if ( http->partial_len ) { + len = asprintf ( &range, "Range: bytes=%zd-%zd\r\n", + http->partial_start, + ( http->partial_start + http->partial_len + - 1 ) ); + if ( len < 0 ) { + rc = len; + goto err_range; + } + } else { + range = NULL; + } + + /* Construct authorisation, if applicable */ + if ( http->flags & HTTP_BASIC_AUTH ) { + auth = http_basic_auth ( http ); + if ( ! auth ) { + rc = -ENOMEM; + goto err_auth; + } + } else if ( http->flags & HTTP_DIGEST_AUTH ) { + auth = http_digest_auth ( http, method, path_uri_string ); + if ( ! auth ) { + rc = -ENOMEM; + goto err_auth; + } + } else { + auth = NULL; + } + + /* Construct POST content, if applicable */ + if ( http->uri->params ) { + post = http_post ( http ); + if ( ! post ) { + rc = -ENOMEM; + goto err_post; + } + len = asprintf ( &content, "Content-Type: " + "application/x-www-form-urlencoded\r\n" + "Content-Length: %zd\r\n", iob_len ( post ) ); + if ( len < 0 ) { + rc = len; + goto err_content; + } + } else { + post = NULL; + content = NULL; + } + + /* Mark request as transmitted */ + http->flags &= ~HTTP_TX_PENDING; + + /* Send request */ + if ( ( rc = xfer_printf ( &http->socket, + "%s %s HTTP/1.1\r\n" + "User-Agent: iPXE/%s\r\n" + "Host: %s\r\n" + "%s%s%s%s" + "\r\n", + method, path_uri_string, product_version, + host_uri_string, + ( ( http->flags & HTTP_CLIENT_KEEPALIVE ) ? + "Connection: keep-alive\r\n" : "" ), + ( range ? range : "" ), + ( auth ? auth : "" ), + ( content ? content : "" ) ) ) != 0 ) { + goto err_xfer; + } + + /* Send POST content, if applicable */ + if ( post ) { + if ( ( rc = xfer_deliver_iob ( &http->socket, + iob_disown ( post ) ) ) != 0 ) + goto err_xfer_post; + } + + err_xfer_post: + err_xfer: + free ( content ); + err_content: + free ( post ); + err_post: + free ( auth ); + err_auth: + free ( range ); + err_range: + free ( path_uri_string ); + err_path_uri: + free ( host_uri_string ); + err_host_uri: + if ( rc != 0 ) + http_close ( http, rc ); +} + +/** + * Check HTTP data transfer flow control window + * + * @v http HTTP request + * @ret len Length of window + */ +static size_t http_xfer_window ( struct http_request *http ) { + + /* New block commands may be issued only when we are idle */ + return ( ( http->rx_state == HTTP_RX_IDLE ) ? 1 : 0 ); +} + +/** + * Initiate HTTP partial read + * + * @v http HTTP request + * @v partial Partial transfer interface + * @v offset Starting offset + * @v buffer Data buffer + * @v len Length + * @ret rc Return status code + */ +static int http_partial_read ( struct http_request *http, + struct interface *partial, + size_t offset, userptr_t buffer, size_t len ) { + + /* Sanity check */ + if ( http_xfer_window ( http ) == 0 ) + return -EBUSY; + + /* Initialise partial transfer parameters */ + http->rx_buffer = buffer; + http->partial_start = offset; + http->partial_len = len; + + /* Schedule request */ + http->rx_state = HTTP_RX_RESPONSE; + http->flags = ( HTTP_TX_PENDING | HTTP_CLIENT_KEEPALIVE ); + if ( ! len ) + http->flags |= HTTP_HEAD_ONLY; + process_add ( &http->process ); + + /* Attach to parent interface and return */ + intf_plug_plug ( &http->partial, partial ); + + return 0; +} + +/** + * Issue HTTP block device read + * + * @v http HTTP request + * @v block Block data interface + * @v lba Starting logical block address + * @v count Number of blocks to transfer + * @v buffer Data buffer + * @v len Length of data buffer + * @ret rc Return status code + */ +static int http_block_read ( struct http_request *http, + struct interface *block, + uint64_t lba, unsigned int count, + userptr_t buffer, size_t len __unused ) { + + return http_partial_read ( http, block, ( lba * HTTP_BLKSIZE ), + buffer, ( count * HTTP_BLKSIZE ) ); +} + +/** + * Read HTTP block device capacity + * + * @v http HTTP request + * @v block Block data interface + * @ret rc Return status code + */ +static int http_block_read_capacity ( struct http_request *http, + struct interface *block ) { + + return http_partial_read ( http, block, 0, 0, 0 ); +} + +/** + * Describe HTTP device in an ACPI table + * + * @v http HTTP request + * @v acpi ACPI table + * @v len Length of ACPI table + * @ret rc Return status code + */ +static int http_acpi_describe ( struct http_request *http, + struct acpi_description_header *acpi, + size_t len ) { + + DBGC ( http, "HTTP %p cannot yet describe device in an ACPI table\n", + http ); + ( void ) acpi; + ( void ) len; + return 0; +} + +/** HTTP socket interface operations */ +static struct interface_operation http_socket_operations[] = { + INTF_OP ( xfer_window, struct http_request *, http_socket_window ), + INTF_OP ( xfer_deliver, struct http_request *, http_socket_deliver ), + INTF_OP ( xfer_window_changed, struct http_request *, http_step ), + INTF_OP ( intf_close, struct http_request *, http_socket_close ), +}; + +/** HTTP socket interface descriptor */ +static struct interface_descriptor http_socket_desc = + INTF_DESC_PASSTHRU ( struct http_request, socket, + http_socket_operations, xfer ); + +/** HTTP partial transfer interface operations */ +static struct interface_operation http_partial_operations[] = { + INTF_OP ( intf_close, struct http_request *, http_close ), +}; + +/** HTTP partial transfer interface descriptor */ +static struct interface_descriptor http_partial_desc = + INTF_DESC ( struct http_request, partial, http_partial_operations ); + +/** HTTP data transfer interface operations */ +static struct interface_operation http_xfer_operations[] = { + INTF_OP ( xfer_window, struct http_request *, http_xfer_window ), + INTF_OP ( block_read, struct http_request *, http_block_read ), + INTF_OP ( block_read_capacity, struct http_request *, + http_block_read_capacity ), + INTF_OP ( intf_close, struct http_request *, http_close ), + INTF_OP ( acpi_describe, struct http_request *, http_acpi_describe ), +}; + +/** HTTP data transfer interface descriptor */ +static struct interface_descriptor http_xfer_desc = + INTF_DESC_PASSTHRU ( struct http_request, xfer, + http_xfer_operations, socket ); + +/** HTTP process descriptor */ +static struct process_descriptor http_process_desc = + PROC_DESC_ONCE ( struct http_request, process, http_step ); + +/** + * Initiate an HTTP connection, with optional filter + * + * @v xfer Data transfer interface + * @v uri Uniform Resource Identifier + * @v default_port Default port number + * @v filter Filter to apply to socket, or NULL + * @ret rc Return status code + */ +int http_open_filter ( struct interface *xfer, struct uri *uri, + unsigned int default_port, + int ( * filter ) ( struct interface *xfer, + const char *name, + struct interface **next ) ) { + struct http_request *http; + int rc; + + /* Sanity checks */ + if ( ! uri->host ) + return -EINVAL; + + /* Allocate and populate HTTP structure */ + http = zalloc ( sizeof ( *http ) ); + if ( ! http ) + return -ENOMEM; + ref_init ( &http->refcnt, http_free ); + intf_init ( &http->xfer, &http_xfer_desc, &http->refcnt ); + intf_init ( &http->partial, &http_partial_desc, &http->refcnt ); + http->uri = uri_get ( uri ); + http->default_port = default_port; + http->filter = filter; + intf_init ( &http->socket, &http_socket_desc, &http->refcnt ); + process_init ( &http->process, &http_process_desc, &http->refcnt ); + timer_init ( &http->timer, http_retry, &http->refcnt ); + http->flags = HTTP_TX_PENDING; + + /* Open socket */ + if ( ( rc = http_socket_open ( http ) ) != 0 ) + goto err; + + /* Attach to parent interface, mortalise self, and return */ + intf_plug_plug ( &http->xfer, xfer ); + ref_put ( &http->refcnt ); + return 0; + + err: + DBGC ( http, "HTTP %p could not create request: %s\n", + http, strerror ( rc ) ); + http_close ( http, rc ); + ref_put ( &http->refcnt ); + return rc; +}