use crate::config::CFG;
use crate::config::VIEWER_SERVED_MIME_TYPES_HMAP;
use crate::note::Note;
use crate::viewer::error::ViewerError;
use crate::viewer::init::LOCALHOST;
use parse_hyperlinks_extras::iterator_html::HyperlinkInlineImage;
use percent_encoding::percent_decode_str;
use std::collections::HashSet;
use std::fs;
use std::io::{ErrorKind, Read, Write};
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use std::net::SocketAddrV4;
use std::net::{TcpListener, TcpStream};
use std::path::Path;
use std::path::PathBuf;
use std::str;
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::SystemTime;
use url::Url;
const TCP_READ_BUFFER_SIZE: usize = 0x400;
const TCP_WRITE_BUFFER_SIZE: usize = 0x1000;
pub const SSE_CLIENT_CODE1: &str = r#"
var evtSource = new EventSource("http://"#;
pub const SSE_CLIENT_CODE2: &str = r#"/events");
evtSource.addEventListener("update", function(e) {
localStorage.setItem('scrollPosition', window.scrollY);
window.location.reload(true);
});
window.addEventListener('load', function() {
if(localStorage.getItem('scrollPosition') !== null)
window.scrollTo(0, localStorage.getItem('scrollPosition'));
});
"#;
const PATH_UPDIR_ALIAS: &str = "ParentDir..";
const SSE_EVENT_PATH: &str = "/events";
pub const FAVICON: &[u8] = include_bytes!("favicon.ico");
pub const FAVICON_PATH: &str = "/favicon.ico";
const MAX_AGE: u64 = 600;
#[derive(Debug, Clone, Copy)]
pub enum SseToken {
Ping,
Update,
}
pub fn manage_connections(
event_tx_list: Arc<Mutex<Vec<SyncSender<SseToken>>>>,
listener: TcpListener,
doc_path: PathBuf,
) {
let sse_port = listener.local_addr().unwrap().port();
let doc_local_links = Arc::new(RwLock::new(HashSet::new()));
let conn_counter = Arc::new(());
for stream in listener.incoming() {
match stream {
Ok(stream) => {
let (event_tx, event_rx) = sync_channel(0);
event_tx_list.lock().unwrap().push(event_tx);
let doc_path = doc_path.clone();
let doc_local_links = doc_local_links.clone();
let conn_counter = conn_counter.clone();
thread::spawn(move || {
let mut st = ServerThread::new(
event_rx,
stream,
sse_port,
doc_path,
doc_local_links,
conn_counter,
);
st.serve_connection()
});
}
Err(e) => log::warn!("TCP connection failed: {}", e),
}
}
}
struct ServerThread {
rx: Receiver<SseToken>,
stream: TcpStream,
sse_port: u16,
doc_path: PathBuf,
doc_local_links: Arc<RwLock<HashSet<PathBuf>>>,
conn_counter: Arc<()>,
}
impl ServerThread {
fn new(
rx: Receiver<SseToken>,
stream: TcpStream,
sse_port: u16,
doc_path: PathBuf,
doc_local_links: Arc<RwLock<HashSet<PathBuf>>>,
conn_counter: Arc<()>,
) -> Self {
Self {
rx,
stream,
sse_port,
doc_path,
doc_local_links,
conn_counter,
}
}
fn serve_connection(&mut self) {
match Self::serve_connection2(self) {
Ok(_) => (),
Err(e) => {
log::debug!(
"TCP peer port {}: Closed connection because of error: {}",
self.stream
.peer_addr()
.unwrap_or_else(|_| SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(0, 0, 0, 0),
0
)))
.port(),
e
);
}
}
}
#[inline]
#[allow(clippy::needless_return)]
fn serve_connection2(&mut self) -> Result<(), ViewerError> {
let open_connections = Arc::<()>::strong_count(&self.conn_counter) - 1;
log::trace!(
"TCP peer port {}: New incoming TCP connection ({} open).",
self.stream.peer_addr()?.port(),
open_connections
);
if open_connections > CFG.viewer.tcp_connections_max {
self.respond_service_unavailable()?;
return Err(ViewerError::TcpConnectionsExceeded {
max_conn: CFG.viewer.tcp_connections_max,
});
}
'tcp_connection: loop {
let mut read_buffer = [0u8; TCP_READ_BUFFER_SIZE];
let mut buffer = Vec::new();
let (method, path) = 'assemble_tcp_chunks: loop {
match self.stream.read(&mut read_buffer) {
Ok(0) => {
log::trace!(
"TCP peer port {}: Connection closed by peer.",
self.stream.peer_addr()?.port()
);
break 'tcp_connection;
}
Err(e) => {
return Err(ViewerError::StreamRead { error: e });
}
Ok(n) => {
buffer.extend_from_slice(&read_buffer[..n]);
log::trace!(
"TCP peer port {}: chunk: {:?} ...",
self.stream.peer_addr()?.port(),
std::str::from_utf8(&read_buffer)
.unwrap_or_default()
.chars()
.take(60)
.collect::<String>()
);
}
}
let mut headers = [httparse::EMPTY_HEADER; 16];
let mut req = httparse::Request::new(&mut headers);
let res = req.parse(&buffer)?;
if res.is_partial() {
continue 'assemble_tcp_chunks;
}
if res.is_complete() {
if let (Some(method), Some(path)) = (req.method, req.path) {
break 'assemble_tcp_chunks (method, path);
}
};
return Err(ViewerError::StreamParse {
source_str: std::str::from_utf8(&*buffer)
.unwrap_or_default()
.chars()
.take(60)
.collect::<String>(),
});
};
if method != "GET" {
self.respond_method_not_allowed(method)?;
continue 'tcp_connection;
}
let path = percent_decode_str(path).decode_utf8()?;
match &*path {
"/" => {
let html = Self::render_content_and_error(self)?;
self.respond_content_ok(Path::new("/"), "text/html", html.as_bytes())?;
}
SSE_EVENT_PATH => {
self.respond_event_ok()?;
self.stream.set_nonblocking(true)?;
'_event: loop {
let msg = self.rx.recv()?;
match self.stream.read(&mut read_buffer) {
Ok(0) => {
log::trace!(
"TCP peer port {}: Event connection closed by peer.",
self.stream.peer_addr()?.port()
);
break 'tcp_connection;
}
Ok(_) => {}
Err(e) => {
if e.kind() != ErrorKind::WouldBlock {
return Err(ViewerError::StreamRead { error: e });
}
}
}
let event = match msg {
SseToken::Update => "event: update\r\ndata:\r\n\r\n".to_string(),
SseToken::Ping => ": ping\r\n\r\n".to_string(),
};
self.stream.write_all(event.as_bytes())?;
log::debug!(
"TCP peer port {} ({} open TCP conn.): pushed '{:?}' in event connection to web browser.",
self.stream.peer_addr()?.port(),
Arc::<()>::strong_count(&self.conn_counter) - 1,
msg,
);
}
}
FAVICON_PATH => {
self.respond_content_ok(Path::new(&FAVICON_PATH), "image/x-icon", FAVICON)?;
}
_ => {
let doc_path = self.doc_path.canonicalize()?;
let doc_dir = doc_path.parent().unwrap_or_else(|| Path::new(""));
let path = Path::new(
path.strip_prefix('/')
.ok_or(ViewerError::UrlMustStartWithSlash)?,
);
let mut reqpath = doc_dir.to_owned();
for p in path.iter() {
if p == "." {
continue;
}
if p == PATH_UPDIR_ALIAS || p == ".." {
reqpath.pop();
} else {
reqpath.push(p);
}
}
let extension = &*reqpath
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_lowercase();
let mime_type = match VIEWER_SERVED_MIME_TYPES_HMAP.get(&*extension) {
Some(mt) => mt,
None => {
log::warn!(
"TCP peer port {}: \
files with extension '{}' are not served. Rejecting: '{}'",
self.stream.peer_addr()?.port(),
reqpath
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
reqpath.to_str().unwrap_or_default(),
);
self.respond_not_found(&reqpath)?;
continue 'tcp_connection;
}
};
let doc_local_links = self
.doc_local_links
.read()
.expect("Can not read `doc_local_links`! RwLock is poisoned. Panic.");
if !doc_local_links.contains(path) {
log::warn!(
"TCP peer port {}: target not referenced in note file, rejecting: '{}'",
self.stream.peer_addr()?.port(),
path.to_str().unwrap_or(""),
);
drop(doc_local_links);
self.respond_not_found(&reqpath)?;
continue 'tcp_connection;
}
drop(doc_local_links);
#[allow(clippy::or_fun_call)]
let doc_parent_dir = doc_dir.parent().unwrap_or(Path::new(""));
if !reqpath.starts_with(doc_parent_dir) {
log::warn!(
"TCP peer port {}:\
file '{}' is not in directory '{}', rejecting.",
self.stream.peer_addr()?.port(),
reqpath.to_str().unwrap_or_default(),
doc_parent_dir.to_str().unwrap_or_default()
);
self.respond_not_found(&reqpath)?;
continue 'tcp_connection;
}
if fs::metadata(&reqpath)?.is_file() {
self.respond_file_ok(&reqpath, mime_type)?;
} else {
self.respond_not_found(&reqpath)?;
}
}
}; }
log::trace!(
"TCP peer port {}: ({} open). Closing this TCP connection.",
self.stream.peer_addr()?.port(),
Arc::<()>::strong_count(&self.conn_counter) - 2,
);
Ok(())
}
fn respond_event_ok(&mut self) -> Result<(), ViewerError> {
let response = format!(
"\
HTTP/1.1 200 OK\r\n\
Date: {}\r\n\
Access-Control-Allow-Origin: *\r\n\
Cache-Control: no-cache\r\n\
Content-Type: text/event-stream\r\n\
\r\n",
httpdate::fmt_http_date(SystemTime::now()),
);
self.stream.write_all(response.as_bytes())?;
log::debug!(
"TCP peer port {}: 200 OK, served event header, \
keeping event connection open ...",
self.stream.peer_addr()?.port(),
);
Ok(())
}
fn respond_file_ok(&mut self, reqpath: &Path, mime_type: &str) -> Result<(), ViewerError> {
let response = format!(
"HTTP/1.1 200 OK\r\n\
Date: {}\r\n\
Cache-Control: private, max-age={}\r\n\
Content-Type: {}\r\n\
Content-Length: {}\r\n\r\n",
httpdate::fmt_http_date(SystemTime::now()),
MAX_AGE,
mime_type,
fs::metadata(&reqpath)?.len(),
);
self.stream.write_all(response.as_bytes())?;
let mut buffer = [0; TCP_WRITE_BUFFER_SIZE];
let mut file = fs::File::open(&reqpath)?;
while let Ok(n) = file.read(&mut buffer[..]) {
if n == 0 {
break;
};
self.stream.write_all(&buffer[..n])?;
}
log::debug!(
"TCP peer port {}: 200 OK, served file: '{}'",
self.stream.peer_addr()?.port(),
reqpath.to_str().unwrap_or_default()
);
Ok(())
}
fn respond_content_ok(
&mut self,
reqpath: &Path,
mime_type: &str,
content: &[u8],
) -> Result<(), ViewerError> {
let response = format!(
"HTTP/1.1 200 OK\r\n\
Date: {}\r\n\
Cache-Control: private, max-age={}\r\n\
Content-Type: {}\r\n\
Content-Length: {}\r\n\r\n",
httpdate::fmt_http_date(SystemTime::now()),
MAX_AGE,
mime_type,
content.len(),
);
self.stream.write_all(response.as_bytes())?;
self.stream.write_all(content)?;
log::debug!(
"TCP peer port {}: 200 OK, served file: '{}'",
self.stream.peer_addr()?.port(),
reqpath.to_str().unwrap_or_default()
);
Ok(())
}
fn respond_not_found(&mut self, reqpath: &Path) -> Result<(), ViewerError> {
self.stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n")?;
log::debug!(
"TCP peer port {}: 404 \"Not found\" served: '{}'",
self.stream.peer_addr()?.port(),
reqpath.to_str().unwrap_or_default()
);
Ok(())
}
fn respond_method_not_allowed(&mut self, path: &str) -> Result<(), ViewerError> {
self.stream
.write_all(b"HTTP/1.1 405 Method Not Allowed\r\n\r\n")?;
log::debug!(
"TCP peer port {}: 405 \"Method Not Allowed\" served: '{}'",
self.stream.peer_addr()?.port(),
path,
);
Ok(())
}
fn respond_service_unavailable(&mut self) -> Result<(), ViewerError> {
self.stream
.write_all(b"HTTP/1.1 503 Service Unavailable\r\n\r\n")?;
Ok(())
}
#[inline]
fn render_content_and_error(&self) -> Result<String, ViewerError> {
let js = format!(
"{}{}:{}{}",
SSE_CLIENT_CODE1, LOCALHOST, self.sse_port, SSE_CLIENT_CODE2
);
let file_path_ext = self
.doc_path
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
match Note::from_existing_note(&self.doc_path)
.and_then(|mut note| {
note.render_content(file_path_ext, &CFG.viewer.rendition_tmpl, &js)
})
.and_then(|html| {
let mut doc_local_links = self
.doc_local_links
.write()
.expect("Can not write `doc_local_links`. RwLock is poisoned. Panic.");
doc_local_links.clear();
for ((_, _, _), link) in HyperlinkInlineImage::new(&html) {
if let Ok(url) = Url::parse(&link) {
if url.has_host() {
continue;
};
};
let path = PathBuf::from(&*percent_decode_str(&link).decode_utf8()?);
doc_local_links.insert(path);
}
if doc_local_links.is_empty() {
log::debug!(
"Viewer: note file has no local hyperlinks. No additional local files are served.",
);
} else {
log::info!(
"Viewer: referenced local files: {}",
doc_local_links
.iter()
.map(|p|{
let mut s = "\n '".to_string();
s.push_str(p.as_path().to_str().unwrap_or_default());
s
}).collect::<String>()
);
}
Ok(html)
}) {
Ok(html) => Ok(html),
Err(e) => {
Note::render_erroneous_content(&self.doc_path, &CFG.viewer.error_tmpl, &js, e)
.map_err(|e| { ViewerError::RenderErrorPage {
tmpl: "[viewer] error_tmpl".to_string(),
source: e,
}})
}
}
}
}