Merge /run/media/user/d3de9062-a284-4b94-8900-0c416f57f9ac/projects/ptth
commit
292ade0f46
|
@ -20,7 +20,7 @@ futures = "0.3.7"
|
|||
futures-util = "0.3.8"
|
||||
handlebars = "3.5.3"
|
||||
http = "0.2.3"
|
||||
hyper = { version = "0.14.4", features = ["http1", "server", "stream", "tcp"] }
|
||||
hyper = { version = "0.14.20", features = ["http1", "http2", "server", "stream", "tcp"] }
|
||||
itertools = "0.9.0"
|
||||
rand = "0.8.3"
|
||||
rmp-serde = "0.15.5"
|
||||
|
|
|
@ -135,6 +135,11 @@ async fn handle_http_request (
|
|||
|
||||
let (tx, rx) = oneshot::channel ();
|
||||
|
||||
let tx = relay_state::ResponseRendezvous {
|
||||
timeout: Instant::now () + Duration::from_secs (120),
|
||||
tx,
|
||||
};
|
||||
|
||||
let req_id = rusty_ulid::generate_ulid_string ();
|
||||
|
||||
debug! ("Forwarding {}", req_id);
|
||||
|
@ -791,6 +796,45 @@ pub async fn run_relay (
|
|||
});
|
||||
}
|
||||
|
||||
// Set a task to periodically sweep and time-out requests where the client
|
||||
// and server are never going to rendezvous
|
||||
|
||||
let state_2 = Arc::clone (&state);
|
||||
tokio::spawn (async move {
|
||||
let mut interval = tokio::time::interval (Duration::from_secs (60));
|
||||
interval.set_missed_tick_behavior (tokio::time::MissedTickBehavior::Skip);
|
||||
|
||||
loop {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use rusty_ulid::Ulid;
|
||||
|
||||
interval.tick ().await;
|
||||
|
||||
{
|
||||
let timeout_ms = Utc::now ().timestamp () - 120_000;
|
||||
if let Ok (timeout_ms) = u64::try_from (timeout_ms) {
|
||||
let timeout_ulid = Ulid::from_timestamp_with_rng (timeout_ms, &mut rand::thread_rng ()).to_string ();
|
||||
|
||||
let mut request_rendezvous = state_2.request_rendezvous.lock ().await;
|
||||
request_rendezvous.iter_mut ()
|
||||
.for_each (|(k, v)| {
|
||||
match v {
|
||||
RequestRendezvous::ParkedServer (_) => (),
|
||||
RequestRendezvous::ParkedClients (requests) => requests.retain (|req| req.id.as_str () >= timeout_ulid.as_str ()),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let now = Instant::now ();
|
||||
let response_rendezvous = state_2.response_rendezvous.read ().await;
|
||||
response_rendezvous.retain (|_, v| v.timeout >= now);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let make_svc = make_service_fn (|_conn| {
|
||||
let state = state.clone ();
|
||||
let handlebars = handlebars.clone ();
|
||||
|
@ -849,7 +893,7 @@ pub async fn run_relay (
|
|||
std::mem::swap (&mut swapped, &mut response_rendezvous);
|
||||
|
||||
for (_, sender) in swapped {
|
||||
sender.send (Err (ShuttingDown)).ok ();
|
||||
sender.tx.send (Err (ShuttingDown)).ok ();
|
||||
}
|
||||
|
||||
let mut request_rendezvous = state.request_rendezvous.lock ().await;
|
||||
|
|
|
@ -61,7 +61,10 @@ pub enum RequestRendezvous {
|
|||
ParkedServer (oneshot::Sender <Result <http_serde::WrappedRequest, ShuttingDownError>>),
|
||||
}
|
||||
|
||||
type ResponseRendezvous = oneshot::Sender <Result <(http_serde::ResponseParts, Body), ShuttingDownError>>;
|
||||
pub (crate) struct ResponseRendezvous {
|
||||
pub timeout: Instant,
|
||||
pub tx: oneshot::Sender <Result <(http_serde::ResponseParts, Body), ShuttingDownError>>,
|
||||
}
|
||||
|
||||
#[derive (Clone)]
|
||||
pub struct ServerStatus {
|
||||
|
|
|
@ -119,6 +119,21 @@ pub async fn v1_server_list (state: &Relay)
|
|||
}
|
||||
}
|
||||
|
||||
fn get_api_key (headers: &hyper::HeaderMap) -> Option <&str>
|
||||
{
|
||||
if let Some (key) = headers.get ("X-ApiKey").and_then (|v| v.to_str ().ok ()) {
|
||||
return Some (key);
|
||||
}
|
||||
|
||||
if let Some (s) = headers.get ("Authorization").and_then (|v| v.to_str ().ok ()) {
|
||||
if let Some (key) = s.strip_prefix ("Bearer ") {
|
||||
return Some (key);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[instrument (level = "trace", skip (req, state))]
|
||||
async fn api_v1 (
|
||||
req: Request <Body>,
|
||||
|
@ -132,7 +147,7 @@ async fn api_v1 (
|
|||
AuditEvent,
|
||||
};
|
||||
|
||||
let api_key = req.headers ().get ("X-ApiKey");
|
||||
let api_key = get_api_key (req.headers ());
|
||||
|
||||
let api_key = match api_key {
|
||||
None => return Ok (error_reply (StatusCode::FORBIDDEN, strings::NO_API_KEY)?),
|
||||
|
@ -176,7 +191,10 @@ async fn api_v1 (
|
|||
path: path_rest.to_string (),
|
||||
})).await;
|
||||
|
||||
if path_rest == "test" {
|
||||
if path_rest == "metrics" {
|
||||
Ok (metrics (req, state).await?)
|
||||
}
|
||||
else if path_rest == "test" {
|
||||
Ok (error_reply (StatusCode::OK, "You're valid!")?)
|
||||
}
|
||||
else if path_rest == "server_list" {
|
||||
|
@ -205,6 +223,65 @@ async fn api_v1 (
|
|||
}
|
||||
}
|
||||
|
||||
#[instrument (level = "trace", skip (req, state))]
|
||||
async fn metrics (
|
||||
req: Request <Body>,
|
||||
state: &Relay,
|
||||
)
|
||||
-> Result <Response <Body>, RequestError>
|
||||
{
|
||||
let mut s = String::with_capacity (4 * 1_024);
|
||||
|
||||
let mut push_metric = |name, help, kind, value| {
|
||||
if let Some (help) = help {
|
||||
s.push_str (format! ("# HELP {} {}\n", name, help).as_str ());
|
||||
}
|
||||
s.push_str (format! ("# TYPE {} {}\n", name, kind).as_str ());
|
||||
s.push_str (format! ("{} {}\n", name, value).as_str ());
|
||||
};
|
||||
|
||||
let request_rendezvous_count = {
|
||||
let g = state.request_rendezvous.lock ().await;
|
||||
g.len ()
|
||||
};
|
||||
|
||||
let server_status_count;
|
||||
let connected_server_count;
|
||||
|
||||
let now = Utc::now ();
|
||||
|
||||
{
|
||||
let g = state.server_status.lock ().await;
|
||||
server_status_count = g.len ();
|
||||
connected_server_count = g.iter ()
|
||||
.filter (|(_, s)| now - s.last_seen < chrono::Duration::seconds (60))
|
||||
.count ();
|
||||
}
|
||||
|
||||
let response_rendezvous_count = {
|
||||
let g = state.response_rendezvous.read ().await;
|
||||
g.len ()
|
||||
};
|
||||
|
||||
push_metric ("request_rendezvous_count", None, "gauge", request_rendezvous_count.to_string ());
|
||||
push_metric ("server_status_count", None, "gauge", server_status_count.to_string ());
|
||||
push_metric ("connected_server_count", None, "gauge", connected_server_count.to_string ());
|
||||
push_metric ("response_rendezvous_count", None, "gauge", response_rendezvous_count.to_string ());
|
||||
|
||||
#[cfg (target_os = "linux")]
|
||||
{
|
||||
if let Some (rss) = tokio::fs::read_to_string ("/proc/self/status").await
|
||||
.ok ()
|
||||
.and_then (|s| get_rss_from_status (s.as_str ()))
|
||||
{
|
||||
push_metric ("relay_vm_rss", Some ("VmRSS of the relay process, in kB"), "gauge", rss.to_string ());
|
||||
}
|
||||
}
|
||||
|
||||
Ok (Response::builder ()
|
||||
.body (Body::from (s))?)
|
||||
}
|
||||
|
||||
#[instrument (level = "trace", skip (req, state))]
|
||||
pub async fn handle (
|
||||
req: Request <Body>,
|
||||
|
@ -230,6 +307,20 @@ pub async fn handle (
|
|||
}
|
||||
}
|
||||
|
||||
fn get_rss_from_status (proc_status: &str) -> Option <u64>
|
||||
{
|
||||
use std::str::FromStr;
|
||||
|
||||
for line in proc_status.lines () {
|
||||
if let Some (rest) = line.strip_prefix ("VmRSS:\t").and_then (|s| s.strip_suffix (" kB"))
|
||||
{
|
||||
return u64::from_str (rest.trim_start ()).ok ();
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg (test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
|
@ -246,8 +337,9 @@ mod tests {
|
|||
struct TestCase {
|
||||
// Inputs
|
||||
path_rest: &'static str,
|
||||
auth_header: Option <&'static str>,
|
||||
valid_key: Option <&'static str>,
|
||||
input_key: Option <&'static str>,
|
||||
x_api_key: Option <&'static str>,
|
||||
|
||||
// Expected
|
||||
expected_status: StatusCode,
|
||||
|
@ -268,9 +360,15 @@ mod tests {
|
|||
x
|
||||
}
|
||||
|
||||
fn input_key (&self, v: Option <&'static str>) -> Self {
|
||||
fn auth_header (&self, v: Option <&'static str>) -> Self {
|
||||
let mut x = self.clone ();
|
||||
x.input_key = v;
|
||||
x.auth_header = v;
|
||||
x
|
||||
}
|
||||
|
||||
fn x_api_key (&self, v: Option <&'static str>) -> Self {
|
||||
let mut x = self.clone ();
|
||||
x.x_api_key = v;
|
||||
x
|
||||
}
|
||||
|
||||
|
@ -298,13 +396,16 @@ mod tests {
|
|||
.expected_body (format! ("{}\n", body))
|
||||
}
|
||||
|
||||
async fn test (&self) {
|
||||
async fn test (&self, name: &str) {
|
||||
let mut input = Request::builder ()
|
||||
.method ("GET")
|
||||
.uri (format! ("http://127.0.0.1:4000/scraper/{}", self.path_rest));
|
||||
|
||||
if let Some (input_key) = self.input_key {
|
||||
input = input.header ("X-ApiKey", input_key);
|
||||
if let Some (auth_header) = self.auth_header {
|
||||
input = input.header ("Authorization", auth_header);
|
||||
}
|
||||
if let Some (x_api_key) = self.x_api_key {
|
||||
input = input.header ("X-ApiKey", x_api_key);
|
||||
}
|
||||
let input = input.body (Body::empty ()).unwrap ();
|
||||
|
||||
|
@ -331,15 +432,15 @@ mod tests {
|
|||
expected_headers.insert (*key, (*value).try_into ().expect ("Couldn't convert header value"));
|
||||
}
|
||||
|
||||
assert_eq! (actual_head.status, self.expected_status);
|
||||
assert_eq! (actual_head.headers, expected_headers);
|
||||
assert_eq! (actual_head.status, self.expected_status, "{}", name);
|
||||
assert_eq! (actual_head.headers, expected_headers, "{}", name);
|
||||
|
||||
let actual_body = hyper::body::to_bytes (actual_body).await;
|
||||
let actual_body = actual_body.expect ("Body should be convertible to bytes");
|
||||
let actual_body = actual_body.to_vec ();
|
||||
let actual_body = String::from_utf8 (actual_body).expect ("Body should be UTF-8");
|
||||
|
||||
assert_eq! (actual_body, self.expected_body);
|
||||
assert_eq! (actual_body, self.expected_body, "{}", name);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -351,7 +452,8 @@ mod tests {
|
|||
let base_case = TestCase {
|
||||
path_rest: "v1/test",
|
||||
valid_key: Some ("bogus"),
|
||||
input_key: Some ("bogus"),
|
||||
auth_header: None,
|
||||
x_api_key: Some ("bogus"),
|
||||
expected_status: StatusCode::OK,
|
||||
expected_headers: vec! [
|
||||
("content-type", "text/plain"),
|
||||
|
@ -359,21 +461,47 @@ mod tests {
|
|||
expected_body: "You're valid!\n".to_string (),
|
||||
};
|
||||
|
||||
for case in &[
|
||||
base_case.clone (),
|
||||
base_case.path_rest ("v9999/test")
|
||||
.expected (StatusCode::NOT_FOUND, strings::UNKNOWN_API_VERSION),
|
||||
base_case.valid_key (None)
|
||||
.expected (StatusCode::FORBIDDEN, strings::FORBIDDEN),
|
||||
base_case.input_key (Some ("borgus"))
|
||||
.expected (StatusCode::FORBIDDEN, strings::FORBIDDEN),
|
||||
base_case.path_rest ("v1/toast")
|
||||
.expected (StatusCode::NOT_FOUND, strings::UNKNOWN_API_ENDPOINT),
|
||||
base_case.input_key (None)
|
||||
.expected (StatusCode::FORBIDDEN, strings::NO_API_KEY),
|
||||
] {
|
||||
case.test ().await;
|
||||
}
|
||||
base_case
|
||||
.test ("00").await;
|
||||
|
||||
base_case
|
||||
.path_rest ("v9999/test")
|
||||
.expected (StatusCode::NOT_FOUND, strings::UNKNOWN_API_VERSION)
|
||||
.test ("01").await;
|
||||
|
||||
base_case
|
||||
.valid_key (None)
|
||||
.expected (StatusCode::FORBIDDEN, strings::FORBIDDEN)
|
||||
.test ("02").await;
|
||||
|
||||
base_case
|
||||
.x_api_key (Some ("borgus"))
|
||||
.expected (StatusCode::FORBIDDEN, strings::FORBIDDEN)
|
||||
.test ("03").await;
|
||||
|
||||
base_case
|
||||
.path_rest ("v1/toast")
|
||||
.expected (StatusCode::NOT_FOUND, strings::UNKNOWN_API_ENDPOINT)
|
||||
.test ("04").await;
|
||||
|
||||
base_case
|
||||
.x_api_key (None)
|
||||
.expected (StatusCode::FORBIDDEN, strings::NO_API_KEY)
|
||||
.test ("05").await;
|
||||
|
||||
base_case
|
||||
.x_api_key (None)
|
||||
.auth_header (Some ("Bearer bogus"))
|
||||
.expected (StatusCode::OK, "You're valid!")
|
||||
.test ("06").await;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rss () {
|
||||
let input = "VmHWM: 584 kB\nVmRSS: 584 kB\nRssAnon: 68 kB\n";
|
||||
|
||||
assert_eq! (get_rss_from_status (input), Some (584));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,7 +188,7 @@ pub async fn handle_response (
|
|||
};
|
||||
|
||||
// UKAUFFY4 (Send half)
|
||||
if tx.send (Ok ((resp_parts, body))).is_err () {
|
||||
if tx.tx.send (Ok ((resp_parts, body))).is_err () {
|
||||
let msg = "Failed to connect to client";
|
||||
error! (msg);
|
||||
return Ok (error_reply (StatusCode::BAD_GATEWAY, msg)?);
|
||||
|
|
|
@ -47,13 +47,12 @@ fn main ()
|
|||
let app = app::App::default();
|
||||
let mut wind = Window::new (100, 100, 500, 180, "PTTH server");
|
||||
|
||||
let config_file_opt = match ptth_server::load_toml::load::<ConfigFile, _> ("./config/ptth_server.toml")
|
||||
{
|
||||
let config_file_opt = match ptth_server::load_toml::load::<ConfigFile, _> ("./config/ptth_server.toml") {
|
||||
Ok (x) => Some (x),
|
||||
Err (e) => {
|
||||
eprintln! ("Error in config TOML: {:?}", e);
|
||||
eprintln! ("Error in `./config/ptth_server.toml`: {:?}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let (hit_tx, mut hit_rx) = mpsc::channel (1);
|
||||
|
@ -61,9 +60,7 @@ fn main ()
|
|||
let fltk_tx = fltk_tx;
|
||||
|
||||
rt.spawn (async move {
|
||||
eprintln! ("Entering channel task");
|
||||
while hit_rx.recv ().await.is_some () {
|
||||
eprintln! ("fltk_tx");
|
||||
fltk_tx.send (Message::Hit);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -115,7 +115,7 @@ AIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAA" rel="icon" type="image/x-icon" />
|
|||
|
||||
{{#each entries}}
|
||||
<tr>
|
||||
<td><a class="entry" href="{{this.encoded_file_name}}{{this.trailing_slash}}">
|
||||
<td><a class="entry" href="./{{this.encoded_file_name}}{{this.trailing_slash}}">
|
||||
{{this.icon}} {{this.file_name}}{{this.trailing_slash}}</a></td>
|
||||
<td><span class="grey">{{this.size}}</span></td>
|
||||
</tr>
|
||||
|
|
Loading…
Reference in New Issue