· 5 years ago · May 31, 2020, 03:34 PM
1# FillInHack script
2
3import os
4import re
5import sys
6import math
7import signal
8import socket
9import ctypes
10import timeit
11import platform
12import threading
13
14__version__ = '0.3.4'
15
16# Some global variables we use
17user_agent = None
18source = None
19shutdown_event = None
20scheme = 'http'
21
22# Set title
23ctypes.windll.Kernel32.SetConsoleTitleW('FillInHack - Speed Test')
24
25
26# Used for bound_interface
27socket_socket = socket.socket
28
29try:
30 import xml.etree.cElementTree as ET
31except ImportError:
32 try:
33 import xml.etree.ElementTree as ET
34 except ImportError:
35 from xml.dom import minidom as DOM
36 ET = None
37
38# Begin import game to handle Python 2 and Python 3
39try:
40 from urllib2 import urlopen, Request, HTTPError, URLError
41except ImportError:
42 from urllib.request import urlopen, Request, HTTPError, URLError
43
44try:
45 from httplib import HTTPConnection, HTTPSConnection
46except ImportError:
47 e_http_py2 = sys.exc_info()
48 try:
49 from http.client import HTTPConnection, HTTPSConnection
50 except ImportError:
51 e_http_py3 = sys.exc_info()
52 raise SystemExit('Your python installation is missing required HTTP '
53 'client classes:\n\n'
54 'Python 2: %s\n'
55 'Python 3: %s' % (e_http_py2[1], e_http_py3[1]))
56
57try:
58 from Queue import Queue
59except ImportError:
60 from queue import Queue
61
62try:
63 from urlparse import urlparse
64except ImportError:
65 from urllib.parse import urlparse
66
67try:
68 from urlparse import parse_qs
69except ImportError:
70 try:
71 from urllib.parse import parse_qs
72 except ImportError:
73 from cgi import parse_qs
74
75try:
76 from hashlib import md5
77except ImportError:
78 from md5 import md5
79
80try:
81 from argparse import ArgumentParser as ArgParser
82except ImportError:
83 from optparse import OptionParser as ArgParser
84
85try:
86 import builtins
87except ImportError:
88 def print_(*args, **kwargs):
89 """The new-style print function taken from
90 https://pypi.python.org/pypi/six/
91
92 """
93 fp = kwargs.pop("file", sys.stdout)
94 if fp is None:
95 return
96
97 def write(data):
98 if not isinstance(data, basestring):
99 data = str(data)
100 fp.write(data)
101
102 want_unicode = False
103 sep = kwargs.pop("sep", None)
104 if sep is not None:
105 if isinstance(sep, unicode):
106 want_unicode = True
107 elif not isinstance(sep, str):
108 raise TypeError("sep must be None or a string")
109 end = kwargs.pop("end", None)
110 if end is not None:
111 if isinstance(end, unicode):
112 want_unicode = True
113 elif not isinstance(end, str):
114 raise TypeError("end must be None or a string")
115 if kwargs:
116 raise TypeError("invalid keyword arguments to print()")
117 if not want_unicode:
118 for arg in args:
119 if isinstance(arg, unicode):
120 want_unicode = True
121 break
122 if want_unicode:
123 newline = unicode("\n")
124 space = unicode(" ")
125 else:
126 newline = "\n"
127 space = " "
128 if sep is None:
129 sep = space
130 if end is None:
131 end = newline
132 for i, arg in enumerate(args):
133 if i:
134 write(sep)
135 write(arg)
136 write(end)
137else:
138 print_ = getattr(builtins, 'print')
139 del builtins
140
141
142class SpeedtestCliServerListError(Exception):
143 """Internal Exception class used to indicate to move on to the next
144 URL for retrieving speedtest.net server details
145
146 """
147
148
149def bound_socket(*args, **kwargs):
150 """Bind socket to a specified source IP address"""
151
152 global source
153 sock = socket_socket(*args, **kwargs)
154 sock.bind((source, 0))
155 return sock
156
157
158def distance(origin, destination):
159 """Determine distance between 2 sets of [lat,lon] in km"""
160
161 lat1, lon1 = origin
162 lat2, lon2 = destination
163 radius = 6371 # km
164
165 dlat = math.radians(lat2 - lat1)
166 dlon = math.radians(lon2 - lon1)
167 a = (math.sin(dlat / 2) * math.sin(dlat / 2) +
168 math.cos(math.radians(lat1)) *
169 math.cos(math.radians(lat2)) * math.sin(dlon / 2) *
170 math.sin(dlon / 2))
171 c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
172 d = radius * c
173
174 return d
175
176
177def build_user_agent():
178 """Build a Mozilla/5.0 compatible User-Agent string"""
179
180 global user_agent
181 if user_agent:
182 return user_agent
183
184 ua_tuple = (
185 'Mozilla/5.0',
186 '(%s; U; %s; en-us)' % (platform.system(), platform.architecture()[0]),
187 'Python/%s' % platform.python_version(),
188 '(KHTML, like Gecko)',
189 'speedtest-cli/%s' % __version__
190 )
191 user_agent = ' '.join(ua_tuple)
192 return user_agent
193
194
195def build_request(url, data=None, headers={}):
196 """Build a urllib2 request object
197
198 This function automatically adds a User-Agent header to all requests
199
200 """
201
202 if url[0] == ':':
203 schemed_url = '%s%s' % (scheme, url)
204 else:
205 schemed_url = url
206
207 headers['User-Agent'] = user_agent
208 return Request(schemed_url, data=data, headers=headers)
209
210
211def catch_request(request):
212 """Helper function to catch common exceptions encountered when
213 establishing a connection with a HTTP/HTTPS request
214
215 """
216
217 try:
218 uh = urlopen(request)
219 return uh, False
220 except (HTTPError, URLError, socket.error):
221 e = sys.exc_info()[1]
222 return None, e
223
224
225class FileGetter(threading.Thread):
226 """Thread class for retrieving a URL"""
227
228 def __init__(self, url, start):
229 self.url = url
230 self.result = None
231 self.starttime = start
232 threading.Thread.__init__(self)
233
234 def run(self):
235 self.result = [0]
236 try:
237 if (timeit.default_timer() - self.starttime) <= 10:
238 request = build_request(self.url)
239 f = urlopen(request)
240 while 1 and not shutdown_event.isSet():
241 self.result.append(len(f.read(10240)))
242 if self.result[-1] == 0:
243 break
244 f.close()
245 except IOError:
246 pass
247
248
249def downloadSpeed(files, quiet=False):
250 """Function to launch FileGetter threads and calculate download speeds"""
251
252 start = timeit.default_timer()
253
254 def producer(q, files):
255 for file in files:
256 thread = FileGetter(file, start)
257 thread.start()
258 q.put(thread, True)
259 if not quiet and not shutdown_event.isSet():
260 sys.stdout.write('.')
261 sys.stdout.flush()
262
263 finished = []
264
265 def consumer(q, total_files):
266 while len(finished) < total_files:
267 thread = q.get(True)
268 while thread.isAlive():
269 thread.join(timeout=0.1)
270 finished.append(sum(thread.result))
271 del thread
272
273 q = Queue(6)
274 prod_thread = threading.Thread(target=producer, args=(q, files))
275 cons_thread = threading.Thread(target=consumer, args=(q, len(files)))
276 start = timeit.default_timer()
277 prod_thread.start()
278 cons_thread.start()
279 while prod_thread.isAlive():
280 prod_thread.join(timeout=0.1)
281 while cons_thread.isAlive():
282 cons_thread.join(timeout=0.1)
283 return (sum(finished) / (timeit.default_timer() - start))
284
285
286class FilePutter(threading.Thread):
287 """Thread class for putting a URL"""
288
289 def __init__(self, url, start, size):
290 self.url = url
291 chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
292 data = chars * (int(round(int(size) / 36.0)))
293 self.data = ('content1=%s' % data[0:int(size) - 9]).encode()
294 del data
295 self.result = None
296 self.starttime = start
297 threading.Thread.__init__(self)
298
299 def run(self):
300 try:
301 if ((timeit.default_timer() - self.starttime) <= 10 and
302 not shutdown_event.isSet()):
303 request = build_request(self.url, data=self.data)
304 f = urlopen(request)
305 f.read(11)
306 f.close()
307 self.result = len(self.data)
308 else:
309 self.result = 0
310 except IOError:
311 self.result = 0
312
313
314def uploadSpeed(url, sizes, quiet=False):
315 """Function to launch FilePutter threads and calculate upload speeds"""
316
317 start = timeit.default_timer()
318
319 def producer(q, sizes):
320 for size in sizes:
321 thread = FilePutter(url, start, size)
322 thread.start()
323 q.put(thread, True)
324 if not quiet and not shutdown_event.isSet():
325 sys.stdout.write('.')
326 sys.stdout.flush()
327
328 finished = []
329
330 def consumer(q, total_sizes):
331 while len(finished) < total_sizes:
332 thread = q.get(True)
333 while thread.isAlive():
334 thread.join(timeout=0.1)
335 finished.append(thread.result)
336 del thread
337
338 q = Queue(6)
339 prod_thread = threading.Thread(target=producer, args=(q, sizes))
340 cons_thread = threading.Thread(target=consumer, args=(q, len(sizes)))
341 start = timeit.default_timer()
342 prod_thread.start()
343 cons_thread.start()
344 while prod_thread.isAlive():
345 prod_thread.join(timeout=0.1)
346 while cons_thread.isAlive():
347 cons_thread.join(timeout=0.1)
348 return (sum(finished) / (timeit.default_timer() - start))
349
350
351def getAttributesByTagName(dom, tagName):
352 """Retrieve an attribute from an XML document and return it in a
353 consistent format
354
355 Only used with xml.dom.minidom, which is likely only to be used
356 with python versions older than 2.5
357 """
358 elem = dom.getElementsByTagName(tagName)[0]
359 return dict(list(elem.attributes.items()))
360
361
362def getConfig():
363 """Download the speedtest.net configuration and return only the data
364 we are interested in
365 """
366
367 request = build_request('://www.speedtest.net/speedtest-config.php')
368 uh, e = catch_request(request)
369 if e:
370 print_('Could not retrieve speedtest.net configuration: %s' % e)
371 sys.exit(1)
372 configxml = []
373 while 1:
374 configxml.append(uh.read(10240))
375 if len(configxml[-1]) == 0:
376 break
377 if int(uh.code) != 200:
378 return None
379 uh.close()
380 try:
381 try:
382 root = ET.fromstring(''.encode().join(configxml))
383 config = {
384 'client': root.find('client').attrib,
385 'times': root.find('times').attrib,
386 'download': root.find('download').attrib,
387 'upload': root.find('upload').attrib}
388 except AttributeError: # Python3 branch
389 root = DOM.parseString(''.join(configxml))
390 config = {
391 'client': getAttributesByTagName(root, 'client'),
392 'times': getAttributesByTagName(root, 'times'),
393 'download': getAttributesByTagName(root, 'download'),
394 'upload': getAttributesByTagName(root, 'upload')}
395 except SyntaxError:
396 print_('Failed to parse speedtest.net configuration')
397 sys.exit(1)
398 del root
399 del configxml
400 return config
401
402
403def closestServers(client, all=False):
404 """Determine the 5 closest speedtest.net servers based on geographic
405 distance
406 """
407
408 urls = [
409 '://www.speedtest.net/speedtest-servers-static.php',
410 '://c.speedtest.net/speedtest-servers-static.php',
411 '://www.speedtest.net/speedtest-servers.php',
412 '://c.speedtest.net/speedtest-servers.php',
413 ]
414 errors = []
415 servers = {}
416 for url in urls:
417 try:
418 request = build_request(url)
419 uh, e = catch_request(request)
420 if e:
421 errors.append('%s' % e)
422 raise SpeedtestCliServerListError
423 serversxml = []
424 while 1:
425 serversxml.append(uh.read(10240))
426 if len(serversxml[-1]) == 0:
427 break
428 if int(uh.code) != 200:
429 uh.close()
430 raise SpeedtestCliServerListError
431 uh.close()
432 try:
433 try:
434 root = ET.fromstring(''.encode().join(serversxml))
435 elements = root.getiterator('server')
436 except AttributeError: # Python3 branch
437 root = DOM.parseString(''.join(serversxml))
438 elements = root.getElementsByTagName('server')
439 except SyntaxError:
440 raise SpeedtestCliServerListError
441 for server in elements:
442 try:
443 attrib = server.attrib
444 except AttributeError:
445 attrib = dict(list(server.attributes.items()))
446 d = distance([float(client['lat']),
447 float(client['lon'])],
448 [float(attrib.get('lat')),
449 float(attrib.get('lon'))])
450 attrib['d'] = d
451 if d not in servers:
452 servers[d] = [attrib]
453 else:
454 servers[d].append(attrib)
455 del root
456 del serversxml
457 del elements
458 except SpeedtestCliServerListError:
459 continue
460
461 # We were able to fetch and parse the list of speedtest.net servers
462 if servers:
463 break
464
465 if not servers:
466 print_('Failed to retrieve list of speedtest.net servers:\n\n %s' %
467 '\n'.join(errors))
468 sys.exit(1)
469
470 closest = []
471 for d in sorted(servers.keys()):
472 for s in servers[d]:
473 closest.append(s)
474 if len(closest) == 5 and not all:
475 break
476 else:
477 continue
478 break
479
480 del servers
481 return closest
482
483
484def getBestServer(servers):
485 """Perform a speedtest.net latency request to determine which
486 speedtest.net server has the lowest latency
487 """
488
489 results = {}
490 for server in servers:
491 cum = []
492 url = '%s/latency.txt' % os.path.dirname(server['url'])
493 urlparts = urlparse(url)
494 for i in range(0, 3):
495 try:
496 if urlparts[0] == 'https':
497 h = HTTPSConnection(urlparts[1])
498 else:
499 h = HTTPConnection(urlparts[1])
500 headers = {'User-Agent': user_agent}
501 start = timeit.default_timer()
502 h.request("GET", urlparts[2], headers=headers)
503 r = h.getresponse()
504 total = (timeit.default_timer() - start)
505 except (HTTPError, URLError, socket.error):
506 cum.append(3600)
507 continue
508 text = r.read(9)
509 if int(r.status) == 200 and text == 'test=test'.encode():
510 cum.append(total)
511 else:
512 cum.append(3600)
513 h.close()
514 avg = round((sum(cum) / 6) * 1000, 3)
515 results[avg] = server
516 fastest = sorted(results.keys())[0]
517 best = results[fastest]
518 best['latency'] = fastest
519
520 return best
521
522
523def ctrl_c(signum, frame):
524 """Catch Ctrl-C key sequence and set a shutdown_event for our threaded
525 operations
526 """
527
528 global shutdown_event
529 shutdown_event.set()
530 raise SystemExit('\nCancelling...')
531
532
533def version():
534 """Print the version"""
535
536 raise SystemExit(__version__)
537
538
539def speedtest():
540 """Run the full speedtest.net test"""
541
542 global shutdown_event, source, scheme
543 shutdown_event = threading.Event()
544
545 signal.signal(signal.SIGINT, ctrl_c)
546
547 description = (
548 'Command line interface for testing internet bandwidth using '
549 'speedtest.net.\n'
550 '------------------------------------------------------------'
551 '--------------\n'
552 'https://github.com/sivel/speedtest-cli')
553
554 parser = ArgParser(description=description)
555 # Give optparse.OptionParser an `add_argument` method for
556 # compatibility with argparse.ArgumentParser
557 try:
558 parser.add_argument = parser.add_option
559 except AttributeError:
560 pass
561 parser.add_argument('--bytes', dest='units', action='store_const',
562 const=('byte', 1), default=('bit', 8),
563 help='Display values in bytes instead of bits. Does '
564 'not affect the image generated by --share')
565 parser.add_argument('--share', action='store_true',
566 help='Generate and provide a URL to the speedtest.net '
567 'share results image')
568 parser.add_argument('--simple', action='store_true',
569 help='Suppress verbose output, only show basic '
570 'information')
571 parser.add_argument('--list', action='store_true',
572 help='Display a list of speedtest.net servers '
573 'sorted by distance')
574 parser.add_argument('--server', help='Specify a server ID to test against')
575 parser.add_argument('--mini', help='URL of the Speedtest Mini server')
576 parser.add_argument('--source', help='Source IP address to bind to')
577 parser.add_argument('--timeout', default=10, type=int,
578 help='HTTP timeout in seconds. Default 10')
579 parser.add_argument('--secure', action='store_true',
580 help='Use HTTPS instead of HTTP when communicating '
581 'with speedtest.net operated servers')
582 parser.add_argument('--version', action='store_true',
583 help='Show the version number and exit')
584
585 options = parser.parse_args()
586 if isinstance(options, tuple):
587 args = options[0]
588 else:
589 args = options
590 del options
591
592 # Print the version and exit
593 if args.version:
594 version()
595
596 socket.setdefaulttimeout(args.timeout)
597
598 # Pre-cache the user agent string
599 build_user_agent()
600
601 # If specified bind to a specific IP address
602 if args.source:
603 source = args.source
604 socket.socket = bound_socket
605
606 if args.secure:
607 scheme = 'https'
608
609 if not args.simple:
610 print_('>>>> Retrieving FillInHack configuration...')
611 try:
612 config = getConfig()
613 except URLError:
614 print_('Cannot retrieve speedtest configuration')
615 sys.exit(1)
616
617 if not args.simple:
618 print_('>>>> Retrieving FillInHack server list...')
619 if args.list or args.server:
620 servers = closestServers(config['client'], True)
621 if args.list:
622 serverList = []
623 for server in servers:
624 line = ('%(id)4s) %(sponsor)s (%(name)s, %(country)s) '
625 '[%(d)0.2f km]' % server)
626 serverList.append(line)
627 print_('\n'.join(serverList).encode('utf-8', 'ignore'))
628 sys.exit(0)
629 else:
630 servers = closestServers(config['client'])
631
632 if not args.simple:
633 print_('>>>> Server Host: %(isp)s | IP: %(ip)s' % config['client']) # PRINT HOST AND IP
634
635 if args.server:
636 try:
637 best = getBestServer(filter(lambda x: x['id'] == args.server,
638 servers))
639 except IndexError:
640 print_('Invalid server ID')
641 sys.exit(1)
642 elif args.mini:
643 name, ext = os.path.splitext(args.mini)
644 if ext:
645 url = os.path.dirname(args.mini)
646 else:
647 url = args.mini
648 urlparts = urlparse(url)
649 try:
650 request = build_request(args.mini)
651 f = urlopen(request)
652 except:
653 print_('Invalid Speedtest Mini URL')
654 sys.exit(1)
655 else:
656 text = f.read()
657 f.close()
658 extension = re.findall('upload_extension: "([^"]+)"', text.decode())
659 if not extension:
660 for ext in ['php', 'asp', 'aspx', 'jsp']:
661 try:
662 request = build_request('%s/speedtest/upload.%s' %
663 (args.mini, ext))
664 f = urlopen(request)
665 except:
666 pass
667 else:
668 data = f.read().strip()
669 if (f.code == 200 and
670 len(data.splitlines()) == 1 and
671 re.match('size=[0-9]', data)):
672 extension = [ext]
673 break
674 if not urlparts or not extension:
675 print_('Please provide the full URL of your Speedtest Mini server')
676 sys.exit(1)
677 servers = [{
678 'sponsor': 'Speedtest Mini',
679 'name': urlparts[1],
680 'd': 0,
681 'url': '%s/speedtest/upload.%s' % (url.rstrip('/'), extension[0]),
682 'latency': 0,
683 'id': 0
684 }]
685 try:
686 best = getBestServer(servers)
687 except:
688 best = servers[0]
689 else:
690 if not args.simple:
691 print_('>>>> Cautam cel mai bun server')
692 print_('>>>> Server gasit, incepem testele')
693 best = getBestServer(servers)
694
695 if not args.simple:
696 print_(('>>>> Server Intretinut de %(sponsor)s (%(name)s) %(d)0.2f km '
697 '%(latency)s ' % best).encode('utf-8', 'ignore'))
698 else:
699 print_('Ping: %(latency)s ms' % best)
700
701 sizes = [350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000]
702 urls = []
703 for size in sizes:
704 for i in range(0, 4):
705 urls.append('%s/random%sx%s.jpg' %
706 (os.path.dirname(best['url']), size, size))
707 if not args.simple:
708 print_('>>>> Testing download speed', end='')
709 dlspeed = downloadSpeed(urls, args.simple)
710 if not args.simple:
711 print_()
712 print_('>>>> Download-ul este de: %0.2f M%s/s' %
713
714 ((dlspeed / 1000 / 1000) * args.units[1], args.units[0]))
715
716 sizesizes = [int(.25 * 1000 * 1000), int(.5 * 1000 * 1000)]
717 sizes = []
718 for size in sizesizes:
719 for i in range(0, 25):
720 sizes.append(size)
721 if not args.simple:
722 print_('>>>> Testing upload speed', end='')
723 ulspeed = uploadSpeed(best['url'], sizes, args.simple)
724 if not args.simple:
725 print_()
726 print_('>>>> Upload-ul de flood este : %0.2f M%s/s' %
727 ((ulspeed / 1000 / 1000) * args.units[1], args.units[0]))
728
729 if args.share and args.mini:
730 print_('Cannot generate a speedtest.net share results image while '
731 'testing against a Speedtest Mini server')
732 elif args.share:
733 dlspeedk = int(round((dlspeed / 1000) * 8, 0))
734 ping = int(round(best['latency'], 0))
735 ulspeedk = int(round((ulspeed / 1000) * 8, 0))
736
737 # Build the request to send results back to speedtest.net
738 # We use a list instead of a dict because the API expects parameters
739 # in a certain order
740 apiData = [
741 'download=%s' % dlspeedk,
742 'ping=%s' % ping,
743 'upload=%s' % ulspeedk,
744 'promo=',
745 'startmode=%s' % 'pingselect',
746 'recommendedserverid=%s' % best['id'],
747 'accuracy=%s' % 1,
748 'serverid=%s' % best['id'],
749 'hash=%s' % md5(('%s-%s-%s-%s' %
750 (ping, ulspeedk, dlspeedk, '297aae72'))
751 .encode()).hexdigest()]
752
753 headers = {'Referer': 'http://c.speedtest.net/flash/speedtest.swf'}
754 request = build_request('://www.speedtest.net/api/api.php',
755 data='&'.join(apiData).encode(),
756 headers=headers)
757 f, e = catch_request(request)
758 if e:
759 print_('Could not submit results to speedtest.net: %s' % e)
760 sys.exit(1)
761 response = f.read()
762 code = f.code
763 f.close()
764
765 if int(code) != 200:
766 print_('Could not submit results to speedtest.net')
767 sys.exit(1)
768
769 qsargs = parse_qs(response.decode())
770 resultid = qsargs.get('resultid')
771 if not resultid or len(resultid) != 1:
772 print_('Could not submit results to speedtest.net')
773 sys.exit(1)
774
775 print_('Share results: %s://www.speedtest.net/result/%s.png' %
776 (scheme, resultid[0]))
777
778
779def main():
780 try:
781 speedtest()
782 except KeyboardInterrupt:
783 print_('\nCancelling...')
784
785
786if __name__ == '__main__':
787 main()