]> piware.de Git - learn-rust.git/blob - actix-server/src/main.rs
44ed45cc60bc055feedb1b3d8cb2ddb872bd1171
[learn-rust.git] / actix-server / src / main.rs
1 use std::path::Path;
2
3 use actix::{Actor, ActorContext, StreamHandler};
4 use actix_web::{get, route, web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder, Result};
5 use actix_web::http::header;
6 use actix_web::middleware::Logger;
7 use actix_files::{Files, NamedFile};
8 use actix_web_actors::ws;
9
10 #[route("/hello/{name}", method="GET", method="HEAD")]
11 async fn hello(params: web::Path<String>, req: HttpRequest) -> Result<String> {
12     let name = params.into_inner();
13
14     match req.headers().get(header::USER_AGENT) {
15         Some(agent) => Ok(format!("Hello {} from {}!", name, agent.to_str().unwrap())),
16         None => Ok(format!("Hello {}!", name))
17     }
18 }
19
20 #[get("/file/{path:.*}")]
21 async fn static_file(params: web::Path<String>, req: HttpRequest) -> Result<impl Responder> {
22     let request_path = params.into_inner();
23     let disk_path = "../static/".to_string() + &request_path;
24
25     // if the client accepts gzip encoding, try that first
26     if let Some(accept_encoding) = req.headers().get(header::ACCEPT_ENCODING) {
27         if accept_encoding.to_str().unwrap().contains("gzip") {
28             let path_gz = disk_path.clone() + ".gz";
29             if Path::new(&path_gz).is_file() {
30                 log::debug!("client accepts gzip encoding, sending pre-compressed file {}", &path_gz);
31                 return Ok(NamedFile::open_async(path_gz).await?
32                           .customize()
33                           .insert_header(header::ContentEncoding::Gzip));
34             }
35         }
36     }
37
38     // uncompressed file
39     Ok(NamedFile::open_async(disk_path).await?.customize())
40 }
41
42 struct WsEcho;
43
44 impl Actor for WsEcho {
45     type Context = ws::WebsocketContext<Self>;
46 }
47
48 impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsEcho {
49     fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
50         log::info!("WsEcho got message {:?}", msg);
51         match msg {
52             Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
53             Ok(ws::Message::Text(text)) => ctx.text(text),
54             Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
55             Ok(ws::Message::Close(reason)) => {
56                 ctx.close(reason);
57                 ctx.stop();
58             },
59             _ => ctx.stop(),
60         }
61     }
62 }
63
64 #[get("/ws-echo")]
65 async fn ws_echo(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
66     ws::start(WsEcho {}, &req, stream)
67 }
68
69 struct WsRev;
70
71 impl Actor for WsRev {
72     type Context = ws::WebsocketContext<Self>;
73 }
74
75 impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsRev {
76     fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
77         log::info!("WsRev got message {:?}", msg);
78         match msg {
79             Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
80             Ok(ws::Message::Text(text)) => {
81                 let rev = text.chars().rev().collect::<String>();
82                 ctx.text(rev);
83             },
84             Ok(ws::Message::Binary(bin)) => {
85                 let mut rev = bin.to_vec();
86                 rev.reverse();
87                 ctx.binary(rev);
88             }
89             Ok(ws::Message::Close(reason)) => {
90                 ctx.close(reason);
91                 ctx.stop();
92             },
93             _ => ctx.stop(),
94         }
95     }
96 }
97
98 #[get("/ws-rev")]
99 async fn ws_rev(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
100     ws::start(WsRev {}, &req, stream)
101 }
102
103 // App is a template soup, too hard as a proper function
104 macro_rules! get_app {
105     () => {
106         App::new()
107             .service(hello)
108             .service(static_file)
109             .service(Files::new("/dir", "../static"))
110             .service(ws_echo)
111             .service(ws_rev)
112     }
113 }
114
115 #[actix_web::main]
116 async fn main() -> std::io::Result<()> {
117     env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
118
119     HttpServer::new(|| {
120             get_app!()
121             .wrap(Logger::default())
122     })
123         .bind(("127.0.0.1", 3030))?
124         .run()
125         .await
126 }
127
128 #[cfg(test)]
129 mod tests {
130     use actix_web::{App, body, test, web};
131     use actix_web::http::{header, StatusCode};
132     use actix_web_actors::ws;
133     use actix_files::Files;
134
135     use futures_util::sink::SinkExt;
136     use futures_util::StreamExt;
137
138     use super::{hello, static_file, ws_echo, ws_rev};
139
140     #[actix_web::test]
141     async fn test_hello() {
142         let app = test::init_service(get_app!()).await;
143
144         // no user-agent
145         let req = test::TestRequest::get().uri("/hello/rust").to_request();
146         let res = test::call_service(&app, req).await;
147         assert!(res.status().is_success());
148         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
149                    web::Bytes::from_static(b"Hello rust!"));
150
151         // with user-agent
152         let req = test::TestRequest::get()
153             .uri("/hello/rust")
154             .insert_header((header::USER_AGENT, "TestBrowser 0.1"))
155             .to_request();
156         let res = test::call_service(&app, req).await;
157         assert!(res.status().is_success());
158         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
159                    web::Bytes::from_static(b"Hello rust from TestBrowser 0.1!"));
160     }
161
162     #[actix_web::test]
163     async fn test_static_dir() {
164         let app = test::init_service(get_app!()).await;
165
166         let req = test::TestRequest::get().uri("/dir/plain.txt").to_request();
167         let res = test::call_service(&app, req).await;
168         assert!(res.status().is_success());
169         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
170                    web::Bytes::from_static(b"Hello world! This is uncompressed text.\n"));
171
172         // subdir
173         let req = test::TestRequest::get().uri("/dir/dir1/optzip.txt").to_request();
174         let res = test::call_service(&app, req).await;
175         assert!(res.status().is_success());
176         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
177                    web::Bytes::from_static(b"This file is available uncompressed or compressed\n\
178                                              AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"));
179
180         // does not support transparent decompression
181         let req = test::TestRequest::get().uri("/dir/onlycompressed.txt").to_request();
182         let res = test::call_service(&app, req).await;
183         assert_eq!(res.status(), StatusCode::NOT_FOUND);
184     }
185
186     #[actix_web::test]
187     async fn test_static_file() {
188         let app = test::init_service(get_app!()).await;
189
190         // uncompressed
191         let req = test::TestRequest::get().uri("/file/dir1/optzip.txt").to_request();
192         let res = test::call_service(&app, req).await;
193         assert!(res.status().is_success());
194         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
195                    web::Bytes::from_static(b"This file is available uncompressed or compressed\n\
196                                              AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"));
197
198         // gzipped
199         let req = test::TestRequest::get()
200             .uri("/file/dir1/optzip.txt")
201             .insert_header((header::ACCEPT_ENCODING, "deflate, gzip"))
202             .to_request();
203         let res = test::call_service(&app, req).await;
204         assert!(res.status().is_success());
205         let res_bytes = body::to_bytes(res.into_body()).await.unwrap();
206         assert_eq!(res_bytes.len(), 63); // file size of ../static/dir1/optzip.txt.gz
207         assert_eq!(res_bytes[0], 31);
208     }
209
210     #[actix_web::test]
211     async fn test_ws_echo() {
212         let mut srv = actix_test::start(|| get_app!());
213         let mut client = srv.ws_at("/ws-echo").await.unwrap();
214
215         // text echo
216         client.send(ws::Message::Text("hello".into())).await.unwrap();
217         let received = client.next().await.unwrap().unwrap();
218         assert_eq!(received, ws::Frame::Text("hello".into()));
219
220         // binary echo
221         client.send(ws::Message::Binary(web::Bytes::from_static(&[42, 99]))).await.unwrap();
222         let received = client.next().await.unwrap().unwrap();
223         assert_eq!(received, ws::Frame::Binary(web::Bytes::from_static(&[42, 99])));
224     }
225
226     #[actix_web::test]
227     async fn test_ws_rev() {
228         let mut srv = actix_test::start(|| get_app!());
229         let mut client = srv.ws_at("/ws-rev").await.unwrap();
230
231         // text reversed
232         client.send(ws::Message::Text("hello".into())).await.unwrap();
233         let received = client.next().await.unwrap().unwrap();
234         assert_eq!(received, ws::Frame::Text("olleh".into()));
235
236         // binary reversed
237         client.send(ws::Message::Binary(web::Bytes::from_static(&[42, 99]))).await.unwrap();
238         let received = client.next().await.unwrap().unwrap();
239         assert_eq!(received, ws::Frame::Binary(web::Bytes::from_static(&[99, 42])));
240     }
241 }