├── .github └── workflows │ └── publish.yml ├── .gitignore ├── Cargo.toml ├── LICENSE.md ├── README.md └── src └── lib.rs /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to crates.io 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - id: get_version 18 | name: Get version 19 | uses: battila7/get-version-action@v2 20 | 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | override: true 25 | 26 | # TODO: figure out if we need caching 27 | - uses: Swatinem/rust-cache@v1 28 | 29 | - name: Bump crate version 30 | uses: thomaseizinger/set-crate-version@master 31 | with: 32 | version: ${{ steps.get_version.outputs.version-without-v }} 33 | 34 | - uses: stefanzweifel/git-auto-commit-action@v4 35 | name: Commit Cargo.toml version change 36 | with: 37 | commit_message: Bumped crate version to ${{ steps.get_version.outputs.version-without-v }} 38 | branch: main 39 | file_pattern: Cargo.toml 40 | 41 | - name: Publish crate 42 | uses: katyo/publish-crates@v1 43 | with: 44 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tide-governor" 3 | description = "A rate-limiting middleware for tide" 4 | homepage = "https://github.com/ohmree/tide-governor" 5 | repository = "https://github.com/ohmree/tide-governor" 6 | documentation = "https://docs.rs/tide-governor" 7 | version = "1.0.3" 8 | authors = ["OhmRee <13455401+ohmree@users.noreply.github.com>"] 9 | edition = "2018" 10 | license = "MIT" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | governor = "0.6.0" 16 | lazy_static = "1.4.0" 17 | tide = { version = "0.16", default-features = false } 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © `2020` `Ohmree` 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tide-governor 2 | 3 | A [tide] middleware that provides rate-limiting functionality backed by [governor] 4 | 5 | # Example 6 | ```rust 7 | use tide_governor::GovernorMiddleware; 8 | use std::env; 9 | 10 | #[async_std::main] 11 | async fn main() -> tide::Result<()> { 12 | let mut app = tide::new(); 13 | app.at("/") 14 | .with(GovernorMiddleware::per_minute(4)?) 15 | .get(|_| async move { todo!() }); 16 | app.at("/foo/:bar") 17 | .with(GovernorMiddleware::per_hour(360)?) 18 | .put(|_| async move { todo!() }); 19 | 20 | app.listen(format!("http://localhost:{}", env::var("PORT")?)) 21 | .await?; 22 | Ok(()) 23 | } 24 | ``` 25 | 26 | [tide]: https://github.com/http-rs/tide/ 27 | [governor]: https://github.com/antifuchs/governor 28 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A [tide] middleware that implements rate-limiting using [governor]. 2 | //! # Example 3 | //! ```rust 4 | //! use tide_governor::GovernorMiddleware; 5 | //! use std::env; 6 | //! 7 | //! #[async_std::main] 8 | //! async fn main() -> tide::Result<()> { 9 | //! let mut app = tide::new(); 10 | //! app.at("/") 11 | //! .with(GovernorMiddleware::per_minute(4)?) 12 | //! .get(|_| async move { todo!() }); 13 | //! app.at("/foo/:bar") 14 | //! .with(GovernorMiddleware::per_hour(360)?) 15 | //! .put(|_| async move { todo!() }); 16 | //! 17 | //! app.listen(format!("http://localhost:{}", env::var("PORT")?)) 18 | //! .await?; 19 | //! Ok(()) 20 | //! } 21 | //! ``` 22 | //! [tide]: https://github.com/http-rs/tide 23 | //! [governor]: https://github.com/antifuchs/governor 24 | 25 | // TODO: figure out how to add jitter support using `governor::Jitter`. 26 | // TODO: add usage examples (both in the docs and in an examples directory). 27 | // TODO: add unit tests. 28 | use governor::{ 29 | clock::{Clock, DefaultClock}, 30 | state::keyed::DefaultKeyedStateStore, 31 | Quota, RateLimiter, 32 | }; 33 | use lazy_static::lazy_static; 34 | use std::{ 35 | convert::TryInto, 36 | error::Error, 37 | net::{IpAddr, SocketAddr}, 38 | num::NonZeroU32, 39 | sync::Arc, 40 | time::Duration, 41 | }; 42 | use tide::{ 43 | http::StatusCode, 44 | log::{debug, trace}, 45 | utils::async_trait, 46 | Middleware, Next, Request, Response, Result, 47 | }; 48 | 49 | lazy_static! { 50 | static ref CLOCK: DefaultClock = DefaultClock::default(); 51 | } 52 | 53 | /// Once the rate limit has been reached, the middleware will respond with 54 | /// status code 429 (too many requests) and a `Retry-After` header with the amount 55 | /// of time that needs to pass before another request will be allowed. 56 | #[derive(Debug, Clone)] 57 | pub struct GovernorMiddleware { 58 | limiter: Arc, DefaultClock>>, 59 | } 60 | 61 | impl GovernorMiddleware { 62 | /// Constructs a rate-limiting middleware from a [`Duration`] that allows one request in the given time interval. 63 | /// 64 | /// If the time interval is zero, returns `None`. 65 | #[must_use] 66 | pub fn with_period(duration: Duration) -> Option { 67 | Some(Self { 68 | limiter: Arc::new(RateLimiter::::keyed(Quota::with_period( 69 | duration, 70 | )?)), 71 | }) 72 | } 73 | 74 | /// Constructs a rate-limiting middleware that allows a specified number of requests every second. 75 | /// 76 | /// Returns an error if `times` can't be converted into a [`NonZeroU32`]. 77 | pub fn per_second(times: T) -> Result 78 | where 79 | T: TryInto, 80 | T::Error: Error + Send + Sync + 'static, 81 | { 82 | Ok(Self { 83 | limiter: Arc::new(RateLimiter::::keyed(Quota::per_second( 84 | times.try_into()?, 85 | ))), 86 | }) 87 | } 88 | 89 | /// Constructs a rate-limiting middleware that allows a specified number of requests every minute. 90 | /// 91 | /// Returns an error if `times` can't be converted into a [`NonZeroU32`]. 92 | pub fn per_minute(times: T) -> Result 93 | where 94 | T: TryInto, 95 | T::Error: Error + Send + Sync + 'static, 96 | { 97 | Ok(Self { 98 | limiter: Arc::new(RateLimiter::::keyed(Quota::per_minute( 99 | times.try_into()?, 100 | ))), 101 | }) 102 | } 103 | 104 | /// Constructs a rate-limiting middleware that allows a specified number of requests every hour. 105 | /// 106 | /// Returns an error if `times` can't be converted into a [`NonZeroU32`]. 107 | pub fn per_hour(times: T) -> Result 108 | where 109 | T: TryInto, 110 | T::Error: Error + Send + Sync + 'static, 111 | { 112 | Ok(Self { 113 | limiter: Arc::new(RateLimiter::::keyed(Quota::per_hour( 114 | times.try_into()?, 115 | ))), 116 | }) 117 | } 118 | } 119 | 120 | #[async_trait] 121 | impl Middleware for GovernorMiddleware { 122 | async fn handle(&self, req: Request, next: Next<'_, State>) -> tide::Result { 123 | let remote = req.remote().ok_or_else(|| { 124 | tide::Error::from_str( 125 | StatusCode::InternalServerError, 126 | "failed to get request remote address", 127 | ) 128 | })?; 129 | let remote: IpAddr = match remote.parse::() { 130 | Ok(r) => r.ip(), 131 | Err(_) => remote.parse()?, 132 | }; 133 | trace!("remote: {}", remote); 134 | 135 | match self.limiter.check_key(&remote) { 136 | Ok(_) => { 137 | debug!("allowing remote {}", remote); 138 | Ok(next.run(req).await) 139 | } 140 | Err(negative) => { 141 | let wait_time = negative.wait_time_from(CLOCK.now()); 142 | let res = Response::builder(StatusCode::TooManyRequests) 143 | .header( 144 | tide::http::headers::RETRY_AFTER, 145 | wait_time.as_secs().to_string(), 146 | ) 147 | .build(); 148 | debug!( 149 | "blocking address {} for {} seconds", 150 | remote, 151 | wait_time.as_secs() 152 | ); 153 | Ok(res) 154 | } 155 | } 156 | } 157 | } 158 | --------------------------------------------------------------------------------