Advance forge rollout, Ethereum rails, and NBC sources
This commit is contained in:
parent
be26313225
commit
7d84510eac
88 changed files with 11230 additions and 302 deletions
144
scripts/netboot-http-server.py
Executable file
144
scripts/netboot-http-server.py
Executable file
|
|
@ -0,0 +1,144 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Minimal HTTP server for netboot artifacts with optional CIDR/token controls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
import pathlib
|
||||
import urllib.parse
|
||||
from http import HTTPStatus
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from typing import Union
|
||||
|
||||
|
||||
ALLOWED_FILES = {
|
||||
"kernel": "application/octet-stream",
|
||||
"bzImage": "application/octet-stream",
|
||||
"initrd": "application/octet-stream",
|
||||
"netboot.ipxe": "text/plain; charset=utf-8",
|
||||
}
|
||||
|
||||
Network = Union[ipaddress.IPv4Network, ipaddress.IPv6Network]
|
||||
|
||||
|
||||
class NetbootHTTPServer(ThreadingHTTPServer):
|
||||
def __init__(
|
||||
self,
|
||||
server_address: tuple[str, int],
|
||||
root: pathlib.Path,
|
||||
allowed_networks: list[Network],
|
||||
netboot_token: str | None,
|
||||
) -> None:
|
||||
super().__init__(server_address, NetbootRequestHandler)
|
||||
self.root = root
|
||||
self.allowed_networks = allowed_networks
|
||||
self.netboot_token = netboot_token
|
||||
|
||||
|
||||
class NetbootRequestHandler(BaseHTTPRequestHandler):
|
||||
server_version = "ec-netboot/1.0"
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def do_GET(self) -> None:
|
||||
self._serve_file(include_body=True)
|
||||
|
||||
def do_HEAD(self) -> None:
|
||||
self._serve_file(include_body=False)
|
||||
|
||||
def _serve_file(self, include_body: bool) -> None:
|
||||
if not self._client_allowed():
|
||||
self._send_error(HTTPStatus.FORBIDDEN, "client not allowed")
|
||||
return
|
||||
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
path = parsed.path.lstrip("/")
|
||||
if path not in ALLOWED_FILES:
|
||||
self._send_error(HTTPStatus.NOT_FOUND, "file not found")
|
||||
return
|
||||
|
||||
if path == "netboot.ipxe" and self.server.netboot_token:
|
||||
query = urllib.parse.parse_qs(parsed.query, keep_blank_values=True)
|
||||
token = query.get("token", [""])[0]
|
||||
if token != self.server.netboot_token:
|
||||
self._send_error(HTTPStatus.FORBIDDEN, "missing or invalid token")
|
||||
return
|
||||
|
||||
if path == "bzImage":
|
||||
file_path = self.server.root / "kernel"
|
||||
else:
|
||||
file_path = self.server.root / path
|
||||
if not file_path.is_file():
|
||||
self._send_error(HTTPStatus.NOT_FOUND, "file not found")
|
||||
return
|
||||
|
||||
stat = file_path.stat()
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", ALLOWED_FILES[path])
|
||||
self.send_header("Content-Length", str(stat.st_size))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
|
||||
if not include_body:
|
||||
return
|
||||
|
||||
with file_path.open("rb") as handle:
|
||||
while True:
|
||||
chunk = handle.read(64 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
self.wfile.write(chunk)
|
||||
|
||||
def _client_allowed(self) -> bool:
|
||||
networks = self.server.allowed_networks
|
||||
if not networks:
|
||||
return True
|
||||
|
||||
try:
|
||||
client_ip = ipaddress.ip_address(self.client_address[0])
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return any(client_ip in network for network in networks)
|
||||
|
||||
def _send_error(self, code: HTTPStatus, message: str) -> None:
|
||||
payload = (message + "\n").encode("utf-8")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--bind-ip", required=True)
|
||||
parser.add_argument("--port", type=int, required=True)
|
||||
parser.add_argument("--root", required=True)
|
||||
parser.add_argument("--allow-cidr", action="append", default=[])
|
||||
parser.add_argument("--netboot-token", default="")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
root = pathlib.Path(args.root).resolve()
|
||||
if not root.is_dir():
|
||||
raise SystemExit(f"error: root directory does not exist: {root}")
|
||||
|
||||
allowed_networks = []
|
||||
for cidr in args.allow_cidr:
|
||||
try:
|
||||
allowed_networks.append(ipaddress.ip_network(cidr, strict=False))
|
||||
except ValueError as exc:
|
||||
raise SystemExit(f"error: invalid CIDR '{cidr}': {exc}") from exc
|
||||
|
||||
token = args.netboot_token or None
|
||||
server = NetbootHTTPServer((args.bind_ip, args.port), root, allowed_networks, token)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue