+use std::path::Path;
+
+use actix::{Actor, ActorContext, StreamHandler};
+use actix_web::{get, route, web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder, Result};
+use actix_web::http::header;
+use actix_web::middleware::Logger;
+use actix_files::{Files, NamedFile};
+use actix_web_actors::ws;
+
+#[route("/hello/{name}", method="GET", method="HEAD")]
+async fn hello(params: web::Path<String>, req: HttpRequest) -> Result<String> {
+ let name = params.into_inner();
+
+ match req.headers().get(header::USER_AGENT) {
+ Some(agent) => Ok(format!("Hello {} from {}!", name, agent.to_str().unwrap())),
+ None => Ok(format!("Hello {}!", name))
+ }
+}
+
+#[get("/file/{path:.*}")]
+async fn static_file(params: web::Path<String>, req: HttpRequest) -> Result<impl Responder> {
+ let request_path = params.into_inner();
+ let disk_path = "../static/".to_string() + &request_path;
+
+ // if the client accepts gzip encoding, try that first
+ if let Some(accept_encoding) = req.headers().get(header::ACCEPT_ENCODING) {
+ if accept_encoding.to_str().unwrap().contains("gzip") {
+ let path_gz = disk_path.clone() + ".gz";
+ if Path::new(&path_gz).is_file() {
+ log::debug!("client accepts gzip encoding, sending pre-compressed file {}", &path_gz);
+ return Ok(NamedFile::open_async(path_gz).await?
+ .customize()
+ .insert_header(header::ContentEncoding::Gzip));
+ }
+ }
+ }
+
+ // uncompressed file
+ Ok(NamedFile::open_async(disk_path).await?.customize())
+}
+
+struct WsEcho;
+
+impl Actor for WsEcho {
+ type Context = ws::WebsocketContext<Self>;
+}
+
+impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsEcho {
+ fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
+ log::info!("WsEcho got message {:?}", msg);
+ match msg {
+ Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
+ Ok(ws::Message::Text(text)) => ctx.text(text),
+ Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
+ Ok(ws::Message::Close(reason)) => {
+ ctx.close(reason);
+ ctx.stop();
+ },
+ _ => ctx.stop(),
+ }
+ }
+}
+
+#[get("/ws-echo")]
+async fn ws_echo(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
+ ws::start(WsEcho {}, &req, stream)
+}
+
+struct WsRev;
+
+impl Actor for WsRev {
+ type Context = ws::WebsocketContext<Self>;
+}
+
+impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsRev {
+ fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
+ log::info!("WsRev got message {:?}", msg);
+ match msg {
+ Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
+ Ok(ws::Message::Text(text)) => {
+ let rev = text.chars().rev().collect::<String>();
+ ctx.text(rev);
+ },
+ Ok(ws::Message::Binary(bin)) => {
+ let mut rev = bin.to_vec();
+ rev.reverse();
+ ctx.binary(rev);
+ }
+ Ok(ws::Message::Close(reason)) => {
+ ctx.close(reason);
+ ctx.stop();
+ },
+ _ => ctx.stop(),
+ }
+ }
+}
+
+#[get("/ws-rev")]
+async fn ws_rev(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
+ ws::start(WsRev {}, &req, stream)
+}
+
+// App is a template soup, too hard as a proper function
+macro_rules! get_app {
+ () => {
+ App::new()
+ .service(hello)
+ .service(static_file)
+ .service(Files::new("/dir", "../static"))
+ .service(ws_echo)
+ .service(ws_rev)
+ }
+}
+
+#[actix_web::main]
+async fn main() -> std::io::Result<()> {
+ env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
+
+ HttpServer::new(|| {
+ get_app!()
+ .wrap(Logger::default())
+ })
+ .bind(("127.0.0.1", 3030))?
+ .run()
+ .await
+}
+
+#[cfg(test)]
+mod tests {
+ use actix_web::{App, body, test, web};
+ use actix_web::http::{header, StatusCode};
+ use actix_web_actors::ws;
+ use actix_files::Files;
+
+ use futures_util::sink::SinkExt;
+ use futures_util::StreamExt;
+
+ use super::{hello, static_file, ws_echo, ws_rev};
+
+ #[actix_web::test]
+ async fn test_hello() {
+ let app = test::init_service(get_app!()).await;
+
+ // no user-agent
+ let req = test::TestRequest::get().uri("/hello/rust").to_request();
+ let res = test::call_service(&app, req).await;
+ assert!(res.status().is_success());
+ assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
+ web::Bytes::from_static(b"Hello rust!"));
+
+ // with user-agent
+ let req = test::TestRequest::get()
+ .uri("/hello/rust")
+ .insert_header((header::USER_AGENT, "TestBrowser 0.1"))
+ .to_request();
+ let res = test::call_service(&app, req).await;
+ assert!(res.status().is_success());
+ assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
+ web::Bytes::from_static(b"Hello rust from TestBrowser 0.1!"));
+ }
+
+ #[actix_web::test]
+ async fn test_static_dir() {
+ let app = test::init_service(get_app!()).await;
+
+ let req = test::TestRequest::get().uri("/dir/plain.txt").to_request();
+ let res = test::call_service(&app, req).await;
+ assert!(res.status().is_success());
+ assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
+ web::Bytes::from_static(b"Hello world! This is uncompressed text.\n"));
+
+ // subdir
+ let req = test::TestRequest::get().uri("/dir/dir1/optzip.txt").to_request();
+ let res = test::call_service(&app, req).await;
+ assert!(res.status().is_success());
+ assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
+ web::Bytes::from_static(b"This file is available uncompressed or compressed\n\
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"));
+
+ // does not support transparent decompression
+ let req = test::TestRequest::get().uri("/dir/onlycompressed.txt").to_request();
+ let res = test::call_service(&app, req).await;
+ assert_eq!(res.status(), StatusCode::NOT_FOUND);
+ }
+
+ #[actix_web::test]
+ async fn test_static_file() {
+ let app = test::init_service(get_app!()).await;
+
+ // uncompressed
+ let req = test::TestRequest::get().uri("/file/dir1/optzip.txt").to_request();
+ let res = test::call_service(&app, req).await;
+ assert!(res.status().is_success());
+ assert_eq!(body::to_bytes(res.into_body()).await.unwrap(),
+ web::Bytes::from_static(b"This file is available uncompressed or compressed\n\
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"));
+
+ // gzipped
+ let req = test::TestRequest::get()
+ .uri("/file/dir1/optzip.txt")
+ .insert_header((header::ACCEPT_ENCODING, "deflate, gzip"))
+ .to_request();
+ let res = test::call_service(&app, req).await;
+ assert!(res.status().is_success());
+ let res_bytes = body::to_bytes(res.into_body()).await.unwrap();
+ assert_eq!(res_bytes.len(), 63); // file size of ../static/dir1/optzip.txt.gz
+ assert_eq!(res_bytes[0], 31);
+ }
+
+ #[actix_web::test]
+ async fn test_ws_echo() {
+ let mut srv = actix_test::start(|| get_app!());
+ let mut client = srv.ws_at("/ws-echo").await.unwrap();
+
+ // text echo
+ client.send(ws::Message::Text("hello".into())).await.unwrap();
+ let received = client.next().await.unwrap().unwrap();
+ assert_eq!(received, ws::Frame::Text("hello".into()));
+
+ // binary echo
+ client.send(ws::Message::Binary(web::Bytes::from_static(&[42, 99]))).await.unwrap();
+ let received = client.next().await.unwrap().unwrap();
+ assert_eq!(received, ws::Frame::Binary(web::Bytes::from_static(&[42, 99])));
+ }
+
+ #[actix_web::test]
+ async fn test_ws_rev() {
+ let mut srv = actix_test::start(|| get_app!());
+ let mut client = srv.ws_at("/ws-rev").await.unwrap();
+
+ // text reversed
+ client.send(ws::Message::Text("hello".into())).await.unwrap();
+ let received = client.next().await.unwrap().unwrap();
+ assert_eq!(received, ws::Frame::Text("olleh".into()));
+
+ // binary reversed
+ client.send(ws::Message::Binary(web::Bytes::from_static(&[42, 99]))).await.unwrap();
+ let received = client.next().await.unwrap().unwrap();
+ assert_eq!(received, ws::Frame::Binary(web::Bytes::from_static(&[99, 42])));
+ }
+}