diff --git a/Cargo.lock b/Cargo.lock index c2e04bd..8c925dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,11 +8,13 @@ version = "0.1.0" dependencies = [ "chrono", "clap", + "digest_auth", "env_logger", "log", "mlua", "reqwest", "rusqlite", + "rustls", "serde", "serde_json", "thiserror", @@ -117,28 +119,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "base64" version = "0.22.1" @@ -157,6 +137,15 @@ version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "1.12.1" @@ -186,8 +175,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -197,12 +184,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chrono" version = "0.4.45" @@ -256,15 +237,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "colorchoice" version = "1.0.5" @@ -307,6 +279,25 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "defmt" version = "1.1.0" @@ -339,6 +330,29 @@ dependencies = [ "thiserror", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "digest_auth" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3054f4e81d395e50822796c5e99ca522e6ba7be98947d6d4b0e5e61640bdb894" +dependencies = [ + "digest", + "hex", + "md-5", + "rand", + "sha2", +] + [[package]] name = "displaydoc" version = "0.2.6" @@ -350,12 +364,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "either" version = "1.16.0" @@ -454,12 +462,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futures-channel" version = "0.3.32" @@ -500,30 +502,24 @@ dependencies = [ ] [[package]] -name = "getrandom" -version = "0.2.17" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", + "typenum", + "version_check", ] [[package]] name = "getrandom" -version = "0.3.4" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", - "r-efi", - "wasip2", - "wasm-bindgen", + "wasi", ] [[package]] @@ -575,6 +571,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.4.2" @@ -904,16 +906,6 @@ dependencies = [ "syn", ] -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.102" @@ -963,12 +955,6 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "lua-src" version = "550.0.0" @@ -988,6 +974,16 @@ dependencies = [ "which", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.8.2" @@ -1187,62 +1183,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" version = "1.0.45" @@ -1252,27 +1192,22 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "rand" -version = "0.9.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ + "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" -version = "0.9.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", @@ -1280,11 +1215,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.5" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.3.4", + "getrandom", ] [[package]] @@ -1347,7 +1282,6 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "quinn", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -1371,7 +1305,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.17", + "getrandom", "libc", "untrusted", "windows-sys 0.52.0", @@ -1412,8 +1346,8 @@ version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ - "aws-lc-rs", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -1438,7 +1372,6 @@ version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ - "web-time", "zeroize", ] @@ -1475,7 +1408,6 @@ version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1593,6 +1525,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "2.0.1" @@ -1747,21 +1690,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.52.3" @@ -1889,6 +1817,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1962,15 +1896,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" version = "0.2.125" @@ -2036,21 +1961,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" dependencies = [ "rustls-pki-types", ] @@ -2149,16 +2064,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -2176,31 +2082,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -2209,102 +2098,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - [[package]] name = "writeable" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index 818b852..6c976e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,16 @@ env_logger = "0.11" log = "0.4" clap = { version = "4.6.1", features = ["derive"] } mlua = { version = "0.11.6", features = ["lua55", "vendored", "serde", "async", "anyhow"]} -reqwest = "0.13.4" +# reqwest's default rustls provider (aws-lc-rs) null-derefs during the TLS +# handshake in this environment, and native-tls would link system OpenSSL. +# Use rustls with the self-contained, vendored `ring` provider (installed as the +# process default in stdlib::http). `rustls-no-provider` stops reqwest from +# pulling aws-lc-rs back in. +reqwest = { version = "0.13.4", default-features = false, features = ["rustls-no-provider", "charset", "http2", "system-proxy"] } +rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } rusqlite = { version = "0.32", features = ["bundled"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" thiserror = "2.0.18" tokio = { version = "1.52.3", features = ["full"] } +digest_auth = "0.3" diff --git a/lua/stdlib/http.lua b/lua/stdlib/http.lua new file mode 100644 index 0000000..5c4e4f8 --- /dev/null +++ b/lua/stdlib/http.lua @@ -0,0 +1,66 @@ +-- Lua-side of the http module. +-- http.request (stateless) and http.session (constructor) are provided by Rust +-- before this runs. This layer adds resp.json(), method shorthands, the +-- JSON/postJSON helpers, and a metatable for session objects. + +-- Attach resp.json() to a response table: parses resp.body via utils.fromJSON. +local function wrap_resp(resp) + resp.json = function() return utils.fromJSON(resp.body) end + return resp +end + +-- Wrap the stateless http.request so responses carry resp.json(). +local _request = http.request +http.request = function(method, url, opts) + return wrap_resp(_request(method, url, opts)) +end + +-- Method shorthands for the stateless module. +for _, m in ipairs({ "get", "post", "put", "patch", "delete", "head" }) do + http[m] = function(url, opts) return http.request(m:upper(), url, opts) end +end + +function http.getJSON(url, opts) + local resp = http.get(url, opts) + return resp.json(), resp +end + +function http.postJSON(url, body, opts) + opts = opts or {} + opts.json = body + return http.post(url, opts) +end + +-- Session metatable. http.session() returns a raw Rust table whose methods are +-- _request, save, load, clearCookies and cookies. The metatable adds the +-- request wrapper (for resp.json()) and the method shorthands on top. +local session_mt = {} +session_mt.__index = session_mt + +function session_mt:request(method, url, opts) + return wrap_resp(self:_request(method, url, opts)) +end + +function session_mt:get(url, opts) return self:request("GET", url, opts) end +function session_mt:post(url, opts) return self:request("POST", url, opts) end +function session_mt:put(url, opts) return self:request("PUT", url, opts) end +function session_mt:patch(url, opts) return self:request("PATCH", url, opts) end +function session_mt:delete(url, opts) return self:request("DELETE", url, opts) end +function session_mt:head(url, opts) return self:request("HEAD", url, opts) end + +function session_mt:getJSON(url, opts) + local resp = self:get(url, opts) + return resp.json(), resp +end + +function session_mt:postJSON(url, body, opts) + opts = opts or {} + opts.json = body + return self:post(url, opts) +end + +-- Wrap the Rust session constructor to install the metatable. +local _session = http.session +http.session = function(path) + return setmetatable(_session(path), session_mt) +end diff --git a/src/stdlib/http.rs b/src/stdlib/http.rs new file mode 100644 index 0000000..6aba9ba --- /dev/null +++ b/src/stdlib/http.rs @@ -0,0 +1,529 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use mlua::prelude::LuaResult; +use mlua::{Lua, Table as LuaTable, Value as LuaValue}; + +use crate::stdlib::utils::lua_to_json; + +const HTTP_LUA: &str = include_str!("../../lua/stdlib/http.lua"); + +// --------------------------------------------------------------------------- +// Cookie jar +// --------------------------------------------------------------------------- + +/// A minimal in-memory cookie jar: bare domain (no leading dot) → name → value. +/// Deliberately simple — it covers the >95% case of `Set-Cookie` flows without +/// pulling in the `cookie_store` crate, and serializes cleanly to JSONL. +#[derive(Default)] +struct CookieJar { + cookies: HashMap>, +} + +impl CookieJar { + /// All cookies (name, value) whose stored domain matches `host`: either an + /// exact match or `host` being a subdomain of the stored domain. + fn cookies_for(&self, host: &str) -> Vec<(String, String)> { + let mut out = Vec::new(); + for (domain, names) in &self.cookies { + let suffix = format!(".{domain}"); + if host == domain || host.ends_with(&suffix) { + for (name, value) in names { + out.push((name.clone(), value.clone())); + } + } + } + out + } + + /// Parse a single `Set-Cookie` header value and store the cookie. Extracts + /// `name=value` (first segment) and an optional `Domain=` attribute, falling + /// back to the request host. Malformed headers are ignored. + fn set_from_header(&mut self, host: &str, header: &str) { + let mut segments = header.split(';'); + let first = match segments.next() { + Some(s) => s.trim(), + None => return, + }; + let (name, value) = match first.split_once('=') { + Some((n, v)) => (n.trim(), v.trim()), + None => return, + }; + if name.is_empty() { + return; + } + + let mut domain = host.to_ascii_lowercase(); + for seg in segments { + if let Some((k, v)) = seg.split_once('=') + && k.trim().eq_ignore_ascii_case("domain") + { + let d = v.trim().trim_start_matches('.').to_ascii_lowercase(); + if !d.is_empty() { + domain = d; + } + } + } + + self.cookies + .entry(domain) + .or_default() + .insert(name.to_string(), value.to_string()); + } + + /// Serialize as JSONL — one `{"domain","name","value"}` object per line. + fn to_jsonl(&self) -> String { + let mut out = String::new(); + for (domain, names) in &self.cookies { + for (name, value) in names { + let obj = serde_json::json!({ + "domain": domain, + "name": name, + "value": value, + }); + out.push_str(&obj.to_string()); + out.push('\n'); + } + } + out + } + + /// Merge cookies from JSONL produced by `to_jsonl`. Bad lines are skipped. + fn merge_jsonl(&mut self, src: &str) { + for line in src.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + let Ok(v) = serde_json::from_str::(line) else { + continue; + }; + let domain = v.get("domain").and_then(|x| x.as_str()); + let name = v.get("name").and_then(|x| x.as_str()); + let value = v.get("value").and_then(|x| x.as_str()); + if let (Some(d), Some(n), Some(val)) = (domain, name, value) { + self.cookies + .entry(d.to_string()) + .or_default() + .insert(n.to_string(), val.to_string()); + } + } + } +} + +// --------------------------------------------------------------------------- +// Request execution +// --------------------------------------------------------------------------- + +/// `application/x-www-form-urlencoded` body from key/value pairs. Implemented +/// inline so the build needs no optional reqwest features. +fn form_urlencode(pairs: &[(String, String)]) -> String { + fn encode(s: &str) -> String { + let mut out = String::new(); + for b in s.bytes() { + match b { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + out.push(b as char) + } + b' ' => out.push('+'), + _ => out.push_str(&format!("%{b:02X}")), + } + } + out + } + pairs + .iter() + .map(|(k, v)| format!("{}={}", encode(k), encode(v))) + .collect::>() + .join("&") +} + +const MAX_REDIRECTS: u32 = 10; + +/// Shared by the stateless `http.request` and a session's `:request`. When `jar` +/// is `Some`, the matching jar cookies are sent and any `Set-Cookie` responses +/// are stored back. +/// +/// Redirects are followed manually (the client is built with +/// `redirect::Policy::none`) so that `Set-Cookie` headers on 30x responses — +/// the common login → redirect → dashboard pattern — are captured into the jar, +/// which reqwest's transparent redirect following would otherwise hide. +async fn execute_request( + lua: Lua, + client: reqwest::Client, + jar: Option>>, + method: String, + url: String, + opts: Option, +) -> LuaResult { + let mut method = reqwest::Method::from_bytes(method.to_ascii_uppercase().as_bytes()) + .map_err(|e| mlua::Error::external(format!("http: invalid method '{method}': {e}")))?; + + // Parse opts once into owned pieces so each redirect hop can rebuild the + // request (a reqwest RequestBuilder is single-use). + let mut timeout: Option = None; + let mut custom_headers: Vec<(String, String)> = Vec::new(); + let mut opts_cookies: Vec<(String, String)> = Vec::new(); + // Body, with the Content-Type it implies (None for a raw body). + let mut body: Option<(Vec, Option<&'static str>)> = None; + // Auth: (is_digest, username, password). + let mut auth: Option<(bool, String, String)> = None; + + if let Some(opts) = opts.as_ref() { + if let Some(t) = opts.get::>("timeout")? { + timeout = Some(Duration::try_from_secs_f64(t).map_err(|_| { + mlua::Error::external( + "http: timeout must be a non-negative finite number of seconds", + ) + })?); + } + if let Some(headers) = opts.get::>("headers")? { + for pair in headers.pairs::() { + custom_headers.push(pair?); + } + } + if let Some(cookies) = opts.get::>("cookies")? { + for pair in cookies.pairs::() { + opts_cookies.push(pair?); + } + } + + // Body: json > form > raw body (first one present wins). + if let Some(json_val) = opts.get::>("json")? { + let json = lua_to_json(json_val, 0)?; + let s = serde_json::to_string(&json).map_err(mlua::Error::external)?; + body = Some((s.into_bytes(), Some("application/json"))); + } else if let Some(form) = opts.get::>("form")? { + let mut pairs = Vec::new(); + for pair in form.pairs::() { + pairs.push(pair?); + } + body = Some(( + form_urlencode(&pairs).into_bytes(), + Some("application/x-www-form-urlencoded"), + )); + } else if let Some(b) = opts.get::>("body")? { + body = Some((b.as_bytes().to_vec(), None)); + } + + // Auth: { username, password, scheme = "basic" (default) | "digest" }. + if let Some(auth_tbl) = opts.get::>("auth")? { + let username = auth_tbl + .get::>("username")? + .ok_or_else(|| mlua::Error::external("http: auth.username is required"))?; + let password = auth_tbl + .get::>("password")? + .ok_or_else(|| mlua::Error::external("http: auth.password is required"))?; + let is_digest = match auth_tbl.get::>("scheme")?.as_deref() { + None | Some("basic") => false, + Some("digest") => true, + Some(other) => { + return Err(mlua::Error::external(format!( + "http: auth.scheme must be \"basic\" or \"digest\", got \"{other}\"" + ))); + } + }; + auth = Some((is_digest, username, password)); + } + } + + let is_digest = matches!(auth.as_ref(), Some((true, _, _))); + let mut digest_header: Option = None; + let mut digest_tried = false; + + let mut url = url; + let mut redirects_left = MAX_REDIRECTS; + + let resp = loop { + // Host used for cookie matching and as the Set-Cookie domain fallback; + // recomputed each hop since a redirect may cross hosts. + let host = reqwest::Url::parse(&url) + .ok() + .and_then(|u| u.host_str().map(|h| h.to_ascii_lowercase())); + + let mut req = client.request(method.clone(), &url); + + if let Some(t) = timeout { + req = req.timeout(t); + } + for (k, v) in &custom_headers { + req = req.header(k, v); + } + + // Cookie header: jar cookies for this host, then per-request cookies + // which override on name collision. + let mut cookie_map: HashMap = HashMap::new(); + if let (Some(jar), Some(host)) = (jar.as_ref(), host.as_ref()) { + for (n, v) in jar.lock().unwrap().cookies_for(host) { + cookie_map.insert(n, v); + } + } + for (k, v) in &opts_cookies { + cookie_map.insert(k.clone(), v.clone()); + } + if !cookie_map.is_empty() { + let header = cookie_map + .iter() + .map(|(k, v)| format!("{k}={v}")) + .collect::>() + .join("; "); + req = req.header(reqwest::header::COOKIE, header); + } + + if let Some((bytes, ct)) = body.as_ref() { + if let Some(ct) = ct { + req = req.header(reqwest::header::CONTENT_TYPE, *ct); + } + req = req.body(bytes.clone()); + } + + // Auth. Basic goes out on every hop; digest's Authorization is set only + // after the 401 challenge below has been answered (digest_header). + if let Some((digest, username, password)) = auth.as_ref() { + if *digest { + if let Some(h) = digest_header.as_ref() { + req = req.header(reqwest::header::AUTHORIZATION, h); + } + } else { + req = req.basic_auth(username, Some(password)); + } + } + + let resp = req + .send() + .await + .map_err(|e| mlua::Error::external(format!("http: {e}")))?; + let status = resp.status(); + + // Store Set-Cookie from this hop into the jar. + if let (Some(jar), Some(host)) = (jar.as_ref(), host.as_ref()) { + let mut j = jar.lock().unwrap(); + for value in resp.headers().get_all(reqwest::header::SET_COOKIE).iter() { + if let Ok(s) = value.to_str() { + j.set_from_header(host, s); + } + } + } + + // Digest auth: answer a 401 challenge once, then retry the same request + // with the computed Authorization header. + if is_digest && !digest_tried && status == reqwest::StatusCode::UNAUTHORIZED { + let challenge = resp + .headers() + .get(reqwest::header::WWW_AUTHENTICATE) + .and_then(|v| v.to_str().ok()) + .map(str::to_string); + if let Some((_, username, password)) = auth.as_ref() + && let Some(challenge) = challenge + && let Ok(mut prompt) = digest_auth::parse(&challenge) + { + let uri = reqwest::Url::parse(&url) + .map(|u| match u.query() { + Some(q) => format!("{}?{}", u.path(), q), + None => u.path().to_string(), + }) + .unwrap_or_else(|_| url.clone()); + let ctx = digest_auth::AuthContext::new_with_method( + username.as_str(), + password.as_str(), + uri, + body.as_ref().map(|(b, _)| b.as_slice()), + digest_auth::HttpMethod::from(method.as_str()), + ); + if let Ok(answer) = prompt.respond(&ctx) { + digest_header = Some(answer.to_header_string()); + digest_tried = true; + continue; + } + } + } + + // Follow a redirect if there is one to follow. + if status.is_redirection() + && redirects_left > 0 + && let Some(next) = resp + .headers() + .get(reqwest::header::LOCATION) + .and_then(|l| l.to_str().ok()) + .and_then(|loc| reqwest::Url::parse(&url).and_then(|base| base.join(loc)).ok()) + { + redirects_left -= 1; + // 303, and 301/302 on a POST, degrade to a bodyless GET — the + // behaviour browsers and reqwest's own redirect policy apply. + let code = status.as_u16(); + if code == 303 || ((code == 301 || code == 302) && method == reqwest::Method::POST) { + method = reqwest::Method::GET; + body = None; + } + url = next.to_string(); + continue; + } + + break resp; + }; + + let status = resp.status().as_u16(); + + // Response headers: lowercase names, first value wins. + let headers_tbl = lua.create_table()?; + for (name, value) in resp.headers().iter() { + let lname = name.as_str().to_ascii_lowercase(); + if !headers_tbl.contains_key(lname.as_str())? { + headers_tbl.raw_set(lname.as_str(), lua.create_string(value.as_bytes())?)?; + } + } + + let bytes = resp + .bytes() + .await + .map_err(|e| mlua::Error::external(format!("http: {e}")))?; + + let out = lua.create_table()?; + out.raw_set("status", status)?; + out.raw_set("ok", (200..=299).contains(&status))?; + out.raw_set("headers", headers_tbl)?; + out.raw_set("body", lua.create_string(&bytes)?)?; + Ok(out) +} + +// --------------------------------------------------------------------------- +// Session +// --------------------------------------------------------------------------- + +/// Build the plain Lua table that backs a session. State (the cookie jar) lives +/// in the closures; the Lua layer applies a metatable for the method shorthands. +/// Methods are called as `s:method(...)`, so each closure receives the session +/// table as a leading `_this` argument that it ignores. +fn make_session( + lua: &Lua, + client: reqwest::Client, + jar: Arc>, +) -> LuaResult { + let tbl = lua.create_table()?; + + tbl.raw_set( + "_request", + lua.create_async_function({ + let client = client.clone(); + let jar = jar.clone(); + move |lua, (_this, method, url, opts): (LuaTable, String, String, Option)| { + let client = client.clone(); + let jar = jar.clone(); + async move { + execute_request(lua, client, Some(jar), method, url, opts).await + } + } + })?, + )?; + + tbl.raw_set("save", { + let jar = jar.clone(); + lua.create_function(move |_, (_this, path): (LuaTable, String)| { + let jsonl = jar.lock().unwrap().to_jsonl(); + std::fs::write(&path, jsonl) + .map_err(|e| mlua::Error::external(format!("session:save: {e}"))) + })? + })?; + + tbl.raw_set("load", { + let jar = jar.clone(); + lua.create_function(move |_, (_this, path): (LuaTable, String)| { + let src = std::fs::read_to_string(&path) + .map_err(|e| mlua::Error::external(format!("session:load: {e}")))?; + jar.lock().unwrap().merge_jsonl(&src); + Ok(()) + })? + })?; + + tbl.raw_set("clearCookies", { + let jar = jar.clone(); + lua.create_function(move |_, _this: LuaTable| { + jar.lock().unwrap().cookies.clear(); + Ok(()) + })? + })?; + + tbl.raw_set("cookies", { + let jar = jar.clone(); + lua.create_function(move |lua, _this: LuaTable| { + let outer = lua.create_table()?; + let j = jar.lock().unwrap(); + for (domain, names) in j.cookies.iter() { + let inner = lua.create_table()?; + for (name, value) in names.iter() { + inner.raw_set(name.as_str(), value.as_str())?; + } + outer.raw_set(domain.as_str(), inner)?; + } + Ok(outer) + })? + })?; + + Ok(tbl) +} + +// --------------------------------------------------------------------------- +// Installation +// --------------------------------------------------------------------------- + +pub(super) fn install(lua: &Lua) -> LuaResult<()> { + // reqwest is built with `rustls-no-provider`, so it has no crypto provider + // of its own and panics ("No provider set") unless one is installed as the + // process default before the client is built. `ring` is self-contained + // (compiled in, no system OpenSSL). Idempotent across Lua states — only the + // first install in the process wins, and they would all install ring. + let _ = rustls::crypto::ring::default_provider().install_default(); + + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + // reqwest sends no User-Agent by default; some edges/CDNs reject + // empty-UA requests outright. A per-request `headers` entry overrides it. + .user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))) + // Redirects are followed manually in execute_request so Set-Cookie + // headers on 30x responses can be captured into the cookie jar. + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(mlua::Error::external)?; + + let http = lua.create_table()?; + + // Stateless http.request — no cookie jar. + http.raw_set( + "request", + lua.create_async_function({ + let client = client.clone(); + move |lua, (method, url, opts): (String, String, Option)| { + let client = client.clone(); + async move { execute_request(lua, client, None, method, url, opts).await } + } + })?, + )?; + + // http.session(path?) — constructor returning the raw session table. + http.raw_set( + "session", + lua.create_async_function({ + let client = client.clone(); + move |lua, path: Option| { + let client = client.clone(); + async move { + let mut jar = CookieJar::default(); + if let Some(p) = path { + let src = tokio::fs::read_to_string(&p) + .await + .map_err(|e| mlua::Error::external(format!("http.session: {e}")))?; + jar.merge_jsonl(&src); + } + make_session(&lua, client, Arc::new(Mutex::new(jar))) + } + } + })?, + )?; + + lua.globals().raw_set("http", http)?; + + // Lua side adds: resp.json(), method shorthands, getJSON/postJSON, and the + // session metatable wrapping http.session(). + lua.load(HTTP_LUA).set_name("@[stdlib/http]").exec() +} diff --git a/src/stdlib/mod.rs b/src/stdlib/mod.rs index c7a4402..60074dd 100644 --- a/src/stdlib/mod.rs +++ b/src/stdlib/mod.rs @@ -1,3 +1,4 @@ +mod http; mod logging; mod math; mod os_ext; @@ -12,5 +13,6 @@ pub(crate) fn install(lua: &mlua::Lua) -> mlua::Result<()> { os_ext::install(lua)?; logging::install(lua)?; sqlite::install(lua)?; + http::install(lua)?; Ok(()) } diff --git a/src/stdlib/utils.rs b/src/stdlib/utils.rs index ad9b13a..046d460 100644 --- a/src/stdlib/utils.rs +++ b/src/stdlib/utils.rs @@ -21,7 +21,7 @@ pub(crate) fn is_null(val: &LuaValue) -> bool { // Lua → JSON // --------------------------------------------------------------------------- -fn lua_to_json(val: LuaValue, depth: u32) -> LuaResult { +pub(crate) fn lua_to_json(val: LuaValue, depth: u32) -> LuaResult { const MAX_DEPTH: u32 = 64; if depth > MAX_DEPTH { return Err(mlua::Error::external("utils.toJSON: nesting too deep (max 64 levels)")); @@ -47,7 +47,7 @@ fn lua_to_json(val: LuaValue, depth: u32) -> LuaResult { } } -fn table_to_json(t: mlua::Table, depth: u32) -> LuaResult { +pub(crate) fn table_to_json(t: mlua::Table, depth: u32) -> LuaResult { let seq_len = t.raw_len() as usize; // Try array serialization: every key must be a sequential integer in 1..=seq_len.