/* GNet - Networking library
 * Copyright (C) 2000  David Helder
 * Copyright (C) 2004  Tim-Philipp Müller
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the 
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA  02111-1307, USA.
 */

/***************************************************************************
 *                                                                         *
 *   TODO:                                                                 *
 *    - support http_proxy environment variable (possible also via API)    *
 *    - support authentication                                             *
 *    - read RFC for Http/1.1 from cover to cover and check compliance     *
 *    - think about how to make an HTTPS easy - maybe add public vfuncs?   *
 *                                                                         *
 ***************************************************************************/

#include "conn-http.h"
#include "gnetconfig.h"

#include <string.h>
#include <stdlib.h>

#define GNET_CONN_HTTP_DEFAULT_MAX_REDIRECTS  5
#define GNET_CONN_HTTP_BUF_INCREMENT          8192    /* 8kB */

typedef enum
{
	STATUS_NONE = 0,
	STATUS_SENT_REQUEST,
	STATUS_RECV_HEADERS,
	STATUS_RECV_BODY_NONCHUNKED,
	STATUS_RECV_CHUNK_SIZE,
	STATUS_RECV_CHUNK_BODY,
	STATUS_ERROR,
	STATUS_DONE
} GConnHttpStatus;

struct _GConnHttp
{
	GInetAddrNewAsyncID  ia_id;
	GInetAddr           *ia;

	GConn               *conn;
	
	gboolean             connection_close; /* Connection: close header was given */

	GConnHttpFunc        func;
	gpointer             func_data;

	guint                num_redirects;
	guint                max_redirects;
	gchar               *redirect_location; /* set if auto-redirection is 
	                                           required after body is received */
	
	GURI                *uri;
	GList               *req_headers;  /* request headers we send */
	GList               *resp_headers; /* response headers we got */

	guint                response_code; 

	GConnHttpMethod      method;
	GConnHttpStatus      status;

	guint                timeout;

	gchar               *post_data;
	gsize                post_data_len;
	gsize                post_data_term_len; /* for extra \n\r etc. */
	
	gsize                content_length;
	gsize                content_recv;
	
	gboolean             tenc_chunked;  /* Transfer-Encoding: chunked */
	
	gchar               *buffer;
	gsize                bufalloc; /* number of bytes allocated             */
	gsize                buflen;   /* number of bytes of data in the buffer */
	
	GMainLoop           *loop;
};

typedef struct _GConnHttpHdr GConnHttpHdr;

struct _GConnHttpHdr
{
	gchar  *field;
	gchar  *value;  /* NULL = not set */
};

static const gchar *gen_headers[] =
{
	"Cache-Control", "Connection", "Date", "Pragma", "Trailer",
	"Transfer-Encoding", "Upgrade", "Via", "Warning"
};

static const gchar *req_headers[] =
{
	"Accept", "Accept-Charset", "Accept-Encoding", "Accept-Language",
	"Authorization", "Expect", "From", "Host", "If-Match", "If-Modified-Since",
	"If-None-Match", "If-Range", "If-Unmodified-Since", "Max-Forwards",
	"Proxy-Authorization", "Range", "Referer", "TE", "User-Agent",
	"Content-Length" /* for POST request */
};

#define is_general_header(field)  (is_in_str_arr(gen_headers,G_N_ELEMENTS(gen_headers),field))
#define is_request_header(field)  (is_in_str_arr(req_headers,G_N_ELEMENTS(req_headers),field))


/***************************************************************************
 *
 *   gnet_conn_http_append_to_buf
 *
 ***************************************************************************/

static void
gnet_conn_http_append_to_buf (GConnHttp *conn, const gchar *data, gsize datalen)
{
	g_return_if_fail (conn != NULL);
	g_return_if_fail (data != NULL);
	
        if (conn->buflen + datalen >= conn->bufalloc)
	{
		while (conn->buflen + datalen >= conn->bufalloc)
			conn->bufalloc += GNET_CONN_HTTP_BUF_INCREMENT;

		conn->buffer = g_realloc (conn->buffer, conn->bufalloc);
	}
        
	if (datalen > 0)
	{
		memcpy (conn->buffer + conn->buflen, data, datalen);
		conn->buflen += datalen;
	}
}

/***************************************************************************
 *
 *   gnet_conn_http_reset
 *
 *   reset certain values, but not the connection or URI or address
 *
 ***************************************************************************/

static void
gnet_conn_http_reset (GConnHttp *conn)
{
	GList *node;
	
	conn->num_redirects  = 0;
	conn->max_redirects  = GNET_CONN_HTTP_DEFAULT_MAX_REDIRECTS;
	g_free(conn->redirect_location);
	conn->redirect_location = NULL;
	
	conn->connection_close = FALSE;
	
	conn->content_length = 0;
	conn->content_recv   = 0;
	conn->tenc_chunked   = FALSE;

	/* Note: we keep the request headers as they are*/
	conn->resp_headers = NULL;
	for (node = conn->resp_headers;  node;  node = node->next)
	{
		GConnHttpHdr *hdr = (GConnHttpHdr*)node->data;
		g_free(hdr->field);
		g_free(hdr->value);
		memset(hdr, 0xff, sizeof(GConnHttpHdr));
		g_free(hdr);
	}
	g_list_free(conn->resp_headers);
	conn->resp_headers = NULL;

	conn->response_code = 0;
	if (conn->method != GNET_CONN_HTTP_METHOD_POST)
	{
		g_free(conn->post_data);
		conn->post_data = NULL;
		conn->post_data_len = 0;
	}
	
	conn->buffer   = g_realloc (conn->buffer, GNET_CONN_HTTP_BUF_INCREMENT);
	conn->bufalloc = GNET_CONN_HTTP_BUF_INCREMENT;
	conn->buflen   = 0;
	
	conn->status = STATUS_NONE;
}

/**
 *  gnet_conn_http_new
 *
 *  Creates a #GConnHttp.
 *
 *  Returns: a #GConnHttp.
 *
 **/

GConnHttp *
gnet_conn_http_new (void)
{
	GConnHttp  *conn;
	gchar       agentstr[64];

	conn = g_new0 (GConnHttp, 1);

	conn->buffer   = g_malloc (GNET_CONN_HTTP_BUF_INCREMENT);
	conn->bufalloc = GNET_CONN_HTTP_BUF_INCREMENT;
	conn->buflen   = 0;

	g_snprintf(agentstr, sizeof(agentstr), "GNet-%u.%u.%u",
	           GNET_MAJOR_VERSION, GNET_MINOR_VERSION, GNET_MICRO_VERSION);

	gnet_conn_http_set_user_agent (conn, agentstr);
	gnet_conn_http_set_method (conn, GNET_CONN_HTTP_METHOD_GET, NULL, 0);

	gnet_conn_http_set_header (conn, "Accept", "*/*", 0);
	gnet_conn_http_set_header (conn, "Connection", "Keep-Alive", 0); 

	gnet_conn_http_set_timeout (conn, 30*1000); /* 30 secs */

	return conn;
}

/***************************************************************************
 *
 *   gnet_conn_http_new_event
 *
 ***************************************************************************/

static GConnHttpEvent * 
gnet_conn_http_new_event (GConnHttpEventType type)
{
	GConnHttpEvent *event = NULL;
	gsize           stsize = 0;

	switch (type)
	{
		case GNET_CONN_HTTP_RESOLVED:
			stsize = sizeof(GConnHttpEventResolved);
			break;

		case GNET_CONN_HTTP_RESPONSE:
			stsize = sizeof(GConnHttpEventResponse);
			break;

		case GNET_CONN_HTTP_REDIRECT:
			stsize = sizeof(GConnHttpEventRedirect);
			break;

		case GNET_CONN_HTTP_DATA_PARTIAL:
		case GNET_CONN_HTTP_DATA_COMPLETE:
			stsize = sizeof(GConnHttpEventData);
			break;
		
		case GNET_CONN_HTTP_ERROR:
			stsize = sizeof(GConnHttpEventError);
			break;

		case GNET_CONN_HTTP_CONNECTED:
		default:
			stsize = sizeof(GConnHttpEvent);
			break;
	}

	event = (GConnHttpEvent*) g_malloc0(stsize);
	event->type = type;
	event->stsize = stsize;

	return event;
}

/***************************************************************************
 *
 *   gnet_conn_http_free_event
 *
 ***************************************************************************/

static void
gnet_conn_http_free_event (GConnHttpEvent *event)
{
	g_return_if_fail (event != NULL);
	g_return_if_fail (event->stsize > 0);

	if (event->type == GNET_CONN_HTTP_RESPONSE)
	{
		g_strfreev(((GConnHttpEventResponse*)event)->header_fields);
		g_strfreev(((GConnHttpEventResponse*)event)->header_values);
	}

	if (event->type == GNET_CONN_HTTP_REDIRECT)
	{
		g_free(((GConnHttpEventRedirect*)event)->new_location);
	}
	
	/* poison struct */
	memset(event, 0xff, event->stsize);
	g_free(event);
}

/***************************************************************************
 *
 *   gnet_conn_http_emit_event
 *
 ***************************************************************************/

static void
gnet_conn_http_emit_event (GConnHttp *conn, GConnHttpEvent *event)
{
	g_return_if_fail (conn != NULL);
	g_return_if_fail (event != NULL);
	
	if (conn->func)
		conn->func (conn, event, conn->func_data);
}

/***************************************************************************
 *
 *   gnet_conn_http_emit_error_event
 *
 ***************************************************************************/

static void
gnet_conn_http_emit_error_event (GConnHttp *conn, GConnHttpError code, const gchar *format, ...)
{
	GConnHttpEventError *ev_err;
	GConnHttpEvent      *ev;
	va_list              args;
	
	g_return_if_fail (conn != NULL);
	
	conn->status = STATUS_ERROR;

	ev = gnet_conn_http_new_event(GNET_CONN_HTTP_ERROR);
	ev_err = (GConnHttpEventError*)ev;
	
	ev_err->code = code;
	
	va_start(args, format);
	ev_err->message = g_strdup_vprintf(format, args); 
	va_end(args);
	
	gnet_conn_http_emit_event(conn, ev);
	gnet_conn_http_free_event(ev);
	
	if (conn->loop)
		g_main_loop_quit(conn->loop);
}

/***************************************************************************
 *
 *   is_in_str_arr
 *
 ***************************************************************************/

static gboolean
is_in_str_arr (const gchar **arr, guint num, const gchar *field)
{
	guint i;

	g_return_val_if_fail (arr   != NULL, FALSE);
	g_return_val_if_fail (field != NULL, FALSE);

	for (i = 0;  i < num;  ++i)
	{
		if (g_ascii_strcasecmp(arr[i], field) == 0)
			return TRUE;
	}

	return FALSE;
}


/**
 *  gnet_conn_http_set_header
 *  @conn: a #GConnHttp
 *  @field: a header field, e.g. "Accept"
 *  @value: the header field value, e.g. "text/html"
 *  @flags: one or more flags of #GConnHttpHeaderFlags, or 0
 *
 *  Set header field to send with the HTTP request.
 *
 *  Returns: TRUE if the header field has been set or changed
 *
 **/

gboolean
gnet_conn_http_set_header (GConnHttp   *conn,
                           const gchar *field,
                           const gchar *value,
                           GConnHttpHeaderFlags flags)
{
	GConnHttpHdr *hdr;
	GList        *node;

	g_return_val_if_fail (conn  != NULL, FALSE);
	g_return_val_if_fail (field != NULL, FALSE);

	/* Don't allow 'Host' to be set explicitely
	 *  we'll do that ourselves */
	if (g_ascii_strcasecmp(field, "Host") == 0)
		return FALSE;

	/* only allow uncommon header fields if
	 * explicitely allowed via the flags */
	if ((flags & GNET_CONN_HTTP_FLAG_SKIP_HEADER_CHECK) == 0)
	{
		if (!(is_general_header(field) || is_request_header(field)))
			return FALSE;
	}

	for (node = conn->req_headers;  node;  node = node->next)
	{
		hdr = (GConnHttpHdr*)node->data;
		if (g_str_equal(hdr->field, field))
		{
			g_free(hdr->value);
			hdr->value = g_strdup(value);
			return TRUE;
		}
	}

	hdr = g_new0 (GConnHttpHdr, 1);
	hdr->field = g_strdup(field);
	hdr->value = g_strdup(value);

	conn->req_headers = g_list_append(conn->req_headers, hdr);

	return TRUE;
}


/**
 *  gnet_conn_http_set_user_agent
 *  @conn: a #GConnHttp
 *  @agent: the user agent string to send
 *
 *  Convenience function. Wraps gnet_conn_http_set_header().
 *
 *  Returns: TRUE if the user agent string has been changed.
 *
 **/

gboolean
gnet_conn_http_set_user_agent (GConnHttp *conn, const gchar *agent)
{
	return gnet_conn_http_set_header (conn, "User-Agent", agent, 0);
}


/**
 *  gnet_conn_http_set_uri
 *  @conn: a #GConnHttp
 *  @uri: URI string
 *
 *  Sets the URI to GET or POST, e.g. http://www.google.com
 *
 *  Returns: TRUE if the URI has been accepted.
 *
 **/

gboolean
gnet_conn_http_set_uri (GConnHttp *conn, const gchar *uri)
{
	gchar *old_hostname = NULL;

	g_return_val_if_fail (conn != NULL, FALSE);
	g_return_val_if_fail (uri  != NULL, FALSE);

	if (conn->uri)
	{
		old_hostname = g_strdup(conn->uri->hostname);
		gnet_uri_delete(conn->uri);
		conn->uri = NULL;
	}
	
	/* Add 'http://' prefix if no scheme/protocol is specified */
	if (strstr(uri,"://") == NULL)
	{
		gchar *full_uri = g_strconcat("http://", uri, NULL);
		conn->uri = gnet_uri_new(full_uri);
		g_free(full_uri);
	}
	else
	{
		if (g_ascii_strncasecmp(uri, "http:", 5) != 0)
			return FALSE; /* unsupported protocol */

		conn->uri = gnet_uri_new(uri);
	}
	
	if (conn->uri && old_hostname && g_ascii_strcasecmp(conn->uri->hostname, old_hostname) != 0)
	{
		if (conn->ia)
		{
			gnet_inetaddr_delete(conn->ia);
			conn->ia = NULL;
		}
		if (conn->conn)
		{
			gnet_conn_delete(conn->conn);
			conn->conn = NULL;
		}
	}

	g_free(old_hostname);

	if (conn->uri == NULL)
		return FALSE;

	gnet_uri_set_scheme(conn->uri, "http");

	gnet_uri_escape(conn->uri);

	return TRUE;
}


/**
 *  gnet_conn_http_set_method
 *  @conn: a #GConnHttp
 *  @method: a #GConnHttpMethod
 *  @post_data: post data to send with POST method, or NULL
 *  @post_data_len: the length of the post data to send with POST method, or 0
 *
 *  Sets the HTTP request method. Default is the GET method.
 *
 *  Returns: TRUE if the method has been changed successfully.
 *
 **/

gboolean
gnet_conn_http_set_method (GConnHttp        *conn, 
                           GConnHttpMethod   method,
                           const gchar      *post_data,
                           gsize             post_data_len)
{
	g_return_val_if_fail (conn != NULL, FALSE);

	switch (method)
	{
		case GNET_CONN_HTTP_METHOD_GET:
			conn->method = method;
			return TRUE;

		case GNET_CONN_HTTP_METHOD_POST:
		{
			g_return_val_if_fail (post_data != NULL, FALSE);
			g_return_val_if_fail (post_data_len > 0, FALSE);
			
			conn->method = method;
			
			g_free(conn->post_data);
			conn->post_data = g_memdup(post_data, post_data_len);
			conn->post_data = g_realloc(conn->post_data, post_data_len + 2 + 2 + 1);
			conn->post_data_len = post_data_len;
			
			conn->post_data[conn->post_data_len+0] = '\r';
			conn->post_data[conn->post_data_len+1] = '\n';
			conn->post_data[conn->post_data_len+2] = '\r';
			conn->post_data[conn->post_data_len+3] = '\n';
			conn->post_data[conn->post_data_len+4] = '\000';
			
			conn->post_data_term_len = 0;
			while (conn->post_data_len < 4 
			    || !g_str_equal(conn->post_data + conn->post_data_len + conn->post_data_term_len - 4, "\r\n\r\n")) 
				conn->post_data_term_len += 2;
			
			return TRUE;
		}

		default:
			break;
	}

	return FALSE;
}


/***************************************************************************
 *
 *   gnet_conn_http_conn_connected
 *
 ***************************************************************************/

static void
gnet_conn_http_conn_connected (GConnHttp *conn)
{
	const gchar *resource;
	GString     *request;
	GList       *node;
	gchar       *res;

	gnet_conn_http_reset(conn);
	
	request = g_string_new(NULL);

	res = gnet_uri_get_string(conn->uri);
	resource = res + strlen(conn->uri->scheme) + strlen("://") + strlen(conn->uri->hostname);

	if (*resource == ':')
	{
		resource = strchr(resource, '/');
		if (resource == NULL || *resource == 0x00)
			resource = "/";
	}


	switch (conn->method)
	{
		case GNET_CONN_HTTP_METHOD_GET:
			g_string_append_printf (request, "GET %s HTTP/1.1\r\n", resource);
		break;

		case GNET_CONN_HTTP_METHOD_POST:
		{
			gchar  buf[16];
			

			/* Note: this must be 1.1 */
			g_string_append_printf (request, "POST %s HTTP/1.1\r\n", resource);
			
			g_snprintf(buf, sizeof(buf), "%u", conn->post_data_len);
			
			gnet_conn_http_set_header (conn, "Expect", "100-continue", 0);
			gnet_conn_http_set_header (conn, "Content-Length", buf, 0);
		}
		break;

		default:
			g_warning("Unknown http method in %s\n", __FUNCTION__);
			return;
	}

	for (node = conn->req_headers;  node;  node = node->next)
	{
		GConnHttpHdr *hdr = (GConnHttpHdr*)node->data;
		if (hdr->field && hdr->value && *hdr->field && *hdr->value)
		{
			g_string_append_printf(request, "%s: %s\r\n", hdr->field, hdr->value);
		}
	}

	if (conn->uri->port == 80)
	{
		g_string_append_printf(request, "Host: %s\r\n",
		                       conn->uri->hostname);
	}
	else
	{
		g_string_append_printf(request, "Host: %s:%u\r\n",
		                       conn->uri->hostname,
		                       conn->uri->port);
	}

	g_string_append(request, "\r\n");

/*	g_print ("Sending:\n%s\n", request->str); */
	gnet_conn_write(conn->conn, request->str, request->len);
	conn->status = STATUS_SENT_REQUEST;

	gnet_conn_readline(conn->conn);

	g_string_free(request, TRUE);
	g_free(res);
}

/***************************************************************************
 *
 *   gnet_conn_http_done
 *
 *   Finished receiving data 
 *
 ***************************************************************************/

static void
gnet_conn_http_done (GConnHttp *conn)
{
	GConnHttpEventData *ev_data;
	GConnHttpEvent     *ev;
	
	conn->status = STATUS_DONE;
	
	ev = gnet_conn_http_new_event (GNET_CONN_HTTP_DATA_COMPLETE);
	ev_data = (GConnHttpEventData*)ev;
	ev_data->buffer         = conn->buffer;
	ev_data->buffer_length  = conn->buflen;
	ev_data->content_length = conn->content_length;
	ev_data->data_received  = conn->content_recv;
	gnet_conn_http_emit_event (conn, ev);
	gnet_conn_http_free_event (ev);

	if (conn->connection_close)
		gnet_conn_disconnect(conn->conn);
	
	/* need to do auto-redirect now? */
	if (conn->redirect_location)
	{
		if (gnet_conn_http_set_uri(conn, conn->redirect_location))
		{
			/* send request with new URI */
			gnet_conn_http_run_async (conn, conn->func, conn->func_data);
			return; /* do not quit out own loop just yet */
		}
		
		gnet_conn_http_emit_error_event (conn, GNET_CONN_HTTP_ERROR_UNSPECIFIED, 
		                                 "Auto-redirect failed for some reason.");
	}
	
	if (conn->loop)
		g_main_loop_quit(conn->loop);
}

/***************************************************************************
 *
 *   gnet_conn_http_conn_parse_response_headers
 *
 ***************************************************************************/

static gboolean
gnet_conn_http_conn_parse_response_headers (GConnHttp *conn)
{
	GConnHttpEventRedirect *ev_redirect;
	GConnHttpEventResponse *ev_response;
	GConnHttpEvent         *ev;
	const gchar            *new_location = NULL;
	guint                   num_headers, n;

	GList *node;
	
	num_headers = g_list_length(conn->resp_headers);

	ev = gnet_conn_http_new_event (GNET_CONN_HTTP_RESPONSE);
	ev_response = (GConnHttpEventResponse*)ev;
	ev_response->header_fields = g_new0(gchar *, num_headers+1);
	ev_response->header_values = g_new0(gchar *, num_headers+1);
	ev_response->response_code = conn->response_code;
	
	n = 0;
	conn->tenc_chunked = FALSE;
	for (node = conn->resp_headers;  node;  node = node->next)
	{
		GConnHttpHdr *hdr = (GConnHttpHdr*)node->data;
		
		ev_response->header_fields[n] = g_strdup(hdr->field); /* better safe than sorry */
		ev_response->header_values[n] = g_strdup(hdr->value);

		if (g_ascii_strcasecmp(hdr->field, "Content-Length") == 0)
		{
			conn->content_length = atoi(hdr->value);
		}
		else if (g_ascii_strcasecmp(hdr->field, "Transfer-Encoding") == 0 
		      && g_ascii_strcasecmp(hdr->value, "chunked") == 0)
		{
			conn->tenc_chunked = TRUE;
		}
		else if (g_ascii_strcasecmp(hdr->field, "Location") == 0)
		{
			new_location = hdr->value;
		}
		/* Note: amazon sends garbled 'Connection' string, but it 
		 *  might also be some apache module problem */
		else if (g_ascii_strcasecmp(hdr->field, "Connection") == 0
		      || g_ascii_strcasecmp(hdr->field, "Cneonction") == 0
		      || g_ascii_strcasecmp(hdr->field, "nnCoection") == 0)  
		{
			conn->connection_close = (g_ascii_strcasecmp(hdr->value, "close") == 0);
		}
		else
		{
			;
		}
		
		++n;
	}
	
	/* send generic response event first */
	gnet_conn_http_emit_event (conn, ev);
	gnet_conn_http_free_event (ev);
	
	/* If not a redirection code, continue normally */
	if (conn->response_code < 300 || conn->response_code >= 400)
		return TRUE; /* continue */
		
	ev = gnet_conn_http_new_event (GNET_CONN_HTTP_REDIRECT);
	ev_redirect = (GConnHttpEventRedirect*)ev;
	
	ev_redirect->num_redirects = conn->num_redirects;
	ev_redirect->max_redirects = conn->max_redirects;
	ev_redirect->auto_redirect = TRUE;

	if (conn->response_code == 301 && conn->method == GNET_CONN_HTTP_METHOD_POST)
		ev_redirect->auto_redirect = FALSE;

	if (conn->num_redirects >= conn->max_redirects)
		ev_redirect->auto_redirect = FALSE;

	/* No Location: header field? tough luck, can't do much */
	ev_redirect->new_location = g_strdup(new_location);
	if (new_location == NULL)
		ev_redirect->auto_redirect = FALSE;

	/* send generic redirect event (dispatch first to give
	 *  the client a chance to stop us before we do the
	 *  automatic redirection) */
	gnet_conn_http_emit_event (conn, ev);

	/* do the redirect later after receiving the body (if any) */
	if (ev_redirect->auto_redirect)
		conn->redirect_location = g_strdup(new_location);

	gnet_conn_http_free_event (ev);
	return TRUE; /* continue and receive body */
}


/***************************************************************************
 *
 *   gnet_conn_http_conn_recv_response
 *
 ***************************************************************************/

static void
gnet_conn_http_conn_recv_response (GConnHttp *conn, gchar *data, gsize len)
{
	gchar *endptr, *start;

	/* wait for proper response */
	if (conn->method == GNET_CONN_HTTP_METHOD_POST && len == 1 && *data == 0x00)
	{
		gnet_conn_readline(conn->conn);
		return;
	}
	
	start = strchr(data, ' ');
	if (start)
	{
		conn->response_code = (guint) strtol(start+1, &endptr, 10);

		gnet_conn_readline(conn->conn);
		
		/* may we continue the POST request? */
		if (conn->response_code == 100 && conn->method == GNET_CONN_HTTP_METHOD_POST)
		{
			gnet_conn_write(conn->conn, conn->post_data, conn->post_data_len + conn->post_data_term_len);
			conn->status = STATUS_SENT_REQUEST; /* expecting the response for the content next */
			return;
		}

		/* note: redirection is handled after we have all headers */

		conn->status = STATUS_RECV_HEADERS; /* response ok - expect headers next */
		return;
	}
	
	/* invalid response or problems parsing */
	conn->response_code = 0; 
	conn->status = STATUS_ERROR;
		
	/* can't continue, so let's emit event right away */
	gnet_conn_http_conn_parse_response_headers(conn);
}

/***************************************************************************
 *
 *   gnet_conn_http_conn_recv_headers
 *
 ***************************************************************************/

static void
gnet_conn_http_conn_recv_headers (GConnHttp *conn, gchar *data, gsize len)
{
	gchar *colon;
	
	/* End of headers? */
	if (*data == 0x00 || g_str_equal(data,"\r\n") || g_str_equal(data,"\r") || g_str_equal(data,"\n"))
	{
		if (!gnet_conn_http_conn_parse_response_headers(conn))
			return; /* redirect - TODO: still used? */

		if (conn->tenc_chunked)
		{
			gnet_conn_readline(conn->conn);
			conn->status = STATUS_RECV_CHUNK_SIZE;
			return;
		}
		else
		{
			gnet_conn_read(conn->conn);
			conn->status = STATUS_RECV_BODY_NONCHUNKED;
			return;
		}
		
		g_return_if_reached();
	}

	/* this is a normal header line then */
	colon = strchr(data, ':');
	if (colon)
	{
		GConnHttpHdr *hdr;

		*colon = 0x00;
	
		hdr = g_new0 (GConnHttpHdr, 1);
		hdr->field = g_strdup(data);
		hdr->value = g_strstrip(g_strdup(colon+1));
	
		conn->resp_headers = g_list_append(conn->resp_headers, hdr);
	}

	gnet_conn_readline(conn->conn);
}

/***************************************************************************
 *
 *   gnet_conn_http_conn_recv_chunk_size
 *
 ***************************************************************************/

static void
gnet_conn_http_conn_recv_chunk_size (GConnHttp *conn, gchar *data, gsize len)
{
	gchar *endptr;
	gsize  chunksize;
			
	chunksize = (gsize) strtol (data, &endptr, 16);
	
	if (chunksize == 0)
	{
		gnet_conn_http_done(conn);
		return;
	}
	
	gnet_conn_readn(conn->conn, chunksize+2); /* including \r\n */
	conn->status = STATUS_RECV_CHUNK_BODY;
}

/***************************************************************************
 *
 *   gnet_conn_http_conn_recv_chunk_body
 *
 ***************************************************************************/

static void
gnet_conn_http_conn_recv_chunk_body (GConnHttp *conn, gchar *data, gsize len)
{
	GConnHttpEventData *ev_data;
	GConnHttpEvent     *ev;
	
	/* do not count the '\r\n' at the end of a block */
	if (len >=2 && data[len-2] == '\r' && data[len-1] == '\n')
		len -= 2;
	
	conn->content_recv += len;
	gnet_conn_http_append_to_buf(conn, data, len);

	ev = gnet_conn_http_new_event(GNET_CONN_HTTP_DATA_PARTIAL);
	ev_data = (GConnHttpEventData*)ev;
	ev_data->buffer = conn->buffer;
	ev_data->content_length = conn->content_length;
	ev_data->data_received  = conn->content_recv;
	gnet_conn_http_emit_event(conn, ev);
	gnet_conn_http_free_event(ev);

	gnet_conn_readline(conn->conn);

	conn->status = STATUS_RECV_CHUNK_SIZE;
}

/***************************************************************************
 *
 *   gnet_conn_http_conn_recv_nonchunked_data
 *
 ***************************************************************************/

static void
gnet_conn_http_conn_recv_nonchunked_data (GConnHttp *conn, gchar *data, gsize len)
{
	GConnHttpEventData *ev_data;
	GConnHttpEvent     *ev;

	if (conn->content_length > 0)
	{
		conn->content_recv += len;
		gnet_conn_http_append_to_buf(conn, data, len);

		if (conn->content_recv >= conn->content_length)
		{
			gnet_conn_http_done(conn);
			return;
		}
		
		gnet_conn_read(conn->conn);
	}
	else
	{
		/* len contains terminating NUL with _readline() */
		/* remote closed connection? */
		if (len == 1 && *data == 0x00) 
		{
			gnet_conn_http_done(conn);
			return;
		}
		conn->content_recv += len-1; /* see above */
		gnet_conn_http_append_to_buf(conn, data, len-1);
		gnet_conn_readline(conn->conn);
	}

	ev = gnet_conn_http_new_event(GNET_CONN_HTTP_DATA_PARTIAL);
	ev_data = (GConnHttpEventData*)ev;
	ev_data->buffer = conn->buffer;
	ev_data->content_length = conn->content_length;
	ev_data->data_received  = conn->content_recv;
	gnet_conn_http_emit_event(conn, ev);
	gnet_conn_http_free_event(ev);
}

/***************************************************************************
 *
 *   gnet_conn_http_conn_got_data
 *
 ***************************************************************************/

static void
gnet_conn_http_conn_got_data (GConnHttp *conn, gchar *data, gsize len)
{
	switch (conn->status)
	{
		/* sent request - so this must be the response code */
		case STATUS_SENT_REQUEST:
			gnet_conn_http_conn_recv_response(conn, data, len);
			break;
		
		case STATUS_RECV_HEADERS:
			gnet_conn_http_conn_recv_headers(conn, data, len);
			break;

		case STATUS_RECV_BODY_NONCHUNKED:
			gnet_conn_http_conn_recv_nonchunked_data(conn, data, len);
			break;

		case STATUS_RECV_CHUNK_SIZE:
			gnet_conn_http_conn_recv_chunk_size(conn, data, len);
			break;

		case STATUS_RECV_CHUNK_BODY:
			gnet_conn_http_conn_recv_chunk_body(conn, data, len);
			break;

		default:
			gnet_conn_http_emit_error_event(conn, GNET_CONN_HTTP_ERROR_UNSPECIFIED,
			                                "%s: should not be reached. conn->status = %u",
			                                G_STRLOC, conn->status);
			break;
	}
}

/***************************************************************************
 *
 *   gnet_conn_http_conn_cb
 *
 *   GConn callback function
 *
 ***************************************************************************/

static void
gnet_conn_http_conn_cb (GConn *c, GConnEvent *event, GConnHttp *httpconn)
{
	GConnHttpEvent *ev;
	
	switch (event->type)
	{
		case GNET_CONN_ERROR:
			gnet_conn_disconnect(httpconn->conn);
			gnet_conn_http_emit_error_event(httpconn, GNET_CONN_HTTP_ERROR_UNSPECIFIED,
			                                "GNET_CONN_ERROR (unspecified Gnet GConn error)");
			if (httpconn->loop)
				g_main_loop_quit(httpconn->loop);
			break;
		
		case GNET_CONN_CONNECT:
			gnet_conn_http_conn_connected(httpconn);
			break;
		
		case GNET_CONN_CLOSE:
			gnet_conn_disconnect(httpconn->conn);
                        
			/* Makes sure we retrieve new location if required */
			if (httpconn->redirect_location)
			{
				gnet_conn_http_done(httpconn);
                                break;
			}
                        
			if (httpconn->loop)
				g_main_loop_quit(httpconn->loop);
			break;
		
		case GNET_CONN_TIMEOUT:
			ev = gnet_conn_http_new_event(GNET_CONN_HTTP_TIMEOUT);
			gnet_conn_http_emit_event(httpconn, ev);
			gnet_conn_http_free_event(ev);
			if (httpconn->loop)
				g_main_loop_quit(httpconn->loop);
			break;
		
		case GNET_CONN_READ:
			gnet_conn_http_conn_got_data(httpconn, event->buffer, event->length);
			break;
		
		case GNET_CONN_WRITE:
		case GNET_CONN_READABLE:
		case GNET_CONN_WRITABLE:
			break;
	}
}

/***************************************************************************
 *
 *   gnet_conn_http_ia_cb
 *
 *   GInetAddr async resolve callback function
 *
 ***************************************************************************/

static void
gnet_conn_http_ia_cb (GInetAddr *ia, GConnHttp *conn)
{
	conn->ia_id = 0;
	
	if (ia != conn->ia || ia == NULL)
	{
		GConnHttpEventResolved *ev_resolved;
		GConnHttpEvent         *ev;
		
		conn->ia = ia;
	
		ev = gnet_conn_http_new_event (GNET_CONN_HTTP_RESOLVED);
		ev_resolved = (GConnHttpEventResolved*)ev;
		ev_resolved->ia = conn->ia;
		gnet_conn_http_emit_event (conn, ev);
		gnet_conn_http_free_event (ev);
	}
		
	/* could not resolve hostname? => emit error event and quit */
	if (ia == NULL)
	{
		if (conn->loop)
			g_main_loop_quit(conn->loop);
		return;
	}

	if (conn->conn == NULL)
	{
		conn->conn = gnet_conn_new_inetaddr (ia, (GConnFunc) gnet_conn_http_conn_cb, conn);
	
		if (conn->conn == NULL)
		{
			gnet_conn_http_emit_error_event(conn, GNET_CONN_HTTP_ERROR_UNSPECIFIED,
			                                "%s: Could not create GConn object.",
			                                G_STRLOC);
			return;
		}
		
		gnet_conn_timeout(conn->conn, conn->timeout); 
		gnet_conn_connect(conn->conn);
		gnet_conn_set_watch_error(conn->conn, TRUE);
		return;
	}

	/* re-use existing connection? */
	if (!gnet_conn_is_connected (conn->conn))
	{
		gnet_conn_timeout(conn->conn, conn->timeout);
		gnet_conn_connect(conn->conn);
	}
	else
	{
		gnet_conn_http_conn_connected(conn);
	}
}

/**
 *  gnet_conn_http_run_async
 *  @conn: a #GConnHttp
 *  @func: callback function to communicate progress and errors, or NULL
 *  @user_data: user data to pass to callback function, or NULL
 *
 *  Starts connecting and sending the specified http request. Will 
 *   return immediately. Assumes there is an existing and running 
 *   default Gtk/GLib/Gnome main loop. 
 *
 **/

void
gnet_conn_http_run_async (GConnHttp        *conn,
                          GConnHttpFunc     func,
                          gpointer          user_data)
{
	g_return_if_fail (conn      != NULL);
	g_return_if_fail (func      != NULL || user_data == NULL);
	g_return_if_fail (conn->uri != NULL);
	g_return_if_fail (conn->ia_id == 0);
	
	conn->func = func;
	conn->func_data = user_data;

	if (conn->uri->port == 0)
		gnet_uri_set_port(conn->uri, 80);  // FIXME: 8080 for http proxies

	if (conn->ia == NULL)
	{
		conn->ia_id = gnet_inetaddr_new_async (conn->uri->hostname,
		                                       conn->uri->port,
		                                       (GInetAddrNewAsyncFunc) gnet_conn_http_ia_cb,
		                                       conn);
	}
	else
	{
		gnet_conn_http_ia_cb(conn->ia, conn);
	}
}

/**
 *  gnet_conn_http_run
 *  @conn: a #GConnHttp
 *  @func: callback function to communicate progress and errors, or NULL
 *  @user_data: user data to pass to callback function, or NULL
 *
 *  Starts connecting and sending the specified http request. Will 
 *   return once the operation has finished and either an error has 
 *   occured, or the data has been received in full. This function will
 *   run its own main loop in the default GLib main context.
 *
 *   Returns: TRUE if no error occured befor connecting
 *
 **/

gboolean
gnet_conn_http_run (GConnHttp        *conn,
                    GConnHttpFunc     func,
                    gpointer          user_data)
{
	g_return_val_if_fail (conn      != NULL, FALSE);
	g_return_val_if_fail (conn->uri != NULL, FALSE);
	g_return_val_if_fail (conn->ia_id == 0, FALSE);

	conn->func = func;
	conn->func_data = user_data;

	if (conn->uri->port == 0)
		gnet_uri_set_port(conn->uri, 80);  // FIXME: 8080 for http proxies

	if (conn->ia == NULL)
	{
		conn->ia_id = gnet_inetaddr_new_async (conn->uri->hostname,
		                                       conn->uri->port,
		                                       (GInetAddrNewAsyncFunc) gnet_conn_http_ia_cb,
		                                       conn);
	}
	else
	{
		gnet_conn_http_ia_cb(conn->ia, conn);
	}

	conn->loop = g_main_loop_new (NULL, FALSE);
	
	g_main_loop_run (conn->loop);

	if (conn->status == STATUS_DONE)
	{
		if (conn->content_length > 0)
			return (conn->content_recv >= conn->content_length);
		
		return (conn->content_recv > 0);
	}
	
	return FALSE;
}


/**
 *  gnet_conn_http_steal_buffer
 *  @conn: a #GConnHttp
 *  @buffer: where to store a pointer to the buffer data
 *  @length: where to store the length of the buffer data
 *
 *  Empties the current buffer and returns the contents. The 
 *   main purpose of this function is to make it possible to 
 *   just use gnet_conn_http_run(), check its return value, and 
 *   then get the buffer data without having to install a 
 *   callback function. Also needed to empty the buffer regularly
 *   while receiving large amounts of data.
 *
 *  The caller (you) needs to free the buffer with g_free() when done.
 *
 *  Returns: TRUE if buffer and length have been set
 *
 **/

gboolean         
gnet_conn_http_steal_buffer (GConnHttp        *conn,
                             gchar           **buffer,
                             gsize            *length)
{
	g_return_val_if_fail (conn   != NULL, FALSE);
	g_return_val_if_fail (buffer != NULL, FALSE);
	g_return_val_if_fail (length != NULL, FALSE);

	if (conn->status == STATUS_NONE 
	 || conn->status == STATUS_SENT_REQUEST
	 || conn->status == STATUS_ERROR)
		return FALSE;

	/* Not the best way, but better for app 
	 *  developers trying to trace down leaks */
	*length = conn->buflen;
        *buffer = g_malloc0 (*length + 1);
	memcpy (*buffer, conn->buffer, *length);
	
	conn->buffer   = g_malloc (GNET_CONN_HTTP_BUF_INCREMENT);
	conn->bufalloc = GNET_CONN_HTTP_BUF_INCREMENT;
	conn->buflen   = 0;
	
	return TRUE;
}


/**
 *  gnet_conn_http_set_max_redirects
 *  @conn: a #GConnHttp
 *  @num: the maximum number of allowed automatic redirects
 *
 *  Sets the maximum allowed number of automatic redirects.
 *   Note that the HTTP protocol specification (RFC2616) specifies 
 *   occasions where the client must not redirect automatically 
 *   without user intervention. In those cases, no automatic redirect
 *   will be performed, even if the limit has not been reached yet.
 *
 **/

void             
gnet_conn_http_set_max_redirects (GConnHttp *conn, guint num)
{
	g_return_if_fail (conn != NULL);
	g_return_if_fail (num > 100);
	
	conn->max_redirects = num;
}


/**
 *  gnet_conn_http_delete
 *  @conn: a #GConnHttp
 *
 *  Deletes a #GConnHttp and frees all associated resources.
 *
 **/

void             
gnet_conn_http_delete (GConnHttp *conn)
{
	g_return_if_fail (conn != NULL);

	if (conn->ia_id > 0)
		gnet_inetaddr_new_async_cancel(conn->ia_id);

	if (conn->ia)
		gnet_inetaddr_delete(conn->ia);
		
	if (conn->conn)
		gnet_conn_delete(conn->conn);

	/* concatenate them into resp_headers, so that 
	 *  they will be freed in gnet_conn_http_reset() */
	conn->resp_headers = g_list_concat(conn->resp_headers, conn->req_headers);
	conn->req_headers = NULL;

	gnet_conn_http_reset(conn);

	if (conn->uri)
		gnet_uri_delete(conn->uri);
	
	if (conn->loop != NULL  &&  g_main_loop_is_running(conn->loop))
	{
		g_warning("conn->loop != NULL and still running in. This indicates"
		          "\ta bug in your code! You are not allowed to call\n"
		          "\tgnet_conn_http_delete() before gnet_conn_http_run()\n"
		          "\thas returned. Use gnet_conn_http_cancel() instead.\n");
	}
	
	if (conn->loop)
		g_main_loop_unref(conn->loop);

	g_free(conn->post_data);

	g_free(conn->buffer);

	memset(conn, 0xff, sizeof(GConnHttp));
	g_free(conn);
}


/**
 *  gnet_conn_http_cancel
 *  @conn: a #GConnHttp
 *
 *  Cancels the current http transfer (if any) and makes
 *   gnet_conn_http_run() return immediately. Will do nothing
 *   if the transfer was started with gnet_conn_http_run_async().
 *
 **/

void             
gnet_conn_http_cancel (GConnHttp *conn)
{
	g_return_if_fail (conn != NULL);
	
	if (conn->loop)
		g_main_loop_quit(conn->loop);
}


/**
 *  gnet_conn_http_set_timeout
 *  @conn: a #GConnHttp
 *  @timeout: timeout in milliseconds
 *
 *  Sets a timeout on the http connection.
 *
 **/

void             
gnet_conn_http_set_timeout (GConnHttp *conn, guint timeout)
{
	g_return_if_fail (conn != NULL);

	conn->timeout = timeout;
}

/***************************************************************************
 *
 *   gnet_http_get_cb
 *
 ***************************************************************************/

static void
gnet_http_get_cb (GConnHttp *conn, GConnHttpEvent *event, gpointer user_data)
{
	guint *p_response = (guint*) user_data;
  
	if (p_response != NULL  &&  event->type == GNET_CONN_HTTP_RESPONSE)
	{
		GConnHttpEventResponse *revent;
                revent = (GConnHttpEventResponse*) event;
		*p_response = revent->response_code;
	}
}

/**
 *  gnet_http_get
 *  @url: a URI, e.g. http://www.foo.com
 *  @buffer: where to store a pointer to the data retrieved
 *  @length: where to store the length of the data retrieved
 *  @response: where to store the last HTTP response code 
 *  received from the HTTP server, or NULL.
 *
 *  Convenience function that just retrieves
 *   the provided URI without the need to 
 *   set up a full #GConnHttp. Uses 
 *   gnet_conn_http_run() internally.
 *
 *  Caller (you) needs to free the buffer with g_free() when
 *   no longer needed.
 *
 *   Returns: TRUE if @buffer, @length and @response are set,
 *   otherwise FALSE.
 *
 **/

gboolean 
gnet_http_get (const gchar  *url, 
               gchar       **buffer, 
               gsize        *length, 
               guint        *response)
{
	GConnHttp *conn;
	gboolean   ret;        

	g_return_val_if_fail (url != NULL && *url != 0x00, FALSE);
	g_return_val_if_fail (buffer != NULL, FALSE);
	g_return_val_if_fail (length != NULL, FALSE);

	if (response)
		*response = 0;

	ret = FALSE;
	conn = gnet_conn_http_new();

	if (gnet_conn_http_set_uri(conn,url))
	{
		if (gnet_conn_http_run(conn, gnet_http_get_cb, response))
		{
			if (gnet_conn_http_steal_buffer(conn, buffer, length))
			{
				ret = TRUE;
			}
		}
	}
               
	gnet_conn_http_delete(conn);

	return ret;
}



syntax highlighted by Code2HTML, v. 0.9.1