# 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, # You can obtain one at http://mozilla.org/MPL/2.0/. 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): """ Hack to work around https://bugs.python.org/issue37380 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()