8000 Merge branch 'release-3.1.0' · alexcchan/twilio-python@c15c2bc · GitHub
[go: up one dir, main page]

Skip to content

Commit c15c2bc

Browse files
committed
Merge branch 'release-3.1.0'
2 parents 094f7f6 + fe8b805 commit c15c2bc

File tree

11 files changed

+335
-16
lines changed

11 files changed

+335
-16
lines changed

docs/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ A complete API reference to the :data:`twilio` module.
1010
api/rest/index
1111
api/rest/resources
1212
api/twiml
13+
api/util
1314

docs/api/util.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
====================
2+
:mod:`twilio.util`
3+
====================
4+
5+
.. automodule:: twilio.util
6+
7+
.. autoclass:: twilio.util.RequestValidator
8+
:members:
9+
10+
.. autoclass:: twilio.util.TwilioCapability
11+
:members: allow_client_incoming, allow_client_outgoing, generate
12+
13+

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@
5454
# built documents.
5555
#
5656
# The short X.Y version.
57-
version = '3.0'
57+
version = '3.1'
5858
# The full version, including alpha/beta/rc tags.
59-
release = '3.0.2'
59+
release = '3.1.0'
6060

6161
# The language for content autogenerated by Sphinx. Refer to documentation
6262
# for a list of supported languages.

docs/index.rst

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Small functions useful for validating requests are coming from Twilio
6969
:maxdepth: 1
7070

7171
usage/validation
72+
usage/token-generation
7273

7374
Upgrade Plan
7475
==================
@@ -97,12 +98,3 @@ All development occurs over on `Github <https://github.com/twilio/twilio-python>
9798
Report bugs using the Github `issue tracker <https://github.com/twilio/twilio-python/issues>`_.
9899

99100
If you’ve got questions that aren’t answered by this documentation, ask the `#twilio IRC channel <irc://irc.freenode.net/#twilio>`_
100-
101-
Changelog
102-
=================
103-
Current stable version is 3.0.0.
104-
105-
.. toctree::
106-
:maxdepth: 1
107-
108-
changelog

docs/usage/token-generation.rst

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
.. module:: twilio.util
2+
3+
===========================
4+
Generate Capability Tokens
5+
===========================
6+
7+
`Twilio Client <http://www.twilio.com/api/client>`_ allows you to make and recieve connections in the browser. You can place a call to a phone on the PSTN network, all without leaving your browser. See the `Twilio Client Quickstart <http:/www.twilio.com/docs/quickstart/client>`_ to get up and running with Twilio Client.
8+
9+
Capability tokens are used by `Twilio Client <http://www.twilio.com/api/client>`_ to provide connection security and authorization. The `Capability Token documentation <http://www.twilio.con/docs/tokens>`_ explains indepth the purpose and features of these tokens.
10+
11+
:class:`TwilioCapability` is responsible for the creation of these capability tokens. You'll need your Twilio AccountSid and AuthToken.
12+
13+
.. code-block:: python
14+
15+
from twilio.util import TwilioCapability
16+
17+
account_sid = "AC123123"
18+
auth_token = "secret"
19+
20+
capability = TwilioCability(account_sid, auth_token)
21+
22+
23+
Allow Incoming Connections
24+
==============================
25+
26+
Before a device running `Twilio Client <http://www.twilio.com/api/client>`_ can recieve incoming connections, the instance must first register a name (such as "Alice" or "Bob"). The :meth:`allow_client_incoming` method adds the client name to the capability token.
27+
28+
.. code-block:: python
29+
30+
capability.allow_client_incoming("Alice")
31+
32+
33+
Allow Outgoing Connections
34+
==============================
35+
36+
To make an outgoing connection from a `Twilio Client <http://www.twilio.com/api/client>`_ device, you'll need to choose a `Twilio Application <http://www.twilio.com/docs/api/rest/applications>`_ to handle TwiML URLs. A Twilio Application is a collection of URLs responsible for outputing valid TwiML to control phone calls and SMS.
37+
38+
.. code-block:: python
39+
40+
application_sid = "AP123123" # Twilio Application Sid
41+
capability.allow_client_outgoing(application_sid)
42+
43+
Generate a Token
44+
==================
45+
46+
.. code-block:: python
47+
48+
token = capability.generate()
49+
50+
By default, this token will expire in one hour. If you'd like to change the token expiration, :meth:`generate` takes an optional :attr:`expires` argument.
51+
52+
.. code-block:: python
53+
54+
token = capability.generate(expires=600)
55+
56+
This token will now expire in 10 minutes. If you haven't guessed already, :attr:`expires` is expressed in seconds.

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ sphinx
22
mock
33
httplib2
44
nose
5+
pyjwt

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from setuptools import setup, find_packages
22
setup(
33
name = "twilio",
4-
version = "3.0.2",
4+
version = "3.1.0",
55
description = "Twilio API client and TwiML generator",
66
author = "Twilio",
77
author_email = "help@twilio.com",
88
url = "http://github.com/twilio/twilio-python/",
99
keywords = ["twilio","twiml"],
10-
install_requires = ["httplib2"],
10+
install_requires = ["httplib2", "pyjwt"],
1111
packages = find_packages(),
1212
classifiers = [
1313
"Programming Language :: Python",

tests/test_jwt.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import jwt
2+
import unittest
3+
import urllib
4+
from twilio.util import TwilioCapability
5+
6+
class TokenTest(unittest.TestCase):
7+
8+
def assertIn(self, foo, bar, msg=None):
9+
"""backport for 2.6"""
10+
return self.assertTrue(foo in bar, msg=(msg or "%s not found in %s"
11+
% (foo, bar)))
12+
13+
def test_no_permissions(self):
14+
token = TwilioCapability("AC123", "XXXXX")
15+
payload = token.payload()
16+
self.assertEquals(len(payload), 1)
17+
self.assertEquals(payload["scope"], '')
18+
19+
def test_inbound_permissions(self):
20+
token = TwilioCapability("AC123", "XXXXX")
21+
token.allow_client_incoming("andy")
22+
payload = token.payload()
23+
24+
eurl = "scope:client:incoming?clientName=andy"
25+
self.assertEquals(len(payload), 1)
26+
self.assertEquals(payload['scope'], eurl)
27+
28+
def test_outbound_permissions(self) F438 :
29+
token = TwilioCapability("AC123", "XXXXX")
30+
token.allow_client_outgoing("AP123")
31+
payload = token.payload()
32+
33+
eurl = "scope:client:outgoing?appSid=AP123"
34+
35+
self.assertEquals(len(payload), 1)
36+
self.assertIn(eurl, payload['scope'])
37+
38+
def test_outbound_permissions_params(self):
39+
token = TwilioCapability("AC123", "XXXXX")
40+
token.allow_client_outgoing("AP123", foobar=3)
41+
payload = token.payload()
42+
43+
eurl = "scope:client:outgoing?appSid=AP123&appParams=foobar%3D3"
44+
self.assertEquals(payload["scope"], eurl)
45+
46+
def test_events(self):
47+
token = TwilioCapability("AC123", "XXXXX")
48+
token.allow_event_stream()
49+
payload = token.payload()
50+
51+
event_uri = "scope:stream:subscribe?path=%2F2010-04-01%2FEvents"
52+
self.assertEquals(payload["scope"], event_uri)
53+
54+
def test_events_with_filters(self):
55+
token = TwilioCapability("AC123", "XXXXX")
56+
token.allow_event_stream(foobar="hey")
57+
payload = token.payload()
58+
59+
event_uri = "scope:stream:subscribe?path=%2F2010-04-01%2FEvents&params=foobar%3Dhey"
60+
self.assertEquals(payload["scope"], event_uri)
61+
62+
63+
def test_decode(self):
64+
token = TwilioCapability("AC123", "XXXXX")
65+
token.allow_client_outgoing("AP123", foobar=3)
66+
token.allow_client_incoming("andy")
67+
token.allow_event_stream()
68+
69+
outgoing_uri = "scope:client:outgoing?appSid=AP123&appParams=foobar%3D3&clientName=andy"
70+
incoming_uri = "scope:client:incoming?clientName=andy"
71+
event_uri = "scope:stream:subscribe?path=%2F2010-04-01%2FEvents"
72+
73+
result = jwt.decode(token.generate(), "XXXXX")
74+
scope = result["scope"].split(" ")
75+
76+
self.assertIn(outgoing_uri, scope)
77+
self.assertIn(incoming_uri, scope)
78+
self.assertIn(event_uri, scope)

twilio/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version_info__ = ('3', '0', '2')
1+
__version_info__ = ('3', '1', '0')
22
__version__ = '.'.join(__version_info__)
33

44

twilio/contrib/jwt/__init__.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
""" JSON Web Token implementation
2+
3+
Minimum implementation based on this spec:
4+
http://self-issued.info/docs/draft-jones-json-web-token-01.html
5+
"""
6+
import base64
7+
import hashlib
8+
import hmac
9+
10+
try:
11+
import json
12+
except ImportError:
13+
import simplejson as json
14+
15+
__all__ = ['encode', 'decode', 'DecodeError']
16+
17+
class DecodeError(Exception): pass
18+
19+
signing_methods = {
20+
'HS256': lambda msg, key: hmac.new(key, msg, hashlib.sha256).digest(),
21+
'HS384': lambda msg, key: hmac.new(key, msg, hashlib.sha384).digest(),
22+
'HS512': lambda msg, key: hmac.new(key, msg, hashlib.sha512).digest(),
23+
}
24+
25+
def base64url_decode(input):
26+
input += '=' * (4 - (len(input) % 4))
27+
return base64.urlsafe_b64decode(input)
28+
29+
def base64url_encode(input):
30+
return base64.urlsafe_b64encode(input).replace('=', '')
31+
32+
def header(jwt):
33+
header_segment = jwt.split('.', 1)[0]
34+
try:
35+
return json.loads(base64url_decode(header_segment))
36+
except (ValueError, TypeError):
37+
raise DecodeError("Invalid header encoding")
38+
39+
def encode(payload, key, algorithm='HS256'):
40+
segments = []
41+
header = {"typ": "JWT", "alg": algorithm}
42+
segments.append(base64url_encode(json.dumps(header)))
43+
segments.append(base64url_encode(json.dumps(payload)))
44+
signing_input = '.'.join(segments)
45+
try:
46+
ascii_key = unicode(key).encode('utf8')
47+
signature = signing_methods[algorithm](signing_input, ascii_key)
48+
except KeyError:
49+
raise NotImplementedError("Algorithm not supported")
50+
segments.append(base64url_encode(signature))
51+
return '.'.join(segments)
52+
53+
def decode(jwt, key='', verify=True):
54+
try:
55+
signing_input, crypto_segment = jwt.rsplit('.', 1)
56+
header_segment, payload_segment = signing_input.split('.', 1)
57+
except ValueError:
58+
raise DecodeError("Not enough segments")
59+
try:
60+
header = json.loads(base64url_decode(header_segment))
61+
payload = json.loads(base64url_decode(payload_segment))
62+
signature = base64url_decode(crypto_segment)
63+
except (ValueError, TypeError):
64+
raise DecodeError("Invalid segment encoding")
65+
if verify:
66+
try:
67+
ascii_key = unicode(key).encode('utf8')
68+
if not signature == signing_methods[header['alg']](signing_input, ascii_key):
69+
raise DecodeError("Signature verification failed")
70+
except KeyError:
71+
raise DecodeError("Algorithm not supported")
72+
return payload

0 commit comments

Comments
 (0)
0