#!/usr/bin/python3 # SPDX-License-Identifier: MIT """mdbp backend wrapper via ssh""" import argparse import contextlib import io import json import pathlib import random import re import subprocess import sys import tarfile import typing import urllib.parse from .common import JsonObject, buildjson, get_dsc_files, tar_add class RepoForward: def __init__(self): self.forwards = {} def get_forward(self, destination: str) -> str: try: return self.forwards[destination] except KeyError: forward = "localhost:%d" % random.randrange(1024, 65536) self.forwards[destination] = forward return forward def proxy(self, repoline: str) -> str: """Transform an extra repository line and to pass it through a ssh forward.""" match = re.match( r"^(deb(?:-src)?(?:\s+\[[^]]*\])?)\s+(\S+)\s+(.*)$", repoline ) if not match: raise ValueError( "failed to understand repository specification %r" % repoline ) head, url, tail = match.groups() spliturl = urllib.parse.urlsplit(url) if spliturl.scheme != "http": raise ValueError("cannot proxy url of scheme %r" % spliturl.scheme) netloc = self.get_forward( "%s:%d" % (spliturl.hostname, spliturl.port or 80) ) if spliturl.username: netloc = "@" + netloc if spliturl.password: netloc = ":" + spliturl.password + netloc netloc = spliturl.username + netloc url = urllib.parse.urlunsplit((spliturl.scheme, netloc) + spliturl[2:]) return " ".join((head, url, tail)) def ssh_options(self) -> typing.Iterable[str]: for dest, proxy in self.forwards.items(): yield "-R" yield "%s:%s" % (proxy, dest) def produce_request_tar(buildjsonobj: JsonObject, fileobj: typing.IO[bytes]) -> None: """Write a tar file suitable for mdbp-streamapi into the given `fileobj` based on the given `buildjsonobj`. * An .output.directory is discarded. * A referenced .dsc file and its components is included. """ sendjsonobj = buildjsonobj.copy() sendjsonobj["output"] = sendjsonobj["output"].copy() del sendjsonobj["output"]["directory"] dscpath: typing.Optional[pathlib.Path] try: dscpath = pathlib.Path(buildjsonobj["input"]["source_package_path"]) except KeyError: dscpath = None else: sendjsonobj["input"] = sendjsonobj["input"].copy() sendjsonobj["input"]["source_package_path"] = dscpath.name with tarfile.open(mode="w|", fileobj=fileobj) as tar: info = tarfile.TarInfo("build.json") sendjsonfile = io.BytesIO() for chunk in json.JSONEncoder().iterencode(sendjsonobj): sendjsonfile.write(chunk.encode("utf8")) info.size = sendjsonfile.tell() sendjsonfile.seek(0) tar.addfile(info, sendjsonfile) if dscpath: for path in [dscpath] + get_dsc_files(dscpath): tar_add(tar, path) def main() -> None: """Entry point for mdbp-ssh backend""" parser = argparse.ArgumentParser() parser.add_argument( "--proxyrepos", default=False, action="store_true", help="proxy http repositories over the ssh connection", ) parser.add_argument("host", type=str) parser.add_argument("command", nargs=argparse.REMAINDER) args = parser.parse_args() if len(args.command) < 2: parser.error("missing command or json file") build = buildjson(args.command.pop()) cmd = ["ssh"] if args.proxyrepos and "extrarepositories" in build: repoforward = RepoForward() build["extrarepositories"] = list( map(repoforward.proxy, build["extrarepositories"]) ) cmd.extend(repoforward.ssh_options()) cmd.extend([args.host, "mdbp-streamapi", *args.command]) with contextlib.ExitStack() as stack: proc = stack.enter_context( subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=sys.stdout if build["output"].get("log", True) else subprocess.DEVNULL ), ) assert proc.stdin is not None produce_request_tar(build, proc.stdin) proc.stdin.close() exitcode = 0 try: outtar = stack.enter_context(tarfile.open(fileobj=proc.stdout, mode="r|")) except tarfile.ReadError as err: if str(err) != "empty file": raise exitcode = 1 else: for member in outtar: if "/" in member.name or not member.isfile(): raise ValueError("expected flat tar as output") outtar.extract(member, build["output"]["directory"], set_attrs=False) sys.exit(proc.wait() or exitcode) if __name__ == "__main__": main()