├── src ├── tests.rs ├── redis.rs ├── conf.rs ├── context.rs ├── main.rs ├── redlimit.lua ├── redlimit_lua.rs ├── api.rs └── redlimit.rs ├── clippy.toml ├── .vscode └── settings.json ├── .gitignore ├── Makefile ├── .cargo └── config.toml ├── config ├── test.toml └── default.toml ├── .github └── workflows │ ├── ci.yml │ └── build.yml ├── Dockerfile ├── tests └── request.http ├── Cargo.toml ├── LICENSE ├── README.md └── Cargo.lock /src/tests.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.59" 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[rust]": { 3 | "editor.formatOnSave": true 4 | } 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | # Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | # Added by cargo 14 | 15 | /target 16 | /debug 17 | /.idea 18 | dump.rdb -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # options 2 | ignore_output = &> /dev/null 3 | 4 | .PHONY: run-dev test build docker 5 | 6 | run-dev: 7 | @CONFIG_FILE_PATH=./debug/config.toml cargo run 8 | 9 | test: 10 | @cargo test -- --nocapture 11 | 12 | build: 13 | @cargo build --target x86_64-unknown-linux-gnu --release 14 | @cargo build --target aarch64-unknown-linux-gnu --release 15 | 16 | docker: 17 | @docker buildx build --platform linux/amd64,linux/arm64 -t teambition/redlimit:latest . 18 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo" 3 | lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo" 4 | 5 | # testing 6 | ci-doctest-default = "test --workspace --doc --no-fail-fast -- --nocapture" 7 | ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture" 8 | 9 | # compile docs as docs.rs would 10 | # RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace 11 | -------------------------------------------------------------------------------- /config/test.toml: -------------------------------------------------------------------------------- 1 | env = "test" 2 | namespace = "TEST" 3 | 4 | [log] 5 | level = "info" # debug, info, warn, error 6 | 7 | [server] 8 | port = 8080 9 | cert_file = "" 10 | key_file = "" 11 | workers = 1 12 | 13 | [redis] 14 | host = "127.0.0.1" 15 | port = 6379 16 | username = "" 17 | password = "" 18 | max_connections = 10 19 | 20 | [job] 21 | interval = 1 # seconds 22 | 23 | [rules."*"] # default rule 24 | # 25 | limit = [10, 10000, 3, 1000] 26 | 27 | [rules."-"] # floor rule 28 | limit = [3, 10000, 1, 1000] 29 | 30 | [rules.core] 31 | limit = [100, 10000, 20, 1000] 32 | 33 | [rules.core.path] 34 | "GET /v1/file/list" = 2 # quantity, default to 1 if no matched 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ "main" ] 5 | pull_request: 6 | branches: [ "main" ] 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | services: 11 | # Label used to access the service container 12 | redis: 13 | image: redis:7.0.9-alpine 14 | options: >- 15 | --health-cmd "redis-cli ping" 16 | --health-interval 5s 17 | --health-timeout 3s 18 | --health-retries 5 19 | ports: 20 | # Maps port 6379 on service container to the host 21 | - 6379:6379 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Run clippy 25 | run: cargo clippy --all-targets --all-features 26 | - name: Run tests 27 | run: cargo test 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM --platform=$BUILDPLATFORM rust:latest AS builder 4 | 5 | WORKDIR /src 6 | COPY src ./src 7 | COPY config ./config 8 | COPY Cargo.toml Cargo.lock ./ 9 | RUN cargo build --release 10 | 11 | FROM --platform=$BUILDPLATFORM ubuntu:latest 12 | RUN ln -snf /usr/share/zoneinfo/$CONTAINER_TIMEZONE /etc/localtime && echo $CONTAINER_TIMEZONE > /etc/timezone 13 | RUN apt-get update \ 14 | && apt-get install -y bash curl ca-certificates tzdata locales \ 15 | && update-ca-certificates \ 16 | && rm -rf /var/lib/apt/lists/* \ 17 | && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 18 | ENV LANG en_US.utf8 19 | 20 | WORKDIR /app 21 | COPY --from=builder /src/config ./config 22 | COPY --from=builder /src/target/release/redlimit ./ 23 | ENTRYPOINT ["./redlimit"] 24 | -------------------------------------------------------------------------------- /tests/request.http: -------------------------------------------------------------------------------- 1 | # REST Client https://github.com/Huachao/vscode-restclient 2 | 3 | ### 4 | GET http://127.0.0.1:8080/version 5 | Accept: application/json 6 | 7 | ### 8 | GET http://127.0.0.1:8080/redlist 9 | Accept: application/json 10 | 11 | ### 12 | POST http://127.0.0.1:8080/redlist 13 | Content-Type: application/json 14 | 15 | { 16 | "user1": 50000, 17 | "user2": 120000, 18 | "ip3": 120000 19 | } 20 | 21 | ### 22 | GET http://127.0.0.1:8080/redrules 23 | Accept: application/json 24 | 25 | ### 26 | POST http://127.0.0.1:8080/redrules 27 | Content-Type: application/json 28 | 29 | { 30 | "scope": "core", 31 | "rules": { 32 | "GET /v1/file/list": [10, 10000], 33 | "GET /v2/file/list": [8, 20000] 34 | } 35 | } 36 | 37 | ### 38 | POST http://127.0.0.1:8080/limiting 39 | Content-Type: application/json 40 | 41 | { 42 | "scope": "core", 43 | "path": "POST /v1/file/list", 44 | "id": "user123" 45 | } 46 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redlimit" 3 | version = "0.2.10" 4 | edition = "2021" 5 | description = "A redis-based distributed rate limit HTTP service." 6 | publish = false 7 | repository = "https://github.com/teambition/redlimit" 8 | license-file = "LICENSE" 9 | keywords = ["ratelimit", "redis", "distributed"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | rustls = "0.20" 15 | rustls-pemfile = "1" 16 | actix-web = { version = "4", features = ["rustls"] } 17 | actix-utils = "3" 18 | futures-core = "0.3" 19 | tokio = { version = "1.27", features = ["full"] } 20 | tokio-util = "0.7" 21 | rustis = { version = "0.10", features = ["pool"] } 22 | serde = { version = "1", features = ["derive"] } 23 | serde_json = "1" 24 | log = { version = "0.4", features = ["kv_unstable_serde"] } 25 | bb8 = "0.8" 26 | async-trait = "0.1" 27 | config = { version = "0.13", features = ["toml"] } 28 | anyhow = "1" 29 | structured-logger = "0.5" 30 | 31 | [profile.release] 32 | lto = true 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Teambition 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | env: 7 | REGISTRY: ghcr.io 8 | IMAGE_NAME: ${{ github.repository }} 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | packages: write 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v2 20 | 21 | - name: Log in to alipan registry 22 | uses: docker/login-action@v2 23 | with: 24 | registry: ${{ secrets.CR_REGISTRY }} 25 | username: ${{ secrets.CR_USERNAME }} 26 | password: ${{ secrets.CR_PASSWORD }} 27 | 28 | - name: Log in to the Container registry 29 | uses: docker/login-action@v2 30 | with: 31 | registry: ${{ env.REGISTRY }} 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Extract metadata (tags, labels) for Docker 36 | id: meta 37 | uses: docker/metadata-action@v4 38 | with: 39 | images: | 40 | ${{ secrets.CR_REGISTRY }}/alipan/redlimit 41 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 42 | tags: | 43 | type=semver,pattern={{raw}} 44 | 45 | - name: Build and push Docker image 46 | uses: docker/build-push-action@v4 47 | with: 48 | context: . 49 | push: true 50 | platforms: linux/amd64,linux/arm64 51 | tags: ${{ steps.meta.outputs.tags }} 52 | labels: ${{ steps.meta.outputs.labels }} 53 | -------------------------------------------------------------------------------- /config/default.toml: -------------------------------------------------------------------------------- 1 | env = "development" 2 | # The prefix of redis key 3 | namespace = "RL" 4 | 5 | [log] 6 | # Log level: "trace", "debug", "info", "warn", "error" 7 | level = "info" 8 | 9 | [server] 10 | # The address to bind to. 11 | port = 8080 12 | # cert file path to enable https, example: "/etc/https/mydomain.crt" 13 | cert_file = "" 14 | # key file path to enable https, example: "/etc/https/mydomain.key" 15 | key_file = "" 16 | # The number of workers to start (per bind address). 17 | # By default, the number of available physical CPUs is used as the worker count. 18 | workers = 2 19 | 20 | [redis] 21 | # Redis server address 22 | host = "127.0.0.1" 23 | # Redis server port 24 | port = 6379 25 | # Redis server username 26 | username = "" 27 | # Redis server password 28 | password = "" 29 | # The maximum number of connections managed by the pool, should > 0. 30 | max_connections = 100 31 | 32 | [job] 33 | # The interval to sync redlimit dynamic rules from redis. 34 | interval = 3 # seconds 35 | 36 | # The default rule that will be used if no matched limiting "scope" found. 37 | [rules."*"] 38 | # , , , 39 | limit = [10, 10000, 3, 1000] 40 | 41 | # The floor rule that will be used if limiting "id" exists in redlist. 42 | [rules."-"] 43 | limit = [3, 10000, 1, 1000] 44 | 45 | # A rule for scope named "core". You can add more rules for other scopes. 46 | [rules.core] 47 | limit = [100, 10000, 50, 2000] 48 | 49 | # A list of "path" in scope "core". 50 | [rules.core.path] 51 | # A path named "GET /v1/file/list" in scope "core", it's quantity is 5, default to 1 if no "path" matched. 52 | # You can add more for scope "core". 53 | "GET /v1/file/list" = 5 54 | 55 | [rules.biz] 56 | limit = [100, 10000, 50, 2000] 57 | # default quantity is 1, but we can set it to other value (>= 1). 58 | quantity = 10 59 | 60 | [rules.biz.path] 61 | "GET /v1/app/info" = 1 62 | "GET /v2/app/info" = 3 -------------------------------------------------------------------------------- /src/redis.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use rustis::bb8::{CustomizeConnection, ErrorSink, Pool}; 3 | use rustis::client::{Config, PooledClientManager, ServerConfig}; 4 | use tokio::time::Duration; 5 | 6 | pub type RedisPool = Pool; 7 | 8 | pub async fn new(cfg: super::conf::Redis) -> Result { 9 | let config = Config { 10 | server: ServerConfig::Standalone { 11 | host: cfg.host, 12 | port: cfg.port, 13 | }, 14 | username: Some(cfg.username).filter(|s| !s.is_empty()), 15 | password: Some(cfg.password).filter(|s| !s.is_empty()), 16 | connect_timeout: Duration::from_secs(3), 17 | command_timeout: Duration::from_millis(100), 18 | keep_alive: Some(Duration::from_secs(600)), 19 | ..Config::default() 20 | }; 21 | 22 | let max_size = if cfg.max_connections > 0 { 23 | cfg.max_connections as u32 24 | } else { 25 | 10 26 | }; 27 | let min_idle = if max_size <= 10 { 1 } else { max_size / 10 }; 28 | 29 | let manager = PooledClientManager::new(config).unwrap(); 30 | RedisPool::builder() 31 | .max_size(max_size) 32 | .min_idle(Some(min_idle)) 33 | .max_lifetime(None) 34 | .idle_timeout(Some(Duration::from_secs(600))) 35 | .connection_timeout(Duration::from_secs(3)) 36 | .error_sink(Box::new(RedisMonitor {})) 37 | .connection_customizer(Box::new(RedisMonitor {})) 38 | .build(manager) 39 | .await 40 | } 41 | 42 | #[derive(Debug, Clone, Copy)] 43 | struct RedisMonitor; 44 | 45 | impl ErrorSink for RedisMonitor { 46 | fn sink(&self, error: E) { 47 | log::error!(target: "redis", "{}", error); 48 | } 49 | 50 | fn boxed_clone(&self) -> Box> { 51 | Box::new(*self) 52 | } 53 | } 54 | 55 | #[async_trait] 56 | impl CustomizeConnection for RedisMonitor { 57 | async fn on_acquire(&self, _connection: &mut C) -> Result<(), E> { 58 | log::info!(target: "redis", "connection acquired"); 59 | Ok(()) 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use rustis::resp; 66 | 67 | use super::{super::conf, *}; 68 | 69 | #[actix_web::test] 70 | async fn redis_pool_works() -> anyhow::Result<()> { 71 | let pool = new(conf::Redis { 72 | host: "127.0.0.1".to_string(), 73 | port: 6379, 74 | username: String::new(), 75 | password: String::new(), 76 | max_connections: 10, 77 | }) 78 | .await?; 79 | 80 | let data = pool.get().await?.send(resp::cmd("PING"), None).await?; 81 | assert_eq!("PONG", data.to::()?); 82 | 83 | Ok(()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/conf.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use config::{Config, ConfigError, File, FileFormat}; 4 | use serde::Deserialize; 5 | 6 | #[derive(Debug, Deserialize, Clone)] 7 | pub struct Log { 8 | pub level: String, 9 | } 10 | 11 | #[derive(Debug, Deserialize, Clone)] 12 | pub struct Server { 13 | pub port: u16, 14 | pub cert_file: String, 15 | pub key_file: String, 16 | pub workers: u16, 17 | } 18 | 19 | #[derive(Debug, Deserialize, Clone)] 20 | pub struct Redis { 21 | pub host: String, 22 | pub port: u16, 23 | pub username: String, 24 | pub password: String, 25 | pub max_connections: u16, 26 | } 27 | 28 | #[derive(Debug, Deserialize, Clone)] 29 | pub struct Job { 30 | pub interval: u64, 31 | } 32 | 33 | #[derive(Debug, Deserialize, Clone)] 34 | pub struct Rule { 35 | pub limit: Vec, 36 | 37 | #[serde(default)] 38 | pub quantity: u64, 39 | #[serde(default)] 40 | pub path: HashMap, 41 | } 42 | 43 | #[derive(Debug, Deserialize, Clone)] 44 | pub struct Conf { 45 | pub env: String, 46 | pub namespace: String, 47 | pub log: Log, 48 | pub server: Server, 49 | pub redis: Redis, 50 | pub job: Job, 51 | pub rules: HashMap, 52 | } 53 | 54 | impl Conf { 55 | pub fn new() -> Result { 56 | let file_name = 57 | std::env::var("CONFIG_FILE_PATH").unwrap_or_else(|_| "./config/default.toml".into()); 58 | Self::from(&file_name) 59 | } 60 | 61 | pub fn from(file_name: &str) -> Result { 62 | let builder = Config::builder().add_source(File::new(file_name, FileFormat::Toml)); 63 | builder.build()?.try_deserialize::() 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | use super::*; 70 | 71 | #[actix_web::test] 72 | async fn config_works() -> anyhow::Result<()> { 73 | let cfg = Conf::new()?; 74 | assert_eq!("development", cfg.env); 75 | assert_eq!("info", cfg.log.level); 76 | assert_eq!(8080, cfg.server.port); 77 | assert_eq!("127.0.0.1", cfg.redis.host); 78 | assert_eq!(6379, cfg.redis.port); 79 | assert_eq!(3, cfg.job.interval); 80 | 81 | let default_rules = cfg 82 | .rules 83 | .get("*") 84 | .ok_or(anyhow::Error::msg("'*' not exists"))?; 85 | assert_eq!(vec![10, 10000, 3, 1000], default_rules.limit); 86 | assert!(default_rules.path.is_empty()); 87 | 88 | let floor_rules = cfg 89 | .rules 90 | .get("-") 91 | .ok_or(anyhow::Error::msg("'-' not exists"))?; 92 | assert_eq!(vec![3, 10000, 1, 1000], floor_rules.limit); 93 | assert!(floor_rules.path.is_empty()); 94 | 95 | let core_rules = cfg 96 | .rules 97 | .get("core") 98 | .ok_or(anyhow::Error::msg("'core' not exists"))?; 99 | assert_eq!(vec![100, 10000, 50, 2000], core_rules.limit); 100 | assert_eq!(0, core_rules.quantity); 101 | assert_eq!( 102 | 5, 103 | core_rules.path.get("GET /v1/file/list").unwrap().to_owned() 104 | ); 105 | 106 | let biz_rules = cfg 107 | .rules 108 | .get("biz") 109 | .ok_or(anyhow::Error::msg("'biz' not exists"))?; 110 | assert_eq!(vec![100, 10000, 50, 2000], biz_rules.limit); 111 | assert_eq!(10, biz_rules.quantity); 112 | assert_eq!( 113 | 1, 114 | biz_rules.path.get("GET /v1/app/info").unwrap().to_owned() 115 | ); 116 | 117 | Ok(()) 118 | } 119 | 120 | #[actix_web::test] 121 | async fn config_from_env_works() -> anyhow::Result<()> { 122 | let cfg = Conf::from("./config/test.toml")?; 123 | assert_eq!("test", cfg.env); 124 | assert_eq!("info", cfg.log.level); 125 | 126 | Ok(()) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::{Ref, RefMut}, 3 | collections::HashMap, 4 | time::Instant, 5 | }; 6 | 7 | use actix_utils::future::{ready, Ready}; 8 | use actix_web::{ 9 | dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, 10 | error::ErrorInternalServerError, 11 | Error, HttpMessage, HttpRequest, 12 | }; 13 | use futures_core::future::LocalBoxFuture; 14 | use serde_json::Value; 15 | 16 | pub use structured_logger::unix_ms; 17 | 18 | pub struct ContextTransform; 19 | 20 | pub struct Context { 21 | pub unix_ms: u64, 22 | pub start: Instant, 23 | pub log: HashMap, 24 | } 25 | 26 | impl Context { 27 | pub fn new() -> Self { 28 | Context { 29 | unix_ms: unix_ms(), 30 | start: Instant::now(), 31 | log: HashMap::new(), 32 | } 33 | } 34 | } 35 | 36 | pub trait ContextExt { 37 | fn context(&self) -> Result, Error>; 38 | fn context_mut(&self) -> Result, Error>; 39 | } 40 | 41 | impl ContextExt for HttpRequest { 42 | fn context(&self) -> Result, Error> { 43 | if self.extensions().get::().is_none() { 44 | return Err(ErrorInternalServerError( 45 | "no context in http request extensions", 46 | )); 47 | } 48 | 49 | Ok(Ref::map(self.extensions(), |ext| ext.get().unwrap())) 50 | } 51 | 52 | fn context_mut(&self) -> Result, Error> { 53 | if self.extensions().get::().is_none() { 54 | return Err(ErrorInternalServerError( 55 | "no context in http request extensions", 56 | )); 57 | } 58 | 59 | Ok(RefMut::map(self.extensions_mut(), |ext| { 60 | ext.get_mut().unwrap() 61 | })) 62 | } 63 | } 64 | 65 | impl Transform for ContextTransform 66 | where 67 | S: Service, Error = Error>, 68 | S::Future: 'static, 69 | B: 'static, 70 | { 71 | type Response = ServiceResponse; 72 | type Error = Error; 73 | type InitError = (); 74 | type Transform = ContextMiddleware; 75 | type Future = Ready>; 76 | 77 | fn new_transform(&self, service: S) -> Self::Future { 78 | ready(Ok(ContextMiddleware { service })) 79 | } 80 | } 81 | 82 | pub struct ContextMiddleware { 83 | service: S, 84 | } 85 | 86 | impl Service for ContextMiddleware 87 | where 88 | S: Service, Error = Error>, 89 | S::Future: 'static, 90 | B: 'static, 91 | { 92 | type Response = ServiceResponse; 93 | type Error = Error; 94 | type Future = LocalBoxFuture<'static, Result>; 95 | 96 | forward_ready!(service); 97 | 98 | fn call(&self, req: ServiceRequest) -> Self::Future { 99 | let log_method = req.method().to_string(); 100 | let log_path = req.path().to_string(); 101 | let log_xid = req 102 | .headers() 103 | .get("x-request-id") 104 | .map_or("", |h| h.to_str().unwrap()) 105 | .to_string(); 106 | 107 | let ctx = Context::new(); 108 | req.request().extensions_mut().insert(ctx); 109 | let fut = self.service.call(req); 110 | Box::pin(async move { 111 | let res = fut.await?; 112 | { 113 | let ctx = res.request().context_mut().unwrap(); 114 | log::info!(target: "api", 115 | method = log_method, 116 | path = log_path, 117 | xid = log_xid, 118 | status = res.response().status().as_u16(), 119 | start = ctx.unix_ms, 120 | elapsed = ctx.start.elapsed().as_millis() as u64, 121 | kv = log::as_serde!(&ctx.log); 122 | "", 123 | ); 124 | } 125 | Ok(res) 126 | }) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::BufReader}; 2 | 3 | use actix_web::{web, App, HttpServer}; 4 | use rustls::{Certificate, PrivateKey, ServerConfig}; 5 | use rustls_pemfile::{certs, read_one, Item}; 6 | use structured_logger::{async_json::new_writer, Builder}; 7 | use tokio::{io, time::Duration}; 8 | 9 | mod api; 10 | mod conf; 11 | mod context; 12 | mod redis; 13 | mod redlimit; 14 | mod redlimit_lua; 15 | 16 | const APP_NAME: &str = env!("CARGO_PKG_NAME"); 17 | const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); 18 | 19 | #[tokio::main] 20 | async fn main() -> anyhow::Result<()> { 21 | let cfg = conf::Conf::new().unwrap_or_else(|err| panic!("config error: {}", err)); 22 | 23 | Builder::with_level(cfg.log.level.as_str()) 24 | .with_target_writer("api", new_writer(io::stdout())) 25 | .init(); 26 | 27 | log::debug!("{:?}", cfg); 28 | 29 | let pool = web::Data::new( 30 | redis::new(cfg.redis) 31 | .await 32 | .unwrap_or_else(|err| panic!("redis connection pool error: {}", err)), 33 | ); 34 | 35 | if let Err(err) = redlimit::init_redlimit_fn(pool.clone()).await { 36 | panic!("redis FUNCTION error: {}", err) 37 | } 38 | 39 | let redrules = web::Data::new(redlimit::RedRules::new(&cfg.namespace, &cfg.rules)); 40 | 41 | // background jobs relating to local, disposable tasks 42 | let (redlimit_sync_handle, cancel_redlimit_sync) = 43 | redlimit::init_redlimit_sync(pool.clone(), redrules.clone(), cfg.job.interval); 44 | 45 | let server = HttpServer::new(move || { 46 | App::new() 47 | .app_data(web::Data::new(api::AppInfo { 48 | name: APP_NAME.to_string(), 49 | version: APP_VERSION.to_string(), 50 | })) 51 | .app_data(pool.clone()) 52 | .app_data(redrules.clone()) 53 | .wrap(context::ContextTransform {}) 54 | .service(web::resource("/limiting").route(web::post().to(api::post_limiting))) 55 | .service( 56 | web::resource("/redlist") 57 | .route(web::get().to(api::get_redlist)) 58 | .route(web::post().to(api::post_redlist)), 59 | ) 60 | .service( 61 | web::resource("/redrules") 62 | .route(web::get().to(api::get_redrules)) 63 | .route(web::post().to(api::post_redrules)), 64 | ) 65 | .route("/version", web::get().to(api::version)) 66 | }) 67 | .workers(cfg.server.workers as usize) 68 | .keep_alive(Duration::from_secs(25)) 69 | .shutdown_timeout(10); 70 | 71 | log::info!("redlimit service start at 0.0.0.0:{}", cfg.server.port); 72 | let addr = ("0.0.0.0", cfg.server.port); 73 | if cfg.server.key_file.is_empty() || cfg.server.cert_file.is_empty() { 74 | server.bind(addr)?.run().await?; 75 | } else { 76 | let config = load_rustls_config(cfg.server); 77 | server.bind_rustls(addr, config)?.run().await?; 78 | } 79 | 80 | cancel_redlimit_sync.cancel(); 81 | redlimit_sync_handle.await.unwrap(); 82 | log::info!("redlimit service shutdown gracefully"); 83 | 84 | Ok(()) 85 | } 86 | 87 | fn load_rustls_config(cfg: conf::Server) -> rustls::ServerConfig { 88 | // init server config builder with safe defaults 89 | let config = ServerConfig::builder() 90 | .with_safe_defaults() 91 | .with_no_client_auth(); 92 | 93 | // load TLS key/cert files 94 | let cert_file = &mut BufReader::new( 95 | File::open(cfg.cert_file.as_str()).expect("cannot open certificate file"), 96 | ); 97 | let key_file = &mut BufReader::new( 98 | File::open(cfg.key_file.as_str()).expect("cannot open private key file"), 99 | ); 100 | 101 | // convert files to key/cert objects 102 | let cert_chain = certs(cert_file) 103 | .unwrap() 104 | .into_iter() 105 | .map(Certificate) 106 | .collect(); 107 | 108 | let key = match read_one(key_file).unwrap() { 109 | Some(Item::RSAKey(key)) => PrivateKey(key), 110 | Some(Item::PKCS8Key(key)) => PrivateKey(key), 111 | Some(Item::ECKey(key)) => PrivateKey(key), 112 | _ => panic!("cannot locate private key"), 113 | }; 114 | 115 | config 116 | .with_single_cert(cert_chain, key) 117 | .expect("cannot build rustls::ServerConfig") 118 | } 119 | -------------------------------------------------------------------------------- /src/redlimit.lua: -------------------------------------------------------------------------------- 1 | #!lua name=redlimit 2 | 3 | local function unix_ms() 4 | local now = redis.call('TIME') 5 | return tonumber(now[1]) * 1000 + math.floor(tonumber(now[2]) / 1000) 6 | end 7 | 8 | -- keys: 9 | -- args (should be well formed): [ ] 10 | -- return: [ or 0, or 0] 11 | local function limiting(keys, args) 12 | local quantity = tonumber(args[1]) or 1 13 | local max_count = tonumber(args[2]) or 0 14 | local period = tonumber(args[3]) or 0 15 | local max_burst = tonumber(args[4]) or 0 16 | local burst_period = tonumber(args[5]) or 1000 17 | 18 | local result = {quantity, 0} 19 | if quantity > max_count then 20 | result[2] = 1 21 | return result 22 | end 23 | 24 | local burst = 0 25 | local burst_at = 0 26 | local limit = redis.call('HMGET', keys[1], 'c', 'b', 't') 27 | -- field:c(count in period) 28 | -- field:b(burst in burst period) 29 | -- field:t(burst start time, millisecond) 30 | 31 | if limit[1] then 32 | result[1] = tonumber(limit[1]) + quantity 33 | 34 | if max_burst > 0 then 35 | local ts = unix_ms() 36 | burst = tonumber(limit[2]) + quantity 37 | burst_at = tonumber(limit[3]) 38 | if burst_at + burst_period <= ts then 39 | burst = quantity 40 | burst_at = ts 41 | elseif burst > max_burst then 42 | result[1] = result[1] - quantity 43 | result[2] = burst_at + burst_period - ts 44 | return result 45 | end 46 | end 47 | 48 | if result[1] > max_count then 49 | result[1] = result[1] - quantity 50 | result[2] = redis.call('PTTL', keys[1]) 51 | 52 | if result[2] <= 0 then 53 | result[2] = 1 54 | redis.call('DEL', keys[1]) 55 | end 56 | elseif max_burst > 0 then 57 | redis.call('HSET', keys[1], 'c', result[1], 'b', burst, 't', burst_at) 58 | else 59 | redis.call('HSET', keys[1], 'c', result[1]) 60 | end 61 | 62 | else 63 | if max_burst > 0 then 64 | burst = quantity 65 | burst_at = unix_ms() 66 | end 67 | 68 | redis.call('HSET', keys[1], 'c', quantity, 'b', burst, 't', burst_at) 69 | redis.call('PEXPIRE', keys[1], period) 70 | end 71 | 72 | return result 73 | end 74 | 75 | -- keys: 76 | -- args: [ ...] 77 | -- return: integer or error 78 | local function redlist_add(keys, args) 79 | local cursor_key = keys[1] .. ':LC' 80 | local ttl_key = keys[1] .. ':LT' 81 | local ts = unix_ms() 82 | local members = redis.call('ZRANGE', ttl_key, '-inf', '(' .. ts, 'BYSCORE') 83 | if #members > 0 then 84 | redis.call('ZREM', ttl_key, unpack(members)) 85 | redis.call('ZREM', cursor_key, unpack(members)) 86 | end 87 | 88 | if #args == 0 then 89 | return 0 90 | end 91 | 92 | local cursor_members = {} 93 | local ttl_members = {} 94 | for i = 1, #args, 2 do 95 | cursor_members[i] = ts + i 96 | cursor_members[i + 1] = args[i] 97 | ttl_members[i] = ts + (tonumber(args[i + 1]) or 1000) 98 | ttl_members[i + 1] = args[i] 99 | end 100 | 101 | redis.call('ZADD', ttl_key, unpack(ttl_members)) 102 | return redis.call('ZADD', cursor_key, unpack(cursor_members)) 103 | end 104 | 105 | -- keys: 106 | -- args: 107 | -- return: [, , , , ...] or error 108 | local function redlist_scan(keys, args) 109 | local cursor_key = keys[1] .. ':LC' 110 | local ttl_key = keys[1] .. ':LT' 111 | local cursor = tonumber(args[1]) or 0 112 | 113 | local res = {} 114 | local members = redis.call('ZRANGE', cursor_key, cursor, 'inf', 'BYSCORE', 'LIMIT', 0, 10000) 115 | if #members > 0 then 116 | local ttls = redis.call('ZMSCORE', ttl_key, unpack(members)) 117 | table.insert(res, redis.call('ZSCORE', cursor_key, members[#members])) 118 | for i = 1, #members, 1 do 119 | table.insert(res, members[i]) 120 | table.insert(res, ttls[i] or '0') 121 | end 122 | end 123 | return res 124 | end 125 | 126 | -- keys: 127 | -- args: 128 | -- return: integer or error 129 | local function redrules_add(keys, args) 130 | local ttl_key = keys[1] .. ':RT' 131 | local data_key = keys[1] .. ':RD' 132 | local ts = unix_ms() 133 | local members = redis.call('ZRANGE', ttl_key, '-inf', '(' .. ts, 'BYSCORE') 134 | if #members > 0 then 135 | redis.call('HDEL', data_key, unpack(members)) 136 | redis.call('ZREM', ttl_key, unpack(members)) 137 | end 138 | 139 | if #args == 0 then 140 | return 0 141 | end 142 | 143 | local id = args[1] .. ':' .. args[2] 144 | local quantity = tonumber(args[3]) or 1 145 | local ttl = ts + (tonumber(args[4]) or 1000) 146 | redis.call('ZADD', ttl_key, ttl, id) 147 | return redis.call('HSET', data_key, id, cjson.encode({args[1], args[2], quantity, ttl})) 148 | end 149 | 150 | -- keys: 151 | -- return: array or error 152 | local function redrules_all(keys, args) 153 | local data_key = keys[1] .. ':RD' 154 | return redis.call('HVALS', data_key) 155 | end 156 | 157 | redis.register_function('limiting', limiting) 158 | redis.register_function('redlist_add', redlist_add) 159 | redis.register_function('redlist_scan', redlist_scan) 160 | redis.register_function('redrules_add', redrules_add) 161 | redis.register_function('redrules_all', redrules_all) 162 | -------------------------------------------------------------------------------- /src/redlimit_lua.rs: -------------------------------------------------------------------------------- 1 | pub static REDLIMIT: &str = r#"#!lua name=redlimit 2 | 3 | local function unix_ms() 4 | local now = redis.call('TIME') 5 | return tonumber(now[1]) * 1000 + math.floor(tonumber(now[2]) / 1000) 6 | end 7 | 8 | -- keys: 9 | -- args (should be well formed): [ ] 10 | -- return: [ or 0, or 0] 11 | local function limiting(keys, args) 12 | local quantity = tonumber(args[1]) or 1 13 | local max_count = tonumber(args[2]) or 0 14 | local period = tonumber(args[3]) or 0 15 | local max_burst = tonumber(args[4]) or 0 16 | local burst_period = tonumber(args[5]) or 1000 17 | 18 | local result = {quantity, 0} 19 | if quantity > max_count then 20 | result[2] = 1 21 | return result 22 | end 23 | 24 | local burst = 0 25 | local burst_at = 0 26 | local limit = redis.call('HMGET', keys[1], 'c', 'b', 't') 27 | -- field:c(count in period) 28 | -- field:b(burst in burst period) 29 | -- field:t(burst start time, millisecond) 30 | 31 | if limit[1] then 32 | result[1] = tonumber(limit[1]) + quantity 33 | 34 | if max_burst > 0 then 35 | local ts = unix_ms() 36 | burst = tonumber(limit[2]) + quantity 37 | burst_at = tonumber(limit[3]) 38 | if burst_at + burst_period <= ts then 39 | burst = quantity 40 | burst_at = ts 41 | elseif burst > max_burst then 42 | result[1] = result[1] - quantity 43 | result[2] = burst_at + burst_period - ts 44 | return result 45 | end 46 | end 47 | 48 | if result[1] > max_count then 49 | result[1] = result[1] - quantity 50 | result[2] = redis.call('PTTL', keys[1]) 51 | 52 | if result[2] <= 0 then 53 | result[2] = 1 54 | redis.call('DEL', keys[1]) 55 | end 56 | elseif max_burst > 0 then 57 | redis.call('HSET', keys[1], 'c', result[1], 'b', burst, 't', burst_at) 58 | else 59 | redis.call('HSET', keys[1], 'c', result[1]) 60 | end 61 | 62 | else 63 | if max_burst > 0 then 64 | burst = quantity 65 | burst_at = unix_ms() 66 | end 67 | 68 | redis.call('HSET', keys[1], 'c', quantity, 'b', burst, 't', burst_at) 69 | redis.call('PEXPIRE', keys[1], period) 70 | end 71 | 72 | return result 73 | end 74 | 75 | -- keys: 76 | -- args: [ ...] 77 | -- return: integer or error 78 | local function redlist_add(keys, args) 79 | local cursor_key = keys[1] .. ':LC' 80 | local ttl_key = keys[1] .. ':LT' 81 | local ts = unix_ms() 82 | local members = redis.call('ZRANGE', ttl_key, '-inf', '(' .. ts, 'BYSCORE') 83 | if #members > 0 then 84 | redis.call('ZREM', ttl_key, unpack(members)) 85 | redis.call('ZREM', cursor_key, unpack(members)) 86 | end 87 | 88 | if #args == 0 then 89 | return 0 90 | end 91 | 92 | local cursor_members = {} 93 | local ttl_members = {} 94 | for i = 1, #args, 2 do 95 | cursor_members[i] = ts + i 96 | cursor_members[i + 1] = args[i] 97 | ttl_members[i] = ts + (tonumber(args[i + 1]) or 1000) 98 | ttl_members[i + 1] = args[i] 99 | end 100 | 101 | redis.call('ZADD', ttl_key, unpack(ttl_members)) 102 | return redis.call('ZADD', cursor_key, unpack(cursor_members)) 103 | end 104 | 105 | -- keys: 106 | -- args: 107 | -- return: [, , , , ...] or error 108 | local function redlist_scan(keys, args) 109 | local cursor_key = keys[1] .. ':LC' 110 | local ttl_key = keys[1] .. ':LT' 111 | local cursor = tonumber(args[1]) or 0 112 | 113 | local res = {} 114 | local members = redis.call('ZRANGE', cursor_key, cursor, 'inf', 'BYSCORE', 'LIMIT', 0, 10000) 115 | if #members > 0 then 116 | local ttls = redis.call('ZMSCORE', ttl_key, unpack(members)) 117 | table.insert(res, redis.call('ZSCORE', cursor_key, members[#members])) 118 | for i = 1, #members, 1 do 119 | table.insert(res, members[i]) 120 | table.insert(res, ttls[i] or '0') 121 | end 122 | end 123 | return res 124 | end 125 | 126 | -- keys: 127 | -- args: 128 | -- return: integer or error 129 | local function redrules_add(keys, args) 130 | local ttl_key = keys[1] .. ':RT' 131 | local data_key = keys[1] .. ':RD' 132 | local ts = unix_ms() 133 | local members = redis.call('ZRANGE', ttl_key, '-inf', '(' .. ts, 'BYSCORE') 134 | if #members > 0 then 135 | redis.call('HDEL', data_key, unpack(members)) 136 | redis.call('ZREM', ttl_key, unpack(members)) 137 | end 138 | 139 | if #args == 0 then 140 | return 0 141 | end 142 | 143 | local id = args[1] .. ':' .. args[2] 144 | local quantity = tonumber(args[3]) or 1 145 | local ttl = ts + (tonumber(args[4]) or 1000) 146 | redis.call('ZADD', ttl_key, ttl, id) 147 | return redis.call('HSET', data_key, id, cjson.encode({args[1], args[2], quantity, ttl})) 148 | end 149 | 150 | -- keys: 151 | -- return: array or error 152 | local function redrules_all(keys, args) 153 | local data_key = keys[1] .. ':RD' 154 | return redis.call('HVALS', data_key) 155 | end 156 | 157 | redis.register_function('limiting', limiting) 158 | redis.register_function('redlist_add', redlist_add) 159 | redis.register_function('redlist_scan', redlist_scan) 160 | redis.register_function('redrules_add', redrules_add) 161 | redis.register_function('redrules_all', redrules_all) 162 | 163 | "#; 164 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use actix_web::{http::StatusCode, web, Error, HttpRequest, HttpResponse}; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::{json, to_value, Value}; 6 | use tokio::time::{timeout, Duration}; 7 | 8 | use crate::{context::ContextExt, redis::RedisPool, redlimit, redlimit::RedRules}; 9 | 10 | #[derive(Serialize, Deserialize)] 11 | pub struct AppInfo { 12 | pub name: String, 13 | pub version: String, 14 | } 15 | 16 | pub async fn version( 17 | req: HttpRequest, 18 | info: web::Data, 19 | pool: web::Data, 20 | ) -> Result { 21 | let state = pool.state(); 22 | let mut ctx = req.context_mut().unwrap(); 23 | ctx.log 24 | .insert("connections".to_string(), Value::from(state.connections)); 25 | ctx.log.insert( 26 | "idle_connections".to_string(), 27 | Value::from(state.idle_connections), 28 | ); 29 | respond_result(info) 30 | } 31 | 32 | #[derive(Deserialize)] 33 | pub struct LimitRequest { 34 | scope: String, 35 | path: String, 36 | id: String, 37 | } 38 | 39 | #[derive(Serialize)] 40 | pub struct LimitResponse { 41 | limit: u64, // x-ratelimit-limit 42 | remaining: u64, // x-ratelimit-remaining 43 | reset: u64, // x-ratelimit-reset 44 | retry: u64, // retry-after delay-milliseconds 45 | } 46 | 47 | pub async fn post_limiting( 48 | req: HttpRequest, 49 | pool: web::Data, 50 | rules: web::Data, 51 | input: web::Json, 52 | ) -> Result { 53 | let input = input.into_inner(); 54 | let ts = req.context()?.unix_ms; 55 | let args = rules 56 | .limit_args(ts, &input.scope, &input.path, &input.id) 57 | .await; 58 | let limit = args.1; 59 | 60 | let rt = if pool.state().connections > 0 { 61 | match timeout( 62 | Duration::from_millis(100), 63 | redlimit::limiting(pool, &rules.ns.limiting_key(&input.scope, &input.id), args), 64 | ) 65 | .await 66 | { 67 | Ok(rt) => rt, 68 | Err(_) => Err(anyhow::Error::msg("limiting timeout".to_string())), 69 | } 70 | } else { 71 | Err(anyhow::Error::msg("no redis connection".to_string())) 72 | }; 73 | 74 | let rt = match rt { 75 | Ok(rt) => rt, 76 | Err(err) => { 77 | log::warn!("post_limiting error: {}", err); 78 | redlimit::LimitResult(0, 0) 79 | } 80 | }; 81 | 82 | let mut ctx = req.context_mut()?; 83 | ctx.log 84 | .insert("scope".to_string(), Value::from(input.scope)); 85 | ctx.log.insert("path".to_string(), Value::from(input.path)); 86 | ctx.log.insert("id".to_string(), Value::from(input.id)); 87 | ctx.log.insert("count".to_string(), Value::from(rt.0)); 88 | ctx.log 89 | .insert("bursted".to_string(), Value::from(rt.0 < limit && rt.1 > 0)); 90 | ctx.log.insert("limited".to_string(), Value::from(rt.1 > 0)); 91 | 92 | respond_result(LimitResponse { 93 | limit, 94 | remaining: if limit > rt.0 { limit - rt.0 } else { 0 }, 95 | reset: if rt.1 > 0 { (ts + rt.1) / 1000 } else { 0 }, 96 | retry: rt.1, 97 | }) 98 | } 99 | 100 | pub async fn get_redlist( 101 | req: HttpRequest, 102 | rules: web::Data, 103 | ) -> Result { 104 | let ts = req.context()?.unix_ms; 105 | let rt = rules.redlist(ts).await; 106 | respond_result(rt) 107 | } 108 | 109 | pub async fn post_redlist( 110 | pool: web::Data, 111 | rules: web::Data, 112 | input: web::Json>, 113 | ) -> Result { 114 | if let Err(err) = redlimit::redlist_add(pool, rules.ns.as_str(), &input.into_inner()).await { 115 | log::error!("redlist_add error: {}", err); 116 | return respond_error(500, err.to_string()); 117 | } 118 | 119 | respond_result("ok") 120 | } 121 | 122 | pub async fn get_redrules( 123 | req: HttpRequest, 124 | rules: web::Data, 125 | ) -> Result { 126 | let ts = req.context()?.unix_ms; 127 | let rt = rules.redrules(ts).await; 128 | respond_result(rt) 129 | } 130 | 131 | #[derive(Deserialize)] 132 | pub struct RedRulesRequest { 133 | scope: String, 134 | rules: HashMap, 135 | } 136 | 137 | pub async fn post_redrules( 138 | pool: web::Data, 139 | rules: web::Data, 140 | input: web::Json, 141 | ) -> Result { 142 | let input = input.into_inner(); 143 | if let Err(err) = 144 | redlimit::redrules_add(pool, rules.ns.as_str(), &input.scope, &input.rules).await 145 | { 146 | log::error!("redlist_add error: {}", err); 147 | return respond_error(500, err.to_string()); 148 | } 149 | 150 | respond_result("ok") 151 | } 152 | 153 | fn respond_result(result: impl serde::ser::Serialize) -> Result { 154 | match to_value(result) { 155 | Ok(result) => Ok(HttpResponse::Ok() 156 | .content_type("application/json") 157 | .json(json!({ "result": result }))), 158 | Err(err) => respond_error(500, err.to_string()), 159 | } 160 | } 161 | 162 | fn respond_error(code: u16, err_msg: String) -> Result { 163 | let err_json = json!({ "error": {"code": code, "message": err_msg }}); 164 | Ok(HttpResponse::build(StatusCode::from_u16(code).unwrap()) 165 | .content_type("application/json") 166 | .json(err_json)) 167 | } 168 | 169 | #[cfg(test)] 170 | mod tests { 171 | use super::*; 172 | use actix_web::{http::header::ContentType, test, App}; 173 | 174 | const APP_NAME: &str = env!("CARGO_PKG_NAME"); 175 | const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); 176 | 177 | #[actix_web::test] 178 | async fn get_version_works() -> anyhow::Result<()> { 179 | let cfg = super::super::conf::Conf::new()?; 180 | let pool = web::Data::new(super::super::redis::new(cfg.redis.clone()).await?); 181 | let info = web::Data::new(AppInfo { 182 | name: APP_NAME.to_string(), 183 | version: APP_VERSION.to_string(), 184 | }); 185 | 186 | let app = test::init_service( 187 | App::new() 188 | .app_data(pool.clone()) 189 | .app_data(info.clone()) 190 | .wrap(super::super::context::ContextTransform {}) 191 | .route("/", web::get().to(version)), 192 | ) 193 | .await; 194 | let req = test::TestRequest::default() 195 | .insert_header(ContentType::json()) 196 | .to_request(); 197 | let resp = test::call_service(&app, req).await; 198 | assert!(resp.status().is_success()); 199 | 200 | Ok(()) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

RedLimit

3 |

4 | RedLimit is a redis-based distributed rate limit HTTP service, implemented with Rust. 5 |

6 |
7 | 8 | [![CI](https://github.com/teambition/redlimit/actions/workflows/ci.yml/badge.svg)](https://github.com/teambition/redlimit/actions/workflows/ci.yml) 9 | [![Build](https://github.com/teambition/redlimit/actions/workflows/build.yml/badge.svg)](https://github.com/teambition/redlimit/actions/workflows/build.yml) 10 | [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/teambition/redlimit/main/LICENSE) 11 | 12 | ## 介绍 13 | 14 | RedLimit 是基于 Redis 7 最新 FUNCTION 特性和 IO 多线程能力实现的分布式 API 限速 HTTP 服务,使用 Rust 语言开发。特征如下: 15 | 1. 高性能,RedLimit 服务本身无状态,可水平扩展,限速状态保存在 Redis 实例上。Redis 实例是高负载的主要瓶颈,本服务的设计原则之一是尽量降低 Redis 的 CPU 开销。 16 | 2. 无需 Redis 持久化存储,允许状态数据丢失,允许切换 Redis 实例。一方面是因为服务会自动加载 FUNCTION 脚本,另一方面限速状态值也无需持久保存。 17 | 3. 自动降级,当 Redis 服务不可用时或者负载高延迟过大(100ms)时,RedLimit 服务会自动降级为不限速,不会影响业务;当 Redis 服务恢复时,RedLimit 服务会自动恢复限速能力。 18 | 4. 灵活的限速策略,支持爆发性限速控制,支持临时限速权重调整,支持临时限速名单,详见下文。 19 | 20 | 生产环境实际开销:用 k8s 部署的 RedLimit 服务,Redis 7 实例为 8 核 arm64 CPU,开启了多线程支持,25000 QPS 时,RedLimit 服务 8 个 pod 消耗 CPU 总计为 3,Redis 实例消耗 CPU 为 1.2,内存消耗很少,可忽略。 21 | ## 限速策略 22 | 23 | 限速策略分为静态限速策略和动态限速策略两部分。 24 | 25 | ### 静态限速策略 26 | 静态限速策略在 config https://github.com/teambition/redlimit/blob/main/config/default.toml 文件中配置,每次更新需要重启 RedLimit 服务(基于 k8s Deployment 的 `RollingUpdate` 重启不会影响业务)。 27 | 28 | 以默认配置为例来了解一下静态限速策略: 29 | ```toml 30 | [rules.core] 31 | limit = [100, 10000, 50, 2000] 32 | 33 | [rules.core.path] 34 | "GET /v1/file/list" = 5 35 | ``` 36 | 37 | 这是一个 `scope` 为 "core" 的策略,其中: 38 | * `limit = [100, 10000, 50, 2000]` 是 "core" 的限速策略值,前两个值定义常规限速值,此示例表示 10000 毫秒内最多消耗 100 个 token。后两个值定义 burst 爆发性或并发性限速值,此示例表示 2000 毫秒内最多消耗 50 个 token。 39 | * `"GET /v1/file/list" = 5` 是 "core" 下的一个自定义 token 权重的限速路径,表示 `GET /v1/file/list` 这个路径一次请求要消耗 5 个 token,而默认只消耗 1 个 token,所以这个路径并发超过 10 个请求会触发爆发性限速,10 秒内逐步发出超过 20 个请求也会触发常规限速。 40 | 41 | 一个限速请求如下: 42 | ``` 43 | POST http://localhost:8080/limiting 44 | Content-Type: application/json 45 | ``` 46 | 请求数据如下: 47 | ```json 48 | { 49 | "scope": "core", 50 | "path": "GET /v1/file/list", 51 | "id": "user123" 52 | } 53 | ``` 54 | 其中: 55 | * `scope` 是限速作用域,对应了 config 中的某个限速策略,没找到则使用 "*" 默认限速策略,示例中即表明使用 `[100, 10000, 50, 2000]` 这组策略值,可以为空。 56 | * `path` 是限速路径,对应了 config 中的 `scope` 下限速路径定义的一次请求 token 消耗数量,默认为 1 token。其字面含义由业务自行定义,可以为空。 57 | * `id` 是限速主体标记,可以是用户 ID、设备 ID、IP 等。 58 | 59 | 响应结果如下: 60 | ```json 61 | { 62 | "result": { 63 | "limit": 100, 64 | "remaining": 95, 65 | "reset": 0, 66 | "retry": 0 67 | } 68 | } 69 | ``` 70 | 其中: 71 | * `limit` 对应 `x-ratelimit-limit`,表示当前周期(10000 毫秒)内有 100 个 token。 72 | * `remaining` 对应 `x-ratelimit-remaining`,表示当前周期(10000 毫秒)内还剩 95 个 token。 73 | * `reset` 对应 `x-ratelimit-reset`,表示限速计数状态重置的时间点,UNIX EPOCH 秒数,由于精度低,为 0 也可能处于被限速状态。 74 | * `retry` 对应 `retry-after`,但其精度单位为毫秒,为 0 一定表示未被限速,n >= 1 表示被限速,n 毫秒后可以重试。 75 | 76 | 同时,本次 HTTP 请求会生成一条 JSON 请求日志,类似这样: 77 | ```json 78 | {"elapsed":4,"kv":{"id":"user123","scope":"core","count":1,"bursted":false,"path":"POST /v1/file/list","limited":false},"level":"INFO","message":"","method":"POST","path":"/limiting","start":1679914348751,"status":200,"target":"api","timestamp":1679914348756,"xid":""} 79 | ``` 80 | 其中: 81 | * `path` 为本次请求的 API 路径。 82 | * `xid` 为本次请求的 `x-request-id`,请求未携带则为空。 83 | * `status` 为本次请求响应状态,正常请求都将响应 200,包括 Redis 处于异常状态时的请求。 84 | * `elapsed` 为本次请求所消耗的时间,单位为毫秒,一般为 0,最大约 100ms 左右。 85 | * `kv.scope`, `kv.path`, `kv.id` 为本次请求的参数。 86 | * `kv.count` 为本次请求后在当前周期内累积消耗的 token 数,正常请求都应该 >= 1,为 0 表示本次请求时 Redis 异常或超时,自动降级为不限速。 87 | * `kv.limited` 为 true 时表示本次请求被限速。 88 | * `kv.bursted` 为 true 时表示本次请求突破了 burst 爆发值,被限速,此时 `limited` 也一定为 true。 89 | 90 | ### 动态限速策略 91 | 动态限速策略包括 redlist 和 redrules 两种,具有生命周期,超过生命周期则失效,详见下文。 92 | 动态限速策略通过 HTTP API 动态添加或更新到 Redis 中,并同步给各个 RedLimit 服务运行实例。 93 | 94 | ## 使用 95 | ### 启动服务 96 | RedLimit 服务依赖 Redis 7,请先启动 Redis 服务并在 config 中配置好。 97 | 98 | 本地开发环境运行: 99 | ```bash 100 | cargo run 101 | ``` 102 | 103 | 或通过 `CONFIG_FILE_PATH` 环境变量指定 config 文件运行: 104 | ```bash 105 | CONFIG_FILE_PATH=/my/config.toml cargo run 106 | ``` 107 | 108 | RedLimit 也提供了 docker 镜像,可以通过 docker 或 k8s 运行(请自行定义配置), 109 | 见:https://github.com/teambition/redlimit/pkgs/container/redlimit 110 | 111 | ## API 112 | 113 | ### 检查限速状态:`POST /limiting` 114 | 详情见上文。 115 | ```bash 116 | POST http://localhost:8080/limiting 117 | Content-Type: application/json 118 | ``` 119 | 请求数据如下: 120 | ```json 121 | { 122 | "scope": "core", 123 | "path": "POST /v1/file/list", 124 | "id": "user123" 125 | } 126 | ``` 127 | 响应结果如下: 128 | ```json 129 | { 130 | "result": { 131 | "limit": 100, 132 | "remaining": 95, 133 | "reset": 0, 134 | "retry": 0 135 | } 136 | } 137 | ``` 138 | 139 | ### 查看服务状态:`GET /version` 140 | 该 API 可用于健康检测。 141 | ```bash 142 | GET http://localhost:8080/version 143 | ``` 144 | 145 | 响应结果如下: 146 | ```json 147 | { 148 | "result": { 149 | "name": "redlimit", 150 | "version": "0.2.4" 151 | } 152 | } 153 | ``` 154 | 155 | 同时该 API 会产生如下访问日志: 156 | ```json 157 | {"elapsed":0,"kv":{"idle_connections":6,"connections":6},"level":"INFO","message":"","method":"GET","path":"/version","start":1679914386823,"status":200,"target":"api","timestamp":1679914386823,"xid":""} 158 | ``` 159 | 其中 `kv.idle_connections`, `kv.connections` 为当前服务中 redis pool 状态,`kv.connections` 为 0 表示 redis 服务异常。 160 | 161 | ### 创建或更新限速名单:`POST /redlist` 162 | RedLimit 支持动态添加限速红名单,名单中的 `id` 都将使用 config 中的 `rules."-"` 规则。 163 | ```bash 164 | POST http://localhost:8080/redlist 165 | Content-Type: application/json 166 | ``` 167 | 请求数据如下: 168 | ```json 169 | { 170 | "user1": 50000, 171 | "user2": 120000, 172 | "ip3": 120000 173 | } 174 | ``` 175 | 其中,key 为限速主体标记 `id`,value 为规则有效期,单位为毫秒。如果 `id` 不存在,则创建;如果 `id` 存在,则更新其有效期。 176 | 示例中,"user1"、"user2"、"ip3" 三个 ID 都将使用 config 中的 `rules."-"` 规则,即 `[3, 10000, 1, 1000]`。 177 | 对 "user1" 的限制将在 50 秒后失效,对 "user2" 和 "ip3" 的限制将在 120 秒后失效。 178 | 179 | 响应结果如下: 180 | ```json 181 | { 182 | "result": "ok", 183 | } 184 | ``` 185 | 186 | ### 查看所有有效动态限速名单:`GET /redlist` 187 | 该 API 一次性返回所有有效期内的动态限速名单,不支持分页,所以限速名单不应该太多,最好不要超过 10 万个。 188 | ```bash 189 | GET http://localhost:8080/redlist 190 | ``` 191 | 192 | 响应结果如下: 193 | ```json 194 | { 195 | "result": { 196 | "ip3": 1679536722731, 197 | "user1": 1679536652731, 198 | "user2": 1679536722731 199 | } 200 | } 201 | ``` 202 | 其中,key 为限速主体标记 `id`,value 为该 `id` 将失效的 UNIX EPOCH 时间点,单位为毫秒,已失效的限速主体不会返回。 203 | 204 | ### 创建或更新限速策略的限速路径权重:`POST /redrules` 205 | RedLimit 支持动态调整限速策略下限速路径的 token 权重。 206 | ```bash 207 | POST http://localhost:8080/redrules 208 | Content-Type: application/json 209 | ``` 210 | 请求数据如下: 211 | ```json 212 | { 213 | "scope": "core", 214 | "rules": { 215 | "GET /v1/file/list": [10, 10000], 216 | "GET /v2/file/list": [8, 20000] 217 | } 218 | } 219 | ``` 220 | 其中,`scope` 为目标限速策略,此示例为 "core",`rules` 中的 key 为限速路径 `path`,value[0] 为该路径一次请求 token 消耗数量,value[1] 为该路径规则有效期,单位为毫秒。如果 `path` 不存在,则创建;如果 `path` 存在,则更新其 token 权重和有效期。 221 | 示例中,"GET /v1/file/list" 路径的 token 权重为 10,有效期为 10 秒,"GET /v2/file/list" 路径的 token 权重为 8,有效期为 20 秒。 222 | 223 | 响应结果如下: 224 | ```json 225 | { 226 | "result": "ok", 227 | } 228 | ``` 229 | 230 | ### 查看所有有效动态限速策略:`GET /redrules` 231 | 该 API 一次性返回所有有效期内的动态限速策略,不支持分页,所以动态限速策略不应该太多,最好不要超过 1 万个。 232 | ```bash 233 | GET http://localhost:8080/redrules 234 | ``` 235 | 236 | 响应结果如下: 237 | ```json 238 | { 239 | "result": { 240 | "core:GET /v1/file/list": [10, 1679536684628], 241 | "core:GET /v2/file/list": [8, 1679536694627] 242 | } 243 | } 244 | ``` 245 | 其中,key 为限速作用域 `scope` 和限速路径 `path` 的组合,value[0] 为该路径一次请求 token 消耗数量,value[1] 为该路径策略将失效的 UNIX EPOCH 时间点,单位为毫秒,已失效的限速策略不会返回。 246 | 247 | ## License 248 | Copyright © 2023 [teambition](https://github.com/teambition). 249 | 250 | teambition/redlimit is licensed under the MIT License. See [LICENSE](./LICENSE) for the full license text. 251 | -------------------------------------------------------------------------------- /src/redlimit.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | time::{Duration, Instant}, 4 | }; 5 | 6 | use actix_web::web; 7 | use anyhow::{Error, Result}; 8 | use rustis::{client::Client, resp}; 9 | use serde::{Deserialize, Serialize}; 10 | use tokio::{sync::RwLock, task::JoinHandle, time::sleep}; 11 | use tokio_util::sync::CancellationToken; 12 | 13 | use super::{conf::Rule, context::unix_ms, redis::RedisPool, redlimit_lua}; 14 | 15 | pub struct RedRules { 16 | pub ns: NS, 17 | floor: Vec, 18 | defaut: Rule, 19 | rules: HashMap, 20 | dyn_rules: RwLock, 21 | } 22 | 23 | pub struct NS(String); 24 | 25 | impl NS { 26 | pub fn new(namespace: String) -> Self { 27 | NS(namespace) 28 | } 29 | 30 | pub fn redlist_key(id: &str) -> &str { 31 | id 32 | } 33 | 34 | pub fn redrules_key(scope: &str, path: &str) -> String { 35 | format!("{}:{}", scope, path) 36 | } 37 | 38 | pub fn limiting_key(&self, scope: &str, id: &str) -> String { 39 | format!("{}:{}:{}", self.0, scope, id) 40 | } 41 | 42 | pub fn as_str(&self) -> &str { 43 | self.0.as_str() 44 | } 45 | } 46 | 47 | pub struct DynRedRules { 48 | redrules: HashMap, // ns:scope:path -> (quantity, ttl) 49 | redlist: HashMap, // ns:id -> ttl 50 | redlist_cursor: u64, 51 | } 52 | 53 | impl RedRules { 54 | pub fn new(namespace: &str, rules: &HashMap) -> Self { 55 | let mut rr = RedRules { 56 | ns: NS::new(namespace.to_string()), 57 | floor: vec![2, 10000, 1, 1000], 58 | defaut: Rule { 59 | limit: vec![5, 5000, 2, 1000], 60 | quantity: 1, 61 | path: HashMap::new(), 62 | }, 63 | rules: HashMap::new(), 64 | dyn_rules: RwLock::new(DynRedRules { 65 | redrules: HashMap::new(), 66 | redlist: HashMap::new(), 67 | redlist_cursor: 0, 68 | }), 69 | }; 70 | 71 | for (scope, rule) in rules { 72 | match scope.as_str() { 73 | "*" => rr.defaut = rule.clone(), 74 | "-" => rr.floor = rule.limit.clone(), 75 | _ => { 76 | rr.rules.insert(scope.clone(), rule.clone()); 77 | } 78 | } 79 | } 80 | rr 81 | } 82 | 83 | pub async fn redlist(&self, now: u64) -> HashMap { 84 | let dr = self.dyn_rules.read().await; 85 | let mut redlist = HashMap::new(); 86 | for (k, v) in &dr.redlist { 87 | if *v >= now { 88 | redlist.insert(k.clone(), *v); 89 | } 90 | } 91 | redlist 92 | } 93 | 94 | pub async fn redrules(&self, now: u64) -> HashMap { 95 | let dr = self.dyn_rules.read().await; 96 | let mut redrules = HashMap::new(); 97 | for (k, v) in &dr.redrules { 98 | if v.1 >= now { 99 | redrules.insert(k.clone(), *v); 100 | } 101 | } 102 | redrules 103 | } 104 | 105 | pub async fn limit_args(&self, now: u64, scope: &str, path: &str, id: &str) -> LimitArgs { 106 | if id.is_empty() { 107 | return LimitArgs::new(0, &vec![]); 108 | } 109 | 110 | let dr = self.dyn_rules.read().await; 111 | if let Some(ttl) = dr.redlist.get(NS::redlist_key(id)) { 112 | if *ttl >= now { 113 | return LimitArgs::new(1, &self.floor); 114 | } 115 | } 116 | 117 | let rule = self.rules.get(scope).unwrap_or(&self.defaut); 118 | if let Some((quantity, ttl)) = dr.redrules.get(&NS::redrules_key(scope, path)) { 119 | if *ttl >= now { 120 | return LimitArgs::new(*quantity, &rule.limit); 121 | } 122 | } 123 | 124 | let quantity = *rule.path.get(path).unwrap_or(&rule.quantity); 125 | let quantity = if quantity > 0 { quantity } else { 1 }; 126 | LimitArgs::new(quantity, &rule.limit) 127 | } 128 | 129 | pub async fn dyn_update( 130 | &self, 131 | now: u64, 132 | redlist_cursor: u64, 133 | redlist: HashMap, 134 | redrules: HashMap, 135 | ) { 136 | let mut dr = self.dyn_rules.write().await; 137 | if redlist_cursor > dr.redlist_cursor { 138 | dr.redlist_cursor = redlist_cursor; 139 | } 140 | 141 | dr.redlist.retain(|_, v| *v > now); 142 | for (k, v) in redlist { 143 | if v > now { 144 | dr.redlist.insert(k, v); 145 | } 146 | } 147 | 148 | dr.redrules.retain(|_, v| v.1 > now); 149 | for (k, v) in redrules { 150 | if v.1 > now { 151 | dr.redrules.insert(k, v); 152 | } 153 | } 154 | } 155 | } 156 | 157 | // (quantity, max count per period, period with millisecond, max burst, burst 158 | // period with millisecond) 159 | #[derive(PartialEq, Debug)] 160 | pub struct LimitArgs(pub u64, pub u64, pub u64, pub u64, pub u64); 161 | 162 | impl LimitArgs { 163 | pub fn new(quantity: u64, others: &Vec) -> Self { 164 | let mut args = LimitArgs(quantity, 0, 0, 0, 0); 165 | match others.len() { 166 | 2 => { 167 | args.1 = others[0]; 168 | args.2 = others[1]; 169 | } 170 | 3 => { 171 | args.1 = others[0]; 172 | args.2 = others[1]; 173 | args.3 = others[2]; 174 | } 175 | 4 => { 176 | args.1 = others[0]; 177 | args.2 = others[1]; 178 | args.3 = others[2]; 179 | args.4 = others[3]; 180 | } 181 | _ => {} 182 | } 183 | args 184 | } 185 | 186 | pub fn is_valid(&self) -> bool { 187 | self.0 > 0 188 | && self.0 <= self.1 189 | && self.2 > 0 190 | && self.2 <= 60 * 1000 191 | && (self.3 == 0 || self.0 <= self.3) 192 | && (self.4 == 0 || self.4 <= self.2) 193 | } 194 | } 195 | 196 | #[derive(Serialize, PartialEq, Debug)] 197 | // LimitResult.0: request count; 198 | // LimitResult.1: 0: not limited, > 0: limited, milliseconds to wait; 199 | pub struct LimitResult(pub u64, pub u64); 200 | 201 | pub async fn limiting( 202 | pool: web::Data, 203 | limiting_key: &str, 204 | args: LimitArgs, 205 | ) -> Result { 206 | if !args.is_valid() { 207 | return Ok(LimitResult(0, 0)); 208 | } 209 | 210 | let mut cmd = resp::cmd("FCALL") 211 | .arg("limiting") 212 | .arg(1) 213 | .arg(limiting_key) 214 | .arg(args.0) 215 | .arg(args.1) 216 | .arg(args.2); 217 | if args.3 > 0 { 218 | cmd = cmd.arg(args.3); 219 | } 220 | if args.4 > 0 { 221 | cmd = cmd.arg(args.4); 222 | } 223 | 224 | let data = pool.get().await?.send(cmd, None).await?; 225 | if let Ok(rt) = data.to::<(u64, u64)>() { 226 | return Ok(LimitResult(rt.0, rt.1)); 227 | } 228 | 229 | Ok(LimitResult(0, 0)) 230 | } 231 | 232 | pub async fn redrules_add( 233 | pool: web::Data, 234 | ns: &str, 235 | scope: &str, 236 | rules: &HashMap, 237 | ) -> Result<()> { 238 | if !rules.is_empty() { 239 | let cli = pool.get().await?; 240 | for (k, v) in rules { 241 | let cmd = resp::cmd("FCALL") 242 | .arg("redrules_add") 243 | .arg(1) 244 | .arg(ns) 245 | .arg(scope) 246 | .arg(k) 247 | .arg(v.0) 248 | .arg(v.1); 249 | cli.send(cmd, None).await?; 250 | } 251 | } 252 | Ok(()) 253 | } 254 | 255 | pub async fn redlist_add( 256 | pool: web::Data, 257 | ns: &str, 258 | list: &HashMap, 259 | ) -> Result<()> { 260 | if !list.is_empty() { 261 | let cli = pool.get().await?; 262 | let mut cmd = resp::cmd("FCALL").arg("redlist_add").arg(1).arg(ns); 263 | 264 | for (k, v) in list { 265 | cmd = cmd.arg(k).arg(*v); 266 | } 267 | 268 | cli.send(cmd, None).await?; 269 | } 270 | Ok(()) 271 | } 272 | 273 | pub async fn init_redlimit_fn(pool: web::Data) -> anyhow::Result<()> { 274 | let cmd = resp::cmd("FUNCTION") 275 | .arg("LOAD") 276 | .arg(redlimit_lua::REDLIMIT); 277 | 278 | let data = pool.get().await?.send(cmd, None).await?; 279 | if data.is_error() { 280 | let err = data.to_string(); 281 | if !err.contains("already exists") { 282 | return Err(Error::msg(err)); 283 | } 284 | } 285 | Ok(()) 286 | } 287 | 288 | pub fn init_redlimit_sync( 289 | pool: web::Data, 290 | redrules: web::Data, 291 | interval_secs: u64, 292 | ) -> (JoinHandle<()>, CancellationToken) { 293 | let cancel_redrules_sync = CancellationToken::new(); 294 | ( 295 | tokio::spawn(spawn_redlimit_sync( 296 | pool, 297 | redrules, 298 | cancel_redrules_sync.clone(), 299 | interval_secs, 300 | )), 301 | cancel_redrules_sync, 302 | ) 303 | } 304 | 305 | async fn spawn_redlimit_sync( 306 | pool: web::Data, 307 | redrules: web::Data, 308 | stop_signal: CancellationToken, 309 | interval_secs: u64, 310 | ) { 311 | loop { 312 | tokio::select! { 313 | _ = stop_signal.cancelled() => { 314 | log::info!("gracefully shutting down redlimit sync job"); 315 | break; 316 | } 317 | _ = sleep(Duration::from_secs(interval_secs)) => {} 318 | }; 319 | 320 | let rt = redlimit_sync_job(pool.clone(), redrules.clone()).await; 321 | if rt.is_err() { 322 | log::error!("redlimit_sync_job error: {:?}", rt); 323 | 324 | // auto load function 325 | if rt.unwrap_err().to_string().contains("Function not found") { 326 | match init_redlimit_fn(pool.clone()).await { 327 | Ok(_) => { 328 | log::warn!("init_redlimit_fn success"); 329 | } 330 | Err(e) => { 331 | log::error!("init_redlimit_fn error: {:?}", e); 332 | } 333 | } 334 | } 335 | } 336 | } 337 | } 338 | 339 | async fn redlimit_sync_job( 340 | pool: web::Data, 341 | redrules: web::Data, 342 | ) -> anyhow::Result<()> { 343 | let redis = pool.get().await?; 344 | let cursor = redrules.dyn_rules.read().await.redlist_cursor; 345 | let inow = Instant::now(); 346 | let now = unix_ms(); 347 | 348 | let dyn_rules = redrules_load(redis.clone(), redrules.ns.as_str(), now).await?; 349 | 350 | let dyn_list = redlist_load(redis.clone(), redrules.ns.as_str(), now, cursor).await?; 351 | 352 | let cursor = dyn_list.0; 353 | let rules_len = dyn_rules.len(); 354 | let list_len = dyn_list.1.len(); 355 | if !dyn_rules.is_empty() || !dyn_list.1.is_empty() { 356 | redrules 357 | .dyn_update(now, cursor, dyn_list.1, dyn_rules) 358 | .await; 359 | } 360 | 361 | log::info!(target: "sync", 362 | cursor = cursor, 363 | redrules = rules_len, 364 | redlist = list_len, 365 | elapsed = inow.elapsed().as_millis() as u64; 366 | "ok", 367 | ); 368 | 369 | Ok(()) 370 | } 371 | 372 | #[derive(Deserialize)] 373 | struct RedRuleEntry(String, String, u64, u64); 374 | 375 | async fn redrules_load( 376 | redis: Client, 377 | ns: &str, 378 | now: u64, 379 | ) -> anyhow::Result> { 380 | let redrules_cmd = resp::cmd("FCALL").arg("redrules_all").arg(1).arg(ns); 381 | 382 | let data = redis.send(redrules_cmd, None).await?.to::>()?; 383 | let mut rt: HashMap = HashMap::new(); 384 | let mut has_stale = false; 385 | for s in data { 386 | if let Ok(v) = serde_json::from_str::(&s) { 387 | if v.3 > now { 388 | rt.insert(NS::redrules_key(&v.0, &v.1), (v.2, v.3)); 389 | } else { 390 | has_stale = true 391 | } 392 | } 393 | } 394 | 395 | if has_stale { 396 | let sweep_cmd = resp::cmd("FCALL").arg("redrules_add").arg(1).arg(ns); 397 | redis.send(sweep_cmd, None).await?; 398 | } 399 | 400 | Ok(rt) 401 | } 402 | 403 | const REDLIST_SCAN_COUNT: usize = 10000; 404 | async fn redlist_load( 405 | redis: Client, 406 | ns: &str, 407 | now: u64, 408 | cursor: u64, 409 | ) -> anyhow::Result<(u64, HashMap)> { 410 | let mut cursor = cursor; 411 | let mut has_stale = false; 412 | let mut rt: HashMap = HashMap::new(); 413 | 414 | 'next_cursor: loop { 415 | let blacklist_cmd = resp::cmd("FCALL") 416 | .arg("redlist_scan") 417 | .arg(1) 418 | .arg(ns) 419 | .arg(cursor); 420 | 421 | let data = redis.send(blacklist_cmd, None).await?.to::>()?; 422 | let has_next = data.len() >= REDLIST_SCAN_COUNT; 423 | 424 | let mut iter = data.into_iter(); 425 | match iter.next() { 426 | Some(c) => { 427 | let new_cursor = c.parse::()?; 428 | if cursor == new_cursor { 429 | cursor += 1; 430 | } else { 431 | cursor = new_cursor; 432 | } 433 | } 434 | None => { 435 | break; 436 | } 437 | } 438 | 439 | loop { 440 | if let Some(id) = iter.next() { 441 | match iter.next() { 442 | Some(ttl) => { 443 | let ttl = ttl.parse::()?; 444 | if ttl > now { 445 | rt.insert(id, ttl); 446 | } else { 447 | has_stale = true; 448 | } 449 | continue; 450 | } 451 | None => { 452 | break 'next_cursor; 453 | } 454 | } 455 | } 456 | break; 457 | } 458 | 459 | if !has_next { 460 | break; 461 | } 462 | } 463 | 464 | if has_stale { 465 | let sweep_cmd = resp::cmd("FCALL").arg("redlist_add").arg(1).arg(ns); 466 | redis.send(sweep_cmd, None).await?; 467 | } 468 | 469 | Ok((cursor, rt)) 470 | } 471 | 472 | #[cfg(test)] 473 | mod tests { 474 | 475 | use actix_web::web; 476 | 477 | use super::{ 478 | super::{conf, redis}, 479 | *, 480 | }; 481 | 482 | #[actix_web::test] 483 | async fn limit_args_works() -> anyhow::Result<()> { 484 | assert_eq!(LimitArgs(1, 0, 0, 0, 0), LimitArgs::new(1, &vec![])); 485 | assert_eq!(LimitArgs(2, 0, 0, 0, 0), LimitArgs::new(2, &vec![])); 486 | assert_eq!(LimitArgs(2, 0, 0, 0, 0), LimitArgs::new(2, &vec![100])); 487 | 488 | assert_eq!( 489 | LimitArgs(3, 100, 10000, 0, 0), 490 | LimitArgs::new(3, &vec![100, 10000]) 491 | ); 492 | 493 | assert_eq!( 494 | LimitArgs(3, 100, 10000, 10, 0), 495 | LimitArgs::new(3, &vec![100, 10000, 10]) 496 | ); 497 | 498 | assert_eq!( 499 | LimitArgs(1, 100, 10000, 50, 2000), 500 | LimitArgs::new(1, &vec![100, 10000, 50, 2000]) 501 | ); 502 | 503 | assert_eq!( 504 | LimitArgs(1, 0, 0, 0, 0), 505 | LimitArgs::new(1, &vec![100, 10000, 50, 2000, 1]) 506 | ); 507 | 508 | Ok(()) 509 | } 510 | 511 | #[actix_web::test] 512 | async fn red_rules_works() -> anyhow::Result<()> { 513 | let cfg = conf::Conf::new()?; 514 | let redrules = RedRules::new(&cfg.namespace, &cfg.rules); 515 | 516 | { 517 | assert_eq!(vec![3, 10000, 1, 1000], redrules.floor); 518 | 519 | assert_eq!(vec![10, 10000, 3, 1000], redrules.defaut.limit); 520 | assert!(redrules.defaut.path.is_empty()); 521 | 522 | assert_eq!(0, redrules.dyn_rules.read().await.redlist_cursor); 523 | 524 | let core_rules = redrules 525 | .rules 526 | .get("core") 527 | .ok_or(anyhow::Error::msg("'core' not exists"))?; 528 | assert_eq!(vec![100, 10000, 50, 2000], core_rules.limit); 529 | assert_eq!( 530 | 5, 531 | core_rules.path.get("GET /v1/file/list").unwrap().to_owned() 532 | ); 533 | 534 | assert!(redrules.rules.get("core2").is_none()); 535 | } 536 | 537 | { 538 | assert!(redrules.redlist(0).await.is_empty()); 539 | assert!(redrules.redrules(0).await.is_empty()); 540 | 541 | assert_eq!( 542 | LimitArgs(5, 100, 10000, 50, 2000), 543 | redrules 544 | .limit_args(0, "core", "GET /v1/file/list", "user1") 545 | .await 546 | ); 547 | assert_eq!( 548 | LimitArgs(5, 100, 10000, 50, 2000), 549 | redrules 550 | .limit_args(0, "core", "GET /v1/file/list", "user2") 551 | .await, 552 | "any user" 553 | ); 554 | 555 | assert_eq!( 556 | LimitArgs(1, 100, 10000, 50, 2000), 557 | redrules 558 | .limit_args(0, "core", "GET /v2/file/list", "user1") 559 | .await, 560 | "path not exists" 561 | ); 562 | 563 | assert_eq!( 564 | LimitArgs(1, 10, 10000, 3, 1000), 565 | redrules 566 | .limit_args(0, "core2", "GET /v1/file/list", "user1") 567 | .await, 568 | "scope not exists" 569 | ); 570 | 571 | assert_eq!( 572 | LimitArgs(1, 100, 10000, 50, 2000), 573 | redrules 574 | .limit_args(0, "biz", "GET /v1/app/info", "user1") 575 | .await 576 | ); 577 | assert_eq!( 578 | LimitArgs(3, 100, 10000, 50, 2000), 579 | redrules 580 | .limit_args(0, "biz", "GET /v2/app/info", "user1") 581 | .await 582 | ); 583 | assert_eq!( 584 | LimitArgs(10, 100, 10000, 50, 2000), 585 | redrules 586 | .limit_args(0, "biz", "GET /v3/app/info", "user1") 587 | .await, 588 | "any user" 589 | ); 590 | } 591 | 592 | let ts = unix_ms(); 593 | { 594 | let mut dyn_blacklist = HashMap::new(); 595 | dyn_blacklist.insert("user1".to_owned(), ts + 1000); 596 | redrules 597 | .dyn_update(ts, 1, dyn_blacklist, HashMap::new()) 598 | .await; 599 | 600 | { 601 | let dr = redrules.dyn_rules.read().await; 602 | assert_eq!(1, dr.redlist_cursor); 603 | } 604 | 605 | assert_eq!(1, redrules.redlist(0).await.len()); 606 | assert_eq!(1, redrules.redlist(ts + 1000).await.len()); 607 | assert!(redrules.redlist(ts + 1001).await.is_empty()); 608 | assert!(redrules.redrules(0).await.is_empty()); 609 | 610 | assert_eq!( 611 | LimitArgs(1, 3, 10000, 1, 1000), 612 | redrules 613 | .limit_args(0, "core", "GET /v1/file/list", "user1") 614 | .await, 615 | "limited by dyn_blacklist" 616 | ); 617 | assert_eq!( 618 | LimitArgs(5, 100, 10000, 50, 2000), 619 | redrules 620 | .limit_args(0, "core", "GET /v1/file/list", "user2") 621 | .await, 622 | "not limited by dyn_blacklist" 623 | ); 624 | assert_eq!( 625 | LimitArgs(1, 3, 10000, 1, 1000), 626 | redrules 627 | .limit_args(ts, "core", "GET /v1/file/list", "user1") 628 | .await, 629 | "limited by dyn_blacklist" 630 | ); 631 | assert_eq!( 632 | LimitArgs(5, 100, 10000, 50, 2000), 633 | redrules 634 | .limit_args(ts + 1001, "core", "GET /v1/file/list", "user1") 635 | .await, 636 | "not limited by dyn_blacklist after ttl" 637 | ); 638 | } 639 | 640 | { 641 | let mut dyn_rules = HashMap::new(); 642 | dyn_rules.insert("core:GET /v1/file/list".to_owned(), (3, ts + 1000)); 643 | dyn_rules.insert("core:GET /v2/file/list".to_owned(), (5, ts + 1000)); 644 | redrules.dyn_update(ts, 2, HashMap::new(), dyn_rules).await; 645 | 646 | { 647 | let dr = redrules.dyn_rules.read().await; 648 | assert_eq!(2, dr.redlist_cursor); 649 | } 650 | 651 | assert_eq!(1, redrules.redlist(0).await.len()); 652 | assert_eq!(2, redrules.redrules(0).await.len()); 653 | assert_eq!(2, redrules.redrules(ts + 1000).await.len()); 654 | assert!(redrules.redrules(ts + 1001).await.is_empty()); 655 | 656 | assert_eq!( 657 | LimitArgs(1, 3, 10000, 1, 1000), 658 | redrules 659 | .limit_args(0, "core", "GET /v1/file/list", "user1") 660 | .await, 661 | "limited by dyn_blacklist" 662 | ); 663 | assert_eq!( 664 | LimitArgs(3, 100, 10000, 50, 2000), 665 | redrules 666 | .limit_args(0, "core", "GET /v1/file/list", "user2") 667 | .await, 668 | "limited by dyn_rules" 669 | ); 670 | assert_eq!( 671 | LimitArgs(5, 100, 10000, 50, 2000), 672 | redrules 673 | .limit_args(0, "core", "GET /v2/file/list", "user2") 674 | .await, 675 | "limited by dyn_rules" 676 | ); 677 | 678 | assert_eq!( 679 | LimitArgs(5, 100, 10000, 50, 2000), 680 | redrules 681 | .limit_args(ts + 1001, "core", "GET /v1/file/list", "user1") 682 | .await, 683 | "not limited by dyn_blacklist after ttl" 684 | ); 685 | assert_eq!( 686 | LimitArgs(5, 100, 10000, 50, 2000), 687 | redrules 688 | .limit_args(ts + 1001, "core", "GET /v1/file/list", "user2") 689 | .await, 690 | "not limited by dyn_blacklist after ttl" 691 | ); 692 | assert_eq!( 693 | LimitArgs(1, 100, 10000, 50, 2000), 694 | redrules 695 | .limit_args(ts + 1001, "core", "GET /v2/file/list", "user2") 696 | .await, 697 | "not limited by dyn_blacklist after ttl" 698 | ); 699 | } 700 | 701 | { 702 | redrules 703 | .dyn_update(ts + 1001, ts, HashMap::new(), HashMap::new()) 704 | .await; 705 | 706 | { 707 | let dr = redrules.dyn_rules.read().await; 708 | assert_eq!(ts, dr.redlist_cursor); 709 | } 710 | 711 | assert!( 712 | redrules.redlist(0).await.is_empty(), 713 | "auto sweep stale rules" 714 | ); 715 | assert!( 716 | redrules.redrules(0).await.is_empty(), 717 | "auto sweep stale rules" 718 | ); 719 | 720 | let mut dyn_rules = HashMap::new(); 721 | dyn_rules.insert("core:GET /v1/file/list".to_owned(), (3, ts + 1000)); // stale rules 722 | dyn_rules.insert("core:GET /v1/file/list".to_owned(), (5, ts + 1002)); 723 | 724 | redrules 725 | .dyn_update(ts + 1001, ts + 1, HashMap::new(), dyn_rules) 726 | .await; 727 | 728 | { 729 | let dr = redrules.dyn_rules.read().await; 730 | assert_eq!(ts + 1, dr.redlist_cursor); 731 | } 732 | 733 | assert!(redrules.redlist(0).await.is_empty()); 734 | assert_eq!( 735 | 1, 736 | redrules.redrules(0).await.len(), 737 | "stale rules should not be added" 738 | ); 739 | } 740 | 741 | Ok(()) 742 | } 743 | 744 | #[actix_web::test] 745 | async fn init_redlimit_fn_works() -> anyhow::Result<()> { 746 | let cfg = conf::Conf::new()?; 747 | let pool = web::Data::new(redis::new(cfg.redis.clone()).await?); 748 | 749 | assert!(init_redlimit_fn(pool.clone()).await.is_ok()); 750 | assert!(init_redlimit_fn(pool.clone()).await.is_ok()); 751 | 752 | Ok(()) 753 | } 754 | 755 | #[actix_web::test] 756 | async fn limiting_works() -> anyhow::Result<()> { 757 | let cfg = conf::Conf::new()?; 758 | let pool = web::Data::new(redis::new(cfg.redis.clone()).await?); 759 | 760 | let res = limiting(pool.clone(), "TT:core:user1", LimitArgs(1, 8, 1000, 5, 300)).await?; 761 | assert_eq!(LimitResult(1, 0), res); 762 | 763 | let res = limiting(pool.clone(), "TT:core:user1", LimitArgs(3, 8, 1000, 5, 300)).await?; 764 | assert_eq!(LimitResult(4, 0), res); 765 | 766 | let res = limiting(pool.clone(), "TT:core:user1", LimitArgs(3, 8, 1000, 5, 300)).await?; 767 | assert_eq!(4, res.0); 768 | assert!(res.1 > 0); 769 | 770 | sleep(Duration::from_millis(res.1 + 1)).await; 771 | let res = limiting(pool.clone(), "TT:core:user1", LimitArgs(3, 8, 1000, 5, 300)).await?; 772 | assert_eq!(LimitResult(7, 0), res); 773 | 774 | let res = limiting(pool.clone(), "TT:core:user1", LimitArgs(2, 8, 1000, 5, 300)).await?; 775 | assert_eq!(7, res.0); 776 | assert!(res.1 > 0); 777 | 778 | let res = limiting(pool.clone(), "TT:core:user1", LimitArgs(1, 8, 1000, 5, 300)).await?; 779 | assert_eq!(LimitResult(8, 0), res); 780 | 781 | let res = limiting(pool.clone(), "TT:core:user1", LimitArgs(1, 8, 1000, 5, 300)).await?; 782 | assert_eq!(8, res.0); 783 | assert!(res.1 > 0); 784 | 785 | sleep(Duration::from_millis(res.1 + 1)).await; 786 | let res = limiting(pool.clone(), "TT:core:user1", LimitArgs(1, 8, 1000, 5, 300)).await?; 787 | assert_eq!(LimitResult(1, 0), res); 788 | 789 | let res = limiting(pool.clone(), "TT:core:user1", LimitArgs(1, 1, 1000, 5, 300)).await?; 790 | assert_eq!(1, res.0); 791 | assert!(res.1 > 0, "with new max count"); 792 | 793 | Ok(()) 794 | } 795 | 796 | #[actix_web::test] 797 | async fn redrules_add_load_works() -> anyhow::Result<()> { 798 | let ns = "redrules_add_load_works"; 799 | let cfg = conf::Conf::new()?; 800 | let pool = web::Data::new(redis::new(cfg.redis.clone()).await?); 801 | let ts = unix_ms(); 802 | 803 | let cli = pool.get().await?; 804 | 805 | let dyn_redrules = redrules_load(cli.clone(), ns, ts).await?; 806 | assert!(dyn_redrules.is_empty()); 807 | 808 | let mut rules = HashMap::new(); 809 | redrules_add(pool.clone(), ns, "core", &rules).await?; 810 | let dyn_redrules = redrules_load(cli.clone(), ns, ts).await?; 811 | assert!(dyn_redrules.is_empty()); 812 | 813 | rules.insert("path1".to_owned(), (2, 100)); 814 | redrules_add(pool.clone(), ns, "core", &rules).await?; 815 | let dyn_redrules = redrules_load(cli.clone(), ns, ts).await?; 816 | assert_eq!(1, dyn_redrules.len()); 817 | 818 | redrules_add(pool.clone(), ns, "core2", &rules).await?; 819 | let dyn_redrules = redrules_load(cli.clone(), ns, ts).await?; 820 | assert_eq!(2, dyn_redrules.len()); 821 | 822 | let rt = dyn_redrules 823 | .get("core:path1") 824 | .ok_or(anyhow::Error::msg("'core:path1' not exists"))? 825 | .to_owned(); 826 | assert_eq!(2, rt.0); 827 | assert!(rt.1 > ts); 828 | 829 | let rt = dyn_redrules 830 | .get("core2:path1") 831 | .ok_or(anyhow::Error::msg("'core2:path1' not exists"))? 832 | .to_owned(); 833 | assert_eq!(2, rt.0); 834 | assert!(rt.1 > ts); 835 | 836 | let dyn_redrules = redrules_load(cli.clone(), ns, ts + 210).await?; 837 | assert_eq!(0, dyn_redrules.len()); 838 | 839 | let dyn_redrules = redrules_load(cli.clone(), ns, ts).await?; 840 | assert_eq!(2, dyn_redrules.len()); 841 | 842 | sleep(Duration::from_millis(210)).await; 843 | let dyn_redrules = redrules_load(cli.clone(), ns, ts + 210).await?; 844 | assert_eq!(0, dyn_redrules.len(), "will sweep stale rules"); 845 | let dyn_redrules = redrules_load(cli.clone(), ns, ts).await?; 846 | assert_eq!(0, dyn_redrules.len(), "should sweeped stale rules"); 847 | 848 | Ok(()) 849 | } 850 | 851 | #[actix_web::test] 852 | async fn redlist_add_load_works() -> anyhow::Result<()> { 853 | let ns = "redlist_add_load_works"; 854 | let cfg = conf::Conf::new()?; 855 | let pool = web::Data::new(redis::new(cfg.redis.clone()).await?); 856 | let ts = unix_ms(); 857 | let cli = pool.get().await?; 858 | 859 | let dyn_redlist = redlist_load(cli.clone(), ns, ts, 0).await?; 860 | assert!(dyn_redlist.1.is_empty()); 861 | 862 | let mut rules: HashMap = HashMap::new(); 863 | redlist_add(pool.clone(), ns, &rules).await?; 864 | let dyn_redlist = redlist_load(cli.clone(), ns, ts, 0).await?; 865 | assert!(dyn_redlist.1.is_empty()); 866 | 867 | rules.insert("user1".to_owned(), 100); 868 | redlist_add(pool.clone(), ns, &rules).await?; 869 | let dyn_redlist = redlist_load(cli.clone(), ns, ts, 0).await?; 870 | assert!(dyn_redlist.0 > ts - 1000); 871 | assert_eq!(1, dyn_redlist.1.len()); 872 | 873 | redlist_add(pool.clone(), ns, &rules).await?; 874 | let dyn_redlist = redlist_load(cli.clone(), ns, ts, dyn_redlist.0).await?; 875 | assert!(dyn_redlist.0 > ts); 876 | assert_eq!(1, dyn_redlist.1.len()); 877 | 878 | let rt = dyn_redlist 879 | .1 880 | .get("user1") 881 | .ok_or(anyhow::Error::msg("'user1' not exists"))? 882 | .to_owned(); 883 | assert!(rt > ts); 884 | 885 | let dyn_redlist = redlist_load(cli.clone(), ns, ts + 210, 0).await?; 886 | assert_eq!(0, dyn_redlist.1.len()); 887 | let dyn_redlist = redlist_load(cli.clone(), ns, ts, 0).await?; 888 | assert_eq!(1, dyn_redlist.1.len()); 889 | 890 | sleep(Duration::from_millis(210)).await; 891 | let dyn_redlist = redlist_load(cli.clone(), ns, ts + 210, 0).await?; 892 | assert_eq!(0, dyn_redlist.1.len(), "will sweep stale rules"); 893 | let dyn_redlist = redlist_load(cli.clone(), ns, ts, 0).await?; 894 | assert_eq!(0, dyn_redlist.1.len(), "should sweeped stale rules"); 895 | 896 | Ok(()) 897 | } 898 | } 899 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "actix-codec" 7 | version = "0.5.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" 10 | dependencies = [ 11 | "bitflags", 12 | "bytes", 13 | "futures-core", 14 | "futures-sink", 15 | "log", 16 | "memchr", 17 | "pin-project-lite", 18 | "tokio", 19 | "tokio-util", 20 | ] 21 | 22 | [[package]] 23 | name = "actix-http" 24 | version = "3.3.1" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" 27 | dependencies = [ 28 | "actix-codec", 29 | "actix-rt", 30 | "actix-service", 31 | "actix-tls", 32 | "actix-utils", 33 | "ahash 0.8.3", 34 | "base64 0.21.0", 35 | "bitflags", 36 | "brotli", 37 | "bytes", 38 | "bytestring", 39 | "derive_more", 40 | "encoding_rs", 41 | "flate2", 42 | "futures-core", 43 | "h2", 44 | "http", 45 | "httparse", 46 | "httpdate", 47 | "itoa", 48 | "language-tags", 49 | "local-channel", 50 | "mime", 51 | "percent-encoding", 52 | "pin-project-lite", 53 | "rand", 54 | "sha1", 55 | "smallvec", 56 | "tokio", 57 | "tokio-util", 58 | "tracing", 59 | "zstd", 60 | ] 61 | 62 | [[package]] 63 | name = "actix-macros" 64 | version = "0.2.3" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" 67 | dependencies = [ 68 | "quote", 69 | "syn 1.0.109", 70 | ] 71 | 72 | [[package]] 73 | name = "actix-router" 74 | version = "0.5.1" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" 77 | dependencies = [ 78 | "bytestring", 79 | "http", 80 | "regex", 81 | "serde", 82 | "tracing", 83 | ] 84 | 85 | [[package]] 86 | name = "actix-rt" 87 | version = "2.8.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" 90 | dependencies = [ 91 | "futures-core", 92 | "tokio", 93 | ] 94 | 95 | [[package]] 96 | name = "actix-server" 97 | version = "2.2.0" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" 100 | dependencies = [ 101 | "actix-rt", 102 | "actix-service", 103 | "actix-utils", 104 | "futures-core", 105 | "futures-util", 106 | "mio", 107 | "num_cpus", 108 | "socket2", 109 | "tokio", 110 | "tracing", 111 | ] 112 | 113 | [[package]] 114 | name = "actix-service" 115 | version = "2.0.2" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" 118 | dependencies = [ 119 | "futures-core", 120 | "paste", 121 | "pin-project-lite", 122 | ] 123 | 124 | [[package]] 125 | name = "actix-tls" 126 | version = "3.0.3" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "9fde0cf292f7cdc7f070803cb9a0d45c018441321a78b1042ffbbb81ec333297" 129 | dependencies = [ 130 | "actix-codec", 131 | "actix-rt", 132 | "actix-service", 133 | "actix-utils", 134 | "futures-core", 135 | "log", 136 | "pin-project-lite", 137 | "tokio-rustls", 138 | "tokio-util", 139 | "webpki-roots", 140 | ] 141 | 142 | [[package]] 143 | name = "actix-utils" 144 | version = "3.0.1" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" 147 | dependencies = [ 148 | "local-waker", 149 | "pin-project-lite", 150 | ] 151 | 152 | [[package]] 153 | name = "actix-web" 154 | version = "4.3.1" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" 157 | dependencies = [ 158 | "actix-codec", 159 | "actix-http", 160 | "actix-macros", 161 | "actix-router", 162 | "actix-rt", 163 | "actix-server", 164 | "actix-service", 165 | "actix-tls", 166 | "actix-utils", 167 | "actix-web-codegen", 168 | "ahash 0.7.6", 169 | "bytes", 170 | "bytestring", 171 | "cfg-if", 172 | "cookie", 173 | "derive_more", 174 | "encoding_rs", 175 | "futures-core", 176 | "futures-util", 177 | "http", 178 | "itoa", 179 | "language-tags", 180 | "log", 181 | "mime", 182 | "once_cell", 183 | "pin-project-lite", 184 | "regex", 185 | "serde", 186 | "serde_json", 187 | "serde_urlencoded", 188 | "smallvec", 189 | "socket2", 190 | "time", 191 | "url", 192 | ] 193 | 194 | [[package]] 195 | name = "actix-web-codegen" 196 | version = "4.2.0" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" 199 | dependencies = [ 200 | "actix-router", 201 | "proc-macro2", 202 | "quote", 203 | "syn 1.0.109", 204 | ] 205 | 206 | [[package]] 207 | name = "adler" 208 | version = "1.0.2" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 211 | 212 | [[package]] 213 | name = "ahash" 214 | version = "0.7.6" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 217 | dependencies = [ 218 | "getrandom", 219 | "once_cell", 220 | "version_check", 221 | ] 222 | 223 | [[package]] 224 | name = "ahash" 225 | version = "0.8.3" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" 228 | dependencies = [ 229 | "cfg-if", 230 | "getrandom", 231 | "once_cell", 232 | "version_check", 233 | ] 234 | 235 | [[package]] 236 | name = "aho-corasick" 237 | version = "0.7.20" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 240 | dependencies = [ 241 | "memchr", 242 | ] 243 | 244 | [[package]] 245 | name = "alloc-no-stdlib" 246 | version = "2.0.4" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 249 | 250 | [[package]] 251 | name = "alloc-stdlib" 252 | version = "0.2.2" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 255 | dependencies = [ 256 | "alloc-no-stdlib", 257 | ] 258 | 259 | [[package]] 260 | name = "anyhow" 261 | version = "1.0.70" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" 264 | 265 | [[package]] 266 | name = "async-trait" 267 | version = "0.1.68" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" 270 | dependencies = [ 271 | "proc-macro2", 272 | "quote", 273 | "syn 2.0.13", 274 | ] 275 | 276 | [[package]] 277 | name = "atoi" 278 | version = "2.0.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" 281 | dependencies = [ 282 | "num-traits", 283 | ] 284 | 285 | [[package]] 286 | name = "autocfg" 287 | version = "1.1.0" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 290 | 291 | [[package]] 292 | name = "base64" 293 | version = "0.13.1" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 296 | 297 | [[package]] 298 | name = "base64" 299 | version = "0.21.0" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" 302 | 303 | [[package]] 304 | name = "bb8" 305 | version = "0.8.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "1627eccf3aa91405435ba240be23513eeca466b5dc33866422672264de061582" 308 | dependencies = [ 309 | "async-trait", 310 | "futures-channel", 311 | "futures-util", 312 | "parking_lot", 313 | "tokio", 314 | ] 315 | 316 | [[package]] 317 | name = "bitflags" 318 | version = "1.3.2" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 321 | 322 | [[package]] 323 | name = "block-buffer" 324 | version = "0.10.4" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 327 | dependencies = [ 328 | "generic-array", 329 | ] 330 | 331 | [[package]] 332 | name = "brotli" 333 | version = "3.3.4" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" 336 | dependencies = [ 337 | "alloc-no-stdlib", 338 | "alloc-stdlib", 339 | "brotli-decompressor", 340 | ] 341 | 342 | [[package]] 343 | name = "brotli-decompressor" 344 | version = "2.3.4" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" 347 | dependencies = [ 348 | "alloc-no-stdlib", 349 | "alloc-stdlib", 350 | ] 351 | 352 | [[package]] 353 | name = "bumpalo" 354 | version = "3.12.0" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 357 | 358 | [[package]] 359 | name = "bytes" 360 | version = "1.4.0" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" 363 | 364 | [[package]] 365 | name = "bytestring" 366 | version = "1.3.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" 369 | dependencies = [ 370 | "bytes", 371 | ] 372 | 373 | [[package]] 374 | name = "cc" 375 | version = "1.0.79" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 378 | dependencies = [ 379 | "jobserver", 380 | ] 381 | 382 | [[package]] 383 | name = "cfg-if" 384 | version = "1.0.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 387 | 388 | [[package]] 389 | name = "config" 390 | version = "0.13.3" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" 393 | dependencies = [ 394 | "async-trait", 395 | "json5", 396 | "lazy_static", 397 | "nom", 398 | "pathdiff", 399 | "ron", 400 | "rust-ini", 401 | "serde", 402 | "serde_json", 403 | "toml", 404 | "yaml-rust", 405 | ] 406 | 407 | [[package]] 408 | name = "convert_case" 409 | version = "0.4.0" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 412 | 413 | [[package]] 414 | name = "cookie" 415 | version = "0.16.2" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" 418 | dependencies = [ 419 | "percent-encoding", 420 | "time", 421 | "version_check", 422 | ] 423 | 424 | [[package]] 425 | name = "cpufeatures" 426 | version = "0.2.6" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" 429 | dependencies = [ 430 | "libc", 431 | ] 432 | 433 | [[package]] 434 | name = "crc16" 435 | version = "0.4.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff" 438 | 439 | [[package]] 440 | name = "crc32fast" 441 | version = "1.3.2" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 444 | dependencies = [ 445 | "cfg-if", 446 | ] 447 | 448 | [[package]] 449 | name = "crypto-common" 450 | version = "0.1.6" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 453 | dependencies = [ 454 | "generic-array", 455 | "typenum", 456 | ] 457 | 458 | [[package]] 459 | name = "ctor" 460 | version = "0.1.26" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" 463 | dependencies = [ 464 | "quote", 465 | "syn 1.0.109", 466 | ] 467 | 468 | [[package]] 469 | name = "derive_more" 470 | version = "0.99.17" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 473 | dependencies = [ 474 | "convert_case", 475 | "proc-macro2", 476 | "quote", 477 | "rustc_version", 478 | "syn 1.0.109", 479 | ] 480 | 481 | [[package]] 482 | name = "digest" 483 | version = "0.10.6" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" 486 | dependencies = [ 487 | "block-buffer", 488 | "crypto-common", 489 | ] 490 | 491 | [[package]] 492 | name = "dlv-list" 493 | version = "0.3.0" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" 496 | 497 | [[package]] 498 | name = "dtoa" 499 | version = "1.0.6" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "65d09067bfacaa79114679b279d7f5885b53295b1e2cfb4e79c8e4bd3d633169" 502 | 503 | [[package]] 504 | name = "encoding_rs" 505 | version = "0.8.32" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" 508 | dependencies = [ 509 | "cfg-if", 510 | ] 511 | 512 | [[package]] 513 | name = "erased-serde" 514 | version = "0.3.25" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "4f2b0c2380453a92ea8b6c8e5f64ecaafccddde8ceab55ff7a8ac1029f894569" 517 | dependencies = [ 518 | "serde", 519 | ] 520 | 521 | [[package]] 522 | name = "fast-float" 523 | version = "0.2.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "95765f67b4b18863968b4a1bd5bb576f732b29a4a28c7cd84c09fa3e2875f33c" 526 | 527 | [[package]] 528 | name = "flate2" 529 | version = "1.0.25" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" 532 | dependencies = [ 533 | "crc32fast", 534 | "miniz_oxide", 535 | ] 536 | 537 | [[package]] 538 | name = "fnv" 539 | version = "1.0.7" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 542 | 543 | [[package]] 544 | name = "form_urlencoded" 545 | version = "1.1.0" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 548 | dependencies = [ 549 | "percent-encoding", 550 | ] 551 | 552 | [[package]] 553 | name = "futures-channel" 554 | version = "0.3.28" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" 557 | dependencies = [ 558 | "futures-core", 559 | "futures-sink", 560 | ] 561 | 562 | [[package]] 563 | name = "futures-core" 564 | version = "0.3.28" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" 567 | 568 | [[package]] 569 | name = "futures-macro" 570 | version = "0.3.28" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" 573 | dependencies = [ 574 | "proc-macro2", 575 | "quote", 576 | "syn 2.0.13", 577 | ] 578 | 579 | [[package]] 580 | name = "futures-sink" 581 | version = "0.3.28" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" 584 | 585 | [[package]] 586 | name = "futures-task" 587 | version = "0.3.28" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" 590 | 591 | [[package]] 592 | name = "futures-util" 593 | version = "0.3.28" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" 596 | dependencies = [ 597 | "futures-channel", 598 | "futures-core", 599 | "futures-macro", 600 | "futures-sink", 601 | "futures-task", 602 | "pin-project-lite", 603 | "pin-utils", 604 | "slab", 605 | ] 606 | 607 | [[package]] 608 | name = "generic-array" 609 | version = "0.14.7" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 612 | dependencies = [ 613 | "typenum", 614 | "version_check", 615 | ] 616 | 617 | [[package]] 618 | name = "getrandom" 619 | version = "0.2.8" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 622 | dependencies = [ 623 | "cfg-if", 624 | "libc", 625 | "wasi", 626 | ] 627 | 628 | [[package]] 629 | name = "h2" 630 | version = "0.3.16" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" 633 | dependencies = [ 634 | "bytes", 635 | "fnv", 636 | "futures-core", 637 | "futures-sink", 638 | "futures-util", 639 | "http", 640 | "indexmap", 641 | "slab", 642 | "tokio", 643 | "tokio-util", 644 | "tracing", 645 | ] 646 | 647 | [[package]] 648 | name = "hashbrown" 649 | version = "0.12.3" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 652 | dependencies = [ 653 | "ahash 0.7.6", 654 | ] 655 | 656 | [[package]] 657 | name = "hermit-abi" 658 | version = "0.2.6" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 661 | dependencies = [ 662 | "libc", 663 | ] 664 | 665 | [[package]] 666 | name = "http" 667 | version = "0.2.9" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" 670 | dependencies = [ 671 | "bytes", 672 | "fnv", 673 | "itoa", 674 | ] 675 | 676 | [[package]] 677 | name = "httparse" 678 | version = "1.8.0" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 681 | 682 | [[package]] 683 | name = "httpdate" 684 | version = "1.0.2" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 687 | 688 | [[package]] 689 | name = "idna" 690 | version = "0.3.0" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 693 | dependencies = [ 694 | "unicode-bidi", 695 | "unicode-normalization", 696 | ] 697 | 698 | [[package]] 699 | name = "indexmap" 700 | version = "1.9.3" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 703 | dependencies = [ 704 | "autocfg", 705 | "hashbrown", 706 | ] 707 | 708 | [[package]] 709 | name = "itoa" 710 | version = "1.0.6" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 713 | 714 | [[package]] 715 | name = "jobserver" 716 | version = "0.1.26" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" 719 | dependencies = [ 720 | "libc", 721 | ] 722 | 723 | [[package]] 724 | name = "js-sys" 725 | version = "0.3.61" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" 728 | dependencies = [ 729 | "wasm-bindgen", 730 | ] 731 | 732 | [[package]] 733 | name = "json5" 734 | version = "0.4.1" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" 737 | dependencies = [ 738 | "pest", 739 | "pest_derive", 740 | "serde", 741 | ] 742 | 743 | [[package]] 744 | name = "language-tags" 745 | version = "0.3.2" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" 748 | 749 | [[package]] 750 | name = "lazy_static" 751 | version = "1.4.0" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 754 | 755 | [[package]] 756 | name = "libc" 757 | version = "0.2.140" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" 760 | 761 | [[package]] 762 | name = "linked-hash-map" 763 | version = "0.5.6" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 766 | 767 | [[package]] 768 | name = "local-channel" 769 | version = "0.1.3" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" 772 | dependencies = [ 773 | "futures-core", 774 | "futures-sink", 775 | "futures-util", 776 | "local-waker", 777 | ] 778 | 779 | [[package]] 780 | name = "local-waker" 781 | version = "0.1.3" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" 784 | 785 | [[package]] 786 | name = "lock_api" 787 | version = "0.4.9" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 790 | dependencies = [ 791 | "autocfg", 792 | "scopeguard", 793 | ] 794 | 795 | [[package]] 796 | name = "log" 797 | version = "0.4.17" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 800 | dependencies = [ 801 | "cfg-if", 802 | "serde", 803 | "value-bag", 804 | ] 805 | 806 | [[package]] 807 | name = "memchr" 808 | version = "2.5.0" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 811 | 812 | [[package]] 813 | name = "mime" 814 | version = "0.3.17" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 817 | 818 | [[package]] 819 | name = "minimal-lexical" 820 | version = "0.2.1" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 823 | 824 | [[package]] 825 | name = "miniz_oxide" 826 | version = "0.6.2" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" 829 | dependencies = [ 830 | "adler", 831 | ] 832 | 833 | [[package]] 834 | name = "mio" 835 | version = "0.8.6" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" 838 | dependencies = [ 839 | "libc", 840 | "log", 841 | "wasi", 842 | "windows-sys", 843 | ] 844 | 845 | [[package]] 846 | name = "nom" 847 | version = "7.1.3" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 850 | dependencies = [ 851 | "memchr", 852 | "minimal-lexical", 853 | ] 854 | 855 | [[package]] 856 | name = "num-traits" 857 | version = "0.2.15" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 860 | dependencies = [ 861 | "autocfg", 862 | ] 863 | 864 | [[package]] 865 | name = "num_cpus" 866 | version = "1.15.0" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 869 | dependencies = [ 870 | "hermit-abi", 871 | "libc", 872 | ] 873 | 874 | [[package]] 875 | name = "once_cell" 876 | version = "1.17.1" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 879 | 880 | [[package]] 881 | name = "ordered-multimap" 882 | version = "0.4.3" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" 885 | dependencies = [ 886 | "dlv-list", 887 | "hashbrown", 888 | ] 889 | 890 | [[package]] 891 | name = "parking_lot" 892 | version = "0.12.1" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 895 | dependencies = [ 896 | "lock_api", 897 | "parking_lot_core", 898 | ] 899 | 900 | [[package]] 901 | name = "parking_lot_core" 902 | version = "0.9.7" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" 905 | dependencies = [ 906 | "cfg-if", 907 | "libc", 908 | "redox_syscall", 909 | "smallvec", 910 | "windows-sys", 911 | ] 912 | 913 | [[package]] 914 | name = "paste" 915 | version = "1.0.12" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" 918 | 919 | [[package]] 920 | name = "pathdiff" 921 | version = "0.2.1" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" 924 | 925 | [[package]] 926 | name = "percent-encoding" 927 | version = "2.2.0" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 930 | 931 | [[package]] 932 | name = "pest" 933 | version = "2.5.7" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "7b1403e8401ad5dedea73c626b99758535b342502f8d1e361f4a2dd952749122" 936 | dependencies = [ 937 | "thiserror", 938 | "ucd-trie", 939 | ] 940 | 941 | [[package]] 942 | name = "pest_derive" 943 | version = "2.5.7" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "be99c4c1d2fc2769b1d00239431d711d08f6efedcecb8b6e30707160aee99c15" 946 | dependencies = [ 947 | "pest", 948 | "pest_generator", 949 | ] 950 | 951 | [[package]] 952 | name = "pest_generator" 953 | version = "2.5.7" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "e56094789873daa36164de2e822b3888c6ae4b4f9da555a1103587658c805b1e" 956 | dependencies = [ 957 | "pest", 958 | "pest_meta", 959 | "proc-macro2", 960 | "quote", 961 | "syn 2.0.13", 962 | ] 963 | 964 | [[package]] 965 | name = "pest_meta" 966 | version = "2.5.7" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "6733073c7cff3d8459fda0e42f13a047870242aed8b509fe98000928975f359e" 969 | dependencies = [ 970 | "once_cell", 971 | "pest", 972 | "sha2", 973 | ] 974 | 975 | [[package]] 976 | name = "pin-project-lite" 977 | version = "0.2.9" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 980 | 981 | [[package]] 982 | name = "pin-utils" 983 | version = "0.1.0" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 986 | 987 | [[package]] 988 | name = "pkg-config" 989 | version = "0.3.26" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 992 | 993 | [[package]] 994 | name = "ppv-lite86" 995 | version = "0.2.17" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 998 | 999 | [[package]] 1000 | name = "proc-macro2" 1001 | version = "1.0.56" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" 1004 | dependencies = [ 1005 | "unicode-ident", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "quote" 1010 | version = "1.0.26" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" 1013 | dependencies = [ 1014 | "proc-macro2", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "rand" 1019 | version = "0.8.5" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1022 | dependencies = [ 1023 | "libc", 1024 | "rand_chacha", 1025 | "rand_core", 1026 | ] 1027 | 1028 | [[package]] 1029 | name = "rand_chacha" 1030 | version = "0.3.1" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1033 | dependencies = [ 1034 | "ppv-lite86", 1035 | "rand_core", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "rand_core" 1040 | version = "0.6.4" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1043 | dependencies = [ 1044 | "getrandom", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "redlimit" 1049 | version = "0.2.10" 1050 | dependencies = [ 1051 | "actix-utils", 1052 | "actix-web", 1053 | "anyhow", 1054 | "async-trait", 1055 | "bb8", 1056 | "config", 1057 | "futures-core", 1058 | "log", 1059 | "rustis", 1060 | "rustls", 1061 | "rustls-pemfile", 1062 | "serde", 1063 | "serde_json", 1064 | "structured-logger", 1065 | "tokio", 1066 | "tokio-util", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "redox_syscall" 1071 | version = "0.2.16" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 1074 | dependencies = [ 1075 | "bitflags", 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "regex" 1080 | version = "1.7.3" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" 1083 | dependencies = [ 1084 | "aho-corasick", 1085 | "memchr", 1086 | "regex-syntax", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "regex-syntax" 1091 | version = "0.6.29" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1094 | 1095 | [[package]] 1096 | name = "ring" 1097 | version = "0.16.20" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" 1100 | dependencies = [ 1101 | "cc", 1102 | "libc", 1103 | "once_cell", 1104 | "spin", 1105 | "untrusted", 1106 | "web-sys", 1107 | "winapi", 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "ron" 1112 | version = "0.7.1" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" 1115 | dependencies = [ 1116 | "base64 0.13.1", 1117 | "bitflags", 1118 | "serde", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "rust-ini" 1123 | version = "0.18.0" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" 1126 | dependencies = [ 1127 | "cfg-if", 1128 | "ordered-multimap", 1129 | ] 1130 | 1131 | [[package]] 1132 | name = "rustc_version" 1133 | version = "0.4.0" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 1136 | dependencies = [ 1137 | "semver", 1138 | ] 1139 | 1140 | [[package]] 1141 | name = "rustis" 1142 | version = "0.10.2" 1143 | source = "registry+https://github.com/rust-lang/crates.io-index" 1144 | checksum = "1e0f8d3c24d30ec69b0e3823104eb59c116c860074def8f0478d7b490cf1f2f6" 1145 | dependencies = [ 1146 | "atoi", 1147 | "bb8", 1148 | "bytes", 1149 | "crc16", 1150 | "dtoa", 1151 | "fast-float", 1152 | "futures-channel", 1153 | "futures-util", 1154 | "itoa", 1155 | "log", 1156 | "memchr", 1157 | "rand", 1158 | "serde", 1159 | "smallvec", 1160 | "socket2", 1161 | "tokio", 1162 | "tokio-util", 1163 | "url", 1164 | ] 1165 | 1166 | [[package]] 1167 | name = "rustls" 1168 | version = "0.20.8" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" 1171 | dependencies = [ 1172 | "log", 1173 | "ring", 1174 | "sct", 1175 | "webpki", 1176 | ] 1177 | 1178 | [[package]] 1179 | name = "rustls-pemfile" 1180 | version = "1.0.2" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" 1183 | dependencies = [ 1184 | "base64 0.21.0", 1185 | ] 1186 | 1187 | [[package]] 1188 | name = "ryu" 1189 | version = "1.0.13" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 1192 | 1193 | [[package]] 1194 | name = "scopeguard" 1195 | version = "1.1.0" 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" 1197 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1198 | 1199 | [[package]] 1200 | name = "sct" 1201 | version = "0.7.0" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" 1204 | dependencies = [ 1205 | "ring", 1206 | "untrusted", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "semver" 1211 | version = "1.0.17" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" 1214 | 1215 | [[package]] 1216 | name = "serde" 1217 | version = "1.0.159" 1218 | source = "registry+https://github.com/rust-lang/crates.io-index" 1219 | checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" 1220 | dependencies = [ 1221 | "serde_derive", 1222 | ] 1223 | 1224 | [[package]] 1225 | name = "serde_derive" 1226 | version = "1.0.159" 1227 | source = "registry+https://github.com/rust-lang/crates.io-index" 1228 | checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" 1229 | dependencies = [ 1230 | "proc-macro2", 1231 | "quote", 1232 | "syn 2.0.13", 1233 | ] 1234 | 1235 | [[package]] 1236 | name = "serde_fmt" 1237 | version = "1.0.1" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "2963a69a2b3918c1dc75a45a18bd3fcd1120e31d3f59deb1b2f9b5d5ffb8baa4" 1240 | dependencies = [ 1241 | "serde", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "serde_json" 1246 | version = "1.0.95" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" 1249 | dependencies = [ 1250 | "itoa", 1251 | "ryu", 1252 | "serde", 1253 | ] 1254 | 1255 | [[package]] 1256 | name = "serde_urlencoded" 1257 | version = "0.7.1" 1258 | source = "registry+https://github.com/rust-lang/crates.io-index" 1259 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1260 | dependencies = [ 1261 | "form_urlencoded", 1262 | "itoa", 1263 | "ryu", 1264 | "serde", 1265 | ] 1266 | 1267 | [[package]] 1268 | name = "sha1" 1269 | version = "0.10.5" 1270 | source = "registry+https://github.com/rust-lang/crates.io-index" 1271 | checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" 1272 | dependencies = [ 1273 | "cfg-if", 1274 | "cpufeatures", 1275 | "digest", 1276 | ] 1277 | 1278 | [[package]] 1279 | name = "sha2" 1280 | version = "0.10.6" 1281 | source = "registry+https://github.com/rust-lang/crates.io-index" 1282 | checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" 1283 | dependencies = [ 1284 | "cfg-if", 1285 | "cpufeatures", 1286 | "digest", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "signal-hook-registry" 1291 | version = "1.4.1" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 1294 | dependencies = [ 1295 | "libc", 1296 | ] 1297 | 1298 | [[package]] 1299 | name = "slab" 1300 | version = "0.4.8" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" 1303 | dependencies = [ 1304 | "autocfg", 1305 | ] 1306 | 1307 | [[package]] 1308 | name = "smallvec" 1309 | version = "1.10.0" 1310 | source = "registry+https://github.com/rust-lang/crates.io-index" 1311 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 1312 | dependencies = [ 1313 | "serde", 1314 | ] 1315 | 1316 | [[package]] 1317 | name = "socket2" 1318 | version = "0.4.9" 1319 | source = "registry+https://github.com/rust-lang/crates.io-index" 1320 | checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" 1321 | dependencies = [ 1322 | "libc", 1323 | "winapi", 1324 | ] 1325 | 1326 | [[package]] 1327 | name = "spin" 1328 | version = "0.5.2" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 1331 | 1332 | [[package]] 1333 | name = "structured-logger" 1334 | version = "0.5.3" 1335 | source = "registry+https://github.com/rust-lang/crates.io-index" 1336 | checksum = "80aa05a2109721d16a570f252689bfbe268b6631b3463d525e4a7f063f4aed80" 1337 | dependencies = [ 1338 | "log", 1339 | "parking_lot", 1340 | "serde", 1341 | "serde_json", 1342 | "tokio", 1343 | ] 1344 | 1345 | [[package]] 1346 | name = "sval" 1347 | version = "1.0.0-alpha.5" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "45f6ee7c7b87caf59549e9fe45d6a69c75c8019e79e212a835c5da0e92f0ba08" 1350 | dependencies = [ 1351 | "serde", 1352 | ] 1353 | 1354 | [[package]] 1355 | name = "syn" 1356 | version = "1.0.109" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1359 | dependencies = [ 1360 | "proc-macro2", 1361 | "quote", 1362 | "unicode-ident", 1363 | ] 1364 | 1365 | [[package]] 1366 | name = "syn" 1367 | version = "2.0.13" 1368 | source = "registry+https://github.com/rust-lang/crates.io-index" 1369 | checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" 1370 | dependencies = [ 1371 | "proc-macro2", 1372 | "quote", 1373 | "unicode-ident", 1374 | ] 1375 | 1376 | [[package]] 1377 | name = "thiserror" 1378 | version = "1.0.40" 1379 | source = "registry+https://github.com/rust-lang/crates.io-index" 1380 | checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" 1381 | dependencies = [ 1382 | "thiserror-impl", 1383 | ] 1384 | 1385 | [[package]] 1386 | name = "thiserror-impl" 1387 | version = "1.0.40" 1388 | source = "registry+https://github.com/rust-lang/crates.io-index" 1389 | checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" 1390 | dependencies = [ 1391 | "proc-macro2", 1392 | "quote", 1393 | "syn 2.0.13", 1394 | ] 1395 | 1396 | [[package]] 1397 | name = "time" 1398 | version = "0.3.20" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" 1401 | dependencies = [ 1402 | "itoa", 1403 | "serde", 1404 | "time-core", 1405 | "time-macros", 1406 | ] 1407 | 1408 | [[package]] 1409 | name = "time-core" 1410 | version = "0.1.0" 1411 | source = "registry+https://github.com/rust-lang/crates.io-index" 1412 | checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" 1413 | 1414 | [[package]] 1415 | name = "time-macros" 1416 | version = "0.2.8" 1417 | source = "registry+https://github.com/rust-lang/crates.io-index" 1418 | checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" 1419 | dependencies = [ 1420 | "time-core", 1421 | ] 1422 | 1423 | [[package]] 1424 | name = "tinyvec" 1425 | version = "1.6.0" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1428 | dependencies = [ 1429 | "tinyvec_macros", 1430 | ] 1431 | 1432 | [[package]] 1433 | name = "tinyvec_macros" 1434 | version = "0.1.1" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1437 | 1438 | [[package]] 1439 | name = "tokio" 1440 | version = "1.27.0" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" 1443 | dependencies = [ 1444 | "autocfg", 1445 | "bytes", 1446 | "libc", 1447 | "mio", 1448 | "num_cpus", 1449 | "parking_lot", 1450 | "pin-project-lite", 1451 | "signal-hook-registry", 1452 | "socket2", 1453 | "tokio-macros", 1454 | "windows-sys", 1455 | ] 1456 | 1457 | [[package]] 1458 | name = "tokio-macros" 1459 | version = "2.0.0" 1460 | source = "registry+https://github.com/rust-lang/crates.io-index" 1461 | checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" 1462 | dependencies = [ 1463 | "proc-macro2", 1464 | "quote", 1465 | "syn 2.0.13", 1466 | ] 1467 | 1468 | [[package]] 1469 | name = "tokio-rustls" 1470 | version = "0.23.4" 1471 | source = "registry+https://github.com/rust-lang/crates.io-index" 1472 | checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" 1473 | dependencies = [ 1474 | "rustls", 1475 | "tokio", 1476 | "webpki", 1477 | ] 1478 | 1479 | [[package]] 1480 | name = "tokio-util" 1481 | version = "0.7.7" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" 1484 | dependencies = [ 1485 | "bytes", 1486 | "futures-core", 1487 | "futures-sink", 1488 | "pin-project-lite", 1489 | "tokio", 1490 | "tracing", 1491 | ] 1492 | 1493 | [[package]] 1494 | name = "toml" 1495 | version = "0.5.11" 1496 | source = "registry+https://github.com/rust-lang/crates.io-index" 1497 | checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" 1498 | dependencies = [ 1499 | "serde", 1500 | ] 1501 | 1502 | [[package]] 1503 | name = "tracing" 1504 | version = "0.1.37" 1505 | source = "registry+https://github.com/rust-lang/crates.io-index" 1506 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 1507 | dependencies = [ 1508 | "cfg-if", 1509 | "log", 1510 | "pin-project-lite", 1511 | "tracing-core", 1512 | ] 1513 | 1514 | [[package]] 1515 | name = "tracing-core" 1516 | version = "0.1.30" 1517 | source = "registry+https://github.com/rust-lang/crates.io-index" 1518 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 1519 | dependencies = [ 1520 | "once_cell", 1521 | ] 1522 | 1523 | [[package]] 1524 | name = "typenum" 1525 | version = "1.16.0" 1526 | source = "registry+https://github.com/rust-lang/crates.io-index" 1527 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 1528 | 1529 | [[package]] 1530 | name = "ucd-trie" 1531 | version = "0.1.5" 1532 | source = "registry+https://github.com/rust-lang/crates.io-index" 1533 | checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" 1534 | 1535 | [[package]] 1536 | name = "unicode-bidi" 1537 | version = "0.3.13" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 1540 | 1541 | [[package]] 1542 | name = "unicode-ident" 1543 | version = "1.0.8" 1544 | source = "registry+https://github.com/rust-lang/crates.io-index" 1545 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 1546 | 1547 | [[package]] 1548 | name = "unicode-normalization" 1549 | version = "0.1.22" 1550 | source = "registry+https://github.com/rust-lang/crates.io-index" 1551 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1552 | dependencies = [ 1553 | "tinyvec", 1554 | ] 1555 | 1556 | [[package]] 1557 | name = "untrusted" 1558 | version = "0.7.1" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 1561 | 1562 | [[package]] 1563 | name = "url" 1564 | version = "2.3.1" 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" 1566 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 1567 | dependencies = [ 1568 | "form_urlencoded", 1569 | "idna", 1570 | "percent-encoding", 1571 | ] 1572 | 1573 | [[package]] 1574 | name = "value-bag" 1575 | version = "1.0.0-alpha.9" 1576 | source = "registry+https://github.com/rust-lang/crates.io-index" 1577 | checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" 1578 | dependencies = [ 1579 | "ctor", 1580 | "erased-serde", 1581 | "serde", 1582 | "serde_fmt", 1583 | "sval", 1584 | "version_check", 1585 | ] 1586 | 1587 | [[package]] 1588 | name = "version_check" 1589 | version = "0.9.4" 1590 | source = "registry+https://github.com/rust-lang/crates.io-index" 1591 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1592 | 1593 | [[package]] 1594 | name = "wasi" 1595 | version = "0.11.0+wasi-snapshot-preview1" 1596 | source = "registry+https://github.com/rust-lang/crates.io-index" 1597 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1598 | 1599 | [[package]] 1600 | name = "wasm-bindgen" 1601 | version = "0.2.84" 1602 | source = "registry+https://github.com/rust-lang/crates.io-index" 1603 | checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" 1604 | dependencies = [ 1605 | "cfg-if", 1606 | "wasm-bindgen-macro", 1607 | ] 1608 | 1609 | [[package]] 1610 | name = "wasm-bindgen-backend" 1611 | version = "0.2.84" 1612 | source = "registry+https://github.com/rust-lang/crates.io-index" 1613 | checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" 1614 | dependencies = [ 1615 | "bumpalo", 1616 | "log", 1617 | "once_cell", 1618 | "proc-macro2", 1619 | "quote", 1620 | "syn 1.0.109", 1621 | "wasm-bindgen-shared", 1622 | ] 1623 | 1624 | [[package]] 1625 | name = "wasm-bindgen-macro" 1626 | version = "0.2.84" 1627 | source = "registry+https://github.com/rust-lang/crates.io-index" 1628 | checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" 1629 | dependencies = [ 1630 | "quote", 1631 | "wasm-bindgen-macro-support", 1632 | ] 1633 | 1634 | [[package]] 1635 | name = "wasm-bindgen-macro-support" 1636 | version = "0.2.84" 1637 | source = "registry+https://github.com/rust-lang/crates.io-index" 1638 | checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" 1639 | dependencies = [ 1640 | "proc-macro2", 1641 | "quote", 1642 | "syn 1.0.109", 1643 | "wasm-bindgen-backend", 1644 | "wasm-bindgen-shared", 1645 | ] 1646 | 1647 | [[package]] 1648 | name = "wasm-bindgen-shared" 1649 | version = "0.2.84" 1650 | source = "registry+https://github.com/rust-lang/crates.io-index" 1651 | checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" 1652 | 1653 | [[package]] 1654 | name = "web-sys" 1655 | version = "0.3.61" 1656 | source = "registry+https://github.com/rust-lang/crates.io-index" 1657 | checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" 1658 | dependencies = [ 1659 | "js-sys", 1660 | "wasm-bindgen", 1661 | ] 1662 | 1663 | [[package]] 1664 | name = "webpki" 1665 | version = "0.22.0" 1666 | source = "registry+https://github.com/rust-lang/crates.io-index" 1667 | checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" 1668 | dependencies = [ 1669 | "ring", 1670 | "untrusted", 1671 | ] 1672 | 1673 | [[package]] 1674 | name = "webpki-roots" 1675 | version = "0.22.6" 1676 | source = "registry+https://github.com/rust-lang/crates.io-index" 1677 | checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" 1678 | dependencies = [ 1679 | "webpki", 1680 | ] 1681 | 1682 | [[package]] 1683 | name = "winapi" 1684 | version = "0.3.9" 1685 | source = "registry+https://github.com/rust-lang/crates.io-index" 1686 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1687 | dependencies = [ 1688 | "winapi-i686-pc-windows-gnu", 1689 | "winapi-x86_64-pc-windows-gnu", 1690 | ] 1691 | 1692 | [[package]] 1693 | name = "winapi-i686-pc-windows-gnu" 1694 | version = "0.4.0" 1695 | source = "registry+https://github.com/rust-lang/crates.io-index" 1696 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1697 | 1698 | [[package]] 1699 | name = "winapi-x86_64-pc-windows-gnu" 1700 | version = "0.4.0" 1701 | source = "registry+https://github.com/rust-lang/crates.io-index" 1702 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1703 | 1704 | [[package]] 1705 | name = "windows-sys" 1706 | version = "0.45.0" 1707 | source = "registry+https://github.com/rust-lang/crates.io-index" 1708 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1709 | dependencies = [ 1710 | "windows-targets", 1711 | ] 1712 | 1713 | [[package]] 1714 | name = "windows-targets" 1715 | version = "0.42.2" 1716 | source = "registry+https://github.com/rust-lang/crates.io-index" 1717 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 1718 | dependencies = [ 1719 | "windows_aarch64_gnullvm", 1720 | "windows_aarch64_msvc", 1721 | "windows_i686_gnu", 1722 | "windows_i686_msvc", 1723 | "windows_x86_64_gnu", 1724 | "windows_x86_64_gnullvm", 1725 | "windows_x86_64_msvc", 1726 | ] 1727 | 1728 | [[package]] 1729 | name = "windows_aarch64_gnullvm" 1730 | version = "0.42.2" 1731 | source = "registry+https://github.com/rust-lang/crates.io-index" 1732 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 1733 | 1734 | [[package]] 1735 | name = "windows_aarch64_msvc" 1736 | version = "0.42.2" 1737 | source = "registry+https://github.com/rust-lang/crates.io-index" 1738 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1739 | 1740 | [[package]] 1741 | name = "windows_i686_gnu" 1742 | version = "0.42.2" 1743 | source = "registry+https://github.com/rust-lang/crates.io-index" 1744 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1745 | 1746 | [[package]] 1747 | name = "windows_i686_msvc" 1748 | version = "0.42.2" 1749 | source = "registry+https://github.com/rust-lang/crates.io-index" 1750 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1751 | 1752 | [[package]] 1753 | name = "windows_x86_64_gnu" 1754 | version = "0.42.2" 1755 | source = "registry+https://github.com/rust-lang/crates.io-index" 1756 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1757 | 1758 | [[package]] 1759 | name = "windows_x86_64_gnullvm" 1760 | version = "0.42.2" 1761 | source = "registry+https://github.com/rust-lang/crates.io-index" 1762 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1763 | 1764 | [[package]] 1765 | name = "windows_x86_64_msvc" 1766 | version = "0.42.2" 1767 | source = "registry+https://github.com/rust-lang/crates.io-index" 1768 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1769 | 1770 | [[package]] 1771 | name = "yaml-rust" 1772 | version = "0.4.5" 1773 | source = "registry+https://github.com/rust-lang/crates.io-index" 1774 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 1775 | dependencies = [ 1776 | "linked-hash-map", 1777 | ] 1778 | 1779 | [[package]] 1780 | name = "zstd" 1781 | version = "0.12.3+zstd.1.5.2" 1782 | source = "registry+https://github.com/rust-lang/crates.io-index" 1783 | checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" 1784 | dependencies = [ 1785 | "zstd-safe", 1786 | ] 1787 | 1788 | [[package]] 1789 | name = "zstd-safe" 1790 | version = "6.0.4+zstd.1.5.4" 1791 | source = "registry+https://github.com/rust-lang/crates.io-index" 1792 | checksum = "7afb4b54b8910cf5447638cb54bf4e8a65cbedd783af98b98c62ffe91f185543" 1793 | dependencies = [ 1794 | "libc", 1795 | "zstd-sys", 1796 | ] 1797 | 1798 | [[package]] 1799 | name = "zstd-sys" 1800 | version = "2.0.7+zstd.1.5.4" 1801 | source = "registry+https://github.com/rust-lang/crates.io-index" 1802 | checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5" 1803 | dependencies = [ 1804 | "cc", 1805 | "libc", 1806 | "pkg-config", 1807 | ] 1808 | --------------------------------------------------------------------------------