summaryrefslogtreecommitdiff
path: root/src/trg-client.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/trg-client.c')
-rw-r--r--src/trg-client.c706
1 files changed, 706 insertions, 0 deletions
diff --git a/src/trg-client.c b/src/trg-client.c
new file mode 100644
index 0000000..f20677a
--- /dev/null
+++ b/src/trg-client.c
@@ -0,0 +1,706 @@
+/*
+ * transmission-remote-gtk - A GTK RPC client to Transmission
+ * Copyright (C) 2011-2013 Alan Fitton
+
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <stddef.h>
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gi18n.h>
+
+#ifdef HAVE_LIBPROXY
+#include <proxy.h>
+#endif
+
+#include <curl/curl.h>
+#include <curl/easy.h>
+
+#include "json.h"
+#include "trg-prefs.h"
+#include "protocol-constants.h"
+#include "util.h"
+#include "requests.h"
+#include "trg-client.h"
+
+/* This class manages/does quite a few things, and is passed around a lot. It:
+ *
+ * 1) Holds/inits the single TrgPrefs object for managing configuration.
+ * 2) Manages a thread pool for making requests
+ * (each thread has its own CURL client in thread local storage)
+ * 3) Holds current connection details needed by CURL clients.
+ * (session ID, username, password, URL, ssl, proxy)
+ * 4) Holds a hash table for looking up a torrent by its ID.
+ * 5) Dispatches synchronous/asyncrhonous requests and tracks failures.
+ * 6) Holds connection state, an update serial, and provides signals for
+ * connect/disconnect.
+ * 7) Provides a mutex for locking updates.
+ * 8) Holds the latest session object sent in a session-get response.
+ */
+
+G_DEFINE_TYPE(TrgClient, trg_client, G_TYPE_OBJECT)
+enum {
+ TC_SESSION_UPDATED, TC_SIGNAL_COUNT
+};
+
+static guint signals[TC_SIGNAL_COUNT] = { 0 };
+
+struct _TrgClientPrivate {
+ char *session_id;
+ gint connid;
+ guint failCount;
+ guint retries;
+ guint timeout;
+ gint64 updateSerial;
+ JsonObject *session;
+ gboolean ssl;
+ gdouble version;
+ char *url;
+ char *username;
+ char *password;
+ char *proxy;
+ GHashTable *torrentTable;
+ GThreadPool *pool;
+ TrgPrefs *prefs;
+ GPrivate *tlsKey;
+ gint configSerial;
+ GMutex *configMutex;
+ gboolean seedRatioLimited;
+ gdouble seedRatioLimit;
+};
+
+static void dispatch_async_threadfunc(trg_request * reqrsp,
+ TrgClient * tc);
+
+static void
+trg_client_get_property(GObject * object, guint property_id,
+ GValue * value, GParamSpec * pspec)
+{
+ switch (property_id) {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+trg_client_set_property(GObject * object, guint property_id,
+ const GValue * value, GParamSpec * pspec)
+{
+ switch (property_id) {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void trg_client_dispose(GObject * object)
+{
+ G_OBJECT_CLASS(trg_client_parent_class)->dispose(object);
+}
+
+static void trg_client_class_init(TrgClientClass * klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+ g_type_class_add_private(klass, sizeof(TrgClientPrivate));
+
+ object_class->get_property = trg_client_get_property;
+ object_class->set_property = trg_client_set_property;
+ object_class->dispose = trg_client_dispose;
+
+ signals[TC_SESSION_UPDATED] = g_signal_new("session-updated",
+ G_TYPE_FROM_CLASS
+ (object_class),
+ G_SIGNAL_RUN_LAST |
+ G_SIGNAL_ACTION,
+ G_STRUCT_OFFSET
+ (TrgClientClass,
+ session_updated), NULL,
+ NULL,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE, 1,
+ G_TYPE_POINTER);
+}
+
+static void trg_client_init(TrgClient * self)
+{
+ self->priv =
+ G_TYPE_INSTANCE_GET_PRIVATE(self, TRG_TYPE_CLIENT,
+ TrgClientPrivate);
+}
+
+TrgClient *trg_client_new(void)
+{
+ TrgClient *tc = g_object_new(TRG_TYPE_CLIENT, NULL);
+ TrgClientPrivate *priv = tc->priv;
+ TrgPrefs *prefs = priv->prefs = trg_prefs_new();
+
+ trg_prefs_load(prefs);
+
+ priv->configMutex = g_mutex_new();
+ priv->tlsKey = g_private_new(NULL);
+ priv->seedRatioLimited = FALSE;
+ priv->seedRatioLimit = 0.00;
+
+ priv->pool = g_thread_pool_new((GFunc) dispatch_async_threadfunc, tc,
+ DISPATCH_POOL_SIZE, TRUE, NULL);
+
+ tr_formatter_size_init(disk_K, _(disk_K_str), _(disk_M_str),
+ _(disk_G_str), _(disk_T_str));
+ tr_formatter_speed_init(speed_K, _(speed_K_str), _(speed_M_str),
+ _(speed_G_str), _(speed_T_str));
+
+ return tc;
+}
+
+const gchar *trg_client_get_version_string(TrgClient * tc)
+{
+ return session_get_version_string(tc->priv->session);
+}
+
+gdouble trg_client_get_version(TrgClient * tc)
+{
+ return tc->priv->version;
+}
+
+gint64 trg_client_get_rpc_version(TrgClient * tc)
+{
+ TrgClientPrivate *priv = tc->priv;
+ return session_get_rpc_version(priv->session);
+}
+
+void trg_client_inc_connid(TrgClient * tc)
+{
+ TrgClientPrivate *priv = tc->priv;
+ g_atomic_int_inc(&priv->connid);
+}
+
+void trg_client_set_session(TrgClient * tc, JsonObject * session)
+{
+ TrgClientPrivate *priv = tc->priv;
+
+ if (priv->session)
+ json_object_unref(priv->session);
+ else
+ priv->version = session_get_version(session);
+
+ priv->session = session;
+ json_object_ref(session);
+
+ priv->seedRatioLimit = session_get_seed_ratio_limit(session);
+ priv->seedRatioLimited = session_get_seed_ratio_limited(session);
+
+ g_signal_emit(tc, signals[TC_SESSION_UPDATED], 0, session);
+}
+
+TrgPrefs *trg_client_get_prefs(TrgClient * tc)
+{
+ return tc->priv->prefs;
+}
+
+int trg_client_populate_with_settings(TrgClient * tc)
+{
+ TrgClientPrivate *priv = tc->priv;
+ TrgPrefs *prefs = priv->prefs;
+
+ gint port;
+ gchar *host, *path;
+#ifdef HAVE_LIBPROXY
+ pxProxyFactory *pf = NULL;
+#endif
+
+ g_mutex_lock(priv->configMutex);
+
+ trg_prefs_set_connection(prefs, trg_prefs_get_profile(prefs));
+
+ g_free(priv->url);
+ priv->url = NULL;
+
+ g_free(priv->username);
+ priv->username = NULL;
+
+ g_free(priv->password);
+ priv->password = NULL;
+
+ port =
+ trg_prefs_get_int(prefs, TRG_PREFS_KEY_PORT, TRG_PREFS_CONNECTION);
+
+ host = trg_prefs_get_string(prefs, TRG_PREFS_KEY_HOSTNAME,
+ TRG_PREFS_CONNECTION);
+ path = trg_prefs_get_string(prefs, TRG_PREFS_KEY_RPC_URL_PATH, TRG_PREFS_CONNECTION);
+
+ if (!host || strlen(host) < 1) {
+ g_free(host);
+ g_mutex_unlock(priv->configMutex);
+ return TRG_NO_HOSTNAME_SET;
+ }
+#ifndef CURL_NO_SSL
+ priv->ssl = trg_prefs_get_bool(prefs, TRG_PREFS_KEY_SSL,
+ TRG_PREFS_CONNECTION);
+#else
+ priv->ssl = FALSE;
+#endif
+
+
+ priv->url = g_strdup_printf("%s://%s:%d%s",
+ priv->ssl ? HTTPS_URI_PREFIX :
+ HTTP_URI_PREFIX, host, port, path);
+ g_free(host);
+ g_free(path);
+
+ priv->username = trg_prefs_get_string(prefs, TRG_PREFS_KEY_USERNAME,
+ TRG_PREFS_CONNECTION);
+
+ priv->password = trg_prefs_get_string(prefs, TRG_PREFS_KEY_PASSWORD,
+ TRG_PREFS_CONNECTION);
+
+ g_free(priv->proxy);
+ priv->proxy = NULL;
+
+#ifdef HAVE_LIBPROXY
+ if ((pf = px_proxy_factory_new())) {
+ char **proxies = px_proxy_factory_get_proxies(pf, priv->url);
+ int i;
+
+ for (i = 0; proxies[i]; i++) {
+ if (g_str_has_prefix(proxies[i], HTTP_URI_PREFIX)
+ || g_str_has_prefix(proxies[i], HTTPS_URI_PREFIX)) {
+ g_free(priv->proxy);
+ priv->proxy = proxies[i];
+ } else {
+ g_free(proxies[i]);
+ }
+ }
+
+ g_free(proxies);
+ px_proxy_factory_free(pf);
+ }
+#endif
+
+ priv->configSerial++;
+ g_mutex_unlock(priv->configMutex);
+ return 0;
+}
+
+gchar *trg_client_get_password(TrgClient * tc)
+{
+ return tc->priv->password;
+}
+
+gchar *trg_client_get_username(TrgClient * tc)
+{
+ return tc->priv->username;
+}
+
+gchar *trg_client_get_url(TrgClient * tc)
+{
+ return tc->priv->url;
+}
+
+gchar *trg_client_get_session_id(TrgClient * tc)
+{
+ TrgClientPrivate *priv = tc->priv;
+ return priv->session_id ? g_strdup(priv->session_id) : NULL;
+}
+
+void trg_client_set_session_id(TrgClient * tc, gchar * session_id)
+{
+ TrgClientPrivate *priv = tc->priv;
+
+ g_mutex_lock(priv->configMutex);
+
+ if (priv->session_id)
+ g_free(priv->session_id);
+
+ priv->session_id = session_id;
+
+ g_mutex_unlock(priv->configMutex);
+}
+
+void trg_client_status_change(TrgClient * tc, gboolean connected)
+{
+ TrgClientPrivate *priv = tc->priv;
+
+ if (!connected) {
+ if (priv->session) {
+ json_object_unref(priv->session);
+ priv->session = NULL;
+ }
+ g_mutex_lock(priv->configMutex);
+ trg_prefs_set_connection(priv->prefs, NULL);
+ g_mutex_unlock(priv->configMutex);
+ }
+}
+
+JsonObject *trg_client_get_session(TrgClient * tc)
+{
+ return tc->priv->session;
+}
+
+void
+trg_client_thread_pool_push(TrgClient * tc, gpointer data, GError ** err)
+{
+ g_thread_pool_push(tc->priv->pool, data, err);
+}
+
+void trg_client_inc_serial(TrgClient * tc)
+{
+ TrgClientPrivate *priv = tc->priv;
+ priv->updateSerial++;
+}
+
+gint64 trg_client_get_serial(TrgClient * tc)
+{
+ return tc->priv->updateSerial;
+}
+
+#ifndef CURL_NO_SSL
+gboolean trg_client_get_ssl(TrgClient * tc)
+{
+ return tc->priv->ssl;
+}
+#endif
+
+gchar *trg_client_get_proxy(TrgClient * tc)
+{
+ return tc->priv->proxy;
+}
+
+void trg_client_set_torrent_table(TrgClient * tc, GHashTable * table)
+{
+ TrgClientPrivate *priv = tc->priv;
+ priv->torrentTable = table;
+}
+
+GHashTable *trg_client_get_torrent_table(TrgClient * tc)
+{
+ TrgClientPrivate *priv = tc->priv;
+ return priv->torrentTable;
+}
+
+gboolean trg_client_is_connected(TrgClient * tc)
+{
+ TrgClientPrivate *priv = tc->priv;
+ return priv->session != NULL;
+}
+
+void trg_client_configlock(TrgClient * tc)
+{
+ g_mutex_lock(tc->priv->configMutex);
+}
+
+guint trg_client_get_failcount(TrgClient * tc)
+{
+ return tc->priv->failCount;
+}
+
+guint trg_client_inc_failcount(TrgClient * tc)
+{
+ TrgClientPrivate *priv = tc->priv;
+ return ++(priv->failCount);
+}
+
+void trg_client_reset_failcount(TrgClient * tc)
+{
+ tc->priv->failCount = 0;
+}
+
+void trg_client_configunlock(TrgClient * tc)
+{
+ g_mutex_unlock(tc->priv->configMutex);
+}
+
+/* formerly http.c */
+
+void trg_response_free(trg_response * response)
+{
+ if (response->obj)
+ json_object_unref(response->obj);
+ g_free(response);
+}
+
+static size_t
+http_receive_callback(void *ptr, size_t size, size_t nmemb, void *data)
+{
+ size_t realsize = size * nmemb;
+ trg_response *mem = (trg_response *) data;
+
+ mem->raw = g_realloc(mem->raw, mem->size + realsize + 1);
+ if (mem->raw) {
+ memcpy(&(mem->raw[mem->size]), ptr, realsize);
+ mem->size += realsize;
+ mem->raw[mem->size] = 0;
+ }
+
+ return realsize;
+}
+
+static size_t
+header_callback(void *ptr, size_t size, size_t nmemb, void *data)
+{
+ char *header = (char *) (ptr);
+ TrgClient *tc = TRG_CLIENT(data);
+ gchar *session_id;
+
+ if (g_str_has_prefix(header, X_TRANSMISSION_SESSION_ID_HEADER_PREFIX)) {
+ char *nl;
+
+ session_id = g_strdup(header);
+ nl = strrchr(session_id, '\r');
+ if (nl)
+ *nl = '\0';
+
+ trg_client_set_session_id(tc, session_id);
+ }
+
+ return (nmemb * size);
+}
+
+static void trg_tls_update(TrgClient * tc, trg_tls * tls, gint serial)
+{
+ gchar *proxy;
+
+ curl_easy_setopt(tls->curl, CURLOPT_PASSWORD,
+ trg_client_get_password(tc));
+ curl_easy_setopt(tls->curl, CURLOPT_USERNAME,
+ trg_client_get_username(tc));
+ curl_easy_setopt(tls->curl, CURLOPT_URL, trg_client_get_url(tc));
+
+#ifndef CURL_NO_SSL
+ if (trg_client_get_ssl(tc))
+ curl_easy_setopt(tls->curl, CURLOPT_SSL_VERIFYPEER, 0);
+#endif
+
+ proxy = trg_client_get_proxy(tc);
+ if (proxy) {
+ curl_easy_setopt(tls->curl, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
+ curl_easy_setopt(tls->curl, CURLOPT_PROXY, proxy);
+ }
+
+ tls->serial = serial;
+}
+
+trg_tls *trg_tls_new(TrgClient * tc)
+{
+ trg_tls *tls = g_new0(trg_tls, 1);
+
+ tls->curl = curl_easy_init();
+ curl_easy_setopt(tls->curl, CURLOPT_USERAGENT, PACKAGE_NAME);
+ curl_easy_setopt(tls->curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
+ curl_easy_setopt(tls->curl, CURLOPT_WRITEFUNCTION,
+ &http_receive_callback);
+ curl_easy_setopt(tls->curl, CURLOPT_HEADERFUNCTION, &header_callback);
+ curl_easy_setopt(tls->curl, CURLOPT_WRITEHEADER, (void *) tc);
+
+ tls->serial = -1;
+
+ return tls;
+}
+
+static int
+trg_http_perform_inner(TrgClient * tc, gchar * reqstr,
+ trg_response * response, gboolean recurse)
+{
+ TrgClientPrivate *priv = tc->priv;
+ TrgPrefs *prefs = trg_client_get_prefs(tc);
+ gpointer threadLocalStorage = g_private_get(priv->tlsKey);
+ trg_tls *tls;
+ long httpCode = 0;
+ gchar *session_id;
+ struct curl_slist *headers = NULL;
+
+ if (!threadLocalStorage) {
+ tls = trg_tls_new(tc);
+ g_private_set(priv->tlsKey, tls);
+ } else {
+ tls = (trg_tls *) threadLocalStorage;
+ }
+
+ g_mutex_lock(priv->configMutex);
+
+ if (priv->configSerial > tls->serial)
+ trg_tls_update(tc, tls, priv->configSerial);
+
+ session_id = trg_client_get_session_id(tc);
+ if (session_id) {
+ headers = curl_slist_append(NULL, session_id);
+ curl_easy_setopt(tls->curl, CURLOPT_HTTPHEADER, headers);
+ }
+
+ curl_easy_setopt(tls->curl, CURLOPT_TIMEOUT,
+ (long) trg_prefs_get_int(prefs, TRG_PREFS_KEY_TIMEOUT,
+ TRG_PREFS_CONNECTION));
+
+ g_mutex_unlock(priv->configMutex);
+
+ response->size = 0;
+ response->raw = NULL;
+
+ curl_easy_setopt(tls->curl, CURLOPT_POSTFIELDS, reqstr);
+ curl_easy_setopt(tls->curl, CURLOPT_WRITEDATA, (void *) response);
+ response->status = curl_easy_perform(tls->curl);
+
+ if (session_id) {
+ g_free(session_id);
+ curl_slist_free_all(headers);
+ }
+
+ curl_easy_getinfo(tls->curl, CURLINFO_RESPONSE_CODE, &httpCode);
+
+ if (response->status == CURLE_OK) {
+ if (httpCode == HTTP_CONFLICT && recurse == TRUE)
+ return trg_http_perform_inner(tc, reqstr, response, FALSE);
+ else if (httpCode != HTTP_OK)
+ response->status = (-httpCode) - 100;
+ }
+
+ return response->status;
+}
+
+int trg_http_perform(TrgClient * tc, gchar * reqstr, trg_response * reqrsp)
+{
+ return trg_http_perform_inner(tc, reqstr, reqrsp, TRUE);
+}
+
+/* formerly dispatch.c */
+
+trg_response *dispatch(TrgClient * tc, JsonNode * req)
+{
+ gchar *serialized = trg_serialize(req);
+ json_node_free(req);
+#ifdef DEBUG
+ if (g_getenv("TRG_SHOW_OUTGOING"))
+ g_debug("=>(OUTgoing)=>: %s", serialized);
+#endif
+ return dispatch_str(tc, serialized);
+}
+
+trg_response *dispatch_str(TrgClient * tc, gchar * req)
+{
+ trg_response *response = g_new0(trg_response, 1);
+ GError *decode_error = NULL;
+ JsonNode *result;
+
+ trg_http_perform(tc, req, response);
+ g_free(req);
+
+ if (response->status == CURLE_OK)
+ response->obj = trg_deserialize(response, &decode_error);
+
+ g_free(response->raw);
+ response->raw = NULL;
+
+ if (response->status != CURLE_OK)
+ return response;
+
+ if (decode_error) {
+ g_error("JSON decoding error: %s", decode_error->message);
+ g_error_free(decode_error);
+ response->status = FAIL_JSON_DECODE;
+ return response;
+ }
+
+ result = json_object_get_member(response->obj, FIELD_RESULT);
+ if (!result || g_strcmp0(json_node_get_string(result), FIELD_SUCCESS))
+ response->status = FAIL_RESPONSE_UNSUCCESSFUL;
+
+ return response;
+}
+
+static void dispatch_async_threadfunc(trg_request * req, TrgClient * tc)
+{
+ TrgClientPrivate *priv = tc->priv;
+
+ trg_response *rsp;
+
+ if (req->str)
+ rsp = dispatch_str(tc, req->str);
+ else
+ rsp = dispatch(tc, req->node);
+
+ rsp->cb_data = req->cb_data;
+
+ if (req->callback && req->connid == g_atomic_int_get(&priv->connid))
+ g_idle_add(req->callback, rsp);
+ else
+ trg_response_free(rsp);
+
+ g_free(req);
+}
+
+static gboolean
+dispatch_async_common(TrgClient * tc,
+ trg_request * trg_req,
+ GSourceFunc callback, gpointer data)
+{
+ TrgClientPrivate *priv = tc->priv;
+ GError *error = NULL;
+
+ trg_req->callback = callback;
+ trg_req->cb_data = data;
+ trg_req->connid = g_atomic_int_get(&priv->connid);
+
+ trg_client_thread_pool_push(tc, trg_req, &error);
+ if (error) {
+ g_error("thread creation error: %s\n", error->message);
+ g_error_free(error);
+ g_free(trg_req);
+ return FALSE;
+ } else {
+ return TRUE;
+ }
+}
+
+gboolean
+dispatch_async(TrgClient * tc, JsonNode * req,
+ GSourceFunc callback, gpointer data)
+{
+ trg_request *trg_req = g_new0(trg_request, 1);
+ trg_req->node = req;
+
+ return dispatch_async_common(tc, trg_req, callback, data);
+}
+
+gboolean
+dispatch_async_str(TrgClient * tc, gchar * req,
+ GSourceFunc callback, gpointer data)
+{
+ trg_request *trg_req = g_new0(trg_request, 1);
+ trg_req->str = req;
+
+ return dispatch_async_common(tc, trg_req, callback, data);
+}
+
+gboolean trg_client_update_session(TrgClient * tc, GSourceFunc callback,
+ gpointer data)
+{
+ return dispatch_async(tc, session_get(), callback, data);
+}
+
+gdouble trg_client_get_seed_ratio_limit(TrgClient * tc)
+{
+ return tc->priv->seedRatioLimit;
+}
+
+gboolean trg_client_get_seed_ratio_limited(TrgClient * tc)
+{
+ return tc->priv->seedRatioLimited;
+}