Guest User

Untitled

a guest
Jan 17th, 2019
181
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.76 KB | None | 0 0
  1. # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software
  2. # Foundation; All Rights Reserved
  3.  
  4. """A HTTPSConnection/Handler with additional proxy and cert validation features.
  5.  
  6. In particular, monkey patches in Python r74203 to provide support for CONNECT
  7. proxies and adds SSL cert validation if the ssl module is present.
  8. """
  9.  
  10. __author__ = "{frew,nick.johnson}@google.com (Fred Wulff and Nick Johnson)"
  11.  
  12. import base64
  13. import httplib
  14. import logging
  15. import socket
  16. from urllib import splitpasswd
  17. from urllib import splittype
  18. from urllib import splituser
  19. import urllib2
  20.  
  21.  
  22. class InvalidCertificateException(httplib.HTTPException):
  23. """Raised when a certificate is provided with an invalid hostname."""
  24.  
  25. def __init__(self, host, cert, reason):
  26. """Constructor.
  27.  
  28. Args:
  29. host: The hostname the connection was made to.
  30. cert: The SSL certificate (as a dictionary) the host returned.
  31. reason: user readable error reason.
  32. """
  33. httplib.HTTPException.__init__(self)
  34. self.host = host
  35. self.cert = cert
  36. self.reason = reason
  37.  
  38. def __str__(self):
  39. return ("Host %s returned an invalid certificate (%s): %s\n"
  40. "To learn more, see "
  41. "http://code.google.com/appengine/kb/general.html#rpcssl" %
  42. (self.host, self.reason, self.cert))
  43.  
  44.  
  45. try:
  46. import ssl
  47. _CAN_VALIDATE_CERTS = True
  48. except ImportError:
  49. _CAN_VALIDATE_CERTS = False
  50.  
  51.  
  52. def can_validate_certs():
  53. """Return True if we have the SSL package and can validate certificates."""
  54. return _CAN_VALIDATE_CERTS
  55.  
  56.  
  57. # Reexport SSLError so clients don't have to to do their own checking for ssl's
  58. # existence.
  59. if can_validate_certs():
  60. SSLError = ssl.SSLError
  61. else:
  62. SSLError = None
  63.  
  64.  
  65. def create_fancy_connection(tunnel_host=None, key_file=None,
  66. cert_file=None, ca_certs=None,
  67. proxy_authorization=None):
  68. # This abomination brought to you by the fact that
  69. # the HTTPHandler creates the connection instance in the middle
  70. # of do_open so we need to add the tunnel host to the class.
  71.  
  72. class PresetProxyHTTPSConnection(httplib.HTTPSConnection):
  73. """An HTTPS connection that uses a proxy defined by the enclosing scope."""
  74.  
  75. def __init__(self, *args, **kwargs):
  76. httplib.HTTPSConnection.__init__(self, *args, **kwargs)
  77.  
  78. self._tunnel_host = tunnel_host
  79. if tunnel_host:
  80. logging.debug("Creating preset proxy https conn: %s", tunnel_host)
  81.  
  82. self.key_file = key_file
  83. self.cert_file = cert_file
  84. self.ca_certs = ca_certs
  85. if can_validate_certs():
  86. if self.ca_certs:
  87. self.cert_reqs = ssl.CERT_REQUIRED
  88. else:
  89. self.cert_reqs = ssl.CERT_NONE
  90.  
  91. def _get_hostport(self, host, port):
  92. # Python 2.7.7rc1 (hg r90728:568041fd8090), 3.4.1 and 3.5 rename
  93. # _set_hostport to _get_hostport and changes it's functionality. The
  94. # Python 2.7.7rc1 version of this method is included here for
  95. # compatibility with earlier versions of Python. Without this, HTTPS over
  96. # HTTP CONNECT proxies cannot be used.
  97.  
  98. # This method may be removed if compatibility with Python <2.7.7rc1 is not
  99. # required.
  100.  
  101. # Python bug: http://bugs.python.org/issue7776
  102. if port is None:
  103. i = host.rfind(":")
  104. j = host.rfind("]") # ipv6 addresses have [...]
  105. if i > j:
  106. try:
  107. port = int(host[i+1:])
  108. except ValueError:
  109. if host[i+1:] == "": # http://foo.com:/ == http://foo.com/
  110. port = self.default_port
  111. else:
  112. raise httplib.InvalidURL("nonnumeric port: '%s'" % host[i+1:])
  113. host = host[:i]
  114. else:
  115. port = self.default_port
  116. if host and host[0] == "[" and host[-1] == "]":
  117. host = host[1:-1]
  118.  
  119. return (host, port)
  120.  
  121. def _tunnel(self):
  122. self.host, self.port = self._get_hostport(self._tunnel_host, None)
  123. logging.info("Connecting through tunnel to: %s:%d",
  124. self.host, self.port)
  125.  
  126. self.send("CONNECT %s:%d HTTP/1.0\r\n" % (self.host, self.port))
  127.  
  128. if proxy_authorization:
  129. self.send("Proxy-Authorization: %s\r\n" % proxy_authorization)
  130.  
  131. # blank line
  132. self.send("\r\n")
  133.  
  134. response = self.response_class(self.sock, strict=self.strict,
  135. method=self._method)
  136. # pylint: disable=protected-access
  137. (_, code, message) = response._read_status()
  138.  
  139. if code != 200:
  140. self.close()
  141. raise socket.error("Tunnel connection failed: %d %s" %
  142. (code, message.strip()))
  143.  
  144. while True:
  145. line = response.fp.readline()
  146. if line == "\r\n":
  147. break
  148.  
  149. def _get_valid_hosts_for_cert(self, cert):
  150. """Returns a list of valid host globs for an SSL certificate.
  151.  
  152. Args:
  153. cert: A dictionary representing an SSL certificate.
  154. Returns:
  155. list: A list of valid host globs.
  156. """
  157. if "subjectAltName" in cert:
  158. return [x[1] for x in cert["subjectAltName"] if x[0].lower() == "dns"]
  159. else:
  160. # Return a list of commonName fields
  161. return [x[0][1] for x in cert["subject"]
  162. if x[0][0].lower() == "commonname"]
  163.  
  164. def _validate_certificate_hostname(self, cert, hostname):
  165. """Perform RFC2818/6125 validation against a cert and hostname.
  166.  
  167. Args:
  168. cert: A dictionary representing an SSL certificate.
  169. hostname: The hostname to test.
  170. Returns:
  171. bool: Whether or not the hostname is valid for this certificate.
  172. """
  173. hosts = self._get_valid_hosts_for_cert(cert)
  174. for host in hosts:
  175. # Wildcards are only valid when the * exists at the end of the last
  176. # (left-most) label, and there are at least 3 labels in the expression.
  177. if ("*." in host and host.count("*") == 1 and
  178. host.count(".") > 1 and "." in hostname):
  179. left_expected, right_expected = host.split("*.")
  180. left_hostname, right_hostname = hostname.split(".", 1)
  181. if (left_hostname.startswith(left_expected) and
  182. right_expected == right_hostname):
  183. return True
  184. elif host == hostname:
  185. return True
  186. return False
  187.  
  188. def connect(self):
  189. # TODO(frew): When we drop support for <2.6 (in the far distant future),
  190. # change this to socket.create_connection.
  191. self.sock = _create_connection((self.host, self.port))
  192.  
  193. if self._tunnel_host:
  194. self._tunnel()
  195.  
  196. # ssl and FakeSocket got deprecated. Try for the new hotness of wrap_ssl,
  197. # with fallback. Note: Since can_validate_certs() just checks for the
  198. # ssl module, it's equivalent to attempting to import ssl from
  199. # the function, but doesn't require a dynamic import, which doesn't
  200. # play nicely with dev_appserver.
  201. if can_validate_certs():
  202. self.sock = ssl.wrap_socket(self.sock,
  203. keyfile=self.key_file,
  204. certfile=self.cert_file,
  205. ca_certs=self.ca_certs,
  206. cert_reqs=self.cert_reqs)
  207.  
  208. if self.cert_reqs & ssl.CERT_REQUIRED:
  209. cert = self.sock.getpeercert()
  210. hostname = self.host.split(":", 0)[0]
  211. if not self._validate_certificate_hostname(cert, hostname):
  212. raise InvalidCertificateException(hostname, cert,
  213. "hostname mismatch")
  214. else:
  215. ssl_socket = socket.ssl(self.sock,
  216. keyfile=self.key_file,
  217. certfile=self.cert_file)
  218. self.sock = httplib.FakeSocket(self.sock, ssl_socket)
  219.  
  220. return PresetProxyHTTPSConnection
  221.  
  222.  
  223. # Here to end of _create_connection copied wholesale from Python 2.6"s socket.py
  224. _GLOBAL_DEFAULT_TIMEOUT = object()
  225.  
  226.  
  227. def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT):
  228. """Connect to *address* and return the socket object.
  229.  
  230. Convenience function. Connect to *address* (a 2-tuple ``(host,
  231. port)``) and return the socket object. Passing the optional
  232. *timeout* parameter will set the timeout on the socket instance
  233. before attempting to connect. If no *timeout* is supplied, the
  234. global default timeout setting returned by :func:`getdefaulttimeout`
  235. is used.
  236. """
  237.  
  238. msg = "getaddrinfo returns an empty list"
  239. host, port = address
  240. for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
  241. af, socktype, proto, canonname, sa = res
  242. sock = None
  243. try:
  244. sock = socket.socket(af, socktype, proto)
  245. if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
  246. sock.settimeout(timeout)
  247. sock.connect(sa)
  248. return sock
  249.  
  250. except socket.error, msg:
  251. if sock is not None:
  252. sock.close()
  253.  
  254. raise socket.error, msg
  255.  
  256.  
  257. class FancyRequest(urllib2.Request):
  258. """A request that allows the use of a CONNECT proxy."""
  259.  
  260. def __init__(self, *args, **kwargs):
  261. urllib2.Request.__init__(self, *args, **kwargs)
  262. self._tunnel_host = None
  263. self._key_file = None
  264. self._cert_file = None
  265. self._ca_certs = None
  266.  
  267. def set_proxy(self, host, type):
  268. saved_type = None
  269.  
  270. if self.get_type() == "https" and not self._tunnel_host:
  271. self._tunnel_host = self.get_host()
  272. saved_type = self.get_type()
  273. urllib2.Request.set_proxy(self, host, type)
  274.  
  275. if saved_type:
  276. # Don't set self.type, we want to preserve the
  277. # type for tunneling.
  278. self.type = saved_type
  279.  
  280. def set_ssl_info(self, key_file=None, cert_file=None, ca_certs=None):
  281. self._key_file = key_file
  282. self._cert_file = cert_file
  283. self._ca_certs = ca_certs
  284.  
  285.  
  286. class FancyProxyHandler(urllib2.ProxyHandler):
  287. """A ProxyHandler that works with CONNECT-enabled proxies."""
  288.  
  289. # Taken verbatim from /usr/lib/python2.5/urllib2.py
  290. def _parse_proxy(self, proxy):
  291. """Return (scheme, user, password, host/port) given a URL or an authority.
  292.  
  293. If a URL is supplied, it must have an authority (host:port) component.
  294. According to RFC 3986, having an authority component means the URL must
  295. have two slashes after the scheme:
  296.  
  297. >>> _parse_proxy('file:/ftp.example.com/')
  298. Traceback (most recent call last):
  299. ValueError: proxy URL with no authority: 'file:/ftp.example.com/'
  300.  
  301. The first three items of the returned tuple may be None.
  302.  
  303. Examples of authority parsing:
  304.  
  305. >>> _parse_proxy('proxy.example.com')
  306. (None, None, None, 'proxy.example.com')
  307. >>> _parse_proxy('proxy.example.com:3128')
  308. (None, None, None, 'proxy.example.com:3128')
  309.  
  310. The authority component may optionally include userinfo (assumed to be
  311. username:password):
  312.  
  313. >>> _parse_proxy('joe:password@proxy.example.com')
  314. (None, 'joe', 'password', 'proxy.example.com')
  315. >>> _parse_proxy('joe:password@proxy.example.com:3128')
  316. (None, 'joe', 'password', 'proxy.example.com:3128')
  317.  
  318. Same examples, but with URLs instead:
  319.  
  320. >>> _parse_proxy('http://proxy.example.com/')
  321. ('http', None, None, 'proxy.example.com')
  322. >>> _parse_proxy('http://proxy.example.com:3128/')
  323. ('http', None, None, 'proxy.example.com:3128')
  324. >>> _parse_proxy('http://joe:password@proxy.example.com/')
  325. ('http', 'joe', 'password', 'proxy.example.com')
  326. >>> _parse_proxy('http://joe:password@proxy.example.com:3128')
  327. ('http', 'joe', 'password', 'proxy.example.com:3128')
  328.  
  329. Everything after the authority is ignored:
  330.  
  331. >>> _parse_proxy('ftp://joe:password@proxy.example.com/rubbish:3128')
  332. ('ftp', 'joe', 'password', 'proxy.example.com')
  333.  
  334. Test for no trailing '/' case:
  335.  
  336. >>> _parse_proxy('http://joe:password@proxy.example.com')
  337. ('http', 'joe', 'password', 'proxy.example.com')
  338.  
  339. """
  340. scheme, r_scheme = splittype(proxy)
  341. if not r_scheme.startswith("/"):
  342. # authority
  343. scheme = None
  344. authority = proxy
  345. else:
  346. # URL
  347. if not r_scheme.startswith("//"):
  348. raise ValueError("proxy URL with no authority: %r" % proxy)
  349. # We have an authority, so for RFC 3986-compliant URLs (by ss 3.
  350. # and 3.3.), path is empty or starts with '/'
  351. end = r_scheme.find("/", 2)
  352. if end == -1:
  353. end = None
  354. authority = r_scheme[2:end]
  355. userinfo, hostport = splituser(authority)
  356. if userinfo is not None:
  357. user, password = splitpasswd(userinfo)
  358. else:
  359. user = password = None
  360. return scheme, user, password, hostport
  361.  
  362. def proxy_open(self, req, proxy, type):
  363. # This block is copied wholesale from Python2.6 urllib2.
  364. # It is idempotent, so the superclass method call executes as normal
  365. # if invoked.
  366. orig_type = req.get_type()
  367. proxy_type, user, password, hostport = self._parse_proxy(proxy)
  368. if proxy_type is None:
  369. proxy_type = orig_type
  370. if user and password:
  371. user_pass = "%s:%s" % (urllib2.unquote(user), urllib2.unquote(password))
  372. creds = base64.b64encode(user_pass).strip()
  373. # Later calls overwrite earlier calls for the same header
  374. req.add_header("Proxy-authorization", "Basic " + creds)
  375. hostport = urllib2.unquote(hostport)
  376. req.set_proxy(hostport, proxy_type)
  377. # This condition is the change
  378. if orig_type == "https":
  379. return None
  380.  
  381. return urllib2.ProxyHandler.proxy_open(self, req, proxy, type)
  382.  
  383.  
  384. class FancyHTTPSHandler(urllib2.HTTPSHandler):
  385. """An HTTPSHandler that works with CONNECT-enabled proxies."""
  386.  
  387. def do_open(self, http_class, req, *args, **kwargs):
  388. proxy_authorization = None
  389. for header in req.headers:
  390. if header.lower() == "proxy-authorization":
  391. proxy_authorization = req.headers[header]
  392. break
  393.  
  394. # Intentionally very specific so as to opt for false negatives
  395. # rather than false positives.
  396. try:
  397. return urllib2.HTTPSHandler.do_open(
  398. self,
  399. create_fancy_connection(req._tunnel_host,
  400. req._key_file,
  401. req._cert_file,
  402. req._ca_certs,
  403. proxy_authorization),
  404. req, *args, **kwargs)
  405. except urllib2.URLError, url_error:
  406. try:
  407. import ssl
  408. if (type(url_error.reason) == ssl.SSLError and
  409. url_error.reason.args[0] == 1):
  410. # Display the reason to the user. Need to use args for python2.5
  411. # compat.
  412. raise InvalidCertificateException(req.host, "",
  413. url_error.reason.args[1])
  414. except ImportError:
  415. pass
  416.  
  417. raise url_error
  418.  
  419.  
  420. # We have to implement this so that we persist the tunneling behavior
  421. # through redirects.
  422. class FancyRedirectHandler(urllib2.HTTPRedirectHandler):
  423. """A redirect handler that persists CONNECT-enabled proxy information."""
  424.  
  425. def redirect_request(self, req, *args, **kwargs):
  426. new_req = urllib2.HTTPRedirectHandler.redirect_request(
  427. self, req, *args, **kwargs)
  428. # Same thing as in our set_proxy implementation, but in this case
  429. # we"ve only got a Request to work with, so it was this or copy
  430. # everything over piecemeal.
  431. #
  432. # Note that we do not persist tunneling behavior from an http request
  433. # to an https request, because an http request does not set _tunnel_host.
  434. #
  435. # Also note that in Python < 2.6, you will get an error in
  436. # FancyHTTPSHandler.do_open() on an https urllib2.Request that uses an http
  437. # proxy, since the proxy type will be set to http instead of https.
  438. # (FancyRequest, and urllib2.Request in Python >= 2.6 set the proxy type to
  439. # https.) Such an urllib2.Request could result from this redirect
  440. # if you are redirecting from an http request (since an an http request
  441. # does not have _tunnel_host set, and thus you will not set the proxy
  442. # in the code below), and if you have defined a proxy for https in, say,
  443. # FancyProxyHandler, and that proxy has type http.
  444. if hasattr(req, "_tunnel_host") and isinstance(new_req, urllib2.Request):
  445. if new_req.get_type() == "https":
  446. if req._tunnel_host:
  447. # req is proxied, so copy the proxy info.
  448. new_req._tunnel_host = new_req.get_host()
  449. new_req.set_proxy(req.host, "https")
  450. else:
  451. # req is not proxied, so just make sure _tunnel_host is defined.
  452. new_req._tunnel_host = None
  453. new_req.type = "https"
  454. if hasattr(req, "_key_file") and isinstance(new_req, urllib2.Request):
  455. # Copy the auxiliary data in case this or any further redirect is https
  456. new_req._key_file = req._key_file
  457. new_req._cert_file = req._cert_file
  458. new_req._ca_certs = req._ca_certs
  459.  
  460. return new_req
Add Comment
Please, Sign In to add comment