/* * 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 *host = http->uri->host; 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; int request_len = unparse_uri ( NULL, 0, http->uri, URI_PATH_BIT | URI_QUERY_BIT ); if ( xfer_window ( &http->socket ) ) { char request[request_len + 1]; /* Construct path?query request */ unparse_uri ( request, sizeof ( request ), http->uri, URI_PATH_BIT | URI_QUERY_BIT ); /* We want to execute only once */ process_del ( &http->process ); /* Construct authorisation, if applicable */ if ( user ) { /* Make "user:password" string from decoded fields */ snprintf ( user_pw, sizeof ( user_pw ), "%s:%s", user, password ); /* 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 HTTP/1.0\r\n" "User-Agent: gPXE/" VERSION "\r\n" "%s%s%s" "Host: %s\r\n" "\r\n", http->uri->path ? "" : "/", request, ( 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, };