├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── Gruntfile.js ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── build ├── .csslintrc ├── .eslintrc ├── build_app_engine_package.py ├── copy_js_files.py ├── copy_portable.py ├── ensure_gcloud_sdk_is_installed.py ├── gen_js_enums.py ├── remove_python_tests.py ├── run_python_tests.py ├── start-tests.sh └── test_file_herder.py ├── karma.conf.js ├── package-lock.json ├── package.json ├── requirements.txt ├── src ├── app_engine │ ├── .gitignore │ ├── analytics.py │ ├── analytics_enums.py │ ├── analytics_enums_test.py │ ├── analytics_page.py │ ├── analytics_page_test.py │ ├── analytics_test.py │ ├── apiauth.py │ ├── app.yaml │ ├── apprtc.py │ ├── apprtc_test.py │ ├── bigquery │ │ ├── README.md │ │ ├── analytics_schema.json │ │ └── enums.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 │ ├── css │ └── main.css │ ├── html │ ├── full_template.html │ ├── google1b7eb21c5b594ba0.html │ ├── help.html │ ├── index_template.html │ ├── params.html │ └── robots.txt │ ├── images │ ├── apprtc-128.png │ ├── apprtc-16.png │ ├── apprtc-22.png │ ├── apprtc-32.png │ ├── apprtc-48.png │ └── webrtc-icon-192x192.png │ └── js │ ├── README.md │ ├── analytics.js │ ├── analytics_test.js │ ├── appcontroller.js │ ├── appcontroller_test.js │ ├── call.js │ ├── call_test.js │ ├── constants.js │ ├── infobox.js │ ├── infobox_test.js │ ├── loopback.js │ ├── peerconnectionclient.js │ ├── peerconnectionclient_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 └── tools └── turn-prober ├── README ├── turn-prober.html └── turn-prober.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | /src/web_app/js/enums.js 4 | src/web_app/js/adapter.js 5 | bower_components 6 | browser* 7 | firefox* 8 | node_modules 9 | out 10 | secrets.json 11 | source-context*.json 12 | temp/* 13 | validation-report.json 14 | validation-status.json 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: python 3 | python: 2.7.13 4 | 5 | cache: 6 | directories: 7 | - node_modules 8 | - temp/google-cloud-sdk 9 | - browsers 10 | - $HOME/.cache/pip 11 | 12 | addons: 13 | apt: 14 | packages: 15 | - pulseaudio 16 | 17 | env: 18 | global: 19 | - DISPLAY=:99.0 20 | matrix: 21 | - BROWSER=chrome BVER=stable 22 | - BROWSER=chrome BVER=beta 23 | - BROWSER=chrome BVER=unstable 24 | - BROWSER=firefox BVER=stable 25 | 26 | matrix: 27 | fast_finish: true 28 | 29 | allow_failures: 30 | - env: BROWSER=chrome BVER=unstable 31 | 32 | before_install: 33 | - nvm install 8 34 | - nvm use 8 35 | 36 | before_script: 37 | - npm install 38 | - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/cucumber_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1600x1200x16 39 | - pulseaudio --start 40 | 41 | script: 42 | - ./node_modules/.bin/grunt --verbose 43 | 44 | notifications: 45 | email: 46 | recipients: 47 | forward-webrtc-github@webrtc.org 48 | on_success: change 49 | on_failure: always 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #######DEMO APP, DO NOT USE THIS FOR ANYTHING BUT TESTING PURPOSES, ITS NOT MEANT FOR PRODUCTION###### 2 | 3 | FROM golang:1.17.5-alpine3.15 4 | 5 | # Install and download deps. 6 | RUN apk add --no-cache git curl python2 build-base openssl-dev openssl 7 | RUN git clone https://github.com/webrtc/apprtc.git 8 | 9 | # AppRTC GAE setup 10 | 11 | # Required to run GAE dev_appserver.py. 12 | RUN curl https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-367.0.0-linux-x86_64.tar.gz --output gcloud.tar.gz \ 13 | && tar -xf gcloud.tar.gz \ 14 | && google-cloud-sdk/bin/gcloud components install app-engine-python-extras app-engine-python cloud-datastore-emulator --quiet \ 15 | && rm -f gcloud.tar.gz 16 | 17 | # Mimick build step by manually copying everything into the appropriate folder and run build script. 18 | RUN python apprtc/build/build_app_engine_package.py apprtc/src/ apprtc/out/ \ 19 | && curl https://webrtc.github.io/adapter/adapter-latest.js --output apprtc/src/web_app/js/adapter.js \ 20 | && cp apprtc/src/web_app/js/*.js apprtc/out/js/ 21 | 22 | # Wrap AppRTC GAE app in a bash script due to needing to run two apps within one container. 23 | RUN echo -e "#!/bin/sh\n" > /go/start.sh \ 24 | && echo -e "`pwd`/google-cloud-sdk/bin/dev_appserver.py --host 0.0.0.0 `pwd`/apprtc/out/app.yaml &\n" >> /go/start.sh 25 | 26 | # Collider setup 27 | # Go environment setup. 28 | RUN export GOPATH=$HOME/goWorkspace/ \ 29 | && go env -w GO111MODULE=off 30 | 31 | RUN ln -s `pwd`/apprtc/src/collider/collidermain $GOPATH/src \ 32 | && ln -s `pwd`/apprtc/src/collider/collidertest $GOPATH/src \ 33 | && ln -s `pwd`/apprtc/src/collider/collider $GOPATH/src \ 34 | && cd $GOPATH/src \ 35 | && go get collidermain \ 36 | && go install collidermain 37 | 38 | # Add Collider executable to the start.sh bash script. 39 | RUN echo -e "$GOPATH/bin/collidermain -port=8089 -tls=true -room-server=http://localhost &\n" >> /go/start.sh 40 | 41 | ENV STUNNEL_VERSION 5.60 42 | 43 | WORKDIR /usr/src 44 | RUN curl https://www.stunnel.org/archive/5.x/stunnel-${STUNNEL_VERSION}.tar.gz --output stunnel.tar.gz\ 45 | && tar -xf /usr/src/stunnel.tar.gz 46 | WORKDIR /usr/src/stunnel-${STUNNEL_VERSION} 47 | RUN ./configure --prefix=/usr && make && make install 48 | 49 | RUN mkdir /cert 50 | RUN openssl req -x509 -out /cert/cert.crt -keyout /cert/key.pem \ 51 | -newkey rsa:2048 -nodes -sha256 \ 52 | -subj '/CN=localhost' -extensions EXT -config <( \ 53 | printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") \ 54 | && cat /cert/key.pem > /cert/cert.pem \ 55 | && cat /cert/cert.crt >> /cert/cert.pem \ 56 | && chmod 600 /cert/cert.pem /cert/key.pem /cert/cert.crt 57 | 58 | RUN echo -e "foreground=yes\n" > /usr/etc/stunnel/stunnel.conf \ 59 | && echo -e "[AppRTC GAE]\n" >> /usr/etc/stunnel/stunnel.conf \ 60 | && echo -e "accept=0.0.0.0:443\n" >> /usr/etc/stunnel/stunnel.conf \ 61 | && echo -e "connect=0.0.0.0:8080\n" >> /usr/etc/stunnel/stunnel.conf \ 62 | && echo -e "cert=/cert/cert.pem\n" >> /usr/etc/stunnel/stunnel.conf 63 | 64 | RUN echo -e "/usr/bin/stunnel &\n" >> /go/start.sh \ 65 | && echo -e "wait -n\n" >> /go/start.sh \ 66 | && echo -e "exit $?\n" >> /go/start.sh \ 67 | && chmod +x /go/start.sh 68 | 69 | # Start the bash wrapper that keeps both collider and the AppRTC GAE app running. 70 | CMD /go/start.sh 71 | 72 | ## Instructions (Tested on Debian 11 only): 73 | # - Download the Dockerfile from the AppRTC repo and put it in a folder, e.g. 'apprtc' 74 | # - Build the Dockerfile into an image: 'sudo docker build apprtc/' 75 | # Note the image ID from the build command, e.g. something like 'Successfully built 503621f4f7bd'. 76 | # - Run: 'sudo docker run -p 443:443 -p 8089:8089 --rm -ti 503621f4f7bd' 77 | # The container will now run in interactive mode and output logging. If you do not want this, omit the '-ti' argument. 78 | # The '-p' options are port mappings to the GAE app and Collider instances, the host ones can be changed. 79 | # 80 | # - On the same machine that this docker image is running on you can now join apprtc calls using 81 | # https://localhost/?wshpp=localhost:8089&wstls=true, once you join the URL will have 82 | # appended the room name which you can share, e.g. 'http://localhost:8080/r/315402015?wshpp=localhost:8089&wstls=true'. 83 | # If you want to connect to this instance from another machine, use the IP address of the machine running this docker container 84 | # instead of localhost. 85 | # 86 | # Keep in mind that you need to pass in those 'wshpp' and 'wstls' URL parameters everytime you join with as they override 87 | # the websocket server address. 88 | # 89 | # The steps assume sudo is required for docker, that can be avoided but is out of scope. 90 | 91 | ## TODO 92 | # Verify if this docker container run on more OS's? 93 | 94 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* globals module */ 4 | var out_app_engine_dir = 'out/app_engine'; 5 | var app_engine_path = 'temp/google-cloud-sdk/platform/google_appengine' 6 | // Check if running on travis, if so do not install in User due to using 7 | // pythonEnv. 8 | var isTravis = ('TRAVIS' in process.env && 'CI' in process.env) ? 9 | '' : '--user'; 10 | 11 | module.exports = function(grunt) { 12 | require('load-grunt-tasks')(grunt); 13 | 14 | // configure project 15 | grunt.initConfig({ 16 | // make node configurations available 17 | pkg: grunt.file.readJSON('package.json'), 18 | 19 | csslint: { 20 | options: { 21 | csslintrc: 'build/.csslintrc' 22 | }, 23 | strict: { 24 | options: { 25 | import: 2 26 | }, 27 | src: ['src/**/*.css' 28 | ] 29 | }, 30 | lax: { 31 | options: { 32 | import: false 33 | }, 34 | src: ['src/**/*.css' 35 | ] 36 | } 37 | }, 38 | 39 | htmllint: { 40 | all: { 41 | src: [ 42 | 'src/**/*_template.html' 43 | ] 44 | } 45 | }, 46 | eslint: { 47 | options: { 48 | configFile: 'build/.eslintrc' 49 | }, 50 | target: ['src/**/*.js', '!src/**/enums.js', '!src/**/adapter.js' ] 51 | }, 52 | 53 | shell: { 54 | pipInstall : { 55 | command: ['pip install', isTravis, '--requirement requirements.txt'] 56 | .join(' ') 57 | }, 58 | ensureGcloudSDKIsInstalled: { 59 | command: 'python build/ensure_gcloud_sdk_is_installed.py' 60 | }, 61 | runPythonTests: { 62 | command: ['python', 'build/run_python_tests.py', 63 | app_engine_path, out_app_engine_dir].join(' ') 64 | }, 65 | buildAppEnginePackage: { 66 | command: ['python', './build/build_app_engine_package.py', 'src', 67 | out_app_engine_dir].join(' ') 68 | }, 69 | buildAppEnginePackageWithTests: { 70 | command: ['python', './build/build_app_engine_package.py', 'src', 71 | out_app_engine_dir, '--include-tests'].join(' ') 72 | }, 73 | removePythonTestsFromOutAppEngineDir: { 74 | command: ['python', './build/remove_python_tests.py', 75 | out_app_engine_dir].join(' ') 76 | }, 77 | genJsEnums: { 78 | command: ['python', './build/gen_js_enums.py', 'src', 79 | 'src/web_app/js'].join(' ') 80 | }, 81 | copyAdapter: { 82 | command: ['python', './build/copy_portable.py', 83 | 'node_modules/webrtc-adapter/out/adapter.js', 84 | 'src/web_app/js/adapter.js'].join(' ') 85 | }, 86 | copyJsFiles: { 87 | command: ['python', './build/copy_js_files.py', 88 | 'src/web_app/js', out_app_engine_dir + '/js'].join(' ') 89 | }, 90 | runUnitTests: { 91 | command: 'bash ./build/start-tests.sh' 92 | }, 93 | }, 94 | karma: { 95 | unit: { 96 | configFile: 'karma.conf.js' 97 | } 98 | } 99 | }); 100 | 101 | // Set default tasks to run when grunt is called without parameters. 102 | grunt.registerTask('default', ['runLinting', 'runPythonTests', 'build', 103 | 'runUnitTests']); 104 | grunt.registerTask('runLinting', ['csslint', 'eslint']); 105 | grunt.registerTask('runPythonTests', ['shell:pipInstall', 106 | 'shell:ensureGcloudSDKIsInstalled', 107 | 'shell:buildAppEnginePackageWithTests', 108 | 'shell:runPythonTests', 109 | 'shell:removePythonTestsFromOutAppEngineDir']); 110 | grunt.registerTask('runUnitTests', [ 111 | 'shell:genJsEnums', 'shell:copyAdapter', 'shell:runUnitTests']), 112 | grunt.registerTask('build', ['shell:buildAppEnginePackage', 113 | 'shell:genJsEnums', 114 | 'shell:copyAdapter', 115 | 'shell:copyJsFiles']); 116 | }; 117 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Browsers and versions affected** 2 | 3 | 4 | **Description** 5 | 6 | 7 | **Steps to reproduce** 8 | 9 | 10 | **Expected results** 11 | 12 | 13 | **Actual results** 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Description** 2 | 3 | 4 | **Purpose** 5 | -------------------------------------------------------------------------------- /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/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "array-bracket-spacing": 2, 4 | "block-spacing": [2, "never"], 5 | "brace-style": [2, "1tbs", { "allowSingleLine": false }], 6 | "camelcase": [2, {"properties": "always"}], 7 | "curly": 2, 8 | "default-case": 2, 9 | "dot-notation": 2, 10 | "eqeqeq": 2, 11 | "indent": [ 12 | 2, 13 | 2, 14 | { 15 | "SwitchCase": 1, 16 | "VariableDeclarator": 2, 17 | "MemberExpression": 2, 18 | "CallExpression": {"arguments": 2} 19 | } 20 | ], 21 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 22 | "max-len": [2, 80, 2, {"ignoreUrls": true}], 23 | "new-cap": 2, 24 | "no-console": 0, 25 | "no-else-return": 2, 26 | "no-eval": 2, 27 | "no-multi-spaces": 2, 28 | "no-multiple-empty-lines": [2, {"max": 2}], 29 | "no-shadow": 0, 30 | "no-trailing-spaces": 2, 31 | "no-unused-expressions": 2, 32 | "no-unused-vars": [2, {"args": "none"}], 33 | "object-curly-spacing": [2, "never"], 34 | "padded-blocks": [2, "never"], 35 | "quotes": [ 36 | 2, 37 | "single" 38 | ], 39 | "semi": [ 40 | 2, 41 | "always" 42 | ], 43 | "space-before-blocks": 2, 44 | "space-before-function-paren": [2, "never"], 45 | "spaced-comment": 2, 46 | "valid-typeof": 2 47 | }, 48 | "env": { 49 | "es6": true, 50 | "browser": true, 51 | "node": false 52 | }, 53 | "extends": ["eslint:recommended", "webrtc"], 54 | "globals": { 55 | "adapter": true, 56 | "AppController": true, 57 | "audioContext": true, 58 | "browserSupportsIPHandlingPolicy": true, 59 | "browserSupportsNonProxiedUdpBoolean": true, 60 | "Call": true, 61 | "chrome": true, 62 | "ga": true, 63 | "getPolicyFromBooleans": true, 64 | "remoteVideo": true, 65 | "trace": true, 66 | "queryStringToDictionary": true 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /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 | #import requests 13 | 14 | import test_file_herder 15 | 16 | USAGE = """%prog src_path dest_path 17 | Build the GAE source code package. 18 | 19 | src_path Path to the source code root directory. 20 | dest_path Path to the root directory to push/deploy GAE from.""" 21 | 22 | 23 | def call_cmd_and_return_output_lines(cmd): 24 | try: 25 | process = subprocess.Popen(cmd, stdout=subprocess.PIPE) 26 | output = process.communicate()[0] 27 | return output.split('\n') 28 | except OSError as e: 29 | print str(e) 30 | return [] 31 | 32 | 33 | def build_version_info_file(dest_path): 34 | """Build the version info JSON file.""" 35 | version_info = { 36 | 'gitHash': None, 37 | 'time': None, 38 | 'branch': None 39 | } 40 | 41 | lines = call_cmd_and_return_output_lines(['git', 'log', '-1']) 42 | for line in lines: 43 | if line.startswith('commit'): 44 | version_info['gitHash'] = line.partition(' ')[2].strip() 45 | elif line.startswith('Date'): 46 | version_info['time'] = line.partition(':')[2].strip() 47 | if version_info['gitHash'] is not None and version_info['time'] is not None: 48 | break 49 | 50 | lines = call_cmd_and_return_output_lines(['git', 'branch']) 51 | for line in lines: 52 | if line.startswith('*'): 53 | version_info['branch'] = line.partition(' ')[2].strip() 54 | break 55 | 56 | try: 57 | with open(dest_path, 'w') as f: 58 | f.write(json.dumps(version_info)) 59 | except IOError as e: 60 | print str(e) 61 | 62 | 63 | # Copy pako zlib from node_modules to third_party/pako 64 | def copyPako(dest_path): 65 | dest_js_path = os.path.join(dest_path, 'third_party', 'pako') 66 | os.makedirs(dest_js_path) 67 | shutil.copy('node_modules/pako/dist/pako.min.js', dest_js_path) 68 | 69 | 70 | def CopyApprtcSource(src_path, dest_path): 71 | if os.path.exists(dest_path): 72 | shutil.rmtree(dest_path) 73 | os.makedirs(dest_path) 74 | 75 | simply_copy_subdirs = ['bigquery', 'css', 'images', 'third_party'] 76 | 77 | for dirpath, unused_dirnames, files in os.walk(src_path): 78 | for subdir in simply_copy_subdirs: 79 | if dirpath.endswith(subdir): 80 | shutil.copytree(dirpath, os.path.join(dest_path, subdir)) 81 | 82 | if dirpath.endswith('html'): 83 | dest_html_path = os.path.join(dest_path, 'html') 84 | os.makedirs(dest_html_path) 85 | for name in files: 86 | # Template files must be in the root directory. 87 | if name.endswith('_template.html'): 88 | shutil.copy(os.path.join(dirpath, name), dest_path) 89 | else: 90 | shutil.copy(os.path.join(dirpath, name), dest_html_path) 91 | elif dirpath.endswith('app_engine'): 92 | for name in files: 93 | if (name.endswith('.py') and 'test' not in name 94 | or name.endswith('.yaml')): 95 | shutil.copy(os.path.join(dirpath, name), dest_path) 96 | elif dirpath.endswith('js'): 97 | for name in files: 98 | # loopback.js is not compiled by Closure 99 | # and need to be copied separately. 100 | if name in ['loopback.js']: 101 | dest_js_path = os.path.join(dest_path, 'js') 102 | if not os.path.exists(dest_js_path): 103 | os.makedirs(dest_js_path) 104 | shutil.copy(os.path.join(dirpath, name), dest_js_path) 105 | 106 | build_version_info_file(os.path.join(dest_path, 'version_info.json')) 107 | 108 | 109 | def main(): 110 | parser = optparse.OptionParser(USAGE) 111 | parser.add_option("-t", "--include-tests", action="store_true", 112 | help='Also copy python tests to the out dir.') 113 | options, args = parser.parse_args() 114 | if len(args) != 2: 115 | parser.error('Error: Exactly 2 arguments required.') 116 | 117 | src_path, dest_path = args[0:2] 118 | CopyApprtcSource(src_path, dest_path) 119 | #copyPako(dest_path) 120 | if options.include_tests: 121 | app_engine_code = os.path.join(src_path, 'app_engine') 122 | test_file_herder.CopyTests(os.path.join(src_path, 'app_engine'), dest_path) 123 | 124 | 125 | if __name__ == '__main__': 126 | sys.exit(main()) 127 | -------------------------------------------------------------------------------- /build/copy_js_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import optparse 4 | import os 5 | import shutil 6 | import sys 7 | 8 | def main(): 9 | parser = optparse.OptionParser("copy.py ") 10 | _, args = parser.parse_args() 11 | if len(args) != 2: 12 | parser.error('Error: Exactly 2 arguments required.') 13 | src, dest = args 14 | for fl in os.listdir(src): 15 | if fl.endswith('.js') and not 'test' in fl: 16 | shutil.copy(os.path.join(src, fl), os.path.join(dest, fl)) 17 | 18 | 19 | if __name__ == '__main__': 20 | sys.exit(main()) 21 | -------------------------------------------------------------------------------- /build/copy_portable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import optparse 4 | import shutil 5 | import sys 6 | 7 | 8 | def main(): 9 | parser = optparse.OptionParser("copy.py ") 10 | _, args = parser.parse_args() 11 | if len(args) != 2: 12 | parser.error('Error: Exactly 2 arguments required.') 13 | 14 | shutil.copy(args[0], args[1]) 15 | 16 | 17 | if __name__ == '__main__': 18 | sys.exit(main()) 19 | -------------------------------------------------------------------------------- /build/ensure_gcloud_sdk_is_installed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import subprocess 5 | import sys 6 | import tarfile 7 | import urllib2 8 | 9 | # We are downloading a specific version of the Gcloud SDK because we have not 10 | # found a URL to fetch the "latest" version. 11 | # The installation updates the SDK so there is no need to update the downloaded 12 | # version too often. 13 | # If it is needed to update the downloaded version please refer to: 14 | # https://cloud.google.com/sdk/downloads#versioned 15 | 16 | GCLOUD_DOWNLOAD_URL = 'https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/' 17 | GCLOUD_SDK_TAR_FILE = 'google-cloud-sdk-308.0.0-linux-x86_64.tar.gz' 18 | GCLOUD_SDK_INSTALL_FOLDER = 'google-cloud-sdk' 19 | TEMP_DIR = 'temp' 20 | GCLOUD_SDK_PATH = os.path.join(TEMP_DIR, GCLOUD_SDK_INSTALL_FOLDER) 21 | 22 | def _Download(url, to): 23 | print 'Downloading %s to %s...' % (url, to) 24 | request = urllib2.urlopen(url) 25 | try: 26 | f = urllib2.urlopen(request) 27 | with open(to, 'wb') as to_file: 28 | to_file.write(f.read()) 29 | except urllib2.HTTPError, r: 30 | print('Could not download: %s Error: %d. %s' % ( 31 | to, r.code, r.reason)) 32 | raise 33 | 34 | 35 | def _Extract(file_to_extract_path, destination_path): 36 | print 'Extracting %s in %s...' % (file_to_extract_path, destination_path) 37 | with tarfile.open(file_to_extract_path, 'r:gz') as tar_file: 38 | tar_file.extractall(destination_path) 39 | 40 | 41 | def _EnsureAppEngineIsInstalled(path_to_gcloud_sdk): 42 | gcloud_exec = os.path.join(path_to_gcloud_sdk, 'bin', 'gcloud') 43 | subprocess.call([gcloud_exec, '--quiet', 44 | 'components', 'install', 'app-engine-python']) 45 | subprocess.call([gcloud_exec, '--quiet', 46 | 'components', 'update']) 47 | 48 | 49 | def _Cleanup(file_paths_to_remove): 50 | for file_path in file_paths_to_remove: 51 | if os.path.exists(file_path): 52 | print 'Cleaning up %s' % file_path 53 | os.remove(file_path) 54 | 55 | 56 | def main(): 57 | if not os.path.exists(TEMP_DIR): 58 | os.mkdir(TEMP_DIR) 59 | 60 | if os.path.isfile(os.path.join(GCLOUD_SDK_PATH, 'bin', 'gcloud')): 61 | print 'Already has %s, skipping the download' % GCLOUD_SDK_INSTALL_FOLDER 62 | _EnsureAppEngineIsInstalled(GCLOUD_SDK_PATH) 63 | _Cleanup([os.path.join(TEMP_DIR, GCLOUD_SDK_TAR_FILE)]) 64 | return 65 | 66 | _Download(GCLOUD_DOWNLOAD_URL + GCLOUD_SDK_TAR_FILE, 67 | os.path.join(TEMP_DIR, GCLOUD_SDK_TAR_FILE)) 68 | _Extract(os.path.join(TEMP_DIR, GCLOUD_SDK_TAR_FILE), TEMP_DIR) 69 | _EnsureAppEngineIsInstalled(GCLOUD_SDK_PATH) 70 | _Cleanup([os.path.join(TEMP_DIR, GCLOUD_SDK_TAR_FILE)]) 71 | 72 | 73 | if __name__ == "__main__": 74 | sys.exit(main()) 75 | -------------------------------------------------------------------------------- /build/gen_js_enums.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2015 Google Inc. All Rights Reserved. 4 | 5 | import inspect 6 | import json 7 | import optparse 8 | import os 9 | import sys 10 | 11 | USAGE = """%prog src_path dst_path 12 | Generate analytics enums for use in Javascript. 13 | 14 | src_path Path to the source code root directory. 15 | dst_path Path to store the 'enums.js' file.""" 16 | 17 | 18 | def main(): 19 | parser = optparse.OptionParser(USAGE) 20 | _, args = parser.parse_args() 21 | 22 | if len(args) != 2: 23 | parser.error('Error: 2 arguments required.') 24 | 25 | src_path, dst_path = args[0:2] 26 | json_path = os.path.join(src_path, 'app_engine', 'bigquery') 27 | 28 | 29 | print src_path, '>>>', dst_path 30 | outfile = os.path.join(dst_path, 'enums.js') 31 | with open(outfile, 'w') as fp: 32 | fp.write("/* file generated by gen_js_enums.py */\n") 33 | fp.write("'use strict';\n") 34 | fp.write("\n") 35 | 36 | fp.write("var enums = ") 37 | fp.write(json.dumps( 38 | json.load(open(os.path.join(json_path, 'enums.json'))), 39 | indent=2)) 40 | fp.write(";\n") 41 | 42 | 43 | if __name__ == '__main__': 44 | main() 45 | -------------------------------------------------------------------------------- /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 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 | 14 | 15 | def _WebTestIsInstalled(): 16 | try: 17 | import webtest 18 | return True 19 | except ImportError: 20 | print 'You need to install webtest dependencies before you can proceed ' 21 | print 'running the tests. To do this you need to have pip installed.' 22 | print 'Go to https://packaging.python.org/installing/ and follow the ' 23 | print 'instructions and then rerun the grunt command.' 24 | return False 25 | 26 | 27 | def main(sdk_path, test_path): 28 | if not os.path.exists(sdk_path): 29 | return 'Missing %s: try grunt shell:getPythonTestDeps.' % sdk_path 30 | if not os.path.exists(test_path): 31 | return 'Missing %s: try grunt build.' % test_path 32 | 33 | sys.path.insert(0, sdk_path) 34 | import dev_appserver 35 | dev_appserver.fix_sys_path() 36 | if not _WebTestIsInstalled(): 37 | return 1 38 | suite = unittest.loader.TestLoader().discover(test_path, 39 | pattern="*test.py") 40 | ok = unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful() 41 | return 0 if ok else 1 42 | 43 | 44 | if __name__ == '__main__': 45 | parser = optparse.OptionParser(USAGE) 46 | options, args = parser.parse_args() 47 | if len(args) != 2: 48 | parser.error('Error: Exactly 2 arguments required.') 49 | 50 | sdk_path, test_path = args[0:2] 51 | sys.exit(main(sdk_path, test_path)) 52 | -------------------------------------------------------------------------------- /build/start-tests.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2016 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 | #!/bin/sh 8 | 9 | # Run with a default set of parameters 10 | BINDIR=./browsers/bin 11 | export BROWSER=${BROWSER-chrome} 12 | export BVER=${BVER-stable} 13 | BROWSERBIN=$BINDIR/$BROWSER-$BVER 14 | OS="`uname`" 15 | if [ ! -x $BROWSERBIN ]; then 16 | # Travis-multirunner only supports Linux (partial mac support exists) 17 | if [[ "$OSTYPE" =~ ^linux ]]; then 18 | echo "Installing browser" 19 | ./node_modules/travis-multirunner/setup.sh 20 | fi 21 | fi 22 | echo "Start unit tests using Karma and $BROWSER browser" 23 | BROWSER_UPPER=$(echo "$BROWSER" | tr '[:lower:]' '[:upper:]') 24 | # Only use travis-multirunner downloaded browsers on Linux. 25 | if [[ "$OSTYPE" =~ ^linux ]]; then 26 | export ${BROWSER_UPPER}_BIN="$BROWSERBIN" 27 | fi 28 | 29 | ./node_modules/karma-cli/bin/karma start karma.conf.js 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | module.exports = function(config) { 3 | var browser = (process.env.BROWSER === '' || process.env.BROWSER === 'chrome') 4 | ? 'Chrome_no_sandbox' : process.env.BROWSER 5 | var travis = process.env.TRAVIS; 6 | var files = function() { 7 | // List of tests that can run in 'any' browser. 8 | var filteredFiles = [ 9 | // Application files. 10 | 'src/web_app/js/adapter.js', 11 | 'src/web_app/js/constants.js', 12 | 'src/web_app/js/util.js', 13 | 'src/web_app/js/sdputils.js', 14 | 'src/web_app/js/enums.js', 15 | 'src/web_app/js/analytics.js', 16 | 'src/web_app/js/appcontroller.js', 17 | 'src/web_app/js/appwindow.js', 18 | 'src/web_app/js/call.js', 19 | 'src/web_app/js/infobox.js', 20 | 'src/web_app/js/loopback.js', 21 | 'src/web_app/js/peerconnectionclient.js', 22 | 'src/web_app/js/roomselection.js', 23 | 'src/web_app/js/signalingchannel.js', 24 | 'src/web_app/js/stats.js', 25 | 'src/web_app/js/storage.js', 26 | 'src/web_app/js/windowport.js', 27 | // Test files. 28 | 'src/web_app/js/testpolyfills.js', 29 | 'src/web_app/js/test_mocks.js', 30 | 'src/web_app/js/appcontroller_test.js', 31 | 'src/web_app/js/analytics_test.js', 32 | 'src/web_app/js/call_test.js', 33 | 'src/web_app/js/infobox_test.js', 34 | 'src/web_app/js/peerconnectionclient_test.js', 35 | 'src/web_app/js/remotewebsocket_test.js', 36 | 'src/web_app/js/roomselection_test.js', 37 | 'src/web_app/js/sdputils_test.js', 38 | 'src/web_app/js/signalingchannel_test.js', 39 | 'src/web_app/js/utils_test.js' 40 | ]; 41 | return filteredFiles; 42 | } 43 | 44 | let chromeFlags = [ 45 | '--use-fake-device-for-media-stream', 46 | '--use-fake-ui-for-media-stream', 47 | '--no-sandbox', 48 | '--headless', '--disable-gpu', '--remote-debugging-port=9222' 49 | ]; 50 | config.set({ 51 | 52 | // base path that will be used to resolve all patterns (eg. files, exclude) 53 | basePath: '', 54 | 55 | // frameworks to use 56 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 57 | frameworks: ['jasmine'], 58 | 59 | // list of files / patterns to load in the browser 60 | // Make sure to have them in the correct order. 61 | files: files(), 62 | 63 | // list of files to exclude 64 | exclude: [ 65 | ], 66 | 67 | // preprocess matching files before serving them to the browser 68 | // available preprocessors: 69 | // https://npmjs.org/browse/keyword/karma-preprocessor 70 | preprocessors: { 71 | // Enable this with the autoWatch feature below if you want eslint to run 72 | // on each file change. 73 | // 'src/web_app/js/*.js': ['eslint'] 74 | }, 75 | 76 | eslint: { 77 | stopOnError: false, 78 | stopOnWarning: false, 79 | showWarnings: true, 80 | engine: { 81 | configFile: 'build/.eslintrc' 82 | } 83 | }, 84 | 85 | // Capture browser JavaScript console log output. 86 | client: { 87 | // Enable console capture on travis only. 88 | captureConsole: (travis) ? true : false 89 | }, 90 | 91 | // test results reporter to use 92 | // possible values: 'dots', 'progress' 93 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 94 | reporters: ['progress'], 95 | 96 | // web server port 97 | port: 9876, 98 | 99 | // enable / disable colors in the output (reporters and logs) 100 | colors: true, 101 | 102 | // level of logging 103 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || 104 | // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 105 | // Enable full logging on travis only. 106 | logLevel: (travis) ? config.LOG_DEBUG : config.LOG_DISABLE, 107 | 108 | // enable / disable watching file and executing tests whenever any file 109 | // changes 110 | autoWatch: false, 111 | 112 | // start these browsers 113 | // available browser launchers: 114 | // https://npmjs.org/browse/keyword/karma-launcher 115 | browsers: [browser[0].toUpperCase() + browser.substr(1)], 116 | 117 | customLaunchers: { 118 | Chrome_no_sandbox: { 119 | base: 'Chrome', 120 | flags: chromeFlags 121 | } 122 | }, 123 | 124 | // Continuous Integration mode 125 | // if true, Karma captures browsers, runs the tests and exits 126 | singleRun: true, 127 | 128 | // Concurrency level 129 | // how many browser should be started simultaneous 130 | concurrency: 1 131 | }) 132 | } 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apprtc", 3 | "version": "1.0.0", 4 | "description": "Project checking for AppRTC repo", 5 | "license": "BSD-3-Clause", 6 | "main": "Gruntfile.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/webrtc/apprtc.git" 10 | }, 11 | "scripts": { 12 | "test": "grunt" 13 | }, 14 | "devDependencies": { 15 | "eslint-config-webrtc": ">=1.0.0", 16 | "grunt": "^1.5.3", 17 | "grunt-cli": "^1.3.2", 18 | "grunt-contrib-compress": "^1.6.0", 19 | "grunt-contrib-csslint": "^2.0.0", 20 | "grunt-eslint": "^22.0.0", 21 | "grunt-html": "^12.1.0", 22 | "grunt-jinja-new-grunt": ">=0.5.0", 23 | "grunt-shell": "^3.0.1", 24 | "jasmine-core": "^3.5.0", 25 | "karma": "^6.3.16", 26 | "karma-chrome-launcher": "^3.1.0", 27 | "karma-cli": "^2.0.0", 28 | "karma-eslint": ">=2.2.0", 29 | "karma-firefox-launcher": "^1.3.0", 30 | "karma-jasmine": "^3.1.0", 31 | "load-grunt-tasks": ">=5.1.0", 32 | "travis-multirunner": "^4.6.0", 33 | "webrtc-adapter": "^7.7.0" 34 | }, 35 | "dependencies": { 36 | "pako": "^1.0.10" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | webtest 3 | -------------------------------------------------------------------------------- /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 logging 7 | import os 8 | import sys 9 | import time 10 | 11 | sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party')) 12 | 13 | from analytics_enums import EventType, LogField, ClientType 14 | import apiauth 15 | import constants 16 | 17 | from google.appengine.api import app_identity 18 | 19 | 20 | class Analytics(object): 21 | """Class used to encapsulate analytics logic. Used interally in the module. 22 | 23 | All data is streamed to BigQuery. 24 | 25 | """ 26 | 27 | def __init__(self): 28 | self.bigquery_table = constants.BIGQUERY_TABLE 29 | 30 | if constants.IS_DEV_SERVER: 31 | self.bigquery_dataset = constants.BIGQUERY_DATASET_LOCAL 32 | else: 33 | self.bigquery_dataset = constants.BIGQUERY_DATASET_PROD 34 | 35 | # Attempt to initialize a connection to BigQuery. 36 | self.bigquery = self._build_bigquery_object() 37 | if self.bigquery is None: 38 | logging.warning('Unable to build BigQuery API object. Logging disabled.') 39 | 40 | def _build_bigquery_object(self): 41 | return apiauth.build(scope=constants.BIGQUERY_URL, 42 | service_name='bigquery', 43 | version='v2') 44 | 45 | def _timestamp_from_millis(self, time_ms): 46 | """Convert back to seconds as float and then to ISO format.""" 47 | return datetime.datetime.fromtimestamp(float(time_ms)/1000.).isoformat() 48 | 49 | def report_event(self, event_type, room_id=None, time_ms=None, 50 | client_time_ms=None, host=None, flow_id=None, 51 | client_type=None): 52 | """Report an event to BigQuery. 53 | 54 | Args: 55 | event_type: Event to report. One of analytics.EventType. 56 | room_id: Room ID related to the given event type. 57 | time_ms: Time that the event occurred on the server. Will be automatically 58 | populated if not given explicitly. 59 | client_time_ms: Time that an event occurred on the client, if the event 60 | originated on the client. 61 | host: Hostname this is being logged on. 62 | flow_id: ID to group a set of events together. 63 | client_type: Type of client logging the event. 64 | One of analytics.ClientType. 65 | """ 66 | # Be forgiving. If an event is a string or is an unknown number we 67 | # still log it but log it as the string value. 68 | event_type_name = EventType.Name.get(event_type, str(event_type)) 69 | event = {LogField.EVENT_TYPE: event_type_name} 70 | 71 | if room_id is not None: 72 | event[LogField.ROOM_ID] = room_id 73 | 74 | if flow_id is not None: 75 | event[LogField.FLOW_ID] = flow_id 76 | 77 | if client_type is not None: 78 | client_type_name = ClientType.Name.get(client_type, str(client_type)) 79 | event[LogField.CLIENT_TYPE] = client_type_name 80 | 81 | if client_time_ms is not None: 82 | event[LogField.CLIENT_TIMESTAMP] = self._timestamp_from_millis( 83 | client_time_ms) 84 | 85 | if host is not None: 86 | event[LogField.HOST] = host 87 | 88 | if time_ms is None: 89 | time_ms = time.time() * 1000. 90 | 91 | event[LogField.TIMESTAMP] = self._timestamp_from_millis(time_ms) 92 | 93 | obj = {'rows': [{'json': event}]} 94 | 95 | logging.info('Event: %s', obj) 96 | if self.bigquery is not None: 97 | response = self.bigquery.tabledata().insertAll( 98 | projectId=app_identity.get_application_id(), 99 | datasetId=self.bigquery_dataset, 100 | tableId=self.bigquery_table, 101 | body=obj).execute() 102 | logging.info('BigQuery response: %s', response) 103 | 104 | 105 | analytics = None 106 | 107 | 108 | def report_event(*args, **kwargs): 109 | """Used by other modules to actually do logging. 110 | 111 | A passthrough to a global Analytics instance intialized on use. 112 | 113 | Args: 114 | *args: passed directly to Analytics.report_event. 115 | **kwargs: passed directly to Analytics.report_event. 116 | """ 117 | global analytics 118 | 119 | # Initialization is delayed until the first use so that our 120 | # environment is ready and available. This is a problem with unit 121 | # tests since the testbed needs to initialized before creating an 122 | # Analytics instance. 123 | if analytics is None: 124 | analytics = Analytics() 125 | 126 | analytics.report_event(*args, **kwargs) 127 | -------------------------------------------------------------------------------- /src/app_engine/analytics_enums.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All Rights Reserved. 2 | 3 | """Module for all the analytics enums. 4 | 5 | Analytics enums are separated out to this module so that they can be 6 | loaded from build scripts. Loading the analytics or analytics_page 7 | module requires appengine and Google API python libraries. This module 8 | requires only python standard library modules. 9 | 10 | """ 11 | 12 | import json 13 | import os 14 | 15 | 16 | class EnumClass(object): 17 | """Type for loading enums from a JSON file. 18 | 19 | Builds an object instance from a dictionary where keys are 20 | attributes and sub-dictionaries are additional instances of this 21 | class. It is intended to be used for building objects to hold enums. 22 | 23 | A dictionary of the form, 24 | 25 | { 26 | 'ENUM1': 5, 27 | 'SubEnum': { 28 | 'SUBENUM1': 10 29 | } 30 | } 31 | 32 | will be loaded as an instance where, 33 | 34 | instance.ENUM1 == 5 35 | instance.SubEnum.SUBENUM1 == 10 36 | """ 37 | 38 | def __init__(self, enum_dict): 39 | reverse = {} 40 | for key, val in enum_dict.iteritems(): 41 | if isinstance(val, dict): 42 | # Make a new class and populate its values. 43 | setattr(self, key, EnumClass(val)) 44 | else: 45 | setattr(self, key, val) 46 | reverse[val] = key 47 | 48 | setattr(self, 'Name', reverse) 49 | 50 | 51 | ENUMS = json.load(open(os.path.join(os.path.dirname(__file__), 52 | 'bigquery', 53 | 'enums.json'))) 54 | EventType = EnumClass(ENUMS['EventType']) 55 | RequestField = EnumClass(ENUMS['RequestField']) 56 | ClientType = EnumClass(ENUMS['ClientType']) 57 | 58 | 59 | class BigquerySchemaClass(object): 60 | """Metaclass for loading the bigquery schema from JSON.""" 61 | 62 | def __init__(self, schema_dict): 63 | for field in schema_dict: 64 | setattr(self, field['name'].upper(), field['name']) 65 | 66 | LogField = BigquerySchemaClass( 67 | json.load(open(os.path.join(os.path.dirname(__file__), 68 | 'bigquery', 69 | 'analytics_schema.json')))) 70 | -------------------------------------------------------------------------------- /src/app_engine/analytics_enums_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | 3 | import unittest 4 | 5 | from analytics_enums import BigquerySchemaClass 6 | from analytics_enums import EnumClass 7 | 8 | 9 | class AnalyticsEnumsTest(unittest.TestCase): 10 | """Test the EnumClass behaves as expected.""" 11 | 12 | def testEnumClass(self): 13 | value_dict = { 14 | 'FOO': 10, 15 | 'BAR': 42, 16 | 'BAZ': 'test', 17 | 'SubEnum': { 18 | 'BIM': 'bang', 19 | 'BEN': 'boom', 20 | }} 21 | 22 | my_enum = EnumClass(value_dict) 23 | 24 | self.assertEqual(value_dict['FOO'], my_enum.FOO) 25 | self.assertEqual(value_dict['BAR'], my_enum.BAR) 26 | self.assertEqual(value_dict['BAZ'], my_enum.BAZ) 27 | self.assertEqual(value_dict['SubEnum']['BIM'], my_enum.SubEnum.BIM) 28 | self.assertEqual(value_dict['SubEnum']['BEN'], my_enum.SubEnum.BEN) 29 | self.assertTrue(isinstance(my_enum.SubEnum, EnumClass)) 30 | 31 | def testBigquerySchemaClass(self): 32 | field1 = 'field1' 33 | field2 = 'field2' 34 | schema_dict = [ 35 | { 36 | 'name': 'field1', 37 | 'type': 'string', 38 | 'mode': 'nullable' 39 | }, 40 | { 41 | 'name': 'field2', 42 | 'type': 'timestamp', 43 | 'mode': 'nullable' 44 | }, 45 | ] 46 | 47 | my_enum = BigquerySchemaClass(schema_dict) 48 | 49 | self.assertEqual(field1, my_enum.FIELD1) 50 | self.assertEqual(field2, my_enum.FIELD2) 51 | -------------------------------------------------------------------------------- /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 analytics 9 | from analytics_enums import RequestField 10 | import constants 11 | import webapp2 12 | 13 | 14 | class AnalyticsPage(webapp2.RequestHandler): 15 | """Client Analytics data handler. 16 | 17 | Each POST body to the AnalyticsPage is a JSON object of the form, 18 | { 19 | 'request_time_ms': , 20 | 'type': 21 | 'event': { 22 | 'event_type': , 23 | 'event_time_ms': , 24 | 'room_id': , 25 | } 26 | } 27 | 28 | 'request_time_ms': Required field set by the client to indicate when 29 | the request was send by the client. 30 | 31 | 'type': Required field describing the type of request. In the case 32 | of the 'event' type the 'event' field contains data 33 | pertinent to the request. However, the request type may 34 | correspond to one or more fields. 35 | 36 | 'event': Data relevant to an 'event' request. 37 | 38 | In order to handle client clock skew, the time an event 39 | occurred (event_time_ms) is adjusted based on the 40 | difference between the client clock and the server 41 | clock. The difference between the client clock and server 42 | clock is calculated as the difference between 43 | 'request_time_ms' provide by the client and the time at 44 | which the server processes the request. This ignores the 45 | latency of opening a connection and sending the body of the 46 | message to the server. 47 | 48 | To avoid problems with daylight savings the client should 49 | report 'event_time_ms' and 'request_time_ms' as UTC. The 50 | report will be recorded using local server time. 51 | 52 | """ 53 | 54 | def _write_response(self, result): 55 | self.response.write(json.dumps({ 56 | 'result': result 57 | })) 58 | 59 | def _time(self): 60 | """Overridden in unit tests to validate time calculations.""" 61 | return time.time() 62 | 63 | def post(self): 64 | try: 65 | msg = json.loads(self.request.body) 66 | except ValueError: 67 | return self._write_response(constants.RESPONSE_INVALID_REQUEST) 68 | 69 | response = constants.RESPONSE_INVALID_REQUEST 70 | 71 | # Verify required fields. 72 | request_type = msg.get(RequestField.TYPE) 73 | request_time_ms = msg.get(RequestField.REQUEST_TIME_MS) 74 | if request_time_ms is None or request_type is None: 75 | self._write_response(constants.RESPONSE_INVALID_REQUEST) 76 | return 77 | 78 | # Handle specific event types. 79 | if (request_type == RequestField.MessageType.EVENT and 80 | msg.get(RequestField.EVENT) is not None): 81 | response = self._handle_event(msg) 82 | 83 | self._write_response(response) 84 | return 85 | 86 | def _handle_event(self, msg): 87 | request_time_ms = msg.get(RequestField.REQUEST_TIME_MS) 88 | client_type = msg.get(RequestField.CLIENT_TYPE) 89 | 90 | event = msg.get(RequestField.EVENT) 91 | if event is None: 92 | return constants.RESPONSE_INVALID_REQUEST 93 | 94 | event_type = event.get(RequestField.EventField.EVENT_TYPE) 95 | if event_type is None: 96 | return constants.RESPONSE_INVALID_REQUEST 97 | 98 | room_id = event.get(RequestField.EventField.ROOM_ID) 99 | flow_id = event.get(RequestField.EventField.FLOW_ID) 100 | 101 | # Time that the event occurred according to the client clock. 102 | try: 103 | client_event_time_ms = float(event.get( 104 | RequestField.EventField.EVENT_TIME_MS)) 105 | except (TypeError, ValueError): 106 | return constants.RESPONSE_INVALID_REQUEST 107 | 108 | # Time the request was sent based on the client clock. 109 | try: 110 | request_time_ms = float(request_time_ms) 111 | except (TypeError, ValueError): 112 | return constants.RESPONSE_INVALID_REQUEST 113 | 114 | # Server time at the time of request. 115 | receive_time_ms = self._time() * 1000. 116 | 117 | # Calculate event time as client event time adjusted to server 118 | # local time. Server clock offset is gived by the difference 119 | # between the time the client sent thes request and the time the 120 | # server received the request. This method ignores the latency of 121 | # sending the request to the server. 122 | event_time_ms = client_event_time_ms + (receive_time_ms - request_time_ms) 123 | 124 | analytics.report_event(event_type=event_type, 125 | room_id=room_id, 126 | time_ms=event_time_ms, 127 | client_time_ms=client_event_time_ms, 128 | host=self.request.host, 129 | flow_id=flow_id, client_type=client_type) 130 | 131 | return constants.RESPONSE_SUCCESS 132 | -------------------------------------------------------------------------------- /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 | runtime: python27 2 | threadsafe: true 3 | api_version: 1 4 | 5 | handlers: 6 | - url: /(.*\.html) 7 | static_files: html/\1 8 | upload: html/(.*\.html) 9 | 10 | - url: /robots.txt 11 | static_files: html/robot.txt 12 | upload: html/robots.txt 13 | 14 | - url: /pako 15 | static_dir: third_party/pako 16 | 17 | - url: /images 18 | static_dir: images 19 | 20 | - url: /js 21 | static_dir: js 22 | 23 | - url: /css 24 | static_dir: css 25 | 26 | - url: /compute/.* 27 | script: apprtc.app 28 | login: admin 29 | 30 | - url: /probe.* 31 | script: probers.app 32 | secure: always 33 | 34 | - url: /.* 35 | script: apprtc.app 36 | secure: always 37 | 38 | libraries: 39 | - name: jinja2 40 | version: latest 41 | - name: ssl 42 | version: latest 43 | - name: pycrypto 44 | version: latest 45 | 46 | env_variables: 47 | BYPASS_JOIN_CONFIRMATION: false 48 | # Only change these while developing, do not commit to source! 49 | # Use appcfg.py --env_variable=ICE_SERVER_API_KEY:KEY \ 50 | # in order to replace variables when deploying. 51 | ICE_SERVER_API_KEY: "" 52 | # Comma-separated list of ICE urls to return when no ice server 53 | # is specified. 54 | ICE_SERVER_URLS: "" 55 | # A message that is always displayed on the app page. 56 | # This is useful for cases like indicating to the user that this 57 | # is a demo deployment of the app. 58 | HEADER_MESSAGE: "" 59 | -------------------------------------------------------------------------------- /src/app_engine/bigquery/README.md: -------------------------------------------------------------------------------- 1 | # Bigquery / Analytics related data. 2 | 3 | * `analytics_schema.json` defines the Bigquery table structure. 4 | 5 | * `enums.json` contains enums used by AppRTC analytics. The 6 | `EventType` field defines possible enum that are stored in the 7 | `event_type` field of the Bigquery table. The `RequestField` 8 | defines fields for the Analytics request to the AppRTC server. 9 | -------------------------------------------------------------------------------- /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 | "name": "flow_id", 29 | "type": "integer", 30 | "mode": "nullable" 31 | }, 32 | { 33 | "name": "client_type", 34 | "type": "string", 35 | "mode": "nullable" 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /src/app_engine/bigquery/enums.json: -------------------------------------------------------------------------------- 1 | { 2 | "EventType": { 3 | "ROOM_SIZE_2": 2, 4 | "ICE_CONNECTION_STATE_CONNECTED": 3 5 | }, 6 | 7 | "RequestField": { 8 | "TYPE": "type", 9 | "REQUEST_TIME_MS": "request_time_ms", 10 | "EVENT": "event", 11 | "CLIENT_TYPE": "client_type", 12 | 13 | "MessageType": { 14 | "EVENT": "event" 15 | }, 16 | 17 | "EventField": { 18 | "EVENT_TYPE": "event_type", 19 | "ROOM_ID": "room_id", 20 | "EVENT_TIME_MS": "event_time_ms", 21 | "FLOW_ID": "flow_id" 22 | } 23 | }, 24 | 25 | "ClientType": { 26 | "UNKNOWN": 0, 27 | "JS": 1, 28 | "DESKTOP": 2, 29 | "IOS": 3, 30 | "ANDROID": 4 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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 | # Deprecated domains which we should to redirect to REDIRECT_URL. 10 | REDIRECT_DOMAINS = [ 11 | 'apprtc.appspot.com', 'apprtc.webrtc.org', 'www.appr.tc' 12 | ] 13 | # URL which we should redirect to if matching in REDIRECT_DOMAINS. 14 | REDIRECT_URL = 'https://appr.tc' 15 | 16 | ROOM_MEMCACHE_EXPIRATION_SEC = 60 * 60 * 24 17 | MEMCACHE_RETRY_LIMIT = 100 18 | 19 | LOOPBACK_CLIENT_ID = 'LOOPBACK_CLIENT_ID' 20 | 21 | # Turn/Stun server override. This allows AppRTC to connect to turn servers 22 | # directly rather than retrieving them from an ICE server provider. 23 | ICE_SERVER_OVERRIDE = None 24 | # Enable by uncomment below and comment out above, then specify turn and stun 25 | # ICE_SERVER_OVERRIDE = [ 26 | # { 27 | # "urls": [ 28 | # "turn:hostname/IpToTurnServer:19305?transport=udp", 29 | # "turn:hostname/IpToTurnServer:19305?transport=tcp" 30 | # ], 31 | # "username": "TurnServerUsername", 32 | # "credential": "TurnServerCredentials" 33 | # }, 34 | # { 35 | # "urls": [ 36 | # "stun:hostname/IpToStunServer:19302" 37 | # ] 38 | # } 39 | # ] 40 | 41 | ICE_SERVER_BASE_URL = 'https://appr.tc' 42 | ICE_SERVER_URL_TEMPLATE = '%s/v1alpha/iceconfig?key=%s' 43 | ICE_SERVER_API_KEY = os.environ.get('ICE_SERVER_API_KEY') 44 | HEADER_MESSAGE = os.environ.get('HEADER_MESSAGE') 45 | ICE_SERVER_URLS = [url for url in os.environ.get('ICE_SERVER_URLS', '').split(',') if url] 46 | 47 | # Dictionary keys in the collider instance info constant. 48 | WSS_INSTANCE_HOST_KEY = 'host_port_pair' 49 | WSS_INSTANCE_NAME_KEY = 'vm_name' 50 | WSS_INSTANCE_ZONE_KEY = 'zone' 51 | WSS_INSTANCES = [{ 52 | WSS_INSTANCE_HOST_KEY: 'apprtc-ws.webrtc.org:443', 53 | WSS_INSTANCE_NAME_KEY: 'wsserver-std', 54 | WSS_INSTANCE_ZONE_KEY: 'us-central1-a' 55 | }, { 56 | WSS_INSTANCE_HOST_KEY: 'apprtc-ws-2.webrtc.org:443', 57 | WSS_INSTANCE_NAME_KEY: 'wsserver-std-2', 58 | WSS_INSTANCE_ZONE_KEY: 'us-central1-f' 59 | }] 60 | 61 | WSS_HOST_PORT_PAIRS = [ins[WSS_INSTANCE_HOST_KEY] for ins in WSS_INSTANCES] 62 | 63 | # memcache key for the active collider host. 64 | WSS_HOST_ACTIVE_HOST_KEY = 'wss_host_active_host' 65 | 66 | # Dictionary keys in the collider probing result. 67 | WSS_HOST_IS_UP_KEY = 'is_up' 68 | WSS_HOST_STATUS_CODE_KEY = 'status_code' 69 | WSS_HOST_ERROR_MESSAGE_KEY = 'error_message' 70 | 71 | RESPONSE_ERROR = 'ERROR' 72 | RESPONSE_ROOM_FULL = 'FULL' 73 | RESPONSE_UNKNOWN_ROOM = 'UNKNOWN_ROOM' 74 | RESPONSE_UNKNOWN_CLIENT = 'UNKNOWN_CLIENT' 75 | RESPONSE_DUPLICATE_CLIENT = 'DUPLICATE_CLIENT' 76 | RESPONSE_SUCCESS = 'SUCCESS' 77 | RESPONSE_INVALID_REQUEST = 'INVALID_REQUEST' 78 | 79 | IS_DEV_SERVER = os.environ.get('APPLICATION_ID', '').startswith('dev') 80 | 81 | BIGQUERY_URL = 'https://www.googleapis.com/auth/bigquery' 82 | 83 | # Dataset used in production. 84 | BIGQUERY_DATASET_PROD = 'prod' 85 | 86 | # Dataset used when running locally. 87 | BIGQUERY_DATASET_LOCAL = 'dev' 88 | 89 | # BigQuery table within the dataset. 90 | BIGQUERY_TABLE = 'analytics' 91 | -------------------------------------------------------------------------------- /src/app_engine/cron.yaml: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: collider probing job on 5 min interval 3 | url: /probe/collider 4 | schedule: every 5 minutes from 00:02 to 23:59 5 | -------------------------------------------------------------------------------- /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 `apprtc` repository 10 | 11 | git clone https://github.com/webrtc/apprtc.git 12 | 13 | 3. Make sure to set the $GOPATH according to the Go instructions in step 1 14 | 15 | E.g. `export GOPATH=$HOME/goWorkspace/` 16 | `mkdir $GOPATH/src` 17 | 18 | 4. Link the collider directories into `$GOPATH/src` 19 | 20 | ln -s `pwd`/apprtc/src/collider/collider $GOPATH/src 21 | ln -s `pwd`/apprtc/src/collider/collidermain $GOPATH/src 22 | ln -s `pwd`/apprtc/src/collider/collidertest $GOPATH/src 23 | 24 | 5. Install dependencies 25 | 26 | go get collidermain 27 | 28 | 6. Install `collidermain` 29 | 30 | go install collidermain 31 | 32 | ## Running 33 | 34 | $GOPATH/bin/collidermain -port=8089 -tls=true 35 | 36 | ## Testing 37 | 38 | go test collider 39 | 40 | ## Deployment 41 | These instructions assume you are using Debian 7/8 and Go 1.6.3. 42 | 43 | 1. Change [roomSrv](https://github.com/webrtc/apprtc/blob/master/src/collider/collidermain/main.go#L16) to your AppRTC server instance e.g. 44 | 45 | ```go 46 | var roomSrv = flag.String("room-server", "https://your.apprtc.server", "The origin of the room server") 47 | ``` 48 | 49 | 2. Then repeat step 6 in the Building section. 50 | 51 | ### Install Collider 52 | 1. Login on the machine that is going to run Collider. 53 | 2. Create a Collider directory, this guide assumes it's created in the root (`/collider`). 54 | 3. Create a certificate directory, this guide assumes it's created in the root (`/cert`). 55 | 4. Copy `$GOPATH/bin/collidermain ` from your development machine to the `/collider` directory on your Collider machine. 56 | 57 | ### Certificates 58 | If you are deploying this in production, you should use certificates so that you can use secure websockets. Place the `cert.pem` and `key.pem` files in `/cert/`. E.g. `/cert/cert.pem` and `/cert/key.pem` 59 | 60 | ### Auto restart 61 | 1\. Add a `/collider/start.sh` file: 62 | 63 | ```bash 64 | #!/bin/sh - 65 | /collider/collidermain 2>> /collider/collider.log 66 | ``` 67 | 68 | 2\. Make it executable by running `chmod 744 start.sh`. 69 | 70 | #### If using inittab otherwise jump to step 5: 71 | 72 | 3\. Add the following line to `/etc/inittab` to allow automatic restart of the Collider process (make sure to either add `coll` as an user or replace it below with the user that should run collider): 73 | ```bash 74 | coll:2:respawn:/collider/start.sh 75 | ``` 76 | 4\. Run `init q` to apply the inittab change without rebooting. 77 | 78 | #### If using systemd: 79 | 80 | 5\. Create a service by doing `sudo nano /lib/systemd/system/collider.service` and adding the following: 81 | 82 | ``` 83 | [Unit] 84 | Description=AppRTC signalling server (Collider) 85 | 86 | [Service] 87 | ExecStart=/collider/start.sh 88 | StandardOutput=null 89 | 90 | [Install] 91 | WantedBy=multi-user.target 92 | Alias=collider.service 93 | ``` 94 | 6\. Enable the service: `sudo systemctl enable collider.service` 95 | 96 | 7\. Verify it's up and running: `sudo systemctl status collider.service` 97 | 98 | 99 | #### Rotating Logs 100 | To enable rotation of the `/collider/collider.log` file add the following contents to the `/etc/logrotate.d/collider` file: 101 | 102 | ``` 103 | /collider/collider.log { 104 | daily 105 | compress 106 | copytruncate 107 | dateext 108 | missingok 109 | notifempty 110 | rotate 10 111 | sharedscripts 112 | } 113 | ``` 114 | 115 | The log is rotated daily and removed after 10 days. Archived logs are in `/collider`. 116 | -------------------------------------------------------------------------------- /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://appr.tc", "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/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/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://appr.tc/?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 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 |
hd=trueUse HD camera resolution constraints, i.e. minWidth: 1280, minHeight: 720
stereo=true&audio=echoCancellation=falseTurn on stereo audio
debug=loopbackConnect to yourself, e.g. to test firewalls
ts=[turnserver]Set TURN server different from the default
apikey=[apikey]Turn server API key
audio=true&video=falseAudio only
audio=falseVideo only
audio=echoCancellation=falseDisable all audio processing
audio=googEchoCancellation=falseDisable echo cancellation
audio=googAutoGainControl=falseDisable gain control
audio=googNoiseSuppression=falseDisable noise suppression
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
vsc=VP8Set preferred video send codec to VP8
vrc=H264Set preferred video receive codec to H264
dscp=trueEnable DSCP
ipv6=trueEnable IPv6
arbr=[bitrate]Set audio receive bitrate, kbps
asbr=[bitrate]Set audio send bitrate
vrbr=[bitrate]Set video receive bitrate
vsbr=[bitrate]Set video send bitrate
videofec=falseTurn off video FEC
opusfec=falseTurn off Opus FEC
opusdtx=trueTurn on Opus DTX
opusmaxpbr=8000Set the maximum sample rate that the receiver can operate, for optimal Opus encoding performance
94 | 95 |
96 | 97 | github.com/webrtc/apprtc 98 | 99 |
100 | 101 | 102 | -------------------------------------------------------------------------------- /src/web_app/html/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: /$ 3 | Disallow: / 4 | -------------------------------------------------------------------------------- /src/web_app/images/apprtc-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrtc/apprtc/82157d6a04a8b0bcee6e3d7d1f3fccbaa2cdb524/src/web_app/images/apprtc-128.png -------------------------------------------------------------------------------- /src/web_app/images/apprtc-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrtc/apprtc/82157d6a04a8b0bcee6e3d7d1f3fccbaa2cdb524/src/web_app/images/apprtc-16.png -------------------------------------------------------------------------------- /src/web_app/images/apprtc-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrtc/apprtc/82157d6a04a8b0bcee6e3d7d1f3fccbaa2cdb524/src/web_app/images/apprtc-22.png -------------------------------------------------------------------------------- /src/web_app/images/apprtc-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrtc/apprtc/82157d6a04a8b0bcee6e3d7d1f3fccbaa2cdb524/src/web_app/images/apprtc-32.png -------------------------------------------------------------------------------- /src/web_app/images/apprtc-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrtc/apprtc/82157d6a04a8b0bcee6e3d7d1f3fccbaa2cdb524/src/web_app/images/apprtc-48.png -------------------------------------------------------------------------------- /src/web_app/images/webrtc-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrtc/apprtc/82157d6a04a8b0bcee6e3d7d1f3fccbaa2cdb524/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/analytics.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 that 5 | * can be found in the LICENSE file in the root of the source tree. 6 | */ 7 | 8 | /* globals sendAsyncUrlRequest, enums, JSON */ 9 | 10 | 'use strict'; 11 | 12 | /* 13 | * The analytics object is used to send up client-side logging 14 | * information. 15 | * @param {string} URL to the room server. 16 | * @constructor 17 | */ 18 | var Analytics = function(roomServer) { 19 | /* @private {String} Room server URL. */ 20 | this.analyticsPath_ = roomServer + '/a/'; 21 | }; 22 | 23 | // Disable check here due to jscs not recognizing the types below. 24 | /* jscs: disable */ 25 | /** 26 | * Defines a type for our event objects. 27 | * @typedef { 28 | * enums.RequestField.EventField.EVENT_TYPE: enums.EventType, 29 | * enums.RequestField.EventField.ROOM_ID: ?string, 30 | * enums.RequestField.EventField.FLOWN_ID: ?number, 31 | * enums.RequestField.EventField.EVENT_TIME_MS: number 32 | * } 33 | * @private 34 | */ 35 | /* jscs: enable */ 36 | Analytics.EventObject_ = {}; 37 | 38 | /** 39 | * Report an event. 40 | * 41 | * @param {enums.EventType} eventType The event string to record. 42 | * @param {String=} roomId The current room ID. 43 | * @param {Number=} flowId The current room ID. 44 | */ 45 | Analytics.prototype.reportEvent = function(eventType, roomId, flowId) { 46 | var eventObj = {}; 47 | eventObj[enums.RequestField.EventField.EVENT_TYPE] = eventType; 48 | eventObj[enums.RequestField.EventField.EVENT_TIME_MS] = Date.now(); 49 | 50 | if (roomId) { 51 | eventObj[enums.RequestField.EventField.ROOM_ID] = roomId; 52 | } 53 | if (flowId) { 54 | eventObj[enums.RequestField.EventField.FLOW_ID] = flowId; 55 | } 56 | this.sendEventRequest_(eventObj); 57 | }; 58 | 59 | /** 60 | * Send an event object to the server. 61 | * 62 | * @param {Analytics.EventObject_} eventObj Event object to send. 63 | * @private 64 | */ 65 | Analytics.prototype.sendEventRequest_ = function(eventObj) { 66 | var request = {}; 67 | request[enums.RequestField.TYPE] = enums.RequestField.MessageType.EVENT; 68 | request[enums.RequestField.REQUEST_TIME_MS] = Date.now(); 69 | request[enums.RequestField.EVENT] = eventObj; 70 | 71 | sendAsyncUrlRequest('POST', this.analyticsPath_, 72 | JSON.stringify(request)) 73 | .then(function() {}.bind(this), function(error) { 74 | trace('Failed to send event request: ' + error.message); 75 | }.bind(this)); 76 | }; 77 | -------------------------------------------------------------------------------- /src/web_app/js/analytics_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 Analytics, describe, expect, it, beforeEach, afterEach, 12 | sendAsyncUrlRequest:true, Mock, enums */ 13 | 14 | 'use strict'; 15 | 16 | describe('AnalyticsTest', function() { 17 | var url; 18 | var analytics; 19 | var eventTime; 20 | var eventType; 21 | var realDateNow; 22 | var realSendAsyncRequest; 23 | 24 | beforeEach(function() { 25 | url = 'https://test.org'; 26 | analytics = new Analytics(url); 27 | eventType = enums.EventType.ROOM_SIZE_2; 28 | eventTime = 1234; 29 | realDateNow = Date.now; 30 | // Mock global calls. 31 | realSendAsyncRequest = sendAsyncUrlRequest; 32 | sendAsyncUrlRequest = Mock.createSendAsyncUrlRequestMock(); 33 | Date.now = function() { 34 | return eventTime; 35 | }; 36 | }); 37 | 38 | afterEach(function() { 39 | sendAsyncUrlRequest = realSendAsyncRequest; 40 | Date.now = realDateNow; 41 | }); 42 | 43 | it('Report with all fields', function() { 44 | var roomId = 'my awesome room'; 45 | var flowId = 24; 46 | // Test reportEvent with all optional arguments. 47 | analytics.reportEvent(eventType, roomId, flowId); 48 | 49 | // Verify xhr request. 50 | expect(sendAsyncUrlRequest.calls().length).toEqual(1); 51 | var call = sendAsyncUrlRequest.calls()[0]; 52 | expect(call.method).toEqual('POST'); 53 | expect(call.url.indexOf(url) === 0).toBeTruthy(); 54 | 55 | var actualRequest = JSON.parse(call.body); 56 | expect(actualRequest[enums.RequestField.TYPE]) 57 | .toEqual(enums.RequestField.MessageType.EVENT); 58 | expect(actualRequest[enums.RequestField.REQUEST_TIME_MS]) 59 | .toEqual(eventTime); 60 | 61 | var actualEvent = actualRequest[enums.RequestField.EVENT]; 62 | expect(actualEvent[enums.RequestField.EventField.EVENT_TYPE]) 63 | .toEqual(eventType); 64 | expect(actualEvent[enums.RequestField.EventField.EVENT_TIME_MS]) 65 | .toEqual(eventTime); 66 | expect(actualEvent[enums.RequestField.EventField.ROOM_ID]).toEqual(roomId); 67 | expect(actualEvent[enums.RequestField.EventField.FLOW_ID]).toEqual(flowId); 68 | }); 69 | 70 | it('Report without any optional fields', function() { 71 | // Test reportEvent with all optional arguments. 72 | analytics.reportEvent(eventType); 73 | 74 | // Verify xhr request. 75 | expect(sendAsyncUrlRequest.calls().length).toEqual(1); 76 | var call = sendAsyncUrlRequest.calls()[0]; 77 | expect(call.method).toEqual('POST'); 78 | expect(call.url.indexOf(url) === 0).toBeTruthy(); 79 | 80 | var actualRequest = JSON.parse(call.body); 81 | expect(actualRequest[enums.RequestField.TYPE]) 82 | .toEqual(enums.RequestField.MessageType.EVENT); 83 | expect(actualRequest[enums.RequestField.REQUEST_TIME_MS]) 84 | .toEqual(eventTime); 85 | 86 | var actualEvent = actualRequest[enums.RequestField.EVENT]; 87 | expect(actualEvent[enums.RequestField.EventField.EVENT_TYPE]) 88 | .toEqual(eventType); 89 | expect(actualEvent[enums.RequestField.EventField.EVENT_TIME_MS]) 90 | .toEqual(eventTime); 91 | expect(actualEvent[enums.RequestField.EventField.ROOM_ID]) 92 | .toEqual(undefined); 93 | expect(actualEvent[enums.RequestField.EventField.FLOW_ID]) 94 | .toEqual(undefined); 95 | }); 96 | }); 97 | 98 | -------------------------------------------------------------------------------- /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, describe, expect, it, beforeEach, afterEach, 12 | UI_CONSTANTS, $, RoomSelection:true, Call:true */ 13 | 14 | 'use strict'; 15 | 16 | describe('AppControllerTest', function() { 17 | var MockCall; 18 | var MockRoomSelection; 19 | var roomSelectionBackup_; 20 | var callBackup_; 21 | var loadingParams_; 22 | var mainElem; 23 | 24 | MockRoomSelection = function() {}; 25 | MockRoomSelection.RecentlyUsedList = function() { 26 | return { 27 | pushRecentRoom: function() {} 28 | }; 29 | }; 30 | MockRoomSelection.matchRandomRoomPattern = function() { 31 | return false; 32 | }; 33 | 34 | MockCall = function() {}; 35 | MockCall.prototype.start = function() {}; 36 | MockCall.prototype.hangup = function() {}; 37 | 38 | beforeEach(function(done) { 39 | roomSelectionBackup_ = RoomSelection; 40 | RoomSelection = MockRoomSelection; 41 | callBackup_ = Call; 42 | Call = MockCall; 43 | loadingParams_ = { 44 | mediaConstraints: { 45 | audio: true, video: true 46 | } 47 | }; 48 | 49 | // Insert mock DOM elements. 50 | mainElem = document.createElement('div'); 51 | document.body.insertBefore(mainElem, document.body.firstChild); 52 | for (var key in UI_CONSTANTS) { 53 | var elem; 54 | if (key.toLowerCase().includes('button')) { 55 | elem = document.createElement('button'); 56 | } else { 57 | elem = document.createElement('div'); 58 | } 59 | elem.id = UI_CONSTANTS[key].substr(1); 60 | mainElem.appendChild(elem); 61 | } 62 | 63 | loadingParams_.roomId = 'myRoom'; 64 | new AppController(loadingParams_); 65 | 66 | // Needed due to a race where the textContent node is not updated in the div 67 | // before testing it. 68 | $(UI_CONSTANTS.confirmJoinRoomSpan) 69 | .addEventListener('DOMSubtreeModified', function() { 70 | done(); 71 | }); 72 | }); 73 | 74 | afterEach(function() { 75 | RoomSelection = roomSelectionBackup_; 76 | Call = callBackup_; 77 | }); 78 | 79 | it('Confirm to join', function() { 80 | // Verifies that the confirm-to-join UI is visible and the text matches the 81 | // room. 82 | expect($(UI_CONSTANTS.confirmJoinRoomSpan).textContent) 83 | .toEqual(' "' + loadingParams_.roomId + '"'); 84 | expect($(UI_CONSTANTS.confirmJoinDiv).classList.contains('hidden')) 85 | .toBeFalsy(); 86 | }); 87 | 88 | it('Hide UI after clicking the join button', function(done) { 89 | // Verifies that the UI is hidden after clicking the button. 90 | // There seems to be a delay for the beforeEach() to update the DOM tree, 91 | // need to wait a few seconds before clicking the button as it calls upon 92 | // a method that adds a 'hidden' class to an element which we then try to 93 | // find. 94 | setTimeout(function() { 95 | $(UI_CONSTANTS.confirmJoinButton).addEventListener('click', function() { 96 | expect($(UI_CONSTANTS.confirmJoinDiv).classList.contains('hidden')) 97 | .toBeTruthy(); 98 | done(); 99 | }); 100 | $(UI_CONSTANTS.confirmJoinButton).click(); 101 | }, 2000); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /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 describe, Call, expect, it, FAKE_ICE_SERVER, beforeEach, afterEach, 12 | SignalingChannel:true, MockWindowPort, FAKE_WSS_POST_URL, FAKE_ROOM_ID, 13 | FAKE_CLIENT_ID, apprtc, Constants, xhrs, MockXMLHttpRequest, 14 | XMLHttpRequest:true */ 15 | 16 | 'use strict'; 17 | 18 | describe('Call test', function() { 19 | var FAKE_LEAVE_URL = '/leave/' + FAKE_ROOM_ID + '/' + FAKE_CLIENT_ID; 20 | var MEDIA_STREAM_OBJECT = {value: 'stream'}; 21 | var realXMLHttpRequest = XMLHttpRequest; 22 | 23 | var mockSignalingChannels = []; 24 | 25 | var MockSignalingChannel = function() { 26 | this.isOpen = null; 27 | this.sends = []; 28 | mockSignalingChannels.push(this); 29 | }; 30 | 31 | MockSignalingChannel.prototype.open = function() { 32 | this.isOpen = true; 33 | return Promise.resolve(); 34 | }; 35 | 36 | MockSignalingChannel.prototype.getWssPostUrl = function() { 37 | return FAKE_WSS_POST_URL; 38 | }; 39 | 40 | MockSignalingChannel.prototype.send = function(data) { 41 | this.sends.push(data); 42 | }; 43 | 44 | MockSignalingChannel.prototype.close = function() { 45 | this.isOpen = false; 46 | }; 47 | 48 | function mockRequestUserMedia() { 49 | return new Promise(function(resolve) { 50 | resolve(MEDIA_STREAM_OBJECT); 51 | }); 52 | } 53 | 54 | beforeEach(function() { 55 | mockSignalingChannels = []; 56 | this.signalingChannelBackup_ = SignalingChannel; 57 | SignalingChannel = MockSignalingChannel; 58 | this.requestUserMediaBackup_ = 59 | navigator.mediaDevices.getUserMedia; 60 | navigator.mediaDevices.getUserMedia = mockRequestUserMedia; 61 | 62 | this.params_ = { 63 | mediaConstraints: { 64 | audio: true, video: true 65 | }, 66 | roomId: FAKE_ROOM_ID, 67 | clientId: FAKE_CLIENT_ID, 68 | peerConnectionConfig: {iceServers: FAKE_ICE_SERVER} 69 | }; 70 | 71 | XMLHttpRequest = MockXMLHttpRequest; 72 | }); 73 | 74 | afterEach(function() { 75 | SignalingChannel = this.signalingChannelBackup_; 76 | navigator.mediaDevices.getUserMedia = this.requestUserMediaBackup_; 77 | // Removes the xhrs queue. 78 | XMLHttpRequest.cleanQueue(); 79 | XMLHttpRequest = realXMLHttpRequest; 80 | }); 81 | 82 | it('Restart initializes media', function(done) { 83 | var call = new Call(this.params_); 84 | call.onlocalstreamadded = function(stream) { 85 | expect(stream).toEqual(MEDIA_STREAM_OBJECT); 86 | done(); 87 | }; 88 | call.restart(); 89 | }); 90 | 91 | it('hangup sync', function() { 92 | var call = new Call(this.params_); 93 | var stopCalled = false; 94 | var closeCalled = false; 95 | call.localStream_ = { 96 | stop: function() { 97 | stopCalled = true; 98 | } 99 | }; 100 | call.pcClient_ = { 101 | close: function() { 102 | closeCalled = true; 103 | } 104 | }; 105 | 106 | expect(xhrs.length).toEqual(0); 107 | expect(mockSignalingChannels[0].sends.length).toEqual(0); 108 | expect(mockSignalingChannels[0].isOpen).toBeNull(); 109 | // var realXMLHttpRequest = XMLHttpRequest; 110 | // XMLHttpRequest = MockXMLHttpRequest; 111 | 112 | call.hangup(false); 113 | // XMLHttpRequest = realXMLHttpRequest; 114 | 115 | expect(stopCalled).toBeTruthy(); 116 | expect(closeCalled).toBeTruthy(); 117 | // Send /leave. 118 | expect(xhrs.length).toEqual(1); 119 | expect(xhrs[0].url).toEqual(FAKE_LEAVE_URL); 120 | expect(xhrs[0].method).toEqual('POST'); 121 | 122 | expect(mockSignalingChannels.length).toEqual(1); 123 | // Send 'bye' to ws. 124 | expect(mockSignalingChannels[0].sends.length).toEqual(1); 125 | expect(mockSignalingChannels[0].sends[0]) 126 | .toEqual(JSON.stringify({type: 'bye'})); 127 | 128 | // Close ws. 129 | expect(mockSignalingChannels[0].isOpen).toBeFalsy(); 130 | 131 | // Clean up params state. 132 | expect(call.params_.roomId).toBeNull(); 133 | expect(call.params_.clientId).toBeNull(); 134 | expect(call.params_.previousRoomId).toEqual(FAKE_ROOM_ID); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /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 describe, it, expect, InfoBox */ 12 | 13 | 'use strict'; 14 | 15 | describe('Infobox test', function() { 16 | it('Format bitrate', function() { 17 | expect(InfoBox.formatBitrate_(789)).toEqual('789 bps'); 18 | expect(InfoBox.formatBitrate_(78912)).toEqual('78.9 kbps'); 19 | expect(InfoBox.formatBitrate_(7891234)).toEqual('7.89 Mbps'); 20 | }); 21 | 22 | it('Format interval', function() { 23 | expect(InfoBox.formatInterval_(1999)).toEqual('00:01'); 24 | expect(InfoBox.formatInterval_(12500)).toEqual('00:12'); 25 | expect(InfoBox.formatInterval_(83123)).toEqual('01:23'); 26 | expect(InfoBox.formatInterval_(754000)).toEqual('12:34'); 27 | expect(InfoBox.formatInterval_(5025000)).toEqual('01:23:45'); 28 | expect(InfoBox.formatInterval_(45296000)).toEqual('12:34:56'); 29 | expect(InfoBox.formatInterval_(445543000)).toEqual('123:45:43'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /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 | loopbackWebSocket.close(); 25 | } 26 | trace('Setting up loopback WebSocket.'); 27 | // TODO(tkchin): merge duplicate code once SignalingChannel abstraction 28 | // exists. 29 | loopbackWebSocket = new WebSocket(wssUrl); 30 | 31 | var sendLoopbackMessage = function(message) { 32 | var msgString = JSON.stringify({ 33 | cmd: 'send', 34 | msg: JSON.stringify(message) 35 | }); 36 | loopbackWebSocket.send(msgString); 37 | }; 38 | 39 | loopbackWebSocket.onopen = function() { 40 | trace('Loopback WebSocket opened.'); 41 | var registerMessage = { 42 | cmd: 'register', 43 | roomid: roomId, 44 | clientid: LOOPBACK_CLIENT_ID 45 | }; 46 | loopbackWebSocket.send(JSON.stringify(registerMessage)); 47 | }; 48 | 49 | loopbackWebSocket.onmessage = function(event) { 50 | var wssMessage; 51 | var message; 52 | try { 53 | wssMessage = JSON.parse(event.data); 54 | message = JSON.parse(wssMessage.msg); 55 | } catch (e) { 56 | trace('Error parsing JSON: ' + event.data); 57 | return; 58 | } 59 | if (wssMessage.error) { 60 | trace('WSS error: ' + wssMessage.error); 61 | return; 62 | } 63 | if (message.type === 'offer') { 64 | message.type = 'answer'; 65 | message.sdp = message.sdp 66 | .replace('a=ice-options:google-ice\r\n', '') 67 | // As of Chrome M51, an additional crypto method has been added when 68 | // using SDES. This works in a P2P due to the negotiation phase removes 69 | // this line but for loopback where we reuse the offer, that is skipped 70 | // and remains in the answer and breaks the call. 71 | // https://bugs.chromium.org/p/chromium/issues/detail?id=616263 72 | // https://bugs.chromium.org/p/chromium/issues/detail?id=1077740 73 | .replace(/a=crypto:[1-9]+ .*\r\n/g, ''); 74 | sendLoopbackMessage(message); 75 | } else if (message.type === 'candidate') { 76 | sendLoopbackMessage(message); 77 | } 78 | }; 79 | 80 | loopbackWebSocket.onclose = function(event) { 81 | trace('Loopback WebSocket closed with code:' + event.code + ' reason:' + 82 | event.reason); 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /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 describe, expect, it, maybePreferCodec, removeCodecParam, 12 | setCodecParam */ 13 | 14 | 'use strict'; 15 | 16 | describe('Sdp utils test', function() { 17 | var SDP_WITH_AUDIO_CODECS = [ 18 | 'v=0', 19 | 'm=audio 9 RTP/SAVPF 111 103 104 0 9', 20 | 'a=rtcp-mux', 21 | 'a=rtpmap:111 opus/48000/2', 22 | 'a=fmtp:111 minptime=10', 23 | 'a=rtpmap:103 ISAC/16000', 24 | 'a=rtpmap:9 G722/8000', 25 | 'a=rtpmap:0 PCMU/8000', 26 | 'a=rtpmap:8 PCMA/8000', 27 | ].join('\r\n'); 28 | 29 | it('moves Isac 16K to default when preferred', function() { 30 | var result = maybePreferCodec(SDP_WITH_AUDIO_CODECS, 'audio', 'send', 31 | 'iSAC/16000'); 32 | var audioLine = result.split('\r\n')[1]; 33 | expect(audioLine).toEqual('m=audio 9 RTP/SAVPF 103 111 104 0 9'); 34 | }); 35 | 36 | it('does nothing if preferred codec not found', function() { 37 | var result = maybePreferCodec(SDP_WITH_AUDIO_CODECS, 'audio', 'send', 38 | 'iSAC/123456'); 39 | var audioLine = result.split('\r\n')[1]; 40 | expect(audioLine).toEqual(SDP_WITH_AUDIO_CODECS.split('\r\n')[1]); 41 | }); 42 | 43 | it('moves codec even if payload type is same as udp port', function() { 44 | var result = maybePreferCodec(SDP_WITH_AUDIO_CODECS, 45 | 'audio', 46 | 'send', 47 | 'G722/8000'); 48 | var audioLine = result.split('\r\n')[1]; 49 | expect(audioLine).toEqual('m=audio 9 RTP/SAVPF 9 111 103 104 0'); 50 | }); 51 | 52 | it('remove and set codec param modify then fmtp line', function() { 53 | var result = setCodecParam(SDP_WITH_AUDIO_CODECS, 'opus/48000', 54 | 'minptime', '20'); 55 | var audioLine = result.split('\r\n')[4]; 56 | expect(audioLine).toEqual('a=fmtp:111 minptime=20'); 57 | 58 | result = setCodecParam(result, 'opus/48000', 'useinbandfec', '1'); 59 | audioLine = result.split('\r\n')[4]; 60 | expect(audioLine).toEqual('a=fmtp:111 minptime=20;useinbandfec=1'); 61 | 62 | result = removeCodecParam(result, 'opus/48000', 'minptime'); 63 | audioLine = result.split('\r\n')[4]; 64 | expect(audioLine).toEqual('a=fmtp:111 useinbandfec=1'); 65 | 66 | var newResult = removeCodecParam(result, 'opus/48000', 'minptime'); 67 | expect(newResult).toEqual(result); 68 | }); 69 | 70 | it('remove and set codec param modify fmtp line', function() { 71 | var result = setCodecParam(SDP_WITH_AUDIO_CODECS, 'opus/48000', 72 | 'minptime', '20'); 73 | var audioLine = result.split('\r\n')[4]; 74 | expect(audioLine).toEqual('a=fmtp:111 minptime=20'); 75 | 76 | result = setCodecParam(result, 'opus/48000', 'useinbandfec', '1'); 77 | audioLine = result.split('\r\n')[4]; 78 | expect(audioLine).toEqual('a=fmtp:111 minptime=20;useinbandfec=1'); 79 | 80 | result = removeCodecParam(result, 'opus/48000', 'minptime'); 81 | audioLine = result.split('\r\n')[4]; 82 | expect(audioLine).toEqual('a=fmtp:111 useinbandfec=1'); 83 | 84 | var newResult = removeCodecParam(result, 'opus/48000', 'minptime'); 85 | expect(newResult).toEqual(result); 86 | }); 87 | 88 | it('remove and set codec param remove and add fmtp line if needed', 89 | function() { 90 | var result = removeCodecParam(SDP_WITH_AUDIO_CODECS, 'opus/48000', 91 | 'minptime'); 92 | var audioLine = result.split('\r\n')[4]; 93 | expect(audioLine).toEqual('a=rtpmap:103 ISAC/16000'); 94 | result = setCodecParam(result, 'opus/48000', 'inbandfec', '1'); 95 | audioLine = result.split('\r\n')[4]; 96 | expect(audioLine).toEqual('a=fmtp:111 inbandfec=1'); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /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, 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 | this.websocket_ = new WebSocket(this.wssUrl_); 39 | 40 | this.websocket_.onopen = function() { 41 | trace('Signaling channel opened.'); 42 | 43 | this.websocket_.onerror = function() { 44 | trace('Signaling channel error.'); 45 | }; 46 | this.websocket_.onclose = function(event) { 47 | // TODO(tkchin): reconnect to WSS. 48 | trace('Channel closed with code:' + event.code + 49 | ' reason:' + event.reason); 50 | this.websocket_ = null; 51 | this.registered_ = false; 52 | }; 53 | 54 | if (this.clientId_ && this.roomId_) { 55 | this.register(this.roomId_, this.clientId_); 56 | } 57 | 58 | resolve(); 59 | }.bind(this); 60 | 61 | this.websocket_.onmessage = function(event) { 62 | trace('WSS->C: ' + event.data); 63 | 64 | var message = parseJSON(event.data); 65 | if (!message) { 66 | trace('Failed to parse WSS message: ' + event.data); 67 | return; 68 | } 69 | if (message.error) { 70 | trace('Signaling server error message: ' + message.error); 71 | return; 72 | } 73 | this.onmessage(message.msg); 74 | }.bind(this); 75 | 76 | this.websocket_.onerror = function() { 77 | reject(Error('WebSocket error.')); 78 | }; 79 | }.bind(this)); 80 | }; 81 | 82 | SignalingChannel.prototype.register = function(roomId, clientId) { 83 | if (this.registered_) { 84 | trace('ERROR: SignalingChannel has already registered.'); 85 | return; 86 | } 87 | 88 | this.roomId_ = roomId; 89 | this.clientId_ = clientId; 90 | 91 | if (!this.roomId_) { 92 | trace('ERROR: missing roomId.'); 93 | } 94 | if (!this.clientId_) { 95 | trace('ERROR: missing clientId.'); 96 | } 97 | if (!this.websocket_ || this.websocket_.readyState !== WebSocket.OPEN) { 98 | trace('WebSocket not open yet; saving the IDs to register later.'); 99 | return; 100 | } 101 | trace('Registering signaling channel.'); 102 | var registerMessage = { 103 | cmd: 'register', 104 | roomid: this.roomId_, 105 | clientid: this.clientId_ 106 | }; 107 | this.websocket_.send(JSON.stringify(registerMessage)); 108 | this.registered_ = true; 109 | 110 | // TODO(tkchin): Better notion of whether registration succeeded. Basically 111 | // check that we don't get an error message back from the socket. 112 | trace('Signaling channel registered.'); 113 | }; 114 | 115 | SignalingChannel.prototype.close = function(async) { 116 | if (this.websocket_) { 117 | this.websocket_.close(); 118 | this.websocket_ = null; 119 | } 120 | 121 | if (!this.clientId_ || !this.roomId_) { 122 | return; 123 | } 124 | // Tell WSS that we're done. 125 | var path = this.getWssPostUrl(); 126 | 127 | return sendUrlRequest('DELETE', path, async).catch(function(error) { 128 | trace('Error deleting web socket connection: ' + error.message); 129 | }.bind(this)).then(function() { 130 | this.clientId_ = null; 131 | this.roomId_ = null; 132 | this.registered_ = false; 133 | }.bind(this)); 134 | }; 135 | 136 | SignalingChannel.prototype.send = function(message) { 137 | if (!this.roomId_ || !this.clientId_) { 138 | trace('ERROR: SignalingChannel has not registered.'); 139 | return; 140 | } 141 | trace('C->WSS: ' + message); 142 | 143 | var wssMessage = { 144 | cmd: 'send', 145 | msg: message 146 | }; 147 | var msgString = JSON.stringify(wssMessage); 148 | 149 | if (this.websocket_ && this.websocket_.readyState === WebSocket.OPEN) { 150 | this.websocket_.send(msgString); 151 | } else { 152 | var path = this.getWssPostUrl(); 153 | var xhr = new XMLHttpRequest(); 154 | xhr.open('POST', path, true); 155 | xhr.send(wssMessage.msg); 156 | } 157 | }; 158 | 159 | SignalingChannel.prototype.getWssPostUrl = function() { 160 | return this.wssPostUrl_ + '/' + this.roomId_ + '/' + this.clientId_; 161 | }; 162 | -------------------------------------------------------------------------------- /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 describe, expect, it, beforeEach, afterEach, fail, WebSocket:true, 12 | XMLHttpRequest:true, SignalingChannel, webSockets:true, xhrs:true, 13 | FAKE_WSS_URL, FAKE_WSS_POST_URL, FAKE_ROOM_ID, FAKE_CLIENT_ID, 14 | MockXMLHttpRequest, MockWebSocket */ 15 | 16 | 'use strict'; 17 | 18 | describe('Signaling Channel Test', function() { 19 | beforeEach(function() { 20 | webSockets = []; 21 | xhrs = []; 22 | 23 | this.realWebSocket = WebSocket; 24 | WebSocket = MockWebSocket; 25 | 26 | this.channel = 27 | new SignalingChannel(FAKE_WSS_URL, FAKE_WSS_POST_URL); 28 | 29 | this.realXMLHttpRequest = XMLHttpRequest; 30 | XMLHttpRequest = MockXMLHttpRequest; 31 | }); 32 | 33 | afterEach(function() { 34 | WebSocket = this.realWebSocket; 35 | XMLHttpRequest = this.realXMLHttpRequest; 36 | }); 37 | 38 | it('open success', function(done) { 39 | var promise = this.channel.open(); 40 | expect(webSockets.length).toEqual(1); 41 | 42 | promise.then(function() { 43 | done(); 44 | }).catch(function() { 45 | fail('Websocket could not be opened.'); 46 | }); 47 | 48 | var socket = webSockets[0]; 49 | socket.simulateOpenResult(true); 50 | }); 51 | 52 | it('receive message', function(done) { 53 | this.channel.open(); 54 | var socket = webSockets[0]; 55 | socket.simulateOpenResult(true); 56 | 57 | expect(socket.onmessage).not.toBeNull(); 58 | 59 | this.channel.onmessage = function(msg) { 60 | expect(msg).toEqual(expectedMsg); 61 | done(); 62 | }; 63 | 64 | var expectedMsg = 'hi'; 65 | var event = { 66 | 'data': JSON.stringify({'msg': expectedMsg}) 67 | }; 68 | socket.onmessage(event); 69 | }); 70 | 71 | it('open failure', function(done) { 72 | var promise = this.channel.open(); 73 | expect(webSockets.length).toEqual(1); 74 | 75 | promise.then(function() { 76 | fail('WebSocket could be opened'); 77 | }).catch(function() { 78 | done(); 79 | }); 80 | 81 | var socket = webSockets[0]; 82 | socket.simulateOpenResult(false); 83 | }); 84 | 85 | it('register before open', function() { 86 | this.channel.open(); 87 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 88 | 89 | var socket = webSockets[0]; 90 | socket.simulateOpenResult(true); 91 | 92 | expect(socket.messages.length).toEqual(1); 93 | 94 | var registerMessage = { 95 | cmd: 'register', 96 | roomid: FAKE_ROOM_ID, 97 | clientid: FAKE_CLIENT_ID 98 | }; 99 | expect(socket.messages[0]).toEqual(JSON.stringify(registerMessage)); 100 | }); 101 | 102 | it('register after open', function() { 103 | this.channel.open(); 104 | var socket = webSockets[0]; 105 | socket.simulateOpenResult(true); 106 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 107 | 108 | expect(socket.messages.length).toEqual(1); 109 | 110 | var registerMessage = { 111 | cmd: 'register', 112 | roomid: FAKE_ROOM_ID, 113 | clientid: FAKE_CLIENT_ID 114 | }; 115 | expect(socket.messages[0]).toEqual(JSON.stringify(registerMessage)); 116 | }); 117 | 118 | it('send before open', function() { 119 | this.channel.open(); 120 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 121 | var message = 'hello'; 122 | this.channel.send(message); 123 | 124 | expect(xhrs.length).toEqual(1); 125 | expect(xhrs[0].readyState).toEqual(2); 126 | expect(xhrs[0].url) 127 | .toEqual(FAKE_WSS_POST_URL + '/' + FAKE_ROOM_ID + '/' + FAKE_CLIENT_ID); 128 | expect(xhrs[0].method).toEqual('POST'); 129 | expect(xhrs[0].body).toEqual(message); 130 | }); 131 | 132 | it('send after open', function() { 133 | this.channel.open(); 134 | var socket = webSockets[0]; 135 | socket.simulateOpenResult(true); 136 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 137 | 138 | var message = 'hello'; 139 | var wsMessage = { 140 | cmd: 'send', 141 | msg: message 142 | }; 143 | this.channel.send(message); 144 | 145 | expect(socket.messages.length).toEqual(2); 146 | expect(socket.messages[1]).toEqual(JSON.stringify(wsMessage)); 147 | }); 148 | 149 | it('close after register', function() { 150 | this.channel.open(); 151 | var socket = webSockets[0]; 152 | socket.simulateOpenResult(true); 153 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 154 | 155 | expect(socket.readyState).toEqual(WebSocket.OPEN); 156 | this.channel.close(); 157 | expect(socket.readyState).toEqual(WebSocket.CLOSED); 158 | 159 | expect(xhrs.length).toEqual(1); 160 | expect(xhrs[0].readyState).toEqual(4); 161 | expect(xhrs[0].url) 162 | .toEqual(FAKE_WSS_POST_URL + '/' + FAKE_ROOM_ID + '/' + FAKE_CLIENT_ID); 163 | expect(xhrs[0].method).toEqual('DELETE'); 164 | }); 165 | 166 | it('close before register', function() { 167 | this.channel.open(); 168 | this.channel.close(); 169 | expect(xhrs.length).toEqual(0); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /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 | 13 | 'use strict'; 14 | 15 | var Storage = function() {}; 16 | 17 | // Get a value from local browser storage. Calls callback with value. 18 | Storage.prototype.getStorage = function(key, callback) { 19 | // Use localStorage. 20 | var value = localStorage.getItem(key); 21 | if (callback) { 22 | window.setTimeout(function() { 23 | callback(value); 24 | }, 0); 25 | } 26 | }; 27 | 28 | // Set a value in local browser storage. Calls callback after completion. 29 | Storage.prototype.setStorage = function(key, value, callback) { 30 | // Use localStorage. 31 | localStorage.setItem(key, value); 32 | if (callback) { 33 | window.setTimeout(callback, 0); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /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 expect */ 12 | /* exported FAKE_CANDIDATE, FAKE_SDP, FAKE_ICE_SERVER, FAKE_SEND_EXCEPTION, 13 | FAKE_WSS_POST_URL, FAKE_WSS_URL, FAKE_WSS_POST_URL, FAKE_ROOM_ID, 14 | FAKE_CLIENT_ID, MockWebSocket, MockXMLHttpRequest, webSockets, xhrs, 15 | MockWindowPort, Mock */ 16 | 17 | 'use strict'; 18 | 19 | var FAKE_WSS_URL = 'wss://foo.com'; 20 | var FAKE_WSS_POST_URL = 'https://foo.com'; 21 | var FAKE_ROOM_ID = 'bar'; 22 | var FAKE_CLIENT_ID = 'barbar'; 23 | var FAKE_SEND_EXCEPTION = 'Send exception'; 24 | var FAKE_ICE_SERVER = [ 25 | { 26 | credential: 'foobar', 27 | urls: ['turn:192.168.1.200:19305?transport:udp'], 28 | username: 'barfoo', 29 | }, 30 | {urls: ['stun:stun.l.google.com:19302']} 31 | ]; 32 | 33 | var FAKE_CANDIDATE = 'candidate:702786350 2 udp 41819902 8.8.8.8 60769 ' + 34 | 'typ relay raddr 8.8.8.8 rport 1234 ' + 35 | 'tcptype active ' + 36 | 'ufrag abc ' + 37 | 'generation 0'; 38 | 39 | var FAKE_SDP = 'v=0\r\n' + 40 | 'o=- 166855176514521964 2 IN IP4 127.0.0.1\r\n' + 41 | 's=-\r\n' + 42 | 't=0 0\r\n' + 43 | 'a=msid-semantic:WMS *\r\n' + 44 | 'm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n' + 45 | 'c=IN IP4 0.0.0.0\r\n' + 46 | 'a=rtcp:9 IN IP4 0.0.0.0\r\n' + 47 | 'a=ice-ufrag:someufrag\r\n' + 48 | 'a=ice-pwd:somelongpwdwithenoughrandomness\r\n' + 49 | 'a=fingerprint:sha-256 8C:71:B3:8D:A5:38:FD:8F:A4:2E:A2:65:6C:86:52' + 50 | ':BC:E0:6E:94:F2:9F:7C:4D:B5:DF:AF:AA:6F:44:90:8D:F4\r\n' + 51 | 'a=setup:actpass\r\n' + 52 | 'a=rtcp-mux\r\n' + 53 | 'a=mid:mid1\r\n' + 54 | 'a=sendonly\r\n' + 55 | 'a=rtpmap:111 opus/48000/2\r\n' + 56 | 'a=msid:stream1 track1\r\n' + 57 | 'a=ssrc:1001 cname:some\r\n'; 58 | 59 | var webSockets = []; 60 | var MockWebSocket = function(url) { 61 | expect(url).toEqual(FAKE_WSS_URL); 62 | 63 | this.url = url; 64 | this.messages = []; 65 | this.readyState = WebSocket.CONNECTING; 66 | 67 | this.onopen = null; 68 | this.onclose = null; 69 | this.onerror = null; 70 | this.onmessage = null; 71 | 72 | webSockets.push(this); 73 | }; 74 | 75 | MockWebSocket.CONNECTING = WebSocket.CONNECTING; 76 | MockWebSocket.OPEN = WebSocket.OPEN; 77 | MockWebSocket.CLOSED = WebSocket.CLOSED; 78 | 79 | MockWebSocket.prototype.simulateOpenResult = function(success) { 80 | if (success) { 81 | this.readyState = WebSocket.OPEN; 82 | if (this.onopen) { 83 | this.onopen(); 84 | } 85 | } else { 86 | this.readyState = WebSocket.CLOSED; 87 | if (this.onerror) { 88 | this.onerror(Error('Mock open error')); 89 | } 90 | } 91 | }; 92 | 93 | MockWebSocket.prototype.send = function(msg) { 94 | if (this.readyState !== WebSocket.OPEN) { 95 | throw 'Send called when the connection is not open'; 96 | } 97 | 98 | if (this.throwOnSend) { 99 | throw FAKE_SEND_EXCEPTION; 100 | } 101 | 102 | this.messages.push(msg); 103 | }; 104 | 105 | MockWebSocket.prototype.close = function() { 106 | this.readyState = WebSocket.CLOSED; 107 | }; 108 | 109 | var Mock = {}; 110 | 111 | Mock.createSendAsyncUrlRequestMock = function() { 112 | var calls = []; 113 | var fn = function(method, url, body) { 114 | calls.push({method: method, url: url, body: body}); 115 | return new Promise(function() {}); 116 | }; 117 | fn.calls = function() { 118 | return calls; 119 | }; 120 | return fn; 121 | }; 122 | 123 | var xhrs = []; 124 | var MockXMLHttpRequest = function() { 125 | this.url = null; 126 | this.method = null; 127 | this.async = true; 128 | this.body = null; 129 | this.readyState = 0; 130 | this.status = 0; 131 | 132 | xhrs.push(this); 133 | }; 134 | MockXMLHttpRequest.prototype.open = function(method, path, async) { 135 | this.url = path; 136 | this.method = method; 137 | this.async = async; 138 | this.readyState = 1; 139 | }; 140 | MockXMLHttpRequest.prototype.send = function(body) { 141 | this.body = body; 142 | if (this.async) { 143 | this.readyState = 2; 144 | this.status = 200; 145 | } else { 146 | this.readyState = 4; 147 | this.status = 200; 148 | } 149 | }; 150 | // Clean up xhr queue for the next test. 151 | MockXMLHttpRequest.cleanQueue = function() { 152 | xhrs = []; 153 | }; 154 | 155 | var MockWindowPort = function() { 156 | this.messages = []; 157 | this.onMessage_ = null; 158 | }; 159 | 160 | MockWindowPort.prototype.addMessageListener = function(callback) { 161 | this.onMessage_ = callback; 162 | }; 163 | 164 | MockWindowPort.prototype.sendMessage = function(message) { 165 | this.messages.push(message); 166 | }; 167 | 168 | MockWindowPort.prototype.simulateMessageFromBackground = function(message) { 169 | if (this.onMessage_) { 170 | this.onMessage_(message); 171 | } 172 | }; 173 | -------------------------------------------------------------------------------- /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 | 'use strict'; 11 | 12 | Function.prototype.bind = Function.prototype.bind || function(thisp) { 13 | var fn = this; 14 | var suppliedArgs = Array.prototype.slice.call(arguments, 1); 15 | return function() { 16 | return fn.apply(thisp, 17 | suppliedArgs.concat(Array.prototype.slice.call(arguments))); 18 | }; 19 | }; 20 | 21 | if (!window.performance) { 22 | window.performance = function() {}; 23 | window.performance.now = function() { 24 | return 0; 25 | }; 26 | } 27 | 28 | window.RTCSessionDescription = window.RTCSessionDescription || function(input) { 29 | this.type = input.type; 30 | this.sdp = input.sdp; 31 | }; 32 | 33 | window.RTCIceCandidate = window.RTCIceCandidate || function(candidate) { 34 | this.sdpMLineIndex = candidate.sdpMLineIndex; 35 | this.candidate = candidate.candidate; 36 | }; 37 | 38 | var PROMISE_STATE = { 39 | PENDING: 0, 40 | FULLFILLED: 1, 41 | REJECTED: 2 42 | }; 43 | 44 | var MyPromise = function(executor) { 45 | this.state_ = PROMISE_STATE.PENDING; 46 | this.resolveCallback_ = null; 47 | this.rejectCallback_ = null; 48 | 49 | this.value_ = null; 50 | this.reason_ = null; 51 | executor(this.onResolve_.bind(this), this.onReject_.bind(this)); 52 | }; 53 | 54 | MyPromise.all = function(promises) { 55 | var values = new Array(promises.length); 56 | return new MyPromise(function(values, resolve, reject) { 57 | function onResolve(values, index, value) { 58 | values[index] = value || null; 59 | 60 | for (var i = 0; i < values.length; ++i) { 61 | if (values[i] === undefined) { 62 | return; 63 | } 64 | } 65 | resolve(values); 66 | } 67 | for (var i = 0; i < promises.length; ++i) { 68 | promises[i].then(onResolve.bind(null, values, i), reject); 69 | } 70 | }.bind(null, values)); 71 | }; 72 | 73 | /* jshint ignore:start */ 74 | MyPromise.resolve = function(value) { 75 | return new MyPromise(function(resolve) { 76 | resolve(value); 77 | }); 78 | }; 79 | 80 | MyPromise.reject = function(error) { 81 | return new MyPromise(function(resolve, reject) { 82 | reject(error); 83 | }); 84 | }; 85 | /* jshint ignore:end */ 86 | 87 | MyPromise.prototype.then = function(onResolve, onReject) { 88 | switch (this.state_) { 89 | case PROMISE_STATE.PENDING: 90 | this.resolveCallback_ = onResolve; 91 | this.rejectCallback_ = onReject; 92 | break; 93 | case PROMISE_STATE.FULLFILLED: 94 | onResolve(this.value_); 95 | break; 96 | case PROMISE_STATE.REJECTED: 97 | if (onReject) { 98 | onReject(this.reason_); 99 | } 100 | break; 101 | default: 102 | onReject(this.reason_); 103 | } 104 | return this; 105 | }; 106 | 107 | MyPromise.prototype.catch = function(onReject) { 108 | switch (this.state_) { 109 | case PROMISE_STATE.PENDING: 110 | this.rejectCallback_ = onReject; 111 | break; 112 | case PROMISE_STATE.FULLFILLED: 113 | break; 114 | case PROMISE_STATE.REJECTED: 115 | onReject(this.reason_); 116 | break; 117 | default: 118 | onReject(this.reason_); 119 | } 120 | return this; 121 | }; 122 | 123 | MyPromise.prototype.onResolve_ = function(value) { 124 | if (this.state_ !== PROMISE_STATE.PENDING) { 125 | return; 126 | } 127 | this.state_ = PROMISE_STATE.FULLFILLED; 128 | if (this.resolveCallback_) { 129 | this.resolveCallback_(value); 130 | } else { 131 | this.value_ = value; 132 | } 133 | }; 134 | 135 | MyPromise.prototype.onReject_ = function(reason) { 136 | if (this.state_ !== PROMISE_STATE.PENDING) { 137 | return; 138 | } 139 | this.state_ = PROMISE_STATE.REJECTED; 140 | if (this.rejectCallback_) { 141 | this.rejectCallback_(reason); 142 | } else { 143 | this.reason_ = reason; 144 | } 145 | }; 146 | 147 | window.Promise = window.Promise || MyPromise; 148 | 149 | // Provide a shim for phantomjs, where chrome is not defined. 150 | var myChrome = (function() { 151 | var onConnectCallback_; 152 | return { 153 | app: { 154 | runtime: { 155 | onLaunched: { 156 | addListener: function(callback) { 157 | console.log( 158 | 'chrome.app.runtime.onLaunched.addListener called:' + callback); 159 | } 160 | } 161 | }, 162 | window: { 163 | create: function(fileName, callback) { 164 | console.log( 165 | 'chrome.window.create called: ' + 166 | fileName + ', ' + callback); 167 | } 168 | } 169 | }, 170 | runtime: { 171 | onConnect: { 172 | addListener: function(callback) { 173 | console.log( 174 | 'chrome.runtime.onConnect.addListener called: ' + callback); 175 | onConnectCallback_ = callback; 176 | } 177 | } 178 | }, 179 | callOnConnect: function(port) { 180 | if (onConnectCallback_) { 181 | onConnectCallback_(port); 182 | } 183 | } 184 | }; 185 | })(); 186 | 187 | window.chrome = myChrome; 188 | -------------------------------------------------------------------------------- /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 describe, expect, it, filterIceServersUrls, randomString, 12 | queryStringToDictionary */ 13 | 14 | 'use strict'; 15 | 16 | describe('Utils test', function() { 17 | var PEERCONNECTION_CONFIG = { 18 | iceServers: [ 19 | { 20 | urls: [ 21 | 'turn:turn.example1.com', 22 | 'turn:turn.example.com?transport=tcp', 23 | 'turn:turn.example.com?transport=udp', 24 | 'turn:turn.example1.com:8888', 25 | 'turn:turn.example.com:8888?transport=tcp', 26 | 'turn:turn.example.com:8888?transport=udp' 27 | ], 28 | username: 'username', 29 | credential: 'credential' 30 | }, 31 | { 32 | urls: [ 33 | 'stun:stun.example1.com', 34 | 'stun:stun.example.com?transport=tcp', 35 | 'stun:stun.example.com?transport=udp', 36 | 'stun:stun.example1.com:8888', 37 | 'stun:stun.example.com:8888?transport=tcp', 38 | 'stun:stun.example.com:8888?transport=udp' 39 | ] 40 | }, 41 | { 42 | // This should not appear at all due to it being empty after filtering 43 | // it. 44 | urls: [ 45 | 'stun:stun2.example.com?transport=tcp' 46 | ] 47 | } 48 | ] 49 | }; 50 | 51 | var PEERCONNECTION_CONFIG_FILTERED = { 52 | iceServers: [ 53 | { 54 | urls: [ 55 | 'turn:turn.example1.com?transport=udp', 56 | 'turn:turn.example.com?transport=udp', 57 | 'turn:turn.example1.com:8888?transport=udp', 58 | 'turn:turn.example.com:8888?transport=udp' 59 | ], 60 | username: 'username', 61 | credential: 'credential' 62 | }, 63 | { 64 | urls: [ 65 | 'stun:stun.example1.com?transport=udp', 66 | 'stun:stun.example.com?transport=udp', 67 | 'stun:stun.example1.com:8888?transport=udp', 68 | 'stun:stun.example.com:8888?transport=udp' 69 | ] 70 | } 71 | ] 72 | }; 73 | 74 | it('filter Ice Servers URLS', function() { 75 | filterIceServersUrls(PEERCONNECTION_CONFIG, 'udp'); 76 | // Only transport=udp URLs should remain.' 77 | expect(PEERCONNECTION_CONFIG).toEqual(PEERCONNECTION_CONFIG_FILTERED); 78 | }); 79 | 80 | it('random Returns Correct Length', function() { 81 | expect(randomString(13).length).toEqual(13); 82 | expect(randomString(5).length).toEqual(5); 83 | expect(randomString(10).length).toEqual(10); 84 | }); 85 | 86 | it('random Returns Correct Characters', function() { 87 | var str = randomString(500); 88 | 89 | // randomString should return only the digits 0-9. 90 | var positiveRe = /^[0-9]+$/; 91 | var negativeRe = /[^0-9]/; 92 | 93 | var positiveResult = positiveRe.exec(str); 94 | var negativeResult = negativeRe.exec(str); 95 | 96 | expect(positiveResult.index).toEqual(0); 97 | expect(negativeResult).toBeNull(); 98 | }); 99 | 100 | it('query String To Dictionary', function() { 101 | var dictionary = { 102 | 'foo': 'a', 103 | 'baz': '', 104 | 'bar': 'b', 105 | 'tee': '', 106 | }; 107 | 108 | var buildQuery = function(data, includeEqualsOnEmpty) { 109 | var queryString = '?'; 110 | for (var key in data) { 111 | queryString += key; 112 | if (data[key] || includeEqualsOnEmpty) { 113 | queryString += '='; 114 | } 115 | queryString += data[key] + '&'; 116 | } 117 | queryString = queryString.slice(0, -1); 118 | return queryString; 119 | }; 120 | 121 | // Build query where empty value is formatted as &tee=&. 122 | var query = buildQuery(dictionary, true); 123 | var result = queryStringToDictionary(query); 124 | expect(JSON.stringify(result)).toEqual(JSON.stringify(dictionary)); 125 | 126 | // Build query where empty value is formatted as &tee&. 127 | query = buildQuery(dictionary, false); 128 | result = queryStringToDictionary(query); 129 | expect(JSON.stringify(result)).toEqual(JSON.stringify(dictionary)); 130 | 131 | result = queryStringToDictionary('?'); 132 | expect(Object.keys(result).length).toEqual(0); 133 | 134 | result = queryStringToDictionary('?='); 135 | expect(Object.keys(result).length).toEqual(0); 136 | 137 | result = queryStringToDictionary('?&='); 138 | expect(Object.keys(result).length).toEqual(0); 139 | 140 | result = queryStringToDictionary(''); 141 | expect(Object.keys(result).length).toEqual(0); 142 | 143 | result = queryStringToDictionary('?=abc'); 144 | expect(Object.keys(result).length).toEqual(0); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------