/* * Steam Mobile Plugin for Pidgin * Copyright (C) 2012-2016 Eion Robb * * 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 3 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 "steam_connection.h" #if !PURPLE_VERSION_CHECK(3, 0, 0) #define purple_connection_error purple_connection_error_reason #endif #if !GLIB_CHECK_VERSION (2, 22, 0) #define g_hostname_is_ip_address(hostname) (g_ascii_isdigit(hostname[0]) && g_strstr_len(hostname, 4, ".")) #endif static void steam_attempt_connection(SteamConnection *); static void steam_next_connection(SteamAccount *sa); #include static gchar *steam_gunzip(const guchar *gzip_data, gssize *len_ptr) { gsize gzip_data_len = *len_ptr; z_stream zstr; int gzip_err = 0; gchar *data_buffer; gulong gzip_len = G_MAXUINT16; GString *output_string = NULL; data_buffer = g_new0(gchar, gzip_len); zstr.next_in = NULL; zstr.avail_in = 0; zstr.zalloc = Z_NULL; zstr.zfree = Z_NULL; zstr.opaque = 0; gzip_err = inflateInit2(&zstr, MAX_WBITS+32); if (gzip_err != Z_OK) { g_free(data_buffer); purple_debug_error("steam", "no built-in gzip support in zlib\n"); return NULL; } zstr.next_in = (Bytef *)gzip_data; zstr.avail_in = gzip_data_len; zstr.next_out = (Bytef *)data_buffer; zstr.avail_out = gzip_len; gzip_err = inflate(&zstr, Z_SYNC_FLUSH); if (gzip_err == Z_DATA_ERROR) { inflateEnd(&zstr); inflateInit2(&zstr, -MAX_WBITS); if (gzip_err != Z_OK) { g_free(data_buffer); purple_debug_error("steam", "Cannot decode gzip header\n"); return NULL; } zstr.next_in = (Bytef *)gzip_data; zstr.avail_in = gzip_data_len; zstr.next_out = (Bytef *)data_buffer; zstr.avail_out = gzip_len; gzip_err = inflate(&zstr, Z_SYNC_FLUSH); } output_string = g_string_new(""); while (gzip_err == Z_OK) { //append data to buffer output_string = g_string_append_len(output_string, data_buffer, gzip_len - zstr.avail_out); //reset buffer pointer zstr.next_out = (Bytef *)data_buffer; zstr.avail_out = gzip_len; gzip_err = inflate(&zstr, Z_SYNC_FLUSH); } if (gzip_err == Z_STREAM_END) { output_string = g_string_append_len(output_string, data_buffer, gzip_len - zstr.avail_out); } else { purple_debug_error("steam", "gzip inflate error\n"); } inflateEnd(&zstr); g_free(data_buffer); if (len_ptr) *len_ptr = output_string->len; return g_string_free(output_string, FALSE); } void steam_connection_close(SteamConnection *steamcon) { steamcon->sa->conns = g_slist_remove(steamcon->sa->conns, steamcon); if (steamcon->connect_data != NULL) { purple_proxy_connect_cancel(steamcon->connect_data); steamcon->connect_data = NULL; } if (steamcon->ssl_conn != NULL) { purple_ssl_close(steamcon->ssl_conn); steamcon->ssl_conn = NULL; } if (steamcon->fd >= 0) { close(steamcon->fd); steamcon->fd = -1; } if (steamcon->input_watcher > 0) { purple_input_remove(steamcon->input_watcher); steamcon->input_watcher = 0; } purple_timeout_remove(steamcon->timeout_watcher); g_free(steamcon->rx_buf); steamcon->rx_buf = NULL; steamcon->rx_len = 0; } void steam_connection_destroy(SteamConnection *steamcon) { steam_connection_close(steamcon); if (steamcon->request != NULL) g_string_free(steamcon->request, TRUE); g_free(steamcon->url); g_free(steamcon->hostname); g_free(steamcon); } static void steam_update_cookies(SteamAccount *sa, const gchar *headers) { const gchar *cookie_start; const gchar *cookie_end; gchar *cookie_name; gchar *cookie_value; int header_len; g_return_if_fail(headers != NULL); header_len = strlen(headers); /* look for the next "Set-Cookie: " */ /* grab the data up until ';' */ cookie_start = headers; while ((cookie_start = strstr(cookie_start, "\r\nSet-Cookie: ")) && (cookie_start - headers) < header_len) { cookie_start += 14; cookie_end = strchr(cookie_start, '='); cookie_name = g_strndup(cookie_start, cookie_end-cookie_start); cookie_start = cookie_end + 1; cookie_end = strchr(cookie_start, ';'); cookie_value= g_strndup(cookie_start, cookie_end-cookie_start); cookie_start = cookie_end; g_hash_table_replace(sa->cookie_table, cookie_name, cookie_value); } } static gboolean steam_connection_requeue_delay(gpointer data) { SteamConnection *steamcon = data; if (steamcon && steamcon->sa && steamcon->sa->waiting_conns) g_queue_push_head(steamcon->sa->waiting_conns, steamcon); return FALSE; } static void steam_connection_process_data(SteamConnection *steamcon) { gssize len; gchar *tmp; len = steamcon->rx_len; tmp = g_strstr_len(steamcon->rx_buf, len, "\r\n\r\n"); if (tmp == NULL) { /* This is a corner case that occurs when the connection is * prematurely closed either on the client or the server. * This can either be no data at all or a partial set of * headers. We pass along the data to be good, but don't * do any fancy massaging. In all likelihood the result will * be tossed by the connection callback func anyways */ tmp = g_strndup(steamcon->rx_buf, len); } else { tmp += 4; len -= g_strstr_len(steamcon->rx_buf, len, "\r\n\r\n") - steamcon->rx_buf + 4; tmp = g_memdup(tmp, len + 1); tmp[len] = '\0'; steamcon->rx_buf[steamcon->rx_len - len] = '\0'; steam_update_cookies(steamcon->sa, steamcon->rx_buf); if (strstr(steamcon->rx_buf, "Content-Encoding: gzip")) { /* we've received compressed gzip data, decompress */ gchar *gunzipped; gunzipped = steam_gunzip((const guchar *)tmp, &len); g_free(tmp); tmp = gunzipped; } if (strstr(steamcon->rx_buf, "429 Too Many Requests")) { g_free(steamcon->rx_buf); steamcon->rx_buf = NULL; steamcon->rx_len = 0; g_free(tmp); //We got rate-limited, try again SteamConnection *steamcon_dup = g_memdup(steamcon, sizeof(SteamConnection)); steamcon->request = NULL; steamcon->url = NULL; steamcon->hostname = NULL; purple_timeout_add_seconds(1, steam_connection_requeue_delay, steamcon_dup); return; } } g_free(steamcon->rx_buf); steamcon->rx_buf = NULL; if (steamcon->callback != NULL) { if (!len) { purple_debug_error("steam", "No data in response\n"); } else { JsonParser *parser = json_parser_new(); if (!json_parser_load_from_data(parser, tmp, len, NULL)) { if (steamcon->error_callback != NULL) { steamcon->error_callback(steamcon->sa, tmp, len, steamcon->user_data); } else { purple_debug_error("steam", "Error parsing response: %s\n", tmp); } } else { JsonNode *root = json_parser_get_root(parser); JsonObject *jsonobj = json_node_get_object(root); //purple_debug_info("steam", "Got response: %s\n", tmp); purple_debug_info("steam", "executing callback for %s\n", steamcon->url); steamcon->callback(steamcon->sa, jsonobj, steamcon->user_data); } g_object_unref(parser); } } g_free(tmp); } static void steam_fatal_connection_cb(SteamConnection *steamcon) { PurpleConnection *pc = steamcon->sa->pc; purple_debug_error("steam", "fatal connection error\n"); steam_connection_destroy(steamcon); /* We died. Do not pass Go. Do not collect $200 */ /* In all seriousness, don't attempt to call the normal callback here. * That may lead to the wrong error message being displayed */ purple_connection_error(pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, _("Server closed the connection.")); } static void steam_post_or_get_readdata_cb(gpointer data, gint source, PurpleInputCondition cond) { SteamConnection *steamcon; SteamAccount *sa; gchar buf[4096]; gssize len; steamcon = data; sa = steamcon->sa; if (steamcon->method & STEAM_METHOD_SSL) { len = purple_ssl_read(steamcon->ssl_conn, buf, sizeof(buf) - 1); } else { len = recv(steamcon->fd, buf, sizeof(buf) - 1, 0); } if (len < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { /* Try again later */ return; } if (steamcon->method & STEAM_METHOD_SSL && steamcon->rx_len > 0) { /* * This is a slightly hacky workaround for a bug in either * GNU TLS or in the SSL implementation on steam's web * servers. The sequence of events is: * 1. We attempt to read the first time and successfully read * the server's response. * 2. We attempt to read a second time and libpurple's call * to gnutls_record_recv() returns the error * GNUTLS_E_UNEXPECTED_PACKET_LENGTH, or * "A TLS packet with unexpected length was received." * * Normally the server would have closed the connection * cleanly and this second read() request would have returned * 0. Or maybe it's normal for SSL connections to be severed * in this manner? In any case, this differs from the behavior * of the standard recv() system call. */ purple_debug_warning("steam", "ssl error, but data received. attempting to continue\n"); } else { /* Try resend the request */ steamcon->retry_count++; if (steamcon->retry_count < 3) { steam_connection_close(steamcon); steamcon->request_time = time(NULL); g_queue_push_head(sa->waiting_conns, steamcon); steam_next_connection(sa); } else { steam_fatal_connection_cb(steamcon); } return; } } if (len > 0) { buf[len] = '\0'; steamcon->rx_buf = g_realloc(steamcon->rx_buf, steamcon->rx_len + len + 1); memcpy(steamcon->rx_buf + steamcon->rx_len, buf, len + 1); steamcon->rx_len += len; /* Wait for more data before processing */ return; } /* The server closed the connection, let's parse the data */ steam_connection_process_data(steamcon); steam_connection_destroy(steamcon); steam_next_connection(sa); } static void steam_post_or_get_ssl_readdata_cb (gpointer data, PurpleSslConnection *ssl, PurpleInputCondition cond) { steam_post_or_get_readdata_cb(data, -1, cond); } static void steam_post_or_get_connect_cb(gpointer data, gint source, const gchar *error_message) { SteamConnection *steamcon; gssize len; steamcon = data; steamcon->connect_data = NULL; if (error_message) { purple_debug_error("steam", "post_or_get_connect failure to %s\n", steamcon->url); purple_debug_error("steam", "post_or_get_connect_cb %s\n", error_message); steam_fatal_connection_cb(steamcon); return; } steamcon->fd = source; len = write(steamcon->fd, steamcon->request->str, steamcon->request->len); if (len != steamcon->request->len) { purple_debug_error("steam", "post_or_get_connect failed to write request\n"); steam_fatal_connection_cb(steamcon); return; } steamcon->input_watcher = purple_input_add(steamcon->fd, PURPLE_INPUT_READ, steam_post_or_get_readdata_cb, steamcon); } static void steam_post_or_get_ssl_connect_cb(gpointer data, PurpleSslConnection *ssl, PurpleInputCondition cond) { SteamConnection *steamcon; gssize len; steamcon = data; purple_debug_info("steam", "post_or_get_ssl_connect_cb\n"); len = purple_ssl_write(steamcon->ssl_conn, steamcon->request->str, steamcon->request->len); if (len != steamcon->request->len) { purple_debug_error("steam", "post_or_get_ssl_connect failed to write request\n"); steam_fatal_connection_cb(steamcon); return; } purple_ssl_input_add(steamcon->ssl_conn, steam_post_or_get_ssl_readdata_cb, steamcon); } static void steam_host_lookup_cb(GSList *hosts, gpointer data, const char *error_message) { GSList *host_lookup_list; struct sockaddr_in *addr; gchar *hostname; gchar *ip_address; SteamAccount *sa; PurpleDnsQueryData *query; /* Extract variables */ host_lookup_list = data; sa = host_lookup_list->data; host_lookup_list = g_slist_delete_link(host_lookup_list, host_lookup_list); hostname = host_lookup_list->data; host_lookup_list = g_slist_delete_link(host_lookup_list, host_lookup_list); query = host_lookup_list->data; host_lookup_list = g_slist_delete_link(host_lookup_list, host_lookup_list); /* The callback has executed, so we no longer need to keep track of * the original query. This always needs to run when the cb is * executed. */ sa->dns_queries = g_slist_remove(sa->dns_queries, query); /* Any problems, capt'n? */ if (error_message != NULL) { purple_debug_warning("steam", "Error doing host lookup: %s\n", error_message); return; } if (hosts == NULL) { purple_debug_warning("steam", "Could not resolve host name\n"); return; } /* Discard the length... */ hosts = g_slist_delete_link(hosts, hosts); /* Copy the address then free it... */ addr = hosts->data; ip_address = g_strdup(inet_ntoa(addr->sin_addr)); g_free(addr); hosts = g_slist_delete_link(hosts, hosts); /* * DNS lookups can return a list of IP addresses, but we only cache * the first one. So free the rest. */ while (hosts != NULL) { /* Discard the length... */ hosts = g_slist_delete_link(hosts, hosts); /* Free the address... */ g_free(hosts->data); hosts = g_slist_delete_link(hosts, hosts); } g_hash_table_insert(sa->hostname_ip_cache, hostname, ip_address); } static void steam_cookie_foreach_cb(gchar *cookie_name, gchar *cookie_value, GString *str) { /* TODO: Need to escape name and value? */ g_string_append_printf(str, "%s=%s;", cookie_name, cookie_value); } /** * Serialize the sa->cookie_table hash table to a string. */ gchar *steam_cookies_to_string(SteamAccount *sa) { GString *str; str = g_string_new(NULL); g_hash_table_foreach(sa->cookie_table, (GHFunc)steam_cookie_foreach_cb, str); return g_string_free(str, FALSE); } static void steam_ssl_connection_error(PurpleSslConnection *ssl, PurpleSslErrorType errortype, gpointer data) { SteamConnection *steamcon = data; SteamAccount *sa = steamcon->sa; PurpleConnection *pc = sa->pc; steamcon->ssl_conn = NULL; /* Try resend the request */ steamcon->retry_count++; if (steamcon->retry_count < 3) { steam_connection_close(steamcon); steamcon->request_time = time(NULL); g_queue_push_head(sa->waiting_conns, steamcon); steam_next_connection(sa); } else { steam_connection_destroy(steamcon); purple_connection_ssl_error(pc, errortype); } } SteamConnection * steam_post_or_get(SteamAccount *sa, SteamMethod method, const gchar *host, const gchar *url, const gchar *postdata, SteamProxyCallbackFunc callback_func, gpointer user_data, gboolean keepalive) { GString *request; gchar *cookies; SteamConnection *steamcon; gchar *real_url; gboolean is_proxy = FALSE; const gchar *user_agent; const gchar* const *languages; gchar *language_names; PurpleProxyInfo *proxy_info = NULL; gchar *proxy_auth; gchar *proxy_auth_base64; /* TODO: Fix keepalive and use it as much as possible */ keepalive = FALSE; if (host == NULL) host = "api.steampowered.com"; if (sa && sa->account) { if (purple_account_get_bool(sa->account, "use-https", FALSE)) method |= STEAM_METHOD_SSL; } if (sa && sa->account && !(method & STEAM_METHOD_SSL)) { proxy_info = purple_proxy_get_setup(sa->account); if (purple_proxy_info_get_type(proxy_info) == PURPLE_PROXY_USE_GLOBAL) proxy_info = purple_global_proxy_get_info(); if (purple_proxy_info_get_type(proxy_info) == PURPLE_PROXY_HTTP) { is_proxy = TRUE; } } if (is_proxy == TRUE) { real_url = g_strdup_printf("http://%s%s", host, url); } else { real_url = g_strdup(url); } cookies = steam_cookies_to_string(sa); user_agent = purple_account_get_string(sa->account, "user-agent", "Steam 1.2.0 / iPhone"); if (method & STEAM_METHOD_POST && !postdata) postdata = ""; /* Build the request */ request = g_string_new(NULL); g_string_append_printf(request, "%s %s HTTP/1.0\r\n", (method & STEAM_METHOD_POST) ? "POST" : "GET", real_url); if (is_proxy == FALSE) g_string_append_printf(request, "Host: %s\r\n", host); g_string_append_printf(request, "Connection: %s\r\n", (keepalive ? "Keep-Alive" : "close")); g_string_append_printf(request, "User-Agent: %s\r\n", user_agent); if (method & STEAM_METHOD_POST) { g_string_append_printf(request, "Content-Type: application/x-www-form-urlencoded\r\n"); g_string_append_printf(request, "Content-length: %zu\r\n", strlen(postdata)); } g_string_append_printf(request, "Accept: */*\r\n"); //Only use cookies for steamcommunity.com if (g_str_equal(host, "steamcommunity.com")) g_string_append_printf(request, "Cookie: %s\r\n", cookies); g_string_append_printf(request, "Accept-Encoding: gzip\r\n"); if (is_proxy == TRUE) { if (purple_proxy_info_get_username(proxy_info) && purple_proxy_info_get_password(proxy_info)) { proxy_auth = g_strdup_printf("%s:%s", purple_proxy_info_get_username(proxy_info), purple_proxy_info_get_password(proxy_info)); proxy_auth_base64 = purple_base64_encode((guchar *)proxy_auth, strlen(proxy_auth)); g_string_append_printf(request, "Proxy-Authorization: Basic %s\r\n", proxy_auth_base64); g_free(proxy_auth_base64); g_free(proxy_auth); } } /* Tell the server what language we accept, so that we get error messages in our language (rather than our IP's) */ languages = g_get_language_names(); language_names = g_strjoinv(", ", (gchar **)languages); purple_util_chrreplace(language_names, '_', '-'); g_string_append_printf(request, "Accept-Language: %s\r\n", language_names); g_free(language_names); purple_debug_info("steam", "getting url %s\n", url); g_string_append_printf(request, "\r\n"); if (method & STEAM_METHOD_POST) g_string_append_printf(request, "%s", postdata); /* If it needs to go over a SSL connection, we probably shouldn't print * it in the debug log. Without this condition a user's password is * printed in the debug log */ if (method == STEAM_METHOD_POST) purple_debug_info("steam", "sending request data:\n%s\n", postdata); g_free(cookies); steamcon = g_new0(SteamConnection, 1); steamcon->sa = sa; steamcon->url = real_url; steamcon->method = method; steamcon->hostname = g_strdup(host); steamcon->request = request; steamcon->callback = callback_func; steamcon->user_data = user_data; steamcon->fd = -1; steamcon->connection_keepalive = keepalive; steamcon->request_time = time(NULL); g_queue_push_head(sa->waiting_conns, steamcon); steam_next_connection(sa); return steamcon; } static void steam_next_connection(SteamAccount *sa) { SteamConnection *steamcon; g_return_if_fail(sa != NULL); if (!g_queue_is_empty(sa->waiting_conns)) { if(g_slist_length(sa->conns) < STEAM_MAX_CONNECTIONS) { steamcon = g_queue_pop_tail(sa->waiting_conns); steam_attempt_connection(steamcon); } } } static gboolean steam_connection_timedout(gpointer userdata) { SteamConnection *steamcon = userdata; SteamAccount *sa = steamcon->sa; /* Try resend the request */ steamcon->retry_count++; if (steamcon->retry_count < 3) { steam_connection_close(steamcon); steamcon->request_time = time(NULL); g_queue_push_head(sa->waiting_conns, steamcon); steam_next_connection(sa); } else { steam_fatal_connection_cb(steamcon); } return FALSE; } static void steam_attempt_connection(SteamConnection *steamcon) { gboolean is_proxy = FALSE; SteamAccount *sa = steamcon->sa; PurpleProxyInfo *proxy_info = NULL; if (sa && sa->account && !(steamcon->method & STEAM_METHOD_SSL)) { proxy_info = purple_proxy_get_setup(sa->account); if (purple_proxy_info_get_type(proxy_info) == PURPLE_PROXY_USE_GLOBAL) proxy_info = purple_global_proxy_get_info(); if (purple_proxy_info_get_type(proxy_info) == PURPLE_PROXY_HTTP) { is_proxy = TRUE; } } #if 0 /* Connection to attempt retries. This code doesn't work perfectly, but * remains here for future reference if needed */ if (time(NULL) - steamcon->request_time > 5) { /* We've continuously tried to remake this connection for a * bit now. It isn't happening, sadly. Time to die. */ purple_debug_error("steam", "could not connect after retries\n"); steam_fatal_connection_cb(steamcon); return; } purple_debug_info("steam", "making connection attempt\n"); /* TODO: If we're retrying the connection, consider clearing the cached * DNS value. This will require some juggling with the hostname param */ /* TODO/FIXME: This retries almost instantenously, which in some cases * runs at blinding speed. Slow it down. */ /* TODO/FIXME: this doesn't retry properly on non-ssl connections */ #endif sa->conns = g_slist_prepend(sa->conns, steamcon); /* * Do a separate DNS lookup for the given host name and cache it * for next time. * * TODO: It would be better if we did this before we call * purple_proxy_connect(), so we could re-use the result. * Or even better: Use persistent HTTP connections for servers * that we access continually. * * TODO: This cache of the hostname<-->IP address does not respect * the TTL returned by the DNS server. We should expire things * from the cache after some amount of time. */ if (!is_proxy && !(steamcon->method & STEAM_METHOD_SSL) && !g_hostname_is_ip_address(steamcon->hostname)) { /* Don't do this for proxy connections, since proxies do the DNS lookup */ gchar *host_ip; host_ip = g_hash_table_lookup(sa->hostname_ip_cache, steamcon->hostname); if (host_ip != NULL) { g_free(steamcon->hostname); steamcon->hostname = g_strdup(host_ip); } else if (sa->account && !sa->account->disconnecting) { GSList *host_lookup_list = NULL; PurpleDnsQueryData *query; host_lookup_list = g_slist_prepend( host_lookup_list, g_strdup(steamcon->hostname)); host_lookup_list = g_slist_prepend( host_lookup_list, sa); query = purple_dnsquery_a( #if PURPLE_VERSION_CHECK(3, 0, 0) steamcon->sa->account, #endif steamcon->hostname, 80, steam_host_lookup_cb, host_lookup_list); sa->dns_queries = g_slist_prepend(sa->dns_queries, query); host_lookup_list = g_slist_append(host_lookup_list, query); } } if (steamcon->method & STEAM_METHOD_SSL) { steamcon->ssl_conn = purple_ssl_connect(sa->account, steamcon->hostname, 443, steam_post_or_get_ssl_connect_cb, steam_ssl_connection_error, steamcon); } else { steamcon->connect_data = purple_proxy_connect(NULL, sa->account, steamcon->hostname, 80, steam_post_or_get_connect_cb, steamcon); } steamcon->timeout_watcher = purple_timeout_add_seconds(120, steam_connection_timedout, steamcon); return; }