]> piware.de Git - learn-rust.git/blob - actix-server/src/main.rs
actix-server: Add echo 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 #[actix_web::main]
70 async fn main() -> std::io::Result<()> {
71     env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
72
73     HttpServer::new(|| {
74         App::new()
75             .service(hello)
76             .service(static_file)
77             .service(Files::new("/dir", "../static"))
78             .service(ws_echo)
79             .wrap(Logger::default())
80     })
81         .bind(("127.0.0.1", 3030))?
82         .run()
83         .await
84 }
85
86 #[cfg(test)]
87 mod tests {
88     use actix_web::{App, body, test, web};
89     use actix_web::http::{header, StatusCode};
90     use actix_web_actors::ws;
91
92     use futures_util::sink::SinkExt;
93     use futures_util::StreamExt;
94
95     use super::{hello, static_file, ws_echo};
96
97     #[actix_web::test]
98     async fn test_hello() {
99         // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn
100         let app = test::init_service(App::new().service(hello)).await;
101
102         // no user-agent
103         let req = test::TestRequest::get().uri("/hello/rust").to_request();
104         let res = test::call_service(&app, req).await;
105         assert!(res.status().is_success());
106         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
107                    web::Bytes::from_static(b"Hello rust!"));
108
109         // with user-agent
110         let req = test::TestRequest::get()
111             .uri("/hello/rust")
112             .insert_header((header::USER_AGENT, "TestBrowser 0.1"))
113             .to_request();
114         let res = test::call_service(&app, req).await;
115         assert!(res.status().is_success());
116         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
117                    web::Bytes::from_static(b"Hello rust from TestBrowser 0.1!"));
118     }
119
120     #[actix_web::test]
121     async fn test_static_dir() {
122         // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn
123         let app = test::init_service(App::new().service(actix_files::Files::new("/dir", "../static"))).await;
124
125         let req = test::TestRequest::get().uri("/dir/plain.txt").to_request();
126         let res = test::call_service(&app, req).await;
127         assert!(res.status().is_success());
128         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
129                    web::Bytes::from_static(b"Hello world! This is uncompressed text.\n"));
130
131         // subdir
132         let req = test::TestRequest::get().uri("/dir/dir1/optzip.txt").to_request();
133         let res = test::call_service(&app, req).await;
134         assert!(res.status().is_success());
135         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
136                    web::Bytes::from_static(b"This file is available uncompressed or compressed\n\
137                                              AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"));
138
139         // does not support transparent decompression
140         let req = test::TestRequest::get().uri("/dir/onlycompressed.txt").to_request();
141         let res = test::call_service(&app, req).await;
142         assert_eq!(res.status(), StatusCode::NOT_FOUND);
143     }
144
145     #[actix_web::test]
146     async fn test_static_file() {
147         // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn
148         let app = test::init_service(App::new().service(static_file)).await;
149
150         // uncompressed
151         let req = test::TestRequest::get().uri("/file/dir1/optzip.txt").to_request();
152         let res = test::call_service(&app, req).await;
153         assert!(res.status().is_success());
154         assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
155                    web::Bytes::from_static(b"This file is available uncompressed or compressed\n\
156                                              AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"));
157
158         // gzipped
159         let req = test::TestRequest::get()
160             .uri("/file/dir1/optzip.txt")
161             .insert_header((header::ACCEPT_ENCODING, "deflate, gzip"))
162             .to_request();
163         let res = test::call_service(&app, req).await;
164         assert!(res.status().is_success());
165         let res_bytes = body::to_bytes(res.into_body()).await.unwrap();
166         assert_eq!(res_bytes.len(), 63); // file size of ../static/dir1/optzip.txt.gz
167         assert_eq!(res_bytes[0], 31);
168     }
169
170     #[actix_web::test]
171     async fn test_ws_echo() {
172         // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn
173         let mut srv = actix_test::start(|| App::new().service(ws_echo));
174         let mut client = srv.ws_at("/ws-echo").await.unwrap();
175
176         // text echo
177         client.send(ws::Message::Text("hello".into())).await.unwrap();
178         let received = client.next().await.unwrap().unwrap();
179         assert_eq!(received, ws::Frame::Text("hello".into()));
180
181         // binary echo
182         client.send(ws::Message::Binary(web::Bytes::from_static(&[42, 99]))).await.unwrap();
183         let received = client.next().await.unwrap().unwrap();
184         assert_eq!(received, ws::Frame::Binary(web::Bytes::from_static(&[42, 99])));
185     }
186 }