/*
* Copyright (C) 2014 Juan Pablo Ugarte.
*
* 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 (at your option) 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Authors:
* Juan Pablo Ugarte <juanpablougarte@gmail.com>
*/
#include <glade-http.h>
#include <string.h>
#include <stdio.h>
#define HTTP_BUFFER_SIZE 16
struct _GladeHTTPPrivate
{
gchar *host;
gint port;
gboolean tls;
GladeHTTPStatus status;
GSocketConnection *connection;
GCancellable *cancellable;
GString *data;
GString *response;
gchar response_buffer[HTTP_BUFFER_SIZE];
};
enum
{
PROP_0,
PROP_HOST,
PROP_PORT,
PROP_TLS,
N_PROPERTIES
};
enum
{
REQUEST_DONE,
STATUS,
LAST_SIGNAL
};
static GParamSpec *properties[N_PROPERTIES];
static guint http_signals[LAST_SIGNAL] = { 0 };
G_DEFINE_TYPE_WITH_PRIVATE (GladeHTTP, glade_http, G_TYPE_OBJECT);
static void
glade_http_emit_request_done (GladeHTTP *http, gchar *response)
{
gint http_major, http_minor, code, i;
gchar **headers, **values, *blank, *data, tmp;
if ((blank = g_strstr_len (response, -1, "\r\n\r\n")))
data = blank + 4;
else if ((blank = g_strstr_len (response, -1, "\n\n")))
data = blank + 2;
else
return;
tmp = *blank;
*blank = '\0';
headers = g_strsplit (response, "\n", 0);
*blank = tmp;
values = g_new0 (gchar *, g_strv_length (headers));
for (i = 0; headers[i]; i++)
{
g_strchug (headers[i]);
if (i)
{
gchar *colon = g_strstr_len (headers[i], -1, ":");
if (colon)
{
*colon++ = '\0';
values[i-1] = g_strstrip (colon);
}
else
values[i-1] = "";
}
else
{
if (sscanf (response, "HTTP/%d.%d %d", &http_major, &http_minor, &code) != 3)
http_major = http_minor = code = 0;
}
}
/* emit request-done */
g_signal_emit (http, http_signals[REQUEST_DONE], 0, code,
(headers[0]) ? &headers[1] : NULL, values,
data);
g_strfreev (headers);
g_free (values);
}
static void
glade_http_emit_status (GladeHTTP *http, GladeHTTPStatus status, GError *error)
{
GladeHTTPPrivate *priv = http->priv;
if (priv->status == status)
return;
priv->status = status;
g_signal_emit (http, http_signals[STATUS], 0, status, error);
}
static void
on_read_ready (GObject *source, GAsyncResult *res, gpointer data)
{
GladeHTTPPrivate *priv = GLADE_HTTP (data)->priv;
GError *error = NULL;
gssize bytes_read;
glade_http_emit_status (data, GLADE_HTTP_RECEIVING, NULL);
bytes_read = g_input_stream_read_finish (G_INPUT_STREAM (source), res, &error);
if (bytes_read > 0)
{
g_string_append_len (priv->response, priv->response_buffer, bytes_read);
/* NOTE: We do not need to parse content-lenght because we do not support
* multiples HTTP requests in the same connection.
*/
if (priv->cancellable)
g_cancellable_reset (priv->cancellable);
g_input_stream_read_async (g_io_stream_get_input_stream (G_IO_STREAM (priv->connection)),
priv->response_buffer, HTTP_BUFFER_SIZE,
G_PRIORITY_DEFAULT, priv->cancellable,
on_read_ready, data);
return;
}
else if (bytes_read < 0)
{
glade_http_emit_status (data, GLADE_HTTP_ERROR, error);
g_error_free (error);
return;
}
/* emit request-done */
glade_http_emit_request_done (data, priv->response->str);
glade_http_emit_status (data, GLADE_HTTP_READY, NULL);
}
static void
on_write_ready (GObject *source, GAsyncResult *res, gpointer data)
{
GladeHTTPPrivate *priv = GLADE_HTTP (data)->priv;
GError *error = NULL;
gsize count;
count = g_output_stream_write_finish (G_OUTPUT_STREAM (source), res, &error);
if (error == NULL && priv->data->len != count)
error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
"Error sending data data to %s",
priv->host);
if (error)
{
glade_http_emit_status (data, GLADE_HTTP_ERROR, error);
g_error_free (error);
return;
}
glade_http_emit_status (data, GLADE_HTTP_WAITING, NULL);
g_input_stream_read_async (g_io_stream_get_input_stream (G_IO_STREAM (priv->connection)),
priv->response_buffer, HTTP_BUFFER_SIZE,
G_PRIORITY_DEFAULT, priv->cancellable, on_read_ready, data);
}
static void
on_connect_ready (GObject *source, GAsyncResult *res, gpointer data)
{
GladeHTTPPrivate *priv = GLADE_HTTP (data)->priv;
GError *error = NULL;
priv->connection = g_socket_client_connect_to_host_finish (G_SOCKET_CLIENT (source), res, &error);
if (error)
{
glade_http_emit_status (data, GLADE_HTTP_ERROR, error);
g_error_free (error);
return;
}
if (priv->connection)
{
glade_http_emit_status (data, GLADE_HTTP_SENDING, NULL);
g_output_stream_write_async (g_io_stream_get_output_stream (G_IO_STREAM (priv->connection)),
priv->data->str, priv->data->len, G_PRIORITY_DEFAULT,
priv->cancellable, on_write_ready, data);
}
}
static void
glade_http_init (GladeHTTP *http)
{
GladeHTTPPrivate *priv;
priv = http->priv = glade_http_get_instance_private (http);
priv->data = g_string_new ("");
priv->response = g_string_new ("");
}
static void
glade_http_clear (GladeHTTP *http)
{
GladeHTTPPrivate *priv = http->priv;
if (priv->cancellable)
g_cancellable_cancel (priv->cancellable);
g_clear_object (&priv->connection);
g_clear_object (&priv->cancellable);
g_string_assign (priv->data, "");
g_string_assign (priv->response, "");
priv->status = GLADE_HTTP_READY;
}
static void
glade_http_finalize (GObject *object)
{
GladeHTTPPrivate *priv = GLADE_HTTP (object)->priv;
glade_http_clear (GLADE_HTTP (object));
g_string_free (priv->data, TRUE);
g_string_free (priv->response, TRUE);
G_OBJECT_CLASS (glade_http_parent_class)->finalize (object);
}
static void
glade_http_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
GladeHTTPPrivate *priv;
g_return_if_fail (GLADE_IS_HTTP (object));
priv = GLADE_HTTP (object)->priv;
switch (prop_id)
{
case PROP_HOST:
g_free (priv->host);
priv->host = g_strdup (g_value_get_string (value));
break;
case PROP_PORT:
priv->port = g_value_get_int (value);
break;
case PROP_TLS:
priv->tls = g_value_get_boolean (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
glade_http_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
GladeHTTPPrivate *priv;
g_return_if_fail (GLADE_IS_HTTP (object));
priv = GLADE_HTTP (object)->priv;
switch (prop_id)
{
case PROP_HOST:
g_value_set_string (value, priv->host);
break;
case PROP_PORT:
g_value_set_int (value, priv->port);
break;
case PROP_TLS:
g_value_set_boolean (value, priv->tls);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
glade_http_class_init (GladeHTTPClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = glade_http_finalize;
object_class->set_property = glade_http_set_property;
object_class->get_property = glade_http_get_property;
properties[PROP_HOST] =
g_param_spec_string ("host", "Host",
"Host to connect to",
NULL, G_PARAM_READWRITE);
properties[PROP_PORT] =
g_param_spec_int ("port", "TCP Port",
"TCP port to connect to",
0, 65536, 80, G_PARAM_READWRITE);
properties[PROP_TLS] =
g_param_spec_boolean ("tls", "TLS",
"Wheter to use tls encryption or not",
FALSE, G_PARAM_READWRITE);
/* Install all properties */
g_object_class_install_properties (object_class, N_PROPERTIES, properties);
http_signals[REQUEST_DONE] =
g_signal_new ("request-done",
G_OBJECT_CLASS_TYPE (klass), 0,
G_STRUCT_OFFSET (GladeHTTPClass, request_done),
NULL, NULL, NULL,
G_TYPE_NONE, 4,
G_TYPE_INT, G_TYPE_STRV, G_TYPE_STRV, G_TYPE_STRING);
http_signals[STATUS] =
g_signal_new ("status",
G_OBJECT_CLASS_TYPE (klass), 0,
G_STRUCT_OFFSET (GladeHTTPClass, status),
NULL, NULL, NULL,
G_TYPE_NONE, 2,
G_TYPE_INT, G_TYPE_ERROR);
}
GladeHTTP *
glade_http_new (const gchar *host, gint port, gboolean tls)
{
return GLADE_HTTP (g_object_new (GLADE_TYPE_HTTP,
"host", host,
"port", port,
"tls", tls,
NULL));
}
const gchar *
glade_http_get_host (GladeHTTP *http)
{
g_return_val_if_fail (GLADE_IS_HTTP (http), NULL);
return http->priv->host;
}
gint
glade_http_get_port (GladeHTTP *http)
{
g_return_val_if_fail (GLADE_IS_HTTP (http), 0);
return http->priv->port;
}
void
glade_http_request_send_async (GladeHTTP *http,
GCancellable *cancellable,
const gchar *format,
...)
{
GladeHTTPPrivate *priv;
GSocketClient *client;
va_list ap;
g_return_if_fail (GLADE_IS_HTTP (http));
priv = http->priv;
client = g_socket_client_new ();
glade_http_clear (http);
va_start (ap, format);
g_string_vprintf (priv->data, format, ap);
va_end (ap);
priv->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
if (priv->tls)
{
g_socket_client_set_tls (client, TRUE);
g_socket_client_set_tls_validation_flags (client, 0);
}
glade_http_emit_status (http, GLADE_HTTP_CONNECTING, NULL);
g_socket_client_connect_to_host_async (client,
priv->host,
priv->port,
cancellable,
on_connect_ready,
http);
g_object_unref (client);
}