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}