Skip to content

Commit 4ea1cee

Browse files
authored
adds server with post api (#1)
* adds server with post api * CORS setup
1 parent ace9521 commit 4ea1cee

13 files changed

+958
-3
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
Cargo.lock
22
/target
33
mockup.html
4+
config.json
5+
certs/*.pem

Cargo.toml

+10-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ edition = "2021"
77
name = "psv"
88
path = "src/main.rs"
99

10+
[[bin]]
11+
name = "psv-server"
12+
path = "src/main-server.rs"
13+
1014
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1115

1216
[dependencies]
@@ -16,5 +20,10 @@ scraper = "0.19"
1620
chrono = "0.4.31"
1721
semver = "1.0.20"
1822
rand = "0.8.5"
23+
regex = "1.10.2"
1924
reqwest = { version = "0.11", features = ["json"] }
20-
openssl = { version = "0.10", features = ["vendored"] }
25+
openssl = { version = "0.10", features = ["vendored"] }
26+
axum = "0.6.20"
27+
axum-server = { version = "0.3", features = ["tls-rustls"] }
28+
serde = {version="1.0", features=["derive"]}
29+
serde_json = "1.0"

certs/gen.sh

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
openssl req -nodes -new -x509 -keyout key.pem -out cert.pem -days 365
2+
#openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use semver::{BuildMetadata, Prerelease, Version};
33
pub mod util;
44
pub mod model;
55
pub mod generate;
6+
pub mod server;
67

78
const MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
89
const MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");

src/main-server.rs

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use psv::server::http::ServerHttp;
2+
use psv::server::https::Server;
3+
use psv::program_version;
4+
5+
use tokio::task::spawn;
6+
7+
#[tokio::main]
8+
async fn main() {
9+
10+
let args: Vec<String> = std::env::args().collect();
11+
12+
if args.iter().any(|x| x == "-v")
13+
{
14+
println!("Version: {}", program_version());
15+
std::process::exit(0);
16+
}
17+
18+
if args.iter().any(|x| x == "-d")
19+
{
20+
unsafe { psv::OPTIONS.debug = true; }
21+
}
22+
23+
if args.iter().any(|x| x == "-t")
24+
{
25+
unsafe { psv::OPTIONS.debug_timestamp = true; }
26+
}
27+
28+
let server = Server::new(0,0,0,0);
29+
30+
let http_server = ServerHttp::new(0,0,0,0);
31+
32+
let _http_redirect = spawn(http_server.serve());
33+
34+
server.serve().await;
35+
36+
}

src/model.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use serde::{Deserialize, Serialize};
2+
13
#[derive(Debug)]
24
/// A model of the Google Play App store's listitem children
35
/// ```feature```: feature image url
@@ -60,7 +62,7 @@ impl AppEntry
6062
}
6163
}
6264

63-
#[derive(Debug)]
65+
#[derive(Debug, Serialize, Deserialize, Clone)]
6466
/// A model of a mockup Google Play store's listitem child
6567
/// ```feature_link```: feature image url
6668
/// ```icon_link```: icon image url

src/server/api.rs

+266
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
use std::str::from_utf8;
2+
3+
use axum::{
4+
body::Bytes, http::{HeaderMap, Request}, middleware::Next, response::{Html, IntoResponse, Response}
5+
};
6+
use openssl::conf;
7+
use reqwest::StatusCode;
8+
use serde::Deserialize;
9+
10+
use crate::{generate::generate_mockup, model::UserAppEntry};
11+
12+
use super::{config::read_config, is_authentic};
13+
14+
/// A trait representing an API request to the server
15+
/// - For example [crate::server::api::stats::Generate]
16+
pub trait ApiRequest
17+
{
18+
/// Validate a request's hmac given a token read from config.json
19+
/// - See [crate::config::Config] and [crate::web::is_authentic]
20+
fn is_authentic(headers: HeaderMap, body: Bytes) -> StatusCode;
21+
/// Deserialise the Bytes body from JSON
22+
fn deserialise_payload(&mut self, headers: HeaderMap, body: Bytes) -> StatusCode;
23+
/// Formulate a response form the server returned as a String
24+
/// - Also perform any actions inherent to this Api call
25+
async fn into_response(&self) -> (Option<String>, StatusCode);
26+
/// Axum middleware to
27+
/// 1. check headers for an api request type
28+
/// 2. authenticate the request (HMAC)
29+
/// 3. respond to it
30+
/// 4. continue on to the next reqeust
31+
async fn filter<B>
32+
(
33+
headers: HeaderMap,
34+
request: Request<B>,
35+
next: Next<B>
36+
) -> Result<Response, StatusCode>
37+
where B: axum::body::HttpBody<Data = Bytes>;
38+
39+
}
40+
41+
/// Payload for [Generate] Api request
42+
/// - ```from_utc```: takes a utc date to compile statistics from
43+
/// - ```to_utc```: takes a utc date to compile statistics to
44+
/// - ```post_discord```: whether to post to dicsord or not
45+
#[derive(Deserialize, Debug)]
46+
pub struct GeneratePayload
47+
{
48+
pub app: UserAppEntry,
49+
pub query: Option<String>,
50+
pub position: Option<usize>
51+
}
52+
53+
/// Payload for [Generate] Api request, see [GeneratePayload]
54+
/// - Takes a utc date to compile statistics from, and a switch to post a discord message
55+
/// - All saved hit statistics after from_utc will be included
56+
pub struct Generate
57+
{
58+
payload: GeneratePayload
59+
}
60+
61+
impl Generate
62+
{
63+
pub fn new() -> Generate
64+
{
65+
Generate
66+
{
67+
payload: GeneratePayload
68+
{
69+
app: UserAppEntry::new("FEATURE", "ICON", "TITLE", "DEVELOPER", "STARS", "LINK"),
70+
query: Some("particles".to_string()),
71+
position: Some(0)
72+
}
73+
}
74+
}
75+
}
76+
77+
impl ApiRequest for Generate
78+
{
79+
fn is_authentic(headers: HeaderMap, body: Bytes) -> StatusCode
80+
{
81+
82+
let config = match read_config()
83+
{
84+
Some(c) => c,
85+
None =>
86+
{
87+
return StatusCode::INTERNAL_SERVER_ERROR;
88+
}
89+
};
90+
91+
match config.api_token
92+
{
93+
Some(t) =>
94+
{
95+
is_authentic
96+
(
97+
headers,
98+
"psv-token",
99+
t,
100+
body
101+
)
102+
},
103+
None => StatusCode::ACCEPTED
104+
}
105+
}
106+
107+
fn deserialise_payload(&mut self, _headers: HeaderMap, body: Bytes) -> StatusCode
108+
{
109+
110+
self.payload = match from_utf8(&body)
111+
{
112+
Ok(s) =>
113+
{
114+
match serde_json::from_str(s)
115+
{
116+
Ok(p) => p,
117+
Err(e) =>
118+
{
119+
crate::debug(format!("{} deserialising POST payload",e), Some("Stats Digest".to_string()));
120+
return StatusCode::BAD_REQUEST
121+
}
122+
}
123+
}
124+
Err(e) =>
125+
{
126+
crate::debug(format!("{} deserialising POST payload",e), Some("Stats Digest".to_string()));
127+
return StatusCode::BAD_REQUEST
128+
}
129+
};
130+
131+
StatusCode::OK
132+
}
133+
134+
async fn into_response(&self) -> (Option<String>, StatusCode)
135+
{
136+
let config = match read_config()
137+
{
138+
Some(c) => c,
139+
None =>
140+
{
141+
return (None, StatusCode::INTERNAL_SERVER_ERROR);
142+
}
143+
};
144+
145+
let query = match self.payload.query.clone()
146+
{
147+
Some(q) => q,
148+
None => "particles".to_string()
149+
};
150+
151+
// get a live example from the Play Store
152+
let html = reqwest::get(format!("https://play.google.com/store/search?q={}&c=apps",query))
153+
.await.unwrap()
154+
.text()
155+
.await.unwrap();
156+
157+
let position = match self.payload.position
158+
{
159+
Some(p) => p+2,
160+
None => 3
161+
};
162+
163+
crate::debug(format!("Generating from\n {:?}\n{}", self.payload.app.clone(), position), None);
164+
165+
let generated = match generate_mockup
166+
(
167+
html,
168+
self.payload.app.clone(),
169+
Some(position)
170+
)
171+
{
172+
Ok(g) => g,
173+
Err(e) => {println!("{}", e); std::process::exit(1);}
174+
};
175+
176+
(Some(generated), StatusCode::OK)
177+
}
178+
179+
async fn filter<B>
180+
(
181+
headers: HeaderMap,
182+
request: Request<B>,
183+
next: Next<B>
184+
) -> Result<Response, StatusCode>
185+
where B: axum::body::HttpBody<Data = Bytes>
186+
{
187+
188+
if !headers.contains_key("api")
189+
{
190+
return Ok(next.run(request).await)
191+
}
192+
193+
let config = match read_config()
194+
{
195+
Some(c) => c,
196+
None =>
197+
{
198+
return Err(StatusCode::INTERNAL_SERVER_ERROR)
199+
}
200+
};
201+
202+
let api = match std::str::from_utf8(headers["api"].as_bytes())
203+
{
204+
Ok(u) => u,
205+
Err(_) =>
206+
{
207+
crate::debug("no/mangled user agent".to_string(), None);
208+
return Ok(next.run(request).await)
209+
}
210+
};
211+
212+
match api == "Generate"
213+
{
214+
true => {},
215+
false => { return Ok(next.run(request).await) }
216+
}
217+
218+
let body = request.into_body();
219+
let bytes = match body.collect().await {
220+
Ok(collected) => collected.to_bytes(),
221+
Err(_) => {
222+
return Err(StatusCode::BAD_REQUEST)
223+
}
224+
};
225+
226+
match Generate::is_authentic(headers.clone(), bytes.clone())
227+
{
228+
StatusCode::ACCEPTED => {},
229+
e => { return Ok(e.into_response()) }
230+
}
231+
232+
let mut response = Generate::new();
233+
234+
match response.deserialise_payload(headers, bytes)
235+
{
236+
StatusCode::OK => {},
237+
e => { return Ok(e.into_response()) }
238+
}
239+
240+
let (result, status) = response.into_response().await;
241+
242+
match result
243+
{
244+
Some(s) =>
245+
{
246+
let mut response = Html(s).into_response();
247+
let time_stamp = chrono::offset::Utc::now().to_rfc3339();
248+
response.headers_mut().insert("date", time_stamp.parse().unwrap());
249+
response.headers_mut().insert("cache-control", format!("public, max-age={}", config.cache_period_seconds).parse().unwrap());
250+
251+
match config.cors_allow_address
252+
{
253+
Some(a) =>
254+
{
255+
response.headers_mut().insert("Access-Control-Allow-Origin", format!("{}",a).parse().unwrap());
256+
response.headers_mut().insert("Access-Control-Allow-Methods", "POST".parse().unwrap());
257+
},
258+
None => {}
259+
}
260+
Ok(response)
261+
},
262+
None => { Err(status) }
263+
}
264+
}
265+
266+
}

0 commit comments

Comments
 (0)