import errno
import urllib2
import urlparse
import httplib
import logging
import socket

logger = logging.getLogger(__name__)

#
# We use NSS due to missing SSL SNI support in OpenSSL library on CentOS/RHEL 5.
# It's required to successfully check windows 2012 IIS domains from CentOS/RHEL 5 PPA MN.
#
nss_imported = False
try:
	import nss.io as io
	import nss.nss as nss
	import nss.ssl as ssl
	nss_imported = True
except ImportError:
	logger.debug(u'Unable to import NSS. We will use OpenSSL')
	import ssl

nss_is_initialized = False

class HttpClient(object):
	def __init__(self, website_availability_check_timeout):
		self.website_availability_check_timeout = website_availability_check_timeout

	def get(self, url, server_ip=None):
		"""Make an HTTP GET request.

		Args:
			url: request URL
			server_ip: Web server IP, for which the request will be sent. Host
			name is passed in HTTP 'Host' request header.
		"""
		return self._request(url, server_ip)

	def _request(self, url, server_ip):
		"""Make an HTTP request using the given server_ip."""
		request = self._get_request_object(url, server_ip)

		socket.setdefaulttimeout(int(self.website_availability_check_timeout))
		if nss_imported:
			opener = urllib2.build_opener(NoRedirection, HTTPSHandler)
			response = opener.open(request)
		else:
			if hasattr(ssl, "_create_unverified_context"):
				# Special handling for Python >= 2.7.9, which got enabled SSL certificate verification by default.
				# Checking for attribute is required, because old versions of Python does not have such
				# internal method and don't know about contexts, and don't have such check by default.

				# As site's certificates are often not valid, we should not check them.
				# Just do HTTPS request ignoring all certificate validity issues.
				unverified_context = ssl._create_unverified_context()

				# Custom HTTPS connection and handler are in-place patch to make SNI requests to
				# Apache work. We replace hostname specified in SNI request here. Otherwise, Apache will respond
				# with 400 HTTP code, which means "Host" header and SNI server hostname mismatch.
				_, server_hostname = self._get_vhost_request(url, server_ip)
				server_hostname = server_hostname.lower()

				class CustomHTTPSConnection(httplib.HTTPSConnection):
					def connect(self):
						httplib.HTTPConnection.connect(self)
						self.sock = self._context.wrap_socket(self.sock, server_hostname=server_hostname)

				class CustomHTTPSHandler(HTTPSHandler):
					def https_open(self, req):
						return self.do_open(CustomHTTPSConnection, req, context=self._context)

				opener = urllib2.build_opener(
					NoRedirection,
					CustomHTTPSHandler(context=unverified_context)
				)
			else:
				opener = urllib2.build_opener(NoRedirection, urllib2.HTTPSHandler())

			try:
				response = opener.open(request)
			except urllib2.URLError:
				# Workaround for https://jira.sw.ru/browse/PMT-147 (
				# Unable to check HTTPS on site hosting on Windows 2012 if
				# migration checker run on Windows)
				url = request.get_full_url()
				if self._get_protocol(url) == 'https':
					logger.debug(u'Exception:', exc_info=True)
					logger.warning(u'Unable to make HTTPS request "%s".' % url)
					return HttpResponse.dummy(code=200, body='')
				else:
					raise
		return HttpResponse.parse(response, remote_ip=server_ip)

	def _get_request_object(self, url, server_ip):
		"""Construct urllib2.Request object."""
		if server_ip:
			(url, header_host) = self._get_vhost_request(url, server_ip)
			request = urllib2.Request(url)
			request.add_header('Host', header_host)
			logger.debug("HTTP request: curl -H 'Host: %s' '%s'" % (header_host, url))
		else:
			request = urllib2.Request(url)
			logger.debug("HTTP request: curl '%s'" % (url,))

		return request

	def _get_vhost_request(self, url, server_ip):
		"""Construct HTTP request for a name-based virtual host.

		Replace domain name in the URL with server_ip; specify domain name in
		HTTP header 'Host'.
		"""
		if self._is_ipv6(server_ip):
			request_host = '[%s]' % server_ip
		else:
			request_host = server_ip
		parsed_url = list(urlparse.urlparse(url))
		header_host = parsed_url[1].encode('idna')
		hostport = parsed_url[1].split(':') # example.com:80 -> ['example.com', 80]
		if len(hostport) > 1:
			# use port number specified in URL
			parsed_url[1] = "%s:%s" %(request_host, hostport[1])
		else:
			parsed_url[1] = request_host
		url = urlparse.urlunparse(parsed_url)
		return (url, header_host)

	def _get_protocol(self, url):
		"""Return protocol part of the URL."""
		urlparts = urlparse.urlparse(url)
		return urlparts[0].lower() 

	@staticmethod
	def _is_ipv6(string):
		# We cannot use socket.inet_pton because it is not available on Windows
		# and may be some Unix OSes
		# https://docs.python.org/2/library/socket.html#socket.inet_pton
		# Availability: Unix (maybe not all platforms).
		return ':' in string

class HTTPSHandler(urllib2.HTTPSHandler):
	def https_open(self, req):
		return self.do_open(NSSConnection, req)

class NoRedirection(urllib2.HTTPErrorProcessor):
	def http_response(self, request, response):
		return response
	https_response = http_response

class HttpResponse(object):
	"""HTTP server response that includes HTTP headers and data.

	This is an interface to response object returned by urllib2
	HTTP request.
	"""
	def __init__(self, data, body=None, remote_ip=None):
		"""Construct HttpResponse object from an existing HTTP response.

		Args:
			data: Response object, as returned by urllib2.
			body: Response body
			remote_ip: IP address of the server
		"""
		self.data = data
		self.body = body
		self.remote_ip = remote_ip

	@staticmethod
	def parse(response, remote_ip=None):
		"""Create a response object based on 'urlopen()' returned value."""
		return HttpResponse(
			response, body=response.read(), remote_ip=remote_ip
		) 

	@staticmethod
	def dummy(code=200, body=None, headers=None):
		"""Create an empty HttpResponse with status code 200."""
		class DummyResponseData(object):
			code = None
		response = DummyResponseData()
		response.code = code
		response.headers = headers
		http_response = HttpResponse(response)
		if body is not None:
			http_response.body = body
		else:
			http_response.body = ''
		return http_response

	def header(self, header):
		"""Return content of response header."""
		if self.data:
			return self.data.headers.getheader(header)
		else:
			return None

	@property
	def code(self):
		if self.data:
			return self.data.code
		else:
			return None

	@property
	def code_class(self):
		"""Return the first digit of HTTP status code (1xx .. 5xx)."""
		if self.code:
			return self.code // 100

class NSSConnection(httplib.HTTPSConnection):
	NSS_CERT_DB = '/etc/pki/nssdb'
	SOCKET_TIMEOUT = 30

	def __init__(self, host, port=None, strict=None, db_dir=NSS_CERT_DB, **kwargs):
		httplib.HTTPSConnection.__init__(self, host, port, strict, **kwargs)

		if not db_dir:
			raise RuntimeError('db_dir is required')

		if not nss_is_initialized:
			nss.nss_init(db_dir)

		self.sock = None
		self._hostname = self.host
		ssl.set_domestic_policy()

	def connect(self):
		addr_info = io.AddrInfo(self.host)
		for net_addr in addr_info:
			net_addr.port = self.port
			self._create_socket(net_addr.family)
			try:
				self.sock.connect(net_addr, timeout=io.seconds_to_interval(self.SOCKET_TIMEOUT))
				return
			except Exception, e:
				logger.debug(u'Exception:', exc_info=e)

		raise IOError(errno.ENOTCONN, u'Could not connect to %s at port %d' % (self.host, self.port))

	def _create_socket(self, family):
		# We accept any server's SSL certificate
		def auth_certificate_callback(sock, check_sig, is_server, certdb):
			return True

		# We do not use client SSL certificates
		def client_auth_data_callback(ca_names, chosen_nickname, password, certdb):
			return False

		self.sock = ssl.SSLSocket(family)
		self.sock.set_ssl_option(ssl.SSL_SECURITY, True)
		self.sock.set_ssl_option(ssl.SSL_HANDSHAKE_AS_CLIENT, True)
		self.sock.set_hostname(self._hostname)

		self.sock.set_auth_certificate_callback(auth_certificate_callback, nss.get_default_certdb())
		self.sock.set_client_auth_data_callback(client_auth_data_callback, '', '', nss.get_default_certdb())

	def _send_request(self, method, url, body, headers):
		host_header_value = [v for k, v in headers.iteritems() if k.lower() == 'host']
		if host_header_value:
			self._hostname = host_header_value[0]

		httplib.HTTPSConnection._send_request(self, method, url, body, headers)
