summaryrefslogtreecommitdiffstats
path: root/remoting/client/appengine
diff options
context:
space:
mode:
authorajwong@chromium.org <ajwong@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-02-24 23:13:21 +0000
committerajwong@chromium.org <ajwong@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-02-24 23:13:21 +0000
commit999d862cc8675b78fdeab5b0a4b1689b0505f276 (patch)
tree316c8f12dc260addddf029fb249a286d3b68cc82 /remoting/client/appengine
parent6ee62b63cec739d231bf451168219576df623e20 (diff)
downloadchromium_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.py67
-rw-r--r--remoting/client/appengine/app.yaml22
-rw-r--r--remoting/client/appengine/auth.py320
-rw-r--r--remoting/client/appengine/chromoting_oauth_setup.html23
-rw-r--r--remoting/client/appengine/chromoting_session.html61
-rw-r--r--remoting/client/appengine/client_login.html40
-rw-r--r--remoting/client/appengine/hostlist.html51
-rw-r--r--remoting/client/appengine/main.py56
-rw-r--r--remoting/client/appengine/static_files/base.js32
-rw-r--r--remoting/client/appengine/static_files/chromoticon.pngbin0 -> 332 bytes
-rw-r--r--remoting/client/appengine/static_files/chromoting_session.js190
-rw-r--r--remoting/client/appengine/static_files/client.js357
-rw-r--r--remoting/client/appengine/static_files/machine.pngbin0 -> 4992 bytes
-rw-r--r--remoting/client/appengine/static_files/main.css157
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
new file mode 100644
index 0000000..a5cb2b2
--- /dev/null
+++ b/remoting/client/appengine/static_files/chromoticon.png
Binary files differ
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
new file mode 100644
index 0000000..635a9bb
--- /dev/null
+++ b/remoting/client/appengine/static_files/machine.png
Binary files differ
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;
+}