diff --git a/Cargo.lock b/Cargo.lock index c6dedd8..594f7cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -38,6 +47,17 @@ dependencies = [ "term", ] +[[package]] +name = "async-recursion" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.53" @@ -66,6 +86,27 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.5.3", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + [[package]] name = "base32" version = "0.4.0" @@ -78,6 +119,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bdca834647821e0b13d9539a8634eb62d3501b6b6c2cec1722786ee6671b851" + [[package]] name = "beef" version = "0.5.1" @@ -128,15 +175,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.10.2" @@ -166,9 +204,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "candid" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba9e536514a3c655568e23e36e68cbef20ee6595f641719ade03a849a13ed0ac" +checksum = "a4f5cdd24185c6d6edba09bcfe06cf9c1e7fd579cbe5e4ae2dffba9c474a5a7f" dependencies = [ "anyhow", "binread", @@ -238,7 +276,7 @@ version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16" dependencies = [ - "heck 0.4.0", + "heck", "proc-macro-error", "proc-macro2", "quote", @@ -269,6 +307,12 @@ dependencies = [ "tokio-util 0.7.1", ] +[[package]] +name = "const-oid" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "722e23542a15cea1f65d4a1419c4cfd7a26706c70871a13a04238ca3f40f1661" + [[package]] name = "core-foundation" version = "0.9.3" @@ -329,6 +373,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f2b443d17d49dad5ef0ede301c3179cc923b8822f3393b4d2c28c269dd4a122" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.3" @@ -340,28 +396,37 @@ dependencies = [ ] [[package]] -name = "diff" -version = "0.1.12" +name = "data-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" [[package]] -name = "digest" -version = "0.9.0" +name = "der" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "13dd2ae565c0a381dde7fade45fce95984c568bdcb4700a4fdbe3175e0380b2f" dependencies = [ - "generic-array", + "const-oid", + "pem-rfc7468", + "zeroize", ] +[[package]] +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + [[package]] name = "digest" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ - "block-buffer 0.10.2", + "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -391,12 +456,45 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" +[[package]] +name = "ecdsa" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd46e0c364655e5baf2f5e99b603e7a09905da9966d7928d7470af393b28670" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint", + "der", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "ena" version = "0.14.0" @@ -415,6 +513,28 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "fastrand" version = "1.7.0" @@ -424,6 +544,16 @@ dependencies = [ "instant", ] +[[package]] +name = "ff" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df689201f395c6b90dfe87127685f8dbfc083a5e779e613575d8bd7314300c3e" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "fixedbitset" version = "0.2.0" @@ -439,7 +569,7 @@ dependencies = [ "cfg-if", "crc32fast", "libc", - "miniz_oxide", + "miniz_oxide 0.4.4", ] [[package]] @@ -564,6 +694,23 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + +[[package]] +name = "group" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7391856def869c1c81063a03457c676fbcd419709c3dfb33d8d319de484b154d" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "h2" version = "0.3.12" @@ -595,15 +742,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.4.0" @@ -625,6 +763,15 @@ 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 = "http" version = "0.2.6" @@ -638,9 +785,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", @@ -714,30 +861,35 @@ dependencies = [ [[package]] name = "ic-agent" -version = "0.15.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b198398233a8bd3b8247bc1922899c257ba7985bea1c197b0ee31bef9d3043" +checksum = "665da6fb50b32661c306ddc538ee5aa6f6d68f3d54b1649c1595403acaa552cb" dependencies = [ "async-trait", "base32", "base64", "byteorder", + "futures-util", "garcon", "hex", "http", + "http-body", "hyper-rustls", "ic-types", + "k256", "leb128", "mime", - "openssl", "pem", + "pkcs8", "rand", "reqwest", "ring", "rustls", + "sec1", "serde", "serde_bytes", "serde_cbor", + "sha2", "simple_asn1", "thiserror", "url", @@ -745,24 +897,24 @@ dependencies = [ [[package]] name = "ic-types" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e78ec6f58886cdc252d6f912dc794211bd6bbc39ddc9dcda434b2dc16c335b3" +checksum = "82e9cac29df1f2906137c327ed24dfc398460eda054abaa6107273c11afe6384" dependencies = [ - "base32", "crc32fast", + "data-encoding", "hex", "serde", "serde_bytes", - "sha2 0.9.9", + "sha2", "thiserror", ] [[package]] name = "ic-utils" -version = "0.15.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36568bc5b48c8a1bb5506522a803cc581b3c7b7b9377eba32bf0ed6b8d5689d1" +checksum = "e2ff623519da2958ce6c4c8623ef5530db7878ef0082556b3c1cc1cfde7ba9d6" dependencies = [ "async-trait", "candid", @@ -770,7 +922,9 @@ dependencies = [ "ic-agent", "leb128", "num-bigint", + "once_cell", "paste", + "semver", "serde", "serde_bytes", "strum", @@ -783,10 +937,12 @@ name = "icx-proxy" version = "0.9.2" dependencies = [ "anyhow", + "async-recursion", "async-trait", "base64", "candid", "clap", + "failure", "flate2", "garcon", "hex", @@ -799,7 +955,7 @@ dependencies = [ "serde", "serde_cbor", "serde_json", - "sha2 0.10.2", + "sha2", "slog", "slog-async", "slog-term", @@ -873,6 +1029,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8a5a96d92d849c4499d99461da81c9cdc1467418a8ed2aaeb407e8d85940ed" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + [[package]] name = "lalrpop" version = "0.19.7" @@ -1016,6 +1184,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.1" @@ -1143,16 +1320,19 @@ dependencies = [ ] [[package]] -name = "once_cell" -version = "1.10.0" +name = "object" +version = "0.28.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" +dependencies = [ + "memchr", +] [[package]] -name = "opaque-debug" -version = "0.3.0" +name = "once_cell" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" [[package]] name = "openssl" @@ -1259,6 +1439,15 @@ dependencies = [ "base64", ] +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1302,6 +1491,16 @@ 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 = "pkg-config" version = "0.3.24" @@ -1502,13 +1701,11 @@ dependencies = [ "http-body", "hyper", "hyper-rustls", - "hyper-tls", "ipnet", "js-sys", "lazy_static", "log", "mime", - "native-tls", "percent-encoding", "pin-project-lite", "rustls", @@ -1517,8 +1714,8 @@ dependencies = [ "serde_json", "serde_urlencoded", "tokio", - "tokio-native-tls", "tokio-rustls", + "tokio-util 0.6.9", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -1527,6 +1724,17 @@ dependencies = [ "winreg", ] +[[package]] +name = "rfc6979" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c86280f057430a52f4861551b092a01b419b8eacefc7c995eacb9dc132fe32" +dependencies = [ + "crypto-bigint", + "hmac", + "zeroize", +] + [[package]] name = "ring" version = "0.16.20" @@ -1542,6 +1750,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + [[package]] name = "rustls" version = "0.20.4" @@ -1622,6 +1836,20 @@ dependencies = [ "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.6.1" @@ -1645,6 +1873,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" + [[package]] name = "serde" version = "1.0.136" @@ -1722,19 +1956,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "sha2" version = "0.10.2" @@ -1743,7 +1964,7 @@ checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.3", + "digest", ] [[package]] @@ -1755,6 +1976,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "simple_asn1" version = "0.6.1" @@ -1832,6 +2063,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "string_cache" version = "0.8.3" @@ -1853,23 +2094,29 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" [[package]] name = "strum_macros" -version = "0.23.1" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck 0.3.3", + "heck", "proc-macro2", "quote", "rustversion", "syn", ] +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.89" @@ -1881,6 +2128,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "take_mut" version = "0.2.2" @@ -2159,12 +2418,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" - [[package]] name = "unicode-width" version = "0.1.9" @@ -2412,3 +2665,9 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" diff --git a/Cargo.toml b/Cargo.toml index 1072a63..8a097ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,8 +26,8 @@ garcon = { version = "0.2.3", features = ["async"] } hex = "0.4" hyper = { version = "0.14", features = ["full"] } hyper-tls = "0.5" -ic-agent = { version = "0.15" } -ic-utils = { version = "0.15", features = ["raw"] } +ic-agent = { version = "0.20.0" } +ic-utils = { version = "0.20.0", features = ["raw"] } lazy-regex = "2" tokio = { version = "1", features = ["full"] } serde = "1" @@ -40,7 +40,8 @@ slog-term = "2" url = "2" redis = { version = "0.21.5", features = ["aio", "tokio-comp"] } async-trait = "0.1.53" - +async-recursion = "1.0.0" +failure = "0.1.8" [features] skip_body_verification = [] \ No newline at end of file diff --git a/dump.rdb b/dump.rdb new file mode 100644 index 0000000..2b9ebba Binary files /dev/null and b/dump.rdb differ diff --git a/src/canister.rs b/src/canister.rs index eec6b80..f50026e 100644 --- a/src/canister.rs +++ b/src/canister.rs @@ -104,10 +104,6 @@ pub async fn resolve_canister_id_from_uri( canister_id_resolver: impl ResolveCanisterId, logger: &slog::Logger, ) -> Option<(Principal, String)> { - // let (_, canister_id) = url::form_urlencoded::parse(url.query()?.as_bytes()) - // .find(|(name, _)| name == "canisterId")?; - // Principal::from_text(canister_id.as_ref()).ok() - let mut segment = path_segments(url)?; if let Some("-") = segment.next() { let x = segment.next()?; diff --git a/src/ma_.rs b/src/ma_.rs new file mode 100644 index 0000000..c4aa75c --- /dev/null +++ b/src/ma_.rs @@ -0,0 +1,818 @@ +use crate::canister::resolve_canister_id_from_uri; +use crate::canister::PhoneBookCanisterParam; +use crate::canister::{RealAccess, RedisParam}; +use clap::{crate_authors, crate_version, Parser}; +use flate2::read::{DeflateDecoder, GzDecoder}; +use hyper::{ + body, + body::Bytes, + service::{make_service_fn, service_fn}, + Body, Request, Response, Server, StatusCode, Uri, +}; +use ic_agent::{ + agent::http_transport::ReqwestHttpReplicaV2Transport, + export::Principal, + ic_types::{hash_tree::LookupResult, HashTree}, + lookup_value, Agent, AgentError, Certificate, +}; +use ic_utils::{ + call::AsyncCall, + call::SyncCall, + interfaces::http_request::{ + HeaderField, HttpRequestCanister, HttpRequestStreamingCallbackAny, HttpResponse, + StreamingCallbackHttpResponse, StreamingStrategy, Token, + }, +}; +use lazy_regex::regex_captures; +use redis::Commands; +use sha2::{Digest, Sha256}; +use slog::Drain; +use std::{ + convert::Infallible, + error::Error, + io::Read, + net::SocketAddr, + path::PathBuf, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, + }, +}; +use tokio::sync::mpsc; + +mod canister; +//mod config; +mod logging; + +type HttpResponseAny = HttpResponse; + +// Limit the total number of calls to an HTTP Request loop to 1000 for now. +const MAX_HTTP_REQUEST_STREAM_CALLBACK_CALL_COUNT: i32 = 1000; +//set str because clap need str for default value. +const DEFAULT_REDIS_EXPIRY_CACHE_TIMEOUT_IN_SECOND: &'static str = "86400"; //24h = 3600 * 24 + +// The maximum length of a body we should log as tracing. +const MAX_LOG_BODY_SIZE: usize = 100; +const MAX_LOG_CERT_NAME_SIZE: usize = 100; +const MAX_LOG_CERT_B64_SIZE: usize = 2000; + +// The limit of a buffer we should decompress ~10mb. +const MAX_CHUNK_SIZE_TO_DECOMPRESS: usize = 1024; +const MAX_CHUNKS_TO_DECOMPRESS: u64 = 10_240; + +#[derive(Parser)] +#[clap( + version = crate_version!(), + author = crate_authors!(), + propagate_version = true, +)] +pub(crate) struct Opts { + /// Verbose level. By default, INFO will be used. Add a single `-v` to upgrade to + /// DEBUG, and another `-v` to upgrade to TRACE. + #[clap(long, short('v'), parse(from_occurrences))] + verbose: u64, + + /// Quiet level. The opposite of verbose. A single `-q` will drop the logging to + /// WARN only, then another one to ERR, and finally another one for FATAL. Another + /// `-q` will silence ALL logs. + #[clap(long, short('q'), parse(from_occurrences))] + quiet: u64, + + /// Mode to use the logging. "stderr" will output logs in STDERR, "file" will output + /// logs in a file, and "tee" will do both. + #[clap(long("log"), default_value("stderr"), possible_values(&["stderr", "tee", "file"]))] + logmode: String, + + /// File to output the log to, when using logmode=tee or logmode=file. + #[clap(long)] + logfile: Option, + + /// The address to bind to. + #[clap(long, default_value = "127.0.0.1:3000")] + address: SocketAddr, + + /// A replica to use as backend. Locally, this should be a local instance or the + /// boundary node. Multiple replicas can be passed and they'll be used round-robin. + #[clap(long, default_value = "http://localhost:8000/")] + replica: Vec, + + /// Whether or not this is run in a debug context (e.g. errors returned in responses + /// should show full stack and error details). + #[clap(long)] + debug: bool, + + /// Whether or not to fetch the root key from the replica back end. Do not use this when + /// talking to the Internet Computer blockchain mainnet as it is unsecure. + #[clap(long)] + fetch_root_key: bool, + + /// A map of domain names to canister IDs. + /// Format: domain.name:canister-id + #[clap(long, short('r'))] + redis_url: String, + + /// A map of domain names to canister IDs. + /// Format: domain.name:canister-id + #[clap(long, short('p'))] + phonebook_id: String, + + /// The address to bind to. + #[clap(long, default_value = DEFAULT_REDIS_EXPIRY_CACHE_TIMEOUT_IN_SECOND)] + redis_cache_timeout: usize, +} + +fn decode_hash_tree( + name: &str, + value: Option, + logger: &slog::Logger, +) -> Result, ()> { + match value { + Some(tree) => base64::decode(tree).map_err(|e| { + slog::warn!(logger, "Unable to decode {} from base64: {}", name, e); + }), + _ => Err(()), + } +} + +struct HeadersData { + certificate: Option, ()>>, + tree: Option, ()>>, + encoding: Option, +} + +fn extract_headers_data(headers: &[HeaderField], logger: &slog::Logger) -> HeadersData { + let mut headers_data = HeadersData { + certificate: None, + tree: None, + encoding: None, + }; + + for HeaderField(name, value) in headers { + if name.eq_ignore_ascii_case("IC-CERTIFICATE") { + for field in value.split(',') { + if let Some((_, name, b64_value)) = regex_captures!("^(.*)=:(.*):$", field.trim()) { + slog::trace!( + logger, + ">> certificate {:.l1$}: {:.l2$}", + name, + b64_value, + l1 = MAX_LOG_CERT_NAME_SIZE, + l2 = MAX_LOG_CERT_B64_SIZE + ); + let bytes = decode_hash_tree(name, Some(b64_value.to_string()), logger); + if name == "certificate" { + headers_data.certificate = Some(match (headers_data.certificate, bytes) { + (None, bytes) => bytes, + (Some(Ok(certificate)), Ok(bytes)) => { + slog::warn!(logger, "duplicate certificate field: {:?}", bytes); + Ok(certificate) + } + (Some(Ok(certificate)), Err(_)) => { + slog::warn!( + logger, + "duplicate certificate field (failed to decode)" + ); + Ok(certificate) + } + (Some(Err(_)), bytes) => { + slog::warn!( + logger, + "duplicate certificate field (failed to decode)" + ); + bytes + } + }); + } else if name == "tree" { + headers_data.tree = Some(match (headers_data.tree, bytes) { + (None, bytes) => bytes, + (Some(Ok(tree)), Ok(bytes)) => { + slog::warn!(logger, "duplicate tree field: {:?}", bytes); + Ok(tree) + } + (Some(Ok(tree)), Err(_)) => { + slog::warn!(logger, "duplicate tree field (failed to decode)"); + Ok(tree) + } + (Some(Err(_)), bytes) => { + slog::warn!(logger, "duplicate tree field (failed to decode)"); + bytes + } + }); + } + } + } + } else if name.eq_ignore_ascii_case("CONTENT-ENCODING") { + let enc = value.trim().to_string(); + headers_data.encoding = Some(enc); + } + } + + headers_data +} + +async fn forward_request( + request: Request, + agent: Arc, + redis_param: Option<&RedisParam>, + phonebook_param: Option<&PhoneBookCanisterParam>, + logger: slog::Logger, + canister_params: TargetCanisterParams, +) -> Result, Box> { + let ( canister_id, found_uri ) = match canister_params.clone() { + TargetCanisterParams { canister_id, found_uri } => (canister_id, found_uri) + }; + let request_uri = request.uri(); + + slog::trace!( + logger, + "<< {} {} {:?} resolved to {}/-/{}", + request.method(), + request.uri(), + &request.version(), + canister_id, + found_uri, + ); + let skip_validation = skip_validation(&request_uri); + + let (parts, body) = request.into_parts(); + + let method = parts.method; + let headers = parts + .headers + .iter() + .filter_map(|(name, value)| { + Some(HeaderField( + name.as_str().into(), + value.to_str().ok()?.into(), + )) + }) + .inspect(|HeaderField(name, value)| { + slog::trace!(logger, "<< {}: {}", name, value); + }) + .collect::>(); + + let entire_body = body::to_bytes(body).await?.to_vec(); + + slog::trace!(logger, "<<"); + if logger.is_trace_enabled() { + let body = String::from_utf8_lossy( + &entire_body[0..usize::min(entire_body.len(), MAX_LOG_BODY_SIZE)], + ); + slog::trace!( + logger, + "<< \"{}\"{}", + &body.escape_default(), + if body.len() > MAX_LOG_BODY_SIZE { + format!("... {} bytes total", body.len()) + } else { + String::new() + } + ); + } + + let canister = HttpRequestCanister::create(agent.as_ref(), canister_id); + let query_result = canister + .http_request_custom( + method.as_str(), + found_uri.as_str(), + headers.iter().cloned(), + &entire_body, + ) + .call() + .await; + + fn handle_result( + result: Result<(HttpResponseAny,), AgentError>, + ) -> Result, Box>> { + // If the result is a Replica error, returns the 500 code and message. There is no information + // leak here because a user could use `dfx` to get the same reply. + match result { + Ok((http_response,)) => Ok(http_response), + Err(AgentError::ReplicaError { + reject_code, + reject_message, + }) => Err(Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(format!(r#"Replica Error ({}): "{}""#, reject_code, reject_message).into()) + .unwrap())), + Err(e) => Err(Err(e.into())), // <-- PLACE TO HANDLE 307 REDIRECT WITH RECURSION> + } + } + + let http_response = match handle_result(query_result) { + Ok(http_response) => http_response, + Err(response_or_error) => return response_or_error, + }; + + let http_response = if http_response.upgrade == Some(true) { + let waiter = garcon::Delay::builder() + .throttle(std::time::Duration::from_millis(500)) + .timeout(std::time::Duration::from_secs(15)) + .build(); + let update_result = canister + .http_request_update_custom( + method.as_str(), + found_uri.as_str(), + headers.iter().cloned(), + &entire_body, + ) + .call_and_wait(waiter) + .await; + let http_response = match handle_result(update_result) { + Ok(http_response) => http_response, + Err(response_or_error) => return response_or_error, + }; + http_response + } else { + http_response + }; + + let mut builder = Response::builder().status(StatusCode::from_u16(http_response.status_code)?); + for HeaderField(name, value) in &http_response.headers { + builder = builder.header(name.as_ref(), value.as_ref()); + } + + let headers_data = extract_headers_data(&http_response.headers, &logger); + let body = if logger.is_trace_enabled() { + Some(http_response.body.clone()) + } else { + None + }; + + let is_streaming = http_response.streaming_strategy.is_some(); + let response = if let Some(streaming_strategy) = http_response.streaming_strategy { + let (mut sender, body) = body::Body::channel(); + let agent = agent.as_ref().clone(); + sender.send_data(Bytes::from(http_response.body)).await?; + + match streaming_strategy { + StreamingStrategy::Callback(callback) => { + let streaming_canister_id = callback.callback.0.principal; + let method_name = callback.callback.0.method; + let mut callback_token = callback.token; + let logger = logger.clone(); + tokio::spawn(async move { + let canister = HttpRequestCanister::create(&agent, streaming_canister_id); + // We have not yet called http_request_stream_callback. + let mut count = 0; + loop { + count += 1; + if count > MAX_HTTP_REQUEST_STREAM_CALLBACK_CALL_COUNT { + sender.abort(); + break; + } + + match canister + .http_request_stream_callback(&method_name, callback_token) + .call() + .await + { + Ok((StreamingCallbackHttpResponse { body, token },)) => { + if sender.send_data(Bytes::from(body)).await.is_err() { + sender.abort(); + break; + } + if let Some(next_token) = token { + callback_token = next_token; + } else { + break; + } + } + Err(e) => { + slog::debug!(logger, "Error happened during streaming: {}", e); + sender.abort(); + break; + } + } + } + }); + } + } + + builder.body(body)? + } else { + if !skip_validation { + let body_valid = validate( + &headers_data, + &canister_id, + &agent, + &parts.uri, + &http_response.body, + logger.clone(), + ); + if body_valid.is_err() { + return Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(body_valid.unwrap_err().into()) + .unwrap()); + } + } + builder.body(http_response.body.into())? + }; + + if logger.is_trace_enabled() { + slog::trace!( + logger, + ">> {:?} {} {}", + &response.version(), + response.status().as_u16(), + response.status().to_string() + ); + + for (name, value) in response.headers() { + let value = String::from_utf8_lossy(value.as_bytes()); + slog::trace!(logger, ">> {}: {}", name, value); + } + + let body = body.unwrap_or_else(|| b"... streaming ...".to_vec()); + + slog::trace!(logger, ">>"); + slog::trace!( + logger, + ">> \"{}\"{}", + String::from_utf8_lossy(&body[..usize::min(MAX_LOG_BODY_SIZE, body.len())]) + .escape_default(), + if is_streaming { + "... streaming".to_string() + } else if body.len() > MAX_LOG_BODY_SIZE { + format!("... {} bytes total", body.len()) + } else { + String::new() + } + ); + } + + Ok(response) +} + +fn skip_validation(url: &hyper::Uri) -> bool { + url.query() + .map(|query| if query.contains("_raw") { true } else { false }) + .unwrap_or(false) +} + +fn validate( + headers_data: &HeadersData, + canister_id: &Principal, + agent: &Agent, + uri: &Uri, + response_body: &[u8], + logger: slog::Logger, +) -> Result<(), String> { + let body_sha = if let Some(body_sha) = + decode_body_to_sha256(response_body, headers_data.encoding.clone()) + { + body_sha + } else { + return Err("Body could not be decoded".into()); + }; + + let body_valid = match ( + headers_data.certificate.as_ref(), + headers_data.tree.as_ref(), + ) { + (Some(Ok(certificate)), Some(Ok(tree))) => match validate_body( + Certificates { certificate, tree }, + canister_id, + agent, + uri, + &body_sha, + logger.clone(), + ) { + Ok(true) => Ok(()), + Ok(false) => Err("Body does not pass verification".to_string()), + Err(e) => Err(format!("Certificate validation failed: {}", e)), + }, + (Some(_), _) | (_, Some(_)) => Err("Body does not pass verification".to_string()), + + // TODO: Remove this (FOLLOW-483) + // Canisters don't have to provide certified variables + // This should change in the future, grandfathering in current implementations + (None, None) => Ok(()), + }; + + if body_valid.is_err() && !cfg!(feature = "skip_body_verification") { + return body_valid; + } + + Ok(()) +} + +fn decode_body_to_sha256(body: &[u8], encoding: Option) -> Option<[u8; 32]> { + let mut sha256 = Sha256::new(); + let mut decoded = [0u8; MAX_CHUNK_SIZE_TO_DECOMPRESS]; + match encoding.as_deref() { + Some("gzip") => { + let mut decoder = GzDecoder::new(body); + for _ in 0..MAX_CHUNKS_TO_DECOMPRESS { + let bytes = decoder.read(&mut decoded).ok()?; + if bytes == 0 { + return Some(sha256.finalize().into()); + } + sha256.update(&decoded[0..bytes]); + } + if decoder.bytes().next().is_some() { + return None; + } + } + Some("deflate") => { + let mut decoder = DeflateDecoder::new(body); + for _ in 0..MAX_CHUNKS_TO_DECOMPRESS { + let bytes = decoder.read(&mut decoded).ok()?; + if bytes == 0 { + return Some(sha256.finalize().into()); + } + sha256.update(&decoded[0..bytes]); + } + if decoder.bytes().next().is_some() { + return None; + } + } + _ => sha256.update(body), + }; + Some(sha256.finalize().into()) +} + +struct Certificates<'a> { + certificate: &'a Vec, + tree: &'a Vec, +} + +fn validate_body( + certificates: Certificates, + canister_id: &Principal, + agent: &Agent, + uri: &Uri, + body_sha: &[u8; 32], + logger: slog::Logger, +) -> anyhow::Result { + let cert: Certificate = + serde_cbor::from_slice(certificates.certificate).map_err(AgentError::InvalidCborData)?; + let tree: HashTree = + serde_cbor::from_slice(certificates.tree).map_err(AgentError::InvalidCborData)?; + + if let Err(e) = agent.verify(&cert, *canister_id, false) { + slog::trace!(logger, ">> certificate failed verification: {}", e); + return Ok(false); + } + + let certified_data_path = vec![ + "canister".into(), + canister_id.into(), + "certified_data".into(), + ]; + let witness = match lookup_value(&cert, certified_data_path) { + Ok(witness) => witness, + Err(e) => { + slog::trace!( + logger, + ">> Could not find certified data for this canister in the certificate: {}", + e + ); + return Ok(false); + } + }; + let digest = tree.digest(); + + if witness != digest { + slog::trace!( + logger, + ">> witness ({}) did not match digest ({})", + hex::encode(witness), + hex::encode(digest) + ); + + return Ok(false); + } + + let path = ["http_assets".into(), uri.path().into()]; + let tree_sha = match tree.lookup_path(&path) { + LookupResult::Found(v) => v, + _ => match tree.lookup_path(&["http_assets".into(), "/index.html".into()]) { + LookupResult::Found(v) => v, + _ => { + slog::trace!( + logger, + ">> Invalid Tree in the header. Does not contain path {:?}", + path + ); + return Ok(false); + } + }, + }; + + Ok(body_sha == tree_sha) +} + +fn ok() -> Result, Box> { + Ok(Response::builder() + .status(StatusCode::OK) + .body("OK".into())?) +} + +fn unable_to_fetch_root_key() -> Result, Box> { + Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("Unable to fetch root key".into())?) +} + +#[derive(Clone, Debug)] +pub struct TargetCanisterParams { + canister_id: Principal, + found_uri: String, +} + +#[allow(clippy::too_many_arguments)] +async fn handle_request( + request: Request, + replica_url: String, + redis_param: Arc>, + phonebook_param: Option, + logger: slog::Logger, + fetch_root_key: bool, + debug: bool, +) -> Result, Infallible> { + let request_uri = request.uri(); + slog::trace!(logger, "handle_request.request_uri:{}", request_uri); + let result = if request_uri.path().starts_with("/healthcheck") { + ok() + } else { + let agent = Arc::new( + ic_agent::Agent::builder() + .with_transport(ReqwestHttpReplicaV2Transport::create(replica_url).unwrap()) + .build() + .expect("Could not create agent..."), + ); + if fetch_root_key && agent.fetch_root_key().await.is_err() { + unable_to_fetch_root_key() + } else { + let request_uri = request.uri(); + let (canister_id, found_uri) = match resolve_canister_id_from_uri( + &request_uri, + redis_param.as_ref().as_ref(), + phonebook_param.as_ref(), + RealAccess, + &logger, + ) + .await + { + None => { + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body("Could not find a canister id to forward to.".into()) + .unwrap()) + } + Some((x, y)) => (x, y), + }; + + forward_request( + request, + agent, + redis_param.as_ref().as_ref(), + phonebook_param.as_ref(), + logger.clone(), + TargetCanisterParams { canister_id, found_uri }, + ) + .await + } + }; + + match result { + Err(err) => { + slog::warn!(logger, "Internal Error during request:\n{:#?}", err); + + Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(if debug { + format!("Internal Error: {:?}", err).into() + } else { + "Internal Server Error".into() + }) + .unwrap()) + } + Ok(x) => Ok::<_, Infallible>(x), + } +} + +async fn update_redis_thread( + redis_url: &str, + mut redis_rx: mpsc::Receiver<(String, String)>, + redis_cache_timout: usize, + logger: slog::Logger, +) -> Result<(), Box> { + let redis_client = redis::Client::open(redis_url)?; + + while let Some((alias, canister_id)) = redis_rx.recv().await { + slog::info!( + logger, + "Update Redis with alias:canister {}:{}", + alias, + canister_id, + ); + if let Err(err) = redis_client + .get_connection() + .and_then(|mut con| con.set_ex::<_, _, ()>(&alias, &canister_id, redis_cache_timout)) + { + slog::error!(logger, "Error during Redis cache update: {}", err); + } + } + Ok(()) +} + +fn main() -> Result<(), Box> { + let opts: Opts = Opts::parse(); + + let logger = logging::setup_logging(&opts); + + // Prepare a list of agents for each backend replicas. + let replicas = Mutex::new(opts.replica.clone()); + + let counter = AtomicUsize::new(0); + let debug = opts.debug; + let fetch_root_key = opts.fetch_root_key; + + //create Redis cache update channel. + //A cache entry is send to the channel and + // a async thread read it and send to Redis. + let (redis_tx, redis_rx) = mpsc::channel(32); + let redis_logger = logger.clone(); + let th_redis_url = opts.redis_url.clone(); + let th_redis_cache_timeout = opts.redis_cache_timeout; + + //start tokio runtime + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(10) + .enable_all() + .build()?; + + //create name alias resolution struct + let redis_param: Option = runtime.block_on(async { + RedisParam::try_new(Some(&opts.redis_url), Some(redis_tx), &logger).await + }); + let redis_param = Arc::new(redis_param); + + let service = make_service_fn(|_| { + let redis_param = redis_param.clone(); + let logger = logger.clone(); + + // Select an agent. + let replica_url_array = replicas.lock().unwrap(); + let count = counter.fetch_add(1, Ordering::SeqCst); + let replica_url = replica_url_array + .get(count % replica_url_array.len()) + .unwrap_or_else(|| unreachable!()); + let replica_url = replica_url.clone(); + slog::debug!(logger, "make service Replica URL: {}", replica_url); + + let phone_book_id = opts.phonebook_id.clone(); + + async move { + Ok::<_, Infallible>(service_fn(move |req| { + let logger = logger.clone(); + let redis_param = redis_param.clone(); + //update phone book canister call with network replica + let phonebook_param = + PhoneBookCanisterParam::new(&phone_book_id, &replica_url, &logger).ok(); + + handle_request( + req, + replica_url.clone(), + redis_param, + phonebook_param, + logger, + fetch_root_key, + debug, + ) + })) + } + }); + + slog::info!( + logger, + "Starting server. Listening on http://{}/", + opts.address + ); + + runtime.spawn(async move { + if let Err(err) = update_redis_thread( + &th_redis_url, + redis_rx, + th_redis_cache_timeout, + redis_logger.clone(), + ) + .await + { + slog::error!( + redis_logger, + "Error Bad Redis Url can't start client connection: {} for url:{}", + err, + &th_redis_url + ); + } + }); + runtime.block_on(async { + let server = Server::bind(&opts.address).serve(service); + server.await?; + Ok(()) + }) +} diff --git a/src/main.rs b/src/main.rs index c4aa75c..563f457 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,11 @@ use crate::canister::resolve_canister_id_from_uri; use crate::canister::PhoneBookCanisterParam; use crate::canister::{RealAccess, RedisParam}; +use async_recursion::async_recursion; use clap::{crate_authors, crate_version, Parser}; use flate2::read::{DeflateDecoder, GzDecoder}; +use hyper::http::request::Parts; + use hyper::{ body, body::Bytes, @@ -210,32 +213,51 @@ fn extract_headers_data(headers: &[HeaderField], logger: &slog::Logger) -> Heade headers_data } -async fn forward_request( +enum ForwardRequestResponse { + RedirectUrl(String), + Response(Result, anyhow::Error>), +} + +// #[derive(Clone)] +struct ForwardRequestParams { + body: Vec, + parts: Parts, +} + +//From Request to ForwardRequestParams +async fn extract_forward_request_params<'a>( request: Request, +) -> Result { + let (parts, body) = request.into_parts(); + + let body = body::to_bytes(body).await?.to_vec(); + Ok(ForwardRequestParams { body, parts }) +} + +async fn forward_request( + request: ForwardRequestParams, agent: Arc, - redis_param: Option<&RedisParam>, - phonebook_param: Option<&PhoneBookCanisterParam>, + _redis_param: Option<&RedisParam>, + _phonebook_param: Option<&PhoneBookCanisterParam>, logger: slog::Logger, canister_params: TargetCanisterParams, -) -> Result, Box> { - let ( canister_id, found_uri ) = match canister_params.clone() { - TargetCanisterParams { canister_id, found_uri } => (canister_id, found_uri) - }; - let request_uri = request.uri(); +) -> Result { + let ForwardRequestParams { body, parts } = request; + let TargetCanisterParams { + canister_id, + found_uri, + } = canister_params; slog::trace!( logger, "<< {} {} {:?} resolved to {}/-/{}", - request.method(), - request.uri(), - &request.version(), + &parts.method, + &parts.uri, + &parts.version, canister_id, found_uri, ); - let skip_validation = skip_validation(&request_uri); - - let (parts, body) = request.into_parts(); - + let skip_validation = skip_validation(&parts.uri); let method = parts.method; let headers = parts .headers @@ -251,13 +273,9 @@ async fn forward_request( }) .collect::>(); - let entire_body = body::to_bytes(body).await?.to_vec(); - slog::trace!(logger, "<<"); if logger.is_trace_enabled() { - let body = String::from_utf8_lossy( - &entire_body[0..usize::min(entire_body.len(), MAX_LOG_BODY_SIZE)], - ); + let body = String::from_utf8_lossy(&body[0..usize::min(body.len(), MAX_LOG_BODY_SIZE)]); slog::trace!( logger, "<< \"{}\"{}", @@ -276,14 +294,14 @@ async fn forward_request( method.as_str(), found_uri.as_str(), headers.iter().cloned(), - &entire_body, + &body, ) .call() .await; fn handle_result( result: Result<(HttpResponseAny,), AgentError>, - ) -> Result, Box>> { + ) -> Result, anyhow::Error>> { // If the result is a Replica error, returns the 500 code and message. There is no information // leak here because a user could use `dfx` to get the same reply. match result { @@ -299,11 +317,32 @@ async fn forward_request( } } + type QueryResponse = HttpResponse; + + fn is_redirect(query_response: &QueryResponse) -> Option { + if query_response.status_code == StatusCode::TEMPORARY_REDIRECT { + let query_response_headers = query_response.headers.iter().collect::>(); + let query_response_headers_slice = query_response_headers.as_slice(); + + match query_response_headers_slice { + [HeaderField(name, value)] if name == "Location" => Some(value.to_string()), + _ => None, + } + } else { + None + } + } + let http_response = match handle_result(query_result) { Ok(http_response) => http_response, - Err(response_or_error) => return response_or_error, + Err(response_or_error) => return Ok(ForwardRequestResponse::Response(response_or_error)), }; + if let Some(redirect_url) = is_redirect(&http_response) { + slog::trace!(logger, ">> 307 Redirect to {}", redirect_url); + return Ok(ForwardRequestResponse::RedirectUrl(redirect_url)); + } + let http_response = if http_response.upgrade == Some(true) { let waiter = garcon::Delay::builder() .throttle(std::time::Duration::from_millis(500)) @@ -314,13 +353,15 @@ async fn forward_request( method.as_str(), found_uri.as_str(), headers.iter().cloned(), - &entire_body, + &body, ) .call_and_wait(waiter) .await; let http_response = match handle_result(update_result) { Ok(http_response) => http_response, - Err(response_or_error) => return response_or_error, + Err(response_or_error) => { + return Ok(ForwardRequestResponse::Response(response_or_error)) + } }; http_response } else { @@ -401,10 +442,10 @@ async fn forward_request( logger.clone(), ); if body_valid.is_err() { - return Ok(Response::builder() + return Ok(ForwardRequestResponse::Response(Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(body_valid.unwrap_err().into()) - .unwrap()); + .unwrap()))); } } builder.body(http_response.body.into())? @@ -442,7 +483,7 @@ async fn forward_request( ); } - Ok(response) + Ok(ForwardRequestResponse::Response(Ok(response))) } fn skip_validation(url: &hyper::Uri) -> bool { @@ -604,13 +645,13 @@ fn validate_body( Ok(body_sha == tree_sha) } -fn ok() -> Result, Box> { +fn ok() -> Result, anyhow::Error> { Ok(Response::builder() .status(StatusCode::OK) .body("OK".into())?) } -fn unable_to_fetch_root_key() -> Result, Box> { +fn unable_to_fetch_root_key() -> Result, anyhow::Error> { Ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body("Unable to fetch root key".into())?) @@ -619,9 +660,26 @@ fn unable_to_fetch_root_key() -> Result, Box> { #[derive(Clone, Debug)] pub struct TargetCanisterParams { canister_id: Principal, - found_uri: String, + found_uri: String, } +// fn clone_request(request: &Request) -> impl FnOnce(String) -> Request { +// let (parts, body) = request.into_parts(); +// // let method = request.method().clone(); +// // let headers = request.headers().clone(); + +// // let body = request.body().concat + +// |new_url| { +// let new_request = Request::from_parts(Parts { +// uri: new_url.parse().unwrap(), +// ..parts +// }, body); +// new_request +// } +// } + +#[async_recursion] #[allow(clippy::too_many_arguments)] async fn handle_request( request: Request, @@ -632,21 +690,33 @@ async fn handle_request( fetch_root_key: bool, debug: bool, ) -> Result, Infallible> { - let request_uri = request.uri(); - slog::trace!(logger, "handle_request.request_uri:{}", request_uri); + // let redirect_request = clone_request(&request); + let forward_request_params = match extract_forward_request_params(request).await { + Ok(params) => params, + Err(e) => { + slog::error!(logger, ">> Failed to extract forward request params: {}", e); + return Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("Failed to extract forward request params".into()) + .unwrap()); + } + }; + + let ForwardRequestParams { body, parts } = forward_request_params; + let request_uri = &parts.uri; + slog::trace!(logger, "handle_request.request_uri:{}", &request_uri); let result = if request_uri.path().starts_with("/healthcheck") { ok() } else { let agent = Arc::new( ic_agent::Agent::builder() - .with_transport(ReqwestHttpReplicaV2Transport::create(replica_url).unwrap()) + .with_transport(ReqwestHttpReplicaV2Transport::create(replica_url.clone()).unwrap()) .build() .expect("Could not create agent..."), ); if fetch_root_key && agent.fetch_root_key().await.is_err() { unable_to_fetch_root_key() } else { - let request_uri = request.uri(); let (canister_id, found_uri) = match resolve_canister_id_from_uri( &request_uri, redis_param.as_ref().as_ref(), @@ -664,16 +734,79 @@ async fn handle_request( } Some((x, y)) => (x, y), }; - - forward_request( - request, + + //cloning the request, because using the original request will take ownership of the request, but we want to keep it in case we need to redirect. + let headers = parts.headers.clone(); + let method = parts.method.clone(); + let uri = parts.uri.clone(); + let version = parts.version.clone(); + + //we need to build a new request, not to be used directly but to get the parts we need. Because the hyper library doesn't allow you create Parts directly. + let mut normal_request = Request::builder() + .method(method) + .uri(uri) + .version(version) + .body(()) + .expect("Could not create request"); + normal_request.headers_mut().extend(headers); + + let result = forward_request( + ForwardRequestParams { + body: body.clone(), + parts: normal_request.into_parts().0, + }, agent, redis_param.as_ref().as_ref(), phonebook_param.as_ref(), logger.clone(), - TargetCanisterParams { canister_id, found_uri }, + TargetCanisterParams { + canister_id, + found_uri, + }, ) - .await + .await; + + match result { + Ok(ForwardRequestResponse::RedirectUrl(redirect_url)) => { + let mut redirect_request = Request::builder() + .method(parts.method) + .uri(redirect_url) + .version(parts.version) + .body(Body::from(body)) + .expect("Could not create redirect request"); + redirect_request.headers_mut().extend(parts.headers); + + //recursive call to handle redirects. + let response = handle_request( + redirect_request, + replica_url, + redis_param, + phonebook_param, + logger.clone(), + fetch_root_key, + debug, + ) + .await; + + match response { + Ok(response) => Ok(response), + _ => Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("Error forwarding request".into()) + .unwrap()), + } + } + + Ok(ForwardRequestResponse::Response(response)) => response, + + Err(e) => { + slog::error!(logger, ">> Error forwarding request: {}", e); + return Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("Error forwarding request".into()) + .unwrap()); + } + } } }; diff --git a/start_dev.sh b/start_dev.sh new file mode 100755 index 0000000..71150d7 --- /dev/null +++ b/start_dev.sh @@ -0,0 +1 @@ +./target/debug/icx-proxy --replica "http://localhost:8000" --redis-url "redis://localhost:6879/" --phonebook-id "rrkah-fqaaa-aaaaa-aaaaq-cai" \ No newline at end of file