Skip to content
Closed
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
20 changes: 10 additions & 10 deletions laterpay/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import re
import string
import time
import urllib
import urllib2

from . import signing
from . import compat

import warnings


Expand Down Expand Up @@ -184,22 +184,22 @@ def get_controls_balance_url(self, forcelang=None):
return url

def _get_dialog_api_url(self, url):
return '%s/dialog-api?url=%s' % (self.web_root, urllib.quote_plus(url))
return '%s/dialog-api?url=%s' % (self.web_root, compat.quote_plus(url))

def get_login_dialog_url(self, next_url, use_jsevents=False):
url = '%s/account/dialog/login?next=%s%s%s' % (self.web_root, urllib.quote_plus(next_url),
url = '%s/account/dialog/login?next=%s%s%s' % (self.web_root, compat.quote_plus(next_url),
"&jsevents=1" if use_jsevents else "",
"&cp=%s" % self.cp_key)
return self._get_dialog_api_url(url)

def get_signup_dialog_url(self, next_url, use_jsevents=False):
url = '%s/account/dialog/signup?next=%s%s%s' % (self.web_root, urllib.quote_plus(next_url),
url = '%s/account/dialog/signup?next=%s%s%s' % (self.web_root, compat.quote_plus(next_url),
"&jsevents=1" if use_jsevents else "",
"&cp=%s" % self.cp_key)
return self._get_dialog_api_url(url)

def get_logout_dialog_url(self, next_url, use_jsevents=False):
url = '%s/account/dialog/logout?next=%s%s%s' % (self.web_root, urllib.quote_plus(next_url),
url = '%s/account/dialog/logout?next=%s%s%s' % (self.web_root, compat.quote_plus(next_url),
"&jsevents=1" if use_jsevents else "",
"&cp=%s" % self.cp_key)
return self._get_dialog_api_url(url)
Expand Down Expand Up @@ -314,16 +314,16 @@ def _make_request(self, url, params, method='GET'):
}

if method == 'POST':
req = urllib2.Request(url, data=params, headers=headers)
req = compat.Request(url, data=params, headers=headers)
else:
url = "%s?%s" % (url, params)
req = urllib2.Request(url, headers=headers)
req = compat.Request(url, headers=headers)

_log.debug("Making request to %s", url)

try:
response = urllib2.urlopen(req).read()
except urllib2.URLError, e:
response = compat.urlopen(req).read()
except compat.URLError as e:
_log.debug("Request failed with reason: %s", e.reason)
resp = {'status': 'connection_error'}
except:
Expand Down
38 changes: 38 additions & 0 deletions laterpay/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function

import sys

py3k = sys.version_info[:2] >= (3, 0)


if py3k:
string_types = str,

def cmp(a, b):
return (a > b) - (a < b)

def b(s):
return s.encode("latin-1")

from urllib.parse import quote, quote_plus, urlencode
from urllib.parse import urlparse, parse_qs

from urllib.request import Request, urlopen
from urllib.error import URLError

else:
string_types = basestring,

cmp = cmp

def b(s):
return s

from urllib import quote, quote_plus, urlencode

from urlparse import urlparse, parse_qs

from urllib2 import Request, urlopen, URLError

55 changes: 31 additions & 24 deletions laterpay/signing.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function

import functools
import hashlib
import hmac
import time
import urllib
import urlparse

from . import compat


ALLOWED_METHODS = ('GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD')

Expand All @@ -18,32 +22,34 @@ def time_independent_HMAC_compare(a, b):


def create_HMAC(HMAC_secret, *parts):
hash = hmac.new(HMAC_secret, digestmod=hashlib.sha224)
hash = hmac.new(compat.b(HMAC_secret), digestmod=hashlib.sha224)
for part in parts:
hash.update(part)
hash.update(compat.b(part))
return hash.hexdigest()


def sort_params(param_dict):

def cmp_params(param1, param2):
result = cmp(param1[0], param2[0])
result = compat.cmp(param1[0], param2[0])
if result == 0:
result = cmp(param1[1], param2[1])
result = compat.cmp(param1[1], param2[1])
return result

param_list = []
for name, value_list in param_dict.iteritems():
for name, value_list in param_dict.items():
if isinstance(value_list, (list, tuple)):
for value in value_list:
if not isinstance(value, basestring):
if not isinstance(value, compat.string_types):
value = str(value)
param_list.append((name, value))
else:
if not isinstance(value_list, basestring):
if not isinstance(value_list, compat.string_types):
value_list = str(value_list)
param_list.append((name, value_list))

if compat.py3k:
return sorted(param_list, key=functools.cmp_to_key(cmp_params))
return sorted(param_list, cmp_params)


Expand Down Expand Up @@ -71,7 +77,7 @@ def normalise_param_structure(params):
return out

# otherwise this is a dictionary, so either it is { a => b } or { a => (b,c) }
for key, value in params.iteritems():
for key, value in params.items():
if not isinstance(value, (list, tuple)):
out[key] = [value]
else:
Expand All @@ -87,33 +93,33 @@ def create_base_message(params, url, method='POST'):

data = {}

url = urllib.quote(_encode_if_unicode(url), safe='')
url = compat.quote(_encode_if_unicode(url), safe='')

if method not in ALLOWED_METHODS:
raise ValueError('method should be one of: {}'.format(ALLOWED_METHODS))

params = normalise_param_structure(params)

for key, values in params.items():
key = urllib.quote(_encode_if_unicode(key), safe='')
key = compat.quote(_encode_if_unicode(key), safe='')

if not isinstance(values, (list, tuple)):
values = [values]

values_str = []

# If any non basestring objects, ``str()`` them.
# If any non compat.string_types objects, ``str()`` them.
for v in values:
if not isinstance(v, basestring):
if not isinstance(v, compat.string_types):
v = str(v)
values_str.append(v)

data[key] = [urllib.quote(_encode_if_unicode(v), safe='') for v in values_str]
data[key] = [compat.quote(_encode_if_unicode(v), safe='') for v in values_str]

sorted_params = sort_params(data)

param_str = '&'.join('{}={}'.format(k, v) for k, v in sorted_params)
param_str = urllib.quote(param_str, safe='')
param_str = compat.quote(param_str, safe='')

return msg.format(method=method, url=url, params=param_str)

Expand All @@ -123,7 +129,7 @@ def sign(secret, params, url, method='POST'):
Create signature for given `params`, `url` and HTTP method.

How params are canonicalized:
- `urllib.quote` every key and value that will be signed
- `compat.quote` every key and value that will be signed
- sort the param list
- `'&'join` the params

Expand Down Expand Up @@ -189,7 +195,7 @@ def sign_and_encode(secret, params, url, method="GET"):
value = _encode_if_unicode(v)
sorted_data.append((k, value))

encoded = urllib.urlencode(sorted_data)
encoded = compat.urlencode(sorted_data)
hmac = sign(secret, params, url=url, method=method)

return "%s&hmac=%s" % (encoded, hmac)
Expand All @@ -216,12 +222,12 @@ def sign_get_url(secret, url, signature_paramname="hmac"):
:type signature_paramname: str
:returns: ``str`` -- the URL, including the signature as an URL parameter
"""
parsed = urlparse.urlparse(url)
parsed = compat.urlparse(url)

if parsed.query != "":
# use urlparse.parse_qsl, because .parse_qs seems to create problems
# with urllib.urlencode()
qs = urlparse.parse_qsl(parsed.query, keep_blank_values=True)
# use compat.parse_qsl, because .parse_qs seems to create problems
# with compat.urlencode()
qs = compat.parse_qsl(parsed.query, keep_blank_values=True)

### create string to sign
# .sort() will sort in alphabetical order
Expand All @@ -232,7 +238,7 @@ def sign_get_url(secret, url, signature_paramname="hmac"):

qs.append((signature_paramname, hmac))
return parsed.scheme + "://" + parsed.netloc + parsed.path + \
parsed.params + "?" + urllib.urlencode(qs) + parsed.fragment
parsed.params + "?" + compat.urlencode(qs) + parsed.fragment

return None

Expand All @@ -246,6 +252,7 @@ def _encode_if_unicode(value, encoding='utf-8'):
This utility is needed because some web frameworks can provide
request arguments as ``str`` instances.
"""
if isinstance(value, unicode):

if not compat.py3k and isinstance(value, unicode):
value = value.encode(encoding)
return value
10 changes: 5 additions & 5 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
from __future__ import absolute_import, print_function

import sys
import urlparse
import uuid

if sys.version_info[:2] < (2, 7):
import unittest2 as unittest
else:
import unittest

from laterpay.compat import urlparse, parse_qs
from laterpay import (
APIException,
InvalidItemDefinition,
Expand Down Expand Up @@ -38,10 +38,10 @@ def setUp(self):
'some-secret')

def get_qs_dict(self, url):
o = urlparse.urlparse(url)
d = urlparse.parse_qs(o.query)
o = urlparse.urlparse(d['url'][0])
d = urlparse.parse_qs(o.query)
o = urlparse(url)
d = parse_qs(o.query)
o = urlparse(d['url'][0])
d = parse_qs(o.query)
return d

def assertQueryString(self, url, key, value=None):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
from __future__ import absolute_import, print_function

import sys
import urlparse

if sys.version_info[:2] < (2, 7):
import unittest2 as unittest
else:
import unittest

from laterpay import compat
from laterpay import signing


Expand Down Expand Up @@ -179,7 +179,7 @@ def test_signing_with_item(self):

# changing the price in the url
false_string = 'article_id=154&cp=laternews&jsevents=1&pricing=EUR150&purchase_date=1398861228815&title=VIDEO%3A+Rwanda%27s+genocide%2C+20+years+on&tref=4ebbf443-a12e-4ce9-89e4-999ba93ba1dc&ts=1398861228&url=http%3A%2F%2Flocal.laterpaytest.net%3A8003%2Fmmss%2F154&vat=EU19&hmac=4d41f1adcb7c6bf6cf9c5eb15b179fdbec667d53f2749e2845c87315'
false_params = urlparse.parse_qs(false_string)
false_params = compat.parse_qs(false_string)

self.assertFalse(signing.verify(signature, secret, false_params, base_url, method))

Expand Down