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 FileUtils = imports.misc.fileUtils; const PortalHelperResult = { CANCELLED: 0, COMPLETED: 1, RECHECK: 2 }; const INACTIVITY_TIMEOUT = 30000; //ms const CONNECTIVITY_CHECK_HOST = 'nmcheck.gnome.org'; const CONNECTIVITY_CHECK_URI = 'http://' + CONNECTIVITY_CHECK_HOST; 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 }); this.connect('delete-event', Lang.bind(this, this.destroyWindow)); /* TRANSLATORS: this is the title of the wifi captive portal login window */ this._headerBar = new Gtk.HeaderBar({ title: _("Hotspot Login"), show_close_button: true }); this.set_titlebar(this._headerBar); this._headerBar.show(); if (!url) { url = CONNECTIVITY_CHECK_URI; this._originalUrlWasGnome = true; } else { this._originalUrlWasGnome = false; } this._uri = new Soup.URI(url); this._everSeenRedirect = false; this._originalUrl = url; this._doneCallback = doneCallback; this._lastRecheck = 0; this._recheckAtExit = false; let cacheDir = GLib.Dir.make_tmp("gnome-shell-portal-helper-XXXXXXXX"); this._cacheDir = Gio.File.new_for_path(cacheDir); let dataManager = new WebKit.WebsiteDataManager({ base_data_directory: cacheDir, base_cache_directory: cacheDir }); let webContext = new WebKit.WebContext({ website_data_manager: dataManager }); webContext.set_cache_model(WebKit.CacheModel.DOCUMENT_VIEWER); this._webView = WebKit.WebView.new_with_context(webContext); this._webView.connect('decide-policy', Lang.bind(this, this._onDecidePolicy)); this._webView.load_uri(url); this._webView.connect('notify::uri', Lang.bind(this, this._syncUri)); this._syncUri(); this.add(this._webView); this._webView.show(); this.set_size_request(600, 450); this.maximize(); this.present_with_time(timestamp); this.application.set_accels_for_action('app.quit', ['q', 'w']); }, destroyWindow: function() { this.destroy(); FileUtils.recursivelyDeleteDir(this._cacheDir, true); }, _syncUri: function() { let uri = this._webView.uri; if (uri) this._headerBar.set_subtitle(GLib.uri_unescape_string(uri, null, false)); else this._headerBar.set_subtitle(null); }, 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 (!uri.host_equal(this._uri) && this._originalUrlWasGnome) { if (uri.get_host() == CONNECTIVITY_CHECK_HOST && this._everSeenRedirect) { // Yay, we got to gnome! decision.ignore(); this._doneCallback(PortalHelperResult.COMPLETED); return true; } else if (uri.get_host() != CONNECTIVITY_CHECK_HOST) { this._everSeenRedirect = true; } } // 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 // nmcheck.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; 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 = []; let action = new Gio.SimpleAction({ name: 'quit' }); action.connect('activate', () => { this.active_window.destroyWindow(); }); this.add_action(action); }, 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.destroyWindow(); 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); }