Compare commits
31 Commits
b39e07232c
...
master
Author | SHA1 | Date | |
---|---|---|---|
3fa2c3c530 | |||
3b87d66a36 | |||
8dca766f8e | |||
7b74250c86 | |||
2037576627 | |||
46a40ef66a | |||
b6dd41c4cc | |||
48f9a79b05 | |||
c25396d62a | |||
431816900c | |||
b07d8ae520 | |||
4b7db1f0ab | |||
c8a97be01b | |||
c46cb5ee0e | |||
7fd6c4085b | |||
3de1d25038 | |||
7bc2768c25 | |||
6172b36b26 | |||
8d0dabfe0e | |||
d123d9d09d | |||
ef5c64805a | |||
e0dff00925 | |||
350d8a83b8 | |||
df9c16a0d0 | |||
dfed5daebc | |||
5f3bb5d72e | |||
8574a328c9 | |||
d2daeb5879 | |||
8fd0981e78 | |||
673001ed36 | |||
8542588ded |
15
.drone.yml
Normal file
15
.drone.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: cargo_check
|
||||||
|
image: rust
|
||||||
|
commands:
|
||||||
|
- rustup component add clippy
|
||||||
|
- cargo clippy -- -D warnings
|
||||||
|
- cargo test
|
||||||
|
|
||||||
|
|
||||||
|
|
1606
Cargo.lock
generated
1606
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
@ -1,18 +1,18 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pages_server"
|
name = "pages_server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "3.1.6", features = ["derive", "env"] }
|
clap = { version = "4.5.40", features = ["derive", "env"] }
|
||||||
actix-web = "4"
|
actix-web = "4.11.0"
|
||||||
actix-files = "0.6"
|
actix-files = "0.6.6"
|
||||||
actix-multipart = "0.4"
|
actix-multipart = "0.7.2"
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.11.8"
|
||||||
log = "0.4"
|
log = "0.4.27"
|
||||||
bytes = "1.1.0"
|
bytes = "1.10.1"
|
||||||
futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
|
futures-util = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||||
tar = "0.4.38"
|
tar = "0.4.44"
|
||||||
|
3
renovate.json
Normal file
3
renovate.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["local>renovate/presets"]
|
||||||
|
}
|
122
src/main.rs
122
src/main.rs
@ -3,10 +3,10 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use actix_files::{Files, NamedFile};
|
use actix_files::{Files, NamedFile};
|
||||||
use actix_multipart::Multipart;
|
use actix_multipart::Multipart;
|
||||||
use actix_web::{App, Error, HttpRequest, HttpResponse, HttpServer, web};
|
use actix_web::dev::{ServiceRequest, ServiceResponse, fn_service};
|
||||||
use actix_web::dev::{fn_service, ServiceRequest, ServiceResponse};
|
|
||||||
use actix_web::error::{ErrorBadRequest, ErrorInternalServerError, ErrorUnauthorized};
|
use actix_web::error::{ErrorBadRequest, ErrorInternalServerError, ErrorUnauthorized};
|
||||||
use actix_web::middleware::Logger;
|
use actix_web::middleware::Logger;
|
||||||
|
use actix_web::{App, Error, HttpRequest, HttpResponse, HttpServer, web};
|
||||||
use bytes::BufMut;
|
use bytes::BufMut;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures_util::TryStreamExt;
|
use futures_util::TryStreamExt;
|
||||||
@ -42,6 +42,10 @@ struct Args {
|
|||||||
/// Optional proxy IP
|
/// Optional proxy IP
|
||||||
#[clap(short, long, env)]
|
#[clap(short, long, env)]
|
||||||
proxy_ip: Option<String>,
|
proxy_ip: Option<String>,
|
||||||
|
|
||||||
|
/// Specify whether HTML extensions can be bypassed
|
||||||
|
#[clap(short, long, env)]
|
||||||
|
can_bypass_html_ext: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args {
|
impl Args {
|
||||||
@ -52,13 +56,11 @@ impl Args {
|
|||||||
pub fn default_handler_path(&self) -> Option<PathBuf> {
|
pub fn default_handler_path(&self) -> Option<PathBuf> {
|
||||||
match self.not_found_file.is_empty() {
|
match self.not_found_file.is_empty() {
|
||||||
true => None,
|
true => None,
|
||||||
false => Some(self.storage_path().join(&self.not_found_file))
|
false => Some(self.storage_path().join(&self.not_found_file)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static mut ARGS: Option<Args> = None;
|
|
||||||
|
|
||||||
struct NewFile {
|
struct NewFile {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
@ -77,7 +79,6 @@ pub fn match_ip(pattern: &str, ip: &str) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Get the remote IP address
|
/// Get the remote IP address
|
||||||
fn get_remote_ip(req: &HttpRequest, args: &Args) -> String {
|
fn get_remote_ip(req: &HttpRequest, args: &Args) -> String {
|
||||||
let mut ip = req.peer_addr().unwrap().ip().to_string();
|
let mut ip = req.peer_addr().unwrap().ip().to_string();
|
||||||
@ -89,7 +90,6 @@ fn get_remote_ip(req: &HttpRequest, args: &Args) -> String {
|
|||||||
let header: Vec<String> = header
|
let header: Vec<String> = header
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string()
|
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(|f| f.to_string())
|
.map(|f| f.to_string())
|
||||||
.collect();
|
.collect();
|
||||||
@ -104,14 +104,19 @@ fn get_remote_ip(req: &HttpRequest, args: &Args) -> String {
|
|||||||
ip
|
ip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Replace all the files of the website
|
/// Replace all the files of the website
|
||||||
async fn replace_files(args: web::Data<Args>, req: HttpRequest, mut payload: Multipart) -> Result<HttpResponse, Error> {
|
async fn replace_files(
|
||||||
|
args: web::Data<Args>,
|
||||||
|
req: HttpRequest,
|
||||||
|
mut payload: Multipart,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
// Validate remote IP
|
// Validate remote IP
|
||||||
let remote_ip = get_remote_ip(&req, &args);
|
let remote_ip = get_remote_ip(&req, &args);
|
||||||
if !match_ip(&args.allowed_ips_for_update, &remote_ip) {
|
if !match_ip(&args.allowed_ips_for_update, &remote_ip) {
|
||||||
log::warn!("Block unauthorized attempt to perform site update from {}", remote_ip);
|
log::warn!(
|
||||||
|
"Block unauthorized attempt to perform site update from {}",
|
||||||
|
remote_ip
|
||||||
|
);
|
||||||
return Err(ErrorUnauthorized("You are not allowed to perform updates!"));
|
return Err(ErrorUnauthorized("You are not allowed to perform updates!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,8 +125,9 @@ async fn replace_files(args: web::Data<Args>, req: HttpRequest, mut payload: Mul
|
|||||||
None => {
|
None => {
|
||||||
return Err(ErrorUnauthorized("Token required!"));
|
return Err(ErrorUnauthorized("Token required!"));
|
||||||
}
|
}
|
||||||
Some(t) => t.to_str()
|
Some(t) => t
|
||||||
.map_err(|_| ErrorInternalServerError("Failed to parse token!"))?
|
.to_str()
|
||||||
|
.map_err(|_| ErrorInternalServerError("Failed to parse token!"))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
if !token.eq(&args.update_token) || args.update_token.is_empty() {
|
if !token.eq(&args.update_token) || args.update_token.is_empty() {
|
||||||
@ -131,8 +137,9 @@ async fn replace_files(args: web::Data<Args>, req: HttpRequest, mut payload: Mul
|
|||||||
// Get base folder to keep from tar-file
|
// Get base folder to keep from tar-file
|
||||||
let base_uri = match req.headers().get("BaseURI") {
|
let base_uri = match req.headers().get("BaseURI") {
|
||||||
None => "/",
|
None => "/",
|
||||||
Some(t) => t.to_str()
|
Some(t) => t
|
||||||
.map_err(|_| ErrorInternalServerError("Failed to parse base URI to keep!"))?
|
.to_str()
|
||||||
|
.map_err(|_| ErrorInternalServerError("Failed to parse base URI to keep!"))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut new_files = Vec::new();
|
let mut new_files = Vec::new();
|
||||||
@ -147,8 +154,10 @@ async fn replace_files(args: web::Data<Args>, req: HttpRequest, mut payload: Mul
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut archive = tar::Archive::new(b.as_ref());
|
let mut archive = tar::Archive::new(b.as_ref());
|
||||||
for entry in archive.entries()
|
for entry in archive
|
||||||
.map_err(|_| ErrorInternalServerError("Failed to parse TAR archive!"))? {
|
.entries()
|
||||||
|
.map_err(|_| ErrorInternalServerError("Failed to parse TAR archive!"))?
|
||||||
|
{
|
||||||
let mut file = entry?;
|
let mut file = entry?;
|
||||||
let inner_path = file.header().path()?;
|
let inner_path = file.header().path()?;
|
||||||
let inner_path_str = inner_path.to_string_lossy();
|
let inner_path_str = inner_path.to_string_lossy();
|
||||||
@ -207,6 +216,42 @@ async fn replace_files(args: web::Data<Args>, req: HttpRequest, mut payload: Mul
|
|||||||
Ok(HttpResponse::Ok().into())
|
Ok(HttpResponse::Ok().into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn default_files_handler(req: ServiceRequest) -> Result<ServiceResponse, Error> {
|
||||||
|
let (req, _) = req.into_parts();
|
||||||
|
let args: &web::Data<Args> = req.app_data().unwrap();
|
||||||
|
|
||||||
|
// Search for alternate paths
|
||||||
|
if args.can_bypass_html_ext
|
||||||
|
&& !req.path().ends_with(".html")
|
||||||
|
&& !req.path().ends_with('/')
|
||||||
|
&& !req.path().is_empty()
|
||||||
|
{
|
||||||
|
let alt_file = args
|
||||||
|
.storage_path()
|
||||||
|
.join(format!("{}.html", &req.path()[1..]));
|
||||||
|
|
||||||
|
if alt_file.exists() {
|
||||||
|
let file = NamedFile::open_async(alt_file).await?;
|
||||||
|
let res = file.into_response(&req);
|
||||||
|
return Ok(ServiceResponse::new(req, res));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default handler
|
||||||
|
if let Some(h) = args.default_handler_path() {
|
||||||
|
let file = NamedFile::open_async(h).await?;
|
||||||
|
let res = file.into_response(&req);
|
||||||
|
Ok(ServiceResponse::new(req, res))
|
||||||
|
}
|
||||||
|
// Dummy response
|
||||||
|
else {
|
||||||
|
Ok(ServiceResponse::new(
|
||||||
|
req,
|
||||||
|
HttpResponse::NotFound().body("404 Not found"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
let args: Args = Args::parse();
|
let args: Args = Args::parse();
|
||||||
@ -219,41 +264,34 @@ async fn main() -> std::io::Result<()> {
|
|||||||
panic!("Specified files path does not exists!");
|
panic!("Specified files path does not exists!");
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe { ARGS = Some(args.clone()); }
|
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
|
|
||||||
// Update service
|
// Update service
|
||||||
.service(web::resource("/_mgmt/replace_files")
|
.service(web::resource("/_mgmt/replace_files").route(web::post().to(replace_files)))
|
||||||
.route(web::post().to(replace_files)))
|
|
||||||
|
|
||||||
// Serve a tree of static files at the web root and specify the index file.
|
// Serve a tree of static files at the web root and specify the index file.
|
||||||
// Note that the root path should always be defined as the last item. The paths are
|
// Note that the root path should always be defined as the last item. The paths are
|
||||||
// resolved in the order they are defined. If this would be placed before the `/images`
|
// resolved in the order they are defined. If this would be placed before the `/images`
|
||||||
// path then the service for the static images would never be reached.
|
// path then the service for the static images would never be reached.
|
||||||
.service(Files::new("/", &args.files_path)
|
.service(
|
||||||
|
Files::new("/", &args.files_path)
|
||||||
.index_file(&args.index_file)
|
.index_file(&args.index_file)
|
||||||
|
.default_handler(fn_service(default_files_handler)),
|
||||||
.default_handler(fn_service(|req: ServiceRequest| async {
|
|
||||||
let (req, _) = req.into_parts();
|
|
||||||
if let Some(h) = unsafe { ARGS.as_ref().unwrap() }.default_handler_path().as_ref() {
|
|
||||||
let file = NamedFile::open_async(h).await?;
|
|
||||||
let res = file.into_response(&req);
|
|
||||||
Ok(ServiceResponse::new(req, res))
|
|
||||||
} else {
|
|
||||||
Ok(ServiceResponse::new(req, HttpResponse::NotFound()
|
|
||||||
.body("404 Not found")))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Enable the logger.
|
// Enable the logger.
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.app_data(web::Data::new(args.clone()))
|
.app_data(web::Data::new(args.clone()))
|
||||||
})
|
})
|
||||||
.bind(listen_address)?
|
.bind(listen_address)?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::Args;
|
||||||
|
#[test]
|
||||||
|
fn verify_cli() {
|
||||||
|
use clap::CommandFactory;
|
||||||
|
Args::command().debug_assert()
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user