Source code
Revision control
Copy as Markdown
Other Tools
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
import copy
import os
import re
import subprocess
import sys
import time
from argparse import Namespace
from contextlib import contextmanager
from subprocess import PIPE, Popen
from threading import Thread
@contextmanager
def popenCleanupHack(isWin):
"""
The basic idea is that on old versions of Python on Windows,
we need to clear subprocess._cleanup before we call Popen(),
then restore it afterwards.
"""
savedCleanup = None
if isWin and sys.version_info[0] == 3 and sys.version_info < (3, 7, 5):
savedCleanup = subprocess._cleanup
subprocess._cleanup = lambda: None
try:
yield
finally:
if savedCleanup:
subprocess._cleanup = savedCleanup
class Http3Server(object):
"""
Class which encapsulates the Http3 server
"""
def __init__(self, options, env, logger):
if isinstance(options, Namespace):
options = vars(options)
self._log = logger
self._profileDir = options["profilePath"]
self._env = copy.deepcopy(env)
self._ports = {}
self._echConfig = ""
self._isMochitest = options["isMochitest"]
self._http3ServerPath = options["http3ServerPath"]
self._isWin = options["isWin"]
self._http3ServerProc = {}
self._proxyPort = -1
if options.get("proxyPort"):
self._proxyPort = options["proxyPort"]
def ports(self):
return self._ports
def echConfig(self):
return self._echConfig
def read_streams(self, name, proc, pipe):
output = "stdout" if pipe == proc.stdout else "stderr"
for line in iter(pipe.readline, ""):
self._log.info("server: %s [%s] %s" % (name, output, line))
def start(self):
if not os.path.exists(self._http3ServerPath):
raise Exception("Http3 server not found at %s" % self._http3ServerPath)
self._log.info("mozserve | Found Http3Server path: %s" % self._http3ServerPath)
dbPath = os.path.join(self._profileDir, "cert9.db")
if not os.path.exists(dbPath):
raise Exception("cert db not found at %s" % dbPath)
dbPath = self._profileDir
self._log.info("mozserve | cert db path: %s" % dbPath)
try:
if self._isMochitest:
self._env["MOZ_HTTP3_MOCHITEST"] = "1"
if self._proxyPort != -1:
self._env["MOZ_HTTP3_PROXY_PORT"] = str(self._proxyPort)
with popenCleanupHack(self._isWin):
process = Popen(
[self._http3ServerPath, dbPath],
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
env=self._env,
cwd=os.getcwd(),
universal_newlines=True,
)
self._http3ServerProc["http3Server"] = process
# Check to make sure the server starts properly by waiting for it to
# tell us it's started
msg = process.stdout.readline()
self._log.info("mozserve | http3 server msg: %s" % msg)
name = "http3server"
t1 = Thread(
target=self.read_streams,
args=(name, process, process.stdout),
daemon=True,
)
t1.start()
t2 = Thread(
target=self.read_streams,
args=(name, process, process.stderr),
daemon=True,
)
t2.start()
if "server listening" in msg:
searchObj = re.search(
r"HTTP3 server listening on ports ([0-9]+), ([0-9]+), ([0-9]+), ([0-9]+) and ([0-9]+)."
" EchConfig is @([\x00-\x7F]+)@",
msg,
0,
)
if searchObj:
self._ports["MOZHTTP3_PORT"] = searchObj.group(1)
self._ports["MOZHTTP3_PORT_FAILED"] = searchObj.group(2)
self._ports["MOZHTTP3_PORT_ECH"] = searchObj.group(3)
self._ports["MOZHTTP3_PORT_PROXY"] = searchObj.group(4)
self._ports["MOZHTTP3_PORT_NO_RESPONSE"] = searchObj.group(5)
self._echConfig = searchObj.group(6)
else:
self._log.error("http3server failed to start?")
except OSError as e:
# This occurs if the subprocess couldn't be started
self._log.error("Could not run the http3 server: %s" % (str(e)))
def stop(self):
"""
Shutdown our http3Server process, if it exists
"""
for name, proc in self._http3ServerProc.items():
self._log.info("%s server shutting down ..." % name)
if proc.poll() is not None:
self._log.info("Http3 server %s already dead %s" % (name, proc.poll()))
else:
proc.terminate()
retries = 0
while proc.poll() is None:
time.sleep(0.1)
retries += 1
if retries > 40:
self._log.info("Killing proc")
proc.kill()
break
self._http3ServerProc = {}
class NodeHttp2Server(object):
"""
Class which encapsulates a Node Http/2 server
"""
def __init__(self, name, options, env, logger):
if isinstance(options, Namespace):
options = vars(options)
self._name = name
self._log = logger
self._port = options["port"]
self._env = copy.deepcopy(env)
self._nodeBin = options["nodeBin"]
self._serverPath = options["serverPath"]
self._dstServerPort = options["dstServerPort"]
self._isWin = options["isWin"]
self._nodeProc = None
self._searchStr = options["searchStr"]
self._alpn = options["alpn"]
def port(self):
return self._port
def start(self):
if not os.path.exists(self._serverPath):
raise Exception(
"%s server not found at %s" % (self._name, self._serverPath)
)
self._log.info(
"mozserve | Found %s server path: %s" % (self._name, self._serverPath)
)
if not os.path.exists(self._nodeBin) or not os.path.isfile(self._nodeBin):
raise Exception("node not found at path %s" % (self._nodeBin))
self._log.info("Found node at %s" % (self._nodeBin))
try:
# We pipe stdin to node because the server will exit when its
# stdin reaches EOF
with popenCleanupHack(self._isWin):
process = Popen(
[
self._nodeBin,
self._serverPath,
"serverPort={}".format(self._dstServerPort),
"listeningPort={}".format(self._port),
"alpn={}".format(self._alpn),
],
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
env=self._env,
cwd=os.getcwd(),
universal_newlines=True,
)
self._nodeProc = process
msg = process.stdout.readline()
self._log.info("runtests.py | %s server msg: %s" % (self._name, msg))
if "server listening" in msg:
searchObj = re.search(self._searchStr, msg, 0)
if searchObj:
self._port = int(searchObj.group(1))
self._log.info(
"%s server started at port: %d" % (self._name, self._port)
)
except OSError as e:
# This occurs if the subprocess couldn't be started
self._log.error("Could not run %s server: %s" % (self._name, str(e)))
def stop(self):
"""
Shut down our node process, if it exists
"""
if self._nodeProc is not None:
if self._nodeProc.poll() is not None:
self._log.info("Node server already dead %s" % (self._nodeProc.poll()))
else:
self._nodeProc.terminate()
def dumpOutput(fd, label):
firstTime = True
for msg in fd:
if firstTime:
firstTime = False
self._log.info("Process %s" % label)
self._log.info(msg)
dumpOutput(self._nodeProc.stdout, "stdout")
dumpOutput(self._nodeProc.stderr, "stderr")
self._nodeProc = None
class DoHServer(object):
"""
Class which encapsulates the DoH server
"""
def __init__(self, options, env, logger):
options["searchStr"] = r"DoH server listening on ports ([0-9]+)"
self._server = NodeHttp2Server("DoH", options, env, logger)
def port(self):
return self._server.port()
def start(self):
self._server.start()
def stop(self):
self._server.stop()
class Http2Server(object):
"""
Class which encapsulates the Http2 server
"""
def __init__(self, options, env, logger):
options["searchStr"] = r"Http2 server listening on ports ([0-9]+)"
options["dstServerPort"] = -1
options["alpn"] = ""
self._server = NodeHttp2Server("Http/2", options, env, logger)
def port(self):
return self._server.port()
def start(self):
self._server.start()
def stop(self):
self._server.stop()