Skip to content

Commit

Permalink
adds server with post api
Browse files Browse the repository at this point in the history
  • Loading branch information
Jerboa-app committed Mar 24, 2024
1 parent 0504b45 commit becc287
Show file tree
Hide file tree
Showing 13 changed files with 891 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
Cargo.lock
/target
mockup.html
config.json
certs/*.pem
11 changes: 10 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ edition = "2021"
name = "psv"
path = "src/main.rs"

[[bin]]
name = "psv-server"
path = "src/main-server.rs"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
Expand All @@ -16,5 +20,10 @@ scraper = "0.19"
chrono = "0.4.31"
semver = "1.0.20"
rand = "0.8.5"
regex = "1.10.2"
reqwest = { version = "0.11", features = ["json"] }
openssl = { version = "0.10", features = ["vendored"] }
openssl = { version = "0.10", features = ["vendored"] }
axum = "0.6.20"
axum-server = { version = "0.3", features = ["tls-rustls"] }
serde = {version="1.0", features=["derive"]}
serde_json = "1.0"
2 changes: 2 additions & 0 deletions certs/gen.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
openssl req -nodes -new -x509 -keyout key.pem -out cert.pem -days 365
#openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use semver::{BuildMetadata, Prerelease, Version};
pub mod util;
pub mod model;
pub mod generate;
pub mod server;

const MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
const MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");
Expand Down
36 changes: 36 additions & 0 deletions src/main-server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use psv::server::http::ServerHttp;
use psv::server::https::Server;
use psv::program_version;

use tokio::task::spawn;

#[tokio::main]
async fn main() {

let args: Vec<String> = std::env::args().collect();

if args.iter().any(|x| x == "-v")
{
println!("Version: {}", program_version());
std::process::exit(0);
}

if args.iter().any(|x| x == "-d")
{
unsafe { psv::OPTIONS.debug = true; }
}

if args.iter().any(|x| x == "-t")
{
unsafe { psv::OPTIONS.debug_timestamp = true; }
}

let server = Server::new(0,0,0,0);

let http_server = ServerHttp::new(0,0,0,0);

let _http_redirect = spawn(http_server.serve());

server.serve().await;

}
4 changes: 3 additions & 1 deletion src/model.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use serde::{Deserialize, Serialize};

#[derive(Debug)]
/// A model of the Google Play App store's listitem children
/// ```feature```: feature image url
Expand Down Expand Up @@ -60,7 +62,7 @@ impl AppEntry
}
}

#[derive(Debug)]
#[derive(Debug, Serialize, Deserialize, Clone)]
/// A model of a mockup Google Play store's listitem child
/// ```feature_link```: feature image url
/// ```icon_link```: icon image url
Expand Down
248 changes: 248 additions & 0 deletions src/server/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
use std::str::from_utf8;

use axum::{
body::Bytes, http::{HeaderMap, Request}, middleware::Next, response::{Html, IntoResponse, Response}
};
use reqwest::StatusCode;
use serde::Deserialize;

use crate::{generate::generate_mockup, model::UserAppEntry};

use super::{config::read_config, is_authentic};

/// A trait representing an API request to the server
/// - For example [crate::server::api::stats::Generate]
pub trait ApiRequest
{
/// Validate a request's hmac given a token read from config.json
/// - See [crate::config::Config] and [crate::web::is_authentic]
fn is_authentic(headers: HeaderMap, body: Bytes) -> StatusCode;
/// Deserialise the Bytes body from JSON
fn deserialise_payload(&mut self, headers: HeaderMap, body: Bytes) -> StatusCode;
/// Formulate a response form the server returned as a String
/// - Also perform any actions inherent to this Api call
async fn into_response(&self) -> (Option<String>, StatusCode);
/// Axum middleware to
/// 1. check headers for an api request type
/// 2. authenticate the request (HMAC)
/// 3. respond to it
/// 4. continue on to the next reqeust
async fn filter<B>
(
headers: HeaderMap,
request: Request<B>,
next: Next<B>
) -> Result<Response, StatusCode>
where B: axum::body::HttpBody<Data = Bytes>;

}

/// Payload for [Generate] Api request
/// - ```from_utc```: takes a utc date to compile statistics from
/// - ```to_utc```: takes a utc date to compile statistics to
/// - ```post_discord```: whether to post to dicsord or not
#[derive(Deserialize, Debug)]
pub struct GeneratePayload
{
pub app: UserAppEntry,
pub query: Option<String>,
pub position: Option<usize>
}

/// Payload for [Generate] Api request, see [GeneratePayload]
/// - Takes a utc date to compile statistics from, and a switch to post a discord message
/// - All saved hit statistics after from_utc will be included
pub struct Generate
{
payload: GeneratePayload
}

impl Generate
{
pub fn new() -> Generate
{
Generate
{
payload: GeneratePayload
{
app: UserAppEntry::new("FEATURE", "ICON", "TITLE", "DEVELOPER", "STARS", "LINK"),
query: Some("particles".to_string()),
position: Some(0)
}
}
}
}

impl ApiRequest for Generate
{
fn is_authentic(headers: HeaderMap, body: Bytes) -> StatusCode
{

let config = match read_config()
{
Some(c) => c,
None =>
{
return StatusCode::INTERNAL_SERVER_ERROR;
}
};

is_authentic
(
headers,
"psv-token",
config.api_token,
body
)
}

fn deserialise_payload(&mut self, _headers: HeaderMap, body: Bytes) -> StatusCode
{

self.payload = match from_utf8(&body)
{
Ok(s) =>
{
match serde_json::from_str(s)
{
Ok(p) => p,
Err(e) =>
{
crate::debug(format!("{} deserialising POST payload",e), Some("Stats Digest".to_string()));
return StatusCode::BAD_REQUEST
}
}
}
Err(e) =>
{
crate::debug(format!("{} deserialising POST payload",e), Some("Stats Digest".to_string()));
return StatusCode::BAD_REQUEST
}
};

StatusCode::OK
}

async fn into_response(&self) -> (Option<String>, StatusCode)
{
let config = match read_config()
{
Some(c) => c,
None =>
{
return (None, StatusCode::INTERNAL_SERVER_ERROR);
}
};

let query = match self.payload.query.clone()
{
Some(q) => q,
None => "particles".to_string()
};

// get a live example from the Play Store
let html = reqwest::get(format!("https://play.google.com/store/search?q={}&c=apps",query))
.await.unwrap()
.text()
.await.unwrap();

let position = match self.payload.position
{
Some(p) => p+2,
None => 3
};

crate::debug(format!("Generating from\n {:?}\n{}", self.payload.app.clone(), position), None);

let generated = match generate_mockup
(
html,
self.payload.app.clone(),
Some(position)
)
{
Ok(g) => g,
Err(e) => {println!("{}", e); std::process::exit(1);}
};

(Some(generated), StatusCode::OK)
}

async fn filter<B>
(
headers: HeaderMap,
request: Request<B>,
next: Next<B>
) -> Result<Response, StatusCode>
where B: axum::body::HttpBody<Data = Bytes>
{

if !headers.contains_key("api")
{
return Ok(next.run(request).await)
}

let config = match read_config()
{
Some(c) => c,
None =>
{
return Err(StatusCode::INTERNAL_SERVER_ERROR)
}
};

let api = match std::str::from_utf8(headers["api"].as_bytes())
{
Ok(u) => u,
Err(_) =>
{
crate::debug("no/mangled user agent".to_string(), None);
return Ok(next.run(request).await)
}
};

match api == "Generate"
{
true => {},
false => { return Ok(next.run(request).await) }
}

let body = request.into_body();
let bytes = match body.collect().await {
Ok(collected) => collected.to_bytes(),
Err(_) => {
return Err(StatusCode::BAD_REQUEST)
}
};

match Generate::is_authentic(headers.clone(), bytes.clone())
{
StatusCode::ACCEPTED => {},
e => { return Ok(e.into_response()) }
}

let mut response = Generate::new();

match response.deserialise_payload(headers, bytes)
{
StatusCode::OK => {},
e => { return Ok(e.into_response()) }
}

let (result, status) = response.into_response().await;

match result
{
Some(s) =>
{
let mut response = Html(s).into_response();
let time_stamp = chrono::offset::Utc::now().to_rfc3339();
response.headers_mut().insert("date", time_stamp.parse().unwrap());
response.headers_mut().insert("cache-control", format!("public, max-age={}", config.cache_period_seconds).parse().unwrap());
Ok(response)
},
None => { Err(status) }
}
}

}
Loading

0 comments on commit becc287

Please sign in to comment.