/*
 * 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
 *
 * Hyper Text Transfer Protocol (HTTP)
 *
 */

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <byteswap.h>
#include <errno.h>
#include <assert.h>
#include <gpxe/async.h>
#include <gpxe/uri.h>
#include <gpxe/buffer.h>
#include <gpxe/download.h>
#include <gpxe/resolv.h>
#include <gpxe/tcp.h>
#include <gpxe/tls.h>
#include <gpxe/http.h>

static struct async_operations http_async_operations;

static inline struct http_request *
stream_to_http ( struct stream_application *app ) {
	return container_of ( app, struct http_request, stream );
}

/**
 * Mark HTTP request as complete
 *
 * @v http		HTTP request
 * @v rc		Return status code
 *
 */
static void http_done ( struct http_request *http, int rc ) {

	/* Close stream connection */
	stream_close ( &http->stream );

	/* Prevent further processing of any current packet */
	http->rx_state = HTTP_RX_DEAD;

	/* Free up any dynamically allocated storage */
	empty_line_buffer ( &http->linebuf );

	/* 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->buffer->fill ) ) {
		DBGC ( http, "HTTP %p incorrect length %zd, should be %zd\n",
		       http, http->buffer->fill, http->content_length );
		rc = -EIO;
	}

	/* Mark async operation as complete */
	async_done ( &http->async, 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:
		return 0;
	case 404:
		return -ENOENT;
	case 403:
		return -EPERM;
	default:
		return -EIO;
	}
}

/**
 * Handle HTTP response
 *
 * @v http		HTTP request
 * @v response		HTTP response
 */
static void http_rx_response ( struct http_request *http, char *response ) {
	char *spc;
	int rc = -EIO;

	DBGC ( http, "HTTP %p response \"%s\"\n", http, response );

	/* Check response starts with "HTTP/" */
	if ( strncmp ( response, "HTTP/", 5 ) != 0 )
		goto err;

	/* Locate and check response code */
	spc = strchr ( response, ' ' );
	if ( ! spc )
		goto err;
	http->response = strtoul ( spc, NULL, 10 );
	if ( ( rc = http_response_to_rc ( http->response ) ) != 0 )
		goto err;

	/* Move to received headers */
	http->rx_state = HTTP_RX_HEADER;
	return;

 err:
	DBGC ( http, "HTTP %p bad response\n", http );
	http_done ( http, rc );
	return;
}

/**
 * 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;
	int rc;

	http->content_length = strtoul ( value, &endp, 10 );
	if ( *endp != '\0' ) {
		DBGC ( http, "HTTP %p invalid Content-Length \"%s\"\n",
		       http, value );
		return -EIO;
	}

	/* Try to presize the receive buffer */
	if ( ( rc = expand_buffer ( http->buffer,
				    http->content_length ) ) != 0 ) {
		/* May as well abandon the download now; it will fail */
		DBGC ( http, "HTTP %p could not presize buffer: %s\n",
		       http, strerror ( rc ) );
		return rc;
	}

	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 */
struct http_header_handler http_header_handlers[] = {
	{
		.header = "Content-Length",
		.rx = http_rx_content_length,
	},
	{ NULL, NULL }
};

/**
 * Handle HTTP header
 *
 * @v http		HTTP request
 * @v header		HTTP header
 */
static void http_rx_header ( struct http_request *http, char *header ) {
	struct http_header_handler *handler;
	char *separator;
	char *value;
	int rc = -EIO;

	/* 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;
	}

	DBGC ( http, "HTTP %p header \"%s\"\n", http, header );

	/* Split header at the ": " */
	separator = strstr ( header, ": " );
	if ( ! separator )
		goto err;
	*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 )
				goto err;
			break;
		}
	}
	return;

 err:
	DBGC ( http, "HTTP %p bad header\n", http );
	http_done ( http, rc );
	return;
}

/**
 * Handle new data arriving via HTTP connection in the data phase
 *
 * @v http		HTTP request
 * @v data		New data
 * @v len		Length of new data
 */
static void http_rx_data ( struct http_request *http,
			   const char *data, size_t len ) {
	int rc;

	/* Fill data buffer */
	if ( ( rc = fill_buffer ( http->buffer, data,
				  http->buffer->fill, len ) ) != 0 ) {
		DBGC ( http, "HTTP %p failed to fill data buffer: %s\n",
		       http, strerror ( rc ) );
		http_done ( http, rc );
		return;
	}

	/* Update progress */
	http->async.completed = http->buffer->fill;
	http->async.total = http->content_length;

	/* If we have reached the content-length, stop now */
	if ( http->content_length &&
	     ( http->buffer->fill >= http->content_length ) ) {
		http_done ( http, 0 );
	}
}

/**
 * Handle new data arriving via HTTP connection
 *
 * @v http		HTTP request
 * @v data		New data
 * @v len		Length of new data
 */
static void http_newdata ( struct stream_application *app,
			   void *data, size_t len ) {
	struct http_request *http = stream_to_http ( app );
	const char *buf = data;
	char *line;
	int rc;

	while ( len ) {
		if ( http->rx_state == HTTP_RX_DEAD ) {
			/* Do no further processing */
			return;
		} else if ( http->rx_state == HTTP_RX_DATA ) {
			/* Once we're into the data phase, just fill
			 * the data buffer
			 */
			http_rx_data ( http, buf, len );
			return;
		} else {
			/* In the other phases, buffer and process a
			 * line at a time
			 */
			if ( ( rc = line_buffer ( &http->linebuf, &buf,
						  &len ) ) != 0 ) {
				DBGC ( http, "HTTP %p could not buffer line: "
				       "%s\n", http, strerror ( rc ) );
				http_done ( http, rc );
				return;
			}
			if ( ( line = buffered_line ( &http->linebuf ) ) ) {
				switch ( http->rx_state ) {
				case HTTP_RX_RESPONSE:
					http_rx_response ( http, line );
					break;
				case HTTP_RX_HEADER:
					http_rx_header ( http, line );
					break;
				default:
					assert ( 0 );
					break;
				}
			}
		}
	}
}

/**
 * Send HTTP data
 *
 * @v app		Stream application
 * @v buf		Temporary data buffer
 * @v len		Length of temporary data buffer
 */
static void http_senddata ( struct stream_application *app,
			    void *buf, size_t len ) {
	struct http_request *http = stream_to_http ( app );
	const char *path = http->uri->path;
	const char *host = http->uri->host;
	const char *query = http->uri->query;

	len = snprintf ( buf, len,
			 "GET %s%s%s HTTP/1.1\r\n"
			 "User-Agent: gPXE/" VERSION "\r\n"
			 "Host: %s\r\n"
			 "\r\n",
			 ( path ? path : "/" ),
			 ( query ? "?" : "" ),
			 ( query ? query : "" ),
			 host );

	stream_send ( app, ( buf + http->tx_offset ),
		      ( len - http->tx_offset ) );
}

/**
 * HTTP data acknowledged
 *
 * @v app		Stream application
 * @v len		Length of acknowledged data
 */
static void http_acked ( struct stream_application *app, size_t len ) {
	struct http_request *http = stream_to_http ( app );

	http->tx_offset += len;
}

/**
 * HTTP connection closed by network stack
 *
 * @v app		Stream application
 */
static void http_closed ( struct stream_application *app, int rc ) {
	struct http_request *http = stream_to_http ( app );

	DBGC ( http, "HTTP %p connection closed: %s\n",
	       http, strerror ( rc ) );
	
	http_done ( http, rc );
}

/** HTTP stream operations */
static struct stream_application_operations http_stream_operations = {
	.closed		= http_closed,
	.acked		= http_acked,
	.newdata	= http_newdata,
	.senddata	= http_senddata,
};

/**
 * Initiate a HTTP connection
 *
 * @v uri		Uniform Resource Identifier
 * @v buffer		Buffer into which to download file
 * @v parent		Parent asynchronous operation
 * @ret rc		Return status code
 */
int http_get ( struct uri *uri, struct buffer *buffer, struct async *parent ) {
	struct http_request *http = NULL;
	struct sockaddr_tcpip *st;
	int rc;

	/* Allocate and populate HTTP structure */
	http = malloc ( sizeof ( *http ) );
	if ( ! http )
		return -ENOMEM;
	memset ( http, 0, sizeof ( *http ) );
	http->uri = uri;
	http->buffer = buffer;
	async_init ( &http->async, &http_async_operations, parent );
	http->stream.op = &http_stream_operations;
	st = ( struct sockaddr_tcpip * ) &http->server;
	st->st_port = htons ( uri_port ( http->uri, HTTP_PORT ) );

	/* Open TCP connection */
	if ( ( rc = tcp_open ( &http->stream ) ) != 0 )
		goto err;
	if ( strcmp ( http->uri->scheme, "https" ) == 0 ) {
		st->st_port = htons ( uri_port ( http->uri, HTTPS_PORT ) );
		if ( ( rc = add_tls ( &http->stream ) ) != 0 )
			goto err;
	}

	/* Start name resolution.  The download proper will start when
	 * name resolution completes.
	 */
	if ( ( rc = resolv ( uri->host, &http->server, &http->async ) ) != 0 )
		goto err;

	return 0;

 err:
	DBGC ( http, "HTTP %p could not create request: %s\n", 
	       http, strerror ( rc ) );
	async_uninit ( &http->async );
	free ( http );
	return rc;
}

/**
 * Handle name resolution completion
 *
 * @v async		HTTP asynchronous operation
 * @v signal		SIGCHLD
 */
static void http_sigchld ( struct async *async, enum signal signal __unused ) {
	struct http_request *http =
		container_of ( async, struct http_request, async );
	int rc;

	/* If name resolution failed, abort now */
	async_wait ( async, &rc, 1 );
	if ( rc != 0 ) {
		http_done ( http, rc );
		return;
	}

	/* Otherwise, start the HTTP connection */
	if ( ( rc = stream_connect ( &http->stream, &http->server ) ) != 0 ) {
		DBGC ( http, "HTTP %p could not connect stream: %s\n",
		       http, strerror ( rc ) );
		http_done ( http, rc );
		return;
	}
}

/**
 * Free HTTP connection
 *
 * @v async		Asynchronous operation
 */
static void http_reap ( struct async *async ) {
	free ( container_of ( async, struct http_request, async ) );
}

/** HTTP asynchronous operations */
static struct async_operations http_async_operations = {
	.reap = http_reap,
	.signal = {
		[SIGCHLD] = http_sigchld,
	},
};

/** HTTP download protocol */
struct download_protocol http_download_protocol __download_protocol = {
	.name = "http",
	.start_download = http_get,
};

/** HTTPS download protocol */
struct download_protocol https_download_protocol __download_protocol = {
	.name = "https",
	.start_download = http_get,
};
