]> git.ipfire.org Git - thirdparty/ipxe.git/commitdiff
[http] Support read-only HTTP block devices
authorMichael Brown <mcb30@ipxe.org>
Mon, 27 Jun 2011 17:27:28 +0000 (18:27 +0100)
committerMichael Brown <mcb30@ipxe.org>
Tue, 28 Jun 2011 13:45:14 +0000 (14:45 +0100)
Provide support for HTTP range requests, and expose this functionality
via the iPXE block device API.  This allows SAN booting from a root
path such as:

    sanboot http://boot.ipxe.org/freedos/fdfullcd.iso

Signed-off-by: Michael Brown <mcb30@ipxe.org>
src/net/tcp/http.c

index 432e5cd9b9c71dfb2db8dc1bcc235c19404d5ca6..c4c1b628e9f5e4e9dfd16a07a96e09eb3654d539 100644 (file)
@@ -44,14 +44,23 @@ FILE_LICENCE ( GPL2_OR_LATER );
 #include <ipxe/linebuf.h>
 #include <ipxe/features.h>
 #include <ipxe/base64.h>
+#include <ipxe/blockdev.h>
+#include <ipxe/acpi.h>
 #include <ipxe/http.h>
 
 FEATURE ( FEATURE_PROTOCOL, "HTTP", DHCP_EB_FEATURE_HTTP, 1 );
 
-/** HTTP transmission state */
-enum http_tx_state {
-       HTTP_TX_REQUEST = 0,
-       HTTP_TX_DONE,
+/** Block size used for HTTP block device request */
+#define HTTP_BLKSIZE 512
+
+/** HTTP flags */
+enum http_flags {
+       /** Request is waiting to be transmitted */
+       HTTP_TX_PENDING = 0x0001,
+       /** Fetch header only */
+       HTTP_HEAD_ONLY = 0x0002,
+       /** Keep connection alive */
+       HTTP_KEEPALIVE = 0x0004,
 };
 
 /** HTTP receive state */
@@ -61,6 +70,7 @@ enum http_rx_state {
        HTTP_RX_CHUNK_LEN,
        HTTP_RX_DATA,
        HTTP_RX_TRAILER,
+       HTTP_RX_IDLE,
        HTTP_RX_DEAD,
 };
 
@@ -73,31 +83,38 @@ struct http_request {
        struct refcnt refcnt;
        /** Data transfer interface */
        struct interface xfer;
+       /** Partial transfer interface */
+       struct interface partial;
 
        /** URI being fetched */
        struct uri *uri;
        /** Transport layer interface */
        struct interface socket;
 
+       /** Flags */
+       unsigned int flags;
+       /** Starting offset of partial transfer (if applicable) */
+       size_t partial_start;
+       /** Length of partial transfer (if applicable) */
+       size_t partial_len;
+
        /** TX process */
        struct process process;
-       /** TX state */
-       enum http_tx_state tx_state;
 
-       /** HTTP response code */
-       unsigned int response;
-       /** HTTP Content-Length */
-       size_t content_length;
-       /** HTTP is using Transfer-Encoding: chunked */
-       int chunked;
-       /** Current chunk length */
-       size_t chunk_len;
-       /** Received length */
-       size_t rx_len;
        /** RX state */
        enum http_rx_state rx_state;
+       /** Received length */
+       size_t rx_len;
+       /** Length remaining (or 0 if unknown) */
+       size_t remaining;
+       /** HTTP is using Transfer-Encoding: chunked */
+       int chunked;
+       /** Current chunk length remaining (if applicable) */
+       size_t chunk_remaining;
        /** Line buffer for received header lines */
        struct line_buffer linebuf;
+       /** Receive data buffer (if applicable) */
+       userptr_t rx_buffer;
 };
 
 /**
@@ -115,12 +132,12 @@ static void http_free ( struct refcnt *refcnt ) {
 };
 
 /**
- * Mark HTTP request as complete
+ * Close HTTP request
  *
  * @v http             HTTP request
  * @v rc               Return status code
  */
-static void http_done ( struct http_request *http, int rc ) {
+static void http_close ( struct http_request *http, int rc ) {
 
        /* Prevent further processing of any current packet */
        http->rx_state = HTTP_RX_DEAD;
@@ -128,11 +145,11 @@ static void http_done ( struct http_request *http, int rc ) {
        /* If we had a Content-Length, and the received content length
         * isn't correct, flag an error
         */
-       if ( http->content_length &&
-            ( http->content_length != http->rx_len ) ) {
+       if ( http->remaining != 0 ) {
                DBGC ( http, "HTTP %p incorrect length %zd, should be %zd\n",
-                      http, http->rx_len, http->content_length );
-               rc = -EIO;
+                      http, http->rx_len, ( http->rx_len + http->remaining ) );
+               if ( rc == 0 )
+                       rc = -EIO;
        }
 
        /* Remove process */
@@ -140,9 +157,40 @@ static void http_done ( struct http_request *http, int rc ) {
 
        /* Close all data transfer interfaces */
        intf_shutdown ( &http->socket, rc );
+       intf_shutdown ( &http->partial, rc );
        intf_shutdown ( &http->xfer, rc );
 }
 
+/**
+ * Mark HTTP request as completed successfully
+ *
+ * @v http             HTTP request
+ */
+static void http_done ( struct http_request *http ) {
+
+       /* If we had a Content-Length, and the received content length
+        * isn't correct, force an error
+        */
+       if ( http->remaining != 0 ) {
+               http_close ( http, -EIO );
+               return;
+       }
+
+       /* Enter idle state */
+       http->rx_state = HTTP_RX_IDLE;
+       http->rx_len = 0;
+       assert ( http->remaining == 0 );
+       assert ( http->chunked == 0 );
+       assert ( http->chunk_remaining == 0 );
+
+       /* Close partial transfer interface */
+       intf_restart ( &http->partial, 0 );
+
+       /* Close everything unless we are keeping the connection alive */
+       if ( ! ( http->flags & HTTP_KEEPALIVE ) )
+               http_close ( http, 0 );
+}
+
 /**
  * Convert HTTP response code to return status code
  *
@@ -152,6 +200,7 @@ static void http_done ( struct http_request *http, int rc ) {
 static int http_response_to_rc ( unsigned int response ) {
        switch ( response ) {
        case 200:
+       case 206:
        case 301:
        case 302:
                return 0;
@@ -175,6 +224,7 @@ static int http_response_to_rc ( unsigned int response ) {
  */
 static int http_rx_response ( struct http_request *http, char *response ) {
        char *spc;
+       unsigned int code;
        int rc;
 
        DBGC ( http, "HTTP %p response \"%s\"\n", http, response );
@@ -187,8 +237,8 @@ static int http_rx_response ( struct http_request *http, char *response ) {
        spc = strchr ( response, ' ' );
        if ( ! spc )
                return -EIO;
-       http->response = strtoul ( spc, NULL, 10 );
-       if ( ( rc = http_response_to_rc ( http->response ) ) != 0 )
+       code = strtoul ( spc, NULL, 10 );
+       if ( ( rc = http_response_to_rc ( code ) ) != 0 )
                return rc;
 
        /* Move to received headers */
@@ -227,19 +277,40 @@ static int http_rx_location ( struct http_request *http, const char *value ) {
  */
 static int http_rx_content_length ( struct http_request *http,
                                    const char *value ) {
+       struct block_device_capacity capacity;
+       size_t content_len;
        char *endp;
 
-       http->content_length = strtoul ( value, &endp, 10 );
+       /* Parse content length */
+       content_len = strtoul ( value, &endp, 10 );
        if ( *endp != '\0' ) {
                DBGC ( http, "HTTP %p invalid Content-Length \"%s\"\n",
                       http, value );
                return -EIO;
        }
 
+       /* If we already have an expected content length, and this
+        * isn't it, then complain
+        */
+       if ( http->remaining && ( http->remaining != content_len ) ) {
+               DBGC ( http, "HTTP %p incorrect Content-Length %zd (expected "
+                      "%zd)\n", http, content_len, http->remaining );
+               return -EIO;
+       }
+       if ( ! ( http->flags & HTTP_HEAD_ONLY ) )
+               http->remaining = content_len;
+
        /* Use seek() to notify recipient of filesize */
-       xfer_seek ( &http->xfer, http->content_length );
+       xfer_seek ( &http->xfer, http->remaining );
        xfer_seek ( &http->xfer, 0 );
 
+       /* Report block device capacity if applicable */
+       if ( http->flags & HTTP_HEAD_ONLY ) {
+               capacity.blocks = ( content_len / HTTP_BLKSIZE );
+               capacity.blksize = HTTP_BLKSIZE;
+               capacity.max_count = -1U;
+               block_capacity ( &http->partial, &capacity );
+       }
        return 0;
 }
 
@@ -309,14 +380,15 @@ static int http_rx_header ( struct http_request *http, char *header ) {
        /* An empty header line marks the end of this phase */
        if ( ! header[0] ) {
                empty_line_buffer ( &http->linebuf );
-               if ( http->rx_state == HTTP_RX_HEADER ) {
+               if ( ( http->rx_state == HTTP_RX_HEADER ) &&
+                    ( ! ( http->flags & HTTP_HEAD_ONLY ) ) ) {
                        DBGC ( http, "HTTP %p start of data\n", http );
                        http->rx_state = ( http->chunked ?
                                           HTTP_RX_CHUNK_LEN : HTTP_RX_DATA );
                        return 0;
                } else {
                        DBGC ( http, "HTTP %p end of trailer\n", http );
-                       http_done ( http, 0 );
+                       http_done ( http );
                        return 0;
                }
        }
@@ -358,7 +430,7 @@ static int http_rx_chunk_len ( struct http_request *http, char *length ) {
                return 0;
 
        /* Parse chunk length */
-       http->chunk_len = strtoul ( length, &endp, 16 );
+       http->chunk_remaining = strtoul ( length, &endp, 16 );
        if ( *endp != '\0' ) {
                DBGC ( http, "HTTP %p invalid chunk length \"%s\"\n",
                       http, length );
@@ -366,7 +438,7 @@ static int http_rx_chunk_len ( struct http_request *http, char *length ) {
        }
 
        /* Terminate chunked encoding if applicable */
-       if ( http->chunk_len == 0 ) {
+       if ( http->chunk_remaining == 0 ) {
                DBGC ( http, "HTTP %p end of chunks\n", http );
                http->chunked = 0;
                http->rx_state = HTTP_RX_TRAILER;
@@ -375,8 +447,8 @@ static int http_rx_chunk_len ( struct http_request *http, char *length ) {
 
        /* Use seek() to notify recipient of new filesize */
        DBGC ( http, "HTTP %p start of chunk of length %zd\n",
-              http, http->chunk_len );
-       xfer_seek ( &http->xfer, ( http->rx_len + http->chunk_len ) );
+              http, http->chunk_remaining );
+       xfer_seek ( &http->xfer, ( http->rx_len + http->chunk_remaining ) );
        xfer_seek ( &http->xfer, http->rx_len );
 
        /* Start receiving data */
@@ -422,34 +494,60 @@ static int http_socket_deliver ( struct http_request *http,
        int rc = 0;
 
        while ( iobuf && iob_len ( iobuf ) ) {
+
                switch ( http->rx_state ) {
+               case HTTP_RX_IDLE:
+                       /* Receiving any data in this state is an error */
+                       DBGC ( http, "HTTP %p received %zd bytes while %s\n",
+                              http, iob_len ( iobuf ),
+                              ( ( http->rx_state == HTTP_RX_IDLE ) ?
+                                "idle" : "dead" ) );
+                       rc = -EPROTO;
+                       goto done;
                case HTTP_RX_DEAD:
                        /* Do no further processing */
                        goto done;
                case HTTP_RX_DATA:
                        /* Pass received data to caller */
                        data_len = iob_len ( iobuf );
-                       if ( http->chunk_len && ( http->chunk_len < data_len )){
-                               data_len = http->chunk_len;
+                       if ( http->chunk_remaining &&
+                            ( http->chunk_remaining < data_len ) ) {
+                               data_len = http->chunk_remaining;
+                       }
+                       if ( http->remaining &&
+                            ( http->remaining < data_len ) ) {
+                               data_len = http->remaining;
+                       }
+                       if ( http->rx_buffer != UNULL ) {
+                               /* Copy to partial transfer buffer */
+                               copy_to_user ( http->rx_buffer, http->rx_len,
+                                              iobuf->data, data_len );
+                               iob_pull ( iobuf, data_len );
+                       } else if ( data_len < iob_len ( iobuf ) ) {
+                               /* Deliver partial buffer as raw data */
                                rc = xfer_deliver_raw ( &http->xfer,
                                                        iobuf->data, data_len );
                                iob_pull ( iobuf, data_len );
+                               if ( rc != 0 )
+                                       goto done;
                        } else {
-                               rc = xfer_deliver_iob ( &http->xfer,
-                                                       iob_disown ( iobuf ) );
+                               /* Deliver whole I/O buffer */
+                               if ( ( rc = xfer_deliver_iob ( &http->xfer,
+                                                iob_disown ( iobuf ) ) ) != 0 )
+                                       goto done;
                        }
-                       if ( rc != 0 )
-                               goto done;
-                       if ( http->chunk_len ) {
-                               http->chunk_len -= data_len;
-                               if ( http->chunk_len == 0 )
+                       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;
                        }
-                       http->rx_len += data_len;
-                       if ( http->content_length &&
-                            ( http->rx_len >= http->content_length ) ) {
-                               http_done ( http, 0 );
-                               goto done;
+                       if ( http->remaining ) {
+                               http->remaining -= data_len;
+                               if ( ( http->remaining == 0 ) &&
+                                    ( http->rx_state == HTTP_RX_DATA ) ) {
+                                       http_done ( http );
+                               }
                        }
                        break;
                case HTTP_RX_RESPONSE:
@@ -483,11 +581,25 @@ static int http_socket_deliver ( struct http_request *http,
 
  done:
        if ( rc )
-               http_done ( http, rc );
+               http_close ( http, rc );
        free_iob ( iobuf );
        return rc;
 }
 
+/**
+ * Check HTTP socket flow control window
+ *
+ * @v http             HTTP request
+ * @ret len            Length of window
+ */
+static size_t http_socket_window ( struct http_request *http __unused ) {
+
+       /* Window is always open.  This is to prevent TCP from
+        * stalling if our parent window is not currently open.
+        */
+       return ( ~( ( size_t ) 0 ) );
+}
+
 /**
  * HTTP process
  *
@@ -507,9 +619,11 @@ static void http_step ( struct http_request *http ) {
        int request_len = unparse_uri ( NULL, 0, http->uri,
                                        URI_PATH_BIT | URI_QUERY_BIT );
        char request[ request_len + 1 /* NUL */ ];
+       char range[48]; /* Enough for two 64-bit integers in decimal */
+       int partial;
 
        /* Do nothing if we have already transmitted the request */
-       if ( http->tx_state != HTTP_TX_REQUEST )
+       if ( ! ( http->flags & HTTP_TX_PENDING ) )
                return;
 
        /* Do nothing until socket is ready */
@@ -530,32 +644,151 @@ static void http_step ( struct http_request *http ) {
                base64_encode ( user_pw, user_pw_len, user_pw_base64 );
        }
 
+       /* Force a HEAD request if we have nowhere to send any received data */
+       if ( ( xfer_window ( &http->xfer ) == 0 ) &&
+            ( http->rx_buffer == UNULL ) ) {
+               http->flags |= ( HTTP_HEAD_ONLY | HTTP_KEEPALIVE );
+       }
+
+       /* Determine type of request */
+       partial = ( http->partial_len != 0 );
+       snprintf ( range, sizeof ( range ), "%d-%d", http->partial_start,
+                  ( http->partial_start + http->partial_len - 1 ) );
+
        /* Mark request as transmitted */
-       http->tx_state = HTTP_TX_DONE;
+       http->flags &= ~HTTP_TX_PENDING;
 
        /* Send GET request */
        if ( ( rc = xfer_printf ( &http->socket,
-                                 "GET %s%s HTTP/1.1\r\n"
+                                 "%s %s%s HTTP/1.1\r\n"
                                  "User-Agent: iPXE/" VERSION "\r\n"
-                                 "%s%s%s"
                                  "Host: %s\r\n"
+                                 "%s%s%s%s%s%s%s"
                                  "\r\n",
-                                 http->uri->path ? "" : "/",
-                                 request,
+                                 ( ( http->flags & HTTP_HEAD_ONLY ) ?
+                                   "HEAD" : "GET" ),
+                                 ( http->uri->path ? "" : "/" ),
+                                 request, host,
+                                 ( ( http->flags & HTTP_KEEPALIVE ) ?
+                                   "Connection: Keep-Alive\r\n" : "" ),
+                                 ( partial ? "Range: bytes=" : "" ),
+                                 ( partial ? range : "" ),
+                                 ( partial ? "\r\n" : "" ),
                                  ( user ?
                                    "Authorization: Basic " : "" ),
                                  ( user ? user_pw_base64 : "" ),
-                                 ( user ? "\r\n" : "" ),
-                                 host ) ) != 0 ) {
-               http_done ( http, rc );
+                                 ( user ? "\r\n" : "" ) ) ) != 0 ) {
+               http_close ( http, rc );
        }
 }
 
+/**
+ * Check HTTP data transfer flow control window
+ *
+ * @v http             HTTP request
+ * @ret len            Length of window
+ */
+static size_t http_xfer_window ( struct http_request *http ) {
+
+       /* New block commands may be issued only when we are idle */
+       return ( ( http->rx_state == HTTP_RX_IDLE ) ? 1 : 0 );
+}
+
+/**
+ * Initiate HTTP partial read
+ *
+ * @v http             HTTP request
+ * @v partial          Partial transfer interface
+ * @v offset           Starting offset
+ * @v buffer           Data buffer
+ * @v len              Length
+ * @ret rc             Return status code
+ */
+static int http_partial_read ( struct http_request *http,
+                              struct interface *partial,
+                              size_t offset, userptr_t buffer, size_t len ) {
+
+       /* Sanity check */
+       if ( http_xfer_window ( http ) == 0 )
+               return -EBUSY;
+
+       /* Initialise partial transfer parameters */
+       http->rx_buffer = buffer;
+       http->partial_start = offset;
+       http->partial_len = len;
+       http->remaining = len;
+
+       /* Schedule request */
+       http->rx_state = HTTP_RX_RESPONSE;
+       http->flags = ( HTTP_TX_PENDING | HTTP_KEEPALIVE );
+       if ( ! len )
+               http->flags |= HTTP_HEAD_ONLY;
+       process_add ( &http->process );
+
+       /* Attach to parent interface and return */
+       intf_plug_plug ( &http->partial, partial );
+
+       return 0;
+}
+
+/**
+ * Issue HTTP block device read
+ *
+ * @v http             HTTP request
+ * @v block            Block data interface
+ * @v lba              Starting logical block address
+ * @v count            Number of blocks to transfer
+ * @v buffer           Data buffer
+ * @v len              Length of data buffer
+ * @ret rc             Return status code
+ */
+static int http_block_read ( struct http_request *http,
+                            struct interface *block,
+                            uint64_t lba, unsigned int count,
+                            userptr_t buffer, size_t len __unused ) {
+
+       return http_partial_read ( http, block, ( lba * HTTP_BLKSIZE ),
+                                  buffer, ( count * HTTP_BLKSIZE ) );
+}
+
+/**
+ * Read HTTP block device capacity
+ *
+ * @v http             HTTP request
+ * @v block            Block data interface
+ * @ret rc             Return status code
+ */
+static int http_block_read_capacity ( struct http_request *http,
+                                     struct interface *block ) {
+
+       return http_partial_read ( http, block, 0, 0, 0 );
+}
+
+/**
+ * Describe HTTP device in an ACPI table
+ *
+ * @v http             HTTP request
+ * @v acpi             ACPI table
+ * @v len              Length of ACPI table
+ * @ret rc             Return status code
+ */
+static int http_acpi_describe ( struct http_request *http,
+                               struct acpi_description_header *acpi,
+                               size_t len ) {
+
+       DBGC ( http, "HTTP %p cannot yet describe device in an ACPI table\n",
+              http );
+       ( void ) acpi;
+       ( void ) len;
+       return 0;
+}
+
 /** HTTP socket interface operations */
 static struct interface_operation http_socket_operations[] = {
+       INTF_OP ( xfer_window, struct http_request *, http_socket_window ),
        INTF_OP ( xfer_deliver, struct http_request *, http_socket_deliver ),
        INTF_OP ( xfer_window_changed, struct http_request *, http_step ),
-       INTF_OP ( intf_close, struct http_request *, http_done ),
+       INTF_OP ( intf_close, struct http_request *, http_close ),
 };
 
 /** HTTP socket interface descriptor */
@@ -563,9 +796,23 @@ static struct interface_descriptor http_socket_desc =
        INTF_DESC_PASSTHRU ( struct http_request, socket,
                             http_socket_operations, xfer );
 
+/** HTTP partial transfer interface operations */
+static struct interface_operation http_partial_operations[] = {
+       INTF_OP ( intf_close, struct http_request *, http_close ),
+};
+
+/** HTTP partial transfer interface descriptor */
+static struct interface_descriptor http_partial_desc =
+       INTF_DESC ( struct http_request, partial, http_partial_operations );
+
 /** HTTP data transfer interface operations */
 static struct interface_operation http_xfer_operations[] = {
-       INTF_OP ( intf_close, struct http_request *, http_done ),
+       INTF_OP ( xfer_window, struct http_request *, http_xfer_window ),
+       INTF_OP ( block_read, struct http_request *, http_block_read ),
+       INTF_OP ( block_read_capacity, struct http_request *,
+                 http_block_read_capacity ),
+       INTF_OP ( intf_close, struct http_request *, http_close ),
+       INTF_OP ( acpi_describe, struct http_request *, http_acpi_describe ),
 };
 
 /** HTTP data transfer interface descriptor */
@@ -605,9 +852,11 @@ int http_open_filter ( struct interface *xfer, struct uri *uri,
                return -ENOMEM;
        ref_init ( &http->refcnt, http_free );
        intf_init ( &http->xfer, &http_xfer_desc, &http->refcnt );
+       intf_init ( &http->partial, &http_partial_desc, &http->refcnt );
                http->uri = uri_get ( uri );
        intf_init ( &http->socket, &http_socket_desc, &http->refcnt );
        process_init ( &http->process, &http_process_desc, &http->refcnt );
+       http->flags = HTTP_TX_PENDING;
 
        /* Open socket */
        memset ( &server, 0, sizeof ( server ) );
@@ -630,7 +879,7 @@ int http_open_filter ( struct interface *xfer, struct uri *uri,
  err:
        DBGC ( http, "HTTP %p could not create request: %s\n", 
               http, strerror ( rc ) );
-       http_done ( http, rc );
+       http_close ( http, rc );
        ref_put ( &http->refcnt );
        return rc;
 }