/* purple * * Purple is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * source distribution. * * 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 02111-1301 USA */ #include "http.h" #ifndef _WIN32 #include #include #endif #include "internal.h" #include "glibcompat.h" #include "debug.h" #include "ntlm.h" #include "proxy.h" #include "purple-socket.h" #include #ifndef z_const #define z_const #endif #define PURPLE_HTTP_URL_CREDENTIALS_CHARS "a-z0-9.,~_/*!&%?=+\\^-" #define PURPLE_HTTP_MAX_RECV_BUFFER_LEN 10240 #define PURPLE_HTTP_MAX_READ_BUFFER_LEN 10240 #define PURPLE_HTTP_GZ_BUFF_LEN 1024 #define PURPLE_HTTP_REQUEST_DEFAULT_MAX_REDIRECTS 20 #define PURPLE_HTTP_REQUEST_DEFAULT_TIMEOUT 30 #define PURPLE_HTTP_REQUEST_DEFAULT_MAX_LENGTH 1048576 #define PURPLE_HTTP_REQUEST_HARD_MAX_LENGTH (G_MAXINT32-1) #define PURPLE_HTTP_PROGRESS_WATCHER_DEFAULT_INTERVAL 250000 typedef struct _PurpleHttpSocket PurpleHttpSocket; typedef struct _PurpleHttpHeaders PurpleHttpHeaders; typedef struct _PurpleHttpKeepaliveHost PurpleHttpKeepaliveHost; typedef struct _PurpleHttpKeepaliveRequest PurpleHttpKeepaliveRequest; typedef struct _PurpleHttpGzStream PurpleHttpGzStream; struct _PurpleHttpSocket { PurpleSocket *ps; gboolean is_busy; guint use_count; PurpleHttpKeepaliveHost *host; }; struct _PurpleHttpRequest { int ref_count; gchar *url; gchar *method; PurpleHttpHeaders *headers; PurpleHttpCookieJar *cookie_jar; PurpleHttpKeepalivePool *keepalive_pool; gchar *contents; int contents_length; PurpleHttpContentReader contents_reader; gpointer contents_reader_data; PurpleHttpContentWriter response_writer; gpointer response_writer_data; int timeout; int max_redirects; gboolean http11; guint max_length; }; struct _PurpleHttpConnection { PurpleConnection *gc; PurpleHttpCallback callback; gpointer user_data; gboolean is_reading; gboolean is_keepalive; gboolean is_cancelling; PurpleHttpURL *url; PurpleHttpRequest *request; PurpleHttpResponse *response; PurpleHttpKeepaliveRequest *socket_request; PurpleHttpConnectionSet *connection_set; PurpleHttpSocket *socket; GString *request_header; guint request_header_written, request_contents_written; gboolean main_header_got, headers_got; GString *response_buffer; PurpleHttpGzStream *gz_stream; GString *contents_reader_buffer; gboolean contents_reader_requested; int redirects_count; int length_expected; guint length_got, length_got_decompressed; gboolean is_chunked, in_chunk, chunks_done; int chunk_length, chunk_got; GList *link_global, *link_gc; guint timeout_handle; PurpleHttpProgressWatcher watcher; gpointer watcher_user_data; guint watcher_interval_threshold; gint64 watcher_last_call; guint watcher_delayed_handle; }; struct _PurpleHttpResponse { int code; gchar *error; GString *contents; PurpleHttpHeaders *headers; }; struct _PurpleHttpURL { gchar *protocol; gchar *username; gchar *password; gchar *host; int port; gchar *path; gchar *fragment; }; struct _PurpleHttpHeaders { GList *list; GHashTable *by_name; }; typedef struct { time_t expires; gchar *value; } PurpleHttpCookie; struct _PurpleHttpCookieJar { int ref_count; GHashTable *tab; }; struct _PurpleHttpKeepaliveRequest { PurpleConnection *gc; PurpleSocketConnectCb cb; gpointer user_data; PurpleHttpKeepaliveHost *host; PurpleHttpSocket *hs; }; struct _PurpleHttpKeepaliveHost { PurpleHttpKeepalivePool *pool; gchar *host; int port; gboolean is_ssl; GSList *sockets; /* list of PurpleHttpSocket */ GSList *queue; /* list of PurpleHttpKeepaliveRequest */ guint process_queue_timeout; }; struct _PurpleHttpKeepalivePool { gboolean is_destroying; int ref_count; guint limit_per_host; /* key: purple_http_socket_hash, value: PurpleHttpKeepaliveHost */ GHashTable *by_hash; }; struct _PurpleHttpConnectionSet { gboolean is_destroying; GHashTable *connections; }; struct _PurpleHttpGzStream { gboolean failed; z_stream zs; gsize max_output; gsize decompressed; GString *pending; }; static time_t purple_http_rfc1123_to_time(const gchar *str); static gboolean purple_http_request_is_method(PurpleHttpRequest *request, const gchar *method); static PurpleHttpConnection * purple_http_connection_new( PurpleHttpRequest *request, PurpleConnection *gc); static void purple_http_connection_terminate(PurpleHttpConnection *hc); static void purple_http_conn_notify_progress_watcher(PurpleHttpConnection *hc); static void purple_http_conn_retry(PurpleHttpConnection *http_conn); static PurpleHttpResponse * purple_http_response_new(void); static void purple_http_response_free(PurpleHttpResponse *response); static void purple_http_cookie_jar_parse(PurpleHttpCookieJar *cookie_jar, GList *values); static gchar * purple_http_cookie_jar_gen(PurpleHttpCookieJar *cookie_jar); gchar * purple_http_cookie_jar_dump(PurpleHttpCookieJar *cjar); static PurpleHttpKeepaliveRequest * purple_http_keepalive_pool_request(PurpleHttpKeepalivePool *pool, PurpleConnection *gc, const gchar *host, int port, gboolean is_ssl, PurpleSocketConnectCb cb, gpointer user_data); static void purple_http_keepalive_pool_request_cancel(PurpleHttpKeepaliveRequest *req); static void purple_http_keepalive_pool_release(PurpleHttpSocket *hs, gboolean invalidate); static void purple_http_connection_set_remove(PurpleHttpConnectionSet *set, PurpleHttpConnection *http_conn); static GRegex *purple_http_re_url, *purple_http_re_url_host, *purple_http_re_rfc1123; /* * Values: pointers to running PurpleHttpConnection. */ static GList *purple_http_hc_list; /* * Keys: pointers to PurpleConnection. * Values: GList of pointers to running PurpleHttpConnection. */ static GHashTable *purple_http_hc_by_gc; /* * Keys: pointers to PurpleConnection. * Values: gboolean TRUE. */ static GHashTable *purple_http_cancelling_gc; /* * Keys: pointers to PurpleHttpConnection. * Values: pointers to links in purple_http_hc_list. */ static GHashTable *purple_http_hc_by_ptr; /*** Helper functions *********************************************************/ static time_t purple_http_rfc1123_to_time(const gchar *str) { static const gchar *months[13] = { "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec", NULL }; GMatchInfo *match_info; gchar *d_date, *d_month, *d_year, *d_time; int month; gchar *iso_date; time_t t; g_return_val_if_fail(str != NULL, 0); g_regex_match(purple_http_re_rfc1123, str, 0, &match_info); if (!g_match_info_matches(match_info)) { g_match_info_free(match_info); return 0; } d_date = g_match_info_fetch(match_info, 1); d_month = g_match_info_fetch(match_info, 2); d_year = g_match_info_fetch(match_info, 3); d_time = g_match_info_fetch(match_info, 4); g_match_info_free(match_info); month = 0; while (months[month] != NULL) { if (0 == g_ascii_strcasecmp(d_month, months[month])) break; month++; } month++; iso_date = g_strdup_printf("%s-%02d-%sT%s+00:00", d_year, month, d_date, d_time); g_free(d_date); g_free(d_month); g_free(d_year); g_free(d_time); if (month > 12) { purple_debug_warning("http", "Invalid month: %d\n", month); g_free(iso_date); return 0; } t = purple_str_to_time(iso_date, TRUE, NULL, NULL, NULL); g_free(iso_date); return t; } /*** GZip streams *************************************************************/ static PurpleHttpGzStream * purple_http_gz_new(gsize max_output, gboolean is_deflate) { PurpleHttpGzStream *gzs = g_new0(PurpleHttpGzStream, 1); int windowBits; if (is_deflate) windowBits = -MAX_WBITS; else /* is gzip */ windowBits = MAX_WBITS + 32; if (inflateInit2(&gzs->zs, windowBits) != Z_OK) { purple_debug_error("http", "Cannot initialize zlib stream\n"); g_free(gzs); return NULL; } gzs->max_output = max_output; return gzs; } static GString * purple_http_gz_put(PurpleHttpGzStream *gzs, const gchar *buf, gsize len) { const gchar *compressed_buff; gsize compressed_len; GString *ret; z_stream *zs; g_return_val_if_fail(gzs != NULL, NULL); g_return_val_if_fail(buf != NULL, NULL); if (gzs->failed) return NULL; zs = &gzs->zs; if (gzs->pending) { g_string_append_len(gzs->pending, buf, len); compressed_buff = gzs->pending->str; compressed_len = gzs->pending->len; } else { compressed_buff = buf; compressed_len = len; } zs->next_in = (z_const Bytef*)compressed_buff; zs->avail_in = compressed_len; ret = g_string_new(NULL); while (zs->avail_in > 0) { int gzres; gchar decompressed_buff[PURPLE_HTTP_GZ_BUFF_LEN]; gsize decompressed_len; zs->next_out = (Bytef*)decompressed_buff; decompressed_len = zs->avail_out = sizeof(decompressed_buff); gzres = inflate(zs, Z_FULL_FLUSH); decompressed_len -= zs->avail_out; if (gzres == Z_OK || gzres == Z_STREAM_END) { if (decompressed_len == 0) break; if (gzs->decompressed + decompressed_len >= gzs->max_output) { purple_debug_warning("http", "Maximum amount of" " decompressed data is reached\n"); decompressed_len = gzs->max_output - gzs->decompressed; gzres = Z_STREAM_END; } gzs->decompressed += decompressed_len; g_string_append_len(ret, decompressed_buff, decompressed_len); if (gzres == Z_STREAM_END) break; } else { purple_debug_error("http", "Decompression failed (%d): %s\n", gzres, zs->msg); gzs->failed = TRUE; g_string_free(ret, TRUE); return NULL; } } if (gzs->pending) { g_string_free(gzs->pending, TRUE); gzs->pending = NULL; } if (zs->avail_in > 0) { gzs->pending = g_string_new_len((gchar*)zs->next_in, zs->avail_in); } return ret; } static void purple_http_gz_free(PurpleHttpGzStream *gzs) { if (gzs == NULL) return; inflateEnd(&gzs->zs); if (gzs->pending) g_string_free(gzs->pending, TRUE); g_free(gzs); } /*** HTTP Sockets *************************************************************/ static gchar * purple_http_socket_hash(const gchar *host, int port, gboolean is_ssl) { return g_strdup_printf("%c:%s:%d", (is_ssl ? 'S' : 'R'), host, port); } static PurpleHttpSocket * purple_http_socket_connect_new(PurpleConnection *gc, const gchar *host, int port, gboolean is_ssl, PurpleSocketConnectCb cb, gpointer user_data) { PurpleHttpSocket *hs = g_new0(PurpleHttpSocket, 1); hs->ps = purple_socket_new(gc); purple_socket_set_data(hs->ps, "hs", hs); purple_socket_set_tls(hs->ps, is_ssl); purple_socket_set_host(hs->ps, host); purple_socket_set_port(hs->ps, port); if (!purple_socket_connect(hs->ps, cb, user_data)) { purple_socket_destroy(hs->ps); g_free(hs); return NULL; } if (purple_debug_is_verbose()) purple_debug_misc("http", "new socket created: %p\n", hs); return hs; } static void purple_http_socket_close_free(PurpleHttpSocket *hs) { if (hs == NULL) return; if (purple_debug_is_verbose()) purple_debug_misc("http", "destroying socket: %p\n", hs); purple_socket_destroy(hs->ps); g_free(hs); } /*** Headers collection *******************************************************/ static PurpleHttpHeaders * purple_http_headers_new(void); static void purple_http_headers_free(PurpleHttpHeaders *hdrs); static void purple_http_headers_add(PurpleHttpHeaders *hdrs, const gchar *key, const gchar *value); static const GList * purple_http_headers_get_all(PurpleHttpHeaders *hdrs); static GList * purple_http_headers_get_all_by_name( PurpleHttpHeaders *hdrs, const gchar *key); static const gchar * purple_http_headers_get(PurpleHttpHeaders *hdrs, const gchar *key); static gboolean purple_http_headers_get_int(PurpleHttpHeaders *hdrs, const gchar *key, int *dst); static gboolean purple_http_headers_match(PurpleHttpHeaders *hdrs, const gchar *key, const gchar *value); static gchar * purple_http_headers_dump(PurpleHttpHeaders *hdrs); static PurpleHttpHeaders * purple_http_headers_new(void) { PurpleHttpHeaders *hdrs = g_new0(PurpleHttpHeaders, 1); hdrs->by_name = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_list_free); return hdrs; } static void purple_http_headers_free_kvp(PurpleKeyValuePair *kvp) { g_free(kvp->key); g_free(kvp->value); g_free(kvp); } static void purple_http_headers_free(PurpleHttpHeaders *hdrs) { if (hdrs == NULL) return; g_hash_table_destroy(hdrs->by_name); g_list_free_full(hdrs->list, (GDestroyNotify)purple_http_headers_free_kvp); g_free(hdrs); } static void purple_http_headers_add(PurpleHttpHeaders *hdrs, const gchar *key, const gchar *value) { PurpleKeyValuePair *kvp; GList *named_values, *new_values; gchar *key_low; g_return_if_fail(hdrs != NULL); g_return_if_fail(key != NULL); g_return_if_fail(value != NULL); kvp = g_new0(PurpleKeyValuePair, 1); kvp->key = g_strdup(key); kvp->value = g_strdup(value); hdrs->list = g_list_append(hdrs->list, kvp); key_low = g_ascii_strdown(key, -1); named_values = g_hash_table_lookup(hdrs->by_name, key_low); new_values = g_list_append(named_values, kvp->value); if (named_values) g_free(key_low); else g_hash_table_insert(hdrs->by_name, key_low, new_values); } static void purple_http_headers_remove(PurpleHttpHeaders *hdrs, const gchar *key) { GList *it, *curr; g_return_if_fail(hdrs != NULL); g_return_if_fail(key != NULL); if (!g_hash_table_remove(hdrs->by_name, key)) return; /* Could be optimized to O(1). */ it = g_list_first(hdrs->list); while (it) { PurpleKeyValuePair *kvp = it->data; curr = it; it = g_list_next(it); if (g_ascii_strcasecmp(kvp->key, key) != 0) continue; hdrs->list = g_list_delete_link(hdrs->list, curr); purple_http_headers_free_kvp(kvp); } } static const GList * purple_http_headers_get_all(PurpleHttpHeaders *hdrs) { g_return_val_if_fail(hdrs != NULL, NULL); return hdrs->list; } /* return const */ static GList * purple_http_headers_get_all_by_name( PurpleHttpHeaders *hdrs, const gchar *key) { GList *values; gchar *key_low; g_return_val_if_fail(hdrs != NULL, NULL); g_return_val_if_fail(key != NULL, NULL); key_low = g_ascii_strdown(key, -1); values = g_hash_table_lookup(hdrs->by_name, key_low); g_free(key_low); return values; } static const gchar * purple_http_headers_get(PurpleHttpHeaders *hdrs, const gchar *key) { const GList *values = purple_http_headers_get_all_by_name(hdrs, key); if (!values) return NULL; return values->data; } static gboolean purple_http_headers_get_int(PurpleHttpHeaders *hdrs, const gchar *key, int *dst) { int val; const gchar *str; str = purple_http_headers_get(hdrs, key); if (!str) return FALSE; if (1 != sscanf(str, "%d", &val)) return FALSE; *dst = val; return TRUE; } static gboolean purple_http_headers_match(PurpleHttpHeaders *hdrs, const gchar *key, const gchar *value) { const gchar *str; str = purple_http_headers_get(hdrs, key); if (str == NULL || value == NULL) return str == value; return (g_ascii_strcasecmp(str, value) == 0); } static gchar * purple_http_headers_dump(PurpleHttpHeaders *hdrs) { const GList *hdr; GString *s = g_string_new(""); hdr = purple_http_headers_get_all(hdrs); while (hdr) { PurpleKeyValuePair *kvp = hdr->data; hdr = g_list_next(hdr); g_string_append_printf(s, "%s: %s%s", kvp->key, (gchar*)kvp->value, hdr ? "\n" : ""); } return g_string_free(s, FALSE); } /*** HTTP protocol backend ****************************************************/ static void _purple_http_disconnect(PurpleHttpConnection *hc, gboolean is_graceful); static void _purple_http_gen_headers(PurpleHttpConnection *hc); static gboolean _purple_http_recv_loopbody(PurpleHttpConnection *hc, gint fd); static void _purple_http_recv(gpointer _hc, gint fd, PurpleInputCondition cond); static void _purple_http_send(gpointer _hc, gint fd, PurpleInputCondition cond); /* closes current connection (if exists), estabilishes one and proceeds with * request */ static gboolean _purple_http_reconnect(PurpleHttpConnection *hc); static void _purple_http_error(PurpleHttpConnection *hc, const char *format, ...) G_GNUC_PRINTF(2, 3); static void _purple_http_error(PurpleHttpConnection *hc, const char *format, ...) { va_list args; va_start(args, format); hc->response->error = g_strdup_vprintf(format, args); va_end(args); if (purple_debug_is_verbose()) purple_debug_warning("http", "error: %s\n", hc->response->error); purple_http_conn_cancel(hc); } static void _purple_http_gen_headers(PurpleHttpConnection *hc) { GString *h; PurpleHttpURL *url; const GList *hdr; PurpleHttpRequest *req; PurpleHttpHeaders *hdrs; gchar *request_url, *tmp_url = NULL; PurpleProxyInfo *proxy; gboolean proxy_http = FALSE; const gchar *proxy_username, *proxy_password; g_return_if_fail(hc != NULL); if (hc->request_header != NULL) return; req = hc->request; url = hc->url; hdrs = req->headers; proxy = purple_proxy_get_setup(hc->gc ? purple_connection_get_account(hc->gc) : NULL); proxy_http = (purple_proxy_info_get_proxy_type(proxy) == PURPLE_PROXY_HTTP || purple_proxy_info_get_proxy_type(proxy) == PURPLE_PROXY_USE_ENVVAR); /* this is HTTP proxy, but used with tunelling with CONNECT */ if (proxy_http && url->port != 80) proxy_http = FALSE; hc->request_header = h = g_string_new(""); hc->request_header_written = 0; hc->request_contents_written = 0; if (proxy_http) request_url = tmp_url = purple_http_url_print(url); else request_url = url->path; g_string_append_printf(h, "%s %s HTTP/%s\r\n", req->method ? req->method : "GET", request_url, req->http11 ? "1.1" : "1.0"); g_free(tmp_url); if (!purple_http_headers_get(hdrs, "host")) g_string_append_printf(h, "Host: %s\r\n", url->host); if (!purple_http_headers_get(hdrs, "connection")) { g_string_append(h, "Connection: "); g_string_append(h, hc->is_keepalive ? "Keep-Alive\r\n" : "close\r\n"); } if (!purple_http_headers_get(hdrs, "accept")) g_string_append(h, "Accept: */*\r\n"); if (!purple_http_headers_get(hdrs, "accept-encoding")) g_string_append(h, "Accept-Encoding: gzip, deflate\r\n"); if (!purple_http_headers_get(hdrs, "content-length") && ( req->contents_length > 0 || purple_http_request_is_method(req, "post"))) { g_string_append_printf(h, "Content-Length: %u\r\n", (unsigned int) req->contents_length); } if (proxy_http) g_string_append(h, "Proxy-Connection: close\r\n"); /* TEST: proxy+KeepAlive */ proxy_username = purple_proxy_info_get_username(proxy); if (proxy_http && proxy_username != NULL && proxy_username[0] != '\0') { gchar *proxy_auth, *ntlm_type1, *tmp; int len; proxy_password = purple_proxy_info_get_password(proxy); if (proxy_password == NULL) proxy_password = ""; tmp = g_strdup_printf("%s:%s", proxy_username, proxy_password); len = strlen(tmp); proxy_auth = purple_base64_encode((const guchar *)tmp, len); memset(tmp, 0, len); g_free(tmp); ntlm_type1 = purple_ntlm_gen_type1(purple_get_host_name(), ""); g_string_append_printf(h, "Proxy-Authorization: Basic %s\r\n", proxy_auth); g_string_append_printf(h, "Proxy-Authorization: NTLM %s\r\n", ntlm_type1); g_string_append(h, "Proxy-Connection: close\r\n"); /* TEST: proxy+KeepAlive */ memset(proxy_auth, 0, strlen(proxy_auth)); g_free(proxy_auth); g_free(ntlm_type1); } hdr = purple_http_headers_get_all(hdrs); while (hdr) { PurpleKeyValuePair *kvp = hdr->data; hdr = g_list_next(hdr); g_string_append_printf(h, "%s: %s\r\n", kvp->key, (gchar*)kvp->value); } if (!purple_http_cookie_jar_is_empty(req->cookie_jar)) { gchar * cookies = purple_http_cookie_jar_gen(req->cookie_jar); g_string_append_printf(h, "Cookie: %s\r\n", cookies); g_free(cookies); } g_string_append_printf(h, "\r\n"); if (purple_debug_is_unsafe() && purple_debug_is_verbose()) { purple_debug_misc("http", "Generated request headers:\n%s", h->str); } } static gboolean _purple_http_recv_headers(PurpleHttpConnection *hc, const gchar *buf, int len) { gchar *eol, *delim; if (hc->headers_got) { purple_debug_error("http", "Headers already got\n"); _purple_http_error(hc, _("Error parsing HTTP")); return FALSE; } g_string_append_len(hc->response_buffer, buf, len); if (hc->response_buffer->len > PURPLE_HTTP_MAX_RECV_BUFFER_LEN) { purple_debug_error("http", "Buffer too big when parsing headers\n"); _purple_http_error(hc, _("Error parsing HTTP")); return FALSE; } while ((eol = strstr(hc->response_buffer->str, "\r\n")) != NULL) { gchar *hdrline = hc->response_buffer->str; int hdrline_len = eol - hdrline; hdrline[hdrline_len] = '\0'; if (hdrline[0] == '\0') { if (!hc->main_header_got) { if (purple_debug_is_verbose() && hc->is_keepalive) { purple_debug_misc("http", "Got keep-" "alive terminator from previous" " request\n"); } else { purple_debug_warning("http", "Got empty" " line at the beginning - this " "may be a HTTP server quirk\n"); } } else /* hc->main_header_got */ { hc->headers_got = TRUE; if (purple_debug_is_verbose()) { purple_debug_misc("http", "Got headers " "end\n"); } } } else if (!hc->main_header_got) { hc->main_header_got = TRUE; delim = strchr(hdrline, ' '); if (delim == NULL || 1 != sscanf(delim + 1, "%d", &hc->response->code)) { purple_debug_warning("http", "Invalid response code\n"); _purple_http_error(hc, _("Error parsing HTTP")); return FALSE; } if (purple_debug_is_verbose()) purple_debug_misc("http", "Got main header with code %d\n", hc->response->code); } else { if (purple_debug_is_verbose() && purple_debug_is_unsafe()) purple_debug_misc("http", "Got header: %s\n", hdrline); delim = strchr(hdrline, ':'); if (delim == NULL || delim == hdrline) { purple_debug_warning("http", "Bad header delimiter\n"); _purple_http_error(hc, _("Error parsing HTTP")); return FALSE; } *delim++ = '\0'; while (*delim == ' ') delim++; purple_http_headers_add(hc->response->headers, hdrline, delim); } g_string_erase(hc->response_buffer, 0, hdrline_len + 2); if (hc->headers_got) break; } return TRUE; } static gboolean _purple_http_recv_body_data(PurpleHttpConnection *hc, const gchar *buf, int len) { GString *decompressed = NULL; if (hc->length_expected >= 0 && len + hc->length_got > (guint)hc->length_expected) { len = hc->length_expected - hc->length_got; } hc->length_got += len; if (hc->gz_stream != NULL) { decompressed = purple_http_gz_put(hc->gz_stream, buf, len); if (decompressed == NULL) { _purple_http_error(hc, _("Error while decompressing data")); return FALSE; } buf = decompressed->str; len = decompressed->len; } g_assert(hc->request->max_length <= PURPLE_HTTP_REQUEST_HARD_MAX_LENGTH); if (hc->length_got_decompressed + len > hc->request->max_length) { purple_debug_warning("http", "Maximum length exceeded, truncating\n"); len = hc->request->max_length - hc->length_got_decompressed; hc->length_expected = hc->length_got; } hc->length_got_decompressed += len; if (len == 0) { if (decompressed != NULL) g_string_free(decompressed, TRUE); return TRUE; } if (hc->request->response_writer != NULL) { gboolean succ; succ = hc->request->response_writer(hc, hc->response, buf, hc->length_got_decompressed, len, hc->request->response_writer_data); if (!succ) { if (decompressed != NULL) g_string_free(decompressed, TRUE); purple_debug_error("http", "Cannot write using callback\n"); _purple_http_error(hc, _("Error handling retrieved data")); return FALSE; } } else { if (hc->response->contents == NULL) hc->response->contents = g_string_new(""); g_string_append_len(hc->response->contents, buf, len); } if (decompressed != NULL) g_string_free(decompressed, TRUE); purple_http_conn_notify_progress_watcher(hc); return TRUE; } static gboolean _purple_http_recv_body_chunked(PurpleHttpConnection *hc, const gchar *buf, int len) { gchar *eol, *line; int line_len; if (hc->chunks_done) return FALSE; if (!hc->response_buffer) hc->response_buffer = g_string_new(""); g_string_append_len(hc->response_buffer, buf, len); if (hc->response_buffer->len > PURPLE_HTTP_MAX_RECV_BUFFER_LEN) { purple_debug_error("http", "Buffer too big when searching for chunk\n"); _purple_http_error(hc, _("Error parsing HTTP")); return FALSE; } while (hc->response_buffer->len > 0) { if (hc->in_chunk) { int got_now = hc->response_buffer->len; if (hc->chunk_got + got_now > hc->chunk_length) got_now = hc->chunk_length - hc->chunk_got; hc->chunk_got += got_now; if (!_purple_http_recv_body_data(hc, hc->response_buffer->str, got_now)) return FALSE; g_string_erase(hc->response_buffer, 0, got_now); hc->in_chunk = (hc->chunk_got < hc->chunk_length); continue; } line = hc->response_buffer->str; eol = strstr(line, "\r\n"); if (eol == line) { g_string_erase(hc->response_buffer, 0, 2); line = hc->response_buffer->str; eol = strstr(line, "\r\n"); } if (eol == NULL) { /* waiting for more data (unlikely, but possible) */ if (hc->response_buffer->len > 20) { purple_debug_warning("http", "Chunk length not " "found (buffer too large)\n"); _purple_http_error(hc, _("Error parsing HTTP")); return FALSE; } return TRUE; } line_len = eol - line; if (1 != sscanf(line, "%x", &hc->chunk_length)) { if (purple_debug_is_unsafe()) purple_debug_warning("http", "Chunk length not found in [%s]\n", line); else purple_debug_warning("http", "Chunk length not found\n"); _purple_http_error(hc, _("Error parsing HTTP")); return FALSE; } hc->chunk_got = 0; hc->in_chunk = TRUE; if (purple_debug_is_verbose()) purple_debug_misc("http", "Found chunk of length %d\n", hc->chunk_length); g_string_erase(hc->response_buffer, 0, line_len + 2); if (hc->chunk_length == 0) { hc->chunks_done = TRUE; hc->in_chunk = FALSE; return TRUE; } } return TRUE; } static gboolean _purple_http_recv_body(PurpleHttpConnection *hc, const gchar *buf, int len) { if (hc->is_chunked) return _purple_http_recv_body_chunked(hc, buf, len); return _purple_http_recv_body_data(hc, buf, len); } static gboolean _purple_http_recv_loopbody(PurpleHttpConnection *hc, gint fd) { int len; gchar buf[4096]; gboolean got_anything; len = purple_socket_read(hc->socket->ps, (guchar*)buf, sizeof(buf)); got_anything = (len > 0); if (len < 0 && errno == EAGAIN) return FALSE; if (len < 0) { _purple_http_error(hc, _("Error reading from %s: %s"), hc->url->host, g_strerror(errno)); return FALSE; } /* EOF */ if (len == 0) { if (hc->request->max_length == 0) { /* It's definitely YHttpServer quirk. */ purple_debug_warning("http", "Got EOF, but no data was " "expected (this may be a server quirk)\n"); hc->length_expected = hc->length_got; } if (hc->length_expected >= 0 && hc->length_got < (guint)hc->length_expected) { purple_debug_warning("http", "No more data while reading" " contents\n"); _purple_http_error(hc, _("Error parsing HTTP")); return FALSE; } if (hc->headers_got) hc->length_expected = hc->length_got; else if (hc->length_got == 0 && hc->socket->use_count > 1) { purple_debug_info("http", "Keep-alive connection " "expired (when reading), retrying...\n"); purple_http_conn_retry(hc); return FALSE; } else { const gchar *server = purple_http_headers_get( hc->response->headers, "Server"); if (server && g_ascii_strcasecmp(server, "YHttpServer") == 0) { purple_debug_warning("http", "No more data " "while parsing headers (YHttpServer " "quirk)\n"); hc->headers_got = TRUE; hc->length_expected = hc->length_got = 0; hc->length_got_decompressed = 0; } else { purple_debug_warning("http", "No more data " "while parsing headers\n"); _purple_http_error(hc, _("Error parsing HTTP")); return FALSE; } } } if (!hc->headers_got && len > 0) { if (!_purple_http_recv_headers(hc, buf, len)) return FALSE; len = 0; if (hc->headers_got) { gboolean is_gzip, is_deflate; if (!purple_http_headers_get_int(hc->response->headers, "Content-Length", &hc->length_expected)) hc->length_expected = -1; hc->is_chunked = (purple_http_headers_match( hc->response->headers, "Transfer-Encoding", "chunked")); is_gzip = purple_http_headers_match( hc->response->headers, "Content-Encoding", "gzip"); is_deflate = purple_http_headers_match( hc->response->headers, "Content-Encoding", "deflate"); if (is_gzip || is_deflate) { hc->gz_stream = purple_http_gz_new( hc->request->max_length + 1, is_deflate); } } if (hc->headers_got && hc->response_buffer && hc->response_buffer->len > 0) { int buffer_len = hc->response_buffer->len; gchar *buffer = g_string_free(hc->response_buffer, FALSE); hc->response_buffer = NULL; if (!_purple_http_recv_body(hc, buffer, buffer_len)) { g_free(buffer); return FALSE; } g_free(buffer); } if (!hc->headers_got) return got_anything; } if (len > 0) { if (!_purple_http_recv_body(hc, buf, len)) return FALSE; } if (hc->is_chunked && hc->chunks_done && hc->length_expected < 0) hc->length_expected = hc->length_got; if (hc->length_expected >= 0 && hc->length_got >= (guint)hc->length_expected) { const gchar *redirect; if (hc->is_chunked && !hc->chunks_done) { if (len == 0) { _purple_http_error(hc, _("Chunked connection terminated")); return FALSE; } if (purple_debug_is_verbose()) { purple_debug_misc("http", "I need the terminating empty chunk\n"); } return TRUE; } if (!hc->headers_got) { hc->response->code = 0; purple_debug_warning("http", "No headers got\n"); _purple_http_error(hc, _("Error parsing HTTP")); return FALSE; } if (purple_debug_is_unsafe() && purple_debug_is_verbose()) { gchar *hdrs = purple_http_headers_dump( hc->response->headers); purple_debug_misc("http", "Got response headers: %s\n", hdrs); g_free(hdrs); } purple_http_cookie_jar_parse(hc->request->cookie_jar, purple_http_headers_get_all_by_name( hc->response->headers, "Set-Cookie")); if (purple_debug_is_unsafe() && purple_debug_is_verbose() && !purple_http_cookie_jar_is_empty( hc->request->cookie_jar)) { gchar *cookies = purple_http_cookie_jar_dump( hc->request->cookie_jar); purple_debug_misc("http", "Cookies: %s\n", cookies); g_free(cookies); } if (hc->response->code == 407) { _purple_http_error(hc, _("Invalid proxy credentials")); return FALSE; } redirect = purple_http_headers_get(hc->response->headers, "location"); if (redirect && (hc->request->max_redirects == -1 || hc->request->max_redirects > hc->redirects_count)) { PurpleHttpURL *url = purple_http_url_parse(redirect); hc->redirects_count++; if (!url) { if (purple_debug_is_unsafe()) purple_debug_warning("http", "Invalid redirect to %s\n", redirect); else purple_debug_warning("http", "Invalid redirect\n"); _purple_http_error(hc, _("Error parsing HTTP")); } purple_http_url_relative(hc->url, url); purple_http_url_free(url); _purple_http_reconnect(hc); return FALSE; } _purple_http_disconnect(hc, TRUE); purple_http_connection_terminate(hc); return FALSE; } return got_anything; } static void _purple_http_recv(gpointer _hc, gint fd, PurpleInputCondition cond) { PurpleHttpConnection *hc = _hc; while (_purple_http_recv_loopbody(hc, fd)); } static void _purple_http_send_got_data(PurpleHttpConnection *hc, gboolean success, gboolean eof, size_t stored) { int estimated_length; g_return_if_fail(hc != NULL); if (!success) { _purple_http_error(hc, _("Error requesting data to write")); return; } hc->contents_reader_requested = FALSE; g_string_set_size(hc->contents_reader_buffer, stored); if (!eof) return; estimated_length = hc->request_contents_written + stored; if (hc->request->contents_length != -1 && hc->request->contents_length != estimated_length) { purple_debug_warning("http", "Invalid amount of data has been written\n"); } hc->request->contents_length = estimated_length; } static void _purple_http_send(gpointer _hc, gint fd, PurpleInputCondition cond) { PurpleHttpConnection *hc = _hc; int written, write_len; const gchar *write_from; gboolean writing_headers; /* Waiting for data. This could be written more efficiently, by removing * (and later, adding) hs->inpa. */ if (hc->contents_reader_requested) return; _purple_http_gen_headers(hc); writing_headers = (hc->request_header_written < hc->request_header->len); if (writing_headers) { write_from = hc->request_header->str + hc->request_header_written; write_len = hc->request_header->len - hc->request_header_written; } else if (hc->request->contents_reader) { if (hc->contents_reader_requested) return; /* waiting for data */ if (!hc->contents_reader_buffer) hc->contents_reader_buffer = g_string_new(""); if (hc->contents_reader_buffer->len == 0) { hc->contents_reader_requested = TRUE; g_string_set_size(hc->contents_reader_buffer, PURPLE_HTTP_MAX_READ_BUFFER_LEN); hc->request->contents_reader(hc, hc->contents_reader_buffer->str, hc->request_contents_written, PURPLE_HTTP_MAX_READ_BUFFER_LEN, hc->request->contents_reader_data, _purple_http_send_got_data); return; } write_from = hc->contents_reader_buffer->str; write_len = hc->contents_reader_buffer->len; } else { write_from = hc->request->contents + hc->request_contents_written; write_len = hc->request->contents_length - hc->request_contents_written; } if (write_len == 0) { purple_debug_warning("http", "Nothing to write\n"); written = 0; } else { written = purple_socket_write(hc->socket->ps, (const guchar*)write_from, write_len); } if (written < 0 && errno == EAGAIN) return; if (written < 0) { if (hc->request_header_written == 0 && hc->socket->use_count > 1) { purple_debug_info("http", "Keep-alive connection " "expired (when writing), retrying...\n"); purple_http_conn_retry(hc); return; } _purple_http_error(hc, _("Error writing to %s: %s"), hc->url->host, g_strerror(errno)); return; } if (writing_headers) { hc->request_header_written += written; purple_http_conn_notify_progress_watcher(hc); if (hc->request_header_written < hc->request_header->len) return; if (hc->request->contents_length > 0) return; } else { hc->request_contents_written += written; purple_http_conn_notify_progress_watcher(hc); if (hc->contents_reader_buffer) g_string_erase(hc->contents_reader_buffer, 0, written); if (hc->request->contents_length > 0 && hc->request_contents_written < (guint)hc->request->contents_length) { return; } } /* request is completely written, let's read the response */ hc->is_reading = TRUE; purple_socket_watch(hc->socket->ps, PURPLE_INPUT_READ, _purple_http_recv, hc); } static void _purple_http_disconnect(PurpleHttpConnection *hc, gboolean is_graceful) { g_return_if_fail(hc != NULL); if (hc->request_header) g_string_free(hc->request_header, TRUE); hc->request_header = NULL; if (hc->response_buffer) g_string_free(hc->response_buffer, TRUE); hc->response_buffer = NULL; if (hc->socket_request) purple_http_keepalive_pool_request_cancel(hc->socket_request); else { purple_http_keepalive_pool_release(hc->socket, !is_graceful); hc->socket = NULL; } } static void _purple_http_connected(PurpleSocket *ps, const gchar *error, gpointer _hc) { PurpleHttpSocket *hs = NULL; PurpleHttpConnection *hc = _hc; if (ps != NULL) hs = purple_socket_get_data(ps, "hs"); hc->socket_request = NULL; hc->socket = hs; if (error != NULL) { _purple_http_error(hc, _("Unable to connect to %s: %s"), hc->url->host, error); return; } purple_socket_watch(ps, PURPLE_INPUT_WRITE, _purple_http_send, hc); } static gboolean _purple_http_reconnect(PurpleHttpConnection *hc) { PurpleHttpURL *url; gboolean is_ssl = FALSE; g_return_val_if_fail(hc != NULL, FALSE); g_return_val_if_fail(hc->url != NULL, FALSE); _purple_http_disconnect(hc, TRUE); if (purple_debug_is_verbose()) { if (purple_debug_is_unsafe()) { gchar *urlp = purple_http_url_print(hc->url); purple_debug_misc("http", "Connecting to %s...\n", urlp); g_free(urlp); } else purple_debug_misc("http", "Connecting to %s...\n", hc->url->host); } url = hc->url; if (g_strcmp0(url->protocol, "") == 0 || g_ascii_strcasecmp(url->protocol, "http") == 0) { /* do nothing */ } else if (g_ascii_strcasecmp(url->protocol, "https") == 0) { is_ssl = TRUE; } else { _purple_http_error(hc, _("Unsupported protocol: %s"), url->protocol); return FALSE; } if (hc->request->keepalive_pool != NULL) { hc->socket_request = purple_http_keepalive_pool_request( hc->request->keepalive_pool, hc->gc, url->host, url->port, is_ssl, _purple_http_connected, hc); } else { hc->socket = purple_http_socket_connect_new(hc->gc, url->host, url->port, is_ssl, _purple_http_connected, hc); } if (hc->socket_request == NULL && hc->socket == NULL) { _purple_http_error(hc, _("Unable to connect to %s"), url->host); return FALSE; } purple_http_headers_free(hc->response->headers); hc->response->headers = purple_http_headers_new(); hc->response_buffer = g_string_new(""); hc->main_header_got = FALSE; hc->headers_got = FALSE; if (hc->response->contents != NULL) g_string_free(hc->response->contents, TRUE); hc->response->contents = NULL; hc->length_got = 0; hc->length_got_decompressed = 0; hc->length_expected = -1; hc->is_chunked = FALSE; hc->in_chunk = FALSE; hc->chunks_done = FALSE; purple_http_conn_notify_progress_watcher(hc); return TRUE; } /*** Performing HTTP requests *************************************************/ static gboolean purple_http_request_timeout(gpointer _hc) { PurpleHttpConnection *hc = _hc; purple_debug_warning("http", "Timeout reached for request %p\n", hc); purple_http_conn_cancel(hc); return FALSE; } PurpleHttpConnection * purple_http_get(PurpleConnection *gc, PurpleHttpCallback callback, gpointer user_data, const gchar *url) { PurpleHttpRequest *request; PurpleHttpConnection *hc; g_return_val_if_fail(url != NULL, NULL); request = purple_http_request_new(url); hc = purple_http_request(gc, request, callback, user_data); purple_http_request_unref(request); return hc; } PurpleHttpConnection * purple_http_get_printf(PurpleConnection *gc, PurpleHttpCallback callback, gpointer user_data, const gchar *format, ...) { va_list args; gchar *value; PurpleHttpConnection *ret; g_return_val_if_fail(format != NULL, NULL); va_start(args, format); value = g_strdup_vprintf(format, args); va_end(args); ret = purple_http_get(gc, callback, user_data, value); g_free(value); return ret; } PurpleHttpConnection * purple_http_request(PurpleConnection *gc, PurpleHttpRequest *request, PurpleHttpCallback callback, gpointer user_data) { PurpleHttpConnection *hc; g_return_val_if_fail(request != NULL, NULL); if (request->url == NULL) { purple_debug_error("http", "Cannot perform new request - " "URL is not set\n"); return NULL; } if (g_hash_table_lookup(purple_http_cancelling_gc, gc)) { purple_debug_warning("http", "Cannot perform another HTTP " "request while cancelling all related with this " "PurpleConnection\n"); return NULL; } hc = purple_http_connection_new(request, gc); hc->callback = callback; hc->user_data = user_data; hc->url = purple_http_url_parse(request->url); if (purple_debug_is_unsafe()) purple_debug_misc("http", "Performing new request %p for %s.\n", hc, request->url); else purple_debug_misc("http", "Performing new request %p to %s.\n", hc, hc->url ? hc->url->host : ""); if (!hc->url || hc->url->host == NULL || hc->url->host[0] == '\0') { purple_debug_error("http", "Invalid URL requested.\n"); purple_http_connection_terminate(hc); return NULL; } _purple_http_reconnect(hc); hc->timeout_handle = purple_timeout_add_seconds(request->timeout, purple_http_request_timeout, hc); return hc; } /*** HTTP connection API ******************************************************/ static void purple_http_connection_free(PurpleHttpConnection *hc); static gboolean purple_http_conn_notify_progress_watcher_timeout(gpointer _hc); static PurpleHttpConnection * purple_http_connection_new( PurpleHttpRequest *request, PurpleConnection *gc) { PurpleHttpConnection *hc = g_new0(PurpleHttpConnection, 1); g_assert(request != NULL); hc->request = request; purple_http_request_ref(request); hc->response = purple_http_response_new(); hc->is_keepalive = (request->keepalive_pool != NULL); hc->link_global = purple_http_hc_list = g_list_prepend(purple_http_hc_list, hc); g_hash_table_insert(purple_http_hc_by_ptr, hc, hc->link_global); if (gc) { GList *gc_list = g_hash_table_lookup(purple_http_hc_by_gc, gc); g_hash_table_steal(purple_http_hc_by_gc, gc); hc->link_gc = gc_list = g_list_prepend(gc_list, hc); g_hash_table_insert(purple_http_hc_by_gc, gc, gc_list); hc->gc = gc; } return hc; } static void purple_http_connection_free(PurpleHttpConnection *hc) { if (hc->timeout_handle) purple_timeout_remove(hc->timeout_handle); if (hc->watcher_delayed_handle) purple_timeout_remove(hc->watcher_delayed_handle); if (hc->connection_set != NULL) purple_http_connection_set_remove(hc->connection_set, hc); purple_http_url_free(hc->url); purple_http_request_unref(hc->request); purple_http_response_free(hc->response); if (hc->contents_reader_buffer) g_string_free(hc->contents_reader_buffer, TRUE); purple_http_gz_free(hc->gz_stream); if (hc->request_header) g_string_free(hc->request_header, TRUE); purple_http_hc_list = g_list_delete_link(purple_http_hc_list, hc->link_global); g_hash_table_remove(purple_http_hc_by_ptr, hc); if (hc->gc) { GList *gc_list, *gc_list_new; gc_list = g_hash_table_lookup(purple_http_hc_by_gc, hc->gc); g_assert(gc_list != NULL); gc_list_new = g_list_delete_link(gc_list, hc->link_gc); if (gc_list != gc_list_new) { g_hash_table_steal(purple_http_hc_by_gc, hc->gc); if (gc_list_new) g_hash_table_insert(purple_http_hc_by_gc, hc->gc, gc_list_new); } } g_free(hc); } /* call callback and do the cleanup */ static void purple_http_connection_terminate(PurpleHttpConnection *hc) { g_return_if_fail(hc != NULL); purple_debug_misc("http", "Request %p performed %s.\n", hc, purple_http_response_is_successful(hc->response) ? "successfully" : "without success"); if (hc->callback) hc->callback(hc, hc->response, hc->user_data); purple_http_connection_free(hc); } void purple_http_conn_cancel(PurpleHttpConnection *http_conn) { if (http_conn == NULL) return; if (http_conn->is_cancelling) return; http_conn->is_cancelling = TRUE; if (purple_debug_is_verbose()) { purple_debug_misc("http", "Cancelling connection %p...\n", http_conn); } if (http_conn->response) { http_conn->response->code = 0; } _purple_http_disconnect(http_conn, FALSE); purple_http_connection_terminate(http_conn); } static void purple_http_conn_retry(PurpleHttpConnection *http_conn) { if (http_conn == NULL) return; purple_debug_info("http", "Retrying connection %p...\n", http_conn); if (http_conn->response) { http_conn->response->code = 0; } _purple_http_disconnect(http_conn, FALSE); _purple_http_reconnect(http_conn); } void purple_http_conn_cancel_all(PurpleConnection *gc) { GList *gc_list; if (purple_debug_is_verbose()) { purple_debug_misc("http", "Cancelling all running HTTP " "connections\n"); } gc_list = g_hash_table_lookup(purple_http_hc_by_gc, gc); g_hash_table_insert(purple_http_cancelling_gc, gc, GINT_TO_POINTER(1)); while (gc_list) { PurpleHttpConnection *hc = gc_list->data; gc_list = g_list_next(gc_list); purple_http_conn_cancel(hc); } g_hash_table_remove(purple_http_cancelling_gc, gc); if (NULL != g_hash_table_lookup(purple_http_hc_by_gc, gc)) purple_debug_fatal("http", "Couldn't cancel all connections " "related to gc=%p (it shouldn't happen)\n", gc); } gboolean purple_http_conn_is_running(PurpleHttpConnection *http_conn) { if (http_conn == NULL) return FALSE; return (NULL != g_hash_table_lookup(purple_http_hc_by_ptr, http_conn)); } PurpleHttpRequest * purple_http_conn_get_request(PurpleHttpConnection *http_conn) { g_return_val_if_fail(http_conn != NULL, NULL); return http_conn->request; } PurpleHttpCookieJar * purple_http_conn_get_cookie_jar( PurpleHttpConnection *http_conn) { return purple_http_request_get_cookie_jar(purple_http_conn_get_request( http_conn)); } PurpleConnection * purple_http_conn_get_purple_connection( PurpleHttpConnection *http_conn) { g_return_val_if_fail(http_conn != NULL, NULL); return http_conn->gc; } void purple_http_conn_set_progress_watcher(PurpleHttpConnection *http_conn, PurpleHttpProgressWatcher watcher, gpointer user_data, gint interval_threshold) { g_return_if_fail(http_conn != NULL); if (interval_threshold < 0) { interval_threshold = PURPLE_HTTP_PROGRESS_WATCHER_DEFAULT_INTERVAL; } http_conn->watcher = watcher; http_conn->watcher_user_data = user_data; http_conn->watcher_interval_threshold = interval_threshold; } static void purple_http_conn_notify_progress_watcher( PurpleHttpConnection *hc) { gint64 now; gboolean reading_state; int processed, total; g_return_if_fail(hc != NULL); if (hc->watcher == NULL) return; reading_state = hc->is_reading; if (reading_state) { total = hc->length_expected; processed = hc->length_got; } else { total = hc->request->contents_length; processed = hc->request_contents_written; if (total == 0) total = -1; } if (total != -1 && total < processed) { purple_debug_warning("http", "Processed too much\n"); total = processed; } now = g_get_monotonic_time(); if (hc->watcher_last_call + hc->watcher_interval_threshold > now && processed != total) { if (hc->watcher_delayed_handle) return; hc->watcher_delayed_handle = purple_timeout_add_seconds( 1 + hc->watcher_interval_threshold / 1000000, purple_http_conn_notify_progress_watcher_timeout, hc); return; } if (hc->watcher_delayed_handle) purple_timeout_remove(hc->watcher_delayed_handle); hc->watcher_delayed_handle = 0; hc->watcher_last_call = now; hc->watcher(hc, reading_state, processed, total, hc->watcher_user_data); } static gboolean purple_http_conn_notify_progress_watcher_timeout(gpointer _hc) { PurpleHttpConnection *hc = _hc; purple_http_conn_notify_progress_watcher(hc); return FALSE; } /*** Cookie jar API ***********************************************************/ static PurpleHttpCookie * purple_http_cookie_new(const gchar *value); void purple_http_cookie_free(PurpleHttpCookie *cookie); static void purple_http_cookie_jar_set_ext(PurpleHttpCookieJar *cookie_jar, const gchar *name, const gchar *value, time_t expires); static PurpleHttpCookie * purple_http_cookie_new(const gchar *value) { PurpleHttpCookie *cookie = g_new0(PurpleHttpCookie, 1); cookie->value = g_strdup(value); cookie->expires = -1; return cookie; } void purple_http_cookie_free(PurpleHttpCookie *cookie) { g_free(cookie->value); g_free(cookie); } void purple_http_cookie_jar_free(PurpleHttpCookieJar *cookie_jar); PurpleHttpCookieJar * purple_http_cookie_jar_new(void) { PurpleHttpCookieJar *cjar = g_new0(PurpleHttpCookieJar, 1); cjar->ref_count = 1; cjar->tab = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)purple_http_cookie_free); return cjar; } void purple_http_cookie_jar_free(PurpleHttpCookieJar *cookie_jar) { g_hash_table_destroy(cookie_jar->tab); g_free(cookie_jar); } void purple_http_cookie_jar_ref(PurpleHttpCookieJar *cookie_jar) { g_return_if_fail(cookie_jar != NULL); cookie_jar->ref_count++; } PurpleHttpCookieJar * purple_http_cookie_jar_unref( PurpleHttpCookieJar *cookie_jar) { if (cookie_jar == NULL) return NULL; g_return_val_if_fail(cookie_jar->ref_count > 0, NULL); cookie_jar->ref_count--; if (cookie_jar->ref_count > 0) return cookie_jar; purple_http_cookie_jar_free(cookie_jar); return NULL; } static void purple_http_cookie_jar_parse(PurpleHttpCookieJar *cookie_jar, GList *values) { values = g_list_first(values); while (values) { const gchar *cookie = values->data; const gchar *eqsign, *semicolon; gchar *name, *value; time_t expires = -1; values = g_list_next(values); eqsign = strchr(cookie, '='); semicolon = strchr(cookie, ';'); if (eqsign == NULL || eqsign == cookie || (semicolon != NULL && semicolon < eqsign)) { if (purple_debug_is_unsafe()) purple_debug_warning("http", "Invalid cookie: [%s]\n", cookie); else purple_debug_warning("http", "Invalid cookie."); continue; } name = g_strndup(cookie, eqsign - cookie); eqsign++; if (semicolon != NULL) value = g_strndup(eqsign, semicolon - eqsign); else value = g_strdup(eqsign); if (semicolon != NULL) { GMatchInfo *match_info; GRegex *re_expires = g_regex_new( /* XXX: make it static */ "expires=([a-z0-9, :]+)", G_REGEX_OPTIMIZE | G_REGEX_CASELESS, G_REGEX_MATCH_NOTEMPTY, NULL); g_regex_match(re_expires, semicolon, 0, &match_info); if (g_match_info_matches(match_info)) { gchar *expire_date = g_match_info_fetch(match_info, 1); expires = purple_http_rfc1123_to_time( expire_date); g_free(expire_date); } g_match_info_free(match_info); g_regex_unref(re_expires); } purple_http_cookie_jar_set_ext(cookie_jar, name, value, expires); g_free(name); g_free(value); } } static gchar * purple_http_cookie_jar_gen(PurpleHttpCookieJar *cookie_jar) { GHashTableIter it; gchar *key; PurpleHttpCookie *cookie; GString *str; time_t now = time(NULL); g_return_val_if_fail(cookie_jar != NULL, NULL); str = g_string_new(""); g_hash_table_iter_init(&it, cookie_jar->tab); while (g_hash_table_iter_next(&it, (gpointer*)&key, (gpointer*)&cookie)) { if (cookie->expires != -1 && cookie->expires != 0 && cookie->expires <= now) continue; g_string_append_printf(str, "%s=%s; ", key, cookie->value); } if (str->len > 0) g_string_truncate(str, str->len - 2); return g_string_free(str, FALSE); } void purple_http_cookie_jar_set(PurpleHttpCookieJar *cookie_jar, const gchar *name, const gchar *value) { gchar *escaped_name = g_strdup(purple_url_encode(name)); gchar *escaped_value = NULL; if (value) { escaped_value = g_strdup(purple_url_encode(value)); } purple_http_cookie_jar_set_ext(cookie_jar, escaped_name, escaped_value, -1); g_free(escaped_name); g_free(escaped_value); } static void purple_http_cookie_jar_set_ext(PurpleHttpCookieJar *cookie_jar, const gchar *name, const gchar *value, time_t expires) { g_return_if_fail(cookie_jar != NULL); g_return_if_fail(name != NULL); if (expires != -1 && expires != 0 && time(NULL) >= expires) value = NULL; if (value != NULL) { PurpleHttpCookie *cookie = purple_http_cookie_new(value); cookie->expires = expires; g_hash_table_insert(cookie_jar->tab, g_strdup(name), cookie); } else g_hash_table_remove(cookie_jar->tab, name); } gchar * purple_http_cookie_jar_get(PurpleHttpCookieJar *cookie_jar, const gchar *name) { PurpleHttpCookie *cookie; g_return_val_if_fail(cookie_jar != NULL, NULL); g_return_val_if_fail(name != NULL, NULL); cookie = g_hash_table_lookup(cookie_jar->tab, name); if (!cookie) return NULL; return g_strdup(purple_url_decode(cookie->value)); } gchar * purple_http_cookie_jar_dump(PurpleHttpCookieJar *cjar) { GHashTableIter it; gchar *key; PurpleHttpCookie *cookie; GString *str = g_string_new(""); g_hash_table_iter_init(&it, cjar->tab); while (g_hash_table_iter_next(&it, (gpointer*)&key, (gpointer*)&cookie)) g_string_append_printf(str, "%s: %s (expires: %" G_GINT64_FORMAT ")\n", key, cookie->value, (gint64)cookie->expires); if (str->len > 0) g_string_truncate(str, str->len - 1); return g_string_free(str, FALSE); } gboolean purple_http_cookie_jar_is_empty(PurpleHttpCookieJar *cookie_jar) { g_return_val_if_fail(cookie_jar != NULL, TRUE); return g_hash_table_size(cookie_jar->tab) == 0; } /*** HTTP Keep-Alive pool API *************************************************/ static void purple_http_keepalive_host_process_queue(PurpleHttpKeepaliveHost *host); static void purple_http_keepalive_host_free(gpointer _host) { PurpleHttpKeepaliveHost *host = _host; g_free(host->host); g_slist_free_full(host->queue, (GDestroyNotify)purple_http_keepalive_pool_request_cancel); g_slist_free_full(host->sockets, (GDestroyNotify)purple_http_socket_close_free); if (host->process_queue_timeout > 0) { purple_timeout_remove(host->process_queue_timeout); host->process_queue_timeout = 0; } g_free(host); } PurpleHttpKeepalivePool * purple_http_keepalive_pool_new(void) { PurpleHttpKeepalivePool *pool = g_new0(PurpleHttpKeepalivePool, 1); pool->ref_count = 1; pool->by_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, purple_http_keepalive_host_free); return pool; } static void purple_http_keepalive_pool_free(PurpleHttpKeepalivePool *pool) { g_return_if_fail(pool != NULL); if (pool->is_destroying) return; pool->is_destroying = TRUE; g_hash_table_destroy(pool->by_hash); g_free(pool); } void purple_http_keepalive_pool_ref(PurpleHttpKeepalivePool *pool) { g_return_if_fail(pool != NULL); pool->ref_count++; } PurpleHttpKeepalivePool * purple_http_keepalive_pool_unref(PurpleHttpKeepalivePool *pool) { if (pool == NULL) return NULL; g_return_val_if_fail(pool->ref_count > 0, NULL); pool->ref_count--; if (pool->ref_count > 0) return pool; purple_http_keepalive_pool_free(pool); return NULL; } static PurpleHttpKeepaliveRequest * purple_http_keepalive_pool_request(PurpleHttpKeepalivePool *pool, PurpleConnection *gc, const gchar *host, int port, gboolean is_ssl, PurpleSocketConnectCb cb, gpointer user_data) { PurpleHttpKeepaliveRequest *req; PurpleHttpKeepaliveHost *kahost; gchar *hash; g_return_val_if_fail(pool != NULL, NULL); g_return_val_if_fail(host != NULL, NULL); if (pool->is_destroying) { purple_debug_error("http", "pool is destroying\n"); return NULL; } hash = purple_http_socket_hash(host, port, is_ssl); kahost = g_hash_table_lookup(pool->by_hash, hash); if (kahost == NULL) { kahost = g_new0(PurpleHttpKeepaliveHost, 1); kahost->pool = pool; kahost->host = g_strdup(host); kahost->port = port; kahost->is_ssl = is_ssl; g_hash_table_insert(pool->by_hash, g_strdup(hash), kahost); } g_free(hash); req = g_new0(PurpleHttpKeepaliveRequest, 1); req->gc = gc; req->cb = cb; req->user_data = user_data; req->host = kahost; kahost->queue = g_slist_append(kahost->queue, req); purple_http_keepalive_host_process_queue(kahost); return req; } static void _purple_http_keepalive_socket_connected(PurpleSocket *ps, const gchar *error, gpointer _req) { PurpleHttpSocket *hs = NULL; PurpleHttpKeepaliveRequest *req = _req; if (ps != NULL) hs = purple_socket_get_data(ps, "hs"); if (hs != NULL) hs->use_count++; req->cb(ps, error, req->user_data); g_free(req); } static gboolean _purple_http_keepalive_host_process_queue_cb(gpointer _host) { PurpleHttpKeepaliveRequest *req; PurpleHttpKeepaliveHost *host = _host; PurpleHttpSocket *hs = NULL; GSList *it; guint sockets_count; g_return_val_if_fail(host != NULL, FALSE); host->process_queue_timeout = 0; if (host->queue == NULL) return FALSE; sockets_count = 0; it = host->sockets; while (it != NULL) { PurpleHttpSocket *hs_current = it->data; sockets_count++; if (!hs_current->is_busy) { hs = hs_current; break; } it = g_slist_next(it); } /* There are no free sockets and we cannot create another one. */ if (hs == NULL && sockets_count >= host->pool->limit_per_host && host->pool->limit_per_host > 0) { return FALSE; } req = host->queue->data; host->queue = g_slist_remove(host->queue, req); if (hs != NULL) { if (purple_debug_is_verbose()) { purple_debug_misc("http", "locking a (previously used) " "socket: %p\n", hs); } hs->is_busy = TRUE; hs->use_count++; purple_http_keepalive_host_process_queue(host); req->cb(hs->ps, NULL, req->user_data); g_free(req); return FALSE; } hs = purple_http_socket_connect_new(req->gc, req->host->host, req->host->port, req->host->is_ssl, _purple_http_keepalive_socket_connected, req); if (hs == NULL) { purple_debug_error("http", "failed creating new socket"); return FALSE; } req->hs = hs; hs->is_busy = TRUE; hs->host = host; if (purple_debug_is_verbose()) purple_debug_misc("http", "locking a (new) socket: %p\n", hs); host->sockets = g_slist_append(host->sockets, hs); return FALSE; } static void purple_http_keepalive_host_process_queue(PurpleHttpKeepaliveHost *host) { g_return_if_fail(host != NULL); if (host->process_queue_timeout > 0) return; host->process_queue_timeout = purple_timeout_add(0, _purple_http_keepalive_host_process_queue_cb, host); } static void purple_http_keepalive_pool_request_cancel(PurpleHttpKeepaliveRequest *req) { if (req == NULL) return; if (req->host != NULL) req->host->queue = g_slist_remove(req->host->queue, req); if (req->hs != NULL) { if (G_LIKELY(req->host)) { req->host->sockets = g_slist_remove(req->host->sockets, req->hs); } purple_http_socket_close_free(req->hs); /* req should already be free'd here */ } else { req->cb(NULL, _("Cancelled"), req->user_data); g_free(req); } } static void purple_http_keepalive_pool_release(PurpleHttpSocket *hs, gboolean invalidate) { PurpleHttpKeepaliveHost *host; if (hs == NULL) return; if (purple_debug_is_verbose()) purple_debug_misc("http", "releasing a socket: %p\n", hs); purple_socket_watch(hs->ps, 0, NULL, NULL); hs->is_busy = FALSE; host = hs->host; if (host == NULL) { purple_http_socket_close_free(hs); return; } if (invalidate) { host->sockets = g_slist_remove(host->sockets, hs); purple_http_socket_close_free(hs); } purple_http_keepalive_host_process_queue(host); } void purple_http_keepalive_pool_set_limit_per_host(PurpleHttpKeepalivePool *pool, guint limit) { g_return_if_fail(pool != NULL); pool->limit_per_host = limit; } guint purple_http_keepalive_pool_get_limit_per_host(PurpleHttpKeepalivePool *pool) { g_return_val_if_fail(pool != NULL, 0); return pool->limit_per_host; } /*** HTTP connection set API **************************************************/ PurpleHttpConnectionSet * purple_http_connection_set_new(void) { PurpleHttpConnectionSet *set; set = g_new0(PurpleHttpConnectionSet, 1); set->connections = g_hash_table_new(g_direct_hash, g_direct_equal); return set; } void purple_http_connection_set_destroy(PurpleHttpConnectionSet *set) { if (set == NULL) return; set->is_destroying = TRUE; while (TRUE) { GHashTableIter iter; PurpleHttpConnection *http_conn; g_hash_table_iter_init(&iter, set->connections); if (!g_hash_table_iter_next(&iter, (gpointer*)&http_conn, NULL)) break; purple_http_conn_cancel(http_conn); } g_hash_table_destroy(set->connections); g_free(set); } void purple_http_connection_set_add(PurpleHttpConnectionSet *set, PurpleHttpConnection *http_conn) { if (set->is_destroying) return; if (http_conn->connection_set == set) return; if (http_conn->connection_set != NULL) { purple_http_connection_set_remove(http_conn->connection_set, http_conn); } g_hash_table_insert(set->connections, http_conn, GINT_TO_POINTER(1)); http_conn->connection_set = set; } static void purple_http_connection_set_remove(PurpleHttpConnectionSet *set, PurpleHttpConnection *http_conn) { g_hash_table_remove(set->connections, http_conn); if (http_conn->connection_set == set) http_conn->connection_set = NULL; } /*** Request API **************************************************************/ static void purple_http_request_free(PurpleHttpRequest *request); PurpleHttpRequest * purple_http_request_new(const gchar *url) { PurpleHttpRequest *request; request = g_new0(PurpleHttpRequest, 1); request->ref_count = 1; request->url = g_strdup(url); request->headers = purple_http_headers_new(); request->cookie_jar = purple_http_cookie_jar_new(); request->keepalive_pool = purple_http_keepalive_pool_new(); request->timeout = PURPLE_HTTP_REQUEST_DEFAULT_TIMEOUT; request->max_redirects = PURPLE_HTTP_REQUEST_DEFAULT_MAX_REDIRECTS; request->http11 = TRUE; request->max_length = PURPLE_HTTP_REQUEST_DEFAULT_MAX_LENGTH; return request; } static void purple_http_request_free(PurpleHttpRequest *request) { purple_http_headers_free(request->headers); purple_http_cookie_jar_unref(request->cookie_jar); purple_http_keepalive_pool_unref(request->keepalive_pool); g_free(request->method); g_free(request->contents); g_free(request->url); g_free(request); } void purple_http_request_ref(PurpleHttpRequest *request) { g_return_if_fail(request != NULL); request->ref_count++; } PurpleHttpRequest * purple_http_request_unref(PurpleHttpRequest *request) { if (request == NULL) return NULL; g_return_val_if_fail(request->ref_count > 0, NULL); request->ref_count--; if (request->ref_count > 0) return request; purple_http_request_free(request); return NULL; } void purple_http_request_set_url(PurpleHttpRequest *request, const gchar *url) { g_return_if_fail(request != NULL); g_return_if_fail(url != NULL); g_free(request->url); request->url = g_strdup(url); } void purple_http_request_set_url_printf(PurpleHttpRequest *request, const gchar *format, ...) { va_list args; gchar *value; g_return_if_fail(request != NULL); g_return_if_fail(format != NULL); va_start(args, format); value = g_strdup_vprintf(format, args); va_end(args); purple_http_request_set_url(request, value); g_free(value); } const gchar * purple_http_request_get_url(PurpleHttpRequest *request) { g_return_val_if_fail(request != NULL, NULL); return request->url; } void purple_http_request_set_method(PurpleHttpRequest *request, const gchar *method) { g_return_if_fail(request != NULL); g_free(request->method); request->method = g_strdup(method); } const gchar * purple_http_request_get_method(PurpleHttpRequest *request) { g_return_val_if_fail(request != NULL, NULL); return request->method; } static gboolean purple_http_request_is_method(PurpleHttpRequest *request, const gchar *method) { const gchar *rmethod; g_return_val_if_fail(request != NULL, FALSE); g_return_val_if_fail(method != NULL, FALSE); rmethod = purple_http_request_get_method(request); if (rmethod == NULL) return (g_ascii_strcasecmp(method, "get") == 0); return (g_ascii_strcasecmp(method, rmethod) == 0); } void purple_http_request_set_keepalive_pool(PurpleHttpRequest *request, PurpleHttpKeepalivePool *pool) { g_return_if_fail(request != NULL); if (pool != NULL) purple_http_keepalive_pool_ref(pool); if (request->keepalive_pool != NULL) { purple_http_keepalive_pool_unref(request->keepalive_pool); request->keepalive_pool = NULL; } if (pool != NULL) request->keepalive_pool = pool; } PurpleHttpKeepalivePool * purple_http_request_get_keepalive_pool(PurpleHttpRequest *request) { g_return_val_if_fail(request != NULL, FALSE); return request->keepalive_pool; } void purple_http_request_set_contents(PurpleHttpRequest *request, const gchar *contents, int length) { g_return_if_fail(request != NULL); g_return_if_fail(length >= -1); request->contents_reader = NULL; request->contents_reader_data = NULL; g_free(request->contents); if (contents == NULL || length == 0) { request->contents = NULL; request->contents_length = 0; return; } if (length == -1) length = strlen(contents); request->contents = g_memdup(contents, length); request->contents_length = length; } void purple_http_request_set_contents_reader(PurpleHttpRequest *request, PurpleHttpContentReader reader, int contents_length, gpointer user_data) { g_return_if_fail(request != NULL); g_return_if_fail(reader != NULL); g_return_if_fail(contents_length >= -1); g_free(request->contents); request->contents = NULL; request->contents_length = contents_length; request->contents_reader = reader; request->contents_reader_data = user_data; } void purple_http_request_set_response_writer(PurpleHttpRequest *request, PurpleHttpContentWriter writer, gpointer user_data) { g_return_if_fail(request != NULL); if (writer == NULL) user_data = NULL; request->response_writer = writer; request->response_writer_data = user_data; } void purple_http_request_set_timeout(PurpleHttpRequest *request, int timeout) { g_return_if_fail(request != NULL); if (timeout < -1) timeout = -1; request->timeout = timeout; } int purple_http_request_get_timeout(PurpleHttpRequest *request) { g_return_val_if_fail(request != NULL, -1); return request->timeout; } void purple_http_request_set_max_redirects(PurpleHttpRequest *request, int max_redirects) { g_return_if_fail(request != NULL); if (max_redirects < -1) max_redirects = -1; request->max_redirects = max_redirects; } int purple_http_request_get_max_redirects(PurpleHttpRequest *request) { g_return_val_if_fail(request != NULL, -1); return request->max_redirects; } void purple_http_request_set_cookie_jar(PurpleHttpRequest *request, PurpleHttpCookieJar *cookie_jar) { g_return_if_fail(request != NULL); g_return_if_fail(cookie_jar != NULL); purple_http_cookie_jar_ref(cookie_jar); purple_http_cookie_jar_unref(request->cookie_jar); request->cookie_jar = cookie_jar; } PurpleHttpCookieJar * purple_http_request_get_cookie_jar( PurpleHttpRequest *request) { g_return_val_if_fail(request != NULL, NULL); return request->cookie_jar; } void purple_http_request_set_http11(PurpleHttpRequest *request, gboolean http11) { g_return_if_fail(request != NULL); request->http11 = http11; } gboolean purple_http_request_is_http11(PurpleHttpRequest *request) { g_return_val_if_fail(request != NULL, FALSE); return request->http11; } void purple_http_request_set_max_len(PurpleHttpRequest *request, int max_len) { g_return_if_fail(request != NULL); if (max_len < 0 || max_len > PURPLE_HTTP_REQUEST_HARD_MAX_LENGTH) max_len = PURPLE_HTTP_REQUEST_HARD_MAX_LENGTH; request->max_length = max_len; } int purple_http_request_get_max_len(PurpleHttpRequest *request) { g_return_val_if_fail(request != NULL, -1); return request->max_length; } void purple_http_request_header_set(PurpleHttpRequest *request, const gchar *key, const gchar *value) { g_return_if_fail(request != NULL); g_return_if_fail(key != NULL); purple_http_headers_remove(request->headers, key); if (value) purple_http_headers_add(request->headers, key, value); } void purple_http_request_header_set_printf(PurpleHttpRequest *request, const gchar *key, const gchar *format, ...) { va_list args; gchar *value; g_return_if_fail(request != NULL); g_return_if_fail(key != NULL); g_return_if_fail(format != NULL); va_start(args, format); value = g_strdup_vprintf(format, args); va_end(args); purple_http_request_header_set(request, key, value); g_free(value); } void purple_http_request_header_add(PurpleHttpRequest *request, const gchar *key, const gchar *value) { g_return_if_fail(request != NULL); g_return_if_fail(key != NULL); purple_http_headers_add(request->headers, key, value); } /*** HTTP response API ********************************************************/ static PurpleHttpResponse * purple_http_response_new(void) { PurpleHttpResponse *response = g_new0(PurpleHttpResponse, 1); return response; } static void purple_http_response_free(PurpleHttpResponse *response) { if (response->contents != NULL) g_string_free(response->contents, TRUE); g_free(response->error); purple_http_headers_free(response->headers); g_free(response); } gboolean purple_http_response_is_successful(PurpleHttpResponse *response) { int code; g_return_val_if_fail(response != NULL, FALSE); code = response->code; if (code <= 0) return FALSE; /* TODO: HTTP/1.1 100 Continue */ if (code / 100 == 2) return TRUE; return FALSE; } int purple_http_response_get_code(PurpleHttpResponse *response) { g_return_val_if_fail(response != NULL, 0); return response->code; } const gchar * purple_http_response_get_error(PurpleHttpResponse *response) { g_return_val_if_fail(response != NULL, NULL); if (response->error != NULL) return response->error; if (!purple_http_response_is_successful(response)) { static gchar errmsg[200]; if (response->code <= 0) { g_snprintf(errmsg, sizeof(errmsg), _("Unknown HTTP error")); } else { g_snprintf(errmsg, sizeof(errmsg), _("Invalid HTTP response code (%d)"), response->code); } return errmsg; } return NULL; } gsize purple_http_response_get_data_len(PurpleHttpResponse *response) { g_return_val_if_fail(response != NULL, 0); if (response->contents == NULL) return 0; return response->contents->len; } const gchar * purple_http_response_get_data(PurpleHttpResponse *response, size_t *len) { const gchar *ret = ""; g_return_val_if_fail(response != NULL, ""); if (response->contents != NULL) { ret = response->contents->str; if (len) *len = response->contents->len; } else { if (len) *len = 0; } return ret; } const GList * purple_http_response_get_all_headers(PurpleHttpResponse *response) { g_return_val_if_fail(response != NULL, NULL); return purple_http_headers_get_all(response->headers); } const GList * purple_http_response_get_headers_by_name( PurpleHttpResponse *response, const gchar *name) { g_return_val_if_fail(response != NULL, NULL); g_return_val_if_fail(name != NULL, NULL); return purple_http_headers_get_all_by_name(response->headers, name); } const gchar * purple_http_response_get_header(PurpleHttpResponse *response, const gchar *name) { g_return_val_if_fail(response != NULL, NULL); g_return_val_if_fail(name != NULL, NULL); return purple_http_headers_get(response->headers, name); } /*** URL functions ************************************************************/ PurpleHttpURL * purple_http_url_parse(const char *raw_url) { PurpleHttpURL *url; GMatchInfo *match_info; gchar *host_full, *tmp; g_return_val_if_fail(raw_url != NULL, NULL); if (!g_regex_match(purple_http_re_url, raw_url, 0, &match_info)) { if (purple_debug_is_verbose() && purple_debug_is_unsafe()) { purple_debug_warning("http", "Invalid URL provided: %s\n", raw_url); } return NULL; } url = g_new0(PurpleHttpURL, 1); url->protocol = g_match_info_fetch(match_info, 1); host_full = g_match_info_fetch(match_info, 2); url->path = g_match_info_fetch(match_info, 3); url->fragment = g_match_info_fetch(match_info, 4); g_match_info_free(match_info); if (g_strcmp0(url->protocol, "") == 0) { g_free(url->protocol); url->protocol = NULL; } else if (url->protocol != NULL) { tmp = url->protocol; url->protocol = g_ascii_strdown(url->protocol, -1); g_free(tmp); } if (host_full[0] == '\0') { g_free(host_full); host_full = NULL; } if (url->path[0] == '\0') { g_free(url->path); url->path = NULL; } if ((url->protocol == NULL) != (host_full == NULL)) purple_debug_warning("http", "Protocol or host not present " "(unlikely case)\n"); if (host_full) { gchar *port_str; if (!g_regex_match(purple_http_re_url_host, host_full, 0, &match_info)) { if (purple_debug_is_verbose() && purple_debug_is_unsafe()) { purple_debug_warning("http", "Invalid host provided for URL: %s\n", raw_url); } g_free(host_full); purple_http_url_free(url); return NULL; } url->username = g_match_info_fetch(match_info, 1); url->password = g_match_info_fetch(match_info, 2); url->host = g_match_info_fetch(match_info, 3); port_str = g_match_info_fetch(match_info, 4); if (port_str && port_str[0]) url->port = atoi(port_str); if (url->username[0] == '\0') { g_free(url->username); url->username = NULL; } if (url->password[0] == '\0') { g_free(url->password); url->password = NULL; } if (g_strcmp0(url->host, "") == 0) { g_free(url->host); url->host = NULL; } else if (url->host != NULL) { tmp = url->host; url->host = g_ascii_strdown(url->host, -1); g_free(tmp); } g_free(port_str); g_match_info_free(match_info); g_free(host_full); host_full = NULL; } if (url->host != NULL) { if (url->protocol == NULL) url->protocol = g_strdup("http"); if (url->port == 0 && 0 == strcmp(url->protocol, "http")) url->port = 80; if (url->port == 0 && 0 == strcmp(url->protocol, "https")) url->port = 443; if (url->path == NULL) url->path = g_strdup("/"); if (url->path[0] != '/') purple_debug_warning("http", "URL path doesn't start with slash\n"); } return url; } void purple_http_url_free(PurpleHttpURL *parsed_url) { if (parsed_url == NULL) return; g_free(parsed_url->protocol); g_free(parsed_url->username); g_free(parsed_url->password); g_free(parsed_url->host); g_free(parsed_url->path); g_free(parsed_url->fragment); g_free(parsed_url); } void purple_http_url_relative(PurpleHttpURL *base_url, PurpleHttpURL *relative_url) { g_return_if_fail(base_url != NULL); g_return_if_fail(relative_url != NULL); if (relative_url->host) { g_free(base_url->protocol); base_url->protocol = g_strdup(relative_url->protocol); g_free(base_url->username); base_url->username = g_strdup(relative_url->username); g_free(base_url->password); base_url->password = g_strdup(relative_url->password); g_free(base_url->host); base_url->host = g_strdup(relative_url->host); base_url->port = relative_url->port; g_free(base_url->path); base_url->path = NULL; } if (relative_url->path) { if (relative_url->path[0] == '/' || base_url->path == NULL) { g_free(base_url->path); base_url->path = g_strdup(relative_url->path); } else { gchar *last_slash = strrchr(base_url->path, '/'); gchar *tmp; if (last_slash == NULL) base_url->path[0] = '\0'; else last_slash[1] = '\0'; tmp = base_url->path; base_url->path = g_strconcat(base_url->path, relative_url->path, NULL); g_free(tmp); } } g_free(base_url->fragment); base_url->fragment = g_strdup(relative_url->fragment); } gchar * purple_http_url_print(PurpleHttpURL *parsed_url) { GString *url = g_string_new(""); gboolean before_host_printed = FALSE, host_printed = FALSE; gboolean port_is_default = FALSE; if (parsed_url->protocol) { g_string_append_printf(url, "%s://", parsed_url->protocol); before_host_printed = TRUE; if (parsed_url->port == 80 && 0 == strcmp(parsed_url->protocol, "http")) port_is_default = TRUE; if (parsed_url->port == 443 && 0 == strcmp(parsed_url->protocol, "https")) port_is_default = TRUE; } if (parsed_url->username || parsed_url->password) { if (parsed_url->username) g_string_append(url, parsed_url->username); g_string_append_printf(url, ":%s", parsed_url->password ? parsed_url->password : ""); g_string_append(url, "@"); before_host_printed = TRUE; } if (parsed_url->host || parsed_url->port) { if (!parsed_url->host) g_string_append_printf(url, "{???}:%d", parsed_url->port); else { g_string_append(url, parsed_url->host); if (!port_is_default) g_string_append_printf(url, ":%d", parsed_url->port); } host_printed = TRUE; } if (parsed_url->path) { if (!host_printed && before_host_printed) g_string_append(url, "{???}"); g_string_append(url, parsed_url->path); } if (parsed_url->fragment) g_string_append_printf(url, "#%s", parsed_url->fragment); return g_string_free(url, FALSE); } const gchar * purple_http_url_get_protocol(const PurpleHttpURL *parsed_url) { g_return_val_if_fail(parsed_url != NULL, NULL); return parsed_url->protocol; } const gchar * purple_http_url_get_username(const PurpleHttpURL *parsed_url) { g_return_val_if_fail(parsed_url != NULL, NULL); return parsed_url->username; } const gchar * purple_http_url_get_password(const PurpleHttpURL *parsed_url) { g_return_val_if_fail(parsed_url != NULL, NULL); return parsed_url->password; } const gchar * purple_http_url_get_host(const PurpleHttpURL *parsed_url) { g_return_val_if_fail(parsed_url != NULL, NULL); return parsed_url->host; } int purple_http_url_get_port(const PurpleHttpURL *parsed_url) { g_return_val_if_fail(parsed_url != NULL, 0); return parsed_url->port; } const gchar * purple_http_url_get_path(const PurpleHttpURL *parsed_url) { g_return_val_if_fail(parsed_url != NULL, NULL); return parsed_url->path; } const gchar * purple_http_url_get_fragment(const PurpleHttpURL *parsed_url) { g_return_val_if_fail(parsed_url != NULL, NULL); return parsed_url->fragment; } /*** HTTP Subsystem ***********************************************************/ void purple_http_init(void) { purple_http_re_url = g_regex_new("^" "(?:" /* host part beginning */ "([a-z]+)\\:/*" /* protocol */ "([^/]+)" /* username, password, host, port */ ")?" /* host part ending */ "([^#]*)" /* path */ "(?:#" "(.*)" ")?" /* fragment */ "$", G_REGEX_OPTIMIZE | G_REGEX_CASELESS, G_REGEX_MATCH_NOTEMPTY, NULL); purple_http_re_url_host = g_regex_new("^" "(?:" /* user credentials part beginning */ "([" PURPLE_HTTP_URL_CREDENTIALS_CHARS "]+)" /* username */ "(?::([" PURPLE_HTTP_URL_CREDENTIALS_CHARS "]+))" /* password */ "@)?" /* user credentials part ending */ "([a-z0-9.-]+)" /* host */ "(?::([0-9]+))?" /* port*/ "$", G_REGEX_OPTIMIZE | G_REGEX_CASELESS, G_REGEX_MATCH_NOTEMPTY, NULL); purple_http_re_rfc1123 = g_regex_new( "^[a-z]+, " /* weekday */ "([0-9]+) " /* date */ "([a-z]+) " /* month */ "([0-9]+) " /* year */ "([0-9]+:[0-9]+:[0-9]+) " /* time */ "(?:GMT|UTC)$", G_REGEX_OPTIMIZE | G_REGEX_CASELESS, G_REGEX_MATCH_NOTEMPTY, NULL); purple_http_hc_list = NULL; purple_http_hc_by_ptr = g_hash_table_new(g_direct_hash, g_direct_equal); purple_http_hc_by_gc = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)g_list_free); purple_http_cancelling_gc = g_hash_table_new(g_direct_hash, g_direct_equal); } static void purple_http_foreach_conn_cancel(gpointer _hc, gpointer user_data) { PurpleHttpConnection *hc = _hc; purple_http_conn_cancel(hc); } void purple_http_uninit(void) { g_regex_unref(purple_http_re_url); purple_http_re_url = NULL; g_regex_unref(purple_http_re_url_host); purple_http_re_url_host = NULL; g_regex_unref(purple_http_re_rfc1123); purple_http_re_rfc1123 = NULL; if (purple_http_hc_list != NULL) g_list_foreach(purple_http_hc_list, purple_http_foreach_conn_cancel, NULL); if (purple_http_hc_list != NULL || 0 != g_hash_table_size(purple_http_hc_by_ptr) || 0 != g_hash_table_size(purple_http_hc_by_gc)) purple_debug_warning("http", "Couldn't cleanup all connections.\n"); g_list_free(purple_http_hc_list); purple_http_hc_list = NULL; g_hash_table_destroy(purple_http_hc_by_gc); purple_http_hc_by_gc = NULL; g_hash_table_destroy(purple_http_hc_by_ptr); purple_http_hc_by_ptr = NULL; g_hash_table_destroy(purple_http_cancelling_gc); purple_http_cancelling_gc = NULL; }