mirror of
https://github.com/teloxide/teloxide.git
synced 2024-12-22 14:35:36 +01:00
Merge pull request #671 from teloxide/webhooks_secret_token
Add support for `secret_token` for built-in webhooks
This commit is contained in:
commit
c4c3acf742
10 changed files with 184 additions and 47 deletions
|
@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Security checks based on `secret_token` param of `set_webhook` to built-in webhooks
|
||||
|
||||
### Fixed
|
||||
|
||||
- `Dispatcher` no longer "leaks" memory for every inactive user ([PR 657](https://github.com/teloxide/teloxide/pull/657)).
|
||||
|
|
|
@ -14,7 +14,8 @@ exclude = ["media"]
|
|||
[features]
|
||||
default = ["native-tls", "ctrlc_handler", "teloxide-core/default", "auto-send"]
|
||||
|
||||
webhooks-axum = ["axum", "tower", "tower-http"]
|
||||
webhooks = ["rand"]
|
||||
webhooks-axum = ["webhooks", "axum", "tower", "tower-http"]
|
||||
|
||||
sqlite-storage = ["sqlx"]
|
||||
redis-storage = ["redis"]
|
||||
|
@ -56,7 +57,8 @@ full = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
teloxide-core = { version = "0.6.0", default-features = false }
|
||||
#teloxide-core = { version = "0.6.0", default-features = false }
|
||||
teloxide-core = { git = "https://github.com/teloxide/teloxide-core", rev = "b13393d", default-features = false }
|
||||
teloxide-macros = { version = "0.6.2", optional = true }
|
||||
|
||||
serde_json = "1.0"
|
||||
|
@ -91,6 +93,7 @@ bincode = { version = "1.3", optional = true }
|
|||
axum = { version = "0.4.8", optional = true }
|
||||
tower = { version = "0.4.12", optional = true }
|
||||
tower-http = { version = "0.2.5", features = ["trace"], optional = true }
|
||||
rand = { version = "0.8.5", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8.3"
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
/// Implementations of webhook update listeners - an alternative (to
|
||||
/// [`fn@polling`]) way of receiving updates from telegram.
|
||||
#[cfg(any(feature = "webhooks-axum"))]
|
||||
#[cfg(feature = "webhooks")]
|
||||
pub mod webhooks;
|
||||
|
||||
use futures::Stream;
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::net::SocketAddr;
|
|||
use crate::{requests::Requester, types::InputFile};
|
||||
|
||||
/// Options related to setting up webhooks.
|
||||
#[must_use]
|
||||
pub struct Options {
|
||||
/// Local address to listen to.
|
||||
pub address: SocketAddr,
|
||||
|
@ -42,13 +43,28 @@ pub struct Options {
|
|||
///
|
||||
/// Default - false.
|
||||
pub drop_pending_updates: bool,
|
||||
|
||||
/// A secret token to be sent in a header “X-Telegram-Bot-Api-Secret-Token”
|
||||
/// in every webhook request, 1-256 characters. Only characters `A-Z`,
|
||||
/// `a-z`, `0-9`, `_` and `-` are allowed. The header is useful to ensure
|
||||
/// that the request comes from a webhook set by you.
|
||||
///
|
||||
/// Default - teloxide will generate a random token.
|
||||
pub secret_token: Option<String>,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
/// Construct a new webhook options, see [`Options::address`] and
|
||||
/// [`Options::url`] for details.
|
||||
pub fn new(address: SocketAddr, url: url::Url) -> Self {
|
||||
Self { address, url, certificate: None, max_connections: None, drop_pending_updates: false }
|
||||
Self {
|
||||
address,
|
||||
url,
|
||||
certificate: None,
|
||||
max_connections: None,
|
||||
drop_pending_updates: false,
|
||||
secret_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload your public key certificate so that the root certificate in use
|
||||
|
@ -71,6 +87,32 @@ impl Options {
|
|||
pub fn drop_pending_updates(self) -> Self {
|
||||
Self { drop_pending_updates: true, ..self }
|
||||
}
|
||||
|
||||
/// A secret token to be sent in a header “X-Telegram-Bot-Api-Secret-Token”
|
||||
/// in every webhook request, 1-256 characters. Only characters `A-Z`,
|
||||
/// `a-z`, `0-9`, `_` and `-` are allowed. The header is useful to ensure
|
||||
/// that the request comes from a webhook set by you.
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// If the token is invalid.
|
||||
#[track_caller]
|
||||
pub fn secret_token(self, token: String) -> Self {
|
||||
check_secret(token.as_bytes()).expect("Invalid secret token");
|
||||
|
||||
Self { secret_token: Some(token), ..self }
|
||||
}
|
||||
|
||||
/// Returns `self.secret_token`, generating a new one if it's `None`.
|
||||
///
|
||||
/// After a call to this function `self.secret_token` is always `Some(_)`.
|
||||
///
|
||||
/// **Note**: if you leave webhook setup to teloxide, it will automatically
|
||||
/// generate a secret token. Call this function only if you need to know the
|
||||
/// secret (for example because you are calling `set_webhook` by yourself).
|
||||
pub fn get_or_gen_secret_token(&mut self) -> &str {
|
||||
self.secret_token.get_or_insert_with(gen_secret_token)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webhooks-axum")]
|
||||
|
@ -91,6 +133,7 @@ where
|
|||
use crate::requests::Request;
|
||||
use teloxide_core::requests::HasPayload;
|
||||
|
||||
let secret = options.get_or_gen_secret_token().to_owned();
|
||||
let &mut Options {
|
||||
ref url, ref mut certificate, max_connections, drop_pending_updates, ..
|
||||
} = options;
|
||||
|
@ -99,12 +142,47 @@ where
|
|||
req.payload_mut().certificate = certificate.take();
|
||||
req.payload_mut().max_connections = max_connections;
|
||||
req.payload_mut().drop_pending_updates = Some(drop_pending_updates);
|
||||
req.payload_mut().secret_token = Some(secret);
|
||||
|
||||
req.send().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generates a random string consisting of 32 characters (`a-z`, `A-Z`, `0-9`,
|
||||
/// `_` and `-`).
|
||||
fn gen_secret_token() -> String {
|
||||
use rand::{distributions::Uniform, Rng};
|
||||
const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
|
||||
const SECRET_LENGTH: usize = 32;
|
||||
|
||||
let random = rand::thread_rng()
|
||||
.sample_iter(Uniform::new(0, CHARSET.len()))
|
||||
.map(|idx| CHARSET[idx] as char)
|
||||
.take(SECRET_LENGTH);
|
||||
|
||||
let mut secret = String::with_capacity(SECRET_LENGTH);
|
||||
secret.extend(random);
|
||||
|
||||
secret
|
||||
}
|
||||
|
||||
fn check_secret(bytes: &[u8]) -> Result<&[u8], &'static str> {
|
||||
let len = bytes.len();
|
||||
|
||||
if !(1..=256).contains(&len) {
|
||||
return Err("secret token length must be in range 1..=256");
|
||||
}
|
||||
|
||||
let is_not_supported =
|
||||
|c: &_| !matches!(c, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-');
|
||||
if bytes.iter().any(is_not_supported) {
|
||||
return Err("secret token must only contain of `a-z`, `A-Z`, `0-9`, `_` and `-` characters");
|
||||
}
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Returns first (`.0`) field from a tuple as a `&mut` reference.
|
||||
///
|
||||
/// This hack is needed because there isn't currently a way to easily force a
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use std::convert::Infallible;
|
||||
use std::{convert::Infallible, future::Future, pin::Pin};
|
||||
|
||||
use axum::{
|
||||
extract::{FromRequest, RequestParts},
|
||||
http::status::StatusCode,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
dispatching::{
|
||||
stop_token::{AsyncStopFlag, StopToken},
|
||||
update_listeners::{
|
||||
webhooks::{setup_webhook, tuple_first_mut, Options},
|
||||
UpdateListener,
|
||||
},
|
||||
update_listeners::{webhooks::Options, UpdateListener},
|
||||
},
|
||||
requests::Requester,
|
||||
};
|
||||
|
@ -105,15 +107,12 @@ where
|
|||
pub async fn axum_to_router<R>(
|
||||
bot: R,
|
||||
mut options: Options,
|
||||
) -> Result<
|
||||
(impl UpdateListener<Infallible>, impl std::future::Future<Output = ()> + Send, axum::Router),
|
||||
R::Err,
|
||||
>
|
||||
) -> Result<(impl UpdateListener<Infallible>, impl Future<Output = ()> + Send, axum::Router), R::Err>
|
||||
where
|
||||
R: Requester + Send,
|
||||
<R as Requester>::DeleteWebhook: Send,
|
||||
{
|
||||
use crate::requests::Request;
|
||||
use crate::{dispatching::update_listeners::webhooks::setup_webhook, requests::Request};
|
||||
use futures::FutureExt;
|
||||
|
||||
setup_webhook(&bot, &mut options).await?;
|
||||
|
@ -149,12 +148,15 @@ where
|
|||
/// function.
|
||||
pub fn axum_no_setup(
|
||||
options: Options,
|
||||
) -> (impl UpdateListener<Infallible>, impl std::future::Future<Output = ()>, axum::Router) {
|
||||
) -> (impl UpdateListener<Infallible>, impl Future<Output = ()>, axum::Router) {
|
||||
use crate::{
|
||||
dispatching::{stop_token::AsyncStopToken, update_listeners},
|
||||
dispatching::{
|
||||
stop_token::AsyncStopToken,
|
||||
update_listeners::{self, webhooks::tuple_first_mut},
|
||||
},
|
||||
types::Update,
|
||||
};
|
||||
use axum::{extract::Extension, http::StatusCode, response::IntoResponse, routing::post};
|
||||
use axum::{extract::Extension, response::IntoResponse, routing::post};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
use tower::ServiceBuilder;
|
||||
|
@ -167,9 +169,16 @@ pub fn axum_no_setup(
|
|||
|
||||
async fn telegram_request(
|
||||
input: String,
|
||||
secret_header: XTelegramBotApiSecretToken,
|
||||
secret: Extension<Option<String>>,
|
||||
tx: Extension<CSender>,
|
||||
flag: Extension<AsyncStopFlag>,
|
||||
) -> impl IntoResponse {
|
||||
// FIXME: use constant time comparison here
|
||||
if secret_header.0.as_deref() != secret.as_deref().map(str::as_bytes) {
|
||||
return StatusCode::UNAUTHORIZED;
|
||||
}
|
||||
|
||||
let tx = match tx.get() {
|
||||
None => return StatusCode::SERVICE_UNAVAILABLE,
|
||||
// Do not process updates after `.stop()` is called even if the server is still
|
||||
|
@ -206,6 +215,7 @@ pub fn axum_no_setup(
|
|||
.layer(TraceLayer::new_for_http())
|
||||
.layer(Extension(ClosableSender::new(tx)))
|
||||
.layer(Extension(stop_flag.clone()))
|
||||
.layer(Extension(options.secret_token))
|
||||
.into_inner(),
|
||||
);
|
||||
|
||||
|
@ -245,3 +255,32 @@ impl<T> ClosableSender<T> {
|
|||
self.origin.write().unwrap().take();
|
||||
}
|
||||
}
|
||||
|
||||
struct XTelegramBotApiSecretToken(Option<Vec<u8>>);
|
||||
|
||||
impl<B> FromRequest<B> for XTelegramBotApiSecretToken {
|
||||
type Rejection = StatusCode;
|
||||
|
||||
fn from_request<'l0, 'at>(
|
||||
req: &'l0 mut RequestParts<B>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Self, Self::Rejection>> + Send + 'at>>
|
||||
where
|
||||
'l0: 'at,
|
||||
Self: 'at,
|
||||
{
|
||||
use crate::dispatching::update_listeners::webhooks::check_secret;
|
||||
|
||||
let res = req
|
||||
.headers_mut()
|
||||
.and_then(|map| map.remove("x-telegram-bot-api-secret-token"))
|
||||
.map(|header| {
|
||||
check_secret(header.as_bytes())
|
||||
.map(<_>::to_owned)
|
||||
.map_err(|_| StatusCode::BAD_REQUEST)
|
||||
})
|
||||
.transpose()
|
||||
.map(Self);
|
||||
|
||||
Box::pin(async { res }) as _
|
||||
}
|
||||
}
|
||||
|
|
34
src/features.md
Normal file
34
src/features.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
## Cargo features
|
||||
|
||||
| Feature | Description |
|
||||
|----------------------|------------------------------------------------------------------------------------|
|
||||
| `webhooks` | Enables general webhook utilities (almost useless on its own) |
|
||||
| `webhooks-axum` | Enables webhook implementation based on axum framework |
|
||||
| `macros` | Re-exports macros from [`teloxide-macros`]. |
|
||||
| `ctrlc_handler` | Enables the [`Dispatcher::setup_ctrlc_handler`] function (**enabled by default**). |
|
||||
| `auto-send` | Enables the [`AutoSend`](adaptors::AutoSend) bot adaptor (**enabled by default**). |
|
||||
| `throttle` | Enables the [`Throttle`](adaptors::Throttle) bot adaptor. |
|
||||
| `cache-me` | Enables the [`CacheMe`](adaptors::CacheMe) bot adaptor. |
|
||||
| `trace-adaptor` | Enables the [`Trace`](adaptors::Trace) bot adaptor. |
|
||||
| `erased` | Enables the [`ErasedRequester`](adaptors::ErasedRequester) bot adaptor. |
|
||||
| `full` | Enables all the features except `nightly`. |
|
||||
| `nightly` | Enables nightly-only features (see the [teloxide-core features]). |
|
||||
| `native-tls` | Enables the [`native-tls`] TLS implementation (**enabled by default**). |
|
||||
| `rustls` | Enables the [`rustls`] TLS implementation. |
|
||||
| `redis-storage` | Enables the [Redis] storage support for dialogues. |
|
||||
| `sqlite-storage` | Enables the [Sqlite] storage support for dialogues. |
|
||||
| `cbor-serializer` | Enables the [CBOR] serializer for dialogues. |
|
||||
| `bincode-serializer` | Enables the [Bincode] serializer for dialogues. |
|
||||
|
||||
|
||||
[Redis]: https://redis.io/
|
||||
[Sqlite]: https://www.sqlite.org/
|
||||
[CBOR]: https://en.wikipedia.org/wiki/CBOR
|
||||
[Bincode]: https://github.com/servo/bincode
|
||||
[`teloxide-macros`]: https://github.com/teloxide/teloxide-macros
|
||||
[`native-tls`]: https://docs.rs/native-tls
|
||||
[`rustls`]: https://docs.rs/rustls
|
||||
[`teloxide::utils::UpState`]: utils::UpState
|
||||
[teloxide-core features]: https://docs.rs/teloxide-core/latest/teloxide_core/#cargo-features
|
||||
|
||||
[`Dispatcher::setup_ctrlc_handler`]: dispatching::Dispatcher::setup_ctrlc_handler
|
|
@ -1,29 +0,0 @@
|
|||
## Cargo features
|
||||
|
||||
| Feature | Description |
|
||||
|----------|----------|
|
||||
| `redis-storage` | Enables the [Redis] storage support for dialogues.|
|
||||
| `sqlite-storage` | Enables the [Sqlite] storage support for dialogues. |
|
||||
| `cbor-serializer` | Enables the [CBOR] serializer for dialogues. |
|
||||
| `bincode-serializer` | Enables the [Bincode] serializer for dialogues. |
|
||||
| `macros` | Re-exports macros from [`teloxide-macros`]. |
|
||||
| `native-tls` | Enables the [`native-tls`] TLS implementation (enabled by default). |
|
||||
| `rustls` | Enables the [`rustls`] TLS implementation. |
|
||||
| `ctrlc_handler` | Enables the [`Dispatcher::setup_ctrlc_handler`](dispatching::Dispatcher::setup_ctrlc_handler) function. |
|
||||
| `auto-send` | Enables the [`AutoSend`](adaptors::AutoSend) bot adaptor. |
|
||||
| `throttle` | Enables the [`Throttle`](adaptors::Throttle) bot adaptor. |
|
||||
| `cache-me` | Enables the [`CacheMe`](adaptors::CacheMe) bot adaptor. |
|
||||
| `trace-adaptor` | Enables the [`Trace`](adaptors::Trace) bot adaptor. |
|
||||
| `erased` | Enables the [`ErasedRequester`](adaptors::ErasedRequester) bot adaptor. |
|
||||
| `full` | Enables all the features except `nightly`. |
|
||||
| `nightly` | Enables nightly-only features (see the [teloxide-core features]). |
|
||||
|
||||
[Redis]: https://redis.io/
|
||||
[Sqlite]: https://www.sqlite.org/
|
||||
[CBOR]: https://en.wikipedia.org/wiki/CBOR
|
||||
[Bincode]: https://github.com/servo/bincode
|
||||
[`teloxide-macros`]: https://github.com/teloxide/teloxide-macros
|
||||
[`native-tls`]: https://docs.rs/native-tls
|
||||
[`rustls`]: https://docs.rs/rustls
|
||||
[`teloxide::utils::UpState`]: utils::UpState
|
||||
[teloxide-core features]: https://docs.rs/teloxide-core/latest/teloxide_core/#cargo-features
|
|
@ -38,7 +38,7 @@
|
|||
// [1]: https://github.com/rust-lang/rustfmt/issues/4210
|
||||
// [2]: https://github.com/rust-lang/rustfmt/issues/4787
|
||||
// [3]: https://github.com/rust-lang/rust/issues/82768#issuecomment-803935643
|
||||
#![cfg_attr(feature = "nightly", cfg_attr(feature = "nightly", doc = include_str!("features.txt")))]
|
||||
#![cfg_attr(feature = "nightly", cfg_attr(feature = "nightly", doc = include_str!("features.md")))]
|
||||
// https://github.com/teloxide/teloxide/raw/master/logo.svg doesn't work in html_logo_url, I don't know why.
|
||||
#![doc(
|
||||
html_logo_url = "https://github.com/teloxide/teloxide/raw/master/ICON.png",
|
||||
|
|
|
@ -191,6 +191,8 @@ mod tests {
|
|||
last_name: None,
|
||||
username: Some("abcd".to_string()),
|
||||
language_code: None,
|
||||
is_premium: false,
|
||||
added_to_attachment_menu: false,
|
||||
};
|
||||
assert_eq!(user_mention_or_link(&user_with_username), "@abcd");
|
||||
let user_without_username = User {
|
||||
|
@ -200,6 +202,8 @@ mod tests {
|
|||
last_name: None,
|
||||
username: None,
|
||||
language_code: None,
|
||||
is_premium: false,
|
||||
added_to_attachment_menu: false,
|
||||
};
|
||||
assert_eq!(
|
||||
user_mention_or_link(&user_without_username),
|
||||
|
|
|
@ -240,6 +240,8 @@ mod tests {
|
|||
last_name: None,
|
||||
username: Some("abcd".to_string()),
|
||||
language_code: None,
|
||||
is_premium: false,
|
||||
added_to_attachment_menu: false,
|
||||
};
|
||||
assert_eq!(user_mention_or_link(&user_with_username), "@abcd");
|
||||
let user_without_username = User {
|
||||
|
@ -249,6 +251,8 @@ mod tests {
|
|||
last_name: None,
|
||||
username: None,
|
||||
language_code: None,
|
||||
is_premium: false,
|
||||
added_to_attachment_menu: false,
|
||||
};
|
||||
assert_eq!(
|
||||
user_mention_or_link(&user_without_username),
|
||||
|
|
Loading…
Reference in a new issue