Skip to content

Commit 21d5f54

Browse files
committed
work: added basic skeleton of Service and Transform to ...
PreheaderMiddleware Signed-off-by: Martin <martin@hotmail.com.br>
1 parent 35ef291 commit 21d5f54

File tree

4 files changed

+209
-71
lines changed

4 files changed

+209
-71
lines changed

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ license = "MIT"
1111
actix-service = "2"
1212
actix-utils = "3"
1313
actix-web = { version = "4", default-features = false }
14-
smartstring = "1"
1514

1615
futures-util = "0.3"
1716
log = "0.4"
17+
reqwest = { version = "0.11", features = ["tokio-rustls"] }
18+
thiserror = "1"
19+
url = "2"
1820

1921
[dev-dependencies]
2022
actix-web = { version = "4", default_features = false, features = ["macros"] }

src/errors.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use thiserror::Error;
2+
3+
#[derive(Debug, Error)]
4+
pub enum PrerenderError {
5+
#[error("Invalid Url.")]
6+
InvalidUrl,
7+
8+
#[error(transparent)]
9+
ReqwestError(#[from] reqwest::Error),
10+
}

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
33
#![forbid(unsafe_code)]
44
#![deny(nonstandard_style)]
5-
#![allow(clippy::must_use_candidate)]
5+
#![allow(clippy::must_use_candidate, clippy::missing_panics_doc)]
66
#![warn(future_incompatible, missing_debug_implementations)]
77
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
88
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
99

1010
use consts::{IGNORED_EXTENSIONS, USER_AGENTS};
1111

1212
mod consts;
13+
pub mod errors;
1314
pub mod middleware;
1415

1516
// impl<S, B> Service<ServiceRequest> for PrerenderMiddleWare

src/middleware.rs

Lines changed: 194 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,75 @@
1+
use crate::errors::PrerenderError;
12
use crate::{IGNORED_EXTENSIONS, USER_AGENTS};
2-
use actix_web::dev::ServiceRequest;
3+
use actix_service::{Service, Transform};
4+
use actix_utils::future;
5+
use actix_utils::future::Ready;
6+
use actix_web::body::BoxBody;
7+
8+
use actix_web::dev::{ServiceRequest, ServiceResponse};
39
use actix_web::http::header::HeaderMap;
410
use actix_web::http::uri::PathAndQuery;
511
use actix_web::http::{header, Method};
12+
use actix_web::{Error, HttpResponse};
13+
use futures_util::future::LocalBoxFuture;
14+
use futures_util::TryFutureExt;
15+
use reqwest::Client;
16+
use url::Url;
617

7-
#[derive(Debug)]
8-
struct PrerenderMiddleware {
9-
prerender_service_url: String,
18+
#[derive(Debug, Clone)]
19+
pub struct Prerender {
20+
prerender_service_url: Url,
21+
inner_client: Client,
1022
}
1123

12-
impl PrerenderMiddleware {
13-
pub fn prepare_build_api_url(&self, req: &ServiceRequest) -> String {
14-
let req_uri = req.uri();
15-
let req_headers = req.headers();
16-
17-
// TODO: this.host?
18-
let host = req
19-
.uri()
20-
.host()
21-
.or_else(|| {
22-
req_headers
23-
.get("X-Forwarded-Host")
24-
.and_then(|hdr| hdr.to_str().ok())
25-
})
26-
.or_else(|| {
27-
req_headers
28-
.get(header::HOST)
29-
.and_then(|hdr| hdr.to_str().ok())
30-
})
31-
.unwrap();
32-
33-
let scheme = req.uri().scheme_str().unwrap_or("http");
34-
let url_path_query = req_uri.path_and_query().map(PathAndQuery::as_str).unwrap();
35-
36-
format!(
37-
"{}{}://{}{}",
38-
&*self.prerender_service_url, scheme, host, url_path_query
39-
)
40-
}
24+
#[derive(Debug, Clone)]
25+
pub struct Inner {
26+
prerender_service_url: Url,
27+
inner_client: Client,
4128
}
4229

30+
impl Prerender {}
31+
4332
#[derive(Debug)]
44-
struct PrerenderMiddlewareBuilder {}
33+
pub struct PrerenderBuilder {}
4534

46-
impl PrerenderMiddlewareBuilder {
47-
pub fn use_prerender_io() -> PrerenderMiddleware {
48-
PrerenderMiddleware {
49-
prerender_service_url: prerender_url().to_string(),
35+
impl PrerenderBuilder {
36+
pub fn use_prerender_io(mut self) -> Prerender {
37+
Prerender {
38+
prerender_service_url: prerender_url(),
39+
inner_client: Default::default(),
5040
}
5141
}
5242

53-
pub fn use_custom_prerender_url(prerender_service_url: &impl ToString) -> PrerenderMiddleware {
54-
PrerenderMiddleware {
55-
prerender_service_url: prerender_service_url.to_string(),
56-
}
43+
pub fn use_custom_prerender_url(
44+
mut self,
45+
prerender_service_url: &str,
46+
) -> Result<Prerender, PrerenderError> {
47+
let result = Url::parse(prerender_service_url).map_err(|_| PrerenderError::InvalidUrl)?;
48+
49+
Ok(Prerender {
50+
prerender_service_url: result,
51+
inner_client: Default::default(),
52+
})
5753
}
5854
}
5955

60-
impl PrerenderMiddleware {
61-
pub fn builder() -> PrerenderMiddlewareBuilder {
62-
PrerenderMiddlewareBuilder {}
56+
impl Prerender {
57+
pub fn builder() -> PrerenderBuilder {
58+
PrerenderBuilder {}
6359
}
6460
}
6561

62+
fn prerender_url() -> Url {
63+
Url::parse("https://service.prerender.io").unwrap()
64+
}
65+
6666
/// Decides if should prerender the page or not.
6767
///
6868
/// Will NOT prerender on the following cases:
6969
/// * HTTP is not GET or HEAD
7070
/// * User agent is NOT crawler
7171
/// * Is requesting a resource on `IGNORED_EXTENSIONS`
72-
pub fn should_prerender(req: &ServiceRequest) -> bool {
72+
pub(crate) fn should_prerender(req: &ServiceRequest) -> bool {
7373
let request_headers = req.headers();
7474
let mut is_crawler = false;
7575

@@ -78,9 +78,9 @@ pub fn should_prerender(req: &ServiceRequest) -> bool {
7878
}
7979

8080
let req_ua_lowercase = if let Some(user_agent) = request_headers.get(header::USER_AGENT) {
81-
let user_agent = user_agent.to_str();
81+
let user_agent = user_agent.to_str().map(str::to_lowercase);
8282
if let Ok(ua) = user_agent {
83-
ua.to_lowercase()
83+
ua
8484
} else {
8585
return false;
8686
}
@@ -111,37 +111,135 @@ pub fn should_prerender(req: &ServiceRequest) -> bool {
111111
is_crawler
112112
}
113113

114-
pub fn get_prerendered_response(req: ServiceRequest) {
115-
let mut prerender_request_headers = HeaderMap::new();
116-
let forward_headers = true;
114+
#[derive(Debug)]
115+
pub struct PrerenderMiddleware<S> {
116+
pub(crate) service: S,
117+
prerender_service_url: Url,
118+
inner_client: Client,
119+
}
117120

118-
if forward_headers {
119-
prerender_request_headers = req.headers().clone();
120-
prerender_request_headers.remove(header::HOST);
121+
impl<S> PrerenderMiddleware<S> {
122+
pub fn prepare_build_api_url(&self, req: &ServiceRequest) -> String {
123+
let req_uri = req.uri();
124+
let req_headers = req.headers();
125+
126+
// TODO: this.host?
127+
let host = req
128+
.uri()
129+
.host()
130+
.or_else(|| {
131+
req_headers
132+
.get("X-Forwarded-Host")
133+
.and_then(|hdr| hdr.to_str().ok())
134+
})
135+
.or_else(|| {
136+
req_headers
137+
.get(header::HOST)
138+
.and_then(|hdr| hdr.to_str().ok())
139+
})
140+
.unwrap();
141+
142+
let scheme = req.uri().scheme_str().unwrap_or("http");
143+
let url_path_query = req_uri.path_and_query().map(PathAndQuery::as_str).unwrap();
144+
145+
format!(
146+
"{}{}://{}{}",
147+
self.prerender_service_url, scheme, host, url_path_query
148+
)
149+
}
150+
151+
pub async fn get_rendered_response(
152+
&self,
153+
req: ServiceRequest,
154+
) -> Result<ServiceResponse, PrerenderError> {
155+
let mut prerender_request_headers = HeaderMap::new();
156+
let forward_headers = true;
157+
158+
if forward_headers {
159+
prerender_request_headers = req.headers().clone();
160+
prerender_request_headers.remove(header::HOST);
161+
}
162+
163+
prerender_request_headers.append(header::ACCEPT_ENCODING, "gzip".parse().unwrap());
164+
165+
// TODO: accept `X-Prerender-Token`
166+
// prerender_request_headers.insert("X-Prerender-Token", pre_render_token);
167+
168+
let url_to_request = self.prepare_build_api_url(&req);
169+
let prerender_response = self
170+
.inner_client
171+
.get(url_to_request)
172+
.send()
173+
.and_then(|a| a.bytes())
174+
.await?;
175+
176+
let http_response = HttpResponse::Ok().body(prerender_response);
177+
Ok(req.into_response(http_response))
121178
}
179+
}
180+
181+
impl<S> Service<ServiceRequest> for PrerenderMiddleware<S>
182+
where
183+
S: Service<ServiceRequest, Response = ServiceResponse, Error = Error>,
184+
S::Future: 'static,
185+
{
186+
type Response = ServiceResponse<BoxBody>;
187+
type Error = Error;
188+
type Future = LocalBoxFuture<'static, Result<ServiceResponse<BoxBody>, Error>>;
189+
190+
actix_service::forward_ready!(service);
122191

123-
prerender_request_headers.append(header::ACCEPT_ENCODING, "gzip".parse().unwrap());
192+
fn call(&self, req: ServiceRequest) -> Self::Future {
193+
// life goes on
194+
if !should_prerender(&req) {
195+
let fut = self.service.call(req);
196+
return Box::pin(async move { fut.await });
197+
}
124198

125-
// TODO: accept `X-Prerender-Token`
126-
// prerender_request_headers.insert("X-Prerender-Token", pre_render_token);
199+
// let response = self.get_rendered_response(req).await.map_err(|e| );
200+
todo!()
201+
}
127202
}
128203

129-
pub fn prerender_url() -> &'static str {
130-
"https://service.prerender.io/"
204+
impl<S> Transform<S, ServiceRequest> for Prerender
205+
where
206+
S: Service<ServiceRequest, Response = ServiceResponse, Error = Error>,
207+
S::Future: 'static,
208+
{
209+
type Response = ServiceResponse<BoxBody>;
210+
type Error = Error;
211+
type Transform = PrerenderMiddleware<S>;
212+
type InitError = ();
213+
type Future = Ready<Result<Self::Transform, Self::InitError>>;
214+
215+
fn new_transform(&self, service: S) -> Self::Future {
216+
future::ok(PrerenderMiddleware {
217+
service,
218+
prerender_service_url: self.prerender_service_url.clone(),
219+
inner_client: self.inner_client.clone(),
220+
})
221+
}
131222
}
132223

133224
#[cfg(test)]
134225
mod tests {
135-
use crate::middleware::should_prerender;
136-
use actix_web::http::{header, Method};
226+
use crate::middleware::{prerender_url, should_prerender, Prerender, PrerenderMiddleware};
227+
use actix_service::Transform;
228+
use actix_web::http::header;
229+
use actix_web::middleware::Compat;
137230
use actix_web::test::TestRequest;
231+
use actix_web::{test, App};
138232

139233
fn init_logger() {
140234
let _ = env_logger::builder().is_test(true).try_init();
141235
}
142236

143-
#[test]
144-
fn test_human_valid_resource() {
237+
fn compat_compat() {
238+
let _ = App::new().wrap(Compat::new(Prerender::builder().use_prerender_io()));
239+
}
240+
241+
#[actix_web::test]
242+
async fn test_human_valid_resource() {
145243
let req = TestRequest::get()
146244
.insert_header((
147245
header::USER_AGENT,
@@ -153,8 +251,8 @@ mod tests {
153251
assert!(!should_prerender(&req));
154252
}
155253

156-
#[test]
157-
fn test_crawler_valid_resource() {
254+
#[actix_web::test]
255+
async fn test_crawler_valid_resource() {
158256
let req = TestRequest::get()
159257
.insert_header((
160258
header::USER_AGENT,
@@ -166,8 +264,8 @@ mod tests {
166264
assert!(should_prerender(&req));
167265
}
168266

169-
#[test]
170-
fn test_crawler_ignored_resource() {
267+
#[actix_web::test]
268+
async fn test_crawler_ignored_resource() {
171269
let req = TestRequest::get()
172270
.insert_header((
173271
header::USER_AGENT,
@@ -180,8 +278,8 @@ mod tests {
180278
assert!(!render);
181279
}
182280

183-
#[test]
184-
fn test_crawler_wrong_http_method() {
281+
#[actix_web::test]
282+
async fn test_crawler_wrong_http_method() {
185283
let req = TestRequest::post()
186284
.insert_header((
187285
header::USER_AGENT,
@@ -193,4 +291,31 @@ mod tests {
193291
let render = should_prerender(&req);
194292
assert!(!render);
195293
}
294+
295+
fn create_middleware() -> Prerender {
296+
Prerender::builder().use_prerender_io()
297+
}
298+
299+
#[actix_web::test]
300+
async fn test_redirect_url() {
301+
let req_url = "http://yourserver.com/clothes/tshirts?query=xl";
302+
303+
let req = TestRequest::get()
304+
.insert_header((
305+
header::USER_AGENT,
306+
"Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0",
307+
))
308+
.uri(req_url)
309+
.to_srv_request();
310+
311+
let middleware = create_middleware()
312+
.new_transform(test::ok_service())
313+
.into_inner()
314+
.unwrap();
315+
316+
assert_eq!(
317+
middleware.prepare_build_api_url(&req),
318+
format!("{}{}", prerender_url(), req_url)
319+
);
320+
}
196321
}

0 commit comments

Comments
 (0)