653 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			653 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from ctypes import windll, wintypes
 | |
| import ctypes
 | |
| import time
 | |
| import re
 | |
| import datetime
 | |
| import struct
 | |
| import locale
 | |
| 
 | |
| wininet = windll.wininet
 | |
| 
 | |
| try:
 | |
|     # Python 3
 | |
|     from urllib.parse import urlparse
 | |
| except (ImportError):
 | |
|     # Python 2
 | |
|     from urlparse import urlparse
 | |
| 
 | |
| from ..console_write import console_write
 | |
| from ..unicode import unicode_from_os
 | |
| from .non_http_error import NonHttpError
 | |
| from .http_error import HttpError
 | |
| from .rate_limit_exception import RateLimitException
 | |
| from .downloader_exception import DownloaderException
 | |
| from .decoding_downloader import DecodingDownloader
 | |
| from .limiting_downloader import LimitingDownloader
 | |
| from .caching_downloader import CachingDownloader
 | |
| 
 | |
| 
 | |
| class WinINetDownloader(DecodingDownloader, LimitingDownloader, CachingDownloader):
 | |
|     """
 | |
|     A downloader that uses the Windows WinINet DLL to perform downloads. This
 | |
|     has the benefit of utilizing system-level proxy configuration and CA certs.
 | |
| 
 | |
|     :param settings:
 | |
|         A dict of the various Package Control settings. The Sublime Text
 | |
|         Settings API is not used because this code is run in a thread.
 | |
|     """
 | |
| 
 | |
|     # General constants
 | |
|     ERROR_INSUFFICIENT_BUFFER = 122
 | |
| 
 | |
|     # InternetOpen constants
 | |
|     INTERNET_OPEN_TYPE_PRECONFIG = 0
 | |
| 
 | |
|     # InternetConnect constants
 | |
|     INTERNET_SERVICE_HTTP = 3
 | |
|     INTERNET_FLAG_EXISTING_CONNECT = 0x20000000
 | |
|     INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS = 0x00004000
 | |
| 
 | |
|     # InternetSetOption constants
 | |
|     INTERNET_OPTION_CONNECT_TIMEOUT = 2
 | |
|     INTERNET_OPTION_SEND_TIMEOUT = 5
 | |
|     INTERNET_OPTION_RECEIVE_TIMEOUT = 6
 | |
| 
 | |
|     # InternetQueryOption constants
 | |
|     INTERNET_OPTION_SECURITY_CERTIFICATE_STRUCT = 32
 | |
|     INTERNET_OPTION_PROXY = 38
 | |
|     INTERNET_OPTION_PROXY_USERNAME = 43
 | |
|     INTERNET_OPTION_PROXY_PASSWORD = 44
 | |
|     INTERNET_OPTION_CONNECTED_STATE = 50
 | |
| 
 | |
|     # HttpOpenRequest constants
 | |
|     INTERNET_FLAG_KEEP_CONNECTION = 0x00400000
 | |
|     INTERNET_FLAG_RELOAD = 0x80000000
 | |
|     INTERNET_FLAG_NO_CACHE_WRITE = 0x04000000
 | |
|     INTERNET_FLAG_PRAGMA_NOCACHE = 0x00000100
 | |
|     INTERNET_FLAG_SECURE = 0x00800000
 | |
| 
 | |
|     # HttpQueryInfo constants
 | |
|     HTTP_QUERY_RAW_HEADERS_CRLF = 22
 | |
| 
 | |
|     # InternetConnectedState constants
 | |
|     INTERNET_STATE_CONNECTED = 1
 | |
|     INTERNET_STATE_DISCONNECTED = 2
 | |
|     INTERNET_STATE_DISCONNECTED_BY_USER = 0x10
 | |
|     INTERNET_STATE_IDLE = 0x100
 | |
|     INTERNET_STATE_BUSY = 0x200
 | |
| 
 | |
| 
 | |
|     def __init__(self, settings):
 | |
|         self.settings = settings
 | |
|         self.debug = settings.get('debug')
 | |
|         self.network_connection = None
 | |
|         self.tcp_connection = None
 | |
|         self.use_count = 0
 | |
|         self.hostname = None
 | |
|         self.port = None
 | |
|         self.scheme = None
 | |
|         self.was_offline = None
 | |
| 
 | |
|     def close(self):
 | |
|         """
 | |
|         Closes any persistent/open connections
 | |
|         """
 | |
| 
 | |
|         closed = False
 | |
|         changed_state_back = False
 | |
| 
 | |
|         if self.tcp_connection:
 | |
|             wininet.InternetCloseHandle(self.tcp_connection)
 | |
|             self.tcp_connection = None
 | |
|             closed = True
 | |
| 
 | |
|         if self.network_connection:
 | |
|             wininet.InternetCloseHandle(self.network_connection)
 | |
|             self.network_connection = None
 | |
|             closed = True
 | |
| 
 | |
|         if self.was_offline:
 | |
|             dw_connected_state = wintypes.DWORD(self.INTERNET_STATE_DISCONNECTED_BY_USER)
 | |
|             dw_flags = wintypes.DWORD(0)
 | |
|             connected_info = InternetConnectedInfo(dw_connected_state, dw_flags)
 | |
|             wininet.InternetSetOptionA(None,
 | |
|                 self.INTERNET_OPTION_CONNECTED_STATE, ctypes.byref(connected_info), ctypes.sizeof(connected_info))
 | |
|             changed_state_back = True
 | |
| 
 | |
|         if self.debug:
 | |
|             s = '' if self.use_count == 1 else 's'
 | |
|             console_write(u"WinINet %s Debug General" % self.scheme.upper(), True)
 | |
|             console_write(u"  Closing connection to %s on port %s after %s request%s" % (
 | |
|                 self.hostname, self.port, self.use_count, s))
 | |
|             if changed_state_back:
 | |
|                 console_write(u"  Changed Internet Explorer back to Work Offline")
 | |
| 
 | |
|         self.hostname = None
 | |
|         self.port = None
 | |
|         self.scheme = None
 | |
|         self.use_count = 0
 | |
|         self.was_offline = None
 | |
| 
 | |
|     def download(self, url, error_message, timeout, tries, prefer_cached=False):
 | |
|         """
 | |
|         Downloads a URL and returns the contents
 | |
| 
 | |
|         :param url:
 | |
|             The URL to download
 | |
| 
 | |
|         :param error_message:
 | |
|             A string to include in the console error that is printed
 | |
|             when an error occurs
 | |
| 
 | |
|         :param timeout:
 | |
|             The int number of seconds to set the timeout to
 | |
| 
 | |
|         :param tries:
 | |
|             The int number of times to try and download the URL in the case of
 | |
|             a timeout or HTTP 503 error
 | |
| 
 | |
|         :param prefer_cached:
 | |
|             If a cached version should be returned instead of trying a new request
 | |
| 
 | |
|         :raises:
 | |
|             RateLimitException: when a rate limit is hit
 | |
|             DownloaderException: when any other download error occurs
 | |
| 
 | |
|         :return:
 | |
|             The string contents of the URL
 | |
|         """
 | |
| 
 | |
|         if prefer_cached:
 | |
|             cached = self.retrieve_cached(url)
 | |
|             if cached:
 | |
|                 return cached
 | |
| 
 | |
|         url_info = urlparse(url)
 | |
| 
 | |
|         if not url_info.port:
 | |
|             port = 443 if url_info.scheme == 'https' else 80
 | |
|             hostname = url_info.netloc
 | |
|         else:
 | |
|             port = url_info.port
 | |
|             hostname = url_info.hostname
 | |
| 
 | |
|         path = url_info.path
 | |
|         if url_info.params:
 | |
|             path += ';' + url_info.params
 | |
|         if url_info.query:
 | |
|             path += '?' + url_info.query
 | |
| 
 | |
|         request_headers = {
 | |
|             'Accept-Encoding': 'gzip,deflate'
 | |
|         }
 | |
|         request_headers = self.add_conditional_headers(url, request_headers)
 | |
| 
 | |
|         created_connection = False
 | |
|         # If we switched Internet Explorer out of "Work Offline" mode
 | |
|         changed_to_online = False
 | |
| 
 | |
|         # If the user is requesting a connection to another server, close the connection
 | |
|         if (self.hostname and self.hostname != hostname) or (self.port and self.port != port):
 | |
|             self.close()
 | |
| 
 | |
|         # Reset the error info to a known clean state
 | |
|         ctypes.windll.kernel32.SetLastError(0)
 | |
| 
 | |
|         # Save the internet setup in the class for re-use
 | |
|         if not self.tcp_connection:
 | |
|             created_connection = True
 | |
| 
 | |
|             # Connect to the internet if necessary
 | |
|             state = self.read_option(None, self.INTERNET_OPTION_CONNECTED_STATE)
 | |
|             state = ord(state)
 | |
|             if state & self.INTERNET_STATE_DISCONNECTED or state & self.INTERNET_STATE_DISCONNECTED_BY_USER:
 | |
|                 # Track the previous state so we can go back once complete
 | |
|                 self.was_offline = True
 | |
| 
 | |
|                 dw_connected_state = wintypes.DWORD(self.INTERNET_STATE_CONNECTED)
 | |
|                 dw_flags = wintypes.DWORD(0)
 | |
|                 connected_info = InternetConnectedInfo(dw_connected_state, dw_flags)
 | |
|                 wininet.InternetSetOptionA(None,
 | |
|                     self.INTERNET_OPTION_CONNECTED_STATE, ctypes.byref(connected_info), ctypes.sizeof(connected_info))
 | |
|                 changed_to_online = True
 | |
| 
 | |
|             self.network_connection = wininet.InternetOpenW(self.settings.get('user_agent'),
 | |
|                 self.INTERNET_OPEN_TYPE_PRECONFIG, None, None, 0)
 | |
| 
 | |
|             if not self.network_connection:
 | |
|                 error_string = u'%s %s during network phase of downloading %s.' % (error_message, self.extract_error(), url)
 | |
|                 raise DownloaderException(error_string)
 | |
| 
 | |
|             win_timeout = wintypes.DWORD(int(timeout) * 1000)
 | |
|             # Apparently INTERNET_OPTION_CONNECT_TIMEOUT just doesn't work, leaving it in hoping they may fix in the future
 | |
|             wininet.InternetSetOptionA(self.network_connection,
 | |
|                 self.INTERNET_OPTION_CONNECT_TIMEOUT, win_timeout, ctypes.sizeof(win_timeout))
 | |
|             wininet.InternetSetOptionA(self.network_connection,
 | |
|                 self.INTERNET_OPTION_SEND_TIMEOUT, win_timeout, ctypes.sizeof(win_timeout))
 | |
|             wininet.InternetSetOptionA(self.network_connection,
 | |
|                 self.INTERNET_OPTION_RECEIVE_TIMEOUT, win_timeout, ctypes.sizeof(win_timeout))
 | |
| 
 | |
|             # Don't allow HTTPS sites to redirect to HTTP sites
 | |
|             tcp_flags  = self.INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS
 | |
|             # Try to re-use an existing connection to the server
 | |
|             tcp_flags |= self.INTERNET_FLAG_EXISTING_CONNECT
 | |
|             self.tcp_connection = wininet.InternetConnectW(self.network_connection,
 | |
|                 hostname, port, None, None, self.INTERNET_SERVICE_HTTP, tcp_flags, 0)
 | |
| 
 | |
|             if not self.tcp_connection:
 | |
|                 error_string = u'%s %s during connection phase of downloading %s.' % (error_message, self.extract_error(), url)
 | |
|                 raise DownloaderException(error_string)
 | |
| 
 | |
|             # Normally the proxy info would come from IE, but this allows storing it in
 | |
|             # the Package Control settings file.
 | |
|             proxy_username = self.settings.get('proxy_username')
 | |
|             proxy_password = self.settings.get('proxy_password')
 | |
|             if proxy_username and proxy_password:
 | |
|                 username = ctypes.c_wchar_p(proxy_username)
 | |
|                 password = ctypes.c_wchar_p(proxy_password)
 | |
|                 wininet.InternetSetOptionW(self.tcp_connection,
 | |
|                     self.INTERNET_OPTION_PROXY_USERNAME, ctypes.cast(username, ctypes.c_void_p), len(proxy_username))
 | |
|                 wininet.InternetSetOptionW(self.tcp_connection,
 | |
|                     self.INTERNET_OPTION_PROXY_PASSWORD, ctypes.cast(password, ctypes.c_void_p), len(proxy_password))
 | |
| 
 | |
|             self.hostname = hostname
 | |
|             self.port = port
 | |
|             self.scheme = url_info.scheme
 | |
| 
 | |
|         else:
 | |
|             if self.debug:
 | |
|                 console_write(u"WinINet %s Debug General" % self.scheme.upper(), True)
 | |
|                 console_write(u"  Re-using connection to %s on port %s for request #%s" % (
 | |
|                     self.hostname, self.port, self.use_count))
 | |
| 
 | |
|         error_string = None
 | |
|         while tries > 0:
 | |
|             tries -= 1
 | |
|             try:
 | |
|                 http_connection = None
 | |
| 
 | |
|                 # Keep-alive for better performance
 | |
|                 http_flags  = self.INTERNET_FLAG_KEEP_CONNECTION
 | |
|                 # Prevent caching/retrieving from cache
 | |
|                 http_flags |= self.INTERNET_FLAG_RELOAD
 | |
|                 http_flags |= self.INTERNET_FLAG_NO_CACHE_WRITE
 | |
|                 http_flags |= self.INTERNET_FLAG_PRAGMA_NOCACHE
 | |
|                 # Use SSL
 | |
|                 if self.scheme == 'https':
 | |
|                     http_flags |= self.INTERNET_FLAG_SECURE
 | |
| 
 | |
|                 http_connection = wininet.HttpOpenRequestW(self.tcp_connection, u'GET', path, u'HTTP/1.1', None, None, http_flags, 0)
 | |
|                 if not http_connection:
 | |
|                     error_string = u'%s %s during HTTP connection phase of downloading %s.' % (error_message, self.extract_error(), url)
 | |
|                     raise DownloaderException(error_string)
 | |
| 
 | |
|                 request_header_lines = []
 | |
|                 for header, value in request_headers.items():
 | |
|                     request_header_lines.append(u"%s: %s" % (header, value))
 | |
|                 request_header_lines = u"\r\n".join(request_header_lines)
 | |
| 
 | |
|                 success = wininet.HttpSendRequestW(http_connection, request_header_lines, len(request_header_lines), None, 0)
 | |
| 
 | |
|                 if not success:
 | |
|                     error_string = u'%s %s during HTTP write phase of downloading %s.' % (error_message, self.extract_error(), url)
 | |
|                     raise DownloaderException(error_string)
 | |
| 
 | |
|                 # If we try to query before here, the proxy info will not be available to the first request
 | |
|                 if self.debug:
 | |
|                     proxy_struct = self.read_option(self.network_connection, self.INTERNET_OPTION_PROXY)
 | |
|                     proxy = ''
 | |
|                     if proxy_struct.lpszProxy:
 | |
|                         proxy = proxy_struct.lpszProxy.decode('cp1252')
 | |
|                     proxy_bypass = ''
 | |
|                     if proxy_struct.lpszProxyBypass:
 | |
|                         proxy_bypass = proxy_struct.lpszProxyBypass.decode('cp1252')
 | |
| 
 | |
|                     proxy_username = self.read_option(self.tcp_connection, self.INTERNET_OPTION_PROXY_USERNAME)
 | |
|                     proxy_password = self.read_option(self.tcp_connection, self.INTERNET_OPTION_PROXY_PASSWORD)
 | |
| 
 | |
|                     console_write(u"WinINet Debug Proxy", True)
 | |
|                     console_write(u"  proxy: %s" % proxy)
 | |
|                     console_write(u"  proxy bypass: %s" % proxy_bypass)
 | |
|                     console_write(u"  proxy username: %s" % proxy_username)
 | |
|                     console_write(u"  proxy password: %s" % proxy_password)
 | |
| 
 | |
|                 self.use_count += 1
 | |
| 
 | |
|                 if self.debug and created_connection:
 | |
|                     if self.scheme == 'https':
 | |
|                         cert_struct = self.read_option(http_connection, self.INTERNET_OPTION_SECURITY_CERTIFICATE_STRUCT)
 | |
| 
 | |
|                         if cert_struct.lpszIssuerInfo:
 | |
|                             issuer_info = cert_struct.lpszIssuerInfo.decode('cp1252')
 | |
|                             issuer_parts = issuer_info.split("\r\n")
 | |
|                         else:
 | |
|                             issuer_parts = ['No issuer info']
 | |
| 
 | |
|                         if cert_struct.lpszSubjectInfo:
 | |
|                             subject_info = cert_struct.lpszSubjectInfo.decode('cp1252')
 | |
|                             subject_parts = subject_info.split("\r\n")
 | |
|                         else:
 | |
|                             subject_parts = ["No subject info"]
 | |
| 
 | |
|                         common_name = subject_parts[-1]
 | |
| 
 | |
|                         if cert_struct.ftStart.dwLowDateTime != 0 and cert_struct.ftStart.dwHighDateTime != 0:
 | |
|                             issue_date = self.convert_filetime_to_datetime(cert_struct.ftStart)
 | |
|                             issue_date = issue_date.strftime('%a, %d %b %Y %H:%M:%S GMT')
 | |
|                         else:
 | |
|                             issue_date = u"No issue date"
 | |
| 
 | |
|                         if cert_struct.ftExpiry.dwLowDateTime != 0 and cert_struct.ftExpiry.dwHighDateTime != 0:
 | |
|                             expiration_date = self.convert_filetime_to_datetime(cert_struct.ftExpiry)
 | |
|                             expiration_date = expiration_date.strftime('%a, %d %b %Y %H:%M:%S GMT')
 | |
|                         else:
 | |
|                             expiration_date = u"No expiration date"
 | |
| 
 | |
|                         console_write(u"WinINet HTTPS Debug General", True)
 | |
|                         if changed_to_online:
 | |
|                             console_write(u"  Internet Explorer was set to Work Offline, temporarily going online")
 | |
|                         console_write(u"  Server SSL Certificate:")
 | |
|                         console_write(u"    subject: %s" % ", ".join(subject_parts))
 | |
|                         console_write(u"    issuer: %s" % ", ".join(issuer_parts))
 | |
|                         console_write(u"    common name: %s" % common_name)
 | |
|                         console_write(u"    issue date: %s" % issue_date)
 | |
|                         console_write(u"    expire date: %s" % expiration_date)
 | |
| 
 | |
|                     elif changed_to_online:
 | |
|                         console_write(u"WinINet HTTP Debug General", True)
 | |
|                         console_write(u"  Internet Explorer was set to Work Offline, temporarily going online")
 | |
| 
 | |
|                 if self.debug:
 | |
|                     console_write(u"WinINet %s Debug Write" % self.scheme.upper(), True)
 | |
|                     # Add in some known headers that WinINet sends since we can't get the real list
 | |
|                     console_write(u"  GET %s HTTP/1.1" % path)
 | |
|                     for header, value in request_headers.items():
 | |
|                         console_write(u"  %s: %s" % (header, value))
 | |
|                     console_write(u"  User-Agent: %s" % self.settings.get('user_agent'))
 | |
|                     console_write(u"  Host: %s" % hostname)
 | |
|                     console_write(u"  Connection: Keep-Alive")
 | |
|                     console_write(u"  Cache-Control: no-cache")
 | |
| 
 | |
|                 header_buffer_size = 8192
 | |
| 
 | |
|                 try_again = True
 | |
|                 while try_again:
 | |
|                     try_again = False
 | |
| 
 | |
|                     to_read_was_read = wintypes.DWORD(header_buffer_size)
 | |
|                     headers_buffer = ctypes.create_string_buffer(header_buffer_size)
 | |
| 
 | |
|                     success = wininet.HttpQueryInfoA(http_connection, self.HTTP_QUERY_RAW_HEADERS_CRLF, ctypes.byref(headers_buffer), ctypes.byref(to_read_was_read), None)
 | |
|                     if not success:
 | |
|                         if ctypes.GetLastError() != self.ERROR_INSUFFICIENT_BUFFER:
 | |
|                             error_string = u'%s %s during header read phase of downloading %s.' % (error_message, self.extract_error(), url)
 | |
|                             raise DownloaderException(error_string)
 | |
|                         # The error was a buffer that was too small, so try again
 | |
|                         header_buffer_size = to_read_was_read.value
 | |
|                         try_again = True
 | |
|                         continue
 | |
| 
 | |
|                     headers = b''
 | |
|                     if to_read_was_read.value > 0:
 | |
|                         headers += headers_buffer.raw[:to_read_was_read.value]
 | |
|                     headers = headers.decode('iso-8859-1').rstrip("\r\n").split("\r\n")
 | |
| 
 | |
|                     if self.debug:
 | |
|                         console_write(u"WinINet %s Debug Read" % self.scheme.upper(), True)
 | |
|                         for header in headers:
 | |
|                             console_write(u"  %s" % header)
 | |
| 
 | |
|                 buffer_length = 65536
 | |
|                 output_buffer = ctypes.create_string_buffer(buffer_length)
 | |
|                 bytes_read = wintypes.DWORD()
 | |
| 
 | |
|                 result = b''
 | |
|                 try_again = True
 | |
|                 while try_again:
 | |
|                     try_again = False
 | |
|                     wininet.InternetReadFile(http_connection, output_buffer, buffer_length, ctypes.byref(bytes_read))
 | |
|                     if bytes_read.value > 0:
 | |
|                         result += output_buffer.raw[:bytes_read.value]
 | |
|                         try_again = True
 | |
| 
 | |
|                 general, headers = self.parse_headers(headers)
 | |
|                 self.handle_rate_limit(headers, url)
 | |
| 
 | |
|                 if general['status'] == 503 and tries != 0:
 | |
|                     # GitHub and BitBucket seem to rate limit via 503
 | |
|                     error_string = u'Downloading %s was rate limited' % url
 | |
|                     if tries:
 | |
|                         error_string += ', trying again'
 | |
|                         if self.debug:
 | |
|                             console_write(error_string, True)
 | |
|                     continue
 | |
| 
 | |
|                 encoding = headers.get('content-encoding')
 | |
|                 if encoding:
 | |
|                     result = self.decode_response(encoding, result)
 | |
| 
 | |
|                 result = self.cache_result('get', url, general['status'],
 | |
|                     headers, result)
 | |
| 
 | |
|                 if general['status'] not in [200, 304]:
 | |
|                     raise HttpError("HTTP error %s" % general['status'], general['status'])
 | |
| 
 | |
|                 return result
 | |
| 
 | |
|             except (NonHttpError, HttpError) as e:
 | |
| 
 | |
|                 # GitHub and BitBucket seem to time out a lot
 | |
|                 if str(e).find('timed out') != -1:
 | |
|                     error_string = u'Downloading %s timed out' % url
 | |
|                     if tries:
 | |
|                         error_string += ', trying again'
 | |
|                         if self.debug:
 | |
|                             console_write(error_string, True)
 | |
|                     continue
 | |
| 
 | |
|                 error_string = u'%s %s downloading %s.' % (error_message, e, url)
 | |
| 
 | |
|             finally:
 | |
|                 if http_connection:
 | |
|                     wininet.InternetCloseHandle(http_connection)
 | |
| 
 | |
|             break
 | |
| 
 | |
|         raise DownloaderException(error_string)
 | |
| 
 | |
|     def convert_filetime_to_datetime(self, filetime):
 | |
|         """
 | |
|         Windows returns times as 64-bit unsigned longs that are the number
 | |
|         of hundreds of nanoseconds since Jan 1 1601. This converts it to
 | |
|         a datetime object.
 | |
| 
 | |
|         :param filetime:
 | |
|             A FileTime struct object
 | |
| 
 | |
|         :return:
 | |
|             A (UTC) datetime object
 | |
|         """
 | |
| 
 | |
|         hundreds_nano_seconds = struct.unpack('>Q', struct.pack('>LL', filetime.dwHighDateTime, filetime.dwLowDateTime))[0]
 | |
|         seconds_since_1601 = hundreds_nano_seconds / 10000000
 | |
|         epoch_seconds = seconds_since_1601 - 11644473600 # Seconds from Jan 1 1601 to Jan 1 1970
 | |
|         return datetime.datetime.fromtimestamp(epoch_seconds)
 | |
| 
 | |
|     def extract_error(self):
 | |
|         """
 | |
|         Retrieves and formats an error from WinINet
 | |
| 
 | |
|         :return:
 | |
|             A string with a nice description of the error
 | |
|         """
 | |
| 
 | |
|         error_num = ctypes.GetLastError()
 | |
|         raw_error_string = ctypes.FormatError(error_num)
 | |
| 
 | |
|         error_string = unicode_from_os(raw_error_string)
 | |
| 
 | |
|         # Try to fill in some known errors
 | |
|         if error_string == u"<no description>":
 | |
|             error_lookup = {
 | |
|                 12007: u'host not found',
 | |
|                 12029: u'connection refused',
 | |
|                 12057: u'error checking for server certificate revocation',
 | |
|                 12169: u'invalid secure certificate',
 | |
|                 12157: u'secure channel error, server not providing SSL',
 | |
|                 12002: u'operation timed out'
 | |
|             }
 | |
|             if error_num in error_lookup:
 | |
|                 error_string = error_lookup[error_num]
 | |
| 
 | |
|         if error_string == u"<no description>":
 | |
|             return u"(errno %s)" % error_num
 | |
| 
 | |
|         error_string = error_string[0].upper() + error_string[1:]
 | |
|         return u"%s (errno %s)" % (error_string, error_num)
 | |
| 
 | |
|     def supports_ssl(self):
 | |
|         """
 | |
|         Indicates if the object can handle HTTPS requests
 | |
| 
 | |
|         :return:
 | |
|             If the object supports HTTPS requests
 | |
|         """
 | |
| 
 | |
|         return True
 | |
| 
 | |
|     def read_option(self, handle, option):
 | |
|         """
 | |
|         Reads information about the internet connection, which may be a string or struct
 | |
| 
 | |
|         :param handle:
 | |
|             The handle to query for the info
 | |
| 
 | |
|         :param option:
 | |
|             The (int) option to get
 | |
| 
 | |
|         :return:
 | |
|             A string, or one of the InternetCertificateInfo or InternetProxyInfo structs
 | |
|         """
 | |
| 
 | |
|         option_buffer_size = 8192
 | |
|         try_again = True
 | |
| 
 | |
|         while try_again:
 | |
|             try_again = False
 | |
| 
 | |
|             to_read_was_read = wintypes.DWORD(option_buffer_size)
 | |
|             option_buffer = ctypes.create_string_buffer(option_buffer_size)
 | |
|             ref = ctypes.byref(option_buffer)
 | |
| 
 | |
|             success = wininet.InternetQueryOptionA(handle, option, ref, ctypes.byref(to_read_was_read))
 | |
|             if not success:
 | |
|                 if ctypes.GetLastError() != self.ERROR_INSUFFICIENT_BUFFER:
 | |
|                     raise NonHttpError(self.extract_error())
 | |
|                 # The error was a buffer that was too small, so try again
 | |
|                 option_buffer_size = to_read_was_read.value
 | |
|                 try_again = True
 | |
|                 continue
 | |
| 
 | |
|             if option == self.INTERNET_OPTION_SECURITY_CERTIFICATE_STRUCT:
 | |
|                 length = min(len(option_buffer), ctypes.sizeof(InternetCertificateInfo))
 | |
|                 cert_info = InternetCertificateInfo()
 | |
|                 ctypes.memmove(ctypes.addressof(cert_info), option_buffer, length)
 | |
|                 return cert_info
 | |
|             elif option == self.INTERNET_OPTION_PROXY:
 | |
|                 length = min(len(option_buffer), ctypes.sizeof(InternetProxyInfo))
 | |
|                 proxy_info = InternetProxyInfo()
 | |
|                 ctypes.memmove(ctypes.addressof(proxy_info), option_buffer, length)
 | |
|                 return proxy_info
 | |
|             else:
 | |
|                 option = b''
 | |
|                 if to_read_was_read.value > 0:
 | |
|                     option += option_buffer.raw[:to_read_was_read.value]
 | |
|                 return option.decode('cp1252').rstrip("\x00")
 | |
| 
 | |
|     def parse_headers(self, output):
 | |
|         """
 | |
|         Parses HTTP headers into two dict objects
 | |
| 
 | |
|         :param output:
 | |
|             An array of header lines
 | |
| 
 | |
|         :return:
 | |
|             A tuple of (general, headers) where general is a dict with the keys:
 | |
|               `version` - HTTP version number (string)
 | |
|               `status` - HTTP status code (integer)
 | |
|               `message` - HTTP status message (string)
 | |
|             And headers is a dict with the keys being lower-case version of the
 | |
|             HTTP header names.
 | |
|         """
 | |
| 
 | |
|         general = {
 | |
|             'version': '0.9',
 | |
|             'status':  200,
 | |
|             'message': 'OK'
 | |
|         }
 | |
|         headers = {}
 | |
|         for line in output:
 | |
|             line = line.lstrip()
 | |
|             if line.find('HTTP/') == 0:
 | |
|                 match = re.match('HTTP/(\d\.\d)\s+(\d+)\s+(.*)$', line)
 | |
|                 general['version'] = match.group(1)
 | |
|                 general['status'] = int(match.group(2))
 | |
|                 general['message'] = match.group(3)
 | |
|             else:
 | |
|                 name, value = line.split(':', 1)
 | |
|                 headers[name.lower()] = value.strip()
 | |
| 
 | |
|         return (general, headers)
 | |
| 
 | |
| 
 | |
| class FileTime(ctypes.Structure):
 | |
|     """
 | |
|     A Windows struct used by InternetCertificateInfo for certificate
 | |
|     date information
 | |
|     """
 | |
| 
 | |
|     _fields_ = [
 | |
|         ("dwLowDateTime", wintypes.DWORD),
 | |
|         ("dwHighDateTime", wintypes.DWORD)
 | |
|     ]
 | |
| 
 | |
| 
 | |
| class InternetCertificateInfo(ctypes.Structure):
 | |
|     """
 | |
|     A Windows struct used to store information about an SSL certificate
 | |
|     """
 | |
| 
 | |
|     _fields_ = [
 | |
|         ("ftExpiry", FileTime),
 | |
|         ("ftStart", FileTime),
 | |
|         ("lpszSubjectInfo", ctypes.c_char_p),
 | |
|         ("lpszIssuerInfo", ctypes.c_char_p),
 | |
|         ("lpszProtocolName", ctypes.c_char_p),
 | |
|         ("lpszSignatureAlgName", ctypes.c_char_p),
 | |
|         ("lpszEncryptionAlgName", ctypes.c_char_p),
 | |
|         ("dwKeySize", wintypes.DWORD)
 | |
|     ]
 | |
| 
 | |
| 
 | |
| class InternetProxyInfo(ctypes.Structure):
 | |
|     """
 | |
|     A Windows struct usd to store information about the configured proxy server
 | |
|     """
 | |
| 
 | |
|     _fields_ = [
 | |
|         ("dwAccessType", wintypes.DWORD),
 | |
|         ("lpszProxy", ctypes.c_char_p),
 | |
|         ("lpszProxyBypass", ctypes.c_char_p)
 | |
|     ]
 | |
| 
 | |
| 
 | |
| class InternetConnectedInfo(ctypes.Structure):
 | |
|     """
 | |
|     A Windows struct usd to store information about the global internet connection state
 | |
|     """
 | |
| 
 | |
|     _fields_ = [
 | |
|         ("dwConnectedState", wintypes.DWORD),
 | |
|         ("dwFlags", wintypes.DWORD)
 | |
|     ]
 |