update 2025-03-02 04:19:57

This commit is contained in:
kenzok8 2025-03-02 04:19:57 +08:00
parent b0c91fec6f
commit 04615d488c
2 changed files with 121 additions and 150 deletions

View File

@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-netspeedtest PKG_NAME:=luci-app-netspeedtest
PKG_VERSION:=2.3.0 PKG_VERSION:=2.3.1
PKG_RELEASE:=20250104 PKG_RELEASE:=20250302
LUCI_TITLE:=LuCI Support for netspeedtest LUCI_TITLE:=LuCI Support for netspeedtest
LUCI_DEPENDS:=+python3 +iperf3-ssl +homebox LUCI_DEPENDS:=+python3 +iperf3-ssl +homebox

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2012 Matt Martz # Copyright 2012 Matt Martz
# All Rights Reserved. # All Rights Reserved.
@ -15,18 +15,18 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import os
import re
import csv import csv
import sys import datetime
import math
import errno import errno
import math
import os
import platform
import re
import signal import signal
import socket import socket
import timeit import sys
import datetime
import platform
import threading import threading
import timeit
import xml.parsers.expat import xml.parsers.expat
try: try:
@ -36,7 +36,7 @@ except ImportError:
gzip = None gzip = None
GZIP_BASE = object GZIP_BASE = object
__version__ = '2.1.3' __version__ = '2.1.4b2'
class FakeShutdownEvent(object): class FakeShutdownEvent(object):
@ -49,6 +49,8 @@ class FakeShutdownEvent(object):
"Dummy method to always return false""" "Dummy method to always return false"""
return False return False
is_set = isSet
# Some global variables we use # Some global variables we use
DEBUG = False DEBUG = False
@ -56,6 +58,7 @@ _GLOBAL_DEFAULT_TIMEOUT = object()
PY25PLUS = sys.version_info[:2] >= (2, 5) PY25PLUS = sys.version_info[:2] >= (2, 5)
PY26PLUS = sys.version_info[:2] >= (2, 6) PY26PLUS = sys.version_info[:2] >= (2, 6)
PY32PLUS = sys.version_info[:2] >= (3, 2) PY32PLUS = sys.version_info[:2] >= (3, 2)
PY310PLUS = sys.version_info[:2] >= (3, 10)
# Begin import game to handle Python 2 and Python 3 # Begin import game to handle Python 2 and Python 3
try: try:
@ -266,17 +269,6 @@ else:
write(arg) write(arg)
write(end) write(end)
if PY32PLUS:
etree_iter = ET.Element.iter
elif PY25PLUS:
etree_iter = ET_Element.getiterator
if PY26PLUS:
thread_is_alive = threading.Thread.is_alive
else:
thread_is_alive = threading.Thread.isAlive
# Exception "constants" to support Python 2 through Python 3 # Exception "constants" to support Python 2 through Python 3
try: try:
import ssl import ssl
@ -293,6 +285,23 @@ except ImportError:
ssl = None ssl = None
HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine) HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine)
if PY32PLUS:
etree_iter = ET.Element.iter
elif PY25PLUS:
etree_iter = ET_Element.getiterator
if PY26PLUS:
thread_is_alive = threading.Thread.is_alive
else:
thread_is_alive = threading.Thread.isAlive
def event_is_set(event):
try:
return event.is_set()
except AttributeError:
return event.isSet()
class SpeedtestException(Exception): class SpeedtestException(Exception):
"""Base exception for this module""" """Base exception for this module"""
@ -644,23 +653,7 @@ def get_exception():
return sys.exc_info()[1] return sys.exc_info()[1]
def distance(origin, destination):
"""Determine distance between 2 sets of [lat,lon] in km"""
lat1, lon1 = origin
lat2, lon2 = destination
radius = 6371 # km
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = (math.sin(dlat / 2) * math.sin(dlat / 2) +
math.cos(math.radians(lat1)) *
math.cos(math.radians(lat2)) * math.sin(dlon / 2) *
math.sin(dlon / 2))
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
d = radius * c
return d
def build_user_agent(): def build_user_agent():
@ -769,7 +762,7 @@ def print_dots(shutdown_event):
status status
""" """
def inner(current, total, start=False, end=False): def inner(current, total, start=False, end=False):
if shutdown_event.isSet(): if event_is_set(shutdown_event):
return return
sys.stdout.write('.') sys.stdout.write('.')
@ -808,7 +801,7 @@ class HTTPDownloader(threading.Thread):
try: try:
if (timeit.default_timer() - self.starttime) <= self.timeout: if (timeit.default_timer() - self.starttime) <= self.timeout:
f = self._opener(self.request) f = self._opener(self.request)
while (not self._shutdown_event.isSet() and while (not event_is_set(self._shutdown_event) and
(timeit.default_timer() - self.starttime) <= (timeit.default_timer() - self.starttime) <=
self.timeout): self.timeout):
self.result.append(len(f.read(10240))) self.result.append(len(f.read(10240)))
@ -864,7 +857,7 @@ class HTTPUploaderData(object):
def read(self, n=10240): def read(self, n=10240):
if ((timeit.default_timer() - self.start) <= self.timeout and if ((timeit.default_timer() - self.start) <= self.timeout and
not self._shutdown_event.isSet()): not event_is_set(self._shutdown_event)):
chunk = self.data.read(n) chunk = self.data.read(n)
self.total.append(len(chunk)) self.total.append(len(chunk))
return chunk return chunk
@ -902,7 +895,7 @@ class HTTPUploader(threading.Thread):
request = self.request request = self.request
try: try:
if ((timeit.default_timer() - self.starttime) <= self.timeout and if ((timeit.default_timer() - self.starttime) <= self.timeout and
not self._shutdown_event.isSet()): not event_is_set(self._shutdown_event)):
try: try:
f = self._opener(request) f = self._opener(request)
except TypeError: except TypeError:
@ -948,7 +941,12 @@ class SpeedtestResults(object):
self.client = client or {} self.client = client or {}
self._share = None self._share = None
self.timestamp = '%sZ' % datetime.datetime.utcnow().isoformat() # datetime.datetime.utcnow() is deprecated starting from 3.12
# but datetime.UTC is supported starting from 3.11
if sys.version_info.major >= 3 and sys.version_info.minor >= 11:
self.timestamp = '%sZ' % datetime.datetime.now(datetime.UTC).isoformat()
else:
self.timestamp = '%sZ' % datetime.datetime.utcnow().isoformat()
self.bytes_received = 0 self.bytes_received = 0
self.bytes_sent = 0 self.bytes_sent = 0
@ -1074,6 +1072,25 @@ class SpeedtestResults(object):
return json.dumps(self.dict(), **kwargs) return json.dumps(self.dict(), **kwargs)
def parse_custom_server_response(response):
"""Parse the custom server response format and return a list of servers."""
servers = []
server_blocks = re.findall(r'{(.*?)}', response, re.DOTALL)
for block in server_blocks:
server = {}
for line in block.split('\n'):
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip().strip('"')
if key == 'serverid':
key = 'id'
server[key] = value
server["url"] = f"http://{server['host']}/speedtest/upload.php"
servers.append(server)
return servers
class Speedtest(object): class Speedtest(object):
"""Class for performing standard speedtest.net testing operations""" """Class for performing standard speedtest.net testing operations"""
@ -1096,7 +1113,7 @@ class Speedtest(object):
if config is not None: if config is not None:
self.config.update(config) self.config.update(config)
self.servers = {} self.servers = []
self.closest = [] self.closest = []
self._best = {} self._best = {}
@ -1229,8 +1246,8 @@ class Speedtest(object):
return self.config return self.config
def get_servers(self, servers=None, exclude=None): def get_servers(self, servers=None, exclude=None):
"""Retrieve a the list of speedtest.net servers, optionally filtered """Retrieve the list of speedtest.net servers from the new API URL,
to servers matching those specified in the ``servers`` argument optionally filtered to servers matching those specified in the `servers` argument
""" """
if servers is None: if servers is None:
servers = [] servers = []
@ -1249,105 +1266,67 @@ class Speedtest(object):
'%s is an invalid server type, must be int' % s '%s is an invalid server type, must be int' % s
) )
urls = [ url = 'https://www.speedtest.net/api/embed/vz0azjarf5enop8a/config' # New API URL
'://www.speedtest.net/speedtest-servers-static.php',
'http://c.speedtest.net/speedtest-servers-static.php',
'://www.speedtest.net/speedtest-servers.php',
'http://c.speedtest.net/speedtest-servers.php',
]
headers = {} headers = {}
if gzip: if gzip:
headers['Accept-Encoding'] = 'gzip' headers['Accept-Encoding'] = 'gzip'
errors = [] errors = []
for url in urls: try:
try: request = build_request(url, headers=headers, secure=self._secure)
request = build_request( uh, e = catch_request(request, opener=self._opener)
'%s?threads=%s' % (url,
self.config['threads']['download']), if e:
headers=headers, errors.append('%s' % e)
secure=self._secure raise ServersRetrievalError()
)
uh, e = catch_request(request, opener=self._opener)
if e:
errors.append('%s' % e)
raise ServersRetrievalError()
stream = get_response_stream(uh) stream = get_response_stream(uh)
serversxml_list = []
while 1:
try:
serversxml_list.append(stream.read(1024))
except (OSError, EOFError):
raise ServersRetrievalError(get_exception())
if len(serversxml_list[-1]) == 0:
break
stream.close()
uh.close()
if int(uh.code) != 200:
raise ServersRetrievalError()
serversxml = ''.encode().join(serversxml_list)
printer('Servers XML:\n%s' % serversxml, debug=True)
serversjson_list = []
while 1:
try: try:
try: serversjson_list.append(stream.read(1024))
try: except (OSError, EOFError):
root = ET.fromstring(serversxml) raise ServersRetrievalError(get_exception())
except ET.ParseError: if len(serversjson_list[-1]) == 0:
e = get_exception() break
raise SpeedtestServersError(
'Malformed speedtest.net server list: %s' % e
)
elements = etree_iter(root, 'server')
except AttributeError:
try:
root = DOM.parseString(serversxml)
except ExpatError:
e = get_exception()
raise SpeedtestServersError(
'Malformed speedtest.net server list: %s' % e
)
elements = root.getElementsByTagName('server')
except (SyntaxError, xml.parsers.expat.ExpatError):
raise ServersRetrievalError()
for server in elements: stream.close()
try: uh.close()
attrib = server.attrib
except AttributeError: if int(uh.code) != 200:
attrib = dict(list(server.attributes.items())) raise ServersRetrievalError()
if servers and int(attrib.get('id')) not in servers: serversjson = b''.join(serversjson_list)
continue
if not serversjson:
raise SpeedtestServersError('Empty server list received')
if (int(attrib.get('id')) in self.config['ignore_servers'] printer('Servers JSON:\n%s' % serversjson, debug=True)
or int(attrib.get('id')) in exclude): servers_response = serversjson.decode('utf-8')
continue
try:
elements = parse_custom_server_response(servers_response)
except Exception as e:
raise SpeedtestServersError(
'Malformed speedtest.net server list: %s' % e
)
for server in elements:
if servers and int(server.get('id')) not in servers:
continue
try: if (int(server.get('id')) in self.config['ignore_servers']
d = distance(self.lat_lon, or int(server.get('id')) in exclude):
(float(attrib.get('lat')), continue
float(attrib.get('lon'))))
except Exception:
continue
attrib['d'] = d self.servers.append(server)
try: except ServersRetrievalError:
self.servers[d].append(attrib) raise
except KeyError:
self.servers[d] = [attrib]
break
except ServersRetrievalError:
continue
if (servers or exclude) and not self.servers: if (servers or exclude) and not self.servers:
raise NoMatchedServers() raise NoMatchedServers()
@ -1416,14 +1395,7 @@ class Speedtest(object):
if not self.servers: if not self.servers:
self.get_servers() self.get_servers()
for d in sorted(self.servers.keys()): self.closest = self.servers[:limit]
for s in self.servers[d]:
self.closest.append(s)
if len(self.closest) == limit:
break
else:
continue
break
printer('Closest Servers:\n%r' % self.closest, debug=True) printer('Closest Servers:\n%r' % self.closest, debug=True)
return self.closest return self.closest
@ -1889,16 +1861,15 @@ def shell():
printer('Cannot retrieve speedtest server list', error=True) printer('Cannot retrieve speedtest server list', error=True)
raise SpeedtestCLIError(get_exception()) raise SpeedtestCLIError(get_exception())
for _, servers in sorted(speedtest.servers.items()): for server in speedtest.servers:
for server in servers:
line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) ' line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s)' % server)
'[%(d)0.2f km]' % server) try:
try: printer(line)
printer(line) except IOError:
except IOError: e = get_exception()
e = get_exception() if e.errno != errno.EPIPE:
if e.errno != errno.EPIPE: raise
raise
sys.exit(0) sys.exit(0)
printer('Testing from %(isp)s (%(ip)s)...' % speedtest.config['client'], printer('Testing from %(isp)s (%(ip)s)...' % speedtest.config['client'],
@ -1932,7 +1903,7 @@ def shell():
results = speedtest.results results = speedtest.results
printer('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: ' printer('Hosted by %(sponsor)s (%(name)s): '
'%(latency)s ms' % results.server, quiet) '%(latency)s ms' % results.server, quiet)
if args.download: if args.download: