Skip to content

Commit

Permalink
Merge pull request achillean#73 from wagner-certat/proxy
Browse files Browse the repository at this point in the history
Add proxy parameter
  • Loading branch information
achillean authored Aug 1, 2018
2 parents 6d755dd + bd1fb89 commit 9f256d1
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 67 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
CHANGELOG
=========

unreleased
----------
* New optional parameter `proxies` for all interfaces to specify a proxy array for the requests library (#72)

1.8.1
-----
* Fixed bug that prevented **shodan scan submit** from finishing (#70)
Expand Down
4 changes: 2 additions & 2 deletions shodan/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ def host(format, history, filename, save, ip):
for banner in host['data']:
if banner['port'] in ports:
ports.remove(banner['port'])

# Add the placeholder banners
for port in ports:
banner = {
Expand Down Expand Up @@ -1013,7 +1013,7 @@ def search(color, fields, limit, separator, query):
results = api.search(query, limit=limit)
except shodan.APIError as e:
raise click.ClickException(e.value)

# Error out if no results were found
if results['total'] == 0:
raise click.ClickException('No search results found')
Expand Down
87 changes: 47 additions & 40 deletions shodan/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class Shodan:
:ivar exploits: An instance of `shodan.Shodan.Exploits` that provides access to the Exploits REST API.
:ivar stream: An instance of `shodan.Shodan.Stream` that provides access to the Streaming API.
"""

class Data:

def __init__(self, parent):
Expand All @@ -62,7 +62,7 @@ def list_files(self, dataset):
:returns: A list of objects where each object contains a 'name', 'size', 'timestamp' and 'url'
"""
return self.parent._request('/shodan/data/{}'.format(dataset), {})

class Tools:

def __init__(self, parent):
Expand All @@ -74,16 +74,16 @@ def myip(self):
:returns: str -- your IP address
"""
return self.parent._request('/tools/myip', {})

class Exploits:

def __init__(self, parent):
self.parent = parent

def search(self, query, page=1, facets=None):
"""Search the entire Shodan Exploits archive using the same query syntax
as the website.
:param query: The exploit search query; same syntax as website.
:type query: str
:param facets: A list of strings or tuples to get summary information on.
Expand All @@ -100,17 +100,17 @@ def search(self, query, page=1, facets=None):
query_args['facets'] = create_facet_string(facets)

return self.parent._request('/api/search', query_args, service='exploits')

def count(self, query, facets=None):
"""Search the entire Shodan Exploits archive but only return the total # of results,
not the actual exploits.
:param query: The exploit search query; same syntax as website.
:type query: str
:param facets: A list of strings or tuples to get summary information on.
:type facets: str
:returns: dict -- a dictionary containing the results of the search.
"""
query_args = {
'query': query,
Expand All @@ -119,27 +119,29 @@ def count(self, query, facets=None):
query_args['facets'] = create_facet_string(facets)

return self.parent._request('/api/count', query_args, service='exploits')

class Labs:

def __init__(self, parent):
self.parent = parent

def honeyscore(self, ip):
"""Calculate the probability of an IP being an ICS honeypot.
:param ip: IP address of the device
:type ip: str
:returns: int -- honeyscore ranging from 0.0 to 1.0
"""
return self.parent._request('/labs/honeyscore/{}'.format(ip), {})
def __init__(self, key):

def __init__(self, key, proxies=None):
"""Initializes the API object.
:param key: The Shodan API key.
:type key: str
:param proxies: A proxies array for the requests library, e.g. {'https': 'your proxy'}
:type key: dict
"""
self.api_key = key
self.base_url = 'https://api.shodan.io'
Expand All @@ -148,23 +150,25 @@ def __init__(self, key):
self.exploits = self.Exploits(self)
self.labs = self.Labs(self)
self.tools = self.Tools(self)
self.stream = Stream(key)
self.stream = Stream(key, proxies=proxies)
self._session = requests.Session()

if proxies:
self._session.proxies.update(proxies)

def _request(self, function, params, service='shodan', method='get'):
"""General-purpose function to create web requests to SHODAN.
Arguments:
function -- name of the function you want to execute
params -- dictionary of parameters for the function
Returns
A dictionary containing the function's results.
"""
# Add the API key parameter automatically
params['key'] = self.api_key

# Determine the base_url based on which service we're interacting with
base_url = {
'shodan': self.base_url,
Expand All @@ -187,30 +191,30 @@ def _request(self, function, params, service='shodan', method='get'):
error = data.json()['error']
except Exception as e:
error = 'Invalid API key'

raise APIError(error)

# Parse the text into JSON
try:
data = data.json()
except:
raise APIError('Unable to parse JSON response')

# Raise an exception if an error occurred
if type(data) == dict and 'error' in data:
raise APIError(data['error'])

# Return the data
return data

def count(self, query, facets=None):
"""Returns the total number of search results for the query.
:param query: Search query; identical syntax to the website
:type query: str
:param facets: (optional) A list of properties to get summary information on
:type facets: str
:returns: A dictionary with 1 main property: total. If facets have been provided then another property called "facets" will be available at the top-level of the dictionary. Visit the website for more detailed information.
"""
query_args = {
Expand All @@ -219,7 +223,7 @@ def count(self, query, facets=None):
if facets:
query_args['facets'] = create_facet_string(facets)
return self._request('/shodan/host/count', query_args)

def host(self, ips, history=False, minify=False):
"""Get all available information on an IP.
Expand All @@ -232,14 +236,14 @@ def host(self, ips, history=False, minify=False):
"""
if isinstance(ips, basestring):
ips = [ips]

params = {}
if history:
params['history'] = history
if minify:
params['minify'] = minify
return self._request('/shodan/host/%s' % ','.join(ips), params)

def info(self):
"""Returns information about the current API key, such as a list of add-ons
and other features that are enabled for the current user's API plan.
Expand Down Expand Up @@ -281,7 +285,7 @@ def scan(self, ips, force=False):
"""
if isinstance(ips, basestring):
ips = [ips]

if isinstance(ips, dict):
networks = json.dumps(ips)
else:
Expand Down Expand Up @@ -320,7 +324,7 @@ def scan_status(self, scan_id):
:returns: A dictionary with general information about the scan, including its status in getting processed.
"""
return self._request('/shodan/scan/%s' % scan_id, {})

def search(self, query, page=1, limit=None, offset=None, facets=None, minify=True):
"""Search the SHODAN database.
Expand All @@ -336,8 +340,8 @@ def search(self, query, page=1, limit=None, offset=None, facets=None, minify=Tru
:type facets: str
:param minify: (optional) Whether to minify the banner and only return the important data
:type minify: bool
:returns: A dictionary with 2 main items: matches and total. If facets have been provided then another property called "facets" will be available at the top-level of the dictionary. Visit the website for more detailed information.
:returns: A dictionary with 2 main items: matches and total. If facets have been provided then another property called "facets" will be available at the top-level of the dictionary. Visit the website for more detailed information.
"""
args = {
'query': query,
Expand All @@ -352,9 +356,9 @@ def search(self, query, page=1, limit=None, offset=None, facets=None, minify=Tru

if facets:
args['facets'] = create_facet_string(facets)

return self._request('/shodan/host/search', args)

def search_cursor(self, query, minify=True, retries=5):
"""Search the SHODAN database.
Expand All @@ -369,7 +373,7 @@ def search_cursor(self, query, minify=True, retries=5):
:type minify: bool
:param retries: (optional) How often to retry the search in case it times out
:type minify: int
:returns: A search cursor that can be used as an iterator/ generator.
"""
args = {
Expand All @@ -396,13 +400,13 @@ def search_cursor(self, query, minify=True, retries=5):

tries += 1
time.sleep(1.0) # wait 1 second if the search errored out for some reason

def search_tokens(self, query):
"""Returns information about the search query itself (filters used etc.)
:param query: Search query; identical syntax to the website
:type query: str
:returns: A dictionary with 4 main properties: filters, errors, attributes and string.
"""
query_args = {
Expand Down Expand Up @@ -481,7 +485,8 @@ def create_alert(self, name, ip, expires=0):
'expires': expires,
}

response = api_request(self.api_key, '/shodan/alert', data=data, params={}, method='post')
response = api_request(self.api_key, '/shodan/alert', data=data, params={}, method='post',
proxies=self._session.proxies)

return response

Expand All @@ -494,15 +499,17 @@ def alerts(self, aid=None, include_expired=True):

response = api_request(self.api_key, func, params={
'include_expired': include_expired,
})
},
proxies=self._session.proxies)

return response

def delete_alert(self, aid):
"""Delete the alert with the given ID."""
func = '/shodan/alert/%s' % aid

response = api_request(self.api_key, func, params={}, method='delete')
response = api_request(self.api_key, func, params={}, method='delete',
proxies=self._session.proxies)

return response

32 changes: 18 additions & 14 deletions shodan/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@ def create_facet_string(facets):
facet_str += ','
return facet_str[:-1]


def api_request(key, function, params=None, data=None, base_url='https://api.shodan.io', method='get', retries=1):

def api_request(key, function, params=None, data=None, base_url='https://api.shodan.io',
method='get', retries=1, proxies=None):
"""General-purpose function to create web requests to SHODAN.
Arguments:
function -- name of the function you want to execute
params -- dictionary of parameters for the function
proxies -- a proxies array for the requests library
Returns
A dictionary containing the function's results.
"""
# Add the API key parameter automatically
params['key'] = key
Expand All @@ -44,11 +46,13 @@ def api_request(key, function, params=None, data=None, base_url='https://api.sho
while tries <= retries:
try:
if method.lower() == 'post':
data = requests.post(base_url + function, json.dumps(data), params=params, headers={'content-type': 'application/json'})
data = requests.post(base_url + function, json.dumps(data), params=params,
headers={'content-type': 'application/json'},
proxies=proxies)
elif method.lower() == 'delete':
data = requests.delete(base_url + function, params=params)
data = requests.delete(base_url + function, params=params, proxies=proxies)
else:
data = requests.get(base_url + function, params=params)
data = requests.get(base_url + function, params=params, proxies=proxies)

# Exit out of the loop
break
Expand All @@ -66,17 +70,17 @@ def api_request(key, function, params=None, data=None, base_url='https://api.sho
except:
pass
raise APIError('Invalid API key')

# Parse the text into JSON
try:
data = data.json()
except:
raise APIError('Unable to parse JSON response')

# Raise an exception if an error occurred
if type(data) == dict and data.get('error', None):
raise APIError(data['error'])

# Return the data
return data

Expand All @@ -93,10 +97,10 @@ def iterate_files(files, fast=False):
from ujson import loads
except:
pass

if isinstance(files, basestring):
files = [files]

for filename in files:
# Create a file handle depending on the filetype
if filename.endswith('.gz'):
Expand Down Expand Up @@ -157,7 +161,7 @@ def humanize_bytes(bytes, precision=1):
return '1 byte'
if bytes < 1024:
return '%.*f %s' % (precision, bytes, "bytes")

suffixes = ['KB', 'MB', 'GB', 'TB', 'PB']
multiple = 1024.0 #.0 force float on python 2
for suffix in suffixes:
Expand Down
Loading

0 comments on commit 9f256d1

Please sign in to comment.