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 @@
<span ng-show="currentCall.state == 'ringing' && currentCall && currentCall.type == 'voice'">Incoming Voice Call</span>
<span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
<span ng-show="currentCall.state == 'connected'">Call Connected</span>
- <span ng-show="currentCall.state == 'ended' && currentCall.hangupReason == 'ice_failed'">Media Connection Failed</span>
+ <span ng-show="currentCall.state == 'ended' && currentCall.hangupReason == 'ice_failed'">Media Connection Failed{{ haveTurn ? "" : " (VoIP relaying unsupported by Home Server)" }}</span>
<span ng-show="currentCall.state == 'ended' && !currentCall.hangupReason && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'remote'">Call Rejected</span>
<span ng-show="currentCall.state == 'ended' && !currentCall.hangupReason && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local'">Call Canceled</span>
<span ng-show="currentCall.state == 'ended' && currentCall.hangupReason == 'invite_timeout' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local'">User Not Responding</span>
|