From 6095ead99248963ae70091c5bb3399eed49c0826 Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Wed, 24 Jan 2024 18:58:58 +0100 Subject: Remove Go and C source The Rust implementation now implements all features I need --- .gitignore | 6 +- Cargo.lock | 2364 ++++++++++++++++++++++++++++++++++ Cargo.toml | 8 + cmd/git-lfs-authenticate/main.c | 253 ---- cmd/git-lfs-authenticate/main.go | 141 -- cmd/git-lfs-server/main.go | 897 ------------- cmd/gitolfs3-gen-ed25519-key/main.go | 31 - common/Cargo.toml | 10 + common/src/lib.rs | 368 ++++++ flake.nix | 13 +- git-lfs-authenticate/Cargo.toml | 8 + git-lfs-authenticate/src/main.rs | 236 ++++ go.mod | 27 - go.sum | 53 - rs/.gitignore | 1 - rs/Cargo.lock | 2364 ---------------------------------- rs/Cargo.toml | 8 - rs/common/Cargo.toml | 10 - rs/common/src/lib.rs | 368 ------ rs/git-lfs-authenticate/Cargo.toml | 8 - rs/git-lfs-authenticate/src/main.rs | 236 ---- rs/server/Cargo.toml | 19 - rs/server/src/main.rs | 1028 --------------- rs/shell/Cargo.toml | 6 - rs/shell/src/main.rs | 143 -- server/Cargo.toml | 19 + server/src/main.rs | 1028 +++++++++++++++ shell/Cargo.toml | 6 + shell/src/main.rs | 143 ++ 29 files changed, 4196 insertions(+), 5606 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 cmd/git-lfs-authenticate/main.c delete mode 100644 cmd/git-lfs-authenticate/main.go delete mode 100644 cmd/git-lfs-server/main.go delete mode 100644 cmd/gitolfs3-gen-ed25519-key/main.go create mode 100644 common/Cargo.toml create mode 100644 common/src/lib.rs create mode 100644 git-lfs-authenticate/Cargo.toml create mode 100644 git-lfs-authenticate/src/main.rs delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 rs/.gitignore delete mode 100644 rs/Cargo.lock delete mode 100644 rs/Cargo.toml delete mode 100644 rs/common/Cargo.toml delete mode 100644 rs/common/src/lib.rs delete mode 100644 rs/git-lfs-authenticate/Cargo.toml delete mode 100644 rs/git-lfs-authenticate/src/main.rs delete mode 100644 rs/server/Cargo.toml delete mode 100644 rs/server/src/main.rs delete mode 100644 rs/shell/Cargo.toml delete mode 100644 rs/shell/src/main.rs create mode 100644 server/Cargo.toml create mode 100644 server/src/main.rs create mode 100644 shell/Cargo.toml create mode 100644 shell/src/main.rs diff --git a/.gitignore b/.gitignore index 9a23854..15873fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,7 @@ .direnv/ .idea/ .vscode/ - -# go build -cmd/git-lfs-authenticate/git-lfs-authenticate -cmd/git-lfs-server/git-lfs-server # nix build /result +# cargo build +/target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f3beb9e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2364 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "aws-config" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e64b72d4bdbb41a73d27709c65a25b6e4bfc8321bf70fa3a8b19ce7d4eb81b0" +dependencies = [ + "aws-credential-types", + "aws-http", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 0.2.11", + "hyper 0.14.28", + "ring", + "time", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a7cb3510b95492bd9014b60e2e3bee3e48bc516e220316f8e6b60df18b47331" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-http" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a95d41abe4e941399fdb4bc2f54713eac3c839d98151875948bb24e66ab658f2" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.11", + "http-body 0.4.6", + "pin-project-lite", + "tracing", +] + +[[package]] +name = "aws-runtime" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233cca219c6705d525ace011d6f9bc51aaf32fce5b4c41661d2d7ff22d9b4d49" +dependencies = [ + "aws-credential-types", + "aws-http", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "fastrand", + "http 0.2.11", + "percent-encoding", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634fbe5b6591ee2e281cd2ba8641e9bd752dbf5bf338924d6ad4bd5a3304fe31" +dependencies = [ + "aws-credential-types", + "aws-http", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "http 0.2.11", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "regex-lite", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee41005e0f3a19ae749c7953d9e1f1ef8d2183f76f64966e346fa41c1ba0ed44" +dependencies = [ + "aws-credential-types", + "aws-http", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.11", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa08168f8a27505e7b90f922c32a489feb1f2133878981a15138bebc849ac09c" +dependencies = [ + "aws-credential-types", + "aws-http", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.11", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29102eff04d50ef70f11a48823db33e33c6cc5f027bfb6ff4864efbd5f1f66f3" +dependencies = [ + "aws-credential-types", + "aws-http", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "http 0.2.11", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92384b39aedb258aa734fe0e7b2ffcd13f33e68227251a72cd2635e0acc8f1a" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.11", + "once_cell", + "p256", + "percent-encoding", + "ring", + "sha2", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac0bb78e9e2765699999a02d7bfb4e6ad8f13e0962ebb9f5202b1d8cd76006" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.60.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535a2d5f1e459bc7709580a77152c8d493982db083236c2b1d1c51dc6217e8a3" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc32c", + "crc32fast", + "hex", + "http 0.2.11", + "http-body 0.4.6", + "md-5", + "pin-project-lite", + "sha1", + "sha2", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "682371561562d08ab437766903c6bc28f4f95d7ab2ecfb389bda7849dd98aefe" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.60.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "365ca49744b2bda2f1e2dc03b856da3fa5a28ca5b0a41e41d7ff5305a8fae190" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.11", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.60.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733ccdb727ac63370836aa3b3c483d75ad2ef7bc6507db3efe1d01e8d2e50367" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aff02ae2ee7968bbce2983ffb5ce529d24f4848532300f398347bde8c2196974" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab9cb6fee50680af8ceaa293ae79eba32095ca117161cb323f9ee30dd87d139" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", + "hyper 0.14.28", + "hyper-rustls", + "once_cell", + "pin-project-lite", + "pin-utils", + "rustls", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02ca2da7619517310bfead6d18abcdde90f1439224d887d608503cfacff46dff" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.11", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4bb944488536cd2fef43212d829bc7e9a8bfc4afa079d21170441e7be8d2d0" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.11", + "http-body 0.4.6", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef796feaf894d7fd03869235237aeffe73ed1b29a3927cceeee2eecadf876eba" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8549aa62c5b7db5c57ab915200ee214b4f5d8f19b29a4a8fa0b3ad3bca1380e3" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 0.2.11", + "rustc_version", + "tracing", +] + +[[package]] +name = "axum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.48.5", +] + +[[package]] +name = "common" +version = "0.1.0" +dependencies = [ + "chrono", + "hmac-sha256", + "serde", + "subtle", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32c" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f48d60e5b4d2c53d5c2b1d8a58c849a70ae5e5509b08a48d047e3b65714a74" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint 0.4.9", + "der", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "git-lfs-authenticate" +version = "0.1.0" +dependencies = [ + "chrono", + "common", +] + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.11", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.0.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hmac-sha256" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3688e69b38018fec1557254f64c8dc2cc8ec502890182f395dbb0aa997aa5735" + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.11", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.0.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.2", + "http 1.0.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.11", + "hyper 0.14.28", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.1.0", + "pin-project-lite", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +dependencies = [ + "wasm-bindgen", +] + +[[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.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "outref" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.4", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-lite" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" + +[[package]] +name = "serde" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "server" +version = "0.1.0" +dependencies = [ + "aws-config", + "aws-sdk-s3", + "axum", + "base64", + "chrono", + "common", + "mime", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tower", + "tracing-subscriber", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shell" +version = "0.1.0" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" + +[[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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6439e6b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +resolver = "2" +members = [ + "common", + "git-lfs-authenticate", + "server", + "shell", +] diff --git a/cmd/git-lfs-authenticate/main.c b/cmd/git-lfs-authenticate/main.c deleted file mode 100644 index a7ec031..0000000 --- a/cmd/git-lfs-authenticate/main.c +++ /dev/null @@ -1,253 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include - -#include -#include - -void die(const char *format, ...) { - fputs("Fatal: ", stderr); - va_list ap; - va_start(ap, format); - vfprintf(stderr, format, ap); - va_end(ap); - fputc('\n', stderr); - exit(EXIT_FAILURE); -} - -#define USAGE "Usage: git-lfs-authenticate upload/download" - -bool hasprefix(const char *str, const char *prefix) { - if (strlen(prefix) > strlen(str)) - return false; - return !strncmp(str, prefix, strlen(prefix)); -} - -bool hassuffix(const char *str, const char *suffix) { - if (strlen(suffix) > strlen(str)) - return false; - str += strlen(str) - strlen(suffix); - return !strcmp(str, suffix); -} - -const char *trimspace(const char *str, size_t *length) { - while (*length > 0 && isspace(str[0])) { - str++; (*length)--; - } - while (*length > 0 && isspace(str[*length - 1])) - (*length)--; - return str; -} - -void printescjson(const char *str) { - for (size_t i = 0; i < strlen(str); i++) { - switch (str[i]) { - case '"': fputs("\\\"", stdout); break; - case '\\': fputs("\\\\", stdout); break; - case '\b': fputs("\\b", stdout); break; - case '\f': fputs("\\f", stdout); break; - case '\n': fputs("\\n", stdout); break; - case '\r': fputs("\\r", stdout); break; - case '\t': fputs("\\t", stdout); break; - default: fputc(str[i], stdout); - } - } -} - -void checkrepopath(const char *path) { - if (strstr(path, "//") || strstr(path, "/./") || strstr(path, "/../") - || hasprefix(path, "./") || hasprefix(path, "../") || hasprefix(path, "/../")) - die("Bad repository name: Is unresolved path"); - if (strlen(path) > 100) - die("Bad repository name: Longer than 100 characters"); - if (hasprefix(path, "/")) - die("Bad repository name: Unexpected absolute path"); - if (!hassuffix(path, ".git")) - die("Bad repository name: Expected '.git' repo path suffix"); - - struct stat statbuf; - if (stat(path, &statbuf)) { - if (errno == ENOENT) - die("Repo not found"); - die("Could not stat repo: %s", strerror(errno)); - } - if (!S_ISDIR(statbuf.st_mode)) { - die("Repo not found"); - } -} - -char *readkeyfile(const char *path, size_t *len) { - FILE *f = fopen(path, "r"); - if (!f) - die("Cannot read key file: %s", strerror(errno)); - *len = 0; - size_t bufcap = 4096; - char *buf = malloc(bufcap); - while (!feof(f) && !ferror(f)) { - if (*len + 4096 > bufcap) { - bufcap *= 2; - buf = realloc(buf, bufcap); - } - *len += fread(buf + *len, sizeof(char), 4096, f); - } - if (ferror(f) && !feof(f)) { - OPENSSL_cleanse(buf, *len); - die("Failed to read key file (length: %lu)", *len); - } - fclose(f); - return buf; -} - -#define KEYSIZE 64 - -void readkey(const char *path, uint8_t dest[KEYSIZE]) { - size_t keybuf_len = 0; - char *keybuf = readkeyfile(path, &keybuf_len); - - size_t keystr_len = keybuf_len; - const char *keystr = trimspace(keybuf, &keystr_len); - if (keystr_len != 2 * KEYSIZE) { - OPENSSL_cleanse(keybuf, keybuf_len); - die("Bad key length"); - } - - for (size_t i = 0; i < KEYSIZE; i++) { - const char c = keystr[i]; - uint8_t nibble = 0; - if (c >= '0' && c <= '9') - nibble = c - '0'; - else if (c >= 'a' && c <= 'f') - nibble = c - 'a' + 10; - else if (c >= 'A' && c <= 'F') - nibble = c - 'A' + 10; - else { - OPENSSL_cleanse(keybuf, keybuf_len); - OPENSSL_cleanse(dest, KEYSIZE); - die("Cannot decode key"); - } - size_t ikey = i / 2; - if (i % 2) dest[ikey] |= nibble; - else dest[ikey] = nibble << 4; - } - - OPENSSL_cleanse(keybuf, keybuf_len); - free(keybuf); -} - -void u64tobe(uint64_t x, uint8_t b[8]) { - b[0] = (uint8_t)(x >> 56); - b[1] = (uint8_t)(x >> 48); - b[2] = (uint8_t)(x >> 40); - b[3] = (uint8_t)(x >> 32); - b[4] = (uint8_t)(x >> 24); - b[5] = (uint8_t)(x >> 16); - b[6] = (uint8_t)(x >> 8); - b[7] = (uint8_t)(x >> 0); -} - -void *memcat(void *dest, const void *src, size_t n) { - return memcpy(dest, src, n) + n; -} - -#define MAX_TAG_SIZE EVP_MAX_MD_SIZE - -typedef struct taginfo { - const char *authtype; - const char *repopath; - const char *operation; - const int64_t expiresat_s; -} taginfo_t; - -void maketag(const taginfo_t info, uint8_t key[KEYSIZE], uint8_t dest[MAX_TAG_SIZE], uint32_t *len) { - uint8_t expiresat_b[8]; - u64tobe(info.expiresat_s, expiresat_b); - - const uint8_t zero[1] = { 0 }; - const size_t fullsize = strlen(info.authtype) + - 1 + strlen(info.repopath) + - 1 + strlen(info.operation) + - 1 + sizeof(expiresat_b); - uint8_t *claimbuf = alloca(fullsize); - uint8_t *head = claimbuf; - head = memcat(head, info.authtype, strlen(info.authtype)); - head = memcat(head, zero, 1); - head = memcat(head, info.repopath, strlen(info.repopath)); - head = memcat(head, zero, 1); - head = memcat(head, info.operation, strlen(info.operation)); - head = memcat(head, zero, 1); - head = memcat(head, expiresat_b, sizeof(expiresat_b)); - assert(head == claimbuf + fullsize); - - memset(dest, 0, MAX_TAG_SIZE); - *len = 0; - if (!HMAC(EVP_sha256(), key, KEYSIZE, claimbuf, fullsize, dest, len)) { - OPENSSL_cleanse(key, KEYSIZE); - die("Failed to generate tag"); - } -} - -#define MAX_HEXTAG_STRLEN MAX_TAG_SIZE * 2 - -void makehextag(const taginfo_t info, uint8_t key[KEYSIZE], char dest[MAX_HEXTAG_STRLEN + 1]) { - uint8_t rawtag[MAX_TAG_SIZE]; - uint32_t rawtag_len; - maketag(info, key, rawtag, &rawtag_len); - - memset(dest, 0, MAX_HEXTAG_STRLEN + 1); - for (size_t i = 0; i < rawtag_len; i++) { - uint8_t b = rawtag[i]; - dest[i * 2] = (b >> 4) + ((b >> 4) < 10 ? '0' : 'a'); - dest[i*2 + 1] = (b & 0x0F) + ((b & 0x0F) < 10 ? '0' : 'a'); - } -} - -int main(int argc, char *argv[]) { - if (argc != 3) { - puts(USAGE); - exit(EXIT_FAILURE); - } - - const char *repopath = argv[1]; - const char *operation = argv[2]; - if (strcmp(operation, "download") && strcmp(operation, "upload")) { - puts(USAGE); - exit(EXIT_FAILURE); - } - checkrepopath(repopath); - - const char *hrefbase = getenv("GITOLFS3_HREF_BASE"); - const char *keypath = getenv("GITOLFS3_KEY_PATH"); - - if (!hrefbase || strlen(hrefbase) == 0) - die("Incomplete configuration: Base URL not provided"); - if (hrefbase[strlen(hrefbase) - 1] != '/') - die("Bad configuration: Base URL should end with slash"); - if (!keypath || strlen(keypath) == 0) - die("Incomplete configuration: Key path not provided"); - - uint8_t key[64]; - readkey(keypath, key); - - int64_t expiresin_s = 5 * 60; - int64_t expiresat_s = (int64_t)time(NULL) + expiresin_s; - - taginfo_t taginfo = { - .authtype = "git-lfs-authenticate", - .repopath = repopath, - .operation = operation, - .expiresat_s = expiresat_s, - }; - char hextag[MAX_HEXTAG_STRLEN + 1]; - makehextag(taginfo, key, hextag); - - printf("{\"header\":{\"Authorization\":\"Gitolfs3-Hmac-Sha256 %s\"}," - "\"expires_in\":%ld,\"href\":\"", hextag, expiresin_s); - printescjson(hrefbase); - printescjson(repopath); - printf("/info/lfs?p=1&te=%ld\"}\n", expiresat_s); -} diff --git a/cmd/git-lfs-authenticate/main.go b/cmd/git-lfs-authenticate/main.go deleted file mode 100644 index 59ed978..0000000 --- a/cmd/git-lfs-authenticate/main.go +++ /dev/null @@ -1,141 +0,0 @@ -package main - -import ( - "bytes" - "crypto/hmac" - "crypto/sha256" - "encoding/binary" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path" - "strings" - "time" -) - -func die(msg string, args ...any) { - fmt.Fprint(os.Stderr, "Fatal: ") - fmt.Fprintf(os.Stderr, msg, args...) - fmt.Fprint(os.Stderr, "\n") - os.Exit(1) -} - -type authenticateResponse struct { - // When providing href, the Git LFS client will use href as the base URL - // instead of building the base URL using the Service Discovery mechanism. - // It should end with /info/lfs. See - // https://github.com/git-lfs/git-lfs/blob/baf40ac99850a62fe98515175d52df5c513463ec/docs/api/server-discovery.md#ssh - HRef string `json:"href,omitempty"` - Header map[string]string `json:"header"` - // In seconds. - ExpiresIn int64 `json:"expires_in,omitempty"` - // The expires_at (RFC3339) property could also be used, but we leave it - // out since we don't use it. The Git LFS docs recommend using expires_in - // instead (???) -} - -func wipe(b []byte) { - for i := range b { - b[i] = 0 - } -} - -const usage = "Usage: git-lfs-authenticate upload/download" - -func main() { - // Even though not explicitly described in the Git LFS documentation, the - // git-lfs-authenticate command is expected to either exit succesfully with - // exit code 0 and to then print credentials in the prescribed JSON format - // to standard out. On errors, the command should exit with a non-zero exit - // code and print the error message in plain text to standard error. See - // https://github.com/git-lfs/git-lfs/blob/baf40ac99850a62fe98515175d52df5c513463ec/lfshttp/ssh.go#L76-L117 - - if len(os.Args) != 3 { - fmt.Println(usage) - os.Exit(1) - } - - repo := strings.TrimPrefix(path.Clean(os.Args[1]), "/") - operation := os.Args[2] - if operation != "download" && operation != "upload" { - fmt.Println(usage) - os.Exit(1) - } - if repo == ".." || strings.HasPrefix(repo, "../") { - die("highly illegal repo name (Anzeige ist raus)") - } - if !strings.HasSuffix(repo, ".git") { - die("expected repo name to have '.git' suffix") - } - - repoDir := path.Join(repo) - finfo, err := os.Stat(repoDir) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - die("repo not found") - } - die("could not stat repo: %s", err) - } - if !finfo.IsDir() { - die("repo not found") - } - - hrefBase := os.Getenv("GITOLFS3_HREF_BASE") - if hrefBase == "" { - die("incomplete configuration: base URL not provided") - } - if !strings.HasSuffix(hrefBase, "/") { - hrefBase += "/" - } - - keyPath := os.Getenv("GITOLFS3_KEY_PATH") - if keyPath == "" { - die("incomplete configuration: key path not provided") - } - - keyStr, err := os.ReadFile(keyPath) - if err != nil { - wipe(keyStr) - die("cannot read key") - } - keyStr = bytes.TrimSpace(keyStr) - defer wipe(keyStr) - if hex.DecodedLen(len(keyStr)) != 64 { - die("bad key length") - } - key := make([]byte, 64) - defer wipe(key) - if _, err = hex.Decode(key, keyStr); err != nil { - die("cannot decode key") - } - - expiresIn := time.Minute * 5 - expiresAtUnix := time.Now().Add(expiresIn).Unix() - - tag := hmac.New(sha256.New, key) - io.WriteString(tag, "git-lfs-authenticate") - tag.Write([]byte{0}) - io.WriteString(tag, repo) - tag.Write([]byte{0}) - io.WriteString(tag, operation) - tag.Write([]byte{0}) - binary.Write(tag, binary.BigEndian, &expiresAtUnix) - tagStr := hex.EncodeToString(tag.Sum(nil)) - - response := authenticateResponse{ - Header: map[string]string{ - "Authorization": "Gitolfs3-Hmac-Sha256 " + tagStr, - }, - ExpiresIn: int64(expiresIn.Seconds()), - HRef: fmt.Sprintf("%s%s?p=1&te=%d", - hrefBase, - path.Join(repo, "/info/lfs"), - expiresAtUnix, - ), - } - json.NewEncoder(os.Stdout).Encode(response) -} diff --git a/cmd/git-lfs-server/main.go b/cmd/git-lfs-server/main.go deleted file mode 100644 index eec7d00..0000000 --- a/cmd/git-lfs-server/main.go +++ /dev/null @@ -1,897 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/ed25519" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "hash" - "io" - "mime" - "net" - "net/http" - "net/url" - "os" - "os/exec" - "path" - "regexp" - "runtime/debug" - "slices" - "strconv" - "strings" - "time" - "unicode" - - "github.com/golang-jwt/jwt/v5" - "github.com/minio/minio-go/v7" - "github.com/minio/minio-go/v7/pkg/credentials" - "github.com/rs/xid" -) - -type operation string -type transferAdapter string -type hashAlgo string - -const ( - operationDownload operation = "download" - operationUpload operation = "upload" - transferAdapterBasic transferAdapter = "basic" - hashAlgoSHA256 hashAlgo = "sha256" -) - -const lfsMIME = "application/vnd.git-lfs+json" - -type batchRef struct { - Name string `json:"name"` -} - -type batchRequestObject struct { - OID string `json:"oid"` - Size int64 `json:"size"` -} - -type batchRequest struct { - Operation operation `json:"operation"` - Transfers []transferAdapter `json:"transfers,omitempty"` - Ref *batchRef `json:"ref,omitempty"` - Objects []batchRequestObject `json:"objects"` - HashAlgo hashAlgo `json:"hash_algo,omitempty"` -} - -type batchAction struct { - HRef string `json:"href"` - Header map[string]string `json:"header,omitempty"` - // In seconds. - ExpiresIn int64 `json:"expires_in,omitempty"` - // expires_at (RFC3339) could also be used, but we leave it out since we - // don't use it. -} - -type batchError struct { - Code int `json:"code"` - Message string `json:"message"` -} - -type batchResponseObject struct { - OID string `json:"oid"` - Size int64 `json:"size"` - Authenticated *bool `json:"authenticated"` - Actions map[operation]batchAction `json:"actions,omitempty"` - Error *batchError `json:"error,omitempty"` -} - -type batchResponse struct { - Transfer transferAdapter `json:"transfer,omitempty"` - Objects []batchResponseObject `json:"objects"` - HashAlgo hashAlgo `json:"hash_algo,omitempty"` -} - -type handler struct { - mc *minio.Client - bucket string - anonUser string - gitolitePath string - privateKey ed25519.PrivateKey - baseURL *url.URL - exportAllForwardedHosts []string -} - -func isValidSHA256Hash(hash string) bool { - if len(hash) != 64 { - return false - } - for _, c := range hash { - if !unicode.Is(unicode.ASCII_Hex_Digit, c) { - return false - } - } - return true -} - -type lfsError struct { - Message string `json:"message"` - DocumentationURL string `json:"documentation_url,omitempty"` - RequestID string `json:"request_id,omitempty"` -} - -func makeRespError(ctx context.Context, w http.ResponseWriter, message string, code int) { - err := lfsError{Message: message} - if val := ctx.Value(requestIDKey); val != nil { - err.RequestID = val.(string) - } - w.Header().Set("Content-Type", lfsMIME+"; charset=utf-8") - w.WriteHeader(code) - json.NewEncoder(w).Encode(err) -} - -func makeObjError(obj parsedBatchObject, message string, code int) batchResponseObject { - return batchResponseObject{ - OID: obj.fullHash, - Size: obj.size, - Error: &batchError{ - Message: message, - Code: code, - }, - } -} - -func sha256AsBase64(hash string) string { - raw, err := hex.DecodeString(hash) - if err != nil { - return "" - } - return base64.StdEncoding.EncodeToString(raw) -} - -func (h *handler) handleDownloadObject(ctx context.Context, repo string, obj parsedBatchObject) batchResponseObject { - fullPath := path.Join(repo+".git", "lfs/objects", obj.firstByte, obj.secondByte, obj.fullHash) - - info, err := h.mc.StatObject(ctx, h.bucket, fullPath, minio.StatObjectOptions{Checksum: true}) - if err != nil { - var resp minio.ErrorResponse - if errors.As(err, &resp) && resp.StatusCode == http.StatusNotFound { - return makeObjError(obj, "Object does not exist", http.StatusNotFound) - } - // TODO: consider not making this an object-specific, but rather a - // generic error such that the entire Batch API request fails. - reqlog(ctx, "Failed to query object information (full path: %s): %s", fullPath, err) - return makeObjError(obj, "Failed to query object information", http.StatusInternalServerError) - } - if info.ChecksumSHA256 != "" && strings.ToLower(info.ChecksumSHA256) != obj.fullHash { - return makeObjError(obj, "Object corrupted", http.StatusUnprocessableEntity) - } - if info.Size != obj.size { - return makeObjError(obj, "Incorrect size specified for object or object currupted", http.StatusUnprocessableEntity) - } - - expiresIn := time.Minute * 10 - claims := handleObjectCustomClaims{ - Gitolfs3: handleObjectGitolfs3Claims{ - Type: "basic-transfer", - Operation: operationDownload, - Repository: repo, - OID: obj.fullHash, - Size: obj.size, - }, - RegisteredClaims: &jwt.RegisteredClaims{ - IssuedAt: jwt.NewNumericDate(time.Now()), - ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiresIn)), - }, - } - - token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims) - ss, err := token.SignedString(h.privateKey) - if err != nil { - // TODO: consider not making this an object-specific, but rather a - // generic error such that the entire Batch API request fails. - reqlog(ctx, "Fatal: failed to generate JWT: %s", err) - return makeObjError(obj, "Failed to generate token", http.StatusInternalServerError) - } - uploadPath := path.Join(repo+".git", "info/lfs/objects", obj.firstByte, obj.secondByte, obj.fullHash) - - authenticated := true - return batchResponseObject{ - OID: obj.fullHash, - Size: obj.size, - Authenticated: &authenticated, - Actions: map[operation]batchAction{ - operationDownload: { - Header: map[string]string{ - "Authorization": "Bearer " + ss, - }, - HRef: h.baseURL.ResolveReference(&url.URL{Path: uploadPath}).String(), - ExpiresIn: int64(expiresIn.Seconds()), - }, - }, - } -} - -type handleObjectGitolfs3Claims struct { - Type string `json:"type"` - Operation operation `json:"operation"` - Repository string `json:"repository"` - OID string `json:"oid"` - Size int64 `json:"size"` -} - -type handleObjectCustomClaims struct { - Gitolfs3 handleObjectGitolfs3Claims `json:"gitolfs3"` - *jwt.RegisteredClaims -} - -// Return nil when the object already exists -func (h *handler) handleUploadObject(ctx context.Context, repo string, obj parsedBatchObject) *batchResponseObject { - fullPath := path.Join(repo+".git", "lfs/objects", obj.firstByte, obj.secondByte, obj.fullHash) - _, err := h.mc.StatObject(ctx, h.bucket, fullPath, minio.GetObjectOptions{}) - if err == nil { - // The object exists - return nil - } - - var resp minio.ErrorResponse - if !errors.As(err, &resp) || resp.StatusCode != http.StatusNotFound { - // TODO: consider not making this an object-specific, but rather a - // generic error such that the entire Batch API request fails. - reqlog(ctx, "Failed to generate action href (full path: %s): %s", fullPath, err) - objErr := makeObjError(obj, "Failed to generate action href", http.StatusInternalServerError) - return &objErr - } - - expiresIn := time.Minute * 10 - claims := handleObjectCustomClaims{ - Gitolfs3: handleObjectGitolfs3Claims{ - Type: "basic-transfer", - Operation: operationUpload, - Repository: repo, - OID: obj.fullHash, - Size: obj.size, - }, - RegisteredClaims: &jwt.RegisteredClaims{ - IssuedAt: jwt.NewNumericDate(time.Now()), - ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiresIn)), - }, - } - - token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims) - ss, err := token.SignedString(h.privateKey) - if err != nil { - // TODO: consider not making this an object-specific, but rather a - // generic error such that the entire Batch API request fails. - reqlog(ctx, "Fatal: failed to generate JWT: %s", err) - objErr := makeObjError(obj, "Failed to generate token", http.StatusInternalServerError) - return &objErr - } - - uploadPath := path.Join(repo+".git", "info/lfs/objects", obj.firstByte, obj.secondByte, obj.fullHash) - uploadHRef := h.baseURL.ResolveReference(&url.URL{Path: uploadPath}).String() - // The object does not exist. - authenticated := true - return &batchResponseObject{ - OID: obj.fullHash, - Size: obj.size, - Authenticated: &authenticated, - Actions: map[operation]batchAction{ - operationUpload: { - Header: map[string]string{ - "Authorization": "Bearer " + ss, - }, - HRef: uploadHRef, - ExpiresIn: int64(expiresIn.Seconds()), - }, - }, - } -} - -type validatingReader struct { - promisedSize int64 - promisedSha256 []byte - - reader io.Reader - bytesRead int64 - current hash.Hash - err error -} - -func newValidatingReader(promisedSize int64, promisedSha256 []byte, r io.Reader) *validatingReader { - return &validatingReader{ - promisedSize: promisedSize, - promisedSha256: promisedSha256, - reader: r, - current: sha256.New(), - } -} - -var errTooBig = errors.New("validator: uploaded file bigger than indicated") -var errTooSmall = errors.New("validator: uploaded file smaller than indicated") -var errBadSum = errors.New("validator: bad checksum provided or file corrupted") - -func (i *validatingReader) Read(b []byte) (int, error) { - if i.err != nil { - return 0, i.err - } - n, err := i.reader.Read(b) - i.bytesRead += int64(n) - if i.bytesRead > i.promisedSize { - i.err = errTooBig - return 0, i.err - } - if err != nil && errors.Is(err, io.EOF) { - if i.bytesRead < i.promisedSize { - i.err = errTooSmall - return n, i.err - } - } - // According to the documentation, Hash.Write never returns an error - i.current.Write(b[:n]) - if i.bytesRead == i.promisedSize { - if !bytes.Equal(i.promisedSha256, i.current.Sum(nil)) { - i.err = errBadSum - return 0, i.err - } - } - return n, err -} - -func (h *handler) handlePutObject(w http.ResponseWriter, r *http.Request, repo, oid string) { - ctx := r.Context() - - authz := r.Header.Get("Authorization") - if authz == "" { - makeRespError(ctx, w, "Missing Authorization header", http.StatusBadRequest) - return - } - if !strings.HasPrefix(authz, "Bearer ") { - makeRespError(ctx, w, "Invalid Authorization header", http.StatusBadRequest) - return - } - authz = strings.TrimPrefix(authz, "Bearer ") - - var claims handleObjectCustomClaims - _, err := jwt.ParseWithClaims(authz, &claims, func(token *jwt.Token) (any, error) { - if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok { - return nil, fmt.Errorf("expected signing method EdDSA, got %s", token.Header["alg"]) - } - return h.privateKey.Public(), nil - }) - if err != nil { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return - } - if claims.Gitolfs3.Type != "basic-transfer" { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return - } - if claims.Gitolfs3.Repository != repo { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return - } - if claims.Gitolfs3.OID != oid { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return - } - if claims.Gitolfs3.Operation != operationUpload { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return - } - - // Check with claims - if lengthStr := r.Header.Get("Content-Length"); lengthStr != "" { - length, err := strconv.ParseInt(lengthStr, 10, 64) - if err != nil { - makeRespError(ctx, w, "Bad Content-Length format", http.StatusBadRequest) - return - } - if length != claims.Gitolfs3.Size { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return - } - } - - sha256Raw, err := hex.DecodeString(oid) - if err != nil || len(sha256Raw) != sha256.Size { - makeRespError(ctx, w, "Invalid OID", http.StatusBadRequest) - return - } - - reader := newValidatingReader(claims.Gitolfs3.Size, sha256Raw, r.Body) - - fullPath := path.Join(repo+".git", "lfs/objects", oid[:2], oid[2:4], oid) - _, err = h.mc.PutObject(ctx, h.bucket, fullPath, reader, int64(claims.Gitolfs3.Size), minio.PutObjectOptions{ - SendContentMd5: true, - }) - if err != nil { - if errors.Is(err, errBadSum) { - makeRespError(ctx, w, "Bad checksum (OID does not match contents)", http.StatusBadRequest) - } else if errors.Is(err, errTooSmall) { - makeRespError(ctx, w, "Uploaded object smaller than expected", http.StatusBadRequest) - } else if errors.Is(err, errTooBig) { - makeRespError(ctx, w, "Uploaded object bigger than expected", http.StatusBadRequest) - } else { - reqlog(ctx, "Failed to upload object: %s", err) - makeRespError(ctx, w, "Failed to upload object", http.StatusInternalServerError) - } - return - } -} - -func (h *handler) handleGetObject(w http.ResponseWriter, r *http.Request, repo, oid string) { - ctx := r.Context() - - authz := r.Header.Get("Authorization") - if authz == "" { - makeRespError(ctx, w, "Missing Authorization header", http.StatusBadRequest) - return - } - if !strings.HasPrefix(authz, "Bearer ") { - makeRespError(ctx, w, "Invalid Authorization header", http.StatusBadRequest) - return - } - authz = strings.TrimPrefix(authz, "Bearer ") - - var claims handleObjectCustomClaims - _, err := jwt.ParseWithClaims(authz, &claims, func(token *jwt.Token) (any, error) { - if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok { - return nil, fmt.Errorf("expected signing method EdDSA, got %s", token.Header["alg"]) - } - return h.privateKey.Public(), nil - }) - if err != nil { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return - } - if claims.Gitolfs3.Type != "basic-transfer" { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return - } - if claims.Gitolfs3.Repository != repo { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return - } - if claims.Gitolfs3.OID != oid { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return - } - if claims.Gitolfs3.Operation != operationDownload { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return - } - - sha256Raw, err := hex.DecodeString(oid) - if err != nil || len(sha256Raw) != sha256.Size { - makeRespError(ctx, w, "Invalid OID", http.StatusBadRequest) - return - } - - fullPath := path.Join(repo+".git", "lfs/objects", oid[:2], oid[2:4], oid) - obj, err := h.mc.GetObject(ctx, h.bucket, fullPath, minio.GetObjectOptions{}) - - var resp minio.ErrorResponse - if errors.As(err, &resp) && resp.StatusCode != http.StatusNotFound { - makeRespError(ctx, w, "Not found", http.StatusNotFound) - return - } else if err != nil { - reqlog(ctx, "Failed to get object: %s", err) - makeRespError(ctx, w, "Failed to get object", http.StatusInternalServerError) - return - } - - stat, err := obj.Stat() - if err != nil { - reqlog(ctx, "Failed to stat: %s", err) - makeRespError(ctx, w, "Internal server error", http.StatusInternalServerError) - return - } - - if stat.Size != claims.Gitolfs3.Size { - reqlog(ctx, "Claims size does not match S3 object size") - makeRespError(ctx, w, "Internal server error", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Length", strconv.FormatInt(claims.Gitolfs3.Size, 10)) - w.WriteHeader(http.StatusOK) - - vr := newValidatingReader(claims.Gitolfs3.Size, sha256Raw, obj) - _, err = io.Copy(w, vr) - if errors.Is(err, errBadSum) { - reqlog(ctx, "Bad object checksum") - } -} - -type parsedBatchObject struct { - firstByte string - secondByte string - fullHash string - size int64 -} - -func isLFSMediaType(t string) bool { - if mediaType, params, err := mime.ParseMediaType(t); err == nil { - if mediaType == lfsMIME { - if params["charset"] == "" || strings.ToLower(params["charset"]) == "utf-8" { - return true - } - } - } - return false -} - -var reBatchAPI = regexp.MustCompile(`^([a-zA-Z0-9-_/]+)\.git/info/lfs/objects/batch$`) -var reObjUpload = regexp.MustCompile(`^([a-zA-Z0-9-_/]+)\.git/info/lfs/objects/([0-9a-f]{2})/([0-9a-f]{2})/([0-9a-f]{64})$`) - -type requestID struct{} - -var requestIDKey requestID - -// TODO: make a shared package for this -type lfsAuthGitolfs3Claims struct { - Type string `json:"type"` - Repository string `json:"repository"` - Permission operation `json:"permission"` -} - -type lfsAuthCustomClaims struct { - Gitolfs3 lfsAuthGitolfs3Claims `json:"gitolfs3"` - *jwt.RegisteredClaims -} - -// Request to perform in [on reference ] -type operationRequest struct { - operation operation - repository string - refspec *string -} - -func (h *handler) getGitoliteAccess(repo, user, gitolitePerm string, refspec *string) (bool, error) { - // gitolite access -q: returns only exit code - gitoliteArgs := []string{"access", "-q", repo, user, gitolitePerm} - if refspec != nil { - gitoliteArgs = append(gitoliteArgs, *refspec) - } - cmd := exec.Command(h.gitolitePath, gitoliteArgs...) - err := cmd.Run() - if err != nil { - var exitErr *exec.ExitError - if !errors.As(err, &exitErr) { - return false, fmt.Errorf("(running %s): %w", cmd, err) - } - return false, nil - } - return true, nil -} - -func (h *handler) authorizeBatchAPI(w http.ResponseWriter, r *http.Request, or operationRequest) bool { - user := h.anonUser - ctx := r.Context() - - if or.operation == operationDownload { - // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host - forwardedHost := r.Header.Get("X-Forwarded-Host") - if forwardedHost != "" && slices.Contains(h.exportAllForwardedHosts, forwardedHost) { - // This is a forwarded host for which all repositories are exported, - // regardless of ownership configuration in Gitolite. - return true - } - } - - if authz := r.Header.Get("Authorization"); authz != "" { - if !strings.HasPrefix(authz, "Bearer ") { - makeRespError(ctx, w, "Invalid Authorization header", http.StatusBadRequest) - return false - } - authz = strings.TrimPrefix(authz, "Bearer ") - - var claims lfsAuthCustomClaims - _, err := jwt.ParseWithClaims(authz, &claims, func(token *jwt.Token) (any, error) { - if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok { - return nil, fmt.Errorf("expected signing method EdDSA, got %s", token.Header["alg"]) - } - return h.privateKey.Public(), nil - }) - if err != nil { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return false - } - - if claims.Gitolfs3.Type != "batch-api" { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return false - } - if claims.Gitolfs3.Repository != or.repository { - makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized) - return false - } - if claims.Gitolfs3.Permission == operationDownload && or.operation == operationUpload { - makeRespError(ctx, w, "Forbidden", http.StatusForbidden) - return false - } - - user = claims.Subject - } - - readAccess, err := h.getGitoliteAccess(or.repository, user, "R", or.refspec) - if err != nil { - reqlog(ctx, "Error checking access info: %s", err) - makeRespError(ctx, w, "Failed to query access information", http.StatusInternalServerError) - return false - } - if !readAccess { - makeRespError(ctx, w, "Repository not found", http.StatusNotFound) - return false - } - if or.operation == operationUpload { - writeAccess, err := h.getGitoliteAccess(or.repository, user, "W", or.refspec) - if err != nil { - reqlog(ctx, "Error checking access info: %s", err) - makeRespError(ctx, w, "Failed to query access information", http.StatusInternalServerError) - return false - } - // User has read access but no write access - if !writeAccess { - makeRespError(ctx, w, "Forbidden", http.StatusForbidden) - return false - } - } - - return true -} - -func (h *handler) handleBatchAPI(w http.ResponseWriter, r *http.Request, repo string) { - ctx := r.Context() - - if !slices.ContainsFunc(r.Header.Values("Accept"), isLFSMediaType) { - makeRespError(ctx, w, "Expected "+lfsMIME+" (with UTF-8 charset) in list of acceptable response media types", http.StatusNotAcceptable) - return - } - if !isLFSMediaType(r.Header.Get("Content-Type")) { - makeRespError(ctx, w, "Expected request Content-Type to be "+lfsMIME+" (with UTF-8 charset)", http.StatusUnsupportedMediaType) - return - } - - var body batchRequest - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - makeRespError(ctx, w, "Failed to parse request body as JSON", http.StatusBadRequest) - return - } - if body.Operation != operationDownload && body.Operation != operationUpload { - makeRespError(ctx, w, "Invalid operation specified", http.StatusBadRequest) - return - } - - or := operationRequest{ - operation: body.Operation, - repository: repo, - } - if body.Ref != nil { - or.refspec = &body.Ref.Name - } - if !h.authorizeBatchAPI(w, r.WithContext(ctx), or) { - return - } - - if body.HashAlgo != hashAlgoSHA256 { - makeRespError(ctx, w, "Unsupported hash algorithm specified", http.StatusConflict) - return - } - - if len(body.Transfers) != 0 && !slices.Contains(body.Transfers, transferAdapterBasic) { - makeRespError(ctx, w, "Unsupported transfer adapter specified (supported: basic)", http.StatusConflict) - return - } - - var objects []parsedBatchObject - for _, obj := range body.Objects { - oid := strings.ToLower(obj.OID) - if !isValidSHA256Hash(oid) { - makeRespError(ctx, w, "Invalid hash format in object ID", http.StatusBadRequest) - return - } - objects = append(objects, parsedBatchObject{ - firstByte: oid[:2], - secondByte: oid[2:4], - fullHash: oid, - size: obj.Size, - }) - } - - resp := batchResponse{ - Transfer: transferAdapterBasic, - HashAlgo: hashAlgoSHA256, - } - for _, obj := range objects { - switch body.Operation { - case operationDownload: - resp.Objects = append(resp.Objects, h.handleDownloadObject(ctx, repo, obj)) - case operationUpload: - if respObj := h.handleUploadObject(ctx, repo, obj); respObj != nil { - resp.Objects = append(resp.Objects, *respObj) - } - } - } - - w.Header().Set("Content-Type", lfsMIME) - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(resp) -} - -func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - reqID := xid.New().String() - ctx := context.WithValue(r.Context(), requestIDKey, reqID) - w.Header().Set("X-Request-Id", reqID) - - defer func() { - if r := recover(); r != nil { - reqlog(ctx, "Panic when serving request: %s", debug.Stack()) - } - }() - - reqPath := strings.TrimPrefix(path.Clean(r.URL.Path), "/") - - if submatches := reBatchAPI.FindStringSubmatch(reqPath); len(submatches) == 2 { - if r.Method != http.MethodPost { - makeRespError(ctx, w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - repo := strings.TrimPrefix(path.Clean(submatches[1]), "/") - reqlog(ctx, "Handling batch API request for repository: %s", repo) - - h.handleBatchAPI(w, r.WithContext(ctx), repo) - return - } - - if submatches := reObjUpload.FindStringSubmatch(reqPath); len(submatches) == 5 { - oid0, oid1, oid := submatches[2], submatches[3], submatches[4] - - if !isValidSHA256Hash(oid) { - panic("Regex should only allow valid SHA256 hashes") - } - if oid0 != oid[:2] || oid1 != oid[2:4] { - makeRespError(ctx, w, "Bad URL format: malformed OID pattern", http.StatusBadRequest) - return - } - - repo := strings.TrimPrefix(path.Clean(submatches[1]), "/") - reqlog(ctx, "Handling object PUT for repository: %s, OID: %s", repo, oid) - - switch r.Method { - case http.MethodGet: - h.handleGetObject(w, r.WithContext(ctx), repo, oid) - case http.MethodPut: - h.handlePutObject(w, r.WithContext(ctx), repo, oid) - default: - makeRespError(ctx, w, "Method not allowed", http.StatusMethodNotAllowed) - } - - return - } - - makeRespError(ctx, w, "Not found", http.StatusNotFound) -} - -func reqlog(ctx context.Context, msg string, args ...any) { - if val := ctx.Value(requestIDKey); val != nil { - fmt.Fprintf(os.Stderr, "[%s] ", val.(string)) - } - fmt.Fprintf(os.Stderr, msg, args...) - fmt.Fprint(os.Stderr, "\n") -} - -func log(msg string, args ...any) { - fmt.Fprintf(os.Stderr, msg, args...) - fmt.Fprint(os.Stderr, "\n") -} - -func die(msg string, args ...any) { - log("Environment variables: (dying)") - for _, s := range os.Environ() { - log(" %s", s) - } - log(msg, args...) - os.Exit(1) -} - -func loadPrivateKey(path string) ed25519.PrivateKey { - raw, err := os.ReadFile(path) - if err != nil { - die("Failed to open specified public key: %s", err) - } - raw = bytes.TrimSpace(raw) - - if hex.DecodedLen(len(raw)) != ed25519.SeedSize { - die("Specified public key file does not contain key (seed) of appropriate length") - } - decoded := make([]byte, hex.DecodedLen(len(raw))) - if _, err = hex.Decode(decoded, raw); err != nil { - die("Failed to decode specified public key: %s", err) - } - return ed25519.NewKeyFromSeed(decoded) -} - -func wipe(b []byte) { - for i := range b { - b[i] = 0 - } -} - -func main() { - anonUser := os.Getenv("GITOLFS3_ANON_USER") - privateKeyPath := os.Getenv("GITOLFS3_PRIVATE_KEY_PATH") - endpoint := os.Getenv("GITOLFS3_S3_ENDPOINT") - bucket := os.Getenv("GITOLFS3_S3_BUCKET") - accessKeyIDFile := os.Getenv("GITOLFS3_S3_ACCESS_KEY_ID_FILE") - secretAccessKeyFile := os.Getenv("GITOLFS3_S3_SECRET_ACCESS_KEY_FILE") - gitolitePath := os.Getenv("GITOLFS3_GITOLITE_PATH") - baseURLStr := os.Getenv("GITOLFS3_BASE_URL") - listenHost := os.Getenv("GITOLFS3_LISTEN_HOST") - listenPort := os.Getenv("GITOLFS3_LISTEN_PORT") - exportAllForwardedHostsStr := os.Getenv("GITOLFS3_EXPORT_ALL_FORWARDED_HOSTS") - - listenAddr := net.JoinHostPort(listenHost, listenPort) - exportAllForwardedHosts := strings.Split(exportAllForwardedHostsStr, ",") - - if gitolitePath == "" { - gitolitePath = "gitolite" - } - - if anonUser == "" { - die("Fatal: expected environment variable GITOLFS3_ANON_USER to be set") - } - if privateKeyPath == "" { - die("Fatal: expected environment variable GITOLFS3_PRIVATE_KEY_PATH to be set") - } - if listenPort == "" { - die("Fatal: expected environment variable GITOLFS3_LISTEN_PORT to be set") - } - if baseURLStr == "" { - die("Fatal: expected environment variable GITOLFS3_BASE_URL to be set") - } - if endpoint == "" { - die("Fatal: expected environment variable GITOLFS3_S3_ENDPOINT to be set") - } - if bucket == "" { - die("Fatal: expected environment variable GITOLFS3_S3_BUCKET to be set") - } - - if accessKeyIDFile == "" { - die("Fatal: expected environment variable GITOLFS3_S3_ACCESS_KEY_ID_FILE to be set") - } - if secretAccessKeyFile == "" { - die("Fatal: expected environment variable GITOLFS3_S3_SECRET_ACCESS_KEY_FILE to be set") - } - - accessKeyID, err := os.ReadFile(accessKeyIDFile) - if err != nil { - die("Fatal: failed to read access key ID from specified file: %s", err) - } - secretAccessKey, err := os.ReadFile(secretAccessKeyFile) - if err != nil { - die("Fatal: failed to read secret access key from specified file: %s", err) - } - - privateKey := loadPrivateKey(privateKeyPath) - defer wipe(privateKey) - - baseURL, err := url.Parse(baseURLStr) - if err != nil { - die("Fatal: provided BASE_URL has bad format: %s", err) - } - - mc, err := minio.New(endpoint, &minio.Options{ - Creds: credentials.NewStaticV4(string(accessKeyID), string(secretAccessKey), ""), - Secure: true, - }) - if err != nil { - die("Fatal: failed to create S3 client: %s", err) - } - - h := &handler{mc, bucket, anonUser, gitolitePath, privateKey, baseURL, exportAllForwardedHosts} - if err = http.ListenAndServe(listenAddr, h); err != nil { - die("Fatal: failed to serve CGI: %s", err) - } -} diff --git a/cmd/gitolfs3-gen-ed25519-key/main.go b/cmd/gitolfs3-gen-ed25519-key/main.go deleted file mode 100644 index 8288fd1..0000000 --- a/cmd/gitolfs3-gen-ed25519-key/main.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "crypto/ed25519" - "crypto/rand" - "encoding/hex" - "fmt" - "os" -) - -func wipe(b []byte) { - for i := range b { - b[i] = 0 - } -} - -func main() { - publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to generate ED25519 key: %s", err) - os.Exit(1) - } - defer wipe(privateKey) - - enc := hex.NewEncoder(os.Stdout) - print("Public ") - enc.Write(publicKey) - print("\nPrivate ") - enc.Write(privateKey.Seed()) - println() -} diff --git a/common/Cargo.toml b/common/Cargo.toml new file mode 100644 index 0000000..20d9bdd --- /dev/null +++ b/common/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "common" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = "0.4" +hmac-sha256 = "1.1" +subtle = "2.5" +serde = { version = "1", features = ["derive"] } diff --git a/common/src/lib.rs b/common/src/lib.rs new file mode 100644 index 0000000..995352d --- /dev/null +++ b/common/src/lib.rs @@ -0,0 +1,368 @@ +use chrono::{DateTime, Utc}; +use serde::de; +use serde::{Deserialize, Serialize}; +use std::fmt::Write; +use std::ops; +use std::{fmt, str::FromStr}; +use subtle::ConstantTimeEq; + +#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] +#[repr(u8)] +pub enum Operation { + #[serde(rename = "download")] + Download = 1, + #[serde(rename = "upload")] + Upload = 2, +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct ParseOperationError; + +impl fmt::Display for ParseOperationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "operation should be 'download' or 'upload'") + } +} + +impl FromStr for Operation { + type Err = ParseOperationError; + + fn from_str(s: &str) -> Result { + match s { + "upload" => Ok(Self::Upload), + "download" => Ok(Self::Download), + _ => Err(ParseOperationError), + } + } +} + +#[repr(u8)] +enum AuthType { + BatchApi = 1, + Download = 2, +} + +/// None means out of range. +fn decode_nibble(c: u8) -> Option { + if c.is_ascii_digit() { + Some(c - b'0') + } else if (b'a'..=b'f').contains(&c) { + Some(c - b'a' + 10) + } else if (b'A'..=b'F').contains(&c) { + Some(c - b'A' + 10) + } else { + None + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct HexByte(pub u8); + +impl<'de> Deserialize<'de> for HexByte { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let str = <&str>::deserialize(deserializer)?; + let &[b1, b2] = str.as_bytes() else { + return Err(de::Error::invalid_length( + str.len(), + &"two hexadecimal characters", + )); + }; + let (Some(b1), Some(b2)) = (decode_nibble(b1), decode_nibble(b2)) else { + return Err(de::Error::invalid_value( + de::Unexpected::Str(str), + &"two hexadecimal characters", + )); + }; + Ok(HexByte((b1 << 4) | b2)) + } +} + +impl fmt::Display for HexByte { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let &HexByte(b) = self; + HexFmt(&[b]).fmt(f) + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum ParseHexError { + UnevenNibbles, + InvalidCharacter, + TooShort, + TooLong, +} + +impl fmt::Display for ParseHexError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnevenNibbles => { + write!(f, "uneven amount of nibbles (chars in range [a-zA-Z0-9])") + } + Self::InvalidCharacter => write!(f, "non-hex character encountered"), + Self::TooShort => write!(f, "unexpected end of hex sequence"), + Self::TooLong => write!(f, "longer hex sequence than expected"), + } + } +} + +#[derive(Debug)] +pub enum ReadHexError { + Io(std::io::Error), + Format(ParseHexError), +} + +impl fmt::Display for ReadHexError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Io(e) => e.fmt(f), + Self::Format(e) => e.fmt(f), + } + } +} + +fn parse_hex_exact(value: &str, buf: &mut [u8]) -> Result<(), ParseHexError> { + if value.bytes().len() % 2 == 1 { + return Err(ParseHexError::UnevenNibbles); + } + if value.bytes().len() < 2 * buf.len() { + return Err(ParseHexError::TooShort); + } + if value.bytes().len() > 2 * buf.len() { + return Err(ParseHexError::TooLong); + } + for (i, c) in value.bytes().enumerate() { + if let Some(b) = decode_nibble(c) { + if i % 2 == 0 { + buf[i / 2] = b << 4; + } else { + buf[i / 2] |= b; + } + } else { + return Err(ParseHexError::InvalidCharacter); + } + } + Ok(()) +} + +pub struct SafeByteArray { + inner: [u8; N], +} + +impl SafeByteArray { + pub fn new() -> Self { + Self { inner: [0; N] } + } +} + +impl Default for SafeByteArray { + fn default() -> Self { + Self::new() + } +} + +impl AsRef<[u8]> for SafeByteArray { + fn as_ref(&self) -> &[u8] { + &self.inner + } +} + +impl AsMut<[u8]> for SafeByteArray { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.inner + } +} + +impl Drop for SafeByteArray { + fn drop(&mut self) { + self.inner.fill(0) + } +} + +impl FromStr for SafeByteArray { + type Err = ParseHexError; + + fn from_str(value: &str) -> Result { + let mut sba = Self { inner: [0u8; N] }; + parse_hex_exact(value, &mut sba.inner)?; + Ok(sba) + } +} + +pub type Oid = Digest<32>; + +#[derive(Debug, Copy, Clone)] +pub enum SpecificClaims { + BatchApi(Operation), + Download(Oid), +} + +#[derive(Debug, Copy, Clone)] +pub struct Claims<'a> { + pub specific_claims: SpecificClaims, + pub repo_path: &'a str, + pub expires_at: DateTime, +} + +/// Returns None if the claims are invalid. Repo path length may be no more than 100 bytes. +pub fn generate_tag(claims: Claims, key: impl AsRef<[u8]>) -> Option> { + if claims.repo_path.len() > 100 { + return None; + } + + let mut hmac = hmac_sha256::HMAC::new(key); + match claims.specific_claims { + SpecificClaims::BatchApi(operation) => { + hmac.update([AuthType::BatchApi as u8]); + hmac.update([operation as u8]); + } + SpecificClaims::Download(oid) => { + hmac.update([AuthType::Download as u8]); + hmac.update(oid.as_bytes()); + } + } + hmac.update([claims.repo_path.len() as u8]); + hmac.update(claims.repo_path.as_bytes()); + hmac.update(claims.expires_at.timestamp().to_be_bytes()); + Some(hmac.finalize().into()) +} + +pub struct HexFmt>(pub B); + +impl> fmt::Display for HexFmt { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let HexFmt(buf) = self; + for b in buf.as_ref() { + let (high, low) = (b >> 4, b & 0xF); + let highc = if high < 10 { + high + b'0' + } else { + high - 10 + b'a' + }; + let lowc = if low < 10 { + low + b'0' + } else { + low - 10 + b'a' + }; + f.write_char(highc as char)?; + f.write_char(lowc as char)?; + } + Ok(()) + } +} + +pub struct EscJsonFmt<'a>(pub &'a str); + +impl<'a> fmt::Display for EscJsonFmt<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let EscJsonFmt(buf) = self; + for c in buf.chars() { + match c { + '"' => f.write_str("\\\"")?, // quote + '\\' => f.write_str("\\\\")?, // backslash + '\x08' => f.write_str("\\b")?, // backspace + '\x0C' => f.write_str("\\f")?, // form feed + '\n' => f.write_str("\\n")?, // line feed + '\r' => f.write_str("\\r")?, // carriage return + '\t' => f.write_str("\\t")?, // horizontal tab + _ => f.write_char(c)?, + }; + } + Ok(()) + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Digest { + inner: [u8; N], +} + +impl ops::Index for Digest { + type Output = u8; + + fn index(&self, index: usize) -> &Self::Output { + &self.inner[index] + } +} + +impl Digest { + pub fn as_bytes(&self) -> &[u8; N] { + &self.inner + } +} + +impl fmt::Display for Digest { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + HexFmt(&self.inner).fmt(f) + } +} + +impl Digest { + pub fn new(data: [u8; N]) -> Self { + Self { inner: data } + } +} + +impl From<[u8; N]> for Digest { + fn from(value: [u8; N]) -> Self { + Self::new(value) + } +} + +impl From> for [u8; N] { + fn from(val: Digest) -> Self { + val.inner + } +} + +impl FromStr for Digest { + type Err = ParseHexError; + + fn from_str(value: &str) -> Result { + let mut buf = [0u8; N]; + parse_hex_exact(value, &mut buf)?; + Ok(buf.into()) + } +} + +impl ConstantTimeEq for Digest { + fn ct_eq(&self, other: &Self) -> subtle::Choice { + self.inner.ct_eq(&other.inner) + } +} + +impl PartialEq for Digest { + fn eq(&self, other: &Self) -> bool { + self.ct_eq(other).into() + } +} + +impl Eq for Digest {} + +impl<'de, const N: usize> Deserialize<'de> for Digest { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let hex = <&str>::deserialize(deserializer)?; + Digest::from_str(hex).map_err(de::Error::custom) + } +} + +impl Serialize for Digest { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&format!("{self}")) + } +} + +pub type Key = SafeByteArray<64>; + +pub fn load_key(path: &str) -> Result { + let key_str = std::fs::read_to_string(path).map_err(ReadHexError::Io)?; + key_str.trim().parse().map_err(ReadHexError::Format) +} diff --git a/flake.nix b/flake.nix index db5fc93..458ecac 100644 --- a/flake.nix +++ b/flake.nix @@ -17,19 +17,14 @@ craneLib = crane.lib.${system}; - gitolfs3 = pkgs.buildGoModule { - name = "gitolfs3"; - src = ./.; - vendorHash = "sha256-3JfeOHbqcgv4D3r/W4FwrXRs1raiQeOxifhO7qH5Wnc="; + gitolfs3 = craneLib.buildPackage { + pname = "gitolfs3"; + version = "0.1.0"; + src = craneLib.cleanCargoSource (craneLib.path ./.); }; in { packages.gitolfs3 = gitolfs3; - packages.gitolfs3-rs = craneLib.buildPackage { - pname = "gitolfs3"; - version = "0.1.0"; - src = craneLib.cleanCargoSource (craneLib.path ./rs); - }; packages.default = self.packages.${system}.gitolfs3; devShells.default = pkgs.mkShell { diff --git a/git-lfs-authenticate/Cargo.toml b/git-lfs-authenticate/Cargo.toml new file mode 100644 index 0000000..217250f --- /dev/null +++ b/git-lfs-authenticate/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "git-lfs-authenticate" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = "0.4" +common = { path = "../common" } diff --git a/git-lfs-authenticate/src/main.rs b/git-lfs-authenticate/src/main.rs new file mode 100644 index 0000000..36d7818 --- /dev/null +++ b/git-lfs-authenticate/src/main.rs @@ -0,0 +1,236 @@ +use std::{fmt, process::ExitCode, time::Duration}; + +use chrono::Utc; +use common::{Operation, ParseOperationError}; + +fn help() { + eprintln!("Usage: git-lfs-authenticate upload/download"); +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +enum RepoNameError { + TooLong, + UnresolvedPath, + AbsolutePath, + MissingGitSuffix, +} + +impl fmt::Display for RepoNameError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::TooLong => write!(f, "too long (more than 100 characters)"), + Self::UnresolvedPath => { + write!(f, "contains path one or more path elements '.' and '..'") + } + Self::AbsolutePath => { + write!(f, "starts with '/', which is not allowed") + } + Self::MissingGitSuffix => write!(f, "misses '.git' suffix"), + } + } +} + +// Using `Result<(), E>` here instead of `Option` because `None` typically signifies some error +// state with no further details provided. If we were to return an `Option` type, the user would +// have to first transform it into a `Result` type in order to use the `?` operator, meaning that +// they would have to the following operation to get the type into the right shape: +// `validate_repo_path(path).map_or(Ok(()), Err)`. That would not be very ergonomic. +fn validate_repo_path(path: &str) -> Result<(), RepoNameError> { + if path.len() > 100 { + return Err(RepoNameError::TooLong); + } + if path.contains("//") + || path.contains("/./") + || path.contains("/../") + || path.starts_with("./") + || path.starts_with("../") + { + return Err(RepoNameError::UnresolvedPath); + } + if path.starts_with('/') { + return Err(RepoNameError::AbsolutePath); + } + if !path.ends_with(".git") { + return Err(RepoNameError::MissingGitSuffix); + } + Ok(()) +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +enum ParseCmdlineError { + UnknownOperation(ParseOperationError), + InvalidRepoName(RepoNameError), + UnexpectedArgCount(ArgCountError), +} + +impl From for ParseCmdlineError { + fn from(value: RepoNameError) -> Self { + Self::InvalidRepoName(value) + } +} + +impl From for ParseCmdlineError { + fn from(value: ParseOperationError) -> Self { + Self::UnknownOperation(value) + } +} + +impl From for ParseCmdlineError { + fn from(value: ArgCountError) -> Self { + Self::UnexpectedArgCount(value) + } +} + +impl fmt::Display for ParseCmdlineError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::UnknownOperation(e) => write!(f, "unknown operation: {e}"), + Self::InvalidRepoName(e) => write!(f, "invalid repository name: {e}"), + Self::UnexpectedArgCount(e) => e.fmt(f), + } + } +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +struct ArgCountError { + provided: usize, + expected: usize, +} + +impl fmt::Display for ArgCountError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "got {} argument(s), expected {}", + self.provided, self.expected + ) + } +} + +fn get_cmdline_args() -> Result<[String; N], ArgCountError> { + let args = std::env::args(); + if args.len() - 1 != N { + return Err(ArgCountError { + provided: args.len() - 1, + expected: N, + }); + } + + // Does not allocate. + const EMPTY_STRING: String = String::new(); + let mut values = [EMPTY_STRING; N]; + + // Skip the first element; we do not care about the program name. + for (i, arg) in args.skip(1).enumerate() { + values[i] = arg + } + Ok(values) +} + +fn parse_cmdline() -> Result<(String, Operation), ParseCmdlineError> { + let [repo_path, op_str] = get_cmdline_args::<2>()?; + let op: Operation = op_str.parse()?; + validate_repo_path(&repo_path)?; + Ok((repo_path.to_string(), op)) +} + +fn repo_exists(name: &str) -> bool { + match std::fs::metadata(name) { + Ok(metadata) => metadata.is_dir(), + _ => false, + } +} + +struct Config { + href_base: String, + key_path: String, +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +enum LoadConfigError { + BaseUrlNotProvided, + BaseUrlSlashSuffixMissing, + KeyPathNotProvided, +} + +impl fmt::Display for LoadConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BaseUrlNotProvided => write!(f, "base URL not provided"), + Self::BaseUrlSlashSuffixMissing => write!(f, "base URL does not end with slash"), + Self::KeyPathNotProvided => write!(f, "key path not provided"), + } + } +} + +fn load_config() -> Result { + let Ok(href_base) = std::env::var("GITOLFS3_HREF_BASE") else { + return Err(LoadConfigError::BaseUrlNotProvided); + }; + if !href_base.ends_with('/') { + return Err(LoadConfigError::BaseUrlSlashSuffixMissing); + } + let Ok(key_path) = std::env::var("GITOLFS3_KEY_PATH") else { + return Err(LoadConfigError::KeyPathNotProvided); + }; + Ok(Config { + href_base, + key_path, + }) +} + +fn main() -> ExitCode { + let config = match load_config() { + Ok(config) => config, + Err(e) => { + eprintln!("Failed to load config: {e}"); + return ExitCode::FAILURE; + } + }; + let key = match common::load_key(&config.key_path) { + Ok(key) => key, + Err(e) => { + eprintln!("Failed to load key: {e}"); + return ExitCode::FAILURE; + } + }; + + let (repo_name, operation) = match parse_cmdline() { + Ok(args) => args, + Err(e) => { + eprintln!("Error: {e}\n"); + help(); + // Exit code 2 signifies bad usage of CLI. + return ExitCode::from(2); + } + }; + + if !repo_exists(&repo_name) { + eprintln!("Error: repository does not exist"); + return ExitCode::FAILURE; + } + + let expires_at = Utc::now() + Duration::from_secs(5 * 60); + let Some(tag) = common::generate_tag( + common::Claims { + specific_claims: common::SpecificClaims::BatchApi(operation), + repo_path: &repo_name, + expires_at, + }, + key, + ) else { + eprintln!("Failed to generate validation tag"); + return ExitCode::FAILURE; + }; + + println!( + "{{\"header\":{{\"Authorization\":\"Gitolfs3-Hmac-Sha256 {tag} {}\"}},\ + \"expires_at\":\"{}\",\"href\":\"{}{}/info/lfs\"}}", + expires_at.timestamp(), + common::EscJsonFmt(&expires_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)), + common::EscJsonFmt(&config.href_base), + common::EscJsonFmt(&repo_name), + ); + + ExitCode::SUCCESS +} diff --git a/go.mod b/go.mod deleted file mode 100644 index f6e0916..0000000 --- a/go.mod +++ /dev/null @@ -1,27 +0,0 @@ -module git.fautchen.eu/gitolfs3 - -go 1.21.4 - -require ( - github.com/golang-jwt/jwt/v5 v5.2.0 - github.com/minio/minio-go/v7 v7.0.66 - github.com/rs/xid v1.5.0 -) - -require ( - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/google/uuid v1.5.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.4 // indirect - github.com/klauspost/cpuid/v2 v2.2.6 // indirect - github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/sha256-simd v1.0.1 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - golang.org/x/crypto v0.16.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 2097c78..0000000 --- a/go.sum +++ /dev/null @@ -1,53 +0,0 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= -github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= -github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= -github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw= -github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs= -github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= -github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/rs/.gitignore b/rs/.gitignore deleted file mode 100644 index b83d222..0000000 --- a/rs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/rs/Cargo.lock b/rs/Cargo.lock deleted file mode 100644 index f3beb9e..0000000 --- a/rs/Cargo.lock +++ /dev/null @@ -1,2364 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "aho-corasick" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" -dependencies = [ - "memchr", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "async-trait" -version = "0.1.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "aws-config" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e64b72d4bdbb41a73d27709c65a25b6e4bfc8321bf70fa3a8b19ce7d4eb81b0" -dependencies = [ - "aws-credential-types", - "aws-http", - "aws-runtime", - "aws-sdk-sso", - "aws-sdk-ssooidc", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "hex", - "http 0.2.11", - "hyper 0.14.28", - "ring", - "time", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-credential-types" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a7cb3510b95492bd9014b60e2e3bee3e48bc516e220316f8e6b60df18b47331" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", -] - -[[package]] -name = "aws-http" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a95d41abe4e941399fdb4bc2f54713eac3c839d98151875948bb24e66ab658f2" -dependencies = [ - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "http 0.2.11", - "http-body 0.4.6", - "pin-project-lite", - "tracing", -] - -[[package]] -name = "aws-runtime" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cca219c6705d525ace011d6f9bc51aaf32fce5b4c41661d2d7ff22d9b4d49" -dependencies = [ - "aws-credential-types", - "aws-http", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "fastrand", - "http 0.2.11", - "percent-encoding", - "tracing", - "uuid", -] - -[[package]] -name = "aws-sdk-s3" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634fbe5b6591ee2e281cd2ba8641e9bd752dbf5bf338924d6ad4bd5a3304fe31" -dependencies = [ - "aws-credential-types", - "aws-http", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-checksums", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "bytes", - "http 0.2.11", - "http-body 0.4.6", - "once_cell", - "percent-encoding", - "regex-lite", - "tracing", - "url", -] - -[[package]] -name = "aws-sdk-sso" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee41005e0f3a19ae749c7953d9e1f1ef8d2183f76f64966e346fa41c1ba0ed44" -dependencies = [ - "aws-credential-types", - "aws-http", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "http 0.2.11", - "once_cell", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-ssooidc" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa08168f8a27505e7b90f922c32a489feb1f2133878981a15138bebc849ac09c" -dependencies = [ - "aws-credential-types", - "aws-http", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "http 0.2.11", - "once_cell", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29102eff04d50ef70f11a48823db33e33c6cc5f027bfb6ff4864efbd5f1f66f3" -dependencies = [ - "aws-credential-types", - "aws-http", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "http 0.2.11", - "once_cell", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b92384b39aedb258aa734fe0e7b2ffcd13f33e68227251a72cd2635e0acc8f1a" -dependencies = [ - "aws-credential-types", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "crypto-bigint 0.5.5", - "form_urlencoded", - "hex", - "hmac", - "http 0.2.11", - "once_cell", - "p256", - "percent-encoding", - "ring", - "sha2", - "subtle", - "time", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-async" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eac0bb78e9e2765699999a02d7bfb4e6ad8f13e0962ebb9f5202b1d8cd76006" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-checksums" -version = "0.60.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535a2d5f1e459bc7709580a77152c8d493982db083236c2b1d1c51dc6217e8a3" -dependencies = [ - "aws-smithy-http", - "aws-smithy-types", - "bytes", - "crc32c", - "crc32fast", - "hex", - "http 0.2.11", - "http-body 0.4.6", - "md-5", - "pin-project-lite", - "sha1", - "sha2", - "tracing", -] - -[[package]] -name = "aws-smithy-eventstream" -version = "0.60.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "682371561562d08ab437766903c6bc28f4f95d7ab2ecfb389bda7849dd98aefe" -dependencies = [ - "aws-smithy-types", - "bytes", - "crc32fast", -] - -[[package]] -name = "aws-smithy-http" -version = "0.60.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "365ca49744b2bda2f1e2dc03b856da3fa5a28ca5b0a41e41d7ff5305a8fae190" -dependencies = [ - "aws-smithy-eventstream", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.11", - "http-body 0.4.6", - "once_cell", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-json" -version = "0.60.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733ccdb727ac63370836aa3b3c483d75ad2ef7bc6507db3efe1d01e8d2e50367" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-query" -version = "0.60.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aff02ae2ee7968bbce2983ffb5ce529d24f4848532300f398347bde8c2196974" -dependencies = [ - "aws-smithy-types", - "urlencoding", -] - -[[package]] -name = "aws-smithy-runtime" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab9cb6fee50680af8ceaa293ae79eba32095ca117161cb323f9ee30dd87d139" -dependencies = [ - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "fastrand", - "h2 0.3.24", - "http 0.2.11", - "http-body 0.4.6", - "hyper 0.14.28", - "hyper-rustls", - "once_cell", - "pin-project-lite", - "pin-utils", - "rustls", - "tokio", - "tracing", -] - -[[package]] -name = "aws-smithy-runtime-api" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02ca2da7619517310bfead6d18abcdde90f1439224d887d608503cfacff46dff" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes", - "http 0.2.11", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-types" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4bb944488536cd2fef43212d829bc7e9a8bfc4afa079d21170441e7be8d2d0" -dependencies = [ - "base64-simd", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.11", - "http-body 0.4.6", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time", - "tokio", - "tokio-util", -] - -[[package]] -name = "aws-smithy-xml" -version = "0.60.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef796feaf894d7fd03869235237aeffe73ed1b29a3927cceeee2eecadf876eba" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "aws-types" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8549aa62c5b7db5c57ab915200ee214b4f5d8f19b29a4a8fa0b3ad3bca1380e3" -dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "http 0.2.11", - "rustc_version", - "tracing", -] - -[[package]] -name = "axum" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" -dependencies = [ - "async-trait", - "axum-core", - "bytes", - "futures-util", - "http 1.0.0", - "http-body 1.0.0", - "http-body-util", - "hyper 1.1.0", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.0.0", - "http-body 1.0.0", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "backtrace" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base16ct" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" - -[[package]] -name = "bytes" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" - -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes", - "either", -] - -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-targets 0.48.5", -] - -[[package]] -name = "common" -version = "0.1.0" -dependencies = [ - "chrono", - "hmac-sha256", - "serde", - "subtle", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" - -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32c" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8f48d60e5b4d2c53d5c2b1d8a58c849a70ae5e5509b08a48d047e3b65714a74" -dependencies = [ - "rustc_version", -] - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-bigint" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array", - "rand_core", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "rand_core", - "subtle", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "der" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "ecdsa" -version = "0.14.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" -dependencies = [ - "der", - "elliptic-curve", - "rfc6979", - "signature", -] - -[[package]] -name = "either" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" - -[[package]] -name = "elliptic-curve" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" -dependencies = [ - "base16ct", - "crypto-bigint 0.4.9", - "der", - "digest", - "ff", - "generic-array", - "group", - "pkcs8", - "rand_core", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "fastrand" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" - -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "rand_core", - "subtle", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" - -[[package]] -name = "futures-sink" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" - -[[package]] -name = "futures-task" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" - -[[package]] -name = "futures-util" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "pin-utils", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" - -[[package]] -name = "git-lfs-authenticate" -version = "0.1.0" -dependencies = [ - "chrono", - "common", -] - -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff", - "rand_core", - "subtle", -] - -[[package]] -name = "h2" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.11", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 1.0.0", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" - -[[package]] -name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "hmac-sha256" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3688e69b38018fec1557254f64c8dc2cc8ec502890182f395dbb0aa997aa5735" - -[[package]] -name = "http" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.11", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" -dependencies = [ - "bytes", - "http 1.0.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" -dependencies = [ - "bytes", - "futures-util", - "http 1.0.0", - "http-body 1.0.0", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "0.14.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.24", - "http 0.2.11", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.2", - "http 1.0.0", - "http-body 1.0.0", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.11", - "hyper 0.14.28", - "log", - "rustls", - "rustls-native-certs", - "tokio", - "tokio-rustls", -] - -[[package]] -name = "hyper-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.0.0", - "http-body 1.0.0", - "hyper 1.1.0", - "pin-project-lite", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "itoa" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" - -[[package]] -name = "js-sys" -version = "0.3.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" -dependencies = [ - "wasm-bindgen", -] - -[[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.152" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" - -[[package]] -name = "lock_api" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "outref" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "p256" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" -dependencies = [ - "ecdsa", - "elliptic-curve", - "sha2", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.48.5", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "proc-macro2" -version = "1.0.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.4", - "regex-syntax 0.8.2", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.2", -] - -[[package]] -name = "regex-lite" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" - -[[package]] -name = "rfc6979" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" -dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", -] - -[[package]] -name = "ring" -version = "0.17.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" -dependencies = [ - "cc", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rustls" -version = "0.21.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" -dependencies = [ - "log", - "ring", - "rustls-webpki", - "sct", -] - -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" - -[[package]] -name = "ryu" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" - -[[package]] -name = "schannel" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "sec1" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" - -[[package]] -name = "serde" -version = "1.0.195" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.195" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" -dependencies = [ - "itoa", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "server" -version = "0.1.0" -dependencies = [ - "aws-config", - "aws-sdk-s3", - "axum", - "base64", - "chrono", - "common", - "mime", - "serde", - "serde_json", - "tokio", - "tokio-util", - "tower", - "tracing-subscriber", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shell" -version = "0.1.0" - -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" - -[[package]] -name = "socket2" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" -dependencies = [ - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "spki" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - -[[package]] -name = "syn" -version = "2.0.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "time" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" -dependencies = [ - "deranged", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" -dependencies = [ - "time-core", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.35.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "num_cpus", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-macros" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "uuid" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" - -[[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-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.0", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.0", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" - -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - -[[package]] -name = "zeroize" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/rs/Cargo.toml b/rs/Cargo.toml deleted file mode 100644 index 6439e6b..0000000 --- a/rs/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[workspace] -resolver = "2" -members = [ - "common", - "git-lfs-authenticate", - "server", - "shell", -] diff --git a/rs/common/Cargo.toml b/rs/common/Cargo.toml deleted file mode 100644 index 20d9bdd..0000000 --- a/rs/common/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "common" -version = "0.1.0" -edition = "2021" - -[dependencies] -chrono = "0.4" -hmac-sha256 = "1.1" -subtle = "2.5" -serde = { version = "1", features = ["derive"] } diff --git a/rs/common/src/lib.rs b/rs/common/src/lib.rs deleted file mode 100644 index 995352d..0000000 --- a/rs/common/src/lib.rs +++ /dev/null @@ -1,368 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::de; -use serde::{Deserialize, Serialize}; -use std::fmt::Write; -use std::ops; -use std::{fmt, str::FromStr}; -use subtle::ConstantTimeEq; - -#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] -#[repr(u8)] -pub enum Operation { - #[serde(rename = "download")] - Download = 1, - #[serde(rename = "upload")] - Upload = 2, -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub struct ParseOperationError; - -impl fmt::Display for ParseOperationError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "operation should be 'download' or 'upload'") - } -} - -impl FromStr for Operation { - type Err = ParseOperationError; - - fn from_str(s: &str) -> Result { - match s { - "upload" => Ok(Self::Upload), - "download" => Ok(Self::Download), - _ => Err(ParseOperationError), - } - } -} - -#[repr(u8)] -enum AuthType { - BatchApi = 1, - Download = 2, -} - -/// None means out of range. -fn decode_nibble(c: u8) -> Option { - if c.is_ascii_digit() { - Some(c - b'0') - } else if (b'a'..=b'f').contains(&c) { - Some(c - b'a' + 10) - } else if (b'A'..=b'F').contains(&c) { - Some(c - b'A' + 10) - } else { - None - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct HexByte(pub u8); - -impl<'de> Deserialize<'de> for HexByte { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let str = <&str>::deserialize(deserializer)?; - let &[b1, b2] = str.as_bytes() else { - return Err(de::Error::invalid_length( - str.len(), - &"two hexadecimal characters", - )); - }; - let (Some(b1), Some(b2)) = (decode_nibble(b1), decode_nibble(b2)) else { - return Err(de::Error::invalid_value( - de::Unexpected::Str(str), - &"two hexadecimal characters", - )); - }; - Ok(HexByte((b1 << 4) | b2)) - } -} - -impl fmt::Display for HexByte { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let &HexByte(b) = self; - HexFmt(&[b]).fmt(f) - } -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub enum ParseHexError { - UnevenNibbles, - InvalidCharacter, - TooShort, - TooLong, -} - -impl fmt::Display for ParseHexError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::UnevenNibbles => { - write!(f, "uneven amount of nibbles (chars in range [a-zA-Z0-9])") - } - Self::InvalidCharacter => write!(f, "non-hex character encountered"), - Self::TooShort => write!(f, "unexpected end of hex sequence"), - Self::TooLong => write!(f, "longer hex sequence than expected"), - } - } -} - -#[derive(Debug)] -pub enum ReadHexError { - Io(std::io::Error), - Format(ParseHexError), -} - -impl fmt::Display for ReadHexError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::Io(e) => e.fmt(f), - Self::Format(e) => e.fmt(f), - } - } -} - -fn parse_hex_exact(value: &str, buf: &mut [u8]) -> Result<(), ParseHexError> { - if value.bytes().len() % 2 == 1 { - return Err(ParseHexError::UnevenNibbles); - } - if value.bytes().len() < 2 * buf.len() { - return Err(ParseHexError::TooShort); - } - if value.bytes().len() > 2 * buf.len() { - return Err(ParseHexError::TooLong); - } - for (i, c) in value.bytes().enumerate() { - if let Some(b) = decode_nibble(c) { - if i % 2 == 0 { - buf[i / 2] = b << 4; - } else { - buf[i / 2] |= b; - } - } else { - return Err(ParseHexError::InvalidCharacter); - } - } - Ok(()) -} - -pub struct SafeByteArray { - inner: [u8; N], -} - -impl SafeByteArray { - pub fn new() -> Self { - Self { inner: [0; N] } - } -} - -impl Default for SafeByteArray { - fn default() -> Self { - Self::new() - } -} - -impl AsRef<[u8]> for SafeByteArray { - fn as_ref(&self) -> &[u8] { - &self.inner - } -} - -impl AsMut<[u8]> for SafeByteArray { - fn as_mut(&mut self) -> &mut [u8] { - &mut self.inner - } -} - -impl Drop for SafeByteArray { - fn drop(&mut self) { - self.inner.fill(0) - } -} - -impl FromStr for SafeByteArray { - type Err = ParseHexError; - - fn from_str(value: &str) -> Result { - let mut sba = Self { inner: [0u8; N] }; - parse_hex_exact(value, &mut sba.inner)?; - Ok(sba) - } -} - -pub type Oid = Digest<32>; - -#[derive(Debug, Copy, Clone)] -pub enum SpecificClaims { - BatchApi(Operation), - Download(Oid), -} - -#[derive(Debug, Copy, Clone)] -pub struct Claims<'a> { - pub specific_claims: SpecificClaims, - pub repo_path: &'a str, - pub expires_at: DateTime, -} - -/// Returns None if the claims are invalid. Repo path length may be no more than 100 bytes. -pub fn generate_tag(claims: Claims, key: impl AsRef<[u8]>) -> Option> { - if claims.repo_path.len() > 100 { - return None; - } - - let mut hmac = hmac_sha256::HMAC::new(key); - match claims.specific_claims { - SpecificClaims::BatchApi(operation) => { - hmac.update([AuthType::BatchApi as u8]); - hmac.update([operation as u8]); - } - SpecificClaims::Download(oid) => { - hmac.update([AuthType::Download as u8]); - hmac.update(oid.as_bytes()); - } - } - hmac.update([claims.repo_path.len() as u8]); - hmac.update(claims.repo_path.as_bytes()); - hmac.update(claims.expires_at.timestamp().to_be_bytes()); - Some(hmac.finalize().into()) -} - -pub struct HexFmt>(pub B); - -impl> fmt::Display for HexFmt { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let HexFmt(buf) = self; - for b in buf.as_ref() { - let (high, low) = (b >> 4, b & 0xF); - let highc = if high < 10 { - high + b'0' - } else { - high - 10 + b'a' - }; - let lowc = if low < 10 { - low + b'0' - } else { - low - 10 + b'a' - }; - f.write_char(highc as char)?; - f.write_char(lowc as char)?; - } - Ok(()) - } -} - -pub struct EscJsonFmt<'a>(pub &'a str); - -impl<'a> fmt::Display for EscJsonFmt<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let EscJsonFmt(buf) = self; - for c in buf.chars() { - match c { - '"' => f.write_str("\\\"")?, // quote - '\\' => f.write_str("\\\\")?, // backslash - '\x08' => f.write_str("\\b")?, // backspace - '\x0C' => f.write_str("\\f")?, // form feed - '\n' => f.write_str("\\n")?, // line feed - '\r' => f.write_str("\\r")?, // carriage return - '\t' => f.write_str("\\t")?, // horizontal tab - _ => f.write_char(c)?, - }; - } - Ok(()) - } -} - -#[derive(Debug, Copy, Clone)] -pub struct Digest { - inner: [u8; N], -} - -impl ops::Index for Digest { - type Output = u8; - - fn index(&self, index: usize) -> &Self::Output { - &self.inner[index] - } -} - -impl Digest { - pub fn as_bytes(&self) -> &[u8; N] { - &self.inner - } -} - -impl fmt::Display for Digest { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - HexFmt(&self.inner).fmt(f) - } -} - -impl Digest { - pub fn new(data: [u8; N]) -> Self { - Self { inner: data } - } -} - -impl From<[u8; N]> for Digest { - fn from(value: [u8; N]) -> Self { - Self::new(value) - } -} - -impl From> for [u8; N] { - fn from(val: Digest) -> Self { - val.inner - } -} - -impl FromStr for Digest { - type Err = ParseHexError; - - fn from_str(value: &str) -> Result { - let mut buf = [0u8; N]; - parse_hex_exact(value, &mut buf)?; - Ok(buf.into()) - } -} - -impl ConstantTimeEq for Digest { - fn ct_eq(&self, other: &Self) -> subtle::Choice { - self.inner.ct_eq(&other.inner) - } -} - -impl PartialEq for Digest { - fn eq(&self, other: &Self) -> bool { - self.ct_eq(other).into() - } -} - -impl Eq for Digest {} - -impl<'de, const N: usize> Deserialize<'de> for Digest { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let hex = <&str>::deserialize(deserializer)?; - Digest::from_str(hex).map_err(de::Error::custom) - } -} - -impl Serialize for Digest { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&format!("{self}")) - } -} - -pub type Key = SafeByteArray<64>; - -pub fn load_key(path: &str) -> Result { - let key_str = std::fs::read_to_string(path).map_err(ReadHexError::Io)?; - key_str.trim().parse().map_err(ReadHexError::Format) -} diff --git a/rs/git-lfs-authenticate/Cargo.toml b/rs/git-lfs-authenticate/Cargo.toml deleted file mode 100644 index 217250f..0000000 --- a/rs/git-lfs-authenticate/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "git-lfs-authenticate" -version = "0.1.0" -edition = "2021" - -[dependencies] -chrono = "0.4" -common = { path = "../common" } diff --git a/rs/git-lfs-authenticate/src/main.rs b/rs/git-lfs-authenticate/src/main.rs deleted file mode 100644 index 36d7818..0000000 --- a/rs/git-lfs-authenticate/src/main.rs +++ /dev/null @@ -1,236 +0,0 @@ -use std::{fmt, process::ExitCode, time::Duration}; - -use chrono::Utc; -use common::{Operation, ParseOperationError}; - -fn help() { - eprintln!("Usage: git-lfs-authenticate upload/download"); -} - -#[derive(Debug, Eq, PartialEq, Copy, Clone)] -enum RepoNameError { - TooLong, - UnresolvedPath, - AbsolutePath, - MissingGitSuffix, -} - -impl fmt::Display for RepoNameError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::TooLong => write!(f, "too long (more than 100 characters)"), - Self::UnresolvedPath => { - write!(f, "contains path one or more path elements '.' and '..'") - } - Self::AbsolutePath => { - write!(f, "starts with '/', which is not allowed") - } - Self::MissingGitSuffix => write!(f, "misses '.git' suffix"), - } - } -} - -// Using `Result<(), E>` here instead of `Option` because `None` typically signifies some error -// state with no further details provided. If we were to return an `Option` type, the user would -// have to first transform it into a `Result` type in order to use the `?` operator, meaning that -// they would have to the following operation to get the type into the right shape: -// `validate_repo_path(path).map_or(Ok(()), Err)`. That would not be very ergonomic. -fn validate_repo_path(path: &str) -> Result<(), RepoNameError> { - if path.len() > 100 { - return Err(RepoNameError::TooLong); - } - if path.contains("//") - || path.contains("/./") - || path.contains("/../") - || path.starts_with("./") - || path.starts_with("../") - { - return Err(RepoNameError::UnresolvedPath); - } - if path.starts_with('/') { - return Err(RepoNameError::AbsolutePath); - } - if !path.ends_with(".git") { - return Err(RepoNameError::MissingGitSuffix); - } - Ok(()) -} - -#[derive(Debug, Eq, PartialEq, Copy, Clone)] -enum ParseCmdlineError { - UnknownOperation(ParseOperationError), - InvalidRepoName(RepoNameError), - UnexpectedArgCount(ArgCountError), -} - -impl From for ParseCmdlineError { - fn from(value: RepoNameError) -> Self { - Self::InvalidRepoName(value) - } -} - -impl From for ParseCmdlineError { - fn from(value: ParseOperationError) -> Self { - Self::UnknownOperation(value) - } -} - -impl From for ParseCmdlineError { - fn from(value: ArgCountError) -> Self { - Self::UnexpectedArgCount(value) - } -} - -impl fmt::Display for ParseCmdlineError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::UnknownOperation(e) => write!(f, "unknown operation: {e}"), - Self::InvalidRepoName(e) => write!(f, "invalid repository name: {e}"), - Self::UnexpectedArgCount(e) => e.fmt(f), - } - } -} - -#[derive(Debug, Eq, PartialEq, Copy, Clone)] -struct ArgCountError { - provided: usize, - expected: usize, -} - -impl fmt::Display for ArgCountError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "got {} argument(s), expected {}", - self.provided, self.expected - ) - } -} - -fn get_cmdline_args() -> Result<[String; N], ArgCountError> { - let args = std::env::args(); - if args.len() - 1 != N { - return Err(ArgCountError { - provided: args.len() - 1, - expected: N, - }); - } - - // Does not allocate. - const EMPTY_STRING: String = String::new(); - let mut values = [EMPTY_STRING; N]; - - // Skip the first element; we do not care about the program name. - for (i, arg) in args.skip(1).enumerate() { - values[i] = arg - } - Ok(values) -} - -fn parse_cmdline() -> Result<(String, Operation), ParseCmdlineError> { - let [repo_path, op_str] = get_cmdline_args::<2>()?; - let op: Operation = op_str.parse()?; - validate_repo_path(&repo_path)?; - Ok((repo_path.to_string(), op)) -} - -fn repo_exists(name: &str) -> bool { - match std::fs::metadata(name) { - Ok(metadata) => metadata.is_dir(), - _ => false, - } -} - -struct Config { - href_base: String, - key_path: String, -} - -#[derive(Debug, Eq, PartialEq, Copy, Clone)] -enum LoadConfigError { - BaseUrlNotProvided, - BaseUrlSlashSuffixMissing, - KeyPathNotProvided, -} - -impl fmt::Display for LoadConfigError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::BaseUrlNotProvided => write!(f, "base URL not provided"), - Self::BaseUrlSlashSuffixMissing => write!(f, "base URL does not end with slash"), - Self::KeyPathNotProvided => write!(f, "key path not provided"), - } - } -} - -fn load_config() -> Result { - let Ok(href_base) = std::env::var("GITOLFS3_HREF_BASE") else { - return Err(LoadConfigError::BaseUrlNotProvided); - }; - if !href_base.ends_with('/') { - return Err(LoadConfigError::BaseUrlSlashSuffixMissing); - } - let Ok(key_path) = std::env::var("GITOLFS3_KEY_PATH") else { - return Err(LoadConfigError::KeyPathNotProvided); - }; - Ok(Config { - href_base, - key_path, - }) -} - -fn main() -> ExitCode { - let config = match load_config() { - Ok(config) => config, - Err(e) => { - eprintln!("Failed to load config: {e}"); - return ExitCode::FAILURE; - } - }; - let key = match common::load_key(&config.key_path) { - Ok(key) => key, - Err(e) => { - eprintln!("Failed to load key: {e}"); - return ExitCode::FAILURE; - } - }; - - let (repo_name, operation) = match parse_cmdline() { - Ok(args) => args, - Err(e) => { - eprintln!("Error: {e}\n"); - help(); - // Exit code 2 signifies bad usage of CLI. - return ExitCode::from(2); - } - }; - - if !repo_exists(&repo_name) { - eprintln!("Error: repository does not exist"); - return ExitCode::FAILURE; - } - - let expires_at = Utc::now() + Duration::from_secs(5 * 60); - let Some(tag) = common::generate_tag( - common::Claims { - specific_claims: common::SpecificClaims::BatchApi(operation), - repo_path: &repo_name, - expires_at, - }, - key, - ) else { - eprintln!("Failed to generate validation tag"); - return ExitCode::FAILURE; - }; - - println!( - "{{\"header\":{{\"Authorization\":\"Gitolfs3-Hmac-Sha256 {tag} {}\"}},\ - \"expires_at\":\"{}\",\"href\":\"{}{}/info/lfs\"}}", - expires_at.timestamp(), - common::EscJsonFmt(&expires_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)), - common::EscJsonFmt(&config.href_base), - common::EscJsonFmt(&repo_name), - ); - - ExitCode::SUCCESS -} diff --git a/rs/server/Cargo.toml b/rs/server/Cargo.toml deleted file mode 100644 index edb76d8..0000000 --- a/rs/server/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "server" -version = "0.1.0" -edition = "2021" - -[dependencies] -aws-config = { version = "1.1.2" } -aws-sdk-s3 = "1.12.0" -axum = "0.7" -base64 = "0.21" -chrono = { version = "0.4", features = ["serde"] } -common = { path = "../common" } -mime = "0.3" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -tokio = { version = "1.35", features = ["full"] } -tokio-util = "0.7" -tower = "0.4" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/rs/server/src/main.rs b/rs/server/src/main.rs deleted file mode 100644 index 8baa0d6..0000000 --- a/rs/server/src/main.rs +++ /dev/null @@ -1,1028 +0,0 @@ -use std::collections::HashMap; -use std::collections::HashSet; -use std::process::ExitCode; -use std::sync::Arc; - -use aws_sdk_s3::error::SdkError; -use aws_sdk_s3::operation::head_object::HeadObjectOutput; -use axum::extract::rejection; -use axum::extract::FromRequest; -use axum::extract::Path; -use axum::extract::State; -use axum::http::header; -use axum::http::HeaderMap; -use axum::http::HeaderValue; -use axum::response::Response; -use axum::Json; -use axum::ServiceExt; -use base64::prelude::*; -use chrono::DateTime; -use chrono::Utc; -use common::HexByte; -use serde::de; -use serde::de::DeserializeOwned; -use serde::Deserialize; -use serde::Serialize; -use tower::Layer; - -use axum::{ - async_trait, - extract::{FromRequestParts, OriginalUri, Request}, - http::{request::Parts, StatusCode, Uri}, - response::IntoResponse, - routing::{get, post}, - Extension, Router, -}; - -#[derive(Clone)] -struct RepositoryName(String); - -struct RepositoryNameRejection; - -impl IntoResponse for RepositoryNameRejection { - fn into_response(self) -> Response { - (StatusCode::INTERNAL_SERVER_ERROR, "Missing repository name").into_response() - } -} - -#[async_trait] -impl FromRequestParts for RepositoryName { - type Rejection = RepositoryNameRejection; - - async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - let Ok(Extension(repo_name)) = Extension::::from_request_parts(parts, state).await - else { - return Err(RepositoryNameRejection); - }; - Ok(repo_name) - } -} - -async fn rewrite_url( - mut req: axum::http::Request, -) -> Result, StatusCode> { - let uri = req.uri(); - let original_uri = OriginalUri(uri.clone()); - - let Some(path_and_query) = uri.path_and_query() else { - // L @ no path & query - return Err(StatusCode::BAD_REQUEST); - }; - let Some((repo, path)) = path_and_query.path().split_once("/info/lfs/objects") else { - return Err(StatusCode::NOT_FOUND); - }; - let repo = repo - .trim_start_matches('/') - .trim_end_matches('/') - .to_string(); - if !path.starts_with('/') || !repo.ends_with(".git") { - return Err(StatusCode::NOT_FOUND); - } - - let mut parts = uri.clone().into_parts(); - parts.path_and_query = match path_and_query.query() { - None => path.try_into().ok(), - Some(q) => format!("{path}?{q}").try_into().ok(), - }; - let Ok(new_uri) = Uri::from_parts(parts) else { - return Err(StatusCode::INTERNAL_SERVER_ERROR); - }; - - *req.uri_mut() = new_uri; - req.extensions_mut().insert(original_uri); - req.extensions_mut().insert(RepositoryName(repo)); - - Ok(req) -} - -struct AppState { - s3_client: aws_sdk_s3::Client, - s3_bucket: String, - authz_conf: AuthorizationConfig, - // Should not end with a slash. - base_url: String, -} - -struct Env { - s3_access_key_id: String, - s3_secret_access_key: String, - s3_bucket: String, - s3_region: String, - s3_endpoint: String, - base_url: String, - key_path: String, - listen_host: String, - listen_port: String, - trusted_forwarded_hosts: String, -} - -fn require_env(name: &str) -> Result { - std::env::var(name) - .map_err(|_| format!("environment variable {name} should be defined and valid")) -} - -impl Env { - fn load() -> Result { - Ok(Env { - s3_secret_access_key: require_env("GITOLFS3_S3_SECRET_ACCESS_KEY_FILE")?, - s3_access_key_id: require_env("GITOLFS3_S3_ACCESS_KEY_ID_FILE")?, - s3_region: require_env("GITOLFS3_S3_REGION")?, - s3_endpoint: require_env("GITOLFS3_S3_ENDPOINT")?, - s3_bucket: require_env("GITOLFS3_S3_BUCKET")?, - base_url: require_env("GITOLFS3_BASE_URL")?, - key_path: require_env("GITOLFS3_KEY_PATH")?, - listen_host: require_env("GITOLFS3_LISTEN_HOST")?, - listen_port: require_env("GITOLFS3_LISTEN_PORT")?, - trusted_forwarded_hosts: std::env::var("GITOLFS3_TRUSTED_FORWARDED_HOSTS") - .unwrap_or_default(), - }) - } -} - -fn get_s3_client(env: &Env) -> Result { - let access_key_id = std::fs::read_to_string(&env.s3_access_key_id)?; - let secret_access_key = std::fs::read_to_string(&env.s3_secret_access_key)?; - - let credentials = aws_sdk_s3::config::Credentials::new( - access_key_id, - secret_access_key, - None, - None, - "gitolfs3-env", - ); - let config = aws_config::SdkConfig::builder() - .behavior_version(aws_config::BehaviorVersion::latest()) - .region(aws_config::Region::new(env.s3_region.clone())) - .endpoint_url(&env.s3_endpoint) - .credentials_provider(aws_sdk_s3::config::SharedCredentialsProvider::new( - credentials, - )) - .build(); - Ok(aws_sdk_s3::Client::new(&config)) -} - -#[tokio::main] -async fn main() -> ExitCode { - tracing_subscriber::fmt::init(); - - let env = match Env::load() { - Ok(env) => env, - Err(e) => { - println!("Failed to load configuration: {e}"); - return ExitCode::from(2); - } - }; - - let s3_client = match get_s3_client(&env) { - Ok(s3_client) => s3_client, - Err(e) => { - println!("Failed to create S3 client: {e}"); - return ExitCode::FAILURE; - } - }; - let key = match common::load_key(&env.key_path) { - Ok(key) => key, - Err(e) => { - println!("Failed to load Gitolfs3 key: {e}"); - return ExitCode::FAILURE; - } - }; - - let trusted_forwarded_hosts: HashSet = env - .trusted_forwarded_hosts - .split(',') - .map(|s| s.to_owned()) - .filter(|s| !s.is_empty()) - .collect(); - let base_url = env.base_url.trim_end_matches('/').to_string(); - - let authz_conf = AuthorizationConfig { - key, - trusted_forwarded_hosts, - }; - - let shared_state = Arc::new(AppState { - s3_client, - s3_bucket: env.s3_bucket, - authz_conf, - base_url, - }); - let app = Router::new() - .route("/batch", post(batch)) - .route("/:oid0/:oid1/:oid", get(obj_download)) - .with_state(shared_state); - - let middleware = axum::middleware::map_request(rewrite_url); - let app_with_middleware = middleware.layer(app); - - let Ok(listen_port): Result = env.listen_port.parse() else { - println!("Configured LISTEN_PORT should be an unsigned integer no higher than 65535"); - return ExitCode::from(2); - }; - let addr: (String, u16) = (env.listen_host, listen_port); - let listener = match tokio::net::TcpListener::bind(addr).await { - Ok(listener) => listener, - Err(e) => { - println!("Failed to listen: {e}"); - return ExitCode::FAILURE; - } - }; - - match axum::serve(listener, app_with_middleware.into_make_service()).await { - Ok(_) => ExitCode::SUCCESS, - Err(e) => { - println!("Error serving: {e}"); - ExitCode::FAILURE - } - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] -enum TransferAdapter { - #[serde(rename = "basic")] - Basic, - #[serde(other)] - Unknown, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] -enum HashAlgo { - #[serde(rename = "sha256")] - Sha256, - #[serde(other)] - Unknown, -} - -impl Default for HashAlgo { - fn default() -> Self { - Self::Sha256 - } -} - -#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] -struct BatchRequestObject { - oid: common::Oid, - size: i64, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -struct BatchRef { - name: String, -} - -fn default_transfers() -> Vec { - vec![TransferAdapter::Basic] -} - -#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] -struct BatchRequest { - operation: common::Operation, - #[serde(default = "default_transfers")] - transfers: Vec, - objects: Vec, - #[serde(default)] - hash_algo: HashAlgo, -} - -#[derive(Debug, Clone)] -struct GitLfsJson(Json); - -const LFS_MIME: &str = "application/vnd.git-lfs+json"; - -enum GitLfsJsonRejection { - Json(rejection::JsonRejection), - MissingGitLfsJsonContentType, -} - -impl IntoResponse for GitLfsJsonRejection { - fn into_response(self) -> Response { - match self { - Self::Json(rej) => rej.into_response(), - Self::MissingGitLfsJsonContentType => make_error_resp( - StatusCode::UNSUPPORTED_MEDIA_TYPE, - &format!("Expected request with `Content-Type: {LFS_MIME}`"), - ) - .into_response(), - } - } -} - -fn is_git_lfs_json_mimetype(mimetype: &str) -> bool { - let Ok(mime) = mimetype.parse::() else { - return false; - }; - if mime.type_() != mime::APPLICATION - || mime.subtype() != "vnd.git-lfs" - || mime.suffix() != Some(mime::JSON) - { - return false; - } - match mime.get_param(mime::CHARSET) { - Some(mime::UTF_8) | None => true, - Some(_) => false, - } -} - -fn has_git_lfs_json_content_type(req: &Request) -> bool { - let Some(content_type) = req.headers().get(header::CONTENT_TYPE) else { - return false; - }; - let Ok(content_type) = content_type.to_str() else { - return false; - }; - is_git_lfs_json_mimetype(content_type) -} - -#[async_trait] -impl FromRequest for GitLfsJson -where - T: DeserializeOwned, - S: Send + Sync, -{ - type Rejection = GitLfsJsonRejection; - - async fn from_request(req: Request, state: &S) -> Result { - if !has_git_lfs_json_content_type(&req) { - return Err(GitLfsJsonRejection::MissingGitLfsJsonContentType); - } - Json::::from_request(req, state) - .await - .map(GitLfsJson) - .map_err(GitLfsJsonRejection::Json) - } -} - -impl IntoResponse for GitLfsJson { - fn into_response(self) -> Response { - let GitLfsJson(json) = self; - let mut resp = json.into_response(); - resp.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static("application/vnd.git-lfs+json; charset=utf-8"), - ); - resp - } -} - -#[derive(Debug, Serialize)] -struct GitLfsErrorData<'a> { - message: &'a str, -} - -type GitLfsErrorResponse<'a> = (StatusCode, GitLfsJson>); - -const fn make_error_resp(code: StatusCode, message: &str) -> GitLfsErrorResponse { - (code, GitLfsJson(Json(GitLfsErrorData { message }))) -} - -#[derive(Debug, Serialize, Clone)] -struct BatchResponseObjectAction { - href: String, - #[serde(skip_serializing_if = "HashMap::is_empty")] - header: HashMap, - expires_at: DateTime, -} - -#[derive(Default, Debug, Serialize, Clone)] -struct BatchResponseObjectActions { - #[serde(skip_serializing_if = "Option::is_none")] - upload: Option, - #[serde(skip_serializing_if = "Option::is_none")] - download: Option, - #[serde(skip_serializing_if = "Option::is_none")] - verify: Option, -} - -#[derive(Debug, Clone, Serialize)] -struct BatchResponseObjectError { - code: u16, - message: String, -} - -#[derive(Debug, Serialize, Clone)] -struct BatchResponseObject { - oid: common::Oid, - size: i64, - #[serde(skip_serializing_if = "Option::is_none")] - authenticated: Option, - actions: BatchResponseObjectActions, - #[serde(skip_serializing_if = "Option::is_none")] - error: Option, -} - -impl BatchResponseObject { - fn error(obj: &BatchRequestObject, code: StatusCode, message: String) -> BatchResponseObject { - BatchResponseObject { - oid: obj.oid, - size: obj.size, - authenticated: None, - actions: Default::default(), - error: Some(BatchResponseObjectError { - code: code.as_u16(), - message, - }), - } - } -} - -#[derive(Debug, Serialize, Clone)] -struct BatchResponse { - transfer: TransferAdapter, - objects: Vec, - hash_algo: HashAlgo, -} - -fn validate_checksum(oid: common::Oid, obj: &HeadObjectOutput) -> bool { - if let Some(checksum) = obj.checksum_sha256() { - if let Ok(checksum) = BASE64_STANDARD.decode(checksum) { - if let Ok(checksum32b) = TryInto::<[u8; 32]>::try_into(checksum) { - return common::Oid::from(checksum32b) == oid; - } - } - } - true -} - -fn validate_size(expected: i64, obj: &HeadObjectOutput) -> bool { - if let Some(length) = obj.content_length() { - return length == expected; - } - true -} - -async fn handle_upload_object( - state: &AppState, - repo: &str, - obj: &BatchRequestObject, -) -> Option { - let (oid0, oid1) = (HexByte(obj.oid[0]), HexByte(obj.oid[1])); - let full_path = format!("{repo}/lfs/objects/{}/{}/{}", oid0, oid1, obj.oid); - - match state - .s3_client - .head_object() - .bucket(&state.s3_bucket) - .key(full_path.clone()) - .checksum_mode(aws_sdk_s3::types::ChecksumMode::Enabled) - .send() - .await - { - Ok(result) => { - if validate_size(obj.size, &result) && validate_checksum(obj.oid, &result) { - return None; - } - } - Err(SdkError::ServiceError(e)) if e.err().is_not_found() => {} - Err(e) => { - println!("Failed to HeadObject (repo {repo}, OID {}): {e}", obj.oid); - return Some(BatchResponseObject::error( - obj, - StatusCode::INTERNAL_SERVER_ERROR, - "Failed to query object information".to_string(), - )); - } - }; - - let expires_in = std::time::Duration::from_secs(5 * 60); - let expires_at = Utc::now() + expires_in; - - let Ok(config) = aws_sdk_s3::presigning::PresigningConfig::expires_in(expires_in) else { - return Some(BatchResponseObject::error( - obj, - StatusCode::INTERNAL_SERVER_ERROR, - "Failed to generate upload URL".to_string(), - )); - }; - let Ok(presigned) = state - .s3_client - .put_object() - .bucket(&state.s3_bucket) - .key(full_path) - .checksum_sha256(obj.oid.to_string()) - .content_length(obj.size) - .presigned(config) - .await - else { - return Some(BatchResponseObject::error( - obj, - StatusCode::INTERNAL_SERVER_ERROR, - "Failed to generate upload URL".to_string(), - )); - }; - Some(BatchResponseObject { - oid: obj.oid, - size: obj.size, - authenticated: Some(true), - actions: BatchResponseObjectActions { - upload: Some(BatchResponseObjectAction { - header: presigned - .headers() - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .collect(), - expires_at, - href: presigned.uri().to_string(), - }), - ..Default::default() - }, - error: None, - }) -} - -async fn handle_download_object( - state: &AppState, - repo: &str, - obj: &BatchRequestObject, - trusted: bool, -) -> BatchResponseObject { - let (oid0, oid1) = (HexByte(obj.oid[0]), HexByte(obj.oid[1])); - let full_path = format!("{repo}/lfs/objects/{}/{}/{}", oid0, oid1, obj.oid); - - let result = match state - .s3_client - .head_object() - .bucket(&state.s3_bucket) - .key(&full_path) - .checksum_mode(aws_sdk_s3::types::ChecksumMode::Enabled) - .send() - .await - { - Ok(result) => result, - Err(e) => { - println!("Failed to HeadObject (repo {repo}, OID {}): {e}", obj.oid); - return BatchResponseObject::error( - obj, - StatusCode::INTERNAL_SERVER_ERROR, - "Failed to query object information".to_string(), - ); - } - }; - - // Scaleway actually doesn't provide SHA256 suport, but maybe in the future :) - if !validate_checksum(obj.oid, &result) { - return BatchResponseObject::error( - obj, - StatusCode::UNPROCESSABLE_ENTITY, - "Object corrupted".to_string(), - ); - } - if !validate_size(obj.size, &result) { - return BatchResponseObject::error( - obj, - StatusCode::UNPROCESSABLE_ENTITY, - "Incorrect size specified (or object corrupted)".to_string(), - ); - } - - let expires_in = std::time::Duration::from_secs(5 * 60); - let expires_at = Utc::now() + expires_in; - - if trusted { - let Ok(config) = aws_sdk_s3::presigning::PresigningConfig::expires_in(expires_in) else { - return BatchResponseObject::error( - obj, - StatusCode::INTERNAL_SERVER_ERROR, - "Failed to generate upload URL".to_string(), - ); - }; - let Ok(presigned) = state - .s3_client - .get_object() - .bucket(&state.s3_bucket) - .key(full_path) - .presigned(config) - .await - else { - return BatchResponseObject::error( - obj, - StatusCode::INTERNAL_SERVER_ERROR, - "Failed to generate upload URL".to_string(), - ); - }; - return BatchResponseObject { - oid: obj.oid, - size: obj.size, - authenticated: Some(true), - actions: BatchResponseObjectActions { - download: Some(BatchResponseObjectAction { - header: presigned - .headers() - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .collect(), - expires_at, - href: presigned.uri().to_string(), - }), - ..Default::default() - }, - error: None, - }; - } - - let Some(tag) = common::generate_tag( - common::Claims { - specific_claims: common::SpecificClaims::Download(obj.oid), - repo_path: repo, - expires_at, - }, - &state.authz_conf.key, - ) else { - return BatchResponseObject::error( - obj, - StatusCode::INTERNAL_SERVER_ERROR, - "Internal server error".to_string(), - ); - }; - - let upload_path = format!( - "{repo}/info/lfs/objects/{}/{}/{}", - HexByte(obj.oid[0]), - HexByte(obj.oid[1]), - obj.oid, - ); - - BatchResponseObject { - oid: obj.oid, - size: obj.size, - authenticated: Some(true), - actions: BatchResponseObjectActions { - download: Some(BatchResponseObjectAction { - header: { - let mut map = HashMap::new(); - map.insert( - "Authorization".to_string(), - format!("Gitolfs3-Hmac-Sha256 {tag} {}", expires_at.timestamp()), - ); - map - }, - expires_at, - href: format!("{}/{upload_path}", state.base_url), - }), - ..Default::default() - }, - error: None, - } -} - -struct AuthorizationConfig { - trusted_forwarded_hosts: HashSet, - key: common::Key, -} - -struct Trusted(bool); - -fn forwarded_for_trusted_host( - headers: &HeaderMap, - trusted: &HashSet, -) -> Result> { - if let Some(forwarded_for) = headers.get("X-Forwarded-For") { - if let Ok(forwarded_for) = forwarded_for.to_str() { - if trusted.contains(forwarded_for) { - return Ok(true); - } - } else { - return Err(make_error_resp( - StatusCode::NOT_FOUND, - "Invalid X-Forwarded-For header", - )); - } - } - Ok(false) -} -const REPO_NOT_FOUND: GitLfsErrorResponse = - make_error_resp(StatusCode::NOT_FOUND, "Repository not found"); - -fn authorize_batch( - conf: &AuthorizationConfig, - repo_path: &str, - public: bool, - operation: common::Operation, - headers: &HeaderMap, -) -> Result> { - // - No authentication required for downloading exported repos - // - When authenticated: - // - Download / upload over presigned URLs - // - When accessing over Tailscale: - // - No authentication required for downloading from any repo - - let claims = VerifyClaimsInput { - specific_claims: common::SpecificClaims::BatchApi(operation), - repo_path, - }; - if verify_claims(conf, &claims, headers)? { - return Ok(Trusted(true)); - } - - let trusted = forwarded_for_trusted_host(headers, &conf.trusted_forwarded_hosts)?; - if operation != common::Operation::Download { - if trusted { - return Err(make_error_resp( - StatusCode::FORBIDDEN, - "Authentication required to upload", - )); - } - return Err(REPO_NOT_FOUND); - } - if trusted { - return Ok(Trusted(true)); - } - - if public { - Ok(Trusted(false)) - } else { - Err(REPO_NOT_FOUND) - } -} - -fn repo_exists(name: &str) -> bool { - let Ok(metadata) = std::fs::metadata(name) else { - return false; - }; - metadata.is_dir() -} - -fn is_repo_public(name: &str) -> Option { - if !repo_exists(name) { - return None; - } - std::fs::metadata(format!("{name}/git-daemon-export-ok")) - .ok()? - .is_file() - .into() -} - -async fn batch( - State(state): State>, - headers: HeaderMap, - RepositoryName(repo): RepositoryName, - GitLfsJson(Json(payload)): GitLfsJson, -) -> Response { - let Some(public) = is_repo_public(&repo) else { - return REPO_NOT_FOUND.into_response(); - }; - let Trusted(trusted) = match authorize_batch( - &state.authz_conf, - &repo, - public, - payload.operation, - &headers, - ) { - Ok(authn) => authn, - Err(e) => return e.into_response(), - }; - - if !headers - .get_all("Accept") - .iter() - .filter_map(|v| v.to_str().ok()) - .any(is_git_lfs_json_mimetype) - { - let message = format!("Expected `{LFS_MIME}` in list of acceptable response media types"); - return make_error_resp(StatusCode::NOT_ACCEPTABLE, &message).into_response(); - } - - if payload.hash_algo != HashAlgo::Sha256 { - let message = "Unsupported hashing algorithm specified"; - return make_error_resp(StatusCode::CONFLICT, message).into_response(); - } - if !payload.transfers.is_empty() && !payload.transfers.contains(&TransferAdapter::Basic) { - let message = "Unsupported transfer adapter specified (supported: basic)"; - return make_error_resp(StatusCode::CONFLICT, message).into_response(); - } - - let mut resp = BatchResponse { - transfer: TransferAdapter::Basic, - objects: vec![], - hash_algo: HashAlgo::Sha256, - }; - for obj in payload.objects { - match payload.operation { - common::Operation::Download => resp - .objects - .push(handle_download_object(&state, &repo, &obj, trusted).await), - common::Operation::Upload => { - if let Some(obj_resp) = handle_upload_object(&state, &repo, &obj).await { - resp.objects.push(obj_resp); - } - } - }; - } - GitLfsJson(Json(resp)).into_response() -} - -#[derive(Deserialize, Copy, Clone)] -#[serde(remote = "Self")] -struct FileParams { - oid0: HexByte, - oid1: HexByte, - oid: common::Oid, -} - -impl<'de> Deserialize<'de> for FileParams { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let unchecked @ FileParams { - oid0: HexByte(oid0), - oid1: HexByte(oid1), - oid, - } = FileParams::deserialize(deserializer)?; - if oid0 != oid.as_bytes()[0] { - return Err(de::Error::custom( - "first OID path part does not match first byte of full OID", - )); - } - if oid1 != oid.as_bytes()[1] { - return Err(de::Error::custom( - "second OID path part does not match first byte of full OID", - )); - } - Ok(unchecked) - } -} - -pub struct VerifyClaimsInput<'a> { - pub specific_claims: common::SpecificClaims, - pub repo_path: &'a str, -} - -fn verify_claims( - conf: &AuthorizationConfig, - claims: &VerifyClaimsInput, - headers: &HeaderMap, -) -> Result> { - const INVALID_AUTHZ_HEADER: GitLfsErrorResponse = - make_error_resp(StatusCode::BAD_REQUEST, "Invalid authorization header"); - - if let Some(authz) = headers.get(header::AUTHORIZATION) { - if let Ok(authz) = authz.to_str() { - if let Some(val) = authz.strip_prefix("Gitolfs3-Hmac-Sha256 ") { - let (tag, expires_at) = val.split_once(' ').ok_or(INVALID_AUTHZ_HEADER)?; - let tag: common::Digest<32> = tag.parse().map_err(|_| INVALID_AUTHZ_HEADER)?; - let expires_at: i64 = expires_at.parse().map_err(|_| INVALID_AUTHZ_HEADER)?; - let expires_at = - DateTime::::from_timestamp(expires_at, 0).ok_or(INVALID_AUTHZ_HEADER)?; - let Some(expected_tag) = common::generate_tag( - common::Claims { - specific_claims: claims.specific_claims, - repo_path: claims.repo_path, - expires_at, - }, - &conf.key, - ) else { - return Err(make_error_resp( - StatusCode::INTERNAL_SERVER_ERROR, - "Internal server error", - )); - }; - if tag == expected_tag { - return Ok(true); - } - } - } - return Err(INVALID_AUTHZ_HEADER); - } - Ok(false) -} - -fn authorize_get( - conf: &AuthorizationConfig, - repo_path: &str, - oid: common::Oid, - headers: &HeaderMap, -) -> Result<(), GitLfsErrorResponse<'static>> { - let claims = VerifyClaimsInput { - specific_claims: common::SpecificClaims::Download(oid), - repo_path, - }; - if !verify_claims(conf, &claims, headers)? { - return Err(make_error_resp( - StatusCode::UNAUTHORIZED, - "Repository not found", - )); - } - Ok(()) -} - -async fn obj_download( - State(state): State>, - headers: HeaderMap, - RepositoryName(repo): RepositoryName, - Path(FileParams { oid0, oid1, oid }): Path, -) -> Response { - if let Err(e) = authorize_get(&state.authz_conf, &repo, oid, &headers) { - return e.into_response(); - } - - let full_path = format!("{repo}/lfs/objects/{}/{}/{}", oid0, oid1, oid); - let result = match state - .s3_client - .get_object() - .bucket(&state.s3_bucket) - .key(full_path) - .checksum_mode(aws_sdk_s3::types::ChecksumMode::Enabled) - .send() - .await - { - Ok(result) => result, - Err(e) => { - println!("Failed to GetObject (repo {repo}, OID {oid}): {e}"); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - "Failed to query object information", - ) - .into_response(); - } - }; - - let mut headers = header::HeaderMap::new(); - if let Some(content_type) = result.content_type { - let Ok(header_value) = content_type.try_into() else { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - "Object has invalid content type", - ) - .into_response(); - }; - headers.insert(header::CONTENT_TYPE, header_value); - } - if let Some(content_length) = result.content_length { - headers.insert(header::CONTENT_LENGTH, content_length.into()); - } - - let async_read = result.body.into_async_read(); - let stream = tokio_util::io::ReaderStream::new(async_read); - let body = axum::body::Body::from_stream(stream); - - (headers, body).into_response() -} - -#[test] -fn test_mimetype() { - assert!(is_git_lfs_json_mimetype("application/vnd.git-lfs+json")); - assert!(!is_git_lfs_json_mimetype("application/vnd.git-lfs")); - assert!(!is_git_lfs_json_mimetype("application/json")); - assert!(is_git_lfs_json_mimetype( - "application/vnd.git-lfs+json; charset=utf-8" - )); - assert!(is_git_lfs_json_mimetype( - "application/vnd.git-lfs+json; charset=UTF-8" - )); - assert!(!is_git_lfs_json_mimetype( - "application/vnd.git-lfs+json; charset=ISO-8859-1" - )); -} - -#[test] -fn test_deserialize() { - let json = r#"{"operation":"upload","objects":[{"oid":"8f4123f9a7181f488c5e111d82cefd992e461ae5df01fd2254399e6e670b2d3c","size":170904}], - "transfers":["lfs-standalone-file","basic","ssh"],"ref":{"name":"refs/heads/main"},"hash_algo":"sha256"}"#; - let expected = BatchRequest { - operation: common::Operation::Upload, - objects: vec![BatchRequestObject { - oid: "8f4123f9a7181f488c5e111d82cefd992e461ae5df01fd2254399e6e670b2d3c" - .parse() - .unwrap(), - size: 170904, - }], - transfers: vec![ - TransferAdapter::Unknown, - TransferAdapter::Basic, - TransferAdapter::Unknown, - ], - hash_algo: HashAlgo::Sha256, - }; - assert_eq!( - serde_json::from_str::(json).unwrap(), - expected - ); -} - -#[test] -fn test_validate_claims() { - let key = "00232f7a019bd34e3921ee6c5f04caf48a4489d1be5d1999038950a7054e0bfea369ce2becc0f13fd3c69f8af2384a25b7ac2d52eb52c33722f3c00c50d4c9c2"; - let key: common::Key = key.parse().unwrap(); - - let claims = common::Claims { - expires_at: Utc::now() + std::time::Duration::from_secs(5 * 60), - repo_path: "lfs-test.git", - specific_claims: common::SpecificClaims::BatchApi(common::Operation::Download), - }; - let tag = common::generate_tag(claims, &key).unwrap(); - let header_value = format!( - "Gitolfs3-Hmac-Sha256 {tag} {}", - claims.expires_at.timestamp() - ); - - let conf = AuthorizationConfig { - key, - trusted_forwarded_hosts: HashSet::new(), - }; - let verification_claims = VerifyClaimsInput { - repo_path: claims.repo_path, - specific_claims: claims.specific_claims, - }; - let mut headers = HeaderMap::new(); - headers.insert(header::AUTHORIZATION, header_value.try_into().unwrap()); - - assert!(verify_claims(&conf, &verification_claims, &headers).unwrap()); -} diff --git a/rs/shell/Cargo.toml b/rs/shell/Cargo.toml deleted file mode 100644 index 0dcb6d6..0000000 --- a/rs/shell/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "shell" -version = "0.1.0" -edition = "2021" - -[dependencies] diff --git a/rs/shell/src/main.rs b/rs/shell/src/main.rs deleted file mode 100644 index ef0ef48..0000000 --- a/rs/shell/src/main.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::{os::unix::process::CommandExt, process::ExitCode}; - -fn parse_sq(s: &str) -> Option<(String, &str)> { - #[derive(PartialEq, Eq)] - enum SqState { - Quoted, - Unquoted { may_escape: bool }, - UnquotedEscaped, - } - - let mut result = String::new(); - let mut state = SqState::Unquoted { may_escape: false }; - let mut remaining = ""; - for (i, c) in s.char_indices() { - match state { - SqState::Unquoted { may_escape: false } => { - if c != '\'' { - return None; - } - state = SqState::Quoted - } - SqState::Quoted => { - if c == '\'' { - state = SqState::Unquoted { may_escape: true }; - continue; - } - result.push(c); - } - SqState::Unquoted { may_escape: true } => { - if is_posix_space(c) { - remaining = &s[i..]; - break; - } - if c != '\\' { - return None; - } - state = SqState::UnquotedEscaped; - } - SqState::UnquotedEscaped => { - if c != '\\' && c != '!' { - return None; - } - result.push(c); - state = SqState::Unquoted { may_escape: false }; - } - } - } - - if state != (SqState::Unquoted { may_escape: true }) { - return None; - } - Some((result, remaining)) -} - -fn parse_cmd(mut cmd: &str) -> Option> { - let mut args = Vec::::new(); - - cmd = cmd.trim_matches(is_posix_space); - while cmd != "" { - if cmd.starts_with('\'') { - let (arg, remaining) = parse_sq(cmd)?; - args.push(arg); - cmd = remaining.trim_start_matches(is_posix_space); - } else if let Some((arg, remaining)) = cmd.split_once(is_posix_space) { - args.push(arg.to_owned()); - cmd = remaining.trim_start_matches(is_posix_space); - } else { - args.push(cmd.to_owned()); - cmd = ""; - } - } - - Some(args) -} - -fn is_posix_space(c: char) -> bool { - // Form feed: 0x0c - // Vertical tab: 0x0b - c == ' ' || c == '\x0c' || c == '\n' || c == '\r' || c == '\t' || c == '\x0b' -} - -fn main() -> ExitCode { - let bad_usage = ExitCode::from(2); - - let mut args = std::env::args().skip(1); - if args.next() != Some("-c".to_string()) { - eprintln!("Expected usage: shell -c "); - return bad_usage; - } - let Some(cmd) = args.next() else { - eprintln!("Missing argument for argument '-c'"); - return bad_usage; - }; - if args.next() != None { - eprintln!("Too many arguments passed"); - return bad_usage; - } - - let Some(mut cmd) = parse_cmd(&cmd) else { - eprintln!("Bad command"); - return bad_usage; - }; - - let Some(mut program) = cmd.drain(0..1).next() else { - eprintln!("Bad command"); - return bad_usage; - }; - if program == "git" { - let Some(subcommand) = cmd.drain(0..1).next() else { - eprintln!("Bad command"); - return bad_usage; - }; - program.push('-'); - program.push_str(&subcommand); - } - - let mut args = Vec::new(); - - let git_cmds = ["git-receive-pack", "git-upload-archive", "git-upload-pack"]; - if git_cmds.contains(&program.as_str()) { - if cmd.len() != 1 { - eprintln!("Bad command"); - return bad_usage; - } - let repository = cmd[0].trim_start_matches('/'); - args.push(repository); - } else if program == "git-lfs-authenticate" { - if cmd.len() != 2 { - eprintln!("Bad command"); - return bad_usage; - } - let repository = cmd[0].trim_start_matches('/'); - args.push(repository); - args.push(&cmd[1]); - } else { - eprintln!("Unknown command"); - return bad_usage; - } - - let e = std::process::Command::new(program).args(args).exec(); - eprintln!("Error: {e}"); - ExitCode::FAILURE -} diff --git a/server/Cargo.toml b/server/Cargo.toml new file mode 100644 index 0000000..edb76d8 --- /dev/null +++ b/server/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "server" +version = "0.1.0" +edition = "2021" + +[dependencies] +aws-config = { version = "1.1.2" } +aws-sdk-s3 = "1.12.0" +axum = "0.7" +base64 = "0.21" +chrono = { version = "0.4", features = ["serde"] } +common = { path = "../common" } +mime = "0.3" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1.35", features = ["full"] } +tokio-util = "0.7" +tower = "0.4" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/server/src/main.rs b/server/src/main.rs new file mode 100644 index 0000000..8baa0d6 --- /dev/null +++ b/server/src/main.rs @@ -0,0 +1,1028 @@ +use std::collections::HashMap; +use std::collections::HashSet; +use std::process::ExitCode; +use std::sync::Arc; + +use aws_sdk_s3::error::SdkError; +use aws_sdk_s3::operation::head_object::HeadObjectOutput; +use axum::extract::rejection; +use axum::extract::FromRequest; +use axum::extract::Path; +use axum::extract::State; +use axum::http::header; +use axum::http::HeaderMap; +use axum::http::HeaderValue; +use axum::response::Response; +use axum::Json; +use axum::ServiceExt; +use base64::prelude::*; +use chrono::DateTime; +use chrono::Utc; +use common::HexByte; +use serde::de; +use serde::de::DeserializeOwned; +use serde::Deserialize; +use serde::Serialize; +use tower::Layer; + +use axum::{ + async_trait, + extract::{FromRequestParts, OriginalUri, Request}, + http::{request::Parts, StatusCode, Uri}, + response::IntoResponse, + routing::{get, post}, + Extension, Router, +}; + +#[derive(Clone)] +struct RepositoryName(String); + +struct RepositoryNameRejection; + +impl IntoResponse for RepositoryNameRejection { + fn into_response(self) -> Response { + (StatusCode::INTERNAL_SERVER_ERROR, "Missing repository name").into_response() + } +} + +#[async_trait] +impl FromRequestParts for RepositoryName { + type Rejection = RepositoryNameRejection; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let Ok(Extension(repo_name)) = Extension::::from_request_parts(parts, state).await + else { + return Err(RepositoryNameRejection); + }; + Ok(repo_name) + } +} + +async fn rewrite_url( + mut req: axum::http::Request, +) -> Result, StatusCode> { + let uri = req.uri(); + let original_uri = OriginalUri(uri.clone()); + + let Some(path_and_query) = uri.path_and_query() else { + // L @ no path & query + return Err(StatusCode::BAD_REQUEST); + }; + let Some((repo, path)) = path_and_query.path().split_once("/info/lfs/objects") else { + return Err(StatusCode::NOT_FOUND); + }; + let repo = repo + .trim_start_matches('/') + .trim_end_matches('/') + .to_string(); + if !path.starts_with('/') || !repo.ends_with(".git") { + return Err(StatusCode::NOT_FOUND); + } + + let mut parts = uri.clone().into_parts(); + parts.path_and_query = match path_and_query.query() { + None => path.try_into().ok(), + Some(q) => format!("{path}?{q}").try_into().ok(), + }; + let Ok(new_uri) = Uri::from_parts(parts) else { + return Err(StatusCode::INTERNAL_SERVER_ERROR); + }; + + *req.uri_mut() = new_uri; + req.extensions_mut().insert(original_uri); + req.extensions_mut().insert(RepositoryName(repo)); + + Ok(req) +} + +struct AppState { + s3_client: aws_sdk_s3::Client, + s3_bucket: String, + authz_conf: AuthorizationConfig, + // Should not end with a slash. + base_url: String, +} + +struct Env { + s3_access_key_id: String, + s3_secret_access_key: String, + s3_bucket: String, + s3_region: String, + s3_endpoint: String, + base_url: String, + key_path: String, + listen_host: String, + listen_port: String, + trusted_forwarded_hosts: String, +} + +fn require_env(name: &str) -> Result { + std::env::var(name) + .map_err(|_| format!("environment variable {name} should be defined and valid")) +} + +impl Env { + fn load() -> Result { + Ok(Env { + s3_secret_access_key: require_env("GITOLFS3_S3_SECRET_ACCESS_KEY_FILE")?, + s3_access_key_id: require_env("GITOLFS3_S3_ACCESS_KEY_ID_FILE")?, + s3_region: require_env("GITOLFS3_S3_REGION")?, + s3_endpoint: require_env("GITOLFS3_S3_ENDPOINT")?, + s3_bucket: require_env("GITOLFS3_S3_BUCKET")?, + base_url: require_env("GITOLFS3_BASE_URL")?, + key_path: require_env("GITOLFS3_KEY_PATH")?, + listen_host: require_env("GITOLFS3_LISTEN_HOST")?, + listen_port: require_env("GITOLFS3_LISTEN_PORT")?, + trusted_forwarded_hosts: std::env::var("GITOLFS3_TRUSTED_FORWARDED_HOSTS") + .unwrap_or_default(), + }) + } +} + +fn get_s3_client(env: &Env) -> Result { + let access_key_id = std::fs::read_to_string(&env.s3_access_key_id)?; + let secret_access_key = std::fs::read_to_string(&env.s3_secret_access_key)?; + + let credentials = aws_sdk_s3::config::Credentials::new( + access_key_id, + secret_access_key, + None, + None, + "gitolfs3-env", + ); + let config = aws_config::SdkConfig::builder() + .behavior_version(aws_config::BehaviorVersion::latest()) + .region(aws_config::Region::new(env.s3_region.clone())) + .endpoint_url(&env.s3_endpoint) + .credentials_provider(aws_sdk_s3::config::SharedCredentialsProvider::new( + credentials, + )) + .build(); + Ok(aws_sdk_s3::Client::new(&config)) +} + +#[tokio::main] +async fn main() -> ExitCode { + tracing_subscriber::fmt::init(); + + let env = match Env::load() { + Ok(env) => env, + Err(e) => { + println!("Failed to load configuration: {e}"); + return ExitCode::from(2); + } + }; + + let s3_client = match get_s3_client(&env) { + Ok(s3_client) => s3_client, + Err(e) => { + println!("Failed to create S3 client: {e}"); + return ExitCode::FAILURE; + } + }; + let key = match common::load_key(&env.key_path) { + Ok(key) => key, + Err(e) => { + println!("Failed to load Gitolfs3 key: {e}"); + return ExitCode::FAILURE; + } + }; + + let trusted_forwarded_hosts: HashSet = env + .trusted_forwarded_hosts + .split(',') + .map(|s| s.to_owned()) + .filter(|s| !s.is_empty()) + .collect(); + let base_url = env.base_url.trim_end_matches('/').to_string(); + + let authz_conf = AuthorizationConfig { + key, + trusted_forwarded_hosts, + }; + + let shared_state = Arc::new(AppState { + s3_client, + s3_bucket: env.s3_bucket, + authz_conf, + base_url, + }); + let app = Router::new() + .route("/batch", post(batch)) + .route("/:oid0/:oid1/:oid", get(obj_download)) + .with_state(shared_state); + + let middleware = axum::middleware::map_request(rewrite_url); + let app_with_middleware = middleware.layer(app); + + let Ok(listen_port): Result = env.listen_port.parse() else { + println!("Configured LISTEN_PORT should be an unsigned integer no higher than 65535"); + return ExitCode::from(2); + }; + let addr: (String, u16) = (env.listen_host, listen_port); + let listener = match tokio::net::TcpListener::bind(addr).await { + Ok(listener) => listener, + Err(e) => { + println!("Failed to listen: {e}"); + return ExitCode::FAILURE; + } + }; + + match axum::serve(listener, app_with_middleware.into_make_service()).await { + Ok(_) => ExitCode::SUCCESS, + Err(e) => { + println!("Error serving: {e}"); + ExitCode::FAILURE + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] +enum TransferAdapter { + #[serde(rename = "basic")] + Basic, + #[serde(other)] + Unknown, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] +enum HashAlgo { + #[serde(rename = "sha256")] + Sha256, + #[serde(other)] + Unknown, +} + +impl Default for HashAlgo { + fn default() -> Self { + Self::Sha256 + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +struct BatchRequestObject { + oid: common::Oid, + size: i64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct BatchRef { + name: String, +} + +fn default_transfers() -> Vec { + vec![TransferAdapter::Basic] +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +struct BatchRequest { + operation: common::Operation, + #[serde(default = "default_transfers")] + transfers: Vec, + objects: Vec, + #[serde(default)] + hash_algo: HashAlgo, +} + +#[derive(Debug, Clone)] +struct GitLfsJson(Json); + +const LFS_MIME: &str = "application/vnd.git-lfs+json"; + +enum GitLfsJsonRejection { + Json(rejection::JsonRejection), + MissingGitLfsJsonContentType, +} + +impl IntoResponse for GitLfsJsonRejection { + fn into_response(self) -> Response { + match self { + Self::Json(rej) => rej.into_response(), + Self::MissingGitLfsJsonContentType => make_error_resp( + StatusCode::UNSUPPORTED_MEDIA_TYPE, + &format!("Expected request with `Content-Type: {LFS_MIME}`"), + ) + .into_response(), + } + } +} + +fn is_git_lfs_json_mimetype(mimetype: &str) -> bool { + let Ok(mime) = mimetype.parse::() else { + return false; + }; + if mime.type_() != mime::APPLICATION + || mime.subtype() != "vnd.git-lfs" + || mime.suffix() != Some(mime::JSON) + { + return false; + } + match mime.get_param(mime::CHARSET) { + Some(mime::UTF_8) | None => true, + Some(_) => false, + } +} + +fn has_git_lfs_json_content_type(req: &Request) -> bool { + let Some(content_type) = req.headers().get(header::CONTENT_TYPE) else { + return false; + }; + let Ok(content_type) = content_type.to_str() else { + return false; + }; + is_git_lfs_json_mimetype(content_type) +} + +#[async_trait] +impl FromRequest for GitLfsJson +where + T: DeserializeOwned, + S: Send + Sync, +{ + type Rejection = GitLfsJsonRejection; + + async fn from_request(req: Request, state: &S) -> Result { + if !has_git_lfs_json_content_type(&req) { + return Err(GitLfsJsonRejection::MissingGitLfsJsonContentType); + } + Json::::from_request(req, state) + .await + .map(GitLfsJson) + .map_err(GitLfsJsonRejection::Json) + } +} + +impl IntoResponse for GitLfsJson { + fn into_response(self) -> Response { + let GitLfsJson(json) = self; + let mut resp = json.into_response(); + resp.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static("application/vnd.git-lfs+json; charset=utf-8"), + ); + resp + } +} + +#[derive(Debug, Serialize)] +struct GitLfsErrorData<'a> { + message: &'a str, +} + +type GitLfsErrorResponse<'a> = (StatusCode, GitLfsJson>); + +const fn make_error_resp(code: StatusCode, message: &str) -> GitLfsErrorResponse { + (code, GitLfsJson(Json(GitLfsErrorData { message }))) +} + +#[derive(Debug, Serialize, Clone)] +struct BatchResponseObjectAction { + href: String, + #[serde(skip_serializing_if = "HashMap::is_empty")] + header: HashMap, + expires_at: DateTime, +} + +#[derive(Default, Debug, Serialize, Clone)] +struct BatchResponseObjectActions { + #[serde(skip_serializing_if = "Option::is_none")] + upload: Option, + #[serde(skip_serializing_if = "Option::is_none")] + download: Option, + #[serde(skip_serializing_if = "Option::is_none")] + verify: Option, +} + +#[derive(Debug, Clone, Serialize)] +struct BatchResponseObjectError { + code: u16, + message: String, +} + +#[derive(Debug, Serialize, Clone)] +struct BatchResponseObject { + oid: common::Oid, + size: i64, + #[serde(skip_serializing_if = "Option::is_none")] + authenticated: Option, + actions: BatchResponseObjectActions, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, +} + +impl BatchResponseObject { + fn error(obj: &BatchRequestObject, code: StatusCode, message: String) -> BatchResponseObject { + BatchResponseObject { + oid: obj.oid, + size: obj.size, + authenticated: None, + actions: Default::default(), + error: Some(BatchResponseObjectError { + code: code.as_u16(), + message, + }), + } + } +} + +#[derive(Debug, Serialize, Clone)] +struct BatchResponse { + transfer: TransferAdapter, + objects: Vec, + hash_algo: HashAlgo, +} + +fn validate_checksum(oid: common::Oid, obj: &HeadObjectOutput) -> bool { + if let Some(checksum) = obj.checksum_sha256() { + if let Ok(checksum) = BASE64_STANDARD.decode(checksum) { + if let Ok(checksum32b) = TryInto::<[u8; 32]>::try_into(checksum) { + return common::Oid::from(checksum32b) == oid; + } + } + } + true +} + +fn validate_size(expected: i64, obj: &HeadObjectOutput) -> bool { + if let Some(length) = obj.content_length() { + return length == expected; + } + true +} + +async fn handle_upload_object( + state: &AppState, + repo: &str, + obj: &BatchRequestObject, +) -> Option { + let (oid0, oid1) = (HexByte(obj.oid[0]), HexByte(obj.oid[1])); + let full_path = format!("{repo}/lfs/objects/{}/{}/{}", oid0, oid1, obj.oid); + + match state + .s3_client + .head_object() + .bucket(&state.s3_bucket) + .key(full_path.clone()) + .checksum_mode(aws_sdk_s3::types::ChecksumMode::Enabled) + .send() + .await + { + Ok(result) => { + if validate_size(obj.size, &result) && validate_checksum(obj.oid, &result) { + return None; + } + } + Err(SdkError::ServiceError(e)) if e.err().is_not_found() => {} + Err(e) => { + println!("Failed to HeadObject (repo {repo}, OID {}): {e}", obj.oid); + return Some(BatchResponseObject::error( + obj, + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to query object information".to_string(), + )); + } + }; + + let expires_in = std::time::Duration::from_secs(5 * 60); + let expires_at = Utc::now() + expires_in; + + let Ok(config) = aws_sdk_s3::presigning::PresigningConfig::expires_in(expires_in) else { + return Some(BatchResponseObject::error( + obj, + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to generate upload URL".to_string(), + )); + }; + let Ok(presigned) = state + .s3_client + .put_object() + .bucket(&state.s3_bucket) + .key(full_path) + .checksum_sha256(obj.oid.to_string()) + .content_length(obj.size) + .presigned(config) + .await + else { + return Some(BatchResponseObject::error( + obj, + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to generate upload URL".to_string(), + )); + }; + Some(BatchResponseObject { + oid: obj.oid, + size: obj.size, + authenticated: Some(true), + actions: BatchResponseObjectActions { + upload: Some(BatchResponseObjectAction { + header: presigned + .headers() + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect(), + expires_at, + href: presigned.uri().to_string(), + }), + ..Default::default() + }, + error: None, + }) +} + +async fn handle_download_object( + state: &AppState, + repo: &str, + obj: &BatchRequestObject, + trusted: bool, +) -> BatchResponseObject { + let (oid0, oid1) = (HexByte(obj.oid[0]), HexByte(obj.oid[1])); + let full_path = format!("{repo}/lfs/objects/{}/{}/{}", oid0, oid1, obj.oid); + + let result = match state + .s3_client + .head_object() + .bucket(&state.s3_bucket) + .key(&full_path) + .checksum_mode(aws_sdk_s3::types::ChecksumMode::Enabled) + .send() + .await + { + Ok(result) => result, + Err(e) => { + println!("Failed to HeadObject (repo {repo}, OID {}): {e}", obj.oid); + return BatchResponseObject::error( + obj, + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to query object information".to_string(), + ); + } + }; + + // Scaleway actually doesn't provide SHA256 suport, but maybe in the future :) + if !validate_checksum(obj.oid, &result) { + return BatchResponseObject::error( + obj, + StatusCode::UNPROCESSABLE_ENTITY, + "Object corrupted".to_string(), + ); + } + if !validate_size(obj.size, &result) { + return BatchResponseObject::error( + obj, + StatusCode::UNPROCESSABLE_ENTITY, + "Incorrect size specified (or object corrupted)".to_string(), + ); + } + + let expires_in = std::time::Duration::from_secs(5 * 60); + let expires_at = Utc::now() + expires_in; + + if trusted { + let Ok(config) = aws_sdk_s3::presigning::PresigningConfig::expires_in(expires_in) else { + return BatchResponseObject::error( + obj, + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to generate upload URL".to_string(), + ); + }; + let Ok(presigned) = state + .s3_client + .get_object() + .bucket(&state.s3_bucket) + .key(full_path) + .presigned(config) + .await + else { + return BatchResponseObject::error( + obj, + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to generate upload URL".to_string(), + ); + }; + return BatchResponseObject { + oid: obj.oid, + size: obj.size, + authenticated: Some(true), + actions: BatchResponseObjectActions { + download: Some(BatchResponseObjectAction { + header: presigned + .headers() + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect(), + expires_at, + href: presigned.uri().to_string(), + }), + ..Default::default() + }, + error: None, + }; + } + + let Some(tag) = common::generate_tag( + common::Claims { + specific_claims: common::SpecificClaims::Download(obj.oid), + repo_path: repo, + expires_at, + }, + &state.authz_conf.key, + ) else { + return BatchResponseObject::error( + obj, + StatusCode::INTERNAL_SERVER_ERROR, + "Internal server error".to_string(), + ); + }; + + let upload_path = format!( + "{repo}/info/lfs/objects/{}/{}/{}", + HexByte(obj.oid[0]), + HexByte(obj.oid[1]), + obj.oid, + ); + + BatchResponseObject { + oid: obj.oid, + size: obj.size, + authenticated: Some(true), + actions: BatchResponseObjectActions { + download: Some(BatchResponseObjectAction { + header: { + let mut map = HashMap::new(); + map.insert( + "Authorization".to_string(), + format!("Gitolfs3-Hmac-Sha256 {tag} {}", expires_at.timestamp()), + ); + map + }, + expires_at, + href: format!("{}/{upload_path}", state.base_url), + }), + ..Default::default() + }, + error: None, + } +} + +struct AuthorizationConfig { + trusted_forwarded_hosts: HashSet, + key: common::Key, +} + +struct Trusted(bool); + +fn forwarded_for_trusted_host( + headers: &HeaderMap, + trusted: &HashSet, +) -> Result> { + if let Some(forwarded_for) = headers.get("X-Forwarded-For") { + if let Ok(forwarded_for) = forwarded_for.to_str() { + if trusted.contains(forwarded_for) { + return Ok(true); + } + } else { + return Err(make_error_resp( + StatusCode::NOT_FOUND, + "Invalid X-Forwarded-For header", + )); + } + } + Ok(false) +} +const REPO_NOT_FOUND: GitLfsErrorResponse = + make_error_resp(StatusCode::NOT_FOUND, "Repository not found"); + +fn authorize_batch( + conf: &AuthorizationConfig, + repo_path: &str, + public: bool, + operation: common::Operation, + headers: &HeaderMap, +) -> Result> { + // - No authentication required for downloading exported repos + // - When authenticated: + // - Download / upload over presigned URLs + // - When accessing over Tailscale: + // - No authentication required for downloading from any repo + + let claims = VerifyClaimsInput { + specific_claims: common::SpecificClaims::BatchApi(operation), + repo_path, + }; + if verify_claims(conf, &claims, headers)? { + return Ok(Trusted(true)); + } + + let trusted = forwarded_for_trusted_host(headers, &conf.trusted_forwarded_hosts)?; + if operation != common::Operation::Download { + if trusted { + return Err(make_error_resp( + StatusCode::FORBIDDEN, + "Authentication required to upload", + )); + } + return Err(REPO_NOT_FOUND); + } + if trusted { + return Ok(Trusted(true)); + } + + if public { + Ok(Trusted(false)) + } else { + Err(REPO_NOT_FOUND) + } +} + +fn repo_exists(name: &str) -> bool { + let Ok(metadata) = std::fs::metadata(name) else { + return false; + }; + metadata.is_dir() +} + +fn is_repo_public(name: &str) -> Option { + if !repo_exists(name) { + return None; + } + std::fs::metadata(format!("{name}/git-daemon-export-ok")) + .ok()? + .is_file() + .into() +} + +async fn batch( + State(state): State>, + headers: HeaderMap, + RepositoryName(repo): RepositoryName, + GitLfsJson(Json(payload)): GitLfsJson, +) -> Response { + let Some(public) = is_repo_public(&repo) else { + return REPO_NOT_FOUND.into_response(); + }; + let Trusted(trusted) = match authorize_batch( + &state.authz_conf, + &repo, + public, + payload.operation, + &headers, + ) { + Ok(authn) => authn, + Err(e) => return e.into_response(), + }; + + if !headers + .get_all("Accept") + .iter() + .filter_map(|v| v.to_str().ok()) + .any(is_git_lfs_json_mimetype) + { + let message = format!("Expected `{LFS_MIME}` in list of acceptable response media types"); + return make_error_resp(StatusCode::NOT_ACCEPTABLE, &message).into_response(); + } + + if payload.hash_algo != HashAlgo::Sha256 { + let message = "Unsupported hashing algorithm specified"; + return make_error_resp(StatusCode::CONFLICT, message).into_response(); + } + if !payload.transfers.is_empty() && !payload.transfers.contains(&TransferAdapter::Basic) { + let message = "Unsupported transfer adapter specified (supported: basic)"; + return make_error_resp(StatusCode::CONFLICT, message).into_response(); + } + + let mut resp = BatchResponse { + transfer: TransferAdapter::Basic, + objects: vec![], + hash_algo: HashAlgo::Sha256, + }; + for obj in payload.objects { + match payload.operation { + common::Operation::Download => resp + .objects + .push(handle_download_object(&state, &repo, &obj, trusted).await), + common::Operation::Upload => { + if let Some(obj_resp) = handle_upload_object(&state, &repo, &obj).await { + resp.objects.push(obj_resp); + } + } + }; + } + GitLfsJson(Json(resp)).into_response() +} + +#[derive(Deserialize, Copy, Clone)] +#[serde(remote = "Self")] +struct FileParams { + oid0: HexByte, + oid1: HexByte, + oid: common::Oid, +} + +impl<'de> Deserialize<'de> for FileParams { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let unchecked @ FileParams { + oid0: HexByte(oid0), + oid1: HexByte(oid1), + oid, + } = FileParams::deserialize(deserializer)?; + if oid0 != oid.as_bytes()[0] { + return Err(de::Error::custom( + "first OID path part does not match first byte of full OID", + )); + } + if oid1 != oid.as_bytes()[1] { + return Err(de::Error::custom( + "second OID path part does not match first byte of full OID", + )); + } + Ok(unchecked) + } +} + +pub struct VerifyClaimsInput<'a> { + pub specific_claims: common::SpecificClaims, + pub repo_path: &'a str, +} + +fn verify_claims( + conf: &AuthorizationConfig, + claims: &VerifyClaimsInput, + headers: &HeaderMap, +) -> Result> { + const INVALID_AUTHZ_HEADER: GitLfsErrorResponse = + make_error_resp(StatusCode::BAD_REQUEST, "Invalid authorization header"); + + if let Some(authz) = headers.get(header::AUTHORIZATION) { + if let Ok(authz) = authz.to_str() { + if let Some(val) = authz.strip_prefix("Gitolfs3-Hmac-Sha256 ") { + let (tag, expires_at) = val.split_once(' ').ok_or(INVALID_AUTHZ_HEADER)?; + let tag: common::Digest<32> = tag.parse().map_err(|_| INVALID_AUTHZ_HEADER)?; + let expires_at: i64 = expires_at.parse().map_err(|_| INVALID_AUTHZ_HEADER)?; + let expires_at = + DateTime::::from_timestamp(expires_at, 0).ok_or(INVALID_AUTHZ_HEADER)?; + let Some(expected_tag) = common::generate_tag( + common::Claims { + specific_claims: claims.specific_claims, + repo_path: claims.repo_path, + expires_at, + }, + &conf.key, + ) else { + return Err(make_error_resp( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal server error", + )); + }; + if tag == expected_tag { + return Ok(true); + } + } + } + return Err(INVALID_AUTHZ_HEADER); + } + Ok(false) +} + +fn authorize_get( + conf: &AuthorizationConfig, + repo_path: &str, + oid: common::Oid, + headers: &HeaderMap, +) -> Result<(), GitLfsErrorResponse<'static>> { + let claims = VerifyClaimsInput { + specific_claims: common::SpecificClaims::Download(oid), + repo_path, + }; + if !verify_claims(conf, &claims, headers)? { + return Err(make_error_resp( + StatusCode::UNAUTHORIZED, + "Repository not found", + )); + } + Ok(()) +} + +async fn obj_download( + State(state): State>, + headers: HeaderMap, + RepositoryName(repo): RepositoryName, + Path(FileParams { oid0, oid1, oid }): Path, +) -> Response { + if let Err(e) = authorize_get(&state.authz_conf, &repo, oid, &headers) { + return e.into_response(); + } + + let full_path = format!("{repo}/lfs/objects/{}/{}/{}", oid0, oid1, oid); + let result = match state + .s3_client + .get_object() + .bucket(&state.s3_bucket) + .key(full_path) + .checksum_mode(aws_sdk_s3::types::ChecksumMode::Enabled) + .send() + .await + { + Ok(result) => result, + Err(e) => { + println!("Failed to GetObject (repo {repo}, OID {oid}): {e}"); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to query object information", + ) + .into_response(); + } + }; + + let mut headers = header::HeaderMap::new(); + if let Some(content_type) = result.content_type { + let Ok(header_value) = content_type.try_into() else { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Object has invalid content type", + ) + .into_response(); + }; + headers.insert(header::CONTENT_TYPE, header_value); + } + if let Some(content_length) = result.content_length { + headers.insert(header::CONTENT_LENGTH, content_length.into()); + } + + let async_read = result.body.into_async_read(); + let stream = tokio_util::io::ReaderStream::new(async_read); + let body = axum::body::Body::from_stream(stream); + + (headers, body).into_response() +} + +#[test] +fn test_mimetype() { + assert!(is_git_lfs_json_mimetype("application/vnd.git-lfs+json")); + assert!(!is_git_lfs_json_mimetype("application/vnd.git-lfs")); + assert!(!is_git_lfs_json_mimetype("application/json")); + assert!(is_git_lfs_json_mimetype( + "application/vnd.git-lfs+json; charset=utf-8" + )); + assert!(is_git_lfs_json_mimetype( + "application/vnd.git-lfs+json; charset=UTF-8" + )); + assert!(!is_git_lfs_json_mimetype( + "application/vnd.git-lfs+json; charset=ISO-8859-1" + )); +} + +#[test] +fn test_deserialize() { + let json = r#"{"operation":"upload","objects":[{"oid":"8f4123f9a7181f488c5e111d82cefd992e461ae5df01fd2254399e6e670b2d3c","size":170904}], + "transfers":["lfs-standalone-file","basic","ssh"],"ref":{"name":"refs/heads/main"},"hash_algo":"sha256"}"#; + let expected = BatchRequest { + operation: common::Operation::Upload, + objects: vec![BatchRequestObject { + oid: "8f4123f9a7181f488c5e111d82cefd992e461ae5df01fd2254399e6e670b2d3c" + .parse() + .unwrap(), + size: 170904, + }], + transfers: vec![ + TransferAdapter::Unknown, + TransferAdapter::Basic, + TransferAdapter::Unknown, + ], + hash_algo: HashAlgo::Sha256, + }; + assert_eq!( + serde_json::from_str::(json).unwrap(), + expected + ); +} + +#[test] +fn test_validate_claims() { + let key = "00232f7a019bd34e3921ee6c5f04caf48a4489d1be5d1999038950a7054e0bfea369ce2becc0f13fd3c69f8af2384a25b7ac2d52eb52c33722f3c00c50d4c9c2"; + let key: common::Key = key.parse().unwrap(); + + let claims = common::Claims { + expires_at: Utc::now() + std::time::Duration::from_secs(5 * 60), + repo_path: "lfs-test.git", + specific_claims: common::SpecificClaims::BatchApi(common::Operation::Download), + }; + let tag = common::generate_tag(claims, &key).unwrap(); + let header_value = format!( + "Gitolfs3-Hmac-Sha256 {tag} {}", + claims.expires_at.timestamp() + ); + + let conf = AuthorizationConfig { + key, + trusted_forwarded_hosts: HashSet::new(), + }; + let verification_claims = VerifyClaimsInput { + repo_path: claims.repo_path, + specific_claims: claims.specific_claims, + }; + let mut headers = HeaderMap::new(); + headers.insert(header::AUTHORIZATION, header_value.try_into().unwrap()); + + assert!(verify_claims(&conf, &verification_claims, &headers).unwrap()); +} diff --git a/shell/Cargo.toml b/shell/Cargo.toml new file mode 100644 index 0000000..0dcb6d6 --- /dev/null +++ b/shell/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "shell" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/shell/src/main.rs b/shell/src/main.rs new file mode 100644 index 0000000..ef0ef48 --- /dev/null +++ b/shell/src/main.rs @@ -0,0 +1,143 @@ +use std::{os::unix::process::CommandExt, process::ExitCode}; + +fn parse_sq(s: &str) -> Option<(String, &str)> { + #[derive(PartialEq, Eq)] + enum SqState { + Quoted, + Unquoted { may_escape: bool }, + UnquotedEscaped, + } + + let mut result = String::new(); + let mut state = SqState::Unquoted { may_escape: false }; + let mut remaining = ""; + for (i, c) in s.char_indices() { + match state { + SqState::Unquoted { may_escape: false } => { + if c != '\'' { + return None; + } + state = SqState::Quoted + } + SqState::Quoted => { + if c == '\'' { + state = SqState::Unquoted { may_escape: true }; + continue; + } + result.push(c); + } + SqState::Unquoted { may_escape: true } => { + if is_posix_space(c) { + remaining = &s[i..]; + break; + } + if c != '\\' { + return None; + } + state = SqState::UnquotedEscaped; + } + SqState::UnquotedEscaped => { + if c != '\\' && c != '!' { + return None; + } + result.push(c); + state = SqState::Unquoted { may_escape: false }; + } + } + } + + if state != (SqState::Unquoted { may_escape: true }) { + return None; + } + Some((result, remaining)) +} + +fn parse_cmd(mut cmd: &str) -> Option> { + let mut args = Vec::::new(); + + cmd = cmd.trim_matches(is_posix_space); + while cmd != "" { + if cmd.starts_with('\'') { + let (arg, remaining) = parse_sq(cmd)?; + args.push(arg); + cmd = remaining.trim_start_matches(is_posix_space); + } else if let Some((arg, remaining)) = cmd.split_once(is_posix_space) { + args.push(arg.to_owned()); + cmd = remaining.trim_start_matches(is_posix_space); + } else { + args.push(cmd.to_owned()); + cmd = ""; + } + } + + Some(args) +} + +fn is_posix_space(c: char) -> bool { + // Form feed: 0x0c + // Vertical tab: 0x0b + c == ' ' || c == '\x0c' || c == '\n' || c == '\r' || c == '\t' || c == '\x0b' +} + +fn main() -> ExitCode { + let bad_usage = ExitCode::from(2); + + let mut args = std::env::args().skip(1); + if args.next() != Some("-c".to_string()) { + eprintln!("Expected usage: shell -c "); + return bad_usage; + } + let Some(cmd) = args.next() else { + eprintln!("Missing argument for argument '-c'"); + return bad_usage; + }; + if args.next() != None { + eprintln!("Too many arguments passed"); + return bad_usage; + } + + let Some(mut cmd) = parse_cmd(&cmd) else { + eprintln!("Bad command"); + return bad_usage; + }; + + let Some(mut program) = cmd.drain(0..1).next() else { + eprintln!("Bad command"); + return bad_usage; + }; + if program == "git" { + let Some(subcommand) = cmd.drain(0..1).next() else { + eprintln!("Bad command"); + return bad_usage; + }; + program.push('-'); + program.push_str(&subcommand); + } + + let mut args = Vec::new(); + + let git_cmds = ["git-receive-pack", "git-upload-archive", "git-upload-pack"]; + if git_cmds.contains(&program.as_str()) { + if cmd.len() != 1 { + eprintln!("Bad command"); + return bad_usage; + } + let repository = cmd[0].trim_start_matches('/'); + args.push(repository); + } else if program == "git-lfs-authenticate" { + if cmd.len() != 2 { + eprintln!("Bad command"); + return bad_usage; + } + let repository = cmd[0].trim_start_matches('/'); + args.push(repository); + args.push(&cmd[1]); + } else { + eprintln!("Unknown command"); + return bad_usage; + } + + let e = std::process::Command::new(program).args(args).exec(); + eprintln!("Error: {e}"); + ExitCode::FAILURE +} -- cgit v1.2.3