actix_web_middleware_slack/
lib.rs1use std::future::{ready, Ready};
18
19use std::rc::Rc;
20use std::time::UNIX_EPOCH;
21
22use actix_http::h1::Payload;
23use actix_web::dev::forward_ready;
24use actix_web::http::header::HeaderMap;
25use actix_web::web::BytesMut;
26use actix_web::{
27    body::EitherBody,
28    dev::{Service, ServiceRequest, ServiceResponse, Transform},
29    Error, HttpMessage, HttpResponse,
30};
31use futures_util::future::LocalBoxFuture;
32use futures_util::StreamExt;
33use hmac::{Hmac, Mac};
34use sha2::Sha256;
35
36#[derive(Clone)]
37pub struct Slack {
38    slack_signing_secret: String,
39}
40
41impl Slack {
42    pub fn new(slack_signing_secret: impl Into<String>) -> Self {
43        Self {
44            slack_signing_secret: slack_signing_secret.into(),
45        }
46    }
47}
48
49const HEADER_TIMESTAMP: &str = "X-Slack-Request-Timestamp";
50const HEADER_SIGNATURE: &str = "X-Slack-Signature";
51
52impl<S: 'static, B> Transform<S, ServiceRequest> for Slack
53where
54    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
55    S::Future: 'static,
56    B: 'static,
57{
58    type Response = ServiceResponse<EitherBody<B>>;
59    type Error = Error;
60    type Transform = SlackMiddleware<S>;
61    type InitError = ();
62    type Future = Ready<Result<Self::Transform, Self::InitError>>;
63
64    fn new_transform(&self, service: S) -> Self::Future {
65        ready(Ok(SlackMiddleware {
66            service: Rc::new(service),
67            slack_signing_secret: self.slack_signing_secret.to_string(),
68        }))
69    }
70}
71
72type HmacSha256 = Hmac<Sha256>;
73
74pub struct SlackMiddleware<S> {
75    service: Rc<S>,
76    slack_signing_secret: String,
77}
78
79impl<S, B> Service<ServiceRequest> for SlackMiddleware<S>
80where
81    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
82    S::Future: 'static,
83    B: 'static,
84{
85    type Response = ServiceResponse<EitherBody<B>>;
86    type Error = Error;
87    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
88
89    forward_ready!(service);
90
91    fn call(&self, mut req: ServiceRequest) -> Self::Future {
92        let headers = req.headers();
93        let ts = match get_timestamp(headers) {
94            Some(ts) => ts,
95            None => {
96                return Box::pin(async { Ok(bad_request(req, format!("header '{}' is required", HEADER_TIMESTAMP))) })
97            }
98        };
99        let signature = match get_signature(headers) {
100            Some(signature) => signature,
101            None => {
102                return Box::pin(async { Ok(bad_request(req, format!("header '{}' is required", HEADER_SIGNATURE))) })
103            }
104        };
105
106        let service = self.service.clone();
107        let slack_signing_secret = self.slack_signing_secret.to_string();
108        Box::pin(async move {
109            let mut payload = req.take_payload();
110            let mut body = BytesMut::new();
111            while let Some(item) = payload.next().await {
112                body.extend_from_slice(&item?);
113            }
114            let calculated_signature =
115                sign(ts, String::from_utf8(body.to_vec()).unwrap(), slack_signing_secret.as_bytes());
116            if calculated_signature == signature {
117                let (_, mut payload) = Payload::create(true);
118                payload.unread_data(body.into());
119                req.set_payload(payload.into());
120                let res = service.call(req);
121                res.await.map(ServiceResponse::map_into_left_body)
122            } else {
123                Ok(bad_request(req, "invalid signature".to_string()))
124            }
125        })
126    }
127}
128
129fn sign(ts: u64, body: String, secret: &[u8]) -> String {
130    let sig_basestring = format!("v0:{}:{}", ts, body);
131    let mut hmac = HmacSha256::new_from_slice(secret).unwrap();
132    hmac.update(sig_basestring.as_bytes());
133    format!("v0={}", hex::encode(hmac.finalize().into_bytes()))
134}
135
136fn get_timestamp(header: &HeaderMap) -> Option<u64> {
137    let ts = header.get(HEADER_TIMESTAMP)?;
138    let ts = ts.to_str().ok()?.parse::<u64>().ok()?;
139    let now = std::time::SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
140    if now - ts > 60 * 5 {
141        None
142    } else {
143        Some(ts)
144    }
145}
146
147fn get_signature(header: &HeaderMap) -> Option<String> {
148    let signature = header.get(HEADER_SIGNATURE)?;
149    Some(signature.to_str().ok()?.to_string())
150}
151
152fn bad_request<B>(req: ServiceRequest, body: String) -> ServiceResponse<EitherBody<B>> {
153    let (req, _pl) = req.into_parts();
154    let response = HttpResponse::BadRequest().body(body).map_into_right_body();
155    ServiceResponse::new(req, response)
156}
157
158#[cfg(test)]
159mod tests {
160    use actix_web::dev::{Service, Transform};
161    use actix_web::http::StatusCode;
162    use actix_web::test;
163    use actix_web::test::TestRequest;
164
165    use crate::{sign, Slack, HEADER_SIGNATURE, HEADER_TIMESTAMP};
166    use actix_http::body::to_bytes;
167    use actix_http::h1::Payload;
168    use actix_web::web::Bytes;
169    use std::time::{SystemTime, UNIX_EPOCH};
170
171    const TEST_SECRET: &str = "8f742231b10e8888abcd99yyyzzz85a5";
172    const TEST_BODY : &str = "token=xyzz0WbapA4vBCDEFasx0q6G&team_id=T1DC2JH3J&team_domain=testteamnow&channel_id=G8PSS9T3V&channel_name=foobar&user_id=U2CERLKJA&user_name=roadrunner&command=%2Fwebhook-collect&text=&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT1DC2JH3J%2F397700885554%2F96rGlfmibIGlgcZRskXaIFfN&trigger_id=398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c";
173
174    trait BodyTest {
175        fn as_str(&self) -> &str;
176    }
177
178    impl BodyTest for Bytes {
179        fn as_str(&self) -> &str {
180            std::str::from_utf8(self).unwrap()
181        }
182    }
183
184    #[test]
185    fn test_sign() {
186        assert_eq!(
187            "v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503",
188            sign(1531420618, TEST_BODY.to_string(), TEST_SECRET.as_bytes())
189        );
190    }
191
192    #[tokio::test]
193    async fn no_timestamp_header() {
194        let mw = Slack::new("test").new_transform(test::ok_service()).await.unwrap();
195        let req = TestRequest::default().to_srv_request();
196        let res = mw.call(req).await.unwrap();
197        assert_eq!(res.status(), StatusCode::BAD_REQUEST);
198        let body = to_bytes(res.into_body()).await.unwrap();
199        assert_eq!(body.as_str(), format!("header '{}' is required", HEADER_TIMESTAMP));
200    }
201
202    #[tokio::test]
203    async fn old_timestamp() {
204        let mut now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
205        now -= 60 * 5 + 1;
206        let mut req = TestRequest::default().to_srv_request();
207        req.headers_mut()
208            .insert(HEADER_TIMESTAMP.try_into().unwrap(), now.into());
209        let mw = Slack::new("test").new_transform(test::ok_service()).await.unwrap();
210        let res = mw.call(req).await.unwrap();
211        assert_eq!(res.status(), StatusCode::BAD_REQUEST);
212        let body = to_bytes(res.into_body()).await.unwrap();
213        assert_eq!(body.as_str(), format!("header '{}' is required", HEADER_TIMESTAMP));
214    }
215
216    #[tokio::test]
217    async fn no_signature_header() {
218        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
219        let mut req = TestRequest::default().to_srv_request();
220        req.headers_mut()
221            .insert(HEADER_TIMESTAMP.try_into().unwrap(), now.into());
222        let mw = Slack::new("test").new_transform(test::ok_service()).await.unwrap();
223        let res = mw.call(req).await.unwrap();
224        assert_eq!(res.status(), StatusCode::BAD_REQUEST);
225        let body = to_bytes(res.into_body()).await.unwrap();
226        assert_eq!(body.as_str(), format!("header '{}' is required", HEADER_SIGNATURE));
227    }
228
229    #[tokio::test]
230    async fn invalid_sign() {
231        let mw = Slack::new(TEST_SECRET).new_transform(test::ok_service()).await.unwrap();
232        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
233        let mut req = TestRequest::default().to_srv_request();
234        let _headers = req.headers_mut();
235        req.headers_mut()
236            .insert(HEADER_TIMESTAMP.try_into().unwrap(), now.into());
237        req.headers_mut()
238            .insert(HEADER_SIGNATURE.try_into().unwrap(), "aaa".try_into().unwrap());
239        let res = mw.call(req).await.unwrap();
240        assert_eq!(res.status(), StatusCode::BAD_REQUEST);
241        let body = to_bytes(res.into_body()).await.unwrap();
242        assert_eq!(body.as_str(), "invalid signature");
243    }
244
245    #[tokio::test]
246    async fn success() {
247        let mw = Slack::new(TEST_SECRET).new_transform(test::ok_service()).await.unwrap();
248        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
249        let signature = sign(now, TEST_BODY.to_string(), TEST_SECRET.as_bytes());
250
251        let mut req = TestRequest::default().to_srv_request();
253        let (_, mut payload) = Payload::create(true);
254        payload.unread_data(TEST_BODY.into());
255        req.set_payload(payload.into());
256
257        let _headers = req.headers_mut();
259        req.headers_mut()
260            .insert(HEADER_TIMESTAMP.try_into().unwrap(), now.into());
261        req.headers_mut()
262            .insert(HEADER_SIGNATURE.try_into().unwrap(), signature.try_into().unwrap());
263
264        let res = mw.call(req).await.unwrap();
265        assert_eq!(res.status(), StatusCode::OK);
266    }
267}