Skip to content

Commit aab9648

Browse files
committed
Improved Python3 support
1 parent cd369cd commit aab9648

2 files changed

Lines changed: 53 additions & 33 deletions

File tree

bin/shodan

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ COLORIZE_FIELDS = {
4949
# Make "-h" work like "--help"
5050
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
5151

52+
# Define a basestring type if necessary for Python3 compatibility
53+
try:
54+
basestring
55+
except NameError:
56+
basestring = str
57+
5258
# Utility methods
5359
def get_api_key():
5460
shodan_dir = os.path.expanduser(SHODAN_CONFIG_DIR)
@@ -60,7 +66,7 @@ def get_api_key():
6066
raise click.ClickException('Please run "shodan init <api key>" before using this command')
6167

6268
# Make sure it is a read-only file
63-
os.chmod(keyfile, 0600)
69+
os.chmod(keyfile, 0o600)
6470

6571
with open(keyfile, 'r') as fin:
6672
return fin.read().strip()
@@ -74,7 +80,10 @@ def get_ip(banner):
7480

7581

7682
def escape_data(args):
77-
return args.encode('ascii', 'replace').replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t')
83+
# Ensure the provided string isn't unicode data
84+
if not isinstance(args, str):
85+
args = args.encode('ascii', 'replace')
86+
return args.replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t')
7887

7988
def timestr():
8089
return datetime.datetime.utcnow().strftime('%Y-%m-%d')
@@ -133,6 +142,10 @@ def match_filters(banner, filters):
133142

134143
return True
135144

145+
def write_banner(fout, banner):
146+
line = simplejson.dumps(banner) + '\n'
147+
fout.write(line.encode('utf-8'))
148+
136149

137150
@click.group(context_settings=CONTEXT_SETTINGS)
138151
def main():
@@ -242,8 +255,8 @@ def convert(input, format):
242255
placemark += '<Point><coordinates>{},{}</coordinates></Point>'.format(lon, lat)
243256
placemark += '</Placemark>'
244257

245-
fout.write(placemark)
246-
except Exception, e:
258+
fout.write(placemark.encode('utf-8'))
259+
except Exception as e:
247260
pass
248261

249262
# Get the basename for the input file
@@ -274,7 +287,7 @@ def convert(input, format):
274287

275288
hosts[ip]['ports'].append(banner['port'])
276289

277-
for ip, host in hosts.iteritems():
290+
for ip, host in iter(hosts.items()):
278291
kml_writer(fout, host)
279292

280293
fout.write(KML_FOOTER)
@@ -302,7 +315,7 @@ def init(key):
302315
try:
303316
api = shodan.Shodan(key)
304317
test = api.info()
305-
except shodan.APIError, e:
318+
except shodan.APIError as e:
306319
raise click.ClickException('Invalid API key')
307320

308321
# Store the API key in the user's directory
@@ -311,7 +324,7 @@ def init(key):
311324
fout.write(key.strip())
312325
click.echo(click.style('Successfully initialized', fg='green'))
313326

314-
os.chmod(keyfile, 0600)
327+
os.chmod(keyfile, 0o600)
315328

316329

317330
@main.group()
@@ -331,7 +344,7 @@ def alert_clear():
331344
for alert in alerts:
332345
click.echo('Removing {} ({})'.format(alert['name'], alert['id']))
333346
api.delete_alert(alert['id'])
334-
except shodan.APIError, e:
347+
except shodan.APIError as e:
335348
raise click.ClickException(e.value)
336349
click.echo("Alerts deleted")
337350

@@ -345,7 +358,7 @@ def alert_list(expired):
345358
api = shodan.Shodan(key)
346359
try:
347360
results = api.alerts(include_expired=expired)
348-
except shodan.APIError, e:
361+
except shodan.APIError as e:
349362
raise click.ClickException(e.value)
350363

351364
if len(results) > 0:
@@ -379,7 +392,7 @@ def alert_remove(alert_id):
379392
api = shodan.Shodan(key)
380393
try:
381394
results = api.delete_alert(alert_id)
382-
except shodan.APIError, e:
395+
except shodan.APIError as e:
383396
raise click.ClickException(e.value)
384397
click.echo("Alert deleted")
385398

@@ -401,7 +414,7 @@ def count(query):
401414
api = shodan.Shodan(key)
402415
try:
403416
results = api.count(query)
404-
except shodan.APIError, e:
417+
except shodan.APIError as e:
405418
raise click.ClickException(e.value)
406419

407420
click.echo(results['total'])
@@ -458,7 +471,7 @@ def download(limit, filename, query):
458471
cursor = api.search_cursor(query)
459472
with click.progressbar(cursor, length=limit) as bar:
460473
for banner in bar:
461-
fout.write(simplejson.dumps(banner) + '\n')
474+
write_banner(fout, banner)
462475
count += 1
463476

464477
if count >= limit:
@@ -544,7 +557,7 @@ def host(format, ip):
544557
click.echo('\t\t{:15s}{}\n\t\t{:15s}{}'.format('Bits:', banner['ssl']['dhparams']['bits'], 'Generator:', banner['ssl']['dhparams']['generator']))
545558
if 'fingerprint' in banner['ssl']['dhparams']:
546559
click.echo('\t\t{:15s}{}'.format('Fingerprint:', banner['ssl']['dhparams']['fingerprint']))
547-
except shodan.APIError, e:
560+
except shodan.APIError as e:
548561
raise click.ClickException(e.value)
549562

550563

@@ -555,7 +568,7 @@ def info():
555568
api = shodan.Shodan(key)
556569
try:
557570
results = api.info()
558-
except shodan.APIError, e:
571+
except shodan.APIError as e:
559572
raise click.ClickException(e.value)
560573

561574
click.echo("""Query credits available: {0}
@@ -602,7 +615,7 @@ def parse(color, fields, filters, filename, separator, filenames):
602615

603616
# Append the data
604617
if fout:
605-
fout.write(simplejson.dumps(banner) + '\n')
618+
write_banner(fout, banner)
606619

607620
# Loop over all the fields and print the banner as a row
608621
for field in fields:
@@ -638,7 +651,7 @@ def myip():
638651
api = shodan.Shodan(key)
639652
try:
640653
click.echo(api.tools.myip())
641-
except shodan.APIError, e:
654+
except shodan.APIError as e:
642655
raise click.ClickException(e.value)
643656

644657

@@ -685,7 +698,7 @@ def scan_internet(quiet, port, protocol):
685698
try:
686699
for banner in api.stream.ports([port], timeout=30):
687700
counter += 1
688-
fout.write(simplejson.dumps(banner) + '\n')
701+
write_banner(fout, banner)
689702

690703
if not quiet:
691704
click.echo('{0:<40} {1:<20} {2}'.format(
@@ -694,7 +707,7 @@ def scan_internet(quiet, port, protocol):
694707
';'.join(banner['hostnames'])
695708
)
696709
)
697-
except shodan.APIError, e:
710+
except shodan.APIError as e:
698711
# We stop waiting for results if the scan has been processed by the crawlers and
699712
# there haven't been new results in a while
700713
if done:
@@ -703,7 +716,7 @@ def scan_internet(quiet, port, protocol):
703716
scan = api.scan_status(scan['id'])
704717
if scan['status'] == 'DONE':
705718
done = True
706-
except socket.timeout, e:
719+
except socket.timeout as e:
707720
# We stop waiting for results if the scan has been processed by the crawlers and
708721
# there haven't been new results in a while
709722
if done:
@@ -712,10 +725,10 @@ def scan_internet(quiet, port, protocol):
712725
scan = api.scan_status(scan['id'])
713726
if scan['status'] == 'DONE':
714727
done = True
715-
except Exception, e:
728+
except Exception as e:
716729
raise click.ClickException(repr(e))
717730
click.echo('Scan finished: {0} devices found'.format(counter))
718-
except shodan.APIError, e:
731+
except shodan.APIError as e:
719732
raise click.ClickException(e.value)
720733

721734

@@ -727,9 +740,9 @@ def scan_protocols():
727740
try:
728741
protocols = api.protocols()
729742

730-
for name, description in protocols.iteritems():
743+
for name, description in iter(protocols.items()):
731744
click.echo(click.style('{0:<30}'.format(name), fg='cyan') + description)
732-
except shodan.APIError, e:
745+
except shodan.APIError as e:
733746
raise click.ClickException(e.value)
734747

735748

@@ -799,7 +812,7 @@ def scan_submit(wait, filename, netblocks):
799812
done = True
800813
break
801814

802-
except shodan.APIError, e:
815+
except shodan.APIError as e:
803816
# If the connection timed out before the timeout, that means the streaming server
804817
# that the user tried to reach is down. In that case, lets wait briefly and try
805818
# to connect again!
@@ -814,15 +827,16 @@ def scan_submit(wait, filename, netblocks):
814827
scan = api.scan_status(scan['id'])
815828
if scan['status'] == 'DONE':
816829
done = True
817-
except socket.timeout, e:
830+
except socket.timeout as e:
818831
# If the connection timed out before the timeout, that means the streaming server
819832
# that the user tried to reach is down. In that case, lets wait a second and try
820833
# to connect again!
821834
if (time.time() - scan_start) < wait:
822835
continue
823836

824837
done = True
825-
except Exception, e:
838+
except Exception as e:
839+
raise
826840
finished_event.set()
827841
progress_bar_thread.join()
828842
raise click.ClickException(repr(e))
@@ -862,7 +876,7 @@ def scan_submit(wait, filename, netblocks):
862876
click.echo('\b ')
863877

864878
for ip in sorted(hosts):
865-
host = hosts[ip].items()[0][1]
879+
host = next(iter(hosts[ip].items()))[1]
866880

867881
click.echo(click.style(ip, fg='cyan'), nl=False)
868882
if 'hostnames' in host and host['hostnames']:
@@ -906,13 +920,13 @@ def scan_submit(wait, filename, netblocks):
906920

907921
# Save the banner in a file if necessary
908922
if fout:
909-
fout.write(simplejson.dumps(hosts[ip][port]) + '\n')
923+
write_banner(fout, hosts[ip][port])
910924

911925
click.echo('')
912926
else:
913927
# Prepend a \b to remove the spinner
914928
click.echo('\bNo open ports found or the host has been recently crawled and cant get scanned again so soon.')
915-
except shodan.APIError, e:
929+
except shodan.APIError as e:
916930
raise click.ClickException(e.value)
917931
finally:
918932
# Remove any alert
@@ -951,7 +965,7 @@ def search(color, fields, limit, separator, query):
951965
api = shodan.Shodan(key)
952966
try:
953967
results = api.search(query, limit=limit)
954-
except shodan.APIError, e:
968+
except shodan.APIError as e:
955969
raise click.ClickException(e.value)
956970

957971
# We buffer the entire output so we can use click's pager functionality
@@ -1011,7 +1025,7 @@ def stats(limit, facets, query):
10111025
api = shodan.Shodan(key)
10121026
try:
10131027
results = api.count(query, facets=facets)
1014-
except shodan.APIError, e:
1028+
except shodan.APIError as e:
10151029
raise click.ClickException(e.value)
10161030

10171031
# Print the stats tables
@@ -1089,7 +1103,7 @@ def stream(color, fields, separator, limit, datadir, ports, quiet, timeout, stre
10891103
last_time = cur_time
10901104
fout.close()
10911105
fout = open_file(datadir, last_time)
1092-
fout.write(simplejson.dumps(banner) + '\n')
1106+
write_banner(fout, banner)
10931107

10941108
# Print the banner information to stdout
10951109
if not quiet:
@@ -1132,7 +1146,7 @@ def stream(color, fields, separator, limit, datadir, ports, quiet, timeout, stre
11321146
def async_spinner(finished):
11331147
spinner = itertools.cycle(['-', '/', '|', '\\'])
11341148
while not finished.is_set():
1135-
sys.stdout.write('\b{}'.format(spinner.next()))
1149+
sys.stdout.write('\b{}'.format(next(spinner)))
11361150
sys.stdout.flush()
11371151
finished.wait(0.2)
11381152

shodan/client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828
except:
2929
pass
3030

31+
# Define a basestring type if necessary for Python3 compatibility
32+
try:
33+
basestring
34+
except NameError:
35+
basestring = str
36+
3137

3238
class Shodan:
3339
"""Wrapper around the Shodan REST and Streaming APIs

0 commit comments

Comments
 (0)