--- /dev/null
+((rust-mode . ((rust-indent-offset . 2))))
-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
--- /dev/null
+# 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"
--- /dev/null
+# 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
--- /dev/null
+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.
+
--- /dev/null
+# 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
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)
<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:
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.
--- /dev/null
+Introduction
+============
--- /dev/null
+// 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);
+}
--- /dev/null
+.debhelper
+debhelper-*-stamp
+files
+tmp
+hippotat-client
+hippotat-client.substvars
+hippotat-server
+hippotat-server.substvars
+hippotat-doc
+hippotat-doc.substvars
-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
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
--- /dev/null
+usr/bin/hippotat
--- /dev/null
+usr/share/doc/hippotat
--- /dev/null
+usr/sbin/hippotatd
--- /dev/null
+#!/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
#!/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 $@
--- /dev/null
+../README.md
\ No newline at end of file
--- /dev/null
+# -*- 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']
--- /dev/null
+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.)
--- /dev/null
+Hippotat - Asinine IP over HTTP
+===============================
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ README
+ config.rst
+ settings.rst
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`search`
--- /dev/null
+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).
--- /dev/null
+# 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
--- /dev/null
+// 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()
+}
--- /dev/null
+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/
--- /dev/null
+ 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>.
--- /dev/null
+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
--- /dev/null
+hippotat (0.1~UNRELEASED) unstable; urgency=medium
+
+ *
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sat, 08 Apr 2017 17:57:42 +0100
+
--- /dev/null
+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.
--- /dev/null
+#!/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
--- /dev/null
+// 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");
+ }
+ }
+ }
+}
--- /dev/null
+// 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);
+}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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
+ } } =>
+ {
+ }
+ }
+ }
+}
--- /dev/null
+// 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))
+ })
+}
--- /dev/null
+// 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 = §ion.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!(§ion, 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);
+ })
+}
--- /dev/null
+// 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())?
+ }
+}
--- /dev/null
+// 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")?
+ }
+}
--- /dev/null
+// 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;
--- /dev/null
+// 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> { }
--- /dev/null
+// 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() }
--- /dev/null
+// 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))
+ }
+}
--- /dev/null
+// 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());
+ }
+}
--- /dev/null
+// 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();
+ }
+ }
+}
--- /dev/null
+// 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;
+}
+
+
--- /dev/null
+// 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)?;
+ }
+}
--- /dev/null
+// 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);
+ }
+}
--- /dev/null
+#!/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"
--- /dev/null
+# -*- 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
+}
--- /dev/null
+#!/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 "$@"
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+# 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
--- /dev/null
+#!/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 "$@"
--- /dev/null
+#!/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
--- /dev/null
+#!/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
+)
--- /dev/null
+#!/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 "$@"
--- /dev/null
+#!/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
--- /dev/null
+rndaddtoentcnt
--- /dev/null
+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.
--- /dev/null
+rndaddtoentcnt: rndaddtoentcnt.c
+ $(CC) rndaddtoentcnt.c -o rndaddtoentcnt
+
+.PHONY: clean
+clean:
+ rm -f *.o rndaddtoentcnt
--- /dev/null
+### 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.
--- /dev/null
+#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;
+}
--- /dev/null
+#!/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