# Source code for binaryninja.downloadprovider

    # Copyright (c) 2015-2018 Vector 35 LLC
    #
    # Permission is hereby granted, free of charge, to any person obtaining a copy
    # of this software and associated documentation files (the "Software"), to
    # deal in the Software without restriction, including without limitation the
    # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    # sell copies of the Software, and to permit persons to whom the Software is
    # furnished to do so, subject to the following conditions:
    #
    # The above copyright notice and this permission notice shall be included in
    # all copies or substantial portions of the Software.
    #
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    # IN THE SOFTWARE.

    import abc
    import ctypes
    import sys
    import traceback

    # Binary Ninja Components
    import binaryninja._binaryninjacore as core
    from binaryninja.setting import Setting
    from binaryninja import with_metaclass
    from binaryninja import startup
    from binaryninja import log

    # 2-3 compatibility
    from binaryninja import pyNativeStr

    class DownloadInstance(object):
    	def __init__(self, provider, handle = None):
    		if handle is None:
    			self._cb = core.BNDownloadInstanceCallbacks()
    			self._cb.context = 0
    			self._cb.destroyInstance = self._cb.destroyInstance.__class__(self._destroy_instance)
    			self._cb.performRequest = self._cb.performRequest.__class__(self._perform_request)
    			self.handle = core.BNInitDownloadInstance(provider.handle, self._cb)
    		else:
    			self.handle = core.handle_of_type(handle, core.BNDownloadInstance)
    		self._outputCallbacks = None

    	def __del__(self):
    		core.BNFreeDownloadInstance(self.handle)

    	def _destroy_instance(self, ctxt):
    		try:
    			self.perform_destroy_instance()
    		except:
    			log.log_error(traceback.format_exc())

    	def _perform_request(self, ctxt, url):
    		try:
    			return self.perform_request(url)
    		except:
    			log.log_error(traceback.format_exc())
    			return -1

    	@abc.abstractmethod
    	def perform_destroy_instance(self):
    		raise NotImplementedError

    	@abc.abstractmethod
    	def perform_request(self, ctxt, url):
    		raise NotImplementedError

    	def perform_request(self, url, callbacks):
    		return core.BNPerformDownloadRequest(self.handle, url, callbacks)

    class _DownloadProviderMetaclass(type):
    	@property
    	def list(self):
    		"""List all DownloadProvider types (read-only)"""
    		startup._init_plugins()
    		count = ctypes.c_ulonglong()
    		types = core.BNGetDownloadProviderList(count)
    		result = []
    		for i in xrange(0, count.value):
    			result.append(DownloadProvider(types[i]))
    		core.BNFreeDownloadProviderList(types)
    		return result

    	def __iter__(self):
    		startup._init_plugins()
    		count = ctypes.c_ulonglong()
    		types = core.BNGetDownloadProviderList(count)
    		try:
    			for i in xrange(0, count.value):
    				yield DownloadProvider(types[i])
    		finally:
    			core.BNFreeDownloadProviderList(types)

    	def __getitem__(self, value):
    		startup._init_plugins()
    		provider = core.BNGetDownloadProviderByName(str(value))
    		if provider is None:
    			raise KeyError("'%s' is not a valid download provider" % str(value))
    		return DownloadProvider(provider)

    	def __setattr__(self, name, value):
    		try:
    			type.__setattr__(self, name, value)
    		except AttributeError:
    			raise AttributeError("attribute '%s' is read only" % name)

    class DownloadProvider(with_metaclass(_DownloadProviderMetaclass, object)):
    	name = None
    	instance_class = None
    	_registered_providers = []

    	def __init__(self, handle = None):
    		if handle is not None:
    			self.handle = core.handle_of_type(handle, core.BNDownloadProvider)
    			self.__dict__["name"] = core.BNGetDownloadProviderName(handle)

    	def register(self):
    		self._cb = core.BNDownloadProviderCallbacks()
    		self._cb.context = 0
    		self._cb.createInstance = self._cb.createInstance.__class__(self._create_instance)
    		self.handle = core.BNRegisterDownloadProvider(self.__class__.name, self._cb)
    		self.__class__._registered_providers.append(self)

    	def _create_instance(self, ctxt):
    		try:
    			result = self.__class__.instance_class(self)
    			if result is None:
    				return None
    			return ctypes.cast(core.BNNewDownloadInstanceReference(result.handle), ctypes.c_void_p).value
    		except:
    			log.log_error(traceback.format_exc())
    			return None

    	def create_instance(self):
    		result = core.BNCreateDownloadProviderInstance(self.handle)
    		if result is None:
    			return None
    		return DownloadInstance(self, handle = result)

    if sys.version_info >= (2, 7, 9):
    	try:
    		from urllib.request import urlopen, build_opener, install_opener, ProxyHandler
    		from urllib.error import URLError
    	except ImportError:
    		from urllib2 import urlopen, build_opener, install_opener, ProxyHandler, URLError

    	class PythonDownloadInstance(DownloadInstance):
    		def __init__(self, provider):
    			super(PythonDownloadInstance, self).__init__(provider)

    		@abc.abstractmethod
    		def perform_destroy_instance(self):
    			pass

    		@abc.abstractmethod
    		def perform_request(self, url):
    			try:
    				proxy_setting = Setting('download-client').get_string('https-proxy')
    				if proxy_setting:
    					opener = build_opener(ProxyHandler({'https': proxy_setting}))
    					install_opener(opener)

    				r = urlopen(pyNativeStr(url))
    				total_size = int(r.headers.get('content-length', 0))
    				bytes_sent = 0
    				while True:
    					data = r.read(4096)
    					if not data:
    						break
    					raw_bytes = (ctypes.c_ubyte * len(data)).from_buffer_copy(data)
    					bytes_wrote = core.BNWriteDataForDownloadInstance(self.handle, raw_bytes, len(raw_bytes))
    					if bytes_wrote != len(raw_bytes):
    						core.BNSetErrorForDownloadInstance(self.handle, "Bytes written mismatch!")
    						return -1
    					bytes_sent = bytes_sent + bytes_wrote
    					continue_download = core.BNNotifyProgressForDownloadInstance(self.handle, bytes_sent, total_size)
    					if continue_download is False:
    						core.BNSetErrorForDownloadInstance(self.handle, "Download aborted!")
    						return -1

    				if not bytes_sent:
    					core.BNSetErrorForDownloadInstance(self.handle, "Received no data!")
    					return -1

    			except URLError as e:
    				core.BNSetErrorForDownloadInstance(self.handle, e.__class__.__name__)
    				return -1
    			except:
    				core.BNSetErrorForDownloadInstance(self.handle, "Unknown Exception!")
    				log.log_error(traceback.format_exc())
    				return -1

    			return 0

    	class PythonDownloadProvider(DownloadProvider):
    		name = "DefaultDownloadProvider"
    		instance_class = PythonDownloadInstance

    	PythonDownloadProvider().register()
    else:
    	try:
    		import requests
    		from requests import pyopenssl
    		class PythonDownloadInstance(DownloadInstance):
    			def __init__(self, provider):
    				super(PythonDownloadInstance, self).__init__(provider)

    			@abc.abstractmethod
    			def perform_destroy_instance(self):
    				pass

    			@abc.abstractmethod
    			def perform_request(self, url):
    				try:
    					proxy_setting = Setting('download-client').get_string('https-proxy')
    					if proxy_setting:
    						proxies = {"https": proxy_setting}
    					else:
    						proxies = None

    					r = requests.get(pyNativeStr(url), proxies=proxies)
    					if not r.ok:
    						core.BNSetErrorForDownloadInstance(self.handle, "Received error from server")
    						return -1
    					data = r.content
    					if len(data) == 0:
    						core.BNSetErrorForDownloadInstance(self.handle, "No data received from server!")
    						return -1
    					raw_bytes = (ctypes.c_ubyte * len(data)).from_buffer_copy(data)
    					bytes_wrote = core.BNWriteDataForDownloadInstance(self.handle, raw_bytes, len(raw_bytes))
    					if bytes_wrote != len(raw_bytes):
    						core.BNSetErrorForDownloadInstance(self.handle, "Bytes written mismatch!")
    						return -1
    					continue_download = core.BNNotifyProgressForDownloadInstance(self.handle, bytes_wrote, bytes_wrote)
    					if continue_download is False:
    						core.BNSetErrorForDownloadInstance(self.handle, "Download aborted!")
    						return -1
    				except requests.RequestException as e:
    					core.BNSetErrorForDownloadInstance(self.handle, e.__class__.__name__)
    					return -1
    				except:
    					core.BNSetErrorForDownloadInstance(self.handle, "Unknown Exception!")
    					log.log_error(traceback.format_exc())
    					return -1

    				return 0

    		class PythonDownloadProvider(DownloadProvider):
    			name = "DefaultDownloadProvider"
    			instance_class = PythonDownloadInstance

    		PythonDownloadProvider().register()
    	except ImportError:
    		if sys.platform == "win32":
    			log.log_error("Provided Python version is too old! Only Python 2.7.10 and above are known to work on Windows!")
    		else:
    			log.log_error("On Python versions below 2.7.9, the pip requests[security] package is required for network connectivity!")
    			log.log_error("On an Ubuntu 14.04 install, the following three commands are sufficient to enable networking for the current user:")
    			log.log_error("  sudo apt install python-pip")
    			log.log_error("  python -m pip install pip --upgrade --user")
    			log.log_error("  python -m pip install requests[security] --upgrade --user")