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_VERSION:=2.3.0
PKG_RELEASE:=20250104
PKG_VERSION:=2.3.1
PKG_RELEASE:=20250302
LUCI_TITLE:=LuCI Support for netspeedtest
LUCI_DEPENDS:=+python3 +iperf3-ssl +homebox

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2012 Matt Martz
# All Rights Reserved.
@ -15,18 +15,18 @@
# License for the specific language governing permissions and limitations
# under the License.
import os
import re
import csv
import sys
import math
import datetime
import errno
import math
import os
import platform
import re
import signal
import socket
import timeit
import datetime
import platform
import sys
import threading
import timeit
import xml.parsers.expat
try:
@ -36,7 +36,7 @@ except ImportError:
gzip = None
GZIP_BASE = object
__version__ = '2.1.3'
__version__ = '2.1.4b2'
class FakeShutdownEvent(object):
@ -49,6 +49,8 @@ class FakeShutdownEvent(object):
"Dummy method to always return false"""
return False
is_set = isSet
# Some global variables we use
DEBUG = False
@ -56,6 +58,7 @@ _GLOBAL_DEFAULT_TIMEOUT = object()
PY25PLUS = sys.version_info[:2] >= (2, 5)
PY26PLUS = sys.version_info[:2] >= (2, 6)
PY32PLUS = sys.version_info[:2] >= (3, 2)
PY310PLUS = sys.version_info[:2] >= (3, 10)
# Begin import game to handle Python 2 and Python 3
try:
@ -266,17 +269,6 @@ else:
write(arg)
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
try:
import ssl
@ -293,6 +285,23 @@ except ImportError:
ssl = None
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):
"""Base exception for this module"""
@ -644,23 +653,7 @@ def get_exception():
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():
@ -769,7 +762,7 @@ def print_dots(shutdown_event):
status
"""
def inner(current, total, start=False, end=False):
if shutdown_event.isSet():
if event_is_set(shutdown_event):
return
sys.stdout.write('.')
@ -808,7 +801,7 @@ class HTTPDownloader(threading.Thread):
try:
if (timeit.default_timer() - self.starttime) <= self.timeout:
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) <=
self.timeout):
self.result.append(len(f.read(10240)))
@ -864,7 +857,7 @@ class HTTPUploaderData(object):
def read(self, n=10240):
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)
self.total.append(len(chunk))
return chunk
@ -902,7 +895,7 @@ class HTTPUploader(threading.Thread):
request = self.request
try:
if ((timeit.default_timer() - self.starttime) <= self.timeout and
not self._shutdown_event.isSet()):
not event_is_set(self._shutdown_event)):
try:
f = self._opener(request)
except TypeError:
@ -948,7 +941,12 @@ class SpeedtestResults(object):
self.client = client or {}
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_sent = 0
@ -1074,6 +1072,25 @@ class SpeedtestResults(object):
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 for performing standard speedtest.net testing operations"""
@ -1096,7 +1113,7 @@ class Speedtest(object):
if config is not None:
self.config.update(config)
self.servers = {}
self.servers = []
self.closest = []
self._best = {}
@ -1229,8 +1246,8 @@ class Speedtest(object):
return self.config
def get_servers(self, servers=None, exclude=None):
"""Retrieve a the list of speedtest.net servers, optionally filtered
to servers matching those specified in the ``servers`` argument
"""Retrieve the list of speedtest.net servers from the new API URL,
optionally filtered to servers matching those specified in the `servers` argument
"""
if servers is None:
servers = []
@ -1249,105 +1266,67 @@ class Speedtest(object):
'%s is an invalid server type, must be int' % s
)
urls = [
'://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',
]
url = 'https://www.speedtest.net/api/embed/vz0azjarf5enop8a/config' # New API URL
headers = {}
if gzip:
headers['Accept-Encoding'] = 'gzip'
errors = []
for url in urls:
try:
request = build_request(
'%s?threads=%s' % (url,
self.config['threads']['download']),
headers=headers,
secure=self._secure
)
uh, e = catch_request(request, opener=self._opener)
if e:
errors.append('%s' % e)
raise ServersRetrievalError()
try:
request = build_request(url, headers=headers, secure=self._secure)
uh, e = catch_request(request, opener=self._opener)
if e:
errors.append('%s' % e)
raise ServersRetrievalError()
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)
stream = get_response_stream(uh)
serversjson_list = []
while 1:
try:
try:
try:
root = ET.fromstring(serversxml)
except ET.ParseError:
e = get_exception()
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()
serversjson_list.append(stream.read(1024))
except (OSError, EOFError):
raise ServersRetrievalError(get_exception())
if len(serversjson_list[-1]) == 0:
break
for server in elements:
try:
attrib = server.attrib
except AttributeError:
attrib = dict(list(server.attributes.items()))
stream.close()
uh.close()
if int(uh.code) != 200:
raise ServersRetrievalError()
if servers and int(attrib.get('id')) not in servers:
continue
serversjson = b''.join(serversjson_list)
if not serversjson:
raise SpeedtestServersError('Empty server list received')
if (int(attrib.get('id')) in self.config['ignore_servers']
or int(attrib.get('id')) in exclude):
continue
printer('Servers JSON:\n%s' % serversjson, debug=True)
servers_response = serversjson.decode('utf-8')
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:
d = distance(self.lat_lon,
(float(attrib.get('lat')),
float(attrib.get('lon'))))
except Exception:
continue
if (int(server.get('id')) in self.config['ignore_servers']
or int(server.get('id')) in exclude):
continue
attrib['d'] = d
try:
self.servers[d].append(attrib)
except KeyError:
self.servers[d] = [attrib]
break
except ServersRetrievalError:
continue
self.servers.append(server)
except ServersRetrievalError:
raise
if (servers or exclude) and not self.servers:
raise NoMatchedServers()
@ -1416,14 +1395,7 @@ class Speedtest(object):
if not self.servers:
self.get_servers()
for d in sorted(self.servers.keys()):
for s in self.servers[d]:
self.closest.append(s)
if len(self.closest) == limit:
break
else:
continue
break
self.closest = self.servers[:limit]
printer('Closest Servers:\n%r' % self.closest, debug=True)
return self.closest
@ -1889,16 +1861,15 @@ def shell():
printer('Cannot retrieve speedtest server list', error=True)
raise SpeedtestCLIError(get_exception())
for _, servers in sorted(speedtest.servers.items()):
for server in servers:
line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) '
'[%(d)0.2f km]' % server)
try:
printer(line)
except IOError:
e = get_exception()
if e.errno != errno.EPIPE:
raise
for server in speedtest.servers:
line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s)' % server)
try:
printer(line)
except IOError:
e = get_exception()
if e.errno != errno.EPIPE:
raise
sys.exit(0)
printer('Testing from %(isp)s (%(ip)s)...' % speedtest.config['client'],
@ -1932,7 +1903,7 @@ def shell():
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)
if args.download: