├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── build ├── .csslintrc ├── .jshintrc ├── build_app_engine_package.py ├── get_python_test_deps.py ├── grunt-chrome-build │ └── grunt-chrome-build.js ├── install_webtest_on_linux.py ├── js_test_driver.conf ├── remove_python_tests.py ├── run_python_tests.py ├── run_python_tests.sh └── test_file_herder.py ├── package.json ├── src ├── app_engine │ ├── .gitignore │ ├── analytics.py │ ├── analytics_page.py │ ├── analytics_page_test.py │ ├── analytics_test.py │ ├── apiauth.py │ ├── app.yaml │ ├── apprtc.py │ ├── apprtc_test.py │ ├── bigquery │ │ └── analytics_schema.json │ ├── compute_page.py │ ├── compute_page_test.py │ ├── constants.py │ ├── cron.yaml │ ├── probers.py │ ├── probers_test.py │ └── test_util.py ├── collider │ ├── README.md │ ├── collider │ │ ├── client.go │ │ ├── client_test.go │ │ ├── collider.go │ │ ├── collider_test.go │ │ ├── dashboard.go │ │ ├── dashboard_test.go │ │ ├── messages.go │ │ ├── room.go │ │ ├── roomTable.go │ │ └── room_test.go │ ├── collidermain │ │ └── main.go │ └── collidertest │ │ └── mockrwc.go ├── third_party │ ├── README.md │ ├── apiclient │ │ ├── __init__.py │ │ ├── channel.py │ │ ├── discovery.py │ │ ├── errors.py │ │ ├── http.py │ │ ├── mimeparse.py │ │ ├── model.py │ │ ├── sample_tools.py │ │ └── schema.py │ ├── google_api_python_client-1.2.egg-info │ │ ├── PKG-INFO │ │ ├── SOURCES.txt │ │ ├── dependency_links.txt │ │ ├── installed-files.txt │ │ ├── requires.txt │ │ └── top_level.txt │ ├── httplib2-0.9.egg-info │ │ ├── PKG-INFO │ │ ├── SOURCES.txt │ │ ├── dependency_links.txt │ │ ├── installed-files.txt │ │ └── top_level.txt │ ├── httplib2 │ │ ├── __init__.py │ │ ├── cacerts.txt │ │ ├── iri2uri.py │ │ └── socks.py │ ├── oauth2client │ │ ├── __init__.py │ │ ├── anyjson.py │ │ ├── appengine.py │ │ ├── client.py │ │ ├── clientsecrets.py │ │ ├── crypt.py │ │ ├── django_orm.py │ │ ├── file.py │ │ ├── gce.py │ │ ├── keyring_storage.py │ │ ├── locked_file.py │ │ ├── multistore_file.py │ │ ├── old_run.py │ │ ├── tools.py │ │ ├── util.py │ │ └── xsrfutil.py │ ├── requirements.txt │ └── uritemplate │ │ └── __init__.py └── web_app │ ├── chrome_app │ ├── apprtc-app-store-icon-128.png │ ├── large-tile.png │ ├── manifest.json │ ├── marquee.png │ ├── screenshot1.png │ ├── screenshot2.png │ └── small-tile.png │ ├── css │ └── main.css │ ├── html │ ├── full_template.html │ ├── google1b7eb21c5b594ba0.html │ ├── help.html │ ├── index_template.html │ ├── index_template.json │ ├── manifest.json │ └── params.html │ ├── images │ ├── apprtc-128.png │ ├── apprtc-16.png │ ├── apprtc-22.png │ ├── apprtc-32.png │ ├── apprtc-48.png │ └── webrtc-icon-192x192.png │ └── js │ ├── README.md │ ├── adapter.js │ ├── appcontroller.js │ ├── appcontroller_test.js │ ├── appwindow.js │ ├── background.js │ ├── background_test.js │ ├── call.js │ ├── call_test.js │ ├── constants.js │ ├── infobox.js │ ├── infobox_test.js │ ├── loopback.js │ ├── peerconnectionclient.js │ ├── peerconnectionclient_test.js │ ├── remotewebsocket.js │ ├── remotewebsocket_test.js │ ├── roomselection.js │ ├── roomselection_test.js │ ├── sdputils.js │ ├── sdputils_test.js │ ├── signalingchannel.js │ ├── signalingchannel_test.js │ ├── stats.js │ ├── storage.js │ ├── test_mocks.js │ ├── testpolyfills.js │ ├── util.js │ ├── utils_test.js │ └── windowport.js └── tools └── turn-prober ├── README ├── turn-prober.html └── turn-prober.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | bower_components 4 | google_appengine* 5 | node_modules 6 | out 7 | secrets.json 8 | validation-report.json 9 | validation-status.json 10 | webtest-master* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | 5 | install: 6 | - npm install 7 | 8 | script: 9 | - grunt travis 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | WebRTC welcomes patches/pulls for features and bug fixes. 2 | 3 | For contributors external to Google, follow the instructions given in the [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual). 4 | 5 | In all cases, contributors must sign a contributor license agreement before a contribution can be accepted. Please complete the agreement for an [individual](https://developers.google.com/open-source/cla/individual) or a [corporation](https://developers.google.com/open-source/cla/corporate) as appropriate. 6 | 7 | If you plan to add a significant component or large chunk of code, we recommend you bring this up on the [webrtc-discuss group](https://groups.google.com/forum/#!forum/discuss-webrtc) for a design discussion before writing code. 8 | 9 | If appropriate, write a unit test which demonstrates that your code functions as expected. Tests are the best way to ensure that future contributors do not break your code accidentally. 10 | 11 | To request a change or addition, you must [submit a pull request](https://help.github.com/categories/collaborating/). 12 | 13 | WebRTC developers monitor outstanding pull requests. They may request changes to the pull request before accepting. They will also verify that a CLA has been signed. 14 | 15 | The [Developer's Guide](https://bit.ly/webrtcdevguide) for this repo has more detailed information about code style, structure and validation. 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, The WebRTC project authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | 15 | * Neither the name of Google nor the names of its contributors may 16 | be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/webrtc/apprtc.svg?branch=master)](https://travis-ci.org/webrtc/apprtc) 2 | 3 | # AppRTC Demo Code 4 | 5 | ## Development 6 | 7 | Detailed information on devloping in the [webrtc](https://github.com/webrtc) github repo can be found in the [WebRTC GitHub repo developer's guide](https://docs.google.com/document/d/1tn1t6LW2ffzGuYTK3366w1fhTkkzsSvHsBnOHoDfRzY/edit?pli=1#heading=h.e3366rrgmkdk). 8 | 9 | The development AppRTC server can be accessed by visiting [http://localhost:8080](http://localhost:8080). 10 | 11 | Running AppRTC locally requires the [Google App Engine SDK for Python](https://cloud.google.com/appengine/downloads#Google_App_Engine_SDK_for_Python) and [Grunt](http://gruntjs.com/). 12 | 13 | Detailed instructions for running on Ubuntu Linux are provided below. 14 | 15 | ### Running on Ubuntu Linux 16 | 17 | Install grunt by first installing [npm](https://www.npmjs.com/), 18 | 19 | ``` 20 | sudo apt-get install npm 21 | ``` 22 | 23 | On Ubuntu 14.04 the default packages installs `/usr/bin/nodejs` but the `/usr/bin/node` executable is required for grunt. You can add this by installing the `nodejs-legacy` package, 24 | 25 | ``` 26 | sudo apt-get install nodejs-legacy 27 | ``` 28 | 29 | It is easiest to install a shared version of `grunt-cli` from `npm` using the `-g` flag. This will allow you access the `grunt` command from `/usr/local/bin`. More information can be found on [`gruntjs` Getting Started](http://gruntjs.com/getting-started). 30 | 31 | ``` 32 | sudo npm -g install grunt-cli 33 | ``` 34 | 35 | *Omitting the `-g` flag will install `grunt-cli` to the current directory under the `node_modules` directory.* 36 | 37 | Finally, you will want to install grunt and required grunt dependencies. *This can be done from any directory under your checkout of the [GoogleChrome/webrtc](https://github.com/GoogleChrome/webrtc) repository.* 38 | 39 | ``` 40 | npm install 41 | ``` 42 | 43 | Before you start the AppRTC dev server and *everytime you update the source code you need to recompile the App Engine package by running, 44 | 45 | ``` 46 | grunt build 47 | ``` 48 | 49 | Start the AppRTC dev server from the `out/app_engine` directory by running the Google App Engine SDK dev server, 50 | 51 | ``` 52 | /dev_appserver.py ./out/app_engine 53 | ``` 54 | 55 | ### Testing 56 | 57 | All tests by running `grunt`. 58 | 59 | To run only the Python tests you can call, 60 | 61 | ``` 62 | grunt runPythonTests 63 | ``` 64 | 65 | ### Enabling Local Logging 66 | 67 | *Note that logging is automatically enabled when running on Google App Engine using an implicit service account.* 68 | 69 | By default, logging to a BigQuery from the development server is disabled. Log information is presented on the console. Unless you are modifying the analytics API you will not need to enable remote logging. 70 | 71 | Logging to BigQuery when running LOCALLY requires a `secrets.json` containing Service Account credentials to a Google Developer project where BigQuery is enabled. DO NOT COMMIT `secrets.json` TO THE REPOSITORY. 72 | 73 | To generate a `secrets.json` file in the Google Developers Console for your project: 74 | 1. Go to the project page. 75 | 1. Under *APIs & auth* select *Credentials*. 76 | 1. Confirm a *Service Account* already exists or create it by selecting *Create new Client ID*. 77 | 1. Select *Generate new JSON key* from the *Service Account* area to create and download JSON credentials. 78 | 1. Rename the downloaded file to `secrets.json` and place in the directory containing `analytics.py`. 79 | 80 | When the `Analytics` class detects that AppRTC is running locally, all data is logged to `analytics` table in the `dev` dataset. You can bootstrap the `dev` dataset by following the instructions in the [Bootstrapping/Updating BigQuery](#bootstrappingupdating-bigquery). 81 | 82 | ## BigQuery 83 | 84 | When running on App Engine the `Analytics` class will log to `analytics` table in the `prod` dataset for whatever project is defined in `app.yaml`. 85 | 86 | ### Schema 87 | 88 | `bigquery/analytics_schema.json` contains the fields used in the BigQuery table. New fields can be added to the schema and the table updated. However, fields *cannot* be renamed or removed. *Caution should be taken when updating the production table as reverting schema updates is difficult.* 89 | 90 | Update the BigQuery table from the schema by running, 91 | 92 | ``` 93 | bq update -t prod.analytics bigquery/analytics_schema.json 94 | ``` 95 | 96 | ### Bootstrapping 97 | 98 | Initialize the required BigQuery datasets and tables with the following, 99 | 100 | ``` 101 | bq mk prod 102 | bq mk -t prod.analytics bigquery/analytics_schema.json 103 | ``` 104 | -------------------------------------------------------------------------------- /build/.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "box-model": false, 3 | "ids": false, 4 | "known-properties": false, 5 | "overqualified-elements": false, 6 | "unique-headings": false 7 | } 8 | -------------------------------------------------------------------------------- /build/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "devel": true, 6 | "eqeqeq": true, 7 | "forin": false, 8 | "globalstrict": true, 9 | "quotmark": "single", 10 | "undef": true, 11 | "unused": "strict", 12 | "globals": { 13 | "addExplicitTest": true, 14 | "addTest": true, 15 | "arrayAverage": true, 16 | "arrayMax": true, 17 | "arrayMin": true, 18 | "attachMediaStream": true, 19 | "attachMediaStream": true, 20 | "audioContext": true, 21 | "AudioContext": true, 22 | "Call": true, 23 | "createIceServers": true, 24 | "createIceServer": true, 25 | "createLineChart": true, 26 | "doGetUserMedia": true, 27 | "expectEquals": true, 28 | "getUserMedia": true, 29 | "getUserMedia": true, 30 | "ga": true, 31 | "GumHandler": true, 32 | "MediaStreamTrack": true, 33 | "reattachMediaStream": true, 34 | "report": true, 35 | "reportBug": true, 36 | "reportError": true, 37 | "reportFatal": true, 38 | "reportInfo": true, 39 | "reportSuccess": true, 40 | "RTCIceCandidate": true, 41 | "RTCPeerConnection": true, 42 | "RTCSessionDescription": true, 43 | "setTestProgress": true, 44 | "setTimeoutWithProgressBar": true, 45 | "Ssim": true, 46 | "StatisticsAggregate": true, 47 | "testFinished": true, 48 | "trace": true, 49 | "webrtcDetectedBrowser": true, 50 | "webrtcDetectedVersion": true 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /build/build_app_engine_package.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """Build App Engine source package. 4 | """ 5 | 6 | import json 7 | import optparse 8 | import os 9 | import shutil 10 | import subprocess 11 | import sys 12 | 13 | import test_file_herder 14 | 15 | USAGE = """%prog src_path dest_path 16 | Build the GAE source code package. 17 | 18 | src_path Path to the source code root directory. 19 | dest_path Path to the root directory to push/deploy GAE from.""" 20 | 21 | 22 | def call_cmd_and_return_output_lines(cmd): 23 | try: 24 | process = subprocess.Popen(cmd, stdout=subprocess.PIPE) 25 | output = process.communicate()[0] 26 | return output.split('\n') 27 | except OSError as e: 28 | print str(e) 29 | return [] 30 | 31 | 32 | def build_version_info_file(dest_path): 33 | """Build the version info JSON file.""" 34 | version_info = { 35 | 'gitHash': None, 36 | 'time': None, 37 | 'branch': None 38 | } 39 | 40 | lines = call_cmd_and_return_output_lines(['git', 'log', '-1']) 41 | for line in lines: 42 | if line.startswith('commit'): 43 | version_info['gitHash'] = line.partition(' ')[2].strip() 44 | elif line.startswith('Date'): 45 | version_info['time'] = line.partition(':')[2].strip() 46 | if version_info['gitHash'] is not None and version_info['time'] is not None: 47 | break 48 | 49 | lines = call_cmd_and_return_output_lines(['git', 'branch']) 50 | for line in lines: 51 | if line.startswith('*'): 52 | version_info['branch'] = line.partition(' ')[2].strip() 53 | break 54 | 55 | try: 56 | with open(dest_path, 'w') as f: 57 | f.write(json.dumps(version_info)) 58 | except IOError as e: 59 | print str(e) 60 | 61 | 62 | def CopyApprtcSource(src_path, dest_path): 63 | if os.path.exists(dest_path): 64 | shutil.rmtree(dest_path) 65 | os.makedirs(dest_path) 66 | 67 | simply_copy_subdirs = ['bigquery', 'css', 'images', 'third_party'] 68 | 69 | for dirpath, unused_dirnames, files in os.walk(src_path): 70 | for subdir in simply_copy_subdirs: 71 | if dirpath.endswith(subdir): 72 | shutil.copytree(dirpath, os.path.join(dest_path, subdir)) 73 | 74 | if dirpath.endswith('html'): 75 | dest_html_path = os.path.join(dest_path, 'html') 76 | os.makedirs(dest_html_path) 77 | for name in files: 78 | # Template files must be in the root directory. 79 | if name.endswith('_template.html'): 80 | shutil.copy(os.path.join(dirpath, name), dest_path) 81 | else: 82 | shutil.copy(os.path.join(dirpath, name), dest_html_path) 83 | elif dirpath.endswith('app_engine'): 84 | for name in files: 85 | if (name.endswith('.py') and 'test' not in name 86 | or name.endswith('.yaml')): 87 | shutil.copy(os.path.join(dirpath, name), dest_path) 88 | elif dirpath.endswith('js'): 89 | for name in files: 90 | # loopback.js is not compiled by Closure and needs to be copied 91 | # separately. 92 | if name == 'loopback.js': 93 | dest_js_path = os.path.join(dest_path, 'js') 94 | os.makedirs(dest_js_path) 95 | shutil.copy(os.path.join(dirpath, name), dest_js_path) 96 | break 97 | 98 | build_version_info_file(os.path.join(dest_path, 'version_info.json')) 99 | 100 | 101 | def main(): 102 | parser = optparse.OptionParser(USAGE) 103 | parser.add_option("-t", "--include-tests", action="store_true", 104 | help='Also copy python tests to the out dir.') 105 | options, args = parser.parse_args() 106 | if len(args) != 2: 107 | parser.error('Error: Exactly 2 arguments required.') 108 | 109 | src_path, dest_path = args[0:2] 110 | CopyApprtcSource(src_path, dest_path) 111 | if options.include_tests: 112 | app_engine_code = os.path.join(src_path, 'app_engine') 113 | test_file_herder.CopyTests(os.path.join(src_path, 'app_engine'), dest_path) 114 | 115 | 116 | if __name__ == '__main__': 117 | sys.exit(main()) 118 | -------------------------------------------------------------------------------- /build/get_python_test_deps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import re 5 | import sys 6 | import tarfile 7 | import urllib2 8 | import zipfile 9 | 10 | 11 | GAE_DOWNLOAD_URL = 'https://storage.googleapis.com/appengine-sdks/featured/' 12 | GAE_UPDATECHECK_URL = 'https://appengine.google.com/api/updatecheck' 13 | WEBTEST_URL = 'https://nodeload.github.com/Pylons/webtest/tar.gz/master' 14 | 15 | 16 | def _GetLatestAppEngineSdkVersion(): 17 | response = urllib2.urlopen(GAE_UPDATECHECK_URL) 18 | response_text = response.read() 19 | 20 | match = re.search('(\d*\.\d*\.\d*)', response_text) 21 | if not match: 22 | raise Exception('Could not determine latest GAE SDK version from ' 23 | 'response %s.' % response_text) 24 | gae_sdk_version = match.group(1) 25 | if gae_sdk_version == '1.9.15': 26 | # TODO(phoglund): remove when updatecheck returns the right thing. 27 | gae_sdk_version = '1.9.17' 28 | return gae_sdk_version 29 | 30 | 31 | def _Download(url, to): 32 | print 'Downloading %s to %s...' % (url, to) 33 | response = urllib2.urlopen(url) 34 | with open(to, 'w') as to_file: 35 | to_file.write(response.read()) 36 | 37 | 38 | def _Unzip(path): 39 | print 'Unzipping %s in %s...' % (path, os.getcwd()) 40 | zip_file = zipfile.ZipFile(path) 41 | try: 42 | zip_file.extractall() 43 | finally: 44 | zip_file.close() 45 | 46 | 47 | def _Untar(path): 48 | print 'Untarring %s in %s...' % (path, os.getcwd()) 49 | tar_file = tarfile.open(path, 'r:gz') 50 | try: 51 | tar_file.extractall() 52 | finally: 53 | tar_file.close() 54 | 55 | 56 | def DownloadAppEngineSdkIfNecessary(): 57 | gae_sdk_version = _GetLatestAppEngineSdkVersion() 58 | gae_sdk_file = 'google_appengine_%s.zip' % gae_sdk_version 59 | if os.path.exists(gae_sdk_file): 60 | print 'Already has %s, skipping' % gae_sdk_file 61 | return 62 | 63 | _Download(GAE_DOWNLOAD_URL + gae_sdk_file, gae_sdk_file) 64 | _Unzip(gae_sdk_file) 65 | 66 | 67 | def DownloadWebTestIfNecessary(): 68 | webtest_file = 'webtest-master.tar.gz' 69 | if os.path.exists(webtest_file): 70 | print 'Already has %s, skipping' % webtest_file 71 | return 72 | 73 | _Download(WEBTEST_URL, webtest_file) 74 | _Untar(webtest_file) 75 | 76 | 77 | def main(): 78 | DownloadAppEngineSdkIfNecessary() 79 | DownloadWebTestIfNecessary() 80 | 81 | if __name__ == '__main__': 82 | sys.exit(main()) 83 | -------------------------------------------------------------------------------- /build/grunt-chrome-build/grunt-chrome-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | 5 | grunt.loadNpmTasks('grunt-contrib-compress'); 6 | grunt.loadNpmTasks('grunt-jinja'); 7 | 8 | grunt.registerMultiTask('grunt-chrome-build', 'Build packaged Chrome app from sources.', function () { 9 | grunt.log.writeln('Chrome packaged app build.'); 10 | 11 | var options = this.options(); 12 | 13 | // Steps: 14 | // 1. Delete existing build dir if it exists. 15 | var buildDir = options.buildDir; 16 | if (!buildDir) { 17 | throw grunt.util.error('Missing required buildDir option.'); 18 | } 19 | grunt.log.writeln('Deleting buildDir: ' + buildDir); 20 | grunt.file.delete(buildDir); 21 | 22 | // 2. Create build dir. 23 | grunt.log.writeln('Creating empty buildDir: ' + buildDir); 24 | grunt.file.mkdir(buildDir); 25 | 26 | // 3. Copy sources to build dir. 27 | grunt.log.writeln('Copying resources to buildDir: ' + buildDir); 28 | this.files.forEach(function (f) { 29 | grunt.log.writeln(f.src + '-->' + f.dest); 30 | grunt.file.copy(f.src, f.dest); 31 | }); 32 | 33 | grunt.option('grunt-chrome-build-options', options); 34 | grunt.task.run( 35 | 'grunt-chrome-build-transform', 36 | 'grunt-chrome-build-compress', 37 | 'grunt-chrome-build-package' 38 | ); 39 | }); 40 | 41 | grunt.registerTask('grunt-chrome-build-transform', 'Transform templates to build directory.', function () { 42 | var options = grunt.option('grunt-chrome-build-options'); 43 | var appWindowFiles = { 44 | src: options.appwindowHtmlSrc, 45 | dest: options.appwindowHtmlDest 46 | }; 47 | // 4. Transform template file. 48 | grunt.log.writeln('Transforming files using jinja.'); 49 | grunt.config.set('jinja.chrome-build', { 50 | options: { 51 | templateDirs: ['src/'], 52 | contextRoot: 'src/' 53 | }, 54 | files: [appWindowFiles] 55 | }); 56 | grunt.task.run('jinja:chrome-build'); 57 | }); 58 | 59 | grunt.registerTask('grunt-chrome-build-compress', 'Create zip file in build directory.', function () { 60 | var options = grunt.option('grunt-chrome-build-options'); 61 | var buildDir = options.buildDir; 62 | // 5. Create zip file. 63 | var zipFile = options.zipFile; 64 | if (!zipFile) { 65 | throw grunt.util.error('Missing required zipFile option.'); 66 | } 67 | grunt.log.writeln('Creating zip file:' + zipFile); 68 | grunt.config.set('compress.chrome-build', { 69 | options: { 70 | archive: zipFile 71 | }, 72 | files: [{ 73 | expand: true, 74 | cwd: buildDir, 75 | src: ['**/*'] 76 | }] 77 | }); 78 | 79 | grunt.task.run('compress:chrome-build'); 80 | }); 81 | 82 | grunt.registerTask('grunt-chrome-build-package', 'Create crx package file in build directory.', function () { 83 | var options = grunt.option('grunt-chrome-build-options'); 84 | // This section does not work yet. 85 | // 6. Call chrome to create crx file. 86 | var done = this.async(); 87 | var chromeBinary = options.chromeBinary; 88 | var keyFile = options.keyFile; 89 | if (!chromeBinary || !keyFile) { 90 | grunt.log.writeln('Skipping creation of Chrome package.'); 91 | done(true); 92 | } else { 93 | grunt.log.writeln('Calling Chrome to create package.'); 94 | 95 | var args = [ 96 | '--pack-extension=' + buildDir, 97 | '--pack-extension-key=' + keyFile 98 | ]; 99 | 100 | grunt.log.write(chromeBinary + ' ' + args.join(' ')); 101 | 102 | grunt.util.spawn({ 103 | cmd: chromeBinary, 104 | args: [] 105 | }, function (error, result, code) { 106 | if (error || code !== 0) { 107 | grunt.log.error(); 108 | grunt.log.error(result.stdout); 109 | grunt.log.error(result.stderr); 110 | done(false); 111 | } else { 112 | grunt.log.ok(); 113 | done(true); 114 | } 115 | }); 116 | } 117 | }); 118 | }; 119 | -------------------------------------------------------------------------------- /build/install_webtest_on_linux.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import optparse 4 | import os 5 | import sys 6 | 7 | 8 | def InstallWebTestOnLinux(webtest_dir): 9 | cwd = os.getcwd() 10 | try: 11 | print 'About to install webtest into your system python.' 12 | os.chdir(webtest_dir) 13 | result = os.system('sudo python setup.py install') 14 | if result == 0: 15 | print 'Install successful.' 16 | else: 17 | return ('Failed to install webtest; are you missing setuptools / ' 18 | 'easy_install in your system python?') 19 | finally: 20 | os.chdir(cwd) 21 | 22 | 23 | def main(): 24 | parser = optparse.OptionParser('Usage: %prog webtest_path') 25 | _, args = parser.parse_args() 26 | if len(args) != 1: 27 | parser.error('Expected precisely one argument.') 28 | return InstallWebTestOnLinux(args[0]) 29 | 30 | if __name__ == '__main__': 31 | sys.exit(main()) 32 | -------------------------------------------------------------------------------- /build/js_test_driver.conf: -------------------------------------------------------------------------------- 1 | server: http://localhost:9876 2 | 3 | load: 4 | - ../src/web_app/js/testpolyfills.js 5 | - ../src/web_app/js/test_mocks.js 6 | - ../out/app_engine/js/apprtc.debug.js 7 | - ../out/chrome_app/js/background.js 8 | 9 | test: 10 | - ../src/web_app/js/*_test.js 11 | -------------------------------------------------------------------------------- /build/remove_python_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import optparse 4 | import sys 5 | 6 | import test_file_herder 7 | 8 | 9 | def main(): 10 | parser = optparse.OptionParser('Usage: %prog path_to_tests') 11 | _, args = parser.parse_args() 12 | if len(args) != 1: 13 | parser.error('Expected precisely one argument.') 14 | 15 | return test_file_herder.RemoveTests(args[0]) 16 | 17 | if __name__ == '__main__': 18 | sys.exit(main()) 19 | -------------------------------------------------------------------------------- /build/run_python_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import optparse 5 | import sys 6 | import unittest 7 | 8 | USAGE = """%prog sdk_path test_path webtest_path 9 | Run unit tests for App Engine apps. 10 | 11 | sdk_path Path to the SDK installation. 12 | test_path Path to package containing test modules. 13 | webtest_path Path to the webtest library.""" 14 | 15 | 16 | def _WebTestIsInstalled(): 17 | try: 18 | import webtest 19 | return True 20 | except ImportError: 21 | print 'You need to install webtest dependencies before you can proceed ' 22 | print 'running the tests. To do this you need to get easy_install since ' 23 | print 'that is how webtest provisions its dependencies.' 24 | print 'See https://pythonhosted.org/setuptools/easy_install.html.' 25 | print 'Then:' 26 | print 'cd webtest-master' 27 | print 'python setup.py install' 28 | print '(Prefix with sudo / run in admin shell as necessary).' 29 | return False 30 | 31 | 32 | def main(sdk_path, test_path, webtest_path): 33 | if not os.path.exists(sdk_path): 34 | return 'Missing %s: try grunt shell:getPythonTestDeps.' % sdk_path 35 | if not os.path.exists(test_path): 36 | return 'Missing %s: try grunt build.' % test_path 37 | if not os.path.exists(webtest_path): 38 | return 'Missing %s: try grunt shell:getPythonTestDeps.' % webtest_path 39 | 40 | sys.path.insert(0, sdk_path) 41 | import dev_appserver 42 | dev_appserver.fix_sys_path() 43 | sys.path.append(webtest_path) 44 | if not _WebTestIsInstalled(): 45 | return 1 46 | suite = unittest.loader.TestLoader().discover(test_path, 47 | pattern="*test.py") 48 | ok = unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful() 49 | return 0 if ok else 1 50 | 51 | 52 | if __name__ == '__main__': 53 | parser = optparse.OptionParser(USAGE) 54 | options, args = parser.parse_args() 55 | if len(args) != 3: 56 | parser.error('Error: Exactly 3 arguments required.') 57 | 58 | sdk_path, test_path, webtest_path = args[0:3] 59 | sys.exit(main(sdk_path, test_path, webtest_path)) 60 | -------------------------------------------------------------------------------- /build/run_python_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | function download { 5 | local filename=$1 6 | local url=$2 7 | 8 | if [ -f $filename ]; then 9 | # Only download if newer than the one we have. 10 | curl -z $filename -sS $url -o $filename 11 | else 12 | curl -sS $url -o $filename 13 | fi 14 | } 15 | 16 | VERSION_REGEX='[0-9]*\.[0-9]*\.[0-9]*' 17 | VERSION=$(curl -sS https://appengine.google.com/api/updatecheck | grep release | grep -o $VERSION_REGEX) 18 | if [ "$VERSION" == "1.9.15" ]; then 19 | # TODO(phoglund): remove when updatecheck returns the right thing. 20 | VERSION="1.9.17" 21 | fi 22 | GAE_SDK_FILE=google_appengine_$VERSION.zip 23 | GAE_SDK_URL=https://storage.googleapis.com/appengine-sdks/featured/$GAE_SDK_FILE 24 | 25 | download $GAE_SDK_FILE $GAE_SDK_URL 26 | unzip -quo $GAE_SDK_FILE 27 | 28 | WEBTEST_FILE=webtest-master.tar.gz 29 | WEBTEST_URL=https://nodeload.github.com/Pylons/webtest/tar.gz/master 30 | 31 | if [ ! -d 'webtest-master' ]; then 32 | echo "Downloading webtest-master..." 33 | download $WEBTEST_FILE $WEBTEST_URL 34 | tar xvf $WEBTEST_FILE 35 | 36 | # At least on my box, we must have root to modify your system python. 37 | # This package only needs to be installed once. 38 | echo "Missing webtest; must run sudo to install." 39 | cd webtest-master 40 | sudo python setup.py install 41 | cd .. 42 | fi 43 | 44 | cp src/app_engine/*test*.py out/app_engine/ 45 | 46 | python build/run_python_tests.py google_appengine/ out/app_engine/ webtest-master/ 47 | 48 | rm out/app_engine/*test*.py* 49 | -------------------------------------------------------------------------------- /build/test_file_herder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """Copies and deletes tests.""" 4 | 5 | import os 6 | import shutil 7 | 8 | 9 | def _IsPythonTest(filename): 10 | return 'test' in filename and filename.endswith('.py') 11 | 12 | 13 | def CopyTests(src_path, dest_path): 14 | if not os.path.exists(src_path): 15 | raise Exception('Failed to copy tests from %s; does not exist.' % src_path) 16 | 17 | for dirpath, _, files in os.walk(src_path): 18 | tests = [name for name in files if _IsPythonTest(name)] 19 | for test in tests: 20 | shutil.copy(os.path.join(dirpath, test), dest_path) 21 | 22 | 23 | def RemoveTests(path): 24 | if not os.path.exists(path): 25 | raise Exception('Failed to remove tests from %s; does not exist.' % path) 26 | 27 | for dirpath, _, files in os.walk(path): 28 | tests = [name for name in files if _IsPythonTest(name)] 29 | for test in tests: 30 | to_remove = os.path.join(dirpath, test) 31 | print 'Removing %s.' % to_remove 32 | os.remove(to_remove) 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apprtc", 3 | "version": "1.0.0", 4 | "description": "Project checking for AppRTC repo", 5 | "main": "Gruntfile.js", 6 | "scripts": { 7 | "test": "grunt --verbose" 8 | }, 9 | "devDependencies": { 10 | "grunt": ">=0.4.5", 11 | "grunt-cli": ">=0.1.9", 12 | "grunt-contrib-compress": "^0.13.0", 13 | "grunt-contrib-csslint": ">=0.3.1", 14 | "grunt-contrib-jshint": "^0.10.0", 15 | "grunt-htmlhint": ">=0.4.1", 16 | "grunt-jinja": "^0.3.0", 17 | "grunt-jscs": ">=0.8.1", 18 | "grunt-shell": "^1.1.1", 19 | "grunt-jstestdriver-phantomjs": ">=0.0.7", 20 | "grunt-closurecompiler": ">=0.0.21" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app_engine/.gitignore: -------------------------------------------------------------------------------- 1 | /secrets.json -------------------------------------------------------------------------------- /src/app_engine/analytics.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All Rights Reserved. 2 | 3 | """Module for pushing analytics data to BigQuery.""" 4 | 5 | import datetime 6 | import json 7 | import logging 8 | import os 9 | import sys 10 | import time 11 | 12 | sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party')) 13 | 14 | import apiauth 15 | import constants 16 | 17 | from google.appengine.api import app_identity 18 | 19 | class EventType(object): 20 | # Event signifying that a room enters the state of having exactly 21 | # two participants. 22 | ROOM_SIZE_2 = 'room_size_2' 23 | ICE_CONNECTION_STATE_CONNECTED = 'ice_connection_state_connected' 24 | 25 | class LogField(object): 26 | pass 27 | 28 | with open(os.path.join(os.path.dirname(__file__), 29 | 'bigquery', 'analytics_schema.json')) as f: 30 | schema = json.load(f) 31 | for field in schema: 32 | setattr(LogField, field['name'].upper(), field['name']) 33 | 34 | 35 | class Analytics(object): 36 | """Class used to encapsulate analytics logic. Used interally in the module. 37 | 38 | All data is streamed to BigQuery. 39 | 40 | """ 41 | 42 | def __init__(self): 43 | self.bigquery_table = constants.BIGQUERY_TABLE 44 | 45 | if constants.IS_DEV_SERVER: 46 | self.bigquery_dataset = constants.BIGQUERY_DATASET_LOCAL 47 | else: 48 | self.bigquery_dataset = constants.BIGQUERY_DATASET_PROD 49 | 50 | # Attempt to initialize a connection to BigQuery. 51 | self.bigquery = self._build_bigquery_object() 52 | if self.bigquery is None: 53 | logging.warning('Unable to build BigQuery API object. Logging disabled.') 54 | 55 | def _build_bigquery_object(self): 56 | return apiauth.build(scope=constants.BIGQUERY_URL, 57 | service_name='bigquery', 58 | version='v2') 59 | 60 | def _timestamp_from_millis(self, time_ms): 61 | """Convert back to seconds as float and then to ISO format.""" 62 | return datetime.datetime.fromtimestamp(float(time_ms)/1000.).isoformat() 63 | 64 | def report_event(self, event_type, room_id=None, time_ms=None, 65 | client_time_ms=None, host=None): 66 | """Report an event to BigQuery.""" 67 | event = {LogField.EVENT_TYPE: event_type} 68 | 69 | if room_id is not None: 70 | event[LogField.ROOM_ID] = room_id 71 | 72 | if client_time_ms is not None: 73 | event[LogField.CLIENT_TIMESTAMP] = self._timestamp_from_millis( 74 | client_time_ms) 75 | 76 | if host is not None: 77 | event[LogField.HOST] = host 78 | 79 | if time_ms is None: 80 | time_ms = time.time() * 1000. 81 | 82 | event[LogField.TIMESTAMP] = self._timestamp_from_millis(time_ms) 83 | 84 | obj = {'rows': [{'json': event}]} 85 | 86 | logging.info('Event: %s', obj) 87 | if self.bigquery is not None: 88 | response = self.bigquery.tabledata().insertAll( 89 | projectId=app_identity.get_application_id(), 90 | datasetId=self.bigquery_dataset, 91 | tableId=self.bigquery_table, 92 | body=obj).execute() 93 | logging.info('BigQuery response: %s', response) 94 | 95 | 96 | analytics = None 97 | 98 | 99 | def report_event(*args, **kwargs): 100 | """Used by other modules to actually do logging. 101 | 102 | A passthrough to a global Analytics instance intialized on use. 103 | 104 | Args: 105 | *args: passed directly to Analytics.report_event. 106 | **kwargs: passed directly to Analytics.report_event. 107 | """ 108 | global analytics 109 | 110 | # Initialization is delayed until the first use so that our 111 | # environment is ready and available. This is a problem with unit 112 | # tests since the testbed needs to initialized before creating an 113 | # Analytics instance. 114 | if analytics is None: 115 | analytics = Analytics() 116 | 117 | analytics.report_event(*args, **kwargs) 118 | -------------------------------------------------------------------------------- /src/app_engine/analytics_page.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All Rights Reserved. 2 | 3 | """Module for the AnalyticsPage handler.""" 4 | 5 | import json 6 | import time 7 | 8 | import webapp2 9 | 10 | import analytics 11 | import constants 12 | 13 | class RequestField(object): 14 | TYPE = 'type' 15 | REQUEST_TIME_MS = 'request_time_ms' 16 | EVENT = 'event' 17 | 18 | class MessageType(object): 19 | EVENT = 'event' 20 | 21 | class EventField(object): 22 | EVENT_TYPE = 'event_type' 23 | ROOM_ID = 'room_id' 24 | EVENT_TIME_MS = 'event_time_ms' 25 | 26 | 27 | class AnalyticsPage(webapp2.RequestHandler): 28 | """Client Analytics data handler. 29 | 30 | Each POST body to the AnalyticsPage is a JSON object of the form, 31 | { 32 | 'request_time_ms': , 33 | 'type': 34 | 'event': { 35 | 'event_type': , 36 | 'event_time_ms': , 37 | 'room_id': , 38 | } 39 | } 40 | 41 | 'request_time_ms': Required field set by the client to indicate when 42 | the request was send by the client. 43 | 44 | 'type': Required field describing the type of request. In the case 45 | of the 'event' type the 'event' field contains data 46 | pertinent to the request. However, the request type may 47 | correspond to one or more fields. 48 | 49 | 'event': Data relevant to an 'event' request. 50 | 51 | In order to handle client clock skew, the time an event 52 | occurred (event_time_ms) is adjusted based on the 53 | difference between the client clock and the server 54 | clock. The difference between the client clock and server 55 | clock is calculated as the difference between 56 | 'request_time_ms' provide by the client and the time at 57 | which the server processes the request. This ignores the 58 | latency of opening a connection and sending the body of the 59 | message to the server. 60 | 61 | To avoid problems with daylight savings the client should 62 | report 'event_time_ms' and 'request_time_ms' as UTC. The 63 | report will be recorded using local server time. 64 | 65 | """ 66 | 67 | def _write_response(self, result): 68 | response = {'result': result} 69 | self.response.write(json.dumps({ 70 | 'result': result 71 | })) 72 | 73 | def _time(self): 74 | """Overridden in unit tests to validate time calculations.""" 75 | return time.time() 76 | 77 | def post(self): 78 | try: 79 | msg = json.loads(self.request.body) 80 | except ValueError: 81 | return self._write_response(constants.RESPONSE_INVALID_REQUEST) 82 | 83 | response = constants.RESPONSE_INVALID_REQUEST 84 | 85 | # Verify required fields. 86 | request_type = msg.get(RequestField.TYPE) 87 | request_time_ms = msg.get(RequestField.REQUEST_TIME_MS) 88 | if (request_time_ms is None or request_type is None): 89 | self._write_response(constants.RESPONSE_INVALID_REQUEST) 90 | return 91 | 92 | # Handle specific event types. 93 | if (request_type == RequestField.MessageType.EVENT and 94 | msg.get(RequestField.EVENT) is not None): 95 | response = self._handle_event(msg) 96 | 97 | self._write_response(response) 98 | return 99 | 100 | def _handle_event(self, msg): 101 | request_time_ms = msg.get(RequestField.REQUEST_TIME_MS) 102 | 103 | event = msg.get(RequestField.EVENT) 104 | if event is None: 105 | return constants.RESPONSE_INVALID_REQUEST 106 | 107 | event_type = event.get(RequestField.EventField.EVENT_TYPE) 108 | if event_type is None: 109 | return constants.RESPONSE_INVALID_REQUEST 110 | 111 | room_id = event.get(RequestField.EventField.ROOM_ID) 112 | 113 | # Time that the event occurred according to the client clock. 114 | try: 115 | client_event_time_ms = float(event.get( 116 | RequestField.EventField.EVENT_TIME_MS)) 117 | except (TypeError, ValueError): 118 | return constants.RESPONSE_INVALID_REQUEST 119 | 120 | # Time the request was sent based on the client clock. 121 | try: 122 | request_time_ms = float(request_time_ms) 123 | except (TypeError, ValueError): 124 | return constants.RESPONSE_INVALID_REQUEST 125 | 126 | # Server time at the time of request. 127 | receive_time_ms = self._time() * 1000. 128 | 129 | # Calculate event time as client event time adjusted to server 130 | # local time. Server clock offset is gived by the difference 131 | # between the time the client sent thes request and the time the 132 | # server received the request. This method ignores the latency of 133 | # sending the request to the server. 134 | event_time_ms = client_event_time_ms + (receive_time_ms - request_time_ms) 135 | 136 | analytics.report_event(event_type=event_type, 137 | room_id=room_id, 138 | time_ms=event_time_ms, 139 | client_time_ms=client_event_time_ms, 140 | host=self.request.host) 141 | 142 | return constants.RESPONSE_SUCCESS 143 | -------------------------------------------------------------------------------- /src/app_engine/analytics_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | 3 | import datetime 4 | import json 5 | import time 6 | import unittest 7 | import webtest 8 | 9 | from google.appengine.api import memcache 10 | from google.appengine.ext import testbed 11 | 12 | import analytics 13 | from test_util import CapturingFunction 14 | from test_util import ReplaceFunction 15 | 16 | 17 | class FakeBigQuery(object): 18 | """Handles long function calls to the Google API client.""" 19 | 20 | def __init__(self): 21 | self.tabledata = CapturingFunction(self) 22 | self.insertAll = CapturingFunction(self) 23 | self.execute = CapturingFunction( 24 | {u'kind': u'bigquery#tableDataInsertAllResponse'}) 25 | 26 | 27 | class AnalyticsTest(unittest.TestCase): 28 | """Test the Analytics class in the analytics module.""" 29 | 30 | def fake_build_bigquery_object(self, *args): 31 | self.bigquery = FakeBigQuery() 32 | return self.bigquery 33 | 34 | def now_isoformat(self): 35 | return datetime.datetime.fromtimestamp(self.now).isoformat() 36 | 37 | def create_log_dict(self, record): 38 | return {'body': {'rows': [{'json': record}]}, 39 | 'projectId': 'testbed-test', 40 | 'tableId': 'analytics', 41 | 'datasetId': 'prod'} 42 | 43 | def setUp(self): 44 | # First, create an instance of the Testbed class. 45 | self.testbed = testbed.Testbed() 46 | 47 | # Then activate the testbed, which prepares the service stubs for use. 48 | self.testbed.activate() 49 | 50 | # Inject our own instance of bigquery. 51 | self.build_big_query_replacement = ReplaceFunction( 52 | analytics.Analytics, 53 | '_build_bigquery_object', 54 | self.fake_build_bigquery_object) 55 | 56 | # Inject our own time function 57 | self.now = time.time() 58 | self.time_replacement = ReplaceFunction(time, 'time', lambda: self.now) 59 | 60 | # Instanciate an instance. 61 | self.tics = analytics.Analytics() 62 | 63 | def tearDown(self): 64 | # Cleanup our replacement functions. 65 | del self.time_replacement 66 | del self.build_big_query_replacement 67 | 68 | def testOnlyEvent(self): 69 | event_type = 'an_event' 70 | log_dict = self.create_log_dict( 71 | {analytics.LogField.TIMESTAMP: '{0}'.format(self.now_isoformat()), 72 | analytics.LogField.EVENT_TYPE: event_type}) 73 | 74 | self.tics.report_event(event_type) 75 | self.assertEqual(log_dict, self.bigquery.insertAll.last_kwargs) 76 | 77 | def testEventRoom(self): 78 | event_type = 'an_event_with_room' 79 | room_id = 'my_room_that_is_the_best' 80 | log_dict = self.create_log_dict( 81 | {analytics.LogField.TIMESTAMP: '{0}'.format(self.now_isoformat()), 82 | analytics.LogField.EVENT_TYPE: event_type, 83 | analytics.LogField.ROOM_ID: room_id}) 84 | 85 | self.tics.report_event(event_type, room_id=room_id) 86 | self.assertEqual(log_dict, self.bigquery.insertAll.last_kwargs) 87 | 88 | def testEventAll(self): 89 | event_type = 'an_event_with_everything' 90 | room_id = 'my_room_that_is_the_best' 91 | time_s = self.now + 50 92 | client_time_s = self.now + 60 93 | host = 'super_host.domain.org:8112' 94 | 95 | log_dict = self.create_log_dict( 96 | {analytics.LogField.TIMESTAMP: '{0}'.format( 97 | datetime.datetime.fromtimestamp(time_s).isoformat()), 98 | analytics.LogField.EVENT_TYPE: event_type, 99 | analytics.LogField.ROOM_ID: room_id, 100 | analytics.LogField.CLIENT_TIMESTAMP: '{0}'.format( 101 | datetime.datetime.fromtimestamp(client_time_s).isoformat()), 102 | analytics.LogField.HOST: host}) 103 | 104 | self.tics.report_event(event_type, 105 | room_id=room_id, 106 | time_ms=time_s*1000., 107 | client_time_ms=client_time_s*1000., 108 | host=host) 109 | self.assertEqual(log_dict, self.bigquery.insertAll.last_kwargs) 110 | 111 | 112 | class AnalyticsModuleTest(unittest.TestCase): 113 | """Test global functions in the analytics module.""" 114 | 115 | def setUp(self): 116 | # Create a fake constructor to replace the Analytics class. 117 | self.analytics_fake = CapturingFunction(lambda: self.analytics_fake) 118 | self.analytics_fake.report_event = CapturingFunction() 119 | 120 | # Replace the Analytics class with the fake constructor. 121 | self.analytics_class_replacement = ReplaceFunction(analytics, 'Analytics', 122 | self.analytics_fake) 123 | 124 | def tearDown(self): 125 | # This will replace the Analytics class back to normal. 126 | del self.analytics_class_replacement 127 | 128 | def testModule(self): 129 | event_type = 'an_event_with_everything' 130 | room_id = 'my_room_that_is_the_best' 131 | time_ms = 50*1000. 132 | client_time_ms = 60*1000. 133 | host = 'super_host.domain.org:8112' 134 | 135 | analytics.report_event(event_type, 136 | room_id=room_id, 137 | time_ms=time_ms, 138 | client_time_ms=client_time_ms, 139 | host=host) 140 | 141 | kwargs = { 142 | 'room_id': room_id, 143 | 'time_ms': time_ms, 144 | 'client_time_ms': client_time_ms, 145 | 'host': host, 146 | } 147 | self.assertEqual((event_type,), self.analytics_fake.report_event.last_args) 148 | self.assertEqual(kwargs, self.analytics_fake.report_event.last_kwargs) 149 | -------------------------------------------------------------------------------- /src/app_engine/apiauth.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All Rights Reserved. 2 | 3 | """Google API auth utilities.""" 4 | 5 | import json 6 | import os 7 | import sys 8 | 9 | # Insert our third-party libraries first to avoid conflicts with appengine. 10 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'third_party')) 11 | 12 | from apiclient import discovery 13 | import httplib2 14 | import oauth2client.appengine 15 | import oauth2client.client 16 | 17 | import constants 18 | 19 | 20 | def build(scope, service_name, version): 21 | """Build a service object if authorization is available.""" 22 | credentials = None 23 | if constants.IS_DEV_SERVER: 24 | # Local instances require a 'secrets.json' file. 25 | secrets_path = os.path.join(os.path.dirname(__file__), 'secrets.json') 26 | if os.path.exists(secrets_path): 27 | with open(secrets_path) as f: 28 | auth = json.load(f) 29 | credentials = oauth2client.client.SignedJwtAssertionCredentials( 30 | auth['client_email'], auth['private_key'], scope) 31 | else: 32 | # Use the GAE service credentials. 33 | credentials = oauth2client.appengine.AppAssertionCredentials(scope=scope) 34 | 35 | if credentials is None: 36 | return None 37 | 38 | http = credentials.authorize(httplib2.Http()) 39 | return discovery.build(service_name, version, http=http) 40 | -------------------------------------------------------------------------------- /src/app_engine/app.yaml: -------------------------------------------------------------------------------- 1 | application: apprtc 2 | version: 2 3 | runtime: python27 4 | threadsafe: true 5 | api_version: 1 6 | 7 | handlers: 8 | - url: /(.*\.html) 9 | static_files: html/\1 10 | upload: html/(.*\.html) 11 | 12 | - url: /images 13 | static_dir: images 14 | 15 | - url: /js 16 | static_dir: js 17 | 18 | - url: /css 19 | static_dir: css 20 | 21 | - url: /compute/.* 22 | script: apprtc.app 23 | login: admin 24 | 25 | - url: /probe.* 26 | script: probers.app 27 | secure: always 28 | 29 | - url: /.* 30 | script: apprtc.app 31 | secure: always 32 | 33 | libraries: 34 | - name: jinja2 35 | version: latest 36 | - name: ssl 37 | version: latest 38 | - name: pycrypto 39 | version: latest 40 | 41 | env_variables: 42 | BYPASS_JOIN_CONFIRMATION: false 43 | -------------------------------------------------------------------------------- /src/app_engine/bigquery/analytics_schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "event_type", 4 | "type": "string", 5 | "mode": "nullable" 6 | }, 7 | { 8 | "name": "room_id", 9 | "type": "string", 10 | "mode": "nullable" 11 | }, 12 | { 13 | "name": "timestamp", 14 | "type": "timestamp", 15 | "mode": "nullable" 16 | }, 17 | { 18 | "name": "client_timestamp", 19 | "type": "timestamp", 20 | "mode": "nullable" 21 | }, 22 | { 23 | "name": "host", 24 | "type": "string", 25 | "mode": "nullable" 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /src/app_engine/compute_page.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All Rights Reserved. 2 | 3 | """Compute page for handling tasks related to compute engine.""" 4 | 5 | import logging 6 | 7 | import apiauth 8 | import webapp2 9 | 10 | from google.appengine.api import app_identity 11 | from google.appengine.api import taskqueue 12 | 13 | 14 | # Page actions 15 | # Get the status of an instance. 16 | ACTION_STATUS = 'status' 17 | 18 | # Start the compute instance if it is not already started. When an 19 | # instance is in the TERMINATED state, it will be started. If the 20 | # instance is already RUNNING do nothing. If the instance is in an 21 | # intermediate state--PROVISIONING, STAGING, STOPPING--the task is 22 | # requeued. 23 | ACTION_START = 'start' 24 | 25 | # Stop the compute instance if it is RUNNING. Then queue a task to start it. Do 26 | # nothing if the instance is not RUNNING. 27 | ACTION_RESTART = 'restart' 28 | 29 | # Constants for the Compute Engine API 30 | COMPUTE_STATUS = 'status' 31 | COMPUTE_STATUS_TERMINATED = 'TERMINATED' 32 | COMPUTE_STATUS_RUNNING = 'RUNNING' 33 | 34 | # Seconds between API retries. 35 | START_TASK_MIN_WAIT_S = 3 36 | 37 | COMPUTE_API_URL = 'https://www.googleapis.com/auth/compute' 38 | 39 | 40 | def enqueue_start_task(instance, zone): 41 | taskqueue.add(url='/compute/%s/%s/%s' % (ACTION_START, instance, zone), 42 | countdown=START_TASK_MIN_WAIT_S) 43 | 44 | 45 | def enqueue_restart_task(instance, zone): 46 | taskqueue.add(url='/compute/%s/%s/%s' % (ACTION_RESTART, instance, zone), 47 | countdown=START_TASK_MIN_WAIT_S) 48 | 49 | 50 | class ComputePage(webapp2.RequestHandler): 51 | """Page to handle requests against GCE.""" 52 | 53 | def __init__(self, request, response): 54 | # Call initialize rather than the parent constructor fun. See: 55 | # https://webapp-improved.appspot.com/guide/handlers.html#overriding-init 56 | self.initialize(request, response) 57 | 58 | self.compute_service = self._build_compute_service() 59 | if self.compute_service is None: 60 | logging.warning('Unable to create Compute service object.') 61 | 62 | def _build_compute_service(self): 63 | return apiauth.build(scope=COMPUTE_API_URL, 64 | service_name='compute', 65 | version='v1') 66 | 67 | def _maybe_restart_instance(self, instance, zone): 68 | """Implementation for restart action. 69 | 70 | Args: 71 | instance: Name of the instance to restart. 72 | zone: Name of the zone the instance belongs to. 73 | 74 | """ 75 | if self.compute_service is None: 76 | logging.warning('Compute service unavailable.') 77 | return 78 | 79 | status = self._compute_status(instance, zone) 80 | 81 | logging.info('GCE VM \'%s (%s)\' status: \'%s\'.', 82 | instance, zone, status) 83 | 84 | # Do nothing if the status is not RUNNING to avoid race. This will cover 85 | # most of the cases. 86 | if status == COMPUTE_STATUS_RUNNING: 87 | logging.info('Stopping GCE VM: %s (%s)', instance, zone) 88 | self.compute_service.instances().stop( 89 | project=app_identity.get_application_id(), 90 | instance=instance, 91 | zone=zone).execute() 92 | enqueue_start_task(instance, zone) 93 | 94 | def _maybe_start_instance(self, instance, zone): 95 | """Implementation for start action. 96 | 97 | Args: 98 | instance: Name of the instance to start. 99 | zone: Name of the zone the instance belongs to. 100 | 101 | """ 102 | if self.compute_service is None: 103 | logging.warning('Unable to start Compute instance, service unavailable.') 104 | return 105 | 106 | status = self._compute_status(instance, zone) 107 | 108 | logging.info('GCE VM \'%s (%s)\' status: \'%s\'.', 109 | instance, zone, status) 110 | 111 | if status == COMPUTE_STATUS_TERMINATED: 112 | logging.info('Starting GCE VM: %s (%s)', instance, zone) 113 | self.compute_service.instances().start( 114 | project=app_identity.get_application_id(), 115 | instance=instance, 116 | zone=zone).execute() 117 | 118 | if status != COMPUTE_STATUS_RUNNING: 119 | # If in an intermediate state: PROVISIONING, STAGING, STOPPING, requeue 120 | # the task to check back later. If in TERMINATED state, also requeue the 121 | # task since the start attempt may fail and we should retry. 122 | enqueue_start_task(instance, zone) 123 | 124 | def _compute_status(self, instance, zone): 125 | """Return the status of the compute instance.""" 126 | if self.compute_service is None: 127 | logging.warning('Service unavailable: unable to start GCE VM: %s (%s)', 128 | instance, zone) 129 | return 130 | 131 | info = self.compute_service.instances().get( 132 | project=app_identity.get_application_id(), 133 | instance=instance, 134 | zone=zone).execute() 135 | return info[COMPUTE_STATUS] 136 | 137 | def get(self, action, instance, zone): 138 | if action == ACTION_STATUS: 139 | self.response.write(self._compute_status(instance, zone)) 140 | 141 | def post(self, action, instance, zone): 142 | if action == ACTION_START: 143 | self._maybe_start_instance(instance, zone) 144 | elif action == ACTION_RESTART: 145 | self._maybe_restart_instance(instance, zone) 146 | -------------------------------------------------------------------------------- /src/app_engine/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All Rights Reserved. 2 | 3 | """AppRTC Constants. 4 | 5 | This module contains the constants used in AppRTC Python modules. 6 | """ 7 | import os 8 | 9 | ROOM_MEMCACHE_EXPIRATION_SEC = 60 * 60 * 24 10 | MEMCACHE_RETRY_LIMIT = 100 11 | 12 | LOOPBACK_CLIENT_ID = 'LOOPBACK_CLIENT_ID' 13 | 14 | TURN_BASE_URL = 'https://computeengineondemand.appspot.com' 15 | TURN_URL_TEMPLATE = '%s/turn?username=%s&key=%s' 16 | CEOD_KEY = '4080218913' 17 | 18 | # Dictionary keys in the collider instance info constant. 19 | WSS_INSTANCE_HOST_KEY = 'host_port_pair' 20 | WSS_INSTANCE_NAME_KEY = 'vm_name' 21 | WSS_INSTANCE_ZONE_KEY = 'zone' 22 | WSS_INSTANCES = [{ 23 | WSS_INSTANCE_HOST_KEY: 'apprtc-ws.webrtc.org:443', 24 | WSS_INSTANCE_NAME_KEY: 'wsserver-std', 25 | WSS_INSTANCE_ZONE_KEY: 'us-central1-a' 26 | }, { 27 | WSS_INSTANCE_HOST_KEY: 'apprtc-ws-2.webrtc.org:443', 28 | WSS_INSTANCE_NAME_KEY: 'wsserver-std-2', 29 | WSS_INSTANCE_ZONE_KEY: 'us-central1-f' 30 | }] 31 | 32 | WSS_HOST_PORT_PAIRS = [ins[WSS_INSTANCE_HOST_KEY] for ins in WSS_INSTANCES] 33 | 34 | # memcache key for the active collider host. 35 | WSS_HOST_ACTIVE_HOST_KEY = 'wss_host_active_host' 36 | 37 | # Dictionary keys in the collider probing result. 38 | WSS_HOST_IS_UP_KEY = 'is_up' 39 | WSS_HOST_STATUS_CODE_KEY = 'status_code' 40 | WSS_HOST_ERROR_MESSAGE_KEY = 'error_message' 41 | 42 | RESPONSE_ERROR = 'ERROR' 43 | RESPONSE_ROOM_FULL = 'FULL' 44 | RESPONSE_UNKNOWN_ROOM = 'UNKNOWN_ROOM' 45 | RESPONSE_UNKNOWN_CLIENT = 'UNKNOWN_CLIENT' 46 | RESPONSE_DUPLICATE_CLIENT = 'DUPLICATE_CLIENT' 47 | RESPONSE_SUCCESS = 'SUCCESS' 48 | RESPONSE_INVALID_REQUEST = 'INVALID_REQUEST' 49 | 50 | IS_DEV_SERVER = os.environ.get('APPLICATION_ID', '').startswith('dev') 51 | 52 | BIGQUERY_URL = 'https://www.googleapis.com/auth/bigquery' 53 | 54 | # Dataset used in production. 55 | BIGQUERY_DATASET_PROD = 'prod' 56 | 57 | # Dataset used when running locally. 58 | BIGQUERY_DATASET_LOCAL = 'dev' 59 | 60 | # BigQuery table within the dataset. 61 | BIGQUERY_TABLE = 'analytics' 62 | -------------------------------------------------------------------------------- /src/app_engine/cron.yaml: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: CEOD probing job on 5 min interval 3 | url: /probe/ceod 4 | schedule: every 5 minutes synchronized 5 | 6 | - description: collider probing job on 5 min interval 7 | url: /probe/collider 8 | schedule: every 5 minutes synchronized 9 | -------------------------------------------------------------------------------- /src/app_engine/test_util.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All Rights Reserved. 2 | 3 | """Utilities for unit tests.""" 4 | 5 | 6 | class ReplaceFunction(object): 7 | """Makes it easier to replace a function in a class or module.""" 8 | 9 | def __init__(self, obj, function_name, new_function): 10 | self.obj = obj 11 | self.function_name = function_name 12 | self.old_function = getattr(self.obj, self.function_name) 13 | setattr(self.obj, self.function_name, new_function) 14 | 15 | def __del__(self): 16 | setattr(self.obj, self.function_name, self.old_function) 17 | 18 | 19 | class CapturingFunction(object): 20 | """Captures the last arguments called on a function.""" 21 | 22 | def __init__(self, return_value=None): 23 | self.num_calls = 0 24 | self.return_value = return_value 25 | self.last_args = None 26 | self.last_kwargs = None 27 | 28 | def __call__(self, *args, **kwargs): 29 | self.last_args = args 30 | self.last_kwargs = kwargs 31 | self.num_calls += 1 32 | 33 | if callable(self.return_value): 34 | return self.return_value() 35 | 36 | return self.return_value 37 | -------------------------------------------------------------------------------- /src/collider/README.md: -------------------------------------------------------------------------------- 1 | # Collider 2 | 3 | A websocket-based signaling server in Go. 4 | 5 | ## Building 6 | 7 | 1. Install the Go tools and workspaces as documented at http://golang.org/doc/install and http://golang.org/doc/code.html 8 | 9 | 2. Checkout the `webrtc` repository, 10 | 11 | git clone https://github.com/webrtc/apprtc.git 12 | 13 | 3. Link the collider directories into `$GOPATH/src` 14 | 15 | ln -s apprtc/src/collider/collider $GOPATH/src/ 16 | ln -s apprtc/src/collider/collidermain $GOPATH/src/ 17 | ln -s apprtc/src/collider/collidertest $GOPATH/src/ 18 | 19 | 4. Install dependencies 20 | 21 | go get collidermain 22 | 23 | 5. Install `collidermain` 24 | 25 | go install collidermain 26 | 27 | 28 | ## Running 29 | 30 | $GOPATH/bin/collidermain -port=8089 -tls=true 31 | 32 | ## Testing 33 | 34 | go test collider 35 | 36 | -------------------------------------------------------------------------------- /src/collider/collider/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file in the root of the source 4 | // tree. 5 | 6 | package collider 7 | 8 | import ( 9 | "errors" 10 | "io" 11 | "log" 12 | "time" 13 | ) 14 | 15 | const maxQueuedMsgCount = 1024 16 | 17 | type client struct { 18 | id string 19 | // rwc is the interface to access the websocket connection. 20 | // It is set after the client registers with the server. 21 | rwc io.ReadWriteCloser 22 | // msgs is the queued messages sent from this client. 23 | msgs []string 24 | // timer is used to remove this client if unregistered after a timeout. 25 | timer *time.Timer 26 | } 27 | 28 | func newClient(id string, t *time.Timer) *client { 29 | c := client{id: id, timer: t} 30 | return &c 31 | } 32 | 33 | func (c *client) setTimer(t *time.Timer) { 34 | if c.timer != nil { 35 | c.timer.Stop() 36 | } 37 | c.timer = t 38 | } 39 | 40 | // register binds the ReadWriteCloser to the client if it's not done yet. 41 | func (c *client) register(rwc io.ReadWriteCloser) error { 42 | if c.rwc != nil { 43 | log.Printf("Not registering because the client %s already has a connection", c.id) 44 | return errors.New("Duplicated registration") 45 | } 46 | c.setTimer(nil) 47 | c.rwc = rwc 48 | return nil 49 | } 50 | 51 | // deregister closes the ReadWriteCloser if it exists. 52 | func (c *client) deregister() { 53 | if c.rwc != nil { 54 | c.rwc.Close() 55 | c.rwc = nil 56 | } 57 | } 58 | 59 | // registered returns true if the client has registered. 60 | func (c *client) registered() bool { 61 | return c.rwc != nil 62 | } 63 | 64 | // enqueue adds a message to the client's message queue. 65 | func (c *client) enqueue(msg string) error { 66 | if len(c.msgs) >= maxQueuedMsgCount { 67 | return errors.New("Too many messages queued for the client") 68 | } 69 | c.msgs = append(c.msgs, msg) 70 | return nil 71 | } 72 | 73 | // sendQueued the queued messages to the other client. 74 | func (c *client) sendQueued(other *client) error { 75 | if c.id == other.id || other.rwc == nil { 76 | return errors.New("Invalid client") 77 | } 78 | for _, m := range c.msgs { 79 | sendServerMsg(other.rwc, m) 80 | } 81 | c.msgs = nil 82 | log.Printf("Sent queued messages from %s to %s", c.id, other.id) 83 | return nil 84 | } 85 | 86 | // send sends the message to the other client if the other client has registered, 87 | // or queues the message otherwise. 88 | func (c *client) send(other *client, msg string) error { 89 | if c.id == other.id { 90 | return errors.New("Invalid client") 91 | } 92 | if other.rwc != nil { 93 | return sendServerMsg(other.rwc, msg) 94 | } 95 | return c.enqueue(msg) 96 | } 97 | -------------------------------------------------------------------------------- /src/collider/collider/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file in the root of the source 4 | // tree. 5 | 6 | package collider 7 | 8 | import ( 9 | "collidertest" 10 | "testing" 11 | ) 12 | 13 | func TestNewClient(t *testing.T) { 14 | id := "abc" 15 | c := newClient(id, nil) 16 | if c.id != id { 17 | t.Errorf("newClient(%q).id = %s, want %q", id, c.id, id) 18 | } 19 | if c.rwc != nil { 20 | t.Errorf("newClient(%q).rwc = %v, want nil", id, c.rwc) 21 | } 22 | if c.msgs != nil { 23 | t.Errorf("newClient(%q).msgs = %v, want nil", id, c.msgs) 24 | } 25 | } 26 | 27 | // Tests that registering the client twice will fail. 28 | func TestClientRegister(t *testing.T) { 29 | id := "abc" 30 | c := newClient(id, nil) 31 | var rwc collidertest.MockReadWriteCloser 32 | if err := c.register(&rwc); err != nil { 33 | t.Errorf("newClient(%q).register(%v) got error: %s, want nil", id, &rwc, err.Error()) 34 | } 35 | if c.rwc != &rwc { 36 | t.Errorf("client.rwc after client.register(%v) = %v, want %v", &rwc, c.rwc, &rwc) 37 | } 38 | 39 | // Register again and it should fail. 40 | if err := c.register(&rwc); err == nil { 41 | t.Errorf("Second call of client.register(%v): nil, want !nil error", &rwc) 42 | } 43 | } 44 | 45 | // Tests that queued messages are delivered in sendQueued. 46 | func TestClientSendQueued(t *testing.T) { 47 | src := newClient("abc", nil) 48 | src.enqueue("hello") 49 | 50 | dest := newClient("def", nil) 51 | rwc := collidertest.MockReadWriteCloser{Closed: false} 52 | 53 | dest.register(&rwc) 54 | src.sendQueued(dest) 55 | 56 | if rwc.Msg == "" { 57 | t.Errorf("After sending queued messages from src to dest, dest.rwc.Msg = %v, want non-empty", rwc.Msg) 58 | } 59 | if len(src.msgs) != 0 { 60 | t.Errorf("After sending queued messages from src to dest, src.msgs = %v, want empty", src.msgs) 61 | } 62 | } 63 | 64 | // Tests that messages are queued when the other client is not registered, or delivered immediately otherwise. 65 | func TestClientSend(t *testing.T) { 66 | src := newClient("abc", nil) 67 | dest := newClient("def", nil) 68 | 69 | // The message should be queued since dest has not registered. 70 | m := "hello" 71 | if err := src.send(dest, m); err != nil { 72 | t.Errorf("When dest is not registered, src.send(dest, %q) got error: %s, want nil", m, err.Error()) 73 | } 74 | if len(src.msgs) != 1 || src.msgs[0] != m { 75 | t.Errorf("After src.send(dest, %q) when dest is not registered, src.msgs = %v, want [%q]", m, src.msgs, m) 76 | } 77 | 78 | rwc := collidertest.MockReadWriteCloser{Closed: false} 79 | dest.register(&rwc) 80 | 81 | // The message should be sent this time. 82 | m2 := "hi" 83 | src.send(dest, m2) 84 | 85 | if rwc.Msg == "" { 86 | t.Errorf("When dest is registered, after src.send(dest, %q), dest.rwc.Msg = %v, want %q", m2, rwc.Msg, m2) 87 | } 88 | if len(src.msgs) != 1 || src.msgs[0] != m { 89 | t.Errorf("When dest is registered, after src.send(dest, %q), src.msgs = %v, want [%q]", m2, src.msgs, m) 90 | } 91 | } 92 | 93 | // Tests that deregistering the client will close the ReadWriteCloser. 94 | func TestClientDeregister(t *testing.T) { 95 | c := newClient("abc", nil) 96 | rwc := collidertest.MockReadWriteCloser{Closed: false} 97 | 98 | c.register(&rwc) 99 | c.deregister() 100 | if !rwc.Closed { 101 | t.Errorf("After client.close(), rwc.Closed = %t, want true", rwc.Closed) 102 | } 103 | } 104 | 105 | func TestClientMaxQueuedMsg(t *testing.T) { 106 | c := newClient("abc", nil) 107 | for i := 0; i < maxQueuedMsgCount; i++ { 108 | if err := c.enqueue("msg"); err != nil { 109 | t.Errorf("client.enqueue(...) got error %v after %d calls, want nil", err, i) 110 | } 111 | } 112 | if err := c.enqueue("msg"); err == nil { 113 | t.Error("client.enqueue(...) got no error after maxQueuedMsgCount + 1 calls, want error") 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/collider/collider/dashboard.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file in the root of the source 4 | // tree. 5 | 6 | package collider 7 | 8 | import ( 9 | "sync" 10 | "time" 11 | ) 12 | 13 | const maxErrLogLen = 128 14 | 15 | type errEvent struct { 16 | Time time.Time `json:"t"` 17 | Err string `json:"e"` 18 | } 19 | 20 | type dashboard struct { 21 | lock sync.Mutex 22 | 23 | startTime time.Time 24 | 25 | totalWs int 26 | totalRecvMsgs int 27 | totalSendMsgs int 28 | wsErrs int 29 | httpErrs int 30 | } 31 | 32 | type statusReport struct { 33 | UpTimeSec float64 `json:"upsec"` 34 | OpenWs int `json:"openws"` 35 | TotalWs int `json:"totalws"` 36 | WsErrs int `json:"wserrors"` 37 | HttpErrs int `json:"httperrors"` 38 | } 39 | 40 | func newDashboard() *dashboard { 41 | return &dashboard{startTime: time.Now()} 42 | } 43 | 44 | func (db *dashboard) getReport(rs *roomTable) statusReport { 45 | db.lock.Lock() 46 | defer db.lock.Unlock() 47 | 48 | upTime := time.Since(db.startTime) 49 | return statusReport{ 50 | UpTimeSec: upTime.Seconds(), 51 | OpenWs: rs.wsCount(), 52 | TotalWs: db.totalWs, 53 | WsErrs: db.wsErrs, 54 | HttpErrs: db.httpErrs, 55 | } 56 | } 57 | 58 | func (db *dashboard) incrWs() { 59 | db.lock.Lock() 60 | defer db.lock.Unlock() 61 | db.totalWs += 1 62 | } 63 | 64 | func (db *dashboard) onWsErr(err error) { 65 | db.lock.Lock() 66 | defer db.lock.Unlock() 67 | 68 | db.wsErrs += 1 69 | } 70 | 71 | func (db *dashboard) onHttpErr(err error) { 72 | db.lock.Lock() 73 | defer db.lock.Unlock() 74 | 75 | db.httpErrs += 1 76 | } 77 | -------------------------------------------------------------------------------- /src/collider/collider/dashboard_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file in the root of the source 4 | // tree. 5 | 6 | package collider 7 | 8 | import ( 9 | "collidertest" 10 | "errors" 11 | "log" 12 | "reflect" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | func createNewRoomTable() *roomTable { 18 | return newRoomTable(time.Second, "") 19 | } 20 | 21 | func verifyIntValue(t *testing.T, i interface{}, name string, expected int, tag string) { 22 | v := reflect.ValueOf(i) 23 | f := v.FieldByName(name) 24 | if f.Interface() != expected { 25 | log.Printf("interface=%#v", i) 26 | t.Errorf("%s is %d, want %d", tag, f.Interface(), expected) 27 | } 28 | } 29 | 30 | func verifyStringValue(t *testing.T, i interface{}, name string, expected string, tag string) { 31 | v := reflect.ValueOf(i) 32 | f := v.FieldByName(name) 33 | if f.Interface() != expected { 34 | log.Printf("interface=%#v", i) 35 | t.Errorf("%s is %d, want %d", tag, f.Interface(), expected) 36 | } 37 | } 38 | 39 | func verifyArrayLen(t *testing.T, i interface{}, name string, expected int, tag string) { 40 | v := reflect.ValueOf(i) 41 | f := v.FieldByName(name) 42 | if f.Len() != expected { 43 | log.Printf("interface=%#v", i) 44 | t.Errorf("%s is %d, want %d", tag, f.Len(), expected) 45 | } 46 | } 47 | 48 | func TestDashboardWsCount(t *testing.T) { 49 | rt := createNewRoomTable() 50 | db := newDashboard() 51 | r := db.getReport(rt) 52 | if r.OpenWs != 0 { 53 | t.Errorf("db.getReport().OpenWs is %d, want 0", r.OpenWs) 54 | } 55 | if r.TotalWs != 0 { 56 | t.Errorf("db.getReport().TotalWs is %d, want 0", r.TotalWs) 57 | } 58 | 59 | db.incrWs() 60 | r = db.getReport(rt) 61 | if r.OpenWs != 0 { 62 | t.Errorf("db.getReport().OpenWs is %d, want 0", r.OpenWs) 63 | } 64 | if r.TotalWs != 1 { 65 | t.Errorf("db.getReport().TotalWs is %d, want 1", r.TotalWs) 66 | } 67 | 68 | rt.register("r", "c", &collidertest.MockReadWriteCloser{Closed: false}) 69 | r = db.getReport(rt) 70 | if r.OpenWs != 1 { 71 | t.Errorf("db.getReport().OpenWs is %d, want 1", r.OpenWs) 72 | } 73 | } 74 | 75 | func TestDashboardWsErr(t *testing.T) { 76 | rt := createNewRoomTable() 77 | db := newDashboard() 78 | r := db.getReport(rt) 79 | if r.WsErrs != 0 { 80 | t.Errorf("db.getReport().WsErrs is %d, want 0", r.WsErrs) 81 | } 82 | 83 | db.onWsErr(errors.New("Fake error")) 84 | r = db.getReport(rt) 85 | if r.WsErrs != 1 { 86 | t.Errorf("db.getReport().WsErrs is %d, want 1", r.WsErrs) 87 | } 88 | } 89 | 90 | func TestDashboardHttpErr(t *testing.T) { 91 | rt := createNewRoomTable() 92 | db := newDashboard() 93 | r := db.getReport(rt) 94 | if r.HttpErrs != 0 { 95 | t.Errorf("db.getReport().HttpErrs is %d, want 0", r.HttpErrs) 96 | } 97 | db.onHttpErr(errors.New("Fake error")) 98 | r = db.getReport(rt) 99 | if r.HttpErrs != 1 { 100 | t.Errorf("db.getReport().HttpErrs is %d, want 1", r.HttpErrs) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/collider/collider/messages.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file in the root of the source 4 | // tree. 5 | 6 | package collider 7 | 8 | import ( 9 | "encoding/json" 10 | "io" 11 | ) 12 | 13 | // WebSocket message from the client. 14 | type wsClientMsg struct { 15 | Cmd string `json:"cmd"` 16 | RoomID string `json:"roomid"` 17 | ClientID string `json:"clientid"` 18 | Msg string `json:"msg"` 19 | } 20 | 21 | // wsServerMsg is a message sent to a client on behalf of another client. 22 | type wsServerMsg struct { 23 | Msg string `json:"msg"` 24 | Error string `json:"error"` 25 | } 26 | 27 | // sendServerMsg sends a wsServerMsg composed from |msg| to the connection. 28 | func sendServerMsg(w io.Writer, msg string) error { 29 | m := wsServerMsg{ 30 | Msg: msg, 31 | } 32 | return send(w, m) 33 | } 34 | 35 | // sendServerErr sends a wsServerMsg composed from |errMsg| to the connection. 36 | func sendServerErr(w io.Writer, errMsg string) error { 37 | m := wsServerMsg{ 38 | Error: errMsg, 39 | } 40 | return send(w, m) 41 | } 42 | 43 | // send writes a generic object as JSON to the writer. 44 | func send(w io.Writer, data interface{}) error { 45 | enc := json.NewEncoder(w) 46 | if err := enc.Encode(data); err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /src/collider/collider/room.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file in the root of the source 4 | // tree. 5 | 6 | package collider 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "io" 12 | "log" 13 | "net/http" 14 | "time" 15 | ) 16 | 17 | const maxRoomCapacity = 2 18 | 19 | type room struct { 20 | parent *roomTable 21 | id string 22 | // A mapping from the client ID to the client object. 23 | clients map[string]*client 24 | registerTimeout time.Duration 25 | roomSrvUrl string 26 | } 27 | 28 | func newRoom(p *roomTable, id string, to time.Duration, rs string) *room { 29 | return &room{parent: p, id: id, clients: make(map[string]*client), registerTimeout: to, roomSrvUrl: rs} 30 | } 31 | 32 | // client returns the client, or creates it if it does not exist and the room is not full. 33 | func (rm *room) client(clientID string) (*client, error) { 34 | if c, ok := rm.clients[clientID]; ok { 35 | return c, nil 36 | } 37 | if len(rm.clients) >= maxRoomCapacity { 38 | log.Printf("Room %s is full, not adding client %s", rm.id, clientID) 39 | return nil, errors.New("Max room capacity reached") 40 | } 41 | 42 | var timer *time.Timer 43 | if rm.parent != nil { 44 | timer = time.AfterFunc(rm.registerTimeout, func() { 45 | if c := rm.clients[clientID]; c != nil { 46 | rm.parent.removeIfUnregistered(rm.id, c) 47 | } 48 | }) 49 | } 50 | rm.clients[clientID] = newClient(clientID, timer) 51 | 52 | log.Printf("Added client %s to room %s", clientID, rm.id) 53 | 54 | return rm.clients[clientID], nil 55 | } 56 | 57 | // register binds a client to the ReadWriteCloser. 58 | func (rm *room) register(clientID string, rwc io.ReadWriteCloser) error { 59 | c, err := rm.client(clientID) 60 | if err != nil { 61 | return err 62 | } 63 | if err = c.register(rwc); err != nil { 64 | return err 65 | } 66 | 67 | log.Printf("Client %s registered in room %s", clientID, rm.id) 68 | 69 | // Sends the queued messages from the other client of the room. 70 | if len(rm.clients) > 1 { 71 | for _, otherClient := range rm.clients { 72 | otherClient.sendQueued(c) 73 | } 74 | } 75 | return nil 76 | } 77 | 78 | // send sends the message to the other client of the room, or queues the message if the other client has not joined. 79 | func (rm *room) send(srcClientID string, msg string) error { 80 | src, err := rm.client(srcClientID) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | // Queue the message if the other client has not joined. 86 | if len(rm.clients) == 1 { 87 | return rm.clients[srcClientID].enqueue(msg) 88 | } 89 | 90 | // Send the message to the other client of the room. 91 | for _, oc := range rm.clients { 92 | if oc.id != srcClientID { 93 | return src.send(oc, msg) 94 | } 95 | } 96 | 97 | // The room must be corrupted. 98 | return errors.New(fmt.Sprintf("Corrupted room %+v", rm)) 99 | } 100 | 101 | // remove closes the client connection and removes the client specified by the |clientID|. 102 | func (rm *room) remove(clientID string) { 103 | if c, ok := rm.clients[clientID]; ok { 104 | c.deregister() 105 | delete(rm.clients, clientID) 106 | log.Printf("Removed client %s from room %s", clientID, rm.id) 107 | 108 | // Send bye to the room Server. 109 | resp, err := http.Post(rm.roomSrvUrl+"/bye/"+rm.id+"/"+clientID, "text", nil) 110 | if err != nil { 111 | log.Printf("Failed to post BYE to room server %s: %v", rm.roomSrvUrl, err) 112 | } 113 | if resp != nil && resp.Body != nil { 114 | resp.Body.Close() 115 | } 116 | } 117 | } 118 | 119 | // empty returns true if there is no client in the room. 120 | func (rm *room) empty() bool { 121 | return len(rm.clients) == 0 122 | } 123 | 124 | func (rm *room) wsCount() int { 125 | count := 0 126 | for _, c := range rm.clients { 127 | if c.registered() { 128 | count += 1 129 | } 130 | } 131 | return count 132 | } 133 | -------------------------------------------------------------------------------- /src/collider/collider/roomTable.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file in the root of the source 4 | // tree. 5 | 6 | package collider 7 | 8 | import ( 9 | "io" 10 | "log" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | // A thread-safe map of rooms. 16 | type roomTable struct { 17 | lock sync.Mutex 18 | rooms map[string]*room 19 | registerTimeout time.Duration 20 | roomSrvUrl string 21 | } 22 | 23 | func newRoomTable(to time.Duration, rs string) *roomTable { 24 | return &roomTable{rooms: make(map[string]*room), registerTimeout: to, roomSrvUrl: rs} 25 | } 26 | 27 | // room returns the room specified by |id|, or creates the room if it does not exist. 28 | func (rt *roomTable) room(id string) *room { 29 | rt.lock.Lock() 30 | defer rt.lock.Unlock() 31 | 32 | return rt.roomLocked(id) 33 | } 34 | 35 | // roomLocked gets or creates the room without acquiring the lock. Used when the caller already acquired the lock. 36 | func (rt *roomTable) roomLocked(id string) *room { 37 | if r, ok := rt.rooms[id]; ok { 38 | return r 39 | } 40 | rt.rooms[id] = newRoom(rt, id, rt.registerTimeout, rt.roomSrvUrl) 41 | log.Printf("Created room %s", id) 42 | 43 | return rt.rooms[id] 44 | } 45 | 46 | // remove removes the client. If the room becomes empty, it also removes the room. 47 | func (rt *roomTable) remove(rid string, cid string) { 48 | rt.lock.Lock() 49 | defer rt.lock.Unlock() 50 | 51 | rt.removeLocked(rid, cid) 52 | } 53 | 54 | // removeLocked removes the client without acquiring the lock. Used when the caller already acquired the lock. 55 | func (rt *roomTable) removeLocked(rid string, cid string) { 56 | if r := rt.rooms[rid]; r != nil { 57 | r.remove(cid) 58 | if r.empty() { 59 | delete(rt.rooms, rid) 60 | log.Printf("Removed room %s", rid) 61 | } 62 | } 63 | } 64 | 65 | // send forwards the message to the room. If the room does not exist, it will create one. 66 | func (rt *roomTable) send(rid string, srcID string, msg string) error { 67 | rt.lock.Lock() 68 | defer rt.lock.Unlock() 69 | 70 | r := rt.roomLocked(rid) 71 | return r.send(srcID, msg) 72 | } 73 | 74 | // register forwards the register request to the room. If the room does not exist, it will create one. 75 | func (rt *roomTable) register(rid string, cid string, rwc io.ReadWriteCloser) error { 76 | rt.lock.Lock() 77 | defer rt.lock.Unlock() 78 | 79 | r := rt.roomLocked(rid) 80 | return r.register(cid, rwc) 81 | } 82 | 83 | // deregister clears the client's websocket registration. 84 | // We keep the client around until after a timeout, so that users roaming between networks can seamlessly reconnect. 85 | func (rt *roomTable) deregister(rid string, cid string) { 86 | rt.lock.Lock() 87 | defer rt.lock.Unlock() 88 | 89 | if r := rt.rooms[rid]; r != nil { 90 | if c := r.clients[cid]; c != nil { 91 | if c.registered() { 92 | c.deregister() 93 | 94 | c.setTimer(time.AfterFunc(rt.registerTimeout, func() { 95 | rt.removeIfUnregistered(rid, c) 96 | })) 97 | 98 | log.Printf("Deregistered client %s from room %s", c.id, rid) 99 | return 100 | } 101 | } 102 | } 103 | } 104 | 105 | // removeIfUnregistered removes the client if it has not registered. 106 | func (rt *roomTable) removeIfUnregistered(rid string, c *client) { 107 | log.Printf("Removing client %s from room %s due to timeout", c.id, rid) 108 | 109 | rt.lock.Lock() 110 | defer rt.lock.Unlock() 111 | 112 | if r := rt.rooms[rid]; r != nil { 113 | if c == r.clients[c.id] { 114 | if !c.registered() { 115 | rt.removeLocked(rid, c.id) 116 | return 117 | } 118 | } 119 | } 120 | } 121 | 122 | func (rt *roomTable) wsCount() int { 123 | rt.lock.Lock() 124 | defer rt.lock.Unlock() 125 | 126 | count := 0 127 | for _, r := range rt.rooms { 128 | count = count + r.wsCount() 129 | } 130 | return count 131 | } 132 | -------------------------------------------------------------------------------- /src/collider/collider/room_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file in the root of the source 4 | // tree. 5 | 6 | package collider 7 | 8 | import ( 9 | "collidertest" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func createNewRoom(id string) *room { 15 | return newRoom(nil, id, time.Second, "") 16 | } 17 | 18 | func TestNewRoom(t *testing.T) { 19 | id := "abc" 20 | r := createNewRoom(id) 21 | if r.id != id { 22 | t.Errorf("newRoom(%q).id = %q, want %q", id, r.id, id) 23 | } 24 | if len(r.clients) != 0 { 25 | t.Errorf("newRoom(%q).clients = %v, want empty", id, r.clients) 26 | } 27 | } 28 | 29 | func TestGetOrCreateClient(t *testing.T) { 30 | r := createNewRoom("ab") 31 | id1 := "1" 32 | c1, err := r.client(id1) 33 | if err != nil { 34 | t.Errorf("room.client(%q) got error: %s, want nil", id1, err.Error()) 35 | } 36 | 37 | if c2, _ := r.client("1"); c2 != c1 { 38 | t.Errorf("room.client(%q) = %v, want %v", id1, c2, c1) 39 | } 40 | 41 | id2 := "2" 42 | r.client(id2) 43 | if size := len(r.clients); size != 2 { 44 | t.Errorf("After calling room.client(%q) and room.client(%q), room.clients = %v, want of size 2", id1, id2, r.clients) 45 | } 46 | 47 | // Adding the third client should fail. 48 | id3 := "3" 49 | _, err = r.client(id3) 50 | if err == nil { 51 | t.Errorf("After calling room.client(%q), and room.client(%q), room.client(%q) got no error, want error", id1, id2, id3) 52 | } 53 | } 54 | 55 | // Tests that registering a client will deliver the queued message from the first client. 56 | func TestRoomRegister(t *testing.T) { 57 | r := createNewRoom("a") 58 | id1 := "1" 59 | c1, _ := r.client(id1) 60 | c1.enqueue("hello") 61 | 62 | rwc := collidertest.MockReadWriteCloser{Closed: false} 63 | id2 := "2" 64 | r.register(id2, &rwc) 65 | 66 | if size := len(r.clients); size != 2 { 67 | t.Errorf("After room.client(%q) and room.register(%q, %v), r.clients = %v, want of size 2", id1, id2, &rwc, r.clients) 68 | } 69 | c2, _ := r.client("2") 70 | if c2.rwc != &rwc { 71 | t.Errorf("After room.register(%q, %v), room.client(%q).rwc = %v, want %v", id2, &rwc, id2, c2.rwc, &rwc) 72 | } 73 | if rwc.Msg == "" { 74 | t.Error("After enqueuing a message on the first client and the second client c2 registers, c2.rwc.Msg is empty, want non-empty") 75 | } 76 | } 77 | 78 | // Tests that the message sent before the second client joins will be queued. 79 | func TestRoomSendQueued(t *testing.T) { 80 | r := createNewRoom("a") 81 | id := "1" 82 | m := "hi" 83 | if err := r.send(id, m); err != nil { 84 | t.Errorf("room.send(%q, %q) got error: %s, want nil", id, m, err.Error()) 85 | } 86 | 87 | c, _ := r.client(id) 88 | if len(c.msgs) != 1 { 89 | t.Errorf("After room.send(%q, %q), room.client(%q).msgs = %v, want of size 1", id, m, c.msgs) 90 | } 91 | } 92 | 93 | // Tests that the message sent after the second client joins will be delivered. 94 | func TestRoomSendImmediately(t *testing.T) { 95 | r := createNewRoom("a") 96 | rwc := collidertest.MockReadWriteCloser{Closed: false} 97 | id1, id2, m := "1", "2", "hi" 98 | r.register(id2, &rwc) 99 | 100 | if err := r.send(id1, m); err != nil { 101 | t.Errorf("room.send(%q, %q) got error: %s, want nil", id1, m, err.Error()) 102 | } 103 | c, _ := r.client("1") 104 | if len(c.msgs) != 0 { 105 | t.Errorf("After room.register(%q, ...) and room.send(%q, %q), room.client(%q).msgs = %v, want empty", id2, id1, m, id1, c.msgs) 106 | } 107 | if rwc.Msg == "" { 108 | t.Errorf("After room.register(%q, ...) and room.send(%q, %q), room.client(%q).rwc.Msg = %v, want non-empty", id2, id1, m, id2, rwc.Msg) 109 | } 110 | } 111 | 112 | // Tests that the client is closed and removed by room.remove. 113 | func TestRoomDelete(t *testing.T) { 114 | r := createNewRoom("a") 115 | rwc := collidertest.MockReadWriteCloser{Closed: false} 116 | id := "1" 117 | r.register(id, &rwc) 118 | 119 | r.remove(id) 120 | if !rwc.Closed { 121 | t.Errorf("After room.register(%q, &rwc) and room.remove(%q), rwc.Closed = false, want true", id, id) 122 | } 123 | if len(r.clients) != 0 { 124 | t.Errorf("After room.register(%q, ...) and room.remove(%q), room.clients = %v, want empty", id, id, r.clients) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/collider/collidermain/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file in the root of the source 4 | // tree. 5 | 6 | package main 7 | 8 | import ( 9 | "collider" 10 | "flag" 11 | "log" 12 | ) 13 | 14 | var tls = flag.Bool("tls", true, "whether TLS is used") 15 | var port = flag.Int("port", 443, "The TCP port that the server listens on") 16 | var roomSrv = flag.String("room-server", "https://apprtc.appspot.com", "The origin of the room server") 17 | 18 | func main() { 19 | flag.Parse() 20 | 21 | log.Printf("Starting collider: tls = %t, port = %d, room-server=%s", *tls, *port, *roomSrv) 22 | 23 | c := collider.NewCollider(*roomSrv) 24 | c.Run(*port, *tls) 25 | } 26 | -------------------------------------------------------------------------------- /src/collider/collidertest/mockrwc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file in the root of the source 4 | // tree. 5 | 6 | package collidertest 7 | 8 | type MockReadWriteCloser struct { 9 | Msg string 10 | Closed bool 11 | } 12 | 13 | func (f *MockReadWriteCloser) Read(p []byte) (n int, err error) { 14 | return 0, nil 15 | } 16 | func (f *MockReadWriteCloser) Write(p []byte) (n int, err error) { 17 | f.Msg = string(p) 18 | return len(p), nil 19 | } 20 | func (f *MockReadWriteCloser) Close() error { 21 | f.Closed = true 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /src/third_party/README.md: -------------------------------------------------------------------------------- 1 | # Third-Party Code 2 | 3 | This directory contains third party code licensed separate from the rest of the repository. See individual files for licensing details. 4 | 5 | These are libraries that are required but not provided by AppEngine. 6 | 7 | ## Adding and Upgrading 8 | 9 | The `requirements.txt` file contains a list of versioned dependencies. Add or upgrade packages as follows, 10 | 11 | 1. Add the new package or upgrading an existing package version in the `requirements.txt` file. 12 | 2. Remove the old package directories from the `third_party` directory. *This step may not be needed depending on your version of pip. There is a [bug](https://github.com/pypa/pip/issues/1489) in some versions of `pip` that causes problems with in-place upgrades.* 13 | 3. Install the dependencies by running the following command from the `third_party` parent directory, 14 | 15 | ``` 16 | pip install --target=third_party --upgrade -r third_party/requirements.txt 17 | ``` 18 | 19 | 4. After the upgrade is complete you should commit all directories and files that were added/changed in the `third_party` directory. 20 | -------------------------------------------------------------------------------- /src/third_party/apiclient/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | __version__ = "1.2" 16 | -------------------------------------------------------------------------------- /src/third_party/apiclient/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.4 2 | # 3 | # Copyright (C) 2010 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Errors for the library. 18 | 19 | All exceptions defined by the library 20 | should be defined in this file. 21 | """ 22 | 23 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 24 | 25 | 26 | from oauth2client import util 27 | from oauth2client.anyjson import simplejson 28 | 29 | 30 | class Error(Exception): 31 | """Base error for this module.""" 32 | pass 33 | 34 | 35 | class HttpError(Error): 36 | """HTTP data was invalid or unexpected.""" 37 | 38 | @util.positional(3) 39 | def __init__(self, resp, content, uri=None): 40 | self.resp = resp 41 | self.content = content 42 | self.uri = uri 43 | 44 | def _get_reason(self): 45 | """Calculate the reason for the error from the response content.""" 46 | reason = self.resp.reason 47 | try: 48 | data = simplejson.loads(self.content) 49 | reason = data['error']['message'] 50 | except (ValueError, KeyError): 51 | pass 52 | if reason is None: 53 | reason = '' 54 | return reason 55 | 56 | def __repr__(self): 57 | if self.uri: 58 | return '' % ( 59 | self.resp.status, self.uri, self._get_reason().strip()) 60 | else: 61 | return '' % (self.resp.status, self._get_reason()) 62 | 63 | __str__ = __repr__ 64 | 65 | 66 | class InvalidJsonError(Error): 67 | """The JSON returned could not be parsed.""" 68 | pass 69 | 70 | 71 | class UnknownFileType(Error): 72 | """File type unknown or unexpected.""" 73 | pass 74 | 75 | 76 | class UnknownLinkType(Error): 77 | """Link type unknown or unexpected.""" 78 | pass 79 | 80 | 81 | class UnknownApiNameOrVersion(Error): 82 | """No API with that name and version exists.""" 83 | pass 84 | 85 | 86 | class UnacceptableMimeTypeError(Error): 87 | """That is an unacceptable mimetype for this operation.""" 88 | pass 89 | 90 | 91 | class MediaUploadSizeError(Error): 92 | """Media is larger than the method can accept.""" 93 | pass 94 | 95 | 96 | class ResumableUploadError(HttpError): 97 | """Error occured during resumable upload.""" 98 | pass 99 | 100 | 101 | class InvalidChunkSizeError(Error): 102 | """The given chunksize is not valid.""" 103 | pass 104 | 105 | class InvalidNotificationError(Error): 106 | """The channel Notification is invalid.""" 107 | pass 108 | 109 | class BatchError(HttpError): 110 | """Error occured during batch operations.""" 111 | 112 | @util.positional(2) 113 | def __init__(self, reason, resp=None, content=None): 114 | self.resp = resp 115 | self.content = content 116 | self.reason = reason 117 | 118 | def __repr__(self): 119 | return '' % (self.resp.status, self.reason) 120 | 121 | __str__ = __repr__ 122 | 123 | 124 | class UnexpectedMethodError(Error): 125 | """Exception raised by RequestMockBuilder on unexpected calls.""" 126 | 127 | @util.positional(1) 128 | def __init__(self, methodId=None): 129 | """Constructor for an UnexpectedMethodError.""" 130 | super(UnexpectedMethodError, self).__init__( 131 | 'Received unexpected call %s' % methodId) 132 | 133 | 134 | class UnexpectedBodyError(Error): 135 | """Exception raised by RequestMockBuilder on unexpected bodies.""" 136 | 137 | def __init__(self, expected, provided): 138 | """Constructor for an UnexpectedMethodError.""" 139 | super(UnexpectedBodyError, self).__init__( 140 | 'Expected: [%s] - Provided: [%s]' % (expected, provided)) 141 | -------------------------------------------------------------------------------- /src/third_party/apiclient/sample_tools.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Utilities for making samples. 16 | 17 | Consolidates a lot of code commonly repeated in sample applications. 18 | """ 19 | 20 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 21 | __all__ = ['init'] 22 | 23 | 24 | import argparse 25 | import httplib2 26 | import os 27 | 28 | from apiclient import discovery 29 | from oauth2client import client 30 | from oauth2client import file 31 | from oauth2client import tools 32 | 33 | 34 | def init(argv, name, version, doc, filename, scope=None, parents=[]): 35 | """A common initialization routine for samples. 36 | 37 | Many of the sample applications do the same initialization, which has now 38 | been consolidated into this function. This function uses common idioms found 39 | in almost all the samples, i.e. for an API with name 'apiname', the 40 | credentials are stored in a file named apiname.dat, and the 41 | client_secrets.json file is stored in the same directory as the application 42 | main file. 43 | 44 | Args: 45 | argv: list of string, the command-line parameters of the application. 46 | name: string, name of the API. 47 | version: string, version of the API. 48 | doc: string, description of the application. Usually set to __doc__. 49 | file: string, filename of the application. Usually set to __file__. 50 | parents: list of argparse.ArgumentParser, additional command-line flags. 51 | scope: string, The OAuth scope used. 52 | 53 | Returns: 54 | A tuple of (service, flags), where service is the service object and flags 55 | is the parsed command-line flags. 56 | """ 57 | if scope is None: 58 | scope = 'https://www.googleapis.com/auth/' + name 59 | 60 | # Parser command-line arguments. 61 | parent_parsers = [tools.argparser] 62 | parent_parsers.extend(parents) 63 | parser = argparse.ArgumentParser( 64 | description=doc, 65 | formatter_class=argparse.RawDescriptionHelpFormatter, 66 | parents=parent_parsers) 67 | flags = parser.parse_args(argv[1:]) 68 | 69 | # Name of a file containing the OAuth 2.0 information for this 70 | # application, including client_id and client_secret, which are found 71 | # on the API Access tab on the Google APIs 72 | # Console . 73 | client_secrets = os.path.join(os.path.dirname(filename), 74 | 'client_secrets.json') 75 | 76 | # Set up a Flow object to be used if we need to authenticate. 77 | flow = client.flow_from_clientsecrets(client_secrets, 78 | scope=scope, 79 | message=tools.message_if_missing(client_secrets)) 80 | 81 | # Prepare credentials, and authorize HTTP object with them. 82 | # If the credentials don't exist or are invalid run through the native client 83 | # flow. The Storage object will ensure that if successful the good 84 | # credentials will get written back to a file. 85 | storage = file.Storage(name + '.dat') 86 | credentials = storage.get() 87 | if credentials is None or credentials.invalid: 88 | credentials = tools.run_flow(flow, storage, flags) 89 | http = credentials.authorize(http = httplib2.Http()) 90 | 91 | # Construct a service object via the discovery service. 92 | service = discovery.build(name, version, http=http) 93 | return (service, flags) 94 | -------------------------------------------------------------------------------- /src/third_party/google_api_python_client-1.2.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.1 2 | Name: google-api-python-client 3 | Version: 1.2 4 | Summary: Google API Client Library for Python 5 | Home-page: http://code.google.com/p/google-api-python-client/ 6 | Author: Joe Gregorio 7 | Author-email: jcgregorio@google.com 8 | License: Apache 2.0 9 | Description: The Google API Client for Python is a client library for 10 | accessing the Plus, Moderator, and many other Google APIs. 11 | Keywords: google api client 12 | Platform: UNKNOWN 13 | Classifier: Development Status :: 5 - Production/Stable 14 | Classifier: Intended Audience :: Developers 15 | Classifier: License :: OSI Approved :: Apache Software License 16 | Classifier: Operating System :: POSIX 17 | Classifier: Topic :: Internet :: WWW/HTTP 18 | -------------------------------------------------------------------------------- /src/third_party/google_api_python_client-1.2.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | FAQ 3 | LICENSE 4 | MANIFEST.in 5 | README 6 | setpath.sh 7 | setup.cfg 8 | setup.py 9 | apiclient/__init__.py 10 | apiclient/channel.py 11 | apiclient/discovery.py 12 | apiclient/errors.py 13 | apiclient/http.py 14 | apiclient/mimeparse.py 15 | apiclient/model.py 16 | apiclient/sample_tools.py 17 | apiclient/schema.py 18 | google_api_python_client.egg-info/PKG-INFO 19 | google_api_python_client.egg-info/SOURCES.txt 20 | google_api_python_client.egg-info/dependency_links.txt 21 | google_api_python_client.egg-info/requires.txt 22 | google_api_python_client.egg-info/top_level.txt 23 | oauth2client/__init__.py 24 | oauth2client/anyjson.py 25 | oauth2client/appengine.py 26 | oauth2client/client.py 27 | oauth2client/clientsecrets.py 28 | oauth2client/crypt.py 29 | oauth2client/django_orm.py 30 | oauth2client/file.py 31 | oauth2client/gce.py 32 | oauth2client/keyring_storage.py 33 | oauth2client/locked_file.py 34 | oauth2client/multistore_file.py 35 | oauth2client/old_run.py 36 | oauth2client/tools.py 37 | oauth2client/util.py 38 | oauth2client/xsrfutil.py 39 | uritemplate/__init__.py -------------------------------------------------------------------------------- /src/third_party/google_api_python_client-1.2.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/third_party/google_api_python_client-1.2.egg-info/installed-files.txt: -------------------------------------------------------------------------------- 1 | ../apiclient/http.py 2 | ../apiclient/mimeparse.py 3 | ../apiclient/__init__.py 4 | ../apiclient/channel.py 5 | ../apiclient/schema.py 6 | ../apiclient/model.py 7 | ../apiclient/discovery.py 8 | ../apiclient/errors.py 9 | ../apiclient/sample_tools.py 10 | ../oauth2client/__init__.py 11 | ../oauth2client/django_orm.py 12 | ../oauth2client/clientsecrets.py 13 | ../oauth2client/anyjson.py 14 | ../oauth2client/appengine.py 15 | ../oauth2client/crypt.py 16 | ../oauth2client/file.py 17 | ../oauth2client/old_run.py 18 | ../oauth2client/client.py 19 | ../oauth2client/keyring_storage.py 20 | ../oauth2client/util.py 21 | ../oauth2client/multistore_file.py 22 | ../oauth2client/locked_file.py 23 | ../oauth2client/gce.py 24 | ../oauth2client/tools.py 25 | ../oauth2client/xsrfutil.py 26 | ../uritemplate/__init__.py 27 | ../apiclient/http.pyc 28 | ../apiclient/mimeparse.pyc 29 | ../apiclient/__init__.pyc 30 | ../apiclient/channel.pyc 31 | ../apiclient/schema.pyc 32 | ../apiclient/model.pyc 33 | ../apiclient/discovery.pyc 34 | ../apiclient/errors.pyc 35 | ../apiclient/sample_tools.pyc 36 | ../oauth2client/__init__.pyc 37 | ../oauth2client/django_orm.pyc 38 | ../oauth2client/clientsecrets.pyc 39 | ../oauth2client/anyjson.pyc 40 | ../oauth2client/appengine.pyc 41 | ../oauth2client/crypt.pyc 42 | ../oauth2client/file.pyc 43 | ../oauth2client/old_run.pyc 44 | ../oauth2client/client.pyc 45 | ../oauth2client/keyring_storage.pyc 46 | ../oauth2client/util.pyc 47 | ../oauth2client/multistore_file.pyc 48 | ../oauth2client/locked_file.pyc 49 | ../oauth2client/gce.pyc 50 | ../oauth2client/tools.pyc 51 | ../oauth2client/xsrfutil.pyc 52 | ../uritemplate/__init__.pyc 53 | ./ 54 | top_level.txt 55 | PKG-INFO 56 | SOURCES.txt 57 | dependency_links.txt 58 | requires.txt 59 | -------------------------------------------------------------------------------- /src/third_party/google_api_python_client-1.2.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | httplib2>=0.8 -------------------------------------------------------------------------------- /src/third_party/google_api_python_client-1.2.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | oauth2client 2 | apiclient 3 | uritemplate 4 | -------------------------------------------------------------------------------- /src/third_party/httplib2-0.9.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.1 2 | Name: httplib2 3 | Version: 0.9 4 | Summary: A comprehensive HTTP client library. 5 | Home-page: http://code.google.com/p/httplib2/ 6 | Author: Joe Gregorio 7 | Author-email: joe@bitworking.org 8 | License: MIT 9 | Download-URL: http://httplib2.googlecode.com/files/httplib2-0.9.tar.gz 10 | Description: 11 | 12 | A comprehensive HTTP client library, ``httplib2`` supports many features left out of other HTTP libraries. 13 | 14 | **HTTP and HTTPS** 15 | HTTPS support is only available if the socket module was compiled with SSL support. 16 | 17 | 18 | **Keep-Alive** 19 | Supports HTTP 1.1 Keep-Alive, keeping the socket open and performing multiple requests over the same connection if possible. 20 | 21 | 22 | **Authentication** 23 | The following three types of HTTP Authentication are supported. These can be used over both HTTP and HTTPS. 24 | 25 | * Digest 26 | * Basic 27 | * WSSE 28 | 29 | **Caching** 30 | The module can optionally operate with a private cache that understands the Cache-Control: 31 | header and uses both the ETag and Last-Modified cache validators. Both file system 32 | and memcached based caches are supported. 33 | 34 | 35 | **All Methods** 36 | The module can handle any HTTP request method, not just GET and POST. 37 | 38 | 39 | **Redirects** 40 | Automatically follows 3XX redirects on GETs. 41 | 42 | 43 | **Compression** 44 | Handles both 'deflate' and 'gzip' types of compression. 45 | 46 | 47 | **Lost update support** 48 | Automatically adds back ETags into PUT requests to resources we have already cached. This implements Section 3.2 of Detecting the Lost Update Problem Using Unreserved Checkout 49 | 50 | 51 | **Unit Tested** 52 | A large and growing set of unit tests. 53 | 54 | 55 | Platform: UNKNOWN 56 | Classifier: Development Status :: 4 - Beta 57 | Classifier: Environment :: Web Environment 58 | Classifier: Intended Audience :: Developers 59 | Classifier: License :: OSI Approved :: MIT License 60 | Classifier: Operating System :: OS Independent 61 | Classifier: Programming Language :: Python 62 | Classifier: Programming Language :: Python :: 3 63 | Classifier: Topic :: Internet :: WWW/HTTP 64 | Classifier: Topic :: Software Development :: Libraries 65 | -------------------------------------------------------------------------------- /src/third_party/httplib2-0.9.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | MANIFEST.in 2 | setup.cfg 3 | setup.py 4 | python2/httplib2test.py 5 | python2/httplib2test_appengine.py 6 | python2/httplib2/__init__.py 7 | python2/httplib2/cacerts.txt 8 | python2/httplib2/iri2uri.py 9 | python2/httplib2/socks.py 10 | python2/httplib2.egg-info/PKG-INFO 11 | python2/httplib2.egg-info/SOURCES.txt 12 | python2/httplib2.egg-info/dependency_links.txt 13 | python2/httplib2.egg-info/top_level.txt 14 | python2/httplib2/test/__init__.py 15 | python2/httplib2/test/miniserver.py 16 | python2/httplib2/test/other_cacerts.txt 17 | python2/httplib2/test/smoke_test.py 18 | python2/httplib2/test/test_no_socket.py 19 | python2/httplib2/test/brokensocket/socket.py 20 | python2/httplib2/test/functional/test_proxies.py 21 | python3/httplib2test.py 22 | python3/httplib2/__init__.py 23 | python3/httplib2/cacerts.txt 24 | python3/httplib2/iri2uri.py 25 | python3/httplib2/test/other_cacerts.txt -------------------------------------------------------------------------------- /src/third_party/httplib2-0.9.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/third_party/httplib2-0.9.egg-info/installed-files.txt: -------------------------------------------------------------------------------- 1 | ../httplib2/__init__.py 2 | ../httplib2/socks.py 3 | ../httplib2/iri2uri.py 4 | ../httplib2/cacerts.txt 5 | ../httplib2/__init__.pyc 6 | ../httplib2/socks.pyc 7 | ../httplib2/iri2uri.pyc 8 | ./ 9 | top_level.txt 10 | PKG-INFO 11 | SOURCES.txt 12 | dependency_links.txt 13 | -------------------------------------------------------------------------------- /src/third_party/httplib2-0.9.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | httplib2 2 | -------------------------------------------------------------------------------- /src/third_party/httplib2/iri2uri.py: -------------------------------------------------------------------------------- 1 | """ 2 | iri2uri 3 | 4 | Converts an IRI to a URI. 5 | 6 | """ 7 | __author__ = "Joe Gregorio (joe@bitworking.org)" 8 | __copyright__ = "Copyright 2006, Joe Gregorio" 9 | __contributors__ = [] 10 | __version__ = "1.0.0" 11 | __license__ = "MIT" 12 | __history__ = """ 13 | """ 14 | 15 | import urlparse 16 | 17 | 18 | # Convert an IRI to a URI following the rules in RFC 3987 19 | # 20 | # The characters we need to enocde and escape are defined in the spec: 21 | # 22 | # iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD 23 | # ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF 24 | # / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD 25 | # / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD 26 | # / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD 27 | # / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD 28 | # / %xD0000-DFFFD / %xE1000-EFFFD 29 | 30 | escape_range = [ 31 | (0xA0, 0xD7FF), 32 | (0xE000, 0xF8FF), 33 | (0xF900, 0xFDCF), 34 | (0xFDF0, 0xFFEF), 35 | (0x10000, 0x1FFFD), 36 | (0x20000, 0x2FFFD), 37 | (0x30000, 0x3FFFD), 38 | (0x40000, 0x4FFFD), 39 | (0x50000, 0x5FFFD), 40 | (0x60000, 0x6FFFD), 41 | (0x70000, 0x7FFFD), 42 | (0x80000, 0x8FFFD), 43 | (0x90000, 0x9FFFD), 44 | (0xA0000, 0xAFFFD), 45 | (0xB0000, 0xBFFFD), 46 | (0xC0000, 0xCFFFD), 47 | (0xD0000, 0xDFFFD), 48 | (0xE1000, 0xEFFFD), 49 | (0xF0000, 0xFFFFD), 50 | (0x100000, 0x10FFFD), 51 | ] 52 | 53 | def encode(c): 54 | retval = c 55 | i = ord(c) 56 | for low, high in escape_range: 57 | if i < low: 58 | break 59 | if i >= low and i <= high: 60 | retval = "".join(["%%%2X" % ord(o) for o in c.encode('utf-8')]) 61 | break 62 | return retval 63 | 64 | 65 | def iri2uri(uri): 66 | """Convert an IRI to a URI. Note that IRIs must be 67 | passed in a unicode strings. That is, do not utf-8 encode 68 | the IRI before passing it into the function.""" 69 | if isinstance(uri ,unicode): 70 | (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri) 71 | authority = authority.encode('idna') 72 | # For each character in 'ucschar' or 'iprivate' 73 | # 1. encode as utf-8 74 | # 2. then %-encode each octet of that utf-8 75 | uri = urlparse.urlunsplit((scheme, authority, path, query, fragment)) 76 | uri = "".join([encode(c) for c in uri]) 77 | return uri 78 | 79 | if __name__ == "__main__": 80 | import unittest 81 | 82 | class Test(unittest.TestCase): 83 | 84 | def test_uris(self): 85 | """Test that URIs are invariant under the transformation.""" 86 | invariant = [ 87 | u"ftp://ftp.is.co.za/rfc/rfc1808.txt", 88 | u"http://www.ietf.org/rfc/rfc2396.txt", 89 | u"ldap://[2001:db8::7]/c=GB?objectClass?one", 90 | u"mailto:John.Doe@example.com", 91 | u"news:comp.infosystems.www.servers.unix", 92 | u"tel:+1-816-555-1212", 93 | u"telnet://192.0.2.16:80/", 94 | u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ] 95 | for uri in invariant: 96 | self.assertEqual(uri, iri2uri(uri)) 97 | 98 | def test_iri(self): 99 | """ Test that the right type of escaping is done for each part of the URI.""" 100 | self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}")) 101 | self.assertEqual("http://bitworking.org/?fred=%E2%98%84", iri2uri(u"http://bitworking.org/?fred=\N{COMET}")) 102 | self.assertEqual("http://bitworking.org/#%E2%98%84", iri2uri(u"http://bitworking.org/#\N{COMET}")) 103 | self.assertEqual("#%E2%98%84", iri2uri(u"#\N{COMET}")) 104 | self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}")) 105 | self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}"))) 106 | self.assertNotEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}".encode('utf-8'))) 107 | 108 | unittest.main() 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/third_party/oauth2client/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.2" 2 | 3 | GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/auth' 4 | GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke' 5 | GOOGLE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token' 6 | -------------------------------------------------------------------------------- /src/third_party/oauth2client/anyjson.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Utility module to import a JSON module 16 | 17 | Hides all the messy details of exactly where 18 | we get a simplejson module from. 19 | """ 20 | 21 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 22 | 23 | 24 | try: # pragma: no cover 25 | # Should work for Python2.6 and higher. 26 | import json as simplejson 27 | except ImportError: # pragma: no cover 28 | try: 29 | import simplejson 30 | except ImportError: 31 | # Try to import from django, should work on App Engine 32 | from django.utils import simplejson 33 | -------------------------------------------------------------------------------- /src/third_party/oauth2client/clientsecrets.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Utilities for reading OAuth 2.0 client secret files. 16 | 17 | A client_secrets.json file contains all the information needed to interact with 18 | an OAuth 2.0 protected service. 19 | """ 20 | 21 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 22 | 23 | 24 | from anyjson import simplejson 25 | 26 | # Properties that make a client_secrets.json file valid. 27 | TYPE_WEB = 'web' 28 | TYPE_INSTALLED = 'installed' 29 | 30 | VALID_CLIENT = { 31 | TYPE_WEB: { 32 | 'required': [ 33 | 'client_id', 34 | 'client_secret', 35 | 'redirect_uris', 36 | 'auth_uri', 37 | 'token_uri', 38 | ], 39 | 'string': [ 40 | 'client_id', 41 | 'client_secret', 42 | ], 43 | }, 44 | TYPE_INSTALLED: { 45 | 'required': [ 46 | 'client_id', 47 | 'client_secret', 48 | 'redirect_uris', 49 | 'auth_uri', 50 | 'token_uri', 51 | ], 52 | 'string': [ 53 | 'client_id', 54 | 'client_secret', 55 | ], 56 | }, 57 | } 58 | 59 | 60 | class Error(Exception): 61 | """Base error for this module.""" 62 | pass 63 | 64 | 65 | class InvalidClientSecretsError(Error): 66 | """Format of ClientSecrets file is invalid.""" 67 | pass 68 | 69 | 70 | def _validate_clientsecrets(obj): 71 | if obj is None or len(obj) != 1: 72 | raise InvalidClientSecretsError('Invalid file format.') 73 | client_type = obj.keys()[0] 74 | if client_type not in VALID_CLIENT.keys(): 75 | raise InvalidClientSecretsError('Unknown client type: %s.' % client_type) 76 | client_info = obj[client_type] 77 | for prop_name in VALID_CLIENT[client_type]['required']: 78 | if prop_name not in client_info: 79 | raise InvalidClientSecretsError( 80 | 'Missing property "%s" in a client type of "%s".' % (prop_name, 81 | client_type)) 82 | for prop_name in VALID_CLIENT[client_type]['string']: 83 | if client_info[prop_name].startswith('[['): 84 | raise InvalidClientSecretsError( 85 | 'Property "%s" is not configured.' % prop_name) 86 | return client_type, client_info 87 | 88 | 89 | def load(fp): 90 | obj = simplejson.load(fp) 91 | return _validate_clientsecrets(obj) 92 | 93 | 94 | def loads(s): 95 | obj = simplejson.loads(s) 96 | return _validate_clientsecrets(obj) 97 | 98 | 99 | def _loadfile(filename): 100 | try: 101 | fp = file(filename, 'r') 102 | try: 103 | obj = simplejson.load(fp) 104 | finally: 105 | fp.close() 106 | except IOError: 107 | raise InvalidClientSecretsError('File not found: "%s"' % filename) 108 | return _validate_clientsecrets(obj) 109 | 110 | 111 | def loadfile(filename, cache=None): 112 | """Loading of client_secrets JSON file, optionally backed by a cache. 113 | 114 | Typical cache storage would be App Engine memcache service, 115 | but you can pass in any other cache client that implements 116 | these methods: 117 | - get(key, namespace=ns) 118 | - set(key, value, namespace=ns) 119 | 120 | Usage: 121 | # without caching 122 | client_type, client_info = loadfile('secrets.json') 123 | # using App Engine memcache service 124 | from google.appengine.api import memcache 125 | client_type, client_info = loadfile('secrets.json', cache=memcache) 126 | 127 | Args: 128 | filename: string, Path to a client_secrets.json file on a filesystem. 129 | cache: An optional cache service client that implements get() and set() 130 | methods. If not specified, the file is always being loaded from 131 | a filesystem. 132 | 133 | Raises: 134 | InvalidClientSecretsError: In case of a validation error or some 135 | I/O failure. Can happen only on cache miss. 136 | 137 | Returns: 138 | (client_type, client_info) tuple, as _loadfile() normally would. 139 | JSON contents is validated only during first load. Cache hits are not 140 | validated. 141 | """ 142 | _SECRET_NAMESPACE = 'oauth2client:secrets#ns' 143 | 144 | if not cache: 145 | return _loadfile(filename) 146 | 147 | obj = cache.get(filename, namespace=_SECRET_NAMESPACE) 148 | if obj is None: 149 | client_type, client_info = _loadfile(filename) 150 | obj = {client_type: client_info} 151 | cache.set(filename, obj, namespace=_SECRET_NAMESPACE) 152 | 153 | return obj.iteritems().next() 154 | -------------------------------------------------------------------------------- /src/third_party/oauth2client/django_orm.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """OAuth 2.0 utilities for Django. 16 | 17 | Utilities for using OAuth 2.0 in conjunction with 18 | the Django datastore. 19 | """ 20 | 21 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 22 | 23 | import oauth2client 24 | import base64 25 | import pickle 26 | 27 | from django.db import models 28 | from oauth2client.client import Storage as BaseStorage 29 | 30 | class CredentialsField(models.Field): 31 | 32 | __metaclass__ = models.SubfieldBase 33 | 34 | def __init__(self, *args, **kwargs): 35 | if 'null' not in kwargs: 36 | kwargs['null'] = True 37 | super(CredentialsField, self).__init__(*args, **kwargs) 38 | 39 | def get_internal_type(self): 40 | return "TextField" 41 | 42 | def to_python(self, value): 43 | if value is None: 44 | return None 45 | if isinstance(value, oauth2client.client.Credentials): 46 | return value 47 | return pickle.loads(base64.b64decode(value)) 48 | 49 | def get_db_prep_value(self, value, connection, prepared=False): 50 | if value is None: 51 | return None 52 | return base64.b64encode(pickle.dumps(value)) 53 | 54 | 55 | class FlowField(models.Field): 56 | 57 | __metaclass__ = models.SubfieldBase 58 | 59 | def __init__(self, *args, **kwargs): 60 | if 'null' not in kwargs: 61 | kwargs['null'] = True 62 | super(FlowField, self).__init__(*args, **kwargs) 63 | 64 | def get_internal_type(self): 65 | return "TextField" 66 | 67 | def to_python(self, value): 68 | if value is None: 69 | return None 70 | if isinstance(value, oauth2client.client.Flow): 71 | return value 72 | return pickle.loads(base64.b64decode(value)) 73 | 74 | def get_db_prep_value(self, value, connection, prepared=False): 75 | if value is None: 76 | return None 77 | return base64.b64encode(pickle.dumps(value)) 78 | 79 | 80 | class Storage(BaseStorage): 81 | """Store and retrieve a single credential to and from 82 | the datastore. 83 | 84 | This Storage helper presumes the Credentials 85 | have been stored as a CredenialsField 86 | on a db model class. 87 | """ 88 | 89 | def __init__(self, model_class, key_name, key_value, property_name): 90 | """Constructor for Storage. 91 | 92 | Args: 93 | model: db.Model, model class 94 | key_name: string, key name for the entity that has the credentials 95 | key_value: string, key value for the entity that has the credentials 96 | property_name: string, name of the property that is an CredentialsProperty 97 | """ 98 | self.model_class = model_class 99 | self.key_name = key_name 100 | self.key_value = key_value 101 | self.property_name = property_name 102 | 103 | def locked_get(self): 104 | """Retrieve Credential from datastore. 105 | 106 | Returns: 107 | oauth2client.Credentials 108 | """ 109 | credential = None 110 | 111 | query = {self.key_name: self.key_value} 112 | entities = self.model_class.objects.filter(**query) 113 | if len(entities) > 0: 114 | credential = getattr(entities[0], self.property_name) 115 | if credential and hasattr(credential, 'set_store'): 116 | credential.set_store(self) 117 | return credential 118 | 119 | def locked_put(self, credentials): 120 | """Write a Credentials to the datastore. 121 | 122 | Args: 123 | credentials: Credentials, the credentials to store. 124 | """ 125 | args = {self.key_name: self.key_value} 126 | entity = self.model_class(**args) 127 | setattr(entity, self.property_name, credentials) 128 | entity.save() 129 | 130 | def locked_delete(self): 131 | """Delete Credentials from the datastore.""" 132 | 133 | query = {self.key_name: self.key_value} 134 | entities = self.model_class.objects.filter(**query).delete() 135 | -------------------------------------------------------------------------------- /src/third_party/oauth2client/file.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Utilities for OAuth. 16 | 17 | Utilities for making it easier to work with OAuth 2.0 18 | credentials. 19 | """ 20 | 21 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 22 | 23 | import os 24 | import stat 25 | import threading 26 | 27 | from anyjson import simplejson 28 | from client import Storage as BaseStorage 29 | from client import Credentials 30 | 31 | 32 | class CredentialsFileSymbolicLinkError(Exception): 33 | """Credentials files must not be symbolic links.""" 34 | 35 | 36 | class Storage(BaseStorage): 37 | """Store and retrieve a single credential to and from a file.""" 38 | 39 | def __init__(self, filename): 40 | self._filename = filename 41 | self._lock = threading.Lock() 42 | 43 | def _validate_file(self): 44 | if os.path.islink(self._filename): 45 | raise CredentialsFileSymbolicLinkError( 46 | 'File: %s is a symbolic link.' % self._filename) 47 | 48 | def acquire_lock(self): 49 | """Acquires any lock necessary to access this Storage. 50 | 51 | This lock is not reentrant.""" 52 | self._lock.acquire() 53 | 54 | def release_lock(self): 55 | """Release the Storage lock. 56 | 57 | Trying to release a lock that isn't held will result in a 58 | RuntimeError. 59 | """ 60 | self._lock.release() 61 | 62 | def locked_get(self): 63 | """Retrieve Credential from file. 64 | 65 | Returns: 66 | oauth2client.client.Credentials 67 | 68 | Raises: 69 | CredentialsFileSymbolicLinkError if the file is a symbolic link. 70 | """ 71 | credentials = None 72 | self._validate_file() 73 | try: 74 | f = open(self._filename, 'rb') 75 | content = f.read() 76 | f.close() 77 | except IOError: 78 | return credentials 79 | 80 | try: 81 | credentials = Credentials.new_from_json(content) 82 | credentials.set_store(self) 83 | except ValueError: 84 | pass 85 | 86 | return credentials 87 | 88 | def _create_file_if_needed(self): 89 | """Create an empty file if necessary. 90 | 91 | This method will not initialize the file. Instead it implements a 92 | simple version of "touch" to ensure the file has been created. 93 | """ 94 | if not os.path.exists(self._filename): 95 | old_umask = os.umask(0177) 96 | try: 97 | open(self._filename, 'a+b').close() 98 | finally: 99 | os.umask(old_umask) 100 | 101 | def locked_put(self, credentials): 102 | """Write Credentials to file. 103 | 104 | Args: 105 | credentials: Credentials, the credentials to store. 106 | 107 | Raises: 108 | CredentialsFileSymbolicLinkError if the file is a symbolic link. 109 | """ 110 | 111 | self._create_file_if_needed() 112 | self._validate_file() 113 | f = open(self._filename, 'wb') 114 | f.write(credentials.to_json()) 115 | f.close() 116 | 117 | def locked_delete(self): 118 | """Delete Credentials file. 119 | 120 | Args: 121 | credentials: Credentials, the credentials to store. 122 | """ 123 | 124 | os.unlink(self._filename) 125 | -------------------------------------------------------------------------------- /src/third_party/oauth2client/gce.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Utilities for Google Compute Engine 16 | 17 | Utilities for making it easier to use OAuth 2.0 on Google Compute Engine. 18 | """ 19 | 20 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 21 | 22 | import httplib2 23 | import logging 24 | import uritemplate 25 | 26 | from oauth2client import util 27 | from oauth2client.anyjson import simplejson 28 | from oauth2client.client import AccessTokenRefreshError 29 | from oauth2client.client import AssertionCredentials 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | # URI Template for the endpoint that returns access_tokens. 34 | META = ('http://metadata.google.internal/0.1/meta-data/service-accounts/' 35 | 'default/acquire{?scope}') 36 | 37 | 38 | class AppAssertionCredentials(AssertionCredentials): 39 | """Credentials object for Compute Engine Assertion Grants 40 | 41 | This object will allow a Compute Engine instance to identify itself to 42 | Google and other OAuth 2.0 servers that can verify assertions. It can be used 43 | for the purpose of accessing data stored under an account assigned to the 44 | Compute Engine instance itself. 45 | 46 | This credential does not require a flow to instantiate because it represents 47 | a two legged flow, and therefore has all of the required information to 48 | generate and refresh its own access tokens. 49 | """ 50 | 51 | @util.positional(2) 52 | def __init__(self, scope, **kwargs): 53 | """Constructor for AppAssertionCredentials 54 | 55 | Args: 56 | scope: string or iterable of strings, scope(s) of the credentials being 57 | requested. 58 | """ 59 | self.scope = util.scopes_to_string(scope) 60 | 61 | # Assertion type is no longer used, but still in the parent class signature. 62 | super(AppAssertionCredentials, self).__init__(None) 63 | 64 | @classmethod 65 | def from_json(cls, json): 66 | data = simplejson.loads(json) 67 | return AppAssertionCredentials(data['scope']) 68 | 69 | def _refresh(self, http_request): 70 | """Refreshes the access_token. 71 | 72 | Skip all the storage hoops and just refresh using the API. 73 | 74 | Args: 75 | http_request: callable, a callable that matches the method signature of 76 | httplib2.Http.request, used to make the refresh request. 77 | 78 | Raises: 79 | AccessTokenRefreshError: When the refresh fails. 80 | """ 81 | uri = uritemplate.expand(META, {'scope': self.scope}) 82 | response, content = http_request(uri) 83 | if response.status == 200: 84 | try: 85 | d = simplejson.loads(content) 86 | except StandardError, e: 87 | raise AccessTokenRefreshError(str(e)) 88 | self.access_token = d['accessToken'] 89 | else: 90 | raise AccessTokenRefreshError(content) 91 | -------------------------------------------------------------------------------- /src/third_party/oauth2client/keyring_storage.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Google Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """A keyring based Storage. 16 | 17 | A Storage for Credentials that uses the keyring module. 18 | """ 19 | 20 | __author__ = 'jcgregorio@google.com (Joe Gregorio)' 21 | 22 | import keyring 23 | import threading 24 | 25 | from client import Storage as BaseStorage 26 | from client import Credentials 27 | 28 | 29 | class Storage(BaseStorage): 30 | """Store and retrieve a single credential to and from the keyring. 31 | 32 | To use this module you must have the keyring module installed. See 33 | . This is an optional module and is not 34 | installed with oauth2client by default because it does not work on all the 35 | platforms that oauth2client supports, such as Google App Engine. 36 | 37 | The keyring module is a cross-platform 38 | library for access the keyring capabilities of the local system. The user will 39 | be prompted for their keyring password when this module is used, and the 40 | manner in which the user is prompted will vary per platform. 41 | 42 | Usage: 43 | from oauth2client.keyring_storage import Storage 44 | 45 | s = Storage('name_of_application', 'user1') 46 | credentials = s.get() 47 | 48 | """ 49 | 50 | def __init__(self, service_name, user_name): 51 | """Constructor. 52 | 53 | Args: 54 | service_name: string, The name of the service under which the credentials 55 | are stored. 56 | user_name: string, The name of the user to store credentials for. 57 | """ 58 | self._service_name = service_name 59 | self._user_name = user_name 60 | self._lock = threading.Lock() 61 | 62 | def acquire_lock(self): 63 | """Acquires any lock necessary to access this Storage. 64 | 65 | This lock is not reentrant.""" 66 | self._lock.acquire() 67 | 68 | def release_lock(self): 69 | """Release the Storage lock. 70 | 71 | Trying to release a lock that isn't held will result in a 72 | RuntimeError. 73 | """ 74 | self._lock.release() 75 | 76 | def locked_get(self): 77 | """Retrieve Credential from file. 78 | 79 | Returns: 80 | oauth2client.client.Credentials 81 | """ 82 | credentials = None 83 | content = keyring.get_password(self._service_name, self._user_name) 84 | 85 | if content is not None: 86 | try: 87 | credentials = Credentials.new_from_json(content) 88 | credentials.set_store(self) 89 | except ValueError: 90 | pass 91 | 92 | return credentials 93 | 94 | def locked_put(self, credentials): 95 | """Write Credentials to file. 96 | 97 | Args: 98 | credentials: Credentials, the credentials to store. 99 | """ 100 | keyring.set_password(self._service_name, self._user_name, 101 | credentials.to_json()) 102 | 103 | def locked_delete(self): 104 | """Delete Credentials file. 105 | 106 | Args: 107 | credentials: Credentials, the credentials to store. 108 | """ 109 | keyring.set_password(self._service_name, self._user_name, '') 110 | -------------------------------------------------------------------------------- /src/third_party/oauth2client/xsrfutil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.5 2 | # 3 | # Copyright 2010 the Melange authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Helper methods for creating & verifying XSRF tokens.""" 18 | 19 | __authors__ = [ 20 | '"Doug Coker" ', 21 | '"Joe Gregorio" ', 22 | ] 23 | 24 | 25 | import base64 26 | import hmac 27 | import os # for urandom 28 | import time 29 | 30 | from oauth2client import util 31 | 32 | 33 | # Delimiter character 34 | DELIMITER = ':' 35 | 36 | # 1 hour in seconds 37 | DEFAULT_TIMEOUT_SECS = 1*60*60 38 | 39 | @util.positional(2) 40 | def generate_token(key, user_id, action_id="", when=None): 41 | """Generates a URL-safe token for the given user, action, time tuple. 42 | 43 | Args: 44 | key: secret key to use. 45 | user_id: the user ID of the authenticated user. 46 | action_id: a string identifier of the action they requested 47 | authorization for. 48 | when: the time in seconds since the epoch at which the user was 49 | authorized for this action. If not set the current time is used. 50 | 51 | Returns: 52 | A string XSRF protection token. 53 | """ 54 | when = when or int(time.time()) 55 | digester = hmac.new(key) 56 | digester.update(str(user_id)) 57 | digester.update(DELIMITER) 58 | digester.update(action_id) 59 | digester.update(DELIMITER) 60 | digester.update(str(when)) 61 | digest = digester.digest() 62 | 63 | token = base64.urlsafe_b64encode('%s%s%d' % (digest, 64 | DELIMITER, 65 | when)) 66 | return token 67 | 68 | 69 | @util.positional(3) 70 | def validate_token(key, token, user_id, action_id="", current_time=None): 71 | """Validates that the given token authorizes the user for the action. 72 | 73 | Tokens are invalid if the time of issue is too old or if the token 74 | does not match what generateToken outputs (i.e. the token was forged). 75 | 76 | Args: 77 | key: secret key to use. 78 | token: a string of the token generated by generateToken. 79 | user_id: the user ID of the authenticated user. 80 | action_id: a string identifier of the action they requested 81 | authorization for. 82 | 83 | Returns: 84 | A boolean - True if the user is authorized for the action, False 85 | otherwise. 86 | """ 87 | if not token: 88 | return False 89 | try: 90 | decoded = base64.urlsafe_b64decode(str(token)) 91 | token_time = long(decoded.split(DELIMITER)[-1]) 92 | except (TypeError, ValueError): 93 | return False 94 | if current_time is None: 95 | current_time = time.time() 96 | # If the token is too old it's not valid. 97 | if current_time - token_time > DEFAULT_TIMEOUT_SECS: 98 | return False 99 | 100 | # The given token should match the generated one with the same time. 101 | expected_token = generate_token(key, user_id, action_id=action_id, 102 | when=token_time) 103 | if len(token) != len(expected_token): 104 | return False 105 | 106 | # Perform constant time comparison to avoid timing attacks 107 | different = 0 108 | for x, y in zip(token, expected_token): 109 | different |= ord(x) ^ ord(y) 110 | if different: 111 | return False 112 | 113 | return True 114 | -------------------------------------------------------------------------------- /src/third_party/requirements.txt: -------------------------------------------------------------------------------- 1 | google-api-python-client==1.2 2 | -------------------------------------------------------------------------------- /src/third_party/uritemplate/__init__.py: -------------------------------------------------------------------------------- 1 | # Early, and incomplete implementation of -04. 2 | # 3 | import re 4 | import urllib 5 | 6 | RESERVED = ":/?#[]@!$&'()*+,;=" 7 | OPERATOR = "+./;?|!@" 8 | EXPLODE = "*+" 9 | MODIFIER = ":^" 10 | TEMPLATE = re.compile(r"{(?P[\+\./;\?|!@])?(?P[^}]+)}", re.UNICODE) 11 | VAR = re.compile(r"^(?P[^=\+\*:\^]+)((?P[\+\*])|(?P[:\^]-?[0-9]+))?(=(?P.*))?$", re.UNICODE) 12 | 13 | def _tostring(varname, value, explode, operator, safe=""): 14 | if type(value) == type([]): 15 | if explode == "+": 16 | return ",".join([varname + "." + urllib.quote(x, safe) for x in value]) 17 | else: 18 | return ",".join([urllib.quote(x, safe) for x in value]) 19 | if type(value) == type({}): 20 | keys = value.keys() 21 | keys.sort() 22 | if explode == "+": 23 | return ",".join([varname + "." + urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) 24 | else: 25 | return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) 26 | else: 27 | return urllib.quote(value, safe) 28 | 29 | 30 | def _tostring_path(varname, value, explode, operator, safe=""): 31 | joiner = operator 32 | if type(value) == type([]): 33 | if explode == "+": 34 | return joiner.join([varname + "." + urllib.quote(x, safe) for x in value]) 35 | elif explode == "*": 36 | return joiner.join([urllib.quote(x, safe) for x in value]) 37 | else: 38 | return ",".join([urllib.quote(x, safe) for x in value]) 39 | elif type(value) == type({}): 40 | keys = value.keys() 41 | keys.sort() 42 | if explode == "+": 43 | return joiner.join([varname + "." + urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys]) 44 | elif explode == "*": 45 | return joiner.join([urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys]) 46 | else: 47 | return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) 48 | else: 49 | if value: 50 | return urllib.quote(value, safe) 51 | else: 52 | return "" 53 | 54 | def _tostring_query(varname, value, explode, operator, safe=""): 55 | joiner = operator 56 | varprefix = "" 57 | if operator == "?": 58 | joiner = "&" 59 | varprefix = varname + "=" 60 | if type(value) == type([]): 61 | if 0 == len(value): 62 | return "" 63 | if explode == "+": 64 | return joiner.join([varname + "=" + urllib.quote(x, safe) for x in value]) 65 | elif explode == "*": 66 | return joiner.join([urllib.quote(x, safe) for x in value]) 67 | else: 68 | return varprefix + ",".join([urllib.quote(x, safe) for x in value]) 69 | elif type(value) == type({}): 70 | if 0 == len(value): 71 | return "" 72 | keys = value.keys() 73 | keys.sort() 74 | if explode == "+": 75 | return joiner.join([varname + "." + urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys]) 76 | elif explode == "*": 77 | return joiner.join([urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys]) 78 | else: 79 | return varprefix + ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys]) 80 | else: 81 | if value: 82 | return varname + "=" + urllib.quote(value, safe) 83 | else: 84 | return varname 85 | 86 | TOSTRING = { 87 | "" : _tostring, 88 | "+": _tostring, 89 | ";": _tostring_query, 90 | "?": _tostring_query, 91 | "/": _tostring_path, 92 | ".": _tostring_path, 93 | } 94 | 95 | 96 | def expand(template, vars): 97 | def _sub(match): 98 | groupdict = match.groupdict() 99 | operator = groupdict.get('operator') 100 | if operator is None: 101 | operator = '' 102 | varlist = groupdict.get('varlist') 103 | 104 | safe = "@" 105 | if operator == '+': 106 | safe = RESERVED 107 | varspecs = varlist.split(",") 108 | varnames = [] 109 | defaults = {} 110 | for varspec in varspecs: 111 | m = VAR.search(varspec) 112 | groupdict = m.groupdict() 113 | varname = groupdict.get('varname') 114 | explode = groupdict.get('explode') 115 | partial = groupdict.get('partial') 116 | default = groupdict.get('default') 117 | if default: 118 | defaults[varname] = default 119 | varnames.append((varname, explode, partial)) 120 | 121 | retval = [] 122 | joiner = operator 123 | prefix = operator 124 | if operator == "+": 125 | prefix = "" 126 | joiner = "," 127 | if operator == "?": 128 | joiner = "&" 129 | if operator == "": 130 | joiner = "," 131 | for varname, explode, partial in varnames: 132 | if varname in vars: 133 | value = vars[varname] 134 | #if not value and (type(value) == type({}) or type(value) == type([])) and varname in defaults: 135 | if not value and value != "" and varname in defaults: 136 | value = defaults[varname] 137 | elif varname in defaults: 138 | value = defaults[varname] 139 | else: 140 | continue 141 | retval.append(TOSTRING[operator](varname, value, explode, operator, safe=safe)) 142 | if "".join(retval): 143 | return prefix + joiner.join(retval) 144 | else: 145 | return "" 146 | 147 | return TEMPLATE.sub(_sub, template) 148 | -------------------------------------------------------------------------------- /src/web_app/chrome_app/apprtc-app-store-icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISBX/apprtc-server/bfad44d7dcc12bf18f71218ca7899c4825e4ad38/src/web_app/chrome_app/apprtc-app-store-icon-128.png -------------------------------------------------------------------------------- /src/web_app/chrome_app/large-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISBX/apprtc-server/bfad44d7dcc12bf18f71218ca7899c4825e4ad38/src/web_app/chrome_app/large-tile.png -------------------------------------------------------------------------------- /src/web_app/chrome_app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AppRTC", 3 | "description": "AppRTC video chat application", 4 | "version": "0.1", 5 | "manifest_version": 2, 6 | "app": { 7 | "background": { 8 | "scripts": ["js/apprtc.debug.js", "js/background.js"] 9 | } 10 | }, 11 | "icons": { 12 | "16": "images/apprtc-16.png", 13 | "22": "images/apprtc-22.png", 14 | "32": "images/apprtc-32.png", 15 | "48": "images/apprtc-48.png", 16 | "128": "images/apprtc-128.png" 17 | }, 18 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlUCCvjX6IHnzDs/ox7lfm+42wPTfSWAjg3AnqphscK1Pls0UvkU5KAwNQ1t57W0cH+6fvq34grUSSQn1PoLnJn4x/YBrHz2vzKGm63FBfWJfxJAK0qAJLIBcGlRtYUpPdCyhfiw8hWVqiULobZB2xewwxyJpPpaPiG8xVUoQCKg7TVVHl3WV5sRnvB9Nzc//WTjDmNPea+KEsTD7ku/YCghkx4OmyE0jy9xItk5F0f5ubFe1UYagq8siPXc4qOqw55Hd7XyC1QxwdFH/8JfCaN4SnlG5hKoPhobLvsEwMbOjPAZRFDOxAVsmynoLT1WeG/qMMgi312+c781EU3IOxQIDAQAB", 19 | "permissions": [ 20 | "audioCapture", 21 | "videoCapture", 22 | "storage", 23 | "fullscreen", 24 | "https://apprtc.appspot.com/", 25 | "https://computeengineondemand.appspot.com/" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/web_app/chrome_app/marquee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISBX/apprtc-server/bfad44d7dcc12bf18f71218ca7899c4825e4ad38/src/web_app/chrome_app/marquee.png -------------------------------------------------------------------------------- /src/web_app/chrome_app/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISBX/apprtc-server/bfad44d7dcc12bf18f71218ca7899c4825e4ad38/src/web_app/chrome_app/screenshot1.png -------------------------------------------------------------------------------- /src/web_app/chrome_app/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISBX/apprtc-server/bfad44d7dcc12bf18f71218ca7899c4825e4ad38/src/web_app/chrome_app/screenshot2.png -------------------------------------------------------------------------------- /src/web_app/chrome_app/small-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISBX/apprtc-server/bfad44d7dcc12bf18f71218ca7899c4825e4ad38/src/web_app/chrome_app/small-tile.png -------------------------------------------------------------------------------- /src/web_app/html/full_template.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | AppRTC: Room full 24 | 25 | 26 | 27 | 28 | 29 | 52 | 53 | 54 | 55 | 56 |
Sorry, this room is full.
57 | 58 | 59 | 60 | 65 | 66 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/web_app/html/google1b7eb21c5b594ba0.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google1b7eb21c5b594ba0.html -------------------------------------------------------------------------------- /src/web_app/html/help.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 13 | WebRtc Demo App Help 14 | 15 | 16 | TODO 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/web_app/html/index_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "chromeapp": true, 3 | "description": "This data is used to build the Chrome App. index.html is transformed to appwindow.html using jinja2 and the data from this file." 4 | } 5 | -------------------------------------------------------------------------------- /src/web_app/html/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "AppRTC", 3 | "name": "WebRTC reference app", 4 | "icons": [ 5 | { 6 | "src": "/images/webrtc-icon-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "density": "4.0" 10 | } 11 | ], 12 | "start_url": "/", 13 | "display": "standalone", 14 | "orientation": "portrait" 15 | } 16 | -------------------------------------------------------------------------------- /src/web_app/html/params.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | AppRTC parameters 13 | 14 | 15 | 16 | 17 | 18 | 47 | 48 | 49 |
50 | 51 |

AppRTC parameters

52 | 53 |
54 | 55 |

A number of settings for the AppRTC video chat application can be changed by adding URL parameters.

56 | 57 |

For example: https://apprtc.appspot.com/?hd=true&stereo=true&debug=loopback

58 | 59 |

The file using the parameters is apprtc.py. More Google-specific parameters are available from the MediaConstraints interface.

60 | 61 |

For more information see AppRTC : Google's WebRTC test app and its parameters.

62 | 63 |
64 | 65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
hd=trueUse HD camera resolution constraints, i.e. minWidth: 1280, minHeight: 720
stereo=trueTurn on stereo audio
debug=loopbackConnect to yourself, e.g. to test firewalls
ts=[turnserver]Set TURN server different from the default
audio=true&video=falseAudio only
audio=falseVideo only
audio=googEchoCancellation=false,googAutoGainControl=trueDisable echo cancellation and enable gain control
audio=googNoiseReduction=trueEnable noise reduction
asc=ISAC/16000Set preferred audio send codec to be ISAC at 16kHz (use on Android)
arc=opus/48000Set preferred audio receive codec Opus at 48kHz
dtls=falseDisable Datagram Transport Layer Security
dscp=trueEnable DSCP
ipv6=trueEnable IPv6
arbr=[bitrate]Set audio receive bitrate, kbps
asbr=[bitrate]Set audio send bitrate
vsbr=[bitrate]Set video receive bitrate
vrbr=[bitrate]Set video send bitrate
86 | 87 |
88 | 89 | github.com/GoogleChrome/webrtc 90 | 91 |
92 | 93 | 94 | -------------------------------------------------------------------------------- /src/web_app/images/apprtc-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISBX/apprtc-server/bfad44d7dcc12bf18f71218ca7899c4825e4ad38/src/web_app/images/apprtc-128.png -------------------------------------------------------------------------------- /src/web_app/images/apprtc-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISBX/apprtc-server/bfad44d7dcc12bf18f71218ca7899c4825e4ad38/src/web_app/images/apprtc-16.png -------------------------------------------------------------------------------- /src/web_app/images/apprtc-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISBX/apprtc-server/bfad44d7dcc12bf18f71218ca7899c4825e4ad38/src/web_app/images/apprtc-22.png -------------------------------------------------------------------------------- /src/web_app/images/apprtc-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISBX/apprtc-server/bfad44d7dcc12bf18f71218ca7899c4825e4ad38/src/web_app/images/apprtc-32.png -------------------------------------------------------------------------------- /src/web_app/images/apprtc-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISBX/apprtc-server/bfad44d7dcc12bf18f71218ca7899c4825e4ad38/src/web_app/images/apprtc-48.png -------------------------------------------------------------------------------- /src/web_app/images/webrtc-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ISBX/apprtc-server/bfad44d7dcc12bf18f71218ca7899c4825e4ad38/src/web_app/images/webrtc-icon-192x192.png -------------------------------------------------------------------------------- /src/web_app/js/README.md: -------------------------------------------------------------------------------- 1 | # Javascript object hierarchy # 2 | 3 | AppController: The controller that connects the UI and the model "Call". It owns 4 | Call, InfoBox and RoomSelection. 5 | 6 | Call: Manages everything needed to make a call. It owns SignalingChannel and 7 | PeerConnectionClient. 8 | 9 | SignalingChannel: Wrapper of the WebSocket connection. 10 | 11 | PeerConnectionClient: Wrapper of RTCPeerConnection. 12 | 13 | InfoBox: Wrapper of the info div utilities. 14 | 15 | RoomSelection: Wrapper for the room selection UI. It owns Storage. 16 | 17 | Storage: Wrapper for localStorage/Chrome app storage API. 18 | -------------------------------------------------------------------------------- /src/web_app/js/appcontroller_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals AppController, TestCase, UI_CONSTANTS, assertEquals, assertFalse, 12 | assertTrue, $, RoomSelection:true, Call:true */ 13 | 14 | 'use strict'; 15 | 16 | var MockRoomSelection = function() {}; 17 | MockRoomSelection.RecentlyUsedList = function() { 18 | return { 19 | pushRecentRoom: function() {} 20 | }; 21 | }; 22 | MockRoomSelection.matchRandomRoomPattern = function() { 23 | return false; 24 | }; 25 | 26 | var MockCall = function() {}; 27 | MockCall.prototype.start = function() {}; 28 | MockCall.prototype.hangup = function() {}; 29 | 30 | var AppControllerTest = new TestCase('AppControllerTest'); 31 | 32 | AppControllerTest.prototype.setUp = function() { 33 | this.roomSelectionBackup_ = RoomSelection; 34 | RoomSelection = MockRoomSelection; 35 | 36 | this.callBackup_ = Call; 37 | Call = MockCall; 38 | 39 | // Insert mock DOM elements. 40 | for (var key in UI_CONSTANTS) { 41 | var elem = document.createElement('div'); 42 | elem.id = UI_CONSTANTS[key].substr(1); 43 | document.body.appendChild(elem); 44 | } 45 | 46 | this.loadingParams_ = { 47 | mediaConstraints: { 48 | audio: true, video: true 49 | } 50 | }; 51 | }; 52 | 53 | AppControllerTest.prototype.tearDown = function() { 54 | RoomSelection = this.roomSelectionBackup_; 55 | Call = this.callBackup_; 56 | }; 57 | 58 | AppControllerTest.prototype.testConfirmToJoin = function() { 59 | this.loadingParams_.roomId = 'myRoom'; 60 | new AppController(this.loadingParams_); 61 | 62 | // Verifies that the confirm-to-join UI is visible and the text matches the 63 | // room. 64 | assertEquals(' "' + this.loadingParams_.roomId + '"', 65 | $(UI_CONSTANTS.confirmJoinRoomSpan).textContent); 66 | assertFalse($(UI_CONSTANTS.confirmJoinDiv).classList.contains('hidden')); 67 | 68 | // Verifies that the UI is hidden after clicking the button. 69 | $(UI_CONSTANTS.confirmJoinButton).onclick(); 70 | assertTrue($(UI_CONSTANTS.confirmJoinDiv).classList.contains('hidden')); 71 | }; 72 | -------------------------------------------------------------------------------- /src/web_app/js/appwindow.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | // Variables defined in and used from main.js. 11 | /* globals randomString, AppController, sendAsyncUrlRequest, parseJSON */ 12 | /* exported params */ 13 | 'use strict'; 14 | 15 | // Generate random room id and connect. 16 | 17 | var roomServer = 'https://apprtc.appspot.com'; 18 | var loadingParams = { 19 | errorMessages: [], 20 | suggestedRoomId: randomString(9), 21 | roomServer: roomServer, 22 | connect: false, 23 | paramsFunction: function() { 24 | return new Promise(function(resolve, reject) { 25 | trace('Initializing; retrieving params from: ' + roomServer + '/params'); 26 | sendAsyncUrlRequest('GET', roomServer + '/params').then(function(result) { 27 | var serverParams = parseJSON(result); 28 | var newParams = {}; 29 | if (!serverParams) { 30 | resolve(newParams); 31 | return; 32 | } 33 | 34 | // Convert from server format to expected format. 35 | // TODO(tkchin): clean up response format. JSHint doesn't like it. 36 | /* jshint ignore:start */ 37 | //jscs:disable requireCamelCaseOrUpperCaseIdentifiers 38 | newParams.isLoopback = serverParams.is_loopback === 'true'; 39 | newParams.mediaConstraints = parseJSON(serverParams.media_constraints); 40 | newParams.offerConstraints = parseJSON(serverParams.offer_constraints); 41 | newParams.peerConnectionConfig = parseJSON(serverParams.pc_config); 42 | newParams.peerConnectionConstraints = 43 | parseJSON(serverParams.pc_constraints); 44 | newParams.turnRequestUrl = serverParams.turn_url; 45 | newParams.turnTransports = serverParams.turn_transports; 46 | newParams.wssUrl = serverParams.wss_url; 47 | newParams.wssPostUrl = serverParams.wss_post_url; 48 | newParams.versionInfo = parseJSON(serverParams.version_info); 49 | //jscs:enable requireCamelCaseOrUpperCaseIdentifiers 50 | /* jshint ignore:end */ 51 | newParams.messages = serverParams.messages; 52 | 53 | trace('Initializing; parameters from server: '); 54 | trace(JSON.stringify(newParams)); 55 | resolve(newParams); 56 | }).catch(function(error) { 57 | trace('Initializing; error getting params from server: ' + 58 | error.message); 59 | reject(error); 60 | }); 61 | }); 62 | } 63 | }; 64 | 65 | new AppController(loadingParams); 66 | -------------------------------------------------------------------------------- /src/web_app/js/background.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | // Variables defined in and used from chrome. 12 | /* globals chrome, sendAsyncUrlRequest, Constants, parseJSON */ 13 | 14 | 'use strict'; 15 | (function() { 16 | chrome.app.runtime.onLaunched.addListener(function() { 17 | chrome.app.window.create('appwindow.html', { 18 | 'width': 800, 19 | 'height': 600, 20 | 'left': 0, 21 | 'top': 0 22 | }); 23 | }); 24 | 25 | // Send event notification from background to window. 26 | var sendWSEventMessageToWindow = function(port, wsEvent, data) { 27 | var message = { 28 | action: Constants.WS_ACTION, 29 | wsAction: Constants.EVENT_ACTION, 30 | wsEvent: wsEvent 31 | }; 32 | if (data) { 33 | message.data = data; 34 | } 35 | trace('B -> W: ' + JSON.stringify(message)); 36 | try { 37 | port.postMessage(message); 38 | } catch (ex) { 39 | trace('Error sending message: ' + ex); 40 | } 41 | }; 42 | 43 | var handleWebSocketRequest = function(port, message) { 44 | if (message.wsAction === Constants.WS_CREATE_ACTION) { 45 | trace('RWS: creating web socket: ' + message.wssUrl); 46 | var ws = new WebSocket(message.wssUrl); 47 | port.wssPostUrl_ = message.wssPostUrl; 48 | ws.onopen = function() { 49 | sendWSEventMessageToWindow(port, Constants.WS_EVENT_ONOPEN); 50 | }; 51 | ws.onerror = function() { 52 | sendWSEventMessageToWindow(port, Constants.WS_EVENT_ONERROR); 53 | }; 54 | ws.onclose = function(event) { 55 | sendWSEventMessageToWindow(port, Constants.WS_EVENT_ONCLOSE, event); 56 | }; 57 | ws.onmessage = function(event) { 58 | sendWSEventMessageToWindow(port, Constants.WS_EVENT_ONMESSAGE, event); 59 | }; 60 | port.webSocket_ = ws; 61 | } else if (message.wsAction === Constants.WS_SEND_ACTION) { 62 | trace('RWS: sending: ' + message.data); 63 | if (port.webSocket_ && port.webSocket_.readyState === WebSocket.OPEN) { 64 | try { 65 | port.webSocket_.send(message.data); 66 | } catch (ex) { 67 | sendWSEventMessageToWindow(port, Constants.WS_EVENT_SENDERROR, ex); 68 | } 69 | } else { 70 | // Attempt to send message using wss port url. 71 | trace('RWS: web socket not ready, falling back to POST.'); 72 | var msg = parseJSON(message.data); 73 | if (msg) { 74 | sendAsyncUrlRequest('POST', port.wssPostUrl_, msg.msg); 75 | } 76 | } 77 | } else if (message.wsAction === Constants.WS_CLOSE_ACTION) { 78 | trace('RWS: close'); 79 | if (port.webSocket_) { 80 | port.webSocket_.close(); 81 | port.webSocket = null; 82 | } 83 | } 84 | }; 85 | 86 | var executeCleanupTask = function(port, message) { 87 | trace('Executing queue action: ' + JSON.stringify(message)); 88 | if (message.action === Constants.XHR_ACTION) { 89 | var method = message.method; 90 | var url = message.url; 91 | var body = message.body; 92 | return sendAsyncUrlRequest(method, url, body); 93 | } else if (message.action === Constants.WS_ACTION) { 94 | handleWebSocketRequest(port, message); 95 | } else { 96 | trace('Unknown action in cleanup queue: ' + message.action); 97 | } 98 | }; 99 | 100 | var executeCleanupTasks = function(port, queue) { 101 | var promise = Promise.resolve(); 102 | if (!queue) { 103 | return promise; 104 | } 105 | 106 | var catchFunction = function(error) { 107 | trace('Error executing cleanup action: ' + error.message); 108 | }; 109 | 110 | while (queue.length > 0) { 111 | var queueMessage = queue.shift(); 112 | promise = promise.then( 113 | executeCleanupTask.bind(null, port, queueMessage) 114 | ).catch(catchFunction); 115 | } 116 | return promise; 117 | }; 118 | 119 | var handleMessageFromWindow = function(port, message) { 120 | var action = message.action; 121 | if (action === Constants.WS_ACTION) { 122 | handleWebSocketRequest(port, message); 123 | } else if (action === Constants.QUEUECLEAR_ACTION) { 124 | port.queue_ = []; 125 | } else if (action === Constants.QUEUEADD_ACTION) { 126 | if (!port.queue_) { 127 | port.queue_ = []; 128 | } 129 | port.queue_.push(message.queueMessage); 130 | } else { 131 | trace('Unknown action from window: ' + action); 132 | } 133 | }; 134 | 135 | chrome.runtime.onConnect.addListener(function(port) { 136 | port.onDisconnect.addListener(function() { 137 | // Execute the cleanup queue. 138 | executeCleanupTasks(port, port.queue_).then(function() { 139 | // Close web socket. 140 | if (port.webSocket_) { 141 | trace('Closing web socket.'); 142 | port.webSocket_.close(); 143 | port.webSocket_ = null; 144 | } 145 | }); 146 | }); 147 | 148 | port.onMessage.addListener(function(message) { 149 | // Handle message from window to background. 150 | trace('W -> B: ' + JSON.stringify(message)); 151 | handleMessageFromWindow(port, message); 152 | }); 153 | }); 154 | })(); 155 | -------------------------------------------------------------------------------- /src/web_app/js/call_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals TestCase, SignalingChannel:true, requestUserMedia:true, 12 | assertEquals, assertTrue, MockWindowPort, FAKE_WSS_POST_URL, FAKE_ROOM_ID, 13 | FAKE_CLIENT_ID, apprtc, Constants, xhrs, MockXMLHttpRequest, assertFalse, 14 | XMLHttpRequest:true */ 15 | 16 | 'use strict'; 17 | 18 | var FAKE_LEAVE_URL = '/leave/' + FAKE_ROOM_ID + '/' + FAKE_CLIENT_ID; 19 | var MEDIA_STREAM_OBJECT = {value: 'stream'}; 20 | 21 | var mockSignalingChannels = []; 22 | 23 | var MockSignalingChannel = function() { 24 | this.isOpen = null; 25 | this.sends = []; 26 | mockSignalingChannels.push(this); 27 | }; 28 | 29 | MockSignalingChannel.prototype.open = function() { 30 | this.isOpen = true; 31 | return Promise.resolve(); 32 | }; 33 | 34 | MockSignalingChannel.prototype.getWssPostUrl = function() { 35 | return FAKE_WSS_POST_URL; 36 | }; 37 | 38 | MockSignalingChannel.prototype.send = function(data) { 39 | this.sends.push(data); 40 | }; 41 | 42 | MockSignalingChannel.prototype.close = function() { 43 | this.isOpen = false; 44 | }; 45 | 46 | var CallTest = new TestCase('CallTest'); 47 | 48 | function mockRequestUserMedia() { 49 | return new Promise(function(resolve) { 50 | resolve(MEDIA_STREAM_OBJECT); 51 | }); 52 | } 53 | 54 | CallTest.prototype.setUp = function() { 55 | mockSignalingChannels = []; 56 | this.signalingChannelBackup_ = SignalingChannel; 57 | SignalingChannel = MockSignalingChannel; 58 | this.requestUserMediaBackup_ = requestUserMedia; 59 | requestUserMedia = mockRequestUserMedia; 60 | 61 | this.params_ = { 62 | mediaConstraints: { 63 | audio: true, video: true 64 | }, 65 | roomId: FAKE_ROOM_ID, 66 | clientId: FAKE_CLIENT_ID 67 | }; 68 | }; 69 | 70 | CallTest.prototype.tearDown = function() { 71 | SignalingChannel = this.signalingChannelBackup_; 72 | requestUserMedia = this.requestUserMediaBackup_; 73 | }; 74 | 75 | CallTest.prototype.testRestartInitializesMedia = function() { 76 | var call = new Call(this.params_); 77 | var mediaStarted = false; 78 | call.onlocalstreamadded = function(stream) { 79 | mediaStarted = true; 80 | assertEquals(MEDIA_STREAM_OBJECT, stream); 81 | }; 82 | call.restart(); 83 | assertTrue(mediaStarted); 84 | }; 85 | 86 | CallTest.prototype.testSetUpCleanupQueue = function() { 87 | var realWindowPort = apprtc.windowPort; 88 | apprtc.windowPort = new MockWindowPort(); 89 | 90 | var call = new Call(this.params_); 91 | assertEquals(0, apprtc.windowPort.messages.length); 92 | call.queueCleanupMessages_(); 93 | assertEquals(3, apprtc.windowPort.messages.length); 94 | 95 | var verifyXhrMessage = function(message, method, url) { 96 | assertEquals(Constants.QUEUEADD_ACTION, message.action); 97 | assertEquals(Constants.XHR_ACTION, message.queueMessage.action); 98 | assertEquals(method, message.queueMessage.method); 99 | assertEquals(url, message.queueMessage.url); 100 | assertEquals(null, message.queueMessage.body); 101 | }; 102 | 103 | verifyXhrMessage(apprtc.windowPort.messages[0], 'POST', FAKE_LEAVE_URL); 104 | verifyXhrMessage(apprtc.windowPort.messages[2], 'DELETE', 105 | FAKE_WSS_POST_URL); 106 | 107 | var message = apprtc.windowPort.messages[1]; 108 | assertEquals(Constants.QUEUEADD_ACTION, message.action); 109 | assertEquals(Constants.WS_ACTION, message.queueMessage.action); 110 | assertEquals(Constants.WS_SEND_ACTION, message.queueMessage.wsAction); 111 | var data = JSON.parse(message.queueMessage.data); 112 | assertEquals('send', data.cmd); 113 | var msg = JSON.parse(data.msg); 114 | assertEquals('bye', msg.type); 115 | 116 | apprtc.windowPort = realWindowPort; 117 | }; 118 | 119 | CallTest.prototype.testClearCleanupQueue = function() { 120 | var realWindowPort = apprtc.windowPort; 121 | apprtc.windowPort = new MockWindowPort(); 122 | 123 | var call = new Call(this.params_); 124 | call.queueCleanupMessages_(); 125 | assertEquals(3, apprtc.windowPort.messages.length); 126 | 127 | call.clearCleanupQueue_(); 128 | assertEquals(4, apprtc.windowPort.messages.length); 129 | var message = apprtc.windowPort.messages[3]; 130 | assertEquals(Constants.QUEUECLEAR_ACTION, message.action); 131 | 132 | apprtc.windowPort = realWindowPort; 133 | }; 134 | 135 | CallTest.prototype.testCallHangupSync = function() { 136 | var call = new Call(this.params_); 137 | var stopCalled = false; 138 | var closeCalled = false; 139 | call.localStream_ = {stop: function() {stopCalled = true; }}; 140 | call.pcClient_ = {close: function() {closeCalled = true; }}; 141 | 142 | assertEquals(0, xhrs.length); 143 | assertEquals(0, mockSignalingChannels[0].sends.length); 144 | assertTrue(mockSignalingChannels[0].isOpen !== false); 145 | var realXMLHttpRequest = XMLHttpRequest; 146 | XMLHttpRequest = MockXMLHttpRequest; 147 | 148 | call.hangup(false); 149 | XMLHttpRequest = realXMLHttpRequest; 150 | 151 | assertEquals(true, stopCalled); 152 | assertEquals(true, closeCalled); 153 | // Send /leave. 154 | assertEquals(1, xhrs.length); 155 | assertEquals(FAKE_LEAVE_URL, xhrs[0].url); 156 | assertEquals('POST', xhrs[0].method); 157 | 158 | assertEquals(1, mockSignalingChannels.length); 159 | // Send 'bye' to ws. 160 | assertEquals(1, mockSignalingChannels[0].sends.length); 161 | assertEquals(JSON.stringify({type: 'bye'}), 162 | mockSignalingChannels[0].sends[0]); 163 | 164 | // Close ws. 165 | assertFalse(mockSignalingChannels[0].isOpen); 166 | 167 | // Clean up params state. 168 | assertEquals(null, call.params_.roomId); 169 | assertEquals(null, call.params_.clientId); 170 | assertEquals(FAKE_ROOM_ID, call.params_.previousRoomId); 171 | }; 172 | -------------------------------------------------------------------------------- /src/web_app/js/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* exported Constants */ 12 | 'use strict'; 13 | 14 | var Constants = { 15 | // Action type for remote web socket communication. 16 | WS_ACTION: 'ws', 17 | // Action type for remote xhr communication. 18 | XHR_ACTION: 'xhr', 19 | // Action type for adding a command to the remote clean up queue. 20 | QUEUEADD_ACTION: 'addToQueue', 21 | // Action type for clearing the remote clean up queue. 22 | QUEUECLEAR_ACTION: 'clearQueue', 23 | // Web socket action type specifying that an event occured. 24 | EVENT_ACTION: 'event', 25 | 26 | // Web socket action type to create a remote web socket. 27 | WS_CREATE_ACTION: 'create', 28 | // Web socket event type onerror. 29 | WS_EVENT_ONERROR: 'onerror', 30 | // Web socket event type onmessage. 31 | WS_EVENT_ONMESSAGE: 'onmessage', 32 | // Web socket event type onopen. 33 | WS_EVENT_ONOPEN: 'onopen', 34 | // Web socket event type onclose. 35 | WS_EVENT_ONCLOSE: 'onclose', 36 | // Web socket event sent when an error occurs while calling send. 37 | WS_EVENT_SENDERROR: 'onsenderror', 38 | // Web socket action type to send a message on the remote web socket. 39 | WS_SEND_ACTION: 'send', 40 | // Web socket action type to close the remote web socket. 41 | WS_CLOSE_ACTION: 'close' 42 | }; 43 | -------------------------------------------------------------------------------- /src/web_app/js/infobox_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals TestCase, assertEquals, InfoBox */ 12 | 13 | 'use strict'; 14 | 15 | var InfoBoxTest = new TestCase('InfoBoxTest'); 16 | 17 | InfoBoxTest.prototype.testFormatBitrate = function() { 18 | assertEquals('Format bps.', '789 bps', InfoBox.formatBitrate_(789)); 19 | assertEquals('Format kbps.', '78.9 kbps', InfoBox.formatBitrate_(78912)); 20 | assertEquals('Format Mbps.', '7.89 Mbps', InfoBox.formatBitrate_(7891234)); 21 | }; 22 | 23 | InfoBoxTest.prototype.testFormatInterval = function() { 24 | assertEquals('Format 00:01', '00:01', InfoBox.formatInterval_(1999)); 25 | assertEquals('Format 00:12', '00:12', InfoBox.formatInterval_(12500)); 26 | assertEquals('Format 01:23', '01:23', InfoBox.formatInterval_(83123)); 27 | assertEquals('Format 12:34', '12:34', InfoBox.formatInterval_(754000)); 28 | assertEquals('Format 01:23:45', '01:23:45', 29 | InfoBox.formatInterval_(5025000)); 30 | assertEquals('Format 12:34:56', '12:34:56', 31 | InfoBox.formatInterval_(45296000)); 32 | assertEquals('Format 123:45:43', '123:45:43', 33 | InfoBox.formatInterval_(445543000)); 34 | }; 35 | -------------------------------------------------------------------------------- /src/web_app/js/loopback.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* exported setupLoopback */ 12 | 13 | 'use strict'; 14 | 15 | // We handle the loopback case by making a second connection to the WSS so that 16 | // we receive the same messages that we send out. When receiving an offer we 17 | // convert that offer into an answer message. When receiving candidates we 18 | // echo back the candidate. Answer is ignored because we should never receive 19 | // one while in loopback. Bye is ignored because there is no work to do. 20 | var loopbackWebSocket = null; 21 | var LOOPBACK_CLIENT_ID = 'loopback_client_id'; 22 | function setupLoopback(wssUrl, roomId) { 23 | if (loopbackWebSocket) { 24 | return; 25 | } 26 | // TODO(tkchin): merge duplicate code once SignalingChannel abstraction 27 | // exists. 28 | loopbackWebSocket = new WebSocket(wssUrl); 29 | 30 | var sendLoopbackMessage = function(message) { 31 | var msgString = JSON.stringify({ 32 | cmd: 'send', 33 | msg: JSON.stringify(message) 34 | }); 35 | loopbackWebSocket.send(msgString); 36 | }; 37 | 38 | loopbackWebSocket.onopen = function() { 39 | var registerMessage = { 40 | cmd: 'register', 41 | roomid: roomId, 42 | clientid: LOOPBACK_CLIENT_ID 43 | }; 44 | loopbackWebSocket.send(JSON.stringify(registerMessage)); 45 | }; 46 | 47 | loopbackWebSocket.onmessage = function(event) { 48 | var wssMessage; 49 | var message; 50 | try { 51 | wssMessage = JSON.parse(event.data); 52 | message = JSON.parse(wssMessage.msg); 53 | } catch (e) { 54 | trace('Error parsing JSON: ' + event.data); 55 | return; 56 | } 57 | if (wssMessage.error) { 58 | trace('WSS error: ' + wssMessage.error); 59 | return; 60 | } 61 | if (message.type === 'offer') { 62 | var loopbackAnswer = wssMessage.msg; 63 | loopbackAnswer = loopbackAnswer.replace('"offer"', '"answer"'); 64 | loopbackAnswer = 65 | loopbackAnswer.replace('a=ice-options:google-ice\\r\\n', ''); 66 | sendLoopbackMessage(JSON.parse(loopbackAnswer)); 67 | } else if (message.type === 'candidate') { 68 | sendLoopbackMessage(message); 69 | } 70 | }; 71 | 72 | loopbackWebSocket.onclose = function(event) { 73 | trace('Loopback closed with code:' + event.code + ' reason:' + 74 | event.reason); 75 | // TODO(tkchin): try to reconnect. 76 | loopbackWebSocket = null; 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /src/web_app/js/remotewebsocket.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals apprtc, Constants */ 12 | /* exported RemoteWebSocket */ 13 | 14 | 'use strict'; 15 | 16 | // This class is used as a proxy for the WebSocket owned by background.js. 17 | // This proxy class sends commands and receives events via a Port object 18 | // opened to communicate with background.js in a Chrome App. 19 | // The WebSocket object must be owned by background.js so the call can be 20 | // properly terminated when the app window is closed. 21 | var RemoteWebSocket = function(wssUrl, wssPostUrl) { 22 | this.wssUrl_ = wssUrl; 23 | apprtc.windowPort.addMessageListener(this.handleMessage_.bind(this)); 24 | this.sendMessage_({ 25 | action: Constants.WS_ACTION, 26 | wsAction: Constants.WS_CREATE_ACTION, 27 | wssUrl: wssUrl, 28 | wssPostUrl: wssPostUrl 29 | }); 30 | this.readyState = WebSocket.CONNECTING; 31 | }; 32 | 33 | RemoteWebSocket.prototype.sendMessage_ = function(message) { 34 | apprtc.windowPort.sendMessage(message); 35 | }; 36 | 37 | RemoteWebSocket.prototype.send = function(data) { 38 | if (this.readyState !== WebSocket.OPEN) { 39 | throw 'Web socket is not in OPEN state: ' + this.readyState; 40 | } 41 | this.sendMessage_({ 42 | action: Constants.WS_ACTION, 43 | wsAction: Constants.WS_SEND_ACTION, 44 | data: data 45 | }); 46 | }; 47 | 48 | RemoteWebSocket.prototype.close = function() { 49 | if (this.readyState === WebSocket.CLOSING || 50 | this.readyState === WebSocket.CLOSED) { 51 | return; 52 | } 53 | this.readyState = WebSocket.CLOSING; 54 | this.sendMessage_({ 55 | action: Constants.WS_ACTION, 56 | wsAction: Constants.WS_CLOSE_ACTION 57 | }); 58 | }; 59 | 60 | RemoteWebSocket.prototype.handleMessage_ = function(message) { 61 | if (message.action === Constants.WS_ACTION && 62 | message.wsAction === Constants.EVENT_ACTION) { 63 | if (message.wsEvent === Constants.WS_EVENT_ONOPEN) { 64 | this.readyState = WebSocket.OPEN; 65 | if (this.onopen) { 66 | this.onopen(); 67 | } 68 | } else if (message.wsEvent === Constants.WS_EVENT_ONCLOSE) { 69 | this.readyState = WebSocket.CLOSED; 70 | if (this.onclose) { 71 | this.onclose(message.data); 72 | } 73 | } else if (message.wsEvent === Constants.WS_EVENT_ONERROR) { 74 | if (this.onerror) { 75 | this.onerror(message.data); 76 | } 77 | } else if (message.wsEvent === Constants.WS_EVENT_ONMESSAGE) { 78 | if (this.onmessage) { 79 | this.onmessage(message.data); 80 | } 81 | } else if (message.wsEvent === Constants.WS_EVENT_SENDERROR) { 82 | if (this.onsenderror) { 83 | this.onsenderror(message.data); 84 | } 85 | trace('ERROR: web socket send failed: ' + message.data); 86 | } 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /src/web_app/js/remotewebsocket_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals TestCase, assertEquals, Constants, FAKE_WSS_URL, apprtc, 12 | RemoteWebSocket, MockWindowPort */ 13 | 14 | 'use strict'; 15 | var TEST_MESSAGE = 'foobar'; 16 | 17 | var RemoteWebSocketTest = new TestCase('RemoteWebSocketTest'); 18 | 19 | RemoteWebSocketTest.prototype.setUp = function() { 20 | this.realWindowPort = apprtc.windowPort; 21 | apprtc.windowPort = new MockWindowPort(); 22 | 23 | this.rws_ = new RemoteWebSocket(FAKE_WSS_URL); 24 | // Should have an message to request create. 25 | assertEquals(1, apprtc.windowPort.messages.length); 26 | assertEquals(Constants.WS_ACTION, apprtc.windowPort.messages[0].action); 27 | assertEquals(Constants.WS_CREATE_ACTION, 28 | apprtc.windowPort.messages[0].wsAction); 29 | assertEquals(FAKE_WSS_URL, apprtc.windowPort.messages[0].wssUrl); 30 | assertEquals(WebSocket.CONNECTING, this.rws_.readyState); 31 | 32 | }; 33 | 34 | RemoteWebSocketTest.prototype.tearDown = function() { 35 | apprtc.windowPort = this.realWindowPort; 36 | }; 37 | 38 | RemoteWebSocketTest.prototype.testSendBeforeOpen = function() { 39 | var exception = false; 40 | try { 41 | this.rws_.send(TEST_MESSAGE); 42 | } catch (ex) { 43 | if (ex) { 44 | exception = true; 45 | } 46 | } 47 | 48 | assertEquals(true, exception); 49 | }; 50 | 51 | RemoteWebSocketTest.prototype.testSend = function() { 52 | apprtc.windowPort.simulateMessageFromBackground({ 53 | action: Constants.WS_ACTION, 54 | wsAction: Constants.EVENT_ACTION, 55 | wsEvent: Constants.WS_EVENT_ONOPEN, 56 | data: TEST_MESSAGE 57 | }); 58 | 59 | assertEquals(1, apprtc.windowPort.messages.length); 60 | this.rws_.send(TEST_MESSAGE); 61 | assertEquals(2, apprtc.windowPort.messages.length); 62 | assertEquals(Constants.WS_ACTION, apprtc.windowPort.messages[1].action); 63 | assertEquals(Constants.WS_SEND_ACTION, 64 | apprtc.windowPort.messages[1].wsAction); 65 | assertEquals(TEST_MESSAGE, apprtc.windowPort.messages[1].data); 66 | }; 67 | 68 | RemoteWebSocketTest.prototype.testClose = function() { 69 | var message = null; 70 | var called = false; 71 | this.rws_.onclose = function(e) { 72 | called = true; 73 | message = e; 74 | }; 75 | 76 | assertEquals(1, apprtc.windowPort.messages.length); 77 | this.rws_.close(); 78 | 79 | assertEquals(2, apprtc.windowPort.messages.length); 80 | assertEquals(Constants.WS_ACTION, apprtc.windowPort.messages[1].action); 81 | assertEquals(Constants.WS_CLOSE_ACTION, 82 | apprtc.windowPort.messages[1].wsAction); 83 | 84 | assertEquals(WebSocket.CLOSING, this.rws_.readyState); 85 | apprtc.windowPort.simulateMessageFromBackground({ 86 | action: Constants.WS_ACTION, 87 | wsAction: Constants.EVENT_ACTION, 88 | wsEvent: Constants.WS_EVENT_ONCLOSE, 89 | data: TEST_MESSAGE 90 | }); 91 | assertEquals(true, called); 92 | assertEquals(TEST_MESSAGE, message); 93 | assertEquals(WebSocket.CLOSED, this.rws_.readyState); 94 | }; 95 | 96 | RemoteWebSocketTest.prototype.testOnError = function() { 97 | var message = null; 98 | var called = false; 99 | this.rws_.onerror = function(e) { 100 | called = true; 101 | message = e; 102 | }; 103 | 104 | apprtc.windowPort.simulateMessageFromBackground({ 105 | action: Constants.WS_ACTION, 106 | wsAction: Constants.EVENT_ACTION, 107 | wsEvent: Constants.WS_EVENT_ONERROR, 108 | data: TEST_MESSAGE 109 | }); 110 | assertEquals(true, called); 111 | assertEquals(TEST_MESSAGE, message); 112 | }; 113 | 114 | RemoteWebSocketTest.prototype.testOnOpen = function() { 115 | var called = false; 116 | this.rws_.onopen = function() { 117 | called = true; 118 | }; 119 | 120 | apprtc.windowPort.simulateMessageFromBackground({ 121 | action: Constants.WS_ACTION, 122 | wsAction: Constants.EVENT_ACTION, 123 | wsEvent: Constants.WS_EVENT_ONOPEN, 124 | data: TEST_MESSAGE 125 | }); 126 | assertEquals(true, called); 127 | assertEquals(WebSocket.OPEN, this.rws_.readyState); 128 | }; 129 | 130 | RemoteWebSocketTest.prototype.testOnMessage = function() { 131 | var message = null; 132 | var called = false; 133 | this.rws_.onmessage = function(e) { 134 | called = true; 135 | message = e; 136 | }; 137 | 138 | apprtc.windowPort.simulateMessageFromBackground({ 139 | action: Constants.WS_ACTION, 140 | wsAction: Constants.EVENT_ACTION, 141 | wsEvent: Constants.WS_EVENT_ONMESSAGE, 142 | data: TEST_MESSAGE 143 | }); 144 | assertEquals(true, called); 145 | assertEquals(TEST_MESSAGE, message); 146 | }; 147 | 148 | RemoteWebSocketTest.prototype.testOnSendError = function() { 149 | var message = null; 150 | var called = false; 151 | this.rws_.onsenderror = function(e) { 152 | called = true; 153 | message = e; 154 | }; 155 | 156 | apprtc.windowPort.simulateMessageFromBackground({ 157 | action: Constants.WS_ACTION, 158 | wsAction: Constants.EVENT_ACTION, 159 | wsEvent: Constants.WS_EVENT_SENDERROR, 160 | data: TEST_MESSAGE 161 | }); 162 | assertEquals(true, called); 163 | assertEquals(TEST_MESSAGE, message); 164 | }; 165 | -------------------------------------------------------------------------------- /src/web_app/js/sdputils_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals TestCase, maybePreferCodec, removeCodecParam, setCodecParam, 12 | assertEquals */ 13 | 14 | 'use strict'; 15 | 16 | var SDP_WITH_AUDIO_CODECS = 17 | ['v=0', 18 | 'm=audio 9 RTP/SAVPF 111 103 104 0 9', 19 | 'a=rtcp-mux', 20 | 'a=rtpmap:111 opus/48000/2', 21 | 'a=fmtp:111 minptime=10', 22 | 'a=rtpmap:103 ISAC/16000', 23 | 'a=rtpmap:9 G722/8000', 24 | 'a=rtpmap:0 PCMU/8000', 25 | 'a=rtpmap:8 PCMA/8000', 26 | ].join('\r\n'); 27 | 28 | var SdpUtilsTest = new TestCase('SdpUtilsTest'); 29 | 30 | SdpUtilsTest.prototype.testMovesIsac16KToDefaultWhenPreferred = function() { 31 | var result = maybePreferCodec(SDP_WITH_AUDIO_CODECS, 'audio', 'send', 32 | 'iSAC/16000'); 33 | var audioLine = result.split('\r\n')[1]; 34 | assertEquals('iSAC 16K (of type 103) should be moved to front.', 35 | 'm=audio 9 RTP/SAVPF 103 111 104 0 9', 36 | audioLine); 37 | }; 38 | 39 | SdpUtilsTest.prototype.testDoesNothingIfPreferredCodecNotFound = function() { 40 | var result = maybePreferCodec(SDP_WITH_AUDIO_CODECS, 'audio', 'send', 41 | 'iSAC/123456'); 42 | var audioLine = result.split('\r\n')[1]; 43 | assertEquals('SDP should be unaffected since the codec does not exist.', 44 | SDP_WITH_AUDIO_CODECS.split('\r\n')[1], 45 | audioLine); 46 | }; 47 | 48 | SdpUtilsTest.prototype.testMovesCodecEvenIfPayloadTypeIsSameAsUdpPort = 49 | function() { 50 | var result = maybePreferCodec(SDP_WITH_AUDIO_CODECS, 51 | 'audio', 52 | 'send', 53 | 'G722/8000'); 54 | var audioLine = result.split('\r\n')[1]; 55 | assertEquals('G722/8000 (of type 9) should be moved to front.', 56 | 'm=audio 9 RTP/SAVPF 9 111 103 104 0', 57 | audioLine); 58 | }; 59 | 60 | SdpUtilsTest.prototype.testRemoveAndSetCodecParamModifyFmtpLine = 61 | function() { 62 | var result = setCodecParam(SDP_WITH_AUDIO_CODECS, 'opus/48000', 63 | 'minptime', '20'); 64 | var audioLine = result.split('\r\n')[4]; 65 | assertEquals('minptime=10 should be modified in a=fmtp:111 line.', 66 | 'a=fmtp:111 minptime=20', audioLine); 67 | 68 | result = setCodecParam(result, 'opus/48000', 'useinbandfec', '1'); 69 | audioLine = result.split('\r\n')[4]; 70 | assertEquals('useinbandfec=1 should be added to a=fmtp:111 line.', 71 | 'a=fmtp:111 minptime=20; useinbandfec=1', audioLine); 72 | 73 | result = removeCodecParam(result, 'opus/48000', 'minptime'); 74 | audioLine = result.split('\r\n')[4]; 75 | assertEquals('minptime should be removed from a=fmtp:111 line.', 76 | 'a=fmtp:111 useinbandfec=1', audioLine); 77 | 78 | var newResult = removeCodecParam(result, 'opus/48000', 'minptime'); 79 | assertEquals('removeCodecParam should not affect sdp ' + 80 | 'if param did not exist', result, newResult); 81 | }; 82 | 83 | SdpUtilsTest.prototype.testRemoveAndSetCodecParamRemoveAndAddFmtpLineIfNeeded = 84 | function() { 85 | var result = removeCodecParam(SDP_WITH_AUDIO_CODECS, 'opus/48000', 86 | 'minptime'); 87 | var audioLine = result.split('\r\n')[4]; 88 | assertEquals('a=fmtp:111 line should be deleted.', 89 | 'a=rtpmap:103 ISAC/16000', audioLine); 90 | result = setCodecParam(result, 'opus/48000', 'inbandfec', '1'); 91 | audioLine = result.split('\r\n')[4]; 92 | assertEquals('a=fmtp:111 line should be added.', 93 | 'a=fmtp:111 inbandfec=1', audioLine); 94 | }; 95 | -------------------------------------------------------------------------------- /src/web_app/js/signalingchannel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals parseJSON, trace, sendUrlRequest, isChromeApp, RemoteWebSocket */ 12 | /* exported SignalingChannel */ 13 | 14 | 'use strict'; 15 | 16 | // This class implements a signaling channel based on WebSocket. 17 | var SignalingChannel = function(wssUrl, wssPostUrl) { 18 | this.wssUrl_ = wssUrl; 19 | this.wssPostUrl_ = wssPostUrl; 20 | this.roomId_ = null; 21 | this.clientId_ = null; 22 | this.websocket_ = null; 23 | this.registered_ = false; 24 | 25 | // Public callbacks. Keep it sorted. 26 | this.onerror = null; 27 | this.onmessage = null; 28 | }; 29 | 30 | SignalingChannel.prototype.open = function() { 31 | if (this.websocket_) { 32 | trace('ERROR: SignalingChannel has already opened.'); 33 | return; 34 | } 35 | 36 | trace('Opening signaling channel.'); 37 | return new Promise(function(resolve, reject) { 38 | if (isChromeApp()) { 39 | this.websocket_ = new RemoteWebSocket(this.wssUrl_, this.wssPostUrl_); 40 | } else { 41 | this.websocket_ = new WebSocket(this.wssUrl_); 42 | } 43 | 44 | this.websocket_.onopen = function() { 45 | trace('Signaling channel opened.'); 46 | 47 | this.websocket_.onerror = function() { 48 | trace('Signaling channel error.'); 49 | }; 50 | this.websocket_.onclose = function(event) { 51 | // TODO(tkchin): reconnect to WSS. 52 | trace('Channel closed with code:' + event.code + 53 | ' reason:' + event.reason); 54 | this.websocket_ = null; 55 | this.registered_ = false; 56 | }; 57 | 58 | if (this.clientId_ && this.roomId_) { 59 | this.register(this.roomId_, this.clientId_); 60 | } 61 | 62 | resolve(); 63 | }.bind(this); 64 | 65 | this.websocket_.onmessage = function(event) { 66 | trace('WSS->C: ' + event.data); 67 | 68 | var message = parseJSON(event.data); 69 | if (!message) { 70 | trace('Failed to parse WSS message: ' + event.data); 71 | return; 72 | } 73 | if (message.error) { 74 | trace('Signaling server error message: ' + message.error); 75 | return; 76 | } 77 | this.onmessage(message.msg); 78 | }.bind(this); 79 | 80 | this.websocket_.onerror = function() { 81 | reject(Error('WebSocket error.')); 82 | }; 83 | }.bind(this)); 84 | }; 85 | 86 | SignalingChannel.prototype.register = function(roomId, clientId) { 87 | if (this.registered_) { 88 | trace('ERROR: SignalingChannel has already registered.'); 89 | return; 90 | } 91 | 92 | this.roomId_ = roomId; 93 | this.clientId_ = clientId; 94 | 95 | if (!this.roomId_) { 96 | trace('ERROR: missing roomId.'); 97 | } 98 | if (!this.clientId_) { 99 | trace('ERROR: missing clientId.'); 100 | } 101 | if (!this.websocket_ || this.websocket_.readyState !== WebSocket.OPEN) { 102 | trace('WebSocket not open yet; saving the IDs to register later.'); 103 | return; 104 | } 105 | trace('Registering signaling channel.'); 106 | var registerMessage = { 107 | cmd: 'register', 108 | roomid: this.roomId_, 109 | clientid: this.clientId_ 110 | }; 111 | this.websocket_.send(JSON.stringify(registerMessage)); 112 | this.registered_ = true; 113 | 114 | // TODO(tkchin): Better notion of whether registration succeeded. Basically 115 | // check that we don't get an error message back from the socket. 116 | trace('Signaling channel registered.'); 117 | }; 118 | 119 | SignalingChannel.prototype.close = function(async) { 120 | if (this.websocket_) { 121 | this.websocket_.close(); 122 | this.websocket_ = null; 123 | } 124 | 125 | if (!this.clientId_ || !this.roomId_) { 126 | return; 127 | } 128 | // Tell WSS that we're done. 129 | var path = this.getWssPostUrl(); 130 | 131 | return sendUrlRequest('DELETE', path, async).catch(function(error) { 132 | trace('Error deleting web socket connection: ' + error.message); 133 | }.bind(this)).then(function() { 134 | this.clientId_ = null; 135 | this.roomId_ = null; 136 | this.registered_ = false; 137 | }.bind(this)); 138 | }; 139 | 140 | SignalingChannel.prototype.send = function(message) { 141 | if (!this.roomId_ || !this.clientId_) { 142 | trace('ERROR: SignalingChannel has not registered.'); 143 | return; 144 | } 145 | trace('C->WSS: ' + message); 146 | 147 | var wssMessage = { 148 | cmd: 'send', 149 | msg: message 150 | }; 151 | var msgString = JSON.stringify(wssMessage); 152 | 153 | if (this.websocket_ && this.websocket_.readyState === WebSocket.OPEN) { 154 | this.websocket_.send(msgString); 155 | } else { 156 | var path = this.getWssPostUrl(); 157 | var xhr = new XMLHttpRequest(); 158 | xhr.open('POST', path, true); 159 | xhr.send(wssMessage.msg); 160 | } 161 | }; 162 | 163 | SignalingChannel.prototype.getWssPostUrl = function() { 164 | return this.wssPostUrl_ + '/' + this.roomId_ + '/' + this.clientId_; 165 | }; 166 | -------------------------------------------------------------------------------- /src/web_app/js/signalingchannel_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals TestCase, assertEquals, assertNotNull, assertTrue, assertFalse, 12 | WebSocket:true, XMLHttpRequest:true, SignalingChannel, webSockets:true, 13 | xhrs:true, FAKE_WSS_URL, FAKE_WSS_POST_URL, FAKE_ROOM_ID, FAKE_CLIENT_ID, 14 | MockXMLHttpRequest, MockWebSocket */ 15 | 16 | 'use strict'; 17 | 18 | var SignalingChannelTest = new TestCase('SignalingChannelTest'); 19 | 20 | SignalingChannelTest.prototype.setUp = function() { 21 | webSockets = []; 22 | xhrs = []; 23 | 24 | this.realWebSocket = WebSocket; 25 | WebSocket = MockWebSocket; 26 | 27 | this.channel = 28 | new SignalingChannel(FAKE_WSS_URL, FAKE_WSS_POST_URL); 29 | }; 30 | 31 | SignalingChannelTest.prototype.tearDown = function() { 32 | WebSocket = this.realWebSocket; 33 | }; 34 | 35 | SignalingChannelTest.prototype.testOpenSuccess = function() { 36 | var promise = this.channel.open(); 37 | assertEquals(1, webSockets.length); 38 | 39 | var resolved = false; 40 | var rejected = false; 41 | promise.then(function() { 42 | resolved = true; 43 | }).catch (function() { 44 | rejected = true; 45 | }); 46 | 47 | var socket = webSockets[0]; 48 | socket.simulateOpenResult(true); 49 | assertTrue(resolved); 50 | assertFalse(rejected); 51 | }; 52 | 53 | SignalingChannelTest.prototype.testReceiveMessage = function() { 54 | this.channel.open(); 55 | var socket = webSockets[0]; 56 | socket.simulateOpenResult(true); 57 | 58 | assertNotNull(socket.onmessage); 59 | 60 | var msgs = []; 61 | this.channel.onmessage = function(msg) { 62 | msgs.push(msg); 63 | }; 64 | 65 | var expectedMsg = 'hi'; 66 | var event = { 67 | 'data': JSON.stringify({'msg': expectedMsg}) 68 | }; 69 | socket.onmessage(event); 70 | assertEquals(1, msgs.length); 71 | assertEquals(expectedMsg, msgs[0]); 72 | }; 73 | 74 | SignalingChannelTest.prototype.testOpenFailure = function() { 75 | var promise = this.channel.open(); 76 | assertEquals(1, webSockets.length); 77 | 78 | var resolved = false; 79 | var rejected = false; 80 | promise.then(function() { 81 | resolved = true; 82 | }).catch (function() { 83 | rejected = true; 84 | }); 85 | 86 | var socket = webSockets[0]; 87 | socket.simulateOpenResult(false); 88 | assertFalse(resolved); 89 | assertTrue(rejected); 90 | }; 91 | 92 | SignalingChannelTest.prototype.testRegisterBeforeOpen = function() { 93 | this.channel.open(); 94 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 95 | 96 | var socket = webSockets[0]; 97 | socket.simulateOpenResult(true); 98 | 99 | assertEquals(1, socket.messages.length); 100 | 101 | var registerMessage = { 102 | cmd: 'register', 103 | roomid: FAKE_ROOM_ID, 104 | clientid: FAKE_CLIENT_ID 105 | }; 106 | assertEquals(JSON.stringify(registerMessage), socket.messages[0]); 107 | }; 108 | 109 | SignalingChannelTest.prototype.testRegisterAfterOpen = function() { 110 | this.channel.open(); 111 | var socket = webSockets[0]; 112 | socket.simulateOpenResult(true); 113 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 114 | 115 | assertEquals(1, socket.messages.length); 116 | 117 | var registerMessage = { 118 | cmd: 'register', 119 | roomid: FAKE_ROOM_ID, 120 | clientid: FAKE_CLIENT_ID 121 | }; 122 | assertEquals(JSON.stringify(registerMessage), socket.messages[0]); 123 | }; 124 | 125 | SignalingChannelTest.prototype.testSendBeforeOpen = function() { 126 | // Stubbing XMLHttpRequest cannot be done in setUp since it caused PhantomJS 127 | // to hang. 128 | var realXMLHttpRequest = XMLHttpRequest; 129 | XMLHttpRequest = MockXMLHttpRequest; 130 | 131 | this.channel.open(); 132 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 133 | 134 | var message = 'hello'; 135 | this.channel.send(message); 136 | 137 | assertEquals(1, xhrs.length); 138 | assertEquals(2, xhrs[0].readyState); 139 | assertEquals(FAKE_WSS_POST_URL + '/' + FAKE_ROOM_ID + '/' + FAKE_CLIENT_ID, 140 | xhrs[0].url); 141 | assertEquals('POST', xhrs[0].method); 142 | assertEquals(message, xhrs[0].body); 143 | 144 | XMLHttpRequest = realXMLHttpRequest; 145 | }; 146 | 147 | SignalingChannelTest.prototype.testSendAfterOpen = function() { 148 | this.channel.open(); 149 | var socket = webSockets[0]; 150 | socket.simulateOpenResult(true); 151 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 152 | 153 | var message = 'hello'; 154 | var wsMessage = { 155 | cmd: 'send', 156 | msg: message 157 | }; 158 | this.channel.send(message); 159 | assertEquals(2, socket.messages.length); 160 | assertEquals(JSON.stringify(wsMessage), socket.messages[1]); 161 | }; 162 | 163 | SignalingChannelTest.prototype.testCloseAfterRegister = function() { 164 | var realXMLHttpRequest = XMLHttpRequest; 165 | XMLHttpRequest = MockXMLHttpRequest; 166 | 167 | this.channel.open(); 168 | var socket = webSockets[0]; 169 | socket.simulateOpenResult(true); 170 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 171 | 172 | assertEquals(WebSocket.OPEN, socket.readyState); 173 | this.channel.close(); 174 | assertEquals(WebSocket.CLOSED, socket.readyState); 175 | 176 | assertEquals(1, xhrs.length); 177 | assertEquals(4, xhrs[0].readyState); 178 | assertEquals(FAKE_WSS_POST_URL + '/' + FAKE_ROOM_ID + '/' + FAKE_CLIENT_ID, 179 | xhrs[0].url); 180 | assertEquals('DELETE', xhrs[0].method); 181 | 182 | XMLHttpRequest = realXMLHttpRequest; 183 | }; 184 | 185 | SignalingChannelTest.prototype.testCloseBeforeRegister = function() { 186 | var realXMLHttpRequest = XMLHttpRequest; 187 | XMLHttpRequest = MockXMLHttpRequest; 188 | 189 | this.channel.open(); 190 | this.channel.close(); 191 | 192 | assertEquals(0, xhrs.length); 193 | XMLHttpRequest = realXMLHttpRequest; 194 | }; 195 | -------------------------------------------------------------------------------- /src/web_app/js/stats.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* exported computeBitrate, computeE2EDelay, computeRate, 12 | extractStatAsInt, refreshStats */ 13 | 14 | 'use strict'; 15 | 16 | // Return the integer stat |statName| from the object with type |statObj| in 17 | // |stats|, or null if not present. 18 | function extractStatAsInt(stats, statObj, statName) { 19 | // Ignore stats that have a 'nullish' value. 20 | // The correct fix is indicated in 21 | // https://code.google.com/p/webrtc/issues/detail?id=3377. 22 | var str = extractStat(stats, statObj, statName); 23 | if (str) { 24 | var val = parseInt(str); 25 | if (val !== -1) { 26 | return val; 27 | } 28 | } 29 | return null; 30 | } 31 | 32 | // Return the stat |statName| from the object with type |statObj| in |stats| 33 | // as a string, or null if not present. 34 | function extractStat(stats, statObj, statName) { 35 | var report = getStatsReport(stats, statObj, statName); 36 | if (report && report.names().indexOf(statName) !== -1) { 37 | return report.stat(statName); 38 | } 39 | return null; 40 | } 41 | 42 | // Return the stats report with type |statObj| in |stats|, with the stat 43 | // |statName| (if specified), and value |statVal| (if specified). Return 44 | // undef if not present. 45 | function getStatsReport(stats, statObj, statName, statVal) { 46 | if (stats) { 47 | for (var i = 0; i < stats.length; ++i) { 48 | var report = stats[i]; 49 | if (report.type === statObj) { 50 | var found = true; 51 | // If |statName| is present, ensure |report| has that stat. 52 | // If |statVal| is present, ensure the value matches. 53 | if (statName) { 54 | var val = report.stat(statName); 55 | found = (statVal !== undefined) ? (val === statVal) : val; 56 | } 57 | if (found) { 58 | return report; 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | // Takes two stats reports and determines the rate based on two counter readings 66 | // and the time between them (which is in units of milliseconds). 67 | function computeRate(newReport, oldReport, statName) { 68 | var newVal = newReport.stat(statName); 69 | var oldVal = (oldReport) ? oldReport.stat(statName) : null; 70 | if (newVal === null || oldVal === null) { 71 | return null; 72 | } 73 | return (newVal - oldVal) / (newReport.timestamp - oldReport.timestamp) * 1000; 74 | } 75 | 76 | // Convert a byte rate to a bit rate. 77 | function computeBitrate(newReport, oldReport, statName) { 78 | return computeRate(newReport, oldReport, statName) * 8; 79 | } 80 | 81 | // Computes end to end delay based on the capture start time (in NTP format) 82 | // and the current render time (in seconds since start of render). 83 | function computeE2EDelay(captureStart, remoteVideoCurrentTime) { 84 | if (!captureStart) { 85 | return null; 86 | } 87 | 88 | // Adding offset (milliseconds between 1900 and 1970) to get NTP time. 89 | var nowNTP = Date.now() + 2208988800000; 90 | return nowNTP - captureStart - remoteVideoCurrentTime * 1000; 91 | } 92 | -------------------------------------------------------------------------------- /src/web_app/js/storage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* exported Storage */ 12 | /* globals isChromeApp, chrome */ 13 | 14 | 'use strict'; 15 | 16 | var Storage = function() {}; 17 | 18 | // Get a value from local browser storage. Calls callback with value. 19 | // Handles variation in API between localStorage and Chrome app storage. 20 | Storage.prototype.getStorage = function(key, callback) { 21 | if (isChromeApp()) { 22 | // Use chrome.storage.local. 23 | chrome.storage.local.get(key, function(values) { 24 | // Unwrap key/value pair. 25 | if (callback) { 26 | window.setTimeout(function() { 27 | callback(values[key]); 28 | }, 0); 29 | } 30 | }); 31 | } else { 32 | // Use localStorage. 33 | var value = localStorage.getItem(key); 34 | if (callback) { 35 | window.setTimeout(function() { 36 | callback(value); 37 | }, 0); 38 | } 39 | } 40 | }; 41 | 42 | // Set a value in local browser storage. Calls callback after completion. 43 | // Handles variation in API between localStorage and Chrome app storage. 44 | Storage.prototype.setStorage = function(key, value, callback) { 45 | if (isChromeApp()) { 46 | // Use chrome.storage.local. 47 | var data = {}; 48 | data[key] = value; 49 | chrome.storage.local.set(data, callback); 50 | } else { 51 | // Use localStorage. 52 | localStorage.setItem(key, value); 53 | if (callback) { 54 | window.setTimeout(callback, 0); 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/web_app/js/test_mocks.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals assertEquals */ 12 | /* exported FAKE_WSS_POST_URL, FAKE_WSS_URL, FAKE_WSS_POST_URL, FAKE_ROOM_ID, 13 | FAKE_CLIENT_ID, MockWebSocket, MockXMLHttpRequest, webSockets, xhrs, 14 | MockWindowPort, FAKE_SEND_EXCEPTION */ 15 | 16 | 'use strict'; 17 | 18 | var FAKE_WSS_URL = 'wss://foo.com'; 19 | var FAKE_WSS_POST_URL = 'https://foo.com'; 20 | var FAKE_ROOM_ID = 'bar'; 21 | var FAKE_CLIENT_ID = 'barbar'; 22 | var FAKE_SEND_EXCEPTION = 'Send exception'; 23 | 24 | var webSockets = []; 25 | var MockWebSocket = function(url) { 26 | assertEquals(FAKE_WSS_URL, url); 27 | 28 | this.url = url; 29 | this.messages = []; 30 | this.readyState = WebSocket.CONNECTING; 31 | 32 | this.onopen = null; 33 | this.onclose = null; 34 | this.onerror = null; 35 | this.onmessage = null; 36 | 37 | webSockets.push(this); 38 | }; 39 | 40 | MockWebSocket.CONNECTING = WebSocket.CONNECTING; 41 | MockWebSocket.OPEN = WebSocket.OPEN; 42 | MockWebSocket.CLOSED = WebSocket.CLOSED; 43 | 44 | MockWebSocket.prototype.simulateOpenResult = function(success) { 45 | if (success) { 46 | this.readyState = WebSocket.OPEN; 47 | if (this.onopen) { 48 | this.onopen(); 49 | } 50 | } else { 51 | this.readyState = WebSocket.CLOSED; 52 | if (this.onerror) { 53 | this.onerror(Error('Mock open error')); 54 | } 55 | } 56 | }; 57 | 58 | MockWebSocket.prototype.send = function(msg) { 59 | if (this.readyState !== WebSocket.OPEN) { 60 | throw 'Send called when the connection is not open'; 61 | } 62 | 63 | if (this.throwOnSend) { 64 | throw FAKE_SEND_EXCEPTION; 65 | } 66 | 67 | this.messages.push(msg); 68 | }; 69 | 70 | MockWebSocket.prototype.close = function() { 71 | this.readyState = WebSocket.CLOSED; 72 | }; 73 | 74 | var xhrs = []; 75 | var MockXMLHttpRequest = function() { 76 | this.url = null; 77 | this.method = null; 78 | this.async = true; 79 | this.body = null; 80 | this.readyState = 0; 81 | 82 | xhrs.push(this); 83 | }; 84 | MockXMLHttpRequest.prototype.open = function(method, path, async) { 85 | this.url = path; 86 | this.method = method; 87 | this.async = async; 88 | this.readyState = 1; 89 | }; 90 | MockXMLHttpRequest.prototype.send = function(body) { 91 | this.body = body; 92 | if (this.async) { 93 | this.readyState = 2; 94 | } else { 95 | this.readyState = 4; 96 | } 97 | }; 98 | 99 | var MockWindowPort = function() { 100 | this.messages = []; 101 | this.onMessage_ = null; 102 | }; 103 | 104 | MockWindowPort.prototype.addMessageListener = function(callback) { 105 | this.onMessage_ = callback; 106 | }; 107 | 108 | MockWindowPort.prototype.sendMessage = function(message) { 109 | this.messages.push(message); 110 | }; 111 | 112 | MockWindowPort.prototype.simulateMessageFromBackground = function(message) { 113 | if (this.onMessage_) { 114 | this.onMessage_(message); 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /src/web_app/js/testpolyfills.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | 'use strict'; 12 | 13 | Function.prototype.bind = Function.prototype.bind || function(thisp) { 14 | var fn = this; 15 | var suppliedArgs = Array.prototype.slice.call(arguments, 1); 16 | return function() { 17 | return fn.apply(thisp, 18 | suppliedArgs.concat(Array.prototype.slice.call(arguments))); 19 | }; 20 | }; 21 | 22 | if (!window.performance) { 23 | window.performance = function() {}; 24 | window.performance.now = function() { return 0; }; 25 | } 26 | 27 | window.RTCSessionDescription = window.RTCSessionDescription || function(input) { 28 | this.type = input.type; 29 | this.sdp = input.sdp; 30 | }; 31 | 32 | window.RTCIceCandidate = window.RTCIceCandidate || function(candidate) { 33 | this.sdpMLineIndex = candidate.sdpMLineIndex; 34 | this.candidate = candidate.candidate; 35 | }; 36 | 37 | var PROMISE_STATE = { 38 | PENDING: 0, 39 | FULLFILLED: 1, 40 | REJECTED: 2 41 | }; 42 | 43 | var MyPromise = function(executor) { 44 | this.state_ = PROMISE_STATE.PENDING; 45 | this.resolveCallback_ = null; 46 | this.rejectCallback_ = null; 47 | 48 | this.value_ = null; 49 | this.reason_ = null; 50 | executor(this.onResolve_.bind(this), this.onReject_.bind(this)); 51 | }; 52 | 53 | MyPromise.all = function(promises) { 54 | var values = new Array(promises.length); 55 | return new MyPromise(function(values, resolve, reject) { 56 | function onResolve(values, index, value) { 57 | values[index] = value || null; 58 | 59 | for (var i = 0; i < values.length; ++i) { 60 | if (values[i] === undefined) { 61 | return; 62 | } 63 | } 64 | resolve(values); 65 | } 66 | for (var i = 0; i < promises.length; ++i) { 67 | promises[i].then(onResolve.bind(null, values, i), reject); 68 | } 69 | }.bind(null, values)); 70 | }; 71 | 72 | MyPromise.resolve = function(value) { 73 | return new MyPromise(function(resolve) { 74 | resolve(value); 75 | }); 76 | }; 77 | 78 | MyPromise.reject = function(error) { 79 | // JSHint flags the unused variable resolve. 80 | return new MyPromise(function(resolve, reject) { // jshint ignore:line 81 | reject(error); 82 | }); 83 | }; 84 | 85 | MyPromise.prototype.then = function(onResolve, onReject) { 86 | switch (this.state_) { 87 | case PROMISE_STATE.PENDING: 88 | this.resolveCallback_ = onResolve; 89 | this.rejectCallback_ = onReject; 90 | break; 91 | case PROMISE_STATE.FULLFILLED: 92 | onResolve(this.value_); 93 | break; 94 | case PROMISE_STATE.REJECTED: 95 | if (onReject) { 96 | onReject(this.reason_); 97 | } 98 | break; 99 | } 100 | return this; 101 | }; 102 | 103 | MyPromise.prototype.catch = function(onReject) { 104 | switch (this.state_) { 105 | case PROMISE_STATE.PENDING: 106 | this.rejectCallback_ = onReject; 107 | break; 108 | case PROMISE_STATE.FULLFILLED: 109 | break; 110 | case PROMISE_STATE.REJECTED: 111 | onReject(this.reason_); 112 | break; 113 | } 114 | return this; 115 | }; 116 | 117 | MyPromise.prototype.onResolve_ = function(value) { 118 | if (this.state_ !== PROMISE_STATE.PENDING) { 119 | return; 120 | } 121 | this.state_ = PROMISE_STATE.FULLFILLED; 122 | if (this.resolveCallback_) { 123 | this.resolveCallback_(value); 124 | } else { 125 | this.value_ = value; 126 | } 127 | }; 128 | 129 | MyPromise.prototype.onReject_ = function(reason) { 130 | if (this.state_ !== PROMISE_STATE.PENDING) { 131 | return; 132 | } 133 | this.state_ = PROMISE_STATE.REJECTED; 134 | if (this.rejectCallback_) { 135 | this.rejectCallback_(reason); 136 | } else { 137 | this.reason_ = reason; 138 | } 139 | }; 140 | 141 | window.Promise = window.Promise || MyPromise; 142 | 143 | // Provide a shim for phantomjs, where chrome is not defined. 144 | var myChrome = (function() { 145 | var onConnectCallback_; 146 | return { 147 | app: { 148 | runtime: { 149 | onLaunched: { 150 | addListener: function(callback) { 151 | console.log( 152 | 'chrome.app.runtime.onLaunched.addListener called:' + callback); 153 | } 154 | } 155 | }, 156 | window: { 157 | create: function(fileName, callback) { 158 | console.log( 159 | 'chrome.window.create called: ' + 160 | fileName + ', ' + callback); 161 | } 162 | } 163 | }, 164 | runtime: { 165 | onConnect: { 166 | addListener: function(callback) { 167 | console.log( 168 | 'chrome.runtime.onConnect.addListener called: ' + callback); 169 | onConnectCallback_ = callback; 170 | } 171 | } 172 | }, 173 | callOnConnect: function(port) { 174 | if (onConnectCallback_) { 175 | onConnectCallback_(port); 176 | } 177 | } 178 | }; 179 | })(); 180 | 181 | window.chrome = window.chrome || myChrome; 182 | -------------------------------------------------------------------------------- /src/web_app/js/utils_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals TestCase, filterTurnUrls, assertEquals, randomString, 12 | queryStringToDictionary */ 13 | 14 | 'use strict'; 15 | 16 | var TURN_URLS = [ 17 | 'turn:turn.example.com?transport=tcp', 18 | 'turn:turn.example.com?transport=udp', 19 | 'turn:turn.example.com:8888?transport=udp', 20 | 'turn:turn.example.com:8888?transport=tcp' 21 | ]; 22 | 23 | var TURN_URLS_UDP = [ 24 | 'turn:turn.example.com?transport=udp', 25 | 'turn:turn.example.com:8888?transport=udp', 26 | ]; 27 | 28 | var TURN_URLS_TCP = [ 29 | 'turn:turn.example.com?transport=tcp', 30 | 'turn:turn.example.com:8888?transport=tcp' 31 | ]; 32 | 33 | var UtilsTest = new TestCase('UtilsTest'); 34 | 35 | UtilsTest.prototype.testFilterTurnUrlsUdp = function() { 36 | var urls = TURN_URLS.slice(0); // make a copy 37 | filterTurnUrls(urls, 'udp'); 38 | assertEquals('Only transport=udp URLs should remain.', TURN_URLS_UDP, urls); 39 | }; 40 | 41 | UtilsTest.prototype.testFilterTurnUrlsTcp = function() { 42 | var urls = TURN_URLS.slice(0); // make a copy 43 | filterTurnUrls(urls, 'tcp'); 44 | assertEquals('Only transport=tcp URLs should remain.', TURN_URLS_TCP, urls); 45 | }; 46 | 47 | UtilsTest.prototype.testRandomReturnsCorrectLength = function() { 48 | assertEquals('13 length string', 13, randomString(13).length); 49 | assertEquals('5 length string', 5, randomString(5).length); 50 | assertEquals('10 length string', 10, randomString(10).length); 51 | }; 52 | 53 | UtilsTest.prototype.testRandomReturnsCorrectCharacters = function() { 54 | var str = randomString(500); 55 | 56 | // randromString should return only the digits 0-9. 57 | var positiveRe = /^[0-9]+$/; 58 | var negativeRe = /[^0-9]/; 59 | 60 | var positiveResult = positiveRe.exec(str); 61 | var negativeResult = negativeRe.exec(str); 62 | 63 | assertEquals( 64 | 'Number only regular expression should match.', 65 | 0, positiveResult.index); 66 | assertEquals( 67 | 'Anything other than digits regular expression should not match.', 68 | null, negativeResult); 69 | }; 70 | 71 | UtilsTest.prototype.testQueryStringToDictionary = function() { 72 | var dictionary = { 73 | 'foo': 'a', 74 | 'baz': '', 75 | 'bar': 'b', 76 | 'tee': '', 77 | }; 78 | 79 | var buildQuery = function(data, includeEqualsOnEmpty) { 80 | var queryString = '?'; 81 | for (var key in data) { 82 | queryString += key; 83 | if (data[key] || includeEqualsOnEmpty) { 84 | queryString += '='; 85 | } 86 | queryString += data[key] + '&'; 87 | } 88 | queryString = queryString.slice(0, -1); 89 | return queryString; 90 | }; 91 | 92 | // Build query where empty value is formatted as &tee=&. 93 | var query = buildQuery(dictionary, true); 94 | var result = queryStringToDictionary(query); 95 | assertEquals(JSON.stringify(dictionary), JSON.stringify(result)); 96 | 97 | // Build query where empty value is formatted as &tee&. 98 | query = buildQuery(dictionary, false); 99 | result = queryStringToDictionary(query); 100 | assertEquals(JSON.stringify(dictionary), JSON.stringify(result)); 101 | 102 | result = queryStringToDictionary('?'); 103 | assertEquals(0, Object.keys(result).length); 104 | 105 | result = queryStringToDictionary('?='); 106 | assertEquals(0, Object.keys(result).length); 107 | 108 | result = queryStringToDictionary('?&='); 109 | assertEquals(0, Object.keys(result).length); 110 | 111 | result = queryStringToDictionary(''); 112 | assertEquals(0, Object.keys(result).length); 113 | 114 | result = queryStringToDictionary('?=abc'); 115 | assertEquals(0, Object.keys(result).length); 116 | }; 117 | -------------------------------------------------------------------------------- /src/web_app/js/windowport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals trace, chrome */ 12 | /* exported apprtc, apprtc.windowPort */ 13 | 14 | 'use strict'; 15 | 16 | // This is used to communicate from the Chrome App window to background.js. 17 | // It opens a Port object to send and receive messages. When the Chrome 18 | // App window is closed, background.js receives notification and can 19 | // handle clean up tasks. 20 | var apprtc = apprtc || {}; 21 | apprtc.windowPort = apprtc.windowPort || {}; 22 | (function() { 23 | var port_; 24 | 25 | apprtc.windowPort.sendMessage = function(message) { 26 | var port = getPort_(); 27 | try { 28 | port.postMessage(message); 29 | } 30 | catch (ex) { 31 | trace('Error sending message via port: ' + ex); 32 | } 33 | }; 34 | 35 | apprtc.windowPort.addMessageListener = function(listener) { 36 | var port = getPort_(); 37 | port.onMessage.addListener(listener); 38 | }; 39 | 40 | var getPort_ = function() { 41 | if (!port_) { 42 | port_ = chrome.runtime.connect(); 43 | } 44 | return port_; 45 | }; 46 | })(); 47 | -------------------------------------------------------------------------------- /tools/turn-prober/README: -------------------------------------------------------------------------------- 1 | This script contains a simple prober that verifies that: 2 | - CEOD vends TURN server URIs with credentials on demand (mimicking apprtc) 3 | - rfc5766-turn-server vends TURN candidates from the servers vended by CEOD. 4 | 5 | To use simply run ./turn-prober.sh 6 | If it prints "PASS" (and exits 0) then all is well. 7 | If it prints a mess of logs (and exits non-0) then something has gone sideways 8 | and apprtc.appspot.com is probably not working well (b/c of missing TURN 9 | functionality). 10 | -------------------------------------------------------------------------------- /tools/turn-prober/turn-prober.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /tools/turn-prober/turn-prober.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | function pids_of() { 4 | local pids="" 5 | for p in `ps axuwww|grep $1|awk '{print $2}'`; do 6 | if [ -x /proc/$p/cwd ] && [ "$(realpath /proc/$p/cwd)" == "$D" ]; then 7 | pids="$pids $p" 8 | fi 9 | done 10 | echo $pids 11 | } 12 | 13 | function kill_all_of() { 14 | # Suppress bash's Killed message 15 | exec 3>&2 16 | exec 2>/dev/null 17 | while [ ! -z "$(pids_of $1)" ]; do 18 | kill $(pids_of $1) 19 | done 20 | exec 2>&3 21 | exec 3>&- 22 | } 23 | 24 | function cleanup() { 25 | kill_all_of c[h]rome 26 | kill_all_of X[v]fb 27 | 28 | rm -rf $D 29 | } 30 | trap cleanup EXIT 31 | 32 | cd $(dirname $0) 33 | WEBPAGE="file://${PWD}/turn-prober.html" 34 | export D=$(mktemp -d) 35 | cd $D 36 | 37 | CHROME_LOG_FILE="${D}/chrome_debug.log" 38 | touch $CHROME_LOG_FILE 39 | 40 | XVFB="xvfb-run -a -e $CHROME_LOG_FILE -f $D/xauth -s '-screen 0 1024x768x24'" 41 | if [ -n "$DISPLAY" ]; then 42 | XVFB="" 43 | fi 44 | 45 | # "eval" below is required by $XVFB containing a quoted argument. 46 | eval $XVFB google-chrome \ 47 | --enable-logging=stderr \ 48 | --no-first-run \ 49 | --disable-web-security \ 50 | --user-data-dir=$D \ 51 | --vmodule="*media/*=3,*turn*=3" \ 52 | $WEBPAGE > $CHROME_LOG_FILE 2>&1 & 53 | CHROME_PID=$! 54 | 55 | while ! grep -q DONE $CHROME_LOG_FILE && pids_of c[h]rome|grep -q .; do 56 | sleep 0.1 57 | done 58 | 59 | kill_all_of c[h]rome 60 | 61 | DONE=$(grep DONE $CHROME_LOG_FILE) 62 | EXIT_CODE=0 63 | if ! grep -q "DONE: PASS" $CHROME_LOG_FILE; then 64 | cat $CHROME_LOG_FILE 65 | EXIT_CODE=1 66 | fi 67 | 68 | exit $EXIT_CODE 69 | --------------------------------------------------------------------------------