diff --git a/Cargo.lock b/Cargo.lock index 0a689cb..39fbafc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.45" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7" +checksum = "62e1f47f7dc0422027a4e370dd4548d4d66b26782e513e98dca1e689e058a80e" [[package]] name = "async-graphql" @@ -350,6 +350,66 @@ dependencies = [ "winapi", ] +[[package]] +name = "clap" +version = "3.0.0-beta.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feff3878564edb93745d58cf63e17b63f24142506e7a20c87a5521ed7bfb1d63" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", + "unicase", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b15c6b4f786ffb6192ffe65a36855bc1fc2444bcd0945ae16748dcd6ed7d0d3" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "dialoguer", + "lazy_static", + "reqwest", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "console" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "regex", + "terminal_size", + "unicode-width", + "winapi", +] + [[package]] name = "const_fn" version = "0.4.8" @@ -367,6 +427,22 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cpufeatures" version = "0.2.1" @@ -543,6 +619,18 @@ dependencies = [ "syn", ] +[[package]] +name = "dialoguer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61579ada4ec0c6031cfac3f86fdba0d195a7ebeb5e36693bd53cb5999a25beeb" +dependencies = [ + "console", + "lazy_static", + "tempfile", + "zeroize", +] + [[package]] name = "digest" version = "0.8.1" @@ -623,6 +711,12 @@ dependencies = [ "serde", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.29" @@ -658,6 +752,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -670,9 +779,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" +checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e" dependencies = [ "futures-channel", "futures-core", @@ -685,9 +794,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" dependencies = [ "futures-core", "futures-sink", @@ -695,15 +804,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" +checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" [[package]] name = "futures-executor" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" +checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" dependencies = [ "futures-core", "futures-task", @@ -723,18 +832,16 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" +checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11" [[package]] name = "futures-macro" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" +checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd" dependencies = [ - "autocfg", - "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -742,15 +849,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" +checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" [[package]] name = "futures-task" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" +checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" [[package]] name = "futures-timer" @@ -760,11 +867,10 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" dependencies = [ - "autocfg", "futures-channel", "futures-core", "futures-io", @@ -774,8 +880,6 @@ dependencies = [ "memchr", "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] @@ -929,15 +1033,15 @@ checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" [[package]] name = "httpdate" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.14" +version = "0.14.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b" +checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c" dependencies = [ "bytes", "futures-channel", @@ -957,6 +1061,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-unix-connector" version = "0.2.2" @@ -1013,6 +1130,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ipnet" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" + [[package]] name = "itertools" version = "0.10.1" @@ -1045,9 +1168,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.107" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" +checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" [[package]] name = "lock_api" @@ -1069,9 +1192,9 @@ dependencies = [ [[package]] name = "loom" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b9df80a3804094bf49bb29881d18f6f05048db72127e84e09c26fc7c2324f5" +checksum = "5df2c4aeb432e60c9e5ae517ca8ed8b63556ce23093b2758fc8837d75439c5ec" dependencies = [ "cfg-if", "generator", @@ -1099,9 +1222,9 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "matchers" -version = "0.0.1" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ "regex-automata", ] @@ -1183,6 +1306,24 @@ dependencies = [ "version_check", ] +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.0" @@ -1250,6 +1391,48 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-sys" +version = "0.9.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_str_bytes" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addaa943333a514159c80c97ff4a93306530d965d27e139188283cd13e06a799" +dependencies = [ + "memchr", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -1386,6 +1569,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" + [[package]] name = "ppv-lite86" version = "0.2.15" @@ -1403,16 +1592,34 @@ dependencies = [ ] [[package]] -name = "proc-macro-hack" -version = "0.5.19" +name = "proc-macro-error" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] [[package]] -name = "proc-macro-nested" -version = "0.1.7" +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" @@ -1559,6 +1766,41 @@ dependencies = [ "winapi", ] +[[package]] +name = "reqwest" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d2927ca2f685faf0fc620ac4834690d29e7abb153add10f5812eef20b5e280" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.16.20" @@ -1690,6 +1932,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + [[package]] name = "scoped-tls" version = "1.0.0" @@ -1712,6 +1964,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "0.9.0" @@ -1749,9 +2024,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.70" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3" +checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" dependencies = [ "indexmap", "itoa", @@ -2104,9 +2379,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" dependencies = [ "proc-macro2", "quote", @@ -2127,6 +2402,34 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.30" @@ -2222,11 +2525,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee" +checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" dependencies = [ - "autocfg", "bytes", "libc", "memchr", @@ -2241,15 +2543,25 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.5.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "114383b041aa6212c579467afa0075fbbdd0718de036100bc0ba7961d8cb9095" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.22.0" @@ -2345,36 +2657,22 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-serde" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b" -dependencies = [ - "serde", - "tracing-core", -] - [[package]] name = "tracing-subscriber" -version = "0.2.25" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +checksum = "7507ec620f809cdf07cccb5bc57b13069a88031b795efd4079b1c71b66c1613d" dependencies = [ "ansi_term", - "chrono", "lazy_static", "matchers", "regex", - "serde", - "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", - "tracing-serde", ] [[package]] @@ -2430,6 +2728,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.7" @@ -2451,6 +2758,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + [[package]] name = "unicode-xid" version = "0.2.2" @@ -2491,6 +2804,12 @@ dependencies = [ "serde", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.3" @@ -2538,6 +2857,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.78" @@ -2579,6 +2910,7 @@ dependencies = [ "rocket", "serde", "sqlx", + "tokio", "uuid", ] @@ -2613,9 +2945,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33ac5ee236a4efbf2c98967e12c6cc0c51d93a744159a52957ba206ae6ef5f7" +checksum = "524b58fa5a20a2fb3014dd6358b70e6579692a56ef6fce928834e488f42f65e8" dependencies = [ "wasm-bindgen", "web-sys", @@ -2637,14 +2969,38 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi", +] + [[package]] name = "yansi" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" + +[[package]] +name = "zeroize" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" diff --git a/Cargo.toml b/Cargo.toml index 36f37d7..55aca0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ "supervisor", - "web" + "web", + "cli", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..640c69a --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "cli" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = "3.0.0-beta.5" +dialoguer = "0.9.0" +lazy_static = "1.4.0" +anyhow = "1.0.48" +reqwest = { version = "0.11.6", features = ["blocking", "json"] } +serde = { version = "1", features = ["derive"] } +thiserror = "1.0.30" + +[dev-dependencies] +serde_json = "1.0.72" \ No newline at end of file diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000..7679c4c --- /dev/null +++ b/cli/README.md @@ -0,0 +1,4 @@ +# pCTF CLI + +This is the command-line interface for the pCTF GraphQL API. +It enables you to easily list challenges, submit flags and recieve prices! \ No newline at end of file diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..074d42a --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,46 @@ +use anyhow::Result; +use clap::Parser; +use lazy_static::lazy_static; +use reqwest::blocking::Client; + +use crate::oauth2::DeviceAuthorizationRequest; + +mod oauth2; + +// Constaints that allow compiling with cargo-install +/// The host/domian used for openid connect discovery +// const OIDC_HOST: &str = "https://pwnhub-dev.eu.auth0.com/"; +/// The id of the oidc client +pub const CLIENT_ID: &str = "XOUgCp9H7k0rkRknnAf8ID6Fz4skI3Wi"; +// pub const OAUTH_AUTH: &str = "https://pwnhub-dev.eu.auth0.com/authorize"; +pub const DEVICE_AUTH: &str = "https://pwnhub-dev.eu.auth0.com/oauth/device/code"; +pub const OAUTH_TOKEN: &str = "https://pwnhub-dev.eu.auth0.com/oauth/token"; +pub const AUDIENCE: &str = "http://localhost:8000"; + +#[derive(Parser)] +enum Commands { + /// Login using discord + Login, +} + +fn main() -> Result<()> { + let cmd = Commands::parse(); + lazy_static! { + static ref CLIENT: Client = Client::new(); + } + // You can handle information about subcommands by requesting their matches by name + // (as below), requesting just the name used, or both at the same time + match cmd { + Commands::Login => { + let request = DeviceAuthorizationRequest::new(&CLIENT)?; + println!( + "Please follow this link: {}\nYour code is {}", + request.verification_uri_complete, request.user_code + ); + println!("Waiting ..."); + let response = request.poll(&CLIENT)?; + println!("Done!\nYour access token is {}", response.access_token); + } + }; + Ok(()) +} diff --git a/cli/src/oauth2.rs b/cli/src/oauth2.rs new file mode 100644 index 0000000..ee433a8 --- /dev/null +++ b/cli/src/oauth2.rs @@ -0,0 +1,87 @@ +//! An wrapper for the oauth2 [Device Authorization Grant][1] +//! +//! [1]: https://datatracker.ietf.org/doc/html/rfc8628 + +mod error; +use std::{ + collections::HashMap, result::Result as StdResult, str::FromStr, thread::sleep, time::Duration, +}; + +use anyhow::Result; +#[doc(inline)] +pub use error::PollResponseError; +use reqwest::{blocking::Client, Url}; +use serde::Deserialize; + +use crate::{AUDIENCE, CLIENT_ID, DEVICE_AUTH, OAUTH_TOKEN}; + +#[non_exhaustive] +#[derive(Deserialize, Debug)] +/// A Device Authorization Request as defined in [RFC 8628 section 3.1][1] +/// +/// The server will return an url that the user should follow to authorize this +/// application +/// [1]: https://datatracker.ietf.org/doc/html/rfc8628#section-3.1 +pub struct DeviceAuthorizationRequest { + pub device_code: String, + pub user_code: String, + pub verification_uri: String, + pub verification_uri_complete: String, + pub expires_in: u64, + pub interval: u64, +} + +#[derive(Debug, Deserialize)] +pub struct DeviceAccessTokenResponse { + pub access_token: String, + pub refresh_token: Option, + pub id_token: Option, + pub token_type: Option, + pub expires_in: u64, +} + +impl DeviceAuthorizationRequest { + pub fn new(client: &Client) -> Result { + let mut params = HashMap::new(); + params.insert("client_id", CLIENT_ID); + params.insert("scope", "openid profile"); + params.insert("audience", AUDIENCE); + client + .post(Url::from_str(DEVICE_AUTH)?) + .form(¶ms) + .send()? + .json() + .map_err(Into::into) + } + + pub fn poll(self, client: &Client) -> Result { + let mut params = HashMap::new(); + params.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); + params.insert("device_code", &self.device_code); + params.insert("client_id", CLIENT_ID); + loop { + #[derive(Deserialize)] + #[serde(untagged)] + /// Enum to determine if a [`DeviceAuthorizationRequest`] was successful or not. + /// This way, we can return a [`std::result::Result`] directly + enum ResponseMatcher { + Ok(DeviceAccessTokenResponse), + Err(PollResponseError), + } + let response: ResponseMatcher = client + .post(Url::from_str(OAUTH_TOKEN)?) + .form(¶ms) + .send()? + .json()?; + match response { + ResponseMatcher::Ok(d) => return StdResult::Ok(d), + ResponseMatcher::Err(e) => match e { + PollResponseError::AuthorizationPending { + error_description: _, + } => sleep(Duration::from_secs(self.interval)), + _ => return StdResult::Err(e.into()), + }, + } + } + } +} diff --git a/cli/src/oauth2/error.rs b/cli/src/oauth2/error.rs new file mode 100644 index 0000000..90d5e4d --- /dev/null +++ b/cli/src/oauth2/error.rs @@ -0,0 +1,153 @@ +//! Typed errors for the oauth2 [Device Authorization Grant][1] +//! +//! [1]: https://datatracker.ietf.org/doc/html/rfc8628 +use serde::Deserialize; +use thiserror::Error; + +macro_rules! poll_response_error { + ( + $( + $(#[$attr:meta])* + $v:ident + ),*$(,)? + ) => { + #[derive(Debug, Error, Deserialize)] + #[serde(tag = "error")] + #[serde(rename_all = "snake_case")] + /// An error type that represents the possible error types defined in + /// [RFC 8628 section 7.3][1] + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc8628#section-7.3 + pub enum PollResponseError { + $( + #[error("{error_description}")] + $(#[$attr])* + $v { + error_description: String, + }, + )* + } + }; +} + +// documentation for variants taken from +// https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 +// and https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 +// note that only invalid_grant is taken from rfc 6749 +poll_response_error! { + /// authorization_pending + /// + /// The authorization request is still pending. The client SHOULD repeat + /// the access token request to the token endpoint (a process known as + /// polling). + AuthorizationPending, + /// slow_down + /// + /// A variant of "authorization_pending", the authorization request is + /// still pending and polling should continue, but the interval MUST + /// be increased by 5 seconds for this and all subsequent requests. + SlowDown, + /// expired_token + /// + /// The "device_code" has expired, and the device authorization + /// session has concluded. The client MAY commence a new device + /// authorization request but SHOULD wait for user interaction before + /// restarting to avoid unnecessary polling. + ExpiredToken, + /// invalid_grant + /// + /// Defined as part of the standard oauth2 errors in [RFC 6749 section 5.2][1]: + /// The provided authorization grant (e.g., authorization + /// code, resource owner credentials) or refresh token is + /// invalid, expired, revoked, does not match the redirection + /// URI used in the authorization request, or was issued to + /// another client. + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 + InvalidGrant, + /// access_denied + /// + /// The authorization request was denied. + AccessDenied, +} + +#[cfg(test)] +mod tests { + use super::PollResponseError; + + #[allow(non_upper_case_globals)] + #[test] + fn parse_errors() { + const authorization_pending: &str = r#" + { + "error": "authorization_pending", + "error_description": "User did not approve the request yet" + } + "#; + const slow_down: &str = r#" + { + "error": "slow_down", + "error_description": "Please poll slower" + } + "#; + const expired_token: &str = r#" + { + "error": "expired_token", + "error_description": "Token expired" + } + "#; + const invalid_grant: &str = r#" + { + "error": "invalid_grant", + "error_description": "Token is invalid" + } + "#; + const access_denied: &str = r#" + { + "error": "access_denied", + "error_description": "User denied the request" + } + "#; + + let ap: PollResponseError = serde_json::from_str(authorization_pending).unwrap(); + assert!(matches!( + ap, + PollResponseError::AuthorizationPending { + error_description: _ + } + )); + + let sd: PollResponseError = serde_json::from_str(slow_down).unwrap(); + println!("{:#?}", sd); + assert!(matches!( + sd, + PollResponseError::SlowDown { + error_description: _ + } + )); + + let et: PollResponseError = serde_json::from_str(expired_token).unwrap(); + assert!(matches!( + et, + PollResponseError::ExpiredToken { + error_description: _ + } + )); + + let ig: PollResponseError = serde_json::from_str(invalid_grant).unwrap(); + assert!(matches!( + ig, + PollResponseError::InvalidGrant { + error_description: _ + } + )); + + let ad: PollResponseError = serde_json::from_str(access_denied).unwrap(); + assert!(matches!( + ad, + PollResponseError::AccessDenied { + error_description: _ + } + )); + } +} diff --git a/web/Cargo.toml b/web/Cargo.toml index 1cec057..556db22 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -13,5 +13,6 @@ uuid = { version = "0.8.2", features = ["serde"] } sqlx = { version = "0.5.9", features = ["chrono", "macros", "migrate", "offline", "postgres", "runtime-tokio-rustls", "uuid"] } serde = { version = "1.0.130", features = ["derive"] } async-trait = "0.1.51" +tokio = { version = "1.15.0", features = ["rt", "macros"]} futures = "0.3.17" chrono = { version = "0.4.19", features = ["serde"] } \ No newline at end of file diff --git a/web/migrations/20211113170055_challenge_type_enum.sql b/web/migrations/20211113170055_challenge_type_enum.sql deleted file mode 100644 index e0c5a13..0000000 --- a/web/migrations/20211113170055_challenge_type_enum.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TYPE challenge_type AS ENUM ( - 'Pwn', - 'Web', - 'Crypto', - 'Reversing' -) \ No newline at end of file diff --git a/web/migrations/20211113171411_add_challenge_type_and_repo_url.sql b/web/migrations/20211113171411_add_challenge_type_and_repo_url.sql deleted file mode 100644 index 5edde22..0000000 --- a/web/migrations/20211113171411_add_challenge_type_and_repo_url.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE ctf_challenges -ADD COLUMN challenge_type CHALLENGE_TYPE NOT NULL, -ADD COLUMN repo_url TEXT NOT NULL; \ No newline at end of file diff --git a/web/migrations/20220103134712_add_challenge_type_enum.sql b/web/migrations/20220103134712_add_challenge_type_enum.sql new file mode 100644 index 0000000..1c8dda5 --- /dev/null +++ b/web/migrations/20220103134712_add_challenge_type_enum.sql @@ -0,0 +1,7 @@ +-- a enum to define the type of challenge +CREATE TYPE challenge_type AS ENUM ( + 'pwn', + 'web', + 'crypto', + 'reversing' +) \ No newline at end of file diff --git a/web/migrations/20211113153756_ctf_challenge_table.sql b/web/migrations/20220103135614_init_challenge_table.sql similarity index 53% rename from web/migrations/20211113153756_ctf_challenge_table.sql rename to web/migrations/20220103135614_init_challenge_table.sql index 5d746ee..2ee0cdf 100644 --- a/web/migrations/20211113153756_ctf_challenge_table.sql +++ b/web/migrations/20220103135614_init_challenge_table.sql @@ -1,14 +1,21 @@ -CREATE TABLE ctf_challenges ( +-- stores the ctf challenges +CREATE TABLE challenges ( -- unique id of this challenge - "id" UUID NOT NULL UNIQUE DEFAULT uuid_generate_v4() PRIMARY KEY, + -- for `gen_random_uuid()` ensure that the `pgcrypto` extension is present + -- or else enable it with `CREATE EXTENSION EXISTS pgcrypto` + "id" UUID NOT NULL UNIQUE DEFAULT gen_random_uuid() PRIMARY KEY, -- the name of the challenge displayed to the user "name" VARCHAR(32) NOT NULL UNIQUE CHECK (char_length(name) > 0), + -- the type of this challenge + "type" CHALLENGE_TYPE NOT NULL, -- a short description (max length 120 chars) "short_description" VARCHAR(120) CHECK (char_length(short_description) > 0), -- the long description of the challenge "long_description" TEXT CHECK (char_length(long_description) > 0), - -- hints for the challenge + -- hints for the challenge to help players "hints" TEXT CHECK (char_length(hints) > 0), + -- then this challenge was created "created_at" TIMESTAMPTZ NOT NULL DEFAULT current_timestamp, + -- if the challenge is being deployed by the supervisor "active" BOOLEAN NOT NULL DEFAULT false ) \ No newline at end of file diff --git a/web/src/challenge.rs b/web/src/challenge.rs index 2938188..201db16 100644 --- a/web/src/challenge.rs +++ b/web/src/challenge.rs @@ -1,6 +1,9 @@ +use std::ops::{Deref, DerefMut}; + use crate::loaders::{ - ChallengeHintsLoaderByID, ChallengeNameLoaderByID, ChallengeTypeLoaderByID, - CreatedAtLoaderByID, IsActiveLoaderByID, LongDescriptionLoaderByID, ShortDescriptionLoaderByID, + ChallengeActiveLoaderByID, ChallengeCreatedAtLoaderByID, ChallengeHintsLoaderByID, + ChallengeLongDescriptionLoaderByID as LongDescriptionLoaderByID, ChallengeNameLoaderByID, + ChallengeShortDescriptionLoaderByID as ShortDescriptionLoaderByID, ChallengeTypeLoaderByID, }; use async_graphql::{dataloader::DataLoader as DL, Context, Enum, Object, Result, ID}; use chrono::{DateTime, Utc}; @@ -12,10 +15,10 @@ mod queries; pub use queries::*; #[non_exhaustive] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] /// A ctf challenge pub struct Challenge { - /// The unique identifier of this session used as the primary key + /// The unique identifier of this challenge used as the primary key id: Uuid, } @@ -39,8 +42,7 @@ impl Challenge { Ok(ctx .data_unchecked::>() .load_one(self.id) - .await? - .unwrap()) + .await?) } /// A long(er) description for the challenge @@ -48,8 +50,7 @@ impl Challenge { Ok(ctx .data_unchecked::>() .load_one(self.id) - .await? - .unwrap()) + .await?) } /// Hints that may help/spoiler the challenge @@ -57,14 +58,13 @@ impl Challenge { Ok(ctx .data_unchecked::>() .load_one(self.id) - .await? - .unwrap()) + .await?) } /// If the challenge is currently playable (e.g. if the challenge server /// is online or not) pub async fn is_active(&self, ctx: &Context<'_>) -> Result { Ok(ctx - .data_unchecked::>() + .data_unchecked::>() .load_one(self.id) .await? .unwrap()) @@ -73,7 +73,7 @@ impl Challenge { /// The date and time the challenge was published pub async fn created_at(&self, ctx: &Context<'_>) -> Result> { Ok(ctx - .data_unchecked::>() + .data_unchecked::>() .load_one(self.id) .await? .unwrap()) @@ -98,8 +98,21 @@ impl From for Challenge { } } +impl Deref for Challenge { + type Target = Uuid; + fn deref(&self) -> &Self::Target { + &self.id + } +} + +impl DerefMut for Challenge { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.id + } +} #[non_exhaustive] #[derive(Enum, Clone, Copy, PartialEq, Eq, sqlx::Type, Debug)] +#[sqlx(rename_all = "snake_case", type_name = "challenge_type")] /// The type of a ctf [`Challenge`] pub enum ChallengeType { Pwn, diff --git a/web/src/challenge/queries.rs b/web/src/challenge/queries.rs index 93f36d7..666703d 100644 --- a/web/src/challenge/queries.rs +++ b/web/src/challenge/queries.rs @@ -1,9 +1,8 @@ -use async_graphql::{dataloader::DataLoader as DL, Context, Object, Result, ID}; +use async_graphql::{Context, Object, Result, ID}; use futures::stream::TryStreamExt; use sqlx::PgPool; use super::Challenge; -use crate::loaders::ChallengeLoaderByName; #[non_exhaustive] #[derive(Debug, Default)] @@ -12,23 +11,12 @@ pub struct ChallengeQueries; #[Object] impl ChallengeQueries { - /// return a challenge by it's name. Usually you should not use this - /// function, because minimal changes in the name like a newline would - /// mean, that this query returns none. - /// Instead, use the `Node` implementation of `Challenge` with the id. - async fn challenge(&self, ctx: &Context<'_>, name: String) -> Result> { - Ok(ctx - .data_unchecked::>() - .load_one(name) - .await?) - } - // cache it so its not that bad #[graphql(cache_control(max_age = 300))] /// return all challenges (no pagination yet) async fn challenges(&self, ctx: &Context<'_>) -> Result> { let pool = ctx.data_unchecked::(); - let challenges: Vec<_> = sqlx::query!(r#"SELECT "id" FROM ctf_challenges"#) + let challenges: Vec<_> = sqlx::query!(r#"SELECT "id" FROM challenges LIMIT 1000"#) .fetch(pool) .map_ok(|c| Challenge::from(c.id)) .try_collect() @@ -44,6 +32,6 @@ pub struct ChallengeMutations; #[Object] impl ChallengeMutations { async fn submit_flag(&self, _flag: String, _challenge: ID) -> Option { - todo!("send flag to supervisor") + None } } diff --git a/web/src/loaders/challenge_loaders.rs b/web/src/loaders/challenge_loaders.rs index 666923b..03b585b 100644 --- a/web/src/loaders/challenge_loaders.rs +++ b/web/src/loaders/challenge_loaders.rs @@ -1,70 +1,329 @@ +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + use crate::{ basic_loader, challenge::{Challenge, ChallengeType}, + loader_struct, }; +use async_graphql::dataloader::Loader; use chrono::{DateTime, Utc}; +use futures::prelude::stream::StreamExt; use uuid::Uuid; +mod loader_front; +pub use loader_front::*; + // used to check if a challenge exists in the database basic_loader!( ChallengeLoaderByID, Uuid, Challenge, - r#"SELECT "id" AS ka, "id" AS val FROM ctf_challenges WHERE "id" = ANY($1)"# + r#"SELECT "id" AS ka, "id" AS val FROM challenges WHERE "id" = ANY($1)"# ); -basic_loader!( - ChallengeLoaderByName, - String, - Challenge, - r#"SELECT "name" AS ka, "id" AS val FROM ctf_challenges WHERE "name" = ANY($1)"# -); +loader_struct!(ChallengeLoader); -basic_loader!( - ChallengeNameLoaderByID, - Uuid, - String, - r#"SELECT "id" AS ka, "name" AS val FROM ctf_challenges WHERE "id" = ANY($1)"# -); +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +/// Enum used to select a column for a row +enum ChallengeFieldSelection { + Name, + Type, + ShortDescription, + LongDescription, + Hints, + CreatedAt, + Active, +} -basic_loader!( - ShortDescriptionLoaderByID, - Uuid, - Option, - r#"SELECT "id" AS ka, "short_description" AS val FROM ctf_challenges WHERE "id" = ANY($1)"# -); +/// Corresponding responses to [`ChallengeFieldSelection`] +#[derive(Debug, Clone)] +enum ChallengeField { + Name(String), + Type(ChallengeType), + ShortDescription(String), + LongDescription(String), + Hints(String), + CreatedAt(DateTime), + Active(bool), +} -basic_loader!( - LongDescriptionLoaderByID, - Uuid, - Option, - r#"SELECT "id" AS ka, "long_description" AS val FROM ctf_challenges WHERE "id" = ANY($1)"# -); +#[derive(Debug, sqlx::FromRow)] +/// used to have a typed response with [`sqlx::query_as`] (the function, not the macro) +struct ChallengeResponseRow { + id: Uuid, + name: Option, + #[sqlx(rename = "type")] + typ: Option, + short_description: Option, + long_description: Option, + hints: Option, + created_at: Option>, + active: Option, +} -basic_loader!( - IsActiveLoaderByID, - Uuid, - bool, - r#"SELECT "id" AS ka, "active" AS val FROM ctf_challenges WHERE "id" = ANY($1)"# -); +#[async_trait] +impl Loader<(Challenge, ChallengeFieldSelection)> for ChallengeLoader { + type Value = ChallengeField; + type Error = Arc; + async fn load( + &self, + keys: &[(Challenge, ChallengeFieldSelection)], + ) -> Result, Self::Error> { + let mut collected: HashMap> = HashMap::new(); + for (challenge, selection) in keys { + // when https://github.com/rust-lang/rust/issues/65225 is stabilized, + // this could be replaced with Entry::insert_entry (currently this + // method is called `insert` and is behind the `entry_insert` feature flag). + // get the selections for the `challenge` or insert an empty one and modify that + collected.entry(*challenge).or_default().insert(*selection); + } -basic_loader!( - CreatedAtLoaderByID, - Uuid, - DateTime, - r#"SELECT "id" AS ka, "created_at" AS val FROM ctf_challenges WHERE "id" = ANY($1)"# -); + // the size of the rows + let to_allocate = collected.len(); -basic_loader!( - ChallengeTypeLoaderByID, - Uuid, - ChallengeType, - r#"SELECT "id" AS ka, "challenge_type" AS "val!: ChallengeType" FROM ctf_challenges WHERE "id" = ANY($1)"# -); + // for each column + primary key, we need one Vec + let mut id = Vec::with_capacity(to_allocate); + let mut name = Vec::with_capacity(to_allocate); + let mut typ = Vec::with_capacity(to_allocate); + let mut s_desc = Vec::with_capacity(to_allocate); + let mut l_desc = Vec::with_capacity(to_allocate); + let mut hints = Vec::with_capacity(to_allocate); + let mut created_at = Vec::with_capacity(to_allocate); + let mut active = Vec::with_capacity(to_allocate); -basic_loader!( - ChallengeHintsLoaderByID, - Uuid, - Option, - r#"SELECT "id" AS ka, "hints" AS "val" FROM ctf_challenges WHERE "id" = ANY($1)"# -); + for (challenge, selections) in collected { + id.push(*challenge); + name.push(selections.contains(&ChallengeFieldSelection::Name)); + typ.push(selections.contains(&ChallengeFieldSelection::Type)); + s_desc.push(selections.contains(&ChallengeFieldSelection::ShortDescription)); + l_desc.push(selections.contains(&ChallengeFieldSelection::LongDescription)); + hints.push(selections.contains(&ChallengeFieldSelection::Hints)); + created_at.push(selections.contains(&ChallengeFieldSelection::CreatedAt)); + active.push(selections.contains(&ChallengeFieldSelection::Active)); + } + + let mut transaction = self.pool.begin().await?; + + sqlx::query_file!("src/loaders/challenge_selection_table.sql") + .execute(&mut transaction) + .await?; + + // the following two queries can't be checked at compile time because + // they use a temporary table which is only available during the transaction + sqlx::query(include_str!("challenge_selection_insert.sql")) + .bind(&id) + .bind(&name) + .bind(&typ) + .bind(&s_desc) + .bind(&l_desc) + .bind(&hints) + .bind(&created_at) + .bind(&active) + .execute(&mut transaction) + .await?; + + let mut rows = sqlx::query_as::<_, ChallengeResponseRow>(include_str!( + "challenge_selection_fetch.sql" + )) + .fetch(&mut transaction); + + let mut response = HashMap::new(); + while let Some(row) = rows.next().await { + let row = row?; + let id: Challenge = row.id.into(); + if let Some(name) = row.name { + response.insert( + (id, ChallengeFieldSelection::Name), + ChallengeField::Name(name), + ); + } + if let Some(typ) = row.typ { + response.insert( + (id, ChallengeFieldSelection::Type), + ChallengeField::Type(typ), + ); + } + if let Some(s_desc) = row.short_description { + response.insert( + (id, ChallengeFieldSelection::ShortDescription), + ChallengeField::ShortDescription(s_desc), + ); + } + if let Some(l_desc) = row.long_description { + response.insert( + (id, ChallengeFieldSelection::LongDescription), + ChallengeField::LongDescription(l_desc), + ); + } + if let Some(hints) = row.hints { + response.insert( + (id, ChallengeFieldSelection::Hints), + ChallengeField::Hints(hints), + ); + } + if let Some(created_at) = row.created_at { + response.insert( + (id, ChallengeFieldSelection::CreatedAt), + ChallengeField::CreatedAt(created_at), + ); + } + if let Some(active) = row.active { + response.insert( + (id, ChallengeFieldSelection::Active), + ChallengeField::Active(active), + ); + } + } + drop(rows); + + // finish the transaction so the temporary table is deleted + transaction.commit().await?; + + Ok(response) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use async_graphql::dataloader::Loader; + use uuid::Uuid; + + use crate::challenge::{Challenge, ChallengeType}; + + use super::{ChallengeField, ChallengeFieldSelection, ChallengeLoader}; + + #[tokio::test] + async fn loader_load() { + let pool = sqlx::PgPool::connect( + &std::env::var("PCTF_DB_URI").expect("test needs database connection"), + ) + .await + .expect("cannot connect to database"); + + // now insert some test data + let a = Uuid::from_str("dcbebb24-c149-400c-b09f-3d6839d10900").unwrap(); + let b = Uuid::from_str("f0eb77dd-3766-4569-ab44-c385059a3ed3").unwrap(); + let c = Uuid::from_str("cfe1ae10-e1e0-4149-8627-3e08770a782e").unwrap(); + + let a_name = "a ctf challenge"; + let b_name = "pickle_rick"; + let c_name = "log4j"; + + let a_type = ChallengeType::Crypto; + let b_type = ChallengeType::Web; + let c_type = ChallengeType::Pwn; + + let a_short_desc = "Bayern raus aus Deutschland (Spass)"; + let b_short_desc = "pickle rick from rick and morty and a bit of python"; + // no short description for c + + let a_long_desc = concat!( + "Hass Frau, du nichts, ich Mann\n", + "Blase, bis du kotzt, aber kotz auf mein'n Schwanz\n", + "Hass Frau, du nichts, ich Mann\n", + "Ich fick in dein'n Arsch und danach leckst du ab\n", + "Hass Frau, du nichts, ich Mann\n", + "Fick mich und halt dein Maul\n", + ); + let b_long_desc = concat!( + "Pickerick was a challenge in a previous ctf. ", + "The player had to use a template injection. ", + "With that template injection, the player was able to steal the ", + "secret used for token signing. ", + "Since the token was signed and therefore trusted, the server used ", + "pythons pickle module to load a user from that token, this enabled ", + "RCE. Add a misconfigured sudo and there you go!" + ); + + let a_hints = "SXTN"; + + sqlx::query!( + r#" + INSERT INTO challenges ( + "id", + "name", + "type", + "short_description", + "long_description", + "hints" + ) VALUES ($1, $2, $3, $4, $5, $6), + ($7, $8, $9, $10, $11, null), + ($12, $13, $14, null, null, null) + ON CONFLICT DO NOTHING + "#, + a, + a_name, + // just needed for sqlx, dont ask me + // see https://github.com/launchbadge/sqlx/issues/1004#issuecomment-764964043 + a_type as _, + a_short_desc, + a_long_desc, + a_hints, + b, + b_name, + b_type as _, + b_short_desc, + b_long_desc, + c, + c_name, + c_type as _ + ) + .execute(&pool) + .await + .expect("failed to insert testdata"); + + let loader = ChallengeLoader::new(pool.clone()); + + let a_challenge: Challenge = a.into(); + let b_challenge: Challenge = b.into(); + // we dont request c to proof that only requested data is returned + let c_challenge: Challenge = c.into(); + + let keys = &[ + (a_challenge, ChallengeFieldSelection::Name), + (a_challenge, ChallengeFieldSelection::LongDescription), + (a_challenge, ChallengeFieldSelection::CreatedAt), + (a_challenge, ChallengeFieldSelection::Type), + (b_challenge, ChallengeFieldSelection::ShortDescription), + // should be not in the result set since it is null + (b_challenge, ChallengeFieldSelection::Hints), + ][..]; + let result = loader.load(keys).await.unwrap(); + + assert!(result + .get(&(b_challenge, ChallengeFieldSelection::Hints)) + .is_none()); + + let a_type_resp = if let ChallengeField::Type(typ) = result + .get(&(a_challenge, ChallengeFieldSelection::Type)) + .unwrap() + { + typ + } else { + panic!() + }; + assert_eq!(a_type_resp, &a_type); + + // ensure nothing about c is in the response + for ((i, _), _) in result { + if i == c_challenge { + panic!("c should not be in the response") + } + } + + sqlx::query!( + r#" + DELETE FROM challenges WHERE "id" = ANY($1) + "#, + &[a, b, c][..] + ) + .execute(&pool) + .await + .expect("cannot delete test data"); + } +} diff --git a/web/src/loaders/challenge_loaders/loader_front.rs b/web/src/loaders/challenge_loaders/loader_front.rs new file mode 100644 index 0000000..fa2a136 --- /dev/null +++ b/web/src/loaders/challenge_loaders/loader_front.rs @@ -0,0 +1,122 @@ +use std::{collections::HashMap, sync::Arc}; + +use async_graphql::dataloader::{DataLoader, Loader}; +use chrono::{DateTime, Utc}; +use uuid::Uuid; + +use crate::challenge::{Challenge, ChallengeType}; + +use super::{ChallengeField, ChallengeFieldSelection, ChallengeLoader}; + +/// Macro so I dont have to repeat this seven times: +/* +pub struct ChallengeNameLoaderByID { + loader: Arc>, +} + +#[async_trait] +impl Loader for ChallengeNameLoaderByID { + type Value = String; + type Error = Arc; + async fn load(&self, keys: &[Uuid]) -> Result, Self::Error> { + let keys = keys + .iter() + .map(|k| (Challenge::from(*k), ChallengeFieldSelection::Name)); + Ok(self + .loader + .load_many(keys) + .await? + .into_iter() + .map(|((challenge, _), resp)| { + let name = match resp { + ChallengeField::Name(name) => name, + // this will never trigger as long as ChallengeFieldSelection::Name + // is ChallengeField::Name, if its not, its a bug + _ => panic!("found invalid variant, this is a bug!"), + }; + (*challenge, name) + }) + .collect()) + } +} +*/ +macro_rules! c_loader { + ($name:ident, $val:ty, $requested:path, $expected:path) => { + pub struct $name { + loader: Arc>, + } + + impl $name { + #[deny(unused)] + pub fn new(loader: Arc>) -> Self { + Self { loader } + } + } + + #[async_trait] + impl Loader for $name { + type Value = $val; + type Error = Arc; + + async fn load(&self, keys: &[Uuid]) -> Result, Self::Error> { + let keys = keys.iter().map(|k| (Challenge::from(*k), $requested)); + Ok(self + .loader + .load_many(keys) + .await? + .into_iter() + .map(|((challenge, _), resp)| { + let value = match resp { + $expected(value) => value, + _ => panic!("found invalid variant, this is a bug!"), + }; + (*challenge, value) + }) + .collect()) + } + } + }; +} + +c_loader!( + ChallengeNameLoaderByID, + String, + ChallengeFieldSelection::Name, + ChallengeField::Name +); +c_loader!( + ChallengeTypeLoaderByID, + ChallengeType, + ChallengeFieldSelection::Type, + ChallengeField::Type +); +c_loader!( + ChallengeShortDescriptionLoaderByID, + String, + ChallengeFieldSelection::ShortDescription, + ChallengeField::ShortDescription +); +c_loader!( + ChallengeLongDescriptionLoaderByID, + String, + ChallengeFieldSelection::LongDescription, + ChallengeField::LongDescription +); +c_loader!( + ChallengeHintsLoaderByID, + String, + ChallengeFieldSelection::Hints, + ChallengeField::Hints +); +c_loader!( + ChallengeCreatedAtLoaderByID, + DateTime, + ChallengeFieldSelection::CreatedAt, + ChallengeField::CreatedAt +); +c_loader!( + ChallengeActiveLoaderByID, + bool, + ChallengeFieldSelection::Active, + ChallengeField::Active +); diff --git a/web/src/loaders/challenge_selection_fetch.sql b/web/src/loaders/challenge_selection_fetch.sql new file mode 100644 index 0000000..cf49f40 --- /dev/null +++ b/web/src/loaders/challenge_selection_fetch.sql @@ -0,0 +1,36 @@ +SELECT + -- id / primary key is always needed + challenges.id, + -- now some CASE hackery to select each column only if the bool for the id + -- in the `challenge_selections` table is true + CASE + WHEN challenge_selections.name + THEN challenges.name + END "name", + CASE + WHEN challenge_selections.type + THEN challenges.type + END "type", + CASE + WHEN challenge_selections.short_description + THEN challenges.short_description + END "short_description", + CASE + WHEN challenge_selections.long_description + THEN challenges.long_description + END "long_description", + CASE + WHEN challenge_selections.hints + THEN challenges.hints + END "hints", + CASE + WHEN challenge_selections.created_at + THEN challenges.created_at + END "created_at", + CASE + WHEN challenge_selections.active + THEN challenges.active + END "active" +FROM challenges + INNER JOIN challenge_selections ON (challenge_selections.id = challenges.id) +WHERE challenges.id = ANY(SELECT id FROM challenge_selections) diff --git a/web/src/loaders/challenge_selection_insert.sql b/web/src/loaders/challenge_selection_insert.sql new file mode 100644 index 0000000..1f1416f --- /dev/null +++ b/web/src/loaders/challenge_selection_insert.sql @@ -0,0 +1,21 @@ +-- uses the temporary table in the +INSERT INTO challenge_selections ( + "id", + "name", + "type", + "short_description", + "long_description", + "hints", + "created_at", + "active" +) VALUES ( + -- unnest hackery + unnest($1::UUID[]), + unnest($2::BOOL[]), + unnest($3::BOOL[]), + unnest($4::BOOL[]), + unnest($5::BOOL[]), + unnest($6::BOOL[]), + unnest($7::BOOL[]), + unnest($8::BOOL[]) +) diff --git a/web/src/loaders/challenge_selection_table.sql b/web/src/loaders/challenge_selection_table.sql new file mode 100644 index 0000000..7f5fecb --- /dev/null +++ b/web/src/loaders/challenge_selection_table.sql @@ -0,0 +1,14 @@ +-- this table stores a bool for each row +-- each column in this table corresponds to the column in the `challenges` table +-- if true, the column is requested +CREATE TEMPORARY TABLE challenge_selections ( + "id" UUID PRIMARY KEY, + "name" BOOL NOT NULL, + "type" BOOL NOT NULL, + "short_description" BOOL NOT NULL, + "long_description" BOOL NOT NULL, + "hints" BOOL NOT NULL, + "created_at" BOOL NOT NULL, + "active" BOOL NOT NULL +-- this table is only valid for one transaction +) ON COMMIT DROP diff --git a/web/src/main.rs b/web/src/main.rs index bd8a3f6..43861df 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use async_graphql::{ dataloader::DataLoader as DL, extensions::ApolloTracing, @@ -52,18 +54,23 @@ async fn rocket() -> _ { .await .expect("cannot run database migrations"); + // the dataloader used by the the front loaders + let s_l = Arc::new(DL::new(ChallengeLoader::new(db.clone()))); // generate the schema let schema = Schema::build(Queries::default(), Mutations::default(), EmptySubscription) // register the different data loaders .data(DL::new(ChallengeLoaderByID::new(db.clone()))) - .data(DL::new(ChallengeLoaderByName::new(db.clone()))) - .data(DL::new(ChallengeNameLoaderByID::new(db.clone()))) - .data(DL::new(ShortDescriptionLoaderByID::new(db.clone()))) - .data(DL::new(LongDescriptionLoaderByID::new(db.clone()))) - .data(DL::new(IsActiveLoaderByID::new(db.clone()))) - .data(DL::new(CreatedAtLoaderByID::new(db.clone()))) - .data(DL::new(ChallengeTypeLoaderByID::new(db.clone()))) - .data(DL::new(ChallengeHintsLoaderByID::new(db.clone()))) + .data(DL::new(ChallengeNameLoaderByID::new(s_l.clone()))) + .data(DL::new(ChallengeTypeLoaderByID::new(s_l.clone()))) + .data(DL::new(ChallengeShortDescriptionLoaderByID::new( + s_l.clone(), + ))) + .data(DL::new(ChallengeLongDescriptionLoaderByID::new( + s_l.clone(), + ))) + .data(DL::new(ChallengeHintsLoaderByID::new(s_l.clone()))) + .data(DL::new(ChallengeCreatedAtLoaderByID::new(s_l.clone()))) + .data(DL::new(ChallengeActiveLoaderByID::new(s_l))) .data(db.clone()); // enable tracing if wanted