mirror of
https://github.com/certbot/certbot.git
synced 2026-08-01 02:44:21 +02:00
Remove SimpleHTTP TLS from acme.
This commit is contained in:
@@ -70,20 +70,10 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
|
|||||||
class FakeConnection(object):
|
class FakeConnection(object):
|
||||||
"""Fake OpenSSL.SSL.Connection."""
|
"""Fake OpenSSL.SSL.Connection."""
|
||||||
|
|
||||||
MAKEFILE_SUPPORT = hasattr(socket, "_fileobject")
|
|
||||||
"""Is `makefile` supported on your platform?
|
|
||||||
|
|
||||||
.. warning:: `makefile`, as currently implemented, is supported
|
|
||||||
on select platforms only, as it uses CPython's internal API.
|
|
||||||
You've been warned!
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# pylint: disable=missing-docstring
|
# pylint: disable=missing-docstring
|
||||||
|
|
||||||
def __init__(self, connection):
|
def __init__(self, connection):
|
||||||
self._wrapped = connection
|
self._wrapped = connection
|
||||||
self._makefile_refs = 0
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
return getattr(self._wrapped, name)
|
return getattr(self._wrapped, name)
|
||||||
@@ -92,27 +82,6 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
|
|||||||
# OpenSSL.SSL.Connection.shutdown doesn't accept any args
|
# OpenSSL.SSL.Connection.shutdown doesn't accept any args
|
||||||
return self._wrapped.shutdown()
|
return self._wrapped.shutdown()
|
||||||
|
|
||||||
# stuff below ripped off from
|
|
||||||
# https://hg.python.org/cpython/file/2.7/Lib/ssl.py
|
|
||||||
|
|
||||||
def makefile(self, mode='r', bufsize=-1):
|
|
||||||
assert self.MAKEFILE_SUPPORT, (
|
|
||||||
"You need compatible version for makefile support")
|
|
||||||
self._makefile_refs += 1
|
|
||||||
# SocketServer.StreamRequesthandler.finish will try to
|
|
||||||
# close the wfile/rfile. close=True causes curl: (56)
|
|
||||||
# GnuTLS recv error (-110): The TLS connection was
|
|
||||||
# non-properly terminated.
|
|
||||||
# TODO: doesn't work in Python3
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
return socket._fileobject(self._wrapped, mode, bufsize, close=False)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if self._makefile_refs < 1:
|
|
||||||
self._wrapped.close()
|
|
||||||
else:
|
|
||||||
self._makefile_refs -= 1
|
|
||||||
|
|
||||||
def accept(self): # pylint: disable=missing-docstring
|
def accept(self): # pylint: disable=missing-docstring
|
||||||
sock, addr = self.sock.accept()
|
sock, addr = self.sock.accept()
|
||||||
|
|
||||||
|
|||||||
+19
-50
@@ -44,15 +44,7 @@ class TLSServer(socketserver.TCPServer):
|
|||||||
return socketserver.TCPServer.server_bind(self)
|
return socketserver.TCPServer.server_bind(self)
|
||||||
|
|
||||||
|
|
||||||
class HTTPSServer(TLSServer, BaseHTTPServer.HTTPServer):
|
class ACMEServerMixin: # pylint: disable=old-style-class
|
||||||
"""HTTPS Server."""
|
|
||||||
|
|
||||||
def server_bind(self):
|
|
||||||
self._wrap_sock()
|
|
||||||
BaseHTTPServer.HTTPServer.server_bind(self)
|
|
||||||
|
|
||||||
|
|
||||||
class ACMEServerMixin: # pylint: disable=old-style-class,no-init
|
|
||||||
"""ACME server common settings mixin."""
|
"""ACME server common settings mixin."""
|
||||||
server_version = "ACME standalone client"
|
server_version = "ACME standalone client"
|
||||||
allow_reuse_address = True
|
allow_reuse_address = True
|
||||||
@@ -81,27 +73,23 @@ class ACMEServerMixin: # pylint: disable=old-style-class,no-init
|
|||||||
self.server_close()
|
self.server_close()
|
||||||
|
|
||||||
|
|
||||||
class ACMETLSServer(HTTPSServer, ACMEServerMixin):
|
class DVSNIServer(TLSServer, ACMEServerMixin):
|
||||||
"""ACME TLS Server."""
|
"""DVSNI Server."""
|
||||||
|
|
||||||
SIMPLE_HTTP_SUPPORT = crypto_util.SSLSocket.FakeConnection.MAKEFILE_SUPPORT
|
def __init__(self, server_address, certs):
|
||||||
"""Is SimpleHTTP supported on your platform.
|
|
||||||
|
|
||||||
Please see a warning for `acme.crypto_util.SSLSocket.FakeConnection`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
ACMEServerMixin.__init__(self)
|
ACMEServerMixin.__init__(self)
|
||||||
HTTPSServer.__init__(self, *args, **kwargs)
|
TLSServer.__init__(
|
||||||
|
self, server_address, socketserver.BaseRequestHandler, certs=certs)
|
||||||
|
|
||||||
|
|
||||||
class ACMEServer(BaseHTTPServer.HTTPServer, ACMEServerMixin):
|
class SimpleHTTPServer(BaseHTTPServer.HTTPServer, ACMEServerMixin):
|
||||||
"""ACME Server (non-TLS)."""
|
"""SimpleHTTP Server."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, server_address, resources):
|
||||||
ACMEServerMixin.__init__(self)
|
ACMEServerMixin.__init__(self)
|
||||||
BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
|
BaseHTTPServer.HTTPServer.__init__(
|
||||||
|
self, server_address, SimpleHTTPRequestHandler.partial_init(
|
||||||
|
simple_http_resources=resources))
|
||||||
|
|
||||||
|
|
||||||
class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
@@ -133,14 +121,14 @@ class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
|||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-type", "text/html")
|
self.send_header("Content-type", "text/html")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(self.server.server_version)
|
self.wfile.write(self.server.server_version.encode())
|
||||||
|
|
||||||
def handle_404(self):
|
def handle_404(self):
|
||||||
"""Handler 404 Not Found errors."""
|
"""Handler 404 Not Found errors."""
|
||||||
self.send_response(http_client.NOT_FOUND, message="Not Found")
|
self.send_response(http_client.NOT_FOUND, message="Not Found")
|
||||||
self.send_header("Content-type", "text/html")
|
self.send_header("Content-type", "text/html")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write("404")
|
self.wfile.write(b"404")
|
||||||
|
|
||||||
def handle_simple_http_resource(self):
|
def handle_simple_http_resource(self):
|
||||||
"""Handle SimpleHTTP provisioned resources."""
|
"""Handle SimpleHTTP provisioned resources."""
|
||||||
@@ -171,24 +159,8 @@ class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
|||||||
cls, simple_http_resources=simple_http_resources)
|
cls, simple_http_resources=simple_http_resources)
|
||||||
|
|
||||||
|
|
||||||
class ACMERequestHandler(SimpleHTTPRequestHandler):
|
def simple_dvsni_server(cli_args, forever=True):
|
||||||
"""ACME request handler."""
|
"""Run simple standalone DVSNI server."""
|
||||||
|
|
||||||
def handle_one_request(self):
|
|
||||||
"""Handle single request.
|
|
||||||
|
|
||||||
Makes sure that DVSNI probers are ignored.
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return SimpleHTTPRequestHandler.handle_one_request(self)
|
|
||||||
except OpenSSL.SSL.ZeroReturnError:
|
|
||||||
logger.debug("Client prematurely closed connection (prober?). "
|
|
||||||
"Ignoring request.")
|
|
||||||
|
|
||||||
|
|
||||||
def simple_server(cli_args, forever=True):
|
|
||||||
"""Run simple standalone client server."""
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
@@ -198,7 +170,6 @@ def simple_server(cli_args, forever=True):
|
|||||||
args = parser.parse_args(cli_args[1:])
|
args = parser.parse_args(cli_args[1:])
|
||||||
|
|
||||||
certs = {}
|
certs = {}
|
||||||
resources = {}
|
|
||||||
|
|
||||||
_, hosts, _ = next(os.walk('.'))
|
_, hosts, _ = next(os.walk('.'))
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
@@ -206,15 +177,13 @@ def simple_server(cli_args, forever=True):
|
|||||||
cert_contents = cert_file.read()
|
cert_contents = cert_file.read()
|
||||||
with open(os.path.join(host, "key.pem")) as key_file:
|
with open(os.path.join(host, "key.pem")) as key_file:
|
||||||
key_contents = key_file.read()
|
key_contents = key_file.read()
|
||||||
certs[host] = (
|
certs[host.encode()] = (
|
||||||
OpenSSL.crypto.load_privatekey(
|
OpenSSL.crypto.load_privatekey(
|
||||||
OpenSSL.crypto.FILETYPE_PEM, key_contents),
|
OpenSSL.crypto.FILETYPE_PEM, key_contents),
|
||||||
OpenSSL.crypto.load_certificate(
|
OpenSSL.crypto.load_certificate(
|
||||||
OpenSSL.crypto.FILETYPE_PEM, cert_contents))
|
OpenSSL.crypto.FILETYPE_PEM, cert_contents))
|
||||||
|
|
||||||
handler = ACMERequestHandler.partial_init(
|
server = DVSNIServer(('', int(args.port)), certs=certs)
|
||||||
simple_http_resources=resources)
|
|
||||||
server = ACMETLSServer(('', int(args.port)), handler, certs=certs)
|
|
||||||
six.print_("Serving at https://localhost:{0}...".format(
|
six.print_("Serving at https://localhost:{0}...".format(
|
||||||
server.socket.getsockname()[1]))
|
server.socket.getsockname()[1]))
|
||||||
if forever: # pragma: no cover
|
if forever: # pragma: no cover
|
||||||
@@ -224,4 +193,4 @@ def simple_server(cli_args, forever=True):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(simple_server(sys.argv)) # pragma: no cover
|
sys.exit(simple_dvsni_server(sys.argv)) # pragma: no cover
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import requests
|
|||||||
|
|
||||||
from acme import challenges
|
from acme import challenges
|
||||||
from acme import crypto_util
|
from acme import crypto_util
|
||||||
|
from acme import errors
|
||||||
from acme import jose
|
from acme import jose
|
||||||
from acme import test_util
|
from acme import test_util
|
||||||
|
|
||||||
@@ -30,25 +31,27 @@ class TLSServerTest(unittest.TestCase):
|
|||||||
class ACMEServerMixinTest(unittest.TestCase):
|
class ACMEServerMixinTest(unittest.TestCase):
|
||||||
"""Tests for acme.standalone.ACMEServerMixin."""
|
"""Tests for acme.standalone.ACMEServerMixin."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
from acme.standalone import ACMEServerMixin
|
||||||
|
|
||||||
|
class _MockServer(socketserver.TCPServer, ACMEServerMixin):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
socketserver.TCPServer.__init__(self, *args, **kwargs)
|
||||||
|
ACMEServerMixin.__init__(self)
|
||||||
|
self.server = _MockServer(("", 0), socketserver.BaseRequestHandler)
|
||||||
|
|
||||||
|
def test_serve_shutdown(self):
|
||||||
|
thread = threading.Thread(target=self.server.serve_forever2)
|
||||||
|
thread.start()
|
||||||
|
self.server.shutdown2()
|
||||||
|
|
||||||
def test_shutdown2_not_running(self):
|
def test_shutdown2_not_running(self):
|
||||||
from acme.standalone import ACMEServer
|
self.server.shutdown2()
|
||||||
server = ACMEServer(("", 0), socketserver.BaseRequestHandler)
|
self.server.shutdown2()
|
||||||
server.shutdown2()
|
|
||||||
server.shutdown2()
|
|
||||||
|
|
||||||
|
|
||||||
class ACMEServerTest(unittest.TestCase):
|
class DVSNIServerTest(unittest.TestCase):
|
||||||
"""Test for acme.standalone.ACMEServer."""
|
"""Test for acme.standalone.DVSNIServer."""
|
||||||
|
|
||||||
def test_init(self):
|
|
||||||
from acme.standalone import ACMEServer
|
|
||||||
server = ACMEServer(("", 0), socketserver.BaseRequestHandler)
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
self.assertFalse(server._stopped)
|
|
||||||
|
|
||||||
|
|
||||||
class ACMESimpleHTTPTLSServerTestEndToEnd(unittest.TestCase):
|
|
||||||
"""End-to-end test for ACME TLS server with SimpleHTTP."""
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.certs = {
|
self.certs = {
|
||||||
@@ -56,36 +59,19 @@ class ACMESimpleHTTPTLSServerTestEndToEnd(unittest.TestCase):
|
|||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
test_util.load_cert('cert.pem')._wrapped),
|
test_util.load_cert('cert.pem')._wrapped),
|
||||||
}
|
}
|
||||||
self.account_key = jose.JWK.load(
|
from acme.standalone import DVSNIServer
|
||||||
test_util.load_vector('rsa1024_key.pem'))
|
self.server = DVSNIServer(("", 0), certs=self.certs)
|
||||||
|
# pylint: disable=no-member
|
||||||
from acme.standalone import ACMETLSServer
|
self.thread = threading.Thread(target=self.server.handle_request)
|
||||||
from acme.standalone import ACMERequestHandler
|
self.thread.start()
|
||||||
self.resources = set()
|
|
||||||
handler = ACMERequestHandler.partial_init(
|
|
||||||
simple_http_resources=self.resources)
|
|
||||||
self.server = ACMETLSServer(('', 0), handler, certs=self.certs)
|
|
||||||
self.server_thread = threading.Thread(
|
|
||||||
# pylint: disable=no-member
|
|
||||||
target=self.server.serve_forever2)
|
|
||||||
self.server_thread.start()
|
|
||||||
|
|
||||||
self.port = self.server.socket.getsockname()[1]
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.server.shutdown2()
|
self.server.shutdown2()
|
||||||
self.server_thread.join()
|
self.thread.join()
|
||||||
|
|
||||||
def test_index(self):
|
def test_init(self):
|
||||||
response = requests.get(
|
# pylint: disable=protected-access
|
||||||
'https://localhost:{0}'.format(self.port), verify=False)
|
self.assertFalse(self.server._stopped)
|
||||||
self.assertEqual(response.text, 'ACME standalone client')
|
|
||||||
self.assertTrue(response.ok)
|
|
||||||
|
|
||||||
def test_404(self):
|
|
||||||
response = requests.get(
|
|
||||||
'https://localhost:{0}/foo'.format(self.port), verify=False)
|
|
||||||
self.assertEqual(response.status_code, http_client.NOT_FOUND)
|
|
||||||
|
|
||||||
def test_dvsni(self):
|
def test_dvsni(self):
|
||||||
cert = crypto_util.probe_sni(
|
cert = crypto_util.probe_sni(
|
||||||
@@ -93,9 +79,41 @@ class ACMESimpleHTTPTLSServerTestEndToEnd(unittest.TestCase):
|
|||||||
self.assertEqual(jose.ComparableX509(cert),
|
self.assertEqual(jose.ComparableX509(cert),
|
||||||
jose.ComparableX509(self.certs[b'localhost'][1]))
|
jose.ComparableX509(self.certs[b'localhost'][1]))
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleHTTPServerTest(unittest.TestCase):
|
||||||
|
"""Tests for acme.standalone.SimpleHTTPServer."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.account_key = jose.JWK.load(
|
||||||
|
test_util.load_vector('rsa1024_key.pem'))
|
||||||
|
self.resources = set()
|
||||||
|
|
||||||
|
from acme.standalone import SimpleHTTPServer
|
||||||
|
self.server = SimpleHTTPServer(('', 0), resources=self.resources)
|
||||||
|
|
||||||
|
# pylint: disable=no-member
|
||||||
|
self.port = self.server.socket.getsockname()[1]
|
||||||
|
self.thread = threading.Thread(target=self.server.handle_request)
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.server.shutdown2()
|
||||||
|
self.thread.join()
|
||||||
|
|
||||||
|
def test_index(self):
|
||||||
|
response = requests.get(
|
||||||
|
'http://localhost:{0}'.format(self.port), verify=False)
|
||||||
|
self.assertEqual(response.text, 'ACME standalone client')
|
||||||
|
self.assertTrue(response.ok)
|
||||||
|
|
||||||
|
def test_404(self):
|
||||||
|
response = requests.get(
|
||||||
|
'http://localhost:{0}/foo'.format(self.port), verify=False)
|
||||||
|
self.assertEqual(response.status_code, http_client.NOT_FOUND)
|
||||||
|
|
||||||
def _test_simple_http(self, add):
|
def _test_simple_http(self, add):
|
||||||
chall = challenges.SimpleHTTP(token=(b'x' * 16))
|
chall = challenges.SimpleHTTP(token=(b'x' * 16))
|
||||||
response = challenges.SimpleHTTPResponse(tls=True)
|
response = challenges.SimpleHTTPResponse(tls=False)
|
||||||
|
|
||||||
from acme.standalone import SimpleHTTPRequestHandler
|
from acme.standalone import SimpleHTTPRequestHandler
|
||||||
resource = SimpleHTTPRequestHandler.SimpleHTTPResource(
|
resource = SimpleHTTPRequestHandler.SimpleHTTPResource(
|
||||||
@@ -114,8 +132,8 @@ class ACMESimpleHTTPTLSServerTestEndToEnd(unittest.TestCase):
|
|||||||
self.assertFalse(self._test_simple_http(add=False))
|
self.assertFalse(self._test_simple_http(add=False))
|
||||||
|
|
||||||
|
|
||||||
class TestSimpleServer(unittest.TestCase):
|
class TestSimpleDVSNIServer(unittest.TestCase):
|
||||||
"""Tests for acme.standalone.simple_server."""
|
"""Tests for acme.standalone.simple_dvsni_server."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# mirror ../examples/standalone
|
# mirror ../examples/standalone
|
||||||
@@ -126,9 +144,10 @@ class TestSimpleServer(unittest.TestCase):
|
|||||||
shutil.copy(test_util.vector_path('rsa512_key.pem'),
|
shutil.copy(test_util.vector_path('rsa512_key.pem'),
|
||||||
os.path.join(localhost_dir, 'key.pem'))
|
os.path.join(localhost_dir, 'key.pem'))
|
||||||
|
|
||||||
from acme.standalone import simple_server
|
from acme.standalone import simple_dvsni_server
|
||||||
self.thread = threading.Thread(target=simple_server, kwargs={
|
self.port = 1234
|
||||||
'cli_args': ('xxx', '--port', '1234'),
|
self.thread = threading.Thread(target=simple_dvsni_server, kwargs={
|
||||||
|
'cli_args': ('xxx', '--port', str(self.port)),
|
||||||
'forever': False,
|
'forever': False,
|
||||||
})
|
})
|
||||||
self.old_cwd = os.getcwd()
|
self.old_cwd = os.getcwd()
|
||||||
@@ -145,12 +164,13 @@ class TestSimpleServer(unittest.TestCase):
|
|||||||
while max_attempts:
|
while max_attempts:
|
||||||
max_attempts -= 1
|
max_attempts -= 1
|
||||||
try:
|
try:
|
||||||
response = requests.get('https://localhost:1234', verify=False)
|
cert = crypto_util.probe_sni(b'localhost', b'0.0.0.0', self.port)
|
||||||
except requests.ConnectionError:
|
except errors.Error:
|
||||||
self.assertTrue(max_attempts > 0, "Timeout!")
|
self.assertTrue(max_attempts > 0, "Timeout!")
|
||||||
time.sleep(1) # wait until thread starts
|
time.sleep(1) # wait until thread starts
|
||||||
else:
|
else:
|
||||||
self.assertEqual(response.text, 'ACME standalone client')
|
self.assertEqual(jose.ComparableX509(cert),
|
||||||
|
test_util.load_cert('cert.pem'))
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Standalone Authenticator."""
|
"""Standalone Authenticator."""
|
||||||
import argparse
|
import argparse
|
||||||
import collections
|
import collections
|
||||||
import functools
|
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import socket
|
import socket
|
||||||
@@ -61,25 +60,19 @@ class ServerManager(object):
|
|||||||
if port in self._instances:
|
if port in self._instances:
|
||||||
return self._instances[port].server
|
return self._instances[port].server
|
||||||
|
|
||||||
logger.debug("Starting new server at %s (tls=%s)", port, tls)
|
address = ("", port)
|
||||||
handler = acme_standalone.ACMERequestHandler.partial_init(
|
|
||||||
self.simple_http_resources)
|
|
||||||
|
|
||||||
if tls:
|
|
||||||
cls = functools.partial(
|
|
||||||
acme_standalone.ACMETLSServer, certs=self.certs)
|
|
||||||
else:
|
|
||||||
cls = acme_standalone.ACMEServer
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server = cls(("", port), handler)
|
if tls:
|
||||||
|
server = acme_standalone.DVSNIServer(address, self.certs)
|
||||||
|
else:
|
||||||
|
server = acme_standalone.SimpleHTTPServer(
|
||||||
|
address, self.simple_http_resources)
|
||||||
except socket.error as error:
|
except socket.error as error:
|
||||||
raise errors.StandaloneBindError(error, port)
|
raise errors.StandaloneBindError(error, port)
|
||||||
|
|
||||||
# if port == 0, then random free port on OS is taken
|
# if port == 0, then random free port on OS is taken
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
host, real_port = server.socket.getsockname()
|
host, real_port = server.socket.getsockname()
|
||||||
|
|
||||||
thread = threading.Thread(target=server.serve_forever2)
|
thread = threading.Thread(target=server.serve_forever2)
|
||||||
logger.debug("Starting server at %s:%d", host, real_port)
|
logger.debug("Starting server at %s:%d", host, real_port)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|||||||
Reference in New Issue
Block a user