]> piware.de Git - learn-rust.git/blob - actix-server/src/main.rs
actix-server: Add "reverse input" websocket route
[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 #[actix_web::main]
104 async fn main() -> std::io::Result<()> {
105     env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
106
107     HttpServer::new(|| {
108         App::new()
109             .service(hello)
110             .service(static_file)
111             .service(Files::new("/dir", "../static"))
112             .service(ws_echo)
113             .service(ws_rev)
114             .wrap(Logger::default())
115     })
116         .bind(("127.0.0.1", 3030))?
117         .run()
118         .await
119 }
120
121 #[cfg(test)]
122 mod tests {
123     use actix_web::{App, body, test, web};
124     use actix_web::http::{header, StatusCode};
125     use actix_web_actors::ws;
126
127     use futures_util::sink::SinkExt;
128     use futures_util::StreamExt;
129
130     use super::{hello, static_file, ws_echo, ws_rev};
131
132     #[actix_web::test]
133     async fn test_hello() {
134         // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn
135         let app = test::init_service(App::new().service(hello)).await;
136
137         // no user-agent
138         let req = test::TestRequest::get().uri("/hello/rust").to_request();
139         let res = test::call_service(&app, req).await;
140         assert!(res.status().is_success());
141         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
142                    web::Bytes::from_static(b"Hello rust!"));
143
144         // with user-agent
145         let req = test::TestRequest::get()
146             .uri("/hello/rust")
147             .insert_header((header::USER_AGENT, "TestBrowser 0.1"))
148             .to_request();
149         let res = test::call_service(&app, req).await;
150         assert!(res.status().is_success());
151         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
152                    web::Bytes::from_static(b"Hello rust from TestBrowser 0.1!"));
153     }
154
155     #[actix_web::test]
156     async fn test_static_dir() {
157         // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn
158         let app = test::init_service(App::new().service(actix_files::Files::new("/dir", "../static"))).await;
159
160         let req = test::TestRequest::get().uri("/dir/plain.txt").to_request();
161         let res = test::call_service(&app, req).await;
162         assert!(res.status().is_success());
163         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
164                    web::Bytes::from_static(b"Hello world! This is uncompressed text.\n"));
165
166         // subdir
167         let req = test::TestRequest::get().uri("/dir/dir1/optzip.txt").to_request();
168         let res = test::call_service(&app, req).await;
169         assert!(res.status().is_success());
170         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
171                    web::Bytes::from_static(b"This file is available uncompressed or compressed\n\
172                                              AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"));
173
174         // does not support transparent decompression
175         let req = test::TestRequest::get().uri("/dir/onlycompressed.txt").to_request();
176         let res = test::call_service(&app, req).await;
177         assert_eq!(res.status(), StatusCode::NOT_FOUND);
178     }
179
180     #[actix_web::test]
181     async fn test_static_file() {
182         // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn
183         let app = test::init_service(App::new().service(static_file)).await;
184
185         // uncompressed
186         let req = test::TestRequest::get().uri("/file/dir1/optzip.txt").to_request();
187         let res = test::call_service(&app, req).await;
188         assert!(res.status().is_success());
189         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
190                    web::Bytes::from_static(b"This file is available uncompressed or compressed\n\
191                                              AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"));
192
193         // gzipped
194         let req = test::TestRequest::get()
195             .uri("/file/dir1/optzip.txt")
196             .insert_header((header::ACCEPT_ENCODING, "deflate, gzip"))
197             .to_request();
198         let res = test::call_service(&app, req).await;
199         assert!(res.status().is_success());
200         let res_bytes = body::to_bytes(res.into_body()).await.unwrap();
201         assert_eq!(res_bytes.len(), 63); // file size of ../static/dir1/optzip.txt.gz
202         assert_eq!(res_bytes[0], 31);
203     }
204
205     #[actix_web::test]
206     async fn test_ws_echo() {
207         // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn
208         let mut srv = actix_test::start(|| App::new().service(ws_echo));
209         let mut client = srv.ws_at("/ws-echo").await.unwrap();
210
211         // text echo
212         client.send(ws::Message::Text("hello".into())).await.unwrap();
213         let received = client.next().await.unwrap().unwrap();
214         assert_eq!(received, ws::Frame::Text("hello".into()));
215
216         // binary echo
217         client.send(ws::Message::Binary(web::Bytes::from_static(&[42, 99]))).await.unwrap();
218         let received = client.next().await.unwrap().unwrap();
219         assert_eq!(received, ws::Frame::Binary(web::Bytes::from_static(&[42, 99])));
220     }
221
222     #[actix_web::test]
223     async fn test_ws_rev() {
224         // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn
225         let mut srv = actix_test::start(|| App::new().service(ws_rev));
226         let mut client = srv.ws_at("/ws-rev").await.unwrap();
227
228         // text reversed
229         client.send(ws::Message::Text("hello".into())).await.unwrap();
230         let received = client.next().await.unwrap().unwrap();
231         assert_eq!(received, ws::Frame::Text("olleh".into()));
232
233         // binary reversed
234         client.send(ws::Message::Binary(web::Bytes::from_static(&[42, 99]))).await.unwrap();
235         let received = client.next().await.unwrap().unwrap();
236         assert_eq!(received, ws::Frame::Binary(web::Bytes::from_static(&[99, 42])));
237     }
238 }