]> www.infradead.org Git - pidgin-chime.git/commitdiff
Refactor the login/sign-in code.
authorIsaac Jurado <ijurado@amazon.com>
Tue, 1 May 2018 11:02:17 +0000 (13:02 +0200)
committerIsaac Jurado <ijurado@amazon.com>
Tue, 1 May 2018 14:52:28 +0000 (16:52 +0200)
To conform with the backend/frontend split of this package, the sign in code has
been refactored by separtaing the screen scrapping parts from the front
end (user interaction) parts.

The split between sign in provider types is no more, so all the login dirty code
lives together in chime/chime-signin.c.

Also, a new utility (chime-get-token) has been incorporated to ease the debug of
the sign in process, and also as an easy way to obtain a session token for Chime
bot developers.

13 files changed:
Makefile.am
chime-get-token.c [new file with mode: 0644]
chime/chime-connection-private.h
chime/chime-connection.c
chime/chime-connection.h
chime/chime-signin.c [new file with mode: 0644]
login-amazon.c [deleted file]
login-private.h [deleted file]
login-warpdrive.c [deleted file]
login.c [deleted file]
prpl/authenticate.c [new file with mode: 0644]
prpl/chime.c
prpl/chime.h

index ae329b66bb6c1a3563464f14a4f0b8c804d803ec..f7f88729126cefc4d5672c5030dc538fad06253a 100644 (file)
@@ -14,9 +14,8 @@ PROTOBUF_SRCS = protobuf/auth_message.pb-c.c protobuf/auth_message.pb-c.h \
                protobuf/rt_message.pb-c.c protobuf/rt_message.pb-c.h
 
 PRPL_SRCS =    prpl/chime.h prpl/chime.c prpl/buddy.c prpl/rooms.c prpl/chat.c \
-               prpl/messages.c prpl/conversations.c prpl/meeting.c prpl/attachments.c
-
-LOGIN_SRCS =   login.c login-amazon.c login-warpdrive.c login-private.h
+               prpl/messages.c prpl/conversations.c prpl/meeting.c prpl/attachments.c \
+               prpl/authenticate.c
 
 WEBSOCKET_SRCS = chime/chime-websocket-connection.c chime/chime-websocket-connection.h \
                chime/chime-websocket.c
@@ -32,8 +31,14 @@ CHIME_SRCS = chime/chime-connection.c chime/chime-connection.h \
                chime/chime-call-transport.c \
                chime/chime-call-screen.c chime/chime-call-screen.h \
                chime/chime-juggernaut.c \
+               chime/chime-signin.c \
                chime/chime-meeting.c chime/chime-meeting.h
 
+EXTRA_PROGRAMS = chime-get-token
+chime_get_token_SOURCES = chime-get-token.c
+chime_get_token_CFLAGS = $(SOUP_CFLAGS) $(JSON_CFLAGS)
+chime_get_token_LDADD = libchime.la
+
 noinst_LTLIBRARIES = libchime.la
 
 libchime_la_SOURCES = $(CHIME_SRCS) $(WEBSOCKET_SRCS) $(PROTOBUF_SRCS)
diff --git a/chime-get-token.c b/chime-get-token.c
new file mode 100644 (file)
index 0000000..758d052
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * Obtain a login token through the command line.
+ */
+#include <glib.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <termios.h>
+
+#include "chime/chime-connection.h"
+
+static GMainLoop *loop;
+static int status;
+
+static gchar *read_string(const char *prompt, gboolean echo)
+{
+       #define MAX_LEN 128
+       struct termios conf, save;
+       char buf[MAX_LEN], *ret;
+
+       fputs(prompt, stdout);
+       fflush(stdout);
+       if (!echo) {
+               if (tcgetattr(STDIN_FILENO, &conf) == -1)
+                       return NULL;
+               save = conf;
+               conf.c_lflag &= ~ECHO;
+               if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &conf) == -1)
+                       return NULL;
+       }
+       ret = fgets(buf, MAX_LEN, stdin);
+       if (!echo) {
+               tcsetattr(STDIN_FILENO, TCSANOW, &save);
+               fputs("\n", stdout);
+       }
+       return g_strdup(g_strchomp(ret));
+}
+
+static void authenticate(ChimeConnection *conn, gpointer state, gboolean user_required)
+{
+       gchar *user = NULL, *password;
+
+       if (user_required) {
+               user = read_string("Username: ", TRUE);
+       }
+       password = read_string("Password: ", FALSE);
+       chime_connection_authenticate(state, user, password);
+       g_free(user);
+       g_free(password);
+}
+
+static void token_acquired(ChimeConnection *conn, GParamSpec *pspec, gpointer ignored)
+{
+       printf("Session token:\t%s\n", chime_connection_get_session_token(conn));
+       g_main_loop_quit(loop);
+}
+
+static void disconnected(ChimeConnection *conn, GError *error)
+{
+       if (error) {
+               fprintf(stderr, "ERROR: %s\n", error->message);
+               status = EXIT_FAILURE;
+       } else {
+               status = EXIT_SUCCESS;
+       }
+       if (g_main_loop_is_running(loop))
+               g_main_loop_quit(loop);
+}
+
+static void connected(ChimeConnection *conn, gpointer display_name, gpointer ignored)
+{
+       printf("Disconnecting...\n");
+       chime_connection_disconnect(conn);
+}
+
+int main(int argc, char *argv[])
+{
+       ChimeConnection *conn;
+       gchar *account;
+
+       if (argc < 2)
+               account = read_string("Account e-mail: ", TRUE);
+       else
+               account = argv[1];
+
+       loop = g_main_loop_new(NULL, FALSE);
+       status = EXIT_SUCCESS;
+
+       conn = chime_connection_new(NULL, account, NULL, "foo", NULL);
+
+       g_signal_connect(conn, "authenticate",
+                        G_CALLBACK(authenticate), NULL);
+       g_signal_connect(conn, "notify::session-token",
+                        G_CALLBACK(token_acquired), NULL);
+       g_signal_connect(conn, "disconnected",
+                        G_CALLBACK(disconnected), NULL);
+       g_signal_connect(conn, "connected",
+                        G_CALLBACK(connected), NULL);
+
+       chime_connection_connect(conn);
+       g_main_loop_run(loop);
+       chime_connection_disconnect(conn);
+       g_object_unref(conn);
+       g_main_loop_unref(loop);
+       return status;
+}
index 14f2dd7a92405abddf85d0f268ac285b0857924d..ee59412c3f0c40921bfad78082aa481ac758c9f0 100644 (file)
@@ -108,6 +108,7 @@ typedef struct {
 
        /* Service config */
        JsonNode *reg_node;
+       const gchar *account_email;
        const gchar *display_name;
        const gchar *email;
 
@@ -255,7 +256,7 @@ void chime_connection_open_call(ChimeConnection *cxn, ChimeCall *call, gboolean
 gboolean chime_call_participant_audio_stats(ChimeCall *call, const gchar *profile_id, int vol, int signal_strength);
 
 
-/* login.c */
+/* chime-login.c */
 void chime_initial_login(ChimeConnection *cxn);
 
 #endif /* __CHIME_CONNECTION_PRIVATE_H__ */
index f2b89011d0e1d10df1263d6054c989ae1c839498..02eb7cd195ea2f42055144687a2afbe24b3b9d1f 100644 (file)
@@ -28,12 +28,14 @@ enum
     PROP_SESSION_TOKEN,
     PROP_DEVICE_TOKEN,
     PROP_SERVER,
+    PROP_ACCOUNT_EMAIL,
     LAST_PROP
 };
 
 static GParamSpec *props[LAST_PROP];
 
 enum {
+       AUTHENTICATE,
        CONNECTED,
        DISCONNECTED,
        NEW_CONTACT,
@@ -148,6 +150,9 @@ chime_connection_get_property(GObject    *object,
        case PROP_SERVER:
                g_value_set_string(value, priv->server);
                break;
+       case PROP_ACCOUNT_EMAIL:
+               g_value_set_string(value, priv->account_email);
+               break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
                break;
@@ -176,6 +181,9 @@ chime_connection_set_property(GObject      *object,
        case PROP_SERVER:
                priv->server = g_value_dup_string(value);
                break;
+       case PROP_ACCOUNT_EMAIL:
+               priv->account_email = g_value_dup_string(value);
+               break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
                break;
@@ -240,8 +248,22 @@ chime_connection_class_init(ChimeConnectionClass *klass)
                                    G_PARAM_CONSTRUCT_ONLY |
                                    G_PARAM_STATIC_STRINGS);
 
+       props[PROP_ACCOUNT_EMAIL] =
+               g_param_spec_string("account-email",
+                                   "account e-mail",
+                                   "account e-mail",
+                                   NULL,
+                                   G_PARAM_READWRITE |
+                                   G_PARAM_CONSTRUCT_ONLY |
+                                   G_PARAM_STATIC_STRINGS);
+
        g_object_class_install_properties(object_class, LAST_PROP, props);
 
+       signals[AUTHENTICATE] =
+               g_signal_new ("authenticate",
+                             G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_FIRST,
+                             0, NULL, NULL, NULL, G_TYPE_NONE, 2, G_TYPE_POINTER, G_TYPE_BOOLEAN);
+
        signals[CONNECTED] =
                g_signal_new ("connected",
                              G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_FIRST,
@@ -336,7 +358,7 @@ chime_connection_init(ChimeConnection *self)
 #define SIGNIN_DEFAULT "https://signin.id.ue1.app.chime.aws/"
 
 ChimeConnection *
-chime_connection_new(void *prpl_conn, const gchar *server,
+chime_connection_new(void *prpl_conn, const gchar *email, const gchar *server,
                     const gchar *device_token, const gchar *session_token)
 {
        if (!server || !*server)
@@ -344,6 +366,7 @@ chime_connection_new(void *prpl_conn, const gchar *server,
 
        return g_object_new (CHIME_TYPE_CONNECTION,
                             "purple-connection", prpl_conn,
+                            "account-email", email,
                             "server", server ? server : SIGNIN_DEFAULT,
                             "device-token", device_token,
                             "session-token", session_token,
@@ -501,7 +524,7 @@ chime_connection_connect(ChimeConnection    *self)
 
        if (!priv->session_token || !*priv->session_token) {
                priv->state = CHIME_STATE_DISCONNECTED;
-               chime_initial_login(self);
+               chime_connection_signin(self);
                return;
        }
 
index 21f668fca36969aaf0cdbc42170a74c533c096ea..1361f5f3c789669249f78be90e4620a6b846f400 100644 (file)
@@ -58,6 +58,7 @@ typedef void (*ChimeSoupMessageCallback)(ChimeConnection *cxn,
                                         gpointer cb_data);
 
 ChimeConnection *chime_connection_new                        (void *prpl_conn,
+                                                             const gchar *email,
                                                              const gchar *server,
                                                              const gchar *device_token,
                                                              const gchar *session_token);
@@ -88,6 +89,11 @@ const gchar     *chime_connection_get_session_token          (ChimeConnection  *
 void             chime_connection_set_session_token          (ChimeConnection  *self,
                                                               const gchar      *sess_tok);
 
+
+void chime_connection_signin (ChimeConnection *self);
+void chime_connection_authenticate (gpointer opaque,
+                                   const gchar *username,
+                                   const gchar *password);
 void chime_connection_log_out_async (ChimeConnection    *self,
                                     GCancellable       *cancellable,
                                     GAsyncReadyCallback callback,
diff --git a/chime/chime-signin.c b/chime/chime-signin.c
new file mode 100644 (file)
index 0000000..6c8dcba
--- /dev/null
@@ -0,0 +1,1065 @@
+/*
+ * Pidgin/libpurple Chime client plugin
+ *
+ * Copyright © 2017 Amazon.com, Inc. or its affiliates.
+ *
+ * Author: David Woodhouse <dwmw2@infradead.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ */
+
+/*
+ * The sign-in process in the official clients is handled by a web view widget,
+ * just as if the user was signin into a web application.  We don't have a fully
+ * blown embedded web browser to delegate on, therefore we need to implement
+ * some web scrapping.
+ *
+ * OVERVIEW OF THE SIGN-IN PROCESS
+ *
+ * The initial login page presents a search form with a single input field: the
+ * e-mail address.  This form is submitted by an AJAX request that expects a
+ * JSON response indicating the auth provider to use and its entry point.  Two
+ * different providers are recognized here: "amazon" and "wd" (WarpDrive).
+ *
+ * The Amazon provider is purely web based.  So by following HTTP re-directions,
+ * tracking cookies and scrapping HTML forms (with hidden inputs) is enough.
+ *
+ * The WarpDrive provider implements ActiveDirectory based authentication over
+ * the web.  Unfortunately, the final password submission is sent over GWT-RPC.
+ * A GWT-RPC message requires a number of parameters that need to be discovered
+ * by means of extra HTTP requests.  Therefore, this module includes a minimal
+ * implementation of GWT-RPC based on the following documents:
+ *
+ * https://docs.google.com/document/d/1eG0YocsYYbNAtivkLtcaiEE5IOF5u4LUol8-LL0TIKU
+ * https://blog.gdssecurity.com/labs/2009/10/8/gwt-rpc-in-a-nutshell.html
+ *
+ * Once the password has been sent (whatever the provider is), the server will
+ * return an HTML response containing the session token as a "chime://" URI.
+ * When present, it will signal the end of a successful authentication and the
+ * token will allow this plugin to talk to all the necessary services.
+ *
+ * The pillars for the success of this implementation are:
+ *
+ *   - SOUP: HTTP re-direction handling and cookie tracking
+ *   - LibXML2: HTML parsing and DOM navigation
+ *   - JSON-GLib: JSON parsing
+ *   - GLib: everything else
+ */
+
+#include <glib/gi18n.h>
+#include <libxml/HTMLparser.h>
+#include <libxml/tree.h>
+#include <libxml/xpath.h>
+
+#include "chime-connection-private.h"
+
+#define GWT_ID_REGEX  "['\"]([A-Z0-9]{30,35})['\"]"  /* Magic */
+#define WARPDRIVE_INTERFACE  "com.amazonaws.warpdrive.console.client.GalaxyInternalGWTService"
+
+/* A parsed and navigable HTML document */
+struct dom {
+       xmlDoc *document;
+       xmlXPathContext *context;
+};
+
+/* A scrapped form */
+struct form {
+       gchar *referer;
+       gchar *method;
+       gchar *action;
+       gchar *email_name;
+       gchar *password_name;
+       GHashTable *params;
+};
+
+/* Sign-in process state information */
+struct signin {
+       ChimeConnection *connection;
+       SoupSession *session;
+       gchar *email;
+       /* Amazon provider state fields */
+       struct form *form;
+       /* WarpDrive provider state fields */
+       gchar *directory;
+       gchar *client_id;
+       gchar *redirect_url;
+       gchar *region;
+       gchar *username;
+       /* GWT-RPC specific parameters */
+       SoupURI *gwt_rpc_uri;
+       gchar *gwt_module_base;
+       gchar *gwt_permutation;
+       gchar *gwt_policy;
+};
+
+static void free_dom(struct dom *d)
+{
+       if (!d) return;
+       xmlXPathFreeContext(d->context);
+       xmlFreeDoc(d->document);
+       g_free(d);
+}
+
+static void free_form(struct form *f)
+{
+       if (!f) return;
+       g_free(f->referer);
+       g_free(f->method);
+       g_free(f->action);
+       g_free(f->email_name);
+       g_free(f->password_name);
+       g_hash_table_destroy(f->params);
+       g_free(f);
+}
+
+static void free_signin(struct signin *s)
+{
+       if (!s) return;
+       g_free(s->email);
+       g_free(s->gwt_policy);
+       g_free(s->gwt_permutation);
+       g_free(s->gwt_module_base);
+       soup_uri_free(s->gwt_rpc_uri);
+       g_free(s->username);
+       g_free(s->region);
+       g_free(s->redirect_url);
+       g_free(s->client_id);
+       g_free(s->directory);
+       free_form(s->form);
+       soup_session_abort(s->session);
+       g_object_unref(s->session);
+       g_object_unref(s->connection);
+}
+
+static void fail(struct signin *state, GError *error)
+{
+       g_assert(state != NULL && error != NULL);
+
+       chime_debug("Sign-in failure: %s\n", error->message);
+       chime_connection_fail_error(state->connection, error);
+       g_error_free(error);
+       free_signin(state);
+}
+
+static void fail_bad_response(struct signin *state, const gchar *fmt, ...)
+{
+       va_list args;
+
+       va_start(args, fmt);
+       fail(state, g_error_new_valist(CHIME_ERROR, CHIME_ERROR_BAD_RESPONSE, fmt, args));
+       va_end(args);
+}
+
+static void fail_response_error(struct signin *state, const gchar *location, SoupMessage *msg)
+{
+       chime_debug("Server returned error %d %s (%s)\n", msg->status_code, msg->reason_phrase, location);
+       fail(state, g_error_new(CHIME_ERROR, CHIME_ERROR_REQUEST_FAILED,
+                               _("A request failed during sign-in")));
+}
+
+
+
+#define fail_on_response_error(msg, state)                             \
+       do {                                                            \
+               if (!SOUP_STATUS_IS_SUCCESSFUL((msg)->status_code)) {   \
+                       fail_response_error((state), G_STRLOC, (msg));  \
+                       return;                                         \
+               }                                                       \
+       } while (0)
+
+#define fail_gwt_discovery(state, ...)                                 \
+       do {                                                            \
+               chime_debug(__VA_ARGS__);                               \
+               fail_bad_response((state), _("Error during corporate authentication setup")); \
+       } while (0)
+
+static void cancel_signin(struct signin *state)
+{
+       fail(state, g_error_new(CHIME_ERROR, CHIME_ERROR_AUTH_FAILED,
+                               _("Sign-in canceled by the user")));
+}
+
+static void cancel_signin_cb(SoupSession *session, SoupMessage *msg, gpointer data)
+{
+       cancel_signin(data);
+}
+
+/*
+ * XPath helpers to simplify querying the DOM of a parsed HTML response.
+ */
+
+static gboolean xpath_exists(struct dom *dom, const gchar *fmt, ...)
+{
+       gboolean found;
+       gchar *expression;
+       va_list args;
+       xmlXPathObject *results;
+
+       if (!dom)
+               return FALSE;
+
+       va_start(args, fmt);
+       expression = g_strdup_vprintf(fmt, args);
+       va_end(args);
+       results = xmlXPathEval(BAD_CAST expression, dom->context);
+       found = results && results->type == XPATH_NODESET &&
+               results->nodesetval && results->nodesetval->nodeNr > 0;
+       xmlXPathFreeObject(results);
+       g_free(expression);
+       return found;
+}
+
+static xmlNode **xpath_nodes(struct dom *dom, guint *count, const char *fmt, ...)
+{
+       gchar *expression;
+       va_list args;
+       xmlNode **nodes;
+       xmlXPathObject *results;
+
+       if (!dom)
+               return NULL;
+
+       va_start(args, fmt);
+       expression = g_strdup_vprintf(fmt, args);
+       va_end(args);
+       results = xmlXPathEval(BAD_CAST expression, dom->context);
+       if (results && results->type == XPATH_NODESET && results->nodesetval) {
+               *count = (guint) results->nodesetval->nodeNr;
+               nodes = g_memdup(results->nodesetval->nodeTab,
+                                results->nodesetval->nodeNr * sizeof(xmlNode *));
+       } else {
+               *count = 0;
+               nodes = NULL;
+       }
+       xmlXPathFreeObject(results);
+       g_free(expression);
+       return nodes;
+}
+
+static gchar *xpath_string(struct dom *dom, const gchar *fmt, ...)
+{
+       gchar *expression, *wrapped, *value = NULL;
+       va_list args;
+       xmlXPathObject *results;
+
+       if (!dom)
+               return NULL;
+
+       va_start(args, fmt);
+       expression = g_strdup_vprintf(fmt, args);
+       va_end(args);
+       wrapped = g_strdup_printf("string(%s)", expression);
+       results = xmlXPathEval(BAD_CAST wrapped, dom->context);
+       if (results && results->type == XPATH_STRING)
+               value = g_strdup((gchar *) results->stringval);
+       xmlXPathFreeObject(results);
+       g_free(wrapped);
+       g_free(expression);
+       return value;
+}
+
+/*
+ * Convenience helper to scrap an HTML form with the necessary information for
+ * later submission.  This is something we are going to need repeatedly.
+ */
+static struct form *scrap_form(struct dom *dom, SoupURI *action_base, const gchar *form_xpath)
+{
+       gchar *action;
+       guint i, n;
+       struct form *form = NULL;
+       xmlNode **inputs;
+
+       if (!xpath_exists(dom, form_xpath)) {
+               chime_debug("XPath query returned no results: %s\n", form_xpath);
+               return form;
+       }
+
+       form = g_new0(struct form, 1);
+
+       form->referer = soup_uri_to_string(action_base, FALSE);
+       form->method = xpath_string(dom, "%s/@method", form_xpath);
+       if (form->method) {
+               for (i = 0;  form->method[i] != '\0';  i++)
+                       form->method[i] = g_ascii_toupper(form->method[i]);
+       } else {
+               form->method = g_strdup(SOUP_METHOD_GET);
+       }
+
+       action = xpath_string(dom, "%s/@action", form_xpath);
+       if (action) {
+               SoupURI *dst = soup_uri_new_with_base(action_base, action);
+               form->action = soup_uri_to_string(dst, FALSE);
+               soup_uri_free(dst);
+       } else {
+               form->action = soup_uri_to_string(action_base, FALSE);
+       }
+
+       form->email_name = xpath_string(dom, "%s//input[@type='email'][1]/@name", form_xpath);
+       form->password_name = xpath_string(dom, "%s//input[@type='password'][1]/@name", form_xpath);
+
+       form->params = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+       inputs = xpath_nodes(dom, &n, "%s//input[@type='hidden']", form_xpath);
+       for (i = 0;  i < n;  i++) {
+               gchar *name, *value;
+               xmlChar *text;
+               xmlAttr *attribute = xmlHasProp(inputs[i], BAD_CAST "name");
+               if (!attribute)
+                       continue;
+               text = xmlNodeGetContent((xmlNode *) attribute);
+               name = g_strdup((gchar *) text);  /* Avoid mixing allocators */
+               xmlFree(text);
+               attribute = xmlHasProp(inputs[i], BAD_CAST "value");
+               if (attribute) {
+                       text = xmlNodeGetContent((xmlNode *) attribute);
+                       value = g_strdup((gchar *) text);
+                       xmlFree(text);
+               } else {
+                       value = g_strdup("");
+               }
+               g_hash_table_insert(form->params, name, value);
+       }
+
+       g_free(inputs);
+       g_free(action);
+       return form;
+}
+
+static gchar *escaped(const gchar *src)
+{
+       GString *dst = g_string_new("");
+       guint i = 0;
+
+       for (i = 0;  src[i] != '\0';  i++)
+               switch (src[i]) {
+               case '\\':
+                       g_string_append(dst, "\\\\");
+                       break;
+               case '|':
+                       /* GWT escapes the pipe character with backslash exclamation */
+                       g_string_append(dst, "\\!");
+                       break;
+               default:
+                       g_string_append_c(dst, src[i]);
+               }
+       return g_string_free(dst, FALSE);
+}
+
+/*
+ * Helper function to compose a GWT-RPC request.  See the comments at the top of
+ * the file for more information about GWT-RPC reverse engineering.
+ */
+static SoupMessage *gwt_request(struct signin *state,
+                               const gchar *interface,
+                               const gchar *method,
+                               guint field_count, ...)
+{
+       GHashTable *strings = g_hash_table_new(g_str_hash, g_str_equal);
+       GHashTableIter iterator;
+       GString *body = g_string_new("7|0|");
+       SoupMessage *msg;
+       const gchar **table;
+       gpointer value, index;
+       gulong i, sc = 0;  /* sc: strings count */
+       va_list fields;
+
+       /* Populate the strings table */
+       g_hash_table_insert(strings, state->gwt_module_base, (gpointer) ++sc);
+       g_hash_table_insert(strings, state->gwt_policy, (gpointer) ++sc);
+       g_hash_table_insert(strings, (gchar *) interface, (gpointer) ++sc);
+       g_hash_table_insert(strings, (gchar *) method, (gpointer) ++sc);
+       va_start(fields, field_count);
+       for (i = 0;  i < field_count;  i++) {
+               gchar *field = va_arg(fields, gchar *);
+               if (field && !g_hash_table_contains(strings, field))
+                       g_hash_table_insert(strings, field, (gpointer) ++sc);
+       }
+       va_end(fields);
+       /* Write the strings table, sorted by table index */
+       g_string_append_printf(body, "%lu|", sc);
+       table = g_new(const gchar *, sc);
+       g_hash_table_iter_init(&iterator, strings);
+       while (g_hash_table_iter_next(&iterator, &value, &index))
+               table[((gulong) index) - 1] = value;
+       for (i = 0;  i < sc;  i++)
+               g_string_append_printf(body, "%s|", table[i]);
+       g_free(table);
+       /* Now add the request components, by their table index (NULLs are
+          converted to zeroes) */
+       g_string_append_printf(body, "%lu|", (gulong)
+                              g_hash_table_lookup(strings, state->gwt_module_base));
+       g_string_append_printf(body, "%lu|", (gulong)
+                              g_hash_table_lookup(strings, state->gwt_policy));
+       g_string_append_printf(body, "%lu|", (gulong)
+                              g_hash_table_lookup(strings, interface));
+       g_string_append_printf(body, "%lu|", (gulong)
+                              g_hash_table_lookup(strings, method));
+       g_string_append(body, "1|");  /* Argument count, only 1 supported */
+       va_start(fields, field_count);
+       for (i = 0;  i < field_count;  i++) {
+               gchar *field = va_arg(fields, gchar *);
+               if (field)
+                       g_string_append_printf(body, "%lu|", (gulong)
+                                              g_hash_table_lookup(strings, field));
+               else
+                       g_string_append(body, "0|");
+       }
+       va_end(fields);
+       /* The request body is ready, now add the headers */
+       msg = soup_message_new_from_uri(SOUP_METHOD_POST, state->gwt_rpc_uri);
+       soup_message_set_request(msg, "text/x-gwt-rpc; charset=utf-8",
+                                SOUP_MEMORY_TAKE, body->str, body->len);
+       soup_message_headers_append(msg->request_headers, "X-GWT-Module-Base",
+                                   state->gwt_module_base);
+       soup_message_headers_append(msg->request_headers, "X-GWT-Permutation",
+                                   state->gwt_permutation);
+
+       g_string_free(body, FALSE);
+       g_hash_table_destroy(strings);
+       return msg;
+}
+
+/*
+ * SoupMessage response parsing helpers.
+ */
+
+static struct dom *parse_html(SoupMessage *msg)
+{
+       GHashTable *params;
+       const gchar *ctype;
+       gchar *url = NULL;
+       struct dom *dom = NULL;
+       xmlDoc *document;
+       xmlXPathContext *context;
+
+       ctype = soup_message_headers_get_content_type(msg->response_headers, &params);
+       if (g_strcmp0(ctype, "text/html") || !msg->response_body ||
+           msg->response_body->length <= 0) {
+               chime_debug("Empty HTML response or unexpected content %s\n", ctype);
+               goto out;
+       }
+
+       url = soup_uri_to_string(soup_message_get_uri(msg), FALSE);
+       document = htmlReadMemory(msg->response_body->data,
+                                 msg->response_body->length,
+                                 url, g_hash_table_lookup(params, "charset"),
+                                 HTML_PARSE_NODEFDTD | HTML_PARSE_NOERROR |
+                                 HTML_PARSE_NOWARNING | HTML_PARSE_NONET |
+                                 HTML_PARSE_RECOVER);
+       if (!document) {
+               chime_debug("Failed to parse HTML\n");
+               goto out;
+       }
+
+       context = xmlXPathNewContext(document);
+       if (!context) {
+               chime_debug("Failed to create XPath context\n");
+               xmlFreeDoc(document);
+               goto out;
+       }
+
+       dom = g_new0(struct dom, 1);
+       dom->document = document;
+       dom->context = context;
+ out:
+       g_free(url);
+       g_hash_table_destroy(params);
+       return dom;
+}
+
+static GHashTable *parse_json(SoupMessage *msg)
+{
+       GError *error = NULL;
+       GHashTable *result = NULL;
+       GList *members, *member;
+       JsonNode *node;
+       JsonObject *object;
+       JsonParser *parser;
+       const gchar *ctype;
+
+       ctype = soup_message_headers_get_content_type(msg->response_headers, NULL);
+       if (g_strcmp0(ctype, "application/json") || !msg->response_body ||
+           msg->response_body->length <= 0) {
+               chime_debug("Empty JSON response or unexpected content %s\n", ctype);
+               return result;
+       }
+
+       parser = json_parser_new();
+       if (!json_parser_load_from_data(parser, msg->response_body->data,
+                                       msg->response_body->length, &error)) {
+               chime_debug("JSON parsing error: %s\n", error->message);
+               goto out;
+       }
+
+       node = json_parser_get_root(parser);
+       if (!JSON_NODE_HOLDS_OBJECT(node)) {
+               chime_debug("Unexpected JSON type %d\n", JSON_NODE_TYPE(node));
+               goto out;
+       }
+
+       object = json_node_get_object(node);
+       result = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+
+       members = json_object_get_members(object);
+       for (member = g_list_first(members);  member != NULL;  member = member->next) {
+               node = json_object_get_member(object, member->data);
+               if (JSON_NODE_HOLDS_VALUE(node))
+                       g_hash_table_insert(result, g_strdup(member->data),
+                                           g_strdup(json_node_get_string(node)));
+       }
+       g_list_free(members);
+ out:
+       g_error_free(error);
+       g_object_unref(parser);
+       return result;
+}
+
+static gchar *parse_regex(SoupMessage *msg, const gchar *regex, guint group)
+{
+       GMatchInfo *match;
+       GRegex *matcher;
+       gchar *text = NULL;
+
+       if (!msg->response_body || msg->response_body->length <= 0) {
+               chime_debug("Empty text response\n");
+               return text;
+       }
+
+       matcher = g_regex_new(regex, 0, 0, NULL);
+       if (g_regex_match_full(matcher, msg->response_body->data,
+                              msg->response_body->length, 0, 0, &match, NULL))
+               text = g_match_info_fetch(match, group);
+
+       g_match_info_free(match);
+       g_regex_unref(matcher);
+       return text;
+}
+
+/*
+ * Parse a GWT-RPC response, returning an array of strings, its length and the
+ * success status.  See the comments at the top of the file for more information
+ * about GWT-RPC reverse engineering.
+ */
+static gchar **parse_gwt(SoupMessage *msg, gboolean *ok, guint *count)
+{
+       GError *error = NULL;
+       JsonArray *body, *strings;
+       JsonNode *node;
+       JsonParser *parser;
+       const gchar *ctype;
+       gchar **fields = NULL;
+       guint i, length, max;
+
+       *count = 0;
+       ctype = soup_message_headers_get_content_type(msg->response_headers, NULL);
+       if (g_strcmp0(ctype, "application/json") || !msg->response_body ||
+           msg->response_body->length < 5 ||  /* "//OK" or "//EX" */
+           !g_str_has_prefix(msg->response_body->data, "//")) {
+               chime_debug("Unexpected GWT response format\n");
+               return fields;
+       }
+       *ok = !strncmp(msg->response_body->data + 2, "OK", 2);
+
+       /* Parse the JSON content */
+       parser = json_parser_new();
+       if (!json_parser_load_from_data(parser, msg->response_body->data + 4,
+                                       msg->response_body-> length - 4, &error)) {
+               chime_debug("GWT-JSON parsing error: %s\n", error->message);
+               goto out;
+       }
+       /* Get the content array */
+       node = json_parser_get_root(parser);
+       if (!JSON_NODE_HOLDS_ARRAY(node)) {
+               chime_debug("Unexpected GWT-JSON type %d\n", JSON_NODE_TYPE(node));
+               goto out;
+       }
+       body = json_node_get_array(node);
+       length = json_array_get_length(body);
+       if (length < 4) {
+               chime_debug("GWT response array length %d too short\n", length);
+               goto out;
+       }
+       /* Get the strings table */
+       length -= 3;
+       node = json_array_get_element(body, length);
+       if (!JSON_NODE_HOLDS_ARRAY(node)) {
+               chime_debug("Could not find GWT response strings table\n");
+               goto out;
+       }
+       strings = json_node_get_array(node);
+       max = json_array_get_length(strings);
+       /* Traverse the rest of the elements in reverse order, replacing the
+          indices by the (copied) string values in the result array */
+       *count = length;
+       fields = g_new0(gchar *, length + 1);
+       for (i = 0;  i < length;  i++) {
+               const gchar *value = NULL;
+               gint64 j = json_array_get_int_element(body, length - i - 1);
+               if (j > 0 && j <= max)
+                       value = json_array_get_string_element(strings, j - 1);
+               fields[i] = g_strdup(value);
+       }
+ out:
+       g_error_free(error);
+       g_object_unref(parser);
+       return fields;
+}
+
+static void amazon_prepare_signin_form(struct signin *state, struct dom *dom, SoupMessage *msg)
+{
+       if (state->form) {
+               free_form(state->form);
+               state->form = NULL;
+       }
+
+       state->form = scrap_form(dom, soup_message_get_uri(msg), "//form[@name='signIn']");
+       if (state->form && state->form->email_name)
+               g_hash_table_insert(state->form->params,
+                                   g_strdup(state->form->email_name),
+                                   g_strdup(state->email));
+}
+
+/*
+ * Soup and signal callbacks implementing the different sign-in types.  Defined
+ * in reverse order to chain them together without using forward declarations.
+ * Therefore, for better understanding of real sign-in step sequence, go to the
+ * bottom of the file and read the functions upwards.
+ */
+
+static void session_token_cb(SoupSession *session, SoupMessage *msg, gpointer data)
+{
+       gchar *token;
+       struct signin *state = data;
+
+       fail_on_response_error(msg, state);
+
+       token = parse_regex(msg, "['\"]chime://sso_sessions\\?Token=([^'\"]+)['\"]", 1);
+       if (!token) {
+               chime_debug("Could not find session token in final sign-in response\n");
+               fail_bad_response(state, _("Unable to retrieve session token"));
+               return;
+       }
+
+       chime_connection_set_session_token(state->connection, token);
+       chime_connection_connect(state->connection);
+       free_signin(state);
+       g_free(token);
+}
+
+static void amazon_signin_result_cb(SoupSession *session, SoupMessage *msg, gpointer data)
+{
+       struct dom *dom;
+       struct form *form;
+       struct signin *state = data;
+
+       fail_on_response_error(msg, state);
+
+       dom = parse_html(msg);
+       form = scrap_form(dom, soup_message_get_uri(msg), "//form[@name='consent-form']");
+       if (form) {
+               SoupMessage *next;
+
+               g_hash_table_insert(form->params, g_strdup("consentApproved"), g_strdup(""));
+               next = soup_form_request_new_from_hash(form->method, form->action, form->params);
+               soup_session_queue_message(session, next, session_token_cb, state);
+               goto out;
+       }
+
+       amazon_prepare_signin_form(state, dom, msg);
+       if (state->form) {
+               if (state->form->email_name && state->form->password_name)
+                       g_signal_emit_by_name(state->connection, "authenticate", state, FALSE);
+               else
+                       fail_bad_response(state, _("Unexpected Amazon sign-in form during retry"));
+       } else {
+               session_token_cb(session, msg, state);
+       }
+ out:
+       free_form(form);
+       free_dom(dom);
+}
+
+static void amazon_send_credentials(struct signin *state, const gchar *password)
+{
+       SoupMessage *msg;
+
+       if (!(password && *password)) {
+               g_signal_emit_by_name(state->connection, "authenticate", state, FALSE);
+               return;
+       }
+
+       g_hash_table_insert(state->form->params,
+                           g_strdup(state->form->password_name),
+                           g_strdup(password));
+       msg = soup_form_request_new_from_hash(state->form->method,
+                                             state->form->action,
+                                             state->form->params);
+       soup_message_headers_append(msg->request_headers, "Referer", state->form->referer);
+       soup_message_headers_append(msg->request_headers, "Accept-Language", "en-US,en;q=0.5");
+       soup_session_queue_message(state->session, msg, amazon_signin_result_cb, state);
+
+       free_form(state->form);
+       state->form = NULL;
+}
+
+static void amazon_signin_cb(SoupSession *session, SoupMessage *msg, gpointer data)
+{
+       struct dom *dom = NULL;
+       struct signin *state = data;
+
+       fail_on_response_error(msg, state);
+
+       dom = parse_html(msg);
+       amazon_prepare_signin_form(state, dom, msg);
+       if (!(state->form && state->form->email_name && state->form->password_name))
+               fail_bad_response(state, _("Could not find Amazon sign in form"));
+       else
+               g_signal_emit_by_name(state->connection, "authenticate", state, FALSE);
+       free_dom(dom);
+}
+
+static void wd_credentials_response_cb(SoupSession *session, SoupMessage *msg, gpointer data)
+{
+       SoupMessage *next;
+       gboolean ok;
+       gchar **response;
+       guint count;
+       struct signin *state = data;
+
+       fail_on_response_error(msg, state);
+
+       response = parse_gwt(msg, &ok, &count);
+       if (!response) {
+               fail_gwt_discovery(state, "Unable to parse authentication response\n");
+               return;
+       }
+       if (!ok) {
+               if (count > 3 && !g_strcmp0(response[3], "AuthenticationFailedException"))
+                       g_signal_emit_by_name(state->connection, "authenticate", state, TRUE);
+               else
+                       fail_bad_response(state, _("Unexpected corporate authentication failure"));
+               goto out;
+       }
+       next = soup_form_request_new(SOUP_METHOD_GET, state->redirect_url,
+                                    "organization", state->directory,
+                                    "region", state->region,
+                                    "auth_code", response[2], NULL);
+       soup_session_queue_message(session, next, session_token_cb, state);
+ out:
+       g_strfreev(response);
+}
+
+static void wd_send_credentials(struct signin *state, const gchar *user, const gchar *password)
+{
+       SoupMessage *msg;
+       gchar *safe_user, *safe_password;
+       static const gchar *type = "com.amazonaws.warpdrive.console.shared.LoginRequest_v4/3859384737";
+
+       safe_user = escaped(user);
+       safe_password = escaped(password);
+
+       msg = gwt_request(state, WARPDRIVE_INTERFACE, "authenticateUser", 11,
+                         type, type, "", "", state->client_id, "", NULL,
+                         state->directory, safe_password, "", safe_user);
+
+       soup_session_queue_message(state->session, msg, wd_credentials_response_cb, state);
+       g_free(safe_password);
+       g_free(safe_user);
+}
+
+static void gwt_region_cb(SoupSession *session, SoupMessage *msg, gpointer data)
+{
+       gboolean ok;
+       gchar **response;
+       guint count;
+       struct signin *state = data;
+
+       fail_on_response_error(msg, state);
+
+       response = parse_gwt(msg, &ok, &count);
+       if (!response) {
+               fail_gwt_discovery(state, "Region response parsed NULL\n");
+               return;
+       }
+       if (!ok) {
+               fail_gwt_discovery(state, "GWT exception during region discovery\n");
+               goto out;
+       }
+
+       state->region = g_strdup(response[count - 1]);
+       if (!state->region) {
+               fail_gwt_discovery(state, "NULL region value\n");
+               goto out;
+       }
+
+       g_signal_emit_by_name(state->connection, "authenticate", state, TRUE);
+ out:
+       g_strfreev(response);
+}
+
+static void gwt_policy_cb(SoupSession *session, SoupMessage *msg, gpointer data)
+{
+       SoupMessage *next;
+       static const gchar *type = "com.amazonaws.warpdrive.console.shared.ValidateClientRequest_v2/2136236667";
+       struct signin *state = data;
+
+       fail_on_response_error(msg, state);
+
+       state->gwt_policy = parse_regex(msg, GWT_ID_REGEX, 1);
+       if (!state->gwt_policy) {
+               fail_gwt_discovery(state, "No GWT policy found\n");
+               return ;
+       }
+
+       next = gwt_request(state, WARPDRIVE_INTERFACE, "validateClient", 8,
+                          type, type, "ONFAILURE", state->client_id,
+                          state->directory, NULL, NULL, state->redirect_url);
+
+       soup_session_queue_message(session, next, gwt_region_cb, state);
+}
+
+static void gwt_entry_point_cb(SoupSession *session, SoupMessage *msg, gpointer data)
+{
+       SoupMessage *next;
+       SoupURI *base, *destination;
+       gchar *policy_path;
+       struct signin *state = data;
+
+       fail_on_response_error(msg, state);
+
+       state->gwt_permutation = parse_regex(msg, GWT_ID_REGEX, 1);
+       if (!state->gwt_permutation) {
+               fail_gwt_discovery(state, "No GWT permutation found\n");
+               return;
+       }
+
+       policy_path = g_strdup_printf("deferredjs/%s/5.cache.js",
+                                     state->gwt_permutation);
+       base = soup_uri_new(state->gwt_module_base);
+       destination = soup_uri_new_with_base(base, policy_path);
+
+       next = soup_message_new_from_uri(SOUP_METHOD_GET, destination);
+       soup_session_queue_message(session, next, gwt_policy_cb, state);
+
+       soup_uri_free(destination);
+       soup_uri_free(base);
+       g_free(policy_path);
+}
+
+/*
+ * Initial WD sign in page scrapping.
+ *
+ * Ironically, most of the relevant data coming from this response is placed in
+ * the GET parameters (both from the initial URL and the redirection).  From the
+ * HTML body we only want the location of the GWT bootstrapping Javascript code.
+ */
+static void wd_signin_cb(SoupSession *session, SoupMessage *msg, gpointer data)
+{
+       GHashTable *params;
+       SoupMessage *next;
+       SoupURI *initial, *base;
+       gchar *sep, *js = NULL;
+       struct dom *dom = NULL;
+       struct signin *state = data;
+
+       fail_on_response_error(msg, state);
+
+       initial = soup_message_get_first_party(msg);
+       params = soup_form_decode(soup_uri_get_query(initial));
+       state->directory = g_strdup(g_hash_table_lookup(params, "directory"));
+       if (!state->directory) {
+               fail_gwt_discovery(state, "Directory identifier not found\n");
+               goto out;
+       }
+
+       g_hash_table_destroy(params);  /* Reuse the variable */
+       base = soup_message_get_uri(msg);
+       params = soup_form_decode(soup_uri_get_query(base));
+       state->client_id = g_strdup(g_hash_table_lookup(params, "client_id"));
+       state->redirect_url = g_strdup(g_hash_table_lookup(params, "redirect_uri"));
+       if (!(state->client_id && state->redirect_url)) {
+               fail_gwt_discovery(state, "Client ID or callback missing\n");
+               goto out;
+       }
+       state->gwt_rpc_uri = soup_uri_new_with_base(base, "WarpDriveLogin/GalaxyInternalService");
+
+       dom = parse_html(msg);
+       js = xpath_string(dom, "//script[contains(@src, '/WarpDriveLogin/')][1]/@src");
+       if (!(dom && js)) {
+               fail_gwt_discovery(state, "JS bootstrap URL not found\n");
+               goto out;
+       }
+       sep = strrchr(js, '/');
+       state->gwt_module_base = g_strndup(js, (sep - js) + 1);
+
+       next = soup_message_new(SOUP_METHOD_GET, js);
+       soup_session_queue_message(session, next, gwt_entry_point_cb, state);
+ out:
+       g_free(js);
+       free_dom(dom);
+       g_hash_table_destroy(params);
+}
+
+static void signin_search_result_cb(SoupSession *session, SoupMessage *msg, gpointer data)
+{
+       GHashTable *provider_info;
+       SoupMessage *next;
+       SoupSessionCallback handler;
+       SoupURI *destination;
+       gchar *type, *path;
+       struct signin *state = data;
+
+       if (msg->status_code == 400) {
+               /* This is a known quirk */
+               fail_bad_response(state, _("Invalid e-mail address <%s>"), state->email);
+               return;
+       }
+
+       fail_on_response_error(msg, state);
+
+       provider_info = parse_json(msg);
+       if (!provider_info) {
+               fail_bad_response(state, _("Error searching for sign-in provider"));
+               return;
+       }
+
+       type = g_hash_table_lookup(provider_info, "provider");
+       if (!g_strcmp0(type, "amazon")) {
+               handler = amazon_signin_cb;
+       } else if (!g_strcmp0(type, "wd")) {
+               handler = wd_signin_cb;
+       } else {
+               chime_debug("Unrecognized sign-in provider %s\n", type);
+               fail_bad_response(state, _("Unknown sign-in provider"));
+               goto out;
+       }
+
+       path = g_hash_table_lookup(provider_info, "path");
+       if (!path) {
+               chime_debug("Server did not provide a path\n");
+               fail_bad_response(state, _("Incomplete provider response"));
+               goto out;
+       }
+
+       destination = soup_uri_new_with_base(soup_message_get_uri(msg), path);
+       next = soup_message_new_from_uri(SOUP_METHOD_GET, destination);
+       soup_message_set_first_party(next, destination);
+       soup_session_queue_message(session, next, handler, state);
+       soup_uri_free(destination);
+ out:
+       g_hash_table_destroy(provider_info);
+}
+
+static void signin_page_cb(SoupSession *session, SoupMessage *msg, gpointer data)
+{
+       SoupMessage *next;
+       struct signin *state = data;
+       struct dom *dom = NULL;
+       struct form *form = NULL;
+
+       fail_on_response_error(msg, state);
+
+       dom = parse_html(msg);
+       form = scrap_form(dom, soup_message_get_uri(msg), "//form[@id='picker_email']");
+       if (!(form && form->email_name)) {
+               fail_bad_response(state, _("Error initiating sign in"));
+               goto out;
+       }
+
+       g_hash_table_insert(form->params, g_strdup(form->email_name),
+                           g_strdup(state->email));
+       next = soup_form_request_new_from_hash(form->method, form->action, form->params);
+       soup_session_queue_message(session, next, signin_search_result_cb, state);
+ out:
+       free_form(form);
+       free_dom(dom);
+}
+
+/*
+ * Sign-in process entry point.
+ *
+ * This is where the plugin initiates the authentication process.  Control is
+ * transferred to this module until a connection is canceled or restarted once
+ * we have the session token.
+ *
+ * A new, independent, SoupSession is created to keep track of the massive
+ * amount of cookies only until we obtain the authentication token.
+ */
+void chime_connection_signin(ChimeConnection *self)
+{
+       SoupMessage *msg;
+       gchar *server;
+       guint signal_id;
+       gulong handler;
+       struct signin *state;
+
+       g_return_if_fail(CHIME_IS_CONNECTION(self));
+
+       /* Make sure the "authenticate" signal is connected */
+       signal_id = g_signal_lookup("authenticate", G_OBJECT_TYPE(self));
+       g_assert(signal_id != 0);
+       handler = g_signal_handler_find(self, G_SIGNAL_MATCH_ID, signal_id, 0,
+                                       NULL, NULL, NULL);
+       if (handler == 0 || !g_signal_handler_is_connected(self, handler)) {
+               chime_debug("Signal \"authenticate\" must be connected to complete sign-in\n");
+               chime_connection_fail(self, CHIME_ERROR_AUTH_FAILED, _("Internal API error"));
+               return;
+       }
+
+       state = g_new0(struct signin, 1);
+       state->connection = g_object_ref(self);
+       state->session = soup_session_new_with_options(SOUP_SESSION_ADD_FEATURE_BY_TYPE,
+                                                      SOUP_TYPE_COOKIE_JAR,
+                                                      SOUP_SESSION_USER_AGENT,
+                                                      "libchime " PACKAGE_VERSION " ",
+                                                      NULL);
+
+       g_object_get(self, "account-email", &state->email, NULL);
+       if (!(state->email && *state->email)) {
+               chime_debug("The ChimeConnection object does not indicate an account name\n");
+               fail(state, g_error_new(CHIME_ERROR, CHIME_ERROR_AUTH_FAILED,
+                                       _("Internal API error")));
+               return;
+       }
+
+       if (getenv("CHIME_DEBUG") && atoi(getenv("CHIME_DEBUG")) > 1) {
+               SoupLogger *l = soup_logger_new(SOUP_LOGGER_LOG_BODY, -1);
+               soup_session_add_feature(state->session, SOUP_SESSION_FEATURE(l));
+               g_object_unref(l);
+       }
+
+       g_object_get(self, "server", &server, NULL);
+       msg = soup_message_new(SOUP_METHOD_GET, server);
+       soup_session_queue_message(state->session, msg, signin_page_cb, state);
+       g_free(server);
+}
+
+/*
+ * Credentials submission function.
+ *
+ * Because authentication needs interaction, the process hands control to the
+ * client in order query the user.  Once the credentials have been collected,
+ * the client should return the control back to the authentication process using
+ * this function.
+ *
+ * If the credentials are NULL, it is interpreted as an error and the sign-in
+ * procedure is canceled.
+ */
+void chime_connection_authenticate(gpointer opaque, const gchar *user, const gchar *password)
+{
+       struct signin *state = opaque;
+       g_assert(opaque != NULL);
+
+       if (state->region && user && *user && password && *password)
+               wd_send_credentials(state, user, password);
+       else if (state->form && password && *password)
+               amazon_send_credentials(state, password);
+       else
+               cancel_signin(state);
+}
diff --git a/login-amazon.c b/login-amazon.c
deleted file mode 100644 (file)
index ebf51a1..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Pidgin/libpurple Chime client plugin
- *
- * Copyright © 2017 Amazon.com, Inc. or its affiliates.
- *
- * Author: David Woodhouse <dwmw2@infradead.org>
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * version 2.1, as published by the Free Software Foundation.
- *
- * 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
- * Lesser General Public License for more details.
- */
-
-#include <glib/gi18n.h>
-#include <request.h>
-
-#include "login-private.h"
-
-#define SIGN_IN_FORM  "//form[@name='signIn']"
-#define CONSENT_FORM  "//form[@name='consent-form']"
-#define PASS_FIELD  "password"
-#define PASS_TITLE  _("Amazon Login")
-#define PASS_LABEL  _("Please enter the password for <%s>")
-#define PASS_FAIL   _("Authentication failed")
-#define PASS_OK  _("Sign In")
-#define PASS_CANCEL  _("Cancel")
-
-struct login_amzn {
-       struct login b;  /* Base */
-       struct login_form *form;
-};
-
-/* Break dependency loop */
-static void request_credentials(struct login_amzn *state, gboolean retry);
-
-static void clear_form(struct login_amzn *state)
-{
-       g_return_if_fail(state != NULL && state->form != NULL);
-       login_free_form(state->form);
-}
-
-static void login_result_cb(SoupSession *session, SoupMessage *msg, gpointer data)
-{
-       struct login_amzn *state = data;
-       struct login_form *form;
-
-       login_fail_on_error(msg, state);
-
-       /* Here the same HTML document is parsed several times, but this is
-          better than having a more complicated private API for the sake of
-          reducing CPU usage */
-       form = chime_login_parse_form(msg, CONSENT_FORM);
-       if (form) {
-               SoupMessage *next;
-
-               /* It appears that this is the first login ever, so the server
-                  has presented a consent form;  we obviously say "yes" */
-               g_hash_table_insert(form->params, g_strdup("consentApproved"), g_strdup(""));
-               next = soup_form_request_new_from_hash(form->method,
-                                                      form->action,
-                                                      form->params);
-
-               soup_session_queue_message(login_session(state), next, login_result_cb, state);
-
-               login_free_form(form);
-               return;
-       }
-
-       form = chime_login_parse_form(msg, SIGN_IN_FORM);
-       if (form) {
-               if (!(form->email_name && form->password_name)) {
-                       chime_login_bad_response(state, _("Could not find Amazon login form"));
-                       return;
-               }
-               /* Authentication failed */
-               g_hash_table_insert(form->params,
-                                   g_strdup(form->email_name),
-                                   g_strdup(login_account_email(state)));
-               state->form = form;
-               request_credentials(state, TRUE);
-               return;
-       }
-
-       chime_login_token_cb(session, msg, state);
-}
-
-static void send_credentials(struct login_amzn *state, const gchar *password)
-{
-       SoupMessage *msg;
-
-       if (!(password && *password)) {
-               request_credentials(state, TRUE);
-               return;
-       }
-
-       g_hash_table_insert(state->form->params,
-                           g_strdup(state->form->password_name),
-                           g_strdup(password));
-
-       msg = soup_form_request_new_from_hash(state->form->method,
-                                             state->form->action,
-                                             state->form->params);
-       soup_session_queue_message(login_session(state), msg, login_result_cb, state);
-
-       clear_form(state);
-}
-
-static void gather_credentials_and_send(struct login_amzn *state, PurpleRequestFields *fields)
-{
-       send_credentials(state, purple_request_fields_get_string(fields, PASS_FIELD));
-}
-
-static void request_credentials_with_fields(struct login_amzn *state, gboolean retry)
-{
-       PurpleRequestField *password;
-       PurpleRequestFieldGroup *group;
-       PurpleRequestFields *fields;
-       gchar *text;
-
-       fields = purple_request_fields_new();
-       group = purple_request_field_group_new(NULL);
-
-       password = purple_request_field_string_new(PASS_FIELD, _("Password"), NULL, FALSE);
-       purple_request_field_string_set_masked(password, TRUE);
-       purple_request_field_set_required(password, TRUE);
-       purple_request_field_group_add_field(group, password);
-
-       purple_request_fields_add_group(fields, group);
-       text = g_strdup_printf(PASS_LABEL, login_account_email(state));
-
-       purple_request_fields(login_connection(state)->prpl_conn,
-                             PASS_TITLE, text, retry ? PASS_FAIL : NULL, fields,
-                             PASS_OK, G_CALLBACK(gather_credentials_and_send),
-                             PASS_CANCEL, G_CALLBACK(chime_login_cancel_ui),
-                             login_account(state), NULL, NULL, state);
-}
-
-static void request_credentials_with_input(struct login_amzn *state, gboolean retry)
-{
-       gchar *text;
-
-       text = g_strdup_printf(PASS_LABEL, login_account_email(state));
-
-       purple_request_input(login_connection(state)->prpl_conn,
-                            PASS_TITLE, text, retry ? PASS_FAIL : NULL, NULL,
-                            FALSE, TRUE, (gchar *) "password",
-                            PASS_OK, G_CALLBACK(send_credentials),
-                            PASS_CANCEL, G_CALLBACK(chime_login_cancel_ui),
-                            login_account(state), NULL, NULL, state);
-}
-
-static void request_credentials(struct login_amzn *state, gboolean retry)
-{
-       /* When loging in with Amazon, we only request a password.  Therefore we
-          may only use request_input.  However, request_fields provides a
-          better user experience, so we still prefer it */
-       if (purple_request_get_ui_ops()->request_fields)
-               request_credentials_with_fields(state, retry);
-       else
-               request_credentials_with_input(state, retry);
-}
-
-void chime_login_amazon(SoupSession *session, SoupMessage *msg, gpointer data)
-{
-       struct login_amzn *state;
-
-       login_fail_on_error(msg, data);
-       state = chime_login_extend_state(data, sizeof(struct login_amzn),
-                                        (GDestroyNotify) clear_form);
-
-       state->form = chime_login_parse_form(msg, SIGN_IN_FORM);
-       if (!(state->form && state->form->email_name && state->form->password_name)) {
-               chime_login_bad_response(state, _("Could not find Amazon login form"));
-               return;
-       }
-
-       g_hash_table_insert(state->form->params, g_strdup(state->form->email_name),
-                           g_strdup(login_account_email(state)));
-       request_credentials(state, FALSE);
-}
diff --git a/login-private.h b/login-private.h
deleted file mode 100644 (file)
index f639e99..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Pidgin/libpurple Chime client plugin
- *
- * Copyright © 2017 Amazon.com, Inc. or its affiliates.
- *
- * Author: David Woodhouse <dwmw2@infradead.org>
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * version 2.1, as published by the Free Software Foundation.
- *
- * 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
- * Lesser General Public License for more details.
- */
-
-#ifndef __CHIME_LOGIN_PRIVATE_H__
-#define __CHIME_LOGIN_PRIVATE_H__
-
-#include "chime.h"
-
-struct login {
-       SoupSession *session;
-       ChimeConnection *connection;
-       GDestroyNotify release_sub;
-};
-
-struct login_form {
-       gchar *method;
-       gchar *action;
-       gchar *email_name;
-       gchar *password_name;
-       GHashTable *params;
-       GDestroyNotify release;
-};
-
-gpointer chime_login_extend_state(gpointer data, gsize size, GDestroyNotify destroy);
-void chime_login_free_state(struct login *state);
-
-void chime_login_cancel_ui(struct login *state, gpointer foo);
-void chime_login_cancel_cb(SoupSession *session, SoupMessage *msg, gpointer data);
-void chime_login_token_cb(SoupSession *session, SoupMessage *msg, gpointer data);
-
-void chime_login_request_failed(gpointer state, const gchar *location, SoupMessage *msg);
-void chime_login_bad_response(gpointer state, const gchar *fmt, ...);
-
-
-gchar *chime_login_parse_regex(SoupMessage *msg, const gchar *regex, guint group);
-gchar **chime_login_parse_xpaths(SoupMessage *msg, guint count, ...);
-GHashTable *chime_login_parse_json_object(SoupMessage *msg);
-struct login_form *chime_login_parse_form(SoupMessage *msg, const gchar *form_xpath);
-
-/* Each provider is implemented in a separate .c file */
-void chime_login_amazon(SoupSession *session, SoupMessage *msg, gpointer data);
-void chime_login_warpdrive(SoupSession *sessioin, SoupMessage *msg, gpointer data);
-
-#define login_session(state)                   \
-       (((struct login *) (state))->session)
-#define login_connection(state)                        \
-       (((struct login *) (state))->connection)
-#define login_prplconn(state)                  \
-       ((PurpleConnection *)login_connection(state)->prpl_conn)
-#define login_account(state)                   \
-       (login_prplconn(state)->account)
-#define login_account_email(state)             \
-       (login_account(state)->username)
-
-#define login_fail_on_error(msg, state)                                \
-       do {                                                            \
-               if (!SOUP_STATUS_IS_SUCCESSFUL((msg)->status_code)) {   \
-                       chime_login_request_failed((state), G_STRLOC, (msg)); \
-                       return;                                         \
-               }                                                       \
-       } while (0)
-
-#define login_free_form(form)                          \
-       g_clear_pointer(&(form), (form)->release)
-
-#endif  /* __CHIME_LOGIN_PRIVATE_H__ */
diff --git a/login-warpdrive.c b/login-warpdrive.c
deleted file mode 100644 (file)
index 052eba1..0000000
+++ /dev/null
@@ -1,513 +0,0 @@
-/*
- * Pidgin/libpurple Chime client plugin
- *
- * Copyright © 2017 Amazon.com, Inc. or its affiliates.
- *
- * Author: David Woodhouse <dwmw2@infradead.org>
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * version 2.1, as published by the Free Software Foundation.
- *
- * 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
- * Lesser General Public License for more details.
- */
-
-#include <debug.h>
-#include <glib/gi18n.h>
-#include <request.h>
-
-#include "login-private.h"
-
-#define WARPDRIVE_INTERFACE  "com.amazonaws.warpdrive.console.client.GalaxyInternalGWTService"
-#define GWT_BOOTSTRAP  "//script[contains(@src, '/WarpDriveLogin/')][1]/@src"
-#define GWT_ID_REGEX  "['\"]([A-Z0-9]{30,35})['\"]"
-#define GWT_RPC_PATH  "WarpDriveLogin/GalaxyInternalService"
-#define USER_FIELD  "username"
-#define PASS_FIELD  "password"
-#define AUTH_TITLE  _("Corporate Login")
-#define AUTH_FAIL  _("Authentication failed")
-#define AUTH_SEND  _("Sign In")
-#define AUTH_CANCEL  _("Cancel")
-
-#define discovery_failure(state, label)                                        \
-       do {                                                            \
-               purple_debug_error("chime", "%s: %s", G_STRLOC, (label)); \
-               chime_login_bad_response((state), _("Error during corporate login setup")); \
-       } while (0)
-
-struct login_wd {
-       struct login b;  /* Base */
-       gchar *directory;
-       gchar *client_id;
-       gchar *redirect_url;
-       gchar *region;
-       gchar *username;
-       /* GWT-RPC specific parameters */
-       SoupURI *gwt_rpc_uri;
-       gchar *gwt_module_base;
-       gchar *gwt_permutation;
-       gchar *gwt_policy;
-};
-
-/* Break dependency loop */
-static void request_credentials(struct login_wd *state, gboolean retry);
-
-static void free_wd_state(struct login_wd *state)
-{
-       g_free(state->directory);
-       g_free(state->client_id);
-       g_free(state->redirect_url);
-       g_free(state->region);
-       g_free(state->username);
-       soup_uri_free(state->gwt_rpc_uri);
-       g_free(state->gwt_module_base);
-       g_free(state->gwt_permutation);
-       g_free(state->gwt_policy);
-}
-
-static gchar *escaped(const gchar *src)
-{
-       GString *dst = g_string_new("");
-       guint i = 0;
-
-       for (i = 0;  src[i] != '\0';  i++)
-               switch (src[i]) {
-               case '\\':
-                       g_string_append(dst, "\\\\");
-                       break;
-               case '|':
-                       /* GWT escapes the pipe charater with backslash exclamation */
-                       g_string_append(dst, "\\!");
-                       break;
-               default:
-                       g_string_append_c(dst, src[i]);
-               }
-       return g_string_free(dst, FALSE);
-}
-
-static void set_username(struct login_wd *state, const gchar *username)
-{
-       if (state->username)
-               g_free(state->username);
-
-       state->username = escaped(username);
-}
-
-/*
- * Compose a GWT-RPC request.  For more information about how these requests are
- * formatted, check the following links:
- *
- * https://docs.google.com/document/d/1eG0YocsYYbNAtivkLtcaiEE5IOF5u4LUol8-LL0TIKU
- * http://www.gdssecurity.com/l/b/page/2/
- */
-static SoupMessage *gwt_request(struct login_wd *state,
-                               const gchar *interface,
-                               const gchar *method,
-                               guint field_count, ...)
-{
-       GHashTable *strings = g_hash_table_new(g_str_hash, g_str_equal);
-       GHashTableIter iterator;
-       GString *body = g_string_new("7|0|");
-       SoupMessage *msg;
-       const gchar **table;
-       gpointer value, index;
-       gulong i, sc = 0;  /* sc: strings count */
-       va_list fields;
-
-       /* Populate the strings table */
-       g_hash_table_insert(strings, state->gwt_module_base, (gpointer) ++sc);
-       g_hash_table_insert(strings, state->gwt_policy, (gpointer) ++sc);
-       g_hash_table_insert(strings, (gchar *) interface, (gpointer) ++sc);
-       g_hash_table_insert(strings, (gchar *) method, (gpointer) ++sc);
-       va_start(fields, field_count);
-       for (i = 0;  i < field_count;  i++) {
-               gchar *field = va_arg(fields, gchar *);
-               if (field && !g_hash_table_contains(strings, field))
-                       g_hash_table_insert(strings, field, (gpointer) ++sc);
-       }
-       va_end(fields);
-       /* Write the strings table, sorted by table index */
-       g_string_append_printf(body, "%lu|", sc);
-       table = g_new(const gchar *, sc);
-       g_hash_table_iter_init(&iterator, strings);
-       while (g_hash_table_iter_next(&iterator, &value, &index))
-               table[((gulong) index) - 1] = value;
-       for (i = 0;  i < sc;  i++)
-               g_string_append_printf(body, "%s|", table[i]);
-       g_free(table);
-       /* Now add the request components, by their table index (NULLs are
-          converted to zeroes) */
-       g_string_append_printf(body, "%lu|", (gulong)
-                              g_hash_table_lookup(strings, state->gwt_module_base));
-       g_string_append_printf(body, "%lu|", (gulong)
-                              g_hash_table_lookup(strings, state->gwt_policy));
-       g_string_append_printf(body, "%lu|", (gulong)
-                              g_hash_table_lookup(strings, interface));
-       g_string_append_printf(body, "%lu|", (gulong)
-                              g_hash_table_lookup(strings, method));
-       g_string_append(body, "1|");  /* Argument count, only 1 supported */
-       va_start(fields, field_count);
-       for (i = 0;  i < field_count;  i++) {
-               gchar *field = va_arg(fields, gchar *);
-               if (field)
-                       g_string_append_printf(body, "%lu|", (gulong)
-                                              g_hash_table_lookup(strings, field));
-               else
-                       g_string_append(body, "0|");
-       }
-       va_end(fields);
-       /* The request body is ready, now add the headers */
-       msg = soup_message_new_from_uri(SOUP_METHOD_POST, state->gwt_rpc_uri);
-       soup_message_set_request(msg, "text/x-gwt-rpc; charset=utf-8",
-                                SOUP_MEMORY_TAKE, body->str, body->len);
-       soup_message_headers_append(msg->request_headers, "X-GWT-Module-Base",
-                                   state->gwt_module_base);
-       soup_message_headers_append(msg->request_headers, "X-GWT-Permutation",
-                                   state->gwt_permutation);
-
-       g_string_free(body, FALSE);
-       g_hash_table_destroy(strings);
-       return msg;
-}
-
-/*
- * Parse a GWT-RPC response, returning an array of strings, its length and the
- * success status.
- *
- * GWT-RPC responses have a very peculiar format.  For details, please check the
- * following links:
- *
- * https://docs.google.com/document/d/1eG0YocsYYbNAtivkLtcaiEE5IOF5u4LUol8-LL0TIKU
- * https://blog.gdssecurity.com/labs/2009/10/8/gwt-rpc-in-a-nutshell.html
- */
-static gchar **parse_gwt(SoupMessage *msg, gboolean *ok, guint *count)
-{
-       GError *error = NULL;
-       JsonArray *body, *strings;
-       JsonNode *node;
-       JsonParser *parser;
-       const gchar *ctype;
-       gchar **fields = NULL;
-       guint i, length, max;
-
-       *count = 0;
-       ctype = soup_message_headers_get_content_type(msg->response_headers, NULL);
-       if (g_strcmp0(ctype, "application/json") || !msg->response_body ||
-           msg->response_body->length < 5 ||  /* "//OK" or "//EX" */
-           !g_str_has_prefix(msg->response_body->data, "//")) {
-               purple_debug_error("chime", "Unexpected GWT response format");
-               return fields;
-       }
-       *ok = !strncmp(msg->response_body->data + 2, "OK", 2);
-
-       /* Parse the JSON content */
-       parser = json_parser_new();
-       if (!json_parser_load_from_data(parser, msg->response_body->data + 4,
-                                       msg->response_body-> length - 4, &error)) {
-               purple_debug_error("chime", "GWT-JSON parsing error: %s", error->message);
-               goto out;
-       }
-       /* Get the content array */
-       node = json_parser_get_root(parser);
-       if (!JSON_NODE_HOLDS_ARRAY(node)) {
-               purple_debug_error("chime", "Unexpected GWT-JSON type %d", JSON_NODE_TYPE(node));
-               goto out;
-       }
-       body = json_node_get_array(node);
-       length = json_array_get_length(body);
-       if (length < 4) {
-               purple_debug_error("chime", "GWT response array length %d too short", length);
-               goto out;
-       }
-       /* Get the strings table */
-       length -= 3;
-       node = json_array_get_element(body, length);
-       if (!JSON_NODE_HOLDS_ARRAY(node)) {
-               purple_debug_error("chime", "Could not find GWT response strings table");
-               goto out;
-       }
-       strings = json_node_get_array(node);
-       max = json_array_get_length(strings);
-       /* Traverse the rest of the elements in reverse order, replacing the
-          indices by the (copied) string values in the result array */
-       *count = length;
-       fields = g_new0(gchar *, length + 1);
-       for (i = 0;  i < length;  i++) {
-               const gchar *value = NULL;
-               gint64 j = json_array_get_int_element(body, length - i - 1);
-               if (j > 0 && j <= max)
-                       value = json_array_get_string_element(strings, j - 1);
-               fields[i] = g_strdup(value);
-       }
- out:
-       g_error_free(error);
-       g_object_unref(parser);
-       return fields;
-}
-
-static void gwt_auth_cb(SoupSession *session, SoupMessage *msg, gpointer data)
-{
-       SoupMessage *next;
-       gboolean ok;
-       gchar **response;
-       guint count;
-       struct login_wd *state = data;
-
-       login_fail_on_error(msg, state);
-
-       response = parse_gwt(msg, &ok, &count);
-       if (!response) {
-               purple_debug_error("chime", "NULL parsed GWT response during auth");
-               chime_login_bad_response(state, _("Unexpected authentication failure"));
-               return;
-       }
-       if (!ok) {
-               if (count > 3 && !g_strcmp0(response[3], "AuthenticationFailedException"))
-                       request_credentials(state, TRUE);
-               else
-                       chime_login_bad_response(state, _("Unexpected authentication failure"));
-               goto out;
-       }
-       next = soup_form_request_new(SOUP_METHOD_GET, state->redirect_url,
-                                    "organization", state->directory,
-                                    "region", state->region,
-                                    "auth_code", response[2], NULL);
-       soup_session_queue_message(session, next, chime_login_token_cb, state);
- out:
-       g_strfreev(response);
-}
-
-static void send_credentials(struct login_wd *state, const gchar *password)
-{
-       SoupMessage *msg;
-       gchar *safe_password;
-       static const gchar *type = "com.amazonaws.warpdrive.console.shared.LoginRequest_v4/3859384737";
-
-       if (!(password && *password)) {
-               request_credentials(state, TRUE);
-               return;
-       }
-
-       safe_password = escaped(password);
-
-       msg = gwt_request(state, WARPDRIVE_INTERFACE, "authenticateUser", 11,
-                         type, type, "", "", state->client_id, "", NULL,
-                         state->directory, safe_password, "", state->username);
-
-       soup_session_queue_message(login_session(state), msg, gwt_auth_cb, state);
-
-       g_free(safe_password);
-}
-
-static void gather_credentials_and_send(struct login_wd *state, PurpleRequestFields *fields)
-{
-       const gchar *username, *password;
-
-       username = purple_request_fields_get_string(fields, USER_FIELD);
-       password = purple_request_fields_get_string(fields, PASS_FIELD);
-       if (!(username && *username && password && *password)) {
-               request_credentials(state, TRUE);
-               return;
-       }
-
-       set_username(state, username);
-       send_credentials(state, password);
-}
-
-static void request_credentials_with_fields(struct login_wd *state, gboolean retry)
-{
-       PurpleRequestField *username, *password;
-       PurpleRequestFieldGroup *group;
-       PurpleRequestFields *fields;
-
-       fields = purple_request_fields_new();
-       group = purple_request_field_group_new(NULL);
-
-       username = purple_request_field_string_new(USER_FIELD, _("Username"), NULL, FALSE);
-       purple_request_field_set_required(username, TRUE);
-       purple_request_field_group_add_field(group, username);
-
-       password = purple_request_field_string_new(PASS_FIELD, _("Password"), NULL, FALSE);
-       purple_request_field_string_set_masked(password, TRUE);
-       purple_request_field_set_required(password, TRUE);
-       purple_request_field_group_add_field(group, password);
-
-       purple_request_fields_add_group(fields, group);
-
-       purple_request_fields(login_connection(state)->prpl_conn, AUTH_TITLE,
-                             _("Please sign in with your corporate credentials"),
-                             retry ? AUTH_FAIL : NULL, fields,
-                             AUTH_SEND, G_CALLBACK(gather_credentials_and_send),
-                             AUTH_CANCEL, G_CALLBACK(chime_login_cancel_ui),
-                             login_account(state), NULL, NULL, state);
-}
-
-static void request_password_with_input(struct login_wd *state, const gchar *username)
-{
-       if (!(username && *username)) {
-               request_credentials(state, TRUE);
-               return;
-       }
-
-       set_username(state, username);
-
-       purple_request_input(login_connection(state)->prpl_conn, AUTH_TITLE,
-                            _("Corporate password"), NULL,
-                            NULL, FALSE, TRUE, (gchar *) "password",
-                            AUTH_SEND, G_CALLBACK(send_credentials),
-                            AUTH_CANCEL, G_CALLBACK(chime_login_cancel_ui),
-                            login_account(state), NULL, NULL, state);
-}
-
-static void request_username_with_input(struct login_wd *state, gboolean retry)
-{
-       purple_request_input(login_connection(state)->prpl_conn, AUTH_TITLE,
-                            _("Corporate username"), retry ? AUTH_FAIL : NULL,
-                            NULL, FALSE, FALSE, NULL,
-                            _("OK"), G_CALLBACK(request_password_with_input),
-                            AUTH_CANCEL, G_CALLBACK(chime_login_cancel_ui),
-                            login_account(state), NULL, NULL, state);
-}
-
-static void request_credentials(struct login_wd *state, gboolean retry)
-{
-       if (purple_request_get_ui_ops()->request_fields)
-               request_credentials_with_fields(state, retry);
-       else
-               request_username_with_input(state, retry);
-}
-
-static void gwt_region_cb(SoupSession *session, SoupMessage *msg, gpointer data)
-{
-       gboolean ok;
-       gchar **response;
-       guint count;
-       struct login_wd *state = data;
-
-       login_fail_on_error(msg, state);
-
-       response = parse_gwt(msg, &ok, &count);
-       if (!response) {
-               discovery_failure(state, "region response parsed NULL");
-               return;
-       }
-       if (!ok) {
-               discovery_failure(state, "GWT exception during region discovery");
-               goto out;
-       }
-
-       state->region = g_strdup(response[count - 1]);
-       if (!state->region) {
-               discovery_failure(state, "NULL region value");
-               goto out;
-       }
-
-       request_credentials(state, FALSE);
- out:
-       g_strfreev(response);
-}
-
-static void gwt_policy_cb(SoupSession *session, SoupMessage *msg, gpointer data)
-{
-       SoupMessage *next;
-       static const gchar *type = "com.amazonaws.warpdrive.console.shared.ValidateClientRequest_v2/2136236667";
-       struct login_wd *state = data;
-
-       login_fail_on_error(msg, state);
-
-       state->gwt_policy = chime_login_parse_regex(msg, GWT_ID_REGEX, 1);
-       if (!state->gwt_policy) {
-               discovery_failure(state, "no GWT policy found");
-               return;
-       }
-
-       next = gwt_request(state, WARPDRIVE_INTERFACE, "validateClient", 8,
-                          type, type, "ONFAILURE", state->client_id,
-                          state->directory, NULL, NULL, state->redirect_url);
-
-       soup_session_queue_message(session, next, gwt_region_cb, state);
-}
-
-static void gwt_entry_point_cb(SoupSession *session, SoupMessage *msg, gpointer data)
-{
-       SoupMessage *next;
-       SoupURI *base, *destination;
-       gchar *policy_path;
-       struct login_wd *state = data;
-
-       login_fail_on_error(msg, state);
-
-       state->gwt_permutation = chime_login_parse_regex(msg, GWT_ID_REGEX, 1);
-       if (!state->gwt_permutation) {
-               discovery_failure(state, "no GWT permutation found");
-               return;
-       }
-
-       policy_path = g_strdup_printf("deferredjs/%s/5.cache.js",
-                                     state->gwt_permutation);
-       base = soup_uri_new(state->gwt_module_base);
-       destination = soup_uri_new_with_base(base, policy_path);
-
-       next = soup_message_new_from_uri(SOUP_METHOD_GET, destination);
-       soup_session_queue_message(session, next, gwt_policy_cb, state);
-
-       soup_uri_free(destination);
-       soup_uri_free(base);
-       g_free(policy_path);
-}
-
-/*
- * Initial WD login scrapping.
- *
- * Ironically, most of the relevant data coming from this response is placed in
- * the GET parameters (both from the initial URL and the redirection).  From the
- * HTML body we only want the location of the GWT bootstrapping Javascript code.
- */
-void chime_login_warpdrive(SoupSession *session, SoupMessage *msg, gpointer data)
-{
-       GHashTable *params;
-       SoupMessage *next;
-       SoupURI *initial, *base;
-       gchar *sep, **gwt = NULL;
-       struct login_wd *state;
-
-       login_fail_on_error(msg, data);
-       state = chime_login_extend_state(data, sizeof(struct login_wd),
-                                        (GDestroyNotify) free_wd_state);
-
-       initial = soup_message_get_first_party(msg);
-       params = soup_form_decode(soup_uri_get_query(initial));
-       state->directory = g_strdup(g_hash_table_lookup(params, "directory"));
-       if (!state->directory) {
-               discovery_failure(state, "directory identifier not found");
-               goto out;
-       }
-
-       g_hash_table_destroy(params);  /* Reuse the variable */
-       base = soup_message_get_uri(msg);
-       params = soup_form_decode(soup_uri_get_query(base));
-       state->client_id = g_strdup(g_hash_table_lookup(params, "client_id"));
-       state->redirect_url = g_strdup(g_hash_table_lookup(params, "redirect_uri"));
-       if (!(state->client_id && state->redirect_url)) {
-               discovery_failure(state, "client ID or callback missing");
-               goto out;
-       }
-       state->gwt_rpc_uri = soup_uri_new_with_base(base, GWT_RPC_PATH);
-
-       gwt = chime_login_parse_xpaths(msg, 1, GWT_BOOTSTRAP);
-       if (!(gwt && gwt[0])) {
-               discovery_failure(state, "JS bootstrap URL not found");
-               goto out;
-       }
-       sep = strrchr(gwt[0], '/');
-       state->gwt_module_base = g_strndup(gwt[0], (sep - gwt[0]) + 1);
-
-       next = soup_message_new(SOUP_METHOD_GET, gwt[0]);
-       soup_session_queue_message(session, next, gwt_entry_point_cb, state);
- out:
-       g_strfreev(gwt);
-       g_hash_table_destroy(params);
-}
diff --git a/login.c b/login.c
deleted file mode 100644 (file)
index 7ccc642..0000000
--- a/login.c
+++ /dev/null
@@ -1,497 +0,0 @@
-/*
- * Pidgin/libpurple Chime client plugin
- *
- * Copyright © 2017 Amazon.com, Inc. or its affiliates.
- *
- * Author: David Woodhouse <dwmw2@infradead.org>
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * version 2.1, as published by the Free Software Foundation.
- *
- * 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
- * Lesser General Public License for more details.
- */
-
-#include <debug.h>
-#include <request.h>
-#include <glib/gi18n.h>
-#include <libxml/HTMLparser.h>
-#include <libxml/tree.h>
-#include <libxml/xpath.h>
-
-#include "login-private.h"
-#include "chime-connection-private.h"
-
-#define SEARCH_FORM  "//form[@id='picker_email']"
-#define TOKEN_REGEX  "['\"]chime://sso_sessions\\?Token=([^'\"]+)['\"]"
-
-gpointer chime_login_extend_state(gpointer state, gsize size, GDestroyNotify destroy)
-{
-       gpointer new;
-
-       new = g_realloc(state, size);
-       memset((struct login *) new + 1, 0, size - sizeof(struct login));
-       ((struct login *) new)->release_sub = destroy;
-       return new;
-}
-
-void chime_login_free_state(struct login *state)
-{
-       g_return_if_fail(state != NULL);
-
-       if (state->release_sub)
-               state->release_sub(state);
-
-       soup_session_abort(state->session);
-       g_object_unref(state->session);
-       g_object_unref(state->connection);
-       g_free(state);
-}
-
-static void free_form(struct login_form *form)
-{
-       g_free(form->method);
-       g_free(form->action);
-       g_free(form->email_name);
-       g_free(form->password_name);
-       g_hash_table_destroy(form->params);
-       g_free(form);
-}
-
-static void fail(struct login *state, GError *error)
-{
-       g_assert(error != NULL);
-       purple_debug_error("chime", "Login failure: %s", error->message);
-       chime_connection_fail_error(state->connection, error);
-       g_error_free(error);
-       chime_login_free_state(state);
-}
-
-void chime_login_cancel_ui(struct login *state, gpointer foo)
-{
-       fail(state, g_error_new(CHIME_ERROR, CHIME_ERROR_AUTH_FAILED,
-                               _("Authentication canceled by the user")));
-}
-
-void chime_login_cancel_cb(SoupSession *session, SoupMessage *msg, gpointer data)
-{
-       chime_login_cancel_ui(data, NULL);
-}
-
-void chime_login_request_failed(gpointer state, const gchar *location, SoupMessage *msg)
-{
-       purple_debug_error("chime", "%s: Server returned error %d %s", location,
-                          msg->status_code, msg->reason_phrase);
-       fail(state, g_error_new(CHIME_ERROR, CHIME_ERROR_REQUEST_FAILED,
-                               _("A request failed during authentication")));
-}
-
-void chime_login_bad_response(gpointer state, const gchar *fmt, ...)
-{
-       va_list args;
-
-       va_start(args, fmt);
-       fail(state, g_error_new_valist(CHIME_ERROR, CHIME_ERROR_BAD_RESPONSE, fmt, args));
-       va_end(args);
-}
-
-static gboolean xpath_exists(xmlXPathContext *ctx, const gchar *fmt, ...)
-{
-       gboolean found;
-       gchar *expression;
-       va_list args;
-       xmlXPathObject *results;
-
-       va_start(args, fmt);
-       expression = g_strdup_vprintf(fmt, args);
-       va_end(args);
-       results = xmlXPathEval(BAD_CAST expression, ctx);
-       found = results && results->type == XPATH_NODESET &&
-               results->nodesetval && results->nodesetval->nodeNr > 0;
-       xmlXPathFreeObject(results);
-       g_free(expression);
-       return found;
-}
-
-static xmlNode **xpath_nodes(xmlXPathContext *ctx, guint *count, const gchar *fmt, ...)
-{
-       gchar *expression;
-       va_list args;
-       xmlNode **nodes;
-       xmlXPathObject *results;
-
-       va_start(args, fmt);
-       expression = g_strdup_vprintf(fmt, args);
-       va_end(args);
-       results = xmlXPathEval(BAD_CAST expression, ctx);
-       if (results && results->type == XPATH_NODESET && results->nodesetval) {
-               *count = (guint) results->nodesetval->nodeNr;
-               nodes = g_memdup(results->nodesetval->nodeTab,
-                                results->nodesetval->nodeNr * sizeof(xmlNode *));
-       } else {
-               *count = 0;
-               nodes = NULL;
-       }
-       xmlXPathFreeObject(results);
-       g_free(expression);
-       return nodes;
-}
-
-static gchar *xpath_string(xmlXPathContext *ctx, const gchar *fmt, ...)
-{
-       gchar *expression, *wrapped, *value = NULL;
-       va_list args;
-       xmlXPathObject *results;
-
-       va_start(args, fmt);
-       expression = g_strdup_vprintf(fmt, args);
-       va_end(args);
-       wrapped = g_strdup_printf("string(%s)", expression);
-       results = xmlXPathEval(BAD_CAST wrapped, ctx);
-       if (results && results->type == XPATH_STRING)
-               value = g_strdup((gchar *) results->stringval);
-       xmlXPathFreeObject(results);
-       g_free(wrapped);
-       g_free(expression);
-       return value;
-
-}
-
-static xmlDoc *parse_html(SoupMessage *msg)
-{
-       GHashTable *params;
-       const gchar *ctype;
-       gchar *url;
-       xmlDoc *document = NULL;
-
-       ctype = soup_message_headers_get_content_type(msg->response_headers, &params);
-       if (g_strcmp0(ctype, "text/html") || !msg->response_body ||
-           msg->response_body->length <= 0) {
-               purple_debug_error("chime", "Empty HTML response or unexpected content %s", ctype);
-               goto out;
-       }
-
-       url = soup_uri_to_string(soup_message_get_uri(msg), FALSE);
-       document = htmlReadMemory(msg->response_body->data,
-                                 msg->response_body->length,
-                                 url, g_hash_table_lookup(params, "charset"),
-                                 HTML_PARSE_NODEFDTD | HTML_PARSE_NOERROR |
-                                 HTML_PARSE_NOWARNING | HTML_PARSE_NONET |
-                                 HTML_PARSE_RECOVER);
-       g_free(url);
- out:
-       g_hash_table_destroy(params);
-       return document;
-}
-
-gchar *chime_login_parse_regex(SoupMessage *msg, const gchar *regex, guint group)
-{
-       GMatchInfo *match;
-       GRegex *matcher;
-       gchar *text = NULL;
-
-       if (!msg->response_body || msg->response_body->length <= 0) {
-               purple_debug_error("chime", "Empty text response");
-               return text;
-       }
-
-       matcher = g_regex_new(regex, 0, 0, NULL);
-       if (g_regex_match_full(matcher, msg->response_body->data,
-                              msg->response_body->length, 0, 0, &match, NULL))
-               text = g_match_info_fetch(match, group);
-
-       g_match_info_free(match);
-       g_regex_unref(matcher);
-       return text;
-}
-
-gchar **chime_login_parse_xpaths(SoupMessage *msg, guint count, ...)
-{
-       gchar **values = NULL;
-       guint i;
-       va_list args;
-       xmlDoc *html;
-       xmlXPathContext *ctx;
-
-       html = parse_html(msg);
-       if (!html)
-               return values;
-
-       ctx = xmlXPathNewContext(html);
-       if (!ctx) {
-               purple_debug_error("chime", "Failed to create XPath context to parse form");
-               goto out;
-       }
-
-       values = g_new0(gchar *, count + 1);
-
-       va_start(args, count);
-       for (i = 0;  i < count;  i++)
-               values[i] = xpath_string(ctx, va_arg(args, const gchar *));
-       va_end(args);
- out:
-       xmlXPathFreeContext(ctx);
-       xmlFreeDoc(html);
-       return values;
-}
-
-GHashTable *chime_login_parse_json_object(SoupMessage *msg)
-{
-       GError *error = NULL;
-       GHashTable *result = NULL;
-       GList *members, *member;
-       JsonNode *node;
-       JsonObject *object;
-       JsonParser *parser;
-       const gchar *ctype;
-
-       ctype = soup_message_headers_get_content_type(msg->response_headers, NULL);
-       if (g_strcmp0(ctype, "application/json") || !msg->response_body ||
-           msg->response_body->length <= 0) {
-               purple_debug_error("chime", "Empty JSON response or unexpected content %s", ctype);
-               return result;
-       }
-
-       parser = json_parser_new();
-       if (!json_parser_load_from_data(parser, msg->response_body->data,
-                                       msg->response_body->length, &error)) {
-               purple_debug_error("chime", "JSON parsing error: %s", error->message);
-               goto out;
-       }
-
-       node = json_parser_get_root(parser);
-       if (!JSON_NODE_HOLDS_OBJECT(node)) {
-               purple_debug_error("chime", "Unexpected JSON type %d", JSON_NODE_TYPE(node));
-               goto out;
-       }
-
-       object = json_node_get_object(node);
-       result = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
-
-       members = json_object_get_members(object);
-       for (member = g_list_first(members);  member != NULL;  member = member->next) {
-               node = json_object_get_member(object, member->data);
-               if (JSON_NODE_HOLDS_VALUE(node))
-                       g_hash_table_insert(result, g_strdup(member->data),
-                                           g_strdup(json_node_get_string(node)));
-       }
-       g_list_free(members);
- out:
-       g_clear_error(&error);
-       g_object_unref(parser);
-       return result;
-}
-
-struct login_form *chime_login_parse_form(SoupMessage *msg, const gchar *form_xpath)
-{
-       gchar *action;
-       guint i, n;
-       struct login_form *form = NULL;
-       xmlDoc *html;
-       xmlNode **inputs;
-       xmlXPathContext *ctx;
-
-       html = parse_html(msg);
-       if (!html)
-               return form;
-
-       ctx = xmlXPathNewContext(html);
-       if (!ctx) {
-               purple_debug_error("chime", "Failed to create XPath context to parse form");
-               goto out;
-       }
-
-       if (!xpath_exists(ctx, form_xpath)) {
-               purple_debug_error("chime", "XPath query returned no results: %s", form_xpath);
-               goto out;
-       }
-
-       form = g_new0(struct login_form, 1);
-       form->release = (GDestroyNotify) free_form;
-
-       form->method = xpath_string(ctx, "%s/@method", form_xpath);
-       if (form->method) {
-               for (i = 0;  form->method[i] != '\0';  i++)
-                       form->method[i] = g_ascii_toupper(form->method[i]);
-       } else {
-               form->method = g_strdup(SOUP_METHOD_GET);
-       }
-
-       action = xpath_string(ctx, "%s/@action", form_xpath);
-       if (action) {
-               SoupURI *dst = soup_uri_new_with_base(soup_message_get_uri(msg), action);
-               form->action = soup_uri_to_string(dst, FALSE);
-               soup_uri_free(dst);
-       } else {
-               form->action = soup_uri_to_string(soup_message_get_uri(msg), FALSE);
-       }
-
-       form->email_name = xpath_string(ctx, "%s//input[@type='email'][1]/@name", form_xpath);
-       form->password_name = xpath_string(ctx, "%s//input[@type='password'][1]/@name", form_xpath);
-
-       form->params = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
-       inputs = xpath_nodes(ctx, &n, "%s//input[@type='hidden']", form_xpath);
-       for (i = 0;  i < n;  i++) {
-               gchar *name, *value;
-               xmlChar *text;
-               xmlAttr *attribute = xmlHasProp(inputs[i], BAD_CAST "name");
-               if (!attribute)
-                       continue;
-               text = xmlNodeGetContent((xmlNode *) attribute);
-               name = g_strdup((gchar *) text);  /* Avoid mixing allocators */
-               xmlFree(text);
-               attribute = xmlHasProp(inputs[i], BAD_CAST "value");
-               if (attribute) {
-                       text = xmlNodeGetContent((xmlNode *) attribute);
-                       value = g_strdup((gchar *) text);
-                       xmlFree(text);
-               } else {
-                       value = g_strdup("");
-               }
-               g_hash_table_insert(form->params, name, value);
-       }
-
-       g_free(inputs);
-       g_free(action);
- out:
-       xmlXPathFreeContext(ctx);
-       xmlFreeDoc(html);
-       return form;
-}
-
-void chime_login_token_cb(SoupSession *session, SoupMessage *msg, gpointer data)
-{
-       gchar *token;
-       struct login *state = data;
-
-       login_fail_on_error(msg, state);
-
-       token = chime_login_parse_regex(msg, TOKEN_REGEX, 1);
-       if (!token) {
-               purple_debug_error("chime", "Could not find session token in final login response");
-               chime_login_bad_response(state, _("Unable to retrieve session token"));
-               return;
-       }
-
-       chime_connection_set_session_token(state->connection, token);
-       chime_connection_connect(state->connection);
-       chime_login_free_state(state);
-       g_free(token);
-}
-
-static void signin_search_result_cb(SoupSession *session, SoupMessage *msg, gpointer data)
-{
-       GHashTable *provider_info;
-       SoupMessage *next;
-       SoupSessionCallback handler;
-       SoupURI *destination;
-       gchar *type, *path;
-       struct login *state = data;
-
-       if (msg->status_code == 400) {
-               chime_login_bad_response(state, _("Invalid e-mail address <%s>"),
-                                        login_account_email(state));
-               return;
-       }
-
-       login_fail_on_error(msg, state);
-
-       provider_info = chime_login_parse_json_object(msg);
-       if (!provider_info) {
-               chime_login_bad_response(state, _("Error parsing provider JSON"));
-               return;
-       }
-
-       type = g_hash_table_lookup(provider_info, "provider");
-       if (!g_strcmp0(type, "amazon")) {
-               handler = chime_login_amazon;
-       } else if (!g_strcmp0(type, "wd")) {
-               handler = chime_login_warpdrive;
-       } else {
-               purple_debug_error("chime", "Unrecognized provider %s", type);
-               chime_login_bad_response(state, _("Unknown login provider"));
-               goto out;
-       }
-
-       path = g_hash_table_lookup(provider_info, "path");
-       if (!path) {
-               purple_debug_error("chime", "Server did not provide a path");
-               chime_login_bad_response(state, _("Incomplete provider response"));
-               goto out;
-       }
-
-       destination = soup_uri_new_with_base(soup_message_get_uri(msg), path);
-       next = soup_message_new_from_uri(SOUP_METHOD_GET, destination);
-       soup_message_set_first_party(next, destination);
-       soup_session_queue_message(session, next, handler, state);
-       soup_uri_free(destination);
- out:
-       g_hash_table_destroy(provider_info);
-}
-
-static void signin_page_cb(SoupSession *session, SoupMessage *msg, gpointer data)
-{
-       SoupMessage *next;
-       struct login *state = data;
-       struct login_form *form;
-
-       login_fail_on_error(msg, state);
-
-       form = chime_login_parse_form(msg, SEARCH_FORM);
-       if (!(form && form->email_name)) {
-               chime_login_bad_response(state, _("Could not find provider search form"));
-               goto out;
-       }
-
-       g_hash_table_insert(form->params, g_strdup(form->email_name),
-                           g_strdup(login_account_email(state)));
-       next = soup_form_request_new_from_hash(form->method, form->action, form->params);
-       soup_session_queue_message(session, next, signin_search_result_cb, state);
- out:
-       login_free_form(form);
-}
-
-/*
- * Login process entry point.
- *
- * This is where the plugin initiates the authentication process.  Control is
- * transferred to this module until a connection is canceled or restarted once
- * we have the session token.
- */
-void chime_initial_login(ChimeConnection *cxn)
-{
-       ChimeConnectionPrivate *priv;
-       PurpleRequestUiOps *ui_ops;
-       SoupMessage *msg;
-       struct login *state;
-
-       g_return_if_fail(CHIME_IS_CONNECTION(cxn));
-
-       ui_ops = purple_request_get_ui_ops();
-       if (!(ui_ops && ui_ops->request_input)) {
-               purple_debug_error("chime", "Cannot proceed to Chime login: missing essential UI operations");
-               chime_connection_fail(cxn, CHIME_ERROR_AUTH_FAILED,
-                                     _("Cannot login to Chime with this software"));
-               return;
-       }
-
-       state = g_new0(struct login, 1);
-       state->connection = g_object_ref(cxn);
-       state->session = soup_session_new_with_options(SOUP_SESSION_ADD_FEATURE_BY_TYPE,
-                                                      SOUP_TYPE_COOKIE_JAR,
-                                                      SOUP_SESSION_USER_AGENT,
-                                                      "Pidgin-Chime " PACKAGE_VERSION " ",
-                                                      NULL);
-       /* TODO: This needs to go somewhere else */
-       if (getenv("CHIME_DEBUG") && atoi(getenv("CHIME_DEBUG")) > 0) {
-               SoupLogger *l = soup_logger_new(SOUP_LOGGER_LOG_BODY, -1);
-               soup_session_add_feature(state->session, SOUP_SESSION_FEATURE(l));
-               g_object_unref(l);
-       }
-       priv = CHIME_CONNECTION_GET_PRIVATE(cxn);
-       msg = soup_message_new(SOUP_METHOD_GET, priv->server);
-       soup_session_queue_message(state->session, msg, signin_page_cb, state);
-}
diff --git a/prpl/authenticate.c b/prpl/authenticate.c
new file mode 100644 (file)
index 0000000..a15cc8c
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * Pidgin/libpurple Chime client plugin
+ *
+ * Copyright © 2017 Amazon.com, Inc. or its affiliates.
+ *
+ * Author: David Woodhouse <dwmw2@infradead.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ */
+
+#include <glib/gi18n.h>
+#include <request.h>
+
+#include "chime.h"
+
+#define TITLE_LABEL  _("Chime Sign In Authentication")
+#define USER_LABEL  _("Username")
+#define PASS_LABEL  _("Password")
+#define SEND_LABEL  _("Sign In")
+#define CANCEL_LABEL  _("Cancel")
+#define USER_FIELD  "username"
+#define PASS_FIELD  "password"
+
+struct auth_data {
+       PurpleConnection *conn;
+       gpointer state;
+       gboolean user_required;
+       gchar *username;
+       gchar *password;
+};
+
+static void send_credentials(struct auth_data *data)
+{
+       chime_connection_authenticate(data->state, data->username, data->password);
+       g_free(data->username);
+       g_free(data->password);
+       g_free(data);
+}
+
+static void gather_credentials_from_fields(struct auth_data *data, PurpleRequestFields *fields)
+{
+       if (data->user_required)
+               data->username = g_strdup(purple_request_fields_get_string(fields, USER_FIELD));
+       data->password = g_strdup(purple_request_fields_get_string(fields, PASS_FIELD));
+       send_credentials(data);
+}
+
+static void request_credentials_with_fields(struct auth_data *data)
+{
+       PurpleRequestField *username, *password;
+       PurpleRequestFieldGroup *group;
+       PurpleRequestFields *fields;
+
+       fields = purple_request_fields_new();
+       group = purple_request_field_group_new(NULL);
+
+       if (data->user_required) {
+               username = purple_request_field_string_new(USER_FIELD, USER_LABEL, NULL, FALSE);
+               purple_request_field_set_required(username, TRUE);
+               purple_request_field_group_add_field(group, username);
+       }
+
+       password = purple_request_field_string_new(PASS_FIELD, PASS_LABEL, NULL, FALSE);
+       purple_request_field_string_set_masked(password, TRUE);
+       purple_request_field_set_required(password, TRUE);
+       purple_request_field_group_add_field(group, password);
+
+       purple_request_fields_add_group(fields, group);
+
+       purple_request_fields(data->conn, TITLE_LABEL, NULL, NULL, fields,
+                             SEND_LABEL, G_CALLBACK(gather_credentials_from_fields),
+                             CANCEL_LABEL, G_CALLBACK(send_credentials),
+                             data->conn->account, NULL, NULL, data);
+}
+
+static void gather_password_from_input(struct auth_data *data, const gchar *password)
+{
+       data->password = g_strdup(password);
+       send_credentials(data);
+}
+
+static void request_password_with_input(struct auth_data *data, const gchar *username)
+{
+       data->username = g_strdup(username);
+       purple_request_input(data->conn, TITLE_LABEL, PASS_LABEL,
+                            NULL, NULL, FALSE, TRUE, (gchar *) PASS_FIELD,
+                            SEND_LABEL, G_CALLBACK(gather_password_from_input),
+                            CANCEL_LABEL, G_CALLBACK(send_credentials),
+                            data->conn->account, NULL, NULL, data);
+}
+
+static void request_username_with_input(struct auth_data *data)
+{
+       if (data->user_required)
+               purple_request_input(data->conn, TITLE_LABEL, USER_LABEL,
+                                    NULL, NULL, FALSE, FALSE, (gchar *) USER_FIELD,
+                                    _("OK"), G_CALLBACK(request_password_with_input),
+                                    CANCEL_LABEL, G_CALLBACK(send_credentials),
+                                    data->conn->account, NULL, NULL, data);
+       else
+               request_password_with_input(data, NULL);
+}
+
+void purple_request_credentials(PurpleConnection *conn, gpointer state, gboolean user_required)
+{
+       struct auth_data *data = g_new0(struct auth_data, 1);
+       data->conn = conn;
+       data->state = state;
+       data->user_required = user_required;
+       if (purple_request_get_ui_ops()->request_fields)
+               request_credentials_with_fields(data);
+       else
+               request_username_with_input(data);
+}
index e9f0b2315305caf23e3c559e5f150892f7791076..e6a7b235bbdcf8fe7419307651197f8d36ed6f82 100644 (file)
@@ -65,7 +65,7 @@ static void chime_purple_plugin_destroy(PurplePlugin *plugin)
 
 static const char *chime_purple_list_icon(PurpleAccount *a, PurpleBuddy *b)
 {
-        return "chime";
+       return "chime";
 }
 
 static void on_set_idle_ready(GObject *source, GAsyncResult *result, gpointer user_data)
@@ -88,6 +88,11 @@ static void chime_purple_set_idle(PurpleConnection *conn, int idle_time)
                                                 NULL);
 }
 
+static void on_chime_authenticate(ChimeConnection *cxn, gpointer state, gboolean user_required, PurpleConnection *conn)
+{
+       purple_request_credentials(conn, state, user_required);
+}
+
 static void on_chime_connected(ChimeConnection *cxn, const gchar *display_name, PurpleConnection *conn)
 {
        purple_debug(PURPLE_DEBUG_INFO, "chime", "Chime connected as %s\n", display_name);
@@ -225,10 +230,13 @@ static void chime_purple_login(PurpleAccount *account)
        purple_chime_init_chats(conn);
        purple_chime_init_messages(conn);
 
-       pc->cxn = chime_connection_new(conn, server, devtoken, token);
+       pc->cxn = chime_connection_new(conn, purple_account_get_username(account),
+                                      server, devtoken, token);
 
        g_signal_connect(pc->cxn, "notify::session-token",
                         G_CALLBACK(on_session_token_changed), conn);
+       g_signal_connect(pc->cxn, "authenticate",
+                        G_CALLBACK(on_chime_authenticate), conn);
        g_signal_connect(pc->cxn, "connected",
                         G_CALLBACK(on_chime_connected), conn);
        g_signal_connect(pc->cxn, "disconnected",
@@ -243,8 +251,6 @@ static void chime_purple_login(PurpleAccount *account)
           on close, and it doesn't use it anyway. */
        g_signal_connect(pc->cxn, "log-message",
                         G_CALLBACK(on_chime_log_message), NULL);
-
-
 }
 
 static void disconnect_contact(ChimeConnection *cxn, ChimeContact *contact,
@@ -505,4 +511,3 @@ static void chime_purple_init_plugin(PurplePlugin *plugin)
 }
 
 PURPLE_INIT_PLUGIN(chime, chime_purple_init_plugin, chime_plugin_info);
-
index c8b4b22ecbebc7b4bd6a02471e4003c2e14212e8..a52ca44cacc281c9266128efc7acaef6e48515f5 100644 (file)
@@ -54,6 +54,9 @@ struct purple_chime {
 
 #define PURPLE_CHIME_CXN(conn) (CHIME_CONNECTION(((struct purple_chime *)purple_connection_get_protocol_data(conn))->cxn))
 
+/* authenticate.c */
+void purple_request_credentials(PurpleConnection *conn, gpointer state, gboolean user_required);
+
 /* chime.c */
 /* BEWARE: msg_id is allocated, msg_time is const. I am going to hate myself
    for that one day, but it's convenient for now... */