chiark / gitweb /
Add 'uml/rndaddtoentcnt/' from commit '3cef9b224336ac4147aade20738420193e525fc5'
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Sat, 7 Aug 2021 11:14:07 +0000 (12:14 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Sat, 7 Aug 2021 11:14:07 +0000 (12:14 +0100)
git-subtree-dir: uml/rndaddtoentcnt
git-subtree-mainline: 488191f4770edfd9990ab847b061e4227f79a091
git-subtree-split: 3cef9b224336ac4147aade20738420193e525fc5

37 files changed:
.dir-locals.el [new file with mode: 0644]
.gitignore
Cargo.lock [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
DEVELOPER-CERTIFICATE [new file with mode: 0644]
GPL-3 [new file with mode: 0644]
Makefile
PROTOCOL [new file with mode: 0644]
README.md
docs/README.md [new symlink]
docs/conf.py [new file with mode: 0644]
docs/config.rst [new file with mode: 0644]
docs/index.rst [new file with mode: 0644]
docs/settings.rst [new file with mode: 0644]
macros/Cargo.toml [new file with mode: 0644]
macros/macros.rs [new file with mode: 0644]
src/bin/client.rs [new file with mode: 0644]
src/bin/server.rs [new file with mode: 0644]
src/config.rs [new file with mode: 0644]
src/lib.rs [new file with mode: 0644]
src/prelude.rs [new file with mode: 0644]
src/queue.rs [new file with mode: 0644]
src/reporter.rs [new file with mode: 0644]
src/rope.rs [new file with mode: 0644]
src/slip.rs [new file with mode: 0644]
src/types.rs [new file with mode: 0644]
src/utils.rs [new file with mode: 0644]
test.cfg [new file with mode: 0644]
uml/psusan-uml-inside [new file with mode: 0755]
uml/psusan-uml-psusan [new file with mode: 0755]
uml/psusan-uml-run [new file with mode: 0755]
uml/psusan-uml-setup [new file with mode: 0755]
uml/rndaddtoentcnt/.gitignore [new file with mode: 0644]
uml/rndaddtoentcnt/LICENSE [moved from LICENSE with 100% similarity]
uml/rndaddtoentcnt/Makefile [new file with mode: 0644]
uml/rndaddtoentcnt/README.md [new file with mode: 0644]
uml/rndaddtoentcnt/rndaddtoentcnt.c [moved from rndaddtoentcnt.c with 100% similarity]

diff --git a/.dir-locals.el b/.dir-locals.el
new file mode 100644 (file)
index 0000000..7210430
--- /dev/null
@@ -0,0 +1 @@
+((rust-mode . ((rust-indent-offset . 2))))
index a997b674191d4d9f766168792520ee48a6328e18..812defb989256412792ab0c70f8fe1dceb112a60 100644 (file)
@@ -1 +1,4 @@
-rndaddtoentcnt
+/target
+/docs/html
+/docs/doctrees
+/stamp
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644 (file)
index 0000000..ef78702
--- /dev/null
@@ -0,0 +1,1228 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[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 = "anyhow"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
+
+[[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.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[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 = "configparser"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7201ee416d124d589a820111ba755930df8b75855321a9a1b87312a0597ec8f"
+
+[[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 = "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 = "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 = "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 = "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.0"
+dependencies = [
+ "anyhow",
+ "base64",
+ "cervine",
+ "configparser",
+ "env_logger",
+ "extend",
+ "fehler",
+ "futures",
+ "hippotat-macros",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "itertools",
+ "lazy-regex",
+ "log",
+ "parking_lot",
+ "regex",
+ "sha2",
+ "structopt",
+ "thiserror",
+ "tokio",
+ "void",
+]
+
+[[package]]
+name = "hippotat-macros"
+version = "0.0.0"
+dependencies = [
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "http"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[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",
+ "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 = "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 = "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.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
+
+[[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 = "memchr"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
+
+[[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 = "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 = "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 = "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",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[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 = "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 = "tokio"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2602b8af3767c285202012822834005f596c811042315fa7e9f5b12b2a43207"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "once_cell",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "tokio-macros",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
+
+[[package]]
+name = "tracing"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "typenum"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "version_check"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+
+[[package]]
+name = "void"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..1212275
--- /dev/null
@@ -0,0 +1,52 @@
+# Copyright 2021 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+[package]
+name = "hippotat"
+version = "0.0.0"
+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="src/bin/client.rs"
+
+[[bin]]
+name="hippotatd"
+path="src/bin/server.rs"
+
+[dependencies]
+
+hippotat-macros = { path = "macros" }
+
+# versions specified here are mostly just guesses at what is needed
+# (or currently available):
+anyhow = "1"
+base64 = "0.13"
+configparser = "2"
+env_logger = "0.9"
+futures = "0.3"
+hyper = { version = "0.14", features = ["full"] }
+hyper-tls = "0.5"
+ipnet = "2"
+itertools = "0.10"
+parking_lot = "0.11"
+regex = "1.5"
+log = "0.4"
+sha2 = "0.9"
+structopt = "0.3"
+tokio = { version = "1", features = ["full"] }
+thiserror = "1"
+void = "1"
+
+# Not in sid:
+extend = "1"           # no deps not in sid
+fehler = "1"           # no deps (other than fehler-macros, obvs)
+lazy-regex = "2"       # no deps not in sid
+cervine = "0.0"                # no (non-dev)-deps not in sid
diff --git a/DEVELOPER-CERTIFICATE b/DEVELOPER-CERTIFICATE
new file mode 100644 (file)
index 0000000..912d22e
--- /dev/null
@@ -0,0 +1,38 @@
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+1 Letterman Drive
+Suite D4700
+San Francisco, CA, 94129
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+    have the right to submit it under the open source license
+    indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+    of my knowledge, is covered under an appropriate open source
+    license and I have the right under that license to submit that
+    work with modifications, whether created in whole or in part
+    by me, under the same open source license (unless I am
+    permitted to submit under a different license), as indicated
+    in the file; or
+
+(c) The contribution was provided directly to me by some other
+    person who certified (a), (b) or (c) and I have not modified
+    it.
+
+(d) I understand and agree that this project and the contribution
+    are public and that a record of the contribution (including all
+    personal information I submit with it, including my sign-off) is
+    maintained indefinitely and may be redistributed consistent with
+    this project or the open source license(s) involved.
+
diff --git a/GPL-3 b/GPL-3
new file mode 100644 (file)
index 0000000..94a9ed0
--- /dev/null
+++ b/GPL-3
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
index 7b3c88129c94604e430e3e44c33db2d2451e0c0d..08b4dbfb200bfaaf8ea3687e85f275c6b077e05e 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,49 @@
-rndaddtoentcnt: rndaddtoentcnt.c
-       $(CC) rndaddtoentcnt.c -o rndaddtoentcnt
+# Copyright 2020-2021 Ian Jackson and contributors to Otter
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+SHELL=/bin/bash
+
+default: all
+
+CARGO          ?= cargo
+TARGET_DIR     ?= target
+
+SPHINXBUILD    ?= sphinx-build
+
+ifneq (,$(wildcard ../Cargo.nail))
+
+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
+
+endif # Cargo.nail
+
+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 $@
+
+all:   cargo-build doc
+
+cargo-build: stamp/cargo-build
+
+stamp/cargo-build: $(call rsrcs,.)
+       $(NAILING_CARGO) build $(CARGO_BUILD_OPTIONS)
+       $(stamp)
+
+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)
+       $(SPHINXBUILD) -M html docs docs $(SPHINXOPTS)
 
-.PHONY: clean
 clean:
-       rm -f *.o rndaddtoentcnt
+       rm -rf stamp/* doc/html
+       $(NAILING_CARGO) clean
+
+.PHONY: cargo-build all doc clean
diff --git a/PROTOCOL b/PROTOCOL
new file mode 100644 (file)
index 0000000..b39253b
--- /dev/null
+++ b/PROTOCOL
@@ -0,0 +1,45 @@
+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
+                       max_batch_down
+ 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>)
+and the hash function is SHA256
+
+
+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
+
+difficult because client-generated nonces would have to never go
+backwaards which basically means never-rewinding state on the client.
index 9f85b297483d70e2c731fabb87443ac295b0539c..c516b33174ea355933b2c99e3400281e32bb3609 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,15 +1,2 @@
-### 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.
+Introduction
+============
diff --git a/docs/README.md b/docs/README.md
new file mode 120000 (symlink)
index 0000000..32d46ee
--- /dev/null
@@ -0,0 +1 @@
+../README.md
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644 (file)
index 0000000..f37c66f
--- /dev/null
@@ -0,0 +1,187 @@
+# -*- coding: utf-8 -*-
+#
+# Configuration file for the Sphinx documentation builder.
+#
+# This file does only contain a selection of the most common options. For a
+# full list see the documentation:
+# http://www.sphinx-doc.org/en/master/config
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'Hippotat'
+copyright = '2021 Ian Jackson and the contributors to Hippotat'
+author = 'Ian Jackson et al'
+
+# The short X.Y version
+version = ''
+# The full version, including alpha/beta/rc tags
+release = ''
+
+
+# -- General configuration ---------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    'recommonmark',
+    'sphinx.ext.autosectionlabel',
+]
+autosectionlabel_prefix_document = True
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+#source_suffix = ['.rst', '.md']
+#source_suffix = '.rst'
+
+# https://github.com/readthedocs/recommonmark (retrieved 8.4.2021)
+from recommonmark.parser import CommonMarkParser
+source_parsers = {
+    '.md': CommonMarkParser,
+}
+source_suffix = {
+   '.rst': 'restructuredtext',
+   '.txt': 'markdown',
+   '.md': 'markdown',
+}
+
+# The master toctree document.
+master_doc = 'index'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = None
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'classic'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# The default sidebars (for documents that don't match any pattern) are
+# defined by theme itself.  Builtin themes are using these templates by
+# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
+# 'searchbox.html']``.
+#
+# html_sidebars = {}
+
+
+# -- Options for HTMLHelp output ---------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Hippotatdoc'
+
+
+# -- Options for LaTeX output ------------------------------------------------
+
+latex_elements = {
+    # The paper size ('letterpaper' or 'a4paper').
+    #
+    # 'papersize': 'letterpaper',
+
+    # The font size ('10pt', '11pt' or '12pt').
+    #
+    # 'pointsize': '10pt',
+
+    # Additional stuff for the LaTeX preamble.
+    #
+    # 'preamble': '',
+
+    # Latex figure (float) alignment
+    #
+    # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+latex_documents = [
+    (master_doc, 'Hippotat.tex', 'Hippotat Documentation',
+     'Ian Jackson and the contributors to Hippotat', 'manual'),
+]
+
+
+# -- Options for manual page output ------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    (master_doc, 'otter', 'Hippotat Documentation',
+     [author], 1)
+]
+
+
+# -- Options for Texinfo output ----------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+    (master_doc, 'Hippotat', 'Hippotat Documentation',
+     author, 'Hippotat', 'Online Table Top Environment Renderer',
+     'Games'),
+]
+
+
+# -- Options for Epub output -------------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = project
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#
+# epub_identifier = ''
+
+# A unique identification for the text.
+#
+# epub_uid = ''
+
+# A list of files that should not be packed into the epub file.
+epub_exclude_files = ['search.html']
diff --git a/docs/config.rst b/docs/config.rst
new file mode 100644 (file)
index 0000000..08d5d77
--- /dev/null
@@ -0,0 +1,64 @@
+Configuration scheme
+====================
+
+Configuration is in an INI-like format.  Sections start with lines
+``[...]``.  Every setting must be in a section.  ``#`` and ``;``
+comment lines are supported.  Settings ``nmae = value``.  Whitespace
+around the name and value is ignored.
+
+The configuration files are resolved to a configuration for each
+pairwise link between a client and a server.
+
+Sections
+--------
+
+The same config key may appear in multiple sections; for example, in a
+section specific to a link, as well as one for all links to a server.
+
+Unless otherwise specified, any particular config setting for a
+particular link is the value from the first of the following
+applicable sections, or failing that the built-in default:
+
+  * ``[<servername> <client>]``
+  * ``[<client>]``
+  * ``[<servername>]`` (often ``[SERVER]``)
+  * ``[COMMON]``
+
+``<client>`` is the client's virtual address in IPv4 or IPv6 literal
+syntax (without any surrounding ``[..]``.
+
+``<servername>`` must be in the syntrax of a valid lowercase DNS
+hostname (and not look like an address), or be literally ``SERVER``.
+
+There are also these special sections:
+
+ * ``[<servername> LIMIT]``
+ * ``[LIMIT]``
+
+Files
+-----
+
+Both client and server read the files
+
+ * ``/etc/hippotat/main.cfg`` (if it exists)
+ * ``/etc/hippotat/config.d/*``
+ * ``/etc/hippotat/secrets.d/*``
+
+Here ``*`` means all contained files whose names consists of only
+ascii alphanumerics plus ``-`` and ``_``.
+
+The ``--config`` option can be used to override the directory (usually
+``/etc/hippotat``).  Additonally each ``--extra-config`` option names
+an existing file or directory to be processed analagously.
+
+The ini file format sections from these files are all unioned.  Later
+files (in the list above, or alphabetically later) can override
+settings from earlier ones.
+
+Note that although it is conventional for information for a particular
+server or client to be in a file named after that endpoint, there is
+no semantic link: all the files are always read and the appropriate
+section from each is applied to every link.
+
+(If ``main.cfg`` does not exist, ``master.cfg`` will be tried for
+backward compatibility reasons.)
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644 (file)
index 0000000..31973fa
--- /dev/null
@@ -0,0 +1,16 @@
+Hippotat - Asinine IP over HTTP
+===============================
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+
+   README
+   config.rst
+   settings.rst
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`search`
diff --git a/docs/settings.rst b/docs/settings.rst
new file mode 100644 (file)
index 0000000..e4cef04
--- /dev/null
@@ -0,0 +1,196 @@
+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 of 10000 bytes.
+  Server uses minimum of client and server value (old servers
+  just uses server's value).
+
+  [``65536`` (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
+----------------------------------------------------
+
+These are not looked up 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 - *this is not checked*.
+  [``1500`` (bytes)]
+
+``ifname_server``
+  | Virtual interface name on the server.  [``shippo%d``]
+  | Any ``%d`` is interpolated (by the kernel).
+
+``ifname_client``
+  | Virtual interface name on the client.  [``hippo%d``]
+  | Any ``%d`` is interpolated (by the kernel).
+
+  
+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)]
+
+
+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``]
+
+``max_batch_up``
+  Size limit for request upbound payloads. [``4000`` (bytes)]
+
+``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]
diff --git a/macros/Cargo.toml b/macros/Cargo.toml
new file mode 100644 (file)
index 0000000..7bf497b
--- /dev/null
@@ -0,0 +1,21 @@
+# Copyright 2021 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.0"
+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 = "1"
+proc-macro2 = "1"
+quote = "1"
+
+[lib]
+path = "macros.rs"
+proc-macro = true
diff --git a/macros/macros.rs b/macros/macros.rs
new file mode 100644 (file)
index 0000000..88828ae
--- /dev/null
@@ -0,0 +1,130 @@
+// Copyright 2021 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 itertools::Itertools;
+
+/// Generates config resolver method
+/// 
+/// Atrributes:
+///
+///  * `limited`, `server`, `client`: cooked sets of settings;
+///    default `SKL` is `Ordinary` except for `limited`
+///  * `special(method, SKL)`
+///
+/// Generated code
+///
+/// ```no_run
+/// impl<'c> ResolveContext<'c> {
+///
+///   const FIELDS: &'static [(&'static str, SectionKindList)] = &[ ... ];
+///
+///   #[throws(AE)]
+///   fn resolve_instance(&self) -> InstanceConfig {
+///     InstanceConfig {
+///       ...
+///        max_batch_down: self.limited("max_batch_down")?,
+///        ...
+///      }
+///   }
+/// }
+/// ```
+#[proc_macro_derive(ResolveConfig, attributes(
+  limited, server, client, computed, special
+))]
+pub fn resolve(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+  let input = parse_macro_input!(input as DeriveInput);
+
+  let fields = match input.data {
+    Data::Struct(DataStruct { fields: syn::Fields::Named(ref f),.. }) => f,
+    _ => panic!(),
+  };
+
+  let target = &input.ident;
+
+  let mut names = vec![];
+  let mut output = vec![];
+  for field in &fields.named {
+    //dbg!(field);
+    let fname = &field.ident.as_ref().unwrap();
+    let fname_span = fname.span();
+    let mut skl = quote_spanned!{fname_span=> SectionKindList::Ordinary };
+    let mut method = quote_spanned!{fname_span=> ordinary };
+    for attr in &field.attrs {
+      if attr.tokens.is_empty() {
+        let atspan = attr.path.segments.last().unwrap().ident.span();
+        method = attr.path.to_token_stream();
+        if &attr.path == &parse_quote!{ limited } {
+          skl = quote_spanned!{atspan=> SectionKindList::Limited };
+        }
+      } 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    = get_path(tskl);
+      }
+    }
+    let fname_string = fname.to_string();
+    let fname_lit = Literal::string( &fname_string );
+
+    names.push(quote!{
+      (#fname_lit, #skl),
+    });
+    //dbg!(&method);
+    output.push(quote!{
+      #fname: rctx. #method ( #fname_lit )?,
+    });
+    //eprintln!("{:?} method={:?} skl={:?}", field.ident, method, skl);
+  }
+  //dbg!(&output);
+
+  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 )*
+        })
+      }
+    }
+  };
+  //eprintln!("{}", &output);
+  output.into()
+}
+
+#[proc_macro]
+pub fn into_crlfs(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+  let input: proc_macro2::TokenStream = input.into();
+  let token: LitStr = syn::parse2(input).expect("expected literal");
+  let input = token.value();
+  let output = input.split_inclusive('\n')
+    .map(|s| s.trim_start_matches(&[' ','\t'][..]))
+    .map(|s| match s.strip_suffix("\n") {
+      None => [s, ""],
+      Some(l) => [l, "\r\n"],
+    })
+    .flatten()
+    .collect::<String>();
+  //dbg!(&output);
+  let output = LitStr::new(&output, token.span());
+  let output = quote!(#output);
+  output.into()
+}
diff --git a/src/bin/client.rs b/src/bin/client.rs
new file mode 100644 (file)
index 0000000..75e868d
--- /dev/null
@@ -0,0 +1,362 @@
+// Copyright 2021 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;
+
+const MAX_BATCH_DOWN_RESP_OVERHEAD: usize = 10_000;
+
+#[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 = SystemTime::now()
+    .duration_since(UNIX_EPOCH)
+    .unwrap_or_else(|_| Duration::default()) // clock is being weird
+    .as_secs();
+  let time_t = format!("{:x}", time_t);
+  let hmac = token_hmac(c.ic.secret.0.as_bytes(), time_t.as_bytes());
+  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.max_batch_down,
+  );
+
+  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![
+      array::IntoIter::new([
+        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 bytes={} 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 resp = resp.into_body();
+      let max_body = c.ic.max_batch_down.sat() + MAX_BATCH_DOWN_RESP_OVERHEAD;
+      let resp = read_limited_body(max_body, resp).await?;
+
+      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 = tokio::process::Command::new("sh")
+    .args(&["-c", &ic.ipif])
+    .stdin (process::Stdio::piped())
+    .stdout(process::Stdio::piped())
+    .stderr(process::Stdio::piped())
+    .kill_on_drop(true)
+    .spawn().context("spawn ipif")?;
+  
+  let stderr = ipif.stderr.take().unwrap();
+  let ic_name = ic.to_string();
+  let _ = task::spawn(async move {
+    let mut stderr = tokio::io::BufReader::new(stderr).lines();
+    while let Some(l) = stderr.next_line().await? {
+      error!("{}: ipif stderr: {}", &ic_name, l.trim_end());
+    }
+    Ok::<_,io::Error>(())
+  });
+
+  let mut req_num: ReqNum = 0;
+
+  let tx_stream = ipif.stdout.take().unwrap();
+  let mut rx_stream = ipif.stdin .take().unwrap();
+
+  let mut tx_stream = tokio::io::BufReader::new(tx_stream).split(SLIP_END);
+  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: FrameQueue = default();
+
+  let trouble = async {
+    loop {
+      let rx_queue_space = 
+        if rx_queue.remaining() < ic.max_batch_down.sat() {
+          Ok(())
+        } else {
+          Err(())
+        };
+      
+      select! {
+        biased;
+
+        y = rx_stream.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 = tx_stream.next_segment(),
+        if tx_queue.is_empty() =>
+        {
+          let data =
+            data.context("read from ipif")?
+            .ok_or_else(|| io::Error::from(io::ErrorKind::UnexpectedEof))?;
+          //eprintln!("data={:?}", DumpHex(&data));
+
+          match check1(Slip2Mime, ic.mtu, &data, |header| {
+            let addr = ip_packet_addr::<false>(header)?;
+            if addr != ic.link.client.0 { throw!(PE::Src(addr)) }
+            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 {
+            reporter.lock().success();
+            //eprintln!("got={:?}", DumpHex(&got));
+            checkn(SlipNoConv,ic.mtu, &got, &mut rx_queue, |header| {
+              let addr = ip_packet_addr::<true>(header)?;
+              if addr != ic.link.client.0 { throw!(PE::Dst(addr)) }
+              Ok(())
+            }, |e| error!("{} #{}: rx discarding: {}", &ic, req_num, e));
+          
+          }
+        },
+
+        _ = 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;
+
+  drop(tx_stream);
+
+  match ipif.wait().await {
+    Err(e) => error!("{}: also, failed to await ipif child: {}", &ic, e),
+    Ok(st) if st.success() => { },
+    Ok(st) => error!("{}: ipif process failed: {}", &ic, st),
+  }
+
+  trouble
+}
+
+#[tokio::main]
+async fn main() -> Result<(), AE> {
+  let opts = Opts::from_args();
+
+  let ics = config::read(&opts.config, LinkEnd::Client)?;
+  if ics.is_empty() { throw!(anyhow!("no associations with server(s)")); }
+
+  opts.log.log_init()?;
+
+  let https = HttpsConnector::new();
+  let hclient = hyper::Client::builder().build::<_, hyper::Body>(https);
+  let hclient = Arc::new(hclient);
+
+  info!("starting");
+  let () = future::select_all(
+    ics.into_iter().map(|ic| Box::pin(async {
+      let assocname = ic.to_string();
+      info!("{} starting", &assocname);
+      let hclient = hclient.clone();
+      let join = task::spawn(async {
+        run_client(ic, hclient).await.void_unwrap_err()
+      });
+      match join.await {
+        Ok(e) => {
+          error!("{} failed: {:?}", &assocname, e);
+        },
+        Err(je) => {
+          error!("{} panicked!", &assocname);
+          panic::resume_unwind(je.into_panic());
+        },
+      }
+    }))
+  ).await.0;
+
+  error!("quitting because one of your client connections crashed");
+  process::exit(16);
+}
diff --git a/src/bin/server.rs b/src/bin/server.rs
new file mode 100644 (file)
index 0000000..d97cf1c
--- /dev/null
@@ -0,0 +1,27 @@
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use hippotat::prelude::*;
+
+#[derive(StructOpt,Debug)]
+pub struct Opts {
+  #[structopt(flatten)]
+  log: LogOpts,
+
+  #[structopt(flatten)]
+  config: config::Opts,
+}
+
+#[tokio::main]
+async fn main() -> Result<(), AE> {
+  let opts = Opts::from_args();
+
+  let ics = config::read(&opts.config, LinkEnd::Server)?;
+
+  opts.log.log_init()?;
+
+  dbg!(ics);
+
+  Ok(())
+}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644 (file)
index 0000000..16225b1
--- /dev/null
@@ -0,0 +1,861 @@
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+use configparser::ini::Ini; // xxx ignores empty sections, fix or replace
+
+#[derive(hippotat_macros::ResolveConfig)]
+#[derive(Debug,Clone)]
+pub struct InstanceConfig {
+  // Exceptional settings
+  #[special(special_link, SKL::ServerName)] pub   link:   LinkName,
+  pub                                             secret: Secret,
+  #[special(special_ipif, SKL::Ordinary)] 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,
+
+  // Ordinary settings:
+  pub addrs:                        Vec<IpAddr>,
+  pub vnetwork:                     Vec<IpNet>,
+  pub vaddr:                        IpAddr,
+  pub vrelay:                       IpAddr,
+  pub port:                         u16,
+  pub mtu:                          u32,
+  pub ifname_server:                String,
+  pub ifname_client:                String,
+
+  // Ordinary settings, used by server only:
+  #[server]  pub max_clock_skew:               Duration,
+
+  // Ordinary settings, used by client only:
+  #[client]  pub http_timeout_grace:           Duration,
+  #[client]  pub max_requests_outstanding:     u32,
+  #[client]  pub max_batch_up:                 u32,
+  #[client]  pub http_retry:                   Duration,
+  #[client]  pub success_report_interval:      Duration,
+  #[client]  pub url:                          Uri,
+  #[client]  pub vroutes:                      Vec<IpNet>,
+
+  // 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_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,Clone)]
+struct RawVal { raw: Option<String>, loc: Arc<PathBuf> }
+type SectionMap = HashMap<String, RawVal>;
+
+#[derive(Debug)]
+struct RawValRef<'v,'l,'s> {
+  raw: Option<&'v str>,
+  key: &'static str,
+  loc: &'l Path,
+  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(Default,Debug)]
+struct Aggregate {
+  keys_allowed: HashMap<&'static str, SectionKindList>,
+  sections: HashMap<SectionName, SectionMap>,
+}
+
+type OkAnyway<'f,A> = &'f dyn Fn(ErrorKind) -> 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 k = e.kind();
+    let a = self(k)?;
+    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 {
+  #[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 ini = Ini::new_cs();
+    ini.set_default_section(OUTSIDE_SECTION);
+    ini.read(s).map_err(|e| anyhow!("{}", e)).context("parse as INI")?;
+    let map = mem::take(ini.get_mut_map());
+    if map.get(OUTSIDE_SECTION).is_some() {
+      throw!(anyhow!("INI file contains settings outside a section"));
+    }
+
+    let loc = Arc::new(path_for_loc.to_owned());
+
+    for (sn, vars) in map {
+      let sn = sn.parse().dcontext(&sn)?;
+
+      for key in vars.keys() {
+        let skl = self.keys_allowed.get(key.as_str()).ok_or_else(
+          || anyhow!("unknown configuration key {:?}", key)
+        )?;
+        if ! skl.contains(&sn) {
+          throw!(anyhow!("configuration key {:?} not applicable \
+                          in this kind of section {:?}", key, &sn))
+        }
+      }
+
+      let ent = self.sections.entry(sn).or_default();
+      for (key, raw) in vars {
+        let raw = match raw {
+          Some(raw) if raw.starts_with('\'') || raw.starts_with('"') => Some(
+            (||{
+              if raw.contains('\\') {
+                throw!(
+                  anyhow!("quoted value contains backslash, not supported")
+                );
+              }
+              let unq = raw[1..].strip_suffix(&raw[0..1])
+                .ok_or_else(
+                  || anyhow!("mismatched quotes around quoted value")
+                )?
+                .to_owned();
+              Ok::<_,AE>(unq)
+            })()
+              .with_context(|| format!("key {:?}", key))
+              .dcontext(path_for_loc)?
+          ),
+          x => x,
+        };
+        let key = key.replace('-',"_");
+        ent.insert(key, RawVal { raw, 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, &|k| match k {
+      EK::NotFound => Some(Anyway::None),
+      EK::IsADirectory => 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 = |k| match k {
+          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, &|k| match k {
+      EK::IsADirectory => 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.contains_key("secret");
+      //dbg!(&section, has_secret());
+
+      match section {
+        SN::Link(l) => {
+          if ! note_server(&l.server) { continue }
+          note_client(&l.client);
+          if has_secret() { links.insert(l.clone()); }
+        },
+        SN::Server(ref s) => {
+          if ! note_server(s) { continue }
+          if has_secret() { secrets_anyclient.insert(s); }
+        },
+        SN::Client(ref c) => {
+          note_client(c);
+          if has_secret() { secrets_anyserver.insert(c); }
+        },
+        SN::Common => {
+          if has_secret() { secret_global = true; }
+        },
+        _ => { },
+      }
+    }
+
+    //dbg!(&putative_servers, &putative_clients);
+    //dbg!(&secrets_anyserver, &secrets_anyclient, &secret_global);
+
+    // Add links which are justified by blanket secrets
+    for (client, server) in iproduct!(
+      putative_clients.into_iter().filter(
+        |c| secret_global
+         || secrets_anyserver.contains(c)
+         || ! secrets_anyclient.is_empty()
+      ),
+      putative_servers.iter().cloned().filter(
+        |s| secret_global
+         || secrets_anyclient.contains(s)
+         || ! secrets_anyserver.is_empty()
+      )
+    ) {
+      links.insert(LinkName {
+        client: client.clone(),
+        server: server.clone(),
+      });
+    }
+
+    links
+  }
+}
+
+struct ResolveContext<'c> {
+  agg: &'c Aggregate,
+  link: &'c LinkName,
+  end: LinkEnd,
+  all_sections: Vec<SectionName>,
+}
+
+trait Parseable: Sized {
+  fn parse(s: Option<&str>) -> Result<Self, AE>;
+  fn default() -> Result<Self, AE> {
+    Err(anyhow!("setting must be specified"))
+  }
+  #[throws(AE)]
+  fn default_for_key(key: &str) -> Self {
+    Self::default().with_context(|| key.to_string())?
+  }
+}
+
+impl Parseable for Duration {
+  #[throws(AE)]
+  fn parse(s: Option<&str>) -> Duration {
+    // todo: would be nice to parse with humantime maybe
+    Duration::from_secs( s.value()?.parse()? )
+  }
+}
+macro_rules! parseable_from_str { ($t:ty $(, $def:expr)? ) => {
+  impl Parseable for $t {
+    #[throws(AE)]
+    fn parse(s: Option<&str>) -> $t { s.value()?.parse()? }
+    $( #[throws(AE)] fn default() -> Self { $def } )?
+  }
+} }
+parseable_from_str!{u16, default() }
+parseable_from_str!{u32, default() }
+parseable_from_str!{String, default() }
+parseable_from_str!{IpNet, default() }
+parseable_from_str!{IpAddr, Ipv4Addr::UNSPECIFIED.into() }
+parseable_from_str!{Uri, default() }
+
+impl<T:Parseable> Parseable for Vec<T> {
+  #[throws(AE)]
+  fn parse(s: Option<&str>) -> Vec<T> {
+    s.value()?
+      .split_ascii_whitespace()
+      .map(|s| Parseable::parse(Some(s)))
+      .collect::<Result<Vec<_>,_>>()?
+  }
+  #[throws(AE)]
+  fn default() -> Self { default() }
+}
+
+
+#[derive(Debug,Copy,Clone)]
+enum SectionKindList {
+  Ordinary,
+  Limited,
+  Limits,
+  ClientAgnostic,
+  ServerName,
+}
+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) -> bool {
+    match self {
+      SKL::Ordinary       => matches!(s, SN::Link(_)
+                                       | SN::Client(_)
+                                       | SN::Server(_)
+                                       | SN::Common),
+
+      SKL::Limits         => matches!(s, SN::ServerLimit(_)
+                                       | SN::GlobalLimit),
+
+      SKL::ClientAgnostic => matches!(s, SN::Common
+                                       | SN::Server(_)),
+
+      SKL::Limited        => SKL::Ordinary.contains(s)
+                           | SKL::Limits  .contains(s),
+
+      SKL::ServerName     => matches!(s, SN::Common)
+                           | matches!(s, SN::Server(ServerName(name))
+                                         if name == SPECIAL_SERVER_SECTION),
+    }
+  }
+}
+
+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(raw) = self.sections
+        .get(section)
+        .and_then(|vars: &SectionMap| vars.get(key))
+      {
+        return Some(RawValRef {
+          raw: raw.raw.as_deref(),
+          loc: &raw.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))
+    )
+  }
+
+  #[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) -> T
+  where T: Parseable
+  {
+    match self.first_of(key, SKL::Ordinary)? {
+      Some(y) => y,
+      None => Parseable::default_for_key(key)?,
+    }
+  }
+
+  #[throws(AE)]
+  pub fn limited<T>(&self, key: &'static str) -> T
+  where T: Parseable + Ord
+  {
+    let val = self.ordinary(key)?;
+    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) -> T
+  where T: Parseable + Default {
+    match self.end {
+      LinkEnd::Client => self.ordinary(key)?,
+      LinkEnd::Server => default(),
+    }
+  }
+  #[throws(AE)]
+  pub fn server<T>(&self, key: &'static str) -> T
+  where T: Parseable + Default {
+    match self.end {
+      LinkEnd::Server => self.ordinary(key)?,
+      LinkEnd::Client => default(),
+    }
+  }
+
+  #[throws(AE)]
+  pub fn computed<T>(&self, _key: &'static str) -> T
+  where T: Default
+  {
+    default()
+  }
+
+  #[throws(AE)]
+  pub fn special_ipif(&self, key: &'static str) -> String {
+    match self.end {
+      LinkEnd::Client => self.ordinary(key)?,
+      LinkEnd::Server => {
+        self.first_of(key, SKL::ClientAgnostic)?
+          .unwrap_or_default()
+      },
+    }
+  }
+
+  #[throws(AE)]
+  pub fn special_link(&self, _key: &'static str) -> LinkName {
+    self.link.clone()
+  }
+}
+
+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;
+    }
+  }
+}
+
+#[throws(AE)]
+pub fn read(opts: &Opts, end: LinkEnd) -> Vec<InstanceConfig> {
+  let agg = (||{
+    let mut agg = Aggregate::default();
+    agg.keys_allowed.extend(
+      InstanceConfig::FIELDS.iter().cloned()
+    );
+
+    agg.read_string(DEFAULT_CONFIG.into(),
+                    "<build-in defaults>".as_ref()).unwrap();
+
+    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::Ordinary).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
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644 (file)
index 0000000..7f5a854
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+#![feature(io_error_more)] // EK::IsADirectory
+
+pub mod prelude;
+
+pub mod config;
+pub mod slip;
+pub mod reporter;
+pub mod queue;
+pub mod types;
+pub mod utils;
diff --git a/src/prelude.rs b/src/prelude.rs
new file mode 100644 (file)
index 0000000..fd9fd83
--- /dev/null
@@ -0,0 +1,68 @@
+// Copyright 2021 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::{TryFrom, TryInto};
+pub use std::borrow::Cow;
+pub use std::cmp::{min, max};
+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};
+pub use std::path::{Path, PathBuf};
+pub use std::panic;
+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 anyhow::{anyhow, Context};
+pub use cervine::Cow as Cervine;
+pub use extend::ext;
+pub use fehler::{throw, throws};
+pub use futures::{poll, future, StreamExt as _};
+pub use hyper::body::{Bytes, Buf as _};
+pub use hyper::Uri;
+pub use hyper_tls::HttpsConnector;
+pub use ipnet::IpNet;
+pub use itertools::{iproduct, Itertools};
+pub use lazy_regex::{regex_is_match, regex_replace_all};
+pub use log::{trace, debug, info, warn, error};
+pub use structopt::StructOpt;
+pub use thiserror::Error;
+pub use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
+pub use tokio::pin;
+pub use tokio::select;
+pub use tokio::task;
+pub use tokio::time::{Duration, Instant};
+pub use void::{self, Void, ResultVoidExt, ResultVoidErrExt};
+
+pub use crate::config::{self, InstanceConfig, u32Ext as _};
+pub use crate::utils::*;
+pub use crate::queue::*;
+pub use crate::reporter::*;
+pub use crate::types::*;
+pub use crate::slip::*;
+
+pub type ReqNum = u64;
+
+pub use anyhow::Error as AE;
+pub use ErrorKind as EK;
+pub use PacketError as PE;
+
+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 use base64::STANDARD as BASE64_CONFIG;
+
+pub fn default<T:Default>() -> T { Default::default() }
diff --git a/src/queue.rs b/src/queue.rs
new file mode 100644 (file)
index 0000000..ba1d4ff
--- /dev/null
@@ -0,0 +1,83 @@
+// Copyright 2021 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<E> {
+  content: usize,
+  eaten1: usize, // 0 <= eaten1 < queue.front()...len()
+  queue: VecDeque<E>,
+}
+
+#[derive(Default,Debug,Clone)]
+pub struct FrameQueue {
+  queue: Queue<Cervine<'static, Box<[u8]>, [u8]>>,
+}
+
+impl<E> Debug for Queue<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> Queue<E> where E: AsRef<[u8]> {
+  pub fn push<B: Into<E>>(&mut self, b: B) {
+    self.push_(b.into());
+  }
+  pub 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 }
+}
+
+impl FrameQueue {
+  pub fn push<B: Into<Box<[u8]>>>(&mut self, b: B) {
+    self.push_(b.into());
+  }
+  pub fn push_(&mut self, b: Box<[u8]>) {
+    self.queue.push_(Cervine::Owned(b));
+    self.queue.push_(Cervine::Borrowed(&SLIP_END_SLICE));
+  }
+  pub fn is_empty(&self) -> bool { self.queue.is_empty() }
+}
+
+impl<E> Extend<E> for FrameQueue where E: Into<Box<[u8]>> {
+  fn extend<I>(&mut self, it: I)
+  where I: IntoIterator<Item=E>
+  {
+    for b in it { self.push(b) }
+  }
+}
+
+impl<E> hyper::body::Buf for Queue<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 FrameQueue {
+  fn remaining(&self) -> usize { self.queue.remaining() }
+  fn chunk(&self) -> &[u8] { self.queue.chunk() }
+  fn advance(&mut self, cnt: usize) { self.queue.advance(cnt) }
+}
diff --git a/src/reporter.rs b/src/reporter.rs
new file mode 100644 (file)
index 0000000..0d199ec
--- /dev/null
@@ -0,0 +1,104 @@
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[derive(StructOpt,Debug)]
+pub struct LogOpts {
+  /// Increase debug level
+  #[structopt(long, short="D", parse(from_occurrences))]
+  debug: usize,
+}
+
+impl LogOpts {
+  #[throws(AE)]
+  pub fn log_init(&self) {
+    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();
+  }
+}
+
+// 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
+      },
+    }
+  }
+}
diff --git a/src/rope.rs b/src/rope.rs
new file mode 100644 (file)
index 0000000..fd400fe
--- /dev/null
@@ -0,0 +1,42 @@
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[derive(Default,Clone)]
+pub struct Queue {
+  content: usize,
+  eaten1: usize, // 0 <= eaten1 < queue.front()...len()
+  queue: VecDeque<Box<[u8]>>,
+}
+
+pub impl Queue {
+  pub fn push<B: Into<Box<[u8]>>>(&mut self, b: B) {
+    self.push_(b.into());
+  }
+  pub fn push_(&mut self, b: Box<[u8]>) {
+    let l = b.len();
+    self.push(b);
+    b.content += b;
+  }
+  pub fn is_empty(&self) { self.content == 0 }
+}
+
+impl bytes::Buf for Queue {
+  fn remaining(&self) -> usize { self.content }
+  fn chunk(&self) -> usize {
+    let front = if let(f) = self.queue.front() { f } else { return &[] };
+    front[ self.eaten1.. ]
+  }
+  fn advance(&self, cnt: usize) {
+    eaten1 += cnt;
+    loop {
+      if eaten1 == 0 { break }
+      let front = self.queue.front().unwrap();
+      if eaten1 < front.len() { break; }
+      eaten1 -= front.len();
+      self.queue.pop_front().unwrap();
+    }
+  }
+}
diff --git a/src/slip.rs b/src/slip.rs
new file mode 100644 (file)
index 0000000..67b4d40
--- /dev/null
@@ -0,0 +1,213 @@
+// Copyright 2021 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("bad, IPv{vsn}, len={len}")]     Bad { len: usize, vsn: u8 },
+}
+
+pub trait SlipMime { 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; }
+
+pub fn checkn<AC, EH, OUT, M: SlipMime+Copy>(
+  mime: M,
+  mtu: u32,
+  data: &[u8],
+  out: &mut OUT,
+  addr_chk: AC,
+  mut error_handler: EH
+) where OUT: Extend<Box<[u8]>>,
+        AC: Fn(&[u8]) -> Result<(), PacketError> + Copy,
+        EH: FnMut(PacketError),
+{
+  //  eprintln!("before: {:?}", DumpHex(data));
+  if data.is_empty() { return }
+  for packet in data.split(|&c| c == SLIP_END) {
+    match check1(mime, mtu, packet, addr_chk) {
+      Err(e) => error_handler(e),
+      Ok(packet) => out.extend(iter::once(packet)),
+    }
+  }
+//  eprintln!(" after: {:?}", DumpHex(data));
+}
+
+#[throws(PacketError)]
+pub fn check1<AC, M: SlipMime>(
+  _mime: M,
+  mtu: u32,
+  packet: &[u8],
+  addr_chk: AC,
+) -> Box<[u8]>
+where AC: Fn(&[u8]) -> Result<(), PacketError>,
+{
+  if packet.len() == 0 {
+    throw!(PacketError::Empty)
+  }
+  if packet.len() > mtu.sat() {
+    throw!(PacketError::MTU { len: packet.len(), mtu });
+  }
+
+  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[..];
+
+  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 ..];
+    } else {
+      let _ = wheader.write(&[SLIP_MIME_ESC]);
+      walk = &mut walk[i+1 ..];
+    }
+  }
+  let _ = wheader.write(walk);
+
+  addr_chk(&header)?;
+
+  packet
+}
+
+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>(packet: &[u8]) -> IpAddr {
+  let vsn = (packet.get(0).ok_or_else(|| PE::Empty)? & 0xf0) >> 4;
+  match vsn {
+    4 if packet.len() >= 20 => {
+      let slice = &packet[if DST { 16 } else { 12 }..][0..4];
+      Ipv4Addr::from(*<&[u8;4]>::try_from(slice).unwrap()).into()
+    },
+
+    6 if packet.len() >= 40 => {
+      let slice = &packet[if DST { 24 } else { 8 }..][0..16];
+      Ipv6Addr::from(*<&[u8;16]>::try_from(slice).unwrap()).into()
+    },
+
+    _ => throw!(PE::Bad{ vsn, len: packet.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)?; }
+  }
+}
+
+#[test]
+fn mime_slip_to_mime() {
+  use PacketError as PE;
+  const MTU: u32 = 10;
+
+  fn chk<M: SlipMime>(i: &[u8], exp_p: &[&[u8]], exp_e: &[PacketError]) {
+    dbg!(M::CONV_TO, DumpHex(i));
+    let mut got_e = vec![];
+    let mut got_p = vec![];
+    check::<_,_,_,M>(MTU, i, &mut got_p, |_|Ok(()), |e| got_e.push(e));
+    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 );
+  }
+
+  chk::<Slip2Mime>
+     ( &[ SLIP_END, SLIP_ESC, SLIP_ESC_END, b'-',     b'X' ],
+    &[           &[ b'-',     SLIP_ESC_END, SLIP_ESC, b'X' ] ],
+    &[ PE::Empty ]);
+
+  chk::<Slip2Mime>
+     ( &[ SLIP_END, SLIP_ESC, b'y' ], &[],
+    &[ PE::Empty,   PE::SLIP ]);
+
+  chk::<Slip2Mime>
+     ( &[ SLIP_END, b'-',     b'y' ],
+    &[           &[ SLIP_ESC, b'y' ] ],
+    &[ PE::Empty ]);
+
+  chk::<Slip2Mime>
+     ( &[b'x'; 20],
+    &[             ],
+    &[ PE::MTU { len: 20, mtu: MTU } ]);
+
+  chk::<SlipNoConv>
+     ( &[ SLIP_END, SLIP_ESC, SLIP_ESC_END, b'-',     b'X' ],
+    &[           &[ SLIP_ESC, SLIP_ESC_END, b'-',     b'X' ] ],
+    &[ PE::Empty, ]);
+}
+
+
diff --git a/src/types.rs b/src/types.rs
new file mode 100644 (file)
index 0000000..ca40df0
--- /dev/null
@@ -0,0 +1,67 @@
+// Copyright 2021 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 Ipv4Addr);
+
+#[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 {
+    let v4addr: Ipv4Addr = s.parse()
+      .context("invalid client name (IPv4 address)")?;
+    if s != v4addr.to_string() {
+      throw!(anyhow!("invalid client name (unusual IPv4 address syntax)"));
+    }
+    ClientName(v4addr)
+  }
+}
+
+impl FromStr for ServerName {
+  type Err = AE;
+  #[throws(AE)]
+  fn from_str(s: &str) -> Self {
+    if ! regex_is_match!(r"
+        ^ (?: SERVER
+            | [0-9a-z][-0-9a-z]* (:? \.
+              [0-9a-z][-0-9a-z]*        )*
+          ) $"x, s) {
+      throw!(anyhow!("bad syntax for server name"));
+    }
+    if ! regex_is_match!(r"[A-Za-z-]", s) {
+      throw!(anyhow!("bad syntax for server name \
+                      (too much like an IPv4 address)"));
+    }
+    ServerName(s.into())
+  }
+}
+
+impl Display for ServerName {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) { Display::fmt(&self.0, f)?; }
+}
+impl Display for ClientName {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) { Display::fmt(&self.0, f)?; }
+}
+impl Display for LinkName {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) {
+    write!(f, "[{} {}]", &self.server, &self.client)?;
+  }
+}
diff --git a/src/utils.rs b/src/utils.rs
new file mode 100644 (file)
index 0000000..2271fd4
--- /dev/null
@@ -0,0 +1,184 @@
+// Copyright 2021 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())
+  }
+}
+
+#[throws(AE)]
+pub async fn read_limited_body<S,E>(limit: usize, mut stream: S) -> Box<[u8]>
+where S: futures::Stream<Item=Result<hyper::body::Bytes,E>> + Unpin,
+      // we also require that the Stream is cancellation-safe
+      E: std::error::Error + Sync + Send + 'static,
+{
+  let mut accum = vec![];
+  while let Some(item) = stream.next().await {
+    let b = item.context("HTTP error fetching response body")?;
+    if accum.len() + b.len() > limit {
+      throw!(anyhow!("maximum response body size {} exceeded", limit));
+    }
+    accum.extend(b);
+  }
+  accum.into()
+}
+
+use sha2::Digest as _;
+
+type HmacH = sha2::Sha256;
+const HMAC_B: usize = 64;
+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!(&key, &ikey, &okey);
+
+  let h1 = HmacH::new()
+    .chain(&ikey)
+    .chain(message)
+    .finalize();
+  let h2 = HmacH::new()
+    .chain(&okey)
+    .chain(h1)
+    .finalize();
+  h2.into()
+}
+
+#[test]
+fn hmac_test_vectors(){
+  // C&P from RFC 4231
+  let vectors = r#"
+   Key =          0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b
+                  0b0b0b0b                          (20 bytes)
+   Data =         4869205468657265                  ("Hi There")
+
+   HMAC-SHA-256 = b0344c61d8db38535ca8afceaf0bf12b
+                  881dc200c9833da726e9376c2e32cff7
+
+    
+   Key =          4a656665                          ("Jefe")
+   Data =         7768617420646f2079612077616e7420  ("what do ya want ")
+                  666f72206e6f7468696e673f          ("for nothing?")
+
+   HMAC-SHA-256 = 5bdcc146bf60754e6a042426089575c7
+                  5a003f089d2739839dec58b964ec3843
+
+
+   Key =          aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaa                          (20 bytes)
+   Data =         dddddddddddddddddddddddddddddddd
+                  dddddddddddddddddddddddddddddddd
+                  dddddddddddddddddddddddddddddddd
+                  dddd                              (50 bytes)
+
+   HMAC-SHA-256 = 773ea91e36800e46854db8ebd09181a7
+                  2959098b3ef8c122d9635514ced565fe
+
+
+   Key =          0102030405060708090a0b0c0d0e0f10
+                  111213141516171819                (25 bytes)
+   Data =         cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd
+                  cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd
+                  cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd
+                  cdcd                              (50 bytes)
+
+   HMAC-SHA-256 = 82558a389a443c0ea4cc819899f2083a
+                  85f0faa3e578f8077a2e3ff46729665b
+
+
+
+   Key =          aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaa                            (131 bytes)
+   Data =         54657374205573696e67204c61726765  ("Test Using Large")
+                  72205468616e20426c6f636b2d53697a  ("r Than Block-Siz")
+                  65204b6579202d2048617368204b6579  ("e Key - Hash Key")
+                  204669727374                      (" First")
+
+   HMAC-SHA-256 = 60e431591ee0b67f0d8a26aacbf5b77f
+                  8e0bc6213728c5140546040f0ee37f54
+
+   Key =          aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+                  aaaaaa                            (131 bytes)
+   Data =         54686973206973206120746573742075  ("This is a test u")
+                  73696e672061206c6172676572207468  ("sing a larger th")
+                  616e20626c6f636b2d73697a65206b65  ("an block-size ke")
+                  7920616e642061206c61726765722074  ("y and a larger t")
+                  68616e20626c6f636b2d73697a652064  ("han block-size d")
+                  6174612e20546865206b6579206e6565  ("ata. The key nee")
+                  647320746f2062652068617368656420  ("ds to be hashed ")
+                  6265666f7265206265696e6720757365  ("before being use")
+                  642062792074686520484d414320616c  ("d by the HMAC al")
+                  676f726974686d2e                  ("gorithm.")
+
+   HMAC-SHA-256 = 9b09ffa71b942fcb27635fbcd5b0e944
+                  bfdc63644f0713938a7f51535c3a35e2
+"#;
+  let vectors = regex_replace_all!{
+    r#"\(.*\)"#,
+    vectors.trim_end(),
+    |_| "",
+  };
+  let vectors = regex_replace_all!{
+    r#" *\n                  "#,
+    &vectors,
+    |_| "",
+  };
+  let vectors = regex_replace_all!{
+    r#"\s*\n"#,
+    &vectors,
+    |_| "\n",
+  };
+  let mut lines = vectors.split('\n');
+  assert_eq!( lines.next().unwrap(), "" );
+  let mut get = |prefix| {
+    let l = lines.next()?;
+    dbg!(l);
+    let b = l.strip_prefix(prefix).unwrap().as_bytes().chunks(2)
+      .map(|s| str::from_utf8(s).unwrap())
+      .map(|s| { assert_eq!(s.len(), 2); u8::from_str_radix(s,16).unwrap() })
+      .collect::<Vec<u8>>();
+    Some(b)
+  };
+  while let Some(key) = get("   Key =          ") {
+    let data = get("   Data =         ").unwrap();
+    let exp = get("   HMAC-SHA-256 = ").unwrap();
+    let got = token_hmac(&key, &data);
+    assert_eq!(&got[..], &exp);
+  }
+}
diff --git a/test.cfg b/test.cfg
new file mode 100644 (file)
index 0000000..9cc92d9
--- /dev/null
+++ b/test.cfg
@@ -0,0 +1,24 @@
+[SERVER]
+
+ipif = PATH=/usr/local/sbin:/sbin:/usr/sbin:$PATH really /home/ian/things/Userv/userv-utils.git/ipif/service \* -- %(local)s,%(peer)s,%(mtu)s,slip '%(rnets)s'
+
+addrs = 127.0.0.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 = PATH=/usr/local/sbin:/sbin:/usr/sbin:$PATH really ./fake-userv /home/ian/things/Userv/userv-utils.git/ipif/service \* -- %(local)s,%(peer)s,%(mtu)s,slip '%(rnets)s'
+
+# ./hippotat -D -c test.cfg
+
+[192.0.2.4]
+#secret = zorkmids
+
+# dd if=/dev/urandom bs=1024 count=16384 | nc -q 0 -n -v 192.0.2.1 8100
diff --git a/uml/psusan-uml-inside b/uml/psusan-uml-inside
new file mode 100755 (executable)
index 0000000..24658e3
--- /dev/null
@@ -0,0 +1,30 @@
+#!/bin/bash
+set -ex
+
+mkdir /dev/pts
+mount -t proc none /proc
+mount -t devpts none /dev/pts
+mount -t tmpfs none /tmp
+mount -t tmpfs none /run
+
+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
+
+exec psusan
diff --git a/uml/psusan-uml-psusan b/uml/psusan-uml-psusan
new file mode 100755 (executable)
index 0000000..617824d
--- /dev/null
@@ -0,0 +1,21 @@
+#!/bin/bash
+set -e
+
+fifo=tmp/uml/q
+mkfifo -m600 $fifo
+
+(
+ # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=991958
+ : <$fifo
+ cat
+)                                                              |       \
+ bwrap --dev-bind / / --tmpfs /dev/shm                                 \
+ linux mem=512M rootfstype=hostfs rootflags=/ rw                       \
+       con=fd:2,fd:2 con1=fd:0,fd:1 init="${0%/*}"/psusan-uml-inside   \
+       -- psusan-uml-tmp=$PWD/tmp/uml                          |       \
+(
+ read banner
+ : >$fifo
+ printf '%s\n' "$banner"
+ cat
+)
diff --git a/uml/psusan-uml-run b/uml/psusan-uml-run
new file mode 100755 (executable)
index 0000000..a7fe40b
--- /dev/null
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -e
+
+HOME=$PWD/tmp/uml
+
+plink -ssh-connection -share $PWD "$@"
diff --git a/uml/psusan-uml-setup b/uml/psusan-uml-setup
new file mode 100755 (executable)
index 0000000..7e9da78
--- /dev/null
@@ -0,0 +1,9 @@
+#!/bin/bash
+set -e
+
+mkdir -p tmp
+rm -rf tmp/uml
+mkdir -p -m2700 tmp/uml
+dd if=/dev/urandom of=tmp/uml/random-seed bs=1k count=4
+
+"${0%/*}"/psusan-uml-run -proxycmd "${0%/*}"/psusan-uml-psusan -N -v -v
diff --git a/uml/rndaddtoentcnt/.gitignore b/uml/rndaddtoentcnt/.gitignore
new file mode 100644 (file)
index 0000000..a997b67
--- /dev/null
@@ -0,0 +1 @@
+rndaddtoentcnt
similarity index 100%
rename from LICENSE
rename to uml/rndaddtoentcnt/LICENSE
diff --git a/uml/rndaddtoentcnt/Makefile b/uml/rndaddtoentcnt/Makefile
new file mode 100644 (file)
index 0000000..7b3c881
--- /dev/null
@@ -0,0 +1,6 @@
+rndaddtoentcnt: rndaddtoentcnt.c
+       $(CC) rndaddtoentcnt.c -o rndaddtoentcnt
+
+.PHONY: clean
+clean:
+       rm -f *.o rndaddtoentcnt
diff --git a/uml/rndaddtoentcnt/README.md b/uml/rndaddtoentcnt/README.md
new file mode 100644 (file)
index 0000000..9f85b29
--- /dev/null
@@ -0,0 +1,15 @@
+### rndaddtoentcnt
+
+Seeding the random number generator by writing to /dev/urandom does not update the entropy count.
+
+This utility makes the RNDADDTOENTCNT ioctl call needed to do this.
+
+Used in startup scripts after initializing /dev/urandom with a presaved seed.
+
+Example:
+
+    dd if=/path/to/some/random-seed-file of=/dev/urandom bs=512 count=1
+
+    /path/to/rdnaddtoentcnt <entropy-bit-count>
+
+where entropy-bit-count is a number between 1 and (8 * 512) depending on how much you trust the seed file.