chiark / gitweb /
Merge remote-tracking branch 'python/master' into old-python/
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Sun, 25 Sep 2022 15:46:54 +0000 (16:46 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Sun, 25 Sep 2022 15:46:54 +0000 (16:46 +0100)
Merge into subdirectory.  This retains the history.  We will copy some
files and delete others.

Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
91 files changed:
.dir-locals.el [new file with mode: 0644]
.gitignore
Cargo.lock [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
DEVELOPER-CERTIFICATE [new file with mode: 0644]
GPL-3 [moved from GPLv3 with 100% similarity]
Makefile [new file with mode: 0644]
PROTOCOL
README.md [new file with mode: 0644]
client/client.rs [new file with mode: 0644]
debian/.gitignore [new file with mode: 0644]
debian/changelog
debian/compat
debian/control
debian/hippotat-client.install [new file with mode: 0644]
debian/hippotat-doc.install [new file with mode: 0644]
debian/hippotat-server.install [new file with mode: 0644]
debian/hippotatd.hippotatd.init [new file with mode: 0644]
debian/rules
docs/README.md [new symlink]
docs/conf.py [new file with mode: 0644]
docs/config.rst [new file with mode: 0644]
docs/index.rst [new file with mode: 0644]
docs/settings.rst [new file with mode: 0644]
macros/Cargo.toml [new file with mode: 0644]
macros/macros.rs [new file with mode: 0644]
old-python/.gitignore [new file with mode: 0644]
old-python/AGPLv3+CAFv2 [moved from AGPLv3+CAFv2 with 100% similarity]
old-python/CONTRIBUTING [moved from CONTRIBUTING with 100% similarity]
old-python/COPYING [moved from COPYING with 100% similarity]
old-python/GPLv3 [new file with mode: 0644]
old-python/PROTOCOL [new file with mode: 0644]
old-python/README.config [moved from README.config with 100% similarity]
old-python/debian/changelog [new file with mode: 0644]
old-python/debian/compat [new file with mode: 0644]
old-python/debian/control [new file with mode: 0644]
old-python/debian/copyright [moved from debian/copyright with 100% similarity]
old-python/debian/hippotat.dirs [moved from debian/hippotat.dirs with 100% similarity]
old-python/debian/hippotat.hippotatd.init [moved from debian/hippotat.hippotatd.init with 100% similarity]
old-python/debian/hippotat.install [moved from debian/hippotat.install with 100% similarity]
old-python/debian/hippotat.postinst [moved from debian/hippotat.postinst with 100% similarity]
old-python/debian/hippotat.postrm [moved from debian/hippotat.postrm with 100% similarity]
old-python/debian/rules [new file with mode: 0755]
old-python/fake-userv [moved from fake-userv with 100% similarity]
old-python/form.html [moved from form.html with 100% similarity]
old-python/hippotat [moved from hippotat with 100% similarity]
old-python/hippotatd [moved from hippotatd with 100% similarity]
old-python/hippotatlib/__init__.py [moved from hippotatlib/__init__.py with 100% similarity]
old-python/hippotatlib/ownsource.py [moved from hippotatlib/ownsource.py with 100% similarity]
old-python/hippotatlib/slip.py [moved from hippotatlib/slip.py with 100% similarity]
old-python/setup.py [moved from setup.py with 100% similarity]
old-python/sgo-demo.cfg [moved from sgo-demo.cfg with 100% similarity]
old-python/simple.cfg [moved from simple.cfg with 100% similarity]
old-python/srcbombtest.py [moved from srcbombtest.py with 100% similarity]
old-python/subst-sys-path [moved from subst-sys-path with 100% similarity]
old-python/test.cfg [moved from test.cfg with 100% similarity]
old-python/w3mstracetodump [moved from w3mstracetodump with 100% similarity]
server/daemon.rs [new file with mode: 0644]
server/server.rs [new file with mode: 0644]
server/slocal.rs [new file with mode: 0644]
server/suser.rs [new file with mode: 0644]
server/sweb.rs [new file with mode: 0644]
src/config.rs [new file with mode: 0644]
src/ini.rs [new file with mode: 0644]
src/ipif.rs [new file with mode: 0644]
src/lib.rs [new file with mode: 0644]
src/multipart.rs [new file with mode: 0644]
src/prelude.rs [new file with mode: 0644]
src/queue.rs [new file with mode: 0644]
src/reporter.rs [new file with mode: 0644]
src/rope.rs [new file with mode: 0644]
src/slip.rs [new file with mode: 0644]
src/types.rs [new file with mode: 0644]
src/utils.rs [new file with mode: 0644]
test/capture-log [new file with mode: 0755]
test/common [new file with mode: 0644]
test/go-with-unshare [new file with mode: 0755]
test/netns-setup [new file with mode: 0755]
test/t-basic [new file with mode: 0755]
test/test.cfg [new file with mode: 0644]
test/with-unshare [new file with mode: 0755]
uml/psusan-uml-inside [new file with mode: 0755]
uml/psusan-uml-psusan [new file with mode: 0755]
uml/psusan-uml-run [new file with mode: 0755]
uml/psusan-uml-setup [new file with mode: 0755]
uml/rndaddtoentcnt/.gitignore [new file with mode: 0644]
uml/rndaddtoentcnt/LICENSE [new file with mode: 0644]
uml/rndaddtoentcnt/Makefile [new file with mode: 0644]
uml/rndaddtoentcnt/README.md [new file with mode: 0644]
uml/rndaddtoentcnt/rndaddtoentcnt.c [new file with mode: 0644]
uml/run-test [new file with mode: 0755]

diff --git a/.dir-locals.el b/.dir-locals.el
new file mode 100644 (file)
index 0000000..7210430
--- /dev/null
@@ -0,0 +1 @@
+((rust-mode . ((rust-indent-offset . 2))))
index f5dd26ce5eb112cf7183b36fea11f48889393a9e..812defb989256412792ab0c70f8fe1dceb112a60 100644 (file)
@@ -1,17 +1,4 @@
-data.dump.dbg
-[tuv]
-tmp
-srcbomb.tar.gz
-srcpkgsbomb.tar
-
-build
-.pybuild
-hippotat.egg-info
-
-debian/files
-debian/debhelper-*-stamp
-debian/*.debhelper.log
-debian/hippotat.substvars
-debian/hippotat.*.debhelper
-
-debian/hippotat/
+/target
+/docs/html
+/docs/doctrees
+/stamp
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644 (file)
index 0000000..912669d
--- /dev/null
@@ -0,0 +1,1409 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "backtrace"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bytes"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
+
+[[package]]
+name = "cc"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
+
+[[package]]
+name = "cervine"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f0db89834ef04fc63d2f136327b42d532b45def0345213d28690a3446c7bdb5"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "env_logger"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "error-chain"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "extend"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5c89e2933a4ec753dc007a4d6a7f9b6dc8e89b8fe89cabc252ccddf39c08bb1"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "eyre"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb"
+dependencies = [
+ "indenter",
+ "once_cell",
+]
+
+[[package]]
+name = "fehler"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5729fe49ba028cd550747b6e62cd3d841beccab5390aa398538c31a2d983635"
+dependencies = [
+ "fehler-macros",
+]
+
+[[package]]
+name = "fehler-macros"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccb5acb1045ebbfa222e2c50679e392a71dd77030b78fb0189f2d9c5974400f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "futures"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57"
+dependencies = [
+ "autocfg",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53"
+
+[[package]]
+name = "futures-task"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2"
+
+[[package]]
+name = "futures-util"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78"
+dependencies = [
+ "autocfg",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "proc-macro-hack",
+ "proc-macro-nested",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
+
+[[package]]
+name = "h2"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "heck"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hippotat"
+version = "0.0.1"
+dependencies = [
+ "backtrace",
+ "base64",
+ "cervine",
+ "env_logger",
+ "extend",
+ "eyre",
+ "fehler",
+ "futures",
+ "heck 0.4.0",
+ "hippotat-macros",
+ "hyper",
+ "hyper-tls",
+ "indenter",
+ "ipnet",
+ "itertools",
+ "lazy-regex",
+ "lazy_static",
+ "libc",
+ "log",
+ "memchr",
+ "mime",
+ "nix",
+ "parking_lot",
+ "pin-project-lite",
+ "regex",
+ "sha2",
+ "structopt",
+ "subtle",
+ "syslog",
+ "thiserror",
+ "tokio",
+ "void",
+]
+
+[[package]]
+name = "hippotat-macros"
+version = "0.0.1"
+dependencies = [
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "hostname"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
+dependencies = [
+ "libc",
+ "match_cfg",
+ "winapi",
+]
+
+[[package]]
+name = "http"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa 0.4.7",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68"
+
+[[package]]
+name = "httpdate"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "hyper"
+version = "0.14.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7728a72c4c7d72665fde02204bcbd93b247721025b222ef78606f14513e0fd03"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa 0.4.7",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "indenter"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
+
+[[package]]
+name = "indexmap"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
+
+[[package]]
+name = "itertools"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+
+[[package]]
+name = "itoa"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
+
+[[package]]
+name = "lazy-regex"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17d198f91272f6e788a5c0bd5d741cf778da4e5bc761ec67b32d5d3b0db34a54"
+dependencies = [
+ "lazy-regex-proc_macros",
+ "once_cell",
+ "regex",
+]
+
+[[package]]
+name = "lazy-regex-proc_macros"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c12938b1b92cf5be22940527e15b79fd0c7e706e34bc70816f6a72b3484f84e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.132"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
+
+[[package]]
+name = "lock_api"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "match_cfg"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
+
+[[package]]
+name = "memchr"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
+dependencies = [
+ "adler",
+ "autocfg",
+]
+
+[[package]]
+name = "mio"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "winapi",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "nix"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb"
+dependencies = [
+ "autocfg",
+ "bitflags",
+ "cfg-if",
+ "libc",
+ "memoffset",
+ "pin-utils",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c55827317fb4c08822499848a14237d2874d6f139828893017237e7ab93eb386"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "openssl"
+version = "0.10.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+
+[[package]]
+name = "proc-macro-nested"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49"
+
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "security-framework"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12"
+dependencies = [
+ "block-buffer",
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"
+
+[[package]]
+name = "smallvec"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
+
+[[package]]
+name = "socket2"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "structopt"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69b041cdcb67226aca307e6e7be44c8806423d83e018bd662360a93dabce4d71"
+dependencies = [
+ "clap",
+ "lazy_static",
+ "structopt-derive",
+]
+
+[[package]]
+name = "structopt-derive"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7813934aecf5f51a54775e00068c237de98489463968231a51746bbbc03f9c10"
+dependencies = [
+ "heck 0.3.3",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "subtle"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+
+[[package]]
+name = "syn"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "syslog"
+version = "6.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978044cc68150ad5e40083c9f6a725e6fd02d7ba1bcf691ec2ff0d66c0b41acc"
+dependencies = [
+ "error-chain",
+ "hostname",
+ "libc",
+ "log",
+ "time",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rand",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b"
+dependencies = [
+ "itoa 1.0.3",
+ "libc",
+ "num_threads",
+]
+
+[[package]]
+name = "tokio"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2602b8af3767c285202012822834005f596c811042315fa7e9f5b12b2a43207"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "once_cell",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "tokio-macros",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
+
+[[package]]
+name = "tracing"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "typenum"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "version_check"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+
+[[package]]
+name = "void"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..005aaa6
--- /dev/null
@@ -0,0 +1,66 @@
+# Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+[package]
+name = "hippotat"
+version = "0.0.1"
+edition = "2018"
+description="Asinine HTTP-over-IP"
+license="GPL-3.0-or-later"
+repository="https://salsa.debian.org/iwj/hippotat"
+
+[workspace]
+members = ["macros"]
+
+[[bin]]
+name="hippotat"
+path="client/client.rs"
+
+[[bin]]
+name="hippotatd"
+path="server/server.rs"
+
+[dependencies]
+
+hippotat-macros = { version = "0.0.1", path = "macros" }
+
+# versions specified here are mostly just guesses at what is needed
+# (or currently available):
+backtrace = "0.3"
+base64 = "0.13"
+env_logger = "0.9"
+futures = "0.3"
+heck = "0.4"
+hyper = { version = "0.14", features = ["full"] }
+hyper-tls = "0.5"
+ipnet = "2"
+itertools = "0.10"
+libc = "0.2" # just for EISDIR due to IsADirectory
+mime = "0.3"
+parking_lot = "0.11"
+regex = "1.5"
+lazy_static = "1.4"
+log = "0.4"
+memchr = "2"
+nix = "0.25"
+pin-project-lite = "0.2"
+sha2 = "0.9"
+structopt = "0.3"
+subtle = "2"
+syslog = "6"
+tokio = { version = "1", features = ["full"] }
+thiserror = "1"
+void = "1"
+
+# for daemonic behaviours
+#  daemonize 0.4.1 in sid
+#  syslog    4.0 in sid, 5.0 in upstream, ideally want 5.0 (new API)
+
+# Not in sid:
+extend = "1"           # no deps not in sid
+eyre = "0.6"           # deps not in sid: indenter (see below)
+indenter = "0.3"       # no deps not in sid
+fehler = "1"           # no deps (other than fehler-macros, obvs)
+lazy-regex = "2"       # no deps not in sid
+cervine = "0.0"                # no (non-dev)-deps not in sid
diff --git a/DEVELOPER-CERTIFICATE b/DEVELOPER-CERTIFICATE
new file mode 100644 (file)
index 0000000..912d22e
--- /dev/null
@@ -0,0 +1,38 @@
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+1 Letterman Drive
+Suite D4700
+San Francisco, CA, 94129
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+    have the right to submit it under the open source license
+    indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+    of my knowledge, is covered under an appropriate open source
+    license and I have the right under that license to submit that
+    work with modifications, whether created in whole or in part
+    by me, under the same open source license (unless I am
+    permitted to submit under a different license), as indicated
+    in the file; or
+
+(c) The contribution was provided directly to me by some other
+    person who certified (a), (b) or (c) and I have not modified
+    it.
+
+(d) I understand and agree that this project and the contribution
+    are public and that a record of the contribution (including all
+    personal information I submit with it, including my sign-off) is
+    maintained indefinitely and may be redistributed consistent with
+    this project or the open source license(s) involved.
+
diff --git a/GPLv3 b/GPL-3
similarity index 100%
rename from GPLv3
rename to GPL-3
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..d5511e1
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,78 @@
+# Copyright 2020-2022 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+SHELL=/bin/bash
+
+default: all
+
+SPHINXBUILD    ?= sphinx-build
+
+INSTALL                ?= install
+
+ifneq (,$(NAILING_CARGO))
+
+NAILING_CARGO ?= nailing-cargo
+CARGO = $(NAILING_CARGO)
+BUILD_SUBDIR ?= ../Build
+TARGET_DIR ?= $(BUILD_SUBDIR)/$(notdir $(PWD))/target
+NAILING_CARGO_JUST_RUN ?= $(NAILING_CARGO) --just-run -q ---
+
+else
+
+CARGO          ?= cargo
+TARGET_DIR     ?= target
+
+endif # Cargo.nail
+
+CARGO_RELEASE ?= release
+TARGET_RELEASE_DIR ?= $(TARGET_DIR)/$(CARGO_RELEASE)
+
+ifneq (debug,$(CARGO_RELEASE))
+CARGO_RELEASE_ARG ?= --$(CARGO_RELEASE)
+endif
+
+rsrcs = $(shell $(foreach x,$(MAKEFILE_FIND_X),set -$x;)\
+    find -H $1 \( -name Cargo.toml -o -name Cargo.lock -o -name Cargo.lock.example -o -name \*.rs \) )
+stamp=@mkdir -p stamp; touch $@
+
+TESTS=$(notdir $(wildcard test/t-*[^~]))
+
+all:   cargo-build doc
+
+check: cargo-test $(addprefix stamp/,$(TESTS))
+
+cargo-build: stamp/cargo-build
+cargo-test: stamp/cargo-test
+
+stamp/cargo-%: $(call rsrcs,.)
+       $(CARGO) $* $(CARGO_RELEASE_ARG) $(CARGO_BUILD_OPTIONS)
+       $(stamp)
+
+stamp/t-%: test/t-% stamp/cargo-build $(wildcard test/*[^~])
+       $(NAILING_CARGO_JUST_RUN) \
+       $(abspath test/capture-log) tmp/t-$*.log \
+       $(abspath test/go-with-unshare test/t-$*)
+       @echo OK t-$*; touch $@
+
+doc:   docs/html/index.html
+       @echo 'Documentation can now be found here:'
+       @echo '  file://$(PWD)/$<'
+
+docs/html/index.html: docs/conf.py $(wildcard docs/*.md docs/*.rst docs/*.png)
+       rm -rf docs/html
+       $(SPHINXBUILD) -M html docs docs $(SPHINXOPTS)
+
+install: all
+       $(INSTALL) -d $(DESTDIR)/usr/{bin,sbin,share/doc/hippotat}
+       $(INSTALL) -m 755 $(TARGET_RELEASE_DIR)/hippotat $(DESTDIR)/usr/bin/.
+       $(INSTALL) -m 755 $(TARGET_RELEASE_DIR)/hippotatd $(DESTDIR)/usr/sbin/.
+       cp -r docs/html $(DESTDIR)/usr/share/doc/hippotat/
+
+clean:
+       rm -rf stamp/* doc/html
+
+very-clean: clean
+       $(NAILING_CARGO) clean
+
+.PHONY: cargo-build all doc clean
index e18cf0e1554da169e6ffdcd8994ba3a727f8c17c..eea9c5b805e7bc9133edf1a861005e13fe99a292 100644 (file)
--- a/PROTOCOL
+++ b/PROTOCOL
@@ -22,6 +22,9 @@ Client form parameters (multipart/form-data):
                        token
                        target_requests_outstanding
                        http_timeout
+                        mtu                     } not supplied
+                       max_batch_down          }  by older
+                       max_batch_up            }  clients
  d              data (SLIP format, with SLIP_ESC and `-' swapped)
 
 
@@ -29,6 +32,7 @@ Authentication token is:
         <time_t in hex with no leading 0s> <hmac in base64>
 (separated by a single space).  The hmac is
         HMAC(secret, <time_t in hex>)
+and the hash function is SHA256
 
 
 Possible future nonce-based authentication:
@@ -39,4 +43,5 @@ meaning is:
 also server keeps bitmap of the previous ?64 nonces,
  whether client has sent them
 
-client picks.... xxx
+difficult because client-generated nonces would have to never go
+backwaards which basically means never-rewinding state on the client.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..c516b33
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+Introduction
+============
diff --git a/client/client.rs b/client/client.rs
new file mode 100644 (file)
index 0000000..f623771
--- /dev/null
@@ -0,0 +1,351 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use hippotat::prelude::*;
+use hippotat_macros::into_crlfs;
+
+#[derive(StructOpt,Debug)]
+pub struct Opts {
+  #[structopt(flatten)]
+  log: LogOpts,
+
+  #[structopt(flatten)]
+  config: config::Opts,
+}
+
+type OutstandingRequest<'r> = Pin<Box<
+    dyn Future<Output=Option<Box<[u8]>>> + Send + 'r
+    >>;
+
+impl<T> HCC for T where
+        T: hyper::client::connect::Connect + Clone + Send + Sync + 'static { }
+trait HCC: hyper::client::connect::Connect + Clone + Send + Sync + 'static { }
+
+struct ClientContext<'c,C> {
+  ic: &'c InstanceConfig,
+  hclient: &'c Arc<hyper::Client<C>>,
+  reporter: &'c parking_lot::Mutex<Reporter<'c>>,
+}
+
+#[derive(Debug)]
+struct TxQueued {
+  expires: Instant,
+  data: Box<[u8]>,
+}
+
+#[throws(AE)]
+fn submit_request<'r, 'c:'r, C:HCC>(
+  c: &'c ClientContext<C>,
+  req_num: &mut ReqNum,
+  reqs: &mut Vec<OutstandingRequest<'r>>,
+  upbound: FramesData,
+) {
+  let show_timeout = c.ic.http_timeout
+    .saturating_add(Duration::from_nanos(999_999_999))
+    .as_secs();
+
+  let time_t = time_t_now();
+  let time_t = format!("{:x}", time_t);
+  let hmac = token_hmac(c.ic.secret.0.as_bytes(), time_t.as_bytes());
+  //dbg!(DumpHex(&hmac));
+  let mut token = time_t;
+  write!(token, " ").unwrap();
+  base64::encode_config_buf(&hmac, BASE64_CONFIG, &mut token);
+
+  let req_num = { *req_num += 1; *req_num };
+
+  let prefix1 = format!(into_crlfs!(
+    r#"--b
+       Content-Type: text/plain; charset="utf-8"
+       Content-Disposition: form-data; name="m"
+
+       {}
+       {}
+       {}
+       {}
+       {}
+       {}
+       {}"#),
+                       &c.ic.link.client,
+                       token,
+                       c.ic.target_requests_outstanding,
+                       show_timeout,
+                       c.ic.mtu,
+                       c.ic.max_batch_down,
+                       c.ic.max_batch_up,
+  );
+
+  let prefix2 = format!(into_crlfs!(
+    r#"
+       --b
+       Content-Type: application/octet-stream
+       Content-Disposition: form-data; name="d"
+
+       "#),
+  );
+  let suffix = format!(into_crlfs!(
+    r#"
+       --b--
+       "#),
+  );
+
+  macro_rules! content { {
+    $out:ty,
+    $iter:ident,
+    $into:ident,
+  } => {
+    itertools::chain![
+      IntoIterator::into_iter([
+        prefix1.$into(),
+        prefix2.$into(),
+      ]).take(
+        if upbound.is_empty() { 1 } else { 2 }
+      ),
+      Itertools::intersperse(
+        upbound.$iter().map(|u| { let out: $out = u.$into(); out }),
+        SLIP_END_SLICE.$into()
+      ),
+      [ suffix.$into() ],
+    ]
+  }}
+
+  let body_len: usize = content!(
+    &[u8],
+    iter,
+    as_ref,
+  ).map(|b| b.len()).sum();
+
+  trace!("{} #{}: req; tx body_len={} frames={}",
+         &c.ic, req_num, body_len, upbound.len());
+
+  let body = hyper::body::Body::wrap_stream(
+    futures::stream::iter(
+      content!(
+        Bytes,
+        into_iter,
+        into,
+      ).map(Ok::<Bytes,Void>)
+    )
+  );
+
+  let req = hyper::Request::post(&c.ic.url)
+    .header("Content-Type", r#"multipart/form-data; boundary="b""#)
+    .header("Content-Length", body_len)
+    .body(body)
+    .context("construct request")?;
+
+  let resp = c.hclient.request(req);
+  let fut = Box::pin(async move {
+    let r = async { tokio::time::timeout( c.ic.effective_http_timeout, async {
+      let resp = resp.await.context("make request")?;
+      let status = resp.status();
+      let mut resp = resp.into_body();
+      let max_body = c.ic.max_batch_down.sat() + MAX_OVERHEAD;
+      let resp = read_limited_bytes(
+        max_body, default(), default(), &mut resp
+      ).await
+        .discard_data().context("fetching response body")?;
+
+      if ! status.is_success() {
+        throw!(anyhow!("HTTP error status={} body={:?}",
+                       &status, String::from_utf8_lossy(&resp)));
+      }
+
+      Ok::<_,AE>(resp)
+    }).await? }.await;
+
+    let r = c.reporter.lock().filter(Some(req_num), r);
+
+    if let Some(r) = &r {
+      trace!("{} #{}: rok; rx bytes={}", &c.ic, req_num, r.len());
+    } else {
+      tokio::time::sleep(c.ic.http_retry).await;
+    }
+    r
+  });
+  reqs.push(fut);
+}
+
+async fn run_client<C:HCC>(
+  ic: InstanceConfig,
+  hclient: Arc<hyper::Client<C>>
+) -> Result<Void, AE>
+{
+  debug!("{}: config: {:?}", &ic, &ic);
+
+  let reporter = parking_lot::Mutex::new(Reporter::new(&ic));
+
+  let c = ClientContext {
+    reporter: &reporter,
+    hclient: &hclient,
+    ic: &ic,
+  };
+
+  let mut ipif = Ipif::start(&ic.ipif, Some(ic.to_string()))?;
+
+  let mut req_num: ReqNum = 0;
+
+  let mut tx_queue: VecDeque<TxQueued> = default();
+  let mut upbound = Frames::default();
+
+  let mut reqs: Vec<OutstandingRequest>
+    = Vec::with_capacity(ic.max_requests_outstanding.sat());
+
+  let mut rx_queue: FrameQueueBuf = default();
+
+  let trouble = async {
+    loop {
+      let rx_queue_space = 
+        if rx_queue.remaining() < ic.max_batch_down.sat() {
+          Ok(())
+        } else {
+          Err(())
+        };
+      
+      select! {
+        biased;
+
+        y = ipif.rx.write_all_buf(&mut rx_queue),
+        if ! rx_queue.is_empty() =>
+        {
+          let () = y.context("write rx data to ipif")?;
+        },
+
+        () = async {
+          let expires = tx_queue.front().unwrap().expires;
+          tokio::time::sleep_until(expires).await
+        },
+        if ! tx_queue.is_empty() =>
+        {
+          let _ = tx_queue.pop_front();
+        },
+
+        data = Ipif::next_frame(&mut ipif.tx),
+        if tx_queue.is_empty() =>
+        {
+          let data = data?;
+          //eprintln!("data={:?}", DumpHex(&data));
+
+          match slip::process1(Slip2Mime, ic.mtu, &data, |header| {
+            let saddr = ip_packet_addr::<false>(header)?;
+            if saddr != ic.link.client.0 { throw!(PE::Src(saddr)) }
+            Ok(())
+          }) {
+            Ok((data, ())) => tx_queue.push_back(TxQueued {
+              data,
+              expires: Instant::now() + ic.max_queue_time
+            }),
+            Err(PE::Empty) => { },
+            Err(e@ PE::Src(_)) => debug!("{}: tx discarding: {}", &ic, e),
+            Err(e) => error!("{}: tx discarding: {}", &ic, e),
+          };
+        },
+
+        _ = async { },
+        if ! upbound.tried_full() &&
+           ! tx_queue.is_empty() =>
+        {
+          while let Some(TxQueued { data, expires }) = tx_queue.pop_front() {
+            match upbound.add(ic.max_batch_up, data.into()/*todo:504*/) {
+              Err(data) => {
+                tx_queue.push_front(TxQueued { data: data.into(), expires });
+                break;
+              }
+              Ok(()) => { },
+            }
+          }
+        },
+
+        _ = async { },
+        if rx_queue_space.is_ok() &&
+          (reqs.len() < ic.target_requests_outstanding.sat() ||
+           (reqs.len() < ic.max_requests_outstanding.sat() &&
+            ! upbound.is_empty()))
+          =>
+        {
+          submit_request(&c, &mut req_num, &mut reqs,
+                         mem::take(&mut upbound).into())?;
+        },
+
+        (got, goti, _) = async { future::select_all(&mut reqs).await },
+          if ! reqs.is_empty() =>
+        {
+          reqs.swap_remove(goti);
+
+          if let Some(got) = got {
+            
+            //eprintln!("got={:?}", DumpHex(&got));
+            match slip::processn(SlipNoConv,ic.mtu, &got, |header| {
+              let addr = ip_packet_addr::<true>(header)?;
+              if addr != ic.link.client.0 { throw!(PE::Dst(addr)) }
+              Ok(())
+            },
+            |(o,())| future::ready(Ok({ rx_queue.push_esc(o); })),
+            |e| Ok::<_,SlipFramesError<Void>>( {
+              error!("{} #{}: rx discarding: {}", &ic, req_num, e);
+            })).await
+            {
+              Ok(()) => reporter.lock().success(),
+              Err(SlipFramesError::ErrorOnlyBad) => {
+                reqs.push(Box::pin(async {
+                  tokio::time::sleep(ic.http_retry).await;
+                  None
+                }));
+              },
+              Err(SlipFramesError::Other(v)) => unreachable!("{}", v),
+            }
+          }
+        },
+
+        _ = tokio::time::sleep(c.ic.effective_http_timeout),
+        if rx_queue_space.is_err() =>
+        {
+          reporter.lock().filter(None, Err::<Void,_>(
+            anyhow!("rx queue full, blocked")
+          ));
+        },
+      }
+    }
+  }.await;
+
+  ipif.quitting(Some(&ic)).await;
+  trouble
+}
+
+#[tokio::main]
+async fn main() {
+  let opts = Opts::from_args();
+  let (ics,) = config::startup("hippotat", LinkEnd::Client,
+                               &opts.config, &opts.log, |ics|Ok((ics,)));
+
+  let https = HttpsConnector::new();
+  let hclient = hyper::Client::builder()
+    .http1_preserve_header_case(true)
+    .build::<_, hyper::Body>(https);
+  let hclient = Arc::new(hclient);
+
+  info!("starting");
+  let () = future::select_all(
+    ics.into_iter().map(|ic| Box::pin(async {
+      let assocname = ic.to_string();
+      info!("{} starting", &assocname);
+      let hclient = hclient.clone();
+      let join = task::spawn(async {
+        run_client(ic, hclient).await.void_unwrap_err()
+      });
+      match join.await {
+        Ok(e) => {
+          error!("{} failed: {}", &assocname, e);
+        },
+        Err(je) => {
+          error!("{} panicked!", &assocname);
+          panic::resume_unwind(je.into_panic());
+        },
+      }
+    }))
+  ).await.0;
+
+  error!("quitting because one of your client connections crashed");
+  process::exit(16);
+}
diff --git a/debian/.gitignore b/debian/.gitignore
new file mode 100644 (file)
index 0000000..5090c6f
--- /dev/null
@@ -0,0 +1,10 @@
+.debhelper
+debhelper-*-stamp
+files
+tmp
+hippotat-client
+hippotat-client.substvars
+hippotat-server
+hippotat-server.substvars
+hippotat-doc
+hippotat-doc.substvars
index 298792cd39c7e2f8ae7931e1221579cd7ded2ff2..c3929a9fcf7cee070fa01f362fe7b71519f605e0 100644 (file)
@@ -1,6 +1,6 @@
-hippotat (0.1~UNRELEASED) unstable; urgency=medium
+hippotat (0.1) unstable; urgency=medium
 
-  * 
+  * Testing some packaging.
 
- -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sat, 08 Apr 2017 17:57:42 +0100
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Tue, 13 Sep 2022 14:03:22 +0100
 
index ec635144f60048986bc560c5576355344005e6e7..f599e28b8ab0d8c9c57a486c89c4a5132dcbd3b2 100644 (file)
@@ -1 +1 @@
-9
+10
index b5e59358f36771b013e8834fedd4032c9623adf3..af738d75a4dcb48a028c22eaa6ace1317c139393 100644 (file)
@@ -1,11 +1,22 @@
 Source: hippotat
-Build-Depends: debhelper (>= 9), dh-python, python3
 Maintainer: Ian Jackson <ijackson@chiark.greenend.org.uk>
+Section: network
+Priority: optional
 
-Package: hippotat
-Depends: python3, ${python3:Depends}
-Recommends: userv, userv-utils (>= 0.6.0~~iwj4), cpio
-Suggests: authbind
-Architecture: all
-Description: IP Over HTTP (Asinine)
- IP-over-HTTP client and server.
+Package: hippotat-client
+Architecture: any
+Depends: ${misc:Depends}
+Recommends: hippotat-doc, userv-utils
+Description: Asinine IP Over HTTP - client
+
+Package: hippotat-server
+Architecture: any
+Depends: ${misc:Depends}
+Recommends: hippotat-doc, userv-utils
+Description: Asinine IP Over HTTP - server
+
+Package: hippotat-doc
+Architecture: any
+Depends: ${misc:Depends}
+Recommends: userv-utils
+Description: Asinine IP Over HTTP - documentation
diff --git a/debian/hippotat-client.install b/debian/hippotat-client.install
new file mode 100644 (file)
index 0000000..90bd301
--- /dev/null
@@ -0,0 +1 @@
+usr/bin/hippotat
diff --git a/debian/hippotat-doc.install b/debian/hippotat-doc.install
new file mode 100644 (file)
index 0000000..230a074
--- /dev/null
@@ -0,0 +1 @@
+usr/share/doc/hippotat
diff --git a/debian/hippotat-server.install b/debian/hippotat-server.install
new file mode 100644 (file)
index 0000000..05896c9
--- /dev/null
@@ -0,0 +1 @@
+usr/sbin/hippotatd
diff --git a/debian/hippotatd.hippotatd.init b/debian/hippotatd.hippotatd.init
new file mode 100644 (file)
index 0000000..1df3399
--- /dev/null
@@ -0,0 +1,118 @@
+#!/bin/sh
+
+### BEGIN INIT INFO
+# Provides:            hippotatd
+# Required-Start:      $syslog $network userv
+# Required-Stop:       $syslog $network
+# Default-Start:       2 3 4 5
+# Default-Stop:                0 1 6
+# Short-Description:   hippotatd
+# Description:          Asinine IP over HTTP server
+### END INIT INFO
+
+DAEMON=/usr/sbin/hippotatd
+MAIN_CONFIG=/etc/hippotat/main.cfg
+USER=_hippotat
+PIDFILE=/var/run/hippotat/hippotatd.pid
+LOGFACILITY=daemon
+CHECK_FIREWALL=true
+# HIPPOTATD_ARGS
+AS_USER=as_user_userv
+DESCRIPTION='Asinine IP over HTTP server'
+if type authbind >/dev/null 2>&1; then AUTHBIND=authbind; fi
+
+test -e /etc/default/hippotatd &&
+. /etc/default/hippotatd
+
+set -e
+
+test -f $DAEMON || exit 0
+egrep '^[^     #]' $MAIN_CONFIG >/dev/null 2>&1 || exit 0
+
+. /lib/lsb/init-functions
+
+as_user_userv () {
+       userv --override '
+               execute-from-path
+               no-suppress-args
+       ' $USER "$@"
+}
+
+ssd () {
+       set +e
+       start-stop-daemon --quiet --user $USER --pidfile=$PIDFILE "$@"
+       rc=$?
+       set -e
+}
+ensure_dirs () {
+       pidfiledir=${PIDFILE%/*}
+       if test -d ${pidfiledir}; then return; fi
+       mkdir -m 755 $pidfiledir
+       chown $USER $pidfiledir
+}
+
+dump_firewall () {
+       iptables -L -v -n
+}
+
+print_config () {
+       $AS_USER $DAEMON $HIPPOTATD_ARGS --print-config "$1"
+}
+
+check_firewall () {
+       vnetwork=$(print_config vnetwork)
+       if dump_firewall | fgrep " $vnetwork " >/dev/null; then :; else
+               log_failure_msg \
+ "no entry in firewall for insecure vnetwork $vnetwork"
+               exit 1
+       fi
+}
+
+do_start () {
+       check_firewall
+       ensure_dirs
+       ssd     --chuid $USER --start                           \
+               --startas /bin/sh -- -ec '"$@"' x               \
+               $AUTHBIND $DAEMON --daemon --pidfile=$PIDFILE   \
+               --syslog-facility=$LOGFACILITY $HIPPOTATD_ARGS
+}
+do_stop () {
+       ssd     --stop --oknodo --retry 5
+}
+
+case "$1" in
+start)
+       log_daemon_msg "Starting $DESCRIPTION" hippotatd
+       do_start
+       log_end_msg $rc
+       exit $rc
+       ;;
+
+stop)
+       log_daemon_msg "Stopping $DESCRIPTION" hippotatd
+       do_stop
+       log_end_msg $rc
+       exit $rc
+       ;;
+
+restart|force-reload)
+       log_daemon_msg "Restarting $DESCRIPTION" hippotatd
+       do_stop
+       sleep 1
+       do_start
+       log_end_msg $rc
+       ;;
+
+reload)
+       log_failure_msg "Cannot reload hippotat - need restart"
+       exit 1
+       ;;
+
+*)
+       echo >&2 "$0: unknown action $1"
+       exit 1
+       ;;
+
+esac
+
+exit 0
index 419c3b74a5bd75490f58a2e68d98f8cd8218fc1f..2d33f6ac8992b7da84b39a5bca0742c4962d3349 100755 (executable)
@@ -1,25 +1,4 @@
 #!/usr/bin/make -f
 
-SHELL=/bin/bash
-
-export PYBUILD_INSTALL_DIR=/usr/share/hippotat/python3
-
 %:
-       dh $@ --with python3 --buildsystem=pybuild
-
-i=debian/hippotat
-
-debian/copyright: COPYING AGPLv3+CAFv2
-       cat $^ >$@.tmp && mv -f $@.tmp $@
-
-override_dh_python3:
-       dh_python3 -O--buildsystem=pybuild
-       dh_installdirs /usr/sbin
-       mv $i/usr/{bin,sbin}/hippotatd
-
-override_dh_installinit:
-       dh_installinit --name=hippotatd
-
-override_dh_compress:
-       find $i/usr/{bin,sbin} -type f | xargs ./subst-sys-path
-       dh_compress
+       dh $@
diff --git a/docs/README.md b/docs/README.md
new file mode 120000 (symlink)
index 0000000..32d46ee
--- /dev/null
@@ -0,0 +1 @@
+../README.md
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644 (file)
index 0000000..f37c66f
--- /dev/null
@@ -0,0 +1,187 @@
+# -*- coding: utf-8 -*-
+#
+# Configuration file for the Sphinx documentation builder.
+#
+# This file does only contain a selection of the most common options. For a
+# full list see the documentation:
+# http://www.sphinx-doc.org/en/master/config
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'Hippotat'
+copyright = '2021 Ian Jackson and the contributors to Hippotat'
+author = 'Ian Jackson et al'
+
+# The short X.Y version
+version = ''
+# The full version, including alpha/beta/rc tags
+release = ''
+
+
+# -- General configuration ---------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    'recommonmark',
+    'sphinx.ext.autosectionlabel',
+]
+autosectionlabel_prefix_document = True
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+#source_suffix = ['.rst', '.md']
+#source_suffix = '.rst'
+
+# https://github.com/readthedocs/recommonmark (retrieved 8.4.2021)
+from recommonmark.parser import CommonMarkParser
+source_parsers = {
+    '.md': CommonMarkParser,
+}
+source_suffix = {
+   '.rst': 'restructuredtext',
+   '.txt': 'markdown',
+   '.md': 'markdown',
+}
+
+# The master toctree document.
+master_doc = 'index'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = None
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'classic'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# The default sidebars (for documents that don't match any pattern) are
+# defined by theme itself.  Builtin themes are using these templates by
+# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
+# 'searchbox.html']``.
+#
+# html_sidebars = {}
+
+
+# -- Options for HTMLHelp output ---------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Hippotatdoc'
+
+
+# -- Options for LaTeX output ------------------------------------------------
+
+latex_elements = {
+    # The paper size ('letterpaper' or 'a4paper').
+    #
+    # 'papersize': 'letterpaper',
+
+    # The font size ('10pt', '11pt' or '12pt').
+    #
+    # 'pointsize': '10pt',
+
+    # Additional stuff for the LaTeX preamble.
+    #
+    # 'preamble': '',
+
+    # Latex figure (float) alignment
+    #
+    # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+latex_documents = [
+    (master_doc, 'Hippotat.tex', 'Hippotat Documentation',
+     'Ian Jackson and the contributors to Hippotat', 'manual'),
+]
+
+
+# -- Options for manual page output ------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    (master_doc, 'otter', 'Hippotat Documentation',
+     [author], 1)
+]
+
+
+# -- Options for Texinfo output ----------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+    (master_doc, 'Hippotat', 'Hippotat Documentation',
+     author, 'Hippotat', 'Online Table Top Environment Renderer',
+     'Games'),
+]
+
+
+# -- Options for Epub output -------------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = project
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#
+# epub_identifier = ''
+
+# A unique identification for the text.
+#
+# epub_uid = ''
+
+# A list of files that should not be packed into the epub file.
+epub_exclude_files = ['search.html']
diff --git a/docs/config.rst b/docs/config.rst
new file mode 100644 (file)
index 0000000..08d5d77
--- /dev/null
@@ -0,0 +1,64 @@
+Configuration scheme
+====================
+
+Configuration is in an INI-like format.  Sections start with lines
+``[...]``.  Every setting must be in a section.  ``#`` and ``;``
+comment lines are supported.  Settings ``nmae = value``.  Whitespace
+around the name and value is ignored.
+
+The configuration files are resolved to a configuration for each
+pairwise link between a client and a server.
+
+Sections
+--------
+
+The same config key may appear in multiple sections; for example, in a
+section specific to a link, as well as one for all links to a server.
+
+Unless otherwise specified, any particular config setting for a
+particular link is the value from the first of the following
+applicable sections, or failing that the built-in default:
+
+  * ``[<servername> <client>]``
+  * ``[<client>]``
+  * ``[<servername>]`` (often ``[SERVER]``)
+  * ``[COMMON]``
+
+``<client>`` is the client's virtual address in IPv4 or IPv6 literal
+syntax (without any surrounding ``[..]``.
+
+``<servername>`` must be in the syntrax of a valid lowercase DNS
+hostname (and not look like an address), or be literally ``SERVER``.
+
+There are also these special sections:
+
+ * ``[<servername> LIMIT]``
+ * ``[LIMIT]``
+
+Files
+-----
+
+Both client and server read the files
+
+ * ``/etc/hippotat/main.cfg`` (if it exists)
+ * ``/etc/hippotat/config.d/*``
+ * ``/etc/hippotat/secrets.d/*``
+
+Here ``*`` means all contained files whose names consists of only
+ascii alphanumerics plus ``-`` and ``_``.
+
+The ``--config`` option can be used to override the directory (usually
+``/etc/hippotat``).  Additonally each ``--extra-config`` option names
+an existing file or directory to be processed analagously.
+
+The ini file format sections from these files are all unioned.  Later
+files (in the list above, or alphabetically later) can override
+settings from earlier ones.
+
+Note that although it is conventional for information for a particular
+server or client to be in a file named after that endpoint, there is
+no semantic link: all the files are always read and the appropriate
+section from each is applied to every link.
+
+(If ``main.cfg`` does not exist, ``master.cfg`` will be tried for
+backward compatibility reasons.)
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644 (file)
index 0000000..31973fa
--- /dev/null
@@ -0,0 +1,16 @@
+Hippotat - Asinine IP over HTTP
+===============================
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+
+   README
+   config.rst
+   settings.rst
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`search`
diff --git a/docs/settings.rst b/docs/settings.rst
new file mode 100644 (file)
index 0000000..7b44350
--- /dev/null
@@ -0,0 +1,203 @@
+Configuration settings
+======================
+
+Exceptional settings
+--------------------
+
+``server``
+  Specifies ``<servername>``.
+  Is looked up in ``[SERVER]`` and ``[COMMON]`` only.
+  If not specified there, it is ``SERVER``.
+
+  Used by server to select the appropriate parts of the
+  rest of the configuration.  Ignored by the client.
+
+``secret``
+  Looked up in the usual way, but used by client and server to
+  determine which possible peerings to try to set up, and which to
+  ignore.
+
+  We define the sets of putative clients and servers, as follows:
+  all those, for which there is any section (even an empty one)
+  whose name is based on ``<client>`` or ``<servername>`` (as applicable).
+  (``LIMIT`` sections do not count.)
+
+  The server queue packets for, and accept requests from, each
+  putative client for which the config search yields a secret.
+
+  Each client will create a local interface, and try to communicate
+  with the server, for each possible pair (putative server,
+  putative client) for which the config search yields a secret.
+
+  The value is a string, fed directly into HMAC.
+
+``ipif``
+  Command to run to create and communicate with local network
+  interface.  Passed to sh -c.  Must speak SLIP on stdin/stdout.
+  The following interpolations aare substituted:
+
+  ============== ============ ============ =============== =================
+  Input          ``%{local}``  ``%{peer}``   ``%{rnets}``    ``%{ifname}``
+  ============== ============ ============ =============== =================
+  **on server**  ``vaddr``    ``vrelay``   ``vnetwork``    ``ifname_server``
+  **on client**  ``client``   ``vaddr``    ``vroutes``     ``ifname_client``
+  ============== ============ ============ =============== =================
+
+  **Always:** ``%{mtu}``, and ``%%`` to indicate a literal ``%``.
+
+  (For compatibility with older hippotat, ``%(var)s`` is supported too
+  but this is deprecated since the extra ``s`` is confusing.)
+
+  On server: applies to all clients; not looked up in client-specific sections.
+  On client: may be different for different servers.
+
+  [string; ``userv root ipif %{local},%{peer},%{mtu},slip '%{rnets}'``]
+
+
+Capped settings
+---------------
+
+Values in ``[<server> LIMIT]`` and ``[LIMIT]`` are a cap (maximum) on
+those from the other sections (including ``COMMON``).  If a larger
+value is obtained, it is (silently) reduced to the limit value.
+
+
+``max_batch_down``
+  Size limit for response payloads.
+
+  On client, incoming response bodies are limited to this (plus
+  a fixed constant metadata overhead).
+  Server uses minimum of client's and server's configured values
+  (old servers just use server's value).
+
+  [``65536`` (bytes); ``LIMIT``: ``262144``]
+
+``max_batch_up``
+  Size limit for request upbound payloads.  On client, used directly,
+  with ``LIMIT`` applied.
+
+  On server, only ``LIMIT`` is relevant, and must be at least the
+  client's configured value (checked).
+
+  [``4000`` (bytes); ``LIMIT``: ``262144``]
+
+``max_queue_time``
+  Discard packets after they have been queued this long
+  waiting for http.
+
+  On server: setting applies to downward packets.
+  On client: setting applies to upward packets.
+
+  [``10`` (s); ``LIMIT``: ``121``]
+
+``http_timeout``
+  On server: return with empty payload any http request oustanding
+  for this long.
+
+  On client: give up on any http request outstanding for
+  for this long plus ``http_timeout_grace``.
+
+  Warning messages about link problems, printed by the client,
+  are rate limited to no more than one per effective timeout.
+
+  Client's effective timeout must be at least server's (checked).
+
+  [``30`` (s); ``LIMIT``: ``121``]
+
+target_requests_outstanding   
+  On client: try to keep this many requests outstanding, to
+  allow for downbound data transfer.
+  On server: whenever number of outstanding requests for
+  a client exceeds this, returns oldest with empty payload.
+  Must match between client and server (checked).
+  [``3``; ``LIMIT``: ``10``]
+
+
+Ordinary settings, used by both, not client-specific
+----------------------------------------------------
+
+On the server these are forbidden in the client-specific config
+sections.
+
+``addrs``
+  Public IP (v4 or v6) address(es) of the server; space-separated.
+  On server: mandatory; used for bind.
+  On client: used only to construct default ``url``.
+  No default.
+
+``vnetwork``
+  Private network range.  Must contain all
+  ``<client>``s.  Must contain ``vaddr`` and ``vrelay``, and is used
+  to compute their defaults.  [CIDR syntax (``<prefix>/<length>``);
+  ``172.24.230.192/28``]
+
+``vaddr``
+  Address of server's virtual interface.
+  [default: first host entry in ``vnetwork``, so ``172.24.230.193``]
+
+``vrelay``
+  Virtual point-to-point address used for tunnel routing
+  (does not appear in packets).
+  [default: first host entry in ``vnetwork`` other than ``vaddr``,
+  so ``172.24.230.194``]
+
+``port``
+  Public port number of the server.
+  On server: used for bind.
+  On client: used only to construct default url.
+  [``80``]
+
+``mtu``
+  Of virtual interface.
+  Must match exactly at each end (checked).
+  [``1500`` (bytes)]
+
+  
+Ordinary settings, used by server only
+--------------------------------------
+
+``max_clock_skew``
+  Permissible clock skew between client and server.
+  Hippotat will not work if clock skew is more than this.
+  Conversely: when moving client from one public network to
+  another, the first network can deny service to the client for
+  this period after the client leaves the first network.
+  [``300`` (s)]
+
+``ifname_server``
+  | Virtual interface name on the server.  [``shippo%d``]
+  | Any ``%d`` is interpolated (by the kernel).
+
+
+Ordinary settings, used by client only
+--------------------------------------
+
+``http_timeout_grace``
+  See ``http_timeout``.  [``5`` (s)]
+
+``max_requests_outstanding``
+  Client will hold off sending more requests than this to
+  server even if it has data to send.  [``6``]
+
+``success_report_interval``
+  If nonzero, report success periodically.  Otherwise just
+  report it when we first have success.  [``3600`` (s)]
+
+``http_retry``
+  If a request fails, wait this long before considering it
+  "finished" - to limit rate of futile requests (and also
+  to limit rate of moaning on stderr).  [``5`` s]
+
+``url``
+  Public url of server.
+  [``http://<first-entry-in-addrs>:<port>/``]
+
+``vroutes``
+  Additional virtual addresses to be found at the server
+  end, space-separated.  Routes to those will be created on
+  the client.  ``vrelay`` is included implicitly.
+  [CIDR syntax, space separated; default: none]
+
+``ifname_client``
+  | Virtual interface name on the client.  [``hippo%d``]
+  | Any ``%d`` is interpolated (by the kernel).
diff --git a/macros/Cargo.toml b/macros/Cargo.toml
new file mode 100644 (file)
index 0000000..4da04be
--- /dev/null
@@ -0,0 +1,21 @@
+# Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+[package]
+name = "hippotat-macros"
+version = "0.0.1"
+edition = "2018"
+description="Asinine HTTP-over-IP, proc-macros"
+license="GPL-3.0-or-later"
+repository="https://salsa.debian.org/iwj/hippotat"
+
+[dependencies]
+itertools = "0.10"
+syn = { version = "1", features=["extra-traits"] }
+proc-macro2 = "1"
+quote = "1"
+
+[lib]
+path = "macros.rs"
+proc-macro = true
diff --git a/macros/macros.rs b/macros/macros.rs
new file mode 100644 (file)
index 0000000..1eaa758
--- /dev/null
@@ -0,0 +1,203 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use syn::{parse_macro_input, parse_quote};
+use syn::{Data, DataStruct, DeriveInput, LitStr, Meta, NestedMeta};
+use quote::{quote, quote_spanned, ToTokens};
+use proc_macro2::{Literal, TokenStream};
+
+use std::cell::RefCell;
+
+use itertools::Itertools;
+
+/// Generates config resolver method
+/// 
+/// Each field ends up having an SKL and a method.
+/// The method actually looks up the value in a particular link context.
+/// SKL is passed to the method, which usually uses it to decide which
+/// sections to look in.  But it is also used by general validation,
+/// unconditionally, to reject settings in the wrong section.
+///
+/// Atrributes:
+///
+///  * `limited`, `server`, `client`: cooked sets of settings;
+///    default `SKL` is `PerClient` except for `limited`
+///  * `global` and `per_client`: set the SKL.
+///  * `special(method, SKL)`
+///
+/// Generated code
+///
+/// ```no_run
+/// impl<'c> ResolveContext<'c> {
+///
+///   // SKL here is used by SectionKindList::contains()
+///   const FIELDS: &'static [(&'static str, SectionKindList)] = &[ ... ];
+///
+///   #[throws(AE)]
+///   fn resolve_instance(&self) -> InstanceConfig {
+///     InstanceConfig {
+///       ...
+///        // SKL here is usually passed to first_of, but the method
+///        // can do something more special.
+///        max_batch_down: self.limited("max_batch_down", SKL::PerClient)?,
+///        ...
+///      }
+///   }
+/// }
+///
+/// pub struct InstanceConfigCommon { ... }
+/// impl InstanceConfigCommon {
+///   pub fn from(l: &[InstanceConfig]) { InstanceConfigCommon {
+///     field: <Type as ResolveGlobal>::resolve(l.iter().map(|e| &e.field)),
+///     ...
+///   } }
+/// }
+/// ```
+#[proc_macro_derive(ResolveConfig, attributes(
+  limited, server, client, computed, special,
+  per_client, global,
+))]
+pub fn resolve(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+  let input = parse_macro_input!(input as DeriveInput);
+
+  let (fields, top_ident) = match input {
+    DeriveInput {
+      ref ident,
+      data: Data::Struct(DataStruct {
+        fields: syn::Fields::Named(ref f),
+        ..
+      }),
+      ..
+    } => (f, ident),
+    _ => panic!(),
+  };
+
+  let target = &input.ident;
+
+  let mut names = vec![];
+  let mut output = vec![];
+  let mut global_fields = vec![];
+  let mut global_assignments = vec![];
+  for field in &fields.named {
+    //dbg!(field);
+    let fname = &field.ident.as_ref().unwrap();
+    let ty = &field.ty;
+    let fname_span = fname.span();
+    let skl = RefCell::new(None);
+    let set_skl = |new| {
+      let mut skl = skl.borrow_mut();
+      if let Some(old) = &*skl { panic!("dup SKL {} and {} for field {}",
+                                        old, new, &fname); }
+      *skl = Some(new);
+    };
+    let mut method = quote_spanned!{fname_span=> ordinary };
+    for attr in &field.attrs {
+      let atspan = attr.path.segments.last().unwrap().ident.span();
+      if attr.tokens.is_empty() {
+        if &attr.path == &parse_quote!{ per_client } {
+          set_skl(quote_spanned!{fname_span=> SectionKindList::PerClient });
+          continue;
+        } else if &attr.path == &parse_quote!{ global } {
+          set_skl(quote_spanned!{fname_span=> SectionKindList::Global });
+          global_fields.push(syn::Field {
+            attrs: vec![],
+            ..field.clone()
+          });
+          global_assignments.push(quote_spanned!(fname_span=>
+            #fname: <#ty as ResolveGlobal>::resolve
+                    (l.iter().map(|e| &e.#fname)),
+          ));
+          continue;
+        }
+        method = attr.path.to_token_stream();
+        if &attr.path == &parse_quote!{ limited } {
+          set_skl(quote_spanned!{atspan=> SectionKindList::Limited });
+        } else if &attr.path == &parse_quote!{ client } {
+          set_skl(quote_spanned!{atspan=> SectionKindList::PerClient });
+        } else if &attr.path == &parse_quote!{ computed } {
+          set_skl(quote_spanned!{atspan=> SectionKindList::None });
+        }
+      } else if &attr.path == &parse_quote!{ special } {
+        let meta = match attr.parse_meta().unwrap() {
+          Meta::List(list) => list,
+          _ => panic!(),
+        };
+        let (tmethod, tskl) = meta.nested.iter().collect_tuple().unwrap();
+        fn get_path(meta: &NestedMeta) -> TokenStream {
+          match meta {
+            NestedMeta::Meta(Meta::Path(ref path)) => path.to_token_stream(),
+            _ => panic!(),
+          }
+        }
+        method = get_path(tmethod);
+        *skl.borrow_mut() = Some(get_path(tskl));
+      }
+    }
+    let fname_string = fname.to_string();
+    let fname_lit = Literal::string( &fname_string );
+    let skl = skl.into_inner()
+      .expect(&format!("SKL not specified! (field {})!", fname));
+
+    names.push(quote!{
+      (#fname_lit, #skl),
+    });
+    //dbg!(&method);
+    output.push(quote!{
+      #fname: rctx. #method ( #fname_lit, #skl )?,
+    });
+    //eprintln!("{:?} method={:?} skl={:?}", field.ident, method, skl);
+  }
+  //dbg!(&output);
+
+  let global = syn::Ident::new(&format!("{}Global", top_ident),
+                               top_ident.span());
+
+  let output = quote! {
+    impl #target {
+      const FIELDS: &'static [(&'static str, SectionKindList)]
+        = &[ #( #names )* ];
+
+      fn resolve_instance(rctx: &ResolveContext)
+          -> ::std::result::Result<#target, anyhow::Error>
+      {
+        ::std::result::Result::Ok(#target {
+          #( #output )*
+        })
+      }
+    }
+
+    #[derive(Debug)]
+    pub struct #global {
+      #( #global_fields ),*
+    }
+
+    impl #global {
+      pub fn from(l: &[#top_ident]) -> #global { #global {
+        #( #global_assignments )*
+      } }
+    }
+  };
+
+  //eprintln!("{}", &output);
+  output.into()
+}
+
+#[proc_macro]
+pub fn into_crlfs(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+  let input: proc_macro2::TokenStream = input.into();
+  let token: LitStr = syn::parse2(input).expect("expected literal");
+  let input = token.value();
+  let output = input.split_inclusive('\n')
+    .map(|s| s.trim_start_matches(&[' ','\t'][..]))
+    .map(|s| match s.strip_suffix("\n") {
+      None => [s, ""],
+      Some(l) => [l, "\r\n"],
+    })
+    .flatten()
+    .collect::<String>();
+  //dbg!(&output);
+  let output = LitStr::new(&output, token.span());
+  let output = quote!(#output);
+  output.into()
+}
diff --git a/old-python/.gitignore b/old-python/.gitignore
new file mode 100644 (file)
index 0000000..f5dd26c
--- /dev/null
@@ -0,0 +1,17 @@
+data.dump.dbg
+[tuv]
+tmp
+srcbomb.tar.gz
+srcpkgsbomb.tar
+
+build
+.pybuild
+hippotat.egg-info
+
+debian/files
+debian/debhelper-*-stamp
+debian/*.debhelper.log
+debian/hippotat.substvars
+debian/hippotat.*.debhelper
+
+debian/hippotat/
similarity index 100%
rename from AGPLv3+CAFv2
rename to old-python/AGPLv3+CAFv2
similarity index 100%
rename from CONTRIBUTING
rename to old-python/CONTRIBUTING
similarity index 100%
rename from COPYING
rename to old-python/COPYING
diff --git a/old-python/GPLv3 b/old-python/GPLv3
new file mode 100644 (file)
index 0000000..94a9ed0
--- /dev/null
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/old-python/PROTOCOL b/old-python/PROTOCOL
new file mode 100644 (file)
index 0000000..e18cf0e
--- /dev/null
@@ -0,0 +1,42 @@
+Server maintains a queue of outbound packets for each user
+
+Packets which are older than the applicable max_queue_time are discarded
+
+Each incoming request to the server takes up to max_batch_down bytes
+from the queue and returns them as the POST response body payload
+
+Each incoming request contains up to max_batch_up bytes of payload.
+It's a multipart/form-data.
+
+Authentication: clock-based lifetime-limited bearer tokens.
+
+Encryption and integrity checking: none.  Use a real VPN over this!
+
+Routing assistance: none in hippotat; can be requested on client
+ from userv-ipif via `vroutes' parameter.  Use with secnet polypath
+ ideally uses the special support in secnet 0.4.x.
+
+Client form parameters (multipart/form-data):
+ m             metadata, newline-separated list (text file) of
+                       client ip address (textual)
+                       token
+                       target_requests_outstanding
+                       http_timeout
+ d              data (SLIP format, with SLIP_ESC and `-' swapped)
+
+
+Authentication token is:
+        <time_t in hex with no leading 0s> <hmac in base64>
+(separated by a single space).  The hmac is
+        HMAC(secret, <time_t in hex>)
+
+
+Possible future nonce-based authentication:
+
+server keeps big nonce counter for each client
+meaning is:
+ nonce counter is most recent nonce client has sent
+also server keeps bitmap of the previous ?64 nonces,
+ whether client has sent them
+
+client picks.... xxx
similarity index 100%
rename from README.config
rename to old-python/README.config
diff --git a/old-python/debian/changelog b/old-python/debian/changelog
new file mode 100644 (file)
index 0000000..298792c
--- /dev/null
@@ -0,0 +1,6 @@
+hippotat (0.1~UNRELEASED) unstable; urgency=medium
+
+  * 
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sat, 08 Apr 2017 17:57:42 +0100
+
diff --git a/old-python/debian/compat b/old-python/debian/compat
new file mode 100644 (file)
index 0000000..ec63514
--- /dev/null
@@ -0,0 +1 @@
+9
diff --git a/old-python/debian/control b/old-python/debian/control
new file mode 100644 (file)
index 0000000..b5e5935
--- /dev/null
@@ -0,0 +1,11 @@
+Source: hippotat
+Build-Depends: debhelper (>= 9), dh-python, python3
+Maintainer: Ian Jackson <ijackson@chiark.greenend.org.uk>
+
+Package: hippotat
+Depends: python3, ${python3:Depends}
+Recommends: userv, userv-utils (>= 0.6.0~~iwj4), cpio
+Suggests: authbind
+Architecture: all
+Description: IP Over HTTP (Asinine)
+ IP-over-HTTP client and server.
similarity index 100%
rename from debian/copyright
rename to old-python/debian/copyright
diff --git a/old-python/debian/rules b/old-python/debian/rules
new file mode 100755 (executable)
index 0000000..419c3b7
--- /dev/null
@@ -0,0 +1,25 @@
+#!/usr/bin/make -f
+
+SHELL=/bin/bash
+
+export PYBUILD_INSTALL_DIR=/usr/share/hippotat/python3
+
+%:
+       dh $@ --with python3 --buildsystem=pybuild
+
+i=debian/hippotat
+
+debian/copyright: COPYING AGPLv3+CAFv2
+       cat $^ >$@.tmp && mv -f $@.tmp $@
+
+override_dh_python3:
+       dh_python3 -O--buildsystem=pybuild
+       dh_installdirs /usr/sbin
+       mv $i/usr/{bin,sbin}/hippotatd
+
+override_dh_installinit:
+       dh_installinit --name=hippotatd
+
+override_dh_compress:
+       find $i/usr/{bin,sbin} -type f | xargs ./subst-sys-path
+       dh_compress
similarity index 100%
rename from fake-userv
rename to old-python/fake-userv
similarity index 100%
rename from form.html
rename to old-python/form.html
similarity index 100%
rename from hippotat
rename to old-python/hippotat
similarity index 100%
rename from hippotatd
rename to old-python/hippotatd
similarity index 100%
rename from setup.py
rename to old-python/setup.py
similarity index 100%
rename from sgo-demo.cfg
rename to old-python/sgo-demo.cfg
similarity index 100%
rename from simple.cfg
rename to old-python/simple.cfg
similarity index 100%
rename from srcbombtest.py
rename to old-python/srcbombtest.py
similarity index 100%
rename from subst-sys-path
rename to old-python/subst-sys-path
similarity index 100%
rename from test.cfg
rename to old-python/test.cfg
similarity index 100%
rename from w3mstracetodump
rename to old-python/w3mstracetodump
diff --git a/server/daemon.rs b/server/daemon.rs
new file mode 100644 (file)
index 0000000..c2b39a4
--- /dev/null
@@ -0,0 +1,218 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use std::convert::TryInto;
+use std::ffi::{c_int, CStr};
+use std::io::IoSlice;
+use std::os::unix::io::RawFd;
+use std::slice;
+use std::str;
+use std::thread::panicking;
+
+use extend::ext;
+
+use nix::errno::*;
+use nix::fcntl::*;
+use nix::unistd::*;
+use nix::sys::stat::*;
+use nix::sys::signal::*;
+use nix::sys::uio::*;
+use nix::sys::wait::*;
+
+use hippotat::prelude as prelude;
+use prelude::default;
+
+pub struct Daemoniser {
+  drop_bomb: Option<()>,
+  intermediate_pid: Pid,
+  null_fd: RawFd,
+  st_wfd: RawFd,
+}
+
+fn crashv(ms: &[IoSlice<'_>]) -> ! {
+  unsafe {
+    let _ = writev(2, ms);
+    libc::_exit(18);
+  }
+}
+  
+macro_rules! crashv { { $( $m:expr ),* $(,)? } => {
+  match [
+    "hippotatd: ",
+    $( $m, )*
+    "\n",
+  ] {
+    ms => {
+      let ms = ms.map(|m| IoSlice::new(m.as_bytes()));
+      crashv(&ms)
+    }
+  }
+} }
+
+macro_rules! cstr { { $b:tt } => {
+  CStr::from_bytes_with_nul($b)
+    .unwrap_or_else(|_| crashm("cstr not nul terminated?! bug!"))
+} }
+
+fn crashm(m: &str) -> ! {
+  crashv!(m)
+}
+fn crashe(m: &str, e: Errno) -> ! {
+  crashv!(m, ": ", e.desc())
+}
+
+#[ext]
+impl<T> nix::Result<T> {
+  fn context(self, m: &str) -> T {
+    match self {
+      Ok(y) => y,
+      Err(e) => crashe(m, e),
+    }
+  }
+}
+
+const ITOA_BUFL: usize = 12;
+fn c_itoa(value: c_int, buf: &mut [u8; ITOA_BUFL]) -> &str {
+  unsafe {
+    *buf = [b'.'; ITOA_BUFL];
+    libc::snprintf({ let buf: *mut u8 = buf.as_mut_ptr(); buf as *mut i8 },
+                   ITOA_BUFL-2,
+                   cstr!(b"%x\0").as_ptr(),
+                   value);
+  }
+  let s = buf.splitn(2, |&c| c == b'\0').next()
+    .unwrap_or_else(|| crashm("splitn no next"));
+  str::from_utf8(s).unwrap_or_else(|_| crashm("non-utf-8 from snprintf!"))
+}
+
+unsafe fn mdup2(oldfd: RawFd, newfd: RawFd, what: &str) {
+  match dup2(oldfd, newfd) {
+    Ok(got) if got == newfd => { },
+    Ok(_) => crashm("dup2 gave wrong return value"),
+    Err(e) => crashv!("dup2 ", what, ": ", e.desc()),
+  }
+}
+
+unsafe fn write_status(st_wfd: RawFd, estatus: u8) {
+  match write(st_wfd, slice::from_ref(&estatus)) {
+    Ok(1) => {}
+    Ok(_) => crashm("write child startup exit status: short write"),
+    Err(e) => crashe("write child startup exit status", e),
+  }
+}
+
+unsafe fn parent(st_rfd: RawFd) -> ! {
+  let mut exitstatus = 0u8;
+  loop {
+    match read(st_rfd, slice::from_mut(&mut exitstatus)) {
+      Ok(0) => crashm("startup/daemonisation failed"),
+      Ok(1) => libc::_exit(exitstatus.into()),
+      Ok(_) => crashm("read startup: excess read!"),
+      Err(e) if e == Errno::EINTR => continue,
+      Err(e) => crashe("read startup signal pipe", e),
+    }
+  }
+}
+
+unsafe fn intermediate(child: Pid, st_wfd: RawFd) -> ! {
+  let mut wstatus: c_int = 0;
+
+  let r = libc::waitpid(child.as_raw(), &mut wstatus, 0);
+  if r == -1 { crashe("await child startup status",
+                      nix::errno::from_i32(errno())) }
+  if r != child.as_raw() { crashm("await child startup status: wrong pid") }
+
+  let cooked = WaitStatus::from_raw(child, wstatus)
+    .context("await child startup status: convert wait status");
+  match cooked {
+    WaitStatus::Exited(_, estatus) => {
+      let estatus: u8 = estatus.try_into()
+        .unwrap_or_else(|_| crashm(
+          "await child startup status: exit status out of range!"));
+      write_status(st_wfd, estatus);
+      libc::_exit(0);
+    }
+
+    WaitStatus::Signaled(_, signal, coredump) => {
+      crashv!("startup failed: died due to signal: ", signal.as_str(),
+              if coredump { " (core dumped)" } else { "" });
+    },
+
+    _ => {
+      crashv!("child startup exit status was strange!  0x",
+              c_itoa(wstatus, &mut default()))
+    }
+  }
+}
+
+impl Daemoniser {
+  /// Start daemonising - call before any threads created!
+  pub fn phase1() -> Self {
+    unsafe {
+      let null_fd = open(cstr!(b"/dev/null\0"), OFlag::O_RDWR, Mode::empty())
+        .context("open /dev/null");
+      mdup2(null_fd, 0, "null onto stdin");
+
+      let (st_rfd, st_wfd) = pipe().context("pipe");
+
+      match fork().context("fork (1)") {
+        ForkResult::Child => { }
+        ForkResult::Parent { child: _ } => {
+          close(st_wfd).context("close st_wfd pipe");
+          parent(st_rfd)
+        },
+      }
+
+      close(st_rfd).context("close st_rfd pipe");
+      setsid().context("setsid");
+      let intermediate_pid = Pid::this();
+
+      match fork().context("fork (2)") {
+        ForkResult::Child => { }
+        ForkResult::Parent { child } => {
+          intermediate(child, st_wfd)
+        },
+      }
+
+      Daemoniser {
+        drop_bomb: Some(()),
+        intermediate_pid,
+        null_fd,
+        st_wfd,
+      }
+    }
+  }
+
+  pub fn complete(mut self) {
+    unsafe {
+      mdup2(self.null_fd, 1, "null over stdin");
+
+      if Pid::parent() != self.intermediate_pid {
+        crashm(
+          "startup complete, but our parent is no longer the intermediate?");
+      }
+      kill(self.intermediate_pid, Some(Signal::SIGKILL))
+        .context("kill intermediate (after startup complete)");
+
+      write_status(self.st_wfd, 0);
+      mdup2(self.null_fd, 2, "null over stderrr");
+
+      self.drop_bomb.take();
+    }
+  }
+}
+
+impl Drop for Daemoniser {
+  fn drop(&mut self) {
+    if let Some(()) = self.drop_bomb.take() {
+      if panicking() {
+        // We will crash in due course, having printed some messages
+        // to stderr, presumably.
+        return
+      } else {
+        panic!("Daemonizer object dropped unexpectedly, startup failed");
+      }
+    }
+  }
+}
diff --git a/server/server.rs b/server/server.rs
new file mode 100644 (file)
index 0000000..02af6f8
--- /dev/null
@@ -0,0 +1,258 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use hippotat::prelude::*;
+
+mod daemon;
+mod suser;
+mod slocal;
+mod sweb;
+
+pub use daemon::Daemoniser;
+pub use sweb::{WebRequest, WebResponse, WebResponseBody};
+pub use suser::User;
+
+#[derive(StructOpt,Debug)]
+pub struct Opts {
+  #[structopt(flatten)]
+  pub log: LogOpts,
+
+  #[structopt(flatten)]
+  pub config: config::Opts,
+
+  /// Daemonise
+  #[structopt(long)]
+  daemon: bool,
+}
+
+pub const METADATA_MAX_LEN: usize = MAX_OVERHEAD;
+
+
+// ----- Backpressure discussion -----
+
+// These two kinds of channels are sent blockingly, so this means the
+// task which calls route_packet can get this far ahead, before a
+// context switch to the receiving task is forced.
+pub const MAXQUEUE_ROUTE2USER: usize = 15;
+pub const MAXQUEUE_ROUTE2LOCAL: usize = 50;
+
+// This channel is sent with try_send, ie non-blocking.  If the user
+// task becomes overloaded, requests will start to be rejected.
+pub const MAXQUEUE_WEBREQ2USER: usize = 5;
+
+// The user task prioritises 1. returning requests or discarding data,
+// 2. handling data routed to it.  Ie it prefers to drain queues.
+//
+// The slocal task prioritises handling routed data and writing it
+// (synchronously) to the local kernel.  So if the local kernel starts
+// blocking, all tasks may end up blocked waiting for things to drain.
+
+
+#[derive(Debug)]
+pub struct Global {
+  config: config::InstanceConfigGlobal,
+  local_rx: mpsc::Sender<RoutedPacket>,
+  all_clients: HashMap<ClientName, User>,
+}
+
+pub struct RoutedPacket {
+  pub data: RoutedPacketData,
+//  pub source: Option<ClientName>, // for eh, tracing, etc.
+}
+
+// not MIME data, valid SLIP (checked)
+pub type RoutedPacketData = Box<[u8]>;
+
+// loop prevention
+// we don't decrement the ttl (naughty) but loops cannot arise
+// because only the server has any routing code, and server
+// has no internal loops, so worst case is
+//  client if -> client -> server -> client' -> client if'
+// and the ifs will decrement the ttl.
+mod may_route {
+  #[derive(Clone,Debug)]
+  pub struct MayRoute(());
+  impl MayRoute {
+    pub fn came_from_outside_hippotatd() -> Self { Self(()) }
+  }
+}
+pub use may_route::MayRoute;
+
+pub async fn route_packet(global: &Global,
+                          transport_conn: &str, source: Option<&ClientName>,
+                          packet: RoutedPacketData, daddr: IpAddr,
+                          _may_route: MayRoute)
+{
+  let c = &global.config;
+  let len = packet.len();
+  let trace = |how: &str, why: &str| {
+    trace!("{} to={:?} came=={} user={} len={} {}",
+           how, daddr, transport_conn,
+           match source {
+             Some(s) => s as &dyn Display,
+             None => &"local",
+           },
+           len, why);
+  };
+
+  let (dest, why) =
+    if daddr == c.vaddr || ! c.vnetwork.iter().any(|n| n.contains(&daddr)) {
+      (Some(&global.local_rx), "via=local")
+    } else if daddr == c.vrelay {
+      (None, " vrelay")
+    } else if let Some(client) = global.all_clients.get(&ClientName(daddr)) {
+      (Some(&client.route), "via=client")
+    } else {
+      (None, "no-client")
+    };
+
+  let dest = if let Some(d) = dest { d } else {
+    trace("discard ", why); return;
+  };
+
+  let packet = RoutedPacket {
+    data: packet,
+//    source: source.cloned(),
+  };
+  match dest.send(packet).await {
+    Ok(()) => trace("forward", why),
+    Err(_) => trace("task-crashed!", why),
+  }
+}
+
+fn main() {
+  let opts = Opts::from_args();
+  let daemon = if opts.daemon {
+    Some(Daemoniser::phase1())
+  } else {
+    None
+  };
+
+  async_main(opts, daemon);
+}
+
+#[tokio::main]
+async fn async_main(opts: Opts, daemon: Option<Daemoniser>) {
+  let mut tasks: Vec<(
+    JoinHandle<AE>,
+    String,
+  )> = vec![];
+
+  config::startup(
+    "hippotatd", LinkEnd::Server,
+    &opts.config, &opts.log, |ics|
+  {
+    let global_config = config::InstanceConfigGlobal::from(&ics);
+
+    let ipif = Ipif::start(&global_config.ipif, None)?;
+
+    let ics = ics.into_iter().map(Arc::new).collect_vec();
+    let (client_handles_send, client_handles_recv) = ics.iter()
+      .map(|_ic| {
+        let (web_send, web_recv) = mpsc::channel(
+          MAXQUEUE_WEBREQ2USER
+        );
+        let (route_send, route_recv) = mpsc::channel(
+          MAXQUEUE_ROUTE2USER
+        );
+        ((web_send, route_send), (web_recv, route_recv))
+      }).unzip::<_,_,Vec<_>,Vec<_>>();
+
+    let all_clients = izip!(
+      &ics,
+      client_handles_send,
+    ).map(|(ic, (web_send, route_send))| {
+      (ic.link.client,
+       User {
+         ic: ic.clone(),
+         web: web_send,
+         route: route_send,
+       })
+    }).collect();
+
+    let (local_rx_send, local_tx_recv) = mpsc::channel(
+      MAXQUEUE_ROUTE2LOCAL
+    );
+
+    let global = Arc::new(Global {
+      config: global_config,
+      local_rx: local_rx_send,
+      all_clients,
+    });
+
+    for (ic, (web_recv, route_recv)) in izip!(
+      ics,
+      client_handles_recv,
+    ) {
+      let global_ = global.clone();
+      let ic_ = ic.clone();
+      tasks.push((tokio::spawn(async move {
+        suser::run(global_, ic_, web_recv, route_recv)
+          .await.void_unwrap_err()
+      }), format!("client {}", &ic)));
+    }
+
+    for addr in &global.config.addrs {
+      let global_ = global.clone();
+      let make_service = hyper::service::make_service_fn(
+        move |conn: &hyper::server::conn::AddrStream| {
+          let global_ = global_.clone();
+          let conn = Arc::new(format!("[{}]", conn.remote_addr()));
+          async { Ok::<_, Void>( hyper::service::service_fn(move |req| {
+            AssertUnwindSafe(
+              sweb::handle(conn.clone(), global_.clone(), req)
+            )
+              .catch_unwind()
+              .map(|r| r.unwrap_or_else(|_|{
+                crash(Err("panicked".into()), "webserver request task")
+              }))
+          }) ) }
+        }
+      );
+
+      let addr = SocketAddr::new(*addr, global.config.port);
+      let server = hyper::Server::try_bind(&addr)
+        .context("bind")?
+        .http1_preserve_header_case(true)
+        .serve(make_service);
+      info!("listening on {}", &addr);
+      let task = tokio::task::spawn(async move {
+        match server.await {
+          Ok(()) => anyhow!("shut down?!"),
+          Err(e) => e.into(),
+        }
+      });
+      tasks.push((task, format!("http server {}", addr)));
+    }
+
+    let global_ = global.clone();
+    let ipif = tokio::task::spawn(async move {
+      slocal::run(global_, local_tx_recv, ipif).await
+        .void_unwrap_err()
+    });
+    tasks.push((ipif, format!("ipif")));
+
+    Ok(())
+  });
+
+  if let Some(daemon) = daemon {
+    daemon.complete();
+  }
+
+  let (output, died_i, _) = future::select_all(
+    tasks.iter_mut().map(|e| &mut e.0)
+  ).await;
+
+  let task = &tasks[died_i].1;
+  let output = output.map_err(|je| je.to_string());
+  crash(output, task);
+}
+
+pub fn crash(what_happened: Result<AE, String>, task: &str) -> ! {
+  match what_happened {
+    Err(je) => error!("task crashed! {}: {}", task, &je),
+    Ok(e)   => error!("task failed! {}: {}",   task, &e ),
+  }
+  process::exit(12);
+}
diff --git a/server/slocal.rs b/server/slocal.rs
new file mode 100644 (file)
index 0000000..ab2f2fc
--- /dev/null
@@ -0,0 +1,76 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use super::*;
+
+pub async fn run(global: Arc<Global>,
+                 mut rx: mpsc::Receiver<RoutedPacket>,
+                 mut ipif: Ipif) -> Result<Void,AE> {
+  let r = async {
+    let mut goodness: i32 = 0;
+    const GOODNESS_SHIFT: u8 = 8;
+    const GOODNESS_MIN: i32 = -16;
+
+    loop {
+      select!{
+        biased;
+
+        data = rx.recv() =>
+        {
+          let data = data.ok_or_else(|| anyhow!("rx stream end!"))?;
+          let mut data = &*data.data;
+          let mut slip_end = &[SLIP_END][..];
+          let mut buf = Buf::chain(&mut data, &mut slip_end);
+          ipif.rx.write_all_buf(&mut buf).await
+            .context("write to ipif")?;
+        },
+
+        data = Ipif::next_frame(&mut ipif.tx) =>
+        {
+          let data = data?;
+          let may_route = MayRoute::came_from_outside_hippotatd();
+
+          goodness -= goodness >> GOODNESS_SHIFT;
+
+          match process1(SlipNoConv, global.config.mtu, &data, |header|{
+            let saddr = ip_packet_addr::<false>(header)?;
+            let daddr = ip_packet_addr::<true>(header)?;
+            Ok((saddr,daddr))
+          }) {
+            Err(PE::Empty) => { },
+
+            Err(pe) => {
+              goodness -= 1;
+              error!("[good={}] invalid data from local tx ipif {}",
+                     goodness, pe);
+              if goodness < GOODNESS_MIN {
+                throw!(anyhow!("too many bad packets, too few good ones!"))
+              }
+            },
+
+            Ok((ref data, (ref saddr, ref daddr)))
+            if ! global.config.vnetwork.iter().any(|n| n.contains(saddr)) => {
+              // pretent as if this came from route
+              trace!(
+                target: "hippotatd",
+ "discard to={:?} came=ipif user=local len={} outside-vnets: from={:?}",
+                daddr, saddr, data.len());
+            },
+
+            Ok((data, (_saddr, daddr))) => {
+              goodness += 1;
+              route_packet(
+                &global, "ipif", None,
+                data, daddr, may_route.clone()
+              ).await;
+            }
+          }
+        },
+      }
+    }
+  }.await;
+
+  ipif.quitting(None).await;
+  r
+}
diff --git a/server/suser.rs b/server/suser.rs
new file mode 100644 (file)
index 0000000..b51ea76
--- /dev/null
@@ -0,0 +1,228 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use super::*;
+
+#[derive(Debug)]
+pub struct User {
+  pub ic: Arc<InstanceConfig>,
+  pub web: mpsc::Sender<WebRequest>,
+  pub route: mpsc::Sender<RoutedPacket>,
+}
+
+pub async fn run(global: Arc<Global>,
+                 ic: Arc<InstanceConfig>,
+                 mut web: mpsc::Receiver<WebRequest>,
+                 mut routed: mpsc::Receiver<RoutedPacket>)
+                 -> Result<Void, AE>
+{
+  struct Outstanding {
+    reply_to: oneshot::Sender<WebResponse>,
+    oi: OutstandingInner,
+  }
+  #[derive(Debug)]
+  struct OutstandingInner {
+    deadline: Instant,
+    target_requests_outstanding: u32,
+    max_batch_down: u32,
+  }
+  let mut outstanding: VecDeque<Outstanding> = default();
+  let mut downbound: PacketQueue<RoutedPacketData> = default();
+
+  let try_send_response = |
+    reply_to: oneshot::Sender<WebResponse>,
+    response: WebResponse
+  | {
+    reply_to.send(response)
+      .unwrap_or_else(|_: WebResponse| {
+        /* oh dear */
+        trace!("unable to send response back to webserver! user={}",
+               &ic.link.client);
+      });
+  };
+
+  loop {
+    let eff_max_batch_down = outstanding
+      .iter()
+      .map(|o| o.oi.max_batch_down)
+      .min()
+      .unwrap_or(ic.max_batch_down)
+      .sat();
+    let earliest_deadline = outstanding
+      .iter()
+      .map(|o| o.oi.deadline)
+      .min();
+
+
+    if let Some(req) = {
+      let now = Instant::now();
+
+      if ! downbound.is_empty() {
+        outstanding.pop_front()
+      } else if let Some((i,_)) = outstanding.iter().enumerate().find({
+        |(_,o)| {
+          outstanding.len() > o.oi.target_requests_outstanding.sat()
+            ||
+          o.oi.deadline < now
+        }
+      }) {
+        Some(outstanding.remove(i).unwrap())
+      } else {
+        None
+      }
+    } {
+      let mut build: FrameQueueBuf = default();
+
+      loop {
+        let next = if let Some(n) = downbound.peek_front() { n }
+                   else { break };
+        // Don't add 1 for the ESC since we will strip one
+        if build.len() + next.len() >= eff_max_batch_down { break }
+        build.esc_push(downbound.pop_front().unwrap());
+      }
+      if ! build.is_empty() {
+        // skip leading ESC
+        build.advance(1);
+      }
+
+      let response = WebResponse {
+        data: Ok(build),
+        warnings: default(),
+      };
+
+      try_send_response(req.reply_to, response);
+    }
+
+    let max = usize::saturating_mul(
+      ic.max_requests_outstanding.sat(),
+      eff_max_batch_down,
+    ).saturating_add(1 /* one boundary SLIP_ESC which we'll trim */);
+
+    while downbound.total_len() > max {
+      let _ = downbound.pop_front();
+      trace!("{} discarding downbound-queue-full", &ic.link);
+    }
+
+    select!{
+      biased;
+
+      data = routed.recv() =>
+      {
+        let data = data.ok_or_else(|| anyhow!("routers shut down!"))?;
+        downbound.push_back(data.data);
+      },
+
+      req = web.recv() =>
+      {
+        let WebRequest {
+          initial, initial_remaining, length_hint, mut body,
+          boundary_finder,
+          reply_to, conn, mut warnings, may_route,
+        } = req.ok_or_else(|| anyhow!("webservers all shut down!"))?;
+
+        match async {
+
+          let initial_used = initial.len() - initial_remaining;
+
+          let whole_request = read_limited_bytes(
+            ic.max_batch_up.sat(),
+            initial,
+            length_hint,
+            &mut body
+          ).await.context("read request body")?;
+
+          let (meta, mut comps) =
+            multipart::ComponentIterator::resume_mid_component(
+              &whole_request[initial_used..],
+              boundary_finder
+            ).context("resume parsing body, after auth checks")?;
+
+          let mut meta = MetadataFieldIterator::new(&meta);
+
+          macro_rules! meta {
+            { $v:ident, ( $( $badcmp:tt )? ), $ret:expr,
+              let $server:ident, $client:ident $($code:tt)*
+            } => {
+              let $v = (||{
+                let $server = ic.$v;
+                let $client $($code)*
+                $(
+                  if $client $badcmp $server {
+                    throw!(anyhow!("mismatch: client={:?} {} server={:?}",
+                                   $client, stringify!($badcmp), $server));
+                  }
+                )?
+                Ok::<_,AE>($ret)
+              })().context(stringify!($v))?;
+              //dbg!(&$v);
+            }
+          }
+          meta!{
+            target_requests_outstanding, ( != ), client,
+            let server, client: u32 = meta.need_parse()?;
+          }
+          meta!{
+            http_timeout, ( > ), client,
+            let server, client = Duration::from_secs(meta.need_parse()?);
+          }
+          meta!{
+            mtu, ( != ), client,
+            let server, client: u32 = meta.parse()?.unwrap_or(server);
+          }
+          meta!{
+            max_batch_down, (), min(client, server),
+            let server, client: u32 = meta.parse()?.unwrap_or(server);
+          }
+          meta!{
+            max_batch_up, ( > ), client,
+            let server, client = meta.parse()?.unwrap_or(server);
+          }
+          let _ = max_batch_up; // we don't use this further
+
+          while let Some(comp) = comps.next(&mut warnings, PartName::d)? {
+            if comp.name != PartName::d {
+              warnings.add(&format_args!("unexpected part {:?}", comp.name))?;
+            }
+            slip::processn(Mime2Slip, mtu, comp.payload, |header| {
+              let saddr = ip_packet_addr::<false>(header)?;
+              if saddr != ic.link.client.0 { throw!(PE::Src(saddr)) }
+              let daddr = ip_packet_addr::<true>(header)?;
+              Ok(daddr)
+            }, |(daddr,packet)| route_packet(
+              &global, &conn, Some(&ic.link.client), daddr,
+              packet, may_route.clone(),
+            ).map(Ok),
+              |e| Ok::<_,SlipFramesError<_>>({ warnings.add(&e)?; })
+            ).await?;
+          }
+
+          let deadline = Instant::now() + http_timeout;
+
+          let oi = OutstandingInner {
+            target_requests_outstanding,
+            max_batch_down,
+            deadline,
+          };
+          Ok::<_,AE>(oi)
+        }.await {
+          Ok(oi) => outstanding.push_back(Outstanding { reply_to, oi }),
+          Err(e) => {
+            try_send_response(reply_to, WebResponse {
+              data: Err(e),
+              warnings,
+            });
+          },
+        }
+      }
+
+      () = async {if let Some(deadline) = earliest_deadline {
+        tokio::time::sleep_until(deadline).await;
+      } else {
+        future::pending().await
+      } } =>
+      {
+      }
+    }
+  }
+}
diff --git a/server/sweb.rs b/server/sweb.rs
new file mode 100644 (file)
index 0000000..bd9316a
--- /dev/null
@@ -0,0 +1,227 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use super::*;
+
+/// Sent from hyper worker pool task to client task
+#[derive(Debug)]
+pub struct WebRequest {
+  // initial part of body
+  // used up to and including first 2 lines of metadata
+  // end delimiter for the metadata not yet located, but in here somewhere
+  pub initial: Box<[u8]>,
+  pub initial_remaining: usize,
+  pub length_hint: usize,
+  pub body: hyper::body::Body,
+  pub boundary_finder: multipart::BoundaryFinder,
+  pub reply_to: oneshot::Sender<WebResponse>,
+  pub warnings: Warnings,
+  pub conn: Arc<String>,
+  pub may_route: MayRoute,
+}
+
+/// Reply from client task to hyper worker pool task
+#[derive(Debug)]
+pub struct WebResponse {
+  pub warnings: Warnings,
+  pub data: Result<WebResponseData, AE>,
+}
+
+pub type WebResponseData = FrameQueueBuf;
+pub type WebResponseBody = BufBody<FrameQueueBuf>;
+
+pub async fn handle(
+  conn: Arc<String>,
+  global: Arc<Global>,
+  req: hyper::Request<hyper::Body>
+) -> Result<hyper::Response<WebResponseBody>, hyper::http::Error> {
+  if req.method() == Method::GET {
+    let mut resp = hyper::Response::new(BufBody::display("hippotat\r\n"));
+    resp.headers_mut().insert(
+      "Content-Type",
+      "text/plain; charset=US-ASCII".try_into().unwrap()
+    );
+    return Ok(resp)
+  }
+
+  let mut warnings: Warnings = default();
+
+  async {
+
+    let get_header = |hn: &str| {
+      let mut values = req.headers().get_all(hn).iter();
+      let v = values.next().ok_or_else(|| anyhow!("missing {}", hn))?;
+      if values.next().is_some() { throw!(anyhow!("multiple {}!", hn)); }
+      let v = v.to_str().context(anyhow!("interpret {} as UTF-8", hn))?;
+      Ok::<_,AE>(v)
+    };
+
+    let mkboundary = |b: &'_ _| format!("\n--{}", b).into_bytes();
+    let boundary = match (||{
+      let t = get_header("Content-Type")?;
+      let t: mime::Mime = t.parse().context("parse Content-Type")?;
+      if t.type_() != "multipart" { throw!(anyhow!("not multipart/")) }
+      let b = mime::BOUNDARY;
+      let b = t.get_param(b).ok_or_else(|| anyhow!("missing boundary=..."))?;
+      if t.subtype() != "form-data" {
+        warnings.add(&"Content-Type not /form-data")?;
+      }
+      let b = mkboundary(b.as_str());
+      Ok::<_,AE>(b)
+    })() {
+      Ok(y) => y,
+      Err(e) => {
+        warnings.add(&e.wrap_err("guessing boundary"))?;
+        mkboundary("b")
+      },
+    };
+
+    let length_hint: usize = (||{
+      let clength = get_header("Content-Length")?;
+      let clength = clength.parse().context("parse Content-Length")?;
+      Ok::<_,AE>(clength)
+    })().unwrap_or_else(
+      |e| { let _ = warnings.add(&e.wrap_err("parsing Content-Length")); 0 }
+    );
+
+    let mut body = req.into_body();
+    let initial = match read_limited_bytes(
+      METADATA_MAX_LEN, default(), length_hint, &mut body
+    ).await {
+      Ok(all) => all,
+      Err(ReadLimitedError::Truncated { sofar,.. }) => sofar,
+      Err(ReadLimitedError::Hyper(e)) => throw!(e),
+    };
+
+    let boundary_finder = memmem::Finder::new(&boundary);
+    let mut boundary_iter = boundary_finder.find_iter(&initial);
+
+    let start = if initial.starts_with(&boundary[1..]) { boundary.len()-1 }
+    else if let Some(start) = boundary_iter.next() { start + boundary.len() }
+    else { throw!(anyhow!("initial boundary not found")) };
+
+    let comp = multipart::process_boundary
+      (&mut warnings, &initial[start..], PartName::m)?
+      .ok_or_else(|| anyhow!(r#"no "m" component"#))?;
+
+    if comp.name != PartName::m { throw!(anyhow!(
+      r#"first multipart component must be name="m""#
+    )) }
+
+    let mut meta = MetadataFieldIterator::new(comp.payload);
+
+    let client: ClientName = meta.need_parse().context("client addr")?;
+
+    let mut hmac_got = [0; HMAC_L];
+    let (client_time, hmac_got_l) = (||{
+      let token: &str = meta.need_next().context(r#"find in "m""#)?;
+      let (time_t, hmac_b64) = token.split_once(' ')
+        .ok_or_else(|| anyhow!("split"))?;
+      let time_t = u64::from_str_radix(time_t, 16).context("parse time_t")?;
+      let l = io::copy(
+        &mut base64::read::DecoderReader::new(&mut hmac_b64.as_bytes(),
+                                              BASE64_CONFIG),
+        &mut &mut hmac_got[..]
+      ).context("parse b64 token")?;
+      let l = l.try_into()?;
+      Ok::<_,AE>((time_t, l))
+    })().context("token")?;
+    let hmac_got = &hmac_got[0..hmac_got_l];
+
+    let client_name = client;
+    let client = global.all_clients.get(&client_name);
+
+    // We attempt to hide whether the client exists we don't try to
+    // hide the hash lookup computationgs, but we do try to hide the
+    // HMAC computation by always doing it.  We hope that the compiler
+    // doesn't produce a specialised implementation for the dummy
+    // secret value.
+    let client_exists = subtle::Choice::from(client.is_some() as u8);
+    let secret = client.map(|c| c.ic.secret.0.as_bytes());
+    let secret = secret.unwrap_or(&[0x55; HMAC_B][..]);
+    let client_time_s = format!("{:x}", client_time);
+    let hmac_exp = token_hmac(secret, client_time_s.as_bytes());
+    // We also definitely want a consttime memeq for the hmac value
+    let hmac_ok = hmac_got.ct_eq(&hmac_exp);
+    //dbg!(DumpHex(&hmac_exp), client.is_some());
+    //dbg!(DumpHex(hmac_got), hmac_ok, client_exists);
+    if ! bool::from(hmac_ok & client_exists) {
+      debug!("{} rejected client {}", &conn, &client_name);
+      let body = BufBody::display("Not authorised\r\n");
+      return Ok(
+        hyper::Response::builder()
+          .status(hyper::StatusCode::FORBIDDEN)
+          .header("Content-Type", r#"text/plain; charset="utf-8""#)
+          .body(body)
+      )
+    }
+
+    let client = client.unwrap();
+    let now = time_t_now();
+    let chk_skew = |a: u64, b: u64, c_ahead_behind| {
+      if let Some(a_ahead) = a.checked_sub(b) {
+        if a_ahead > client.ic.max_clock_skew.as_secs() {
+          throw!(anyhow!("too much clock skew (client {} by {})",
+                         c_ahead_behind, a_ahead));
+        }
+      }
+      Ok::<_,AE>(())
+    };
+    chk_skew(client_time, now, "ahead")?;
+    chk_skew(now, client_time, "behind")?;
+
+    let initial_remaining = meta.remaining_bytes_len();
+
+    //eprintln!("boundary={:?} start={} name={:?} client={}",
+    // boundary, start, &comp.name, &client.ic);
+
+    let (reply_to, reply_recv) = oneshot::channel();
+    trace!("{} {} request, Content-Length={}",
+           &conn, &client_name, length_hint);
+    let wreq = WebRequest {
+      initial,
+      initial_remaining,
+      length_hint,
+      boundary_finder: boundary_finder.into_owned(),
+      body,
+      warnings: mem::take(&mut warnings),
+      reply_to,
+      conn: conn.clone(),
+      may_route: MayRoute::came_from_outside_hippotatd(),
+    };
+
+    client.web.try_send(wreq)
+      .map_err(|_| anyhow!("client user task overloaded"))?;
+
+    let reply: WebResponse = reply_recv.await?;
+    warnings = reply.warnings;
+    let data = reply.data?;
+
+    if warnings.warnings.is_empty() {
+      trace!("{} {} responding, {}",
+             &conn, &client_name, data.len());
+    } else {
+      debug!("{} {} responding, {} warnings={:?}",
+             &conn, &client_name, data.len(),
+             &warnings.warnings);
+    }
+
+    let data = BufBody::new(data);
+    Ok::<_,AE>(
+      hyper::Response::builder()
+        .header("Content-Type", r#"application/octet-stream"#)
+        .body(data)
+    )
+  }.await.unwrap_or_else(|e| {
+    debug!("{} error {}", &conn, &e);
+    let mut errmsg = format!("ERROR\n\n{:?}\n\n", &e);
+    for w in warnings.warnings {
+      write!(errmsg, "warning: {}\n", w).unwrap();
+    }
+    hyper::Response::builder()
+      .status(hyper::StatusCode::BAD_REQUEST)
+      .header("Content-Type", r#"text/plain; charset="utf-8""#)
+      .body(BufBody::display(errmsg))
+  })
+}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644 (file)
index 0000000..8fd1c79
--- /dev/null
@@ -0,0 +1,930 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[derive(hippotat_macros::ResolveConfig)]
+#[derive(Debug,Clone)]
+pub struct InstanceConfig {
+  // Exceptional settings
+  #[special(special_link, SKL::None)]      pub    link:   LinkName,
+  #[per_client]                            pub    secret: Secret,
+  #[global] #[special(special_ipif, SKL::PerClient)] pub ipif: String,
+
+  // Capped settings:
+  #[limited]    pub max_batch_down:               u32,
+  #[limited]    pub max_queue_time:               Duration,
+  #[limited]    pub http_timeout:                 Duration,
+  #[limited]    pub target_requests_outstanding:  u32,
+  #[special(special_max_up, SKL::Limited)]  pub max_batch_up: u32,
+
+  // Ordinary settings, used by both, not client-specifi:
+  #[global]  pub addrs:                        Vec<IpAddr>,
+  #[global]  pub vnetwork:                     Vec<IpNet>,
+  #[global]  pub vaddr:                        IpAddr,
+  #[global]  pub vrelay:                       IpAddr,
+  #[global]  pub port:                         u16,
+  #[global]  pub mtu:                          u32,
+
+  // Ordinary settings, used by server only:
+  #[server] #[per_client] pub max_clock_skew:               Duration,
+  #[server] #[global]     pub ifname_server:                String,
+
+  // Ordinary settings, used by client only:
+  #[client]  pub http_timeout_grace:           Duration,
+  #[client]  pub max_requests_outstanding:     u32,
+  #[client]  pub http_retry:                   Duration,
+  #[client]  pub success_report_interval:      Duration,
+  #[client]  pub url:                          Uri,
+  #[client]  pub vroutes:                      Vec<IpNet>,
+  #[client]  pub ifname_client:                String,
+
+  // Computed, rather than looked up.  Client only:
+  #[computed]  pub effective_http_timeout:     Duration,
+}
+
+static DEFAULT_CONFIG: &str = r#"
+[COMMON]
+max_batch_down = 65536
+max_queue_time = 10
+target_requests_outstanding = 3
+http_timeout = 30
+http_timeout_grace = 5
+max_requests_outstanding = 6
+max_batch_up = 4000
+http_retry = 5
+port = 80
+vroutes = ''
+ifname_client = hippo%d
+ifname_server = shippo%d
+max_clock_skew = 300
+success_report_interval = 3600
+
+ipif = userv root ipif %{local},%{peer},%{mtu},slip,%{ifname} '%{rnets}'
+
+mtu = 1500
+
+vnetwork = 172.24.230.192
+
+[LIMIT]
+max_batch_up = 262144
+max_batch_down = 262144
+max_queue_time = 121
+http_timeout = 121
+target_requests_outstanding = 10
+"#;
+
+#[derive(StructOpt,Debug)]
+pub struct Opts {
+  /// Top-level config file or directory
+  ///
+  /// Look for `main.cfg`, `config.d` and `secrets.d` here.
+  ///
+  /// Or if this is a file, just read that file.
+  #[structopt(long, default_value="/etc/hippotat")]
+  pub config: PathBuf,
+  
+  /// Additional config files or dirs, which can override the others
+  #[structopt(long, multiple=true, number_of_values=1)]
+  pub extra_config: Vec<PathBuf>,
+}
+
+#[ext(pub)]
+impl u32 {
+  fn sat(self) -> usize { self.try_into().unwrap_or(usize::MAX) }
+}
+
+#[ext]
+impl<'s> Option<&'s str> {
+  #[throws(AE)]
+  fn value(self) -> &'s str {
+    self.ok_or_else(|| anyhow!("value needed"))?
+  }
+}
+
+#[derive(Clone)]
+pub struct Secret(pub String);
+impl Parseable for Secret {
+  #[throws(AE)]
+  fn parse(s: Option<&str>) -> Self {
+    let s = s.value()?;
+    if s.is_empty() { throw!(anyhow!("secret value cannot be empty")) }
+    Secret(s.into())
+  }
+  #[throws(AE)]
+  fn default() -> Self { Secret(default()) }
+}
+impl Debug for Secret {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) { write!(f, "Secret(***)")? }
+}
+
+#[derive(Debug,Clone,Hash,Eq,PartialEq)]
+pub enum SectionName {
+  Link(LinkName),
+  Client(ClientName),
+  Server(ServerName), // includes SERVER, which is slightly special
+  ServerLimit(ServerName),
+  GlobalLimit,
+  Common,
+}
+pub use SectionName as SN;
+
+#[derive(Debug)]
+struct RawValRef<'v,'l,'s> {
+  raw: Option<&'v str>, // todo: not Option any more
+  key: &'static str,
+  loc: &'l ini::Loc,
+  section: &'s SectionName,
+}
+
+impl<'v> RawValRef<'v,'_,'_> {
+  #[throws(AE)]
+  fn try_map<F,T>(&self, f: F) -> T
+  where F: FnOnce(Option<&'v str>) -> Result<T, AE> {
+    f(self.raw)
+      .with_context(|| format!(r#"file {:?}, section {}, key "{}""#,
+                               self.loc, self.section, self.key))?
+  }
+}
+
+pub struct Config {
+  pub opts: Opts,
+}
+
+static OUTSIDE_SECTION: &str = "[";
+static SPECIAL_SERVER_SECTION: &str = "SERVER";
+
+#[derive(Debug)]
+struct Aggregate {
+  end: LinkEnd,
+  keys_allowed: HashMap<&'static str, SectionKindList>,
+  sections: HashMap<SectionName, ini::Section>,
+}
+
+type OkAnyway<'f,A> = &'f dyn Fn(&io::Error) -> Option<A>;
+#[ext]
+impl<'f,A> OkAnyway<'f,A> {
+  fn ok<T>(self, r: &Result<T, io::Error>) -> Option<A> {
+    let e = r.as_ref().err()?;
+    let a = self(e)?;
+    Some(a)
+  }
+}
+
+impl FromStr for SectionName {
+  type Err = AE;
+  #[throws(AE)]
+  fn from_str(s: &str) -> Self {
+    match s {
+      "COMMON" => return SN::Common,
+      "LIMIT" => return SN::GlobalLimit,
+      _ => { }
+    };
+    if let Ok(n@ ServerName(_)) = s.parse() { return SN::Server(n) }
+    if let Ok(n@ ClientName(_)) = s.parse() { return SN::Client(n) }
+    let (server, client) = s.split_ascii_whitespace().collect_tuple()
+      .ok_or_else(|| anyhow!(
+        "bad section name {:?} \
+         (must be COMMON, DEFAULT, <server>, <client>, or <server> <client>",
+        s
+      ))?;
+    let server = server.parse().context("server name in link section name")?;
+    if client == "LIMIT" { return SN::ServerLimit(server) }
+    let client = client.parse().context("client name in link section name")?;
+    SN::Link(LinkName { server, client })
+  }
+}
+impl Display for InstanceConfig {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) { Display::fmt(&self.link, f)? }
+}
+
+impl Display for SectionName {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) {
+    match self {
+      SN::Link  (ref l)      => Display::fmt(l, f)?,
+      SN::Client(ref c)      => write!(f, "[{}]"       , c)?,
+      SN::Server(ref s)      => write!(f, "[{}]"       , s)?,
+      SN::ServerLimit(ref s) => write!(f, "[{} LIMIT] ", s)?,
+      SN::GlobalLimit        => write!(f, "[LIMIT]"       )?,
+      SN::Common             => write!(f, "[COMMON]"      )?,
+    }
+  }
+}
+
+impl Aggregate {
+  fn new(
+    end: LinkEnd,
+    keys_allowed: HashMap<&'static str, SectionKindList>
+  ) -> Self { Aggregate {
+    end, keys_allowed,
+    sections: default(),
+  } }
+
+  #[throws(AE)] // AE does not include path
+  fn read_file<A>(&mut self, path: &Path, anyway: OkAnyway<A>) -> Option<A>
+  {
+    let f = fs::File::open(path);
+    if let Some(anyway) = anyway.ok(&f) { return Some(anyway) }
+    let mut f = f.context("open")?;
+
+    let mut s = String::new();
+    let y = f.read_to_string(&mut s);
+    if let Some(anyway) = anyway.ok(&y) { return Some(anyway) }
+    y.context("read")?;
+
+    self.read_string(s, path)?;
+    None
+  }
+
+  #[throws(AE)] // AE does not include path
+  fn read_string(&mut self, s: String, path_for_loc: &Path) {
+    let mut map: ini::Parsed = default();
+    ini::read(&mut map, &mut s.as_bytes(), path_for_loc)
+      .context("parse as INI")?;
+    if map.get(OUTSIDE_SECTION).is_some() {
+      throw!(anyhow!("INI file contains settings outside a section"));
+    }
+
+    for (sn, section) in map {
+      let sn = sn.parse().dcontext(&sn)?;
+      let vars = &section.values;
+
+      for (key, val) in vars {
+        (||{
+          let skl = if key == "server" {
+            SKL::ServerName
+          } else {
+            *self.keys_allowed.get(key.as_str()).ok_or_else(
+              || anyhow!("unknown configuration key")
+            )?
+          };
+          if ! skl.contains(&sn, self.end) {
+            throw!(anyhow!("key not applicable in this kind of section"))
+          }
+          Ok::<_,AE>(())
+        })()
+          .with_context(|| format!("key {:?}", key))
+          .with_context(|| val.loc.to_string())?
+      }
+
+      let ent = self.sections.entry(sn)
+        .or_insert_with(|| ini::Section {
+          loc: section.loc.clone(),
+          values: default(),
+        });
+
+      for (key, ini::Val { val: raw, loc }) in vars {
+        let val = if raw.starts_with('\'') || raw.starts_with('"') {
+          (||{
+            if raw.contains('\\') {
+              throw!(
+                anyhow!("quoted value contains backslash, not supported")
+              );
+            }
+            let quote = &raw[0..1];
+
+            let unq = raw[1..].strip_suffix(quote)
+              .ok_or_else(
+                || anyhow!("mismatched quotes around quoted value")
+              )?
+              .to_owned();
+            if unq.contains(quote) {
+              throw!(anyhow!(
+                "quoted value contains quote (escaping not supported)"
+              ))
+            }
+
+            Ok::<_,AE>(unq)
+          })()
+            .with_context(|| format!("key {:?}", key))
+            .with_context(|| loc.to_string())?
+        } else {
+          raw.clone()
+        };
+        let key = key.replace('-',"_");
+        ent.values.insert(key, ini::Val { val, loc: loc.clone() });
+      }
+    }
+  }
+
+  #[throws(AE)] // AE includes path
+  fn read_dir_d<A>(&mut self, path: &Path, anyway: OkAnyway<A>) -> Option<A>
+  {
+    let dir = fs::read_dir(path);
+    if let Some(anyway) = anyway.ok(&dir) { return Some(anyway) }
+    let dir = dir.context("open directory").dcontext(path)?;
+    for ent in dir {
+      let ent = ent.context("read directory").dcontext(path)?;
+      let leaf = ent.file_name();
+      let leaf = leaf.to_str();
+      let leaf = if let Some(leaf) = leaf { leaf } else { continue }; //utf8?
+      if leaf.len() == 0 { continue }
+      if ! leaf.chars().all(
+        |c| c=='-' || c=='_' || c.is_ascii_alphanumeric()
+      ) { continue }
+
+      // OK we want this one
+      let ent = ent.path();
+      self.read_file(&ent, &|_| None::<Void>).dcontext(&ent)?;
+    }
+    None
+  }
+
+  #[throws(AE)] // AE includes everything
+  fn read_toplevel(&mut self, toplevel: &Path) {
+    enum Anyway { None, Dir }
+    match self.read_file(toplevel, &|e| match e {
+      e if e.kind() == EK::NotFound => Some(Anyway::None),
+      e if e.is_is_a_directory() => Some(Anyway::Dir),
+      _ => None,
+    })
+      .dcontext(toplevel).context("top-level config directory (or file)")?
+    {
+      None | Some(Anyway::None) => { },
+
+      Some(Anyway::Dir) => {
+        struct AnywayNone;
+        let anyway_none = |e: &io::Error| match e {
+          e if e.kind() == EK::NotFound => Some(AnywayNone),
+          _ => None,
+        };
+
+        let mk = |leaf: &str| {
+          [ toplevel, &PathBuf::from(leaf) ]
+            .iter().collect::<PathBuf>()
+        };
+
+        for &(try_main, desc) in &[
+          ("main.cfg", "main config file"),
+          ("master.cfg", "obsolete-named main config file"),
+        ] {
+          let main = mk(try_main);
+
+          match self.read_file(&main, &anyway_none)
+            .dcontext(main).context(desc)?
+          {
+            None => break,
+            Some(AnywayNone) => { },
+          }
+        }
+
+        for &(try_dir, desc) in &[
+          ("config.d", "per-link config directory"),
+          ("secrets.d", "per-link secrets directory"),
+        ] {
+          let dir = mk(try_dir);
+          match self.read_dir_d(&dir, &anyway_none).context(desc)? {
+            None => { },
+            Some(AnywayNone) => { },
+          }
+        }
+      }
+    }
+  }
+
+  #[throws(AE)] // AE includes extra, but does that this is extra
+  fn read_extra(&mut self, extra: &Path) {
+    struct AnywayDir;
+
+    match self.read_file(extra, &|e| match e {
+      e if e.is_is_a_directory() => Some(AnywayDir),
+      _ => None,
+    })
+      .dcontext(extra)?
+    {
+      None => return,
+      Some(AnywayDir) => {
+        self.read_dir_d(extra, &|_| None::<Void>)?;
+      }
+    }
+
+  }
+}
+
+impl Aggregate {
+  fn instances(&self, only_server: Option<&ServerName>) -> BTreeSet<LinkName> {
+    let mut links:              BTreeSet<LinkName> = default();
+
+    let mut secrets_anyserver:  BTreeSet<&ClientName> = default();
+    let mut secrets_anyclient:  BTreeSet<&ServerName> = default();
+    let mut secret_global       = false;
+
+    let mut putative_servers   = BTreeSet::new();
+    let mut putative_clients   = BTreeSet::new();
+
+    let mut note_server = |s| {
+      if let Some(only) = only_server { if s != only { return false } }
+      putative_servers.insert(s);
+      true
+    };
+    let mut note_client = |c| {
+      putative_clients.insert(c);
+    };
+
+    for (section, vars) in &self.sections {
+      let has_secret = || vars.values.contains_key("secret");
+      //dbg!(&section, has_secret());
+
+      match section {
+        SN::Link(l) => {
+          if ! note_server(&l.server) { continue }
+          note_client(&l.client);
+          if has_secret() { links.insert(l.clone()); }
+        },
+        SN::Server(ref s) => {
+          if ! note_server(s) { continue }
+          if has_secret() { secrets_anyclient.insert(s); }
+        },
+        SN::Client(ref c) => {
+          note_client(c);
+          if has_secret() { secrets_anyserver.insert(c); }
+        },
+        SN::Common => {
+          if has_secret() { secret_global = true; }
+        },
+        _ => { },
+      }
+    }
+
+    //dbg!(&putative_servers, &putative_clients);
+    //dbg!(&secrets_anyserver, &secrets_anyclient, &secret_global);
+
+    // Add links which are justified by blanket secrets
+    for (client, server) in iproduct!(
+      putative_clients.into_iter().filter(
+        |c| secret_global
+         || secrets_anyserver.contains(c)
+         || ! secrets_anyclient.is_empty()
+      ),
+      putative_servers.iter().cloned().filter(
+        |s| secret_global
+         || secrets_anyclient.contains(s)
+         || ! secrets_anyserver.is_empty()
+      )
+    ) {
+      links.insert(LinkName {
+        client: client.clone(),
+        server: server.clone(),
+      });
+    }
+
+    links
+  }
+}
+
+struct ResolveContext<'c> {
+  agg: &'c Aggregate,
+  link: &'c LinkName,
+  end: LinkEnd,
+  all_sections: Vec<SectionName>,
+}
+
+trait Parseable: Sized {
+  fn parse(s: Option<&str>) -> Result<Self, AE>;
+  fn default() -> Result<Self, AE> {
+    Err(anyhow!("setting must be specified"))
+  }
+  #[throws(AE)]
+  fn default_for_key(key: &str) -> Self {
+    Self::default().with_context(|| key.to_string())?
+  }
+}
+
+impl Parseable for Duration {
+  #[throws(AE)]
+  fn parse(s: Option<&str>) -> Duration {
+    // todo: would be nice to parse with humantime maybe
+    Duration::from_secs( s.value()?.parse()? )
+  }
+}
+macro_rules! parseable_from_str { ($t:ty $(, $def:expr)? ) => {
+  impl Parseable for $t {
+    #[throws(AE)]
+    fn parse(s: Option<&str>) -> $t { s.value()?.parse()? }
+    $( #[throws(AE)] fn default() -> Self { $def } )?
+  }
+} }
+parseable_from_str!{u16, default() }
+parseable_from_str!{u32, default() }
+parseable_from_str!{String, default() }
+parseable_from_str!{IpNet, default() }
+parseable_from_str!{IpAddr, Ipv4Addr::UNSPECIFIED.into() }
+parseable_from_str!{Uri, default() }
+
+impl<T:Parseable> Parseable for Vec<T> {
+  #[throws(AE)]
+  fn parse(s: Option<&str>) -> Vec<T> {
+    s.value()?
+      .split_ascii_whitespace()
+      .map(|s| Parseable::parse(Some(s)))
+      .collect::<Result<Vec<_>,_>>()?
+  }
+  #[throws(AE)]
+  fn default() -> Self { default() }
+}
+
+
+#[derive(Debug,Copy,Clone,Eq,PartialEq)]
+enum SectionKindList {
+  PerClient,
+  Limited,
+  Limits,
+  Global,
+  ServerName,
+  None,
+}
+use SectionKindList as SKL;
+
+impl SectionName {
+  fn special_server_section() -> Self { SN::Server(ServerName(
+    SPECIAL_SERVER_SECTION.into()
+  )) }
+}
+
+impl SectionKindList {
+  fn contains(self, s: &SectionName, end: LinkEnd) -> bool {
+    match (self, end) {
+      (SKL::PerClient,_) |
+      (SKL::Global, LinkEnd::Client) => matches!(s, SN::Link(_)
+                                                  | SN::Client(_)
+                                                  | SN::Server(_)
+                                                  | SN::Common),
+
+      (SKL::Limits,_)     => matches!(s, SN::ServerLimit(_)
+                                       | SN::GlobalLimit),
+
+      (SKL::Global, LinkEnd::Server) => matches!(s, SN::Common
+                                                  | SN::Server(_)),
+
+      (SKL::Limited,_)    => SKL::PerClient.contains(s, end)
+                           | SKL::Limits   .contains(s, end),
+
+      (SKL::ServerName,_) => matches!(s, SN::Common)
+                           | matches!(s, SN::Server(ServerName(name))
+                                         if name == SPECIAL_SERVER_SECTION),
+      (SKL::None,_)       => false,
+    }
+  }
+}
+
+impl Aggregate {
+  fn lookup_raw<'a,'s,S>(&'a self, key: &'static str, sections: S)
+                       -> Option<RawValRef<'a,'a,'s>>
+  where S: Iterator<Item=&'s SectionName>
+  {
+    for section in sections {
+      if let Some(val) = self.sections
+        .get(section)
+        .and_then(|s: &ini::Section| s.values.get(key))
+      {
+        return Some(RawValRef {
+          raw: Some(&val.val),
+          loc: &val.loc,
+          section, key,
+        })
+      }
+    }
+    None
+  }
+
+  #[throws(AE)]
+  pub fn establish_server_name(&self) -> ServerName {
+    let key = "server";
+    let raw = match self.lookup_raw(
+      key,
+      [ &SectionName::Common, &SN::special_server_section() ].iter().cloned()
+    ) {
+      Some(raw) => raw.try_map(|os| os.value())?,
+      None => SPECIAL_SERVER_SECTION,
+    };
+    ServerName(raw.into())
+  }
+}
+
+impl<'c> ResolveContext<'c> {
+  fn first_of_raw(&'c self, key: &'static str, sections: SectionKindList)
+                  -> Option<RawValRef<'c,'c,'c>> {
+    self.agg.lookup_raw(
+      key,
+      self.all_sections.iter()
+        .filter(|s| sections.contains(s, self.end))
+    )
+  }
+
+  #[throws(AE)]
+  fn first_of<T>(&self, key: &'static str, sections: SectionKindList)
+                 -> Option<T>
+  where T: Parseable
+  {
+    match self.first_of_raw(key, sections) {
+      None => None,
+      Some(raw) => Some(raw.try_map(Parseable::parse)?),
+    }
+  }
+
+  #[throws(AE)]
+  pub fn ordinary<T>(&self, key: &'static str, skl: SKL) -> T
+  where T: Parseable
+  {
+    match self.first_of(key, skl)? {
+      Some(y) => y,
+      None => Parseable::default_for_key(key)?,
+    }
+  }
+
+  #[throws(AE)]
+  pub fn limited<T>(&self, key: &'static str, skl: SKL) -> T
+  where T: Parseable + Ord
+  {
+    assert_eq!(skl, SKL::Limited);
+    let val = self.ordinary(key, SKL::PerClient)?;
+    if let Some(limit) = self.first_of(key, SKL::Limits)? {
+      min(val, limit)
+    } else {
+      val
+    }
+  }
+
+  #[throws(AE)]
+  pub fn client<T>(&self, key: &'static str, skl: SKL) -> T
+  where T: Parseable + Default {
+    match self.end {
+      LinkEnd::Client => self.ordinary(key, skl)?,
+      LinkEnd::Server => default(),
+    }
+  }
+  #[throws(AE)]
+  pub fn server<T>(&self, key: &'static str, skl: SKL) -> T
+  where T: Parseable + Default {
+    match self.end {
+      LinkEnd::Server => self.ordinary(key, skl)?,
+      LinkEnd::Client => default(),
+    }
+  }
+
+  #[throws(AE)]
+  pub fn computed<T>(&self, _key: &'static str, skl: SKL) -> T
+  where T: Default
+  {
+    assert_eq!(skl, SKL::None);
+    default()
+  }
+
+  #[throws(AE)]
+  pub fn special_ipif(&self, key: &'static str, skl: SKL) -> String {
+    assert_eq!(skl, SKL::PerClient); // we tolerate it in per-client sections
+    match self.end {
+      LinkEnd::Client => self.ordinary(key, SKL::PerClient)?,
+      LinkEnd::Server => self.ordinary(key, SKL::Global)?,
+    }
+  }
+
+  #[throws(AE)]
+  pub fn special_link(&self, _key: &'static str, skl: SKL) -> LinkName {
+    assert_eq!(skl, SKL::None);
+    self.link.clone()
+  }
+
+  #[throws(AE)]
+  pub fn special_max_up(&self, key: &'static str, skl: SKL) -> u32 {
+    assert_eq!(skl, SKL::Limited);
+    match self.end {
+      LinkEnd::Client => self.ordinary(key, SKL::Limited)?,
+      LinkEnd::Server => self.ordinary(key, SKL::Limits)?,
+    }
+  }
+}
+
+impl InstanceConfig {
+  #[throws(AE)]
+  fn complete(&mut self, end: LinkEnd) {
+    let mut vhosts = self.vnetwork.iter()
+      .map(|n| n.hosts()).flatten()
+      .filter({ let vaddr = self.vaddr; move |v| v != &vaddr });
+
+    if self.vaddr.is_unspecified() {
+      self.vaddr = vhosts.next().ok_or_else(
+        || anyhow!("vnetwork too small to generate vaddrr")
+      )?;
+    }
+    if self.vrelay.is_unspecified() {
+      self.vrelay = vhosts.next().ok_or_else(
+        || anyhow!("vnetwork too small to generate vrelay")
+      )?;
+    }
+
+    let check_batch = {
+      let mtu = self.mtu;
+      move |max_batch, key| {
+        if max_batch/2 < mtu {
+          throw!(anyhow!("max batch {:?} ({}) must be >= 2 x mtu ({}) \
+                          (to allow for SLIP ESC-encoding)",
+                         key, max_batch, mtu))
+        }
+        Ok::<_,AE>(())
+      }
+    };
+
+    match end {
+      LinkEnd::Client => {
+        if &self.url == &default::<Uri>() {
+          let addr = self.addrs.get(0).ok_or_else(
+            || anyhow!("client needs addrs or url set")
+          )?;
+          self.url = format!(
+            "http://{}{}/",
+            match addr {
+              IpAddr::V4(a) => format!("{}", a),
+              IpAddr::V6(a) => format!("[{}]", a),
+            },
+            match self.port {
+              80 => format!(""),
+              p => format!(":{}", p),
+            })
+            .parse().unwrap()
+        }
+
+        self.effective_http_timeout = {
+          let a = self.http_timeout;
+          let b = self.http_timeout_grace;
+          a.checked_add(b).ok_or_else(
+            || anyhow!("calculate effective http timeout ({:?} + {:?})", a, b)
+          )?
+        };
+
+        {
+          let t = self.target_requests_outstanding;
+          let m = self.max_requests_outstanding;
+          if t > m { throw!(anyhow!(
+            "target_requests_outstanding ({}) > max_requests_outstanding ({})",
+            t, m
+          )) }
+        }
+
+        check_batch(self.max_batch_up, "max_batch_up")?;
+      },
+
+      LinkEnd::Server => {
+        if self.addrs.is_empty() {
+          throw!(anyhow!("missing 'addrs' setting"))
+        }
+        check_batch(self.max_batch_down, "max_batch_down")?;
+      },
+    }
+
+    #[throws(AE)]
+    fn subst(var: &mut String,
+             kv: &mut dyn Iterator<Item=(&'static str, &dyn Display)>
+    ) {
+      let substs = kv
+        .map(|(k,v)| (k.to_string(), v.to_string()))
+        .collect::<HashMap<String, String>>();
+      let bad = parking_lot::Mutex::new(vec![]);
+      *var = regex_replace_all!(
+        r#"%(?:%|\((\w+)\)s|\{(\w+)\}|.)"#,
+        &var,
+        |whole, k1, k2| (|| Ok::<_,String>({
+          if whole == "%%" { "%" }
+          else if let Some(&k) = [k1,k2].iter().find(|&&s| s != "") {
+            substs.get(k).ok_or_else(
+              || format!("unknown key %({})s", k)
+            )?
+          } else {
+            throw!(format!("bad percent escape {:?}", &whole));
+          }
+        }))().unwrap_or_else(|e| { bad.lock().push(e); "" })
+      ).into_owned();
+      let bad = bad.into_inner();
+      if ! bad.is_empty() {
+        throw!(anyhow!("substitution failed: {}", bad.iter().format("; ")));
+      }
+    }
+
+    {
+      use LinkEnd::*;
+      type DD<'d> = &'d dyn Display;
+      fn dv<T:Display>(v: &[T]) -> String {
+        format!("{}", v.iter().format(" "))
+      }
+      let mut ipif = mem::take(&mut self.ipif); // lets us borrow all of self
+      let s = &self; // just for abbreviation, below
+      let vnetwork = dv(&s.vnetwork);
+      let vroutes  = dv(&s.vroutes);
+
+      let keys = &["local",       "peer",    "rnets",   "ifname"];
+      let values = match end {
+ Server => [&s.vaddr as DD      , &s.vrelay, &vnetwork, &s.ifname_server],
+ Client => [&s.link.client as DD, &s.vaddr,  &vroutes,  &s.ifname_client],
+      };
+      let always = [
+        ( "mtu",     &s.mtu as DD ),
+      ];
+
+      subst(
+        &mut ipif,
+        &mut keys.iter().cloned()
+          .zip_eq(values)
+          .chain(always.iter().cloned()),
+      ).context("ipif")?;
+      self.ipif = ipif;
+    }
+  }
+}
+
+trait ResolveGlobal<'i> where Self: 'i {
+  fn resolve<I>(it: I) -> Self
+  where I: Iterator<Item=&'i Self>;
+}
+impl<'i,T> ResolveGlobal<'i> for T where T: Eq + Clone + Debug + 'i {
+  fn resolve<I>(mut it: I) -> Self
+  where I: Iterator<Item=&'i Self>
+  {
+    let first = it.next().expect("empty instances no global!");
+    for x in it { assert_eq!(x, first); }
+    first.clone()
+  }
+}
+
+#[throws(AE)]
+pub fn read(opts: &Opts, end: LinkEnd) -> Vec<InstanceConfig> {
+  let agg = (||{
+    let mut agg = Aggregate::new(
+      end,
+      InstanceConfig::FIELDS.iter().cloned().collect(),
+    );
+
+    agg.read_string(DEFAULT_CONFIG.into(),
+                    "<build-in defaults>".as_ref())
+      .expect("builtin configuration is broken");
+
+    agg.read_toplevel(&opts.config)?;
+    for extra in &opts.extra_config {
+      agg.read_extra(extra).context("extra config")?;
+    }
+
+    //eprintln!("GOT {:#?}", agg);
+
+    Ok::<_,AE>(agg)
+  })().context("read configuration")?;
+
+  let server_name = match end {
+    LinkEnd::Server => Some(agg.establish_server_name()?),
+    LinkEnd::Client => None,
+  };
+
+  let instances = agg.instances(server_name.as_ref());
+  let mut ics = vec![];
+  //dbg!(&instances);
+
+  for link in instances {
+    let rctx = ResolveContext {
+      agg: &agg,
+      link: &link,
+      end,
+      all_sections: vec![
+        SN::Link(link.clone()),
+        SN::Client(link.client.clone()),
+        SN::Server(link.server.clone()),
+        SN::Common,
+        SN::ServerLimit(link.server.clone()),
+        SN::GlobalLimit,
+      ],
+    };
+
+    if rctx.first_of_raw("secret", SKL::PerClient).is_none() { continue }
+
+    let mut ic = InstanceConfig::resolve_instance(&rctx)
+      .with_context(|| format!("resolve config for {}", &link))?;
+
+    ic.complete(end)
+      .with_context(|| format!("complete config for {}", &link))?;
+
+    ics.push(ic);
+  }
+
+  ics
+}
+
+pub fn startup<F,T>(progname: &str, end: LinkEnd,
+                    opts: &Opts, logopts: &LogOpts,
+                    f: F) -> T
+  where F: FnOnce(Vec<InstanceConfig>) -> Result<T,AE>
+{
+  (||{
+    dedup_eyre_setup()?;
+    let ics = config::read(opts, end)?;
+    if ics.is_empty() { throw!(anyhow!("no associations, quitting")); }
+
+    logopts.log_init()?;
+    let t = f(ics)?;
+
+    Ok::<_,AE>(t)
+  })().unwrap_or_else(|e| {
+    eprintln!("{}: startup error: {}", progname, &e);
+    process::exit(8);
+  })
+}
diff --git a/src/ini.rs b/src/ini.rs
new file mode 100644 (file)
index 0000000..143d0fc
--- /dev/null
@@ -0,0 +1,117 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+use std::io::BufRead;
+use std::rc::Rc;
+
+#[derive(Debug,Clone)]
+#[derive(Hash,Eq,PartialEq,Ord,PartialOrd)]
+pub struct Loc {
+  pub file: Arc<PathBuf>,
+  pub lno: usize,
+  pub section: Option<Arc<str>>,
+}
+
+#[derive(Debug,Clone)]
+pub struct Val {
+  pub val: String,
+  pub loc: Loc,
+}
+
+pub type Parsed = HashMap<Arc<str>, Section>;
+
+#[derive(Debug)]
+pub struct Section {
+  /// Location of first encounter
+  pub loc: Loc,
+  pub values: HashMap<String, Val>,
+}
+
+impl Display for Loc {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) {
+    write!(f, "{:?}:{}", &self.file, self.lno)?;
+    if let Some(s) = &self.section {
+      write!(f, " ")?;
+      let dbg = format!("{:?}", &s);
+      if let Some(mid) = (||{
+        let mid = dbg.strip_prefix(r#"""#)?;
+        let mid = mid.strip_suffix(r#"""#)?;
+        Some(mid)
+      })() {
+        write!(f, "[{}]", mid)?;
+      } else {
+        write!(f, "{}", dbg)?;
+      }
+    }
+  }
+}
+
+
+#[throws(AE)]
+pub fn read(parsed: &mut Parsed, file: &mut dyn BufRead, path_for_loc: &Path)
+//->Result<(), AE>
+{
+  let parsed = Rc::new(RefCell::new(parsed));
+  let path: Arc<PathBuf> = path_for_loc.to_owned().into();
+  let mut section: Option<RefMut<Section>> = None;
+  for (lno, line) in file.lines().enumerate() {
+    let line = line.context("read")?;
+    let line = line.trim();
+
+    if line.is_empty() { continue }
+    if regex_is_match!(r#"^ [;\#] "#x, line) { continue }
+
+    let loc = Loc {
+      lno,
+      file: path.clone(),
+      section: section.as_ref().map(|s| s.loc.section.as_ref().unwrap().clone()),
+    };
+    (|| Ok::<(),AE>({
+
+      if let Some((_,new,)) =
+        regex_captures!(r#"^ \[ \s* (.+?) \s* \] $"#x, line)
+      {
+        let new: Arc<str> = new.to_owned().into();
+
+        section.take(); // drops previous RefCell borrow of parsed
+
+        let new_section = RefMut::map(parsed.borrow_mut(), |p| {
+
+          p.entry(new.clone())
+            .or_insert_with(|| {
+              Section {
+                loc: Loc { section: Some(new), file: path.clone(), lno },
+                values: default(),
+                }
+            })
+
+        });
+
+        section = Some(new_section);
+
+      } else if let Some((_, key, val)) =
+        regex_captures!(r#"^ ( [^\[] .*? ) \s* = \s* (.*) $"#x, line)
+      {
+        let val = Val { loc: loc.clone(), val: val.into() };
+
+        section
+          .as_mut()
+          .ok_or_else(|| anyhow!("value outside section"))?
+          .values
+          .insert(key.into(), val);
+
+      } else {
+        throw!(if line.starts_with("[") {
+          anyhow!(r#"syntax error (section missing final "]"?)"#)
+        } else {
+          anyhow!(r#"syntax error (setting missing "="?)"#)
+        })
+      }
+
+    }))().with_context(|| loc.to_string())?
+  }
+}
diff --git a/src/ipif.rs b/src/ipif.rs
new file mode 100644 (file)
index 0000000..3b4da9c
--- /dev/null
@@ -0,0 +1,82 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+type Tx = t_io::Split<t_io::BufReader<t_proc::ChildStdout>>;
+
+pub struct Ipif {
+  pub tx: Tx,
+  pub rx: t_proc::ChildStdin,
+  stderr_task: JoinHandle<io::Result<()>>,
+  child: t_proc::Child,
+}
+
+impl Ipif {
+  #[throws(AE)]
+  pub fn start(cmd: &str, ic_name: Option<String>) -> Self {
+    let mut child = tokio::process::Command::new("sh")
+      .args(&["-c", cmd])
+      .stdin (process::Stdio::piped())
+      .stdout(process::Stdio::piped())
+      .stderr(process::Stdio::piped())
+      .kill_on_drop(true)
+      .spawn().context("spawn ipif")?;
+
+    let stderr = child.stderr.take().unwrap();
+
+    let stderr_task = task::spawn(async move {
+      let mut stderr = t_io::BufReader::new(stderr).lines();
+      while let Some(l) = stderr.next_line().await? {
+        error!("{}ipif stderr: {}",
+               OptionPrefixColon(ic_name.as_ref()),
+               l.trim_end());
+      }
+      Ok::<_,io::Error>(())
+    });
+    let tx = child.stdout.take().unwrap();
+    let rx = child.stdin .take().unwrap();
+    let tx = t_io::BufReader::new(tx).split(SLIP_END);
+
+    Ipif {
+      tx,
+      rx,
+      stderr_task,
+      child,
+    }
+  }
+
+  pub async fn quitting(mut self, ic: Option<&InstanceConfig>) {
+    let icd = OptionPrefixColon(ic);
+    drop(self.rx);
+
+    match self.child.wait().await {
+      Err(e) => error!("{}also, failed to await ipif child: {}", icd, e),
+      Ok(st) => {
+        let stderr_timeout = Duration::from_millis(1000);
+        match tokio::time::timeout(stderr_timeout, self.stderr_task).await {
+          Err::<_,tokio::time::error::Elapsed>(_)
+            => warn!("{}ipif stderr task continues!", icd),
+          Ok(Err(e)) => error!("{}ipif stderr task crashed: {}", icd, e),
+          Ok(Ok(Err(e))) => error!("{}ipif stderr read failed: {}", icd, e),
+          Ok(Ok(Ok(()))) => { },
+        }
+        if ! st.success() {
+          error!("{}ipif process failed: {}", icd, st);
+        }
+      }
+    }
+
+    drop(self.tx);
+  }
+
+  #[throws(AE)]
+  pub async fn next_frame(tx: &mut Tx) -> Vec<u8> {
+    let data = tx.next_segment().await;
+    (||{
+      data?.ok_or_else(|| io::Error::from(io::ErrorKind::UnexpectedEof))
+    })().context("read from ipif")?
+  }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644 (file)
index 0000000..fc641d2
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+pub mod prelude;
+
+pub mod config;
+pub mod ipif;
+pub mod multipart;
+pub mod slip;
+pub mod reporter;
+pub mod queue;
+pub mod types;
+pub mod utils;
+
+pub mod ini;
diff --git a/src/multipart.rs b/src/multipart.rs
new file mode 100644 (file)
index 0000000..c2f0ff4
--- /dev/null
@@ -0,0 +1,210 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[derive(Debug)]
+pub struct Component<'b> {
+  pub name: PartName,
+  pub payload: &'b [u8],
+}
+
+#[derive(Debug)]
+#[derive(Eq,PartialEq,Ord,PartialOrd,Hash)]
+#[allow(non_camel_case_types)]
+pub enum PartName { m, d, Other }
+
+pub type BoundaryFinder = memchr::memmem::Finder<'static>;
+
+#[throws(AE)]
+/// Processes the start of a component (or terminating boundary).
+///
+/// Returned payload is only the start of the payload; the next
+/// boundary has not been identified.
+pub fn process_boundary<'b>(warnings: &mut Warnings,
+                            after_leader: &'b [u8], expected: PartName)
+                            -> Option<Component<'b>> {
+  let rhs = after_leader;
+  let mut rhs =
+       if let Some(rhs) = rhs.strip_prefix(b"\r\n") { rhs         }
+  else if let Some(_  ) = rhs.strip_prefix(b"--"  ) { return None }
+  else if let Some(rhs) = rhs.strip_prefix(b"\n"  ) { rhs         }
+  else { throw!(anyhow!("invalid multipart delimiter")) };
+
+  let mut part_name = None;
+
+  loop {
+    // RHS points to the start of a header line
+    let nl = memchr::memchr(b'\n', rhs)
+      .ok_or_else(|| anyhow!("part headers truncated"))?;
+    let l = &rhs[0..nl]; rhs = &rhs[nl+1..];
+    if l == b"\r" || l == b"" { break } // end of headers
+    if l.starts_with(b"--") { throw!(anyhow!("boundary in part headers")) }
+
+    match (||{
+      let l = str::from_utf8(l).context("interpret part headers as utf-8")?;
+
+      let (_, disposition) = if let Some(y) =
+        regex_captures!(r#"^Content-Disposition[ \t]*:[ \t]*(.*)$"#i, l) { y }
+        else { return Ok(()) };
+
+      let disposition = disposition.trim_end();
+      if disposition.len() >= 100 { throw!(anyhow!(
+        "Content-Disposition value implausibly long"
+      )) }
+
+      // todo: replace with mailparse?
+      // (not in side, dep on charset not in sid)
+      // also seems to box for all the bits
+
+      // This let's us pretend it's a mime type, so we can use mime::Mime
+      let disposition = format!("dummy/{}", disposition);
+
+      let disposition: mime::Mime = disposition.parse()
+        .context("parse Content-Disposition")?;
+      let name = disposition.get_param("name")
+        .ok_or_else(|| anyhow!(r#"find "name" in Content-Disposition"#))?;
+
+      let name = match name.as_ref() {
+        "m" => PartName::m,
+        "d" => PartName::d,
+        _   => PartName::Other,
+      };
+
+      if let Some(_) = mem::replace(&mut part_name, Some(name)) {
+        throw!(anyhow!(r#"multiple "name"s in Content-Disposition(s)"#))
+      }
+      Ok::<_,AE>(())
+    })() {
+      Err(e) => warnings.add(&e)?,
+      Ok(()) => { },
+    };
+  }
+
+  //dbg!(DumpHex(rhs));
+
+  Some(Component { name: part_name.unwrap_or(expected), payload: rhs })
+}
+
+pub struct ComponentIterator<'b> {
+  at_boundary: &'b [u8],
+  boundary_finder: BoundaryFinder,
+}
+
+#[derive(Error,Debug)]
+#[error("missing mime multipart boundary")]
+pub struct MissingBoundary;
+
+impl<'b> ComponentIterator<'b> {
+  #[throws(MissingBoundary)]
+  pub fn resume_mid_component(buf: &'b [u8], boundary_finder: BoundaryFinder)
+                              -> (&'b [u8], Self) {
+    let next_boundary = boundary_finder.find(buf).ok_or(MissingBoundary)?;
+    let part = &buf[0..next_boundary];
+    let part = Self::payload_trim(part);
+
+    //dbg!(DumpHex(part));
+
+    (part, ComponentIterator {
+      at_boundary: &buf[next_boundary..],
+      boundary_finder,
+    })
+  }
+
+  fn payload_trim(payload: &[u8]) -> &[u8] {
+    payload.strip_suffix(b"\r").unwrap_or(payload)
+  }
+
+  #[throws(AE)]
+  pub fn next(&mut self, warnings: &mut Warnings, expected: PartName)
+              -> Option<Component<'b>> {
+    if self.at_boundary.is_empty() { return None }
+
+    let mut comp = match {
+      //dbg!(DumpHex(self.boundary_finder.needle()));
+      let boundary_len = self.boundary_finder.needle().len();
+      //dbg!(boundary_len);
+      process_boundary(warnings,
+                       &self.at_boundary[boundary_len..],
+                       expected)?
+    } {
+      None => {
+        self.at_boundary = &self.at_boundary[0..0];
+        return None;
+      },
+      Some(c) => c,
+    };
+
+    let next_boundary = self.boundary_finder.find(&comp.payload)
+      .ok_or(MissingBoundary)?;
+
+    self.at_boundary = &comp.payload[next_boundary..];
+    comp.payload = Self::payload_trim(&comp.payload[0..next_boundary]);
+
+    //dbg!(DumpHex(comp.payload));
+    //dbg!(DumpHex(&self.at_boundary[0..5]));
+
+    Some(comp)
+  }
+}
+
+pub struct MetadataFieldIterator<'b> {
+  buf: &'b [u8],
+  last: Option<usize>,
+  iter: memchr::Memchr<'b>,
+}
+
+impl<'b> MetadataFieldIterator<'b> {
+  pub fn new(buf: &'b [u8]) -> Self { Self {
+    buf,
+    last: Some(0),
+    iter: memchr::Memchr::new(b'\n', buf),
+  } }
+
+  #[throws(AE)]
+  pub fn need_next(&mut self) -> &'b str
+  {
+    self.next().ok_or_else(|| anyhow!("missing"))??
+  }
+
+  #[throws(AE)]
+  pub fn need_parse<T>(&mut self) -> T
+  where T: FromStr,
+        AE: From<T::Err>,
+  {
+    self.parse()?.ok_or_else(|| anyhow!("missing"))?
+  }
+
+  #[throws(AE)]
+  pub fn parse<T>(&mut self) -> Option<T>
+  where T: FromStr,
+        AE: From<T::Err>,
+  {
+    let s = if let Some(r) = self.next() { r? } else { return None };
+    Some(s.parse()?)
+  }
+
+  pub fn remaining_bytes_len(&self) -> usize {
+    if let Some(last) = self.last {
+      self.buf.len() - last
+    } else {
+      0
+    }
+  }
+}
+                                      
+impl<'b> Iterator for MetadataFieldIterator<'b> {
+  type Item = Result<&'b str, std::str::Utf8Error>;
+  fn next(&mut self) -> Option<Result<&'b str, std::str::Utf8Error>> {
+    let last = self.last?;
+    let (s, last) = match self.iter.next() {
+      Some(nl) => (&self.buf[last..nl], Some(nl+1)),
+      None     => (&self.buf[last..],   None),
+    };
+    self.last = last;
+    let s = str::from_utf8(s).map(|s| s.trim());
+    Some(s)
+  }
+}
+impl<'b> std::iter::FusedIterator for MetadataFieldIterator<'b> { }
diff --git a/src/prelude.rs b/src/prelude.rs
new file mode 100644 (file)
index 0000000..0ca0ec1
--- /dev/null
@@ -0,0 +1,85 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+pub use std::array;
+pub use std::collections::{BTreeSet, HashMap, VecDeque};
+pub use std::convert::{Infallible, TryFrom, TryInto};
+pub use std::borrow::Cow;
+pub use std::cell::{RefCell, RefMut};
+pub use std::cmp::{min, max};
+pub use std::env;
+pub use std::fs;
+pub use std::fmt::{self, Debug, Display, Write as _};
+pub use std::future::Future;
+pub use std::io::{self, Cursor, ErrorKind, Read as _, Write as _};
+pub use std::iter;
+pub use std::mem;
+pub use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
+pub use std::path::{Path, PathBuf};
+pub use std::panic::{self, AssertUnwindSafe};
+pub use std::process;
+pub use std::pin::Pin;
+pub use std::str::{self, FromStr};
+pub use std::sync::Arc;
+pub use std::task::Poll;
+pub use std::time::{SystemTime, UNIX_EPOCH};
+
+pub use cervine::Cow as Cervine;
+pub use extend::ext;
+pub use fehler::{throw, throws};
+pub use futures::{poll, future, FutureExt, StreamExt, TryStreamExt};
+pub use hyper::body::{Bytes, Buf, HttpBody};
+pub use hyper::{Method, Uri};
+pub use hyper_tls::HttpsConnector;
+pub use ipnet::IpNet;
+pub use itertools::{iproduct, izip, Itertools};
+pub use lazy_regex::{regex_captures, regex_is_match, regex_replace_all};
+pub use lazy_static::lazy_static;
+pub use log::{trace, debug, info, warn, error};
+pub use memchr::memmem;
+pub use pin_project_lite::pin_project;
+pub use structopt::StructOpt;
+pub use subtle::ConstantTimeEq;
+pub use thiserror::Error;
+pub use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
+pub use tokio::pin;
+pub use tokio::select;
+pub use tokio::sync::{mpsc, oneshot};
+pub use tokio::task::{self, JoinError, JoinHandle};
+pub use tokio::time::{Duration, Instant};
+pub use void::{self, Void, ResultVoidExt, ResultVoidErrExt};
+
+pub use eyre as anyhow;
+pub use eyre::eyre as anyhow;
+pub use eyre::WrapErr;
+pub use eyre::Error as AE;
+
+pub use crate::config::{self, InstanceConfig, u32Ext as _};
+pub use crate::ini;
+pub use crate::ipif::Ipif;
+pub use crate::multipart::{self, PartName, MetadataFieldIterator};
+pub use crate::utils::*;
+pub use crate::queue::*;
+pub use crate::reporter::*;
+pub use crate::types::*;
+pub use crate::slip::{self, *};
+
+pub type ReqNum = u64;
+
+pub use ErrorKind as EK;
+pub use PacketError as PE;
+pub use tokio::io as t_io;
+pub use tokio::process as t_proc;
+
+pub const SLIP_END:     u8 = 0o300; // c0
+pub const SLIP_ESC:     u8 = 0o333; // db
+pub const SLIP_ESC_END: u8 = 0o334; // dc
+pub const SLIP_ESC_ESC: u8 = 0o335; // dd
+pub const SLIP_MIME_ESC: u8 = b'-'; // 2d
+
+pub const MAX_OVERHEAD: usize = 2_000;
+
+pub use base64::STANDARD as BASE64_CONFIG;
+
+pub fn default<T:Default>() -> T { Default::default() }
diff --git a/src/queue.rs b/src/queue.rs
new file mode 100644 (file)
index 0000000..49fd3c4
--- /dev/null
@@ -0,0 +1,142 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[derive(Default,Clone)]
+pub struct PacketQueue<D> {
+  queue: VecDeque<D>,
+  content: usize,
+}
+
+impl<D> PacketQueue<D> where D: AsRef<[u8]> {
+  pub fn push_back(&mut self, data: D) {
+    self.content += data.as_ref().len();
+    self.queue.push_back(data);
+  }
+
+  pub fn pop_front(&mut self) -> Option<D> {
+    let data = self.queue.pop_front()?;
+    self.content -= data.as_ref().len();
+    Some(data)
+  }
+
+  pub fn content_count(&self) -> usize { self.queue.len() }
+  pub fn content_len(&self) -> usize { self.content }
+  pub fn total_len(&self) -> usize {
+    self.content_count() + self.content_len()
+  }
+
+  pub fn is_empty(&self) -> bool { self.queue.is_empty() }
+  pub fn peek_front(&self) -> Option<&D> { self.queue.front() }
+}
+
+#[derive(Default,Clone)]
+pub struct QueueBuf<E> {
+  content: usize,
+  eaten1: usize, // 0 <= eaten1 < queue.front()...len()
+  queue: VecDeque<E>,
+}
+
+#[derive(Default,Debug,Clone)]
+pub struct FrameQueueBuf {
+  queue: QueueBuf<Cervine<'static, Box<[u8]>, [u8]>>,
+}
+
+impl<E> Debug for QueueBuf<E> where E: AsRef<[u8]> {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) {
+    write!(f, "Queue{{content={},eaten1={},queue=[",
+           self.content, self.eaten1)?;
+    for q in &self.queue { write!(f, "{},", q.as_ref().len())?; }
+    write!(f, "]}}")?;
+  }
+}
+
+impl<E> QueueBuf<E> where E: AsRef<[u8]> {
+  pub fn push<B: Into<E>>(&mut self, b: B) {
+    self.push_(b.into());
+  }
+  fn push_(&mut self, b: E) {
+    let l = b.as_ref().len();
+    self.queue.push_back(b);
+    self.content += l;
+  }
+  pub fn is_empty(&self) -> bool { self.content == 0 }
+  pub fn len(&self) -> usize { self.content }
+}
+
+impl FrameQueueBuf {
+  pub fn push_esc<B: Into<Box<[u8]>>>(&mut self, b: B) {
+    self.push_esc_(b.into());
+  }
+  fn push_esc_(&mut self, b: Box<[u8]>) {
+    self.queue.push_(Cervine::Owned(b));
+    self.queue.push_(Cervine::Borrowed(&SLIP_END_SLICE));
+  }
+  pub fn esc_push(&mut self, b: Box<[u8]>) {
+    self.queue.push_(Cervine::Borrowed(&SLIP_END_SLICE));
+    self.queue.push_(Cervine::Owned(b));
+  }
+  pub fn push_raw(&mut self, b: Box<[u8]>) {
+    self.queue.push_(Cervine::Owned(b));
+  }
+  pub fn is_empty(&self) -> bool { self.queue.is_empty() }
+  pub fn len(&self) -> usize { self.queue.len() }
+}
+
+impl<E> hyper::body::Buf for QueueBuf<E> where E: AsRef<[u8]> {
+  fn remaining(&self) -> usize { self.content }
+  fn chunk(&self) -> &[u8] {
+    let front = if let Some(f) = self.queue.front() { f } else { return &[] };
+    &front.as_ref()[ self.eaten1.. ]
+  }
+  fn advance(&mut self, cnt: usize) {
+    self.content -= cnt;
+    self.eaten1 += cnt;
+    loop {
+      if self.eaten1 == 0 { break }
+      let front = self.queue.front().unwrap();
+      if self.eaten1 < front.as_ref().len() { break; }
+      self.eaten1 -= front.as_ref().len();
+      self.queue.pop_front().unwrap();
+    }
+  }
+}
+
+impl hyper::body::Buf for FrameQueueBuf {
+  fn remaining(&self) -> usize { self.queue.remaining() }
+  fn chunk(&self) -> &[u8] { self.queue.chunk() }
+  fn advance(&mut self, cnt: usize) { self.queue.advance(cnt) }
+}
+
+pin_project!{
+  pub struct BufBody<B:Buf> {
+    body: Option<B>,
+  }
+}
+impl<B:Buf> BufBody<B> {
+  pub fn new(body: B) -> Self { Self { body: Some(body ) } }
+}
+impl BufBody<FrameQueueBuf> {
+  pub fn display<S:Display>(s: S) -> Self {
+    let s = s.to_string().into_bytes();
+    let mut buf: FrameQueueBuf = default();
+    buf.push_raw(s.into());
+    Self::new(buf)
+  }
+}
+
+impl<B:Buf> HttpBody for BufBody<B> {
+  type Error = Void;
+  type Data = B;
+  fn poll_data(self: Pin<&mut Self>, _: &mut std::task::Context<'_>)
+               -> Poll<Option<Result<B, Void>>> {
+    Poll::Ready(Ok(self.project().body.take()).transpose())
+  }
+  fn poll_trailers(self: Pin<&mut Self>, _: &mut std::task::Context<'_>)
+ -> Poll<Result<Option<hyper::HeaderMap<hyper::header::HeaderValue>>, Void>> {
+    Poll::Ready(Ok(None))
+  }
+}
diff --git a/src/reporter.rs b/src/reporter.rs
new file mode 100644 (file)
index 0000000..530534a
--- /dev/null
@@ -0,0 +1,308 @@
+// Copyright 2021-2022 Ian Jackson, yaahc and contributors to Hippotat and Eyre
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[derive(StructOpt,Debug)]
+pub struct LogOpts {
+  /// Increase debug level
+  ///
+  /// May be repeated for more verbosity.
+  ///
+  /// When using syslog, one `-D` this arranges to send to syslog even
+  /// trace messages (mapped onto syslog level `DEBUG`);
+  /// and two -`D` means to send to syslog even messages from lower layers
+  /// (normally just the hippotat modules log to
+  /// syslog).
+  #[structopt(long, short="D", parse(from_occurrences))]
+  debug: usize,
+
+  /// Syslog facility to use
+  #[structopt(long, parse(try_from_str=parse_syslog_facility))]
+  syslog_facility: Option<syslog::Facility>,
+}
+
+#[throws(AE)]
+fn parse_syslog_facility(s: &str) -> syslog::Facility {
+  s.parse().map_err(|()| anyhow!("unrecognised syslog facility: {:?}", s))?
+}
+
+#[derive(Debug)]
+struct LogWrapper<T>{
+  debug: usize,
+  output: T,
+}
+
+impl<T> LogWrapper<T> {
+  fn wanted(&self, md: &log::Metadata<'_>) -> bool {
+    let first = |mod_path| {
+      let mod_path: &str = mod_path; // can't do in args as breaks lifetimes
+      mod_path.split_once("::").map(|s| s.0).unwrap_or(mod_path)
+    };
+    self.debug >= 2 || first(md.target()) == first(module_path!())
+  }
+
+  fn set_max_level(&self) {
+    log::set_max_level(if self.debug < 1 {
+      log::LevelFilter::Debug
+    } else {
+      log::LevelFilter::Trace
+    });
+  }
+}
+
+impl<T> log::Log for LogWrapper<T> where T: log::Log {
+  fn enabled(&self, md: &log::Metadata<'_>) -> bool {
+    self.wanted(md) && self.output.enabled(md)
+  }
+
+  fn log(&self, record: &log::Record<'_>) {
+    if self.wanted(record.metadata()) {
+      let mut wrap = log::Record::builder();
+
+      macro_rules! copy { { $( $f:ident ),* $(,)? } => {
+        $( wrap.$f(record.$f()); )*
+      } }
+      copy!{
+        level, target, module_path, file, line
+      };
+      match format_args!("{}: {}",
+                         heck::AsKebabCase(record.level().as_str()),
+                         record.args()) {
+        args => {
+          wrap.args(args);
+          self.output.log(&wrap.build());
+        }
+      }
+    }
+  }
+
+  fn flush(&self) {
+    self.output.flush()
+  }
+}
+
+impl LogOpts {
+  #[throws(AE)]
+  pub fn log_init(&self) {
+    if let Some(facility) = self.syslog_facility {
+      let f = syslog::Formatter3164 {
+        facility,
+        hostname: None,
+        process: "hippotatd".into(),
+        pid: std::process::id(),
+      };
+      let l = syslog::unix(f)
+        // syslog::Error is not Sync.
+        // https://github.com/Geal/rust-syslog/issues/65
+        .map_err(|e| anyhow!(e.to_string()))
+        .context("set up syslog logger")?;
+      let l = syslog::BasicLogger::new(l);
+      let l = LogWrapper { output: l, debug: self.debug };
+      l.set_max_level();
+      let l = Box::new(l) as _;
+      log::set_boxed_logger(l).context("install syslog logger")?;
+    } else {
+      let env = env_logger::Env::new()
+        .filter("HIPPOTAT_LOG")
+        .write_style("HIPPOTAT_LOG_STYLE");
+  
+      let mut logb = env_logger::Builder::new();
+      logb.filter(Some("hippotat"),
+                  *[ log::LevelFilter::Info,
+                     log::LevelFilter::Debug ]
+                  .get(self.debug)
+                  .unwrap_or(
+                    &log::LevelFilter::Trace
+                  ));
+      logb.parse_env(env);
+      logb.init();
+    }
+  }
+}
+
+pub struct OptionPrefixColon<T>(pub Option<T>);
+impl<T:Display> Display for OptionPrefixColon<T> {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) {
+    if let Some(x) = &self.0 { write!(f, "{}: ", x)? }
+  }
+}
+
+// For clients only, really.
+pub struct Reporter<'r> {
+  ic: &'r InstanceConfig,
+  successes: u64,
+  last_report: Option<Report>,
+}
+
+#[derive(Debug)]
+struct Report {
+  when: Instant,
+  ok: Result<(),()>,
+}         
+
+// Reporting strategy
+//   - report all errors
+//   - report first success after a period of lack of messages
+//   - if error, report last success
+
+impl<'r> Reporter<'r> {
+  pub fn new(ic: &'r InstanceConfig) -> Self { Reporter {
+    ic,
+    successes: 0,
+    last_report: None,
+  } }
+  
+  pub fn success(&mut self) {
+    self.successes += 1;
+    let now = Instant::now();
+    if let Some(rep) = &self.last_report {
+      if now - rep.when < match rep.ok {
+        Ok(()) => match self.ic.success_report_interval {
+          z if z == Duration::default() => return,
+          nonzero => nonzero,
+        },
+        Err(()) => self.ic.effective_http_timeout,
+      } {
+        return
+      }
+    }
+    
+    info!(target:"hippotat", "{} ({}ok): running", self.ic, self.successes);
+    self.last_report = Some(Report { when: now, ok: Ok(()) });
+  }
+
+  pub fn filter<T>(&mut self, req_num: Option<ReqNum>, r: Result<T,AE>)
+                   -> Option<T> {
+    let now = Instant::now();
+    match r {
+      Ok(t) => {
+        Some(t)
+      },
+      Err(e) => {
+        let m = (||{
+          let mut m = self.ic.to_string();
+          if let Some(req_num) = req_num {
+            write!(m, " #{}", req_num)?;
+          }
+          if self.successes > 0 {
+            write!(m, " ({}ok)", self.successes)?;
+            self.successes = 0;
+          }
+          write!(m, ": {}", e)?;
+          Ok::<_,fmt::Error>(m)
+        })().unwrap();
+        warn!(target:"hippotat", "{}", m);
+        self.last_report = Some(Report { when: now, ok: Err(()) });
+        None
+      },
+    }
+  }
+}
+
+use backtrace::Backtrace;
+use eyre::Chain;
+use indenter::indented;
+
+#[derive(Debug)]
+struct EyreDedupHandler {
+  backtrace: Option<Arc<parking_lot::Mutex<Backtrace>>>,
+}
+
+type EyreDynError<'r> = &'r (dyn std::error::Error + 'static);
+
+impl eyre::EyreHandler for EyreDedupHandler {
+  #[throws(fmt::Error)]
+  fn display(&self, error: EyreDynError, f: &mut fmt::Formatter) {
+    let mut last: Option<String> = None;
+    let mut error = Some(error);
+    while let Some(e) = error {
+      let m = e.to_string();
+      match last {
+        None => write!(f, "{}", m)?,
+        Some(l) if l.contains(&m) => { },
+        Some(_) => write!(f, ": {}", m)?,
+      }
+      last = Some(m);
+      error = e.source();
+    }
+  }
+
+  #[throws(fmt::Error)]
+  fn debug(&self, error: EyreDynError, f: &mut fmt::Formatter) {
+    if f.alternate() {
+      return core::fmt::Debug::fmt(error, f)?;
+    }
+
+    write!(f, "{}", error)?;
+
+    if let Some(cause) = error.source() {
+      write!(f, "\n\nCaused by:")?;
+      let multiple = cause.source().is_some();
+
+      for (n, error) in Chain::new(cause).enumerate() {
+        writeln!(f)?;
+        if multiple {
+          write!(indented(f).ind(n), "{}", error)?;
+        } else {
+          write!(indented(f), "{}", error)?;
+        }
+      }
+    }
+
+    if let Some(bt) = &self.backtrace {
+      let mut bt = bt.lock();
+      bt.resolve();
+      write!(f, "\n\nStack backtrace:\n{:?}", bt)?;
+    }
+  }
+}
+
+#[throws(AE)]
+pub fn dedup_eyre_setup() {
+  eyre::set_hook(Box::new(|_error| {
+    lazy_static! {
+      static ref BACKTRACE: bool = {
+        match env::var("RUST_BACKTRACE") {
+          Ok(s) if s.starts_with("1") => true,
+          Ok(s) if s == "0" => false,
+          Err(env::VarError::NotPresent) => false,
+          x => {
+            eprintln!("warning: RUST_BACKTRACE not understood: {:?}", x);
+            false
+          },
+        }
+      };
+    }
+    let backtrace = if *BACKTRACE {
+      let bt = Backtrace::new_unresolved();
+      let bt = Arc::new(bt.into());
+      Some(bt)
+    } else {
+      None
+    };
+    Box::new(EyreDedupHandler { backtrace })
+  }))
+    .context("set error handler")?;
+}
+
+const MAX_WARNINGS: usize = 15;
+
+#[derive(Debug,Default)]
+pub struct Warnings {
+  pub warnings: Vec<String>,
+}
+
+#[derive(Debug,Error)]
+#[error("too many warnings")]
+pub struct TooManyWarnings;
+
+impl Warnings {
+  #[throws(TooManyWarnings)]
+  pub fn add(&mut self, e: &dyn Display) {
+    if self.warnings.len() >= MAX_WARNINGS { throw!(TooManyWarnings) }
+    self.warnings.push(e.to_string());
+  }
+}
diff --git a/src/rope.rs b/src/rope.rs
new file mode 100644 (file)
index 0000000..12054ab
--- /dev/null
@@ -0,0 +1,42 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[derive(Default,Clone)]
+pub struct Queue {
+  content: usize,
+  eaten1: usize, // 0 <= eaten1 < queue.front()...len()
+  queue: VecDeque<Box<[u8]>>,
+}
+
+pub impl Queue {
+  pub fn push<B: Into<Box<[u8]>>>(&mut self, b: B) {
+    self.push_(b.into());
+  }
+  pub fn push_(&mut self, b: Box<[u8]>) {
+    let l = b.len();
+    self.push(b);
+    b.content += b;
+  }
+  pub fn is_empty(&self) { self.content == 0 }
+}
+
+impl bytes::Buf for Queue {
+  fn remaining(&self) -> usize { self.content }
+  fn chunk(&self) -> usize {
+    let front = if let(f) = self.queue.front() { f } else { return &[] };
+    front[ self.eaten1.. ]
+  }
+  fn advance(&self, cnt: usize) {
+    eaten1 += cnt;
+    loop {
+      if eaten1 == 0 { break }
+      let front = self.queue.front().unwrap();
+      if eaten1 < front.len() { break; }
+      eaten1 -= front.len();
+      self.queue.pop_front().unwrap();
+    }
+  }
+}
diff --git a/src/slip.rs b/src/slip.rs
new file mode 100644 (file)
index 0000000..6476014
--- /dev/null
@@ -0,0 +1,260 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+pub static SLIP_END_SLICE: &[u8] = &[SLIP_END];
+
+#[derive(Error,Debug,Copy,Clone,Eq,PartialEq)]
+pub enum PacketError {
+  #[error("empty packet")]                 Empty,
+  #[error("MTU exceeded ({len} > {mtu})")] MTU { len: usize, mtu: u32 },
+  #[error("Invalid SLIP escape sequence")] SLIP,
+  #[error("unexpected src addr {0:?}")]    Src(IpAddr),
+  #[error("unexpected dst addr {0:?}")]    Dst(IpAddr),
+  #[error("truncated, IPv{vsn}, len={len}")] Truncated { len: usize, vsn: u8 },
+}
+
+pub trait SlipMime: Copy { const CONV_TO: Option<bool>; }
+#[derive(Copy,Clone,Debug)] pub struct Slip2Mime;
+#[derive(Copy,Clone,Debug)] pub struct Mime2Slip;
+#[derive(Copy,Clone,Debug)] pub struct SlipNoConv;
+impl SlipMime for Slip2Mime { const CONV_TO: Option<bool> = Some(true); }
+impl SlipMime for Mime2Slip { const CONV_TO: Option<bool> = Some(false); }
+impl SlipMime for SlipNoConv { const CONV_TO: Option<bool> = None; }
+
+#[derive(Debug,Error,Eq,PartialEq)]
+pub enum SlipFramesError<E> where E: std::error::Error + 'static {
+  #[error("only bad IP datagrams")] ErrorOnlyBad,
+  #[error("{0}")] Other(#[from] E),
+}
+  
+#[throws(SlipFramesError<EHE>)]
+pub async fn processn<AC, EH, EHE, OUT, FOUT, ACR, M: SlipMime+Copy>(
+  mime: M,
+  mtu: u32,
+  data: &[u8],
+  addr_chk: AC,
+  mut out: OUT,
+  mut error_handler: EH
+) where AC: Fn(&[u8]) -> Result<ACR, PacketError> + Copy + Send,
+        OUT: FnMut((Box<[u8]>, ACR)) -> FOUT + Send,
+        FOUT: Future<Output=Result<(), PacketError>> + Send,
+        EH: FnMut(PacketError) -> Result<(), SlipFramesError<EHE>> + Send,
+        EHE: std::error::Error + Send + 'static,
+{
+  //  eprintln!("before: {:?}", DumpHex(data));
+  if data.is_empty() { return }
+  let mut ok = false;
+  let mut err = false;
+  for packet in data.split(|&c| c == SLIP_END) {
+    match async {
+      let checked = process1(mime, mtu, packet, addr_chk);
+      if matches!(checked, Err(PacketError::Empty)) { return Ok::<_,PE>(()) }
+      out(checked?).await?;
+      ok = true;
+      Ok::<_,PE>(())
+    }.await {
+      Ok(()) => { },
+      Err(e) => { err=true; error_handler(e)?; },
+    }
+  }
+//  eprintln!(" after: {:?}", DumpHex(data));
+  if err && !ok { throw!(SlipFramesError::ErrorOnlyBad) }
+}
+
+#[throws(PacketError)]
+pub fn process1<AC, M: SlipMime, ACR>(
+  _mime: M,
+  mtu: u32,
+  packet: &[u8],
+  addr_chk: AC,
+) -> (Box<[u8]>, ACR)
+where AC: Fn(&[u8]) -> Result<ACR, PacketError>,
+{
+  if packet.len() == 0 {
+    throw!(PacketError::Empty)
+  }
+
+  let mut packet: Box<[u8]> = packet.to_owned().into();
+  let mut walk: &mut [u8] = &mut packet;
+  let mut header = [0u8; HEADER_FOR_ADDR];
+  let mut wheader = &mut header[..];
+  let mut escapes = 0;
+
+  while let Some((i, was_mime)) = walk.iter().enumerate().find_map(
+    |(i,&c)| match c {
+      SLIP_ESC                               => Some((i,false)),
+      SLIP_MIME_ESC if M::CONV_TO.is_some()  => Some((i,true)),
+      _ => None,
+    }
+  ) {
+    let _ = wheader.write(&walk[0..i]);
+    if M::CONV_TO.is_some() {
+      walk[i] = if was_mime { SLIP_ESC } else { SLIP_MIME_ESC };
+    }
+    if Some(was_mime) != M::CONV_TO {
+      let c = match walk.get(i+1) {
+        Some(&SLIP_ESC_ESC) => SLIP_ESC,
+        Some(&SLIP_ESC_END) => SLIP_END,
+        _ => throw!(PacketError::SLIP),
+      };
+      let _ = wheader.write(&[c]);
+      walk = &mut walk[i+2 ..];
+      escapes += 1;
+    } else {
+      let _ = wheader.write(&[SLIP_MIME_ESC]);
+      walk = &mut walk[i+1 ..];
+    }
+  }
+  let _ = wheader.write(walk);
+  let wheader_len = wheader.len();
+  let header = &header[0.. header.len() - wheader_len];
+
+  let decoded_len = packet.len() - escapes;
+  if decoded_len > mtu.sat() {
+    throw!(PacketError::MTU { len: decoded_len, mtu });
+  }
+
+  let acr = addr_chk(&header)?;
+
+  (packet, acr)
+}
+
+pub type Frame = Vec<u8>;
+pub type FramesData = Vec<Vec<u8>>;
+// todo: https://github.com/tokio-rs/bytes/pull/504
+//   pub type Frame = Box<[u8]>;
+//   pub type FramesData = Vec<Frame>;
+//       `From<Box<[u8]>>` is not implemented for `Bytes`
+// when this is fixed, there are two `into`s in client.rs which 
+// become redundant (search for todo:504)
+
+
+#[derive(Default)]
+pub struct Frames {
+  frames: FramesData,
+  total_len: usize,
+  tried_full: bool,
+}
+
+impl Debug for Frames {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) {
+    write!(f, "Frames{{n={},len={}}}", &self.frames.len(), &self.total_len)?;
+  }
+}
+
+impl Frames {
+  #[throws(Frame)]
+  pub fn add(&mut self, max: u32, frame: Frame) {
+    if frame.len() == 0 { return }
+    let new_total = self.total_len + frame.len() + 1;
+    if new_total > max.sat() { self.tried_full = true; throw!(frame); }
+    self.total_len = new_total;
+    self.frames.push(frame);
+  }
+
+  #[inline] pub fn tried_full(&self) -> bool { self.tried_full }
+  #[inline] pub fn is_empty(&self) -> bool { self.frames.is_empty() }
+}
+
+impl From<Frames> for FramesData {
+  fn from(frames: Frames) -> FramesData { frames.frames }
+}
+
+const HEADER_FOR_ADDR: usize = 40;
+
+#[throws(PacketError)]
+pub fn ip_packet_addr<const DST: bool>(header: &[u8]) -> IpAddr {
+  let vsn = (header.get(0).ok_or_else(|| PE::Empty)? & 0xf0) >> 4;
+  match vsn {
+    4 if header.len() >= 20 => {
+      let slice = &header[if DST { 16 } else { 12 }..][0..4];
+      Ipv4Addr::from(*<&[u8;4]>::try_from(slice).unwrap()).into()
+    },
+
+    6 if header.len() >= 40 => {
+      let slice = &header[if DST { 24 } else { 8 }..][0..16];
+      Ipv6Addr::from(*<&[u8;16]>::try_from(slice).unwrap()).into()
+    },
+
+    _ => throw!(PE::Truncated{ vsn, len: header.len() }),
+  }
+}
+
+#[derive(Copy,Clone,Eq,PartialEq,Ord,PartialOrd,Hash)]
+pub struct DumpHex<'b>(pub &'b [u8]);
+impl Debug for DumpHex<'_> {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) {
+    for v in self.0 { write!(f, "{:02x}", v)?; }
+    match str::from_utf8(self.0) {
+      Ok(s) => write!(f, "={:?}", s)?,
+      Err(x) => write!(f, "={:?}..",
+                       str::from_utf8(&self.0[0..x.valid_up_to()]).unwrap()
+      )?,
+    }
+  }
+}
+
+#[tokio::test]
+async fn mime_slip_to_mime() {
+  use PacketError as PE;
+  const MTU: u32 = 10;
+
+  async fn chk<M:SlipMime>(m: M, i: &[u8],
+                           exp_p: &[&[u8]],
+                           exp_e: &[PacketError],
+                           exp_r: Result<(),SlipFramesError<Void>>)
+  {
+    dbg!(M::CONV_TO, DumpHex(i));
+    let mut got_e = vec![];
+    let mut got_p = vec![];
+    let got_r = processn(
+      m, MTU, i,
+      |_|Ok(()),
+      |(p,())| { got_p.push(p); async { Ok(()) } },
+      |e| Ok::<_,SlipFramesError<Void>>(got_e.push(e))
+    ).await;
+    assert_eq!( got_p.iter().map(|b| DumpHex(b)).collect_vec(),
+                exp_p.iter().map(|b| DumpHex(b)).collect_vec() );
+    assert_eq!( got_e,
+                exp_e );
+    assert_eq!( got_r,
+                exp_r );
+  }
+  use SlipFramesError::ErrorOnlyBad;
+
+  chk(Slip2Mime,
+       &[ SLIP_END, SLIP_ESC, SLIP_ESC_END, b'-',     b'X' ],
+    &[           &[ b'-',     SLIP_ESC_END, SLIP_ESC, b'X' ] ],
+    &[ ],
+      Ok(())).await;
+
+  chk(Slip2Mime,
+       &[ SLIP_END, SLIP_ESC, b'y' ], &[],
+    &[ PE::SLIP ],
+      Err(ErrorOnlyBad)).await;
+
+  chk(Slip2Mime,
+       &[ SLIP_END, b'-',     b'y' ],
+    &[           &[ SLIP_ESC, b'y' ] ],
+    &[ ],
+      Ok(())).await;
+
+  chk(Slip2Mime,
+       &[b'x'; 20],
+    &[             ],
+    &[ PE::MTU { len: 20, mtu: MTU } ],
+      Err(ErrorOnlyBad)).await;
+
+  chk(SlipNoConv,
+       &[ SLIP_END, SLIP_ESC, SLIP_ESC_END, b'-',     b'X' ],
+    &[           &[ SLIP_ESC, SLIP_ESC_END, b'-',     b'X' ] ],
+    &[ ],
+      Ok(())).await;
+}
+
+
diff --git a/src/types.rs b/src/types.rs
new file mode 100644 (file)
index 0000000..1f55aa6
--- /dev/null
@@ -0,0 +1,76 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[derive(Debug,Copy,Clone)]
+pub enum LinkEnd { Server, Client }
+
+#[derive(Debug,Clone,Hash,Eq,PartialEq,Ord,PartialOrd)]
+pub struct ServerName(pub String);
+
+#[derive(Debug,Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd)]
+pub struct ClientName(pub IpAddr);
+
+#[derive(Debug,Clone,Hash,Eq,PartialEq,Ord,PartialOrd)]
+pub struct LinkName {
+  pub server: ServerName,
+  pub client: ClientName,
+}
+
+impl FromStr for ClientName {
+  type Err = AE;
+  #[throws(AE)]
+  fn from_str(s: &str) -> Self {
+    ClientName(
+      if let Ok(v4addr) = s.parse::<Ipv4Addr>() {
+        if s != v4addr.to_string() {
+          throw!(anyhow!("invalid client name (unusual IPv4 address syntax)"));
+        }
+        v4addr.into()
+      } else if let Ok(v6addr) = s.parse::<Ipv6Addr>() {
+        if s != v6addr.to_string() {
+          throw!(anyhow!("invalid client name (non-canonical IPv6 address)"));
+        }
+        v6addr.into()
+      } else {
+        throw!(anyhow!("invalid client name (IPv4 or IPv6 address)"))
+      }
+    )
+  }
+}
+
+impl FromStr for ServerName {
+  type Err = AE;
+  #[throws(AE)]
+  fn from_str(s: &str) -> Self {
+    if ! regex_is_match!(r"
+        ^ (?: SERVER
+            | [0-9a-z][-0-9a-z]* (:? \.
+              [0-9a-z][-0-9a-z]*        )*
+          ) $"x, s) {
+      throw!(anyhow!("bad syntax for server name"));
+    }
+    if ! regex_is_match!(r"[A-Za-z-]", s) {
+      throw!(anyhow!("bad syntax for server name \
+                      (too much like an IPv4 address)"));
+    }
+    ServerName(s.into())
+  }
+}
+
+impl Display for ServerName {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) { Display::fmt(&self.0, f)?; }
+}
+impl Display for ClientName {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) { Display::fmt(&self.0, f)?; }
+}
+impl Display for LinkName {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) {
+    write!(f, "[{} {}]", &self.server, &self.client)?;
+  }
+}
diff --git a/src/utils.rs b/src/utils.rs
new file mode 100644 (file)
index 0000000..22767aa
--- /dev/null
@@ -0,0 +1,231 @@
+// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[ext(pub)]
+impl<T> T where T: Debug {
+  fn to_debug(&self) -> String { format!("{:?}", self) }
+}
+
+#[ext(pub)]
+impl<T,E> Result<T,E> where AE: From<E> {
+  fn dcontext<D:Debug>(self, d: D) -> anyhow::Result<T> {
+    self.map_err(|e| AE::from(e)).with_context(|| d.to_debug())
+  }
+}
+
+#[derive(Error,Debug)]
+pub enum ReadLimitedError {
+  #[error("maximum size {limit} exceeded")]
+  Truncated { sofar: Box<[u8]>, limit: usize },
+
+  #[error("HTTP error {0}")]
+  Hyper(#[from] hyper::Error),
+}
+
+impl ReadLimitedError {
+  pub fn discard_data(&mut self) { match self {
+    ReadLimitedError::Truncated { sofar,.. } => { mem::take(sofar); },
+    _ => { },
+  } }
+}
+#[ext(pub)]
+impl<T> Result<T,ReadLimitedError> {
+  fn discard_data(self) -> Self {
+    self.map_err(|mut e| { e.discard_data(); e })
+  }
+}
+
+// Works around the lack of ErrorKind::IsADirectory
+// #![feature(io_error_more)]
+// https://github.com/rust-lang/rust/issues/86442
+#[ext(pub)]
+impl io::Error {
+  fn is_is_a_directory(&self) -> bool {
+    self.raw_os_error()
+      .unwrap_or_else(|| panic!(
+ "trying to tell whether Kind is IsADirectory for non-OS error io::Error {}",
+        self))
+      == libc::EISDIR
+  }
+}
+
+#[throws(ReadLimitedError)]
+pub async fn read_limited_bytes<S>(limit: usize, initial: Box<[u8]>,
+                                   capacity: usize,
+                                   stream: &mut S) -> Box<[u8]>
+where S: futures::Stream<Item=Result<hyper::body::Bytes,hyper::Error>>
+         + Debug + Unpin,
+      // we also require that the Stream is cancellation-safe
+{
+  let mut accum = initial.into_vec();
+  let capacity = min(limit, capacity);
+  if capacity > accum.len() { accum.reserve(capacity - accum.len()); }
+  while let Some(item) = stream.next().await {
+    let b = item?;
+    accum.extend(b);
+    if accum.len() > limit {
+      throw!(ReadLimitedError::Truncated { limit, sofar: accum.into() })
+    }
+  }
+  accum.into()
+}
+
+pub fn time_t_now() -> u64 {
+  SystemTime::now()
+    .duration_since(UNIX_EPOCH)
+    .unwrap_or_else(|_| Duration::default()) // clock is being weird
+    .as_secs()
+}
+
+use sha2::Digest as _;
+
+pub type HmacH = sha2::Sha256;
+pub const HMAC_B: usize = 64;
+pub const HMAC_L: usize = 32;
+
+pub fn token_hmac(key: &[u8], message: &[u8]) -> [u8; HMAC_L] {
+  let key = {
+    let mut padded = [0; HMAC_B];
+    if key.len() > padded.len() {
+      let digest: [u8; HMAC_L] = HmacH::digest(key).into();
+      padded[0..HMAC_L].copy_from_slice(&digest);
+    } else {
+      padded[0.. key.len()].copy_from_slice(key);
+    }
+    padded
+  };
+  let mut ikey = key;  for k in &mut ikey { *k ^= 0x36; }
+  let mut okey = key;  for k in &mut okey { *k ^= 0x5C; }
+
+  //dbg!(DumpHex(&key), DumpHex(message), DumpHex(&ikey), DumpHex(&okey));
+
+  let h1 = HmacH::new()
+    .chain(&ikey)
+    .chain(message)
+    .finalize();
+  let h2 = HmacH::new()
+    .chain(&okey)
+    .chain(h1)
+    .finalize();
+  h2.into()
+}
+
+#[test]
+fn hmac_test_vectors(){
+  // C&P from RFC 4231
+  let vectors = r#"
+   Key =          0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b
+                  0b0b0b0b                          (20 bytes)
+   Data =         4869205468657265                  ("Hi There")
+
+   HMAC-SHA-256 = b0344c61d8db38535ca8afceaf0bf12b
+                  881dc200c9833da726e9376c2e32cff7
+
+    
+   Key =          4a656665                          ("Jefe")
+   Data =         7768617420646f2079612077616e7420  ("what do ya want ")
+                  666f72206e6f7468696e673f          ("for nothing?")
+
+   HMAC-SHA-256 = 5bdcc146bf60754e6a042426089575c7
+                  5a003f089d2739839dec58b964ec3843
+
+
+   Key =          aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaa                          (20 bytes)
+   Data =         dddddddddddddddddddddddddddddddd
+                  dddddddddddddddddddddddddddddddd
+                  dddddddddddddddddddddddddddddddd
+                  dddd                              (50 bytes)
+
+   HMAC-SHA-256 = 773ea91e36800e46854db8ebd09181a7
+                  2959098b3ef8c122d9635514ced565fe
+
+
+   Key =          0102030405060708090a0b0c0d0e0f10
+                  111213141516171819                (25 bytes)
+   Data =         cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd
+                  cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd
+                  cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd
+                  cdcd                              (50 bytes)
+
+   HMAC-SHA-256 = 82558a389a443c0ea4cc819899f2083a
+                  85f0faa3e578f8077a2e3ff46729665b
+
+
+
+   Key =          aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaa                            (131 bytes)
+   Data =         54657374205573696e67204c61726765  ("Test Using Large")
+                  72205468616e20426c6f636b2d53697a  ("r Than Block-Siz")
+                  65204b6579202d2048617368204b6579  ("e Key - Hash Key")
+                  204669727374                      (" First")
+
+   HMAC-SHA-256 = 60e431591ee0b67f0d8a26aacbf5b77f
+                  8e0bc6213728c5140546040f0ee37f54
+
+   Key =          aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaa                            (131 bytes)
+   Data =         54686973206973206120746573742075  ("This is a test u")
+                  73696e672061206c6172676572207468  ("sing a larger th")
+                  616e20626c6f636b2d73697a65206b65  ("an block-size ke")
+                  7920616e642061206c61726765722074  ("y and a larger t")
+                  68616e20626c6f636b2d73697a652064  ("han block-size d")
+                  6174612e20546865206b6579206e6565  ("ata. The key nee")
+                  647320746f2062652068617368656420  ("ds to be hashed ")
+                  6265666f7265206265696e6720757365  ("before being use")
+                  642062792074686520484d414320616c  ("d by the HMAC al")
+                  676f726974686d2e                  ("gorithm.")
+
+   HMAC-SHA-256 = 9b09ffa71b942fcb27635fbcd5b0e944
+                  bfdc63644f0713938a7f51535c3a35e2
+"#;
+  let vectors = regex_replace_all!{
+    r#"\(.*\)"#,
+    vectors.trim_end(),
+    |_| "",
+  };
+  let vectors = regex_replace_all!{
+    r#" *\n                  "#,
+    &vectors,
+    |_| "",
+  };
+  let vectors = regex_replace_all!{
+    r#"\s*\n"#,
+    &vectors,
+    |_| "\n",
+  };
+  let mut lines = vectors.split('\n');
+  assert_eq!( lines.next().unwrap(), "" );
+  let mut get = |prefix| {
+    let l = lines.next()?;
+    dbg!(l);
+    let b = l.strip_prefix(prefix).unwrap().as_bytes().chunks(2)
+      .map(|s| str::from_utf8(s).unwrap())
+      .map(|s| { assert_eq!(s.len(), 2); u8::from_str_radix(s,16).unwrap() })
+      .collect::<Vec<u8>>();
+    Some(b)
+  };
+  while let Some(key) = get("   Key =          ") {
+    let data = get("   Data =         ").unwrap();
+    let exp = get("   HMAC-SHA-256 = ").unwrap();
+    let got = token_hmac(&key, &data);
+    assert_eq!(&got[..], &exp);
+  }
+}
diff --git a/test/capture-log b/test/capture-log
new file mode 100755 (executable)
index 0000000..3b4c841
--- /dev/null
@@ -0,0 +1,9 @@
+#!/bin/bash
+# Copyright 2020-2022 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+set -e
+
+log="$1"; shift
+mkdir -p tmp
+"$@" 2>&1 | ts >"$log"
diff --git a/test/common b/test/common
new file mode 100644 (file)
index 0000000..4bbd1fd
--- /dev/null
@@ -0,0 +1,80 @@
+# -*- shell-script -*-
+# Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+set -x
+
+ssrc="${0%/*}"
+src="${ssrc%/*}"
+test="${ssrc%/*}/test"
+
+fail () { echo >&2 "$0: fail: $*"; exit 1; }
+
+test-prep () {
+
+    case "${0##*/}" in
+    t-*) tname="${0##*/t-}" ;;
+    *) fail "bad test script name $0" ;;
+    esac
+
+    tmp=tmp/$tname
+    rm -rf "$tmp"
+    mkdir -p $tmp
+
+    $test/netns-setup "$tname"
+
+    trap '
+       rc=$?
+       shutdown
+       if [ $rc = 0 ]; then echo "OK $tname"; fi
+    ' 0
+}
+
+kill-pids () {
+    for p in $pids; do kill -9 $p; done
+}
+
+shutdown () {
+    kill-pids
+}
+
+in-ns () {
+    local client_server=$1; shift
+    $exec ip netns exec hippotat-t-$tname-$client_server "$@"
+}
+
+run-client () {
+    in-ns client \
+    target/debug/hippotat --config $test/test.cfg -DD "$@"
+}
+run-server () {
+    in-ns server \
+    target/debug/hippotatd --config $test/test.cfg -DD "$@"
+}
+spawn () {
+    { exec=exec; "$@"; } &
+    pids+=" $!"
+}
+
+in-ns-await-up () {
+    local sc="$1"; shift
+    local addr="$1"; shift
+    local t=1
+    while sleep $(( $t / 10 )).$(( $t % 10 )); do
+       if in-ns $sc ip -o addr show | fgrep " inet $addr "; then
+           return
+       fi
+       t=$(( $t + 1 ))
+       if [ $t -gt 10 ]; then fail "$sc did not come up $addr"; fi
+    done
+}
+
+start-server () {
+    spawn run-server
+    in-ns-await-up server 192.0.2.1
+}
+start-client () {
+    spawn run-client
+    in-ns-await-up client 192.0.2.3
+}
diff --git a/test/go-with-unshare b/test/go-with-unshare
new file mode 100755 (executable)
index 0000000..96f983f
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/bash
+# Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+set -e
+. "${0%/*}"/common
+
+case "$1" in
+*/*) ;;
+?*) tname="$1"; shift; set -- "$test/$tname" "$@" ;;
+'') fail 'bad usage: need program or test name' ;;
+esac
+
+$src/test/with-unshare "$@"
diff --git a/test/netns-setup b/test/netns-setup
new file mode 100755 (executable)
index 0000000..d91e0f9
--- /dev/null
@@ -0,0 +1,36 @@
+#!/bin/bash
+# Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+set -ex
+
+slug=$1
+
+c_ns=hippotat-t-$slug-client
+s_ns=hippotat-t-$slug-server
+
+ip netns delete $s_ns      2>/dev/null ||:
+ip netns delete $c_ns      2>/dev/null ||:
+
+ip netns add $c_ns
+ip netns add $s_ns
+
+ip link add t.s.$$ type veth peer name t.c.$$
+move_to_netns () {
+    cs=$1; ns=$2
+    ip link set t.$cs.$$ netns $ns
+    ip netns exec $ns ip link set t.$cs.$$ name eth0
+}
+move_to_netns s $s_ns
+move_to_netns c $c_ns
+
+config_netns () {
+    ns=$1; num=$2;
+    ip netns exec $ns ip addr add dev lo   127.0.0.1
+    ip netns exec $ns ip addr add dev eth0 198.51.100.$num/24
+    ip netns exec $ns ip link set     lo   up
+    ip netns exec $ns ip link set     eth0 up
+}
+config_netns $s_ns 1
+config_netns $c_ns 2
diff --git a/test/t-basic b/test/t-basic
new file mode 100755 (executable)
index 0000000..046e17e
--- /dev/null
@@ -0,0 +1,14 @@
+#!/bin/bash
+# Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+set -e
+. "${0%/*}"/common
+test-prep
+
+start-server
+start-client
+
+in-ns client ping -i 0.1 -c 10 192.0.2.1 >$tmp/ping
+grep ' 0% packet loss' $tmp/ping
diff --git a/test/test.cfg b/test/test.cfg
new file mode 100644 (file)
index 0000000..d74e8d3
--- /dev/null
@@ -0,0 +1,28 @@
+# Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+[SERVER]
+
+ipif = exec /usr/lib/userv/ipif \* -- %(local)s,%(peer)s,%(mtu)s,slip '%(rnets)s'
+
+addrs = 198.51.100.1
+port = 8099
+vnetwork = 192.0.2.0/24
+
+# ./hippotatd --debug-select=+ -c test.cfg
+
+# nc -n -v -l -p 8100 -c 'dd of=/dev/null'
+
+[192.0.2.3]
+secret = sesame
+
+[192.0.2.3]
+ipif = exec /usr/lib/userv/ipif \* -- %(local)s,%(peer)s,%(mtu)s,slip '%(rnets)s'
+
+# ./hippotat -D -c test.cfg
+
+[192.0.2.4]
+#secret = zorkmids
+
+# dd if=/dev/urandom bs=1024 count=16384 | nc -q 0 -n -v 192.0.2.1 8100
diff --git a/test/with-unshare b/test/with-unshare
new file mode 100755 (executable)
index 0000000..5d29c87
--- /dev/null
@@ -0,0 +1,18 @@
+#!/bin/bash
+# Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+set -e
+. "${0%/*}"/common
+
+
+#case "$1" in
+#T/*)  prog="$src/test/${1#T/}"; shift; set -- "$prog" "$@" ;;
+#esac
+
+unshare -Urnm bash -xec '
+       mount -t tmpfs tmpfs /run
+       PATH="$PATH:/usr/local/sbin:/sbin:/usr/sbin"
+       exec "$@"
+' x "$@"
diff --git a/uml/psusan-uml-inside b/uml/psusan-uml-inside
new file mode 100755 (executable)
index 0000000..a0a7ca8
--- /dev/null
@@ -0,0 +1,39 @@
+#!/bin/bash
+# Copyright 2021-2022 Ian Jackson, Simon Tatham, and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+set -ex
+
+mkdir /dev/pts
+mount -t proc none /proc
+mount -t sysfs none /sys
+mount -t devpts none /dev/pts
+mount -t tmpfs none /tmp
+mount -t tmpfs none /run
+
+mount --bind /usr/lib/uml/modules/ /lib/modules/   
+modprobe tun
+
+exec 0<>/dev/tty1 1>&0
+stty raw -echo
+
+# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=991959
+PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
+export PATH
+export SHELL=/bin/bash
+
+HOME=$(cat /proc/cmdline)
+case "$HOME" in
+*' psusan-uml-tmp='*) ;;
+*) echo >&2 'psusan-uml-tmp not found in /proc/cmdline'; exit 1;;
+esac
+HOME=${HOME##* psusan-uml-tmp=}
+HOME=${HOME%% *}
+export HOME
+cd "$HOME"
+
+dd if=random-seed of=/dev/urandom
+src/uml/rndaddtoentcnt/rndaddtoentcnt 4096 >&2
+
+exec psusan
diff --git a/uml/psusan-uml-psusan b/uml/psusan-uml-psusan
new file mode 100755 (executable)
index 0000000..5296aa5
--- /dev/null
@@ -0,0 +1,25 @@
+#!/bin/bash
+# Copyright 2021-2022 Ian Jackson, Simon Tatham, and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+set -e
+
+fifo=tmp/uml/q
+mkfifo -m600 $fifo
+
+(
+ # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=991958
+ : <$fifo
+ cat
+)                                                              |       \
+ bwrap --dev-bind / / --tmpfs /dev/shm                                 \
+ linux mem=512M rootfstype=hostfs rootflags=/ rw                       \
+       con=fd:2,fd:2 con1=fd:0,fd:1 init="${0%/*}"/psusan-uml-inside   \
+       -- psusan-uml-tmp=$PWD/tmp/uml                          |       \
+(
+ read banner
+ : >$fifo
+ printf '%s\n' "$banner"
+ cat
+)
diff --git a/uml/psusan-uml-run b/uml/psusan-uml-run
new file mode 100755 (executable)
index 0000000..36a4b53
--- /dev/null
@@ -0,0 +1,10 @@
+#!/bin/sh
+# Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+set -e
+
+HOME=$PWD/tmp/uml
+
+plink -ssh-connection -share $PWD "$@"
diff --git a/uml/psusan-uml-setup b/uml/psusan-uml-setup
new file mode 100755 (executable)
index 0000000..5f4afe7
--- /dev/null
@@ -0,0 +1,18 @@
+#!/bin/bash
+# Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+set -e
+
+mkdir -p tmp
+rm -rf tmp/uml
+mkdir -p -m2700 tmp/uml
+
+ln -s "$PWD" tmp/uml/org-pwd
+ln -s org-pwd/target tmp/uml/target
+ln -s "${0%/*/*}" tmp/uml/src
+
+dd if=/dev/urandom of=tmp/uml/random-seed bs=1k count=4
+
+"${0%/*}"/psusan-uml-run -proxycmd "${0%/*}"/psusan-uml-psusan -N -v -v
diff --git a/uml/rndaddtoentcnt/.gitignore b/uml/rndaddtoentcnt/.gitignore
new file mode 100644 (file)
index 0000000..a997b67
--- /dev/null
@@ -0,0 +1 @@
+rndaddtoentcnt
diff --git a/uml/rndaddtoentcnt/LICENSE b/uml/rndaddtoentcnt/LICENSE
new file mode 100644 (file)
index 0000000..4479b29
--- /dev/null
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Jumpnow Technologies, LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/uml/rndaddtoentcnt/Makefile b/uml/rndaddtoentcnt/Makefile
new file mode 100644 (file)
index 0000000..7b3c881
--- /dev/null
@@ -0,0 +1,6 @@
+rndaddtoentcnt: rndaddtoentcnt.c
+       $(CC) rndaddtoentcnt.c -o rndaddtoentcnt
+
+.PHONY: clean
+clean:
+       rm -f *.o rndaddtoentcnt
diff --git a/uml/rndaddtoentcnt/README.md b/uml/rndaddtoentcnt/README.md
new file mode 100644 (file)
index 0000000..9f85b29
--- /dev/null
@@ -0,0 +1,15 @@
+### rndaddtoentcnt
+
+Seeding the random number generator by writing to /dev/urandom does not update the entropy count.
+
+This utility makes the RNDADDTOENTCNT ioctl call needed to do this.
+
+Used in startup scripts after initializing /dev/urandom with a presaved seed.
+
+Example:
+
+    dd if=/path/to/some/random-seed-file of=/dev/urandom bs=512 count=1
+
+    /path/to/rdnaddtoentcnt <entropy-bit-count>
+
+where entropy-bit-count is a number between 1 and (8 * 512) depending on how much you trust the seed file.
diff --git a/uml/rndaddtoentcnt/rndaddtoentcnt.c b/uml/rndaddtoentcnt/rndaddtoentcnt.c
new file mode 100644 (file)
index 0000000..929bf4d
--- /dev/null
@@ -0,0 +1,46 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/ioctl.h>
+#include <fcntl.h>
+
+#include <linux/random.h>
+
+
+int main(int argc, char **argv)
+{
+    int count, fd;
+
+    if (argc != 2) {
+        printf("Usage: rndaddtoentcnt <entropy-bit-count>\n");
+        exit(1);
+    }
+
+    count = strtoul(argv[1], NULL, 0);
+
+    if (count < 1 || count > 4096) {
+        printf("Count range is 1 to 4096\n");
+        exit(1);
+    }
+
+    fd = open("/dev/urandom", O_WRONLY);
+
+    if (fd < 0) {
+        perror("open(/dev/urandom)");
+        exit(1);
+    }
+
+
+    if (ioctl(fd, RNDADDTOENTCNT, &count) < 0) {
+        perror("ioctl(RNDADDTOENTCNT)");
+        close(fd);
+        exit(1);
+    }
+
+    close(fd);
+
+    return 0;
+}
diff --git a/uml/run-test b/uml/run-test
new file mode 100755 (executable)
index 0000000..07932d4
--- /dev/null
@@ -0,0 +1,22 @@
+#!/bin/sh
+# Copyright 2021-2022 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+# *** This does not work. ***
+# UML is too horribly flaky.  Use test/ instead, which just uses unshare!
+
+set -e
+set -x
+
+uml="${0%/*}"/psusan-uml
+
+if timeout --foreground 5 $uml-run true; then :
+else 
+       $uml-setup 2>&1 |ts >tmp/uml-setup &
+       sleep 5
+       timeout --foreground 5 $uml-run true ||:
+       timeout --foreground 5 $uml-run true
+fi
+
+echo hi