diff options
author | ajwong@chromium.org <ajwong@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-02-24 23:13:21 +0000 |
---|---|---|
committer | ajwong@chromium.org <ajwong@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-02-24 23:13:21 +0000 |
commit | 999d862cc8675b78fdeab5b0a4b1689b0505f276 (patch) | |
tree | 316c8f12dc260addddf029fb249a286d3b68cc82 /remoting/client/appengine | |
parent | 6ee62b63cec739d231bf451168219576df623e20 (diff) | |
download | chromium_src-999d862cc8675b78fdeab5b0a4b1689b0505f276.zip chromium_src-999d862cc8675b78fdeab5b0a4b1689b0505f276.tar.gz chromium_src-999d862cc8675b78fdeab5b0a4b1689b0505f276.tar.bz2 |
Redo the Chromoting extension as an app-engine app.
This will allow for an easier develop/deploy cycle in the short term, which
will facilitate development.
BUG=none
TEST=none
Review URL: http://codereview.chromium.org/6580022
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@75987 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting/client/appengine')
-rw-r--r-- | remoting/client/appengine/api.py | 67 | ||||
-rw-r--r-- | remoting/client/appengine/app.yaml | 22 | ||||
-rw-r--r-- | remoting/client/appengine/auth.py | 320 | ||||
-rw-r--r-- | remoting/client/appengine/chromoting_oauth_setup.html | 23 | ||||
-rw-r--r-- | remoting/client/appengine/chromoting_session.html | 61 | ||||
-rw-r--r-- | remoting/client/appengine/client_login.html | 40 | ||||
-rw-r--r-- | remoting/client/appengine/hostlist.html | 51 | ||||
-rw-r--r-- | remoting/client/appengine/main.py | 56 | ||||
-rw-r--r-- | remoting/client/appengine/static_files/base.js | 32 | ||||
-rw-r--r-- | remoting/client/appengine/static_files/chromoticon.png | bin | 0 -> 332 bytes | |||
-rw-r--r-- | remoting/client/appengine/static_files/chromoting_session.js | 190 | ||||
-rw-r--r-- | remoting/client/appengine/static_files/client.js | 357 | ||||
-rw-r--r-- | remoting/client/appengine/static_files/machine.png | bin | 0 -> 4992 bytes | |||
-rw-r--r-- | remoting/client/appengine/static_files/main.css | 157 |
14 files changed, 1376 insertions, 0 deletions
diff --git a/remoting/client/appengine/api.py b/remoting/client/appengine/api.py new file mode 100644 index 0000000..1b48da0 --- /dev/null +++ b/remoting/client/appengine/api.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +# Copyright (c) 2011 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""API endpoints to get around javascript's single-origin restriction.""" + +import logging + +from django.utils import simplejson as json + +import gdata.client + +from google.appengine.ext import webapp +from google.appengine.ext.webapp import util +from google.appengine.ext.webapp.util import login_required +import auth + + +class GetXmppTokenHandler(webapp.RequestHandler): + """Retrieves the user's XMPP token.""" + @login_required + def get(self): + try: + self.response.headers['Content-Type'] = 'application/json' + self.response.out.write( + json.dumps({'xmpp_token': auth.GetXmppToken().token})) + except auth.NotAuthenticated: + self.response.out.write('User has not authenticated') + self.set_status(400) + return + pass + + +class GetHostListHandler(webapp.RequestHandler): + """Proxies the host-list handlers on the Chromoting directory.""" + @login_required + def get(self): + try: + client = gdata.client.GDClient() + host_list_json = client.Request( + method='GET', + uri="https://www.googleapis.com/chromoting/v1/@me/hosts", + converter=None, + desired_class=None, + auth_token=auth.GetChromotingToken()) + self.response.headers['Content-Type'] = 'application/json' + self.response.out.write(host_list_json.read()) + except auth.NotAuthenticated: + self.response.out.write('User has not authenticated') + self.response.set_status(400) + return + + +def main(): + application = webapp.WSGIApplication( + [ + ('/api/get_xmpp_token', GetXmppTokenHandler), + ('/api/get_host_list', GetHostListHandler) + ], + debug=True) + util.run_wsgi_app(application) + + +if __name__ == '__main__': + main() diff --git a/remoting/client/appengine/app.yaml b/remoting/client/appengine/app.yaml new file mode 100644 index 0000000..ff9db53 --- /dev/null +++ b/remoting/client/appengine/app.yaml @@ -0,0 +1,22 @@ +application: google.com:chromoting +version: 1 +runtime: python +api_version: 1 + +handlers: +- url: /static_files + static_dir: static_files + secure: always + +- url: /api/.* + script: api.py + secure: always + +- url: /auth/.* + script: auth.py + secure: always + +- url: .* + script: main.py + secure: always + diff --git a/remoting/client/appengine/auth.py b/remoting/client/appengine/auth.py new file mode 100644 index 0000000..f40cbdd --- /dev/null +++ b/remoting/client/appengine/auth.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python + +# Copyright (c) 2011 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Provides authentcation related utilities and endpoint handlers. + +All authentication code for the webapp should go through this module. In +general, credentials should be used server-side. The URL endpoints are for +initiating authentication flows, and for managing credential storage per user. +""" + +import os + +import gdata.gauth +import gdata.client + +from google.appengine.ext import db +from google.appengine.api import users +from google.appengine.ext import webapp +from google.appengine.ext.webapp import template +from google.appengine.ext.webapp import util +from google.appengine.ext.webapp.util import login_required + + +SCOPES = ['https://www.googleapis.com/auth/chromoting', + 'https://www.googleapis.com/auth/googletalk' ] + + +class NotAuthenticated(Exception): + """API requiring authentication is called with credentials.""" + pass + + +class OAuthInvalidSetup(Exception): + """OAuth configuration on app is not complete.""" + pass + + +class OAuthConfig(db.Model): + """Stores the configuration data for OAuth. + + Currently used to store the consumer key and secret so that it does not need + to be checked into the source tree. + """ + consumer_key = db.StringProperty() + consumer_secret = db.StringProperty() + + +def GetChromotingToken(throws=True): + """Retrieves the Chromoting OAuth token for the user. + + Args: + throws: bool (optional) Default is True. Throws if no token. + + Returns: + An gdata.gauth.OAuthHmacToken for the current user. + """ + user = users.get_current_user() + access_token = None + if user: + access_token = LoadToken('chromoting_token') + if throws and not access_token: + raise NotAuthenticated() + return access_token + + +def GetXmppToken(throws=True): + """Retrieves the XMPP for Chromoting. + + Args: + throws: bool (optional) Default is True. Throws if no token. + + Returns: + An gdata.gauth.ClientLoginToken for the current user. + """ + user = users.get_current_user() + access_token = None + if user: + access_token = LoadToken('xmpp_token') + if throws and not access_token: + raise NotAuthenticated() + return access_token + + +def ClearChromotingToken(): + """Clears all Chromoting OAuth token state from the datastore.""" + DeleteToken('request_token') + DeleteToken('chromoting_token') + + +def ClearXmppToken(): + """Clears all Chromoting ClientLogin token state from the datastore.""" + DeleteToken('xmpp_token') + + +def GetUserId(): + """Retrieves the user id for the current user. + + Returns: + A string with the user id of the logged in user. + + Raises: + NotAuthenticated if the user is not logged in, or missing an id. + """ + user = users.get_current_user() + if not user: + raise NotAuthenticated() + + if not user.user_id(): + raise NotAuthenticated('no e-mail with google account!') + + return user.user_id() + + +def LoadToken(name): + """Leads a gdata auth token for the current user. + + Tokens are scoped to each user, and retrieved by a name. + + Args: + name: A string with the name of the token for the current user. + + Returns: + The token associated with the name for the user. + """ + user_id = GetUserId(); + return gdata.gauth.AeLoad(user_id + name) + + +def SaveToken(name, token): + """Saves a gdata auth token for the current user. + + Tokens are scoped to each user, and stored by a name. + + Args: + name: A string with the name of the token. + """ + user_id = GetUserId(); + gdata.gauth.AeSave(token, user_id + name) + + +def DeleteToken(name): + """Deletes a stored gdata auth token for the current user. + + Tokens are scoped to each user, and stored by a name. + + Args: + name: A string with the name of the token. + """ + user_id = GetUserId(); + gdata.gauth.AeDelete(user_id + name) + + +def OAuthConfigKey(): + """Generates a standard key path for this app's OAuth configuration.""" + return db.Key.from_path('OAuthConfig', 'oauth_config') + + +def GetOAuthConfig(throws=True): + """Retrieves the OAuthConfig for this app. + + Returns: + The OAuthConfig object for this app. + + Raises: + OAuthInvalidSetup if no OAuthConfig exists. + """ + config = db.get(OAuthConfigKey()) + if throws and not config: + raise OAuthInvalidSetup() + return config + + +class ChromotingAuthHandler(webapp.RequestHandler): + """Initiates getting the OAuth access token for the user. + + This webapp uses 3-legged OAuth. This handlers performs the first step + of getting the OAuth request token, and then forwarding on to the + Google Accounts authorization endpoint for the second step. The final + step is completed by the ChromotingAuthReturnHandler below. + + FYI, all three steps are collectively known as the "OAuth dance." + """ + @login_required + def get(self): + ClearChromotingToken() + client = gdata.client.GDClient() + + oauth_callback_url = ('http://%s/auth/chromoting_auth_return' % + self.request.host) + request_token = client.GetOAuthToken( + SCOPES, oauth_callback_url, GetOAuthConfig().consumer_key, + consumer_secret=GetOAuthConfig().consumer_secret) + + SaveToken('request_token', request_token) + domain = None # Not on an Google Apps domain. + auth_uri = request_token.generate_authorization_url() + self.redirect(str(auth_uri)) + + +class ChromotingAuthReturnHandler(webapp.RequestHandler): + """Finishes the authorization started in ChromotingAuthHandler.i + + After the user authorizes the OAuth request token at the OAuth request + URL they were redirected to in ChromotingAuthHandler, OAuth will send + them back here with an auth token in the URL. + + This handler retrievies the access token, and stores it completing the + OAuth dance. + """ + @login_required + def get(self): + saved_request_token = LoadToken('request_token') + DeleteToken('request_token') + request_token = gdata.gauth.AuthorizeRequestToken( + saved_request_token, self.request.uri) + + # Upgrade the token and save in the user's datastore + client = gdata.client.GDClient() + access_token = client.GetAccessToken(request_token) + SaveToken('chromoting_token', access_token) + self.redirect("/") + + +class XmppAuthHandler(webapp.RequestHandler): + """Prompts Google Accounts credentials and retrieves a ClientLogin token. + + This class takes the user's plaintext username and password, and then + posts a request to ClientLogin to get the access token. + + THIS CLASS SHOULD NOT EXIST. + + We should NOT be taking a user's Google Accounts credentials in our webapp. + However, we need a ClientLogin token for jingle, and this is currently the + only known workaround. + """ + @login_required + def get(self): + ClearXmppToken() + path = os.path.join(os.path.dirname(__file__), 'client_login.html') + self.response.out.write(template.render(path, {})) + + def post(self): + client = gdata.client.GDClient() + email = self.request.get('username') + password = self.request.get('password') + try: + client.ClientLogin( + email, password, 'chromoclient', 'chromiumsync') + SaveToken('xmpp_token', client.auth_token) + except gdata.client.CaptchaChallenge: + self.response.out.write('You need to solve a Captcha. ' + 'Unforutnately, we still have to implement that.') + self.redirect('/') + + +class ClearChromotingTokenHandler(webapp.RequestHandler): + """Endpoint for dropping the user's Chromoting token.""" + @login_required + def get(self): + ClearChromotingToken() + self.redirect('/') + + +class ClearXmppTokenHandler(webapp.RequestHandler): + """Endpoint for dropping the user's Xmpp token.""" + @login_required + def get(self): + ClearXmppToken() + self.redirect('/') + + +class SetupOAuthHandler(webapp.RequestHandler): + """Administrative page for specifying the OAuth consumer key/secret.""" + @login_required + def get(self): + path = os.path.join(os.path.dirname(__file__), + 'chromoting_oauth_setup.html') + self.response.out.write(template.render(path, {})) + + def post(self): + old_consumer_secret = self.request.get('old_consumer_secret') + + query = OAuthConfig.all() + + # If there is an existing key, only allow updating if you know the old + # key. This is a simple safeguard against random users hitting this page. + config = GetOAuthConfig(throws=False) + if config: + if config.consumer_secret != old_consumer_secret: + self.response.out.set_status(400) + self.response.out.write('Incorrect old consumer secret') + return + else: + config = OAuthConfig(key_name = OAuthConfigKey().id_or_name()) + + config.consumer_key = self.request.get('consumer_key') + config.consumer_secret = self.request.get('new_consumer_secret') + config.put() + self.redirect('/') + + +def main(): + application = webapp.WSGIApplication( + [ + ('/auth/chromoting_auth', ChromotingAuthHandler), + ('/auth/xmpp_auth', XmppAuthHandler), + ('/auth/chromoting_auth_return', ChromotingAuthReturnHandler), + ('/auth/clear_xmpp_token', ClearXmppTokenHandler), + ('/auth/clear_chromoting_token', ClearChromotingTokenHandler), + ('/auth/setup_oauth', SetupOAuthHandler) + ], + debug=True) + util.run_wsgi_app(application) + + +if __name__ == '__main__': + main() diff --git a/remoting/client/appengine/chromoting_oauth_setup.html b/remoting/client/appengine/chromoting_oauth_setup.html new file mode 100644 index 0000000..3fc4664 --- /dev/null +++ b/remoting/client/appengine/chromoting_oauth_setup.html @@ -0,0 +1,23 @@ +<!doctype html> +<!-- +Copyright (c) 2011 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<html> + <head> + <title>Chromoting OAuth setup page.</title> + </head> + <body> + <p>This is an administration page to setup the consumer key and secret + to be used with chromoting. Please don't play with this unless you are + on the Chromoting dev team. + <form method="post" name="auth"> + Consumer Key: <input type="text" name="consumer_key" /> + New Consumer Secret: <input type="password" name="new_consumer_secret" /> + Old Consumer Secret: <input type="password" name="old_consumer_secret" /> + <input type="submit" value="Submit" /> + </form> + </body> +</html> diff --git a/remoting/client/appengine/chromoting_session.html b/remoting/client/appengine/chromoting_session.html new file mode 100644 index 0000000..2d26b45 --- /dev/null +++ b/remoting/client/appengine/chromoting_session.html @@ -0,0 +1,61 @@ +<!doctype html> +<!-- +Copyright (c) 2011 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<html> + <head> + <title id="title">Chromoting Session</title> + <link rel="stylesheet" type="text/css" href="static_files/main.css" /> + <script type="text/javascript"> + <!-- + // TODO(ajwong): Total Hack. Do this nicer. + document.xmpp_auth_token="{{xmpp_token.token_string}}"; + document.username="{{username}}"; + document.hostname="{{hostname}}"; + document.hostjid="{{hostjid}}"; + --> + </script> + <script type="text/javascript" src="static_files/base.js"></script> + <script type="text/javascript" src="static_files/chromoting_session.js"> + </script> + </head> + <body class="chromoting_body" onload="init();"> + <div id="status_msg" class="status_msg"></div> + <div id="login_panel" class="local_login_panel"> + <table><tr><td valign="top" + style="text-align:center" nowrap="nowrap" bgcolor="#e8eefa"> + <table align="center" border="0" cellpadding="1" cellspacing="0"> + <tr> + <td align="center" colspan="2"> + Sign in to remote host + </td> + </tr><tr> + <td align="right" nowrap="nowrap"> + <span class="gaia_font">Username:</span> + </td><td> + <input type="text" id="username" value="" class="gaia_font"/> + </td> + </tr><tr> + <td align="right" nowrap="nowrap"> + <span class="gaia_font">Password:</span> + </td> + <td> + <input type="password" id="password" value="" class="gaia_font"/> + </td> + </tr><tr> + <td></td> + <td><input type="button" value="Sign in" class="gaia_font" + onclick="submitLogin();"/></td> + </tr> + </table> + </td></tr></table> + </div> + <div id="plugin_scroll_panel" class="plugin-scroll-panel"> + <embed name="chromoting" id="chromoting" + src="about://none" type="pepper-application/x-chromoting"> + </div> + </body> +</html> diff --git a/remoting/client/appengine/client_login.html b/remoting/client/appengine/client_login.html new file mode 100644 index 0000000..9284109 --- /dev/null +++ b/remoting/client/appengine/client_login.html @@ -0,0 +1,40 @@ +<!doctype html> +<!-- +Copyright (c) 2011 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<html> + <head> + <title>Chromoting Google Accounts Login (needed for XMPP)</title> + </head> + + <body> + <p>Please provide your Google Accounts username/password to get a + ClientLogin token for Google Talk. + + <p> + <form method="post" name="auth"> + Username: <input type="text" name="username" /><br /> + Password: <input type="password" name="password" /><br /> + <input type="submit" value="Submit" /> + </form> + + <hr /> + <p> + Yes, we know we're asking for your Google Accounts credentials in a webapp. + + <p> + No we're not happy with this. + + <p> + Yes, we (think we) need to do it this way. + + <p> + libjingle needs a ClientLogin token and due to single-origin issues, we + have to proxy the request through the webapp. If anyone has a better idea, + please <b>please</b> <b><i>please</i></b> let us know. + + </body> +</html> diff --git a/remoting/client/appengine/hostlist.html b/remoting/client/appengine/hostlist.html new file mode 100644 index 0000000..2f5eb95 --- /dev/null +++ b/remoting/client/appengine/hostlist.html @@ -0,0 +1,51 @@ +<!-- +Copyright (c) 2011 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<html> + <head> + <script type="text/javascript" src="static_files/base.js"></script> + <script type="text/javascript" src="static_files/client.js"></script> + <link rel="stylesheet" type="text/css" href="static_files/main.css" /> + <title>Remote Access Host List</title> + </head> + <body class="hostlist_body" +{% ifnotequal chromoting_token None %} + onload="populateHostList();" +{% endifnotequal %} +> + + <h1>Remote Access</h1> + + <div id="auth_status" class="authstatus"> + <p>Chromoting Token: +{% ifnotequal chromoting_token None %} + OK (<a href="/auth/clear_chromoting_token">clear token</a>) +{% else %} + <a href="/auth/chromoting_auth">Not Authenticated</a> +{% endifnotequal %} + <p>Xmpp Token: +{% ifnotequal xmpp_token None %} + OK (<a href="/auth/clear_xmpp_token">clear token</a>) +{% else %} + <a href="/auth/xmpp_auth">Not Authenticated</a> +{% endifnotequal %} + </div> + <hr /> + + <p class="reload"> + <a href="javascript:populateHostList()">Reload host list</a> + </p> + + <input type=checkbox name="show_offline" id="show_offline" + onClick="updateShowOfflineHosts(this)"/>Show offline hosts + + <div id="hostlist_div" class="hostlist"> + <p class='message'>Initializing...</p> + </div> + + <br /> + </body> +</html> diff --git a/remoting/client/appengine/main.py b/remoting/client/appengine/main.py new file mode 100644 index 0000000..78900c2 --- /dev/null +++ b/remoting/client/appengine/main.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +import logging +import os + +from django.utils import simplejson as json + +import gdata.gauth +import gdata.client + +from google.appengine.api import users +from google.appengine.ext import webapp +from google.appengine.ext.webapp import template +from google.appengine.ext.webapp import util +from google.appengine.ext.webapp.util import login_required + +import auth + +class HostListHandler(webapp.RequestHandler): + """Renders the main hostlist page.""" + @login_required + def get(self): + template_params = { + 'chromoting_token': auth.GetChromotingToken(throws=False), + 'xmpp_token': auth.GetXmppToken(throws=False) + } + path = os.path.join(os.path.dirname(__file__), 'hostlist.html') + self.response.out.write(template.render(path, template_params)) + + +class ChromotingSessionHandler(webapp.RequestHandler): + """Renders one Chromoting session.""" + @login_required + def get(self): + template_params = { + 'hostname': self.request.get('hostname'), + 'username': users.get_current_user().email(), + 'hostjid': self.request.get('hostjid'), + 'xmpp_token': auth.GetXmppToken(), + } + path = os.path.join(os.path.dirname(__file__), 'chromoting_session.html') + self.response.out.write(template.render(path, template_params)) + + +def main(): + application = webapp.WSGIApplication( + [ + ('/', HostListHandler), + ('/session', ChromotingSessionHandler), + ], + debug=True) + util.run_wsgi_app(application) + + +if __name__ == '__main__': + main() diff --git a/remoting/client/appengine/static_files/base.js b/remoting/client/appengine/static_files/base.js new file mode 100644 index 0000000..2c3188d --- /dev/null +++ b/remoting/client/appengine/static_files/base.js @@ -0,0 +1,32 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Namespace for globals. +var chromoting = {}; + +// Cookie reading code taken from quirksmode with modification for escaping. +// http://www.quirksmode.org/js/cookies.html +function setCookie(name, value, days) { + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days*24*60*60*1000)); + var expires = "; expires="+date.toGMTString(); + } else { + var expires = ""; + } + document.cookie = name+"="+escape(value)+expires+"; path=/"; +} + +function getCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for (var i=0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0)==' ') + c = c.substring(1, c.length); + if (c.indexOf(nameEQ) == 0) + return unescape(c.substring(nameEQ.length, c.length)); + } + return null; +} diff --git a/remoting/client/appengine/static_files/chromoticon.png b/remoting/client/appengine/static_files/chromoticon.png Binary files differnew file mode 100644 index 0000000..a5cb2b2 --- /dev/null +++ b/remoting/client/appengine/static_files/chromoticon.png diff --git a/remoting/client/appengine/static_files/chromoting_session.js b/remoting/client/appengine/static_files/chromoting_session.js new file mode 100644 index 0000000..18ee990 --- /dev/null +++ b/remoting/client/appengine/static_files/chromoting_session.js @@ -0,0 +1,190 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Message id so that we can identify (and ignore) message fade operations for +// old messages. This starts at 1 and is incremented for each new message. +chromoting.messageId = 1; + +function init() { + // Kick off the connection. + var plugin = document.getElementById('chromoting'); + + chromoting.plugin = plugin; + chromoting.username = document.username; + chromoting.hostname = document.hostname; + chromoting.hostjid = document.hostjid; + + // Setup the callback that the plugin will call when the connection status + // has changes and the UI needs to be updated. It needs to be an object with + // a 'callback' property that contains the callback function. + plugin.connectionInfoUpdate = pluginCallback; + plugin.loginChallenge = pluginLoginChallenge; + + console.log('connect request received: ' + chromoting.hostname + ' by ' + + chromoting.username); + + // TODO(garykac): Clean exit if |connect| isn't a funtion. + if (typeof plugin.connect === 'function') { + plugin.connect(chromoting.username, chromoting.hostjid, + document.xmpp_auth_token); + } else { + console.log('ERROR: chromoting plugin not loaded'); + } + + document.getElementById('title').innerText = chromoting.hostname; +} + +function submitLogin() { + var username = document.getElementById("username").value; + var password = document.getElementById("password").value; + + // Make the login panel invisible and submit login info. + document.getElementById("login_panel").style.display = "none"; + chromoting.plugin.submitLoginInfo(username, password); +} + +/** + * This is the callback method that the plugin calls to request username and + * password for logging into the remote host. + */ +function pluginLoginChallenge() { + // Make the login panel visible. + document.getElementById("login_panel").style.display = "block"; +} + +/** + * This is a callback that gets called when the desktop size contained in the + * the plugin has changed. + */ +function desktopSizeChanged() { + var width = chromoting.plugin.desktopWidth; + var height = chromoting.plugin.desktopHeight; + + console.log('desktop size changed: ' + width + 'x' + height); + chromoting.plugin.style.width = width + "px"; + chromoting.plugin.style.height = height + "px"; +} + +/** + * Show a client message on the screen. + * If duration is specified, the message fades out after the duration expires. + * Otherwise, the message stays until the state changes. + * + * @param {string} message The message to display. + * @param {number} duration Milliseconds to show message before fading. + */ +function showClientStateMessage(message, duration) { + // Increment message id to ignore any previous fadeout requests. + chromoting.messageId++; + console.log('setting message ' + chromoting.messageId + '!'); + + // Update the status message. + var msg = document.getElementById('status_msg'); + msg.innerText = message; + msg.style.opacity = 1; + msg.style.display = ''; + + if (duration) { + // Set message duration. + window.setTimeout("fade('status_msg', " + chromoting.messageId + ", " + + "100, 10, 200)", + duration); + } +} + +/** + * This is that callback that the plugin invokes to indicate that the + * host/client connection status has changed. + */ +function pluginCallback() { + var status = chromoting.plugin.status; + var quality = chromoting.plugin.quality; + + if (status == chromoting.plugin.STATUS_UNKNOWN) { + setClientStateMessage(''); + } else if (status == chromoting.plugin.STATUS_CONNECTING) { + setClientStateMessage('Connecting to ' + chromoting.hostname + + ' as ' + chromoting.username); + } else if (status == chromoting.plugin.STATUS_INITIALIZING) { + setClientStateMessageFade('Initializing connection to ' + + chromoting.hostname); + } else if (status == chromoting.plugin.STATUS_CONNECTED) { + desktopSizeChanged(); + setClientStateMessageFade('Connected to ' + chromoting.hostname, 1000); + } else if (status == chromoting.plugin.STATUS_CLOSED) { + setClientStateMessage('Closed'); + } else if (status == chromoting.plugin.STATUS_FAILED) { + setClientStateMessage('Failed'); + } +} + +/** + * Show a client message that stays on the screeen until the state changes. + * + * @param {string} message The message to display. + */ +function setClientStateMessage(message) { + // Increment message id to ignore any previous fadeout requests. + chromoting.messageId++; + console.log('setting message ' + chromoting.messageId); + + // Update the status message. + var msg = document.getElementById('status_msg'); + msg.innerText = message; + msg.style.opacity = 1; + msg.style.display = ''; +} + +/** + * Show a client message for the specified amount of time. + * + * @param {string} message The message to display. + * @param {number} duration Milliseconds to show message before fading. + */ +function setClientStateMessageFade(message, duration) { + setClientStateMessage(message); + + // Set message duration. + window.setTimeout("fade('status_msg', " + chromoting.messageId + ", " + + "100, 10, 200)", + duration); +} + +/** + * Fade the specified element. + * For example, to have element 'foo' fade away over 2 seconds, you could use + * either: + * fade('foo', 100, 10, 200) + * - Start at 100%, decrease by 10% each time, wait 200ms between updates. + * fade('foo', 100, 5, 100) + * - Start at 100%, decrease by 5% each time, wait 100ms between updates. + * + * @param {string} name Name of element to fade. + * @param {number} id The id of the message associated with this fade request. + * @param {number} val The new opacity value (0-100) for this element. + * @param {number} delta Amount to adjust the opacity each iteration. + * @param {number} delay Delay (in ms) to wait between each update. + */ +function fade(name, id, val, delta, delay) { + // Ignore the fade call if it does not apply to the current message. + if (id != chromoting.messageId) { + return; + } + + var e = document.getElementById(name); + if (e) { + var newVal = val - delta; + if (newVal > 0) { + // Decrease opacity and set timer for next fade event. + e.style.opacity = newVal / 100; + window.setTimeout("fade('status_msg', " + id + ", " + newVal + ", " + + delta + ", " + delay + ")", + delay); + } else { + // Completely hide the text and stop fading. + e.style.opacity = 0; + e.style.display = 'none'; + } + } +} diff --git a/remoting/client/appengine/static_files/client.js b/remoting/client/appengine/static_files/client.js new file mode 100644 index 0000000..067cedf --- /dev/null +++ b/remoting/client/appengine/static_files/client.js @@ -0,0 +1,357 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Flag to indicate whether or not to show offline hosts in the host list. +chromoting.showOfflineHosts = true; + +// String to identify bad auth tokens. +var BAD_AUTH_TOKEN = 'bad_token'; + +// Number of days before auth token cookies expire. +// TODO(garykac): 21 days is arbitrary. Need to change this to the appropriate +// value from security review. +var AUTH_EXPIRES = 21; + +function init() { + updateLoginStatus(); + + // Defer getting the host list for a little bit so that we don't + // block the display of the extension popup. + window.setTimeout(populateHostList, 100); + + var showOff = getCookie('offline'); + chromoting.showOfflineHosts = (!showOff || showOff == '1'); + updateShowOfflineCheckbox(); +} + +function updateShowOfflineHosts(cbox) { + chromoting.showOfflineHosts = cbox.checked; + + // Save pref in cookie with long expiration. + setCookie('offline', chromoting.showOfflineHosts ? '1' : '0', 1000); + + populateHostList(); +} + +function updateShowOfflineCheckbox() { + var cbox = document.getElementById('show_offline'); + cbox.checked = chromoting.showOfflineHosts; +} + +// Update the login status region (at the bottom of the popup) with the +// current account and links to sign in/out. +function updateLoginStatus() { + var username = getCookie('username'); + + var loginDiv = document.getElementById('login_div'); + clear(loginDiv); + + if (!username) { + var signinLink = document.createElement('a'); + signinLink.setAttribute('href', 'javascript:showDirectoryLogin();'); + signinLink.appendChild(document.createTextNode('Sign In')); + loginDiv.appendChild(signinLink); + } else { + var email = document.createElement('span'); + email.setAttribute('class', 'login_email'); + email.appendChild(document.createTextNode(username)); + loginDiv.appendChild(email); + + loginDiv.appendChild(document.createTextNode(' | ')); + + var signoutLink = document.createElement('a'); + signoutLink.setAttribute('href', 'javascript:logoutAndReload(this.form);'); + signoutLink.appendChild(document.createTextNode('Sign Out')); + loginDiv.appendChild(signoutLink); + } +} + +// Sign out the current user and reload the host list. +function logoutAndReload(form) { + logout(); + populateHostList(); +} + +// Sign out the current user by erasing the auth cookies. +function logout() { + setCookie('username', '', AUTH_EXPIRES); + setCookie('chromoting_auth', '', AUTH_EXPIRES); + setCookie('xmpp_auth', '', AUTH_EXPIRES); + + updateLoginStatus(); +} + +// Sign in to Chromoting Directory services. +function showDirectoryLogin() { + document.getElementById("login_panel").style.display = "block"; +} + +function login() { + var username = document.getElementById("username").value; + var password = document.getElementById("password").value; + + doLogin(username, password, checkLogin); +} + +// Check to see if the login was successful. +function checkLogin() { + var username = getCookie('username'); + var cauth = getCookie('chromoting_auth'); + var xauth = getCookie('xmpp_auth'); + + // Verify login and show login status. + if (cauth == BAD_AUTH_TOKEN || xauth == BAD_AUTH_TOKEN) { + // Erase the username cookie. + setCookie('username', '', AUTH_EXPIRES); + showLoginError("Sign in failed!"); + } else { + // Successful login - update status and update host list. + updateLoginStatus(); + populateHostList(); + + // Hide login dialog and clear out values. + document.getElementById('login_panel').style.display = "none"; + document.getElementById('username').value = ""; + document.getElementById('password').value = ""; + } +} + +function doLogin(username, password, done) { + // Don't call |done| callback until both login requests have completed. + var count = 2; + var barrier = function() { + count--; + if (done && count == 0) { + done(); + } + } + setCookie('username', username, AUTH_EXPIRES); + doGaiaLogin(username, password, 'chromoting', + function(cAuthToken) { + setCookie('chromoting_auth', cAuthToken, AUTH_EXPIRES); + barrier(); + }); + doGaiaLogin(username, password, 'chromiumsync', + function(xAuthToken) { + setCookie('xmpp_auth', xAuthToken, AUTH_EXPIRES); + barrier(); + }); +} + +function doGaiaLogin(username, password, service, done) { + var xhr = new XMLHttpRequest(); + xhr.open('POST', 'https://www.google.com/accounts/ClientLogin', true); + xhr.onreadystatechange = function() { + if (xhr.readyState != 4) { + return; + } + if (xhr.status = 200) { + done(extractAuthToken(xhr.responseText)); + } else { + console.log('Bad status on auth: ' + xhr.statusText); + } + }; + + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + xhr.send('accountType=HOSTED_OR_GOOGLE&Email=' + + encodeURIComponent(username) + '&Passwd=' + + encodeURIComponent(password) + '&service=' + + encodeURIComponent(service) + '&source=chromoclient'); +} + +function showLoginError() { + var errorDiv = document.getElementById('errormsg_div'); + clear(errorDiv); + + errorDiv.appendChild(document.createTextNode( + "The username or password you entered is incorrect [")); + + var helpLink = document.createElement('a'); + helpLink.setAttribute('href', + 'http://www.google.com/support/accounts/bin/answer.py?answer=27444'); + helpLink.setAttribute('target', '_blank'); + helpLink.appendChild(document.createTextNode('?')); + errorDiv.appendChild(helpLink); + + errorDiv.appendChild(document.createTextNode("]")); +} + +function extractAuthToken(message) { + var lines = message.split('\n'); + for (var i = 0; i < lines.length; i++) { + if (lines[i].match('^Auth=.*')) { + return lines[i].split('=')[1]; + } + } + + console.log('Could not parse auth token in : "' + message + '"'); + return BAD_AUTH_TOKEN; +} + +// Open a chromoting connection in a new tab. +function openChromotingTab(hostName, hostJid) { + var background = chrome.extension.getBackgroundPage(); + background.openChromotingTab(hostName, hostJid); +} + +// Erase the content of the specified element. +function clear(e) { + e.innerHTML = ''; +} + +// Clear out the specified element and show the message to the user. +function displayMessage(e, classname, message) { + clear(e); + appendMessage(e, classname, message); +} + +// Append the message text to the specified element. +function appendMessage(e, classname, message) { + var p = document.createElement('p'); + if (classname.length != 0) { + p.setAttribute('class', classname); + } + + p.appendChild(document.createTextNode(message)); + + e.appendChild(p); +} + +function populateHostList() { + var hostlistDiv = document.getElementById('hostlist_div'); + displayMessage(hostlistDiv, 'message', + 'Hosts will appaer if Chromoting Token is "OK".'); + + var xhr = new XMLHttpRequest(); + // Unhide host list. + hostlistDiv.style.display = "block"; + + xhr.onreadystatechange = function() { + if (xhr.readyState == 1) { + displayMessage(hostlistDiv, 'message', 'Loading host list'); + } + if (xhr.readyState != 4) { + return; + } + if (xhr.status == 200) { + var parsed_response = JSON.parse(xhr.responseText); + appendHostLinks(parsed_response.data.items); + } else { + var errorResponse = JSON.parse(xhr.responseText); + + console.log('Error: Bad status on host list query: "' + + xhr.status + ' ' + xhr.statusText); + console.log('Error code ' + errorResponse.error.code); + console.log('Error message ' + errorResponse.error.message); + + clear(hostlistDiv); + if (errorResponse.error.message == "Token expired") { + appendMessage(hostlistDiv, 'message', + 'Authentication token expired. Please sign in again.'); + logout(); + } else if (errorResponse.error.message == "Token invalid") { + appendMessage(hostlistDiv, 'message', + 'Invalid authentication token. Please sign in again.'); + logout(); + } else { + appendMessage(hostlistDiv, 'message', + 'Unable to load host list. Please try again later.'); + appendMessage(hostlistDiv, 'message', + 'Error code: ' + errorResponse.error.code); + appendMessage(hostlistDiv, 'message', + 'Message: ' + errorResponse.error.message); + } + } + }; + + xhr.open('GET', 'api/get_host_list', true); + xhr.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8'); + xhr.send(null); +} + +// Populate the 'hostlist_div' element with the list of hosts for this user. +function appendHostLinks(hostlist) { + var hostlistDiv = document.getElementById('hostlist_div'); + + // Clear the div before adding the host info. + clear(hostlistDiv); + + var numHosts = 0; + var numOfflineHosts = 0; + + // Add the hosts. + // TODO(garykac): We should have some sort of MRU list here or have + // the Chromoting Directory provide a MRU list. + // First, add all of the connected hosts. + for (var i = 0; i < hostlist.length; ++i) { + if (hostlist[i].status == "ONLINE") { + hostlistDiv.appendChild(addHostInfo(hostlist[i])); + numHosts++; + } + } + // Add non-online hosts at the end. + for (var i = 0; i < hostlist.length; ++i) { + if (hostlist[i].status != "ONLINE") { + if (chromoting.showOfflineHosts == 1) { + hostlistDiv.appendChild(addHostInfo(hostlist[i])); + numHosts++; + } + numOfflineHosts++; + } + } + + if (numHosts == 0) { + var message; + if (numOfflineHosts == 0) { + message = 'No hosts available.'; + } else { + message = 'No online hosts available (' + + numOfflineHosts + ' offline hosts).'; + } + message += ' See LINK for info on how to set up a new host.'; + displayMessage(hostlistDiv, 'message', message); + } +} + +// Create a single host description element. +function addHostInfo(host) { + var hostEntry = document.createElement('div'); + hostEntry.setAttribute('class', 'hostentry'); + + var hostIcon = document.createElement('img'); + hostIcon.setAttribute('src', 'static_files/machine.png'); + hostIcon.setAttribute('class', 'hosticon'); + hostEntry.appendChild(hostIcon); + + if (host.status == 'ONLINE') { + var span = document.createElement('span'); + span.setAttribute('class', 'connect'); + var connect = document.createElement('input'); + connect.setAttribute('type', 'button'); + connect.setAttribute('value', 'Connect'); + connect.setAttribute('onclick', "window.open('session?hostname=" + + encodeURIComponent(host.hostName) + "&hostjid=" + + encodeURIComponent(host.jabberId) + "');"); + span.appendChild(connect); + hostEntry.appendChild(span); + } + + var hostName = document.createElement('p'); + hostName.setAttribute('class', 'hostindent hostname'); + hostName.appendChild(document.createTextNode(host.hostName)); + hostEntry.appendChild(hostName); + + var hostStatus = document.createElement('p'); + hostStatus.setAttribute('class', 'hostindent hostinfo hoststatus_' + + ((host.status == 'ONLINE') ? 'good' : 'bad')); + hostStatus.appendChild(document.createTextNode(host.status)); + hostEntry.appendChild(hostStatus); + + var hostInfo = document.createElement('p'); + hostInfo.setAttribute('class', 'hostindent hostinfo'); + hostInfo.appendChild(document.createTextNode(host.jabberId)); + hostEntry.appendChild(hostInfo); + + return hostEntry; +} diff --git a/remoting/client/appengine/static_files/machine.png b/remoting/client/appengine/static_files/machine.png Binary files differnew file mode 100644 index 0000000..635a9bb --- /dev/null +++ b/remoting/client/appengine/static_files/machine.png diff --git a/remoting/client/appengine/static_files/main.css b/remoting/client/appengine/static_files/main.css new file mode 100644 index 0000000..5e5cf29 --- /dev/null +++ b/remoting/client/appengine/static_files/main.css @@ -0,0 +1,157 @@ +.hostlist_body { +} + +p { + color: black; +} + +h1 { + font-family: sans-serif; + font-size: 2em; + font-weight: bold; + margin: 2px 5px 5px 5px +} + +.message { + font-family: sans-serif; + font-size: 1.2em; + padding: 0 4px 0 4px; +} + +.hostlist { + width: 100%; + margin: 0; + border: black 1px solid; +} + +.hostentry { + margin: 0; + padding: 3px; + border-top: blue 1px solid; + min-height: 70px; +} + +.hostentry:first-child { + border-top-style: none; +} + +a.hostentry { text-decoration: none; } + +.hosticon { + float: left; + margin: 2px; +} + +.hostindent { + margin: 2px 4px 2px 75px; +} + +.hostname { + font-family: sans-serif; + font-size: 1.2em; + font-weight: bold; + line-height: 1.2em; +} + +.hoststatus_good { + font-weight: bold; + color: green; +} + +.hoststatus_bad { + font-weight: bold; + color: red; +} + +.hostinfo { + font-family: sans-serif; + font-size: 0.8em; + line-height: 1.1em; +} + +.hostlinks { + font-family: sans-serif; + font-size: 0.7em; + line-height: 1.2em; +} + +.connect { + float: right; +} + +.reload { + font-family: sans-serif; + font-size: 0.8em; + font-style: italic; + margin: 0.25em 0 0.5em 5px; + line-height: 1em; +} + +.login { + font-family: sans-serif; + font-size: 0.9em; + margin: 0 0 3px 5px; +} + +.login_email { + font-weight: bold; +} + +.chromoting_body { + -webkit-user-select: none; + margin: 0; + padding: 0; + overflow: hidden; + scrollbars: no; +} + +.status_msg { + -webkit-user-select: none; + position: absolute; + left: 20px; + top: 20px; + font-family: sans-serif; + font-size: 2em; + font-weight: bold; +} + +.error_msg { + color: red; +} + +.gaia_login_panel { + -webkit-user-select: none; + font-family: arial,sans-serif; + position: absolute; + display: none; + z-index: 1; + top: 50%; + left: 45%; + border: 2px solid #e8eefa; + margin-left: auto; + margin-right: auto; +} + +.local_login_panel { + -webkit-user-select: none; + font-family: arial,sans-serif; + position: absolute; + display: none; + z-index: 1; + top: 50%; + left: 45%; + border: 2px solid #e8eefa; + margin-left: auto; + margin-right: auto; +} + +.plugin-scroll-panel { + -webkit-user-select: none; + overflow: auto; + width: 100%; + height: 100%; +} + +.gaia_font { + font-family: Arial, Helvetica, sans-serif; +} |