[gnome-shell] Add a helper to handle captive portal logins



commit 8c67a70db0a02b67d572a46373cea30a67b8d679
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Mon Feb 17 17:19:18 2014 +0100

    Add a helper to handle captive portal logins
    
    Add a small DBus-activated GtkApplication that embeds a WebKitWebView
    and implements some minimal logic to see if the login succeeds.
    It will try to connect to a custom NM-provided url (the portal login
    page), if one exists, or to www.gnome.org in the normal case of
    a portal doing redirect.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=704416

 data/Makefile.am                             |   22 +++-
 data/org.gnome.Shell.PortalHelper.desktop.in |    9 +
 data/org.gnome.Shell.PortalHelper.service.in |    3 +
 js/js-resources.gresource.xml                |    2 +
 js/portalHelper/main.js                      |  247 ++++++++++++++++++++++++++
 src/Makefile.am                              |   15 ++
 src/gnome-shell-portal-helper.c              |   52 ++++++
 7 files changed, 349 insertions(+), 1 deletions(-)
---
diff --git a/data/Makefile.am b/data/Makefile.am
index c22dd1f..1befb87 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -1,6 +1,24 @@
+CLEANFILES =
+
 desktopdir=$(datadir)/applications
 desktop_DATA = gnome-shell.desktop gnome-shell-wayland.desktop  gnome-shell-extension-prefs.desktop
 
+if HAVE_NETWORKMANAGER
+desktop_DATA += org.gnome.Shell.PortalHelper.desktop
+
+servicedir = $(datadir)/dbus-1/services
+service_DATA = org.gnome.Shell.PortalHelper.service
+
+CLEANFILES += \
+       org.gnome.Shell.PortalHelper.service \
+       org.gnome.Shell.PortalHelper.desktop
+
+endif
+
+%.service: %.service.in
+       $(AM_V_GEN) sed -e "s|@libexecdir[ ]|$(libexecdir)|" \
+           $< > $@ || rm $@
+
 # We substitute in bindir so it works as an autostart
 # file when built in a non-system prefix
 %.desktop.in:%.desktop.in.in
@@ -88,9 +106,11 @@ EXTRA_DIST =                                                \
        $(menu_DATA)                                    \
        $(convert_DATA)                                 \
        $(keys_in_files)                                \
+       org.gnome.Shell.PortalHelper.desktop.in         \
+       org.gnome.Shell.PortalHelper.service.in         \
        org.gnome.shell.gschema.xml.in.in
 
-CLEANFILES =                                           \
+CLEANFILES +=                                          \
        gnome-shell.desktop.in                          \
        gnome-shell-wayland.desktop.in                  \
        gnome-shell-extension-prefs.in                  \
diff --git a/data/org.gnome.Shell.PortalHelper.desktop.in b/data/org.gnome.Shell.PortalHelper.desktop.in
new file mode 100644
index 0000000..c82760f
--- /dev/null
+++ b/data/org.gnome.Shell.PortalHelper.desktop.in
@@ -0,0 +1,9 @@
+[Desktop Entry]
+_Name=Captive Portal
+Type=Application
+Exec=gapplication launch org.gnome.Shell.PortalHelper
+DBusActivatable=true
+NoDisplay=true
+Icon=network-workgroup
+StartupNotify=true
+OnlyShowIn=GNOME;
\ No newline at end of file
diff --git a/data/org.gnome.Shell.PortalHelper.service.in b/data/org.gnome.Shell.PortalHelper.service.in
new file mode 100644
index 0000000..5465a32
--- /dev/null
+++ b/data/org.gnome.Shell.PortalHelper.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.gnome.Shell.PortalHelper
+Exec= libexecdir@/gnome-shell-portal-helper
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 47bdd00..32df2dd 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -26,6 +26,8 @@
 
     <file>perf/core.js</file>
 
+    <file>portalHelper/main.js</file>
+
     <file>ui/altTab.js</file>
     <file>ui/animation.js</file>
     <file>ui/appDisplay.js</file>
diff --git a/js/portalHelper/main.js b/js/portalHelper/main.js
new file mode 100644
index 0000000..bb6a2a5
--- /dev/null
+++ b/js/portalHelper/main.js
@@ -0,0 +1,247 @@
+const Format = imports.format;
+const Gettext = imports.gettext;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+const Pango = imports.gi.Pango;
+const Soup = imports.gi.Soup;
+const WebKit = imports.gi.WebKit2;
+
+const _ = Gettext.gettext;
+
+const Config = imports.misc.config;
+
+const PortalHelperResult = {
+    CANCELLED: 0,
+    COMPLETED: 1,
+    RECHECK: 2
+};
+
+const INACTIVITY_TIMEOUT = 30000; //ms
+const CONNECTIVITY_RECHECK_RATELIMIT_TIMEOUT = 30 * GLib.USEC_PER_SEC;
+
+const HelperDBusInterface = '<node> \
+<interface name="org.gnome.Shell.PortalHelper"> \
+<method name="Authenticate"> \
+    <arg type="o" direction="in" name="connection" /> \
+    <arg type="s" direction="in" name="url" /> \
+    <arg type="u" direction="in" name="timestamp" /> \
+</method> \
+<method name="Close"> \
+    <arg type="o" direction="in" name="connection" /> \
+</method> \
+<method name="Refresh"> \
+    <arg type="o" direction="in" name="connection" /> \
+</method> \
+<signal name="Done"> \
+    <arg type="o" name="connection" /> \
+    <arg type="u" name="result" /> \
+</signal> \
+</interface> \
+</node>';
+
+const PortalWindow = new Lang.Class({
+    Name: 'PortalWindow',
+    Extends: Gtk.ApplicationWindow,
+
+    _init: function(application, url, timestamp, doneCallback) {
+        this.parent({ application: application });
+
+        if (url) {
+            this._uri = new Soup.URI(uri);
+        } else {
+            url = 'http://www.gnome.org';
+            this._uri = null;
+            this._everSeenRedirect = false;
+        }
+        this._originalUrl = url;
+        this._doneCallback = doneCallback;
+        this._lastRecheck = 0;
+        this._recheckAtExit = false;
+
+        this._webView = new WebKit.WebView();
+        this._webView.connect('decide-policy', Lang.bind(this, this._onDecidePolicy));
+        this._webView.load_uri(url);
+        this._webView.connect('notify::title', Lang.bind(this, this._syncTitle));
+        this._syncTitle();
+
+        this.add(this._webView);
+        this._webView.show();
+        this.maximize();
+        this.present_with_time(timestamp);
+    },
+
+    _syncTitle: function() {
+        let title = this._webView.title;
+
+        if (title) {
+            this.title = title;
+        } else {
+            // TRANSLATORS: this is the title of the wifi captive portal login
+            // window, until we know the title of the actual login page
+            this.title = _("Web Authentication Redirect");
+    },
+
+    refresh: function() {
+        this._everSeenRedirect = false;
+        this._webView.load_uri(this._originalUrl);
+    },
+
+    vfunc_delete_event: function(event) {
+        if (this._recheckAtExit)
+            this._doneCallback(PortalHelperResult.RECHECK);
+        else
+            this._doneCallback(PortalHelperResult.CANCELLED);
+        return false;
+    },
+
+    _onDecidePolicy: function(view, decision, type) {
+        if (type == WebKit.PolicyDecisionType.NEW_WINDOW_ACTION) {
+            decision.ignore();
+            return true;
+        }
+
+        if (type != WebKit.PolicyDecisionType.NAVIGATION_ACTION)
+            return false;
+
+        let request = decision.get_request();
+        let uri = new Soup.URI(request.get_uri());
+
+        if (this._uri != null) {
+            if (!uri.host_equal(uri, this._uri)) {
+                // We *may* have finished here, but we don't know for
+                // sure. Tell gnome-shell to run another connectivity check
+                // (but ratelimit the checks, we don't want to spam
+                // gnome.org for portals that have 10 or more internal
+                // redirects - and unfortunately they exist)
+                // If we hit the rate limit, we also queue a recheck
+                // when the window is closed, just in case we miss the
+                // final check and don't realize we're connected
+                // This should not be a problem in the cancelled logic,
+                // because if the user doesn't want to start the login,
+                // we should not see any redirect at all, outside this._uri
+
+                let now = GLib.get_monotonic_time();
+                let shouldRecheck = (now - this._lastRecheck) >
+                    CONNECTIVITY_RECHECK_RATELIMIT_TIMEOUT;
+
+                if (shouldRecheck) {
+                    this._lastRecheck = now;
+                    this._recheckAtExit = false;
+                    this._doneCallback(PortalHelperResult.RECHECK);
+                } else {
+                    this._recheckAtExit = true;
+                }
+            }
+
+            // Update the URI, in case of chained redirects, so we still
+            // think we're doing the login until gnome-shell kills us
+            this._uri = uri;
+        } else {
+            if (uri.get_host() == 'www.gnome.org' && this._everSeenRedirect) {
+                // Yay, we got to gnome!
+                decision.ignore();
+                this._doneCallback(PortalHelperResult.COMPLETED);
+                return true;
+            } else if (uri.get_host() != 'www.gnome.org') {
+                this._everSeenRedirect = true;
+            }
+        }
+
+        decision.use();
+        return true;
+    },
+});
+
+const WebPortalHelper = new Lang.Class({
+    Name: 'WebPortalHelper',
+    Extends: Gtk.Application,
+
+    _init: function() {
+        this.parent({ application_id: 'org.gnome.Shell.PortalHelper',
+                      flags: Gio.ApplicationFlags.IS_SERVICE,
+                      inactivity_timeout: 30000 });
+
+        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(HelperDBusInterface, this);
+        this._queue = [];
+    },
+
+    vfunc_dbus_register: function(connection, path) {
+        this._dbusImpl.export(connection, path);
+        this.parent(connection, path);
+        return true;
+    },
+
+    vfunc_dbus_unregister: function(connection, path) {
+        this._dbusImpl.unexport_from_connection(connection);
+        this.parent(connection, path);
+    },
+
+    vfunc_activate: function() {
+        // If launched manually (for example for testing), force a dummy authentication
+        // session with the default url
+        this.Authenticate('/org/gnome/dummy', '', 0);
+    },
+
+    Authenticate: function(connection, url, timestamp) {
+        this._queue.push({ connection: connection, url: url, timestamp: timestamp });
+
+        this._processQueue();
+    },
+
+    Close: function(connection) {
+        for (let i = 0; i < this._queue.length; i++) {
+            let obj = this._queue[i];
+
+            if (obj.connection == connection) {
+                if (obj.window)
+                    obj.window.destroy();
+                this._queue.splice(i, 1);
+                break;
+            }
+        }
+
+        this._processQueue();
+    },
+
+    Refresh: function(connection) {
+        for (let i = 0; i < this._queue.length; i++) {
+            let obj = this._queue[i];
+
+            if (obj.connection == connection) {
+                if (obj.window)
+                    obj.window.refresh();
+                break;
+            }
+        }
+    },
+
+    _processQueue: function() {
+        if (this._queue.length == 0)
+            return;
+
+        let top = this._queue[0];
+        if (top.window != null)
+            return;
+
+        top.window = new PortalWindow(this, top.uri, top.timestamp, Lang.bind(this, function(result) {
+            this._dbusImpl.emit_signal('Done', new GLib.Variant('(ou)', [top.connection, result]));
+        }));
+    },
+});
+
+function initEnvironment() {
+    String.prototype.format = Format.format;
+}
+
+function main(argv) {
+    initEnvironment();
+
+    Gettext.bindtextdomain(Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
+    Gettext.textdomain(Config.GETTEXT_PACKAGE);
+
+    let app = new WebPortalHelper();
+    return app.run(argv);
+}
diff --git a/src/Makefile.am b/src/Makefile.am
index 124b3de..52ddbfd 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -199,6 +199,21 @@ nodist_gnome_shell_extension_prefs_SOURCES = \
 gnome_shell_extension_prefs_CPPFLAGS = $(gnome_shell_cflags)
 gnome_shell_extension_prefs_LDADD = libgnome-shell-js.la $(GNOME_SHELL_LIBS)
 
+if HAVE_NETWORKMANAGER
+
+libexec_PROGRAMS += gnome-shell-portal-helper
+gnome_shell_portal_helper_SOURCES = \
+       gnome-shell-portal-helper.c \
+       $(NULL)
+nodist_gnome_shell_portal_helper_SOURCES = \
+       $(top_builddir)/js/js-resources.c               \
+       $(top_builddir)/js/js-resources.h               \
+       $(NULL)
+gnome_shell_portal_helper_CPPFLAGS = $(gnome_shell_cflags)
+gnome_shell_portal_helper_LDADD = libgnome-shell-js.la $(GNOME_SHELL_LIBS)
+
+endif
+
 ########################################
 
 libgnome_shell_js_la_SOURCES =         \
diff --git a/src/gnome-shell-portal-helper.c b/src/gnome-shell-portal-helper.c
new file mode 100644
index 0000000..4087f87
--- /dev/null
+++ b/src/gnome-shell-portal-helper.c
@@ -0,0 +1,52 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+#include "config.h"
+
+#include <girepository.h>
+#include <gjs/gjs.h>
+#include <glib/gi18n.h>
+
+int
+main (int argc, char *argv[])
+{
+  const char *search_path[] = { "resource:///org/gnome/shell", NULL };
+  GError *error = NULL;
+  GjsContext *context;
+  int status;
+
+  bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+  textdomain (GETTEXT_PACKAGE);
+
+  g_irepository_prepend_search_path (GNOME_SHELL_PKGLIBDIR);
+
+  context = g_object_new (GJS_TYPE_CONTEXT,
+                          "search-path", search_path,
+                          NULL);
+
+  if (!gjs_context_define_string_array(context, "ARGV",
+                                       argc, (const char**)argv,
+                                       &error))
+    {
+      g_message("Failed to define ARGV: %s", error->message);
+      g_error_free (error);
+
+      return 1;
+    }
+
+
+  if (!gjs_context_eval (context,
+                         "const Main = imports.portalHelper.main; Main.main(ARGV);",
+                         -1,
+                         "<main>",
+                         &status,
+                         &error))
+    {
+      g_message ("Execution of main.js threw exception: %s", error->message);
+      g_error_free (error);
+
+      return status;
+    }
+
+  return 0;
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]