| /* |
| * Copyright (C) 2015 Michael Brown <mbrown@fensystems.co.uk>. |
| * |
| * 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. |
| * |
| * You can also choose to distribute this program under the terms of |
| * the Unmodified Binary Distribution Licence (as given in the file |
| * COPYING.UBDL), provided that you have satisfied its requirements. |
| */ |
| |
| FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); |
| |
| /** |
| * @file |
| * |
| * Hyper Text Transfer Protocol (HTTP) core functionality |
| * |
| */ |
| |
| #include <stdint.h> |
| #include <stdlib.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <strings.h> |
| #include <byteswap.h> |
| #include <errno.h> |
| #include <ctype.h> |
| #include <assert.h> |
| #include <ipxe/uri.h> |
| #include <ipxe/refcnt.h> |
| #include <ipxe/iobuf.h> |
| #include <ipxe/xfer.h> |
| #include <ipxe/open.h> |
| #include <ipxe/process.h> |
| #include <ipxe/retry.h> |
| #include <ipxe/timer.h> |
| #include <ipxe/linebuf.h> |
| #include <ipxe/xferbuf.h> |
| #include <ipxe/blockdev.h> |
| #include <ipxe/acpi.h> |
| #include <ipxe/version.h> |
| #include <ipxe/params.h> |
| #include <ipxe/profile.h> |
| #include <ipxe/vsprintf.h> |
| #include <ipxe/errortab.h> |
| #include <ipxe/efi/efi_path.h> |
| #include <ipxe/http.h> |
| |
| /* 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 EINVAL_STATUS __einfo_error ( EINFO_EINVAL_STATUS ) |
| #define EINFO_EINVAL_STATUS \ |
| __einfo_uniqify ( EINFO_EINVAL, 0x01, "Invalid status line" ) |
| #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 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 EIO_4XX __einfo_error ( EINFO_EIO_4XX ) |
| #define EINFO_EIO_4XX \ |
| __einfo_uniqify ( EINFO_EIO, 0x04, "HTTP 4xx Client Error" ) |
| #define EIO_5XX __einfo_error ( EINFO_EIO_5XX ) |
| #define EINFO_EIO_5XX \ |
| __einfo_uniqify ( EINFO_EIO, 0x05, "HTTP 5xx Server Error" ) |
| #define ENOENT_404 __einfo_error ( EINFO_ENOENT_404 ) |
| #define EINFO_ENOENT_404 \ |
| __einfo_uniqify ( EINFO_ENOENT, 0x01, "Not found" ) |
| #define ENOTSUP_CONNECTION __einfo_error ( EINFO_ENOTSUP_CONNECTION ) |
| #define EINFO_ENOTSUP_CONNECTION \ |
| __einfo_uniqify ( EINFO_ENOTSUP, 0x01, "Unsupported connection header" ) |
| #define ENOTSUP_TRANSFER __einfo_error ( EINFO_ENOTSUP_TRANSFER ) |
| #define EINFO_ENOTSUP_TRANSFER \ |
| __einfo_uniqify ( EINFO_ENOTSUP, 0x02, "Unsupported transfer encoding" ) |
| #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" ) |
| |
| /** 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" }; |
| |
| /** Human-readable error messages */ |
| struct errortab http_errors[] __errortab = { |
| __einfo_errortab ( EINFO_ENOENT_404 ), |
| __einfo_errortab ( EINFO_EIO_4XX ), |
| __einfo_errortab ( EINFO_EIO_5XX ), |
| }; |
| |
| static struct http_state http_request; |
| static struct http_state http_headers; |
| static struct http_state http_trailers; |
| static struct http_transfer_encoding http_transfer_identity; |
| |
| /****************************************************************************** |
| * |
| * Methods |
| * |
| ****************************************************************************** |
| */ |
| |
| /** HTTP HEAD method */ |
| struct http_method http_head = { |
| .name = "HEAD", |
| }; |
| |
| /** HTTP GET method */ |
| struct http_method http_get = { |
| .name = "GET", |
| }; |
| |
| /** HTTP POST method */ |
| struct http_method http_post = { |
| .name = "POST", |
| }; |
| |
| /****************************************************************************** |
| * |
| * Utility functions |
| * |
| ****************************************************************************** |
| */ |
| |
| /** |
| * Handle received HTTP line-buffered data |
| * |
| * @v http HTTP transaction |
| * @v iobuf I/O buffer |
| * @v linebuf Line buffer |
| * @ret rc Return status code |
| */ |
| static int http_rx_linebuf ( struct http_transaction *http, |
| struct io_buffer *iobuf, |
| struct line_buffer *linebuf ) { |
| int consumed; |
| int rc; |
| |
| /* Buffer received line */ |
| consumed = line_buffer ( linebuf, iobuf->data, iob_len ( iobuf ) ); |
| if ( consumed < 0 ) { |
| rc = consumed; |
| DBGC ( http, "HTTP %p could not buffer line: %s\n", |
| http, strerror ( rc ) ); |
| return rc; |
| } |
| |
| /* Consume line */ |
| iob_pull ( iobuf, consumed ); |
| |
| return 0; |
| } |
| |
| /** |
| * Get HTTP response token |
| * |
| * @v line Line position |
| * @v value Token value to fill in (if any) |
| * @ret token Token, or NULL |
| */ |
| char * http_token ( char **line, char **value ) { |
| char *token; |
| char quote = '\0'; |
| char c; |
| |
| /* Avoid returning uninitialised data */ |
| if ( value ) |
| *value = NULL; |
| |
| /* Skip any initial whitespace or commas */ |
| while ( ( isspace ( **line ) ) || ( **line == ',' ) ) |
| (*line)++; |
| |
| /* Check for end of line and record token position */ |
| if ( ! **line ) |
| return NULL; |
| token = *line; |
| |
| /* Scan for end of token */ |
| while ( ( c = **line ) ) { |
| |
| /* Terminate if we hit an unquoted whitespace or comma */ |
| if ( ( isspace ( c ) || ( c == ',' ) ) && ! quote ) |
| break; |
| |
| /* Terminate if we hit a closing quote */ |
| if ( c == quote ) |
| break; |
| |
| /* Check for value separator */ |
| if ( value && ( ! *value ) && ( c == '=' ) ) { |
| |
| /* Terminate key portion of token */ |
| *((*line)++) = '\0'; |
| |
| /* Check for quote character */ |
| c = **line; |
| if ( ( c == '"' ) || ( c == '\'' ) ) { |
| quote = c; |
| (*line)++; |
| } |
| |
| /* Record value portion of token */ |
| *value = *line; |
| |
| } else { |
| |
| /* Move to next character */ |
| (*line)++; |
| } |
| } |
| |
| /* Terminate token, if applicable */ |
| if ( c ) |
| *((*line)++) = '\0'; |
| |
| return token; |
| } |
| |
| /****************************************************************************** |
| * |
| * Transactions |
| * |
| ****************************************************************************** |
| */ |
| |
| /** |
| * Free HTTP transaction |
| * |
| * @v refcnt Reference count |
| */ |
| static void http_free ( struct refcnt *refcnt ) { |
| struct http_transaction *http = |
| container_of ( refcnt, struct http_transaction, refcnt ); |
| |
| empty_line_buffer ( &http->response.headers ); |
| empty_line_buffer ( &http->linebuf ); |
| uri_put ( http->uri ); |
| free ( http ); |
| } |
| |
| /** |
| * Close HTTP transaction |
| * |
| * @v http HTTP transaction |
| * @v rc Reason for close |
| */ |
| static void http_close ( struct http_transaction *http, int rc ) { |
| |
| /* Stop process */ |
| process_del ( &http->process ); |
| |
| /* Stop timer */ |
| stop_timer ( &http->timer ); |
| |
| /* Close all interfaces */ |
| intfs_shutdown ( rc, &http->conn, &http->transfer, &http->content, |
| &http->xfer, NULL ); |
| } |
| |
| /** |
| * Close HTTP transaction with error (even if none specified) |
| * |
| * @v http HTTP transaction |
| * @v rc Reason for close |
| */ |
| static void http_close_error ( struct http_transaction *http, int rc ) { |
| |
| /* Treat any close as an error */ |
| http_close ( http, ( rc ? rc : -EPIPE ) ); |
| } |
| |
| /** |
| * Reopen stale HTTP connection |
| * |
| * @v http HTTP transaction |
| */ |
| static void http_reopen ( struct http_transaction *http ) { |
| int rc; |
| |
| /* Close existing connection */ |
| intf_restart ( &http->conn, -ECANCELED ); |
| |
| /* Reopen connection */ |
| if ( ( rc = http_connect ( &http->conn, http->uri ) ) != 0 ) { |
| DBGC ( http, "HTTP %p could not reconnect: %s\n", |
| http, strerror ( rc ) ); |
| goto err_connect; |
| } |
| |
| /* Reset state */ |
| http->state = &http_request; |
| |
| /* Reschedule transmission process */ |
| process_add ( &http->process ); |
| |
| return; |
| |
| err_connect: |
| http_close ( http, rc ); |
| } |
| |
| /** |
| * Handle retry timer expiry |
| * |
| * @v timer Retry timer |
| * @v over Failure indicator |
| */ |
| static void http_expired ( struct retry_timer *timer, int over __unused ) { |
| struct http_transaction *http = |
| container_of ( timer, struct http_transaction, timer ); |
| |
| /* Reopen connection */ |
| http_reopen ( http ); |
| } |
| |
| /** |
| * HTTP transmit process |
| * |
| * @v http HTTP transaction |
| */ |
| static void http_step ( struct http_transaction *http ) { |
| int rc; |
| |
| /* Do nothing if we have nothing to transmit */ |
| if ( ! http->state->tx ) |
| return; |
| |
| /* Do nothing until connection is ready */ |
| if ( ! xfer_window ( &http->conn ) ) |
| return; |
| |
| /* Notify data transfer interface that window may have changed */ |
| xfer_window_changed ( &http->xfer ); |
| |
| /* Do nothing until data transfer interface is ready */ |
| if ( ! xfer_window ( &http->xfer ) ) |
| return; |
| |
| /* Transmit data */ |
| if ( ( rc = http->state->tx ( http ) ) != 0 ) |
| goto err; |
| |
| return; |
| |
| err: |
| http_close ( http, rc ); |
| } |
| |
| /** |
| * Handle received HTTP data |
| * |
| * @v http HTTP transaction |
| * @v iobuf I/O buffer |
| * @v meta Transfer metadata |
| * @ret rc Return status code |
| * |
| * This function takes ownership of the I/O buffer. |
| */ |
| static int http_conn_deliver ( struct http_transaction *http, |
| struct io_buffer *iobuf, |
| struct xfer_metadata *meta __unused ) { |
| int rc; |
| |
| /* Handle received data */ |
| profile_start ( &http_rx_profiler ); |
| while ( iobuf && iob_len ( iobuf ) ) { |
| |
| /* Sanity check */ |
| if ( ( ! http->state ) || ( ! http->state->rx ) ) { |
| DBGC ( http, "HTTP %p unexpected data\n", http ); |
| rc = -EPROTO_UNSOLICITED; |
| goto err; |
| } |
| |
| /* Receive (some) data */ |
| if ( ( rc = http->state->rx ( http, &iobuf ) ) != 0 ) |
| goto err; |
| } |
| |
| /* Free I/O buffer, if applicable */ |
| free_iob ( iobuf ); |
| |
| profile_stop ( &http_rx_profiler ); |
| return 0; |
| |
| err: |
| free_iob ( iobuf ); |
| http_close ( http, rc ); |
| return rc; |
| } |
| |
| /** |
| * Handle server connection close |
| * |
| * @v http HTTP transaction |
| * @v rc Reason for close |
| */ |
| static void http_conn_close ( struct http_transaction *http, int rc ) { |
| |
| /* Sanity checks */ |
| assert ( http->state != NULL ); |
| assert ( http->state->close != NULL ); |
| |
| /* Restart server connection interface */ |
| intf_restart ( &http->conn, rc ); |
| |
| /* Hand off to state-specific method */ |
| http->state->close ( http, rc ); |
| } |
| |
| /** |
| * Handle received content-decoded data |
| * |
| * @v http HTTP transaction |
| * @v iobuf I/O buffer |
| * @v meta Data transfer metadata |
| */ |
| static int http_content_deliver ( struct http_transaction *http, |
| struct io_buffer *iobuf, |
| struct xfer_metadata *meta ) { |
| int rc; |
| |
| /* Ignore content if this is anything other than a successful |
| * transfer. |
| */ |
| if ( http->response.rc != 0 ) { |
| free_iob ( iobuf ); |
| return 0; |
| } |
| |
| /* Deliver to data transfer interface */ |
| profile_start ( &http_xfer_profiler ); |
| if ( ( rc = xfer_deliver ( &http->xfer, iob_disown ( iobuf ), |
| meta ) ) != 0 ) |
| return rc; |
| profile_stop ( &http_xfer_profiler ); |
| |
| return 0; |
| } |
| |
| /** |
| * Get underlying data transfer buffer |
| * |
| * @v http HTTP transaction |
| * @ret xferbuf Data transfer buffer, or NULL on error |
| */ |
| static struct xfer_buffer * |
| http_content_buffer ( struct http_transaction *http ) { |
| |
| /* Deny access to the data transfer buffer if this is anything |
| * other than a successful transfer. |
| */ |
| if ( http->response.rc != 0 ) |
| return NULL; |
| |
| /* Hand off to data transfer interface */ |
| return xfer_buffer ( &http->xfer ); |
| } |
| |
| /** |
| * Read from block device (when HTTP block device support is not present) |
| * |
| * @v http HTTP transaction |
| * @v data Data interface |
| * @v lba Starting logical block address |
| * @v count Number of logical blocks |
| * @v buffer Data buffer |
| * @v len Length of data buffer |
| * @ret rc Return status code |
| */ |
| __weak int http_block_read ( struct http_transaction *http __unused, |
| struct interface *data __unused, |
| uint64_t lba __unused, unsigned int count __unused, |
| userptr_t buffer __unused, size_t len __unused ) { |
| |
| return -ENOTSUP; |
| } |
| |
| /** |
| * Read block device capacity (when HTTP block device support is not present) |
| * |
| * @v control Control interface |
| * @v data Data interface |
| * @ret rc Return status code |
| */ |
| __weak int http_block_read_capacity ( struct http_transaction *http __unused, |
| struct interface *data __unused ) { |
| |
| return -ENOTSUP; |
| } |
| |
| /** |
| * Describe as an EFI device path |
| * |
| * @v http HTTP transaction |
| * @ret path EFI device path, or NULL on error |
| */ |
| static EFI_DEVICE_PATH_PROTOCOL * |
| http_efi_describe ( struct http_transaction *http ) { |
| |
| return efi_uri_path ( http->uri ); |
| } |
| |
| /** HTTP data transfer interface operations */ |
| static struct interface_operation http_xfer_operations[] = { |
| INTF_OP ( block_read, struct http_transaction *, http_block_read ), |
| INTF_OP ( block_read_capacity, struct http_transaction *, |
| http_block_read_capacity ), |
| INTF_OP ( xfer_window_changed, struct http_transaction *, http_step ), |
| INTF_OP ( intf_close, struct http_transaction *, http_close ), |
| EFI_INTF_OP ( efi_describe, struct http_transaction *, |
| http_efi_describe ), |
| }; |
| |
| /** HTTP data transfer interface descriptor */ |
| static struct interface_descriptor http_xfer_desc = |
| INTF_DESC_PASSTHRU ( struct http_transaction, xfer, |
| http_xfer_operations, content ); |
| |
| /** HTTP content-decoded interface operations */ |
| static struct interface_operation http_content_operations[] = { |
| INTF_OP ( xfer_deliver, struct http_transaction *, |
| http_content_deliver ), |
| INTF_OP ( xfer_buffer, struct http_transaction *, http_content_buffer ), |
| INTF_OP ( intf_close, struct http_transaction *, http_close ), |
| }; |
| |
| /** HTTP content-decoded interface descriptor */ |
| static struct interface_descriptor http_content_desc = |
| INTF_DESC_PASSTHRU ( struct http_transaction, content, |
| http_content_operations, xfer ); |
| |
| /** HTTP transfer-decoded interface operations */ |
| static struct interface_operation http_transfer_operations[] = { |
| INTF_OP ( intf_close, struct http_transaction *, http_close ), |
| }; |
| |
| /** HTTP transfer-decoded interface descriptor */ |
| static struct interface_descriptor http_transfer_desc = |
| INTF_DESC_PASSTHRU ( struct http_transaction, transfer, |
| http_transfer_operations, conn ); |
| |
| /** HTTP server connection interface operations */ |
| static struct interface_operation http_conn_operations[] = { |
| INTF_OP ( xfer_deliver, struct http_transaction *, http_conn_deliver ), |
| INTF_OP ( xfer_window_changed, struct http_transaction *, http_step ), |
| INTF_OP ( pool_reopen, struct http_transaction *, http_reopen ), |
| INTF_OP ( intf_close, struct http_transaction *, http_conn_close ), |
| }; |
| |
| /** HTTP server connection interface descriptor */ |
| static struct interface_descriptor http_conn_desc = |
| INTF_DESC_PASSTHRU ( struct http_transaction, conn, |
| http_conn_operations, transfer ); |
| |
| /** HTTP process descriptor */ |
| static struct process_descriptor http_process_desc = |
| PROC_DESC_ONCE ( struct http_transaction, process, http_step ); |
| |
| /** |
| * Open HTTP transaction |
| * |
| * @v xfer Data transfer interface |
| * @v method Request method |
| * @v uri Request URI |
| * @v range Content range (if any) |
| * @v content Request content (if any) |
| * @ret rc Return status code |
| */ |
| int http_open ( struct interface *xfer, struct http_method *method, |
| struct uri *uri, struct http_request_range *range, |
| struct http_request_content *content ) { |
| struct http_transaction *http; |
| struct uri request_uri; |
| struct uri request_host; |
| size_t request_uri_len; |
| size_t request_host_len; |
| size_t content_len; |
| char *request_uri_string; |
| char *request_host_string; |
| void *content_data; |
| int rc; |
| |
| /* Calculate request URI length */ |
| memset ( &request_uri, 0, sizeof ( request_uri ) ); |
| request_uri.epath = ( uri->epath ? uri->epath : "/" ); |
| request_uri.equery = uri->equery; |
| request_uri_len = |
| ( format_uri ( &request_uri, NULL, 0 ) + 1 /* NUL */); |
| |
| /* Calculate host name length */ |
| memset ( &request_host, 0, sizeof ( request_host ) ); |
| request_host.host = uri->host; |
| request_host.port = uri->port; |
| request_host_len = |
| ( format_uri ( &request_host, NULL, 0 ) + 1 /* NUL */ ); |
| |
| /* Calculate request content length */ |
| content_len = ( content ? content->len : 0 ); |
| |
| /* Allocate and initialise structure */ |
| http = zalloc ( sizeof ( *http ) + request_uri_len + request_host_len + |
| content_len ); |
| if ( ! http ) { |
| rc = -ENOMEM; |
| goto err_alloc; |
| } |
| request_uri_string = ( ( ( void * ) http ) + sizeof ( *http ) ); |
| request_host_string = ( request_uri_string + request_uri_len ); |
| content_data = ( request_host_string + request_host_len ); |
| format_uri ( &request_uri, request_uri_string, request_uri_len ); |
| format_uri ( &request_host, request_host_string, request_host_len ); |
| ref_init ( &http->refcnt, http_free ); |
| intf_init ( &http->xfer, &http_xfer_desc, &http->refcnt ); |
| intf_init ( &http->content, &http_content_desc, &http->refcnt ); |
| intf_init ( &http->transfer, &http_transfer_desc, &http->refcnt ); |
| intf_init ( &http->conn, &http_conn_desc, &http->refcnt ); |
| intf_plug_plug ( &http->transfer, &http->content ); |
| process_init ( &http->process, &http_process_desc, &http->refcnt ); |
| timer_init ( &http->timer, http_expired, &http->refcnt ); |
| http->uri = uri_get ( uri ); |
| http->request.method = method; |
| http->request.uri = request_uri_string; |
| http->request.host = request_host_string; |
| if ( range ) { |
| memcpy ( &http->request.range, range, |
| sizeof ( http->request.range ) ); |
| } |
| if ( content ) { |
| http->request.content.type = content->type; |
| http->request.content.data = content_data; |
| http->request.content.len = content_len; |
| memcpy ( content_data, content->data, content_len ); |
| } |
| http->state = &http_request; |
| DBGC2 ( http, "HTTP %p %s://%s%s\n", http, http->uri->scheme, |
| http->request.host, http->request.uri ); |
| |
| /* Open connection */ |
| if ( ( rc = http_connect ( &http->conn, uri ) ) != 0 ) { |
| DBGC ( http, "HTTP %p could not connect: %s\n", |
| http, strerror ( rc ) ); |
| goto err_connect; |
| } |
| |
| /* Attach to parent interface, mortalise self, and return */ |
| intf_plug_plug ( &http->xfer, xfer ); |
| ref_put ( &http->refcnt ); |
| return 0; |
| |
| err_connect: |
| http_close ( http, rc ); |
| ref_put ( &http->refcnt ); |
| err_alloc: |
| return rc; |
| } |
| |
| /** |
| * Redirect HTTP transaction |
| * |
| * @v http HTTP transaction |
| * @v location New location |
| * @ret rc Return status code |
| */ |
| static int http_redirect ( struct http_transaction *http, |
| const char *location ) { |
| struct uri *location_uri; |
| struct uri *resolved_uri; |
| int rc; |
| |
| DBGC2 ( http, "HTTP %p redirecting to \"%s\"\n", http, location ); |
| |
| /* Parse location URI */ |
| location_uri = parse_uri ( location ); |
| if ( ! location_uri ) { |
| rc = -ENOMEM; |
| goto err_parse_uri; |
| } |
| |
| /* Resolve as relative to original URI */ |
| resolved_uri = resolve_uri ( http->uri, location_uri ); |
| if ( ! resolved_uri ) { |
| rc = -ENOMEM; |
| goto err_resolve_uri; |
| } |
| |
| /* Redirect to new URI */ |
| if ( ( rc = xfer_redirect ( &http->xfer, LOCATION_URI, |
| resolved_uri ) ) != 0 ) { |
| DBGC ( http, "HTTP %p could not redirect: %s\n", |
| http, strerror ( rc ) ); |
| goto err_redirect; |
| } |
| |
| err_redirect: |
| uri_put ( resolved_uri ); |
| err_resolve_uri: |
| uri_put ( location_uri ); |
| err_parse_uri: |
| return rc; |
| } |
| |
| /** |
| * Handle successful transfer completion |
| * |
| * @v http HTTP transaction |
| * @ret rc Return status code |
| */ |
| static int http_transfer_complete ( struct http_transaction *http ) { |
| struct http_authentication *auth; |
| const char *location; |
| int rc; |
| |
| /* Keep connection alive if applicable */ |
| if ( http->response.flags & HTTP_RESPONSE_KEEPALIVE ) |
| pool_recycle ( &http->conn ); |
| |
| /* Restart server connection interface */ |
| intf_restart ( &http->conn, 0 ); |
| |
| /* No more data is expected */ |
| http->state = NULL; |
| |
| /* If transaction is successful, then close the |
| * transfer-decoded interface. The content encoding may |
| * choose whether or not to immediately terminate the |
| * transaction. |
| */ |
| if ( http->response.rc == 0 ) { |
| intf_shutdown ( &http->transfer, 0 ); |
| return 0; |
| } |
| |
| /* Perform redirection, if applicable */ |
| if ( ( location = http->response.location ) ) { |
| if ( ( rc = http_redirect ( http, location ) ) != 0 ) |
| return rc; |
| http_close ( http, 0 ); |
| return 0; |
| } |
| |
| /* Fail unless a retry is permitted */ |
| if ( ! ( http->response.flags & HTTP_RESPONSE_RETRY ) ) |
| return http->response.rc; |
| |
| /* Perform authentication, if applicable */ |
| if ( ( auth = http->response.auth.auth ) ) { |
| http->request.auth.auth = auth; |
| DBGC2 ( http, "HTTP %p performing %s authentication\n", |
| http, auth->name ); |
| if ( ( rc = auth->authenticate ( http ) ) != 0 ) { |
| DBGC ( http, "HTTP %p could not authenticate: %s\n", |
| http, strerror ( rc ) ); |
| return rc; |
| } |
| } |
| |
| /* Restart content decoding interfaces */ |
| intfs_restart ( http->response.rc, &http->content, &http->transfer, |
| NULL ); |
| intf_plug_plug ( &http->transfer, &http->content ); |
| http->len = 0; |
| assert ( http->remaining == 0 ); |
| |
| /* Retry immediately if applicable. We cannot rely on an |
| * immediate timer expiry, since certain Microsoft-designed |
| * HTTP extensions such as NTLM break the fundamentally |
| * stateless nature of HTTP and rely on the same connection |
| * being reused for authentication. See RFC7230 section 2.3 |
| * for further details. |
| */ |
| if ( ! http->response.retry_after ) { |
| http_reopen ( http ); |
| return 0; |
| } |
| |
| /* Start timer to initiate retry */ |
| DBGC2 ( http, "HTTP %p retrying after %d seconds\n", |
| http, http->response.retry_after ); |
| start_timer_fixed ( &http->timer, |
| ( http->response.retry_after * TICKS_PER_SEC ) ); |
| return 0; |
| } |
| |
| /****************************************************************************** |
| * |
| * Requests |
| * |
| ****************************************************************************** |
| */ |
| |
| /** |
| * Construct HTTP request headers |
| * |
| * @v http HTTP transaction |
| * @v buf Buffer |
| * @v len Length of buffer |
| * @ret len Length, or negative error |
| */ |
| static int http_format_headers ( struct http_transaction *http, char *buf, |
| size_t len ) { |
| struct parameters *params = http->uri->params; |
| struct http_request_header *header; |
| struct parameter *param; |
| size_t used; |
| size_t remaining; |
| char *line; |
| int value_len; |
| int rc; |
| |
| /* Construct request line */ |
| used = ssnprintf ( buf, len, "%s %s HTTP/1.1", |
| http->request.method->name, http->request.uri ); |
| if ( used < len ) |
| DBGC2 ( http, "HTTP %p TX %s\n", http, buf ); |
| used += ssnprintf ( ( buf + used ), ( len - used ), "\r\n" ); |
| |
| /* Construct all fixed headers */ |
| for_each_table_entry ( header, HTTP_REQUEST_HEADERS ) { |
| |
| /* Determine header value length */ |
| value_len = header->format ( http, NULL, 0 ); |
| if ( value_len < 0 ) { |
| rc = value_len; |
| return rc; |
| } |
| |
| /* Skip zero-length headers */ |
| if ( ! value_len ) |
| continue; |
| |
| /* Construct header */ |
| line = ( buf + used ); |
| used += ssnprintf ( ( buf + used ), ( len - used ), "%s: ", |
| header->name ); |
| remaining = ( ( used < len ) ? ( len - used ) : 0 ); |
| used += header->format ( http, ( buf + used ), remaining ); |
| if ( used < len ) |
| DBGC2 ( http, "HTTP %p TX %s\n", http, line ); |
| used += ssnprintf ( ( buf + used ), ( len - used ), "\r\n" ); |
| } |
| |
| /* Construct parameter headers, if any */ |
| if ( params ) { |
| |
| /* Construct all parameter headers */ |
| for_each_param ( param, params ) { |
| |
| /* Skip non-header parameters */ |
| if ( ! ( param->flags & PARAMETER_HEADER ) ) |
| continue; |
| |
| /* Add parameter */ |
| used += ssnprintf ( ( buf + used ), ( len - used ), |
| "%s: %s\r\n", param->key, |
| param->value ); |
| } |
| } |
| |
| /* Construct terminating newline */ |
| used += ssnprintf ( ( buf + used ), ( len - used ), "\r\n" ); |
| |
| return used; |
| } |
| |
| /** |
| * Construct HTTP "Host" header |
| * |
| * @v http HTTP transaction |
| * @v buf Buffer |
| * @v len Length of buffer |
| * @ret len Length of header value, or negative error |
| */ |
| static int http_format_host ( struct http_transaction *http, char *buf, |
| size_t len ) { |
| |
| /* Construct host URI */ |
| return snprintf ( buf, len, "%s", http->request.host ); |
| } |
| |
| /** HTTP "Host" header "*/ |
| struct http_request_header http_request_host __http_request_header = { |
| .name = "Host", |
| .format = http_format_host, |
| }; |
| |
| /** |
| * Construct HTTP "User-Agent" header |
| * |
| * @v http HTTP transaction |
| * @v buf Buffer |
| * @v len Length of buffer |
| * @ret len Length of header value, or negative error |
| */ |
| static int http_format_user_agent ( struct http_transaction *http __unused, |
| char *buf, size_t len ) { |
| |
| /* Construct user agent */ |
| return snprintf ( buf, len, "iPXE/%s", product_version ); |
| } |
| |
| /** HTTP "User-Agent" header */ |
| struct http_request_header http_request_user_agent __http_request_header = { |
| .name = "User-Agent", |
| .format = http_format_user_agent, |
| }; |
| |
| /** |
| * Construct HTTP "Connection" header |
| * |
| * @v http HTTP transaction |
| * @v buf Buffer |
| * @v len Length of buffer |
| * @ret len Length of header value, or negative error |
| */ |
| static int http_format_connection ( struct http_transaction *http __unused, |
| char *buf, size_t len ) { |
| |
| /* Always request keep-alive */ |
| return snprintf ( buf, len, "keep-alive" ); |
| } |
| |
| /** HTTP "Connection" header */ |
| struct http_request_header http_request_connection __http_request_header = { |
| .name = "Connection", |
| .format = http_format_connection, |
| }; |
| |
| /** |
| * Construct HTTP "Range" header |
| * |
| * @v http HTTP transaction |
| * @v buf Buffer |
| * @v len Length of buffer |
| * @ret len Length of header value, or negative error |
| */ |
| static int http_format_range ( struct http_transaction *http, |
| char *buf, size_t len ) { |
| |
| /* Construct range, if applicable */ |
| if ( http->request.range.len ) { |
| return snprintf ( buf, len, "bytes=%zd-%zd", |
| http->request.range.start, |
| ( http->request.range.start + |
| http->request.range.len - 1 ) ); |
| } else { |
| return 0; |
| } |
| } |
| |
| /** HTTP "Range" header */ |
| struct http_request_header http_request_range __http_request_header = { |
| .name = "Range", |
| .format = http_format_range, |
| }; |
| |
| /** |
| * Construct HTTP "Content-Type" header |
| * |
| * @v http HTTP transaction |
| * @v buf Buffer |
| * @v len Length of buffer |
| * @ret len Length of header value, or negative error |
| */ |
| static int http_format_content_type ( struct http_transaction *http, |
| char *buf, size_t len ) { |
| |
| /* Construct content type, if applicable */ |
| if ( http->request.content.type ) { |
| return snprintf ( buf, len, "%s", http->request.content.type ); |
| } else { |
| return 0; |
| } |
| } |
| |
| /** HTTP "Content-Type" header */ |
| struct http_request_header http_request_content_type __http_request_header = { |
| .name = "Content-Type", |
| .format = http_format_content_type, |
| }; |
| |
| /** |
| * Construct HTTP "Content-Length" header |
| * |
| * @v http HTTP transaction |
| * @v buf Buffer |
| * @v len Length of buffer |
| * @ret len Length of header value, or negative error |
| */ |
| static int http_format_content_length ( struct http_transaction *http, |
| char *buf, size_t len ) { |
| |
| /* Construct content length, if applicable */ |
| if ( http->request.content.len ) { |
| return snprintf ( buf, len, "%zd", http->request.content.len ); |
| } else { |
| return 0; |
| } |
| } |
| |
| /** HTTP "Content-Length" header */ |
| struct http_request_header http_request_content_length __http_request_header = { |
| .name = "Content-Length", |
| .format = http_format_content_length, |
| }; |
| |
| /** |
| * Construct HTTP "Accept-Encoding" header |
| * |
| * @v http HTTP transaction |
| * @v buf Buffer |
| * @v len Length of buffer |
| * @ret len Length of header value, or negative error |
| */ |
| static int http_format_accept_encoding ( struct http_transaction *http, |
| char *buf, size_t len ) { |
| struct http_content_encoding *encoding; |
| const char *sep = ""; |
| size_t used = 0; |
| |
| /* Construct list of content encodings */ |
| for_each_table_entry ( encoding, HTTP_CONTENT_ENCODINGS ) { |
| if ( encoding->supported && ( ! encoding->supported ( http ) ) ) |
| continue; |
| used += ssnprintf ( ( buf + used ), ( len - used ), |
| "%s%s", sep, encoding->name ); |
| sep = ", "; |
| } |
| |
| return used; |
| } |
| |
| /** HTTP "Accept-Encoding" header */ |
| struct http_request_header http_request_accept_encoding __http_request_header ={ |
| .name = "Accept-Encoding", |
| .format = http_format_accept_encoding, |
| }; |
| |
| /** |
| * Transmit request |
| * |
| * @v http HTTP transaction |
| * @ret rc Return status code |
| */ |
| static int http_tx_request ( struct http_transaction *http ) { |
| struct io_buffer *iobuf; |
| int len; |
| int check_len; |
| int rc; |
| |
| /* Calculate request length */ |
| len = http_format_headers ( http, NULL, 0 ); |
| if ( len < 0 ) { |
| rc = len; |
| DBGC ( http, "HTTP %p could not construct request: %s\n", |
| http, strerror ( rc ) ); |
| goto err_len; |
| } |
| |
| /* Allocate I/O buffer */ |
| iobuf = alloc_iob ( len + 1 /* NUL */ + http->request.content.len ); |
| if ( ! iobuf ) { |
| rc = -ENOMEM; |
| goto err_alloc; |
| } |
| |
| /* Construct request */ |
| check_len = http_format_headers ( http, iob_put ( iobuf, len ), |
| ( len + 1 /* NUL */ ) ); |
| assert ( check_len == len ); |
| memcpy ( iob_put ( iobuf, http->request.content.len ), |
| http->request.content.data, http->request.content.len ); |
| |
| /* Deliver request */ |
| if ( ( rc = xfer_deliver_iob ( &http->conn, |
| iob_disown ( iobuf ) ) ) != 0 ) { |
| DBGC ( http, "HTTP %p could not deliver request: %s\n", |
| http, strerror ( rc ) ); |
| goto err_deliver; |
| } |
| |
| /* Clear any previous response */ |
| empty_line_buffer ( &http->response.headers ); |
| memset ( &http->response, 0, sizeof ( http->response ) ); |
| |
| /* Move to response headers state */ |
| http->state = &http_headers; |
| |
| return 0; |
| |
| err_deliver: |
| free_iob ( iobuf ); |
| err_alloc: |
| err_len: |
| return rc; |
| } |
| |
| /** HTTP request state */ |
| static struct http_state http_request = { |
| .tx = http_tx_request, |
| .close = http_close_error, |
| }; |
| |
| /****************************************************************************** |
| * |
| * Response headers |
| * |
| ****************************************************************************** |
| */ |
| |
| /** |
| * Parse HTTP status line |
| * |
| * @v http HTTP transaction |
| * @v line Status line |
| * @ret rc Return status code |
| */ |
| static int http_parse_status ( struct http_transaction *http, char *line ) { |
| char *endp; |
| char *version; |
| char *vernum; |
| char *status; |
| int response_rc; |
| |
| DBGC2 ( http, "HTTP %p RX %s\n", http, line ); |
| |
| /* Parse HTTP version */ |
| version = http_token ( &line, NULL ); |
| if ( ( ! version ) || ( strncmp ( version, "HTTP/", 5 ) != 0 ) ) { |
| DBGC ( http, "HTTP %p malformed version \"%s\"\n", http, line ); |
| return -EINVAL_STATUS; |
| } |
| |
| /* Keepalive is enabled by default for anything newer than HTTP/1.0 */ |
| vernum = ( version + 5 /* "HTTP/" (presence already checked) */ ); |
| if ( vernum[0] == '0' ) { |
| /* HTTP/0.x : keepalive not enabled by default */ |
| } else if ( strncmp ( vernum, "1.0", 3 ) == 0 ) { |
| /* HTTP/1.0 : keepalive not enabled by default */ |
| } else { |
| /* HTTP/1.1 or newer: keepalive enabled by default */ |
| http->response.flags |= HTTP_RESPONSE_KEEPALIVE; |
| } |
| |
| /* Parse status code */ |
| status = line; |
| http->response.status = strtoul ( status, &endp, 10 ); |
| if ( *endp != ' ' ) { |
| DBGC ( http, "HTTP %p malformed status code \"%s\"\n", |
| http, status ); |
| return -EINVAL_STATUS; |
| } |
| |
| /* Convert HTTP status code to iPXE return status code */ |
| if ( status[0] == '2' ) { |
| /* 2xx Success */ |
| response_rc = 0; |
| } else if ( status[0] == '3' ) { |
| /* 3xx Redirection */ |
| response_rc = -EXDEV; |
| } else if ( http->response.status == 401 ) { |
| /* 401 Unauthorized */ |
| response_rc = -EACCES_401; |
| } else if ( http->response.status == 403 ) { |
| /* 403 Forbidden */ |
| response_rc = -EPERM_403; |
| } else if ( http->response.status == 404 ) { |
| /* 404 Not Found */ |
| response_rc = -ENOENT_404; |
| } else if ( status[0] == '4' ) { |
| /* 4xx Client Error (not already specified) */ |
| response_rc = -EIO_4XX; |
| } else if ( status[0] == '5' ) { |
| /* 5xx Server Error */ |
| response_rc = -EIO_5XX; |
| } else { |
| /* Unrecognised */ |
| response_rc = -EIO_OTHER; |
| } |
| http->response.rc = response_rc; |
| if ( response_rc ) |
| DBGC ( http, "HTTP %p status %s\n", http, status ); |
| |
| return 0; |
| } |
| |
| /** |
| * Parse HTTP header |
| * |
| * @v http HTTP transaction |
| * @v line Header line |
| * @ret rc Return status code |
| */ |
| static int http_parse_header ( struct http_transaction *http, char *line ) { |
| struct http_response_header *header; |
| char *name = line; |
| char *sep; |
| |
| DBGC2 ( http, "HTTP %p RX %s\n", http, line ); |
| |
| /* Extract header name */ |
| sep = strchr ( line, ':' ); |
| if ( ! sep ) { |
| DBGC ( http, "HTTP %p malformed header \"%s\"\n", http, line ); |
| return -EINVAL_HEADER; |
| } |
| *sep = '\0'; |
| |
| /* Extract remainder of line */ |
| line = ( sep + 1 ); |
| while ( isspace ( *line ) ) |
| line++; |
| |
| /* Process header, if recognised */ |
| for_each_table_entry ( header, HTTP_RESPONSE_HEADERS ) { |
| if ( strcasecmp ( name, header->name ) == 0 ) |
| return header->parse ( http, line ); |
| } |
| |
| /* Unrecognised headers should be ignored */ |
| return 0; |
| } |
| |
| /** |
| * Parse HTTP response headers |
| * |
| * @v http HTTP transaction |
| * @ret rc Return status code |
| */ |
| static int http_parse_headers ( struct http_transaction *http ) { |
| char *line; |
| char *next; |
| int rc; |
| |
| /* Get status line */ |
| line = http->response.headers.data; |
| assert ( line != NULL ); |
| next = ( line + strlen ( line ) + 1 /* NUL */ ); |
| |
| /* Parse status line */ |
| if ( ( rc = http_parse_status ( http, line ) ) != 0 ) |
| return rc; |
| |
| /* Process header lines */ |
| while ( 1 ) { |
| |
| /* Move to next line */ |
| line = next; |
| next = ( line + strlen ( line ) + 1 /* NUL */ ); |
| |
| /* Stop on terminating blank line */ |
| if ( ! line[0] ) |
| return 0; |
| |
| /* Process header line */ |
| if ( ( rc = http_parse_header ( http, line ) ) != 0 ) |
| return rc; |
| } |
| } |
| |
| /** |
| * Parse HTTP "Location" header |
| * |
| * @v http HTTP transaction |
| * @v line Remaining header line |
| * @ret rc Return status code |
| */ |
| static int http_parse_location ( struct http_transaction *http, char *line ) { |
| |
| /* Store location */ |
| http->response.location = line; |
| return 0; |
| } |
| |
| /** HTTP "Location" header */ |
| struct http_response_header http_response_location __http_response_header = { |
| .name = "Location", |
| .parse = http_parse_location, |
| }; |
| |
| /** |
| * Parse HTTP "Transfer-Encoding" header |
| * |
| * @v http HTTP transaction |
| * @v line Remaining header line |
| * @ret rc Return status code |
| */ |
| static int http_parse_transfer_encoding ( struct http_transaction *http, |
| char *line ) { |
| struct http_transfer_encoding *encoding; |
| |
| /* Check for known transfer encodings */ |
| for_each_table_entry ( encoding, HTTP_TRANSFER_ENCODINGS ) { |
| if ( strcasecmp ( line, encoding->name ) == 0 ) { |
| http->response.transfer.encoding = encoding; |
| return 0; |
| } |
| } |
| |
| DBGC ( http, "HTTP %p unrecognised Transfer-Encoding \"%s\"\n", |
| http, line ); |
| return -ENOTSUP_TRANSFER; |
| } |
| |
| /** HTTP "Transfer-Encoding" header */ |
| struct http_response_header |
| http_response_transfer_encoding __http_response_header = { |
| .name = "Transfer-Encoding", |
| .parse = http_parse_transfer_encoding, |
| }; |
| |
| /** |
| * Parse HTTP "Connection" header |
| * |
| * @v http HTTP transaction |
| * @v line Remaining header line |
| * @ret rc Return status code |
| */ |
| static int http_parse_connection ( struct http_transaction *http, char *line ) { |
| char *token; |
| |
| /* Check for known connection intentions */ |
| while ( ( token = http_token ( &line, NULL ) ) ) { |
| if ( strcasecmp ( token, "keep-alive" ) == 0 ) |
| http->response.flags |= HTTP_RESPONSE_KEEPALIVE; |
| if ( strcasecmp ( token, "close" ) == 0 ) |
| http->response.flags &= ~HTTP_RESPONSE_KEEPALIVE; |
| } |
| |
| return 0; |
| } |
| |
| /** HTTP "Connection" header */ |
| struct http_response_header http_response_connection __http_response_header = { |
| .name = "Connection", |
| .parse = http_parse_connection, |
| }; |
| |
| /** |
| * Parse HTTP "Content-Length" header |
| * |
| * @v http HTTP transaction |
| * @v line Remaining header line |
| * @ret rc Return status code |
| */ |
| static int http_parse_content_length ( struct http_transaction *http, |
| char *line ) { |
| char *endp; |
| |
| /* Parse length */ |
| http->response.content.len = strtoul ( line, &endp, 10 ); |
| if ( *endp != '\0' ) { |
| DBGC ( http, "HTTP %p invalid Content-Length \"%s\"\n", |
| http, line ); |
| return -EINVAL_CONTENT_LENGTH; |
| } |
| |
| /* Record that we have a content length (since it may be zero) */ |
| http->response.flags |= HTTP_RESPONSE_CONTENT_LEN; |
| |
| return 0; |
| } |
| |
| /** HTTP "Content-Length" header */ |
| struct http_response_header |
| http_response_content_length __http_response_header = { |
| .name = "Content-Length", |
| .parse = http_parse_content_length, |
| }; |
| |
| /** |
| * Parse HTTP "Content-Encoding" header |
| * |
| * @v http HTTP transaction |
| * @v line Remaining header line |
| * @ret rc Return status code |
| */ |
| static int http_parse_content_encoding ( struct http_transaction *http, |
| char *line ) { |
| struct http_content_encoding *encoding; |
| |
| /* Check for known content encodings */ |
| for_each_table_entry ( encoding, HTTP_CONTENT_ENCODINGS ) { |
| if ( encoding->supported && ( ! encoding->supported ( http ) ) ) |
| continue; |
| if ( strcasecmp ( line, encoding->name ) == 0 ) { |
| http->response.content.encoding = encoding; |
| return 0; |
| } |
| } |
| |
| /* Some servers (e.g. Apache) have a habit of specifying |
| * unwarranted content encodings. For example, if Apache |
| * detects (via /etc/httpd/conf/magic) that a file's contents |
| * are gzip-compressed, it will set "Content-Encoding: x-gzip" |
| * regardless of the client's Accept-Encoding header. The |
| * only viable way to handle such servers is to treat unknown |
| * content encodings as equivalent to "identity". |
| */ |
| DBGC ( http, "HTTP %p unrecognised Content-Encoding \"%s\"\n", |
| http, line ); |
| return 0; |
| } |
| |
| /** HTTP "Content-Encoding" header */ |
| struct http_response_header |
| http_response_content_encoding __http_response_header = { |
| .name = "Content-Encoding", |
| .parse = http_parse_content_encoding, |
| }; |
| |
| /** |
| * Parse HTTP "Retry-After" header |
| * |
| * @v http HTTP transaction |
| * @v line Remaining header line |
| * @ret rc Return status code |
| */ |
| static int http_parse_retry_after ( struct http_transaction *http, |
| char *line ) { |
| char *endp; |
| |
| /* Try to parse value as a simple number of seconds */ |
| http->response.retry_after = strtoul ( line, &endp, 10 ); |
| if ( *endp != '\0' ) { |
| /* For any value which is not a simple number of |
| * seconds (e.g. a full HTTP date), just retry after a |
| * fixed delay, since we don't have code able to parse |
| * full HTTP dates. |
| */ |
| http->response.retry_after = HTTP_RETRY_SECONDS; |
| DBGC ( http, "HTTP %p cannot understand Retry-After \"%s\"; " |
| "using %d seconds\n", http, line, HTTP_RETRY_SECONDS ); |
| } |
| |
| /* Allow HTTP request to be retried after specified delay */ |
| http->response.flags |= HTTP_RESPONSE_RETRY; |
| |
| return 0; |
| } |
| |
| /** HTTP "Retry-After" header */ |
| struct http_response_header http_response_retry_after __http_response_header = { |
| .name = "Retry-After", |
| .parse = http_parse_retry_after, |
| }; |
| |
| /** |
| * Handle received HTTP headers |
| * |
| * @v http HTTP transaction |
| * @v iobuf I/O buffer (may be claimed) |
| * @ret rc Return status code |
| */ |
| static int http_rx_headers ( struct http_transaction *http, |
| struct io_buffer **iobuf ) { |
| struct http_transfer_encoding *transfer; |
| struct http_content_encoding *content; |
| char *line; |
| int rc; |
| |
| /* Buffer header line */ |
| if ( ( rc = http_rx_linebuf ( http, *iobuf, |
| &http->response.headers ) ) != 0 ) |
| return rc; |
| |
| /* Wait until we see the empty line marking end of headers */ |
| line = buffered_line ( &http->response.headers ); |
| if ( ( line == NULL ) || ( line[0] != '\0' ) ) |
| return 0; |
| |
| /* Process headers */ |
| if ( ( rc = http_parse_headers ( http ) ) != 0 ) |
| return rc; |
| |
| /* Initialise content encoding, if applicable */ |
| if ( ( content = http->response.content.encoding ) && |
| ( ( rc = content->init ( http ) ) != 0 ) ) { |
| DBGC ( http, "HTTP %p could not initialise %s content " |
| "encoding: %s\n", http, content->name, strerror ( rc ) ); |
| return rc; |
| } |
| |
| /* Presize receive buffer, if we have a content length */ |
| if ( http->response.content.len ) { |
| xfer_seek ( &http->transfer, http->response.content.len ); |
| xfer_seek ( &http->transfer, 0 ); |
| } |
| |
| /* Complete transfer if this is a HEAD request */ |
| if ( http->request.method == &http_head ) { |
| if ( ( rc = http_transfer_complete ( http ) ) != 0 ) |
| return rc; |
| return 0; |
| } |
| |
| /* Default to identity transfer encoding, if none specified */ |
| if ( ! http->response.transfer.encoding ) |
| http->response.transfer.encoding = &http_transfer_identity; |
| |
| /* Move to transfer encoding-specific data state */ |
| transfer = http->response.transfer.encoding; |
| http->state = &transfer->state; |
| |
| /* Initialise transfer encoding */ |
| if ( ( rc = transfer->init ( http ) ) != 0 ) { |
| DBGC ( http, "HTTP %p could not initialise %s transfer " |
| "encoding: %s\n", http, transfer->name, strerror ( rc )); |
| return rc; |
| } |
| |
| return 0; |
| } |
| |
| /** HTTP response headers state */ |
| static struct http_state http_headers = { |
| .rx = http_rx_headers, |
| .close = http_close_error, |
| }; |
| |
| /****************************************************************************** |
| * |
| * Identity transfer encoding |
| * |
| ****************************************************************************** |
| */ |
| |
| /** |
| * Initialise transfer encoding |
| * |
| * @v http HTTP transaction |
| * @ret rc Return status code |
| */ |
| static int http_init_transfer_identity ( struct http_transaction *http ) { |
| int rc; |
| |
| /* Complete transfer immediately if we have a zero content length */ |
| if ( ( http->response.flags & HTTP_RESPONSE_CONTENT_LEN ) && |
| ( http->response.content.len == 0 ) && |
| ( ( rc = http_transfer_complete ( http ) ) != 0 ) ) |
| return rc; |
| |
| return 0; |
| } |
| |
| /** |
| * Handle received data |
| * |
| * @v http HTTP transaction |
| * @v iobuf I/O buffer (may be claimed) |
| * @ret rc Return status code |
| */ |
| static int http_rx_transfer_identity ( struct http_transaction *http, |
| struct io_buffer **iobuf ) { |
| size_t len = iob_len ( *iobuf ); |
| int rc; |
| |
| /* Update lengths */ |
| http->len += len; |
| |
| /* Fail if this transfer would overrun the expected content |
| * length (if any). |
| */ |
| if ( ( http->response.flags & HTTP_RESPONSE_CONTENT_LEN ) && |
| ( http->len > http->response.content.len ) ) { |
| DBGC ( http, "HTTP %p content length overrun\n", http ); |
| return -EIO_CONTENT_LENGTH; |
| } |
| |
| /* Hand off to content encoding */ |
| if ( ( rc = xfer_deliver_iob ( &http->transfer, |
| iob_disown ( *iobuf ) ) ) != 0 ) |
| return rc; |
| |
| /* Complete transfer if we have received the expected content |
| * length (if any). |
| */ |
| if ( ( http->response.flags & HTTP_RESPONSE_CONTENT_LEN ) && |
| ( http->len == http->response.content.len ) && |
| ( ( rc = http_transfer_complete ( http ) ) != 0 ) ) |
| return rc; |
| |
| return 0; |
| } |
| |
| /** |
| * Handle server connection close |
| * |
| * @v http HTTP transaction |
| * @v rc Reason for close |
| */ |
| static void http_close_transfer_identity ( struct http_transaction *http, |
| int rc ) { |
| |
| /* Fail if any error occurred */ |
| if ( rc != 0 ) |
| goto err; |
| |
| /* Fail if we have a content length (since we would have |
| * already closed the connection if we had received the |
| * correct content length). |
| */ |
| if ( http->response.flags & HTTP_RESPONSE_CONTENT_LEN ) { |
| DBGC ( http, "HTTP %p content length underrun\n", http ); |
| rc = EIO_CONTENT_LENGTH; |
| goto err; |
| } |
| |
| /* Indicate that transfer is complete */ |
| if ( ( rc = http_transfer_complete ( http ) ) != 0 ) |
| goto err; |
| |
| return; |
| |
| err: |
| http_close ( http, rc ); |
| } |
| |
| /** Identity transfer encoding */ |
| static struct http_transfer_encoding http_transfer_identity = { |
| .name = "identity", |
| .init = http_init_transfer_identity, |
| .state = { |
| .rx = http_rx_transfer_identity, |
| .close = http_close_transfer_identity, |
| }, |
| }; |
| |
| /****************************************************************************** |
| * |
| * Chunked transfer encoding |
| * |
| ****************************************************************************** |
| */ |
| |
| /** |
| * Initialise transfer encoding |
| * |
| * @v http HTTP transaction |
| * @ret rc Return status code |
| */ |
| static int http_init_transfer_chunked ( struct http_transaction *http ) { |
| |
| /* Sanity checks */ |
| assert ( http->remaining == 0 ); |
| assert ( http->linebuf.len == 0 ); |
| |
| return 0; |
| } |
| |
| /** |
| * Handle received chunk length |
| * |
| * @v http HTTP transaction |
| * @v iobuf I/O buffer (may be claimed) |
| * @ret rc Return status code |
| */ |
| static int http_rx_chunk_len ( struct http_transaction *http, |
| struct io_buffer **iobuf ) { |
| char *line; |
| char *endp; |
| size_t len; |
| int rc; |
| |
| /* Receive into temporary line buffer */ |
| if ( ( rc = http_rx_linebuf ( http, *iobuf, &http->linebuf ) ) != 0 ) |
| return rc; |
| |
| /* Wait until we receive a non-empty line */ |
| line = buffered_line ( &http->linebuf ); |
| if ( ( line == NULL ) || ( line[0] == '\0' ) ) |
| return 0; |
| |
| /* Parse chunk length */ |
| http->remaining = strtoul ( line, &endp, 16 ); |
| if ( *endp != '\0' ) { |
| DBGC ( http, "HTTP %p invalid chunk length \"%s\"\n", |
| http, line ); |
| return -EINVAL_CHUNK_LENGTH; |
| } |
| |
| /* Empty line buffer */ |
| empty_line_buffer ( &http->linebuf ); |
| |
| /* Update expected length */ |
| len = ( http->len + http->remaining ); |
| xfer_seek ( &http->transfer, len ); |
| xfer_seek ( &http->transfer, http->len ); |
| |
| /* If chunk length is zero, then move to response trailers state */ |
| if ( ! http->remaining ) |
| http->state = &http_trailers; |
| |
| return 0; |
| } |
| |
| /** |
| * Handle received chunk data |
| * |
| * @v http HTTP transaction |
| * @v iobuf I/O buffer (may be claimed) |
| * @ret rc Return status code |
| */ |
| static int http_rx_chunk_data ( struct http_transaction *http, |
| struct io_buffer **iobuf ) { |
| struct io_buffer *payload; |
| uint8_t *crlf; |
| size_t len; |
| int rc; |
| |
| /* In the common case of a final chunk in a packet which also |
| * includes the terminating CRLF, strip the terminating CRLF |
| * (which we would ignore anyway) and hence avoid |
| * unnecessarily copying the data. |
| */ |
| if ( iob_len ( *iobuf ) == ( http->remaining + 2 /* CRLF */ ) ) { |
| crlf = ( (*iobuf)->data + http->remaining ); |
| if ( ( crlf[0] == '\r' ) && ( crlf[1] == '\n' ) ) |
| iob_unput ( (*iobuf), 2 /* CRLF */ ); |
| } |
| len = iob_len ( *iobuf ); |
| |
| /* Use whole/partial buffer as applicable */ |
| if ( len <= http->remaining ) { |
| |
| /* Whole buffer is to be consumed: decrease remaining |
| * length and use original I/O buffer as payload. |
| */ |
| payload = iob_disown ( *iobuf ); |
| http->len += len; |
| http->remaining -= len; |
| |
| } else { |
| |
| /* Partial buffer is to be consumed: copy data to a |
| * temporary I/O buffer. |
| */ |
| payload = alloc_iob ( http->remaining ); |
| if ( ! payload ) { |
| rc = -ENOMEM; |
| goto err; |
| } |
| memcpy ( iob_put ( payload, http->remaining ), (*iobuf)->data, |
| http->remaining ); |
| iob_pull ( *iobuf, http->remaining ); |
| http->len += http->remaining; |
| http->remaining = 0; |
| } |
| |
| /* Hand off to content encoding */ |
| if ( ( rc = xfer_deliver_iob ( &http->transfer, |
| iob_disown ( payload ) ) ) != 0 ) |
| goto err; |
| |
| return 0; |
| |
| err: |
| assert ( payload == NULL ); |
| return rc; |
| } |
| |
| /** |
| * Handle received chunked data |
| * |
| * @v http HTTP transaction |
| * @v iobuf I/O buffer (may be claimed) |
| * @ret rc Return status code |
| */ |
| static int http_rx_transfer_chunked ( struct http_transaction *http, |
| struct io_buffer **iobuf ) { |
| |
| /* Handle as chunk length or chunk data as appropriate */ |
| if ( http->remaining ) { |
| return http_rx_chunk_data ( http, iobuf ); |
| } else { |
| return http_rx_chunk_len ( http, iobuf ); |
| } |
| } |
| |
| /** Chunked transfer encoding */ |
| struct http_transfer_encoding http_transfer_chunked __http_transfer_encoding = { |
| .name = "chunked", |
| .init = http_init_transfer_chunked, |
| .state = { |
| .rx = http_rx_transfer_chunked, |
| .close = http_close_error, |
| }, |
| }; |
| |
| /****************************************************************************** |
| * |
| * Response trailers |
| * |
| ****************************************************************************** |
| */ |
| |
| /** |
| * Handle received HTTP trailer |
| * |
| * @v http HTTP transaction |
| * @v iobuf I/O buffer (may be claimed) |
| * @ret rc Return status code |
| */ |
| static int http_rx_trailers ( struct http_transaction *http, |
| struct io_buffer **iobuf ) { |
| char *line; |
| int rc; |
| |
| /* Buffer trailer line */ |
| if ( ( rc = http_rx_linebuf ( http, *iobuf, &http->linebuf ) ) != 0 ) |
| return rc; |
| |
| /* Wait until we see the empty line marking end of trailers */ |
| line = buffered_line ( &http->linebuf ); |
| if ( ( line == NULL ) || ( line[0] != '\0' ) ) |
| return 0; |
| |
| /* Empty line buffer */ |
| empty_line_buffer ( &http->linebuf ); |
| |
| /* Transfer is complete */ |
| if ( ( rc = http_transfer_complete ( http ) ) != 0 ) |
| return rc; |
| |
| return 0; |
| } |
| |
| /** HTTP response trailers state */ |
| static struct http_state http_trailers = { |
| .rx = http_rx_trailers, |
| .close = http_close_error, |
| }; |
| |
| /****************************************************************************** |
| * |
| * Simple URI openers |
| * |
| ****************************************************************************** |
| */ |
| |
| /** |
| * Construct HTTP form parameter list |
| * |
| * @v params Parameter list |
| * @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_form_params ( struct parameters *params, 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, params ) { |
| |
| /* Skip non-form parameters */ |
| if ( ! ( param->flags & PARAMETER_FORM ) ) |
| continue; |
| |
| /* Add the "&", if applicable */ |
| if ( len ) { |
| if ( remaining > 0 ) |
| *buf = '&'; |
| buf++; |
| len++; |
| remaining--; |
| } |
| |
| /* URI-encode the key */ |
| frag_len = uri_encode_string ( 0, param->key, 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_string ( 0, param->value, 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; |
| } |
| |
| /** |
| * Open HTTP transaction for simple URI |
| * |
| * @v xfer Data transfer interface |
| * @v uri Request URI |
| * @ret rc Return status code |
| */ |
| int http_open_uri ( struct interface *xfer, struct uri *uri ) { |
| struct parameters *params = uri->params; |
| struct http_request_content content; |
| struct http_method *method; |
| const char *type; |
| void *data; |
| size_t len; |
| size_t check_len; |
| int rc; |
| |
| /* Calculate length of form parameter list, if any */ |
| len = ( params ? http_form_params ( params, NULL, 0 ) : 0 ); |
| |
| /* Use POST if and only if there are form parameters */ |
| if ( len ) { |
| |
| /* Use POST */ |
| method = &http_post; |
| type = "application/x-www-form-urlencoded"; |
| |
| /* Allocate temporary form parameter list */ |
| data = zalloc ( len + 1 /* NUL */ ); |
| if ( ! data ) { |
| rc = -ENOMEM; |
| goto err_alloc; |
| } |
| |
| /* Construct temporary form parameter list */ |
| check_len = http_form_params ( params, data, |
| ( len + 1 /* NUL */ ) ); |
| assert ( check_len == len ); |
| |
| } else { |
| |
| /* Use GET */ |
| method = &http_get; |
| type = NULL; |
| data = NULL; |
| } |
| |
| /* Construct request content */ |
| content.type = type; |
| content.data = data; |
| content.len = len; |
| |
| /* Open HTTP transaction */ |
| if ( ( rc = http_open ( xfer, method, uri, NULL, &content ) ) != 0 ) |
| goto err_open; |
| |
| err_open: |
| free ( data ); |
| err_alloc: |
| return rc; |
| } |
| |
| /* Drag in HTTP extensions */ |
| REQUIRING_SYMBOL ( http_open ); |
| REQUIRE_OBJECT ( config_http ); |