| /* |
| * Copyright (C) 2007 Michael Brown <mbrown@fensystems.co.uk>. |
| * |
| * This program is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU General Public License as |
| * published by the Free Software Foundation; either version 2 of the |
| * License, or any later version. |
| * |
| * This program is distributed in the hope that it will be useful, but |
| * WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. |
| */ |
| |
| FILE_LICENCE ( GPL2_OR_LATER ); |
| |
| /** |
| * @file |
| * |
| * Hyper Text Transfer Protocol (HTTP) |
| * |
| */ |
| |
| #include <stdint.h> |
| #include <stdlib.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <strings.h> |
| #include <byteswap.h> |
| #include <errno.h> |
| #include <assert.h> |
| #include <gpxe/uri.h> |
| #include <gpxe/refcnt.h> |
| #include <gpxe/iobuf.h> |
| #include <gpxe/xfer.h> |
| #include <gpxe/open.h> |
| #include <gpxe/socket.h> |
| #include <gpxe/tcpip.h> |
| #include <gpxe/process.h> |
| #include <gpxe/linebuf.h> |
| #include <gpxe/features.h> |
| #include <gpxe/base64.h> |
| #include <gpxe/http.h> |
| |
| FEATURE ( FEATURE_PROTOCOL, "HTTP", DHCP_EB_FEATURE_HTTP, 1 ); |
| |
| /** HTTP receive state */ |
| enum http_rx_state { |
| HTTP_RX_RESPONSE = 0, |
| HTTP_RX_HEADER, |
| HTTP_RX_DATA, |
| HTTP_RX_DEAD, |
| }; |
| |
| /** |
| * An HTTP request |
| * |
| */ |
| struct http_request { |
| /** Reference count */ |
| struct refcnt refcnt; |
| /** Data transfer interface */ |
| struct xfer_interface xfer; |
| |
| /** URI being fetched */ |
| struct uri *uri; |
| /** Transport layer interface */ |
| struct xfer_interface socket; |
| |
| /** TX process */ |
| struct process process; |
| |
| /** HTTP response code */ |
| unsigned int response; |
| /** HTTP Content-Length */ |
| size_t content_length; |
| /** Received length */ |
| size_t rx_len; |
| /** RX state */ |
| enum http_rx_state rx_state; |
| /** Line buffer for received header lines */ |
| struct line_buffer linebuf; |
| }; |
| |
| /** |
| * Free HTTP request |
| * |
| * @v refcnt Reference counter |
| */ |
| static void http_free ( struct refcnt *refcnt ) { |
| struct http_request *http = |
| container_of ( refcnt, struct http_request, refcnt ); |
| |
| uri_put ( http->uri ); |
| empty_line_buffer ( &http->linebuf ); |
| free ( http ); |
| }; |
| |
| /** |
| * Mark HTTP request as complete |
| * |
| * @v http HTTP request |
| * @v rc Return status code |
| */ |
| static void http_done ( struct http_request *http, int rc ) { |
| |
| /* Prevent further processing of any current packet */ |
| http->rx_state = HTTP_RX_DEAD; |
| |
| /* 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 ) ) { |
| DBGC ( http, "HTTP %p incorrect length %zd, should be %zd\n", |
| http, http->rx_len, http->content_length ); |
| rc = -EIO; |
| } |
| |
| /* Remove process */ |
| process_del ( &http->process ); |
| |
| /* Close all data transfer interfaces */ |
| xfer_nullify ( &http->socket ); |
| xfer_close ( &http->socket, rc ); |
| xfer_nullify ( &http->xfer ); |
| xfer_close ( &http->xfer, rc ); |
| } |
| |
| /** |
| * Convert HTTP response code to return status code |
| * |
| * @v response HTTP response code |
| * @ret rc Return status code |
| */ |
| static int http_response_to_rc ( unsigned int response ) { |
| switch ( response ) { |
| case 200: |
| case 301: |
| case 302: |
| return 0; |
| case 404: |
| return -ENOENT; |
| case 403: |
| return -EPERM; |
| case 401: |
| return -EACCES; |
| default: |
| return -EIO; |
| } |
| } |
| |
| /** |
| * Handle HTTP response |
| * |
| * @v http HTTP request |
| * @v response HTTP response |
| * @ret rc Return status code |
| */ |
| static int http_rx_response ( struct http_request *http, char *response ) { |
| char *spc; |
| int rc; |
| |
| DBGC ( http, "HTTP %p response \"%s\"\n", http, response ); |
| |
| /* Check response starts with "HTTP/" */ |
| if ( strncmp ( response, "HTTP/", 5 ) != 0 ) |
| return -EIO; |
| |
| /* Locate and check response code */ |
| spc = strchr ( response, ' ' ); |
| if ( ! spc ) |
| return -EIO; |
| http->response = strtoul ( spc, NULL, 10 ); |
| if ( ( rc = http_response_to_rc ( http->response ) ) != 0 ) |
| return rc; |
| |
| /* Move to received headers */ |
| http->rx_state = HTTP_RX_HEADER; |
| return 0; |
| } |
| |
| /** |
| * Handle HTTP Location header |
| * |
| * @v http HTTP request |
| * @v value HTTP header value |
| * @ret rc Return status code |
| */ |
| static int http_rx_location ( struct http_request *http, const char *value ) { |
| int rc; |
| |
| /* Redirect to new location */ |
| DBGC ( http, "HTTP %p redirecting to %s\n", http, value ); |
| if ( ( rc = xfer_redirect ( &http->xfer, LOCATION_URI_STRING, |
| value ) ) != 0 ) { |
| DBGC ( http, "HTTP %p could not redirect: %s\n", |
| http, strerror ( rc ) ); |
| return rc; |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Handle HTTP Content-Length header |
| * |
| * @v http HTTP request |
| * @v value HTTP header value |
| * @ret rc Return status code |
| */ |
| static int http_rx_content_length ( struct http_request *http, |
| const char *value ) { |
| char *endp; |
| |
| http->content_length = strtoul ( value, &endp, 10 ); |
| if ( *endp != '\0' ) { |
| DBGC ( http, "HTTP %p invalid Content-Length \"%s\"\n", |
| http, value ); |
| return -EIO; |
| } |
| |
| /* Use seek() to notify recipient of filesize */ |
| xfer_seek ( &http->xfer, http->content_length, SEEK_SET ); |
| xfer_seek ( &http->xfer, 0, SEEK_SET ); |
| |
| return 0; |
| } |
| |
| /** An HTTP header handler */ |
| struct http_header_handler { |
| /** Name (e.g. "Content-Length") */ |
| const char *header; |
| /** Handle received header |
| * |
| * @v http HTTP request |
| * @v value HTTP header value |
| * @ret rc Return status code |
| * |
| * If an error is returned, the download will be aborted. |
| */ |
| int ( * rx ) ( struct http_request *http, const char *value ); |
| }; |
| |
| /** List of HTTP header handlers */ |
| static struct http_header_handler http_header_handlers[] = { |
| { |
| .header = "Location", |
| .rx = http_rx_location, |
| }, |
| { |
| .header = "Content-Length", |
| .rx = http_rx_content_length, |
| }, |
| { NULL, NULL } |
| }; |
| |
| /** |
| * Handle HTTP header |
| * |
| * @v http HTTP request |
| * @v header HTTP header |
| * @ret rc Return status code |
| */ |
| static int http_rx_header ( struct http_request *http, char *header ) { |
| struct http_header_handler *handler; |
| char *separator; |
| char *value; |
| int rc; |
| |
| /* An empty header line marks the transition to the data phase */ |
| if ( ! header[0] ) { |
| DBGC ( http, "HTTP %p start of data\n", http ); |
| empty_line_buffer ( &http->linebuf ); |
| http->rx_state = HTTP_RX_DATA; |
| return 0; |
| } |
| |
| DBGC ( http, "HTTP %p header \"%s\"\n", http, header ); |
| |
| /* Split header at the ": " */ |
| separator = strstr ( header, ": " ); |
| if ( ! separator ) { |
| DBGC ( http, "HTTP %p malformed header\n", http ); |
| return -EIO; |
| } |
| *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; |
| } |
| |
| /** An HTTP line-based data handler */ |
| struct http_line_handler { |
| /** Handle line |
| * |
| * @v http HTTP request |
| * @v line Line to handle |
| * @ret rc Return status code |
| */ |
| int ( * rx ) ( struct http_request *http, char *line ); |
| }; |
| |
| /** List of HTTP line-based data handlers */ |
| static struct http_line_handler http_line_handlers[] = { |
| [HTTP_RX_RESPONSE] = { .rx = http_rx_response }, |
| [HTTP_RX_HEADER] = { .rx = http_rx_header }, |
| }; |
| |
| /** |
| * Handle new data arriving via HTTP connection in the data phase |
| * |
| * @v http HTTP request |
| * @v iobuf I/O buffer |
| * @ret rc Return status code |
| */ |
| static int http_rx_data ( struct http_request *http, |
| struct io_buffer *iobuf ) { |
| int rc; |
| |
| /* Update received length */ |
| http->rx_len += iob_len ( iobuf ); |
| |
| /* Hand off data buffer */ |
| if ( ( rc = xfer_deliver_iob ( &http->xfer, iobuf ) ) != 0 ) |
| return rc; |
| |
| /* If we have reached the content-length, stop now */ |
| if ( http->content_length && |
| ( http->rx_len >= http->content_length ) ) { |
| http_done ( http, 0 ); |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Handle new data arriving via HTTP connection |
| * |
| * @v socket Transport layer interface |
| * @v iobuf I/O buffer |
| * @v meta Data transfer metadata |
| * @ret rc Return status code |
| */ |
| static int http_socket_deliver_iob ( struct xfer_interface *socket, |
| struct io_buffer *iobuf, |
| struct xfer_metadata *meta __unused ) { |
| struct http_request *http = |
| container_of ( socket, struct http_request, socket ); |
| struct http_line_handler *lh; |
| char *line; |
| ssize_t len; |
| int rc = 0; |
| |
| while ( iob_len ( iobuf ) ) { |
| switch ( http->rx_state ) { |
| case HTTP_RX_DEAD: |
| /* Do no further processing */ |
| goto done; |
| case HTTP_RX_DATA: |
| /* Once we're into the data phase, just fill |
| * the data buffer |
| */ |
| rc = http_rx_data ( http, iob_disown ( iobuf ) ); |
| goto done; |
| case HTTP_RX_RESPONSE: |
| case HTTP_RX_HEADER: |
| /* In the other phases, buffer and process a |
| * line at a time |
| */ |
| len = line_buffer ( &http->linebuf, iobuf->data, |
| iob_len ( iobuf ) ); |
| if ( len < 0 ) { |
| rc = len; |
| DBGC ( http, "HTTP %p could not buffer line: " |
| "%s\n", http, strerror ( rc ) ); |
| goto done; |
| } |
| iob_pull ( iobuf, len ); |
| line = buffered_line ( &http->linebuf ); |
| if ( line ) { |
| lh = &http_line_handlers[http->rx_state]; |
| if ( ( rc = lh->rx ( http, line ) ) != 0 ) |
| goto done; |
| } |
| break; |
| default: |
| assert ( 0 ); |
| break; |
| } |
| } |
| |
| done: |
| if ( rc ) |
| http_done ( http, rc ); |
| free_iob ( iobuf ); |
| return rc; |
| } |
| |
| /** |
| * HTTP process |
| * |
| * @v process Process |
| */ |
| static void http_step ( struct process *process ) { |
| struct http_request *http = |
| container_of ( process, struct http_request, process ); |
| const char *path = http->uri->path; |
| const char *host = http->uri->host; |
| const char *query = http->uri->query; |
| const char *user = http->uri->user; |
| const char *password = |
| ( http->uri->password ? http->uri->password : "" ); |
| size_t user_pw_len = ( user ? ( strlen ( user ) + 1 /* ":" */ + |
| strlen ( password ) ) : 0 ); |
| size_t user_pw_base64_len = base64_encoded_len ( user_pw_len ); |
| char user_pw[ user_pw_len + 1 /* NUL */ ]; |
| char user_pw_base64[ user_pw_base64_len + 1 /* NUL */ ]; |
| int rc; |
| |
| if ( xfer_window ( &http->socket ) ) { |
| |
| /* We want to execute only once */ |
| process_del ( &http->process ); |
| |
| /* Construct authorisation, if applicable */ |
| if ( user ) { |
| char *buf = user_pw; |
| ssize_t remaining = sizeof ( user_pw ); |
| size_t len; |
| |
| /* URI-decode the username and password */ |
| len = uri_decode ( user, buf, remaining ); |
| buf += len; |
| remaining -= len; |
| *(remaining--, buf++) = ':'; |
| len = uri_decode ( password, buf, remaining ); |
| buf += len; |
| remaining -= len; |
| assert ( remaining >= 0 ); |
| |
| /* Base64-encode the "user:password" string */ |
| base64_encode ( user_pw, user_pw_base64 ); |
| } |
| |
| /* Send GET request */ |
| if ( ( rc = xfer_printf ( &http->socket, |
| "GET %s%s%s HTTP/1.0\r\n" |
| "User-Agent: gPXE/" VERSION "\r\n" |
| "%s%s%s" |
| "Host: %s\r\n" |
| "\r\n", |
| ( path ? path : "/" ), |
| ( query ? "?" : "" ), |
| ( query ? query : "" ), |
| ( user ? |
| "Authorization: Basic " : "" ), |
| ( user ? user_pw_base64 : "" ), |
| ( user ? "\r\n" : "" ), |
| host ) ) != 0 ) { |
| http_done ( http, rc ); |
| } |
| } |
| } |
| |
| /** |
| * HTTP connection closed by network stack |
| * |
| * @v socket Transport layer interface |
| * @v rc Reason for close |
| */ |
| static void http_socket_close ( struct xfer_interface *socket, int rc ) { |
| struct http_request *http = |
| container_of ( socket, struct http_request, socket ); |
| |
| DBGC ( http, "HTTP %p socket closed: %s\n", |
| http, strerror ( rc ) ); |
| |
| http_done ( http, rc ); |
| } |
| |
| /** HTTP socket operations */ |
| static struct xfer_interface_operations http_socket_operations = { |
| .close = http_socket_close, |
| .vredirect = xfer_vreopen, |
| .window = unlimited_xfer_window, |
| .alloc_iob = default_xfer_alloc_iob, |
| .deliver_iob = http_socket_deliver_iob, |
| .deliver_raw = xfer_deliver_as_iob, |
| }; |
| |
| /** |
| * Close HTTP data transfer interface |
| * |
| * @v xfer Data transfer interface |
| * @v rc Reason for close |
| */ |
| static void http_xfer_close ( struct xfer_interface *xfer, int rc ) { |
| struct http_request *http = |
| container_of ( xfer, struct http_request, xfer ); |
| |
| DBGC ( http, "HTTP %p interface closed: %s\n", |
| http, strerror ( rc ) ); |
| |
| http_done ( http, rc ); |
| } |
| |
| /** HTTP data transfer interface operations */ |
| static struct xfer_interface_operations http_xfer_operations = { |
| .close = http_xfer_close, |
| .vredirect = ignore_xfer_vredirect, |
| .window = unlimited_xfer_window, |
| .alloc_iob = default_xfer_alloc_iob, |
| .deliver_iob = xfer_deliver_as_raw, |
| .deliver_raw = ignore_xfer_deliver_raw, |
| }; |
| |
| /** |
| * Initiate an HTTP connection, with optional filter |
| * |
| * @v xfer Data transfer interface |
| * @v uri Uniform Resource Identifier |
| * @v default_port Default port number |
| * @v filter Filter to apply to socket, or NULL |
| * @ret rc Return status code |
| */ |
| int http_open_filter ( struct xfer_interface *xfer, struct uri *uri, |
| unsigned int default_port, |
| int ( * filter ) ( struct xfer_interface *xfer, |
| struct xfer_interface **next ) ) { |
| struct http_request *http; |
| struct sockaddr_tcpip server; |
| struct xfer_interface *socket; |
| int rc; |
| |
| /* Sanity checks */ |
| if ( ! uri->host ) |
| return -EINVAL; |
| |
| /* Allocate and populate HTTP structure */ |
| http = zalloc ( sizeof ( *http ) ); |
| if ( ! http ) |
| return -ENOMEM; |
| http->refcnt.free = http_free; |
| xfer_init ( &http->xfer, &http_xfer_operations, &http->refcnt ); |
| http->uri = uri_get ( uri ); |
| xfer_init ( &http->socket, &http_socket_operations, &http->refcnt ); |
| process_init ( &http->process, http_step, &http->refcnt ); |
| |
| /* Open socket */ |
| memset ( &server, 0, sizeof ( server ) ); |
| server.st_port = htons ( uri_port ( http->uri, default_port ) ); |
| socket = &http->socket; |
| if ( filter ) { |
| if ( ( rc = filter ( socket, &socket ) ) != 0 ) |
| goto err; |
| } |
| if ( ( rc = xfer_open_named_socket ( socket, SOCK_STREAM, |
| ( struct sockaddr * ) &server, |
| uri->host, NULL ) ) != 0 ) |
| goto err; |
| |
| /* Attach to parent interface, mortalise self, and return */ |
| xfer_plug_plug ( &http->xfer, xfer ); |
| ref_put ( &http->refcnt ); |
| return 0; |
| |
| err: |
| DBGC ( http, "HTTP %p could not create request: %s\n", |
| http, strerror ( rc ) ); |
| http_done ( http, rc ); |
| ref_put ( &http->refcnt ); |
| return rc; |
| } |
| |
| /** |
| * Initiate an HTTP connection |
| * |
| * @v xfer Data transfer interface |
| * @v uri Uniform Resource Identifier |
| * @ret rc Return status code |
| */ |
| static int http_open ( struct xfer_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, |
| }; |