├── Procfile ├── .jshintignore ├── .gitignore ├── Dockerfile ├── config ├── production.json └── development.json ├── values-stage.yaml ├── values-prod.yaml ├── .jshintrc ├── package.json ├── scripts └── generate-ssl-certs.sh ├── test.js ├── LICENSE ├── server.js ├── .drone.yml ├── README.md └── sockets.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.pem 4 | drone-secrets.sh 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:8 2 | 3 | WORKDIR /app 4 | COPY . . 5 | 6 | RUN npm install --production 7 | ENV NODE_ENV production 8 | CMD ["node", "server.js"] 9 | 10 | -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "isDev": true, 3 | "server": { 4 | "port": 8888 5 | }, 6 | "rooms": { 7 | "/* maxClients */": "/* maximum number of clients per room. 0 = no limit */", 8 | "maxClients": 0 9 | }, 10 | "stunservers": [ 11 | { 12 | "urls": "$STUNSERVER_URL" 13 | } 14 | ], 15 | "turnservers": [] 16 | } 17 | -------------------------------------------------------------------------------- /values-stage.yaml: -------------------------------------------------------------------------------- 1 | deployment: 2 | image: 3 | repository: simplewebrtc/signalmaster 4 | imagePullSecrets: talky-registry 5 | service: 6 | internalPort: 8888 7 | internalPortName: websocket 8 | healthcheckPath: /healthcheck 9 | ingress: 10 | domain: sandbox.stagesimplewebrtc.com 11 | tls: 12 | enabled: false 13 | annotations: 14 | kubernetes.io/ingress.class: gce -------------------------------------------------------------------------------- /values-prod.yaml: -------------------------------------------------------------------------------- 1 | deployment: 2 | image: 3 | repository: simplewebrtc/signalmaster 4 | imagePullSecrets: talky-registry 5 | service: 6 | internalPort: 8888 7 | internalPortName: websocket 8 | healthcheckPath: /healthcheck 9 | ingress: 10 | domain: sandbox.simplewebrtc.com 11 | tls: 12 | enabled: true 13 | secret: simplewebrtc-sandbox 14 | annotations: 15 | kubernetes.io/ingress.class: gce 16 | certmanager.k8s.io/acme-http01-edit-in-place: "true" -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": false, 3 | "expr": true, 4 | "loopfunc": true, 5 | "curly": false, 6 | "evil": true, 7 | "white": true, 8 | "undef": true, 9 | "browser": true, 10 | "predef": [ 11 | "app", 12 | "$", 13 | "FormBot", 14 | "socket", 15 | "confirm", 16 | "alert", 17 | "require", 18 | "__dirname", 19 | "process", 20 | "exports", 21 | "console", 22 | "Buffer", 23 | "module" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "signal-master", 3 | "description": "Simple signaling server for SimpleWebRTC", 4 | "version": "1.0.1", 5 | "dependencies": { 6 | "getconfig": "^4.3.0", 7 | "node-uuid": "1.2.0", 8 | "socket.io": "^1.7.4", 9 | "yetify": "0.0.1" 10 | }, 11 | "main": "server.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:andyet/signal-master.git" 15 | }, 16 | "devDependencies": { 17 | "socket.io-client": "1.3.7", 18 | "precommit-hook": "0.3.10", 19 | "tape": "^2.13.1" 20 | }, 21 | "license": "MIT", 22 | "scripts": { 23 | "test": "node test.js" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "isDev": true, 3 | "server": { 4 | "port": 8888, 5 | "/* secure */": "/* whether this connects via https */", 6 | "secure": false, 7 | "key": null, 8 | "cert": null, 9 | "password": null 10 | }, 11 | "rooms": { 12 | "/* maxClients */": "/* maximum number of clients per room. 0 = no limit */", 13 | "maxClients": 0 14 | }, 15 | "stunservers": [ 16 | { 17 | "urls": "stun:stun.l.google.com:19302" 18 | } 19 | ], 20 | "turnservers": [ 21 | { 22 | "urls": ["turn:your.turn.servers.here"], 23 | "secret": "turnserversharedsecret", 24 | "expiry": 86400 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /scripts/generate-ssl-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -e server.js ] 4 | then 5 | echo "Error: could not find main application server.js file" 6 | echo "You should run the generate-ssl-certs.sh script from the main application root directory" 7 | echo "i.e: bash scripts/generate-ssl-certs.sh" 8 | exit -1 9 | fi 10 | 11 | echo "Generating self-signed certificates..." 12 | mkdir -p ./config/sslcerts 13 | openssl genrsa -out ./config/sslcerts/key.pem 1024 14 | openssl req -new -key ./config/sslcerts/key.pem -out ./config/sslcerts/csr.pem 15 | openssl x509 -req -days 9999 -in ./config/sslcerts/csr.pem -signkey ./config/sslcerts/key.pem -out ./config/sslcerts/cert.pem 16 | rm ./config/sslcerts/csr.pem 17 | chmod 600 ./config/sslcerts/key.pem ./config/sslcerts/cert.pem 18 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | var config = require('getconfig'); 3 | var server = require('./server'); 4 | 5 | var test = tape.createHarness(); 6 | 7 | var output = test.createStream(); 8 | output.pipe(process.stdout); 9 | output.on('end', function () { 10 | console.log('Tests complete, killing server.'); 11 | process.exit(0); 12 | }); 13 | 14 | var io = require('socket.io-client'); 15 | 16 | var socketURL; 17 | if (config.server.secure) { 18 | socketURL = "https://localhost:" + config.server.port; 19 | } else { 20 | socketURL = "http://localhost:" + config.server.port; 21 | } 22 | 23 | var socketOptions = { 24 | transports: ['websocket'], 25 | 'force new connection': true, 26 | "secure": config.server.secure 27 | }; 28 | 29 | test('it should not crash when sent an empty message', function (t) { 30 | t.plan(1); 31 | var client = io.connect(socketURL, socketOptions); 32 | 33 | client.on('connect', function () { 34 | client.emit('message'); 35 | t.ok(true); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Written by Henrik Joreteg. 2 | Copyright © 2013 by &yet, LLC. 3 | Released under the terms of the MIT License: 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 18 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 19 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /*global console*/ 2 | var yetify = require('yetify'), 3 | config = require('getconfig'), 4 | fs = require('fs'), 5 | sockets = require('./sockets'), 6 | port = parseInt(process.env.PORT || config.server.port, 10), 7 | server_handler = function (req, res) { 8 | if (req.url === '/healthcheck') { 9 | console.log(Date.now(), 'healthcheck'); 10 | res.writeHead(200); 11 | res.end(); 12 | return; 13 | } 14 | res.writeHead(404); 15 | res.end(); 16 | }, 17 | server = null; 18 | 19 | // Create an http(s) server instance to that socket.io can listen to 20 | if (config.server.secure) { 21 | server = require('https').Server({ 22 | key: fs.readFileSync(config.server.key), 23 | cert: fs.readFileSync(config.server.cert), 24 | passphrase: config.server.password 25 | }, server_handler); 26 | } else { 27 | server = require('http').Server(server_handler); 28 | } 29 | server.listen(port); 30 | 31 | sockets(server, config); 32 | 33 | if (config.uid) process.setuid(config.uid); 34 | 35 | var httpUrl; 36 | if (config.server.secure) { 37 | httpUrl = "https://localhost:" + port; 38 | } else { 39 | httpUrl = "http://localhost:" + port; 40 | } 41 | console.log(yetify.logo() + ' -- signal master is running at: ' + httpUrl); 42 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | pipeline: 2 | build_docker: 3 | image: plugins/docker 4 | repo: simplewebrtc/signalmaster 5 | tags: 6 | - latest 7 | - ${DRONE_COMMIT_SHA:0:8} 8 | secrets: [ docker_username, docker_password ] 9 | when: 10 | event: [ deployment, tag ] 11 | drone_helm_tag: 12 | image: one000mph/drone-helm:stage 13 | skip_tls_verify: false 14 | chart: andyet/signalmaster 15 | prefix: staging 16 | recreate_pods: true 17 | helm_repos: andyet=https://andyet-helm-charts.storage.googleapis.com/ 18 | release: signalmaster 19 | values_files: values-stage.yaml 20 | values: deployment.image.repository=simplewebrtc/signalmaster:${DRONE_COMMIT_SHA:0:8},secrets.data.STUNSERVER_URL=$${STUNSERVER_URL},secrets.data.TURNSERVER_URL=$${TURNSERVER_URL},secrets.data.TURNSERVER_SECRET=$${TURNSERVER_SECRET} 21 | secrets: [ 22 | staging_STUNSERVER_URL, 23 | staging_TURNSERVER_URL, 24 | staging_TURNSERVER_SECRET, 25 | staging_KUBERNETES_CERTIFICATE, 26 | staging_KUBERNETES_TOKEN, 27 | staging_API_SERVER ] 28 | when: 29 | event: tag 30 | drone_helm_deploy: 31 | image: one000mph/drone-helm:stage 32 | skip_tls_verify: false 33 | chart: andyet/signalmaster 34 | prefix: prod 35 | recreate_pods: true 36 | helm_repos: andyet=https://andyet-helm-charts.storage.googleapis.com/ 37 | release: signalmaster 38 | values_files: values-prod.yaml 39 | values: secrets.data.STUNSERVER_URL=$${STUNSERVER_URL},secrets.data.TURNSERVER_URL=$${TURNSERVER_URL},secrets.data.TURNSERVER_SECRET=$${TURNSERVER_SECRET} 40 | secrets: [ 41 | prod_STUNSERVER_URL, 42 | prod_TURNSERVER_URL, 43 | prod_TURNSERVER_SECRET, 44 | prod_KUBERNETES_CERTIFICATE, 45 | prod_KUBERNETES_TOKEN, 46 | prod_API_SERVER ] 47 | when: 48 | event: deployment 49 | slack: 50 | image: andyet/drone-slack:stable 51 | pull: true 52 | username: drone 53 | channel: io-alerts 54 | secrets: [ slack_webhook, github_access_token, github_slack_lookup ] 55 | when: 56 | status: [ success, failure ] 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | The open-source version of SimpleWebRTC has been deprecated. This repository will remain as-is but is no longer actively maintained. 4 | Read more about the "new" SimpleWebRTC (which is an entirely different thing) on https://simplewebrtc.com 5 | # signalmaster 6 | 7 | A simple signaling server for clients to connect and do signaling for WebRTC. 8 | 9 | Specifically created as a default connection point for [SimpleWebRTC.js](https://github.com/HenrikJoreteg/SimpleWebRTC) 10 | 11 | It also supports vending STUN/TURN servers with the shared secret mechanism as described in [this draft](http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00). This mechanism is implemented e.g. by [rfc-5766-turn-server](https://code.google.com/p/rfc5766-turn-server/) or by a [patched version](https://github.com/otalk/restund) of [restund](http://creytiv.com/restund.html). 12 | 13 | Read more: 14 | - [Introducing SimpleWebRTC and conversat.io](http://blog.andyet.com/2013/02/22/introducing-simplewebrtcjs-and-conversatio/) 15 | - [SimpleWebRTC.com](http://simplewebrtc.com) 16 | - [talky.io](https://talky.io) 17 | 18 | ## Running 19 | 20 | Running the server requires a valid installation of node.js which can be installed from the nodejs.org website. After installing the package you will need to install the node dependencies. 21 | 22 | 1) npm install 23 | 24 | 2) run the server using "node server.js" 25 | 26 | 3) In the console you will see a message which tells you where the server is running: 27 | 28 | "signal master is running at: http://localhost:8888" 29 | 30 | 4) Open a web browser to the specified URL and port to ensure that the server is running properly. You should see the message when you go to the /socket.io/ subfolder (e.g. http://localhost:8888/socket.io/), you should see a message like this: 31 | 32 | {"code":0,"message":"Transport unknown"} 33 | 34 | ### Production Environment 35 | * generate your ssl certs 36 | 37 | ```shell 38 | $ ./scripts/generate-ssl-certs.sh 39 | ``` 40 | * run in Production mode 41 | 42 | ```shell 43 | $ NODE_ENV=production node server.js 44 | ``` 45 | 46 | ## Use with Express 47 | var express = require('express') 48 | var sockets = require('signalmaster/sockets') 49 | 50 | var app = express() 51 | var server = app.listen(port) 52 | sockets(server, config) // config is the same that server.js uses 53 | 54 | ## Docker 55 | 56 | You can build this image by calling: 57 | 58 | docker build -t signalmaster https://github.com/andyet/signalmaster.git 59 | 60 | To run the image execute this: 61 | 62 | docker run --name signalmaster -d -p 8888:8888 signalmaster 63 | 64 | This will start a signal master server on port 8888 exposed on port 8888. 65 | -------------------------------------------------------------------------------- /sockets.js: -------------------------------------------------------------------------------- 1 | var socketIO = require('socket.io'), 2 | uuid = require('node-uuid'), 3 | crypto = require('crypto'); 4 | 5 | module.exports = function (server, config) { 6 | var io = socketIO.listen(server); 7 | 8 | io.sockets.on('connection', function (client) { 9 | client.resources = { 10 | screen: false, 11 | video: true, 12 | audio: false 13 | }; 14 | 15 | // pass a message to another id 16 | client.on('message', function (details) { 17 | if (!details) return; 18 | 19 | var otherClient = io.to(details.to); 20 | if (!otherClient) return; 21 | 22 | details.from = client.id; 23 | otherClient.emit('message', details); 24 | }); 25 | 26 | client.on('shareScreen', function () { 27 | client.resources.screen = true; 28 | }); 29 | 30 | client.on('unshareScreen', function (type) { 31 | client.resources.screen = false; 32 | removeFeed('screen'); 33 | }); 34 | 35 | client.on('join', join); 36 | 37 | function removeFeed(type) { 38 | if (client.room) { 39 | io.sockets.in(client.room).emit('remove', { 40 | id: client.id, 41 | type: type 42 | }); 43 | if (!type) { 44 | client.leave(client.room); 45 | client.room = undefined; 46 | } 47 | } 48 | } 49 | 50 | function join(name, cb) { 51 | // sanity check 52 | if (typeof name !== 'string') return; 53 | // check if maximum number of clients reached 54 | if (config.rooms && config.rooms.maxClients > 0 && 55 | clientsInRoom(name) >= config.rooms.maxClients) { 56 | safeCb(cb)('full'); 57 | return; 58 | } 59 | // leave any existing rooms 60 | removeFeed(); 61 | safeCb(cb)(null, describeRoom(name)); 62 | client.join(name); 63 | client.room = name; 64 | } 65 | 66 | // we don't want to pass "leave" directly because the 67 | // event type string of "socket end" gets passed too. 68 | client.on('disconnect', function () { 69 | removeFeed(); 70 | }); 71 | client.on('leave', function () { 72 | removeFeed(); 73 | }); 74 | 75 | client.on('create', function (name, cb) { 76 | if (arguments.length == 2) { 77 | cb = (typeof cb == 'function') ? cb : function () {}; 78 | name = name || uuid(); 79 | } else { 80 | cb = name; 81 | name = uuid(); 82 | } 83 | // check if exists 84 | var room = io.nsps['/'].adapter.rooms[name]; 85 | if (room && room.length) { 86 | safeCb(cb)('taken'); 87 | } else { 88 | join(name); 89 | safeCb(cb)(null, name); 90 | } 91 | }); 92 | 93 | // support for logging full webrtc traces to stdout 94 | // useful for large-scale error monitoring 95 | client.on('trace', function (data) { 96 | console.log('trace', JSON.stringify( 97 | [data.type, data.session, data.prefix, data.peer, data.time, data.value] 98 | )); 99 | }); 100 | 101 | 102 | // tell client about stun and turn servers and generate nonces 103 | client.emit('stunservers', config.stunservers || []); 104 | 105 | // create shared secret nonces for TURN authentication 106 | // the process is described in draft-uberti-behave-turn-rest 107 | var credentials = []; 108 | // allow selectively vending turn credentials based on origin. 109 | var origin = client.handshake.headers.origin; 110 | if (!config.turnorigins || config.turnorigins.indexOf(origin) !== -1) { 111 | config.turnservers.forEach(function (server) { 112 | var hmac = crypto.createHmac('sha1', server.secret); 113 | // default to 86400 seconds timeout unless specified 114 | var username = Math.floor(new Date().getTime() / 1000) + (parseInt(server.expiry || 86400, 10)) + ""; 115 | hmac.update(username); 116 | credentials.push({ 117 | username: username, 118 | credential: hmac.digest('base64'), 119 | urls: server.urls || server.url 120 | }); 121 | }); 122 | } 123 | client.emit('turnservers', credentials); 124 | }); 125 | 126 | 127 | function describeRoom(name) { 128 | var adapter = io.nsps['/'].adapter; 129 | var clients = adapter.rooms[name] ? adapter.rooms[name].sockets : {}; 130 | var result = { 131 | clients: {} 132 | }; 133 | Object.keys(clients).forEach(function (id) { 134 | result.clients[id] = adapter.nsp.connected[id].resources; 135 | }); 136 | return result; 137 | } 138 | 139 | function clientsInRoom(name) { 140 | return io.sockets.clients(name).length; 141 | } 142 | 143 | }; 144 | 145 | function safeCb(cb) { 146 | if (typeof cb === 'function') { 147 | return cb; 148 | } else { 149 | return function () {}; 150 | } 151 | } 152 | --------------------------------------------------------------------------------