diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index 4b810a2302..5a11fd6c76 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -21,11 +21,12 @@ from .ratelimiting import RatelimitConfig from .repository import ContentRepositoryConfig from .captcha import CaptchaConfig from .email import EmailConfig +from .voip import VoipConfig class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig, RatelimitConfig, ContentRepositoryConfig, CaptchaConfig, - EmailConfig): + EmailConfig, VoipConfig): pass diff --git a/synapse/config/voip.py b/synapse/config/voip.py new file mode 100644 index 0000000000..c5131d9bcd --- /dev/null +++ b/synapse/config/voip.py @@ -0,0 +1,41 @@ +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ._base import Config + + +class VoipConfig(Config): + + def __init__(self, args): + super(VoipConfig, self).__init__(args) + self.turn_uris = args.turn_uris.split(",") if args.turn_uris else None + self.turn_shared_secret = args.turn_shared_secret + self.turn_user_lifetime = args.turn_user_lifetime + + @classmethod + def add_arguments(cls, parser): + super(VoipConfig, cls).add_arguments(parser) + group = parser.add_argument_group("voip") + group.add_argument( + "--turn-uris", type=str, default=None, + help="The public URIs of the TURN server to give to clients" + ) + group.add_argument( + "--turn-shared-secret", type=str, default=None, + help="The shared secret used to compute passwords for the TURN server" + ) + group.add_argument( + "--turn-user-lifetime", type=int, default=(1000 * 60 * 60), + help="How long generated TURN credentials last, in ms" + ) diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index ed785cfbd5..3b9aa59733 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -15,7 +15,7 @@ from . import ( - room, events, register, login, profile, presence, initial_sync, directory + room, events, register, login, profile, presence, initial_sync, directory, voip ) @@ -42,3 +42,4 @@ class RestServletFactory(object): presence.register_servlets(hs, client_resource) initial_sync.register_servlets(hs, client_resource) directory.register_servlets(hs, client_resource) + voip.register_servlets(hs, client_resource) diff --git a/synapse/rest/voip.py b/synapse/rest/voip.py new file mode 100644 index 0000000000..2e4627606f --- /dev/null +++ b/synapse/rest/voip.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + +from base import RestServlet, client_path_pattern + + +import hmac +import hashlib +import base64 + + +class VoipRestServlet(RestServlet): + PATTERN = client_path_pattern("/voip/turnServer$") + + @defer.inlineCallbacks + def on_GET(self, request): + auth_user = yield self.auth.get_user_by_req(request) + + turnUris = self.hs.config.turn_uris + turnSecret = self.hs.config.turn_shared_secret + userLifetime = self.hs.config.turn_user_lifetime + if not turnUris or not turnSecret or not userLifetime: + defer.returnValue( (200, {}) ) + + expiry = self.hs.get_clock().time_msec() + userLifetime + username = "%d:%s" % (expiry, auth_user.to_string()) + + mac = hmac.new(turnSecret, msg=username, digestmod=hashlib.sha1) + # We need to use standard base64 encoding here, *not* syutil's encode_base64 + # because we need to add the standard padding to get the same result as the + # TURN server. + password = base64.b64encode(mac.digest()) + + defer.returnValue( (200, { + 'username': username, + 'password': password, + 'ttl': userLifetime / 1000, + 'uris': turnUris, + }) ) + + def on_OPTIONS(self, request): + return (200, {}) + + +def register_servlets(hs, http_server): + VoipRestServlet(hs).register(http_server) diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js index 7b5d9cffef..3e8811e5fc 100644 --- a/webclient/components/matrix/matrix-call.js +++ b/webclient/components/matrix/matrix-call.js @@ -66,15 +66,67 @@ angular.module('MatrixCall', []) } + MatrixCall.getTurnServer = function() { + matrixService.getTurnServer().then(function(response) { + if (response.data.uris) { + console.log("Got TURN URIs: "+response.data.uris); + MatrixCall.turnServer = response.data; + $rootScope.haveTurn = true; + // re-fetch when we're about to reach the TTL + $timeout(MatrixCall.getTurnServer, MatrixCall.turnServer.ttl * 1000 * 0.9); + } else { + console.log("Got no TURN URIs from HS"); + $rootScope.haveTurn = false; + } + }, function(error) { + console.log("Failed to get TURN URIs"); + MatrixCall.turnServer = {}; + $timeout(MatrixCall.getTurnServer, 60000); + }); + } + + // FIXME: we should prevent any class from being placed or accepted before this has finished + MatrixCall.getTurnServer(); + MatrixCall.CALL_TIMEOUT = 60000; + MatrixCall.FALLBACK_STUN_SERVER = 'stun:stun.l.google.com:19302'; MatrixCall.prototype.createPeerConnection = function() { - var stunServer = 'stun:stun.l.google.com:19302'; var pc; if (window.mozRTCPeerConnection) { - pc = new window.mozRTCPeerConnection({'url': stunServer}); + var iceServers = []; + if (MatrixCall.turnServer) { + if (MatrixCall.turnServer.uris) { + for (var i = 0; i < MatrixCall.turnServer.uris.length; i++) { + iceServers.push({ + 'url': MatrixCall.turnServer.uris[i], + 'username': MatrixCall.turnServer.username, + 'credential': MatrixCall.turnServer.password, + }); + } + } else { + console.log("No TURN server: using fallback STUN server"); + iceServers.push({ 'url' : MatrixCall.FALLBACK_STUN_SERVER }); + } + } + + pc = new window.mozRTCPeerConnection({"iceServers":iceServers}); } else { - pc = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]}); + var iceServers = []; + if (MatrixCall.turnServer) { + if (MatrixCall.turnServer.uris) { + iceServers.push({ + 'urls': MatrixCall.turnServer.uris, + 'username': MatrixCall.turnServer.username, + 'credential': MatrixCall.turnServer.password, + }); + } else { + console.log("No TURN server: using fallback STUN server"); + iceServers.push({ 'urls' : MatrixCall.FALLBACK_STUN_SERVER }); + } + } + + pc = new window.RTCPeerConnection({"iceServers":iceServers}); } var self = this; pc.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); }; diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index fba3923c78..a4f0568bce 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -767,6 +767,10 @@ angular.module('matrixService', []) var deferred = $q.defer(); deferred.reject({data:{error: "Invalid room: " + room_id}}); return deferred.promise; + }, + + getTurnServer: function() { + return doRequest("GET", "/voip/turnServer"); } }; diff --git a/webclient/index.html b/webclient/index.html index 411c2762d3..f233919e3d 100644 --- a/webclient/index.html +++ b/webclient/index.html @@ -69,7 +69,7 @@ Incoming Voice Call Call Connecting... Call Connected - Media Connection Failed + Media Connection Failed{{ haveTurn ? "" : " (VoIP relaying unsupported by Home Server)" }} Call Rejected Call Canceled User Not Responding