qtbase/util/wasm/qtwasmserver/qtwasmserver.py
Piotr Wiercinski caa0aa3fb4 wasm: Fix Brotli compression in qtwasmserver.py
There is not compress() function in brotli.Compressor API.
Use process().
Use requirements.txt instead of Pipfile.

Change-Id: I55a0263f16f36bcb4b96e443f85925b7d5dd15af
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
2025-03-26 19:35:48 +01:00

310 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import argparse
import os
import ssl
import subprocess
import tempfile
import threading
from enum import Enum
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from subprocess import run
from functools import partial
import brotli
import netifaces as ni
from httpcompressionserver import HTTPCompressionRequestHandler
def generate_mkcert_certificate(addresses):
""" "Generates a https certificate for localhost and selected addresses. This
requires that the mkcert utility is installed, and that a certificate
authority key pair (rootCA-key.pem and rootCA.pem) has been generated. The
certificates are written to /tmp, where the http server can find them
ater on."""
cert_file = tempfile.NamedTemporaryFile(
mode="w+b", prefix="qtwasmserver-certificate-", suffix=".pem", delete=True
)
cert_key_file = tempfile.NamedTemporaryFile(
mode="w+b", prefix="qtwasmserver-certificate-key-", suffix=".pem", delete=True
)
# check if mkcert is installed
try:
out = subprocess.check_output(["mkcert", "-CAROOT"])
root_ca_path = out.decode("utf-8").strip()
print(
"Generating certificates with mkcert, using certificate authority files at:"
)
print(f" {root_ca_path} [from 'mkcert -CAROOT'] \n")
except Exception as e:
print("Warning: Unable to run mkcert. Will not start https server.")
print(e)
print(f"Install mkcert from github.com/FiloSottile/mkcert to fix this.\n")
return False, None, None
# generate certificates using mkcert
addresses_string = f"localhost {' '.join(addresses)}"
print("=== begin mkcert output ===\n")
ret = run(
f"mkcert -cert-file {cert_file.name} -key-file {cert_key_file.name} {addresses_string}",
shell=True,
)
print("=== end mkcert output ===\n")
has_certificate = ret.returncode == 0
if not has_certificate:
print(
"Warning: mkcert is not installed or was unable to create a certificate. Will not start HTTPS server."
)
return has_certificate, cert_file, cert_key_file
def send_cross_origin_isolation_headers(handler):
"""Sends COOP and COEP cross origin isolation headers"""
handler.send_header("Cross-Origin-Opener-Policy", "same-origin")
handler.send_header("Cross-Origin-Embedder-Policy", "require-corp")
handler.send_header("Cross-Origin-Resource-Policy", "cross-origin")
def send_empty_favicon(handle):
"""Sends an empty icon to surpess missing faviocon errors"""
self.send_response(200)
self.send_header("Content-Type", "image/x-icon")
self.send_header("Content-Length", 0)
class HttpRequestHandler(SimpleHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def end_headers(self):
if self.cross_origin_isolation == True:
send_cross_origin_isolation_headers(self)
super().end_headers()
class CompressionHttpRequesthandler(HTTPCompressionRequestHandler):
protocol_version = "HTTP/1.1"
# Make sure we compress: Add wasm and octet-stream to compressed_types
compressed_types = HTTPCompressionRequestHandler.compressed_types.copy()
compressed_types.append("application/wasm")
compressed_types.append("application/octet-stream")
def brotli_producer(fileobj):
bufsize = 1024 * 512
# Create compressor with quallity such that applying compression brings an actual speedup,
# i.e. the server must compress fast enough to saturate a typical network
# connection with compressed data. For brotli the quality goes from 0 to 11.
compressor = brotli.Compressor(quality=2)
with fileobj:
while True:
buf = fileobj.read(bufsize)
if not buf:
yield compressor.finish()
return
yield compressor.process(buf)
# must flush compressor state to work around crash/assert in brotlicffi,
# see https://github.com/python-hyper/brotlicffi/issues/167
buf = compressor.flush()
if len(buf) > 0:
yield buf
# Ideally, we would like to use the default gzip/deflate support as well,
# however that makes gzip take precedence over brotli. In practice, all
# browsers support brotli, so we can just use that.
compressions = {} # HTTPCompressionRequestHandler.compressions.copy()
compressions["br"] = brotli_producer
def end_headers(self):
if self.cross_origin_isolation == True:
send_cross_origin_isolation_headers(self)
super().end_headers()
class CompressionMode(Enum):
AUTO = "Auto"
ALWAYS = "Always"
NEVER = "Never"
def select_http_handler_class(compression_mode, address):
"""Returns the http handler class to use, based on the compression mode,
and the address of the server for the auto mode."""
if compression_mode == CompressionMode.ALWAYS:
return CompressionHttpRequesthandler
elif compression_mode == CompressionMode.NEVER:
return HttpRequestHandler
else:
# Select http request handler based on addrees. If the address is
# localhost then compression is typically not worth it since the
# localhost connection is very fast. For other addresses we assume
# typical network bandwidth and enable compression to reduce the download
# size.
if address == "127.0.0.1":
return HttpRequestHandler
else:
return CompressionHttpRequesthandler
# Serve serve_path from http(s)://address:port, with certificates from certdir if set
def serve_on_thread(
address,
port,
secure,
cert_file,
cert_key_file,
compression_mode,
cross_origin_isolation,
serve_path,
):
handler = select_http_handler_class(compression_mode, address)
handler.cross_origin_isolation = cross_origin_isolation
try:
httpd = ThreadingHTTPServer((address, port), partial(handler, directory=serve_path))
except Exception as e:
print(f"\n### Error starting HTTP server: {e}\n")
exit(1)
if secure:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(cert_file.name, cert_key_file.name)
httpd.socket = context.wrap_socket(
httpd.socket,
server_side=True,
)
thread = threading.Thread(target=httpd.serve_forever)
thread.start()
def main():
parser = argparse.ArgumentParser(
description="A development web server for Qt for WebAssembly applications.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--port",
"-p",
help="Port on which to listen for HTTP and HTTPS (PORT + 1)",
type=int,
default=8000,
)
parser.add_argument(
"--address",
"-a",
help="Bind to additional address, in addition to localhost",
action="append",
)
parser.add_argument(
"--all-interfaces",
"-A",
help="Bind to all local interfaces, instead of locahost only",
action="store_true",
)
parser.add_argument(
"--cross-origin-isolation",
"-i",
help="Enables cross-origin isolation mode, required for WebAssembly threads",
action="store_true",
)
parser.add_argument(
"--compress-auto",
help="Enables file compression on non-localhost addresses",
action="store_true",
default=True,
)
parser.add_argument(
"--compress-always",
help="Enables file compression for all addresses",
action="store_true",
)
parser.add_argument(
"--compress-never",
help="Disables file compression",
action="store_true",
)
parser.add_argument(
"path", help="The directory to serve", nargs="?", default=os.getcwd()
)
args = parser.parse_args()
http_port = args.port
https_port = http_port + 1
all_interfaces = args.all_interfaces
cmd_addresses = args.address or []
serve_path = args.path
cross_origin_isolation = args.cross_origin_isolation
if not os.path.isdir(serve_path):
print(f"The provided path '{serve_path}' does not exist or is not a directory")
exit(1)
compression_mode = CompressionMode.AUTO
if args.compress_always:
compression_mode = CompressionMode.ALWAYS
elif args.compress_never:
compression_mode = CompressionMode.NEVER
print("Qt for WebAssembly development server.\n")
print(f"Web server root:\n {serve_path}\n")
addresses = ["127.0.0.1"] + cmd_addresses
if all_interfaces:
addresses += [
addr[ni.AF_INET][0]["addr"]
for addr in map(ni.ifaddresses, ni.interfaces())
if ni.AF_INET in addr
]
addresses = list(set(addresses)) # deduplicate
addresses.sort()
print("Serving at addresses:")
print(f" {addresses}\n")
has_certificate, cert_file, cert_key_file = generate_mkcert_certificate(addresses)
print("Options:")
print(f" Secure server: {has_certificate}")
print(f" Cross Origin Isolation: {cross_origin_isolation}")
print(f" Compression: {compression_mode.value}")
print("")
# Start servers
print(f"Serving at:")
for address in addresses:
print(f" http://{address}:{http_port}")
serve_on_thread(
address,
http_port,
False,
cert_file,
cert_key_file,
compression_mode,
cross_origin_isolation,
serve_path,
)
if has_certificate:
for address in addresses:
print(f" https://{address}:{https_port}")
serve_on_thread(
address,
https_port,
True,
cert_file,
cert_key_file,
compression_mode,
cross_origin_isolation,
serve_path,
)
if __name__ == "__main__":
main()