diff --git a/src/net/tcp/http.c b/src/net/tcp/http.c index 432e5cd9..c4c1b628 100644 --- a/src/net/tcp/http.c +++ b/src/net/tcp/http.c @@ -44,14 +44,23 @@ FILE_LICENCE ( GPL2_OR_LATER ); #include #include #include +#include +#include #include FEATURE ( FEATURE_PROTOCOL, "HTTP", DHCP_EB_FEATURE_HTTP, 1 ); -/** HTTP transmission state */ -enum http_tx_state { - HTTP_TX_REQUEST = 0, - HTTP_TX_DONE, +/** Block size used for HTTP block device request */ +#define HTTP_BLKSIZE 512 + +/** HTTP flags */ +enum http_flags { + /** Request is waiting to be transmitted */ + HTTP_TX_PENDING = 0x0001, + /** Fetch header only */ + HTTP_HEAD_ONLY = 0x0002, + /** Keep connection alive */ + HTTP_KEEPALIVE = 0x0004, }; /** HTTP receive state */ @@ -61,6 +70,7 @@ enum http_rx_state { HTTP_RX_CHUNK_LEN, HTTP_RX_DATA, HTTP_RX_TRAILER, + HTTP_RX_IDLE, HTTP_RX_DEAD, }; @@ -73,31 +83,38 @@ struct http_request { struct refcnt refcnt; /** Data transfer interface */ struct interface xfer; + /** Partial transfer interface */ + struct interface partial; /** URI being fetched */ struct uri *uri; /** 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; - /** TX state */ - enum http_tx_state tx_state; - /** HTTP response code */ - unsigned int response; - /** HTTP Content-Length */ - size_t content_length; - /** HTTP is using Transfer-Encoding: chunked */ - int chunked; - /** Current chunk length */ - size_t chunk_len; - /** Received length */ - size_t rx_len; /** RX state */ enum http_rx_state rx_state; + /** 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; }; /** @@ -115,12 +132,12 @@ static void http_free ( struct refcnt *refcnt ) { }; /** - * Mark HTTP request as complete + * Close HTTP request * * @v http HTTP request * @v rc Return status code */ -static void http_done ( struct http_request *http, int rc ) { +static void http_close ( struct http_request *http, int rc ) { /* Prevent further processing of any current packet */ http->rx_state = HTTP_RX_DEAD; @@ -128,11 +145,11 @@ static void http_done ( struct http_request *http, int rc ) { /* If we had a Content-Length, and the received content length * isn't correct, flag an error */ - if ( http->content_length && - ( http->content_length != http->rx_len ) ) { + if ( http->remaining != 0 ) { DBGC ( http, "HTTP %p incorrect length %zd, should be %zd\n", - http, http->rx_len, http->content_length ); - rc = -EIO; + http, http->rx_len, ( http->rx_len + http->remaining ) ); + if ( rc == 0 ) + rc = -EIO; } /* Remove process */ @@ -140,9 +157,40 @@ static void http_done ( struct http_request *http, int rc ) { /* Close all data transfer interfaces */ intf_shutdown ( &http->socket, rc ); + intf_shutdown ( &http->partial, rc ); intf_shutdown ( &http->xfer, rc ); } +/** + * Mark HTTP request as completed successfully + * + * @v http HTTP request + */ +static void http_done ( struct http_request *http ) { + + /* If we had a Content-Length, and the received content length + * isn't correct, force an error + */ + if ( http->remaining != 0 ) { + http_close ( http, -EIO ); + 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 */ + intf_restart ( &http->partial, 0 ); + + /* Close everything unless we are keeping the connection alive */ + if ( ! ( http->flags & HTTP_KEEPALIVE ) ) + http_close ( http, 0 ); +} + /** * Convert HTTP response code to return status code * @@ -152,6 +200,7 @@ static void http_done ( struct http_request *http, int rc ) { static int http_response_to_rc ( unsigned int response ) { switch ( response ) { case 200: + case 206: case 301: case 302: return 0; @@ -175,6 +224,7 @@ static int http_response_to_rc ( unsigned int response ) { */ static int http_rx_response ( struct http_request *http, char *response ) { char *spc; + unsigned int code; int rc; DBGC ( http, "HTTP %p response \"%s\"\n", http, response ); @@ -187,8 +237,8 @@ static int http_rx_response ( struct http_request *http, char *response ) { spc = strchr ( response, ' ' ); if ( ! spc ) return -EIO; - http->response = strtoul ( spc, NULL, 10 ); - if ( ( rc = http_response_to_rc ( http->response ) ) != 0 ) + code = strtoul ( spc, NULL, 10 ); + if ( ( rc = http_response_to_rc ( code ) ) != 0 ) return rc; /* Move to received headers */ @@ -227,19 +277,40 @@ static int http_rx_location ( struct http_request *http, const char *value ) { */ static int http_rx_content_length ( struct http_request *http, const char *value ) { + struct block_device_capacity capacity; + size_t content_len; char *endp; - http->content_length = strtoul ( value, &endp, 10 ); + /* Parse content length */ + content_len = strtoul ( value, &endp, 10 ); if ( *endp != '\0' ) { DBGC ( http, "HTTP %p invalid Content-Length \"%s\"\n", http, value ); return -EIO; } + /* 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; + } + if ( ! ( http->flags & HTTP_HEAD_ONLY ) ) + http->remaining = content_len; + /* Use seek() to notify recipient of filesize */ - xfer_seek ( &http->xfer, http->content_length ); + 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; } @@ -309,14 +380,15 @@ static int http_rx_header ( struct http_request *http, char *header ) { /* An empty header line marks the end of this phase */ if ( ! header[0] ) { empty_line_buffer ( &http->linebuf ); - if ( http->rx_state == HTTP_RX_HEADER ) { + if ( ( http->rx_state == HTTP_RX_HEADER ) && + ( ! ( http->flags & HTTP_HEAD_ONLY ) ) ) { DBGC ( http, "HTTP %p start of data\n", http ); http->rx_state = ( http->chunked ? HTTP_RX_CHUNK_LEN : HTTP_RX_DATA ); return 0; } else { DBGC ( http, "HTTP %p end of trailer\n", http ); - http_done ( http, 0 ); + http_done ( http ); return 0; } } @@ -358,7 +430,7 @@ static int http_rx_chunk_len ( struct http_request *http, char *length ) { return 0; /* Parse chunk length */ - http->chunk_len = strtoul ( length, &endp, 16 ); + http->chunk_remaining = strtoul ( length, &endp, 16 ); if ( *endp != '\0' ) { DBGC ( http, "HTTP %p invalid chunk length \"%s\"\n", http, length ); @@ -366,7 +438,7 @@ static int http_rx_chunk_len ( struct http_request *http, char *length ) { } /* Terminate chunked encoding if applicable */ - if ( http->chunk_len == 0 ) { + if ( http->chunk_remaining == 0 ) { DBGC ( http, "HTTP %p end of chunks\n", http ); http->chunked = 0; http->rx_state = HTTP_RX_TRAILER; @@ -375,8 +447,8 @@ static int http_rx_chunk_len ( struct http_request *http, char *length ) { /* Use seek() to notify recipient of new filesize */ DBGC ( http, "HTTP %p start of chunk of length %zd\n", - http, http->chunk_len ); - xfer_seek ( &http->xfer, ( http->rx_len + http->chunk_len ) ); + http, http->chunk_remaining ); + xfer_seek ( &http->xfer, ( http->rx_len + http->chunk_remaining ) ); xfer_seek ( &http->xfer, http->rx_len ); /* Start receiving data */ @@ -422,34 +494,60 @@ static int http_socket_deliver ( struct http_request *http, int rc = 0; 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; + 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_len && ( http->chunk_len < data_len )){ - data_len = http->chunk_len; + 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->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 */ rc = xfer_deliver_raw ( &http->xfer, iobuf->data, data_len ); iob_pull ( iobuf, data_len ); + if ( rc != 0 ) + goto done; } else { - rc = xfer_deliver_iob ( &http->xfer, - iob_disown ( iobuf ) ); - } - if ( rc != 0 ) - goto done; - if ( http->chunk_len ) { - http->chunk_len -= data_len; - if ( http->chunk_len == 0 ) - http->rx_state = HTTP_RX_CHUNK_LEN; + /* Deliver whole I/O buffer */ + if ( ( rc = xfer_deliver_iob ( &http->xfer, + iob_disown ( iobuf ) ) ) != 0 ) + goto done; } http->rx_len += data_len; - if ( http->content_length && - ( http->rx_len >= http->content_length ) ) { - http_done ( http, 0 ); - goto done; + 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: @@ -483,11 +581,25 @@ static int http_socket_deliver ( struct http_request *http, done: if ( rc ) - http_done ( http, rc ); + http_close ( http, rc ); free_iob ( iobuf ); 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 ) ); +} + /** * HTTP process * @@ -507,9 +619,11 @@ static void http_step ( struct http_request *http ) { int request_len = unparse_uri ( NULL, 0, http->uri, URI_PATH_BIT | URI_QUERY_BIT ); char request[ request_len + 1 /* NUL */ ]; + char range[48]; /* Enough for two 64-bit integers in decimal */ + int partial; /* Do nothing if we have already transmitted the request */ - if ( http->tx_state != HTTP_TX_REQUEST ) + if ( ! ( http->flags & HTTP_TX_PENDING ) ) return; /* Do nothing until socket is ready */ @@ -530,32 +644,151 @@ static void http_step ( struct http_request *http ) { base64_encode ( user_pw, user_pw_len, user_pw_base64 ); } + /* 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_KEEPALIVE ); + } + + /* Determine type of request */ + partial = ( http->partial_len != 0 ); + snprintf ( range, sizeof ( range ), "%d-%d", http->partial_start, + ( http->partial_start + http->partial_len - 1 ) ); + /* Mark request as transmitted */ - http->tx_state = HTTP_TX_DONE; + http->flags &= ~HTTP_TX_PENDING; /* Send GET request */ if ( ( rc = xfer_printf ( &http->socket, - "GET %s%s HTTP/1.1\r\n" + "%s %s%s HTTP/1.1\r\n" "User-Agent: iPXE/" VERSION "\r\n" - "%s%s%s" "Host: %s\r\n" + "%s%s%s%s%s%s%s" "\r\n", - http->uri->path ? "" : "/", - request, + ( ( http->flags & HTTP_HEAD_ONLY ) ? + "HEAD" : "GET" ), + ( http->uri->path ? "" : "/" ), + request, host, + ( ( http->flags & HTTP_KEEPALIVE ) ? + "Connection: Keep-Alive\r\n" : "" ), + ( partial ? "Range: bytes=" : "" ), + ( partial ? range : "" ), + ( partial ? "\r\n" : "" ), ( user ? "Authorization: Basic " : "" ), ( user ? user_pw_base64 : "" ), - ( user ? "\r\n" : "" ), - host ) ) != 0 ) { - http_done ( http, rc ); + ( user ? "\r\n" : "" ) ) ) != 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; + http->remaining = len; + + /* Schedule request */ + http->rx_state = HTTP_RX_RESPONSE; + http->flags = ( HTTP_TX_PENDING | HTTP_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_done ), + INTF_OP ( intf_close, struct http_request *, http_close ), }; /** HTTP socket interface descriptor */ @@ -563,9 +796,23 @@ 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 ( intf_close, struct http_request *, http_done ), + 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 */ @@ -605,9 +852,11 @@ int http_open_filter ( struct interface *xfer, struct uri *uri, 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 ); intf_init ( &http->socket, &http_socket_desc, &http->refcnt ); process_init ( &http->process, &http_process_desc, &http->refcnt ); + http->flags = HTTP_TX_PENDING; /* Open socket */ memset ( &server, 0, sizeof ( server ) ); @@ -630,7 +879,7 @@ int http_open_filter ( struct interface *xfer, struct uri *uri, err: DBGC ( http, "HTTP %p could not create request: %s\n", http, strerror ( rc ) ); - http_done ( http, rc ); + http_close ( http, rc ); ref_put ( &http->refcnt ); return rc; }