From 28b49ecdfec57f56f483134dbd218cc6a80c7ca4 Mon Sep 17 00:00:00 2001 From: Max Countryman Date: Sun, 17 Sep 2023 09:51:22 -0700 Subject: [PATCH] wip --- Cargo.lock | 2032 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 48 +- README.md | 102 ++ examples/counter.rs | 49 + examples/redis-store.rs | 55 ++ examples/sqlite-store.rs | 51 + rustfmt.toml | 5 + src/extract.rs | 20 + src/lib.rs | 174 +++- src/memory_store.rs | 52 + src/redis_store.rs | 80 ++ src/service.rs | 432 ++++++++ src/session.rs | 575 +++++++++++ src/session_store.rs | 20 + src/sqlite_store.rs | 137 +++ 15 files changed, 3820 insertions(+), 12 deletions(-) create mode 100644 Cargo.lock create mode 100644 README.md create mode 100644 examples/counter.rs create mode 100644 examples/redis-store.rs create mode 100644 examples/sqlite-store.rs create mode 100644 rustfmt.toml create mode 100644 src/extract.rs create mode 100644 src/memory_store.rs create mode 100644 src/redis_store.rs create mode 100644 src/service.rs create mode 100644 src/session.rs create mode 100644 src/session_store.rs create mode 100644 src/sqlite_store.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1f9d40b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2032 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "arcstr" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f907281554a3d0312bb7aab855a8e0ef6cbf1614d06de54105039ca8b34460e" + +[[package]] +name = "async-trait" +version = "0.1.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +dependencies = [ + "serde", +] + +[[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 = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "bytes-utils" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie-factory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + +[[package]] +name = "crc16" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff" + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +dependencies = [ + "serde", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fred" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca2a979eaeb5d8a819edc193860ce54797730559464bc253cd3a2f765e58bd5" +dependencies = [ + "arc-swap", + "arcstr", + "async-trait", + "bytes", + "bytes-utils", + "cfg-if", + "float-cmp", + "futures", + "lazy_static", + "log", + "parking_lot", + "rand", + "redis-protocol", + "semver", + "serde_json", + "sha-1", + "tokio", + "tokio-stream", + "tokio-util", + "url", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "libc" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", + "serde", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "matchit" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" + +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest", +] + +[[package]] +name = "memchr" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redis-protocol" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c31deddf734dc0a39d3112e73490e88b61a05e83e074d211f348404cee4d2c6" +dependencies = [ + "bytes", + "bytes-utils", + "cookie-factory", + "crc16", + "log", + "nom", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rsa" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +dependencies = [ + "byteorder", + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e58421b6bc416714d5115a2ca953718f6c621a51b68e4f4922aea5a4391a721" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4cef4251aabbae751a3710927945901ee1d97ee96d757f6880ebb9a79bfd53" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "208e3165167afd7f3881b16c1ef3f2af69fa75980897aac8874a0696516d12c2" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a4a8336d278c62231d87f24e8a7a74898156e34c1c18942857be2acb29c7dfc" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482" +dependencies = [ + "atoi", + "base64", + "bitflags 2.4.0", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e" +dependencies = [ + "atoi", + "base64", + "bitflags 2.4.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4c21bf34c7cae5b283efb3ac1bcc7670df7561124dc2f8bdc0b59be40f79a2" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "tempfile" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "time" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.4", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-cookies" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40f38d941a2ffd8402b36e02ae407637a9caceb693aaf2edc910437db0f36984" +dependencies = [ + "async-trait", + "axum-core", + "cookie", + "futures-util", + "http", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tower-sessions" +version = "0.0.0" +dependencies = [ + "async-trait", + "axum", + "axum-core", + "fred", + "http", + "hyper", + "parking_lot", + "serde", + "serde_json", + "sqlx", + "thiserror", + "time", + "tokio", + "tower", + "tower-cookies", + "tower-layer", + "tower-service", + "uuid", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.33", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom", + "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/Cargo.toml b/Cargo.toml index 41b357b..9af68f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,54 @@ [package] name = "tower-sessions" -description = "Session manager middleware for tower." +description = "🥠 Cookie-based sessions as a `tower` middleware." version = "0.0.0" edition = "2021" license = "MIT" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["axum-core", "memory-store"] +memory-store = [] +redis-store = ["fred"] +sqlite-store = ["sqlx", "sqlx/sqlite"] [dependencies] +async-trait = "0.1" +http = "0.2" +parking_lot = { version = "0.12", features = ["nightly", "serde"] } +serde = { version = "1.0.188", features = ["derive"] } +serde_json = "1.0.107" +thiserror = "1.0.48" +time = { version = "0.3", features = ["serde"] } +tower-cookies = "0.9" +tower-layer = "0.3" +tower-service = "0.3" +uuid = { version = "1.4.1", features = ["v4", "serde"] } +axum-core = { optional = true, version = "0.3" } +fred = { optional = true, version = "6", features = ["serde-json"] } +sqlx = { optional = true, version = "0.7.1", features = [ + "time", + "uuid", + "runtime-tokio", +] } + +[dev-dependencies] +axum = "0.6" +hyper = "0.14" +tokio = { version = "1", features = ["full"] } +tower = "0.4" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[[example]] +name = "counter" +required-features = ["axum-core", "memory-store"] + +[[example]] +name = "redis-store" +required-features = ["axum-core", "redis-store"] + +[[example]] +name = "sqlite-store" +required-features = ["axum-core", "sqlite-store"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0529a6 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +

+tower-sessions +

+ +

+🥠 Cookie-based sessions as a `tower` middleware. +

+ +
+ + + + + + + + + +
+ +## 🎨 Overview + +This crate provides cookie-based sessions as a `tower` middleware. + +- Wraps `tower-cookies` for cookie management +- Decouples sessions from their storage (`SessionStore`) +- `Session` works as an extractor when using `axum` +- Redis and SQLx stores provided via feature flags +- Works directly with types that implement `Serialize` and `Deserialize` + +## 📦 Install + +To use the crate in your project, add the following to your `Cargo.toml` file: + +```toml +[dependencies] +tower-sessions = "0.0.0" +``` + +## 🤸 Usage + +### `axum` Example + +```rust +use std::net::SocketAddr; + +use axum::{ + error_handling::HandleErrorLayer, response::IntoResponse, routing::get, BoxError, Router, +}; +use http::StatusCode; +use serde::{Deserialize, Serialize}; +use tower::ServiceBuilder; +use tower_sessions::{time::Duration, MemoryStore, Session, SessionManagerLayer}; + +#[derive(Default, Deserialize, Serialize)] +struct Counter(usize); + +#[tokio::main] +async fn main() { + let session_store = MemoryStore::default(); + let session_service = ServiceBuilder::new() + .layer(HandleErrorLayer::new(|_: BoxError| async { + StatusCode::BAD_REQUEST + })) + .layer( + SessionManagerLayer::new(session_store) + .with_secure(false) + .with_max_age(Duration::seconds(10)), + ); + + let app = Router::new() + .route("/", get(handler)) + .layer(session_service); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +async fn handler(session: Session) -> impl IntoResponse { + let counter: Counter = session + .get("counter") + .expect("Could not deserialize.") + .unwrap_or_default(); + + session + .insert("counter", counter.0 + 1) + .expect("Could not serialize."); + + format!("Current count: {}", counter.0) +} +``` + +You can find this [example][counter-example] as well as other example projects in the [example directory][examples]. + +See the [crate documentation][docs] for more usage information. + +[counter-example]: https://github.com/maxcountryman/tower-sessions/tree/main/examples/counter.rs +[examples]: https://github.com/maxcountryman/tower-sessions/tree/main/examples +[docs]: https://docs.rs/tower-sessions diff --git a/examples/counter.rs b/examples/counter.rs new file mode 100644 index 0000000..d078660 --- /dev/null +++ b/examples/counter.rs @@ -0,0 +1,49 @@ +use std::net::SocketAddr; + +use axum::{ + error_handling::HandleErrorLayer, response::IntoResponse, routing::get, BoxError, Router, +}; +use http::StatusCode; +use serde::{Deserialize, Serialize}; +use tower::ServiceBuilder; +use tower_sessions::{time::Duration, MemoryStore, Session, SessionManagerLayer}; + +#[derive(Default, Deserialize, Serialize)] +struct Counter(usize); + +#[tokio::main] +async fn main() { + let session_store = MemoryStore::default(); + let session_service = ServiceBuilder::new() + .layer(HandleErrorLayer::new(|_: BoxError| async { + StatusCode::BAD_REQUEST + })) + .layer( + SessionManagerLayer::new(session_store) + .with_secure(false) + .with_max_age(Duration::seconds(10)), + ); + + let app = Router::new() + .route("/", get(handler)) + .layer(session_service); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +async fn handler(session: Session) -> impl IntoResponse { + let counter: Counter = session + .get("counter") + .expect("Could not deserialize.") + .unwrap_or_default(); + + session + .insert("counter", counter.0 + 1) + .expect("Could not serialize."); + + format!("Current count: {}", counter.0) +} diff --git a/examples/redis-store.rs b/examples/redis-store.rs new file mode 100644 index 0000000..e375d5c --- /dev/null +++ b/examples/redis-store.rs @@ -0,0 +1,55 @@ +use std::net::SocketAddr; + +use axum::{ + error_handling::HandleErrorLayer, response::IntoResponse, routing::get, BoxError, Router, +}; +use http::StatusCode; +use serde::{Deserialize, Serialize}; +use tower::ServiceBuilder; +use tower_sessions::{fred::prelude::*, time::Duration, RedisStore, Session, SessionManagerLayer}; + +#[derive(Serialize, Deserialize, Default)] +struct Counter(usize); + +#[tokio::main] +async fn main() { + let config = RedisConfig::from_url("redis://127.0.0.1:6379/1").unwrap(); + let client = RedisClient::new(config, None, None); + + let _ = client.connect(); + let _ = client.wait_for_connect().await.unwrap(); + + let session_store = RedisStore::new(client); + let session_service = ServiceBuilder::new() + .layer(HandleErrorLayer::new(|_: BoxError| async { + StatusCode::BAD_REQUEST + })) + .layer( + SessionManagerLayer::new(session_store) + .with_secure(false) + .with_max_age(Duration::seconds(10)), + ); + + let app = Router::new() + .route("/", get(handler)) + .layer(session_service); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +async fn handler(session: Session) -> impl IntoResponse { + let counter: Counter = session + .get("counter") + .expect("Could not deserialize.") + .unwrap_or_default(); + + session + .insert("counter", counter.0 + 1) + .expect("Could not serialize."); + + format!("Current count: {}", counter.0) +} diff --git a/examples/sqlite-store.rs b/examples/sqlite-store.rs new file mode 100644 index 0000000..d61f0f5 --- /dev/null +++ b/examples/sqlite-store.rs @@ -0,0 +1,51 @@ +use std::net::SocketAddr; + +use axum::{ + error_handling::HandleErrorLayer, response::IntoResponse, routing::get, BoxError, Router, +}; +use http::StatusCode; +use serde::{Deserialize, Serialize}; +use tower::ServiceBuilder; +use tower_sessions::{sqlx::SqlitePool, time::Duration, Session, SessionManagerLayer, SqliteStore}; + +#[derive(Serialize, Deserialize, Default)] +struct Counter(usize); + +#[tokio::main] +async fn main() { + let pool = SqlitePool::connect("sqlite::memory:").await.unwrap(); + let session_store = SqliteStore::new(pool); + session_store.migrate().await.unwrap(); + let session_service = ServiceBuilder::new() + .layer(HandleErrorLayer::new(|_: BoxError| async { + StatusCode::BAD_REQUEST + })) + .layer( + SessionManagerLayer::new(session_store) + .with_secure(false) + .with_max_age(Duration::seconds(10)), + ); + + let app = Router::new() + .route("/", get(handler)) + .layer(session_service); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +async fn handler(session: Session) -> impl IntoResponse { + let counter: Counter = session + .get("counter") + .expect("Could not deserialize.") + .unwrap_or_default(); + + session + .insert("counter", counter.0 + 1) + .expect("Could not serialize."); + + format!("Current count: {}", counter.0) +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..b0ba595 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,5 @@ +format_code_in_doc_comments = true +format_strings = true +imports_granularity = "Crate" +group_imports = "StdExternalCrate" +wrap_comments = true diff --git a/src/extract.rs b/src/extract.rs new file mode 100644 index 0000000..ee9d1aa --- /dev/null +++ b/src/extract.rs @@ -0,0 +1,20 @@ +use async_trait::async_trait; +use axum_core::extract::FromRequestParts; +use http::{request::Parts, StatusCode}; + +use crate::session::Session; + +#[async_trait] +impl FromRequestParts for Session +where + S: Sync + Send, +{ + type Rejection = (http::StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + parts.extensions.get::().cloned().ok_or(( + StatusCode::INTERNAL_SERVER_ERROR, + "Can't extract session. Is `SessionManagerLayer` enabled?", + )) + } +} diff --git a/src/lib.rs b/src/lib.rs index 7d12d9a..ba4a850 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,168 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} +//! # Overview +//! +//! This crate provides cookie-based sessions as a [`tower`] middleware. +//! +//! Session data is stored in a session store backend, which can be anything +//! that implements [`SessionStore`]. A pointer to this record is kept in the +//! cookie in the form of a UUID v4 identifier. +//! +//! A [`Session`] is provided as a request extension and applications may make +//! use of its interface by inserting, getting, and removing data associated +//! with a visitor. When using [`axum`] an extractor is provided, making +//! session retrieval in the route straightforward. +//! +//! ## Session Life Cycle +//! +//! Sessions are only saved when their internal state has been changed or their +//! life cycle has progressed, such as upon deletion or ID cycling. This +//! helps reduce unnecessary overhead. +//! +//! Further an expiration or no expiration may be provided. In the latter case, +//! the session will be treated as a "session cookie", meaning that the cookie +//! is meant to expire once the browser is closed. +//! +//! ## Backend Stores +//! +//! Stores persist the session's data. Many production use cases will require +//! this be a database of some kind. Redis and SQL stores are provided by +//! enabling the corresponding feature flags. +//! +//! However, custom stores may be implemented and indeed anything that +//! implements `SessionStore` may be used to house the backing session data. +//! +//! For testing, an in-memory store is also provided. Please note, this should +//! generally not be used in production applications. +//! +//! # Example +//! +//! This example demonstrates how you use the middleware with `axum`. +//! +//! ```rust,no_run +//! use std::net::SocketAddr; +//! +//! use axum::{ +//! error_handling::HandleErrorLayer, response::IntoResponse, routing::get, BoxError, Router, +//! }; +//! use http::StatusCode; +//! use serde::{Deserialize, Serialize}; +//! use tower::ServiceBuilder; +//! use tower_sessions::{time::Duration, MemoryStore, Session, SessionManagerLayer}; +//! +//! #[derive(Default, Deserialize, Serialize)] +//! struct Counter(usize); +//! +//! # #[cfg(feature = "axum-core")] +//! #[tokio::main] +//! async fn main() { +//! let session_store = MemoryStore::default(); +//! let session_service = ServiceBuilder::new() +//! .layer(HandleErrorLayer::new(|_: BoxError| async { +//! StatusCode::BAD_REQUEST +//! })) +//! .layer( +//! SessionManagerLayer::new(session_store) +//! .with_secure(false) +//! .with_max_age(Duration::seconds(10)), +//! ); +//! +//! let app = Router::new() +//! .route("/", get(handler)) +//! .layer(session_service); +//! +//! let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); +//! axum::Server::bind(&addr) +//! .serve(app.into_make_service()) +//! .await +//! .unwrap(); +//! } +//! # #[cfg(not(feature = "axum-core"))] +//! # fn main() {} +//! +//! async fn handler(session: Session) -> impl IntoResponse { +//! let counter: Counter = session +//! .get("counter") +//! .expect("Could not deserialize") +//! .unwrap_or_default(); +//! +//! session +//! .insert("counter", counter.0 + 1) +//! .expect("Could not serialize."); +//! +//! format!("Current count: {}", counter.0) +//! } +//! ``` +//! +//! [`tower`]: https://docs.rs/tower/latest/tower/ +//! [`axum`]: https://docs.rs/axum/latest/axum/ + +#![warn(clippy::all, missing_docs, nonstandard_style, future_incompatible)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +#[cfg(feature = "redis-store")] +pub use fred; +#[cfg(feature = "sqlite-store")] +pub use sqlx; +pub use time; +use time::Duration; +use tower_cookies::cookie::SameSite; + +#[cfg(feature = "memory-store")] +#[cfg_attr(docsrs, doc(cfg(feature = "memory-store")))] +pub use self::memory_store::MemoryStore; +#[cfg(feature = "redis-store")] +#[cfg_attr(docsrs, doc(cfg(feature = "redis-store")))] +pub use self::redis_store::RedisStore; +#[cfg(feature = "sqlite-store")] +#[cfg_attr(docsrs, doc(cfg(feature = "sqlite-store")))] +pub use self::sqlite_store::SqliteStore; +#[doc(inline)] +pub use self::{ + service::{SessionManager, SessionManagerLayer}, + session::Session, + session_store::SessionStore, +}; + +#[cfg(feature = "axum-core")] +#[cfg_attr(docsrs, doc(cfg(feature = "axum-core")))] +mod extract; -#[cfg(test)] -mod tests { - use super::*; +#[cfg(feature = "memory-store")] +#[cfg_attr(docsrs, doc(cfg(feature = "memory-store")))] +mod memory_store; + +#[cfg(feature = "redis-store")] +#[cfg_attr(docsrs, doc(cfg(feature = "redis-store")))] +mod redis_store; + +#[cfg(feature = "sqlite-store")] +#[cfg_attr(docsrs, doc(cfg(feature = "sqlite-store")))] +mod sqlite_store; + +pub mod service; +pub mod session; +pub mod session_store; + +/// Defines the configuration for the cookie belonging to the session. +#[derive(Debug, Clone)] +pub struct CookieConfig { + name: String, + same_site: SameSite, + max_age: Option, + secure: bool, + path: String, + domain: Option, +} - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); +impl Default for CookieConfig { + fn default() -> Self { + Self { + name: String::from("tower.sid"), + same_site: SameSite::Strict, + max_age: None, // TODO: Is `Max-Age: "Session"` the right default? + secure: false, + path: String::from("/"), + domain: None, + } } } diff --git a/src/memory_store.rs b/src/memory_store.rs new file mode 100644 index 0000000..fe201e0 --- /dev/null +++ b/src/memory_store.rs @@ -0,0 +1,52 @@ +use std::{collections::HashMap, sync::Arc}; + +use async_trait::async_trait; +use parking_lot::Mutex; +use serde_json::Value; + +use crate::{ + session::{SessionId, SessionRecord}, + Session, SessionStore, +}; + +/// An error type for `MemoryStore`. +#[derive(thiserror::Error, Debug)] +pub enum MemoryStoreError { + /// A variant to map `serde_json` errors. + #[error("JSON serialization/deserialization error: {0}")] + SerdeJsonError(#[from] serde_json::Error), +} + +/// A session store that lives only in memory. This is useful for testing but +/// not recommended for real applications. +#[derive(Clone, Default)] +pub struct MemoryStore(Arc>>); + +#[async_trait] +impl SessionStore for MemoryStore { + type Error = MemoryStoreError; + + async fn save(&self, session_record: &SessionRecord) -> Result<(), Self::Error> { + self.0.lock().insert( + session_record.id(), + serde_json::to_value(session_record).map_err(MemoryStoreError::from)?, + ); + Ok(()) + } + + async fn load(&self, session_id: &SessionId) -> Result, Self::Error> { + let session = if let Some(record_value) = self.0.lock().get(session_id) { + let session_record: SessionRecord = + serde_json::from_value(record_value.clone()).map_err(MemoryStoreError::from)?; + Some(session_record.into()) + } else { + None + }; + Ok(session) + } + + async fn delete(&self, session_id: &SessionId) -> Result<(), Self::Error> { + self.0.lock().remove(session_id); + Ok(()) + } +} diff --git a/src/redis_store.rs b/src/redis_store.rs new file mode 100644 index 0000000..d3a33ab --- /dev/null +++ b/src/redis_store.rs @@ -0,0 +1,80 @@ +use async_trait::async_trait; +use fred::prelude::*; +use time::OffsetDateTime; + +use crate::{ + session::{SessionId, SessionRecord}, + Session, SessionStore, +}; + +/// An error type for `RedisStore`. +#[derive(thiserror::Error, Debug)] +pub enum RedisStoreError { + /// A variant to map to `fred::error::RedisError` errors. + #[error("Redis error: {0}")] + RedisError(#[from] fred::error::RedisError), + + /// A variant to map `serde_json` errors. + #[error("JSON serialization/deserialization error: {0}")] + SerdeJsonError(#[from] serde_json::Error), +} + +/// A Redis session store. +#[derive(Clone, Default)] +pub struct RedisStore { + client: RedisClient, +} + +impl RedisStore { + /// Create a new Redis store with the provided client. + pub fn new(client: RedisClient) -> Self { + Self { client } + } +} + +#[async_trait] +impl SessionStore for RedisStore { + type Error = RedisStoreError; + + async fn save(&self, session_record: &SessionRecord) -> Result<(), Self::Error> { + let expiration = session_record + .expiration_time() + .map(OffsetDateTime::unix_timestamp) + .map(Expiration::EXAT); + + self.client + .set( + session_record.id().to_string(), + serde_json::to_string(&session_record)?, + expiration, + None, + false, + ) + .await?; + + Ok(()) + } + + async fn load(&self, session_id: &SessionId) -> Result, Self::Error> { + let record_value = self + .client + .get::(session_id.to_string()) + .await?; + + let session = match record_value { + serde_json::Value::Null => None, + + record_value => { + let session_record: SessionRecord = serde_json::from_value(record_value.clone())?; + Some(session_record.into()) + } + }; + + Ok(session) + } + + async fn delete(&self, session_id: &SessionId) -> Result<(), Self::Error> { + self.client.del(session_id.to_string()).await?; + Ok(()) + } +} diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 0000000..b0faedc --- /dev/null +++ b/src/service.rs @@ -0,0 +1,432 @@ +//! A middleware that provides [`Session`] as a request extension. +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use http::{Request, Response}; +use tower_cookies::{ + cookie::{time::Duration, SameSite}, + CookieManager, Cookies, +}; +use tower_layer::Layer; +use tower_service::Service; + +use crate::{ + session::{SessionDeletion, SessionId}, + CookieConfig, Session, SessionStore, +}; + +/// A middleware that provides [`Session`] as a request extension. +#[derive(Debug, Clone)] +pub struct SessionManager { + inner: S, + session_store: Store, + cookie_config: CookieConfig, +} + +impl SessionManager { + /// Create a new [`SessionManager`]. + pub fn new(inner: S, session_store: Store, cookie_config: CookieConfig) -> Self { + Self { + inner, + session_store, + cookie_config, + } + } +} + +impl Service> + for SessionManager +where + S: Service, Response = Response> + Clone + Send + 'static, + S::Error: Into> + 'static, + S::Future: Send, + ReqBody: Send + 'static, + ResBody: Send, +{ + type Response = S::Response; + type Error = Box; + type Future = Pin> + Send>>; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(Into::into) + } + + fn call(&mut self, mut req: Request) -> Self::Future { + let session_store = self.session_store.clone(); + let cookie_config = self.cookie_config.clone(); + + let clone = self.inner.clone(); + let mut inner = std::mem::replace(&mut self.inner, clone); + Box::pin(async move { + let cookies = req + .extensions() + .get::() + .cloned() + .expect("Something has gone wrong with tower-cookies."); + + let mut session = if let Some(session_cookie) = cookies.get(&cookie_config.name) { + // We do have a session cookie, so let's see if our store has the associated + // session. + // + // N.B.: Our store will *not* have the session if we've not put data in it yet. + let session_id = session_cookie.value().try_into()?; + session_store.load(&session_id).await? + } else { + // We don't have a session cookie, so let's create a new session. + Some((&cookie_config).into()) + } + .filter(Session::active) + .unwrap_or_else(|| { + // The session was expired so we create a new one here. + (&cookie_config).into() + }); + + req.extensions_mut().insert(session.clone()); + + + let res = Ok(inner.call(req).await.map_err(Into::into)?); + + if let Some(session_deletion) = session.deleted() { + match session_deletion { + SessionDeletion::Deleted => { + session_store.delete(&session.id()).await?; + cookies.remove(session.build_cookie(&cookie_config)); + + // Since the session has been deleted, there's no need for further + // processing. + return res; + } + + SessionDeletion::Cycled(deleted_id) => { + session_store.delete(&deleted_id).await?; + session.id = SessionId::default(); + } + } + }; + + // For further consideration: + // + // We only persist the session in the store when the `modified` flag is set. + // + // However, we could offer additional configuration of this behavior via an + // extended interface in the future. For instance, we might consider providing + // the `Set-Cookie` header whenever modified or if some "always save" marker is + // set. + if session.modified() { + let session_record = (&session).into(); + session_store.save(&session_record).await?; + cookies.add(session.build_cookie(&cookie_config)) + } + + res + }) + } +} + +/// A layer for providing [`Session`] as a request extension. +#[derive(Debug, Clone)] +pub struct SessionManagerLayer { + session_store: Store, + cookie_config: CookieConfig, +} + +impl SessionManagerLayer { + /// Configures the name of the cookie used for the session. + pub fn with_name(mut self, name: &str) -> Self { + self.cookie_config.name = name.to_string(); + self + } + + /// Configures the `"SameSite"` attribute of the cookie used for the + /// session. + pub fn with_same_site(mut self, same_site: SameSite) -> Self { + self.cookie_config.same_site = same_site; + self + } + + /// Configures the `"Max-Age"` attribute of the cookie used for the session. + pub fn with_max_age(mut self, max_age: Duration) -> Self { + self.cookie_config.max_age = Some(max_age); + self + } + + /// Configures the `"Secure"` attribute of the cookie used for the session. + pub fn with_secure(mut self, secure: bool) -> Self { + self.cookie_config.secure = secure; + self + } + + /// Configures the `"Path"` attribute of the cookie used for the session. + pub fn with_path(mut self, path: String) -> Self { + self.cookie_config.path = path; + self + } + + /// Configures the `"Domain"` attribute of the cookie used for the session. + pub fn with_domain(mut self, domain: String) -> Self { + self.cookie_config.domain = Some(domain); + self + } +} + +impl SessionManagerLayer { + /// Create a new [`SessionManagerLayer`] with the provided session store + /// and default cookie configuration. + pub fn new(session_store: Store) -> Self { + let cookie_config = CookieConfig::default(); + + Self { + session_store, + cookie_config, + } + } +} + +impl Layer for SessionManagerLayer { + type Service = CookieManager>; + + fn layer(&self, inner: S) -> Self::Service { + let session_manager = SessionManager { + inner, + session_store: self.session_store.clone(), + cookie_config: self.cookie_config.clone(), + }; + + CookieManager::new(session_manager) + } +} + +#[cfg(all(test, feature = "axum-core", feature = "memory-store"))] +mod tests { + use axum::{body::Body, error_handling::HandleErrorLayer, routing::get, Router}; + use axum_core::{body::BoxBody, BoxError}; + use http::{header, HeaderMap, Request, StatusCode}; + use time::Duration; + use tower::{ServiceBuilder, ServiceExt}; + use tower_cookies::{ + cookie::{self, SameSite}, + Cookie, + }; + + use crate::{MemoryStore, Session, SessionManagerLayer}; + + fn app(max_age: Option) -> Router { + let session_store = MemoryStore::default(); + let mut session_manager = SessionManagerLayer::new(session_store).with_secure(true); + if let Some(max_age) = max_age { + session_manager = session_manager.with_max_age(max_age); + } + let session_service = ServiceBuilder::new() + .layer(HandleErrorLayer::new(|_: BoxError| async { + StatusCode::BAD_REQUEST + })) + .layer(session_manager); + Router::new() + .route("/", get(|_: Session| async move { "Hello, world!" })) + .route( + "/insert", + get(|session: Session| async move { + session.insert("foo", 42).unwrap(); + }), + ) + .route( + "/get", + get(|session: Session| async move { + format!("{}", session.get::("foo").unwrap().unwrap()) + }), + ) + .route( + "/remove", + get(|session: Session| async move { + session.remove::("foo").unwrap(); + }), + ) + .route( + "/cycle_id", + get(|session: Session| async move { + session.cycle_id(); + }), + ) + .route( + "/delete", + get(|session: Session| async move { + session.delete(); + }), + ) + .layer(session_service) + } + + async fn body_string(body: BoxBody) -> String { + let bytes = hyper::body::to_bytes(body).await.unwrap(); + String::from_utf8_lossy(&bytes).into() + } + + fn get_session_cookie(headers: &HeaderMap) -> Result, cookie::ParseError> { + headers + .get_all(header::SET_COOKIE) + .iter() + .flat_map(|header| header.to_str()) + .next() + .ok_or(cookie::ParseError::MissingPair) + .and_then(Cookie::parse_encoded) + } + + #[tokio::test] + async fn no_session_set() { + let req = Request::builder().uri("/").body(Body::empty()).unwrap(); + let res = app(Some(Duration::hours(1))).oneshot(req).await.unwrap(); + + assert!(res + .headers() + .get_all(header::SET_COOKIE) + .iter() + .next() + .is_none()); + } + + #[tokio::test] + async fn bogus_session_cookie() { + let session_cookie = Cookie::new("tower.sid", "00000000-0000-0000-0000-000000000000"); + let req = Request::builder() + .uri("/insert") + .header(header::COOKIE, session_cookie.encoded().to_string()) + .body(Body::empty()) + .unwrap(); + let res = app(Some(Duration::hours(1))).oneshot(req).await.unwrap(); + let session_cookie = get_session_cookie(dbg!(res.headers())).unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + assert_ne!( + session_cookie.value(), + "00000000-0000-0000-0000-000000000000" + ); + } + + #[tokio::test] + async fn malformed_session_cookie() { + let session_cookie = Cookie::new("tower.sid", "malformed"); + let req = Request::builder() + .uri("/") + .header(header::COOKIE, session_cookie.encoded().to_string()) + .body(Body::empty()) + .unwrap(); + let res = app(Some(Duration::hours(1))).oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + } + + #[tokio::test] + async fn insert_session() { + let req = Request::builder() + .uri("/insert") + .body(Body::empty()) + .unwrap(); + let res = app(Some(Duration::hours(1))).oneshot(req).await.unwrap(); + let session_cookie = get_session_cookie(res.headers()).unwrap(); + + assert_eq!(session_cookie.name(), "tower.sid"); + assert_eq!(session_cookie.http_only(), Some(true)); + assert_eq!(session_cookie.same_site(), Some(SameSite::Strict)); + assert!(session_cookie + .max_age() + .is_some_and(|dt| dt <= Duration::hours(1))); + assert_eq!(session_cookie.secure(), Some(true)); + assert_eq!(session_cookie.path(), Some("/")); + } + + #[tokio::test] + async fn session_expiration() { + let req = Request::builder() + .uri("/insert") + .body(Body::empty()) + .unwrap(); + let res = app(None).oneshot(req).await.unwrap(); + let session_cookie = get_session_cookie(res.headers()).unwrap(); + + assert_eq!(session_cookie.name(), "tower.sid"); + assert_eq!(session_cookie.http_only(), Some(true)); + assert_eq!(session_cookie.same_site(), Some(SameSite::Strict)); + assert!(session_cookie.max_age().is_none()); + assert_eq!(session_cookie.secure(), Some(true)); + assert_eq!(session_cookie.path(), Some("/")); + } + + #[tokio::test] + async fn get_session() { + let app = app(Some(Duration::hours(1))); + + let req = Request::builder() + .uri("/insert") + .body(Body::empty()) + .unwrap(); + let res = app.clone().oneshot(req).await.unwrap(); + let session_cookie = get_session_cookie(res.headers()).unwrap(); + + let req = Request::builder() + .uri("/get") + .header(header::COOKIE, session_cookie.encoded().to_string()) + .body(Body::empty()) + .unwrap(); + let res = app.oneshot(req).await.unwrap(); + + assert_eq!(body_string(res.into_body()).await, "42"); + } + + #[tokio::test] + async fn cycle_session_id() { + let app = app(Some(Duration::hours(1))); + + let req = Request::builder() + .uri("/insert") + .body(Body::empty()) + .unwrap(); + let res = app.clone().oneshot(req).await.unwrap(); + let first_session_cookie = get_session_cookie(res.headers()).unwrap(); + + let req = Request::builder() + .uri("/cycle_id") + .header(header::COOKIE, first_session_cookie.encoded().to_string()) + .body(Body::empty()) + .unwrap(); + let res = app.clone().oneshot(req).await.unwrap(); + let second_session_cookie = get_session_cookie(res.headers()).unwrap(); + + let req = Request::builder() + .uri("/get") + .header(header::COOKIE, second_session_cookie.encoded().to_string()) + .body(Body::empty()) + .unwrap(); + let res = app.oneshot(req).await.unwrap(); + + assert_ne!(first_session_cookie.value(), second_session_cookie.value()); + assert_eq!(body_string(res.into_body()).await, "42"); + } + + #[tokio::test] + async fn delete_session() { + let app = app(Some(Duration::hours(1))); + + let req = Request::builder() + .uri("/insert") + .body(Body::empty()) + .unwrap(); + let res = app.clone().oneshot(req).await.unwrap(); + let session_cookie = get_session_cookie(res.headers()).unwrap(); + + let req = Request::builder() + .uri("/delete") + .header(header::COOKIE, session_cookie.encoded().to_string()) + .body(Body::empty()) + .unwrap(); + let res = app.oneshot(req).await.unwrap(); + + let session_cookie = get_session_cookie(res.headers()).unwrap(); + + assert_eq!(session_cookie.value(), ""); + assert_eq!(session_cookie.max_age(), Some(Duration::ZERO)); + } +} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..4823409 --- /dev/null +++ b/src/session.rs @@ -0,0 +1,575 @@ +//! A session which allows HTTP applications to associate data with visitors. +use std::{collections::HashMap, fmt::Display, sync::Arc}; + +use parking_lot::Mutex; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; +use time::Duration; +use tower_cookies::{cookie::time::OffsetDateTime, Cookie}; +use uuid::Uuid; + +use crate::CookieConfig; + +/// Session errors. +#[derive(thiserror::Error, Debug)] +pub enum SessionError { + /// A variant to map `uuid` errors. + #[error("Invalid UUID: {0}")] + InvalidUuid(#[from] uuid::Error), + + /// A variant to map `serde_json` errors. + #[error("JSON serialization/deserialization error: {0}")] + SerdeJsonError(#[from] serde_json::Error), +} + +type SessionResult = Result; + +/// A session which allows HTTP applications to associate data with visitors. +#[derive(Debug, Clone, Default)] +pub struct Session { + pub(crate) id: SessionId, + expiration_time: Option, + inner: Arc>, +} + +impl Session { + /// Create a new session with defaults. + /// + /// Note that an `expiration_time` of none results in a cookie with + /// expiration `"Session"`. + /// + /// # Examples + /// + ///```rust + /// use tower_sessions::Session; + /// let session = Session::new(); + /// ``` + pub fn new() -> Self { + let inner = Inner { + data: HashMap::new(), + modified: false, + deleted: None, + }; + + Self { + id: SessionId::default(), + expiration_time: None, + inner: Arc::new(Mutex::new(inner)), + } + } + + /// A method for setting `expiration_time` in accordance with `max_age`. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::{time::Duration, Session}; + /// let session = Session::new().with_max_age(Duration::minutes(5)); + /// ``` + pub fn with_max_age(mut self, max_age: Duration) -> Self { + let expiration_time = OffsetDateTime::now_utc().saturating_add(max_age); + self.expiration_time = Some(expiration_time); + self + } + + /// Inserts a `impl Serialize` value into the session. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::Session; + /// let session = Session::new(); + /// session.insert("foo", 42).expect("Serialization error."); + /// ``` + /// + /// # Errors + /// + /// This method can fail when [`serde_json::to_value`] fails. + pub fn insert(&self, key: &str, value: impl Serialize) -> SessionResult<()> { + self.insert_value(key, serde_json::to_value(&value)?); + Ok(()) + } + + /// Inserts a `serde_json::Value` into the session. + /// + /// If the key was not present in the underlying map, `None` is returned and + /// `modified` is set to `true`. + /// + /// If the underlying map did have the key and its value is the same as the + /// provided value, `None` is returned and `modified` is not set. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::Session; + /// let session = Session::new(); + /// let value = session.insert_value("foo", serde_json::json!(42)); + /// assert!(value.is_none()); + /// + /// let value = session.insert_value("foo", serde_json::json!(42)); + /// assert!(value.is_none()); + /// + /// let value = session.insert_value("foo", serde_json::json!("bar")); + /// assert_eq!(value, Some(serde_json::json!(42))); + /// ``` + pub fn insert_value(&self, key: &str, value: Value) -> Option { + let mut inner = self.inner.lock(); + if inner.data.get(key) != Some(&value) { + inner.modified = true; + inner.data.insert(key.to_string(), value) + } else { + None + } + } + + /// Gets a value from the store. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::Session; + /// let session = Session::new(); + /// session.insert("foo", 42).unwrap(); + /// let value = session.get::("foo").unwrap(); + /// assert_eq!(value, Some(42)); + /// ``` + /// + /// # Errors + /// + /// This method can fail when [`serde_json::from_value`] fails. + + pub fn get(&self, key: &str) -> SessionResult> { + Ok(self + .get_value(key) + .map(serde_json::from_value) + .transpose()?) + } + + /// Gets a `serde_json::Value` from the store. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::Session; + /// let session = Session::new(); + /// session.insert("foo", 42).unwrap(); + /// let value = session.get_value("foo").unwrap(); + /// assert_eq!(value, serde_json::json!(42)); + /// ``` + pub fn get_value(&self, key: &str) -> Option { + let inner = self.inner.lock(); + inner.data.get(key).cloned() + } + + /// Removes a value from the store, retuning the value of the key if it was + /// present in the underlying map. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::Session; + /// let session = Session::new(); + /// session.insert("foo", 42).unwrap(); + /// let value: Option = session.remove("foo").unwrap(); + /// assert_eq!(value, Some(42)); + /// let value: Option = session.get("foo").unwrap(); + /// assert!(value.is_none()); + /// ``` + /// + /// # Errors + /// + /// This method can fail when [`serde_json::from_value`] fails. + pub fn remove(&self, key: &str) -> SessionResult> { + Ok(self + .remove_value(key) + .map(serde_json::from_value) + .transpose()?) + } + + /// Removes a `serde_json::Value` from the store. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::Session; + /// let session = Session::new(); + /// session.insert("foo", 42).unwrap(); + /// let value = session.remove_value("foo").unwrap(); + /// assert_eq!(value, serde_json::json!(42)); + /// let value: Option = session.get("foo").unwrap(); + /// assert!(value.is_none()); + /// ``` + pub fn remove_value(&self, key: &str) -> Option { + let mut inner = self.inner.lock(); + if let Some(removed) = inner.data.remove(key) { + inner.modified = true; + Some(removed) + } else { + None + } + } + + /// Replaces a value in the session with a new value if the current value + /// matches the old value. + /// + /// If the key was not present in the underlying map or the current value + /// does not match, `false` is returned, indicating failure. + /// + /// If the key was present and its value matches the old value, the new + /// value is inserted, and `true` is returned, indicating success. + /// + /// This method is essential for scenarios where data races need to be + /// prevented. For instance, reading from and writing to a session is + /// not transactional. To ensure that read values are not stale, it's + /// crucial to use `replace_if_equal` when modifying the session. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::Session; + /// let session = Session::new(); + /// session.insert("foo", 42).unwrap(); + /// + /// let success = session.replace_if_equal("foo", 42, 43).unwrap(); + /// assert_eq!(success, true); + /// + /// let success = session.replace_if_equal("foo", 42, 44).unwrap(); + /// assert_eq!(success, false); + /// ``` + /// + /// # Errors + /// + /// This method can fail when [`serde_json::to_value`] fails. + pub fn replace_if_equal( + &self, + key: &str, + old_value: impl Serialize, + new_value: impl Serialize, + ) -> SessionResult { + let mut inner = self.inner.lock(); + match inner.data.get(key) { + Some(current_value) if serde_json::to_value(&old_value)? == *current_value => { + let new_value = serde_json::to_value(&new_value)?; + if *current_value == new_value { + inner.modified = true; + } + inner.data.insert(key.to_string(), new_value); + Ok(true) // Success, old value matched. + } + _ => Ok(false), // Failure, key doesn't exist or old value doesn't match. + } + } + + /// Clears the session data. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::Session; + /// let session = Session::new(); + /// session.insert("foo", 42).unwrap(); + /// session.clear(); + /// assert!(session.get_value("foo").is_none()); + /// ``` + pub fn clear(&self) { + let mut inner = self.inner.lock(); + inner.data.clear(); + } + + /// Sets `deleted` on the session to `SessionDeletion::Deleted`. + /// + /// Setting this flag indicates the session should be deleted from the + /// underlying store. + /// + /// This flag is consumed by a session management system to ensure session + /// life cycle progression. + /// + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::{session::SessionDeletion, Session}; + /// let session = Session::new(); + /// session.delete(); + /// assert!(matches!(session.deleted(), Some(SessionDeletion::Deleted))); + /// ``` + pub fn delete(&self) { + let mut inner = self.inner.lock(); + inner.deleted = Some(SessionDeletion::Deleted); + } + + /// Sets `deleted` on the session to `SessionDeletion::Cycled(self.id))`. + /// + /// Setting this flag indicates the session ID should be cycled while + /// retaining the session's data. + /// + /// This flag is consumed by a session management system to ensure session + /// life cycle progression. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::{session::SessionDeletion, Session}; + /// let session = Session::new(); + /// session.cycle_id(); + /// assert!(matches!( + /// session.deleted(), + /// Some(SessionDeletion::Cycled(cycled_id)) if cycled_id == session.id() + /// )); + /// ``` + pub fn cycle_id(&self) { + let mut inner = self.inner.lock(); + inner.deleted = Some(SessionDeletion::Cycled(self.id)); + inner.modified = true; + } + + /// Sets `deleted` on the session to `SessionDeletion::Deleted` and clears + /// the session data. + /// + /// This helps ensure that session data cannot be accessed beyond this + /// invocation. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::{session::SessionDeletion, Session}; + /// let session = Session::new(); + /// session.insert("foo", 42).unwrap(); + /// session.flush(); + /// assert!(session.get_value("foo").is_none()); + /// assert!(matches!(session.deleted(), Some(SessionDeletion::Deleted))); + /// ``` + pub fn flush(&self) { + self.clear(); + self.delete(); + } + + /// Get the session ID. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::Session; + /// let session = Session::new(); + /// session.id(); + /// ``` + pub fn id(&self) -> SessionId { + self.id + } + + /// Get the session expiration time. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::{ + /// time::{Duration, OffsetDateTime}, + /// Session, + /// }; + /// let session = Session::new().with_max_age(Duration::hours(1)); + /// assert!(session + /// .expiration_time() + /// .is_some_and(|et| et > OffsetDateTime::now_utc())); + /// ``` + pub fn expiration_time(&self) -> Option { + self.expiration_time + } + + /// Returns `true` if the session is active and `false` otherwise. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::{time::Duration, Session}; + /// let session = Session::new(); + /// assert!(session.active()); + /// + /// let session = Session::new().with_max_age(Duration::hours(1)); + /// assert!(session.active()); + /// + /// let session = Session::new().with_max_age(Duration::ZERO); + /// assert!(!session.active()); + /// ``` + pub fn active(&self) -> bool { + if let Some(expiration_time) = self.expiration_time { + expiration_time > OffsetDateTime::now_utc() + } else { + true + } + } + + /// Given a [`CookieConfig`], builds a `Cookie` from the session. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::{CookieConfig, Session}; + /// let session = Session::new(); + /// let cookie_config = CookieConfig::default(); + /// let cookie = session.build_cookie(&cookie_config); + /// assert_eq!(cookie.value(), session.id().to_string()); + /// ``` + pub fn build_cookie<'c>(&self, cookie_config: &CookieConfig) -> Cookie<'c> { + let mut cookie_builder = Cookie::build(cookie_config.name.clone(), self.id.to_string()) + .http_only(true) + .same_site(cookie_config.same_site) + .secure(cookie_config.secure) + .path(cookie_config.path.clone()); + + if let Some(max_age) = self + .expiration_time + .map(|dt| dt - OffsetDateTime::now_utc()) + { + cookie_builder = cookie_builder.max_age(max_age); + } + + if let Some(domain) = &cookie_config.domain { + cookie_builder = cookie_builder.domain(domain.clone()); + } + + cookie_builder.finish() + } + + /// Returns `true` if the session has been modified and `false` otherwise. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::Session; + /// let session = Session::new(); + /// assert!(!session.modified()); + /// session.insert("foo", 42); + /// assert!(session.modified()); + /// ``` + pub fn modified(&self) -> bool { + self.inner.lock().modified + } + + /// Returns `Some(SessionDeletion)` if one has been set and `None` + /// otherwise. + /// + /// # Examples + /// + /// ```rust + /// use tower_sessions::{session::SessionDeletion, Session}; + /// let session = Session::new(); + /// assert!(session.deleted().is_none()); + /// session.delete(); + /// assert!(matches!(session.deleted(), Some(SessionDeletion::Deleted))); + /// session.cycle_id(); + /// assert!(matches!( + /// session.deleted(), + /// Some(SessionDeletion::Cycled(_)) + /// )) + /// ``` + pub fn deleted(&self) -> Option { + self.inner.lock().deleted + } +} + +impl From for Session { + fn from( + SessionRecord { + id, + data, + expiration_time, + }: SessionRecord, + ) -> Self { + let inner = Inner { + data, + modified: false, + deleted: None, + }; + + Self { + id, + expiration_time, + inner: Arc::new(Mutex::new(inner)), + } + } +} + +impl From<&CookieConfig> for Session { + fn from(cookie_config: &CookieConfig) -> Self { + let mut session = Session::default(); + if let Some(max_age) = cookie_config.max_age { + session = session.with_max_age(max_age); + } + session + } +} + +#[derive(Debug, Default)] +struct Inner { + data: HashMap, + modified: bool, + deleted: Option, +} + +/// An ID type for sessions. +#[derive(Copy, Clone, Debug, Deserialize, Serialize, Eq, Hash, PartialEq)] +pub struct SessionId(Uuid); + +impl Default for SessionId { + fn default() -> Self { + Self(Uuid::new_v4()) + } +} + +impl Display for SessionId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0.as_hyphenated().to_string()) + } +} + +impl TryFrom<&str> for SessionId { + type Error = SessionError; + + fn try_from(value: &str) -> Result { + Ok(Self(Uuid::parse_str(value)?)) + } +} + +/// Session deletion, represented as an enumeration of possible deletion types. +#[derive(Debug, Copy, Clone)] +pub enum SessionDeletion { + /// This indicates the session has been completely removed from the store. + Deleted, + + /// This indicates that the provided session ID should be cycled but that + /// the session data should be retained in a new session. + Cycled(SessionId), +} + +/// A type that represents data to be persisted in a store for a session. +/// +/// Saving to and loading from a store utilizes `SessionRecord`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionRecord { + id: SessionId, + expiration_time: Option, + data: HashMap, +} + +impl SessionRecord { + /// Gets the session ID. + pub fn id(&self) -> SessionId { + self.id + } + + /// Gets the session expiration time. + pub fn expiration_time(&self) -> Option { + self.expiration_time + } +} + +impl From<&Session> for SessionRecord { + fn from(session: &Session) -> Self { + let session_guard = session.inner.lock(); + Self { + id: session.id, + expiration_time: session.expiration_time, + data: session_guard.data.clone(), + } + } +} diff --git a/src/session_store.rs b/src/session_store.rs new file mode 100644 index 0000000..45cfa87 --- /dev/null +++ b/src/session_store.rs @@ -0,0 +1,20 @@ +//! An arbitrary store which houses the session data. +use async_trait::async_trait; + +use crate::session::{Session, SessionId, SessionRecord}; + +/// An arbitrary store which houses the session data. +#[async_trait] +pub trait SessionStore: Clone + Send + Sync + 'static { + /// An error that occurs when interacting with the store. + type Error: std::error::Error + Send + Sync; + + /// A method for saving a session in a store. + async fn save(&self, session_record: &SessionRecord) -> Result<(), Self::Error>; + + /// A method for loading a session from a store. + async fn load(&self, session_id: &SessionId) -> Result, Self::Error>; + + /// A method for deleting a session from a store. + async fn delete(&self, session_id: &SessionId) -> Result<(), Self::Error>; +} diff --git a/src/sqlite_store.rs b/src/sqlite_store.rs new file mode 100644 index 0000000..281064c --- /dev/null +++ b/src/sqlite_store.rs @@ -0,0 +1,137 @@ +use async_trait::async_trait; +use sqlx::sqlite::SqlitePool; +use time::OffsetDateTime; + +use crate::{ + session::{SessionId, SessionRecord}, + Session, SessionStore, +}; + +/// An error type for `SqliteStore`. +#[derive(thiserror::Error, Debug)] +pub enum SqliteStoreError { + /// A variant to map `sqlite` errors. + #[error("SQLx error: {0}")] + SqlxError(#[from] sqlx::Error), + + /// A variant to map `serde_json` errors. + #[error("JSON serialization/deserialization error: {0}")] + SerdeJsonError(#[from] serde_json::Error), +} + +/// A SQLite session store. +#[derive(Clone, Debug)] +pub struct SqliteStore { + pool: SqlitePool, + table_name: String, +} + +impl SqliteStore { + /// Create a new SQLite store with the provided connection pool. + pub fn new(pool: SqlitePool) -> Self { + Self { + pool, + table_name: "tower_sessions".into(), + } + } + + /// Set the session table name with the provided name. + pub fn with_table_name(mut self, table_name: impl AsRef) -> Result { + let table_name = table_name.as_ref(); + if !is_valid_table_name(table_name) { + return Err(format!( + "Invalid table name '{}'. Table names must be alphanumeric and may contain \ + hyphens or underscores.", + table_name + )); + } + + self.table_name = table_name.to_owned(); + Ok(self) + } + + /// Migrate the session schema. + pub async fn migrate(&self) -> sqlx::Result<()> { + let query = format!( + r#" + create table if not exists {} + ( + id text primary key not null, + expiration_time integer null, + data text not null + ) + "#, + self.table_name + ); + sqlx::query(&query).execute(&self.pool).await?; + Ok(()) + } +} + +#[async_trait] +impl SessionStore for SqliteStore { + type Error = SqliteStoreError; + + async fn save(&self, session_record: &SessionRecord) -> Result<(), Self::Error> { + let query = format!( + r#" + insert into {} + (id, data, expiration_time) values (?, ?, ?) + on conflict(id) do update set + expiration_time = excluded.expiration_time, + data = excluded.data + "#, + self.table_name + ); + sqlx::query(&query) + .bind(&session_record.id().to_string()) + .bind(serde_json::to_string(&session_record)?) + .bind(session_record.expiration_time()) + .execute(&self.pool) + .await?; + + Ok(()) + } + + async fn load(&self, session_id: &SessionId) -> Result, Self::Error> { + let query = format!( + r#" + select data from {} + where id = ? and (expiration_time is null or expiration_time > ?) + "#, + self.table_name + ); + let record_value: Option = sqlx::query_scalar(&query) + .bind(session_id.to_string()) + .bind(OffsetDateTime::now_utc()) + .fetch_optional(&self.pool) + .await?; + + Ok(record_value + .map(|json| serde_json::from_str::(&json)) + .transpose()? + .map(Into::into)) + } + + async fn delete(&self, session_id: &SessionId) -> Result<(), Self::Error> { + let query = format!( + r#" + delete from {} where id = ? + "#, + self.table_name + ); + sqlx::query(&query) + .bind(&session_id.to_string()) + .execute(&self.pool) + .await?; + + Ok(()) + } +} + +fn is_valid_table_name(name: &str) -> bool { + !name.is_empty() + && name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') +}