From: Michael Brown Date: Mon, 17 Aug 2015 12:16:26 +0000 (+0100) Subject: [http] Rewrite HTTP core to support content encodings X-Git-Tag: v1.20.1~737 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=518a98eb56f073c4fd1f20c730e474a6f2c8c2e9;p=thirdparty%2Fipxe.git [http] Rewrite HTTP core to support content encodings Rewrite the HTTP core to allow for the addition of arbitrary content encoding mechanisms, such as PeerDist and gzip. The core now exposes http_open() which can be used to create requests with an explicitly selected HTTP method, an optional requested content range, and an optional request body. A simple wrapper provides the preexisting behaviour of creating either a GET request or an application/x-www-form-urlencoded POST request (if the URI includes parameters). The HTTP SAN interface is now implemented using the generic block device translator. Individual blocks are requested using http_open() to create a range request. Server connections are now managed via a connection pool; this allows for multiple requests to the same server (e.g. for SAN blocks) to be completely unaware of each other. Repeated HTTPS connections to the same server can reuse a pooled connection, avoiding the per-connection overhead of establishing a TLS session (which can take several seconds if using a client certificate). Support for HTTP SAN booting and for the Basic and Digest authentication schemes is now optional and can be controlled via the SANBOOT_PROTO_HTTP, HTTP_AUTH_BASIC, and HTTP_AUTH_DIGEST build configuration options in config/general.h. Signed-off-by: Michael Brown --- diff --git a/src/config/config.c b/src/config/config.c index 120a1d4fd..1dd912c1d 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -140,6 +140,9 @@ REQUIRE_OBJECT ( slam ); #ifdef SANBOOT_PROTO_ISCSI REQUIRE_OBJECT ( iscsi ); #endif +#ifdef SANBOOT_PROTO_HTTP +REQUIRE_OBJECT ( httpblock ); +#endif /* * Drag in all requested resolvers diff --git a/src/config/config_http.c b/src/config/config_http.c new file mode 100644 index 000000000..f1f1b59ce --- /dev/null +++ b/src/config/config_http.c @@ -0,0 +1,42 @@ +/* + * 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 (at your option) 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 ); + +#include + +/** @file + * + * HTTP extensions + * + */ + +PROVIDE_REQUIRING_SYMBOL(); + +/* + * Drag in HTTP extensions + */ +#ifdef HTTP_AUTH_BASIC +REQUIRE_OBJECT ( httpbasic ); +#endif +#ifdef HTTP_AUTH_DIGEST +REQUIRE_OBJECT ( httpdigest ); +#endif diff --git a/src/config/defaults/pcbios.h b/src/config/defaults/pcbios.h index 4ad812108..3ed8343ce 100644 --- a/src/config/defaults/pcbios.h +++ b/src/config/defaults/pcbios.h @@ -35,6 +35,7 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); #define SANBOOT_PROTO_AOE /* AoE protocol */ #define SANBOOT_PROTO_IB_SRP /* Infiniband SCSI RDMA protocol */ #define SANBOOT_PROTO_FCP /* Fibre Channel protocol */ +#define SANBOOT_PROTO_HTTP /* HTTP SAN protocol */ #define USB_HCD_XHCI /* xHCI USB host controller */ #define USB_HCD_EHCI /* EHCI USB host controller */ diff --git a/src/config/general.h b/src/config/general.h index 35a9030ec..1ebea57e1 100644 --- a/src/config/general.h +++ b/src/config/general.h @@ -67,6 +67,14 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); //#undef SANBOOT_PROTO_AOE /* AoE protocol */ //#undef SANBOOT_PROTO_IB_SRP /* Infiniband SCSI RDMA protocol */ //#undef SANBOOT_PROTO_FCP /* Fibre Channel protocol */ +//#undef SANBOOT_PROTO_HTTP /* HTTP SAN protocol */ + +/* + * HTTP extensions + * + */ +#define HTTP_AUTH_BASIC /* Basic authentication */ +#define HTTP_AUTH_DIGEST /* Digest authentication */ /* * 802.11 cryptosystems and handshaking protocols diff --git a/src/include/ipxe/errfile.h b/src/include/ipxe/errfile.h index 5e46df836..979747ae6 100644 --- a/src/include/ipxe/errfile.h +++ b/src/include/ipxe/errfile.h @@ -248,6 +248,10 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); #define ERRFILE_pccrc ( ERRFILE_NET | 0x003e0000 ) #define ERRFILE_stp ( ERRFILE_NET | 0x003f0000 ) #define ERRFILE_pccrd ( ERRFILE_NET | 0x00400000 ) +#define ERRFILE_httpconn ( ERRFILE_NET | 0x00410000 ) +#define ERRFILE_httpauth ( ERRFILE_NET | 0x00420000 ) +#define ERRFILE_httpbasic ( ERRFILE_NET | 0x00430000 ) +#define ERRFILE_httpdigest ( ERRFILE_NET | 0x00440000 ) #define ERRFILE_image ( ERRFILE_IMAGE | 0x00000000 ) #define ERRFILE_elf ( ERRFILE_IMAGE | 0x00010000 ) diff --git a/src/include/ipxe/http.h b/src/include/ipxe/http.h index 421598b54..a0dff7d00 100644 --- a/src/include/ipxe/http.h +++ b/src/include/ipxe/http.h @@ -9,16 +9,494 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct http_transaction; + +/****************************************************************************** + * + * HTTP URI schemes + * + ****************************************************************************** + */ + /** HTTP default port */ #define HTTP_PORT 80 /** HTTPS default port */ #define HTTPS_PORT 443 -extern int http_open_filter ( struct interface *xfer, struct uri *uri, - unsigned int default_port, - int ( * filter ) ( struct interface *, - const char *, - struct interface ** ) ); +/** An HTTP URI scheme */ +struct http_scheme { + /** Scheme name (e.g. "http" or "https") */ + const char *name; + /** Default port */ + unsigned int port; + /** Transport-layer filter (if any) + * + * @v xfer Data transfer interface + * @v name Host name + * @v next Next interface + * @ret rc Return status code + */ + int ( * filter ) ( struct interface *xfer, const char *name, + struct interface **next ); +}; + +/** HTTP scheme table */ +#define HTTP_SCHEMES __table ( struct http_scheme, "http_schemes" ) + +/** Declare an HTTP scheme */ +#define __http_scheme __table_entry ( HTTP_SCHEMES, 01 ) + +/****************************************************************************** + * + * Connections + * + ****************************************************************************** + */ + +/** An HTTP connection + * + * This represents a potentially reusable connection to an HTTP + * server. + */ +struct http_connection { + /** Reference count */ + struct refcnt refcnt; + /** Connection URI + * + * This encapsulates the server (and protocol) used for the + * connection. This may be the origin server or a proxy + * server. + */ + struct uri *uri; + /** HTTP scheme */ + struct http_scheme *scheme; + /** Transport layer interface */ + struct interface socket; + /** Data transfer interface */ + struct interface xfer; + /** Pooled connection */ + struct pooled_connection pool; +}; + +/****************************************************************************** + * + * HTTP methods + * + ****************************************************************************** + */ + +/** An HTTP method */ +struct http_method { + /** Method name (e.g. "GET" or "POST") */ + const char *name; +}; + +extern struct http_method http_head; +extern struct http_method http_get; +extern struct http_method http_post; + +/****************************************************************************** + * + * Requests + * + ****************************************************************************** + */ + +/** HTTP Digest authentication client nonce count + * + * We choose to generate a new client nonce each time. + */ +#define HTTP_DIGEST_NC "00000001" + +/** HTTP Digest authentication client nonce length + * + * We choose to use a 32-bit hex client nonce. + */ +#define HTTP_DIGEST_CNONCE_LEN 8 + +/** HTTP Digest authentication response length + * + * The Digest authentication response is a Base16-encoded 16-byte MD5 + * checksum. + */ +#define HTTP_DIGEST_RESPONSE_LEN 32 + +/** HTTP request range descriptor */ +struct http_request_range { + /** Range start */ + size_t start; + /** Range length, or zero for no range request */ + size_t len; +}; + +/** HTTP request content descriptor */ +struct http_request_content { + /** Content type (if any) */ + const char *type; + /** Content data (if any) */ + const void *data; + /** Content length */ + size_t len; +}; + +/** HTTP request authentication descriptor */ +struct http_request_auth { + /** Authentication scheme (if any) */ + struct http_authentication *auth; + /** Username */ + const char *username; + /** Password */ + const char *password; + /** Quality of protection */ + const char *qop; + /** Algorithm */ + const char *algorithm; + /** Client nonce */ + char cnonce[ HTTP_DIGEST_CNONCE_LEN + 1 /* NUL */ ]; + /** Response */ + char response[ HTTP_DIGEST_RESPONSE_LEN + 1 /* NUL */ ]; +}; + +/** An HTTP request + * + * This represents a single request to be sent to a server, including + * the values required to construct all headers. + * + * Pointers within this structure must point to storage which is + * guaranteed to remain valid for the lifetime of the containing HTTP + * transaction. + */ +struct http_request { + /** Method */ + struct http_method *method; + /** Request URI string */ + const char *uri; + /** Server host name */ + const char *host; + /** Range descriptor */ + struct http_request_range range; + /** Content descriptor */ + struct http_request_content content; + /** Authentication descriptor */ + struct http_request_auth auth; +}; + +/** An HTTP request header */ +struct http_request_header { + /** Header name (e.g. "User-Agent") */ + const char *name; + /** Construct remaining header line + * + * @v http HTTP transaction + * @v buf Buffer + * @v len Length of buffer + * @ret len Header length if present, or negative error + */ + int ( * format ) ( struct http_transaction *http, char *buf, + size_t len ); +}; + +/** HTTP request header table */ +#define HTTP_REQUEST_HEADERS \ + __table ( struct http_request_header, "http_request_headers" ) + +/** Declare an HTTP request header */ +#define __http_request_header __table_entry ( HTTP_REQUEST_HEADERS, 01 ) + +/****************************************************************************** + * + * Responses + * + ****************************************************************************** + */ + +/** HTTP response transfer descriptor */ +struct http_response_transfer { + /** Transfer encoding */ + struct http_transfer_encoding *encoding; +}; + +/** HTTP response content descriptor */ +struct http_response_content { + /** Content length (may be zero) */ + size_t len; + /** Content encoding */ + struct http_content_encoding *encoding; +}; + +/** HTTP response authorization descriptor */ +struct http_response_auth { + /** Authentication scheme (if any) */ + struct http_authentication *auth; + /** Realm */ + const char *realm; + /** Quality of protection */ + const char *qop; + /** Algorithm */ + const char *algorithm; + /** Nonce */ + const char *nonce; + /** Opaque */ + const char *opaque; +}; + +/** An HTTP response + * + * This represents a single response received from the server, + * including all values parsed from headers. + * + * Pointers within this structure may point into the raw response + * buffer, and so should be invalidated when the response buffer is + * modified or discarded. + */ +struct http_response { + /** Raw response header lines + * + * This is the raw response data received from the server, up + * to and including the terminating empty line. String + * pointers within the response may point into this data + * buffer; NUL terminators will be added (overwriting the + * original terminating characters) as needed. + */ + struct line_buffer headers; + /** Status code + * + * This is the raw HTTP numeric status code (e.g. 404). + */ + unsigned int status; + /** Return status code + * + * This is the iPXE return status code corresponding to the + * HTTP status code (e.g. -ENOENT). + */ + int rc; + /** Redirection location */ + const char *location; + /** Transfer descriptor */ + struct http_response_transfer transfer; + /** Content descriptor */ + struct http_response_content content; + /** Authorization descriptor */ + struct http_response_auth auth; + /** Retry delay (in seconds) */ + unsigned int retry_after; + /** Flags */ + unsigned int flags; +}; + +/** HTTP response flags */ +enum http_response_flags { + /** Keep connection alive after close */ + HTTP_RESPONSE_KEEPALIVE = 0x0001, + /** Content length specified */ + HTTP_RESPONSE_CONTENT_LEN = 0x0002, + /** Transaction may be retried on failure */ + HTTP_RESPONSE_RETRY = 0x0004, +}; + +/** An HTTP response header */ +struct http_response_header { + /** Header name (e.g. "Transfer-Encoding") */ + const char *name; + /** Parse header line + * + * @v http HTTP transaction + * @v line Remaining header line + * @ret rc Return status code + */ + int ( * parse ) ( struct http_transaction *http, char *line ); +}; + +/** HTTP response header table */ +#define HTTP_RESPONSE_HEADERS \ + __table ( struct http_response_header, "http_response_headers" ) + +/** Declare an HTTP response header */ +#define __http_response_header __table_entry ( HTTP_RESPONSE_HEADERS, 01 ) + +/****************************************************************************** + * + * Transactions + * + ****************************************************************************** + */ + +/** HTTP transaction state */ +struct http_state { + /** Transmit data + * + * @v http HTTP transaction + * @ret rc Return status code + */ + int ( * tx ) ( struct http_transaction *http ); + /** Receive data + * + * @v http HTTP transaction + * @v iobuf I/O buffer (may be claimed) + * @ret rc Return status code + */ + int ( * rx ) ( struct http_transaction *http, + struct io_buffer **iobuf ); + /** Server connection closed + * + * @v http HTTP transaction + * @v rc Reason for close + */ + void ( * close ) ( struct http_transaction *http, int rc ); +}; + +/** An HTTP transaction */ +struct http_transaction { + /** Reference count */ + struct refcnt refcnt; + /** Data transfer interface */ + struct interface xfer; + /** Content-decoded interface */ + struct interface content; + /** Transfer-decoded interface */ + struct interface transfer; + /** Server connection */ + struct interface conn; + /** Transmit process */ + struct process process; + /** Reconnection timer */ + struct retry_timer timer; + + /** Request URI */ + struct uri *uri; + /** Request */ + struct http_request request; + /** Response */ + struct http_response response; + /** Temporary line buffer */ + struct line_buffer linebuf; + + /** Transaction state */ + struct http_state *state; + /** Accumulated transfer-decoded length */ + size_t len; + /** Chunk length remaining */ + size_t remaining; +}; + +/****************************************************************************** + * + * Transfer encoding + * + ****************************************************************************** + */ + +/** An HTTP transfer encoding */ +struct http_transfer_encoding { + /** Name */ + const char *name; + /** Initialise transfer encoding + * + * @v http HTTP transaction + * @ret rc Return status code + */ + int ( * init ) ( struct http_transaction *http ); + /** Receive data state */ + struct http_state state; +}; + +/** HTTP transfer encoding table */ +#define HTTP_TRANSFER_ENCODINGS \ + __table ( struct http_transfer_encoding, "http_transfer_encodings" ) + +/** Declare an HTTP transfer encoding */ +#define __http_transfer_encoding __table_entry ( HTTP_TRANSFER_ENCODINGS, 01 ) + +/****************************************************************************** + * + * Content encoding + * + ****************************************************************************** + */ + +/** An HTTP content encoding */ +struct http_content_encoding { + /** Name */ + const char *name; + /** Check if content encoding is supported for this request + * + * @v http HTTP transaction + * @ret supported Content encoding is supported for this request + */ + int ( * supported ) ( struct http_transaction *http ); + /** Initialise content encoding + * + * @v http HTTP transaction + * @ret rc Return status code + */ + int ( * init ) ( struct http_transaction *http ); +}; + +/** HTTP content encoding table */ +#define HTTP_CONTENT_ENCODINGS \ + __table ( struct http_content_encoding, "http_content_encodings" ) + +/** Declare an HTTP content encoding */ +#define __http_content_encoding __table_entry ( HTTP_CONTENT_ENCODINGS, 01 ) + +/****************************************************************************** + * + * Authentication + * + ****************************************************************************** + */ + +/** An HTTP authentication scheme */ +struct http_authentication { + /** Name (e.g. "Digest") */ + const char *name; + /** Perform authentication + * + * @v http HTTP transaction + * @ret rc Return status code + */ + int ( * authenticate ) ( struct http_transaction *http ); + /** Construct remaining "Authorization" header line + * + * @v http HTTP transaction + * @v buf Buffer + * @v len Length of buffer + * @ret len Header length if present, or negative error + */ + int ( * format ) ( struct http_transaction *http, char *buf, + size_t len ); +}; + +/** HTTP authentication scheme table */ +#define HTTP_AUTHENTICATIONS \ + __table ( struct http_authentication, "http_authentications" ) + +/** Declare an HTTP authentication scheme */ +#define __http_authentication __table_entry ( HTTP_AUTHENTICATIONS, 01 ) + +/****************************************************************************** + * + * General + * + ****************************************************************************** + */ + +extern char * http_token ( char **line, char **value ); +extern int http_connect ( struct interface *xfer, struct uri *uri ); +extern int http_open ( struct interface *xfer, struct http_method *method, + struct uri *uri, struct http_request_range *range, + struct http_request_content *content ); +extern int http_open_uri ( struct interface *xfer, struct uri *uri ); #endif /* _IPXE_HTTP_H */ diff --git a/src/net/tcp/http.c b/src/net/tcp/http.c index 783bf4be4..b000ed80f 100644 --- a/src/net/tcp/http.c +++ b/src/net/tcp/http.c @@ -30,26 +30,20 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); * */ -#include #include #include #include FEATURE ( FEATURE_PROTOCOL, "HTTP", DHCP_EB_FEATURE_HTTP, 1 ); -/** - * Initiate an HTTP connection - * - * @v xfer Data transfer interface - * @v uri Uniform Resource Identifier - * @ret rc Return status code - */ -static int http_open ( struct interface *xfer, struct uri *uri ) { - return http_open_filter ( xfer, uri, HTTP_PORT, NULL ); -} - /** HTTP URI opener */ struct uri_opener http_uri_opener __uri_opener = { .scheme = "http", - .open = http_open, + .open = http_open_uri, +}; + +/** HTTP URI scheme */ +struct http_scheme http_scheme __http_scheme = { + .name = "http", + .port = HTTP_PORT, }; diff --git a/src/net/tcp/httpauth.c b/src/net/tcp/httpauth.c new file mode 100644 index 000000000..fb6dcd035 --- /dev/null +++ b/src/net/tcp/httpauth.c @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2015 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. + * + * 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) authentication + * + */ + +#include +#include +#include +#include + +/** + * Identify authentication scheme + * + * @v http HTTP transaction + * @v name Scheme name + * @ret auth Authentication scheme, or NULL + */ +static struct http_authentication * http_authentication ( const char *name ) { + struct http_authentication *auth; + + /* Identify authentication scheme */ + for_each_table_entry ( auth, HTTP_AUTHENTICATIONS ) { + if ( strcasecmp ( name, auth->name ) == 0 ) + return auth; + } + + return NULL; +} + +/** An HTTP "WWW-Authenticate" response field */ +struct http_www_authenticate_field { + /** Name */ + const char *name; + /** Offset */ + size_t offset; +}; + +/** Define an HTTP "WWW-Authenticate" response field */ +#define HTTP_WWW_AUTHENTICATE_FIELD( _name ) { \ + .name = #_name, \ + .offset = offsetof ( struct http_transaction, \ + response.auth._name ), \ + } + +/** + * Set HTTP "WWW-Authenticate" response field value + * + * @v http HTTP transaction + * @v field Response field + * @v value Field value + */ +static inline void +http_www_auth_field ( struct http_transaction *http, + struct http_www_authenticate_field *field, char *value ) { + char **ptr; + + ptr = ( ( ( void * ) http ) + field->offset ); + *ptr = value; +} + +/** HTTP "WWW-Authenticate" fields */ +static struct http_www_authenticate_field http_www_auth_fields[] = { + HTTP_WWW_AUTHENTICATE_FIELD ( realm ), + HTTP_WWW_AUTHENTICATE_FIELD ( qop ), + HTTP_WWW_AUTHENTICATE_FIELD ( algorithm ), + HTTP_WWW_AUTHENTICATE_FIELD ( nonce ), + HTTP_WWW_AUTHENTICATE_FIELD ( opaque ), +}; + +/** + * Parse HTTP "WWW-Authenticate" header + * + * @v http HTTP transaction + * @v line Remaining header line + * @ret rc Return status code + */ +static int http_parse_www_authenticate ( struct http_transaction *http, + char *line ) { + struct http_www_authenticate_field *field; + char *name; + char *key; + char *value; + unsigned int i; + + /* Get scheme name */ + name = http_token ( &line, NULL ); + if ( ! name ) { + DBGC ( http, "HTTP %p malformed WWW-Authenticate \"%s\"\n", + http, value ); + return -EPROTO; + } + + /* Identify scheme */ + http->response.auth.auth = http_authentication ( name ); + if ( ! http->response.auth.auth ) { + DBGC ( http, "HTTP %p unrecognised authentication scheme " + "\"%s\"\n", http, name ); + return -ENOTSUP; + } + + /* Process fields */ + while ( ( key = http_token ( &line, &value ) ) ) { + for ( i = 0 ; i < ( sizeof ( http_www_auth_fields ) / + sizeof ( http_www_auth_fields[0] ) ) ; i++){ + field = &http_www_auth_fields[i]; + if ( strcasecmp ( key, field->name ) == 0 ) + http_www_auth_field ( http, field, value ); + } + } + + /* Allow HTTP request to be retried if the request had not + * already tried authentication. + */ + if ( ! http->request.auth.auth ) + http->response.flags |= HTTP_RESPONSE_RETRY; + + return 0; +} + +/** HTTP "WWW-Authenticate" header */ +struct http_response_header +http_response_www_authenticate __http_response_header = { + .name = "WWW-Authenticate", + .parse = http_parse_www_authenticate, +}; + +/** + * Construct HTTP "Authorization" 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_authorization ( struct http_transaction *http, + char *buf, size_t len ) { + struct http_authentication *auth = http->request.auth.auth; + size_t used; + int auth_len; + int rc; + + /* Do nothing unless we have an authentication scheme */ + if ( ! auth ) + return 0; + + /* Construct header */ + used = snprintf ( buf, len, "%s ", auth->name ); + auth_len = auth->format ( http, ( buf + used ), + ( ( used < len ) ? ( len - used ) : 0 ) ); + if ( auth_len < 0 ) { + rc = auth_len; + return rc; + } + used += auth_len; + + return used; +} + +/** HTTP "Authorization" header */ +struct http_request_header http_request_authorization __http_request_header = { + .name = "Authorization", + .format = http_format_authorization, +}; diff --git a/src/net/tcp/httpbasic.c b/src/net/tcp/httpbasic.c new file mode 100644 index 000000000..7ed7de9e7 --- /dev/null +++ b/src/net/tcp/httpbasic.c @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2015 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. + * + * 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) Basic authentication + * + */ + +#include +#include +#include +#include +#include + +/* Disambiguate the various error causes */ +#define EACCES_USERNAME __einfo_error ( EINFO_EACCES_USERNAME ) +#define EINFO_EACCES_USERNAME \ + __einfo_uniqify ( EINFO_EACCES, 0x01, \ + "No username available for Basic authentication" ) + +/** + * Perform HTTP Basic authentication + * + * @v http HTTP transaction + * @ret rc Return status code + */ +static int http_basic_authenticate ( struct http_transaction *http ) { + struct http_request_auth *req = &http->request.auth; + + /* Record username and password */ + if ( ! http->uri->user ) { + DBGC ( http, "HTTP %p has no username for Basic " + "authentication\n", http ); + return -EACCES_USERNAME; + } + req->username = http->uri->user; + req->password = ( http->uri->password ? http->uri->password : "" ); + + return 0; +} + +/** + * Construct HTTP "Authorization" header for Basic authentication + * + * @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_basic_auth ( struct http_transaction *http, + char *buf, size_t len ) { + struct http_request_auth *req = &http->request.auth; + size_t user_pw_len = ( strlen ( req->username ) + 1 /* ":" */ + + strlen ( req->password ) ); + char user_pw[ user_pw_len + 1 /* NUL */ ]; + + /* Sanity checks */ + assert ( req->username != NULL ); + assert ( req->password != NULL ); + + /* Construct "user:password" string */ + snprintf ( user_pw, sizeof ( user_pw ), "%s:%s", + req->username, req->password ); + + /* Construct response */ + return base64_encode ( user_pw, user_pw_len, buf, len ); +} + +/** HTTP Basic authentication scheme */ +struct http_authentication http_basic_auth __http_authentication = { + .name = "Basic", + .authenticate = http_basic_authenticate, + .format = http_format_basic_auth, +}; + +/* Drag in HTTP authentication support */ +REQUIRING_SYMBOL ( http_basic_auth ); +REQUIRE_OBJECT ( httpauth ); diff --git a/src/net/tcp/httpblock.c b/src/net/tcp/httpblock.c new file mode 100644 index 000000000..e124ad2d6 --- /dev/null +++ b/src/net/tcp/httpblock.c @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2015 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. + * + * 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) block device + * + */ + +#include +#include +#include +#include +#include +#include + +/** Block size used for HTTP block device requests */ +#define HTTP_BLKSIZE 512 + +/** + * Read from block device + * + * @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 + */ +int http_block_read ( struct http_transaction *http, struct interface *data, + uint64_t lba, unsigned int count, userptr_t buffer, + size_t len ) { + struct http_request_range range; + int rc; + + /* Sanity check */ + assert ( len == ( count * HTTP_BLKSIZE ) ); + + /* Construct request range descriptor */ + range.start = ( lba * HTTP_BLKSIZE ); + range.len = len; + + /* Start a range request to retrieve the block(s) */ + if ( ( rc = http_open ( data, &http_get, http->uri, &range, + NULL ) ) != 0 ) + goto err_open; + + /* Insert block device translator */ + if ( ( rc = block_translate ( data, buffer, len ) ) != 0 ) { + DBGC ( http, "HTTP %p could not insert block translator: %s\n", + http, strerror ( rc ) ); + goto err_translate; + } + + return 0; + + err_translate: + intf_restart ( data, rc ); + err_open: + return rc; +} + +/** + * Read block device capacity + * + * @v control Control interface + * @v data Data interface + * @ret rc Return status code + */ +int http_block_read_capacity ( struct http_transaction *http, + struct interface *data ) { + int rc; + + /* Start a HEAD request to retrieve the capacity */ + if ( ( rc = http_open ( data, &http_head, http->uri, NULL, + NULL ) ) != 0 ) + goto err_open; + + /* Insert block device translator */ + if ( ( rc = block_translate ( data, UNULL, HTTP_BLKSIZE ) ) != 0 ) { + DBGC ( http, "HTTP %p could not insert block translator: %s\n", + http, strerror ( rc ) ); + goto err_translate; + } + + return 0; + + err_translate: + intf_restart ( data, rc ); + err_open: + return rc; +} + +/** + * Describe device in ACPI table + * + * @v http HTTP transaction + * @v acpi ACPI table + * @v len Length of ACPI table + * @ret rc Return status code + */ +int http_acpi_describe ( struct http_transaction *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; +} diff --git a/src/net/tcp/httpconn.c b/src/net/tcp/httpconn.c new file mode 100644 index 000000000..7e4877b7b --- /dev/null +++ b/src/net/tcp/httpconn.c @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2015 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. + * + * 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) connection management + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** HTTP pooled connection expiry time */ +#define HTTP_CONN_EXPIRY ( 10 * TICKS_PER_SEC ) + +/** HTTP connection pool */ +static LIST_HEAD ( http_connection_pool ); + +/** + * Identify HTTP scheme + * + * @v uri URI + * @ret scheme HTTP scheme, or NULL + */ +static struct http_scheme * http_scheme ( struct uri *uri ) { + struct http_scheme *scheme; + + /* Sanity check */ + if ( ! uri->scheme ) + return NULL; + + /* Identify scheme */ + for_each_table_entry ( scheme, HTTP_SCHEMES ) { + if ( strcmp ( uri->scheme, scheme->name ) == 0 ) + return scheme; + } + + return NULL; +} + +/** + * Free HTTP connection + * + * @v refcnt Reference count + */ +static void http_conn_free ( struct refcnt *refcnt ) { + struct http_connection *conn = + container_of ( refcnt, struct http_connection, refcnt ); + + /* Free connection */ + uri_put ( conn->uri ); + free ( conn ); +} + +/** + * Close HTTP connection + * + * @v conn HTTP connection + * @v rc Reason for close + */ +static void http_conn_close ( struct http_connection *conn, int rc ) { + + /* Remove from connection pool, if applicable */ + pool_del ( &conn->pool ); + + /* Shut down interfaces */ + intf_shutdown ( &conn->socket, rc ); + intf_shutdown ( &conn->xfer, rc ); + if ( rc == 0 ) { + DBGC2 ( conn, "HTTPCONN %p closed %s://%s\n", + conn, conn->scheme->name, conn->uri->host ); + } else { + DBGC ( conn, "HTTPCONN %p closed %s://%s: %s\n", + conn, conn->scheme->name, conn->uri->host, + strerror ( rc ) ); + } +} + +/** + * Disconnect idle HTTP connection + * + * @v pool Pooled connection + */ +static void http_conn_expired ( struct pooled_connection *pool ) { + struct http_connection *conn = + container_of ( pool, struct http_connection, pool ); + + /* Close connection */ + http_conn_close ( conn, 0 /* Not an error to close idle connection */ ); +} + +/** + * Receive data from transport layer interface + * + * @v http HTTP connection + * @v iobuf I/O buffer + * @v meta Transfer metadata + * @ret rc Return status code + */ +static int http_conn_socket_deliver ( struct http_connection *conn, + struct io_buffer *iobuf, + struct xfer_metadata *meta ) { + + /* Mark connection as alive */ + pool_alive ( &conn->pool ); + + /* Pass on to data transfer interface */ + return xfer_deliver ( &conn->xfer, iobuf, meta ); +} + +/** + * Close HTTP connection transport layer interface + * + * @v http HTTP connection + * @v rc Reason for close + */ +static void http_conn_socket_close ( struct http_connection *conn, int rc ) { + + /* If we are reopenable (i.e. we are a recycled connection + * from the connection pool, and we have received no data from + * the underlying socket since we were pooled), then suggest + * that the client should reopen the connection. + */ + if ( pool_is_reopenable ( &conn->pool ) ) + pool_reopen ( &conn->xfer ); + + /* Close the connection */ + http_conn_close ( conn, rc ); +} + +/** + * Recycle this connection after closing + * + * @v http HTTP connection + */ +static void http_conn_xfer_recycle ( struct http_connection *conn ) { + + /* Mark connection as recyclable */ + pool_recyclable ( &conn->pool ); + DBGC2 ( conn, "HTTPCONN %p keepalive enabled\n", conn ); +} + +/** + * Close HTTP connection data transfer interface + * + * @v conn HTTP connection + * @v rc Reason for close + */ +static void http_conn_xfer_close ( struct http_connection *conn, int rc ) { + + /* Add to the connection pool if keepalive is enabled and no + * error occurred. + */ + if ( ( rc == 0 ) && pool_is_recyclable ( &conn->pool ) ) { + intf_restart ( &conn->xfer, rc ); + pool_add ( &conn->pool, &http_connection_pool, + HTTP_CONN_EXPIRY ); + DBGC2 ( conn, "HTTPCONN %p pooled %s://%s\n", + conn, conn->scheme->name, conn->uri->host ); + return; + } + + /* Otherwise, close the connection */ + http_conn_close ( conn, rc ); +} + +/** HTTP connection socket interface operations */ +static struct interface_operation http_conn_socket_operations[] = { + INTF_OP ( xfer_deliver, struct http_connection *, + http_conn_socket_deliver ), + INTF_OP ( intf_close, struct http_connection *, + http_conn_socket_close ), +}; + +/** HTTP connection socket interface descriptor */ +static struct interface_descriptor http_conn_socket_desc = + INTF_DESC_PASSTHRU ( struct http_connection, socket, + http_conn_socket_operations, xfer ); + +/** HTTP connection data transfer interface operations */ +static struct interface_operation http_conn_xfer_operations[] = { + INTF_OP ( pool_recycle, struct http_connection *, + http_conn_xfer_recycle ), + INTF_OP ( intf_close, struct http_connection *, + http_conn_xfer_close ), +}; + +/** HTTP connection data transfer interface descriptor */ +static struct interface_descriptor http_conn_xfer_desc = + INTF_DESC_PASSTHRU ( struct http_connection, xfer, + http_conn_xfer_operations, socket ); + +/** + * Connect to an HTTP server + * + * @v xfer Data transfer interface + * @v uri Connection URI + * @ret rc Return status code + * + * HTTP connections are pooled. The caller should be prepared to + * receive a pool_reopen() message. + */ +int http_connect ( struct interface *xfer, struct uri *uri ) { + struct http_connection *conn; + struct http_scheme *scheme; + struct sockaddr_tcpip server; + struct interface *socket; + int rc; + + /* Identify scheme */ + scheme = http_scheme ( uri ); + if ( ! scheme ) + return -ENOTSUP; + + /* Sanity check */ + if ( ! uri->host ) + return -EINVAL; + + /* Look for a reusable connection in the pool */ + list_for_each_entry ( conn, &http_connection_pool, pool.list ) { + + /* Sanity checks */ + assert ( conn->uri != NULL ); + assert ( conn->uri->host != NULL ); + + /* Reuse connection, if possible */ + if ( ( scheme == conn->scheme ) && + ( strcmp ( uri->host, conn->uri->host ) == 0 ) ) { + + /* Remove from connection pool, stop timer, + * attach to parent interface, and return. + */ + pool_del ( &conn->pool ); + intf_plug_plug ( &conn->xfer, xfer ); + DBGC2 ( conn, "HTTPCONN %p reused %s://%s\n", + conn, conn->scheme->name, conn->uri->host ); + return 0; + } + } + + /* Allocate and initialise structure */ + conn = zalloc ( sizeof ( *conn ) ); + ref_init ( &conn->refcnt, http_conn_free ); + conn->uri = uri_get ( uri ); + conn->scheme = scheme; + intf_init ( &conn->socket, &http_conn_socket_desc, &conn->refcnt ); + intf_init ( &conn->xfer, &http_conn_xfer_desc, &conn->refcnt ); + pool_init ( &conn->pool, http_conn_expired, &conn->refcnt ); + + /* Open socket */ + memset ( &server, 0, sizeof ( server ) ); + server.st_port = htons ( uri_port ( uri, scheme->port ) ); + socket = &conn->socket; + if ( scheme->filter && + ( ( rc = scheme->filter ( socket, uri->host, &socket ) ) != 0 ) ) + goto err_filter; + if ( ( rc = xfer_open_named_socket ( socket, SOCK_STREAM, + ( struct sockaddr * ) &server, + uri->host, NULL ) ) != 0 ) + goto err_open; + + /* Attach to parent interface, mortalise self, and return */ + intf_plug_plug ( &conn->xfer, xfer ); + ref_put ( &conn->refcnt ); + + DBGC2 ( conn, "HTTPCONN %p created %s://%s:%d\n", conn, + conn->scheme->name, conn->uri->host, ntohs ( server.st_port ) ); + return 0; + + err_open: + err_filter: + DBGC2 ( conn, "HTTPCONN %p could not create %s://%s: %s\n", + conn, conn->scheme->name, conn->uri->host, strerror ( rc ) ); + http_conn_close ( conn, rc ); + ref_put ( &conn->refcnt ); + return rc; +} diff --git a/src/net/tcp/httpcore.c b/src/net/tcp/httpcore.c index f14ce9a82..af3ca9780 100644 --- a/src/net/tcp/httpcore.c +++ b/src/net/tcp/httpcore.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007 Michael Brown . + * Copyright (C) 2015 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 @@ -44,35 +44,26 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); #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_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" ) @@ -82,9 +73,27 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); #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, "HTTP 404 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" ) @@ -92,9 +101,6 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); #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 @@ -104,1546 +110,1821 @@ 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, - /** Include qop parameter in Digest authentication reponse */ - HTTP_DIGEST_AUTH_QOP = 0x0080, - /** Use MD5-sess algorithm for Digest authentication */ - HTTP_DIGEST_AUTH_MD5_SESS = 0x0100, - /** Socket must be reopened */ - HTTP_REOPEN_SOCKET = 0x0200, -}; - -/** 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, -}; +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; -/** - * An HTTP request +/****************************************************************************** + * + * Methods * + ****************************************************************************** */ -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; + +/** HTTP HEAD method */ +struct http_method http_head = { + .name = "HEAD", }; -/** - * 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 ); +/** HTTP GET method */ +struct http_method http_get = { + .name = "GET", +}; - uri_put ( http->uri ); - empty_line_buffer ( &http->linebuf ); - free ( http->auth_realm ); - free ( http->auth_nonce ); - free ( http->auth_opaque ); - free ( http ); +/** HTTP POST method */ +struct http_method http_post = { + .name = "POST", }; -/** - * Close HTTP request +/****************************************************************************** + * + * Utility functions * - * @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 + * Handle received HTTP line-buffered data * - * @v http HTTP request + * @v http HTTP transaction + * @v iobuf I/O buffer + * @v linebuf Line buffer * @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; +static int http_rx_linebuf ( struct http_transaction *http, + struct io_buffer *iobuf, + struct line_buffer *linebuf ) { + int consumed; 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 ) + /* 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; } /** - * Retry HTTP request + * Get HTTP response token * - * @v timer Retry timer - * @v fail Failure indicator + * @v line Line position + * @v value Token value to fill in (if any) + * @ret token Token, or NULL */ -static void http_retry ( struct retry_timer *timer, int fail __unused ) { - struct http_request *http = - container_of ( timer, struct http_request, timer ); - int rc; +char * http_token ( char **line, char **value ) { + char *token; + char quote = '\0'; + char c; - /* 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; - } - } + /* Avoid returning uninitialised data */ + if ( value ) + *value = NULL; - /* 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 ); - } -} + /* Skip any initial whitespace */ + while ( isspace ( **line ) ) + (*line)++; -/** - * Mark HTTP request as completed successfully - * - * @v http HTTP request - */ -static void http_done ( struct http_request *http ) { + /* Check for end of line and record token position */ + if ( ! **line ) + return NULL; + token = *line; - /* 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; - } + /* Scan for end of token */ + while ( ( c = **line ) ) { - /* 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; - } + /* Terminate if we hit an unquoted whitespace */ + if ( isspace ( c ) && ! quote ) + break; - /* 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 ); + /* Terminate if we hit a closing quote */ + if ( c == quote ) + break; - /* Close partial transfer interface */ - if ( ! ( http->flags & HTTP_TRY_AGAIN ) ) - intf_restart ( &http->partial, 0 ); + /* Check for value separator */ + if ( value && ( ! *value ) && ( c == '=' ) ) { - /* Close everything unless we want to keep the connection alive */ - if ( ! ( http->flags & ( HTTP_CLIENT_KEEPALIVE | HTTP_TRY_AGAIN ) ) ) { - http_close ( http, 0 ); - return; - } + /* Terminate key portion of token */ + *((*line)++) = '\0'; - /* 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; + /* 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)++; + } } - /* Start request retry timer */ - start_timer_fixed ( &http->timer, http->retry_delay ); - http->retry_delay = 0; + /* Terminate token, if applicable */ + if ( c ) + *((*line)++) = '\0'; + + return token; } +/****************************************************************************** + * + * Transactions + * + ****************************************************************************** + */ + /** - * Convert HTTP response code to return status code + * Free HTTP transaction * - * @v response HTTP response code - * @ret rc Return status code + * @v refcnt Reference count */ -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; - } +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 ); } /** - * Handle HTTP response + * Close HTTP transaction * - * @v http HTTP request - * @v response HTTP response - * @ret rc Return status code + * @v http HTTP transaction + * @v rc Reason for close */ -static int http_rx_response ( struct http_request *http, char *response ) { - char *spc; +static void http_close ( struct http_transaction *http, int rc ) { - DBGC ( http, "HTTP %p response \"%s\"\n", http, response ); + /* Stop process */ + process_del ( &http->process ); - /* Check response starts with "HTTP/" */ - if ( strncmp ( response, "HTTP/", 5 ) != 0 ) - return -EINVAL_RESPONSE; + /* Stop timer */ + stop_timer ( &http->timer ); - /* Locate and store response code */ - spc = strchr ( response, ' ' ); - if ( ! spc ) - return -EINVAL_RESPONSE; - http->code = strtoul ( spc, NULL, 10 ); + /* Close all interfaces, allowing for the fact that the + * content-decoded and transfer-decoded interfaces may be + * connected to the same object. + */ + intf_shutdown ( &http->conn, rc ); + intf_nullify ( &http->transfer ); + intf_shutdown ( &http->content, rc ); + intf_shutdown ( &http->transfer, rc ); + intf_shutdown ( &http->xfer, rc ); +} - /* Move to receive headers */ - http->rx_state = ( ( http->flags & HTTP_HEAD_ONLY ) ? - HTTP_RX_TRAILER : HTTP_RX_HEADER ); - return 0; +/** + * 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 ) ); } /** - * Handle HTTP Location header + * Reopen stale HTTP connection * - * @v http HTTP request - * @v value HTTP header value - * @ret rc Return status code + * @v http HTTP transaction */ -static int http_rx_location ( struct http_request *http, char *value ) { +static void http_reopen ( struct http_transaction *http ) { 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", + /* 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 ) ); - return rc; + goto err_connect; } - return 0; + /* Reset state */ + http->state = &http_request; + + /* Reschedule transmission process */ + process_add ( &http->process ); + + return; + + err_connect: + http_close ( http, rc ); } /** - * Handle HTTP Content-Length header + * Handle retry timer expiry * - * @v http HTTP request - * @v value HTTP header value - * @ret rc Return status code + * @v timer Retry timer + * @v over Failure indicator */ -static int http_rx_content_length ( struct http_request *http, char *value ) { - struct block_device_capacity capacity; - size_t content_len; - char *endp; +static void http_expired ( struct retry_timer *timer, int over __unused ) { + struct http_transaction *http = + container_of ( timer, struct http_transaction, timer ); - /* 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; - } + /* Reopen connection */ + http_reopen ( http ); +} - /* 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; +/** + * HTTP transmit process + * + * @v http HTTP transaction + */ +static void http_step ( struct http_transaction *http ) { + int rc; - /* Do nothing more if we are retrying the request */ - if ( http->flags & HTTP_TRY_AGAIN ) - return 0; + /* Do nothing if we have nothing to transmit */ + if ( ! http->state->tx ) + return; - /* Use seek() to notify recipient of filesize */ - xfer_seek ( &http->xfer, http->remaining ); - xfer_seek ( &http->xfer, 0 ); + /* Do nothing until connection is ready */ + if ( ! xfer_window ( &http->conn ) ) + return; - /* 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; + /* 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 HTTP Transfer-Encoding header + * Handle received HTTP data * - * @v http HTTP request - * @v value HTTP header value + * @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_rx_transfer_encoding ( struct http_request *http, char *value ){ +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; + } - if ( strcasecmp ( value, "chunked" ) == 0 ) { - /* Mark connection as using chunked transfer encoding */ - http->chunked = 1; + /* 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 HTTP Connection header + * Handle server connection close * - * @v http HTTP request - * @v value HTTP header value - * @ret rc Return status code + * @v http HTTP transaction + * @v rc Reason for close */ -static int http_rx_connection ( struct http_request *http, char *value ) { +static void http_conn_close ( struct http_transaction *http, int rc ) { - if ( strcasecmp ( value, "keep-alive" ) == 0 ) { - /* Mark connection as being kept alive by the server */ - http->flags |= HTTP_SERVER_KEEPALIVE; - } + /* Sanity checks */ + assert ( http->state != NULL ); + assert ( http->state->close != NULL ); - return 0; + /* Restart server connection interface */ + intf_restart ( &http->conn, rc ); + + /* Hand off to state-specific method */ + http->state->close ( http, rc ); } /** - * Handle WWW-Authenticate Basic header + * Handle received content-decoded data * - * @v http HTTP request - * @v params Parameters - * @ret rc Return status code + * @v http HTTP transaction + * @v iobuf I/O buffer + * @v meta Data transfer metadata */ -static int http_rx_basic_auth ( struct http_request *http, char *params ) { - - DBGC ( http, "HTTP %p Basic authentication required (%s)\n", - http, params ); +static int http_content_deliver ( struct http_transaction *http, + struct io_buffer *iobuf, + struct xfer_metadata *meta ) { + int rc; - /* If we received a 401 Unauthorized response, then retry - * using Basic authentication + /* Ignore content if this is anything other than a successful + * transfer. */ - if ( ( http->code == 401 ) && - ( ! ( http->flags & HTTP_BASIC_AUTH ) ) && - ( http->uri->user != NULL ) ) { - http->flags |= ( HTTP_TRY_AGAIN | HTTP_BASIC_AUTH ); + 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; } /** - * Parse Digest authentication parameter + * Get underlying data transfer buffer * - * @v params Parameters - * @v name Parameter name (including trailing "=\"") - * @ret value Parameter value, or NULL + * @v http HTTP transaction + * @ret xferbuf Data transfer buffer, or NULL on error */ -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; +static struct xfer_buffer * +http_content_buffer ( struct http_transaction *http ) { - /* Extract value */ - value = ( key + strlen ( name ) ); - terminator = strchr ( value, '"' ); - if ( ! terminator ) + /* Deny access to the data transfer buffer if this is anything + * other than a successful transfer. + */ + if ( http->response.rc != 0 ) return NULL; - return strndup ( value, ( terminator - value ) ); + + /* Hand off to data transfer interface */ + return xfer_buffer ( &http->xfer ); } /** - * Handle WWW-Authenticate Digest header + * Read from block device (when HTTP block device support is not present) * - * @v http HTTP request - * @v params Parameters + * @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 */ -static int http_rx_digest_auth ( struct http_request *http, char *params ) { +__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 ) { - DBGC ( http, "HTTP %p Digest authentication required (%s)\n", - http, params ); + return -ENOTSUP; +} - /* 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; - } +/** + * 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 ) { - /* 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; - } + return -ENOTSUP; +} - /* Extract opaque */ - free ( http->auth_opaque ); - http->auth_opaque = http_digest_param ( params, "opaque=\"" ); - if ( ! http->auth_opaque ) { - /* Not an error; "opaque" is optional */ - } +/** + * Describe device in ACPI table (when HTTP block device support is not present) + * + * @v http HTTP transaction + * @v acpi ACPI table + * @v len Length of ACPI table + * @ret rc Return status code + */ +__weak int http_acpi_describe ( struct http_transaction *http __unused, + struct acpi_description_header *acpi __unused, + size_t len __unused ) { - /* Check for presence of qop */ - if ( strstr ( params, "qop=\"" ) != NULL ) - http->flags |= HTTP_DIGEST_AUTH_QOP; + return -ENOTSUP; +} - /* Check for MD5-sess. For some bizarre reason, - * RFC2617 requires this to be unquoted, which means - * that http_digest_param() cannot be used. - */ - if ( strstr ( params, "algorithm=MD5-sess" ) != NULL ) - http->flags |= HTTP_DIGEST_AUTH_MD5_SESS; +/** 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 ( acpi_describe, struct http_transaction *, + http_acpi_describe ), + INTF_OP ( xfer_window_changed, struct http_transaction *, http_step ), + INTF_OP ( intf_close, struct http_transaction *, http_close ), +}; - /* Retry using digest authentication */ - http->flags |= ( HTTP_TRY_AGAIN | HTTP_DIGEST_AUTH ); - } +/** 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 ), +}; - return 0; -} +/** HTTP content-decoded interface descriptor */ +static struct interface_descriptor http_content_desc = + INTF_DESC_PASSTHRU ( struct http_transaction, content, + http_content_operations, xfer ); -/** 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 ); +/** HTTP transfer-decoded interface operations */ +static struct interface_operation http_transfer_operations[] = { + INTF_OP ( intf_close, struct http_transaction *, http_close ), }; -/** 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 }, +/** 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 ); + /** - * Handle HTTP WWW-Authenticate header + * Open HTTP transaction * - * @v http HTTP request - * @v value HTTP header value + * @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 */ -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 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; - /* Extract scheme */ - separator = strchr ( value, ' ' ); - if ( ! separator ) { - DBGC ( http, "HTTP %p malformed WWW-Authenticate header\n", - http ); - return -EINVAL_HEADER; + /* Calculate request URI length */ + memset ( &request_uri, 0, sizeof ( request_uri ) ); + request_uri.path = ( uri->path ? uri->path : "/" ); + request_uri.query = uri->query; + 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; } - *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; - } + 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; } /** - * Handle HTTP Retry-After header + * Handle successful transfer completion * - * @v http HTTP request - * @v value HTTP header value + * @v http HTTP transaction * @ret rc Return status code */ -static int http_rx_retry_after ( struct http_request *http, char *value ) { - unsigned long seconds; - char *endp; +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 ); - DBGC ( http, "HTTP %p retry requested (%s)\n", http, value ); + /* Restart server connection interface */ + intf_restart ( &http->conn, 0 ); - /* 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. + /* 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->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 ); + if ( http->response.rc == 0 ) { + intf_shutdown ( &http->transfer, 0 ); + return 0; + } + + /* Perform redirection, if applicable */ + if ( ( location = http->response.location ) ) { + DBGC2 ( http, "HTTP %p redirecting to \"%s\"\n", + http, location ); + if ( ( rc = xfer_redirect ( &http->xfer, LOCATION_URI_STRING, + location ) ) != 0 ) { + DBGC ( http, "HTTP %p could not redirect: %s\n", + http, strerror ( rc ) ); + return rc; } - http->flags |= HTTP_TRY_AGAIN; - http->retry_delay = ( seconds * TICKS_PER_SEC ); + http_close ( http, 0 ); + return 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; + } + } -/** 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. + /* Restart content decoding interfaces (which may be attached + * to the same object). */ - int ( * rx ) ( struct http_request *http, char *value ); -}; + intf_nullify ( &http->content ); + intf_nullify ( &http->transfer ); + intf_restart ( &http->content, http->response.rc ); + intf_restart ( &http->transfer, http->response.rc ); + http->content.desc = &http_content_desc; + http->transfer.desc = &http_transfer_desc; + intf_plug_plug ( &http->transfer, &http->content ); + http->len = 0; + assert ( http->remaining == 0 ); -/** 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 } -}; + /* 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; +} -/** - * Handle HTTP header +/****************************************************************************** * - * @v http HTTP request - * @v header HTTP header - * @ret rc Return status code + * 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_rx_header ( struct http_request *http, char *header ) { - struct http_header_handler *handler; - char *separator; - char *value; +static int http_format_headers ( struct http_transaction *http, char *buf, + size_t len ) { + struct http_request_header *header; + size_t used; + size_t remaining; + char *line; + int value_len; int rc; - /* An empty header line marks the end of this phase */ - if ( ! header[0] ) { - empty_line_buffer ( &http->linebuf ); + /* 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" ); - /* Handle response code */ - if ( ! ( http->flags & HTTP_TRY_AGAIN ) ) { - if ( ( rc = http_response_to_rc ( http->code ) ) != 0 ) - return rc; - } + /* Construct all headers */ + for_each_table_entry ( header, HTTP_REQUEST_HEADERS ) { - /* 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; + /* 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" ); } - DBGC ( http, "HTTP %p header \"%s\"\n", http, header ); + /* Construct terminating newline */ + used += ssnprintf ( ( buf + used ), ( len - used ), "\r\n" ); - /* 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; + return used; } /** - * Handle HTTP chunk length + * Construct HTTP "Host" header * - * @v http HTTP request - * @v length HTTP chunk length - * @ret rc Return status code + * @v http HTTP transaction + * @v buf Buffer + * @v len Length of buffer + * @ret len Length of header value, or negative error */ -static int http_rx_chunk_len ( struct http_request *http, char *length ) { - char *endp; +static int http_format_host ( struct http_transaction *http, char *buf, + size_t len ) { - /* Skip blank lines between chunks */ - if ( length[0] == '\0' ) - return 0; + /* Construct host URI */ + return snprintf ( buf, len, "%s", http->request.host ); +} - /* 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; - } +/** 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 ) { - /* 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; + /* 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 ) { - /* 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 ); + /* Construct content type, if applicable */ + if ( http->request.content.type ) { + return snprintf ( buf, len, "%s", http->request.content.type ); + } else { + return 0; } +} - /* Start receiving data */ - http->rx_state = HTTP_RX_DATA; +/** HTTP "Content-Type" header */ +struct http_request_header http_request_content_type __http_request_header = { + .name = "Content-Type", + .format = http_format_content_type, +}; - return 0; +/** + * 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; + } } -/** 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 ); +/** HTTP "Content-Length" header */ +struct http_request_header http_request_content_length __http_request_header = { + .name = "Content-Length", + .format = http_format_content_length, }; -/** 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 }, +/** + * 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, }; /** - * Handle new data arriving via HTTP connection + * Transmit request * - * @v http HTTP request - * @v iobuf I/O buffer - * @v meta Data transfer metadata + * @v http HTTP transaction * @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; +static int http_tx_request ( struct http_transaction *http ) { + struct io_buffer *iobuf; + int len; + int check_len; + int rc; - profile_start ( &http_rx_profiler ); - while ( iobuf && iob_len ( iobuf ) ) { + /* 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; + } - 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; - } + /* Allocate I/O buffer */ + iobuf = alloc_iob ( len + 1 /* NUL */ + http->request.content.len ); + if ( ! iobuf ) { + rc = -ENOMEM; + goto err_alloc; } - done: - if ( rc ) - http_close ( http, rc ); + /* 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 ); - profile_stop ( &http_rx_profiler ); + 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 + * + ****************************************************************************** + */ + /** - * Check HTTP socket flow control window + * Parse HTTP status line * - * @v http HTTP request - * @ret len Length of window + * @v http HTTP transaction + * @v line Status line + * @ret rc Return status code */ -static size_t http_socket_window ( struct http_request *http __unused ) { +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; + } - /* Window is always open. This is to prevent TCP from - * stalling if our parent window is not currently open. - */ - return ( ~( ( size_t ) 0 ) ); + /* 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; + + return 0; } /** - * Close HTTP socket + * Parse HTTP header * - * @v http HTTP request - * @v rc Reason for close + * @v http HTTP transaction + * @v line Header line + * @ret rc Return status code */ -static void http_socket_close ( struct http_request *http, int rc ) { +static int http_parse_header ( struct http_transaction *http, char *line ) { + struct http_response_header *header; + char *name = line; + char *sep; - /* If we have an error, terminate */ - if ( rc != 0 ) { - http_close ( http, rc ); - return; + DBGC2 ( http, "HTTP %p RX %s\n", http, line ); + + /* Extract header name */ + sep = strstr ( line, ": " ); + if ( ! sep ) { + DBGC ( http, "HTTP %p malformed header \"%s\"\n", http, line ); + return -EINVAL_HEADER; + } + *sep = '\0'; + line = ( sep + 2 /* ": " */ ); + + /* Process header, if recognised */ + for_each_table_entry ( header, HTTP_RESPONSE_HEADERS ) { + if ( strcasecmp ( name, header->name ) == 0 ) + return header->parse ( http, line ); } - /* Mark HTTP request as complete */ - http_done ( http ); + /* Unrecognised headers should be ignored */ + return 0; } /** - * Generate HTTP Basic authorisation string + * Parse HTTP response headers * - * @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. + * @v http HTTP transaction + * @ret rc Return status code */ -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; +static int http_parse_headers ( struct http_transaction *http ) { + char *line; + char *next; + int rc; - /* Sanity check */ - assert ( user != NULL ); + /* Get status line */ + line = http->response.headers.data; + assert ( line != NULL ); + next = ( line + strlen ( line ) + 1 /* NUL */ ); - /* Make "user:password" string from decoded fields */ - snprintf ( user_pw, sizeof ( user_pw ), "%s:%s", user, password ); + /* Parse status line */ + if ( ( rc = http_parse_status ( http, line ) ) != 0 ) + return rc; - /* Base64-encode the "user:password" string */ - base64_encode ( user_pw, user_pw_len, user_pw_base64, - sizeof ( user_pw_base64 ) ); + /* Process header lines */ + while ( 1 ) { - /* Generate the authorisation string */ - len = asprintf ( &auth, "Authorization: Basic %s\r\n", - user_pw_base64 ); - if ( len < 0 ) - return NULL; + /* Move to next line */ + line = next; + next = ( line + strlen ( line ) + 1 /* NUL */ ); - return auth; + /* Stop on terminating blank line */ + if ( ! line[0] ) + return 0; + + /* Process header line */ + if ( ( rc = http_parse_header ( http, line ) ) != 0 ) + return rc; + } } /** - * Initialise HTTP digest + * Parse HTTP "Location" header * - * @v ctx Digest context - * @v string Initial string + * @v http HTTP transaction + * @v line Remaining header line + * @ret rc Return status code */ -static void http_digest_init ( struct md5_context *ctx ) { +static int http_parse_location ( struct http_transaction *http, char *line ) { - digest_init ( &md5_algorithm, ctx ); + /* 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, +}; + /** - * Update HTTP digest with new data + * Parse HTTP "Transfer-Encoding" header * - * @v ctx Digest context - * @v string String to append + * @v http HTTP transaction + * @v line Remaining header line + * @ret rc Return status code */ -static void http_digest_update ( struct md5_context *ctx, const char *string ) { - static const char colon = ':'; +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; + } + } - if ( ctx->len ) - digest_update ( &md5_algorithm, ctx, &colon, sizeof ( colon ) ); - digest_update ( &md5_algorithm, ctx, string, strlen ( string ) ); + 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, +}; + /** - * Finalise HTTP digest + * Parse HTTP "Connection" header * - * @v ctx Digest context - * @v out Buffer for digest output - * @v len Buffer length + * @v http HTTP transaction + * @v line Remaining header line + * @ret rc Return status code */ -static void http_digest_final ( struct md5_context *ctx, char *out, - size_t len ) { - uint8_t digest[MD5_DIGEST_SIZE]; +static int http_parse_connection ( struct http_transaction *http, char *line ) { + + /* Check for known connection intentions */ + if ( strcasecmp ( line, "keep-alive" ) == 0 ) { + http->response.flags |= HTTP_RESPONSE_KEEPALIVE; + return 0; + } + if ( strcasecmp ( line, "close" ) == 0 ) { + http->response.flags &= ~HTTP_RESPONSE_KEEPALIVE; + return 0; + } - digest_final ( &md5_algorithm, ctx, digest ); - base16_encode ( digest, sizeof ( digest ), out, len ); + DBGC ( http, "HTTP %p unrecognised Connection \"%s\"\n", http, line ); + return -ENOTSUP_CONNECTION; } +/** HTTP "Connection" header */ +struct http_response_header http_response_connection __http_response_header = { + .name = "Connection", + .parse = http_parse_connection, +}; + /** - * Generate HTTP Digest authorisation string + * Parse HTTP "Content-Length" header * - * @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. + * @v http HTTP transaction + * @v line Remaining header line + * @ret rc Return status code */ -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; - char cnonce[ 9 /* "xxxxxxxx" + NUL */ ]; - struct md5_context ctx; - char ha1[ base16_encoded_len ( MD5_DIGEST_SIZE ) + 1 /* NUL */ ]; - char ha2[ base16_encoded_len ( MD5_DIGEST_SIZE ) + 1 /* NUL */ ]; - char response[ base16_encoded_len ( MD5_DIGEST_SIZE ) + 1 /* NUL */ ]; - int qop = ( http->flags & HTTP_DIGEST_AUTH_QOP ); - int md5sess = ( qop && ( http->flags & HTTP_DIGEST_AUTH_MD5_SESS ) ); - char *auth; - int len; +static int http_parse_content_length ( struct http_transaction *http, + char *line ) { + char *endp; - /* Sanity checks */ - assert ( user != NULL ); - assert ( realm != NULL ); - assert ( nonce != NULL ); - - /* Generate a client nonce */ - snprintf ( cnonce, sizeof ( cnonce ), "%08lx", random() ); - - /* Generate HA1 */ - http_digest_init ( &ctx ); - http_digest_update ( &ctx, user ); - http_digest_update ( &ctx, realm ); - http_digest_update ( &ctx, password ); - http_digest_final ( &ctx, ha1, sizeof ( ha1 ) ); - if ( md5sess ) { - http_digest_init ( &ctx ); - http_digest_update ( &ctx, ha1 ); - http_digest_update ( &ctx, nonce ); - http_digest_update ( &ctx, cnonce ); - http_digest_final ( &ctx, ha1, sizeof ( ha1 ) ); + /* 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; } - /* Generate HA2 */ - http_digest_init ( &ctx ); - http_digest_update ( &ctx, method ); - http_digest_update ( &ctx, uri ); - http_digest_final ( &ctx, ha2, sizeof ( ha2 ) ); - - /* Generate response */ - http_digest_init ( &ctx ); - http_digest_update ( &ctx, ha1 ); - http_digest_update ( &ctx, nonce ); - if ( qop ) { - http_digest_update ( &ctx, "00000001" /* nc */ ); - http_digest_update ( &ctx, cnonce ); - http_digest_update ( &ctx, "auth" /* qop */ ); - } - http_digest_update ( &ctx, ha2 ); - http_digest_final ( &ctx, response, sizeof ( response ) ); - - /* Generate the authorisation string */ - len = asprintf ( &auth, "Authorization: Digest username=\"%s\", " - "realm=\"%s\", nonce=\"%s\", uri=\"%s\", " - "%s%s%s%s%s%s%s%sresponse=\"%s\"\r\n", - user, realm, nonce, uri, - ( qop ? "qop=\"auth\", algorithm=" : "" ), - ( qop ? ( md5sess ? "MD5-sess, " : "MD5, " ) : "" ), - ( qop ? "nc=00000001, cnonce=\"" : "" ), - ( qop ? cnonce : "" ), - ( qop ? "\", " : "" ), - ( opaque ? "opaque=\"" : "" ), - ( opaque ? opaque : "" ), - ( opaque ? "\", " : "" ), response ); - if ( len < 0 ) - return NULL; + /* Record that we have a content length (since it may be zero) */ + http->response.flags |= HTTP_RESPONSE_CONTENT_LEN; - return auth; + 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, +}; + /** - * Generate HTTP POST parameter list + * Parse HTTP "Content-Encoding" header * - * @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) + * @v http HTTP transaction + * @v line Remaining header line + * @ret rc Return status code */ -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--; +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; } + } - /* URI-encode the key */ - frag_len = uri_encode ( param->key, 0, buf, remaining ); - buf += frag_len; - len += frag_len; - remaining -= frag_len; + /* 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; +} - /* Add the "=" */ - if ( remaining > 0 ) - *buf = '='; - buf++; - len++; - remaining--; +/** HTTP "Content-Encoding" header */ +struct http_response_header +http_response_content_encoding __http_response_header = { + .name = "Content-Encoding", + .parse = http_parse_content_encoding, +}; - /* URI-encode the value */ - frag_len = uri_encode ( param->value, 0, buf, remaining ); - buf += frag_len; - len += frag_len; - remaining -= frag_len; +/** + * 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 ); } - /* Ensure string is NUL-terminated even if no parameters are present */ - if ( remaining > 0 ) - *buf = '\0'; + /* Allow HTTP request to be retried after specified delay */ + http->response.flags |= HTTP_RESPONSE_RETRY; - return len; + 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, +}; + /** - * Generate HTTP POST body + * Handle received HTTP headers * - * @v http HTTP request - * @ret post I/O buffer containing POST body, or NULL on error + * @v http HTTP transaction + * @v iobuf I/O buffer (may be claimed) + * @ret rc Return status code */ -static struct io_buffer * http_post ( struct http_request *http ) { - struct io_buffer *post; - size_t len; - size_t check_len; +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; - /* Calculate length of parameter list */ - len = http_post_params ( http, NULL, 0 ); + /* Buffer header line */ + if ( ( rc = http_rx_linebuf ( http, *iobuf, + &http->response.headers ) ) != 0 ) + return rc; - /* Allocate parameter list */ - post = alloc_iob ( len + 1 /* NUL */ ); - if ( ! post ) - return NULL; + /* 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; - /* 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 ) ); + /* Move to transfer encoding-specific data state */ + transfer = http->response.transfer.encoding; + http->state = &transfer->state; - return post; + /* 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 + * + ****************************************************************************** + */ + /** - * HTTP process + * Initialise transfer encoding * - * @v http HTTP request + * @v http HTTP transaction + * @ret rc Return status code */ -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; +static int http_init_transfer_identity ( struct http_transaction *http ) { int rc; - /* Do nothing if we have already transmitted the request */ - if ( ! ( http->flags & HTTP_TX_PENDING ) ) - return; + /* 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; - /* Do nothing until socket is ready */ - if ( ! xfer_window ( &http->socket ) ) - return; + return 0; +} - /* 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 ); - } +/** + * 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; - /* Determine method */ - method = ( ( http->flags & HTTP_HEAD_ONLY ) ? "HEAD" : - ( http->uri->params ? "POST" : "GET" ) ); + /* Update lengths */ + http->len += len; - /* 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; + /* 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; } - /* 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; - } + /* Hand off to content encoding */ + if ( ( rc = xfer_deliver_iob ( &http->transfer, + iob_disown ( *iobuf ) ) ) != 0 ) + return rc; - /* 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; - } + /* 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; - /* 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; - } + return 0; +} - /* 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; - } +/** + * 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 ) { - /* 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; - } + /* Fail if any error occurred */ + if ( rc != 0 ) + goto err; - /* Send POST content, if applicable */ - if ( post ) { - if ( ( rc = xfer_deliver_iob ( &http->socket, - iob_disown ( post ) ) ) != 0 ) - goto err_xfer_post; + /* 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; } - 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 ); + /* 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 + * + ****************************************************************************** + */ + /** - * Check HTTP data transfer flow control window + * Initialise transfer encoding * - * @v http HTTP request - * @ret len Length of window + * @v http HTTP transaction + * @ret rc Return status code */ -static size_t http_xfer_window ( struct http_request *http ) { +static int http_init_transfer_chunked ( struct http_transaction *http ) { - /* New block commands may be issued only when we are idle */ - return ( ( http->rx_state == HTTP_RX_IDLE ) ? 1 : 0 ); + /* Sanity checks */ + assert ( http->remaining == 0 ); + assert ( http->linebuf.len == 0 ); + + return 0; } /** - * Initiate HTTP partial read + * Handle received chunk length * - * @v http HTTP request - * @v partial Partial transfer interface - * @v offset Starting offset - * @v buffer Data buffer - * @v len Length + * @v http HTTP transaction + * @v iobuf I/O buffer (may be claimed) * @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 ); +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; + } - /* Attach to parent interface and return */ - intf_plug_plug ( &http->partial, partial ); + /* 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; } /** - * Issue HTTP block device read + * Handle received chunk data * - * @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 + * @v http HTTP transaction + * @v iobuf I/O buffer (may be claimed) * @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 ) { +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 http_partial_read ( http, block, ( lba * HTTP_BLKSIZE ), - buffer, ( count * HTTP_BLKSIZE ) ); + return 0; + + err: + assert ( payload == NULL ); + return rc; } /** - * Read HTTP block device capacity + * Handle received chunked data * - * @v http HTTP request - * @v block Block data interface + * @v http HTTP transaction + * @v iobuf I/O buffer (may be claimed) * @ret rc Return status code */ -static int http_block_read_capacity ( struct http_request *http, - struct interface *block ) { +static int http_rx_transfer_chunked ( struct http_transaction *http, + struct io_buffer **iobuf ) { - return http_partial_read ( http, block, 0, 0, 0 ); + /* 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 + * + ****************************************************************************** + */ + /** - * Describe HTTP device in an ACPI table + * Handle received HTTP trailer * - * @v http HTTP request - * @v acpi ACPI table - * @v len Length of ACPI table + * @v http HTTP transaction + * @v iobuf I/O buffer (may be claimed) * @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; +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 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 response trailers state */ +static struct http_state http_trailers = { + .rx = http_rx_trailers, + .close = http_close_error, }; -/** HTTP socket interface descriptor */ -static struct interface_descriptor http_socket_desc = - INTF_DESC_PASSTHRU ( struct http_request, socket, - http_socket_operations, xfer ); +/****************************************************************************** + * + * Simple URI openers + * + ****************************************************************************** + */ -/** HTTP partial transfer interface operations */ -static struct interface_operation http_partial_operations[] = { - INTF_OP ( intf_close, struct http_request *, http_close ), -}; +/** + * Construct HTTP 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_params ( struct parameters *params, char *buf, size_t len ) { + struct parameter *param; + ssize_t remaining = len; + size_t frag_len; -/** HTTP partial transfer interface descriptor */ -static struct interface_descriptor http_partial_desc = - INTF_DESC ( struct http_request, partial, http_partial_operations ); + /* Add each parameter in the form "key=value", joined with "&" */ + len = 0; + for_each_param ( param, params ) { -/** 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 ), -}; + /* Add the "&", if applicable */ + if ( len ) { + if ( remaining > 0 ) + *buf = '&'; + buf++; + len++; + remaining--; + } -/** HTTP data transfer interface descriptor */ -static struct interface_descriptor http_xfer_desc = - INTF_DESC_PASSTHRU ( struct http_request, xfer, - http_xfer_operations, socket ); + /* URI-encode the key */ + frag_len = uri_encode ( param->key, 0, buf, remaining ); + buf += frag_len; + len += frag_len; + remaining -= frag_len; -/** HTTP process descriptor */ -static struct process_descriptor http_process_desc = - PROC_DESC_ONCE ( struct http_request, process, http_step ); + /* 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; +} + +/** + * Open HTTP transaction for simple GET URI + * + * @v xfer Data transfer interface + * @v uri Request URI + * @ret rc Return status code + */ +static int http_open_get_uri ( struct interface *xfer, struct uri *uri ) { + + return http_open ( xfer, &http_get, uri, NULL, NULL ); +} /** - * Initiate an HTTP connection, with optional filter + * Open HTTP transaction for simple POST URI * * @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 + * @v uri Request URI * @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; +static int http_open_post_uri ( struct interface *xfer, struct uri *uri ) { + struct parameters *params = uri->params; + struct http_request_content content; + void *data; + size_t len; + size_t check_len; int rc; - /* Sanity checks */ - if ( ! uri->host ) - return -EINVAL; + /* Calculate length of parameter list */ + len = http_params ( params, NULL, 0 ); - /* 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; + /* Allocate temporary parameter list */ + data = zalloc ( len + 1 /* NUL */ ); + if ( ! data ) { + rc = -ENOMEM; + goto err_alloc; + } - /* Open socket */ - if ( ( rc = http_socket_open ( http ) ) != 0 ) - goto err; + /* Construct temporary parameter list */ + check_len = http_params ( params, data, ( len + 1 /* NUL */ ) ); + assert ( check_len == len ); - /* Attach to parent interface, mortalise self, and return */ - intf_plug_plug ( &http->xfer, xfer ); - ref_put ( &http->refcnt ); - return 0; + /* Construct request content */ + content.type = "application/x-www-form-urlencoded"; + content.data = data; + content.len = len; - err: - DBGC ( http, "HTTP %p could not create request: %s\n", - http, strerror ( rc ) ); - http_close ( http, rc ); - ref_put ( &http->refcnt ); + /* Open HTTP transaction */ + if ( ( rc = http_open ( xfer, &http_post, uri, NULL, &content ) ) != 0 ) + goto err_open; + + err_open: + free ( data ); + err_alloc: return rc; } + +/** + * 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 ) { + + /* Open GET/POST URI as applicable */ + if ( uri->params ) { + return http_open_post_uri ( xfer, uri ); + } else { + return http_open_get_uri ( xfer, uri ); + } +} + +/* Drag in HTTP extensions */ +REQUIRING_SYMBOL ( http_open ); +REQUIRE_OBJECT ( config_http ); diff --git a/src/net/tcp/httpdigest.c b/src/net/tcp/httpdigest.c new file mode 100644 index 000000000..626dd7e9d --- /dev/null +++ b/src/net/tcp/httpdigest.c @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2015 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. + * + * 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) Digest authentication + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Disambiguate the various error causes */ +#define EACCES_USERNAME __einfo_error ( EINFO_EACCES_USERNAME ) +#define EINFO_EACCES_USERNAME \ + __einfo_uniqify ( EINFO_EACCES, 0x01, \ + "No username available for Digest authentication" ) + +/** + * Initialise HTTP Digest + * + * @v ctx Digest context + * @v string Initial string + */ +static void http_digest_init ( struct md5_context *ctx ) { + + /* Initialise MD5 digest */ + digest_init ( &md5_algorithm, ctx ); +} + +/** + * Update HTTP Digest with new data + * + * @v ctx Digest context + * @v string String to append + */ +static void http_digest_update ( struct md5_context *ctx, const char *string ) { + static const char colon = ':'; + + /* Add (possibly colon-separated) field to MD5 digest */ + if ( ctx->len ) + digest_update ( &md5_algorithm, ctx, &colon, sizeof ( colon ) ); + digest_update ( &md5_algorithm, ctx, string, strlen ( string ) ); +} + +/** + * Finalise HTTP Digest + * + * @v ctx Digest context + * @v out Buffer for digest output + * @v len Buffer length + */ +static void http_digest_final ( struct md5_context *ctx, char *out, + size_t len ) { + uint8_t digest[MD5_DIGEST_SIZE]; + + /* Finalise and base16-encode MD5 digest */ + digest_final ( &md5_algorithm, ctx, digest ); + base16_encode ( digest, sizeof ( digest ), out, len ); +} + +/** + * Perform HTTP Digest authentication + * + * @v http HTTP transaction + * @ret rc Return status code + */ +static int http_digest_authenticate ( struct http_transaction *http ) { + struct http_request_auth *req = &http->request.auth; + struct http_response_auth *rsp = &http->response.auth; + char ha1[ base16_encoded_len ( MD5_DIGEST_SIZE ) + 1 /* NUL */ ]; + char ha2[ base16_encoded_len ( MD5_DIGEST_SIZE ) + 1 /* NUL */ ]; + static const char md5sess[] = "MD5-sess"; + static const char md5[] = "MD5"; + struct md5_context ctx; + + /* Check for required response parameters */ + if ( ! rsp->realm ) { + DBGC ( http, "HTTP %p has no realm for Digest authentication\n", + http ); + return -EINVAL; + } + if ( ! rsp->nonce ) { + DBGC ( http, "HTTP %p has no nonce for Digest authentication\n", + http ); + return -EINVAL; + } + + /* Record username and password */ + if ( ! http->uri->user ) { + DBGC ( http, "HTTP %p has no username for Digest " + "authentication\n", http ); + return -EACCES_USERNAME; + } + req->username = http->uri->user; + req->password = ( http->uri->password ? http->uri->password : "" ); + + /* Handle quality of protection */ + if ( rsp->qop ) { + + /* Use "auth" in subsequent request */ + req->qop = "auth"; + + /* Generate a client nonce */ + snprintf ( req->cnonce, sizeof ( req->cnonce ), + "%08lx", random() ); + + /* Determine algorithm */ + req->algorithm = md5; + if ( rsp->algorithm && + ( strcasecmp ( rsp->algorithm, md5sess ) == 0 ) ) { + req->algorithm = md5sess; + } + } + + /* Generate HA1 */ + http_digest_init ( &ctx ); + http_digest_update ( &ctx, req->username ); + http_digest_update ( &ctx, rsp->realm ); + http_digest_update ( &ctx, req->password ); + http_digest_final ( &ctx, ha1, sizeof ( ha1 ) ); + if ( req->algorithm == md5sess ) { + http_digest_init ( &ctx ); + http_digest_update ( &ctx, ha1 ); + http_digest_update ( &ctx, rsp->nonce ); + http_digest_update ( &ctx, req->cnonce ); + http_digest_final ( &ctx, ha1, sizeof ( ha1 ) ); + } + + /* Generate HA2 */ + http_digest_init ( &ctx ); + http_digest_update ( &ctx, http->request.method->name ); + http_digest_update ( &ctx, http->request.uri ); + http_digest_final ( &ctx, ha2, sizeof ( ha2 ) ); + + /* Generate response */ + http_digest_init ( &ctx ); + http_digest_update ( &ctx, ha1 ); + http_digest_update ( &ctx, rsp->nonce ); + if ( req->qop ) { + http_digest_update ( &ctx, HTTP_DIGEST_NC ); + http_digest_update ( &ctx, req->cnonce ); + http_digest_update ( &ctx, req->qop ); + } + http_digest_update ( &ctx, ha2 ); + http_digest_final ( &ctx, req->response, sizeof ( req->response ) ); + + return 0; +} + +/** + * Construct HTTP "Authorization" header for Digest authentication + * + * @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_digest_auth ( struct http_transaction *http, + char *buf, size_t len ) { + struct http_request_auth *req = &http->request.auth; + struct http_response_auth *rsp = &http->response.auth; + size_t used = 0; + + /* Sanity checks */ + assert ( rsp->realm != NULL ); + assert ( rsp->nonce != NULL ); + assert ( req->username != NULL ); + if ( req->qop ) { + assert ( req->algorithm != NULL ); + assert ( req->cnonce[0] != '\0' ); + } + assert ( req->response[0] != '\0' ); + + /* Construct response */ + used += ssnprintf ( ( buf + used ), ( len - used ), + "realm=\"%s\", nonce=\"%s\", uri=\"%s\", " + "username=\"%s\"", rsp->realm, rsp->nonce, + http->request.uri, req->username ); + if ( rsp->opaque ) { + used += ssnprintf ( ( buf + used ), ( len - used ), + ", opaque=\"%s\"", rsp->opaque ); + } + if ( req->qop ) { + used += ssnprintf ( ( buf + used ), ( len - used ), + ", qop=%s, algorithm=%s, cnonce=\"%s\", " + "nc=" HTTP_DIGEST_NC, req->qop, + req->algorithm, req->cnonce ); + } + used += ssnprintf ( ( buf + used ), ( len - used ), + ", response=\"%s\"", req->response ); + + return used; +} + +/** HTTP Digest authentication scheme */ +struct http_authentication http_digest_auth __http_authentication = { + .name = "Digest", + .authenticate = http_digest_authenticate, + .format = http_format_digest_auth, +}; + +/* Drag in HTTP authentication support */ +REQUIRING_SYMBOL ( http_digest_auth ); +REQUIRE_OBJECT ( httpauth ); diff --git a/src/net/tcp/https.c b/src/net/tcp/https.c index 113403947..e91000322 100644 --- a/src/net/tcp/https.c +++ b/src/net/tcp/https.c @@ -30,7 +30,6 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); * */ -#include #include #include #include @@ -38,19 +37,15 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); FEATURE ( FEATURE_PROTOCOL, "HTTPS", DHCP_EB_FEATURE_HTTPS, 1 ); -/** - * Initiate an HTTPS connection - * - * @v xfer Data transfer interface - * @v uri Uniform Resource Identifier - * @ret rc Return status code - */ -static int https_open ( struct interface *xfer, struct uri *uri ) { - return http_open_filter ( xfer, uri, HTTPS_PORT, add_tls ); -} - /** HTTPS URI opener */ struct uri_opener https_uri_opener __uri_opener = { .scheme = "https", - .open = https_open, + .open = http_open_uri, +}; + +/** HTTP URI scheme */ +struct http_scheme https_scheme __http_scheme = { + .name = "https", + .port = HTTPS_PORT, + .filter = add_tls, };