2014-02-17 16:19:18 +00:00
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
} ;
2017-02-10 20:08:01 +00:00
const PortalHelperSecurityLevel = {
NOT _YET _DETERMINED : 0 ,
SECURE : 1 ,
INSECURE : 2
} ;
2014-02-17 16:19:18 +00:00
const INACTIVITY _TIMEOUT = 30000 ; //ms
2016-10-24 14:58:24 +00:00
const CONNECTIVITY _CHECK _HOST = 'nmcheck.gnome.org' ;
const CONNECTIVITY _CHECK _URI = 'http://' + CONNECTIVITY _CHECK _HOST ;
2014-02-17 16:19:18 +00:00
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" / > \
< / m e t h o d > \
< method name = "Close" > \
< arg type = "o" direction = "in" name = "connection" / > \
< / m e t h o d > \
< method name = "Refresh" > \
< arg type = "o" direction = "in" name = "connection" / > \
< / m e t h o d > \
< signal name = "Done" > \
< arg type = "o" name = "connection" / > \
< arg type = "u" name = "result" / > \
< / s i g n a l > \
< / i n t e r f a c e > \
< / n o d e > ' ;
2017-02-10 20:08:01 +00:00
const PortalHeaderBar = new Lang . Class ( {
Name : 'PortalHeaderBar' ,
Extends : Gtk . HeaderBar ,
_init : function ( ) {
this . parent ( { show _close _button : true } ) ;
// See ephy-title-box.c in epiphany for the layout
let vbox = new Gtk . Box ( { orientation : Gtk . Orientation . VERTICAL ,
spacing : 0 } ) ;
this . set _custom _title ( vbox ) ;
/* TRANSLATORS: this is the title of the wifi captive portal login window */
let titleLabel = new Gtk . Label ( { label : _ ( "Hotspot Login" ) ,
wrap : false ,
single _line _mode : true ,
ellipsize : Pango . EllipsizeMode . END } ) ;
titleLabel . get _style _context ( ) . add _class ( 'title' ) ;
vbox . add ( titleLabel ) ;
let hbox = new Gtk . Box ( { orientation : Gtk . Orientation . HORIZONTAL ,
spacing : 4 ,
halign : Gtk . Align . CENTER ,
valign : Gtk . Align . BASELINE } ) ;
hbox . get _style _context ( ) . add _class ( 'subtitle' ) ;
vbox . add ( hbox ) ;
this . _lockImage = new Gtk . Image ( { icon _size : Gtk . IconSize . MENU ,
valign : Gtk . Align . BASELINE } ) ;
hbox . add ( this . _lockImage ) ;
this . subtitleLabel = new Gtk . Label ( { wrap : false ,
single _line _mode : true ,
ellipsize : Pango . EllipsizeMode . END ,
valign : Gtk . Align . BASELINE ,
selectable : true } ) ;
this . subtitleLabel . get _style _context ( ) . add _class ( 'subtitle' ) ;
hbox . add ( this . subtitleLabel ) ;
vbox . show _all ( ) ;
} ,
setSubtitle : function ( label ) {
this . subtitleLabel . set _text ( label ) ;
} ,
setSecurityIcon : function ( securityLevel ) {
switch ( securityLevel ) {
case PortalHelperSecurityLevel . NOT _YET _DETERMINED :
this . _lockImage . hide ( ) ;
break ;
case PortalHelperSecurityLevel . SECURE :
this . _lockImage . show ( ) ;
this . _lockImage . set _from _icon _name ( "channel-secure-symbolic" , Gtk . IconSize . MENU ) ;
this . _lockImage . set _tooltip _text ( null ) ;
break ;
case PortalHelperSecurityLevel . INSECURE :
this . _lockImage . show ( ) ;
this . _lockImage . set _from _icon _name ( "channel-insecure-symbolic" , Gtk . IconSize . MENU ) ;
this . _lockImage . set _tooltip _text ( _ ( 'Your connection to this hotspot login is not secure. Passwords or other information you enter on this page can be viewed by people nearby.' ) ) ;
break ;
}
} ,
} ) ;
2014-02-17 16:19:18 +00:00
const PortalWindow = new Lang . Class ( {
Name : 'PortalWindow' ,
Extends : Gtk . ApplicationWindow ,
_init : function ( application , url , timestamp , doneCallback ) {
this . parent ( { application : application } ) ;
2017-01-23 07:32:30 +00:00
2017-01-23 06:14:17 +00:00
this . connect ( 'delete-event' , Lang . bind ( this , this . destroyWindow ) ) ;
2017-02-10 20:08:01 +00:00
this . _headerBar = new PortalHeaderBar ( ) ;
this . _headerBar . setSecurityIcon ( PortalHelperSecurityLevel . NOT _YET _DETERMINED ) ;
2017-01-23 07:32:30 +00:00
this . set _titlebar ( this . _headerBar ) ;
this . _headerBar . show ( ) ;
2014-02-17 16:19:18 +00:00
2014-07-28 07:36:40 +00:00
if ( ! url ) {
2016-10-24 14:58:24 +00:00
url = CONNECTIVITY _CHECK _URI ;
2014-07-28 07:36:40 +00:00
this . _originalUrlWasGnome = true ;
} else {
this . _originalUrlWasGnome = false ;
2014-02-17 16:19:18 +00:00
}
2014-07-28 07:36:40 +00:00
this . _uri = new Soup . URI ( url ) ;
this . _everSeenRedirect = false ;
2014-02-17 16:19:18 +00:00
this . _originalUrl = url ;
this . _doneCallback = doneCallback ;
this . _lastRecheck = 0 ;
this . _recheckAtExit = false ;
2017-03-23 15:54:28 +00:00
this . _webContext = WebKit . WebContext . new _ephemeral ( ) ;
2017-02-13 09:44:01 +00:00
this . _webContext . set _cache _model ( WebKit . CacheModel . DOCUMENT _VIEWER ) ;
2017-01-23 06:14:17 +00:00
2017-02-13 09:44:01 +00:00
this . _webView = WebKit . WebView . new _with _context ( this . _webContext ) ;
2014-02-17 16:19:18 +00:00
this . _webView . connect ( 'decide-policy' , Lang . bind ( this , this . _onDecidePolicy ) ) ;
2017-02-10 20:08:01 +00:00
this . _webView . connect ( 'load-changed' , Lang . bind ( this , this . _onLoadChanged ) ) ;
this . _webView . connect ( 'insecure-content-detected' , Lang . bind ( this , this . _onInsecureContentDetected ) ) ;
2017-02-13 09:44:01 +00:00
this . _webView . connect ( 'load-failed-with-tls-errors' , Lang . bind ( this , this . _onLoadFailedWithTlsErrors ) ) ;
2014-02-17 16:19:18 +00:00
this . _webView . load _uri ( url ) ;
2017-01-23 07:32:30 +00:00
this . _webView . connect ( 'notify::uri' , Lang . bind ( this , this . _syncUri ) ) ;
this . _syncUri ( ) ;
2014-02-17 16:19:18 +00:00
this . add ( this . _webView ) ;
this . _webView . show ( ) ;
2017-01-23 07:32:05 +00:00
this . set _size _request ( 600 , 450 ) ;
2014-02-17 16:19:18 +00:00
this . maximize ( ) ;
this . present _with _time ( timestamp ) ;
2017-01-17 14:01:01 +00:00
this . application . set _accels _for _action ( 'app.quit' , [ '<Primary>q' , '<Primary>w' ] ) ;
2014-02-17 16:19:18 +00:00
} ,
2017-01-23 06:14:17 +00:00
destroyWindow : function ( ) {
this . destroy ( ) ;
} ,
2017-01-23 07:32:30 +00:00
_syncUri : function ( ) {
let uri = this . _webView . uri ;
if ( uri )
2017-02-10 20:08:01 +00:00
this . _headerBar . setSubtitle ( GLib . uri _unescape _string ( uri , null ) ) ;
2017-01-23 07:32:30 +00:00
else
2017-02-13 10:10:19 +00:00
this . _headerBar . setSubtitle ( '' ) ;
2014-02-17 16:19:18 +00:00
} ,
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 ;
} ,
2017-02-13 10:11:26 +00:00
_onLoadChanged : function ( view , loadEvent ) {
2017-03-23 17:27:40 +00:00
if ( loadEvent == WebKit . LoadEvent . STARTED ) {
2017-02-10 20:08:01 +00:00
this . _headerBar . setSecurityIcon ( PortalHelperSecurityLevel . NOT _YET _DETERMINED ) ;
2017-03-23 17:27:40 +00:00
} else if ( loadEvent == WebKit . LoadEvent . COMMITTED ) {
2017-02-10 20:08:01 +00:00
let tlsInfo = this . _webView . get _tls _info ( ) ;
let ret = tlsInfo [ 0 ] ;
let flags = tlsInfo [ 2 ] ;
if ( ret && flags == 0 )
this . _headerBar . setSecurityIcon ( PortalHelperSecurityLevel . SECURE ) ;
else
this . _headerBar . setSecurityIcon ( PortalHelperSecurityLevel . INSECURE ) ;
}
} ,
2017-02-13 10:11:26 +00:00
_onInsecureContentDetected : function ( ) {
2017-02-10 20:08:01 +00:00
this . _headerBar . setSecurityIcon ( PortalHelperSecurityLevel . INSECURE ) ;
} ,
2017-02-13 09:44:01 +00:00
_onLoadFailedWithTlsErrors : function ( view , failingURI , certificate , errors ) {
this . _headerBar . setSecurityIcon ( PortalHelperSecurityLevel . INSECURE ) ;
let uri = new Soup . URI ( failingURI ) ;
this . _webContext . allow _tls _certificate _for _host ( certificate , uri . get _host ( ) ) ;
this . _webView . load _uri ( failingURI ) ;
return true ;
} ,
2014-02-17 16:19:18 +00:00
_onDecidePolicy : function ( view , decision , type ) {
if ( type == WebKit . PolicyDecisionType . NEW _WINDOW _ACTION ) {
2017-03-24 17:57:29 +00:00
let navigationAction = decision . get _navigation _action ( ) ;
if ( navigationAction . is _user _gesture ( ) ) {
// Even though the portal asks for a new window,
// perform the navigation in the current one. Some
// portals open a window as their last login step and
// ignoring that window causes them to not let the
// user go through. We don't risk popups taking over
// the page because we check that the navigation is
// user initiated.
this . _webView . load _request ( navigationAction . get _request ( ) ) ;
}
2014-02-17 16:19:18 +00:00
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 ( ) ) ;
2014-07-28 07:36:40 +00:00
if ( ! uri . host _equal ( this . _uri ) && this . _originalUrlWasGnome ) {
2016-10-24 14:58:24 +00:00
if ( uri . get _host ( ) == CONNECTIVITY _CHECK _HOST && this . _everSeenRedirect ) {
2014-02-17 16:19:18 +00:00
// Yay, we got to gnome!
decision . ignore ( ) ;
this . _doneCallback ( PortalHelperResult . COMPLETED ) ;
return true ;
2016-10-24 14:58:24 +00:00
} else if ( uri . get _host ( ) != CONNECTIVITY _CHECK _HOST ) {
2014-02-17 16:19:18 +00:00
this . _everSeenRedirect = true ;
}
}
2014-07-28 07:36:40 +00:00
// 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 ;
2014-02-17 16:19:18 +00:00
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 = [ ] ;
2017-01-17 14:01:01 +00:00
let action = new Gio . SimpleAction ( { name : 'quit' } ) ;
2017-01-23 06:14:17 +00:00
action . connect ( 'activate' , ( ) => { this . active _window . destroyWindow ( ) ; } ) ;
2017-01-17 14:01:01 +00:00
this . add _action ( action ) ;
2014-02-17 16:19:18 +00:00
} ,
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 )
2017-01-23 06:14:17 +00:00
obj . window . destroyWindow ( ) ;
2014-02-17 16:19:18 +00:00
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 ( ) ;
2017-03-23 15:54:28 +00:00
if ( ! WebKit . WebContext . new _ephemeral ) {
log ( 'WebKitGTK 2.16 is required for the portal-helper, see https://bugzilla.gnome.org/show_bug.cgi?id=780453' ) ;
return 1 ;
}
2014-02-17 16:19:18 +00:00
Gettext . bindtextdomain ( Config . GETTEXT _PACKAGE , Config . LOCALEDIR ) ;
Gettext . textdomain ( Config . GETTEXT _PACKAGE ) ;
let app = new WebPortalHelper ( ) ;
return app . run ( argv ) ;
}