From 8c67a70db0a02b67d572a46373cea30a67b8d679 Mon Sep 17 00:00:00 2001 From: Giovanni Campagna Date: Mon, 17 Feb 2014 17:19:18 +0100 Subject: [PATCH] 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 deletion(-) create mode 100644 data/org.gnome.Shell.PortalHelper.desktop.in create mode 100644 data/org.gnome.Shell.PortalHelper.service.in create mode 100644 js/portalHelper/main.js create mode 100644 src/gnome-shell-portal-helper.c diff --git a/data/Makefile.am b/data/Makefile.am index c22dd1f97..1befb87c0 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 000000000..c82760f2c --- /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 000000000..5465a3298 --- /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 47bdd0048..32df2dd85 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -26,6 +26,8 @@ perf/core.js + portalHelper/main.js + ui/altTab.js ui/animation.js ui/appDisplay.js diff --git a/js/portalHelper/main.js b/js/portalHelper/main.js new file mode 100644 index 000000000..bb6a2a5e5 --- /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 = ' \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +'; + +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 124b3de93..52ddbfd54 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 000000000..4087f8752 --- /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 +#include +#include + +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, + "
", + &status, + &error)) + { + g_message ("Execution of main.js threw exception: %s", error->message); + g_error_free (error); + + return status; + } + + return 0; +}