Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vonage wrapper #249

Merged
merged 6 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 3.10.0
current_version = 3.11.0
commit = True
tag = True

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pip-delete-this-directory.txt
.coverage
.cache
coverage.xml
htmlcov/

# Translations
*.mo
Expand All @@ -55,3 +56,6 @@ env/

# Pyenv
.python-version

# Credentials
credentials/
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Release 3.11.0
- OpenTok SDK now accepts Vonage credentials so it's possible to use the existing OpenTok SDK with the Vonage Video API
- Add additional headers to some requests
- Internal changes to the `Opentok.create_session` method

# Release 3.10.0
- Add new `max_bitrate` option for archives
- 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.
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ clean:
rm -rf dist build

coverage:
pytest -v --cov
pytest -v --cov=opentok
coverage html

test:
Expand Down
163 changes: 115 additions & 48 deletions opentok/opentok.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime # generate_token
import re
from typing import List, Optional # imports List, Optional type hint
import calendar # generate_token
import base64 # generate_token
Expand All @@ -7,6 +8,7 @@
import hmac # _sign_string
import hashlib
from typing import List
import uuid
import requests # create_session, archiving
import json # archiving
import platform # user-agent
Expand All @@ -19,7 +21,7 @@


# compat
from six.moves.urllib.parse import urlencode
from urllib.parse import urlencode
from six import text_type, u, b, PY3
from enum import Enum

Expand Down Expand Up @@ -111,6 +113,11 @@ class ArchiveModes(Enum):
class Client(object):
"""Use this SDK to create tokens and interface with the server-side portion
of the Opentok API.

You can also interact with this client object with Vonage credentials. Instead of passing
on OpenTok API key and secret, you can pass in a Vonage application ID and private key,
e.g. api_key=VONAGE_APPLICATION_ID, api_secret=VONAGE_PRIVATE_KEY. You do not need to set the API
URL differently, the SDK will set this for you.
"""

TOKEN_SENTINEL = "T1=="
Expand All @@ -124,11 +131,25 @@ def __init__(
timeout=None,
app_version=None,
):

if isinstance(api_secret, (str, bytes)) and re.search(
"[.][a-zA-Z0-9_]+$", api_secret
):
# We have a private key so we assume we are using Vonage credentials
self._using_vonage = True
self._api_url = 'https://video.api.vonage.com'
with open(api_secret, "rb") as key_file:
self.api_secret = key_file.read()
else:
# We are using OpenTok credentials
self._using_vonage = False
self.api_secret = api_secret
self._api_url = api_url

self.api_key = str(api_key)
self.api_secret = api_secret
self.timeout = timeout
self._proxies = None
self.endpoints = Endpoints(api_url, self.api_key)
self.endpoints = Endpoints(self._api_url, self.api_key)
self._app_version = __version__ if app_version == None else app_version
self._user_agent = (
f"OpenTok-Python-SDK/{self.app_version} python/{platform.python_version()}"
Expand Down Expand Up @@ -306,24 +327,41 @@ def generate_token(

if use_jwt:
payload = {}
payload['iss'] = self.api_key
payload['ist'] = 'project'

payload['session_id'] = session_id
payload['role'] = role.value
payload['iat'] = now
payload["exp"] = expire_time
payload['nonce'] = random.randint(0, 999999)
payload['role'] = role.value
payload['scope'] = 'session.connect'
payload['session_id'] = session_id

if initial_layout_class_list:
payload['initial_layout_class_list'] = (
initial_layout_class_list_serialized
)
if data:
payload['connection_data'] = data

headers = {'alg': 'HS256', 'typ': 'JWT'}
if not self._using_vonage:
payload['iss'] = self.api_key
payload['ist'] = 'project'
payload['nonce'] = random.randint(0, 999999)

headers = {'alg': 'HS256', 'typ': 'JWT'}

token = encode(payload, self.api_secret, algorithm="HS256", headers=headers)
token = encode(
payload, self.api_secret, algorithm="HS256", headers=headers
)
else:
payload['application_id'] = self.api_key
payload['jti'] = str(uuid.uuid4())
payload['subject'] = 'video'
payload['acl'] = {'paths': {'/session/**': {}}}

headers = {'alg': 'RS256', 'typ': 'JWT'}

token = encode(
payload, self.api_secret, algorithm="RS256", headers=headers
)

return token

Expand Down Expand Up @@ -500,39 +538,54 @@ def create_session(
"POST to %r with params %r, headers %r, proxies %r",
self.endpoints.get_session_url(),
options,
self.get_headers(),
self.get_json_headers(),
self.proxies,
)
response = requests.post(
self.endpoints.get_session_url(),
data=options,
headers=self.get_headers(),
proxies=self.proxies,
timeout=self.timeout,
)
if not self._using_vonage:
response = requests.post(
self.endpoints.get_session_url(),
data=options,
headers=self.get_headers(),
proxies=self.proxies,
timeout=self.timeout,
)
else:
headers = self.get_headers()
response = requests.post(
self.endpoints.get_session_url(),
data=options,
headers=headers,
proxies=self.proxies,
timeout=self.timeout,
)
response.encoding = "utf-8"

if response.status_code == 403:
raise AuthError("Failed to create session, invalid credentials")
if not response.content:
raise RequestError()
dom = xmldom.parseString(response.content.decode("utf-8"))
except Exception as e:
raise RequestError("Failed to create session: %s" % str(e))

try:
error = dom.getElementsByTagName("error")
if error:
error = error[0]
raise AuthError(
"Failed to create session (code=%s): %s"
% (
error.attributes["code"].value,
error.firstChild.attributes["message"].value,
content_type = response.headers["Content-Type"]
# Legacy behaviour
if content_type != "application/json":
dom = xmldom.parseString(response.content.decode("utf-8"))
error = dom.getElementsByTagName("error")
if error:
error = error[0]
raise AuthError(
"Failed to create session (code=%s): %s"
% (
error.attributes["code"].value,
error.firstChild.attributes["message"].value,
)
)
session_id = (
dom.getElementsByTagName("session_id")[0].childNodes[0].nodeValue
)

session_id = dom.getElementsByTagName("session_id")[0].childNodes[0].nodeValue
else:
session_id = response.json()[0]["session_id"]
return Session(
self,
session_id,
Expand All @@ -546,12 +599,19 @@ def create_session(

def get_headers(self):
"""For internal use."""
if not self._using_vonage:
return {
"User-Agent": "OpenTok-Python-SDK/"
+ self.app_version
+ " python/"
+ platform.python_version(),
"X-OPENTOK-AUTH": self._create_jwt_auth_header(),
"Accept": "application/json",
}
return {
"User-Agent": "OpenTok-Python-SDK/"
+ self.app_version
+ " python/"
+ platform.python_version(),
"X-OPENTOK-AUTH": self._create_jwt_auth_header(),
"User-Agent": self.user_agent + " OpenTok-With-Vonage-API-Backend",
"Authorization": "Bearer " + self._create_jwt_auth_header(),
"Accept": "application/json",
}

def headers(self):
Expand Down Expand Up @@ -1859,13 +1919,13 @@ def stop_render(self, render_id):
logger.debug(
"DELETE to %r with headers %r, proxies %r",
self.endpoints.get_render_url(render_id=render_id),
self.get_headers(),
self.get_json_headers(),
self.proxies,
)

response = requests.delete(
self.endpoints.get_render_url(render_id=render_id),
headers=self.get_headers(),
headers=self.get_json_headers(),
proxies=self.proxies,
timeout=self.timeout,
)
Expand Down Expand Up @@ -1896,14 +1956,14 @@ def list_renders(self, offset=0, count=50):
logger.debug(
"GET to %r with headers %r, params %r, proxies %r",
self.endpoints.get_render_url(),
self.get_headers(),
self.get_json_headers(),
query_params,
self.proxies,
)

response = requests.get(
self.endpoints.get_render_url(),
headers=self.get_headers(),
headers=self.get_json_headers(),
params=query_params,
proxies=self.proxies,
timeout=self.timeout,
Expand Down Expand Up @@ -2090,14 +2150,21 @@ def _sign_string(self, string, secret):
def _create_jwt_auth_header(self):
payload = {
"ist": "project",
"iss": self.api_key,
"iat": int(time.time()), # current time in unix time (seconds)
"exp": int(time.time())
+ (60 * self._jwt_livetime), # 3 minutes in the future (seconds)
"jti": "{0}".format(0, random.random()),
}

return encode(payload, self.api_secret, algorithm="HS256")
if not self._using_vonage:
payload["iss"] = self.api_key
payload["jti"] = str(random.random())
return encode(payload, self.api_secret, algorithm="HS256")

payload["application_id"] = self.api_key
payload["jti"] = str(uuid.uuid4())
headers = {"typ": "JWT", "alg": "RS256"}

return encode(payload, self.api_secret, algorithm='RS256', headers=headers)

def mute_all(
self, session_id: str, excludedStreamIds: Optional[List[str]]
Expand Down Expand Up @@ -2127,7 +2194,7 @@ def mute_all(
options = {"active": True, "excludedStreams": []}

response = requests.post(
url, headers=self.get_headers(), data=json.dumps(options)
url, headers=self.get_json_headers(), data=json.dumps(options)
)

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

response = requests.post(
url, headers=self.get_headers(), data=json.dumps(options)
url, headers=self.get_json_headers(), data=json.dumps(options)
)

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

response = requests.post(url, headers=self.get_headers())
response = requests.post(url, headers=self.get_json_headers())

if response:
return response
Expand Down Expand Up @@ -2315,7 +2382,7 @@ def mute_all(
options = {"active": True, "excludedStreams": []}

response = requests.post(
url, headers=self.get_headers(), data=json.dumps(options)
url, headers=self.get_json_headers(), data=json.dumps(options)
)

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

response = requests.post(
url, headers=self.get_headers(), data=json.dumps(options)
url, headers=self.get_json_headers(), data=json.dumps(options)
)

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

response = requests.post(url, headers=self.get_headers())
response = requests.post(url, headers=self.get_json_headers())

if response:
return response
Expand Down
2 changes: 1 addition & 1 deletion opentok/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# see: http://legacy.python.org/dev/peps/pep-0440/#public-version-identifiers
__version__ = "3.10.0"
__version__ = "3.11.0"

28 changes: 28 additions & 0 deletions tests/fake_data/dummy_private_key.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQdAHqJHs/a+Ra
2ubvSd1vz/aWlJ9BqnMUtB7guTlyggdENAbleIkzep6mUHepDJdQh8Qv6zS3lpUe
K0UkDfr1/FvsvxurGw/YYPagUEhP/HxMbs2rnQTiAdWOT+Ux9vPABoyNYvZB90xN
IVhBDRWgkz1HPQBRNjFcm3NOol83h5Uwp5YroGTWx+rpmIiRhQj3mv6luk102d95
4ulpPpzcYWKIpJNdclJrEkBZaghDZTOpbv79qd+ds9AVp1j8i9cG/owBJpsJWxfw
StMDpNeEZqopeQWmA121sSEsxpAbKJ5DA7F/lmckx74sulKHX1fDWT76cRhloaEQ
VmETdj0VAgMBAAECggEAZ+SBtchz8vKbsBqtAbM/XcR5Iqi1TR2eWMHDJ/65HpSm
+XuyujjerN0e6EZvtT4Uxmq8QaPJNP0kmhI31hXvsB0UVcUUDa4hshb1pIYO3Gq7
Kr8I29EZB2mhndm9Ii9yYhEBiVA66zrNeR225kkWr97iqjhBibhoVr8Vc6oiqcIP
nFy5zSFtQSkhucaPge6rW00JSOD3wg2GM+rgS6r22t8YmqTzAwvwfil5pQfUngal
oywqLOf6CUYXPBleJc1KgaIIP/cSvqh6b/t25o2VXnI4rpRhtleORvYBbH6K6xLa
OWgg6B58T+0/QEqtZIAn4miYtVCkYLB78Ormc7Q9ewKBgQDuSytuYqxdZh/L/RDU
CErFcNO5I1e9fkLAs5dQEBvvdQC74+oA1MsDEVv0xehFa1JwPKSepmvB2UznZg9L
CtR7QKMDZWvS5xx4j0E/b+PiNQ/tlcFZB2UZ0JwviSxdd7omOTscq9c3RIhFHar1
Y38Fixkfm44Ij/K3JqIi2v2QMwKBgQDf8TYOOmAr9UuipUDxMsRSqTGVIY8B+aEJ
W+2aLrqJVkLGTRfrbjzXWYo3+n7kNJjFgNkltDq6HYtufHMYRs/0PPtNR0w0cDPS
Xr7m2LNHTDcBalC/AS4yKZJLNLm+kXA84vkw4qiTjc0LSFxJkouTQzkea0l8EWHt
zRMv/qYVlwKBgBaJOWRJJK/4lo0+M7c5yYh+sSdTNlsPc9Sxp1/FBj9RO26JkXne
pgx2OdIeXWcjTTqcIZ13c71zhZhkyJF6RroZVNFfaCEcBk9IjQ0o0c504jq/7Pc0
gdU9K2g7etykFBDFXNfLUKFDc/fFZIOskzi8/PVGStp4cqXrm23cdBqNAoGBAKtf
A2bP9ViuVjsZCyGJIAPBxlfBXpa8WSe4WZNrvwPqJx9pT6yyp4yE0OkVoJUyStaZ
S5M24NocUd8zDUC+r9TP9d+leAOI+Z87MgumOUuOX2mN2kzQsnFgrrsulhXnZmSx
rNBkI20HTqobrcP/iSAgiU1l/M4c3zwDe3N3A9HxAoGBAM2hYu0Ij6htSNgo/WWr
IEYYXuwf8hPkiuwzlaiWhD3eocgd4S8SsBu/bTCY19hQ2QbBPaYyFlNem+ynQyXx
IOacrgIHCrYnRCxjPfFF/MxgUHJb8ZoiexprP/FME5p0PoRQIEFYa+jVht3hT5wC
9aedWufq4JJb+akO6MVUjTvs
-----END PRIVATE KEY-----
Loading