/* * Copyright 2014-2016 James Geboski * * 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, see . */ #include #include #include #include "facebook-http.h" #include "facebook-util.h" struct _FbHttpPrivate { FbHttpValues *cookies; GHashTable *reqs; gchar *agent; }; struct _FbHttpRequestPrivate { FbHttp *http; gchar *url; url_t purl; gboolean post; FbHttpValues *headers; FbHttpValues *params; FbHttpFunc func; gpointer data; GError *error; struct http_request *request; gboolean freed; }; G_DEFINE_TYPE(FbHttp, fb_http, G_TYPE_OBJECT); G_DEFINE_TYPE(FbHttpRequest, fb_http_request, G_TYPE_OBJECT); static void fb_http_dispose(GObject *obj) { FbHttp *http = FB_HTTP(obj); FbHttpPrivate *priv = http->priv; g_free(priv->agent); fb_http_close_requests(http); g_hash_table_destroy(priv->reqs); fb_http_values_free(priv->cookies); } static void fb_http_class_init(FbHttpClass *klass) { GObjectClass *gklass = G_OBJECT_CLASS(klass); gklass->dispose = fb_http_dispose; g_type_class_add_private(klass, sizeof (FbHttpPrivate)); } static void fb_http_init(FbHttp *http) { FbHttpPrivate *priv; priv = G_TYPE_INSTANCE_GET_PRIVATE(http, FB_TYPE_HTTP, FbHttpPrivate); http->priv = priv; priv->cookies = fb_http_values_new(); priv->reqs = g_hash_table_new(g_direct_hash, g_direct_equal); } static void fb_http_req_close_nuller(struct http_request *request) { } static void fb_http_request_dispose(GObject *obj) { FbHttpRequestPrivate *priv = FB_HTTP_REQUEST(obj)->priv; if ((priv->request != NULL) && !priv->freed) { /* Prevent more than one call to request->func() */ priv->request->func = fb_http_req_close_nuller; priv->request->data = NULL; http_close(priv->request); } if (priv->error != NULL) { g_error_free(priv->error); } g_free(priv->url); fb_http_values_free(priv->headers); fb_http_values_free(priv->params); } static void fb_http_request_class_init(FbHttpRequestClass *klass) { GObjectClass *gklass = G_OBJECT_CLASS(klass); gklass->dispose = fb_http_request_dispose; g_type_class_add_private(klass, sizeof (FbHttpRequestPrivate)); } static void fb_http_request_init(FbHttpRequest *req) { FbHttpRequestPrivate *priv; priv = G_TYPE_INSTANCE_GET_PRIVATE(req, FB_TYPE_HTTP_REQUEST, FbHttpRequestPrivate); req->priv = priv; priv->headers = fb_http_values_new(); priv->params = fb_http_values_new(); } GQuark fb_http_error_quark(void) { static GQuark q; if (G_UNLIKELY(q == 0)) q = g_quark_from_static_string("fb-http-error-quark"); return q; } FbHttp * fb_http_new(const gchar *agent) { FbHttp *http; FbHttpPrivate *priv; http = g_object_new(FB_TYPE_HTTP, NULL); priv = http->priv; priv->agent = g_strdup(agent); return http; } FbHttpValues * fb_http_get_cookies(FbHttp *http) { FbHttpPrivate *priv; g_return_val_if_fail(FB_IS_HTTP(http), NULL); priv = http->priv; return priv->cookies; } void fb_http_close_requests(FbHttp *http) { FbHttpPrivate *priv; FbHttpRequest *req; GHashTableIter iter; g_return_if_fail(FB_IS_HTTP(http)); priv = http->priv; g_hash_table_iter_init(&iter, priv->reqs); while (g_hash_table_iter_next(&iter, (gpointer) &req, NULL)) { g_hash_table_iter_remove(&iter); g_object_unref(req); } } void fb_http_cookies_parse_request(FbHttp *http, FbHttpRequest *req) { FbHttpPrivate *hriv; FbHttpRequestPrivate *priv; gchar **hdrs; gchar **kv; gchar *str; guint i; guint j; g_return_if_fail(FB_IS_HTTP(http)); g_return_if_fail(FB_IS_HTTP_REQUEST(req)); hriv = http->priv; priv = req->priv; if (priv->request == NULL) { return; } hdrs = g_strsplit(priv->request->reply_headers, "\r\n", 0); for (i = 0; hdrs[i] != NULL; i++) { if (g_ascii_strncasecmp(hdrs[i], "Set-Cookie", 10) != 0) { continue; } str = strchr(hdrs[i], ';'); if (str != NULL) { str[0] = 0; } str = strchr(hdrs[i], ':'); if (str == NULL) { continue; } str = g_strstrip(++str); kv = g_strsplit(str, "=", 2); for (j = 0; kv[j] != NULL; j++) { str = g_uri_unescape_string(kv[j], NULL); g_free(kv[j]); kv[j] = str; } if (g_strv_length(kv) > 1) { fb_http_values_set_str(hriv->cookies, kv[0], kv[1]); } g_strfreev(kv); } g_strfreev(hdrs); } FbHttpRequest * fb_http_request_new(FbHttp *http, const gchar *url, gboolean post, FbHttpFunc func, gpointer data) { FbHttpPrivate *hriv; FbHttpRequest *req; FbHttpRequestPrivate *priv; g_return_val_if_fail(FB_IS_HTTP(http), NULL); g_return_val_if_fail(url != NULL, NULL); g_return_val_if_fail(func != NULL, NULL); req = g_object_new(FB_TYPE_HTTP_REQUEST, NULL); priv = req->priv; hriv = http->priv; if (!url_set(&priv->purl, url)) { g_object_unref(req); return NULL; } priv->http = http; priv->url = g_strdup(url); priv->post = post; priv->func = func; priv->data = data; if (hriv->agent != NULL) { fb_http_values_set_str(priv->headers, "User-Agent", hriv->agent); } fb_http_values_set_str(priv->headers, "Host", priv->purl.host); fb_http_values_set_str(priv->headers, "Accept", "*/*"); fb_http_values_set_str(priv->headers, "Connection", "Close"); return req; } const gchar * fb_http_request_get_data(FbHttpRequest *req, gsize *size) { FbHttpRequestPrivate *priv; g_return_val_if_fail(FB_IS_HTTP_REQUEST(req), NULL); priv = req->priv; if (priv->request == NULL) { if (size != NULL) { *size = 0; } return NULL; } if (size != NULL) { *size = priv->request->body_size; } return priv->request->reply_body; } FbHttpValues * fb_http_request_get_headers(FbHttpRequest *req) { FbHttpRequestPrivate *priv; g_return_val_if_fail(FB_IS_HTTP_REQUEST(req), NULL); priv = req->priv; return priv->headers; } FbHttpValues * fb_http_request_get_params(FbHttpRequest *req) { FbHttpRequestPrivate *priv; g_return_val_if_fail(FB_IS_HTTP_REQUEST(req), NULL); priv = req->priv; return priv->params; } const gchar * fb_http_request_get_status(FbHttpRequest *req, gint *code) { FbHttpRequestPrivate *priv; g_return_val_if_fail(FB_IS_HTTP_REQUEST(req), NULL); priv = req->priv; if (priv->request == NULL) { if (code != NULL) { *code = 0; } return NULL; } if (code != NULL) { *code = priv->request->status_code; } return priv->request->status_string; } GError * fb_http_request_take_error(FbHttpRequest *req) { FbHttpRequestPrivate *priv; GError *err; g_return_val_if_fail(FB_IS_HTTP_REQUEST(req), NULL); priv = req->priv; err = priv->error; priv->error = NULL; return err; } static void fb_http_request_debug(FbHttpRequest *req, gboolean response, const gchar *header, const gchar *body) { const gchar *action; const gchar *method; const gchar *status; FbHttpRequestPrivate *priv = req->priv; gchar **lines; gchar *str; gint code; guint i; status = fb_http_request_get_status(req, &code); action = response ? "Response" : "Request"; method = priv->post ? "POST" : "GET"; if (status != NULL) { str = g_strdup_printf(" (%s)", status); } else if (response) { str = g_strdup_printf(" (%d)", code); } else { str = g_strdup(""); } fb_util_debug_info("%s %s (%p): %s%s", method, action, req, priv->url, str); g_free(str); if ((header != NULL) && (strlen(header) > 0)) { lines = g_strsplit(header, "\n", 0); for (i = 0; lines[i] != NULL; i++) { fb_util_debug_info(" %s", lines[i]); } g_strfreev(lines); } else { fb_util_debug_info(" ** No header data **"); fb_util_debug_info("%s", ""); } if ((body != NULL) && (strlen(body) > 0)) { lines = g_strsplit(body, "\n", 0); for (i = 0; lines[i] != NULL; i++) { fb_util_debug_info(" %s", lines[i]); } g_strfreev(lines); } else { fb_util_debug_info(" ** No body data **"); } } static void fb_http_request_cb(struct http_request *request) { const gchar *status; FbHttpRequest *req = request->data; FbHttpRequestPrivate *priv = req->priv; gint code; status = fb_http_request_get_status(req, &code); g_hash_table_remove(priv->http->priv->reqs, req); priv->freed = TRUE; switch (code) { case 200: case 301: case 302: case 303: case 307: break; default: g_set_error(&priv->error, FB_HTTP_ERROR, code, "%s", status); } fb_http_request_debug(req, TRUE, priv->request->reply_headers, priv->request->reply_body); if (G_LIKELY(priv->func != NULL)) { priv->func(req, priv->data); } g_object_unref(req); } void fb_http_request_send(FbHttpRequest *req) { FbHttpPrivate *hriv; FbHttpRequestPrivate *priv; gchar *hdrs; gchar *prms; gchar *str; gsize size; g_return_if_fail(FB_IS_HTTP_REQUEST(req)); priv = req->priv; hriv = priv->http->priv; if (g_hash_table_size(hriv->cookies) > 0) { str = fb_http_values_str_cookies(hriv->cookies); fb_http_values_set_str(priv->headers, "Cookie", str); g_free(str); } prms = fb_http_values_str_params(priv->params, NULL); if (priv->post) { size = strlen(prms); fb_http_values_set_strf(priv->headers, "Content-Length", "%" G_GSIZE_FORMAT, size); fb_http_values_set_str(priv->headers, "Content-Type", "application/x-www-form-urlencoded"); } hdrs = fb_http_values_str_headers(priv->headers); if (priv->post) { str = g_strdup_printf("POST %s HTTP/1.1\r\n%s\r\n%s", priv->purl.file, hdrs, prms); } else { str = g_strdup_printf("GET %s?%s HTTP/1.1\r\n%s\r\n", priv->purl.file, prms, hdrs); } fb_http_request_debug(req, FALSE, hdrs, prms); priv->request = http_dorequest(priv->purl.host, priv->purl.port, priv->purl.proto == PROTO_HTTPS, str, fb_http_request_cb, req); g_free(hdrs); g_free(prms); g_free(str); if (G_UNLIKELY(priv->request == NULL)) { g_set_error(&priv->error, FB_HTTP_ERROR, FB_HTTP_ERROR_INIT, "Failed to init request"); if (G_LIKELY(priv->func != NULL)) { priv->func(req, priv->data); } g_object_unref(req); return; } g_hash_table_replace(hriv->reqs, req, req); } gboolean fb_http_urlcmp(const gchar *url1, const gchar *url2, gboolean protocol) { gboolean ret; url_t purl1; url_t purl2; if ((url1 == NULL) || (url2 == NULL)) { return url1 == url2; } if (!url_set(&purl1, url1) || !url_set(&purl2, url2)) { return g_ascii_strcasecmp(url1, url2) == 0; } ret = (g_ascii_strcasecmp(purl1.host, purl2.host) == 0) && (g_strcmp0(purl1.file, purl2.file) == 0) && (g_strcmp0(purl1.user, purl2.user) == 0) && (g_strcmp0(purl1.pass, purl2.pass) == 0); if (ret && protocol) { ret = (purl1.proto == purl2.proto) && (purl1.port == purl2.port); } return ret; } static gboolean fb_http_value_equal(gconstpointer a, gconstpointer b) { return g_ascii_strcasecmp(a, b) == 0; } FbHttpValues * fb_http_values_new(void) { return g_hash_table_new_full(g_str_hash, fb_http_value_equal, g_free, g_free); } void fb_http_values_free(FbHttpValues *values) { g_hash_table_destroy(values); } void fb_http_values_consume(FbHttpValues *values, FbHttpValues *consume) { GHashTableIter iter; gpointer key; gpointer val; g_hash_table_iter_init(&iter, consume); while (g_hash_table_iter_next(&iter, &key, &val)) { g_hash_table_iter_steal(&iter); g_hash_table_replace(values, key, val); } g_hash_table_destroy(consume); } void fb_http_values_parse(FbHttpValues *values, const gchar *data, gboolean isurl) { const gchar *tail; gchar *key; gchar **params; gchar *val; guint i; g_return_if_fail(data != NULL); if (isurl) { data = strchr(data, '?'); if (data == NULL) { return; } tail = strchr(++data, '#'); if (tail != NULL) { data = g_strndup(data, tail - data); } else { data = g_strdup(data); } } params = g_strsplit(data, "&", 0); for (i = 0; params[i] != NULL; i++) { key = params[i]; val = strchr(params[i], '='); if (val == NULL) { continue; } *(val++) = 0; key = g_uri_unescape_string(key, NULL); val = g_uri_unescape_string(val, NULL); g_hash_table_replace(values, key, val); } if (isurl) { g_free((gchar *) data); } g_strfreev(params); } gchar * fb_http_values_str_cookies(FbHttpValues *values) { GHashTableIter iter; gchar *key; gchar *val; GString *ret; ret = g_string_new(NULL); g_hash_table_iter_init(&iter, values); while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &val)) { if (val == NULL) { val = ""; } if (ret->len > 0) { g_string_append(ret, "; "); } g_string_append_uri_escaped(ret, key, NULL, TRUE); g_string_append_c(ret, '='); g_string_append_uri_escaped(ret, val, NULL, TRUE); } return g_string_free(ret, FALSE); } gchar * fb_http_values_str_headers(FbHttpValues *values) { GHashTableIter iter; gchar *key; gchar *val; GString *ret; ret = g_string_new(NULL); g_hash_table_iter_init(&iter, values); while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &val)) { if (val == NULL) { val = ""; } g_string_append_printf(ret, "%s: %s\r\n", key, val); } return g_string_free(ret, FALSE); } gchar * fb_http_values_str_params(FbHttpValues *values, const gchar *url) { GHashTableIter iter; gchar *key; gchar *val; GString *ret; ret = g_string_new(NULL); g_hash_table_iter_init(&iter, values); while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &val)) { if (val == NULL) { val = ""; } if (ret->len > 0) { g_string_append_c(ret, '&'); } g_string_append_uri_escaped(ret, key, NULL, TRUE); g_string_append_c(ret, '='); g_string_append_uri_escaped(ret, val, NULL, TRUE); } if (url != NULL) { g_string_prepend_c(ret, '?'); g_string_prepend(ret, url); } return g_string_free(ret, FALSE); } gboolean fb_http_values_remove(FbHttpValues *values, const gchar *name) { return g_hash_table_remove(values, name); } GList * fb_http_values_get_keys(FbHttpValues *values) { return g_hash_table_get_keys(values); } static const gchar * fb_http_values_get(FbHttpValues *values, const gchar *name, GError **error) { const gchar *ret; ret = g_hash_table_lookup(values, name); if (ret == NULL) { g_set_error(error, FB_HTTP_ERROR, FB_HTTP_ERROR_NOMATCH, "No matches for %s", name); return NULL; } return ret; } gboolean fb_http_values_get_bool(FbHttpValues *values, const gchar *name, GError **error) { const gchar *val; val = fb_http_values_get(values, name, error); if (val == NULL) { return FALSE; } return bool2int((gchar *) name); } gdouble fb_http_values_get_dbl(FbHttpValues *values, const gchar *name, GError **error) { const gchar *val; val = fb_http_values_get(values, name, error); if (val == NULL) { return 0.0; } return g_ascii_strtod(val, NULL); } gint64 fb_http_values_get_int(FbHttpValues *values, const gchar *name, GError **error) { const gchar *val; val = fb_http_values_get(values, name, error); if (val == NULL) { return 0; } return g_ascii_strtoll(val, NULL, 10); } const gchar * fb_http_values_get_str(FbHttpValues *values, const gchar *name, GError **error) { return fb_http_values_get(values, name, error); } gchar * fb_http_values_dup_str(FbHttpValues *values, const gchar *name, GError **error) { const gchar *str; str = fb_http_values_get_str(values, name, error); return g_strdup(str); } static void fb_http_values_set(FbHttpValues *values, const gchar *name, gchar *value) { gchar *key; key = g_strdup(name); g_hash_table_replace(values, key, value); } void fb_http_values_set_bool(FbHttpValues *values, const gchar *name, gboolean value) { gchar *val; val = g_strdup(value ? "true" : "false"); fb_http_values_set(values, name, val); } void fb_http_values_set_dbl(FbHttpValues *values, const gchar *name, gdouble value) { gchar *val; val = g_strdup_printf("%f", value); fb_http_values_set(values, name, val); } void fb_http_values_set_int(FbHttpValues *values, const gchar *name, gint64 value) { gchar *val; val = g_strdup_printf("%" G_GINT64_FORMAT, value); fb_http_values_set(values, name, val); } void fb_http_values_set_str(FbHttpValues *values, const gchar *name, const gchar *value) { gchar *val; val = g_strdup(value); fb_http_values_set(values, name, val); } void fb_http_values_set_strf(FbHttpValues *values, const gchar *name, const gchar *format, ...) { gchar *val; va_list ap; va_start(ap, format); val = g_strdup_vprintf(format, ap); va_end(ap); fb_http_values_set(values, name, val); }