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

Remove pycurl #15

Closed
wants to merge 6 commits into from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 6 additions & 3 deletions braintree/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ class Configuration(object):
.. [1] `URL Fetch Python API Overview <http://code.google.com/appengine/docs/python/urlfetch/overview.html>`_
"""
@staticmethod
def configure(environment, merchant_id, public_key, private_key):
def configure(environment, merchant_id, public_key, private_key, timeout=15):
Configuration.environment = environment
Configuration.merchant_id = merchant_id
Configuration.public_key = public_key
Configuration.private_key = private_key
Configuration.use_unsafe_ssl = False
Configuration.timeout = timeout

@staticmethod
def gateway():
Expand All @@ -52,18 +53,20 @@ def instantiate():
Configuration.environment,
Configuration.merchant_id,
Configuration.public_key,
Configuration.private_key
Configuration.private_key,
Configuration.timeout
)

@staticmethod
def api_version():
return "2"

def __init__(self, environment, merchant_id, public_key, private_key):
def __init__(self, environment, merchant_id, public_key, private_key, timeout):
self.environment = environment
self.merchant_id = merchant_id
self.public_key = public_key
self.private_key = private_key
self.timeout = timeout

def base_merchant_path(self):
return "/merchants/" + self.merchant_id
Expand Down
60 changes: 60 additions & 0 deletions braintree/util/backports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""The match_hostname() function from Python 3.2, essential when using SSL."""

import re

class CertificateError(ValueError):
pass

def _dnsname_to_pat(dn):
pats = []
for frag in dn.split(r'.'):
if frag == '*':
# When '*' is a fragment by itself, it matches a non-empty dotless
# fragment.
pats.append('[^.]+')
else:
# Otherwise, '*' matches any dotless fragment.
frag = re.escape(frag)
pats.append(frag.replace(r'\*', '[^.]*'))
return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)

def match_hostname(cert, hostname):
"""Verify that *cert* (in decoded format as returned by
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
are mostly followed, but IP addresses are not accepted for *hostname*.

CertificateError is raised on failure. On success, the function
returns nothing.
"""
if not cert:
raise ValueError("empty or no certificate")
dnsnames = []
san = cert.get('subjectAltName', ())

for key, value in san:
if key == 'DNS':
if _dnsname_to_pat(value).match(hostname):
return
dnsnames.append(value)

if not san:
# The subject is only checked when subjectAltName is empty
for sub in cert.get('subject', ()):
for key, value in sub:
# XXX according to RFC 2818, the most specific Common Name
# must be used.
if key == 'commonName':
if _dnsname_to_pat(value).match(hostname):
return
dnsnames.append(value)
if len(dnsnames) > 1:
raise CertificateError("hostname %r "
"doesn't match either of %s"
% (hostname, ', '.join(map(repr, dnsnames))))
elif len(dnsnames) == 1:
raise CertificateError("hostname %r "
"doesn't match %r"
% (hostname, dnsnames[0]))
else:
raise CertificateError("no appropriate commonName or "
"subjectAltName fields were found")
44 changes: 17 additions & 27 deletions braintree/util/http.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import httplib
import base64
import socket
import ssl
from braintree.configuration import Configuration
from braintree.util.backports import match_hostname
from braintree.util.xml_util import XmlUtil
from braintree.exceptions.authentication_error import AuthenticationError
from braintree.exceptions.authorization_error import AuthorizationError
Expand Down Expand Up @@ -52,9 +55,9 @@ def put(self, path, params={}):
def __http_do(self, http_verb, path, params=None):
if self.environment.is_ssl:
self.__verify_ssl()
conn = httplib.HTTPSConnection(self.environment.server, self.environment.port)
conn = httplib.HTTPSConnection(self.environment.server, self.environment.port, timeout=self.config.timeout)
else:
conn = httplib.HTTPConnection(self.environment.server, self.environment.port)
conn = httplib.HTTPConnection(self.environment.server, self.environment.port, timeout=self.config.timeout)

conn.request(
http_verb,
Expand All @@ -64,7 +67,7 @@ def __http_do(self, http_verb, path, params=None):
)
response = conn.getresponse()
status = response.status

if Http.is_error_status(status):
conn.close()
Http.raise_exception_from_status(status)
Expand All @@ -90,27 +93,14 @@ def __headers(self):

def __verify_ssl(self):
if Configuration.use_unsafe_ssl: return

try:
import pycurl
except ImportError, e:
print "Cannot load PycURL. Please refer to Braintree documentation."
print """
If you are in an environment where you absolutely cannot load PycURL
(such as Google App Engine), you can turn off SSL Verification by setting:

Configuration.use_unsafe_ssl = True

This is highly discouraged, however, since it leaves you susceptible to
man-in-the-middle attacks."""
raise e

curl = pycurl.Curl()
# see http://curl.haxx.se/libcurl/c/curl_easy_setopt.html for info on these options
curl.setopt(pycurl.CAINFO, self.environment.ssl_certificate)
curl.setopt(pycurl.SSL_VERIFYPEER, 1)
curl.setopt(pycurl.SSL_VERIFYHOST, 2)
curl.setopt(pycurl.NOBODY, 1)
curl.setopt(pycurl.NOSIGNAL, 1)
curl.setopt(pycurl.URL, self.environment.protocol + self.environment.server_and_port)
curl.perform()

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self.environment.server, self.environment.port))

sslsock = ssl.wrap_socket(
sock,
ssl_version=ssl.PROTOCOL_SSLv3,
cert_reqs=ssl.CERT_REQUIRED,
ca_certs=self.environment.ssl_certificate
)
match_hostname(sslsock.getpeercert(), self.environment.server)
2 changes: 1 addition & 1 deletion braintree/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Version = "2.9.0"
Version = "2.9.0.post1"
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import braintree
from distutils.core import setup
setup(
name="braintree",
name="el-braintree",
version=braintree.version.Version,
description="Braintree Python Library",
author="Braintree",
author_email="support@getbraintree.com",
url="http://www.braintreepaymentsolutions.com/gateway/python",
packages=["braintree", "braintree.exceptions", "braintree.util"],
package_data={"braintree": ["ssl/*"]},
install_requires=["pycurl==7.19.0"],
tests_require=["pycurl==7.19.0", "nose==0.11.3"]
install_requires=[],
tests_require=["nose==0.11.3"]
)
19 changes: 8 additions & 11 deletions tests/integration/test_http.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from braintree.util.backports import CertificateError
from tests.test_helper import *
import pycurl


class TestHttp(unittest.TestCase):

Expand Down Expand Up @@ -28,28 +29,24 @@ def test_successful_connection_to_production(self):
pass

def test_unsuccessful_connection_to_good_ssl_server_with_wrong_cert(self):
environment = Environment(Environment.Sandbox.server, "443", True, Environment.Production.ssl_certificate)
environment = Environment("braintreegateway.com", "443", True, Environment.Production.ssl_certificate)
try:
config = Configuration(environment, "merchant_id", "public_key", "private_key")
http = config.http()
http.get("/")
self.assertTrue(False)
except pycurl.error, e:
error_code, error_msg = e
self.assertEquals(pycurl.E_SSL_CACERT, error_code)
self.assertTrue(re.search('verif(y|ication) failed', error_msg))

except CertificateError, e:
self.assertTrue(re.search("doesn't match", e.message))

def test_unsuccessful_connection_to_ssl_server_with_wrong_domain(self):
try:
environment = Environment("braintreegateway.com", "443", True, Environment.Production.ssl_certificate)
config = Configuration(environment, "merchant_id", "public_key", "private_key")
http = config.http()
http.get("/")
self.assertTrue(False)
except pycurl.error, e:
error_code, error_msg = e
self.assertEquals(pycurl.E_SSL_PEER_CERTIFICATE, error_code)
self.assertTrue(re.search("SSL: certificate subject name", error_msg))
except CertificateError, e:
self.assertTrue(re.search("doesn't match", e.message))

def test_unsafe_ssl_connection(self):
try:
Expand Down