coolerfarmer

Untitled

Aug 2nd, 2014
5,657
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 21.26 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # Copyright 2012-2014 Matt Martz
  4. # All Rights Reserved.
  5. #
  6. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  7. # not use this file except in compliance with the License. You may obtain
  8. # a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  14. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  15. # License for the specific language governing permissions and limitations
  16. # under the License.
  17.  
  18. __version__ = '0.3.0'
  19.  
  20. # Some global variables we use
  21. source = None
  22. shutdown_event = None
  23.  
  24. import os
  25. import re
  26. import sys
  27. import math
  28. import signal
  29. import socket
  30. import timeit
  31. import threading
  32.  
  33. # Used for bound_interface
  34. socket_socket = socket.socket
  35.  
  36. try:
  37. import xml.etree.cElementTree as ET
  38. except ImportError:
  39. try:
  40. import xml.etree.ElementTree as ET
  41. except ImportError:
  42. from xml.dom import minidom as DOM
  43. ET = None
  44.  
  45. # Begin import game to handle Python 2 and Python 3
  46. try:
  47. from urllib2 import urlopen, Request, HTTPError, URLError
  48. except ImportError:
  49. from urllib.request import urlopen, Request, HTTPError, URLError
  50.  
  51. try:
  52. from httplib import HTTPConnection, HTTPSConnection
  53. except ImportError:
  54. from http.client import HTTPConnection, HTTPSConnection
  55.  
  56. try:
  57. from Queue import Queue
  58. except ImportError:
  59. from queue import Queue
  60.  
  61. try:
  62. from urlparse import urlparse
  63. except ImportError:
  64. from urllib.parse import urlparse
  65.  
  66. try:
  67. from urlparse import parse_qs
  68. except ImportError:
  69. try:
  70. from urllib.parse import parse_qs
  71. except ImportError:
  72. from cgi import parse_qs
  73.  
  74. try:
  75. from hashlib import md5
  76. except ImportError:
  77. from md5 import md5
  78.  
  79. try:
  80. from argparse import ArgumentParser as ArgParser
  81. except ImportError:
  82. from optparse import OptionParser as ArgParser
  83.  
  84. try:
  85. import builtins
  86. except ImportError:
  87. def print_(*args, **kwargs):
  88. """The new-style print function taken from
  89. https://pypi.python.org/pypi/six/
  90.  
  91. """
  92. fp = kwargs.pop("file", sys.stdout)
  93. if fp is None:
  94. return
  95.  
  96. def write(data):
  97. if not isinstance(data, basestring):
  98. data = str(data)
  99. fp.write(data)
  100.  
  101. want_unicode = False
  102. sep = kwargs.pop("sep", None)
  103. if sep is not None:
  104. if isinstance(sep, unicode):
  105. want_unicode = True
  106. elif not isinstance(sep, str):
  107. raise TypeError("sep must be None or a string")
  108. end = kwargs.pop("end", None)
  109. if end is not None:
  110. if isinstance(end, unicode):
  111. want_unicode = True
  112. elif not isinstance(end, str):
  113. raise TypeError("end must be None or a string")
  114. if kwargs:
  115. raise TypeError("invalid keyword arguments to print()")
  116. if not want_unicode:
  117. for arg in args:
  118. if isinstance(arg, unicode):
  119. want_unicode = True
  120. break
  121. if want_unicode:
  122. newline = unicode("\n")
  123. space = unicode(" ")
  124. else:
  125. newline = "\n"
  126. space = " "
  127. if sep is None:
  128. sep = space
  129. if end is None:
  130. end = newline
  131. for i, arg in enumerate(args):
  132. if i:
  133. write(sep)
  134. write(arg)
  135. write(end)
  136. else:
  137. print_ = getattr(builtins, 'print')
  138. del builtins
  139.  
  140.  
  141. def bound_socket(*args, **kwargs):
  142. """Bind socket to a specified source IP address"""
  143.  
  144. global source
  145. sock = socket_socket(*args, **kwargs)
  146. sock.bind((source, 0))
  147. return sock
  148.  
  149.  
  150. def distance(origin, destination):
  151. """Determine distance between 2 sets of [lat,lon] in km"""
  152.  
  153. lat1, lon1 = origin
  154. lat2, lon2 = destination
  155. radius = 6371 # km
  156.  
  157. dlat = math.radians(lat2 - lat1)
  158. dlon = math.radians(lon2 - lon1)
  159. a = (math.sin(dlat / 2) * math.sin(dlat / 2) + math.cos(math.radians(lat1))
  160. * math.cos(math.radians(lat2)) * math.sin(dlon / 2)
  161. * math.sin(dlon / 2))
  162. c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
  163. d = radius * c
  164.  
  165. return d
  166.  
  167.  
  168. class FileGetter(threading.Thread):
  169. """Thread class for retrieving a URL"""
  170.  
  171. def __init__(self, url, start):
  172. self.url = url
  173. self.result = None
  174. self.starttime = start
  175. threading.Thread.__init__(self)
  176.  
  177. def run(self):
  178. self.result = [0]
  179. try:
  180. if (timeit.default_timer() - self.starttime) <= 10:
  181. f = urlopen(self.url)
  182. while 1 and not shutdown_event.isSet():
  183. self.result.append(len(f.read(10240)))
  184. if self.result[-1] == 0:
  185. break
  186. f.close()
  187. except IOError:
  188. pass
  189.  
  190.  
  191. def downloadSpeed(files, quiet=False):
  192. """Function to launch FileGetter threads and calculate download speeds"""
  193.  
  194. start = timeit.default_timer()
  195.  
  196. def producer(q, files):
  197. for file in files:
  198. thread = FileGetter(file, start)
  199. thread.start()
  200. q.put(thread, True)
  201. if not quiet and not shutdown_event.isSet():
  202. sys.stdout.write('.')
  203. sys.stdout.flush()
  204.  
  205. finished = []
  206.  
  207. def consumer(q, total_files):
  208. while len(finished) < total_files:
  209. thread = q.get(True)
  210. while thread.isAlive():
  211. thread.join(timeout=0.1)
  212. finished.append(sum(thread.result))
  213. del thread
  214.  
  215. q = Queue(6)
  216. prod_thread = threading.Thread(target=producer, args=(q, files))
  217. cons_thread = threading.Thread(target=consumer, args=(q, len(files)))
  218. start = timeit.default_timer()
  219. prod_thread.start()
  220. cons_thread.start()
  221. while prod_thread.isAlive():
  222. prod_thread.join(timeout=0.1)
  223. while cons_thread.isAlive():
  224. cons_thread.join(timeout=0.1)
  225. return (sum(finished) / (timeit.default_timer() - start))
  226.  
  227.  
  228. class FilePutter(threading.Thread):
  229. """Thread class for putting a URL"""
  230.  
  231. def __init__(self, url, start, size):
  232. self.url = url
  233. chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  234. data = chars * (int(round(int(size) / 36.0)))
  235. self.data = ('content1=%s' % data[0:int(size) - 9]).encode()
  236. del data
  237. self.result = None
  238. self.starttime = start
  239. threading.Thread.__init__(self)
  240.  
  241. def run(self):
  242. try:
  243. if ((timeit.default_timer() - self.starttime) <= 10 and
  244. not shutdown_event.isSet()):
  245. f = urlopen(self.url, self.data)
  246. f.read(11)
  247. f.close()
  248. self.result = len(self.data)
  249. else:
  250. self.result = 0
  251. except IOError:
  252. self.result = 0
  253.  
  254.  
  255. def uploadSpeed(url, sizes, quiet=False):
  256. """Function to launch FilePutter threads and calculate upload speeds"""
  257.  
  258. start = timeit.default_timer()
  259.  
  260. def producer(q, sizes):
  261. for size in sizes:
  262. thread = FilePutter(url, start, size)
  263. thread.start()
  264. q.put(thread, True)
  265. if not quiet and not shutdown_event.isSet():
  266. sys.stdout.write('.')
  267. sys.stdout.flush()
  268.  
  269. finished = []
  270.  
  271. def consumer(q, total_sizes):
  272. while len(finished) < total_sizes:
  273. thread = q.get(True)
  274. while thread.isAlive():
  275. thread.join(timeout=0.1)
  276. finished.append(thread.result)
  277. del thread
  278.  
  279. q = Queue(6)
  280. prod_thread = threading.Thread(target=producer, args=(q, sizes))
  281. cons_thread = threading.Thread(target=consumer, args=(q, len(sizes)))
  282. start = timeit.default_timer()
  283. prod_thread.start()
  284. cons_thread.start()
  285. while prod_thread.isAlive():
  286. prod_thread.join(timeout=0.1)
  287. while cons_thread.isAlive():
  288. cons_thread.join(timeout=0.1)
  289. return (sum(finished) / (timeit.default_timer() - start))
  290.  
  291.  
  292. def getAttributesByTagName(dom, tagName):
  293. """Retrieve an attribute from an XML document and return it in a
  294. consistent format
  295.  
  296. Only used with xml.dom.minidom, which is likely only to be used
  297. with python versions older than 2.5
  298. """
  299. elem = dom.getElementsByTagName(tagName)[0]
  300. return dict(list(elem.attributes.items()))
  301.  
  302.  
  303. def getConfig():
  304. """Download the speedtest.net configuration and return only the data
  305. we are interested in
  306. """
  307.  
  308. uh = urlopen('http://www.speedtest.net/speedtest-config.php')
  309. configxml = []
  310. while 1:
  311. configxml.append(uh.read(10240))
  312. if len(configxml[-1]) == 0:
  313. break
  314. if int(uh.code) != 200:
  315. return None
  316. uh.close()
  317. try:
  318. try:
  319. root = ET.fromstring(''.encode().join(configxml))
  320. config = {
  321. 'client': root.find('client').attrib,
  322. 'times': root.find('times').attrib,
  323. 'download': root.find('download').attrib,
  324. 'upload': root.find('upload').attrib}
  325. except AttributeError:
  326. root = DOM.parseString(''.join(configxml))
  327. config = {
  328. 'client': getAttributesByTagName(root, 'client'),
  329. 'times': getAttributesByTagName(root, 'times'),
  330. 'download': getAttributesByTagName(root, 'download'),
  331. 'upload': getAttributesByTagName(root, 'upload')}
  332. except SyntaxError:
  333. print_('Failed to parse speedtest.net configuration')
  334. sys.exit(1)
  335. del root
  336. del configxml
  337. return config
  338.  
  339.  
  340. def closestServers(client, all=False):
  341. """Determine the 5 closest speedtest.net servers based on geographic
  342. distance
  343. """
  344.  
  345. uh = urlopen('http://c.speedtest.net/speedtest-servers-static.php')
  346. serversxml = []
  347. while 1:
  348. serversxml.append(uh.read(10240))
  349. if len(serversxml[-1]) == 0:
  350. break
  351. if int(uh.code) != 200:
  352. return None
  353. uh.close()
  354. try:
  355. try:
  356. root = ET.fromstring(''.encode().join(serversxml))
  357. elements = root.getiterator('server')
  358. except AttributeError:
  359. root = DOM.parseString(''.join(serversxml))
  360. elements = root.getElementsByTagName('server')
  361. except SyntaxError:
  362. print_('Failed to parse list of speedtest.net servers')
  363. sys.exit(1)
  364. servers = {}
  365. for server in elements:
  366. try:
  367. attrib = server.attrib
  368. except AttributeError:
  369. attrib = dict(list(server.attributes.items()))
  370. d = distance([float(client['lat']), float(client['lon'])],
  371. [float(attrib.get('lat')), float(attrib.get('lon'))])
  372. attrib['d'] = d
  373. if d not in servers:
  374. servers[d] = [attrib]
  375. else:
  376. servers[d].append(attrib)
  377. del root
  378. del serversxml
  379. del elements
  380.  
  381. closest = []
  382. for d in sorted(servers.keys()):
  383. for s in servers[d]:
  384. closest.append(s)
  385. if len(closest) == 5 and not all:
  386. break
  387. else:
  388. continue
  389. break
  390.  
  391. del servers
  392. return closest
  393.  
  394.  
  395. def getBestServer(servers):
  396. """Perform a speedtest.net latency request to determine which
  397. speedtest.net server has the lowest latency
  398. """
  399.  
  400. results = {}
  401. for server in servers:
  402. cum = []
  403. url = '%s/latency.txt' % os.path.dirname(server['url'])
  404. urlparts = urlparse(url)
  405. for i in range(0, 3):
  406. try:
  407. if urlparts[0] == 'https':
  408. h = HTTPSConnection(urlparts[1])
  409. else:
  410. h = HTTPConnection(urlparts[1])
  411. start = timeit.default_timer()
  412. h.request("GET", urlparts[2])
  413. r = h.getresponse()
  414. total = (timeit.default_timer() - start)
  415. except (HTTPError, URLError):
  416. cum.append(3600)
  417. continue
  418. text = r.read(9)
  419. if int(r.status) == 200 and text == 'test=test'.encode():
  420. cum.append(total)
  421. else:
  422. cum.append(3600)
  423. h.close()
  424. avg = round((sum(cum) / 6) * 1000, 3)
  425. results[avg] = server
  426. fastest = sorted(results.keys())[0]
  427. best = results[fastest]
  428. best['latency'] = fastest
  429.  
  430. return best
  431.  
  432.  
  433. def ctrl_c(signum, frame):
  434. """Catch Ctrl-C key sequence and set a shutdown_event for our threaded
  435. operations
  436. """
  437.  
  438. global shutdown_event
  439. shutdown_event.set()
  440. raise SystemExit('\nCancelling...')
  441.  
  442.  
  443. def version():
  444. """Print the version"""
  445.  
  446. raise SystemExit(__version__)
  447.  
  448.  
  449. def speedtest():
  450. """Run the full speedtest.net test"""
  451.  
  452. global shutdown_event, source
  453. shutdown_event = threading.Event()
  454.  
  455. signal.signal(signal.SIGINT, ctrl_c)
  456.  
  457. description = (
  458. 'Command line interface for testing internet bandwidth using '
  459. 'speedtest.net.\n'
  460. '------------------------------------------------------------'
  461. '--------------\n'
  462. 'https://github.com/sivel/speedtest-cli')
  463.  
  464. parser = ArgParser(description=description)
  465. # Give optparse.OptionParser an `add_argument` method for
  466. # compatibility with argparse.ArgumentParser
  467. try:
  468. parser.add_argument = parser.add_option
  469. except AttributeError:
  470. pass
  471. parser.add_argument('--bytes', dest='units', action='store_const',
  472. const=('bytes', 1), default=('bits', 8),
  473. help='Display values in bytes instead of bits. Does '
  474. 'not affect the image generated by --share')
  475. parser.add_argument('--share', action='store_true',
  476. help='Generate and provide a URL to the speedtest.net '
  477. 'share results image')
  478. parser.add_argument('--simple', action='store_true',
  479. help='Suppress verbose output, only show basic '
  480. 'information')
  481. parser.add_argument('--list', action='store_true',
  482. help='Display a list of speedtest.net servers '
  483. 'sorted by distance')
  484. parser.add_argument('--server', help='Specify a server ID to test against')
  485. parser.add_argument('--mini', help='URL of the Speedtest Mini server')
  486. parser.add_argument('--source', help='Source IP address to bind to')
  487. parser.add_argument('--version', action='store_true',
  488. help='Show the version number and exit')
  489.  
  490. options = parser.parse_args()
  491. if isinstance(options, tuple):
  492. args = options[0]
  493. else:
  494. args = options
  495. del options
  496.  
  497. # Print the version and exit
  498. if args.version:
  499. version()
  500.  
  501. # If specified bind to a specific IP address
  502. if args.source:
  503. source = args.source
  504. socket.socket = bound_socket
  505.  
  506. if not args.simple:
  507. print_('Retrieving speedtest.net configuration...')
  508. try:
  509. config = getConfig()
  510. except URLError:
  511. print_('Cannot retrieve speedtest configuration')
  512. sys.exit(1)
  513.  
  514. if not args.simple:
  515. print_('Retrieving speedtest.net server list...')
  516. if args.list or args.server:
  517. servers = closestServers(config['client'], True)
  518. if args.list:
  519. serverList = []
  520. for server in servers:
  521. line = ('%(id)4s) %(sponsor)s (%(name)s, %(country)s) '
  522. '[%(d)0.2f km]' % server)
  523. serverList.append(line)
  524. # Python 2.7 and newer seem to be ok with the resultant encoding
  525. # from parsing the XML, but older versions have some issues.
  526. # This block should detect whether we need to encode or not
  527. try:
  528. unicode()
  529. print_('\n'.join(serverList).encode('utf-8', 'ignore'))
  530. except NameError:
  531. print_('\n'.join(serverList))
  532. except IOError:
  533. pass
  534. sys.exit(0)
  535. else:
  536. servers = closestServers(config['client'])
  537.  
  538. if not args.simple:
  539. print_('Testing from %(isp)s (%(ip)s)...' % config['client'])
  540.  
  541. if args.server:
  542. try:
  543. best = getBestServer(filter(lambda x: x['id'] == args.server,
  544. servers))
  545. except IndexError:
  546. print_('Invalid server ID')
  547. sys.exit(1)
  548. elif args.mini:
  549. name, ext = os.path.splitext(args.mini)
  550. if ext:
  551. url = os.path.dirname(args.mini)
  552. else:
  553. url = args.mini
  554. urlparts = urlparse(url)
  555. try:
  556. f = urlopen(args.mini)
  557. except:
  558. print_('Invalid Speedtest Mini URL')
  559. sys.exit(1)
  560. else:
  561. text = f.read()
  562. f.close()
  563. extension = re.findall('upload_extension: "([^"]+)"', text.decode())
  564. if not extension:
  565. for ext in ['php', 'asp', 'aspx', 'jsp']:
  566. try:
  567. f = urlopen('%s/speedtest/upload.%s' % (args.mini, ext))
  568. except:
  569. pass
  570. else:
  571. data = f.read().strip()
  572. if (f.code == 200 and
  573. len(data.splitlines()) == 1 and
  574. re.match('size=[0-9]', data)):
  575. extension = [ext]
  576. break
  577. if not urlparts or not extension:
  578. print_('Please provide the full URL of your Speedtest Mini server')
  579. sys.exit(1)
  580. servers = [{
  581. 'sponsor': 'Speedtest Mini',
  582. 'name': urlparts[1],
  583. 'd': 0,
  584. 'url': '%s/speedtest/upload.%s' % (url.rstrip('/'), extension[0]),
  585. 'latency': 0,
  586. 'id': 0
  587. }]
  588. try:
  589. best = getBestServer(servers)
  590. except:
  591. best = servers[0]
  592. else:
  593. if not args.simple:
  594. print_('Selecting best server based on latency...')
  595. best = getBestServer(servers)
  596.  
  597. if not args.simple:
  598. # Python 2.7 and newer seem to be ok with the resultant encoding
  599. # from parsing the XML, but older versions have some issues.
  600. # This block should detect whether we need to encode or not
  601. try:
  602. unicode()
  603. print_(('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: '
  604. '%(latency)s ms' % best).encode('utf-8', 'ignore'))
  605. except NameError:
  606. print_('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: '
  607. '%(latency)s ms' % best)
  608. else:
  609. print_('Ping: %(latency)s ms' % best)
  610.  
  611. sizes = [350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000]
  612. urls = []
  613. for size in sizes:
  614. for i in range(0, 4):
  615. urls.append('%s/random%sx%s.jpg' %
  616. (os.path.dirname(best['url']), size, size))
  617. if not args.simple:
  618. print_('Testing download speed', end='')
  619. dlspeed = downloadSpeed(urls, args.simple)
  620. if not args.simple:
  621. print_()
  622. print_('Download: %0.2f M%s/s' %
  623. ((dlspeed / 1000 / 1000) * args.units[1], args.units[0]))
  624.  
  625. sizesizes = [int(.25 * 1000 * 1000), int(.5 * 1000 * 1000)]
  626. sizes = []
  627. for size in sizesizes:
  628. for i in range(0, 25):
  629. sizes.append(size)
  630. if not args.simple:
  631. print_('Testing upload speed', end='')
  632. ulspeed = uploadSpeed(best['url'], sizes, args.simple)
  633. if not args.simple:
  634. print_()
  635. print_('Upload: %0.2f M%s/s' %
  636. ((ulspeed / 1000 / 1000) * args.units[1], args.units[0]))
  637.  
  638. if args.share and args.mini:
  639. print_('Cannot generate a speedtest.net share results image while '
  640. 'testing against a Speedtest Mini server')
  641. elif args.share:
  642. dlspeedk = int(round((dlspeed / 1000) * 8, 0))
  643. ping = int(round(best['latency'], 0))
  644. ulspeedk = int(round((ulspeed / 1000) * 8, 0))
  645.  
  646. # Build the request to send results back to speedtest.net
  647. # We use a list instead of a dict because the API expects parameters
  648. # in a certain order
  649. apiData = [
  650. 'download=%s' % dlspeedk,
  651. 'ping=%s' % ping,
  652. 'upload=%s' % ulspeedk,
  653. 'promo=',
  654. 'startmode=%s' % 'pingselect',
  655. 'recommendedserverid=%s' % best['id'],
  656. 'accuracy=%s' % 1,
  657. 'serverid=%s' % best['id'],
  658. 'hash=%s' % md5(('%s-%s-%s-%s' %
  659. (ping, ulspeedk, dlspeedk, '297aae72'))
  660. .encode()).hexdigest()]
  661.  
  662. req = Request('http://www.speedtest.net/api/api.php',
  663. data='&'.join(apiData).encode())
  664. req.add_header('Referer', 'http://c.speedtest.net/flash/speedtest.swf')
  665. f = urlopen(req)
  666. response = f.read()
  667. code = f.code
  668. f.close()
  669.  
  670. if int(code) != 200:
  671. print_('Could not submit results to speedtest.net')
  672. sys.exit(1)
  673.  
  674. qsargs = parse_qs(response.decode())
  675. resultid = qsargs.get('resultid')
  676. if not resultid or len(resultid) != 1:
  677. print_('Could not submit results to speedtest.net')
  678. sys.exit(1)
  679.  
  680. print_('Share results: http://www.speedtest.net/result/%s.png' %
  681. resultid[0])
  682.  
  683.  
  684. def main():
  685. try:
  686. speedtest()
  687. except KeyboardInterrupt:
  688. print_('\nCancelling...')
  689.  
  690.  
  691. if __name__ == '__main__':
  692. main()
  693.  
  694. # vim:ts=4:sw=4:expandtab
Advertisement
Add Comment
Please, Sign In to add comment