├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG ├── Gruntfile.js ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── circus └── loop-server.ini ├── config ├── loadtest.json ├── sample.json └── test.json ├── loop ├── api-specs.json ├── auth.js ├── config.js ├── constants.js ├── encrypt.js ├── errno.json ├── filestorage │ ├── aws.js │ ├── filesystem.js │ └── index.js ├── fxa.js ├── hmac.js ├── index.js ├── logger.js ├── middlewares.js ├── newrelic.js ├── notifications.js ├── pubsub.js ├── routes │ ├── account.js │ ├── analytics.js │ ├── call-url.js │ ├── calls.js │ ├── fxa-oauth.js │ ├── home.js │ ├── push-server-config.js │ ├── registration.js │ ├── rooms.js │ ├── session.js │ ├── validators.js │ └── videur.js ├── simplepush.js ├── storage │ ├── index.js │ ├── redis.js │ ├── redis_client.js │ └── redis_migration.js ├── tokbox.js ├── tokenlib.js ├── utils.js └── websockets.js ├── package.json ├── redis_usage.py ├── test ├── auth_test.js ├── config_test.js ├── encrypt_test.js ├── filestorage_test.js ├── functional_test.js ├── fxa_oauth_tests.js ├── fxa_test.js ├── headers_tests.js ├── hmac_test.js ├── index_test.js ├── middlewares_test.js ├── nock.js ├── redis_client_test.js ├── redis_migration_test.js ├── rooms_test.js ├── simplepush_test.js ├── storage_test.js ├── support.js ├── tokbox_test.js ├── tokenlib_test.js ├── tools_test.js ├── utils_test.js └── websockets_test.js └── tools ├── README.rst ├── _get_1111579_fxa_impacted.js ├── _get_expiration.js ├── _get_ttl_hawk.js ├── _graph_expiration.sh ├── get_active_inactive_users.js ├── get_average_call-urls_per_user.js ├── get_average_calls_per_user.js ├── get_average_rooms_per_user.js ├── get_expiration_estimate.js ├── get_number_fxa_devices.js ├── get_number_fxa_users.js ├── get_redis_usage.js ├── get_tokbox_sessionid_for_room_token.js ├── hawk_user_info.js ├── migrate_1121403_roomparticipants.js ├── move_redis_data.js ├── remove_old_keys.js ├── send_sentry.js └── utils.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | # http://eslint.org/docs/rules/ 2 | 3 | extends: eslint:recommended 4 | 5 | env: 6 | amd: true 7 | browser: false 8 | mocha: true 9 | node: true 10 | 11 | rules: 12 | eol-last: 2 13 | eqeqeq: 2 14 | new-parens: 2 15 | no-alert: 2 16 | no-array-constructor: 2 17 | no-bitwise: 1 18 | no-caller: 2 19 | no-catch-shadow: 2 20 | no-console: 0 21 | no-div-regex: 2 22 | no-empty-label: 2 23 | no-eq-null: 2 24 | no-eval: 2 25 | no-floating-decimal: 2 26 | no-implied-eval: 2 27 | no-iterator: 2 28 | no-label-var: 2 29 | no-loop-func: 2 30 | no-multi-str: 2 31 | no-native-reassign: 2 32 | no-new-func: 2 33 | no-new-object: 2 34 | no-new-wrappers: 2 35 | no-octal-escape: 2 36 | no-proto: 2 37 | no-return-assign: 2 38 | no-script-url: 2 39 | no-self-compare: 2 40 | no-undef-init: 2 41 | no-with: 2 42 | wrap-iife: 2 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | html-report 4 | lib-cov 5 | config/dev.json 6 | config/stage.json 7 | config/prod.json 8 | circus/*.log 9 | heka*.log 10 | *.pyc 11 | loadtests/venv/ 12 | loadtest.log 13 | loadtests/.env.install 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: 2 | - node_js 3 | - python 4 | 5 | python: 6 | - "2.7" 7 | 8 | node_js: 9 | - "0.10" 10 | - "4" 11 | 12 | env: 13 | - CXX=g++-4.8 14 | 15 | addons: 16 | apt: 17 | sources: 18 | - ubuntu-toolchain-r-test 19 | packages: 20 | - g++-4.8 21 | 22 | sudo: false 23 | 24 | services: redis-server 25 | 26 | before_install: 27 | - export PATH=$HOME/.local/bin:$PATH 28 | - pip install --user `whoami` virtualenv 29 | 30 | script: make travis 31 | 32 | after_script: 33 | - npm run outdated 34 | - npm run audit-shrinkwrap 35 | 36 | notifications: 37 | irc: 38 | channels: 39 | - "irc.mozilla.org#loop" 40 | use_notice: false 41 | skip_join: true 42 | on_success: change 43 | on_failure: always 44 | template: 45 | - "%{repository} (%{branch} - %{commit}: %{author}): %{message} %{build_url}" 46 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | "use strict"; 3 | 4 | require("load-grunt-tasks")(grunt); 5 | 6 | grunt.initConfig({ 7 | "APP": { 8 | "CODE_DIRS": "{,config/**/,loop/**/,test/**/}" 9 | }, 10 | "pkg": require("./package.json"), 11 | 12 | "copyright": { 13 | "options": { 14 | "pattern": "This Source Code Form is subject to the terms of the Mozilla Public" 15 | }, 16 | "src": "<%= eslint.src %>" 17 | }, 18 | 19 | "eslint": { 20 | "src": "<%= APP.CODE_DIRS %>*.js" 21 | }, 22 | 23 | "jsonlint": { 24 | "src": "<%= APP.CODE_DIRS %>*.json" 25 | }, 26 | 27 | "shell": { 28 | "outdated": { 29 | "command": "npm outdated --depth 0" 30 | }, 31 | "shrinkwrap": { 32 | "command": "npm shrinkwrap --dev" 33 | }, 34 | "rm-shrinkwrap": { 35 | "command": "rm npm-shrinkwrap.json" 36 | } 37 | }, 38 | 39 | "todo": { 40 | "options": { 41 | "marks": [ 42 | { 43 | "name": 'FIX', 44 | "pattern": /FIXME/, 45 | "color": 'red' 46 | }, 47 | { 48 | "name": 'TODO', 49 | "pattern": /TODO/, 50 | "color": 'yellow' 51 | }, 52 | { 53 | "name": 'NOTE', 54 | "pattern": /NOTE/, 55 | "color": 'blue' 56 | }, { 57 | "name": 'XXX', 58 | "pattern": /XXX/, 59 | "color": 'yellow' 60 | }, { 61 | "name": 'HACK', 62 | "pattern": /HACK/, 63 | "color": 'red' 64 | } 65 | ] 66 | }, 67 | "src": [ 68 | "<%= eslint.src %>", 69 | "!Gruntfile.js" 70 | ] 71 | }, 72 | 73 | "validate-shrinkwrap": { 74 | } 75 | }); 76 | 77 | grunt.registerTask("lint", ["eslint", "jsonlint"]); 78 | grunt.registerTask("do-shrinkwrap", ["shell:shrinkwrap", "validate-shrinkwrap", "shell:rm-shrinkwrap"]); 79 | grunt.registerTask("audit-shrinkwrap", ["do-shrinkwrap", "shell:outdated"]); 80 | grunt.registerTask("default", ["lint", "copyright"]); 81 | }; 82 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | NODE_LOCAL_BIN=./node_modules/.bin 6 | 7 | .PHONY: test 8 | test: lint cover-mocha spaceleft 9 | 10 | .PHONY: travis 11 | travis: lint separate-tests 12 | 13 | separate-tests: 14 | @env NODE_ENV=test ./node_modules/mocha/bin/mocha test/* --reporter spec -ig websocket 15 | @env NODE_ENV=test ./node_modules/mocha/bin/mocha test/* --reporter spec -g websocket -t 5000 16 | 17 | install: 18 | @npm install 19 | 20 | .PHONY: lint 21 | lint: 22 | @$(NODE_LOCAL_BIN)/grunt lint 23 | 24 | clean: 25 | rm -rf .venv node_modules coverage lib-cov html-report 26 | 27 | .PHONY: cover-mocha 28 | cover-mocha: 29 | @if [ `ulimit -n` -lt 1024 ]; then echo "ulimit is too low. Please run 'ulimit -S -n 2048' before running tests."; exit 1; fi 30 | @env NODE_ENV=test $(NODE_LOCAL_BIN)/istanbul cover \ 31 | $(NODE_LOCAL_BIN)/_mocha -- --reporter spec -t 5000 test/* 32 | @echo aim your browser at coverage/lcov-report/index.html for details 33 | 34 | .PHONY: eslint 35 | eslint: 36 | @$(NODE_LOCAL_BIN)/grunt eslint 37 | 38 | .PHONY: mocha 39 | mocha: 40 | @if [ `ulimit -n` -lt 1024 ]; then echo "ulimit is too low. Please run 'ulimit -S -n 2048' before running tests."; exit 1; fi 41 | @env NODE_ENV=test ./node_modules/mocha/bin/mocha test/* --reporter spec 42 | 43 | .PHONY: spaceleft 44 | spaceleft: 45 | @if which grin 2>&1 >/dev/null; \ 46 | then \ 47 | test "$$(grin " $$" loop/ test/ config/ -l | wc -l)" -ne "0" && \ 48 | grin -l " $$" loop/ test/ config/ | xargs sed -i 's/\s*$$//' || exit 0; \ 49 | fi 50 | 51 | .PHONY: runserver 52 | runserver: 53 | @env NODE_ENV=${NODE_ENV} node loop/index.js 54 | 55 | .PHONY: circus 56 | circus: 57 | circusd circus/loop-server.ini 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Firefox Hello has been discontinued. This repository is no longer being actively maintainted and is kept for historical purposes. -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Rooms 2 | 3 | 4 | ## Questions 5 | 6 | - How do we handle the link clicker session? 7 | - How do we handle two participants asking for the same displayName? 8 | 9 | 10 | ## Code & Test 11 | 12 | + PUT /rooms/token should be PATCH. (+to say) 13 | + Rename req.roomData in req.roomStorageData 14 | + Use a local roomData var instead of updating req.roomData 15 | + Check the room owner on patch and delete 16 | + Rename addUserRoomData to setUserRoomData 17 | + manage the participants in GET /rooms/token and DELETE 18 | + Do not let non-participants get room info (appart owner). 19 | + Handle rejection of new participants if the room is full. 20 | + Write storage test for participant related methods 21 | + We should add CORS handling and test it. 22 | + WebApp Url should be refactored. 23 | + POST /rooms/:token — handle refresh 24 | + POST /rooms/:token — handle leave 25 | + GET /rooms should return user rooms 26 | + GET /rooms should handle the ?version parameter. 27 | + Test roomUserSimplePush to be called on updateTime. 28 | + Add test to make sure updatedTime is updated on: 29 | + user join 30 | + user leaves 31 | + Test notification on POST / PATCH / DELETE /rooms 32 | + Add a test to check participants expiricy (doesn't return items when not needed); 33 | + Handle the account property in the participant obj. 34 | + Encrypt account information in the database. 35 | + Update the load test scripts 36 | - Update the memory usage script with rooms (+ other stuff that needs to be updated) 37 | - Handle the TokBox channel on /rooms (+to say) 38 | 39 | 40 | ## To say 41 | 42 | - It's not a 200 it's a 201 on resource creation; 43 | + PUT /rooms/token should be PATCH; 44 | - Handle the TokBox channel on /rooms; 45 | -------------------------------------------------------------------------------- /circus/loop-server.ini: -------------------------------------------------------------------------------- 1 | [circus] 2 | check_delay = 5 3 | endpoint = tcp://127.0.0.1:5555 4 | pubsub_endpoint = tcp://127.0.0.1:5556 5 | stats_endpoint = tcp://127.0.0.1:5557 6 | 7 | [plugin:flapping] 8 | use = circus.plugins.flapping.Flapping 9 | retry_in = 3 10 | max_retry = 2 11 | 12 | [watcher:loop] 13 | cmd = node loop/index.js --fd $(circus.sockets.loop) 14 | use_sockets = True 15 | warmup_delay = 0 16 | numprocesses = 2 17 | stop_children = true 18 | stop_signal = SIGINT 19 | stdout_stream.class = StdoutStream 20 | stderr_stream.class = StdoutStream 21 | 22 | [socket:loop] 23 | host = 127.0.0.1 24 | port = 5000 25 | 26 | [env:loop] 27 | NODE_ENV = dev 28 | -------------------------------------------------------------------------------- /config/loadtest.json: -------------------------------------------------------------------------------- 1 | { 2 | "ip": "127.0.0.1", 3 | "port": 5000, 4 | "acceptBacklog": 4096, 5 | "publicServerAddress": "127.0.0.1:5000", 6 | "macSecret": "263ceaa5546dce837191be98db91e852ae8d050d6805a402272e0c776193cfba", 7 | "encryptionSecret": "7c69b9ca88e4f127f7280368f7055646", 8 | "userMacAlgorithm": "sha256", 9 | "userMacSecret": "5a25ad4feadd45fdcbb05402658272da", 10 | "sessionSecret": "this is not a secret", 11 | "fakeTokBox": true, 12 | "tokBox": { 13 | "credentials": { 14 | "default": { 15 | "apiKey": "", 16 | "apiSecret": "" 17 | } 18 | }, 19 | "tokenDuration": 86400 20 | }, 21 | "sentryDSN": false, 22 | "allowedOrigins": ["*"], 23 | "statsdEnabled": false, 24 | "fxaAudiences": ["app://loop.services.mozilla.com", "http://loop.services.mozilla.com", "http://localhost", "http://127.0.0.1"], 25 | "fxaOAuth": { 26 | "client_id": "cdaf76e75c7f7a00", 27 | "client_secret": "74a35b3eeef81a4f36ba8e7bf76e9972a3af6bd7c4c2fba7db7ccfc8b324cb8b" 28 | }, 29 | "pushServerURIs": [], 30 | "rooms": { 31 | "HKDFSalt": "6b4a7eec5406fcb3d394e7f64b2a72a2", 32 | "maxSize": 10 33 | }, 34 | "hekaMetrics": { 35 | "activated": true, 36 | "debug": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "ip": "127.0.0.1", 3 | "port": 5000, 4 | "acceptBacklog": 4096, 5 | "macSecret": "263ceaa5546dce837191be98db91e852ae8d050d6805a402272e0c776193cfba", 6 | "encryptionSecret": "7c69b9ca88e4f127f7280368f7055646", 7 | "userMacAlgorithm": "sha256", 8 | "userMacSecret": "5a25ad4feadd45fdcbb05402658272da", 9 | "sessionSecret": "this is not a secret", 10 | "tokBox": { 11 | "credentials": { 12 | "default": { 13 | "apiKey": "", 14 | "apiSecret": "" 15 | } 16 | }, 17 | "tokenDuration": 86400 18 | }, 19 | "sentryDSN": false, 20 | "allowedOrigins": [ 21 | "http://localhost:3000" 22 | ], 23 | "statsdEnabled": false, 24 | "fxaAudiences": [ 25 | "https://loop.services.mozilla.com", 26 | "app://loop.services.mozilla.com" 27 | ], 28 | "fxaOAuth": { 29 | "client_id": "cdaf76e75c7f7a00", 30 | "client_secret": "74a35b3eeef81a4f36ba8e7bf76e9972a3af6bd7c4c2fba7db7ccfc8b324cb8b" 31 | }, 32 | "rooms": { 33 | "HKDFSalt": "80368f70556467c69b9ca88e4f127f72", 34 | "webAppUrl": "http://localhost:3000/{token}" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "ip": "127.0.0.1", 3 | "port": 0, 4 | "acceptBacklog": 4096, 5 | "storage": { 6 | "engine": "redis", 7 | "settings": { 8 | "db": 5, 9 | "migrateFrom": { 10 | "db": 4 11 | } 12 | } 13 | }, 14 | "filestorage": { 15 | "settings": { 16 | "base_dir": "/tmp" 17 | } 18 | }, 19 | "hawkIdSecret": "6b4a7eec5406fcb3d394e7f64b2a72a2", 20 | "macSecret": "263ceaa5546dce837191be98db91e852ae8d050d6805a402272e0c776193cfba", 21 | "encryptionSecret": "7c69b9ca88e4f127f7280368f7055646", 22 | "invalidMacSecret": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 23 | "invalidEncryptionSecret": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 24 | "userMacAlgorithm": "sha256", 25 | "userMacSecret": "5a25ad4feadd45fdcbb05402658272da", 26 | "sessionSecret": "this is not a secret", 27 | "tokBox": { 28 | "credentials": { 29 | "default": { 30 | "apiKey": "44687443", 31 | "apiSecret": "5379ba385ad44c83a2584840d095c08351cd9041" 32 | } 33 | }, 34 | "tokenDuration": 86400 35 | }, 36 | "fakeCallInfo": { 37 | "apiKey": "fake API key", 38 | "session1": "1_MX40NDY4NzQ0Mn5-V2VkIE1hciAxMiwAMzo1OTo1MSBQRFgQMjAxNH4wLjQO5TMzMj3Mfg", 39 | "session2": "2_MX40NDY4NzQ0Mn5-V2VkIE1hciAxMiwAMzo1OTo1MSBQRFgQMjAxNH4wLjQO5TMzMj3Mfg", 40 | "session3": "3_MX40NDY4NzQ0Mn5-V2VkIE1hciAxMiwAMzo1OTo1MSBQRFgQMjAxNH4wLjQO5TMzMj3Mfg", 41 | "token1": "T1==GcFydG5lcl9pZD00NDY4NzQ0MiZzZGtfdmVyc2lvbj10YnJ1YnktdGJyYi12MC45MS4yMDELxTyALT3EJnNpZz00ODkZNTQ1YTZlOWIzGOM2ZjmRMWFiYTdjZmFhNV2mOWUxMzFlMWYwOnJvbGU9cHVibGlzaGVyJnNlc3Npb25faWQ9M9VNWDQwTkRZNE56UTBNbjUtVjJWa0lFWMhjaUF4TWlBd016bzFPVG8xTVNCUVJGUWdNakF4Tkg0d0xqUTVPVE16TWpNM2ZnJmNyZWF0ZV90aW1lPTEzOTQ2MjE5OTcmbm9uY2U9MC4yMzM2MDQyMzM2MzY2OTQ1OCZleHBpcmVfdGltZT0xMzk0NjI1NTgzJmNvmb5lY3Rbp25fZGF0TY0=", 42 | "token2": "T2==GcFydG5lcl9pZD00NDY4NzQ0MiZzZGtfdmVyc2lvbj10YnJ1YnktdGJyYi12MC45MS4yMDELxTyALT3EJnNpZz00ODkZNTQ1YTZlOWIzGOM2ZjmRMWFiYTdjZmFhNV2mOWUxMzFlMWYwOnJvbGU9cHVibGlzaGVyJnNlc3Npb25faWQ9M9VNWDQwTkRZNE56UTBNbjUtVjJWa0lFWMhjaUF4TWlBd016bzFPVG8xTVNCUVJGUWdNakF4Tkg0d0xqUTVPVE16TWpNM2ZnJmNyZWF0ZV90aW1lPTEzOTQ2MjE5OTcmbm9uY2U9MC4yMzM2MDQyMzM2MzY2OTQ1OCZleHBpcmVfdGltZT0xMzk0NjI1NTgzJmNvmb5lY3Rbp25fZGF0TY0=", 43 | "token3": "T3==GcFydG5lcl9pZD00NDY4NzQ0MiZzZGtfdmVyc2lvbj10YnJ1YnktdGJyYi12MC45MS4yMDELxTyALT3EJnNpZz00ODkZNTQ1YTZlOWIzGOM2ZjmRMWFiYTdjZmFhNV2mOWUxMzFlMWYwOnJvbGU9cHVibGlzaGVyJnNlc3Npb25faWQ9M9VNWDQwTkRZNE56UTBNbjUtVjJWa0lFWMhjaUF4TWlBd016bzFPVG8xTVNCUVJGUWdNakF4Tkg0d0xqUTVPVE16TWpNM2ZnJmNyZWF0ZV90aW1lPTEzOTQ2MjE5OTcmbm9uY2U9MC4yMzM2MDQyMzM2MzY2OTQ1OCZleHBpcmVfdGltZT0xMzk0NjI1NTgzJmNvmb5lY3Rbp25fZGF0TY0=" 44 | }, 45 | "allowedOrigins": [ 46 | "http://localhost:3000" 47 | ], 48 | "statsdEnabled": true, 49 | "fxaAudiences": [ 50 | "http://localhost:5000" 51 | ], 52 | "timers": { 53 | "supervisoryDuration": 0.05, 54 | "ringingDuration": 0.05, 55 | "connectionDuration": 0.05 56 | }, 57 | "maxSimplePushUrls": 2, 58 | 59 | "fxaOAuth": { 60 | "client_id": "263ceaa5546dce83", 61 | "client_secret": "852ae8d050d6805a402272e0c776193cfba263ceaa5546dce837191be98db91e" 62 | }, 63 | "rooms": { 64 | "maxRoomNameSize": 15, 65 | "maxRoomOwnerSize": 10, 66 | "maxTTL": 10, 67 | "maxSize": 3, 68 | "participantTTL": 0.05, 69 | "HKDFSalt": "6b4a7eec5406fcb3d394e7f64b2a72a2" 70 | }, 71 | "hekaMetrics": { 72 | "activated": false, 73 | "debug": false, 74 | "level": "DEBUG", 75 | "fmt": "pretty" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /loop/api-specs.json: -------------------------------------------------------------------------------- 1 | { 2 | "service": { 3 | "location": "{{location}}", 4 | "version": "{{version}}", 5 | "videur_version": "0.1", 6 | "resources": { 7 | 8 | "/__heartbeat__": { 9 | "GET": {} 10 | }, 11 | "/": { 12 | "GET": {} 13 | }, 14 | 15 | "/account": { 16 | "DELETE": {} 17 | }, 18 | 19 | "/call-url": { 20 | "GET": {}, 21 | "POST": { 22 | "limits": { 23 | "rates": [ 24 | { 25 | "seconds": 60, 26 | "hits": 60, 27 | "match": "header:Authorization AND header:User-Agent" 28 | }] 29 | } 30 | }, 31 | "PUT": {}, 32 | "DELETE": {} 33 | }, 34 | 35 | "/calls": { 36 | "GET": {}, 37 | "POST": {} 38 | }, 39 | 40 | "regexp:/calls/[a-zA-Z0-9_-]{{callTokenSize}}": { 41 | "POST": {} 42 | }, 43 | 44 | "/fxa-oauth/params": { 45 | "POST": {} 46 | }, 47 | "/fxa-oauth/token": { 48 | "POST": {}, 49 | "GET": {} 50 | }, 51 | 52 | "/push-server-config": { 53 | "GET": {} 54 | }, 55 | 56 | "/registration": { 57 | "POST": {}, 58 | "DELETE": {} 59 | }, 60 | 61 | "/rooms": { 62 | "POST": {}, 63 | "DELETE": {}, 64 | "GET": {} 65 | }, 66 | "regexp:/rooms/[a-zA-Z0-9_-]{{roomTokenSize}}": { 67 | "PATCH": {}, 68 | "DELETE": {}, 69 | "GET": {}, 70 | "POST": {} 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /loop/auth.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var hawk = require('express-hawkauth'); 8 | 9 | var encrypt = require("./encrypt").encrypt; 10 | var errors = require('./errno'); 11 | var hmac = require('./hmac'); 12 | var sendError = require('./utils').sendError; 13 | var fxa = require('./fxa'); 14 | 15 | var USER_TYPES = require('./constants').USER_TYPES; 16 | 17 | 18 | module.exports = function(conf, logError, storage, statsdClient) { 19 | var hawkOptions = { 20 | port: conf.get("protocol") === "https" ? 443 : undefined 21 | }; 22 | 23 | 24 | function unauthorized(res, supported, message) { 25 | var header = supported.join(); 26 | if (message) { 27 | header += ' error="' + message.replace(/"/g, '\"') + '"'; 28 | } 29 | res.set('WWW-Authenticate', header); 30 | sendError(res, 401, errors.INVALID_AUTH_TOKEN, message || "Unauthorized"); 31 | } 32 | 33 | 34 | /** 35 | * Attach the identity of the user to the request if she is registered in the 36 | * database. 37 | **/ 38 | function setUser(req, res, credentials, done) { 39 | req.hawkIdHmac = hmac(credentials.id, conf.get("hawkIdSecret")); 40 | storage.getHawkUser(req.hawkIdHmac, function(err, user) { 41 | if (res.serverError(err)) return; 42 | if (user !== null) { 43 | // If an identity is defined for this hawk session, use it. 44 | req.user = user; 45 | req.userType = USER_TYPES.REGISTERED; 46 | } else { 47 | req.user = req.hawkIdHmac; 48 | req.userType = USER_TYPES.UNREGISTERED; 49 | } 50 | 51 | storage.touchHawkSession(req.user, req.hawkIdHmac, function(err) { 52 | if (res.serverError(err)) return; 53 | done(); 54 | }); 55 | }); 56 | } 57 | 58 | function getHawkSession(tokenId, callback) { 59 | storage.getHawkSession(hmac(tokenId, conf.get("hawkIdSecret")), callback); 60 | } 61 | 62 | function getOAuthHawkSession(tokenId, callback) { 63 | var hawkIdHmac = hmac(tokenId, conf.get("hawkIdSecret")); 64 | storage.getHawkOAuthState(hawkIdHmac, function(err, state) { 65 | if (err) return callback(err); 66 | if (state === null) { 67 | // This means it is not an OAuth session 68 | callback(null, null); 69 | return; 70 | } 71 | storage.getHawkSession(hawkIdHmac, callback); 72 | }); 73 | } 74 | 75 | function createHawkSession(tokenId, authKey, callback) { 76 | var hawkIdHmac = hmac(tokenId, conf.get("hawkIdSecret")); 77 | storage.setHawkSession(hawkIdHmac, authKey, function(err) { 78 | if (statsdClient && err === null) { 79 | statsdClient.increment('loop.activated-users'); 80 | } 81 | callback(err); 82 | }); 83 | } 84 | 85 | function hawkSendError(res, status, payload) { 86 | var errno = errors.INVALID_AUTH_TOKEN; 87 | if (status === 503) { 88 | errno = errors.BACKEND; 89 | } 90 | sendError(res, status, errno, payload); 91 | } 92 | 93 | /** 94 | * Middleware that requires a valid hawk session. 95 | **/ 96 | var requireHawkSession = hawk.getMiddleware({ 97 | hawkOptions: hawkOptions, 98 | getSession: getHawkSession, 99 | setUser: setUser, 100 | sendError: hawkSendError 101 | }); 102 | 103 | var requireRegisteredUser = function(req, res, next) { 104 | storage.getHawkUser(req.hawkIdHmac, function(err, user) { 105 | if (res.serverError(err)) return; 106 | if (user === null) { 107 | sendError(res, 403, errors.INVALID_AUTH_TOKEN, 108 | "You should be a registered user to perform this action."); 109 | return; 110 | } 111 | next(); 112 | }); 113 | }; 114 | 115 | /** 116 | * Middleware that uses a valid hawk session or create one if none already 117 | * exist. 118 | **/ 119 | var attachOrCreateHawkSession = hawk.getMiddleware({ 120 | hawkOptions: hawkOptions, 121 | getSession: getHawkSession, 122 | createSession: createHawkSession, 123 | setUser: setUser, 124 | sendError: hawkSendError 125 | }); 126 | 127 | /** 128 | * Middleware that requires a valid OAuth hawk session. 129 | **/ 130 | var requireOAuthHawkSession = hawk.getMiddleware({ 131 | hawkOptions: hawkOptions, 132 | getSession: getOAuthHawkSession, 133 | setUser: setUser, 134 | sendError: hawkSendError 135 | }); 136 | 137 | /** 138 | * Middleware that uses a valid OAuth hawk session or create one if none already 139 | * exist. 140 | **/ 141 | var attachOrCreateOAuthHawkSession = hawk.getMiddleware({ 142 | hawkOptions: hawkOptions, 143 | getSession: getOAuthHawkSession, 144 | createSession: createHawkSession, 145 | setUser: setUser, 146 | sendError: hawkSendError 147 | }); 148 | 149 | /** 150 | * Middleware that requires a valid FxA assertion. 151 | * 152 | * In case of success, return an hawk session token in the headers. 153 | **/ 154 | var requireFxA = fxa.getMiddleware({ 155 | audiences: conf.get('fxaAudiences'), 156 | trustedIssuers: conf.get('fxaTrustedIssuers') 157 | }, 158 | function(req, res, assertion, next) { 159 | var idpClaims = assertion.idpClaims; 160 | 161 | var identifier = idpClaims['fxa-verifiedEmail'] || 162 | idpClaims.verifiedMSISDN; 163 | 164 | if (identifier === undefined) { 165 | logError(new Error("Assertion is invalid: " + assertion)); 166 | sendError(res, 400, errors.INVALID_AUTH_TOKEN, 167 | "BrowserID assertion is invalid"); 168 | return; 169 | } 170 | 171 | var userHmac = hmac(identifier.toLowerCase(), conf.get('userMacSecret')); 172 | 173 | // generate the hawk session. 174 | hawk.generateHawkSession(createHawkSession, 175 | function(err, tokenId, authKey, sessionToken) { 176 | if (res.serverError(err)) return; 177 | var hawkIdHmac = hmac(tokenId, conf.get("hawkIdSecret")); 178 | var encryptedIdentifier = encrypt(tokenId, identifier); 179 | storage.setHawkUser(userHmac, hawkIdHmac, function(err) { 180 | if (res.serverError(err)) return; 181 | storage.setHawkUserId(hawkIdHmac, encryptedIdentifier, 182 | function(err) { 183 | if (res.serverError(err)) return; 184 | 185 | // return hawk credentials. 186 | hawk.setHawkHeaders(res, sessionToken); 187 | req.hawkIdHmac = hawkIdHmac; 188 | req.user = userHmac; 189 | req.userType = USER_TYPES.REGISTERED; 190 | next(); 191 | }); 192 | }); 193 | } 194 | ); 195 | } 196 | ); 197 | 198 | 199 | function requireBasicAuthToken(req, res, next) { 200 | var authorization, policy, splitted, token; 201 | 202 | authorization = req.headers.authorization; 203 | 204 | if (authorization === undefined) { 205 | unauthorized(res, ["Basic"]); 206 | return; 207 | } 208 | 209 | splitted = authorization.split(" "); 210 | if (splitted.length !== 2) { 211 | unauthorized(res, ["Basic"]); 212 | return; 213 | } 214 | 215 | policy = splitted[0]; 216 | token = new Buffer(splitted[1], 'base64').toString().replace(/:$/g, ''); 217 | 218 | if (policy.toLowerCase() !== 'basic') { 219 | unauthorized(res, ["Basic"], "Unsupported"); 220 | return; 221 | } 222 | 223 | var tokenHmac = hmac(token, conf.get('userMacSecret')); 224 | 225 | // req.token is the roomToken, tokenHmac is the user authentication token. 226 | storage.isRoomAccessTokenValid(req.token, tokenHmac, function(err, isValid) { 227 | if (res.serverError(err)) return; 228 | if (!isValid) { 229 | unauthorized(res, ["Basic"], "Invalid token; it may have expired."); 230 | return; 231 | } 232 | req.participantTokenHmac = tokenHmac; 233 | req.userType = USER_TYPES.UNAUTHENTICATED; 234 | next(); 235 | }); 236 | } 237 | 238 | /** 239 | * Middleware that requires either BrowserID, Hawk, or nothing. 240 | * 241 | * In case no authenticate scheme is provided, creates and return a new hawk 242 | * session. 243 | **/ 244 | 245 | function getAuthenticate(supported, resolve, reject) { 246 | return function authenticate(req, res, next) { 247 | // First thing: check that the headers are valid. Otherwise 401. 248 | var authorization = req.headers.authorization; 249 | 250 | if (authorization !== undefined) { 251 | var splitted = authorization.split(" "); 252 | var policy = splitted[0]; 253 | 254 | // Next, let's check which one the user wants to use. 255 | if (supported.map(function(s) { return s.toLowerCase(); }) 256 | .indexOf(policy.toLowerCase()) === -1) { 257 | unauthorized(res, supported, "Unsupported"); 258 | return; 259 | } 260 | 261 | resolve(policy, req, res, next); 262 | } else { 263 | if (reject !== undefined) { 264 | // Handle unauthenticated. 265 | reject(req, res, next); 266 | } else { 267 | // Accept unauthenticated 268 | next(); 269 | } 270 | } 271 | }; 272 | } 273 | 274 | var authenticate = getAuthenticate(["BrowserID", "Hawk"], 275 | function(policy, req, res, next) { 276 | if (policy.toLowerCase() === "browserid") { 277 | // If that's BrowserID, then check and create hawk credentials, plus 278 | // return them. 279 | requireFxA(req, res, next); 280 | } else if (policy.toLowerCase() === "hawk") { 281 | // If that's Hawk, let's check they're valid. 282 | requireHawkSession(req, res, next); 283 | } 284 | }, function(req, res, next) { 285 | // If unauthenticated create a new Hawk Session 286 | attachOrCreateHawkSession(req, res, next); 287 | }); 288 | 289 | var authenticateWithHawkOrToken = getAuthenticate(["Basic", "Hawk"], 290 | function(policy, req, res, next) { 291 | if (policy.toLowerCase() === "basic") { 292 | // If that's Basic, then check if the token is right 293 | requireBasicAuthToken(req, res, next); 294 | } else if (policy.toLowerCase() === "hawk") { 295 | // If that's Hawk, let's check they're valid. 296 | requireHawkSession(req, res, next); 297 | } 298 | }); 299 | 300 | return { 301 | authenticate: authenticate, 302 | authenticateWithHawkOrToken: authenticateWithHawkOrToken, 303 | requireHawkSession: requireHawkSession, 304 | attachOrCreateHawkSession: attachOrCreateHawkSession, 305 | requireOAuthHawkSession: requireOAuthHawkSession, 306 | attachOrCreateOAuthHawkSession: attachOrCreateOAuthHawkSession, 307 | requireFxA: requireFxA, 308 | requireRegisteredUser: requireRegisteredUser, 309 | requireBasicAuthToken: requireBasicAuthToken, 310 | unauthorized: unauthorized 311 | }; 312 | }; 313 | -------------------------------------------------------------------------------- /loop/constants.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | module.exports = { 8 | CALL_STATES: { 9 | INIT: "init", 10 | HALF_INITIATED: "half-initiated", 11 | ALERTING: "alerting", 12 | CONNECTING: "connecting", 13 | HALF_CONNECTED: "half-connected", 14 | CONNECTED: "connected", 15 | TERMINATED: "terminated" 16 | }, 17 | MESSAGE_EVENTS: { 18 | ACCEPT: "accept", 19 | MEDIA_UP: "media-up", 20 | TERMINATE: "terminate" 21 | }, 22 | MESSAGE_TYPES: { 23 | HELLO: "hello", 24 | ACTION: "action", 25 | PROGRESS: "progress", 26 | ECHO: "echo", 27 | ERROR: "error" 28 | }, 29 | MESSAGE_REASONS: { 30 | BUSY: "busy", 31 | CANCEL: "cancel", 32 | TIMEOUT: "timeout", 33 | CLOSED: "closed", 34 | ANSWERED_ELSEWHERE: "answered-elsewhere" 35 | }, 36 | ERROR_REASONS: { 37 | BAD_AUTHENTICATION: "bad authentication", 38 | BAD_CALLID: "bad callId", 39 | BAD_REASON: "Invalid reason: should be alphanumeric" 40 | }, 41 | USER_TYPES: { 42 | REGISTERED: "Registered", 43 | UNREGISTERED: "Unregistered", 44 | UNAUTHENTICATED: "Link-clicker" 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /loop/encrypt.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | "use strict"; 5 | 6 | var sodium = require("sodium"); 7 | 8 | 9 | /** 10 | * Encrypt a text with a given passphrase. 11 | * 12 | * If the string to encrypt is null, throws an error. 13 | * 14 | * @param {String} passphrase; 15 | * @param {String} text. 16 | * @return {String} encrypted text. 17 | */ 18 | function encrypt(passphrase, text) { 19 | // Handle null 20 | if (text === null) { 21 | throw new Error("Text is empty"); 22 | } 23 | var box = new sodium.SecretBox(passphrase); 24 | var encrypted = box.encrypt(text, "utf8"); 25 | var data = { 26 | cipherText: encrypted.cipherText.toString("base64"), 27 | nonce: encrypted.nonce.toString("base64") 28 | }; 29 | return JSON.stringify(data); 30 | } 31 | 32 | /** 33 | * Decrypts a given text using a given passphrase. 34 | * 35 | * If the encrypted string is null, throws an error. 36 | * 37 | * @param {String} passphrase; 38 | * @param {String} encrypted text. 39 | * @return {String} decrypted text. 40 | */ 41 | function decrypt(passphrase, encryptedString) { 42 | // Handle null 43 | if (encryptedString === null) { 44 | throw new Error("Encrypted string is empty"); 45 | } 46 | 47 | var encrypted = JSON.parse(encryptedString); 48 | var data = {}; 49 | data.cipherText = new Buffer(encrypted.cipherText, "base64"); 50 | data.nonce = new Buffer(encrypted.nonce, "base64"); 51 | var box = new sodium.SecretBox(passphrase); 52 | return box.decrypt(data, "utf8"); 53 | } 54 | 55 | module.exports = { 56 | encrypt: encrypt, 57 | decrypt: decrypt 58 | }; 59 | -------------------------------------------------------------------------------- /loop/errno.json: -------------------------------------------------------------------------------- 1 | { 2 | "INVALID_TOKEN": 105, 3 | "BADJSON": 106, 4 | "INVALID_PARAMETERS": 107, 5 | "MISSING_PARAMETERS": 108, 6 | "INVALID_AUTH_TOKEN": 110, 7 | "EXPIRED": 111, 8 | "REQUEST_TOO_LARGE": 113, 9 | "INVALID_OAUTH_STATE": 114, 10 | "CLIENT_REACHED_CAPACITY": 121, 11 | "USER_UNAVAILABLE": 122, 12 | "BACKEND": 201, 13 | "ROOM_FULL": 202, 14 | "NOT_ROOM_PARTICIPANT": 203, 15 | "NO_LONGER_SUPPORTED": 204, 16 | "UNDEFINED": 999 17 | } 18 | -------------------------------------------------------------------------------- /loop/filestorage/aws.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | var aws = require('aws-sdk'); 7 | var encode = require('../utils').encode; 8 | var decode = require('../utils').decode; 9 | var isUndefined = require('../utils').isUndefined; 10 | 11 | var DEFAULT_PUBLIC_BUCKET = "room_encrypted_files"; 12 | var CONTENT_TYPE = "application/json"; 13 | 14 | function AwsDriver(settings, options, statsdClient) { 15 | this.statsdClient = statsdClient; 16 | this._settings = settings || {}; 17 | this._publicBucket = settings.bucketName || DEFAULT_PUBLIC_BUCKET; 18 | if (!/^[a-zA-Z0-9_\-]+$/.test(this._publicBucket)) { 19 | throw new Error('Illegal Bucket Name: ' + this._publicBucket); 20 | } 21 | this._s3 = new aws.S3(); 22 | } 23 | 24 | AwsDriver.prototype = { 25 | 26 | /** 27 | * Create or override a file. 28 | * 29 | * @param {String} filename, the filename of the object to store. 30 | * @param {String} body, the content of the file to store 31 | * @param {Function} A callback that will be called once data had been 32 | * stored. 33 | **/ 34 | write: function(filename, body, callback) { 35 | if (isUndefined(filename, "filename", callback)) return; 36 | if (body === undefined) return callback(null, null); 37 | 38 | var s3 = this._s3; 39 | var self = this; 40 | var startTime = Date.now(); 41 | s3.putObject({ 42 | Body: encode(body), 43 | Bucket: this._publicBucket, 44 | Key: filename, 45 | ContentType: CONTENT_TYPE 46 | }, function(err) { 47 | if (err) { 48 | err.message = err.message + "(bucket name: " + self._publicBucket + ")"; 49 | return callback(err); 50 | } 51 | if (self.statsdClient !== undefined) { 52 | self.statsdClient.timing( 53 | 'loop.aws.write', 54 | Date.now() - startTime 55 | ); 56 | } 57 | callback(null, filename); 58 | }); 59 | }, 60 | 61 | /** 62 | * Read a given file. 63 | * 64 | * @param {String} filename, the filename of the object to read. 65 | * @param {Function} A callback that will be called once data had been 66 | * read. 67 | **/ 68 | read: function(filename, callback) { 69 | var s3 = this._s3; 70 | var self = this; 71 | var startTime = Date.now(); 72 | s3.getObject({ 73 | Bucket: self._publicBucket, 74 | Key: filename 75 | }, function(err, data) { 76 | if (err) { 77 | if (err.code !== "NoSuchKey") { 78 | err.message = err.message + "(bucket name: " + self._publicBucket + ")"; 79 | return callback(err); 80 | } 81 | return callback(null, null); 82 | } 83 | var body = data.Body.toString(); 84 | if (self.statsdClient !== undefined) { 85 | self.statsdClient.timing( 86 | 'loop.aws.read', 87 | Date.now() - startTime 88 | ); 89 | } 90 | decode(body, callback); 91 | }); 92 | }, 93 | 94 | /** 95 | * Remove a given file. 96 | * 97 | * @param {String} filename, the filename of the object to remove. 98 | * @param {Function} A callback that will be called once data had been 99 | * removed. 100 | **/ 101 | remove: function(filename, callback) { 102 | var s3 = this._s3; 103 | var self = this; 104 | var startTime = Date.now(); 105 | s3.deleteObject({ 106 | Bucket: this._publicBucket, 107 | Key: filename 108 | }, function(err) { 109 | if (err && err.code !== "NoSuchKey") { 110 | err.message = err.message + "(bucket name: " + self._publicBucket + ")"; 111 | return callback(err); 112 | } 113 | if (self.statsdClient !== undefined) { 114 | self.statsdClient.timing( 115 | 'loop.aws.remove', 116 | Date.now() - startTime 117 | ); 118 | } 119 | callback(null, filename); 120 | }); 121 | } 122 | }; 123 | 124 | module.exports = AwsDriver; 125 | -------------------------------------------------------------------------------- /loop/filestorage/filesystem.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | var crypto = require('crypto'); 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | var encode = require('../utils').encode; 10 | var decode = require('../utils').decode; 11 | var isUndefined = require('../utils').isUndefined; 12 | 13 | function Filesystem(settings, options, statsdClient) { 14 | this.statsdClient = statsdClient; 15 | this._base_dir = settings.base_dir; 16 | } 17 | 18 | Filesystem.prototype = { 19 | 20 | /** 21 | * Create or override a file. 22 | * 23 | * @param {String} filename, the filename of the object to store. 24 | * @param {String} body, the content of the file to store 25 | * @param {Function} A callback that will be called once data had been 26 | * stored. 27 | **/ 28 | write: function(filename, body, callback) { 29 | if (isUndefined(filename, "filename", callback)) return; 30 | if (body === undefined) return callback(null, null); 31 | var file_path = this.buildPath(filename); 32 | var self = this; 33 | var startTime = Date.now(); 34 | fs.mkdir(path.dirname(file_path), '0750', function(err) { 35 | if (err && err.code !== 'EEXIST') return callback(err); 36 | fs.writeFile(file_path, encode(body), function(err) { 37 | if (err) return callback(err); 38 | if (self.statsdClient !== undefined) { 39 | self.statsdClient.timing( 40 | 'loop.filesystem.write', 41 | Date.now() - startTime 42 | ); 43 | } 44 | callback(); 45 | }); 46 | }); 47 | }, 48 | 49 | /** 50 | * Read a given file. 51 | * 52 | * @param {String} filename, the filename of the object to store. 53 | * @param {String} body, the content of the file to store 54 | * @param {Function} A callback that will be called once data had been 55 | * stored. 56 | **/ 57 | read: function(filename, callback) { 58 | var self = this; 59 | var startTime = Date.now(); 60 | fs.readFile(self.buildPath(filename), function(err, data) { 61 | if (err) { 62 | if (err.code === "ENOENT") return callback(null, null); 63 | return callback(err); 64 | } 65 | decode(data, function(err, data) { 66 | if (err) return callback(err); 67 | if (self.statsdClient !== undefined) { 68 | self.statsdClient.timing( 69 | 'loop.filesystem.read', 70 | Date.now() - startTime 71 | ); 72 | } 73 | callback(null, data); 74 | }); 75 | }); 76 | }, 77 | 78 | /** 79 | * Remove a given file. 80 | * 81 | * @param {String} filename, the filename of the object to store. 82 | * @param {String} body, the content of the file to store 83 | * @param {Function} A callback that will be called once data had been 84 | * stored. 85 | **/ 86 | remove: function(filename, callback) { 87 | var self = this; 88 | var startTime = Date.now(); 89 | fs.unlink(this.buildPath(filename), function(err) { 90 | if (err && err.code !== "ENOENT") return callback(err); 91 | if (self.statsdClient !== undefined) { 92 | self.statsdClient.timing( 93 | 'loop.filesystem.remove', 94 | Date.now() - startTime 95 | ); 96 | } 97 | callback(); 98 | }); 99 | }, 100 | 101 | /** 102 | * Build a path for the given filename (with a hash of the filename). 103 | * 104 | * @param {String} filename, the filename of the object to store. 105 | * @param {String} body, the content of the file to store 106 | * @param {Function} A callback that will be called once data had been 107 | * stored. 108 | **/ 109 | buildPath: function(filename) { 110 | var shasum = crypto 111 | .createHash("sha256") 112 | .update(filename) 113 | .digest() 114 | .toString('hex'); 115 | return path.join(this._base_dir, 116 | shasum.substring(0, 3), 117 | shasum.substring(3)); 118 | } 119 | }; 120 | 121 | module.exports = Filesystem; 122 | -------------------------------------------------------------------------------- /loop/filestorage/index.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | function getFileStorage(conf, options, statsdClient) { 8 | var engine = conf.engine || 'filesystem'; 9 | var settings = conf.settings || {}; 10 | 11 | var Storage = require('./' + engine + '.js'); 12 | return new Storage(settings, options, statsdClient); 13 | } 14 | 15 | module.exports = getFileStorage; 16 | -------------------------------------------------------------------------------- /loop/fxa.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var https = require('https'); 8 | var request = require('request'); 9 | var conf = require('./config').conf; 10 | var atob = require('atob'); 11 | var sendError = require("./utils").sendError; 12 | var errors = require("./errno"); 13 | 14 | // Don't be limited by the default node.js HTTP agent. 15 | var agent = new https.Agent(); 16 | agent.maxSockets = 1000000; 17 | 18 | /** 19 | * Helper function. Get the audience from the given assertion. 20 | * 21 | * @param {String} assertion The assertion to unpack 22 | * 23 | * @return {Object} the audience of this assertion. 24 | */ 25 | exports.getAssertionAudience = function(assertion) { 26 | var parts = assertion.split('.'); 27 | return JSON.parse(atob(parts[3])).aud; 28 | }; 29 | 30 | /** 31 | * Verifies that the assertion is a valid one, given an audience and a set of 32 | * trusted issuers. 33 | * 34 | * @param {String} assertion, Assertion to check the validity of. 35 | * @param {String} audience, Audience of the given assertion. 36 | * @param {Array} trustedIssuers, A list of trusted issuers. 37 | * @param {Function} callback, a callback that's given the validated assertion. 38 | * Signature is (err, assertion); 39 | **/ 40 | function verifyAssertion(assertion, audiences, trustedIssuers, callback) { 41 | // ensure audiences is an array. 42 | if (Object.prototype.toString.call(audiences) !== '[object Array]' ) { 43 | throw new Error("The 'audiences' parameter should be an array"); 44 | } 45 | try { 46 | var assertionAudience = exports.getAssertionAudience(assertion); 47 | } catch (e) { 48 | callback(new Error("Malformed audience")); 49 | return; 50 | } 51 | var audience; 52 | 53 | // Check we trust the audience of the assertion. 54 | var trustedAudienceIndex = audiences.indexOf(assertionAudience); 55 | if (trustedAudienceIndex !== -1) { 56 | audience = audiences[trustedAudienceIndex]; 57 | } else { 58 | callback(new Error("Invalid audience")); 59 | return; 60 | } 61 | 62 | request.post({ 63 | uri: conf.get('fxaVerifier'), 64 | json: { 65 | audience: audience, 66 | assertion: assertion 67 | } 68 | }, function(err, response, data) { 69 | if (err) return callback(err); 70 | if (data === undefined) { 71 | callback(new Error( 72 | "Verifier service unavailable: " + conf.get('fxaVerifier') + 73 | " returned a " + response.statusCode)); 74 | return; 75 | } 76 | // Check the issuer is trusted. 77 | if (data.status !== "okay") { 78 | callback(data.reason); 79 | return; 80 | } 81 | if (trustedIssuers.indexOf(data.issuer) === -1) { 82 | callback(new Error("Issuer is not trusted")); 83 | return; 84 | } 85 | callback(null, data); 86 | }); 87 | } 88 | 89 | 90 | /** 91 | * Express middleware doing BrowserID authentication. 92 | * 93 | * Checks the Authorization headers are set properly, and if not return 94 | * a 401 with according information. 95 | * 96 | * If the BrowserID assertion is parsed correctly, the user contained into this 97 | * one is set in the req.user property. 98 | */ 99 | function getMiddleware(conf, callback) { 100 | function requireBrowserID(req, res, next) { 101 | var authorization, assertion, policy, splitted; 102 | 103 | function _unauthorized(err){ 104 | var header = "BrowserID"; 105 | var message = err ? err.message : undefined; 106 | if (message) header += ' error="' + message.replace(/"/g, '\"') + '"'; 107 | res.set('WWW-Authenticate', header); 108 | sendError(res, 401, errors.INVALID_AUTH_TOKEN, message || "Unauthorized"); 109 | } 110 | 111 | authorization = req.headers.authorization; 112 | 113 | if (authorization === undefined) { 114 | _unauthorized(); 115 | return; 116 | } 117 | 118 | splitted = authorization.split(" "); 119 | if (splitted.length !== 2) { 120 | _unauthorized(); 121 | return; 122 | } 123 | 124 | policy = splitted[0]; 125 | assertion = splitted[1]; 126 | 127 | if (policy.toLowerCase() !== 'browserid') { 128 | _unauthorized("Unsupported"); 129 | return; 130 | } 131 | 132 | module.exports.verifyAssertion( 133 | assertion, conf.audiences, conf.trustedIssuers, 134 | function(err, data) { 135 | if (err) return _unauthorized(err); 136 | callback(req, res, data, next); 137 | }); 138 | } 139 | 140 | return requireBrowserID; 141 | } 142 | 143 | exports.getMiddleware = getMiddleware; 144 | exports.verifyAssertion = verifyAssertion; 145 | exports.request = request; 146 | -------------------------------------------------------------------------------- /loop/hmac.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var conf = require("./config").conf; 8 | var hexKeyOfSize = require('./config').hexKeyOfSize; 9 | var createHmac = require('crypto').createHmac; 10 | 11 | /** 12 | * Returns the HMac digest of the given payload. 13 | * 14 | * If no options are passed, the global configuration object is used to 15 | * determine which algorithm and secret should be used. 16 | * 17 | * @param {String} payload The string to mac. 18 | * @param {String} secret key encoded as hex. 19 | * @param {String} algorithm Algorithm to use (defaults to sha256). 20 | * @return {String} hexadecimal hash. 21 | **/ 22 | function hmac(payload, secret, algorithm) { 23 | if (secret === undefined) { 24 | throw new Error("You should provide a secret."); 25 | } 26 | 27 | // Test for secret size and validity 28 | hexKeyOfSize(16)(secret); 29 | 30 | if (algorithm === undefined) { 31 | algorithm = conf.get("userMacAlgorithm"); 32 | } 33 | var _hmac = createHmac(algorithm, new Buffer(secret, "hex")); 34 | _hmac.write(payload); 35 | _hmac.end(); 36 | return _hmac.read().toString('hex'); 37 | } 38 | 39 | module.exports = hmac; 40 | -------------------------------------------------------------------------------- /loop/index.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var heapdump = require('heapdump'); 8 | var util = require('util'); 9 | var path = require('path'); 10 | 11 | var conf = require('./config').conf; 12 | 13 | if (conf.get('newRelic').activated) { 14 | require('newrelic'); 15 | } 16 | 17 | // Configure http agents to use more than the default number of sockets. 18 | var http = require('http'); 19 | var https = require('https'); 20 | https.globalAgent.maxSockets = conf.get('maxHTTPSockets'); 21 | http.globalAgent.maxSockets = conf.get('maxHTTPSockets'); 22 | 23 | var express = require('express'); 24 | var bodyParser = require('body-parser'); 25 | var raven = require('raven'); 26 | var cors = require('cors'); 27 | var StatsdClient = require('node-statsd'); 28 | 29 | var PubSub = require('./pubsub'); 30 | 31 | var middlewares = require('./middlewares'); 32 | var websockets = require('./websockets'); 33 | var hekaLogger = middlewares.hekaLogger; 34 | 35 | var TokBox; 36 | 37 | if (conf.get("fakeTokBox")) { 38 | hekaLogger.debug("server", "Calls to TokBox are now mocked."); 39 | TokBox = require('./tokbox').FakeTokBox; 40 | } else { 41 | TokBox = require('./tokbox').TokBox; 42 | } 43 | 44 | var getStorage = require('./storage'); 45 | var storage = getStorage(conf.get("storage"), { 46 | 'tokenDuration': conf.get('tokBox').tokenDuration, 47 | 'roomExtendTTL': conf.get('rooms').extendTTL, 48 | 'hawkSessionDuration': conf.get('hawkSessionDuration'), 49 | 'callDuration': conf.get('callDuration'), 50 | 'roomsDeletedTTL': conf.get('rooms').deletedTTL 51 | }); 52 | 53 | var statsdClient; 54 | if (conf.get('statsdEnabled') === true) { 55 | statsdClient = new StatsdClient(conf.get('statsd')); 56 | } 57 | 58 | 59 | var getFileStorage = require('./filestorage'); 60 | var filestorage = getFileStorage( 61 | conf.get("filestorage"), 62 | {}, 63 | statsdClient 64 | ); 65 | 66 | var tokBox = new TokBox(conf.get('tokBox'), statsdClient); 67 | 68 | var ravenClient = new raven.Client(conf.get('sentryDSN')); 69 | 70 | var startupMessage = 'Server was able to communicate with Sentry'; 71 | ravenClient.captureMessage(startupMessage, {level: 'info'}); 72 | 73 | function logError(err) { 74 | if (conf.get('env') !== 'test') { 75 | hekaLogger.debug("error", err); 76 | } 77 | ravenClient.captureError(err); 78 | } 79 | 80 | var Notifications = require("./notifications"); 81 | var notifications = new Notifications(new PubSub(conf.get('pubsub'), logError)); 82 | 83 | var corsEnabled = cors({ 84 | origin: function(origin, callback) { 85 | var allowedOrigins = conf.get('allowedOrigins'); 86 | var acceptedOrigin = allowedOrigins.indexOf('*') !== -1 || 87 | allowedOrigins.indexOf(origin) !== -1; 88 | callback(null, acceptedOrigin); 89 | } 90 | }); 91 | 92 | var SimplePush = require("./simplepush"); 93 | var simplePush = new SimplePush(statsdClient, logError); 94 | 95 | 96 | var app = express(); 97 | 98 | /** 99 | * Enable CORS for all requests. 100 | **/ 101 | app.use(corsEnabled); 102 | app.use(middlewares.addHeaders); 103 | app.disable('x-powered-by'); 104 | 105 | app.use(bodyParser.json()); 106 | app.use(bodyParser.urlencoded({extended: false})); 107 | app.use(middlewares.handle503(logError)); 108 | app.use(middlewares.logMetrics); 109 | 110 | var apiRouter = express.Router(); 111 | var loopPackageData = require('../package.json'); 112 | var apiPrefix = "/v" + loopPackageData.version.split(".")[0]; 113 | var authMiddlewares = require("./auth"); 114 | var auth = authMiddlewares(conf, logError, storage, statsdClient); 115 | 116 | var getValidators = require("./routes/validators"); 117 | var validators = getValidators(conf, logError, storage); 118 | 119 | var home = require("./routes/home"); 120 | home(apiRouter, conf, logError, storage, tokBox, statsdClient); 121 | 122 | var registration = require("./routes/registration"); 123 | registration(apiRouter, conf, logError, storage, auth, validators); 124 | 125 | var account = require("./routes/account"); 126 | account(apiRouter, storage, auth); 127 | 128 | var callUrl = require("./routes/call-url"); 129 | callUrl(apiRouter, conf, logError, storage, auth, validators, statsdClient); 130 | 131 | var calls = require("./routes/calls"); 132 | var storeUserCallTokens = calls(apiRouter, conf, logError, storage, tokBox, 133 | simplePush, auth, validators); 134 | 135 | var pushServerConfig = require("./routes/push-server-config"); 136 | pushServerConfig(apiRouter, conf); 137 | 138 | if (conf.get("fxaOAuth").activated !== false) { 139 | var fxaOAuth = require("./routes/fxa-oauth"); 140 | fxaOAuth(apiRouter, conf, logError, storage, auth, validators); 141 | } 142 | 143 | var rooms = require("./routes/rooms"); 144 | rooms(apiRouter, conf, logError, storage, filestorage, auth, validators, tokBox, 145 | simplePush, notifications, statsdClient); 146 | 147 | var analytics = require("./routes/analytics").analytics; 148 | analytics(apiRouter, conf, auth, validators); 149 | 150 | var session = require("./routes/session"); 151 | session(apiRouter, conf, storage, auth); 152 | 153 | var videur = require("./routes/videur"); 154 | videur(apiRouter, conf); 155 | 156 | 157 | app.use(apiPrefix, apiRouter); 158 | app.use("/", apiRouter); 159 | 160 | // Exception logging should come at the end of the list of middlewares. 161 | app.use(raven.middleware.express(conf.get('sentryDSN'))); 162 | 163 | // Proceed with extra care if you change the order of these middlwares. 164 | // Redirect must happen last. 165 | app.use(middlewares.handleRedirects(apiPrefix)); 166 | app.use(middlewares.handleUncatchedErrors); 167 | 168 | // Starts HTTP server. 169 | var argv = require('yargs').argv; 170 | var server = http.createServer(app); 171 | 172 | if (argv.hasOwnProperty("fd")) { 173 | var fd = parseInt(argv.fd, 10); 174 | server.listen({fd: fd}, function() { 175 | hekaLogger.debug("server", 'Server listening on fd://' + fd); 176 | }); 177 | } else { 178 | server.listen(conf.get('port'), conf.get('ip'), conf.get('acceptBacklog'), function() { 179 | hekaLogger.debug("server", 'Server listening on http://' + 180 | conf.get('ip') + ':' + conf.get('port')); 181 | }); 182 | } 183 | 184 | // Handle websockets. 185 | var ws = websockets(storage, logError, conf); 186 | try { 187 | ws.register(server); 188 | } catch (e) { 189 | logError(e); 190 | } 191 | 192 | // Handle SIGTERM signal. 193 | function shutdown(callback) { 194 | server.close(function() { 195 | process.exit(0); 196 | if (callback !== undefined) { 197 | callback(); 198 | } 199 | }); 200 | } 201 | 202 | process.on('SIGTERM', shutdown); 203 | 204 | process.on('uncaughtException', function(err) { 205 | ravenClient.captureError(err); 206 | if (conf.get('dumpHeap').activated === true) { 207 | var dir = conf.get('dumpHeap').location; 208 | var now = Date.now(); 209 | var nanos = process.hrtime()[1]; 210 | var filename = util.format('%s-%s.heapsnapshot', now, nanos); 211 | 212 | heapdump.writeSnapshot(path.join(dir, filename), function(err) { 213 | if (err) { 214 | ravenClient.captureError(err); 215 | } 216 | process.exit(1); 217 | }); 218 | } 219 | }); 220 | 221 | module.exports = { 222 | app: app, 223 | apiRouter: apiRouter, 224 | apiPrefix: apiPrefix, 225 | server: server, 226 | conf: conf, 227 | storage: storage, 228 | filestorage: filestorage, 229 | tokBox: tokBox, 230 | statsdClient: statsdClient, 231 | shutdown: shutdown, 232 | storeUserCallTokens: storeUserCallTokens, 233 | auth: auth, 234 | validators: validators 235 | }; 236 | -------------------------------------------------------------------------------- /loop/logger.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var conf = require("./config").conf; 8 | var mozlog = require('mozlog'); 9 | var loopPackageData = require('../package.json'); 10 | 11 | var metricsFileParams = JSON.parse(JSON.stringify(conf.get('hekaMetrics'))); 12 | delete metricsFileParams.activated; 13 | if (metricsFileParams.debug === true) { 14 | metricsFileParams.level = "DEBUG"; 15 | } 16 | metricsFileParams.app = loopPackageData.name; 17 | 18 | mozlog.config(metricsFileParams); 19 | 20 | exports.hekaLogger = mozlog(); 21 | -------------------------------------------------------------------------------- /loop/middlewares.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var conf = require("./config").conf; 8 | var loopPackageData = require('../package'); 9 | var os = require("os"); 10 | 11 | // Assume the hostname will not change once the server is launched. 12 | var hostname = os.hostname(); 13 | var sendError = require("./utils").sendError; 14 | var isoDateString = require("./utils").isoDateString; 15 | var errors = require("./errno.json"); 16 | var hekaLogger = require('./logger').hekaLogger; 17 | var logging = require("express-logging"); 18 | var time = require('./utils').time; 19 | var USER_TYPES = require('./constants').USER_TYPES; 20 | 21 | function handle503(logError) { 22 | return function UnavailableService(req, res, next) { 23 | res.serverError = function raiseError(error) { 24 | if (error) { 25 | logError(error); 26 | sendError(res, 503, errors.BACKEND, "Service Unavailable"); 27 | return true; 28 | } 29 | return false; 30 | }; 31 | 32 | next(); 33 | }; 34 | } 35 | 36 | function addHeaders(req, res, next) { 37 | /* Make sure we don't decorate the writeHead more than one time. */ 38 | if (res._headersMiddleware) { 39 | next(); 40 | return; 41 | } 42 | 43 | var writeHead = res.writeHead; 44 | res._headersMiddleware = true; 45 | res.writeHead = function headersWriteHead() { 46 | if (res.statusCode === 200 || res.statusCode === 401) { 47 | res.setHeader('Timestamp', time()); 48 | } 49 | 50 | if (res.statusCode === 503 || res.statusCode === 429) { 51 | res.setHeader('Retry-After', conf.get('retryAfter')); 52 | } 53 | writeHead.apply(res, arguments); 54 | }; 55 | next(); 56 | } 57 | 58 | 59 | function logMetrics(req, res, next) { 60 | if (conf.get('hekaMetrics').activated === true) { 61 | res.on('finish', function() { 62 | var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; 63 | var action; 64 | if (req.body && req.body.action) { 65 | action = req.body.action; 66 | } 67 | 68 | var line = { 69 | code: res.statusCode, 70 | path: req.path, 71 | method: req.method.toLowerCase(), 72 | query: req.query, 73 | agent: req.headers['user-agent'], 74 | time: isoDateString(new Date()), 75 | uid: req.user, 76 | userType: req.userType || USER_TYPES.UNAUTHENTICATED, 77 | callId: req.callId, 78 | token: req.token, 79 | v: loopPackageData.version, 80 | hostname: hostname, 81 | lang: req.headers["accept-language"], 82 | ip: ip, 83 | errno: res.errno || 0, 84 | action: action 85 | }; 86 | 87 | if (req.headers.hasOwnProperty("x-loop-addon-ver")) { 88 | line.loopAddonVersion = req.headers["x-loop-addon-ver"]; 89 | } 90 | 91 | if (req.hasOwnProperty("callUrlData")) { 92 | line.calleeId = req.callUrlData.userMac; 93 | line.callerId = req.user; 94 | } 95 | 96 | if (req.hasOwnProperty("roomStorageData")) { 97 | if (req.roomStorageData.hasOwnProperty("participants")) { 98 | line.participants = req.roomStorageData.participants.length; 99 | } 100 | 101 | line.sessionId = req.roomStorageData.sessionId; 102 | 103 | if (req.roomStorageData.hasOwnProperty("sessionToken")) { 104 | line.sessionToken = req.roomStorageData.sessionToken; 105 | } 106 | } 107 | 108 | if (req.hasOwnProperty("roomConnectionId")) { 109 | line.roomConnectionId = req.roomConnectionId; 110 | } 111 | 112 | if (req.hasOwnProperty("roomParticipantsCount")) { 113 | line.participants = req.roomParticipantsCount; 114 | } 115 | 116 | if (req.hasOwnProperty("roomStatusData")) { 117 | Object.keys(req.roomStatusData).forEach(function(k) { 118 | line[k] = req.roomStatusData[k]; 119 | }); 120 | } 121 | 122 | if (req.hasOwnProperty("roomToken")) { 123 | line.roomToken = req.roomToken; 124 | } 125 | if (res.statusCode === 401) { 126 | line.authorization = req.headers.authorization; 127 | line.hawk = req.hawk; 128 | line.error = res.get("www-authenticate"); 129 | } 130 | 131 | hekaLogger.info('request.summary', line); 132 | }); 133 | } 134 | if (conf.get('logRequests').activated === true) { 135 | logging(conf.get("logRequests").consoleDateFormat)(req, res, next); 136 | } else { 137 | next(); 138 | } 139 | } 140 | 141 | /** 142 | * Handle all the uncatched errors. 143 | * In this case, we want to catch them and return either a 500 or a 400 in case 144 | * the uncatched error was generated by a previous middleware. 145 | **/ 146 | function handleUncatchedErrors(error, req, res) { 147 | if (error && error.status === 400) { 148 | sendError(res, 400, errors.BAD_JSON, error.body); 149 | } else { 150 | sendError(res, 500, 999, "" + error); 151 | } 152 | } 153 | 154 | // In case of apiPrefix missing redirect the user to the right URL 155 | // In case of apiPrefix present, raise a 404 error. 156 | function handleRedirects(apiPrefix) { 157 | return function(req, res) { 158 | if (req.path.indexOf(apiPrefix) !== 0) { 159 | res.redirect(307, apiPrefix + req.path); 160 | return; 161 | } 162 | sendError(res, 404, 999, "Resource not found."); 163 | } 164 | } 165 | 166 | module.exports = { 167 | addHeaders: addHeaders, 168 | handle503: handle503, 169 | handleUncatchedErrors: handleUncatchedErrors, 170 | handleRedirects: handleRedirects, 171 | hekaLogger: hekaLogger, 172 | logMetrics: logMetrics 173 | }; 174 | -------------------------------------------------------------------------------- /loop/newrelic.js: -------------------------------------------------------------------------------- 1 | var conf = require('./config').conf.get('newRelic'); 2 | 3 | exports.config = { 4 | app_name: [conf.appName], 5 | license_key: conf.licenceKey, 6 | logging: { 7 | level: conf.loggingLevel 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /loop/notifications.js: -------------------------------------------------------------------------------- 1 | var Notifications = function(pubsub) { 2 | this.observers = {}; 3 | this.pubsub = pubsub; 4 | this.setupObservers(); 5 | } 6 | 7 | Notifications.prototype = { 8 | /** 9 | * Setup the pub/sub on redis expire keys. 10 | * 11 | * On each expire notification, trigger the appropriate observers. 12 | **/ 13 | setupObservers: function() { 14 | var self = this; 15 | 16 | self.pubsub.on("pmessage", function(pattern, channel, key) { 17 | Object.keys(self.observers).forEach(function(prefix) { 18 | if (key.indexOf(prefix) === 0) { 19 | self.observers[prefix](key); 20 | } 21 | }) 22 | }); 23 | 24 | self.pubsub.psubscribe("__keyevent*__:expired"); 25 | }, 26 | 27 | /** 28 | * Register a new observer. 29 | * 30 | * When a key expires, if it matches the given prefix, the given callback 31 | * will be triggered. 32 | **/ 33 | on: function(prefix, callback) { 34 | this.observers[prefix] = callback; 35 | } 36 | 37 | } 38 | 39 | module.exports = Notifications; 40 | -------------------------------------------------------------------------------- /loop/pubsub.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | var redis = require("redis"); 7 | 8 | /** 9 | * Pub/Sub implementation using Redis as a backend. 10 | **/ 11 | function RedisPubSub(options, logError) { 12 | var client = redis.createClient( 13 | options.port || 6379, 14 | options.host || "localhost", 15 | options.options 16 | ); 17 | if (options.db) { 18 | client.select(options.db); 19 | } 20 | 21 | // Let's report errors when they occur. 22 | client.on('error', logError); 23 | client.config('set', 'notify-keyspace-events', 'Ex', function(err) { 24 | if (err) logError(err); 25 | }); 26 | return client; 27 | } 28 | 29 | module.exports = RedisPubSub; 30 | -------------------------------------------------------------------------------- /loop/routes/account.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | 8 | module.exports = function (app, storage, auth) { 9 | /** 10 | * Delete an account and all data associated with it. 11 | **/ 12 | app.delete('/account', auth.requireHawkSession, function(req, res) { 13 | storage.deleteUserSimplePushURLs(req.user, function(err) { 14 | if (res.serverError(err)) return; 15 | storage.deleteUserCallUrls(req.user, function(err) { 16 | if (res.serverError(err)) return; 17 | storage.deleteUserCalls(req.user, function(err) { 18 | if (res.serverError(err)) return; 19 | storage.deleteHawkUserId(req.hawkIdHmac, function(err) { 20 | if (res.serverError(err)) return; 21 | storage.deleteHawkSession(req.hawkIdHmac, function(err) { 22 | if (res.serverError(err)) return; 23 | res.status(204).json(); 24 | }); 25 | }); 26 | }); 27 | }); 28 | }); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /loop/routes/analytics.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var errors = require('../errno'); 8 | var sendError = require('../utils').sendError; 9 | 10 | exports.sendAnalytics = require('../utils').sendAnalytics; 11 | 12 | exports.analytics = function (app, conf, auth, validators) { 13 | /** 14 | * Delete an account and all data associated with it. 15 | **/ 16 | app.post('/event', validators.requireParams('event', 'action', 'label'), 17 | auth.requireHawkSession, function(req, res) { 18 | var ga = conf.get("ga"); 19 | if (ga.activated) { 20 | module.exports.sendAnalytics(ga.id, req.user, req.body); 21 | res.status(204).json({}); 22 | } else { 23 | sendError( 24 | res, 405, errors.UNDEFINED, 25 | "Google Analytics events are not configured for this server." 26 | ); 27 | } 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /loop/routes/call-url.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var errors = require('../errno'); 8 | var sendError = require('../utils').sendError; 9 | 10 | module.exports = function (app, conf, logError, storage, auth, validators) { 11 | /** 12 | * Return the list of existing call-urls for this specific user. 13 | **/ 14 | app.get('/call-url', auth.requireHawkSession, function(req, res) { 15 | storage.getUserCallUrls(req.user, function(err, urls) { 16 | if (res.serverError(err)) return; 17 | var callUrlsData = urls.map(function(url) { 18 | delete url.userMac; 19 | return url; 20 | }); 21 | res.status(200).json(callUrlsData); 22 | }); 23 | }); 24 | 25 | /** 26 | * Generates and return a call-url for the given callerId. 27 | **/ 28 | app.post('/call-url', auth.requireHawkSession, 29 | function(req, res) { 30 | // The call-url endpoint is now deprecated. 31 | sendError(res, 405, errors.NO_LONGER_SUPPORTED, "No longer supported"); 32 | return; 33 | }); 34 | 35 | app.put('/call-url/:token', auth.requireHawkSession, 36 | validators.validateToken, validators.validateCallUrlParams, 37 | function(req, res) { 38 | storage.updateUserCallUrlData(req.user, req.token, req.urlData, 39 | function(err) { 40 | if (err && err.notFound === true) { 41 | sendError(res, 404, errors.INVALID_TOKEN, "Not Found."); 42 | return; 43 | } 44 | else if (res.serverError(err)) return; 45 | 46 | res.status(200).json({ 47 | expiresAt: req.urlData.expires 48 | }); 49 | }); 50 | }); 51 | 52 | /** 53 | * Revoke a given call url. 54 | **/ 55 | app.delete('/call-url/:token', auth.requireHawkSession, 56 | validators.validateToken, function(req, res) { 57 | if (req.callUrlData.userMac !== req.user) { 58 | sendError(res, 403, errors.INVALID_AUTH_TOKEN, "Forbidden"); 59 | return; 60 | } 61 | storage.revokeURLToken(req.token, function(err) { 62 | if (res.serverError(err)) return; 63 | res.status(204).json(); 64 | }); 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /loop/routes/fxa-oauth.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | 'use strict'; 6 | 7 | var randomBytes = require('crypto').randomBytes; 8 | var request = require('request'); 9 | var sendError = require('../utils').sendError; 10 | var errors = require('../errno'); 11 | var hmac = require('../hmac'); 12 | var encrypt = require('../encrypt').encrypt; 13 | 14 | module.exports = function (app, conf, logError, storage, auth, validators) { 15 | 16 | var oauthConf = conf.get('fxaOAuth'); 17 | 18 | /** 19 | * Provide the client with the parameters needed for the OAuth dance. 20 | **/ 21 | app.post('/fxa-oauth/params', auth.attachOrCreateOAuthHawkSession, 22 | function(req, res) { 23 | var callback = function(state) { 24 | res.status(200).json({ 25 | client_id: oauthConf.client_id, 26 | redirect_uri: oauthConf.redirect_uri, 27 | profile_uri: oauthConf.profile_uri, 28 | content_uri: oauthConf.content_uri, 29 | oauth_uri: oauthConf.oauth_uri, 30 | scope: oauthConf.scope, 31 | state: state 32 | }); 33 | }; 34 | storage.getHawkOAuthState(req.hawkIdHmac, function(err, state) { 35 | if (res.serverError(err)) return; 36 | if (state === null) { 37 | state = randomBytes(32).toString('hex'); 38 | storage.setHawkOAuthState(req.hawkIdHmac, state, function(err) { 39 | if (res.serverError(err)) return; 40 | callback(state); 41 | }); 42 | } else { 43 | callback(state); 44 | } 45 | }); 46 | }); 47 | 48 | /** 49 | * Returns the current status of the hawk session (e.g. if it's authenticated 50 | * or not. 51 | **/ 52 | app.get('/fxa-oauth/token', auth.requireOAuthHawkSession, function (req, res) { 53 | storage.getHawkOAuthToken(req.hawkIdHmac, function(err, token) { 54 | if (res.serverError(err)) return; 55 | res.status(200).json({ 56 | access_token: token || undefined 57 | }); 58 | }); 59 | }); 60 | 61 | /** 62 | * Trade an OAuth code with an oauth bearer token. 63 | **/ 64 | app.post('/fxa-oauth/token', auth.requireOAuthHawkSession, 65 | validators.requireParams('state', 'code'), function (req, res) { 66 | var state = req.body.state; 67 | var code = req.body.code; 68 | 69 | // State should match an existing state. 70 | storage.getHawkOAuthState(req.hawkIdHmac, function(err, storedState) { 71 | if (res.serverError(err)) return; 72 | 73 | var newState = randomBytes(32).toString('hex'); 74 | storage.setHawkOAuthState(req.hawkIdHmac, newState, function(err) { 75 | if (res.serverError(err)) return; 76 | if (storedState !== state) { 77 | // Reset session state after an attempt was made to compare it. 78 | sendError(res, 400, 79 | errors.INVALID_OAUTH_STATE, "Invalid OAuth state"); 80 | return; 81 | } 82 | 83 | // Trade the OAuth code for a token. 84 | request.post({ 85 | uri: oauthConf.oauth_uri + '/token', 86 | json: { 87 | code: code, 88 | client_id: oauthConf.client_id, 89 | client_secret: oauthConf.client_secret 90 | } 91 | }, function (err, r, body) { 92 | if (res.serverError(err)) return; 93 | 94 | var token = body.access_token; 95 | var tokenType = body.token_type; 96 | var scope = body.scope; 97 | 98 | // store the bearer token 99 | storage.setHawkOAuthToken(req.hawkIdHmac, token); 100 | 101 | // Make a request to the FxA server to have information about the 102 | // profile. 103 | request.get({ 104 | uri: oauthConf.profile_uri + '/profile', 105 | headers: { 106 | Authorization: 'Bearer ' + token 107 | } 108 | }, function (err, r, body) { 109 | if (res.serverError(err)) return; 110 | var data; 111 | try { 112 | data = JSON.parse(body); 113 | } catch (e) { 114 | sendError(res, 503, errors.BADJSON, 115 | e + " JSON: " + body); 116 | return; 117 | } 118 | 119 | if (!data.hasOwnProperty("email")) { 120 | sendError(res, 503, errors.BACKEND, 121 | "email not found in the oauth server request. " + data 122 | ); 123 | return; 124 | } 125 | // Store the appropriate profile information into the database, 126 | // associated with the hawk session. 127 | var userHmac = hmac(data.email.toLowerCase(), conf.get('userMacSecret')); 128 | storage.setHawkUser(userHmac, req.hawkIdHmac, function(err) { 129 | if (res.serverError(err)) return; 130 | storage.setHawkUserId(req.hawkIdHmac, encrypt(req.hawk.id, data.email), 131 | function(err) { 132 | if (res.serverError(err)) return; 133 | res.status(200).json({ 134 | token_type: tokenType, 135 | access_token: token, 136 | scope: scope 137 | }); 138 | }); 139 | }); 140 | }); 141 | }); 142 | }); 143 | }); 144 | }); 145 | }; 146 | -------------------------------------------------------------------------------- /loop/routes/home.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var loopPackageData = require('../../package.json'); 8 | var git = require('git-rev'); 9 | var request = require('request'); 10 | var url = require('url'); 11 | var async = require('async'); 12 | 13 | function isSuccess(statusCode) { 14 | return statusCode === 200; 15 | } 16 | 17 | module.exports = function(app, conf, logError, storage, tokBox, statsdClient) { 18 | function pingPushServer(pushServerURI, callback) { 19 | var serverURL = url.parse(pushServerURI, false, true); 20 | var protocol = serverURL.protocol === 'wss:' ? 'https' : 21 | serverURL.protocol === 'ws:' ? 'http' : serverURL.protocol; 22 | request.get({ 23 | url: url.format({ 24 | protocol: protocol, 25 | host: serverURL.host, 26 | pathname: '/status' 27 | }), 28 | timeout: conf.get('heartbeatTimeout') 29 | }, function(error, response) { 30 | if (error) return callback(error); 31 | callback(null, response.statusCode); 32 | }); 33 | } 34 | 35 | /** 36 | * Checks that the service and its dependencies are healthy. 37 | **/ 38 | app.get("/__heartbeat__", function(req, res) { 39 | function returnStatus(storageStatus, tokboxError, pushStatus, verifierStatus) { 40 | var status, message; 41 | if (storageStatus === true && tokboxError === null && 42 | pushStatus !== false && verifierStatus === true) { 43 | status = 200; 44 | } else { 45 | status = 503; 46 | if (tokboxError !== null) message = "TokBox " + tokboxError; 47 | } 48 | 49 | var data = { 50 | storage: storageStatus === true, 51 | provider: (tokboxError === null) ? true : false, 52 | message: message, 53 | push: pushStatus, 54 | fxaVerifier: verifierStatus 55 | }; 56 | 57 | // Log the erroneous call to Sentry. 58 | if (status === 503) { 59 | logError(new Error("Heartbeat: " + JSON.stringify(data))); 60 | } 61 | 62 | res.status(status).json(data); 63 | } 64 | 65 | storage.ping(function(storageStatus) { 66 | if (storageStatus !== true) { 67 | logError(storageStatus); 68 | } 69 | tokBox.ping({timeout: conf.get('heartbeatTimeout')}, 70 | function(tokboxError) { 71 | if (tokboxError) logError(tokboxError); 72 | request.get({ 73 | url: url.resolve(conf.get('fxaVerifier'), '/status'), 74 | timeout: conf.get('heartbeatTimeout') 75 | }, function(err, response) { 76 | if (err) logError(err); 77 | var verifierStatus = !err && isSuccess(response.statusCode); 78 | var pushServerURIs = conf.get('pushServerURIs'); 79 | async.map(pushServerURIs, pingPushServer, function(error, statusCodes) { 80 | if (error) logError(error); 81 | var pushStatus = (!error && statusCodes.every(isSuccess)); 82 | returnStatus(storageStatus, tokboxError, pushStatus, verifierStatus); 83 | if (statsdClient !== undefined) { 84 | var tag; 85 | if (pushStatus) { 86 | tag = 'success'; 87 | } else { 88 | tag = 'failure'; 89 | } 90 | statsdClient.increment('loop.simplepush.call.heartbeat', 1, [tag]); 91 | } 92 | }); 93 | }); 94 | }); 95 | }); 96 | }); 97 | 98 | 99 | /** 100 | * Checks that the service and its dependencies are healthy. 101 | **/ 102 | app.get("/__lbheartbeat__", function(req, res) { 103 | res.status(200).json({}); 104 | }); 105 | 106 | /** 107 | * Displays some version information at the root of the service. 108 | **/ 109 | app.get("/", function(req, res) { 110 | var metadata = { 111 | name: loopPackageData.name, 112 | description: loopPackageData.description, 113 | version: loopPackageData.version, 114 | homepage: loopPackageData.homepage, 115 | endpoint: conf.get("protocol") + "://" + req.get('host') 116 | }; 117 | 118 | // Adding information about the tokbox backend 119 | metadata.fakeTokBox = conf.get('fakeTokBox'); 120 | metadata.fxaOAuth = conf.get('fxaOAuth').activated; 121 | 122 | // Adding localization information for the client. 123 | metadata.i18n = { 124 | defaultLang: conf.get("i18n").defaultLang 125 | }; 126 | 127 | if (req.headers["accept-language"]) { 128 | metadata.i18n.lang = req.headers["accept-language"].split(",")[0]; 129 | } 130 | 131 | if (!conf.get("displayVersion")) { 132 | delete metadata.version; 133 | } 134 | 135 | // Adding revision if available 136 | git.long(function (commitId) { 137 | if (commitId) { 138 | metadata.rev = commitId; 139 | } 140 | git.branch(function (branch) { 141 | if (branch) { 142 | metadata.branch = branch; 143 | } 144 | 145 | res.status(200).json(metadata); 146 | }); 147 | }); 148 | }); 149 | }; 150 | -------------------------------------------------------------------------------- /loop/routes/push-server-config.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | module.exports = function (app, conf) { 8 | app.get('/push-server-config', function(req, res) { 9 | var urls = conf.get('pushServerURIs'); 10 | var url = urls[Math.floor(Math.random() * urls.length)]; 11 | res.status(200).json({ 12 | 'pushServerURI': url 13 | }); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /loop/routes/registration.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var errors = require("../errno"); 8 | var sendError = require('../utils').sendError; 9 | var getSimplePushURLS = require('../utils').getSimplePushURLS; 10 | 11 | 12 | module.exports = function (app, conf, logError, storage, auth) { 13 | /** 14 | * Registers the given user with the given simple push url. 15 | **/ 16 | app.post('/registration', auth.authenticate, 17 | function(req, res) { 18 | if (req.body !== undefined && !req.accepts("json")) { 19 | sendError(res, 406, errors.BADJSON, 20 | "Request body should be defined as application/json"); 21 | return; 22 | } 23 | getSimplePushURLS(req, function(err, simplePushURLs) { 24 | if (err) { 25 | sendError(res, 400, errors.INVALID_PARAMETERS, err.message); 26 | return; 27 | } 28 | if (Object.keys(simplePushURLs).length !== 0) { 29 | storage.addUserSimplePushURLs(req.user, req.hawkIdHmac, simplePushURLs, 30 | function(err) { 31 | if (res.serverError(err)) return; 32 | res.status(200).json(); 33 | }); 34 | } else { 35 | res.status(200).json(); 36 | } 37 | }); 38 | }); 39 | 40 | /** 41 | * Deletes the given simple push URL (you need to have registered it 42 | * to be able to unregister). 43 | **/ 44 | app.delete('/registration', auth.requireHawkSession, function(req, res) { 45 | storage.removeSimplePushURLs(req.user, req.hawkIdHmac, function(err) { 46 | if (res.serverError(err)) return; 47 | res.status(204).json(); 48 | }); 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /loop/routes/session.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | 8 | module.exports = function (app, conf, storage, auth) { 9 | /** 10 | * Removes the connected user session and drop its simplePushUrls and 11 | * Hawk session. 12 | **/ 13 | app.delete('/session', auth.requireHawkSession, auth.requireRegisteredUser, 14 | function(req, res) { 15 | storage.removeSimplePushURLs(req.user, req.hawkIdHmac, function(err) { 16 | if (res.serverError(err)) return; 17 | storage.deleteHawkSession(req.hawkIdHmac, function(err) { 18 | if (res.serverError(err)) return; 19 | res.status(204).json(); 20 | } ); 21 | }); 22 | }); 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /loop/routes/videur.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var pjson = require("../../package.json"); 8 | var specs = require("../api-specs"); 9 | var config = require('../config').conf; 10 | 11 | 12 | module.exports = function(app, conf) { 13 | var roomTokenSize = Math.ceil(config.get('rooms').tokenSize / 3 * 4); 14 | var callTokenSize = Math.ceil(config.get('callUrls').tokenSize / 3 * 4); 15 | var location = conf.get("protocol") + "://" + conf.get("publicServerAddress"); 16 | 17 | app.get("/api-specs", function(req, res) { 18 | var strSpec = JSON.stringify(specs); 19 | strSpec = strSpec.replace('{roomTokenSize}', roomTokenSize); 20 | strSpec = strSpec.replace('{callTokenSize}', callTokenSize); 21 | strSpec = strSpec.replace('{location}', location); 22 | strSpec = strSpec.replace('{version}', pjson.version); 23 | res.status(200).json(JSON.parse(strSpec)); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /loop/simplepush.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var request = require('request'); 8 | var dedupeArray = require('./utils').dedupeArray; 9 | 10 | /** 11 | * Simple client to handle simple push notifications 12 | **/ 13 | var SimplePush = function(statsdClient, logError) { 14 | this.statsdClient = statsdClient; 15 | this.logError = logError || function() {}; 16 | } 17 | 18 | SimplePush.prototype = { 19 | notify: function(reason, urls, version){ 20 | if (!Array.isArray(urls)) { 21 | urls = [urls]; 22 | } 23 | 24 | urls = dedupeArray(urls); 25 | 26 | var self = this; 27 | 28 | urls.forEach(function(simplePushUrl) { 29 | request.put({ 30 | url: simplePushUrl, 31 | form: { version: version } 32 | }, function(err) { 33 | var status = 'success'; 34 | if (err) { 35 | self.logError(err); 36 | status = 'failure'; 37 | } 38 | if (self.statsdClient !== undefined) { 39 | self.statsdClient.increment("loop.simplepush.call", 1, [reason, status]); 40 | } 41 | }); 42 | }); 43 | } 44 | } 45 | 46 | module.exports = SimplePush; 47 | -------------------------------------------------------------------------------- /loop/storage/index.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | function getStorage(conf, options) { 8 | var engine = conf.engine || 'redis'; 9 | var settings = conf.settings || {}; 10 | 11 | var Storage = require('./' + engine + '.js'); 12 | return new Storage(settings, options); 13 | } 14 | 15 | module.exports = getStorage; 16 | -------------------------------------------------------------------------------- /loop/storage/redis_client.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | var redis = require("redis"); 7 | var async = require("async"); 8 | 9 | var MULTI_OPERATIONS = [ 10 | 'pttl', 'ttl', 'set', 'setex', 'psetex', 'sadd', 'srem', 'pexpire', 11 | 'expire', 'incr', 'decr', 'hmset', 'hset', 'hsetnx', 'hdel', 'del', 12 | 'hgetall', 'get', 'scard' 13 | ]; 14 | 15 | 16 | function createClient(port, host, options) { 17 | if (options === undefined) { 18 | options = {}; 19 | } 20 | var client = redis.createClient(port, host, options); 21 | // Only bypass multi if sharding is enabled. 22 | if (options.sharding === true) { 23 | client.multi = function() { 24 | var self = this; 25 | var Multi = function() { 26 | this.operations = []; 27 | }; 28 | 29 | // Each time an operation is done on a multi, add it to a 30 | // list to execute. 31 | MULTI_OPERATIONS.forEach(function(operation) { 32 | Multi.prototype[operation] = function() { 33 | this.operations.push([ 34 | operation, Array.prototype.slice.call(arguments) 35 | ]); 36 | }; 37 | }); 38 | 39 | Multi.prototype.exec = function(callback){ 40 | async.mapSeries(this.operations, function(operation, done){ 41 | var operationName = operation[0]; 42 | var operationArguments = operation[1]; 43 | 44 | operationArguments.push(done); 45 | self[operationName].apply(self, operationArguments); 46 | }, callback); 47 | }; 48 | return new Multi(); 49 | }; 50 | } 51 | 52 | return client; 53 | } 54 | 55 | module.exports = { 56 | MULTI_OPERATIONS: MULTI_OPERATIONS, 57 | createClient: createClient 58 | } 59 | -------------------------------------------------------------------------------- /loop/storage/redis_migration.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var redis = require("./redis_client"); 8 | var async = require("async"); 9 | var conf = require('../config').conf; 10 | var MULTI_OPERATIONS = require("./redis_client").MULTI_OPERATIONS; 11 | 12 | // Operations supported by the migration backend. 13 | var SUPPORTED_OPERATIONS = [ 14 | 'keys', 'lrange', 'mget', 'sismember', 'smembers', 'get', 'pttl', 'ttl', 15 | 'scard', 'set', 'setex', 'psetex', 'sadd', 'srem', 'pexpire', 16 | 'expire', 'incr', 'decr', 'hmset', 'hgetall', 'hset', 'hget', 'hsetnx', 17 | 'hdel' 18 | ]; 19 | 20 | /** 21 | * Creates a redis proxy client, exposing the same APIs of the default client. 22 | * 23 | * This client takes parameters for a new and an old db, and copies data from 24 | * the old to the new db before asking the new db to answer the original 25 | * request. 26 | * 27 | * @param {Object} options, which should have an `oldDB` and a `newDB` 28 | * key, which respects the semantics of a redis client (port, 29 | * host, options). 30 | * 31 | * @returns {Object} a client whith the same APIs as the one used in the redis 32 | * backend. 33 | **/ 34 | function createClient(options) { 35 | var old_db = getClient(options.oldDB); 36 | var new_db = getClient(options.newDB); 37 | 38 | var Proxy = function(){ 39 | this.old_db = old_db; 40 | this.new_db = new_db; 41 | }; 42 | 43 | /** 44 | * Copy a key from one database to the other. 45 | * Copies also the TTL information. 46 | * 47 | * @param {String} key the key to be copied. 48 | * @param {Function} callback that will be called once the copy is over. 49 | **/ 50 | var copyKey = function(key, callback) { 51 | old_db.pttl(key, function(err, ttl) { 52 | ttl = parseInt(ttl, 10); 53 | if (err) throw err; 54 | if (ttl === -2) { 55 | // The key doesn't exist. 56 | callback(null); 57 | return; 58 | } else if (ttl === -1) { 59 | // Set the ttl to 0 if there is no TTL for the current key (it means 60 | // there is no expiration set for it) 61 | ttl = 0; 62 | } 63 | // Redis client will return buffers if it has buffers as arguments. 64 | // We want to have a buffer here to dump/restore keys using the right 65 | // encoding (otherwise a "DUMP payload version or checksum are wrong" 66 | // error is raised). 67 | old_db.dump(new Buffer(key), function(err, dump) { 68 | if (err) return callback(err); 69 | new_db.restore(key, ttl, dump, function(err) { 70 | if (err) return callback(err); 71 | old_db.del(key, function(err){ 72 | if (err) return callback(err); 73 | callback(null); 74 | return; 75 | }); 76 | }); 77 | }); 78 | }); 79 | }; 80 | 81 | /** 82 | * Decorator which, given an operation, returns a function that will 83 | * check if the key exists in the old db, and if so: 84 | * - dump from the old db and restore in the new one 85 | * - delete the key from the old db 86 | * And in any case, calls the initial operation on the new db. 87 | * 88 | * @param {String} operation — The redis operation name. 89 | * @return {Function} the function which will do the migration. 90 | **/ 91 | var migrateAndExecute = function(operation) { 92 | return function() { 93 | var originalArguments = arguments; 94 | var key = arguments[0]; 95 | var callback = arguments[arguments.length - 1]; 96 | 97 | // Calls the original command with the arguments passed to this function. 98 | var callOriginalCommand = function(err){ 99 | if (err) { return callback(err); } 100 | new_db[operation].apply(new_db, originalArguments); 101 | }; 102 | 103 | // In case we have a keys or a mget command, since we have multiple keys 104 | // involved, copy all of them before running the original command on the 105 | // new database. 106 | if (operation === 'keys') { 107 | old_db.keys(key, function(err, keys) { 108 | if (err) return callback(err); 109 | async.each(keys, copyKey, callOriginalCommand); 110 | }); 111 | } else if (operation === 'mget') { 112 | async.each(key, copyKey, callOriginalCommand); 113 | } else { 114 | copyKey(key, callOriginalCommand); 115 | } 116 | }; 117 | }; 118 | 119 | // For each of the supported operations, proxy the call the the migration 120 | // logic. 121 | SUPPORTED_OPERATIONS.forEach(function(operation) { 122 | Proxy.prototype[operation] = migrateAndExecute(operation); 123 | }); 124 | 125 | // Do not relay flush operations if we aren't using the TEST environment. 126 | Proxy.prototype.flushdb = function(callback) { 127 | if (conf.get('env') !== 'test') { 128 | callback(); 129 | return; 130 | } 131 | old_db.flushdb(function(err) { 132 | if (err) return callback(err); 133 | new_db.flushdb(function(err) { 134 | callback(err); 135 | }); 136 | }); 137 | }; 138 | 139 | // For deletion, we just remove from both databases and return the total 140 | // count of deleted values. 141 | Proxy.prototype.del = function(key, callback) { 142 | var deleted = 0; 143 | old_db.del(key, function(err, number) { 144 | deleted += number; 145 | if (err) return callback(err); 146 | new_db.del(key, function(err, number) { 147 | deleted += number; 148 | callback(err, deleted); 149 | }); 150 | }); 151 | }; 152 | 153 | // For multi, we just chain them as "normal" operations, copying keys for all 154 | // the operations (so we actually lose the benefit offered by multi). 155 | Proxy.prototype.multi = function() { 156 | var self = this; 157 | var Multi = function() { 158 | this.operations = []; 159 | }; 160 | 161 | // Each time an operation is done on a multi, add it to a 162 | // list to execute. 163 | 164 | MULTI_OPERATIONS.forEach(function(operation) { 165 | Multi.prototype[operation] = function() { 166 | this.operations.push([ 167 | operation, Array.prototype.slice.call(arguments) 168 | ]); 169 | }; 170 | }); 171 | 172 | Multi.prototype.exec = function(callback){ 173 | async.mapSeries(this.operations, function(operation, done){ 174 | var operationName = operation[0]; 175 | var operationArguments = operation[1]; 176 | operationArguments.push(done); 177 | 178 | if (operationName === "del") { 179 | self.del.apply(self, operationArguments); 180 | } else { 181 | var executor = migrateAndExecute(operationName); 182 | executor.apply(self, operationArguments); 183 | } 184 | }, callback); 185 | }; 186 | return new Multi(); 187 | }; 188 | 189 | Proxy.prototype.copyKey = copyKey; 190 | 191 | 192 | return new Proxy(); 193 | } 194 | 195 | /** 196 | * Returns a redis client from the options passed in arguments. 197 | * 198 | * @param {Object} options with a (port, host, options) keys. 199 | **/ 200 | function getClient(options) { 201 | var client = redis.createClient( 202 | options.port || 6379, 203 | options.host || "localhost", 204 | { 205 | // This is to return buffers when buffers are sent as params. 206 | detect_buffers: true, 207 | sharding: options.sharding 208 | } 209 | ); 210 | if (options.db) { 211 | client.select(options.db); 212 | } 213 | return client; 214 | } 215 | 216 | module.exports = createClient; 217 | -------------------------------------------------------------------------------- /loop/tokbox.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var randomBytes = require('crypto').randomBytes; 8 | var request = require('request'); 9 | var conf = require('./config').conf; 10 | var time = require('./utils').time; 11 | 12 | // Be sure to use the exported OpenTok so we can mock it in the 13 | // tests. 14 | exports.OpenTok = require('opentok'); 15 | 16 | function TokBox(settings, statsdClient) { 17 | this.credentials = settings.credentials; 18 | if (settings.retryOnError === undefined) { 19 | settings.retryOnError = 3; 20 | } 21 | this.statsdClient = statsdClient; 22 | this.retryOnError = settings.retryOnError; 23 | this.tokenDuration = settings.tokenDuration; 24 | this._opentok = {}; 25 | for (var channel in this.credentials) { 26 | this._opentok[channel] = new exports.OpenTok( 27 | this.credentials[channel].apiKey, 28 | this.credentials[channel].apiSecret, { 29 | apiUrl: this.credentials[channel].apiUrl || conf.get("tokBox").apiUrl, 30 | timeout: settings.timeout 31 | } 32 | ); 33 | } 34 | } 35 | 36 | TokBox.prototype = { 37 | 38 | getSession: function(options, callback) { 39 | if (callback === undefined) { 40 | callback = options; 41 | options = undefined; 42 | } 43 | 44 | options = options || {}; 45 | 46 | if (options.retry === undefined) { 47 | options.retry = this.retryOnError; 48 | } 49 | 50 | var opentok; 51 | 52 | if (this.credentials.hasOwnProperty(options.channel)) { 53 | opentok = this._opentok[options.channel]; 54 | } else { 55 | opentok = this._opentok["default"]; 56 | } 57 | 58 | var self = this; 59 | if (self.statsdClient !== undefined) { 60 | var startTime = Date.now(); 61 | } 62 | opentok.createSession({ 63 | mediaMode: options.mediaMode || "relayed" 64 | }, function(err, session) { 65 | if (err !== null) { 66 | options.retry--; 67 | if (options.retry <= 0) { 68 | callback(err); 69 | return; 70 | } 71 | self.getSession(options, callback); 72 | return; 73 | } 74 | if (self.statsdClient !== undefined) { 75 | self.statsdClient.increment("loop.tokbox.createSession.count"); 76 | self.statsdClient.timing( 77 | 'loop.tokbox.createSession', 78 | Date.now() - startTime 79 | ); 80 | } 81 | callback(null, session, opentok); 82 | }); 83 | }, 84 | 85 | getSessionToken: function(sessionId, role, channel) { 86 | var now = time(); 87 | var expirationTime = now + this.tokenDuration; 88 | 89 | var opentok; 90 | 91 | if (channel !== undefined && this.credentials.hasOwnProperty(channel)) { 92 | opentok = this._opentok[channel]; 93 | } else { 94 | opentok = this._opentok["default"]; 95 | } 96 | 97 | return opentok.generateToken( 98 | sessionId, { 99 | role: role, 100 | expireTime: expirationTime 101 | } 102 | ); 103 | }, 104 | 105 | getSessionTokens: function(options, callback) { 106 | var self = this; 107 | 108 | if (callback === undefined) { 109 | callback = options; 110 | options = {}; 111 | } 112 | 113 | options.mediaMode = options.mediaMode || "relayed"; 114 | 115 | this.getSession(options, function(err, session, opentok) { 116 | if (err) return callback(err); 117 | var sessionId = session.sessionId; 118 | var now = time(); 119 | var expirationTime = now + self.tokenDuration; 120 | callback(null, { 121 | apiKey: opentok.apiKey, 122 | sessionId: sessionId, 123 | callerToken: opentok.generateToken(sessionId, { 124 | role: 'publisher', 125 | expireTime: expirationTime 126 | }), 127 | calleeToken: opentok.generateToken(sessionId, { 128 | role: 'publisher', 129 | expireTime: expirationTime 130 | }) 131 | }); 132 | }); 133 | }, 134 | 135 | ping: function(options, callback) { 136 | if (callback === undefined) { 137 | callback = options; 138 | options = undefined; 139 | } 140 | 141 | options = options || {}; 142 | var timeout = options.timeout; 143 | 144 | request.post({ 145 | url: this._opentok.default._client.c.apiUrl + 146 | this._opentok.default._client.c.endpoints.createSession, 147 | form: {"p2p.preference": "enabled"}, 148 | headers: { 149 | 'User-Agent': 'OpenTok-Node-SDK/2.2.4', 150 | 'X-TB-PARTNER-AUTH': this._opentok.default._client.c.apiKey + 151 | ':' + this._opentok.default._client.c.apiSecret 152 | }, timeout: timeout 153 | }, function(err, resp, body) { 154 | if (err) { 155 | callback(new Error('The request failed: ' + err)); 156 | return; 157 | } 158 | 159 | // handle client errors 160 | if (resp.statusCode === 403) { 161 | callback(new Error( 162 | 'An authentication error occured: (' + 163 | resp.statusCode + ')' + body 164 | )); 165 | return; 166 | } 167 | 168 | // handle server errors 169 | if (resp.statusCode >= 500 && resp.statusCode <= 599) { 170 | callback(new Error( 171 | 'A server error occured: (' + resp.statusCode + ')' + body 172 | )); 173 | return; 174 | } 175 | callback(null); 176 | }); 177 | } 178 | }; 179 | 180 | function FakeTokBox() { 181 | this._counter = 0; 182 | this.serverURL = conf.get("fakeTokBoxURL"); 183 | this.apiKey = "falseApiKey"; 184 | } 185 | 186 | FakeTokBox.prototype = { 187 | _urlSafeBase64RandomBytes: function(number_of_bytes) { 188 | return randomBytes(number_of_bytes).toString('base64') 189 | .replace(/\+/g, '-').replace(/\//g, '_'); 190 | }, 191 | 192 | _fakeApiKey: function() { 193 | return "4468744"; 194 | }, 195 | 196 | _fakeSessionId: function() { 197 | this._token = 0; 198 | this._counter += 1; 199 | return this._counter + '_' + this._urlSafeBase64RandomBytes(51); 200 | 201 | }, 202 | 203 | _generateFakeToken: function() { 204 | this._token += 1; 205 | return 'T' + this._token + '==' + this._urlSafeBase64RandomBytes(293); 206 | }, 207 | 208 | getSession: function(options, callback) { 209 | if (callback === undefined) { 210 | callback = options; 211 | options = {}; 212 | } 213 | 214 | var self = this; 215 | // Do a real HTTP call to have a realistic behavior. 216 | request.get({ 217 | url: self.serverURL, 218 | timeout: options.timeout 219 | }, function(err) { 220 | callback(err, self._fakeSessionId(), {apiKey: self._fakeApiKey()}); 221 | }); 222 | }, 223 | 224 | getSessionToken: function() { 225 | return this._generateFakeToken(); 226 | }, 227 | 228 | getSessionTokens: function(options, callback) { 229 | if (callback === undefined) { 230 | callback = options; 231 | options = {}; 232 | } 233 | var self = this; 234 | // Do a real HTTP call to have a realistic behavior. 235 | request.get({ 236 | url: self.serverURL, 237 | timeout: options.timeout 238 | }, function(err) { 239 | callback(err, { 240 | apiKey: self._fakeApiKey(), 241 | sessionId: self._fakeSessionId(), 242 | callerToken: self._generateFakeToken(), 243 | calleeToken: self._generateFakeToken() 244 | }); 245 | }); 246 | }, 247 | ping: function(options, callback) { 248 | this.getSessionTokens(options, callback); 249 | } 250 | }; 251 | 252 | 253 | exports.TokBox = TokBox; 254 | exports.FakeTokBox = FakeTokBox; 255 | exports.OpenTok = exports.OpenTok; 256 | exports.request = request; 257 | -------------------------------------------------------------------------------- /loop/tokenlib.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var randomBytes = require("crypto").randomBytes; 8 | var ONE_HOUR = 60 * 60; 9 | 10 | /** 11 | * Return a url token of [a-zA-Z0-9-_] character and size length. 12 | */ 13 | function generateToken(bytes) { 14 | return randomBytes(bytes) 15 | .toString("base64") 16 | .replace(/\=/g, '') 17 | .replace(/\//g, '_') 18 | .replace(/\+/g, '-'); 19 | } 20 | 21 | module.exports = { 22 | generateToken: generateToken, 23 | ONE_HOUR: ONE_HOUR 24 | }; 25 | -------------------------------------------------------------------------------- /loop/utils.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var conf = require('./config').conf; 8 | var decrypt = require('./encrypt').decrypt; 9 | var ua = require('universal-analytics'); 10 | 11 | 12 | function sendError(res, code, errno, error, message, info) { 13 | var errmap = {}; 14 | if (code) { 15 | errmap.code = code; 16 | } 17 | if (errno) { 18 | errmap.errno = errno; 19 | } 20 | if (error) { 21 | errmap.error = error; 22 | } 23 | if (message) { 24 | errmap.message = message; 25 | } 26 | if (info) { 27 | errmap.info = info; 28 | } 29 | 30 | res.errno = errno; 31 | res.status(code).json(errmap); 32 | } 33 | 34 | function getProgressURL(host) { 35 | var progressURL; 36 | if (conf.get("protocol") === "https") { 37 | progressURL = "wss://" + host.split(":")[0] + ":443"; 38 | } else { 39 | progressURL = "ws://" + host; 40 | } 41 | 42 | return progressURL + conf.get('progressURLEndpoint'); 43 | } 44 | 45 | function isoDateString(d){ 46 | function pad(n){ 47 | return n < 10 ? '0' + n : n; 48 | } 49 | return d.getUTCFullYear() + '-' + 50 | pad(d.getUTCMonth() + 1) + '-' + 51 | pad(d.getUTCDate()) + 'T' + 52 | pad(d.getUTCHours()) + ':' + 53 | pad(d.getUTCMinutes()) + ':' + 54 | pad(d.getUTCSeconds()) + 'Z'; 55 | } 56 | 57 | function getUserAccount(storage, req, callback) { 58 | if (req.hawkIdHmac === undefined) { 59 | callback(); 60 | return; 61 | } 62 | storage.getHawkUserId(req.hawkIdHmac, function(err, encryptedUserId) { 63 | if (err) return callback(err); 64 | 65 | var userId; 66 | if (encryptedUserId !== null) { 67 | userId = decrypt(req.hawk.id, encryptedUserId); 68 | } 69 | callback(err, userId); 70 | }); 71 | } 72 | 73 | 74 | function getSimplePushURLS(req, callback) { 75 | var simplePushURLs = req.body.simplePushURLs || {}; 76 | 77 | var simplePushURL = req.body.simplePushURL || 78 | req.query.simplePushURL || 79 | req.body.simple_push_url; // Bug 1032966 - Handle old simple_push_url format 80 | 81 | if (simplePushURL !== undefined) { 82 | simplePushURLs.calls = simplePushURL; 83 | } 84 | 85 | if (Object.keys(simplePushURLs).length !== 0) { 86 | for (var topic in simplePushURLs) { 87 | if (simplePushURLs[topic].indexOf('http') !== 0) { 88 | callback(new Error("simplePushURLs." + topic + " should be a valid url")); 89 | return; 90 | } 91 | } 92 | } 93 | 94 | callback(null, simplePushURLs); 95 | } 96 | 97 | /** 98 | * Create a UA instance and sent an event to it. 99 | **/ 100 | function sendAnalytics(gaID, userID, data) { 101 | var userAnalytics = ua(gaID, userID, {strictCidFormat: false, https: true}); 102 | userAnalytics.event(data.event, data.action, data.label).send(); 103 | } 104 | 105 | /** 106 | * Return a unix timestamp in seconds. 107 | **/ 108 | function time() { 109 | return parseInt(Date.now() / 1000, 10); 110 | } 111 | 112 | /** 113 | * Dedupe arrays, see http://stackoverflow.com/questions/9229645/remove-duplicates-from-javascript-array 114 | **/ 115 | function dedupeArray(array) { 116 | return array.sort().filter(function(item, pos) { 117 | return !pos || item !== array[pos - 1]; 118 | }); 119 | } 120 | 121 | function isUndefined(field, fieldName, callback) { 122 | if (field === undefined) { 123 | callback(new Error(fieldName + " should not be undefined")); 124 | return true; 125 | } 126 | return false; 127 | } 128 | 129 | function encode(data) { 130 | return JSON.stringify(data); 131 | } 132 | 133 | function decode(string, callback) { 134 | if (!string) return callback(null, null); 135 | try { 136 | callback(null, JSON.parse(string)); 137 | } catch (e) { 138 | callback(e); 139 | } 140 | } 141 | 142 | function clone(data) { 143 | return JSON.parse(JSON.stringify(data)); 144 | } 145 | 146 | module.exports = { 147 | getProgressURL: getProgressURL, 148 | sendError: sendError, 149 | isoDateString: isoDateString, 150 | time: time, 151 | getUserAccount: getUserAccount, 152 | getSimplePushURLS: getSimplePushURLS, 153 | sendAnalytics: sendAnalytics, 154 | dedupeArray: dedupeArray, 155 | encode: encode, 156 | decode: decode, 157 | isUndefined: isUndefined, 158 | clone: clone 159 | }; 160 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mozilla-loop-server", 3 | "description": "The Mozilla Loop (WebRTC App) server", 4 | "version": "0.22.0-dev", 5 | "author": "Mozilla (https://mozilla.org/)", 6 | "homepage": "https://github.com/mozilla-services/loop-server/", 7 | "bugs": "https://bugzilla.mozilla.org/enter_bug.cgi?product=Loop&component=Server", 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:mozilla-services/loop-server.git" 11 | }, 12 | "engines": { 13 | "node": "0.10.x", 14 | "npm": "1.3.x" 15 | }, 16 | "dependencies": { 17 | "async": "0.9.0", 18 | "atob": "1.1.2", 19 | "aws-sdk": "2.0.23", 20 | "body-parser": "1.12.0", 21 | "connect-validation": "0.2.0", 22 | "convict": "0.6.1", 23 | "cors": "2.5.3", 24 | "express": "4.x", 25 | "express-hawkauth": "0.3.0", 26 | "express-logging": "git://github.com/mozilla-services/express-logging.git", 27 | "git-rev": "0.2.1", 28 | "heapdump": "0.3.5", 29 | "hiredis": "0.2.0", 30 | "hkdf": "0.0.2", 31 | "mozlog": "1.0.2", 32 | "newrelic": "1.16.2", 33 | "node-uuid": "1.4.2", 34 | "opentok": "2.2.4", 35 | "phone": "git://github.com/Natim/node-phone.git#83a4f48", 36 | "raven": "0.7.2", 37 | "redis": "0.12.1", 38 | "request": "2.45.0", 39 | "sodium": "1.0.13", 40 | "node-statsd": "0.1.1", 41 | "strftime": "0.8.2", 42 | "universal-analytics": "0.3.10", 43 | "urlsafe-base64": "1.0.0", 44 | "ws": "1.0.1", 45 | "yargs": "3.0.4" 46 | }, 47 | "devDependencies": { 48 | "chai": "2.0.0", 49 | "eslint": "1.3.0", 50 | "grunt": "0.4.5", 51 | "grunt-cli": "0.1.13", 52 | "grunt-copyright": "0.1.0", 53 | "grunt-eslint": "17.1.0", 54 | "grunt-jsonlint": "1.0.4", 55 | "grunt-nsp-shrinkwrap": "0.0.3", 56 | "grunt-shell": "1.1.1", 57 | "grunt-todo": "0.4.0", 58 | "istanbul": "*", 59 | "load-grunt-tasks": "3.1.0", 60 | "mocha": "2.1.0", 61 | "nock": "1.2.0", 62 | "sinon": "1.12.2", 63 | "superagent-hawk": "0.0.4", 64 | "supertest": "0.15.0" 65 | }, 66 | "scripts": { 67 | "test": "make test", 68 | "start": "make runserver", 69 | "lint": "make lint", 70 | "outdated": "npm outdated --depth 0", 71 | "audit-shrinkwrap": "grunt audit-shrinkwrap --force", 72 | "jsdoc-lint": "eslint loop --rule 'valid-jsdoc: [1, {requireReturn: false, requireParamDescription: false}]'" 73 | }, 74 | "license": "MPL-2.0", 75 | "keywords": [] 76 | } 77 | -------------------------------------------------------------------------------- /redis_usage.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter 3 | import json 4 | 5 | BARE_REDIS = 600000 6 | USER_WEIGHT = 300 7 | CALL_WEIGHT = 1400 8 | REVOCATION_WEIGHT = 150 9 | ROOM_WEIGHT = 700 10 | ROOM_PARTICIPANT_WEIGHT = 550 11 | 12 | 13 | def get_version(): 14 | """Returns the version contained in the package.json file""" 15 | with open('package.json') as f: 16 | package = json.load(f) 17 | return package['version'] 18 | 19 | 20 | def compute_redis_usage(users, daily_calls, monthly_revocations, rooms, rooms_participants): 21 | """Computes the redis usage, in megabytes""" 22 | return ((users * USER_WEIGHT + 23 | daily_calls * users * CALL_WEIGHT + 24 | monthly_revocations * REVOCATION_WEIGHT + 25 | rooms * users * ROOM_WEIGHT + 26 | rooms * users * rooms_participants * ROOM_PARTICIPANT_WEIGHT + 27 | BARE_REDIS) 28 | / 1024) / 1024 29 | 30 | 31 | def main(): 32 | parser = ArgumentParser( 33 | description="Compute redis usage for loop", 34 | formatter_class=ArgumentDefaultsHelpFormatter) 35 | 36 | parser.add_argument( 37 | '-u', '--users', 38 | dest='users', 39 | help='The number of users that will be using this server', 40 | type=int, 41 | default=1000) 42 | 43 | parser.add_argument( 44 | '-c', '--calls', 45 | dest='daily_calls', 46 | nargs='?', 47 | help='The number calls that will be done per user (average)', 48 | type=int, 49 | default=1) 50 | 51 | parser.add_argument( 52 | '-m', '--revocations', 53 | dest='monthly_revocations', 54 | nargs='?', 55 | help='The number of revocation, per month', 56 | type=int, 57 | default=0) 58 | 59 | parser.add_argument( 60 | '-r', '--rooms', 61 | dest='rooms', 62 | nargs='?', 63 | help='The number of rooms per user', 64 | type=int, 65 | default=5) 66 | 67 | parser.add_argument( 68 | '-p', '--participants', 69 | dest='rooms_participants', 70 | nargs='?', 71 | help='The average number of rooms participants', 72 | type=float, 73 | default=1) 74 | 75 | args = parser.parse_args() 76 | 77 | usage = compute_redis_usage( 78 | args.users, 79 | args.daily_calls, 80 | args.monthly_revocations, 81 | args.rooms, 82 | args.rooms_participants 83 | ) 84 | 85 | text = ("""loop-server: v{version} 86 | 87 | The memory usage is {usage} MB for: 88 | - {users} users 89 | - with {daily_calls} daily calls per user, 90 | - {monthly_revocations} call-urls monthly revocations, 91 | - {rooms} rooms with around {rooms_participants} participants in it at all times\n""") 92 | 93 | print(text.format(**{ 94 | 'version': get_version(), 95 | 'users': args.users, 96 | 'daily_calls': args.daily_calls, 97 | 'monthly_revocations': args.monthly_revocations, 98 | 'rooms': args.rooms, 99 | 'rooms_participants': args.rooms_participants, 100 | 'usage': '%.2f' % usage 101 | })) 102 | 103 | if __name__ == '__main__': 104 | main() 105 | -------------------------------------------------------------------------------- /test/auth_test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | "use strict"; 5 | 6 | var expect = require("chai").expect; 7 | var supertest = require("supertest"); 8 | var sinon = require("sinon"); 9 | var assert = sinon.assert; 10 | var randomBytes = require("crypto").randomBytes; 11 | 12 | var hmac = require('../loop/hmac'); 13 | var loop = require("../loop"); 14 | var conf = loop.conf; 15 | var app = loop.app; 16 | var apiRouter = loop.apiRouter; 17 | var apiPrefix = loop.apiPrefix; 18 | var storage = loop.storage; 19 | var requireBasicAuthToken = loop.auth.requireBasicAuthToken; 20 | var authenticateWithHawkOrToken = loop.auth.authenticateWithHawkOrToken; 21 | var Token = require("express-hawkauth").Token; 22 | 23 | 24 | describe("auth.js", function() { 25 | var sandbox; 26 | 27 | beforeEach(function() { 28 | sandbox = sinon.sandbox.create(); 29 | }); 30 | 31 | afterEach(function() { 32 | sandbox.restore(); 33 | }); 34 | 35 | describe('#requireBasicAuthToken', function() { 36 | var jsonReq, expectedToken, expectedTokenHmac; 37 | 38 | // Create a route with the auth middleware installed. 39 | apiRouter.post('/with-requireBasicAuthToken', 40 | requireBasicAuthToken, function(req, res) { 41 | res.status(200).json(req.participantTokenHmac); 42 | }); 43 | 44 | beforeEach(function() { 45 | jsonReq = supertest(app) 46 | .post(apiPrefix + '/with-requireBasicAuthToken'); 47 | 48 | expectedToken = "valid-token"; 49 | expectedTokenHmac = hmac(expectedToken, conf.get('userMacSecret')); 50 | 51 | sandbox.stub(storage, "isRoomAccessTokenValid", 52 | function(roomToken, tokenHmac, callback) { 53 | if (tokenHmac === expectedTokenHmac) { 54 | callback(null, true); 55 | } else { 56 | callback(null, false); 57 | } 58 | }); 59 | }); 60 | 61 | it("should require user authentication", function(done) { 62 | jsonReq 63 | .expect(401) 64 | .end(function(err, res) { 65 | if (err) throw err; 66 | expect(res.headers['www-authenticate']).to.eql('Basic'); 67 | done(); 68 | }); 69 | }); 70 | 71 | it("should reject invalid tokens", function(done) { 72 | // Mock the calls to the external BrowserID verifier. 73 | jsonReq 74 | .auth('invalid-token', '') 75 | .expect(401) 76 | .end(function(err, res) { 77 | if (err) throw err; 78 | expect(res.headers['www-authenticate']) 79 | .to.eql('Basic error="Invalid token; it may have expired."'); 80 | done(); 81 | }); 82 | }); 83 | 84 | it("should accept valid token", function(done) { 85 | jsonReq 86 | .auth(expectedToken, '') 87 | .expect(200) 88 | .end(function(err) { 89 | if (err) throw err; 90 | done(); 91 | }); 92 | }); 93 | 94 | it("should set an 'participantTokenHmac' property on the request object", 95 | function(done) { 96 | jsonReq 97 | .auth(expectedToken, '') 98 | .expect(200) 99 | .end(function(err, res) { 100 | if (err) throw err; 101 | expect(res.body).eql(expectedTokenHmac); 102 | done(); 103 | }); 104 | }); 105 | }); 106 | 107 | describe("authenticateWithHawkOrToken middleware", function() { 108 | var expectedToken, expectedTokenHmac; 109 | 110 | apiRouter.post("/authenticateWithHawkOrToken", authenticateWithHawkOrToken, 111 | function(req, res) { 112 | res.status(200).json(req.participantTokenHmac || req.hawkIdHmac); 113 | }); 114 | 115 | describe("using a token", function() { 116 | beforeEach(function() { 117 | expectedToken = "valid-token"; 118 | expectedTokenHmac = hmac(expectedToken, conf.get('userMacSecret')); 119 | 120 | sandbox.stub(storage, "isRoomAccessTokenValid", 121 | function(roomToken, tokenHmac, callback) { 122 | if (tokenHmac === expectedTokenHmac) { 123 | callback(null, true); 124 | } else { 125 | callback(null, false); 126 | } 127 | }); 128 | }); 129 | 130 | it("should accept token", function(done) { 131 | supertest(app) 132 | .post(apiPrefix + "/authenticateWithHawkOrToken") 133 | .auth(expectedToken, '') 134 | .expect(200) 135 | .end(function(err, res) { 136 | if (err) throw err; 137 | expect(res.body).to.eql(expectedTokenHmac); 138 | done(); 139 | }); 140 | }); 141 | 142 | it("shouldn't accept invalid token", function(done) { 143 | supertest(app) 144 | .post(apiPrefix + "/authenticateWithHawkOrToken") 145 | .auth('wrongToken', '') 146 | .expect(401) 147 | .end(done); 148 | }); 149 | }); 150 | 151 | describe("using an Hawk session", function() { 152 | var hawkCredentials, userHmac; 153 | 154 | beforeEach(function(done) { 155 | // Generate Hawk credentials. 156 | var token = new Token(); 157 | token.getCredentials(function(tokenId, authKey) { 158 | hawkCredentials = { 159 | id: tokenId, 160 | key: authKey, 161 | algorithm: "sha256" 162 | }; 163 | userHmac = hmac(tokenId, conf.get('hawkIdSecret')); 164 | storage.setHawkSession(userHmac, authKey, done); 165 | }); 166 | }); 167 | 168 | it("should accept valid hawk sessions", function(done) { 169 | supertest(app) 170 | .post(apiPrefix + "/authenticateWithHawkOrToken") 171 | .hawk(hawkCredentials) 172 | .expect(200) 173 | .end(done); 174 | }); 175 | 176 | it("shouldn't accept invalid hawk credentials", function(done) { 177 | hawkCredentials.id = randomBytes(16).toString("hex"); 178 | supertest(app) 179 | .post(apiPrefix + "/authenticateWithHawkOrToken") 180 | .hawk(hawkCredentials) 181 | .expect(401) 182 | .end(done); 183 | }); 184 | 185 | it("should update session expiration time on auth", function(done) { 186 | sandbox.spy(storage, "touchHawkSession"); 187 | supertest(app) 188 | .post(apiPrefix + "/authenticateWithHawkOrToken") 189 | .hawk(hawkCredentials) 190 | .expect(200) 191 | .end(function(err) { 192 | if (err) throw err; 193 | assert.calledWithMatch( 194 | storage.touchHawkSession, 195 | userHmac, userHmac 196 | ); 197 | done(); 198 | }); 199 | }); 200 | }); 201 | 202 | it("should accept no Token nor Hawk", 203 | function(done) { 204 | supertest(app) 205 | .post(apiPrefix + "/authenticateWithHawkOrToken") 206 | .expect(200) 207 | .end(function(err, res) { 208 | if (err) throw err; 209 | expect(res.body).to.eql({}); 210 | done(); 211 | }); 212 | }); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /test/config_test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | "use strict"; 5 | 6 | var expect = require("chai").expect; 7 | var config = require("../loop/config"); 8 | 9 | describe("config", function() { 10 | describe("#validateKeys", function() { 11 | it("should throw an error if a key is missing", function() { 12 | expect(function() { 13 | config.validateKeys(['foo'])({}); 14 | }).to.throw(/Should have a foo property/); 15 | }); 16 | 17 | it("should not throw if all keys are valid", function() { 18 | config.validateKeys(['foo'])({foo: 'oh yeah'}); 19 | }); 20 | 21 | it("should not throw any error if it is defined as optional", function() { 22 | config.validateKeys(['foo'], {'optional': true})({}); 23 | }); 24 | }); 25 | 26 | describe("#hexKeyOfSize", function() { 27 | it("should check if all chars are hexadecimals", function() { 28 | expect(function() { 29 | config.hexKeyOfSize(4)("ggggaaaa"); 30 | }).to.throw(/Should be an 4 bytes key encoded as hexadecimal/); 31 | }); 32 | 33 | it("should check the size of the given key", function() { 34 | expect(function() { 35 | config.hexKeyOfSize(4)("aaaafff"); 36 | }).to.throw(/Should be an 4 bytes key encoded as hexadecimal/); 37 | }); 38 | }); 39 | 40 | describe("sample.json", function() { 41 | it("should load.", function() { 42 | require("../config/sample.json"); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/encrypt_test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | "use strict"; 5 | 6 | var expect = require("chai").expect; 7 | var randomBytes = require("crypto").randomBytes; 8 | var encrypt = require("../loop/encrypt"); 9 | 10 | describe("ENCRYPT", function() { 11 | describe("#encrypt/#decrypt", function() { 12 | it("should be able to encrypt and decrypt a string", function() { 13 | var passphrase = randomBytes(32).toString("hex"); 14 | var text = "Bonjour les gens"; 15 | var encrypted = encrypt.encrypt(passphrase, text); 16 | var decrypted = encrypt.decrypt(passphrase, encrypted); 17 | expect(decrypted).to.eql(text); 18 | }); 19 | 20 | it("should error-out if the given string is empty", function() { 21 | var passphrase = randomBytes(32).toString("hex"); 22 | expect(function() { 23 | encrypt.encrypt(passphrase, null); 24 | }).to.throw(/is empty/); 25 | }); 26 | 27 | it("should error-out if the given string is empty", function() { 28 | var passphrase = randomBytes(32).toString("hex"); 29 | expect(function() { 30 | encrypt.decrypt(passphrase, null); 31 | }).to.throw(/is empty/); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/filestorage_test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | var expect = require("chai").expect; 7 | var sinon = require("sinon"); 8 | 9 | var getFileStorage = require("../loop/filestorage"); 10 | var uuid = require("node-uuid"); 11 | var path = require("path"); 12 | var fs = require("fs"); 13 | 14 | var httpMock = require("./nock"); 15 | 16 | describe("Files", function() { 17 | function testStorage(name, verifyNock, createStorage) { 18 | var storage, mock, statsdSpy; 19 | 20 | describe(name, function() { 21 | var sandbox, statsdClient; 22 | 23 | beforeEach(function(done) { 24 | sandbox = sinon.sandbox.create(); 25 | statsdClient = { timing: function() {} }; 26 | statsdSpy = sandbox.spy(statsdClient, "timing"); 27 | mock = httpMock({bucket: 'room_encrypted_files'}); 28 | createStorage({}, statsdClient, function(err, fileStorage) { 29 | storage = fileStorage; 30 | done(err); 31 | }); 32 | }); 33 | 34 | afterEach(function(done) { 35 | sandbox.restore(); 36 | // Ignore remove errors. 37 | mock.removeAws(); 38 | storage.remove("test", function() { 39 | mock.removeAws(); 40 | storage.remove("foobar", function() { 41 | storage = undefined; 42 | mock.done(verifyNock); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | it("should write a file.", function(done) { 49 | mock.writeAws(); 50 | storage.write("test", {"key": "data"}, function(err) { 51 | if (err) throw err; 52 | expect(statsdSpy.called).to.be.true; 53 | statsdSpy.reset(); 54 | mock.readAws(); 55 | storage.read("test", function(err, data) { 56 | if (err) throw err; 57 | expect(data).to.eql({"key": "data"}); 58 | expect(statsdSpy.called).to.be.true; 59 | done(); 60 | }); 61 | }); 62 | }); 63 | 64 | it("should override a file.", function(done) { 65 | mock.writeAws(); 66 | storage.write("foobar", {"key": "data"}, function(err) { 67 | if (err) throw err; 68 | mock.writeAws(); 69 | storage.write("foobar", {"key": "data2"}, function(err) { 70 | if (err) throw err; 71 | mock.readAws(); 72 | storage.read("foobar", function(err, data) { 73 | if (err) throw err; 74 | expect(data).to.eql({"key": "data2"}); 75 | done(); 76 | }); 77 | }); 78 | }); 79 | }); 80 | 81 | it("should remove a file.", function(done) { 82 | mock.writeAws(); 83 | storage.write("foobar", {"key": "data"}, function(err) { 84 | if (err) throw err; 85 | statsdSpy.reset(); 86 | mock.removeAws(); 87 | storage.remove("foobar", function(err) { 88 | if (err) throw err; 89 | expect(statsdSpy.called).to.be.true; 90 | mock.readAws(); 91 | storage.read("foobar", function(err, data) { 92 | if (err) throw err; 93 | expect(data).to.eql(null); 94 | done(); 95 | }); 96 | }); 97 | }); 98 | }); 99 | 100 | it("should not fail when removing an unexisting file.", function(done) { 101 | mock.removeAws(); 102 | storage.remove("foobar", function(err) { 103 | if (err) throw err; 104 | done(); 105 | }); 106 | }); 107 | }); 108 | } 109 | 110 | // Test all the file storages implementation. 111 | 112 | testStorage("AWS", true, 113 | function createAWSStorage(options, statsdClient, callback) { 114 | callback(null, getFileStorage({ 115 | engine: "aws", 116 | settings: {sslEnabled: true} 117 | }, options, statsdClient)); 118 | }); 119 | 120 | testStorage("Filesystem", false, 121 | function createFilesystemStorage(options, statsdClient, callback) { 122 | var test_base_dir = path.join("/tmp", uuid.v4()); 123 | fs.mkdir(test_base_dir, '0750', function(err) { 124 | if (err) return callback(err); 125 | callback(null, getFileStorage({ 126 | engine: "filesystem", 127 | settings: {base_dir: test_base_dir} 128 | }, options, statsdClient)); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /test/fxa_test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | "use strict"; 5 | 6 | var expect = require("chai").expect; 7 | var supertest = require("supertest"); 8 | var sinon = require("sinon"); 9 | 10 | var loop = require("../loop"); 11 | var app = loop.app; 12 | var apiRouter = loop.apiRouter; 13 | var apiPrefix = loop.apiPrefix; 14 | var fxa = require("../loop/fxa"); 15 | var user = "alexis@notmyidea.org"; 16 | 17 | describe("fxa authentication", function() { 18 | var sandbox; 19 | 20 | beforeEach(function() { 21 | sandbox = sinon.sandbox.create(); 22 | }); 23 | 24 | afterEach(function() { 25 | sandbox.restore(); 26 | }); 27 | 28 | describe('middleware', function() { 29 | var jsonReq, expectedAssertion; 30 | 31 | // Create a route with the auth middleware installed. 32 | apiRouter.post('/with-middleware', 33 | fxa.getMiddleware("audience", function(req, res, assertion, next) { 34 | req.user = assertion.email; 35 | next(); 36 | }), function(req, res) { 37 | res.status(200).json(req.user); 38 | }); 39 | 40 | beforeEach(function() { 41 | jsonReq = supertest(app) 42 | .post(apiPrefix + '/with-middleware'); 43 | 44 | expectedAssertion = "BID-ASSERTION"; 45 | 46 | // Mock the calls to the external BrowserID verifier. 47 | sandbox.stub(fxa, "verifyAssertion", 48 | function(assertion, audience, trustedIssuers, callback) { 49 | if (assertion === expectedAssertion) { 50 | callback(null, {email: user}); 51 | } else { 52 | callback(new Error("invalid assertion \"1a2w3e4r5t6y\"")); 53 | } 54 | }); 55 | }); 56 | 57 | it("should require user authentication", function(done) { 58 | jsonReq 59 | .expect(401) 60 | .end(function(err, res) { 61 | if (err) throw err; 62 | expect(res.headers['www-authenticate']).to.eql('BrowserID'); 63 | done(); 64 | }); 65 | }); 66 | 67 | it("should reject invalid browserid assertions", function(done) { 68 | // Mock the calls to the external BrowserID verifier. 69 | jsonReq 70 | .set('Authorization', 'BrowserID ' + "invalid-assertion") 71 | .expect(401) 72 | .end(function(err, res) { 73 | if (err) throw err; 74 | expect(res.headers['www-authenticate']) 75 | .to.eql('BrowserID error="invalid assertion \"1a2w3e4r5t6y\""'); 76 | done(); 77 | }); 78 | }); 79 | 80 | it("should accept valid browserid assertions", function(done) { 81 | jsonReq 82 | .set('Authorization', 'BrowserID ' + expectedAssertion) 83 | .expect(200) 84 | .end(function(err) { 85 | if (err) throw err; 86 | done(); 87 | }); 88 | }); 89 | 90 | it("should set an 'user' property on the request object", function(done) { 91 | jsonReq 92 | .set('Authorization', 'BrowserID ' + expectedAssertion) 93 | .expect(200) 94 | .end(function(err, res) { 95 | if (err) throw err; 96 | expect(res.body).eql("alexis@notmyidea.org"); 97 | done(); 98 | }); 99 | }); 100 | }); 101 | 102 | describe('#verifyAssertion', function() { 103 | var audience, assertion; 104 | beforeEach(function() { 105 | audience = "https://loop.firefox.com"; 106 | assertion = { 107 | "audience": audience, 108 | "expires": 1389791993675, 109 | "issuer": "msisdn.accounts.firefox.com", 110 | "email": "4c352927cd4f4a4aa03d7d1893d950b8@msisdn.accounts.firefox.com", 111 | "status": "okay" 112 | }; 113 | sandbox.stub(fxa, "getAssertionAudience", function() { 114 | return audience; 115 | }); 116 | }); 117 | 118 | it("should throw if audiences is not an array", function() { 119 | var failure = function() { 120 | fxa.verifyAssertion("assertion", "not an array"); 121 | }; 122 | expect(failure).to.Throw(/should be an array/); 123 | }); 124 | 125 | it("should return an error if the verifier errored", function() { 126 | sandbox.stub(fxa.request, "post", function(opts, callback) { 127 | callback(null, "message", { 128 | status: "error", 129 | reason: "something bad" 130 | }); 131 | }); 132 | fxa.verifyAssertion("assertion", [audience], ["trustedIssuer"], 133 | function(err) { 134 | expect(err).eql("something bad"); 135 | }); 136 | }); 137 | 138 | it("should return an error if the verifier is not responding", function() { 139 | sandbox.stub(fxa.request, "post", function(opts, callback) { 140 | callback("error", null, null); 141 | }); 142 | fxa.verifyAssertion("assertion", [audience], ["trusted-issuer"], 143 | function(err) { 144 | expect(err).eql("error"); 145 | }); 146 | }); 147 | 148 | it("should not accept untrusted issuers", function() { 149 | assertion.issuer = "untrusted-issuer"; 150 | sandbox.stub(fxa.request, "post", function(opts, callback) { 151 | callback(null, null, assertion); 152 | }); 153 | 154 | fxa.verifyAssertion("assertion", [audience], ["trusted-issuer"], 155 | function(err) { 156 | expect(err.message).eql("Issuer is not trusted"); 157 | }); 158 | }); 159 | 160 | it("should accept trusted issuers", function() { 161 | assertion.issuer = "trusted-issuer"; 162 | sandbox.stub(fxa.request, "post", function(opts, callback) { 163 | callback(null, null, assertion); 164 | }); 165 | 166 | fxa.verifyAssertion("assertion", [audience], ["trusted-issuer"], 167 | function(err, data) { 168 | expect(err).eql(null); 169 | expect(data).eql(assertion); 170 | }); 171 | }); 172 | 173 | it("should change the audience given to the verifier if it is valid", 174 | function(done) { 175 | // Set the audience we return to app:// 176 | audience = "app://loop.firefox.com"; 177 | 178 | sandbox.stub(fxa.request, "post", function(opts, callback) { 179 | // Should ask the verifier with the app:// scheme. 180 | expect(opts.json.audience).eql('app://loop.firefox.com'); 181 | callback(null, null, assertion); 182 | }); 183 | 184 | // Start the verification. 185 | var validAudiences = ['http://loop.firefox.com', 186 | 'app://loop.firefox.com']; 187 | 188 | fxa.verifyAssertion(assertion, validAudiences, [assertion.issuer], 189 | done); 190 | }); 191 | 192 | it("should reject an invalid audience", function(done) { 193 | audience = "invalid"; 194 | 195 | var validAudiences = ['http://loop.firefox.com', 196 | 'app://loop.firefox.com']; 197 | 198 | fxa.verifyAssertion(assertion, validAudiences, [assertion.issuer], 199 | function(err) { 200 | expect(err.message).to.eql("Invalid audience"); 201 | done(); 202 | }); 203 | }); 204 | 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /test/headers_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | "use strict"; 5 | 6 | var supertest = require("supertest"); 7 | var expect = require("chai").expect; 8 | 9 | var loop = require("../loop"); 10 | var app = loop.app; 11 | var apiPrefix = loop.apiPrefix; 12 | var apiRouter = loop.apiRouter; 13 | var conf = loop.conf; 14 | 15 | describe("#headers", function(){ 16 | 17 | // Create routes to test the middleware 18 | apiRouter.get('/return200/', function(req, res) { 19 | res.status(200).json(); 20 | }); 21 | apiRouter.get('/return400/', function(req, res) { 22 | res.status(400).json(); 23 | }); 24 | apiRouter.get('/return401/', function(req, res) { 25 | res.status(401).json(); 26 | }); 27 | apiRouter.get('/return503/', function(req, res) { 28 | res.status(503).json(); 29 | }); 30 | 31 | it("should set a Timestamp header when returning a 200 ok.", function(done) { 32 | supertest(app).get(apiPrefix + '/return200/').expect(200).end( 33 | function(err, res) { 34 | if (err) throw err; 35 | expect(res.headers.hasOwnProperty('timestamp')).eql(true); 36 | done(); 37 | }); 38 | }); 39 | 40 | it("should set a Timestamp header when returning a 401.", function(done) { 41 | supertest(app).get(apiPrefix + '/return401/').expect(401).end( 42 | function(err, res) { 43 | if (err) throw err; 44 | expect(res.headers.hasOwnProperty('timestamp')).eql(true); 45 | done(); 46 | }); 47 | }); 48 | 49 | it("should not set a Timestamp header when returning a 400.", function(done) { 50 | supertest(app).get(apiPrefix + '/return400/').expect(400).end( 51 | function(err, res) { 52 | if (err) throw err; 53 | 54 | expect(res.headers.hasOwnProperty('x-timestamp')).eql(false); 55 | done(); 56 | }); 57 | }); 58 | 59 | it("should set a Retry-After header when returning a 503.", function(done) { 60 | supertest(app).get(apiPrefix + '/return503/').expect(503).end( 61 | function(err, res) { 62 | if (err) throw err; 63 | expect(res.headers.hasOwnProperty('retry-after')).eql(true); 64 | expect(res.headers['retry-after']).equal( 65 | conf.get('retryAfter').toString()); 66 | done(); 67 | }); 68 | }); 69 | 70 | it("should not return any X-Powered-By headers..", function(done) { 71 | supertest(app).get(apiPrefix + '/return200/').expect(200).end( 72 | function(err, res) { 73 | if (err) throw err; 74 | expect(res.headers.hasOwnProperty('x-powered-by')).eql(false); 75 | done(); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/hmac_test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | /* jshint expr: true */ 5 | "use strict"; 6 | 7 | var expect = require("chai").expect; 8 | var hmac = require("../loop/hmac"); 9 | var conf = require("../loop").conf; 10 | 11 | 12 | describe("#hmac", function() { 13 | 14 | it("should raise on missing secret", function(done) { 15 | expect(function() { 16 | hmac("Payload"); 17 | }).to.throw(/provide a secret./); 18 | done(); 19 | }); 20 | 21 | it("should have the same result for the same payload", function(done){ 22 | var firstTime = hmac("Payload", conf.get("userMacSecret")); 23 | expect(hmac("Payload", conf.get("userMacSecret"))).to.eql(firstTime); 24 | done(); 25 | }); 26 | 27 | it("should handle the algorithm argument", function(done){ 28 | expect(hmac( 29 | "Payload", 30 | conf.get("userMacSecret"), 31 | "sha1")).to.have.length(40); 32 | done(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/middlewares_test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var addHawk = require("superagent-hawk"); 8 | var supertest = addHawk(require("supertest")); 9 | var sinon = require("sinon"); 10 | var loop = require("../loop"); 11 | var app = loop.app; 12 | var apiRouter = loop.apiRouter; 13 | var apiPrefix = loop.apiPrefix; 14 | var logMetrics = require("../loop/middlewares").logMetrics; 15 | var hekaLogger = require("../loop/middlewares").hekaLogger; 16 | var expect = require("chai").expect; 17 | var conf = loop.conf; 18 | var pjson = require('../package.json'); 19 | var os = require("os"); 20 | 21 | var fakeNow = 1393595554796; 22 | 23 | 24 | describe("metrics middleware", function() { 25 | var sandbox; 26 | var logs; 27 | var oldMetrics; 28 | var clock; 29 | 30 | apiRouter.get("/with-metrics-middleware", logMetrics, function(req, res) { 31 | req.user = 'uuid'; 32 | req.callId = '1234'; 33 | req.callUrlData = {userMac: 'userMacHere'}; 34 | req.roomConnectionId = "roomConnectionId"; 35 | req.roomStorageData = {sessionToken: "sessionToken", 36 | sessionId: "sessionId"}; 37 | req.roomParticipantsCount = 5; 38 | req.roomStatusData = {state: 'init', event: 'Session.connectionCreated'}; 39 | req.roomToken = "roomToken"; 40 | res.status(200).json(); 41 | }); 42 | 43 | apiRouter.get("/with-401-on-metrics-middleware", logMetrics, function(req, res) { 44 | req.headers.authorization = "Hawk abcd"; 45 | req.hawk = {hawk: "hawk"}; 46 | res.set("WWW-Authenticate", 'Hawk error="boom"'); 47 | res.status(401).json(); 48 | }); 49 | 50 | beforeEach(function() { 51 | sandbox = sinon.sandbox.create(); 52 | clock = sinon.useFakeTimers(fakeNow); 53 | 54 | oldMetrics = conf.get('hekaMetrics'); 55 | var hekaMetrics = JSON.parse(JSON.stringify(oldMetrics)); 56 | hekaMetrics.activated = true; 57 | conf.set('hekaMetrics', hekaMetrics); 58 | sandbox.stub(hekaLogger, "info", function(op, log) { 59 | log.op = op; 60 | logs.push(log); 61 | }); 62 | logs = []; 63 | }); 64 | 65 | afterEach(function() { 66 | sandbox.restore(); 67 | conf.set('hekaMetrics', oldMetrics); 68 | clock.restore(); 69 | }); 70 | 71 | 72 | it("should write logs to heka", function(done) { 73 | supertest(app) 74 | .get(apiPrefix + '/with-metrics-middleware') 75 | .set('user-agent', 'Mouzilla') 76 | .set('accept-language', 'Breton du sud') 77 | .set('x-forwarded-for', 'ip1, ip2, ip3') 78 | .send({'action': 'join'}) 79 | .expect(200) 80 | .end(function(err) { 81 | if (err) throw err; 82 | var logged = logs[0]; 83 | 84 | expect(logged.op).to.eql('request.summary'); 85 | expect(logged.code).to.eql(200); 86 | expect(logged.path).to.eql('/with-metrics-middleware'); 87 | expect(logged.uid).to.eql('uuid'); 88 | expect(logged.callId).to.eql('1234'); 89 | expect(logged.agent).to.eql('Mouzilla'); 90 | expect(logged.v).to.eql(pjson.version); 91 | expect(logged.hostname).to.eql(os.hostname()); 92 | expect(logged.lang).to.eql('Breton du sud'); 93 | expect(logged.ip).to.eql('ip1, ip2, ip3'); 94 | expect(logged.errno).to.eql(0); 95 | expect(logged.time).to.eql('2014-02-28T13:52:34Z'); 96 | expect(logged.method).to.eql('get'); 97 | expect(logged.calleeId).to.eql('userMacHere'); 98 | expect(logged.callerId).to.eql('uuid'); 99 | expect(logged.action).to.eql('join'); 100 | expect(logged.roomToken).to.eql("roomToken"); 101 | expect(logged.participants).to.eql(5); 102 | expect(logged.roomConnectionId).to.eql("roomConnectionId"); 103 | expect(logged.sessionId).to.eql("sessionId"); 104 | expect(logged.sessionToken).to.eql("sessionToken"); 105 | done(); 106 | }); 107 | }); 108 | 109 | it("should log the add-on version to heka", function(done) { 110 | supertest(app) 111 | .get(apiPrefix + '/with-metrics-middleware') 112 | .set("x-loop-addon-ver", "1.3") 113 | .send({'action': 'join'}) 114 | .expect(200) 115 | .end(function(err) { 116 | if (err) throw err; 117 | var logged = logs[0]; 118 | 119 | expect(logged.op).to.eql('request.summary'); 120 | expect(logged.code).to.eql(200); 121 | expect(logged.path).to.eql('/with-metrics-middleware'); 122 | expect(logged.loopAddonVersion).to.eql("1.3"); 123 | done(); 124 | }); 125 | }); 126 | 127 | it("should log room status details", function (done) { 128 | supertest(app) 129 | .get(apiPrefix + '/with-metrics-middleware') 130 | .end(function(err) { 131 | if (err) throw err; 132 | var logged = logs[0]; 133 | expect(logged.state).to.eql('init'); 134 | expect(logged.event).to.eql('Session.connectionCreated'); 135 | done(); 136 | }); 137 | }); 138 | 139 | it("should write 401 errors to heka", function(done) { 140 | supertest(app) 141 | .get(apiPrefix + '/with-401-on-metrics-middleware') 142 | .expect(401) 143 | .end(function(err) { 144 | if (err) throw err; 145 | var logged = logs[0]; 146 | 147 | expect(logged.op).to.eql('request.summary'); 148 | expect(logged.code).to.eql(401); 149 | expect(logged.path).to.eql('/with-401-on-metrics-middleware'); 150 | expect(logged.method).to.eql('get'); 151 | expect(logged.authorization).to.eql("Hawk abcd"); 152 | expect(logged.hawk).to.eql({hawk: "hawk"}); 153 | expect(logged.error).to.eql('Hawk error="boom"'); 154 | done(); 155 | }); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /test/nock.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var Filesystem = require('../loop/filestorage/filesystem'); 8 | 9 | var local = new Filesystem({base_dir: '/tmp'}); 10 | 11 | function mock(options) { 12 | var _nock = require('nock'); 13 | _nock.enableNetConnect(/127.0.0.1|localhost/); 14 | var bucket = options.bucket; 15 | var u = '/' + bucket + '/XXX'; 16 | var id; 17 | var outstandingMocks = []; 18 | 19 | function nock() { 20 | var scope = _nock.apply(_nock, arguments); 21 | outstandingMocks.push(scope); 22 | return scope; 23 | } 24 | 25 | function done(verifyNock) { 26 | if (verifyNock) { 27 | outstandingMocks.forEach(function(mock) { 28 | mock.done(); 29 | }); 30 | } 31 | outstandingMocks = []; 32 | _nock.cleanAll(); 33 | } 34 | 35 | function writeAws() { 36 | return nock('https://s3.amazonaws.com') 37 | .filteringPath(function filter(_path) { 38 | id = _path.replace('/' + bucket + '/', ''); 39 | return _path.replace(id, 'XXX'); 40 | }) 41 | .put(u) 42 | .reply(200, function(uri, body, callback) { 43 | local.write(id, body, function(err) { 44 | if (err) throw err; 45 | callback(err); 46 | }); 47 | }); 48 | } 49 | 50 | function readAws() { 51 | return nock('https://s3.amazonaws.com') 52 | .filteringPath(function filter(_path) { 53 | id = _path.replace('/' + bucket + '/', ''); 54 | return _path.replace(id, 'XXX'); 55 | }) 56 | .get(u) 57 | .reply(200, function(uri, body, callback) { 58 | local.read(id, function(err, data) { 59 | if (err) throw err; 60 | callback(err, data); 61 | }); 62 | }); 63 | } 64 | 65 | function removeAws() { 66 | return nock('https://s3.amazonaws.com') 67 | .filteringPath(function filter(_path) { 68 | id = _path.replace('/' + bucket + '/', ''); 69 | return _path.replace(id, 'XXX'); 70 | }) 71 | .delete(u) 72 | .reply(204, function(uri, body, callback) { 73 | local.remove(id, function() { 74 | callback(null); 75 | }); 76 | }); 77 | } 78 | 79 | return { 80 | readAws: readAws, 81 | writeAws: writeAws, 82 | removeAws: removeAws, 83 | done: done 84 | }; 85 | } 86 | 87 | module.exports = mock; 88 | -------------------------------------------------------------------------------- /test/redis_client_test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | var expect = require("chai").expect; 7 | var sinon = require("sinon"); 8 | 9 | var redis_client = require("../loop/storage/redis_client"); 10 | 11 | describe("redis_client", function() { 12 | var sandbox; 13 | 14 | beforeEach(function() { 15 | sandbox = sinon.sandbox.create(); 16 | }); 17 | 18 | afterEach(function() { 19 | sandbox.restore(); 20 | }); 21 | 22 | describe("createClient", function() { 23 | it("should return an object", function() { 24 | expect(redis_client.createClient(6379, "localhost")).to.be.an("object"); 25 | }); 26 | 27 | it("should let default multi support with sharding disabled", 28 | function(done) { 29 | var client = redis_client.createClient(6379, "localhost", { 30 | sharding: false 31 | }); 32 | var stub = sandbox.stub(client, "set", function(key, value, cb) { 33 | cb(); 34 | }); 35 | 36 | var multi = client.multi(); 37 | multi.set("foo", "foo"); 38 | multi.set("bar", "bar"); 39 | multi.exec(function(err) { 40 | sinon.assert.notCalled(stub); 41 | done(err); 42 | }); 43 | }); 44 | 45 | describe("#multi", function() { 46 | var client; 47 | beforeEach(function() { 48 | client = redis_client.createClient(6379, "localhost", { 49 | sharding: true 50 | }) 51 | }); 52 | 53 | it("should return an object", function() { 54 | expect(client.multi()).to.be.an("object"); 55 | }); 56 | 57 | it("should expose supported multi operations", function() { 58 | var multi = client.multi(); 59 | expect(Object.getPrototypeOf(multi)) 60 | .to.include.keys(redis_client.MULTI_OPERATIONS); 61 | }); 62 | 63 | it("should stack multi operations and execute them", function(done) { 64 | sandbox.stub(client, "set", function(key, value, cb) { 65 | cb(); 66 | }); 67 | 68 | var multi = client.multi(); 69 | multi.set("foo", "foo"); 70 | multi.set("bar", "bar"); 71 | multi.exec(function(err) { 72 | sinon.assert.calledTwice(client.set); 73 | done(err); 74 | }); 75 | }); 76 | 77 | it("should return a list of operations responses", function(done) { 78 | var set = sandbox.stub(client, "set", function(key, value, cb) { 79 | cb(null, value); 80 | }); 81 | 82 | var multi = client.multi(); 83 | multi.set("foo", "foo"); 84 | multi.set("bar", "bar"); 85 | multi.exec(function(err) { 86 | sinon.assert.calledTwice(client.set); 87 | expect(set.getCall(0).args[0]).to.eql("foo"); 88 | expect(set.getCall(1).args[0]).to.eql("bar"); 89 | done(err); 90 | }); 91 | }); 92 | }) 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /test/redis_migration_test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | var expect = require("chai").expect; 7 | 8 | var getClient = require("../loop/storage/redis_migration"); 9 | var conf = require("../loop").conf; 10 | var async = require("async"); 11 | 12 | describe("redis migration", function() { 13 | var client; 14 | 15 | beforeEach(function() { 16 | var options = { 17 | oldDB: conf.get('storage').settings, 18 | newDB: { db: 4 } 19 | }; 20 | client = getClient(options) 21 | }); 22 | 23 | afterEach(function(done){ 24 | client.flushdb(done); 25 | }); 26 | 27 | it("should copy a key from the old db if it exists", function(done) { 28 | client.old_db.set('key', 'value', function(err){ 29 | if (err) throw err; 30 | client.old_db.expire('key', 2, function(err){ 31 | if (err) throw err; 32 | client.get('key', function(err, data) { 33 | if (err) throw err; 34 | expect(data).to.eql('value'); 35 | client.new_db.get('key', function(err, data) { 36 | if (err) throw err; 37 | expect(data).to.eql('value') 38 | // Check it preserves the TTL info. 39 | client.new_db.pttl('key', function(err, ttl) { 40 | if (err) throw err; 41 | expect(ttl).to.gte(1500); 42 | expect(ttl).to.lte(2000); 43 | // Ensure the old value is deleted properly. 44 | client.old_db.get('key', function(err, data){ 45 | if (err) throw err; 46 | expect(data).to.eql(null); 47 | done(); 48 | }); 49 | }); 50 | }); 51 | }); 52 | }); 53 | }); 54 | }); 55 | 56 | it("should return multi's commands results.", function(done) { 57 | var multi = client.multi(); 58 | multi.set("foo", "foobar"); 59 | multi.get("foo"); 60 | multi.exec(function(err, results) { 61 | if (err) throw err; 62 | expect(results).to.eql(["OK", "foobar"]) 63 | done(); 64 | }); 65 | }); 66 | 67 | it("should copy all keys in case there is a '*' in the key", function(done) { 68 | // Let's create a bunch of keys in the old database 69 | async.each( 70 | ['key1', 'key2', 'key3', 'key4'], 71 | function(key, callback){ 72 | client.old_db.set(key, 'value', callback); 73 | }, 74 | function(){ 75 | client.keys('key*', function(err, keys){ 76 | expect(keys).to.length(4); 77 | if (err) throw err; 78 | client.mget(keys, function(err, values){ 79 | if (err) throw err; 80 | expect(values).to.eql(['value', 'value', 'value', 'value']); 81 | done(); 82 | }); 83 | }); 84 | }); 85 | }); 86 | 87 | it("should have a working multi implementation", function(done) { 88 | var multi = client.multi(); 89 | multi.set('key', 'value'); 90 | multi.set('anotherkey', 'anothervalue'); 91 | multi.exec(function(err) { 92 | if (err) throw err; 93 | client.new_db.get('key', function(err, value) { 94 | if (err) throw err; 95 | expect(value).to.eql('value'); 96 | client.new_db.get('anotherkey', function(err, value) { 97 | if (err) throw err; 98 | expect(value).to.eql('anothervalue'); 99 | done(); 100 | }); 101 | }); 102 | }); 103 | 104 | }); 105 | 106 | describe("with env set to prod", function() { 107 | var env; 108 | beforeEach(function(){ 109 | env = conf.get('env') 110 | conf.set('env', 'prod'); 111 | }); 112 | 113 | afterEach(function() { 114 | conf.set('env', env); 115 | }); 116 | 117 | it("should not flush the db even if asked for", function(done) { 118 | client.set("key", "value", function(err) { 119 | if (err) throw err; 120 | client.flushdb(function(err) { 121 | if (err) throw err; 122 | client.get("key", function(err, result) { 123 | if (err) throw err; 124 | expect(result).to.eql("value"); 125 | done(); 126 | }); 127 | }); 128 | }); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /test/simplepush_test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var expect = require("chai").expect; 8 | var request = require("request"); 9 | var sinon = require("sinon"); 10 | var assert = sinon.assert; 11 | 12 | var SimplePush = require("../loop/simplepush"); 13 | 14 | 15 | describe("simplePush object", function() { 16 | var requests, sandbox; 17 | 18 | beforeEach(function() { 19 | requests = []; 20 | sandbox = sinon.sandbox.create(); 21 | 22 | sandbox.stub(request, "put", function(options, callback) { 23 | requests.push(options); 24 | callback(undefined); 25 | }); 26 | }); 27 | 28 | afterEach(function(){ 29 | sandbox.restore(); 30 | }); 31 | 32 | it("should do a put on each of the given URLs", function() { 33 | var simplePush = new SimplePush(); 34 | simplePush.notify("reason", ["url1", "url2"], 12345); 35 | expect(requests).to.length(2); 36 | }); 37 | 38 | it("should dedupe urls before using them", function() { 39 | var simplePush = new SimplePush(); 40 | simplePush.notify("reason", ["url1", "url2", "url1"], 12345); 41 | expect(requests).to.length(2); 42 | }); 43 | 44 | it("should work even if only one url is passed", function() { 45 | var simplePush = new SimplePush(); 46 | simplePush.notify("reason", "url1", 12345); 47 | expect(requests).to.length(1); 48 | }); 49 | 50 | it("should send the version when doing the request", function() { 51 | var simplePush = new SimplePush(); 52 | simplePush.notify("reason", "url1", 12345); 53 | expect(requests).to.length(1); 54 | expect(requests[0].form.version).to.eql(12345); 55 | }); 56 | 57 | it("should notify using the statsd client if present", function() { 58 | var statsdClient = { increment: function() {} }; 59 | var statsdSpy = sandbox.spy(statsdClient, "increment"); 60 | 61 | var simplePush = new SimplePush(statsdClient); 62 | simplePush.notify("reason", "url1", 12345); 63 | 64 | assert.calledOnce(statsdSpy); 65 | assert.calledWithExactly(statsdSpy, "loop.simplepush.call", 1, ["reason", "success"]); 66 | }); 67 | 68 | it("should notify using the statsd client for errors if present", function() { 69 | // Change request stub. 70 | sandbox.restore(); 71 | sandbox = sinon.sandbox.create(); 72 | 73 | sandbox.stub(request, "put", function(options, callback) { 74 | requests.push(options); 75 | callback("error"); 76 | }); 77 | 78 | var statsdClient = { increment: function() {} }; 79 | var statsdSpy = sandbox.spy(statsdClient, "increment"); 80 | 81 | var simplePush = new SimplePush(statsdClient); 82 | simplePush.notify("reason", "url1", 12345); 83 | 84 | assert.calledOnce(statsdSpy); 85 | assert.calledWithExactly(statsdSpy, "loop.simplepush.call", 1, ["reason", "failure"]); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/support.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | 7 | var expect = require("chai").expect; 8 | 9 | function getMiddlewares(app, method, url) { 10 | var apiRouter; 11 | if (app.hasOwnProperty("_router")) { 12 | apiRouter = app._router; 13 | } else { 14 | apiRouter = app; 15 | } 16 | 17 | var methodStack = apiRouter.stack.filter(function(e) { 18 | if (e.route && e.route.path === url && 19 | e.route.methods[method]) { 20 | return true; 21 | } 22 | return false; 23 | }).shift(); 24 | 25 | return methodStack.route.stack.map(function(e) { 26 | return e.handle; 27 | }); 28 | } 29 | 30 | function intersection(array1, array2) { 31 | return array1.filter(function(n) { 32 | return array2.indexOf(n) !== -1; 33 | }); 34 | } 35 | 36 | function expectFormattedError(res, code, errno, message) { 37 | expect(res.body).eql({ 38 | code: code, 39 | errno: errno, 40 | error: message 41 | }); 42 | } 43 | 44 | module.exports = { 45 | getMiddlewares: getMiddlewares, 46 | intersection: intersection, 47 | expectFormattedError: expectFormattedError 48 | }; 49 | -------------------------------------------------------------------------------- /test/tokenlib_test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | "use strict"; 5 | 6 | var expect = require("chai").expect; 7 | var tokenlib = require("../loop/tokenlib"); 8 | 9 | describe("tokenlib", function() { 10 | describe("#generateToken", function() { 11 | it("should return a token of [a-zA-Z0-9_-].", function() { 12 | var shortToken, s = 10; 13 | while (s > 0) { 14 | shortToken = tokenlib.generateToken(s); 15 | expect(shortToken).to.match(/^[a-zA-Z0-9\-_]+$/); 16 | s--; 17 | } 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/tools_test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | "use strict"; 6 | var async = require('async'); 7 | var expect = require("chai").expect; 8 | var uuid = require('node-uuid'); 9 | var sinon = require('sinon'); 10 | 11 | var loop = require("../loop"); 12 | var time = require('../loop/utils').time; 13 | var getStorage = require("../loop/storage"); 14 | 15 | var moveRedisData = require("../tools/move_redis_data"); 16 | var migrateRoomParticipants = require("../tools/migrate_1121403_roomparticipants"); 17 | var get_session_id_for_rooms = require("../tools/get_tokbox_sessionid_for_room_token"); 18 | var get_number_fxa_users = require("../tools/get_number_fxa_users"); 19 | 20 | 21 | var storage = loop.storage; 22 | 23 | describe('Tools', function() { 24 | 25 | describe('redis migration', function() { 26 | var options = { 27 | engine: "redis", 28 | settings: { 29 | "db": 5, 30 | "migrateFrom": { "db": 4 } 31 | } 32 | }; 33 | 34 | var storage, range, sandbox; 35 | 36 | beforeEach(function(done) { 37 | sandbox = sinon.sandbox.create(); 38 | // Mock console.log to not output during the tests. 39 | sandbox.stub(console, 'log', function(){}); 40 | 41 | range = []; 42 | // Populate the range with a number of items. 43 | for (var i = 0; i < 200; i++) { 44 | range.push(i); 45 | } 46 | 47 | storage = getStorage(options); 48 | // Add items to the old database. 49 | var multi = storage._client.old_db.multi(); 50 | range.forEach(function(i) { 51 | multi.set('old.foo' + i, 'bar'); 52 | multi.setex('old.foofoo' + i, 10, 'barbar'); 53 | multi.hmset('old.myhash' + i, {'foo': 'bar'}); 54 | }); 55 | 56 | multi.exec(function(err) { 57 | if (err) throw err; 58 | // Add items to the new database. 59 | var multi = storage._client.new_db.multi(); 60 | range.forEach(function(i) { 61 | multi.set('new.foo' + i, 'bar'); 62 | multi.setex('new.foofoo' + i, 10, 'barbar'); 63 | multi.hmset('new.myhash' + i, {'foo': 'bar'}); 64 | }); 65 | multi.exec(done); 66 | }); 67 | }); 68 | 69 | afterEach(function(done) { 70 | sandbox.restore(); 71 | storage._client.old_db.flushdb(function(err) { 72 | if (err) throw err; 73 | storage._client.new_db.flushdb(done); 74 | }); 75 | }); 76 | 77 | it("old+new values should be in the new db after migration", function(done){ 78 | moveRedisData(options.settings, function(err, counter) { 79 | if (err) throw err; 80 | expect(counter).to.eql(600); 81 | expect(range).to.length(200); 82 | // Check old and new values are present. 83 | async.each(range, function(i, cb) { 84 | var multi = storage._client.new_db.multi(); 85 | multi.get('old.foo' + i); 86 | multi.ttl('old.foofoo' + i); 87 | multi.hmget('old.myhash' + i, 'foo'); 88 | multi.get('new.foo' + i); 89 | multi.ttl('new.foofoo' + i); 90 | multi.hmget('new.myhash' + i, 'foo'); 91 | multi.exec(function(err, results) { 92 | if (err) throw err; 93 | expect(results[0]).to.eql('bar'); 94 | expect(results[1]).to.be.lte(10); 95 | expect(results[1]).to.be.gt(0); 96 | expect(results[2]).to.eql('bar'); 97 | expect(results[3]).to.eql('bar'); 98 | expect(results[4]).to.be.lte(10); 99 | expect(results[4]).to.be.gt(0); 100 | expect(results[5]).to.eql('bar'); 101 | cb(); 102 | }); 103 | }, done); 104 | }); 105 | }); 106 | 107 | it("should delete keys from the old database once copied", function(done) { 108 | moveRedisData(options.settings, function(err) { 109 | if (err) throw err; 110 | // Check old and new values are present. 111 | var multi = storage._client.old_db.multi(); 112 | multi.get('old.foo'); 113 | multi.ttl('old.foofoo'); 114 | multi.hgetall('old.myhash'); 115 | multi.exec(function(err, results) { 116 | if (err) throw err; 117 | expect(results[0]).to.eql(null); 118 | expect(results[1]).to.eql('-2'); 119 | expect(results[2]).to.eql(''); 120 | done(); 121 | }); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('room participants migration bug 1121413', function() { 127 | // Before each test, populate the database with some old data. 128 | var roomTokens = ['ooByyZNJyEs', 'M6iilFJply8']; 129 | var participants = { 130 | '12345': { 131 | id: uuid.v4(), 132 | hawkIdHmac: '12345', 133 | displayName: 'alexis', 134 | clientMaxSize: 4, 135 | userMac: 'alexis mac', 136 | account: 'alexis account' 137 | }, 138 | '45678': { 139 | id: uuid.v4(), 140 | hawkIdHmac: '45678', 141 | displayName: 'natim', 142 | clientMaxSize: 4, 143 | userMac: 'natim mac', 144 | account: 'natim account' 145 | } 146 | }; 147 | 148 | beforeEach(function(done) { 149 | var multi = storage._client.multi(); 150 | roomTokens.forEach(function(token) { 151 | Object.keys(participants).forEach(function(id) { 152 | // Deep copy. 153 | var participant = JSON.parse(JSON.stringify(participants[id])); 154 | participant.expiresAt = time() + 5000; 155 | 156 | var data = JSON.stringify(participant); 157 | multi.hset('roomparticipants.' + token, id, data); 158 | }); 159 | }); 160 | multi.exec(done); 161 | }); 162 | 163 | afterEach(function(done) { 164 | storage.drop(done); 165 | }); 166 | 167 | it('migrates the old keys to the new format', function(done) { 168 | migrateRoomParticipants(function(err) { 169 | if (err) throw err; 170 | async.each(roomTokens, function(roomToken, ok) { 171 | storage.getRoomParticipants(roomToken, function(err, dbParticipants) { 172 | if (err) return ok(err); 173 | expect(dbParticipants).to.length(2); 174 | expect(dbParticipants[0]).to.eql(participants['12345']); 175 | expect(dbParticipants[1]).to.eql(participants['45678']); 176 | ok(); 177 | }); 178 | }, function(err) { 179 | if (err) throw err; 180 | done(err); 181 | }); 182 | }); 183 | }); 184 | }); 185 | 186 | describe('get_tokbox_session_ids_for_room_tokens', function() { 187 | var roomTokens = ['ooByyZNJyEs', 'M6iilFJply8']; 188 | 189 | beforeEach(function(done) { 190 | var multi = storage._client.multi(); 191 | roomTokens.forEach(function(token) { 192 | multi.set('room.' + token, JSON.stringify({sessionId: 'tokbox_' + token})); 193 | }); 194 | multi.exec(done); 195 | }); 196 | 197 | afterEach(function(done) { 198 | storage.drop(done); 199 | }); 200 | 201 | it("should return the list of rooms with the sessionId", function(done) { 202 | get_session_id_for_rooms(storage._client, roomTokens, function(err, results) { 203 | if (err) throw err; 204 | expect(results).to.eql({ 205 | 'ooByyZNJyEs': 'tokbox_ooByyZNJyEs', 206 | 'M6iilFJply8': 'tokbox_M6iilFJply8' 207 | }); 208 | done(); 209 | }); 210 | }); 211 | }); 212 | 213 | describe('get_number_fxa_users', function() { 214 | before(function(done) { 215 | var multi = storage._client.multi(); 216 | multi.set('userid.12345', 'encrypted_user_id'); 217 | multi.set('userid.56789', 'encrypted_user_id'); 218 | multi.set('hawkuser.56789', 'userMac'); 219 | multi.exec(done); 220 | }); 221 | 222 | after(function(done) { 223 | storage.drop(done); 224 | }); 225 | 226 | it("should return the number of FxA users in the database", function(done) { 227 | get_number_fxa_users(function(results) { 228 | expect(results).to.eql({ 229 | count: 1, 230 | total: 2 231 | }); 232 | done(); 233 | }); 234 | }); 235 | }); 236 | }); 237 | -------------------------------------------------------------------------------- /test/utils_test.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | "use strict"; 5 | 6 | var expect = require('chai').expect; 7 | var utils = require('../loop/utils'); 8 | var conf = require('../loop').conf; 9 | var sinon = require('sinon'); 10 | 11 | 12 | describe("utils", function() { 13 | describe("#getProgressURL", function() { 14 | afterEach(function() { 15 | conf.set("protocol", "http"); 16 | }); 17 | 18 | it("should return a ws:// url if the protocol is http.", function() { 19 | var host = "127.0.0.1:5123"; 20 | conf.set("protocol", "http"); 21 | var progressURL = utils.getProgressURL(host); 22 | expect(progressURL).to.match(/ws:\/\//); 23 | expect(progressURL).to.match(/127.0.0.1:5123/); 24 | }); 25 | 26 | it("should return a wss:// url if the protocol is https.", function() { 27 | var host = "127.0.0.1:5123"; 28 | conf.set("protocol", "https"); 29 | var progressURL = utils.getProgressURL(host); 30 | expect(progressURL).to.match(/wss:\/\//); 31 | expect(progressURL).to.match(/127.0.0.1:443/); 32 | }); 33 | }); 34 | 35 | describe("#time", function() { 36 | var clock, now; 37 | 38 | beforeEach(function() { 39 | now = Date.now() 40 | clock = sinon.useFakeTimers(now); 41 | }); 42 | 43 | afterEach(function() { 44 | clock.restore(); 45 | }); 46 | 47 | it("should return the current timestamp", function() { 48 | expect(utils.time()).to.eql(parseInt(now / 1000, 10)); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tools/README.rst: -------------------------------------------------------------------------------- 1 | Some tools to query the REDIS database 2 | ====================================== 3 | 4 | IDLETIME and TTL warning 5 | ------------------------ 6 | 7 | For now the redis TTL command updates the Least Recently Used value 8 | and thus the IDLETIME of the key. 9 | 10 | Some of this tools are based on the TTL command: 11 | 12 | - ``_get_expiration.js`` 13 | - ``_graph_expiration.sh`` 14 | - ``_get_1111579_fxa_impacted.js`` 15 | - ``_get_ttl_hawk.js`` 16 | 17 | Some of this tool are based on the IDLETIME value: 18 | 19 | - ``get_active_inactive_users.js`` 20 | - ``get_expiration_estimate.js`` 21 | - ``remove_old_keys.js`` 22 | 23 | The use of one of the first list's command will override the result of 24 | the second list's commands. To prevent errorneous usage, commands 25 | based on TTL have their names prefixed with an ``_``. 26 | 27 | 28 | Active and Inactive users 29 | ------------------------- 30 | 31 | - ``get_active_inactive_users.js`` 32 | 33 | This script gives you some information about users activity:: 34 | 35 | Processing 33 keys 36 | 3 sessions used during the last 24 hours. 37 | 30 sessions not used for the last 24 hours. 38 | 0 sessions not used for the last two weeks. 39 | 0 sessions not used for a month. 40 | 41 | It is based on the key idletime. 42 | 43 | 44 | Average calls per user 45 | ---------------------- 46 | 47 | - ``get_average_calls_per_user.js`` 48 | 49 | Return the average number of calls per user:: 50 | 51 | processing 21 users having calls. 52 | 22 calls for 21 users having calls. 53 | Average 1.05 Calls per user. 54 | 22 calls for 21 users having created a call-url. 55 | Average 1.05 calls per user. 56 | 57 | 58 | Average call-urls per user 59 | -------------------------- 60 | 61 | - ``get_average_call-urls_per_user.js`` 62 | 63 | Return the average number of call-urls per user:: 64 | 65 | processing 21 users 66 | 22 URLs for 21 users. 67 | Average 1.05 URLs per user. 68 | 69 | 70 | Average rooms per user 71 | ---------------------- 72 | 73 | - ``get_average_rooms_per_user.js`` 74 | 75 | Return the average number of rooms per user:: 76 | 77 | processing 4 users 78 | 4 rooms for 4 users. 79 | Average 1.00 rooms per user. 80 | 81 | 82 | Keys expirations 83 | ---------------- 84 | 85 | - ``_get_expiration.js`` 86 | - ``_graph_expiration.sh`` 87 | - ``get_expiration_estimate.js`` 88 | 89 | Each redis keys has a Time-To-Live so we know when it would exipre. 90 | This script gives you an agenda of what amount of data will expire at which date. 91 | 92 | :: 93 | 94 | processing 179 keys 95 | 121 keys will never expires. (6077 Bytes) 96 | 2015/1/31 49 49 4944 Bytes (in 24 days) 97 | 2015/2/6 9 58 907 Bytes (in 30 days) 98 | 99 | You can also use ``_graph_expiration.sh`` to draw an histogram of this data 100 | 101 | :: 102 | 103 | 2015/1/31 49 ================= 104 | 2015/2/6 9 ==== 105 | 106 | 107 | These two first commands updates the LRU of the keys. 108 | 109 | If you want an estimation of the expiration, you can use ``get_expiration_estimate.js``:: 110 | 111 | This command will display the creation date and the average expiration date:: 112 | 113 | Processing 179 keys 114 | 2015/1/7 179 11928 Bytes (1 days ago) 115 | 2015/1/31 179 11928 Bytes (in 24 days) 116 | 117 | This ``expiration-estimate`` works better when all keys have a TTL 118 | because it cannot detect the one which will never expire. 119 | 120 | Also because ``get_expiration_estimate`` is based on the IDLETIME, if you 121 | run it after ``_get_expiration.js`` all keys will have the same expiration 122 | date in the average time. 123 | 124 | 125 | Impacted users by the FxA bug 126 | ----------------------------- 127 | 128 | - ``_get_1111579_fxa_impacted.js`` 129 | 130 | We had Bug 1111579 that was converting some existing authenticated 131 | users into unauthenticated users. 132 | 133 | This command let you know the number of impacted sessions and delete broken ones. 134 | 135 | :: 136 | $ node _get_1111579_fxa_impacted 137 | processing 1 keys 138 | . 139 | number of impacted users 0 over 1 140 | 141 | :: 142 | 143 | $ node _get_1111579_fxa_impacted --delete 144 | processing 1 keys 145 | . 146 | number of impacted users 0 over 1 147 | The keys have been removed from the database 148 | 149 | 150 | Hawk User Info 151 | -------------- 152 | 153 | - ``get_hawk_user_info.js`` 154 | 155 | This script takes an HawkId or HawkIdHmac and give you informations about the user. 156 | 157 | Providing an HawkId:: 158 | 159 | $ node get_hawk_user_info.js 88d5a28f545bb406ddc6c6a5276cbfe0aa10fdba425f4808e2d6c3acdbfdaeda 160 | Trying with HawkIdHmac: de9cd5c5ded9e2df982723d96361f56c0d72c936dc177cbff1f147bac1445f63 161 | { anonymous: false, userId: 'foobar@example.com' } 162 | 163 | Providing an HawkIdHmac:: 164 | 165 | $ node get_hawk_user_info.js de9cd5c5ded9e2df982723d96361f56c0d72c936dc177cbff1f147bac1445f63 166 | Trying with HawkIdHmac: dcf3932ac6c0ed48994bb17c5ecc150e03e84a76e523b698c8cc75c2ca278611 167 | Trying with HawkIdHmac: de9cd5c5ded9e2df982723d96361f56c0d72c936dc177cbff1f147bac1445f63 168 | { anonymous: false, userId: '' } 169 | 170 | Providing an unauthenticated HawkIdHmac:: 171 | 172 | $ node get_hawk_user_info.js 81d2afea33181e32023c9042b42157ebf453d3c04435b386ded7c378fb338b01 173 | Trying with HawkIdHmac: c4c9a59a1a12719e395cb64e35d53d515335612e4b3208c51c89beecaa496393 174 | Trying with HawkIdHmac: 81d2afea33181e32023c9042b42157ebf453d3c04435b386ded7c378fb338b01 175 | { anonymous: true } 176 | 177 | 178 | Redis Usage 179 | ----------- 180 | 181 | - ``get_redis_usage.js`` 182 | 183 | This script gives you general information about the redis keys:: 184 | 185 | # Server 186 | [...] 187 | 188 | # Clients 189 | [...] 190 | 191 | # Memory 192 | [...] 193 | 194 | # Persistence 195 | [...] 196 | 197 | # Stats 198 | [...] 199 | 200 | # Replication 201 | [...] 202 | 203 | # CPU 204 | [...] 205 | 206 | # Keyspace 207 | db0:keys=179,expires=58,avg_ttl=2118094581 208 | 209 | ==== 210 | 211 | spurls.*: 64 212 | spurls.6e0a93dd218b767f799be64534c01c1f0706361a6b0caba1ca9c8099d2d8078b.6e0a93dd218b767f799be64534c01c1f0706361a6b0caba1ca9c8099d2d8078b 213 | spurls.a33b8202d462bbfa0bf1559b8ff3e05f710832c5103a142a2263e178810f858f 214 | 215 | callurl.*: 22 216 | callurl.we8ADTMY6o8 217 | callurl.SPwwEPBW7OA 218 | 219 | userUrls.*: 21 220 | userUrls.40057524c466604ecad39c88871a896dee5fd4718cd37373f4703db12fbd5ee7 221 | userUrls.24ce5f27583b5eb2de9655c21a221546e97629e892a871e161ebdab861317829 222 | 223 | call.*: 0 224 | 225 | userCalls.*: 18 226 | userCalls.055620865c42a71a1049d75692411095d9d68ba0843ff4c8a8fc825643c0756e 227 | userCalls.23cf69cbd9265e9b78444f71c43beee6d7f85976df284af575d5c37d4cf780f6 228 | 229 | callstate.*: 0 230 | 231 | hawkuser.*: 1 232 | hawkuser.de9cd5c5ded9e2df982723d96361f56c0d72c936dc177cbff1f147bac1445f63 233 | 234 | userid.*: 1 235 | userid.de9cd5c5ded9e2df982723d96361f56c0d72c936dc177cbff1f147bac1445f63 236 | 237 | hawk.*: 33 238 | hawk.fabaf4f9f60c6f8d97158c75f0b9b2661738130eb654eed13d5ecdc8739d0f1a 239 | hawk.23cf69cbd9265e9b78444f71c43beee6d7f85976df284af575d5c37d4cf780f6 240 | 241 | oauth.token.*: 1 242 | oauth.token.de9cd5c5ded9e2df982723d96361f56c0d72c936dc177cbff1f147bac1445f63 243 | 244 | oauth.state.*: 1 245 | oauth.state.de9cd5c5ded9e2df982723d96361f56c0d72c936dc177cbff1f147bac1445f63 246 | 247 | userRooms.*: 4 248 | userRooms.b8ae434636685b6d31c0b0efb96e649bd67c33c1c3fa9a23caaf3aaf804cfdd9 249 | userRooms.494e14e5f507317b7392eafb3ca2a2372bd61a5735dbc06d9d70abe74b7d1d57 250 | 251 | rooms.*: 0 252 | 253 | 254 | Remove OLD keys 255 | --------------- 256 | 257 | - ``remove_old_keys.js`` 258 | 259 | Count and list the keys that where not used for the last 15 days and 260 | propose to remove them. 261 | 262 | This command uses the IDLETIME of the key to decide whether to remove 263 | it or not. 264 | 265 | :: 266 | 267 | Processing 179 keys 268 | Looking for keys not used since : Thursday, January 08, 2015 269 | 179 keys found. (11928 Bytes) 270 | Would you like to remove these keys? [y/N] 271 | 272 | No key has been removed. 273 | 274 | With the ``--verbose`` option:: 275 | 276 | Processing 179 keys 277 | Looking for keys not used since : Thursday, January 08, 2015 278 | Selected keys: 279 | - callurl.we8ADTMY6o8 280 | - spurls.6e0a93dd218b767f799be64534c01c1f0706361a6b0caba1ca9c8099d2d8078b.6e0a93dd218b767f799be64534c01c1f0706361a6b0caba1ca9c8099d2d8078b 281 | - userUrls.40057524c466604ecad39c88871a896dee5fd4718cd37373f4703db12fbd5ee7 282 | - userUrls.24ce5f27583b5eb2de9655c21a221546e97629e892a871e161ebdab861317829 283 | - hawk.fabaf4f9f60c6f8d97158c75f0b9b2661738130eb654eed13d5ecdc8739d0f1a 284 | 5 keys found. (850 Bytes) 285 | Would you like to remove these keys? [y/N] 286 | 287 | 288 | Ping Sentry 289 | ----------- 290 | 291 | - ``send_sentry.js`` 292 | 293 | A command that send an error message to Sentry to check the Sentry configuration. 294 | 295 | 296 | TTL of an Hawk session 297 | ---------------------- 298 | 299 | - ``_get_ttl_hawk.js`` 300 | 301 | This command tells you the time to live of an hawk session given it's HawkId:: 302 | 303 | $ node ttl_hawk.js 88d5a28f545bb406ddc6c6a5276cbfe0aa10fdba425f4808e2d6c3acdbfdaeda 304 | redis-cli TTL hawk.de9cd5c5ded9e2df982723d96361f56c0d72c936dc177cbff1f147bac1445f63 305 | expire in 2584761 seconds 306 | -------------------------------------------------------------------------------- /tools/_get_1111579_fxa_impacted.js: -------------------------------------------------------------------------------- 1 | var hmac = require('../loop/hmac'); 2 | var conf = require('../loop/config').conf; 3 | var async = require('async'); 4 | var redis = require("redis"); 5 | 6 | var storage = conf.get("storage"); 7 | var hawkIdSecret = conf.get("hawkIdSecret"); 8 | 9 | var args = process.argv.slice(2); 10 | 11 | if (args.indexOf('--help') >= 0) { 12 | console.log("USAGE: " + process.argv.slice(0, 2).join(' ') + " [--delete]"); 13 | process.exit(0); 14 | } 15 | 16 | var delKeys = false; 17 | 18 | if (args.indexOf('--delete') >= 0) { 19 | delKeys = true; 20 | } 21 | 22 | 23 | if (storage.engine === "redis") { 24 | var options = storage.settings; 25 | var client = redis.createClient( 26 | options.port, 27 | options.host, 28 | options.options 29 | ); 30 | if (options.db) client.select(options.db); 31 | 32 | var toDelete = []; 33 | 34 | client.keys("userid.*", function(err, keys){ 35 | console.log("processing", keys.length, "keys"); 36 | async.map(keys, function(key, done) { 37 | var hawkUserKey = key.replace("userid", "hawkuser") 38 | client.ttl(hawkUserKey, function(err, ttl) { 39 | var isImpacted = ttl === -2; 40 | if (isImpacted) { 41 | if (delKeys) { 42 | // Remove the impacted userid 43 | toDelete.push(key); 44 | // Remove the session 45 | toDelete.push(key.replace("userid", "hawk")); 46 | } 47 | 48 | process.stdout.write("i"); // This is an impacted user. 49 | } else { 50 | process.stdout.write("."); 51 | } 52 | done(null, isImpacted); 53 | }); 54 | }, function(err, results){ 55 | var impacted = results.reduce(function(total, current) { 56 | return total + (current === true); 57 | }, 0); 58 | 59 | console.log('\nnumber of impacted users', impacted, "over", results.length); 60 | 61 | if (delKeys === true) { 62 | client.del(toDelete, function(err) { 63 | console.log('\nThe keys have been removed from the database'); 64 | process.exit(0); 65 | 66 | }); 67 | } else { 68 | process.exit(0); 69 | } 70 | }); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /tools/_get_expiration.js: -------------------------------------------------------------------------------- 1 | var conf = require('../loop/config').conf; 2 | var async = require('async'); 3 | var redis = require("redis"); 4 | 5 | var storage = conf.get("storage"); 6 | 7 | var args = process.argv.slice(2); 8 | 9 | var verbose = args.indexOf('--verbose') !== -1; 10 | 11 | if (storage.engine === "redis") { 12 | var options = storage.settings; 13 | var client = redis.createClient( 14 | options.port, 15 | options.host, 16 | options.options 17 | ); 18 | if (options.db) client.select(options.db); 19 | 20 | client.keys("*", function(err, keys){ 21 | if (err) throw err; 22 | console.log("processing", keys.length, "keys"); 23 | 24 | var multi = client.multi(); 25 | var multi2 = client.multi(); 26 | 27 | keys.forEach(function(key) { 28 | if (key) { 29 | multi.ttl(key); 30 | multi2.debug("object", key); 31 | } 32 | 33 | }); 34 | multi.exec(function(err, ttls) { 35 | if (err) throw err; 36 | 37 | multi2.exec(function(err, sizes) { 38 | if (err) throw err; 39 | 40 | var expirations = {}; 41 | var key_sizes = {}; 42 | 43 | if (verbose && ttls.indexOf(-1) !== -1) { 44 | console.log("Keys that will never expires:"); 45 | } 46 | 47 | for(var i = 0; i < keys.length; i++) { 48 | var ttl = ttls[i]; 49 | var size = parseInt(sizes[i].split(' ')[4].split(':')[1], 10); 50 | 51 | if (ttl == -1) { 52 | expirations.never = expirations.never ? expirations.never +1 : 1; 53 | key_sizes.never = key_sizes.never ? key_sizes.never + size : size; 54 | if (verbose) { 55 | console.log(keys[i]); 56 | } 57 | } else { 58 | var day = new Date(Date.now() + ttl * 1000).toDateString(); 59 | expirations[day] = expirations[day] ? expirations[day] + 1 : 1; 60 | key_sizes[day] = key_sizes[day] ? key_sizes[day] + size : size; 61 | } 62 | } 63 | 64 | var expiration_keys = Object.keys(expirations); 65 | expiration_keys.sort(function(a, b) { 66 | if (a === "never") { 67 | return -1; 68 | } else if (b === "never") { 69 | return 1; 70 | } else { 71 | var date_a = new Date(a); 72 | var date_b = new Date(b); 73 | return date_a.getTime() - date_b.getTime(); 74 | } 75 | }); 76 | 77 | var today = new Date(); 78 | var cumulative = 0; 79 | expiration_keys.forEach(function(key) { 80 | if (key === "never") { 81 | console.log(expirations[key] + " keys will never expires. (" + key_sizes[key] + " Bytes)"); 82 | } else { 83 | var date = new Date(key); 84 | var nb_days = parseInt((date.getTime() - today.getTime()) / (24 * 3600 * 1000), 10) + 1; 85 | cumulative += expirations[key]; 86 | console.log(date.getUTCFullYear() + "/" + (date.getUTCMonth() + 1) + "/" + date.getUTCDate() + "\t" + expirations[key] + "\t" + cumulative + "\t" + (key_sizes[key]) + " Bytes\t(in " + nb_days + " days)"); 87 | } 88 | }); 89 | process.exit(0); 90 | }); 91 | }); 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /tools/_get_ttl_hawk.js: -------------------------------------------------------------------------------- 1 | var hmac = require('../loop/hmac'); 2 | var conf = require('../loop/config').conf; 3 | var redis = require("redis"); 4 | 5 | var storage = conf.get("storage"); 6 | 7 | var hawkIdSecret = conf.get("hawkIdSecret"); 8 | 9 | var argv = require('yargs').argv; 10 | 11 | if (argv._.length > 0) { 12 | var hawkId = argv._[0]; 13 | var hawkIdHmac = hmac(hawkId, hawkIdSecret); 14 | console.log("redis-cli TTL hawk." + hawkIdHmac); 15 | 16 | if (storage.engine === "redis") { 17 | var options = storage.settings; 18 | var client = redis.createClient( 19 | options.port, 20 | options.host, 21 | options.options 22 | ); 23 | if (options.db) client.select(options.db); 24 | 25 | client.ttl("hawk." + hawkIdHmac, function(err, result) { 26 | if (err) throw err; 27 | console.log("expire in", result, "seconds"); 28 | process.exit(0); 29 | }); 30 | } 31 | } else { 32 | console.log("USAGE: " + argv.$0 + " hawkId"); 33 | } 34 | -------------------------------------------------------------------------------- /tools/_graph_expiration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | node expiration.js | tail -n+3 | cut -f 1,2 | perl -lane 'print $F[0], "\t", $F[1], "\t", "=" x ($F[1] / 3 + 1)' 3 | -------------------------------------------------------------------------------- /tools/get_active_inactive_users.js: -------------------------------------------------------------------------------- 1 | var conf = require('../loop/config').conf; 2 | var async = require('async'); 3 | var redis = require("redis"); 4 | 5 | var storage = conf.get("storage"); 6 | var utils = require("./utils"); 7 | var keysInformation = utils.keysInformation; 8 | var dbInformation = utils.dbInformation; 9 | 10 | var args = process.argv.slice(2); 11 | 12 | var verbose = args.indexOf('--verbose') !== -1; 13 | 14 | var A_DAY = 3600 * 24; 15 | var TWO_WEEKS = 3600 * 24 * 7 * 2; 16 | var A_MONTH = 3600 * 24 * 30; 17 | 18 | 19 | if (storage.engine === "redis") { 20 | var options = storage.settings; 21 | var client = redis.createClient( 22 | options.port, 23 | options.host, 24 | options.options 25 | ); 26 | if (options.db) client.select(options.db); 27 | 28 | keysInformation(client, 'hawk.*', function(err, keysInfo) { 29 | if (err) throw err; 30 | 31 | console.log("Processing " + keysInfo.length + " keys"); 32 | 33 | var active = 0; 34 | var unactive = 0; 35 | var biweekly = 0; 36 | var monthly = 0; 37 | 38 | keysInfo.forEach(function(key) { 39 | var lruDate = new Date(Date.now() - key.lru_seconds_idle * 1000).getTime(); 40 | var now = Date.now(); 41 | 42 | var delta = (now - lruDate) / 1000; 43 | 44 | if (delta <= A_DAY) { 45 | active++; 46 | } else { 47 | unactive++; 48 | } 49 | 50 | if (delta > TWO_WEEKS) { 51 | biweekly++; 52 | } 53 | 54 | if (delta > A_MONTH) { 55 | monthly++; 56 | } 57 | }); 58 | 59 | console.log(active + " sessions used during the last 24 hours."); 60 | console.log(unactive + " sessions not used for the last 24 hours."); 61 | console.log(biweekly + " sessions not used for the last two weeks."); 62 | console.log(monthly + " sessions not used for a month."); 63 | 64 | process.exit(0); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /tools/get_average_call-urls_per_user.js: -------------------------------------------------------------------------------- 1 | var conf = require('../loop/config').conf; 2 | var async = require('async'); 3 | var redis = require("redis"); 4 | 5 | var storage = conf.get("storage"); 6 | 7 | if (storage.engine === "redis") { 8 | var options = storage.settings; 9 | var client = redis.createClient( 10 | options.port, 11 | options.host, 12 | options.options 13 | ); 14 | if (options.db) client.select(options.db); 15 | 16 | client.keys("userUrls.*", function(err, keys){ 17 | if (err) throw err; 18 | console.log("processing", keys.length, "users"); 19 | 20 | var multi = client.multi(); 21 | keys.forEach(function(key) { 22 | multi.scard(key); 23 | }); 24 | multi.exec(function(err, results) { 25 | if (err) throw err; 26 | var totalUrls = results.reduce(function(total, result) { 27 | return total + result; 28 | }, 0); 29 | process.stdout.write(totalUrls + " URLs for " + 30 | keys.length + " users.\nAverage " + 31 | (totalUrls / keys.length).toFixed(2) + 32 | " URLs per user.\n"); 33 | process.exit(0); 34 | }); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /tools/get_average_calls_per_user.js: -------------------------------------------------------------------------------- 1 | var conf = require('../loop/config').conf; 2 | var async = require('async'); 3 | var redis = require("redis"); 4 | 5 | var storage = conf.get("storage"); 6 | 7 | if (storage.engine === "redis") { 8 | var options = storage.settings; 9 | var client = redis.createClient( 10 | options.port, 11 | options.host, 12 | options.options 13 | ); 14 | if (options.db) client.select(options.db); 15 | 16 | var multi = client.multi(); 17 | multi.eval("return #redis.pcall('keys', 'userUrls.*')", 0); 18 | multi.keys("userUrls.*"); 19 | 20 | multi.exec(function(err, results) { 21 | if (err) throw err; 22 | var users = results[0]; 23 | var keys = results[1]; 24 | console.log("processing", keys.length, "users having calls."); 25 | 26 | var multi = client.multi(); 27 | keys.forEach(function(key) { 28 | multi.scard(key); 29 | }); 30 | multi.exec(function(err, results) { 31 | if (err) throw err; 32 | var totalCalls = results.reduce(function(total, result) { 33 | return total + result; 34 | }, 0); 35 | process.stdout.write(totalCalls + " calls for " + 36 | keys.length + " users having calls.\nAverage " + 37 | (totalCalls / keys.length).toFixed(2) + 38 | " Calls per user.\n"); 39 | process.stdout.write(totalCalls + " calls for " + 40 | users + " users having created a call-url.\nAverage " + 41 | (totalCalls / users).toFixed(2) + 42 | " calls per user.\n"); 43 | process.exit(0); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /tools/get_average_rooms_per_user.js: -------------------------------------------------------------------------------- 1 | var conf = require('../loop/config').conf; 2 | var async = require('async'); 3 | var redis = require("redis"); 4 | 5 | var storage = conf.get("storage"); 6 | 7 | if (storage.engine === "redis") { 8 | var options = storage.settings; 9 | var client = redis.createClient( 10 | options.port, 11 | options.host, 12 | options.options 13 | ); 14 | if (options.db) client.select(options.db); 15 | 16 | client.keys("userRooms.*", function(err, keys){ 17 | if (err) throw err; 18 | console.log("processing", keys.length, "users"); 19 | 20 | var multi = client.multi(); 21 | keys.forEach(function(key) { 22 | multi.scard(key); 23 | }); 24 | multi.exec(function(err, results) { 25 | if (err) throw err; 26 | var totalRooms = results.reduce(function(total, result) { 27 | return total + result; 28 | }, 0); 29 | process.stdout.write(totalRooms + " rooms for " + 30 | keys.length + " users.\nAverage " + 31 | (totalRooms / keys.length).toFixed(2) + 32 | " rooms per user.\n"); 33 | process.exit(0); 34 | }); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /tools/get_expiration_estimate.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var conf = require('../loop/config').conf; 6 | var async = require('async'); 7 | var redis = require("redis"); 8 | 9 | var storage = conf.get("storage"); 10 | var utils = require("./utils"); 11 | var keysInformation = utils.keysInformation; 12 | var dbInformation = utils.dbInformation; 13 | 14 | var args = process.argv.slice(2); 15 | 16 | var verbose = args.indexOf('--verbose') !== -1; 17 | 18 | if (storage.engine === "redis") { 19 | var options = storage.settings; 20 | var client = redis.createClient( 21 | options.port, 22 | options.host, 23 | options.options 24 | ); 25 | if (options.db) client.select(options.db); 26 | 27 | dbInformation(client, options.db, function(err, info) { 28 | if (err) throw err; 29 | 30 | var average_ttl = parseInt(info.avg_ttl / 1000, 10); 31 | console.log("Processing " + info.keys + " keys"); 32 | 33 | keysInformation(client, '*', function(err, keysInfo) { 34 | if (err) throw err; 35 | 36 | var creations = {}; 37 | var sizes = {}; 38 | var expirations = {}; 39 | 40 | keysInfo.forEach(function(key) { 41 | var creationDate = new Date(Date.now() - key.lru_seconds_idle * 1000).toDateString(); 42 | var expirationDate = new Date(Date.now() + (average_ttl - key.lru_seconds_idle) * 1000).toDateString(); 43 | var size = parseInt(key.serializedlength, 10); 44 | expirations[expirationDate] = expirations[expirationDate] ? expirations[expirationDate] + 1 : 1; 45 | creations[creationDate] = creations[creationDate] ? creations[creationDate] + 1 : 1; 46 | sizes[creationDate] = sizes[creationDate] ? sizes[creationDate] + size : size; 47 | sizes[expirationDate] = sizes[expirationDate] ? sizes[expirationDate] + size : size; 48 | }); 49 | 50 | var creation_keys = Object.keys(creations); 51 | creation_keys.sort(function(a, b) { 52 | var date_a = new Date(a); 53 | var date_b = new Date(b); 54 | return date_a.getTime() - date_b.getTime(); 55 | }); 56 | 57 | var today = new Date(); 58 | creation_keys.forEach(function(key) { 59 | var date = new Date(key); 60 | var nb_days = parseInt((today.getTime() - date.getTime()) / (24 * 3600 * 1000), 10) + 1; 61 | console.log(date.getUTCFullYear() + "/" + (date.getUTCMonth() + 1) + "/" + date.getUTCDate() + "\t" + creations[key] + "\t" + (sizes[key]) + " Bytes\t(" + nb_days + " days ago)"); 62 | }); 63 | 64 | var expiration_keys = Object.keys(expirations); 65 | expiration_keys.sort(function(a, b) { 66 | var date_a = new Date(a); 67 | var date_b = new Date(b); 68 | return date_a.getTime() - date_b.getTime(); 69 | }); 70 | 71 | expiration_keys.forEach(function(key) { 72 | var date = new Date(key); 73 | var nb_days = parseInt((date.getTime() - today.getTime()) / (24 * 3600 * 1000), 10) + 1; 74 | console.log(date.getUTCFullYear() + "/" + (date.getUTCMonth() + 1) + "/" + date.getUTCDate() + "\t" + expirations[key] + "\t" + (sizes[key]) + " Bytes\t(in " + nb_days + " days)"); 75 | }); 76 | 77 | 78 | process.exit(0); 79 | }); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /tools/get_number_fxa_devices.js: -------------------------------------------------------------------------------- 1 | var conf = require('../loop/config').conf; 2 | var redis = require("redis"); 3 | 4 | var storage = conf.get("storage"); 5 | 6 | function main(callback) { 7 | if (storage.engine === "redis") { 8 | var options = storage.settings; 9 | var client = redis.createClient( 10 | options.port, 11 | options.host, 12 | options.options 13 | ); 14 | if (options.db) client.select(options.db); 15 | 16 | client.keys('hawkuser.*', function(err, keys) { 17 | if (err) throw err; 18 | var multi = client.multi(); 19 | 20 | keys.forEach(function(key) { 21 | multi.get(key); 22 | }); 23 | 24 | multi.exec(function(err, results) { 25 | var users = {}; 26 | 27 | results.forEach(function(result) { 28 | if(!users.hasOwnProperty(result)) { 29 | users[result] = 1; 30 | } else { 31 | users[result]++; 32 | } 33 | }); 34 | 35 | var total = Object.keys(users).length; 36 | var max = 0; 37 | var sum = 0; 38 | var moreThan1 = 0; 39 | for(var key in users) { 40 | sum += users[key]; 41 | if (users[key] > 1) { 42 | moreThan1++; 43 | } 44 | if (max < users[key]) { 45 | max = users[key]; 46 | } 47 | } 48 | 49 | 50 | callback({ 51 | users: total, 52 | average: sum/total, 53 | max: max, 54 | moreThan1: moreThan1 55 | }); 56 | }); 57 | }); 58 | } 59 | } 60 | 61 | 62 | if (require.main === module) { 63 | main(function(results) { 64 | process.stdout.write(results.users + " FxA users with " + 65 | results.average + " devices on average and a maximum of " + results.max + ", "); 66 | process.stdout.write("and " + results.moreThan1 + " FxA users with more than one device.\n"); 67 | process.exit(0); 68 | }); 69 | } 70 | 71 | module.exports = main; 72 | -------------------------------------------------------------------------------- /tools/get_number_fxa_users.js: -------------------------------------------------------------------------------- 1 | var conf = require('../loop/config').conf; 2 | var redis = require("redis"); 3 | 4 | var storage = conf.get("storage"); 5 | 6 | function main(callback) { 7 | if (storage.engine === "redis") { 8 | var options = storage.settings; 9 | var client = redis.createClient( 10 | options.port, 11 | options.host, 12 | options.options 13 | ); 14 | if (options.db) client.select(options.db); 15 | 16 | var multi = client.multi(); 17 | multi.eval("return #redis.pcall('keys', 'userid.*')", 0); 18 | multi.eval("return #redis.pcall('keys', 'hawkuser.*')", 0); 19 | multi.exec(function (err, results) { 20 | if (err) throw err; 21 | callback({ 22 | total: results[0], 23 | count: results[1] 24 | }); 25 | }); 26 | } 27 | } 28 | 29 | 30 | if (require.main === module) { 31 | main(function(results) { 32 | process.stdout.write(results.count + " FxA users for " + 33 | results.total + " users."); 34 | process.exit(0); 35 | }); 36 | } 37 | 38 | module.exports = main; 39 | -------------------------------------------------------------------------------- /tools/get_redis_usage.js: -------------------------------------------------------------------------------- 1 | var conf = require('../loop/config').conf; 2 | var async = require('async'); 3 | var redis = require("redis"); 4 | 5 | var storage = conf.get("storage"); 6 | 7 | if (storage.engine === "redis") { 8 | var options = storage.settings; 9 | var client = redis.createClient( 10 | options.port, 11 | options.host, 12 | options.options 13 | ); 14 | if (options.db) client.select(options.db); 15 | 16 | client.info(function(err, info){ 17 | if (err) throw err; 18 | process.stdout.write(info); 19 | process.stdout.write("\n ==== \n\n"); 20 | 21 | var KEYS = ["spurls", "callurl", "userUrls", "call", "userCalls", 22 | "callstate", "hawkuser", "userid", "hawk", "oauth.token", 23 | "oauth.state", "userRooms", "rooms"]; 24 | 25 | var multi = client.multi(); 26 | KEYS.forEach(function(key) { 27 | multi.keys(key + ".*"); 28 | }); 29 | 30 | multi.exec(function(err, results) { 31 | if (err) throw err; 32 | var i = 0; 33 | results.forEach(function(result) { 34 | process.stdout.write(KEYS[i] + ".*: \t" + result.length + "\n"); 35 | 36 | // If possible display one or two key sample. 37 | if (result.length > 0) { 38 | process.stdout.write(result[0]); 39 | process.stdout.write("\n"); 40 | } 41 | 42 | // If possible display one or two key sample. 43 | if (result.length > 1) { 44 | process.stdout.write(result[1]); 45 | process.stdout.write("\n"); 46 | } 47 | process.stdout.write("\n"); 48 | i++; 49 | }); 50 | process.exit(0); 51 | }); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /tools/get_tokbox_sessionid_for_room_token.js: -------------------------------------------------------------------------------- 1 | var readline = require('readline'); 2 | var conf = require('../loop/config').conf; 3 | var redis = require('redis'); 4 | 5 | var storage = conf.get('storage'); 6 | var args = process.argv.slice(2); 7 | var verbose = args.indexOf('--verbose') !== -1; 8 | 9 | function get_session_id_for_rooms(client, roomTokens, callback) { 10 | var multi = client.multi(); 11 | 12 | roomTokens.forEach(function(roomToken) { 13 | multi.get('room.' + roomToken); 14 | }); 15 | multi.exec(function(err, results) { 16 | if (err) return callback(err); 17 | 18 | var output = {}; 19 | results.forEach(function(result, i) { 20 | if (result !== null) { 21 | output[roomTokens[i]] = JSON.parse(result).sessionId; 22 | } 23 | }); 24 | callback(null, output); 25 | }); 26 | } 27 | 28 | if (require.main === module && storage.engine === 'redis') { 29 | var options = storage.settings; 30 | var client = redis.createClient( 31 | options.port, 32 | options.host, 33 | options.options 34 | ); 35 | if (options.db) client.select(options.db); 36 | 37 | console.log('Please enter a roomToken per line. Ctrl+D to stop.'); 38 | 39 | rl = readline.createInterface({ 40 | input: process.stdin, 41 | output: process.stdout, 42 | terminal: false 43 | }); 44 | 45 | var roomTokens = []; 46 | 47 | rl.on('line', function (roomToken) { 48 | roomTokens.push(roomToken); 49 | }); 50 | 51 | rl.on('close', function() { 52 | get_session_id_for_rooms(client, roomTokens, function(err, output) { 53 | if (err) throw err; 54 | console.log(JSON.stringify(output)); 55 | process.exit(0); 56 | }); 57 | }); 58 | } 59 | 60 | module.exports = get_session_id_for_rooms; 61 | -------------------------------------------------------------------------------- /tools/hawk_user_info.js: -------------------------------------------------------------------------------- 1 | var hmac = require('../loop/hmac'); 2 | var conf = require('../loop/config').conf; 3 | var decrypt = require('../loop/encrypt').decrypt; 4 | var redis = require("redis"); 5 | 6 | var storage = conf.get("storage"); 7 | 8 | var hawkIdSecret = conf.get("hawkIdSecret"); 9 | 10 | var argv = require('yargs').argv; 11 | 12 | if (argv._.length > 0) { 13 | if (storage.engine === "redis") { 14 | var options = storage.settings; 15 | var client = redis.createClient( 16 | options.port, 17 | options.host, 18 | options.options 19 | ); 20 | if (options.db) client.select(options.db); 21 | 22 | var getInfo = function(hawkId, hawkIdHmac, callback) { 23 | var multi = client.multi(); 24 | console.log("Trying with HawkIdHmac: " + hawkIdHmac); 25 | 26 | multi.get("hawk." + hawkIdHmac); 27 | multi.get("hawkuser." + hawkIdHmac); 28 | 29 | multi.exec(function(err, results) { 30 | if (err) throw err; 31 | if (results[0] === null) { 32 | return callback(null, null); 33 | } 34 | client.get("userid." + hawkIdHmac, function(err, encryptedUserId) { 35 | if (err) return callback(err); 36 | if (encryptedUserId === null) { 37 | return callback(null, { 38 | anonymous: true 39 | }); 40 | } 41 | var userId; 42 | if (hawkId) { 43 | try { 44 | userId = decrypt(hawkId, encryptedUserId); 45 | } catch (e) {} 46 | } 47 | callback(null, { 48 | anonymous: false, 49 | userId: userId || "" 50 | }); 51 | }); 52 | }); 53 | }; 54 | 55 | var displayInfo = function(err, info) { 56 | if (info === null) { 57 | console.log("No information found for this hawkIdHmac."); 58 | } else { 59 | console.log(info); 60 | } 61 | process.exit(0); 62 | }; 63 | 64 | var hawkId = argv._[0]; 65 | var hawkIdHmac = hmac(hawkId, hawkIdSecret); 66 | 67 | getInfo(hawkId, hawkIdHmac, function(err, info) { 68 | if (err) throw err; 69 | if (info === null) { 70 | getInfo(null, hawkId, displayInfo); 71 | return; 72 | } 73 | displayInfo(null, info); 74 | }); 75 | } 76 | } else { 77 | console.log("USAGE: " + argv.$0 + " hawkId || hawkIdHmac"); 78 | } 79 | -------------------------------------------------------------------------------- /tools/migrate_1121403_roomparticipants.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | * Migrate the room participants from the old format to the new one. 6 | * 7 | * Previously, a "roomparticipants.{roomToken}" key was holding the list of 8 | * participants, with their value. The Redis data-type was a hash. 9 | * 10 | * Bug 1121403 changed that in order to have expiration events on the 11 | * participants; the data type is now a set, refering to all the participants 12 | * ids. Participants details are now stored with their own independent key. 13 | * 14 | * This script copies all the old keys to the new format. 15 | **/ 16 | 17 | "use strict"; 18 | 19 | var async = require('async'); 20 | var redis = require("redis"); 21 | var conf = require('../loop/config').conf; 22 | var time = require('../loop/utils').time; 23 | 24 | var storage = conf.get("storage"); 25 | 26 | function main(callback) { 27 | if (storage.engine === "redis") { 28 | var options = storage.settings; 29 | var client = redis.createClient( 30 | options.port, 31 | options.host, 32 | options.options 33 | ); 34 | if (options.db) client.select(options.db); 35 | 36 | client.keys('roomparticipants.*', function(err, keys) { 37 | if (err) throw err; 38 | async.each(keys, function(key, done) { 39 | var roomToken = key.split('.')[1]; 40 | // Access the key using the old format. 41 | client.hgetall(key, function(err, participants){ 42 | // If we have an error, it means we have the right format already. 43 | // Skip to the next key. 44 | if (err) return done(); 45 | 46 | // Delete the hash key since we want to replace it with a set. 47 | client.del(key, function(err) { 48 | if (err) return done(); 49 | var multi = client.multi(); 50 | 51 | async.each(Object.keys(participants), function(id, ok) { 52 | var participant = JSON.parse(participants[id]); 53 | 54 | var ttl = participant.expiresAt - time(); 55 | delete participant.expiresAt; 56 | 57 | multi.psetex( 58 | 'roomparticipant.' + roomToken + '.' + participant.hawkIdHmac, 59 | ttl, 60 | JSON.stringify(participant) 61 | ); 62 | multi.sadd('roomparticipants.' + roomToken, participant.hawkIdHmac); 63 | multi.pexpire('roomparticipants.' + roomToken, ttl); 64 | ok(); 65 | }, function(err) { 66 | multi.exec(done); 67 | }); 68 | }); 69 | }) 70 | }, callback); 71 | }) 72 | } 73 | } 74 | 75 | if (require.main === module) { 76 | main(function() { 77 | process.exit(0); 78 | }); 79 | } 80 | 81 | module.exports = main; 82 | -------------------------------------------------------------------------------- /tools/move_redis_data.js: -------------------------------------------------------------------------------- 1 | var conf = require('../loop/config').conf; 2 | var migrationClient = require("../loop/storage/redis_migration"); 3 | var async = require('async'); 4 | 5 | var moveRedisData = function(options, callback) { 6 | var client = migrationClient({ 7 | oldDB: options.migrateFrom, 8 | newDB: options 9 | }); 10 | 11 | var migratedCounter = 0; 12 | 13 | function scanAndMigrate(cursor) { 14 | if (cursor === undefined) { 15 | cursor = 0; 16 | } 17 | client.old_db.scan(cursor, function(err, results) { 18 | if (err) return callback(err); 19 | var nextCursor = parseInt(results[0], 10); 20 | if (results[1] === "") { 21 | return callback(null, 0); 22 | } 23 | var keys = results[1].split(','); 24 | 25 | console.log("migrating ", keys.length, "keys"); 26 | migratedCounter += keys.length; 27 | 28 | async.each(keys, function(key, done) { 29 | client.copyKey(key, done); 30 | }, function(err) { 31 | if (nextCursor === 0 || err) { 32 | callback(err, migratedCounter); 33 | } else { 34 | scanAndMigrate(nextCursor); 35 | } 36 | }); 37 | }); 38 | }; 39 | 40 | scanAndMigrate(); 41 | } 42 | 43 | function main(options, callback) { 44 | // Actually call the database migration script. 45 | if (options.migrateFrom !== undefined) { 46 | console.log("starting migration"); 47 | console.time("migration"); 48 | moveRedisData(options, function(err, counter){ 49 | console.timeEnd("migration"); 50 | console.log("migrated", counter, "keys"); 51 | callback(err, counter); 52 | }); 53 | } else { 54 | console.log("please, change your configuration to enable migration"); 55 | callback(); 56 | } 57 | } 58 | 59 | if (require.main === module) { 60 | var options = conf.get('storage').settings; 61 | main(options, function() { 62 | process.exit(0); 63 | }); 64 | } 65 | 66 | module.exports = main; 67 | -------------------------------------------------------------------------------- /tools/remove_old_keys.js: -------------------------------------------------------------------------------- 1 | var conf = require('../loop/config').conf; 2 | var async = require('async'); 3 | var redis = require("redis"); 4 | 5 | var storage = conf.get("storage"); 6 | var utils = require("./utils"); 7 | var keysInformation = utils.keysInformation; 8 | var dbInformation = utils.dbInformation; 9 | var sget = utils.sget; 10 | var args = process.argv.slice(2); 11 | 12 | var verbose = args.indexOf('--verbose') !== -1; 13 | var TWO_WEEKS = 3600 * 24 * 7 * 2; 14 | 15 | if (storage.engine === "redis") { 16 | var options = storage.settings; 17 | var client = redis.createClient( 18 | options.port, 19 | options.host, 20 | options.options 21 | ); 22 | if (options.db) client.select(options.db); 23 | 24 | keysInformation(client, '*', function(err, keysInfo) { 25 | if (err) throw err; 26 | 27 | console.log("Processing " + keysInfo.length + " keys"); 28 | console.log("Looking for keys not used since : " + new Date(Date.now() - TWO_WEEKS * 1000).toLocaleDateString()); 29 | 30 | var toDelete = []; 31 | var freed = 0; 32 | 33 | keysInfo.forEach(function(key) { 34 | var lruDate = new Date(Date.now() - key.lru_seconds_idle * 1000).getTime(); 35 | var now = Date.now(); 36 | var size = parseInt(key.serializedlength, 10); 37 | 38 | var delta = (now - lruDate) / 1000; 39 | 40 | if (delta > TWO_WEEKS) { 41 | toDelete.push(key.key); 42 | freed = freed + size; 43 | } 44 | 45 | }); 46 | 47 | if (verbose) { 48 | console.log("Selected keys:"); 49 | toDelete.forEach(function(key) { 50 | console.log("- " + key); 51 | }); 52 | } 53 | console.log(toDelete.length + " keys found. (" + freed + " Bytes)"); 54 | 55 | if (toDelete.length > 0) { 56 | var entry = sget("Would you like to remove these keys? [y/N]"); 57 | if (entry.toLowerCase().indexOf("y") === 0) { 58 | client.del(toDelete, function(err) { 59 | if (err) throw err; 60 | console.log(toDelete.length + " keys have been removed. And " + freed + " Bytes freed."); 61 | process.exit(0); 62 | }); 63 | return; 64 | } 65 | } 66 | console.log("No key has been removed."); 67 | process.exit(0); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /tools/send_sentry.js: -------------------------------------------------------------------------------- 1 | var args = process.argv.slice(2); 2 | 3 | if (args.indexOf('--help') >= 0) { 4 | console.log("USAGE: " + process.argv.slice(0, 2).join(' ') + " [MESSAGE]"); 5 | process.exit(0); 6 | } 7 | 8 | var conf = require('../loop/config').conf; 9 | var raven = require('raven'); 10 | 11 | var message = args[0] || 'Server is able to communicate with Sentry'; 12 | 13 | var ravenClient = new raven.Client(conf.get('sentryDSN')); 14 | ravenClient.on('logged', function(){ 15 | console.log('OK'); 16 | }); 17 | ravenClient.on('error', function(e){ 18 | console.log('KO', e); 19 | }); 20 | 21 | ravenClient.captureMessage(message, {level: 'info'}); 22 | -------------------------------------------------------------------------------- /tools/utils.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | rl = require('readline'); 3 | 4 | /** 5 | * Returns the DEBUG OBJECT key information for each key matching the 6 | * pattern into a list of dict. 7 | * - Contains refcount, encoding, serializedlength, lru and lru_seconds_idle 8 | */ 9 | function keysInformation(client, pattern, callback) { 10 | if (callback === undefined) { 11 | callback = pattern; 12 | pattern = '*'; 13 | } 14 | 15 | client.keys(pattern, function(err, keys){ 16 | if (err) return callback(err); 17 | 18 | var multi = client.multi(); 19 | 20 | keys.forEach(function(key) { 21 | if (key) { 22 | multi.debug("object", key); 23 | } 24 | 25 | }); 26 | multi.exec(function(err, objects) { 27 | if (err) return callback(err); 28 | var results = []; 29 | 30 | keys.forEach(function(key, i) { 31 | var obj = objects[i].split(" "); 32 | var data = {key: key}; 33 | obj.forEach(function(value) { 34 | var keyVal = value.split(":"); 35 | if (keyVal.length === 2) { 36 | data[keyVal[0]] = keyVal[1]; 37 | } 38 | }); 39 | results.push(data); 40 | }); 41 | 42 | callback(null, results); 43 | }); 44 | }); 45 | } 46 | 47 | /** 48 | * Returns the current database information into a dict. 49 | * - keys: Total keys' number, 50 | * - expires: Total expiring keys' number, 51 | * - avg_ttl: Average keys' time to live. 52 | */ 53 | 54 | function dbInformation(client, db, callback) { 55 | if (callback === undefined) { 56 | callback = db; 57 | db = undefined; 58 | } 59 | 60 | client.info("keyspace", function(err, info) { 61 | if (err) return callback(err); 62 | 63 | info.split("\n").forEach(function(line) { 64 | if (line.indexOf("db" + (db || 0)) === 0) { 65 | var info = {}; 66 | line.split(":")[1].split(",").forEach(function(value) { 67 | var keyVal = value.split("="); 68 | info[keyVal[0]] = parseInt(keyVal[1], 10); 69 | }); 70 | return callback(null, info); 71 | } 72 | }); 73 | }); 74 | } 75 | 76 | 77 | /** 78 | * github.com/bucaran/sget 79 | * 80 | * sget. Async / Sync read line for Node. 81 | * 82 | * @copyright (c) 2014 Jorge Bucaran 83 | * @license MIT 84 | */ 85 | /** 86 | * Read a line from stdin sync. If callback is undefined reads it async. 87 | * 88 | * @param {String} message Message to log before reading stdin. 89 | * @param {Function} callback If specified, reads the stdin async. 90 | */ 91 | var sget = function(message, callback) { 92 | win32 = function() { 93 | return ('win32' === process.platform); 94 | }, 95 | readSync = function(buffer) { 96 | var fd = win32() ? process.stdin.fd : fs.openSync('/dev/stdin', 'rs'); 97 | var bytes = fs.readSync(fd, buffer, 0, buffer.length); 98 | if (!win32()) fs.closeSync(fd); 99 | return bytes; 100 | }; 101 | message = message || ''; 102 | if (callback) { 103 | var cli = rl.createInterface(process.stdin, process.stdout); 104 | console.log(message); 105 | cli.prompt(); 106 | cli.on('line', function(data) { 107 | cli.close(); 108 | callback(data); 109 | }); 110 | } else { 111 | return (function(buffer) { 112 | try { 113 | console.log(message); 114 | return buffer.toString(null, 0, readSync(buffer)); 115 | } catch (e) { 116 | throw e; 117 | } 118 | }(new Buffer(sget.bufferSize))); 119 | } 120 | }; 121 | /** 122 | * @type {Number} Size of the buffer to read. 123 | */ 124 | sget.bufferSize = 256; 125 | 126 | 127 | module.exports = { 128 | keysInformation: keysInformation, 129 | dbInformation: dbInformation, 130 | sget: sget 131 | }; 132 | --------------------------------------------------------------------------------