Skip to content

Commit 48cc084

Browse files
authored
Merge pull request #249 from opentok/vonage-wrapper
Vonage wrapper
2 parents 678e6e7 + 8c5a7e5 commit 48cc084

13 files changed

+262
-70
lines changed

.bumpversion.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 3.10.0
2+
current_version = 3.11.0
33
commit = True
44
tag = True
55

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pip-delete-this-directory.txt
3333
.coverage
3434
.cache
3535
coverage.xml
36+
htmlcov/
3637

3738
# Translations
3839
*.mo
@@ -55,3 +56,6 @@ env/
5556

5657
# Pyenv
5758
.python-version
59+
60+
# Credentials
61+
credentials/

CHANGES.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# Release 3.11.0
2+
- OpenTok SDK now accepts Vonage credentials so it's possible to use the existing OpenTok SDK with the Vonage Video API
3+
- Add additional headers to some requests
4+
- Internal changes to the `Opentok.create_session` method
5+
16
# Release 3.10.0
27
- Add new `max_bitrate` option for archives
38
- Change to create JWTs by default in the `Client.generate_token` method. T1 tokens can still be created by setting `use_jwt=False` when generating a token.

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ clean:
44
rm -rf dist build
55

66
coverage:
7-
pytest -v --cov
7+
pytest -v --cov=opentok
88
coverage html
99

1010
test:

opentok/opentok.py

+115-48
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import datetime # generate_token
2+
import re
23
from typing import List, Optional # imports List, Optional type hint
34
import calendar # generate_token
45
import base64 # generate_token
@@ -7,6 +8,7 @@
78
import hmac # _sign_string
89
import hashlib
910
from typing import List
11+
import uuid
1012
import requests # create_session, archiving
1113
import json # archiving
1214
import platform # user-agent
@@ -19,7 +21,7 @@
1921

2022

2123
# compat
22-
from six.moves.urllib.parse import urlencode
24+
from urllib.parse import urlencode
2325
from six import text_type, u, b, PY3
2426
from enum import Enum
2527

@@ -111,6 +113,11 @@ class ArchiveModes(Enum):
111113
class Client(object):
112114
"""Use this SDK to create tokens and interface with the server-side portion
113115
of the Opentok API.
116+
117+
You can also interact with this client object with Vonage credentials. Instead of passing
118+
on OpenTok API key and secret, you can pass in a Vonage application ID and private key,
119+
e.g. api_key=VONAGE_APPLICATION_ID, api_secret=VONAGE_PRIVATE_KEY. You do not need to set the API
120+
URL differently, the SDK will set this for you.
114121
"""
115122

116123
TOKEN_SENTINEL = "T1=="
@@ -124,11 +131,25 @@ def __init__(
124131
timeout=None,
125132
app_version=None,
126133
):
134+
135+
if isinstance(api_secret, (str, bytes)) and re.search(
136+
"[.][a-zA-Z0-9_]+$", api_secret
137+
):
138+
# We have a private key so we assume we are using Vonage credentials
139+
self._using_vonage = True
140+
self._api_url = 'https://video.api.vonage.com'
141+
with open(api_secret, "rb") as key_file:
142+
self.api_secret = key_file.read()
143+
else:
144+
# We are using OpenTok credentials
145+
self._using_vonage = False
146+
self.api_secret = api_secret
147+
self._api_url = api_url
148+
127149
self.api_key = str(api_key)
128-
self.api_secret = api_secret
129150
self.timeout = timeout
130151
self._proxies = None
131-
self.endpoints = Endpoints(api_url, self.api_key)
152+
self.endpoints = Endpoints(self._api_url, self.api_key)
132153
self._app_version = __version__ if app_version == None else app_version
133154
self._user_agent = (
134155
f"OpenTok-Python-SDK/{self.app_version} python/{platform.python_version()}"
@@ -306,24 +327,41 @@ def generate_token(
306327

307328
if use_jwt:
308329
payload = {}
309-
payload['iss'] = self.api_key
310-
payload['ist'] = 'project'
330+
331+
payload['session_id'] = session_id
332+
payload['role'] = role.value
311333
payload['iat'] = now
312334
payload["exp"] = expire_time
313-
payload['nonce'] = random.randint(0, 999999)
314-
payload['role'] = role.value
315335
payload['scope'] = 'session.connect'
316-
payload['session_id'] = session_id
336+
317337
if initial_layout_class_list:
318338
payload['initial_layout_class_list'] = (
319339
initial_layout_class_list_serialized
320340
)
321341
if data:
322342
payload['connection_data'] = data
323343

324-
headers = {'alg': 'HS256', 'typ': 'JWT'}
344+
if not self._using_vonage:
345+
payload['iss'] = self.api_key
346+
payload['ist'] = 'project'
347+
payload['nonce'] = random.randint(0, 999999)
348+
349+
headers = {'alg': 'HS256', 'typ': 'JWT'}
325350

326-
token = encode(payload, self.api_secret, algorithm="HS256", headers=headers)
351+
token = encode(
352+
payload, self.api_secret, algorithm="HS256", headers=headers
353+
)
354+
else:
355+
payload['application_id'] = self.api_key
356+
payload['jti'] = str(uuid.uuid4())
357+
payload['subject'] = 'video'
358+
payload['acl'] = {'paths': {'/session/**': {}}}
359+
360+
headers = {'alg': 'RS256', 'typ': 'JWT'}
361+
362+
token = encode(
363+
payload, self.api_secret, algorithm="RS256", headers=headers
364+
)
327365

328366
return token
329367

@@ -500,39 +538,54 @@ def create_session(
500538
"POST to %r with params %r, headers %r, proxies %r",
501539
self.endpoints.get_session_url(),
502540
options,
503-
self.get_headers(),
541+
self.get_json_headers(),
504542
self.proxies,
505543
)
506-
response = requests.post(
507-
self.endpoints.get_session_url(),
508-
data=options,
509-
headers=self.get_headers(),
510-
proxies=self.proxies,
511-
timeout=self.timeout,
512-
)
544+
if not self._using_vonage:
545+
response = requests.post(
546+
self.endpoints.get_session_url(),
547+
data=options,
548+
headers=self.get_headers(),
549+
proxies=self.proxies,
550+
timeout=self.timeout,
551+
)
552+
else:
553+
headers = self.get_headers()
554+
response = requests.post(
555+
self.endpoints.get_session_url(),
556+
data=options,
557+
headers=headers,
558+
proxies=self.proxies,
559+
timeout=self.timeout,
560+
)
513561
response.encoding = "utf-8"
514-
515562
if response.status_code == 403:
516563
raise AuthError("Failed to create session, invalid credentials")
517564
if not response.content:
518565
raise RequestError()
519-
dom = xmldom.parseString(response.content.decode("utf-8"))
520566
except Exception as e:
521567
raise RequestError("Failed to create session: %s" % str(e))
522568

523569
try:
524-
error = dom.getElementsByTagName("error")
525-
if error:
526-
error = error[0]
527-
raise AuthError(
528-
"Failed to create session (code=%s): %s"
529-
% (
530-
error.attributes["code"].value,
531-
error.firstChild.attributes["message"].value,
570+
content_type = response.headers["Content-Type"]
571+
# Legacy behaviour
572+
if content_type != "application/json":
573+
dom = xmldom.parseString(response.content.decode("utf-8"))
574+
error = dom.getElementsByTagName("error")
575+
if error:
576+
error = error[0]
577+
raise AuthError(
578+
"Failed to create session (code=%s): %s"
579+
% (
580+
error.attributes["code"].value,
581+
error.firstChild.attributes["message"].value,
582+
)
532583
)
584+
session_id = (
585+
dom.getElementsByTagName("session_id")[0].childNodes[0].nodeValue
533586
)
534-
535-
session_id = dom.getElementsByTagName("session_id")[0].childNodes[0].nodeValue
587+
else:
588+
session_id = response.json()[0]["session_id"]
536589
return Session(
537590
self,
538591
session_id,
@@ -546,12 +599,19 @@ def create_session(
546599

547600
def get_headers(self):
548601
"""For internal use."""
602+
if not self._using_vonage:
603+
return {
604+
"User-Agent": "OpenTok-Python-SDK/"
605+
+ self.app_version
606+
+ " python/"
607+
+ platform.python_version(),
608+
"X-OPENTOK-AUTH": self._create_jwt_auth_header(),
609+
"Accept": "application/json",
610+
}
549611
return {
550-
"User-Agent": "OpenTok-Python-SDK/"
551-
+ self.app_version
552-
+ " python/"
553-
+ platform.python_version(),
554-
"X-OPENTOK-AUTH": self._create_jwt_auth_header(),
612+
"User-Agent": self.user_agent + " OpenTok-With-Vonage-API-Backend",
613+
"Authorization": "Bearer " + self._create_jwt_auth_header(),
614+
"Accept": "application/json",
555615
}
556616

557617
def headers(self):
@@ -1859,13 +1919,13 @@ def stop_render(self, render_id):
18591919
logger.debug(
18601920
"DELETE to %r with headers %r, proxies %r",
18611921
self.endpoints.get_render_url(render_id=render_id),
1862-
self.get_headers(),
1922+
self.get_json_headers(),
18631923
self.proxies,
18641924
)
18651925

18661926
response = requests.delete(
18671927
self.endpoints.get_render_url(render_id=render_id),
1868-
headers=self.get_headers(),
1928+
headers=self.get_json_headers(),
18691929
proxies=self.proxies,
18701930
timeout=self.timeout,
18711931
)
@@ -1896,14 +1956,14 @@ def list_renders(self, offset=0, count=50):
18961956
logger.debug(
18971957
"GET to %r with headers %r, params %r, proxies %r",
18981958
self.endpoints.get_render_url(),
1899-
self.get_headers(),
1959+
self.get_json_headers(),
19001960
query_params,
19011961
self.proxies,
19021962
)
19031963

19041964
response = requests.get(
19051965
self.endpoints.get_render_url(),
1906-
headers=self.get_headers(),
1966+
headers=self.get_json_headers(),
19071967
params=query_params,
19081968
proxies=self.proxies,
19091969
timeout=self.timeout,
@@ -2090,14 +2150,21 @@ def _sign_string(self, string, secret):
20902150
def _create_jwt_auth_header(self):
20912151
payload = {
20922152
"ist": "project",
2093-
"iss": self.api_key,
20942153
"iat": int(time.time()), # current time in unix time (seconds)
20952154
"exp": int(time.time())
20962155
+ (60 * self._jwt_livetime), # 3 minutes in the future (seconds)
2097-
"jti": "{0}".format(0, random.random()),
20982156
}
20992157

2100-
return encode(payload, self.api_secret, algorithm="HS256")
2158+
if not self._using_vonage:
2159+
payload["iss"] = self.api_key
2160+
payload["jti"] = str(random.random())
2161+
return encode(payload, self.api_secret, algorithm="HS256")
2162+
2163+
payload["application_id"] = self.api_key
2164+
payload["jti"] = str(uuid.uuid4())
2165+
headers = {"typ": "JWT", "alg": "RS256"}
2166+
2167+
return encode(payload, self.api_secret, algorithm='RS256', headers=headers)
21012168

21022169
def mute_all(
21032170
self, session_id: str, excludedStreamIds: Optional[List[str]]
@@ -2127,7 +2194,7 @@ def mute_all(
21272194
options = {"active": True, "excludedStreams": []}
21282195

21292196
response = requests.post(
2130-
url, headers=self.get_headers(), data=json.dumps(options)
2197+
url, headers=self.get_json_headers(), data=json.dumps(options)
21312198
)
21322199

21332200
if response:
@@ -2164,7 +2231,7 @@ def disable_force_mute(self, session_id: str) -> requests.Response:
21642231
url = self.endpoints.get_mute_all_url(session_id)
21652232

21662233
response = requests.post(
2167-
url, headers=self.get_headers(), data=json.dumps(options)
2234+
url, headers=self.get_json_headers(), data=json.dumps(options)
21682235
)
21692236

21702237
try:
@@ -2198,7 +2265,7 @@ def mute_stream(self, session_id: str, stream_id: str) -> requests.Response:
21982265
if stream_id:
21992266
url = self.endpoints.get_stream_url(session_id, stream_id) + "/mute"
22002267

2201-
response = requests.post(url, headers=self.get_headers())
2268+
response = requests.post(url, headers=self.get_json_headers())
22022269

22032270
if response:
22042271
return response
@@ -2315,7 +2382,7 @@ def mute_all(
23152382
options = {"active": True, "excludedStreams": []}
23162383

23172384
response = requests.post(
2318-
url, headers=self.get_headers(), data=json.dumps(options)
2385+
url, headers=self.get_json_headers(), data=json.dumps(options)
23192386
)
23202387

23212388
if response:
@@ -2350,7 +2417,7 @@ def disable_force_mute(self, session_id: str) -> requests.Response:
23502417
url = self.endpoints.get_mute_all_url(session_id)
23512418

23522419
response = requests.post(
2353-
url, headers=self.get_headers(), data=json.dumps(options)
2420+
url, headers=self.get_json_headers(), data=json.dumps(options)
23542421
)
23552422

23562423
try:
@@ -2382,7 +2449,7 @@ def mute_stream(self, session_id: str, stream_id: str) -> requests.Response:
23822449
if stream_id:
23832450
url = self.endpoints.get_stream_url(session_id, stream_id) + "/mute"
23842451

2385-
response = requests.post(url, headers=self.get_headers())
2452+
response = requests.post(url, headers=self.get_json_headers())
23862453

23872454
if response:
23882455
return response

opentok/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# see: http://legacy.python.org/dev/peps/pep-0440/#public-version-identifiers
2-
__version__ = "3.10.0"
2+
__version__ = "3.11.0"
33

tests/fake_data/dummy_private_key.txt

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQdAHqJHs/a+Ra
3+
2ubvSd1vz/aWlJ9BqnMUtB7guTlyggdENAbleIkzep6mUHepDJdQh8Qv6zS3lpUe
4+
K0UkDfr1/FvsvxurGw/YYPagUEhP/HxMbs2rnQTiAdWOT+Ux9vPABoyNYvZB90xN
5+
IVhBDRWgkz1HPQBRNjFcm3NOol83h5Uwp5YroGTWx+rpmIiRhQj3mv6luk102d95
6+
4ulpPpzcYWKIpJNdclJrEkBZaghDZTOpbv79qd+ds9AVp1j8i9cG/owBJpsJWxfw
7+
StMDpNeEZqopeQWmA121sSEsxpAbKJ5DA7F/lmckx74sulKHX1fDWT76cRhloaEQ
8+
VmETdj0VAgMBAAECggEAZ+SBtchz8vKbsBqtAbM/XcR5Iqi1TR2eWMHDJ/65HpSm
9+
+XuyujjerN0e6EZvtT4Uxmq8QaPJNP0kmhI31hXvsB0UVcUUDa4hshb1pIYO3Gq7
10+
Kr8I29EZB2mhndm9Ii9yYhEBiVA66zrNeR225kkWr97iqjhBibhoVr8Vc6oiqcIP
11+
nFy5zSFtQSkhucaPge6rW00JSOD3wg2GM+rgS6r22t8YmqTzAwvwfil5pQfUngal
12+
oywqLOf6CUYXPBleJc1KgaIIP/cSvqh6b/t25o2VXnI4rpRhtleORvYBbH6K6xLa
13+
OWgg6B58T+0/QEqtZIAn4miYtVCkYLB78Ormc7Q9ewKBgQDuSytuYqxdZh/L/RDU
14+
CErFcNO5I1e9fkLAs5dQEBvvdQC74+oA1MsDEVv0xehFa1JwPKSepmvB2UznZg9L
15+
CtR7QKMDZWvS5xx4j0E/b+PiNQ/tlcFZB2UZ0JwviSxdd7omOTscq9c3RIhFHar1
16+
Y38Fixkfm44Ij/K3JqIi2v2QMwKBgQDf8TYOOmAr9UuipUDxMsRSqTGVIY8B+aEJ
17+
W+2aLrqJVkLGTRfrbjzXWYo3+n7kNJjFgNkltDq6HYtufHMYRs/0PPtNR0w0cDPS
18+
Xr7m2LNHTDcBalC/AS4yKZJLNLm+kXA84vkw4qiTjc0LSFxJkouTQzkea0l8EWHt
19+
zRMv/qYVlwKBgBaJOWRJJK/4lo0+M7c5yYh+sSdTNlsPc9Sxp1/FBj9RO26JkXne
20+
pgx2OdIeXWcjTTqcIZ13c71zhZhkyJF6RroZVNFfaCEcBk9IjQ0o0c504jq/7Pc0
21+
gdU9K2g7etykFBDFXNfLUKFDc/fFZIOskzi8/PVGStp4cqXrm23cdBqNAoGBAKtf
22+
A2bP9ViuVjsZCyGJIAPBxlfBXpa8WSe4WZNrvwPqJx9pT6yyp4yE0OkVoJUyStaZ
23+
S5M24NocUd8zDUC+r9TP9d+leAOI+Z87MgumOUuOX2mN2kzQsnFgrrsulhXnZmSx
24+
rNBkI20HTqobrcP/iSAgiU1l/M4c3zwDe3N3A9HxAoGBAM2hYu0Ij6htSNgo/WWr
25+
IEYYXuwf8hPkiuwzlaiWhD3eocgd4S8SsBu/bTCY19hQ2QbBPaYyFlNem+ynQyXx
26+
IOacrgIHCrYnRCxjPfFF/MxgUHJb8ZoiexprP/FME5p0PoRQIEFYa+jVht3hT5wC
27+
9aedWufq4JJb+akO6MVUjTvs
28+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)