use std::fs::{self, Metadata};
use std::io::{Error as IoError, ErrorKind as IoErrorKind, Write};
use std::mem;
use std::path::Path;
use std::sync::Arc;
use salvo::prelude::*;
use xml::common::{Position, XmlVersion};
use xml::name::{Name as XmlName, OwnedName as OwnedXmlName};
use xml::reader::{EventReader as XmlReader, XmlEvent as XmlREvent};
use xml::writer::{EventWriter as XmlWriter, XmlEvent as XmlWEvent};
use xml::{EmitterConfig as XmlEmitterConfig, ParserConfig as XmlParserConfig};
use crate::config::{AppConfig, log_msg};
use crate::util::webdav::*;
use crate::util::*;
fn default_xml_parser_config() -> XmlParserConfig {
XmlParserConfig {
trim_whitespace: true,
whitespace_to_characters: true,
..Default::default()
}
}
fn default_xml_emitter_config() -> XmlEmitterConfig {
XmlEmitterConfig {
perform_indent: cfg!(debug_assertions),
..Default::default()
}
}
#[handler]
pub async fn handle_propfind(req: &mut Request, depot: &mut Depot, res: &mut Response) {
let config = depot.obtain::<Arc<AppConfig>>().unwrap().clone();
if let Some(resp) = crate::hoops::auth::check_auth(req, &config) {
*res = resp;
return;
}
let url_path_raw = req.uri().path().to_string();
let segments: Vec<&str> = url_path_raw.split('/').filter(|s| !s.is_empty()).collect();
let (req_p, symlink, url_err) = resolve_path(
&config.hosted_directory.1,
&segments,
config.follow_symlinks,
);
if url_err {
set_error(
res,
StatusCode::BAD_REQUEST,
"400 Bad Request",
"Percent-encoding decoded to invalid UTF-8.",
);
return;
}
if !req_p.exists() || config.is_symlink_denied(symlink, &req_p) {
set_error(
res,
StatusCode::NOT_FOUND,
"404 Not Found",
"The requested entity doesn't exist.",
);
return;
}
let depth = req
.headers()
.get("Depth")
.and_then(|v| v.to_str().ok())
.and_then(Depth::parse)
.unwrap_or(Depth::Zero);
let body_bytes = req
.payload()
.await
.ok()
.map(|b| b.to_vec())
.unwrap_or_default();
let props = match parse_propfind(&body_bytes) {
Ok(p) => p,
Err(e) => match e {
PropfindParseError::EmptyBody => PropfindVariant::AllProp,
PropfindParseError::XmlError(msg) => {
let remote = req.remote_addr().to_string();
log_msg(
config.log,
&format!(
"{} tried to PROPFIND {} with invalid XML",
remote,
req_p.display()
),
);
set_error(
res,
StatusCode::BAD_REQUEST,
"400 Bad Request",
&format!("Invalid XML: {}", msg),
);
return;
}
},
};
let remote = req.remote_addr().to_string();
log_msg(
config.log,
&format!(
"{} requested PROPFIND of {} on {} at depth {}",
remote,
props,
req_p.display(),
depth
),
);
let url = req.uri().to_string();
let ua = req
.headers()
.get(salvo::http::header::USER_AGENT)
.and_then(|v| v.to_str().ok());
match write_propfind_output(&config, &url, &req_p, &props, ua, depth) {
Ok(xml_bytes) => {
res.status_code(StatusCode::MULTI_STATUS);
res.headers_mut().insert(
salvo::http::header::CONTENT_TYPE,
"text/xml; charset=utf-8".parse().unwrap(),
);
res.headers_mut()
.insert(salvo::http::header::SERVER, USER_AGENT.parse().unwrap());
res.write_body(xml_bytes).ok();
}
Err(e) => {
set_error(
res,
StatusCode::INTERNAL_SERVER_ERROR,
"500 Internal Server Error",
&format!("XML error: {}", e),
);
}
}
}
fn write_propfind_output(
config: &AppConfig,
url: &str,
path: &Path,
variant: &PropfindVariant,
ua: Option<&str>,
depth: Depth,
) -> Result<Vec<u8>, String> {
let props: &[&[XmlName]] = match variant {
PropfindVariant::AllProp => {
if client_microsoft(ua) {
WEBDAV_ALLPROP_PROPERTIES_WINDOWS
} else {
WEBDAV_ALLPROP_PROPERTIES_NON_WINDOWS
}
}
PropfindVariant::PropName => WEBDAV_PROPNAME_PROPERTIES,
PropfindVariant::Props(_) => &[],
};
let just_names = matches!(variant, PropfindVariant::PropName);
let mut out = initialise_xml_output().map_err(|e| e.to_string())?;
let mut start = XmlWEvent::start_element("D:multistatus")
.ns(WEBDAV_XML_NAMESPACES[0].0, WEBDAV_XML_NAMESPACES[0].1);
for &(prefix, namespace) in &WEBDAV_XML_NAMESPACES[1..] {
start = start.ns(prefix, namespace);
}
out.write(start).map_err(|e| e.to_string())?;
let meta = path.metadata().map_err(|e| e.to_string())?;
if let PropfindVariant::Props(custom) = variant {
write_propfind_response_custom(config, &mut out, url, path, &meta, custom, just_names)
.map_err(|e| e.to_string())?;
} else {
write_propfind_response(config, &mut out, url, path, &meta, props, just_names)
.map_err(|e| e.to_string())?;
}
if meta.is_dir() {
let mut url_owned = url.to_string();
if let PropfindVariant::Props(custom) = variant {
write_propfind_recursive_custom(
config,
&mut out,
&mut url_owned,
path,
custom,
just_names,
depth,
)
.map_err(|e| e.to_string())?;
} else {
write_propfind_recursive(
config,
&mut out,
&mut url_owned,
path,
props,
just_names,
depth,
)
.map_err(|e| e.to_string())?;
}
}
out.write(XmlWEvent::end_element())
.map_err(|e| e.to_string())?;
Ok(out.into_inner())
}
fn write_propfind_response<W: Write>(
config: &AppConfig,
out: &mut XmlWriter<W>,
url: &str,
path: &Path,
meta: &Metadata,
props: &[&[XmlName]],
just_names: bool,
) -> Result<(), xml::writer::Error> {
out.write(XmlWEvent::start_element("D:response"))?;
out.write(XmlWEvent::start_element("D:href"))?;
out.write(XmlWEvent::characters(&escape_specials(url)))?;
out.write(XmlWEvent::end_element())?;
let prop_count: usize = props.iter().map(|pp| pp.len()).sum();
let mut failed_props = Vec::with_capacity(prop_count);
out.write(XmlWEvent::start_element("D:propstat"))?;
out.write(XmlWEvent::start_element("D:prop"))?;
for prop in props.iter().flat_map(|pp| pp.iter()) {
let mut write_name = false;
if !just_names && !write_prop_value(config, out, path, meta, *prop)? {
failed_props.push(*prop);
write_name = true;
}
if just_names || write_name {
write_start_prop_element(out, *prop)?;
out.write(XmlWEvent::end_element())?;
}
}
out.write(XmlWEvent::end_element())?;
out.write(XmlWEvent::start_element("D:status"))?;
if failed_props.len() >= prop_count {
out.write(XmlWEvent::characters("HTTP/1.1 404 Not Found"))?;
out.write(XmlWEvent::end_element())?; out.write(XmlWEvent::end_element())?; out.write(XmlWEvent::end_element())?; return Ok(());
}
out.write(XmlWEvent::characters("HTTP/1.1 200 OK"))?;
out.write(XmlWEvent::end_element())?; out.write(XmlWEvent::end_element())?;
if !failed_props.is_empty() {
out.write(XmlWEvent::start_element("D:propstat"))?;
out.write(XmlWEvent::start_element("D:prop"))?;
for prop in failed_props {
write_start_prop_element(out, prop)?;
out.write(XmlWEvent::end_element())?;
}
out.write(XmlWEvent::end_element())?; out.write(XmlWEvent::start_element("D:status"))?;
out.write(XmlWEvent::characters("HTTP/1.1 404 Not Found"))?;
out.write(XmlWEvent::end_element())?; out.write(XmlWEvent::end_element())?; }
out.write(XmlWEvent::end_element())?; Ok(())
}
fn write_propfind_response_custom<W: Write>(
config: &AppConfig,
out: &mut XmlWriter<W>,
url: &str,
path: &Path,
meta: &Metadata,
props: &[OwnedXmlName],
just_names: bool,
) -> Result<(), xml::writer::Error> {
out.write(XmlWEvent::start_element("D:response"))?;
out.write(XmlWEvent::start_element("D:href"))?;
out.write(XmlWEvent::characters(&escape_specials(url)))?;
out.write(XmlWEvent::end_element())?;
let mut failed_props = Vec::with_capacity(props.len());
out.write(XmlWEvent::start_element("D:propstat"))?;
out.write(XmlWEvent::start_element("D:prop"))?;
for prop in props {
let name = prop.borrow();
let mut write_name = false;
if !just_names && !write_prop_value(config, out, path, meta, name)? {
failed_props.push(name);
write_name = true;
}
if just_names || write_name {
write_start_prop_element(out, name)?;
out.write(XmlWEvent::end_element())?;
}
}
out.write(XmlWEvent::end_element())?; out.write(XmlWEvent::start_element("D:status"))?;
if failed_props.len() >= props.len() {
out.write(XmlWEvent::characters("HTTP/1.1 404 Not Found"))?;
out.write(XmlWEvent::end_element())?; out.write(XmlWEvent::end_element())?; out.write(XmlWEvent::end_element())?; return Ok(());
}
out.write(XmlWEvent::characters("HTTP/1.1 200 OK"))?;
out.write(XmlWEvent::end_element())?; out.write(XmlWEvent::end_element())?;
if !failed_props.is_empty() {
out.write(XmlWEvent::start_element("D:propstat"))?;
out.write(XmlWEvent::start_element("D:prop"))?;
for prop in failed_props {
write_start_prop_element(out, prop)?;
out.write(XmlWEvent::end_element())?;
}
out.write(XmlWEvent::end_element())?; out.write(XmlWEvent::start_element("D:status"))?;
out.write(XmlWEvent::characters("HTTP/1.1 404 Not Found"))?;
out.write(XmlWEvent::end_element())?; out.write(XmlWEvent::end_element())?; }
out.write(XmlWEvent::end_element())?; Ok(())
}
fn write_propfind_recursive<W: Write>(
config: &AppConfig,
out: &mut XmlWriter<W>,
root_url: &mut String,
root_path: &Path,
props: &[&[XmlName]],
just_names: bool,
depth: Depth,
) -> Result<(), xml::writer::Error> {
if !root_url.ends_with('/') {
root_url.push('/');
}
let root_url_orig_len = root_url.len();
let mut links_left = MAX_SYMLINKS;
if let Some(next_depth) = depth.lower() {
let entries: Vec<_> = root_path
.read_dir()
.into_iter()
.flatten()
.flatten()
.collect();
for f in entries {
root_url.truncate(root_url_orig_len);
root_url.push_str(&f.file_name().to_string_lossy());
let mut path = f.path();
let mut symlink = false;
while let Ok(newlink) = path.read_link() {
symlink = true;
if links_left != 0 {
if newlink.is_absolute() {
path = newlink;
} else {
path.pop();
path.push(newlink);
}
links_left -= 1;
} else {
break;
}
}
if path.exists() && !config.is_symlink_denied(symlink, &path) {
let metadata = match path.metadata() {
Ok(m) => m,
Err(_) => continue,
};
write_propfind_response(
config, out, root_url, &path, &metadata, props, just_names,
)?;
if metadata.is_dir() {
write_propfind_recursive(
config, out, root_url, &path, props, just_names, next_depth,
)?;
}
}
}
}
Ok(())
}
fn write_propfind_recursive_custom<W: Write>(
config: &AppConfig,
out: &mut XmlWriter<W>,
root_url: &mut String,
root_path: &Path,
props: &[OwnedXmlName],
just_names: bool,
depth: Depth,
) -> Result<(), xml::writer::Error> {
if !root_url.ends_with('/') {
root_url.push('/');
}
let root_url_orig_len = root_url.len();
let mut links_left = MAX_SYMLINKS;
if let Some(next_depth) = depth.lower() {
let entries: Vec<_> = root_path
.read_dir()
.into_iter()
.flatten()
.flatten()
.collect();
for f in entries {
root_url.truncate(root_url_orig_len);
root_url.push_str(&f.file_name().to_string_lossy());
let mut path = f.path();
let mut symlink = false;
while let Ok(newlink) = path.read_link() {
symlink = true;
if links_left != 0 {
if newlink.is_absolute() {
path = newlink;
} else {
path.pop();
path.push(newlink);
}
links_left -= 1;
} else {
break;
}
}
if path.exists() && !config.is_symlink_denied(symlink, &path) {
let metadata = match path.metadata() {
Ok(m) => m,
Err(_) => continue,
};
write_propfind_response_custom(
config, out, root_url, &path, &metadata, props, just_names,
)?;
if metadata.is_dir() {
write_propfind_recursive_custom(
config, out, root_url, &path, props, just_names, next_depth,
)?;
}
}
}
}
Ok(())
}
fn write_prop_value<W: Write>(
config: &AppConfig,
out: &mut XmlWriter<W>,
path: &Path,
meta: &Metadata,
prop: XmlName,
) -> Result<bool, xml::writer::Error> {
if prop.namespace == Some(WEBDAV_XML_NAMESPACE_DAV.1) {
match prop.local_name {
"creationdate" => {
out.write(XmlWEvent::start_element((
WEBDAV_XML_NAMESPACE_DAV.0,
"creationdate",
)))?;
out.write(XmlWEvent::characters(&file_time_created(meta).to_rfc3339()))?;
}
"getcontentlength" => {
out.write(XmlWEvent::start_element((
WEBDAV_XML_NAMESPACE_DAV.0,
"getcontentlength",
)))?;
out.write(XmlWEvent::characters(&file_length(meta, &path).to_string()))?;
}
"getcontenttype" => {
out.write(XmlWEvent::start_element((
WEBDAV_XML_NAMESPACE_DAV.0,
"getcontenttype",
)))?;
out.write(XmlWEvent::characters(&guess_mime_type(
path,
&config.mime_type_overrides,
)))?;
}
"getlastmodified" => {
out.write(XmlWEvent::start_element((
WEBDAV_XML_NAMESPACE_DAV.0,
"getlastmodified",
)))?;
out.write(XmlWEvent::characters(
&file_time_modified(meta).to_rfc3339(),
))?;
}
"resourcetype" => {
out.write(XmlWEvent::start_element((
WEBDAV_XML_NAMESPACE_DAV.0,
"resourcetype",
)))?;
if !is_actually_file(&meta.file_type(), path) {
out.write(XmlWEvent::start_element((
WEBDAV_XML_NAMESPACE_DAV.0,
"collection",
)))?;
out.write(XmlWEvent::end_element())?;
}
}
_ => return Ok(false),
}
} else if prop.namespace == Some(WEBDAV_XML_NAMESPACE_MICROSOFT.1) {
match prop.local_name {
"Win32CreationTime" => {
out.write(XmlWEvent::start_element((
WEBDAV_XML_NAMESPACE_MICROSOFT.0,
"Win32CreationTime",
)))?;
out.write(XmlWEvent::characters(&file_time_created(meta).to_rfc3339()))?;
}
"Win32FileAttributes" => {
out.write(XmlWEvent::start_element((
WEBDAV_XML_NAMESPACE_MICROSOFT.0,
"Win32FileAttributes",
)))?;
out.write(XmlWEvent::characters(&format!(
"{:08x}",
win32_file_attributes(meta, path)
)))?;
}
"Win32LastAccessTime" => {
out.write(XmlWEvent::start_element((
WEBDAV_XML_NAMESPACE_MICROSOFT.0,
"Win32LastAccessTime",
)))?;
out.write(XmlWEvent::characters(
&file_time_accessed(meta).to_rfc3339(),
))?;
}
"Win32LastModifiedTime" => {
out.write(XmlWEvent::start_element((
WEBDAV_XML_NAMESPACE_MICROSOFT.0,
"Win32LastModifiedTime",
)))?;
out.write(XmlWEvent::characters(
&file_time_modified(meta).to_rfc3339(),
))?;
}
_ => return Ok(false),
}
} else if prop.namespace == Some(WEBDAV_XML_NAMESPACE_APACHE.1) {
match prop.local_name {
"executable" => {
out.write(XmlWEvent::start_element((
WEBDAV_XML_NAMESPACE_APACHE.0,
"executable",
)))?;
out.write(XmlWEvent::characters(if file_executable(meta) {
"T"
} else {
"F"
}))?;
}
_ => return Ok(false),
}
} else {
return Ok(false);
}
out.write(XmlWEvent::end_element())?;
Ok(true)
}
fn write_start_prop_element<W: Write>(
out: &mut XmlWriter<W>,
prop: XmlName,
) -> Result<(), xml::writer::Error> {
if let Some(prop_namespace) = prop.namespace {
if let Some(&(prefix, _)) = WEBDAV_XML_NAMESPACES
.iter()
.find(|(_, ns)| *ns == prop_namespace)
{
return out.write(XmlWEvent::start_element(XmlName {
prefix: Some(prefix),
..prop
}));
}
if prop
.prefix
.map(|pp| WEBDAV_XML_NAMESPACES.iter().any(|(pf, _)| *pf == pp))
.unwrap_or(true)
{
return out.write(
XmlWEvent::start_element(XmlName {
prefix: Some("U"),
..prop
})
.ns("U", prop_namespace),
);
}
}
out.write(XmlWEvent::start_element(prop))
}
fn initialise_xml_output() -> Result<XmlWriter<Vec<u8>>, xml::writer::Error> {
let mut out = XmlWriter::new_with_config(vec![], default_xml_emitter_config());
out.write(XmlWEvent::StartDocument {
version: XmlVersion::Version10,
encoding: Some("utf-8"),
standalone: None,
})?;
Ok(out)
}
#[derive(Debug, Clone)]
pub enum PropfindVariant {
AllProp,
PropName,
Props(Vec<OwnedXmlName>),
}
impl std::fmt::Display for PropfindVariant {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
PropfindVariant::AllProp => f.write_str("all props"),
PropfindVariant::PropName => f.write_str("prop names"),
PropfindVariant::Props(props) => {
for (i, name) in props.iter().enumerate() {
if i != 0 {
f.write_str(", ")?;
}
f.write_str(&name.local_name)?;
}
Ok(())
}
}
}
}
enum PropfindParseError {
EmptyBody,
XmlError(String),
}
fn parse_propfind(body: &[u8]) -> Result<PropfindVariant, PropfindParseError> {
if body.is_empty() {
return Err(PropfindParseError::EmptyBody);
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum State {
Start,
PropFind,
Prop,
InProp,
}
let mut xml = XmlReader::new_with_config(body, default_xml_parser_config());
let mut state = State::Start;
let mut props = vec![];
loop {
let event = xml.next().map_err(|e| {
if e.position() == xml::common::TextPosition::new()
&& e.msg().contains("no root element")
{
PropfindParseError::EmptyBody
} else {
PropfindParseError::XmlError(e.to_string())
}
})?;
match (state, event) {
(State::Start, XmlREvent::StartDocument { .. }) => (),
(State::Start, XmlREvent::StartElement { ref name, .. })
if name.local_name == "propfind" =>
{
state = State::PropFind
}
(State::PropFind, XmlREvent::StartElement { ref name, .. })
if name.local_name == "allprop" =>
{
return Ok(PropfindVariant::AllProp);
}
(State::PropFind, XmlREvent::StartElement { ref name, .. })
if name.local_name == "propname" =>
{
return Ok(PropfindVariant::PropName);
}
(State::PropFind, XmlREvent::StartElement { ref name, .. })
if name.local_name == "prop" =>
{
state = State::Prop
}
(State::Prop, XmlREvent::StartElement { name, .. }) => {
state = State::InProp;
props.push(name);
}
(State::Prop, XmlREvent::EndElement { .. }) => {
return Ok(PropfindVariant::Props(props));
}
(State::InProp, XmlREvent::EndElement { .. }) => state = State::Prop,
(_, ev) => {
return Err(PropfindParseError::XmlError(format!(
"Unexpected event {:?}",
ev
)));
}
}
}
}
#[derive(Debug, Default)]
#[allow(non_snake_case)]
struct ProppatchActionables {
Win32CreationTime: Option<u64>,
Win32LastAccessTime: Option<u64>,
Win32LastModifiedTime: Option<u64>,
executable: Option<bool>,
}
#[handler]
pub async fn handle_proppatch(req: &mut Request, depot: &mut Depot, res: &mut Response) {
let config = depot.obtain::<Arc<AppConfig>>().unwrap().clone();
if let Some(resp) = crate::hoops::auth::check_auth(req, &config) {
*res = resp;
return;
}
if config.writes_temp_dir.is_none() {
set_error(
res,
StatusCode::FORBIDDEN,
"403 Forbidden",
"Write requests not allowed. Use -w.",
);
return;
}
let url_path_raw = req.uri().path().to_string();
let segments: Vec<&str> = url_path_raw.split('/').filter(|s| !s.is_empty()).collect();
let (req_p, symlink, url_err) = resolve_path(
&config.hosted_directory.1,
&segments,
config.follow_symlinks,
);
if url_err {
set_error(
res,
StatusCode::BAD_REQUEST,
"400 Bad Request",
"Percent-encoding decoded to invalid UTF-8.",
);
return;
}
if !req_p.exists() || config.is_symlink_denied(symlink, &req_p) {
set_error(
res,
StatusCode::NOT_FOUND,
"404 Not Found",
"The requested entity doesn't exist.",
);
return;
}
let body_bytes = req
.payload()
.await
.ok()
.map(|b| b.to_vec())
.unwrap_or_default();
let (props, actionables) = match parse_proppatch(&body_bytes) {
Ok(pp) => pp,
Err(e) => {
let remote = req.remote_addr().to_string();
log_msg(
config.log,
&format!(
"{} tried to PROPPATCH {} with invalid XML",
remote,
req_p.display()
),
);
set_error(
res,
StatusCode::BAD_REQUEST,
"400 Bad Request",
&format!("Invalid XML: {}", e),
);
return;
}
};
let remote = req.remote_addr().to_string();
log_msg(
config.log,
&format!("{} requested PROPPATCH on {}", remote, req_p.display()),
);
set_times(
&req_p,
actionables.Win32LastModifiedTime,
actionables.Win32LastAccessTime,
actionables.Win32CreationTime,
);
if let Some(ex) = actionables.executable {
set_executable(&req_p, ex);
}
let url = req.uri().to_string();
match write_proppatch_output(&props, &url) {
Ok(xml_bytes) => {
res.status_code(StatusCode::MULTI_STATUS);
res.headers_mut().insert(
salvo::http::header::CONTENT_TYPE,
"text/xml; charset=utf-8".parse().unwrap(),
);
res.headers_mut()
.insert(salvo::http::header::SERVER, USER_AGENT.parse().unwrap());
res.write_body(xml_bytes).ok();
}
Err(e) => {
set_error(
res,
StatusCode::INTERNAL_SERVER_ERROR,
"500 Internal Server Error",
&format!("XML error: {}", e),
);
}
}
}
fn parse_proppatch(
body: &[u8],
) -> Result<(Vec<(OwnedXmlName, String)>, ProppatchActionables), String> {
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum State {
Start,
PropertyUpdate,
Action,
Prop,
InProp,
}
let mut xml = XmlReader::new_with_config(body, default_xml_parser_config());
let mut state = State::Start;
let mut props = vec![];
let mut propname = None;
let mut is_remove = false;
let mut actionables = ProppatchActionables::default();
let mut propdata = String::new();
loop {
let event = xml.next().map_err(|e| e.to_string())?;
match (state, event) {
(State::Start, XmlREvent::StartDocument { .. }) => (),
(State::Start, XmlREvent::StartElement { ref name, .. })
if name.local_name == "propertyupdate" =>
{
state = State::PropertyUpdate
}
(State::PropertyUpdate, XmlREvent::StartElement { ref name, .. })
if name.local_name == "set" =>
{
state = State::Action;
is_remove = false;
}
(State::PropertyUpdate, XmlREvent::StartElement { ref name, .. })
if name.local_name == "remove" =>
{
state = State::Action;
is_remove = true;
}
(State::PropertyUpdate, XmlREvent::EndElement { .. }) => {
return Ok((props, actionables));
}
(State::Action, XmlREvent::StartElement { ref name, .. })
if name.local_name == "prop" =>
{
state = State::Prop
}
(State::Action, XmlREvent::EndElement { .. }) => state = State::PropertyUpdate,
(State::Prop, XmlREvent::StartElement { name, .. }) => {
state = State::InProp;
propname = Some(name);
}
(State::Prop, XmlREvent::EndElement { .. }) => state = State::Action,
(State::InProp, XmlREvent::EndElement { name, .. }) => {
if Some(&name) == propname.as_ref() {
props.push((name, mem::take(&mut propdata)));
state = State::Prop;
}
}
(State::InProp, XmlREvent::Characters(data)) if !is_remove => {
propdata = data;
match propname.as_ref().map(|n| n.local_name.as_str()) {
Some("Win32CreationTime") => {
actionables.Win32CreationTime = win32time(&propdata)
}
Some("Win32LastAccessTime") => {
actionables.Win32LastAccessTime = win32time(&propdata)
}
Some("Win32LastModifiedTime") => {
actionables.Win32LastModifiedTime = win32time(&propdata)
}
Some("executable") => actionables.executable = Some(propdata == "T"),
_ => propdata.clear(),
}
}
(State::InProp, _) => {}
(_, ev) => return Err(format!("Unexpected event {:?}", ev)),
}
}
}
fn write_proppatch_output(
props: &[(OwnedXmlName, String)],
req_url: &str,
) -> Result<Vec<u8>, String> {
let mut out = initialise_xml_output().map_err(|e| e.to_string())?;
let mut start = XmlWEvent::start_element("D:multistatus")
.ns(WEBDAV_XML_NAMESPACES[0].0, WEBDAV_XML_NAMESPACES[0].1);
for &(prefix, namespace) in &WEBDAV_XML_NAMESPACES[1..] {
start = start.ns(prefix, namespace);
}
out.write(start).map_err(|e| e.to_string())?;
out.write(XmlWEvent::start_element("D:href"))
.map_err(|e| e.to_string())?;
out.write(XmlWEvent::characters(req_url))
.map_err(|e| e.to_string())?;
out.write(XmlWEvent::end_element())
.map_err(|e| e.to_string())?;
out.write(XmlWEvent::start_element("D:propstat"))
.map_err(|e| e.to_string())?;
for (name, _) in props {
out.write(XmlWEvent::start_element("D:prop"))
.map_err(|e| e.to_string())?;
write_start_prop_element(&mut out, name.borrow()).map_err(|e| e.to_string())?;
out.write(XmlWEvent::end_element())
.map_err(|e| e.to_string())?;
out.write(XmlWEvent::end_element())
.map_err(|e| e.to_string())?;
}
out.write(XmlWEvent::start_element("D:status"))
.map_err(|e| e.to_string())?;
out.write(XmlWEvent::characters("HTTP/1.1 409 Conflict"))
.map_err(|e| e.to_string())?;
out.write(XmlWEvent::end_element())
.map_err(|e| e.to_string())?;
out.write(XmlWEvent::end_element())
.map_err(|e| e.to_string())?; out.write(XmlWEvent::end_element())
.map_err(|e| e.to_string())?;
Ok(out.into_inner())
}
#[handler]
pub async fn handle_mkcol(req: &mut Request, depot: &mut Depot, res: &mut Response) {
let config = depot.obtain::<Arc<AppConfig>>().unwrap().clone();
if let Some(resp) = crate::hoops::auth::check_auth(req, &config) {
*res = resp;
return;
}
let url_path_raw = req.uri().path().to_string();
let segments: Vec<&str> = url_path_raw.split('/').filter(|s| !s.is_empty()).collect();
let (req_p, symlink, url_err) = resolve_path(
&config.hosted_directory.1,
&segments,
config.follow_symlinks,
);
let remote = req.remote_addr().to_string();
log_msg(
config.log,
&format!("{} requested to MKCOL at {}", remote, req_p.display()),
);
if url_err {
set_error(
res,
StatusCode::BAD_REQUEST,
"400 Bad Request",
"Percent-encoding decoded to invalid UTF-8.",
);
return;
}
if config.writes_temp_dir.is_none() {
set_error(
res,
StatusCode::FORBIDDEN,
"403 Forbidden",
"Write requests not allowed. Use -w.",
);
return;
}
if !req_p.parent().map(|pp| pp.exists()).unwrap_or(true)
|| config.is_symlink_denied(symlink, &req_p)
{
res.status_code(StatusCode::CONFLICT);
return;
}
let body = req
.payload()
.await
.ok()
.map(|b| b.to_vec())
.unwrap_or_default();
if !body.is_empty() {
res.status_code(StatusCode::UNSUPPORTED_MEDIA_TYPE);
return;
}
match fs::create_dir(&req_p) {
Ok(()) => {
res.status_code(StatusCode::CREATED);
}
Err(e) => match e.kind() {
IoErrorKind::NotFound => {
res.status_code(StatusCode::CONFLICT);
}
IoErrorKind::AlreadyExists => {
res.status_code(StatusCode::METHOD_NOT_ALLOWED);
res.render(Text::Plain("File exists"));
}
_ => {
res.status_code(StatusCode::FORBIDDEN);
}
},
}
}
#[handler]
pub async fn handle_copy(req: &mut Request, depot: &mut Depot, res: &mut Response) {
handle_copy_move(req, depot, res, false).await;
}
#[handler]
pub async fn handle_move(req: &mut Request, depot: &mut Depot, res: &mut Response) {
let config = depot.obtain::<Arc<AppConfig>>().unwrap().clone();
let url_path_raw = req.uri().path().to_string();
let segments: Vec<&str> = url_path_raw.split('/').filter(|s| !s.is_empty()).collect();
let (req_p, ..) = resolve_path(
&config.hosted_directory.1,
&segments,
config.follow_symlinks,
);
let req_p_clone = req_p.clone();
let is_file = req_p
.metadata()
.map(|m| is_actually_file(&m.file_type(), &req_p))
.unwrap_or(true);
handle_copy_move(req, depot, res, true).await;
if res.status_code == Some(StatusCode::CREATED)
|| res.status_code == Some(StatusCode::NO_CONTENT)
{
let removal = if is_file {
fs::remove_file(&req_p_clone)
} else {
fs::remove_dir_all(&req_p_clone)
};
if removal.is_err() {
res.status_code(StatusCode::LOCKED);
res.body(salvo::http::ResBody::None);
}
}
}
async fn handle_copy_move(req: &mut Request, depot: &mut Depot, res: &mut Response, is_move: bool) {
let config = depot.obtain::<Arc<AppConfig>>().unwrap().clone();
if let Some(resp) = crate::hoops::auth::check_auth(req, &config) {
*res = resp;
return;
}
let url_path_raw = req.uri().path().to_string();
let segments: Vec<&str> = url_path_raw.split('/').filter(|s| !s.is_empty()).collect();
let (req_p, symlink, url_err) = resolve_path(
&config.hosted_directory.1,
&segments,
config.follow_symlinks,
);
if url_err {
set_error(
res,
StatusCode::BAD_REQUEST,
"400 Bad Request",
"Percent-encoding decoded to invalid UTF-8.",
);
return;
}
let dest_url = match req
.headers()
.get("Destination")
.and_then(|v| v.to_str().ok())
{
Some(d) => d.to_string(),
None => {
set_error(
res,
StatusCode::BAD_REQUEST,
"400 Bad Request",
"Destination URL invalid or nonexistent.",
);
return;
}
};
let dest_path_str = if dest_url.starts_with("http://") || dest_url.starts_with("https://") {
match dest_url.find("://") {
Some(idx) => {
let after_scheme = &dest_url[idx + 3..];
match after_scheme.find('/') {
Some(slash_idx) => after_scheme[slash_idx..].to_string(),
None => "/".to_string(),
}
}
None => dest_url.clone(),
}
} else if dest_url.starts_with('/') {
dest_url.clone()
} else {
set_error(
res,
StatusCode::BAD_REQUEST,
"400 Bad Request",
"Destination URL invalid.",
);
return;
};
let dest_segments: Vec<&str> = dest_path_str
.split('/')
.filter(|s: &&str| !s.is_empty())
.collect();
let (dest_p, dest_symlink, dest_url_err) = resolve_path(
&config.hosted_directory.1,
&dest_segments,
config.follow_symlinks,
);
if dest_url_err {
set_error(
res,
StatusCode::BAD_REQUEST,
"400 Bad Request",
"Percent-encoding decoded destination to invalid UTF-8.",
);
return;
}
let depth = req
.headers()
.get("Depth")
.and_then(|v| v.to_str().ok())
.and_then(Depth::parse)
.unwrap_or(Depth::Infinity);
let overwrite = req
.headers()
.get("Overwrite")
.and_then(|v| v.to_str().ok())
.and_then(Overwrite::parse)
.unwrap_or_default()
.0;
let remote = req.remote_addr().to_string();
log_msg(
config.log,
&format!(
"{} requested to {}{} {} to {} at depth {}",
remote,
if overwrite { "overwrite-" } else { "" },
if is_move { "MOVE" } else { "COPY" },
req_p.display(),
dest_p.display(),
depth
),
);
if config.writes_temp_dir.is_none() {
set_error(
res,
StatusCode::FORBIDDEN,
"403 Forbidden",
"Write requests not allowed. Use -w.",
);
return;
}
if req_p == dest_p {
res.status_code(StatusCode::FORBIDDEN);
return;
}
if !req_p.exists() || config.is_symlink_denied(symlink, &req_p) {
set_error(
res,
StatusCode::NOT_FOUND,
"404 Not Found",
"The requested entity doesn't exist.",
);
return;
}
if !dest_p.parent().map(|pp| pp.exists()).unwrap_or(true)
|| config.is_symlink_denied(dest_symlink, &dest_p)
{
res.status_code(StatusCode::CONFLICT);
return;
}
let mut overwritten = false;
if dest_p.exists() {
if !overwrite {
res.status_code(StatusCode::PRECONDITION_FAILED);
return;
}
if !is_actually_file(&dest_p.metadata().unwrap().file_type(), &dest_p)
&& fs::remove_dir(&dest_p).is_err()
{
res.status_code(StatusCode::LOCKED);
return;
}
overwritten = true;
}
let source_file = is_actually_file(&req_p.metadata().unwrap().file_type(), &req_p);
if source_file {
copy_response(res, fs::copy(&req_p, &dest_p).map(|_| ()), overwritten);
} else {
match depth {
Depth::Zero if !is_move => copy_response(res, fs::create_dir(&dest_p), overwritten),
Depth::Infinity => match copy_dir(&req_p, &dest_p) {
Ok(errors) => {
if errors.is_empty() {
copy_response(res, Ok(()), overwritten);
} else {
res.status_code(StatusCode::MULTI_STATUS);
if let Ok(xml_bytes) = copy_response_multierror(&errors, &url_path_raw) {
res.headers_mut().insert(
salvo::http::header::CONTENT_TYPE,
"text/xml; charset=utf-8".parse().unwrap(),
);
res.write_body(xml_bytes).ok();
}
}
}
Err(_) => copy_response(res, Err(IoError::other("copy failed")), overwritten),
},
_ => {
set_error(
res,
StatusCode::BAD_REQUEST,
"400 Bad Request",
&format!("Invalid depth: {}", depth),
);
}
}
}
}
fn copy_response(res: &mut Response, op_result: std::io::Result<()>, overwritten: bool) {
match op_result {
Ok(_) => {
if overwritten {
res.status_code(StatusCode::NO_CONTENT);
} else {
res.status_code(StatusCode::CREATED);
}
}
Err(_) => {
res.status_code(StatusCode::INSUFFICIENT_STORAGE);
}
}
}
fn copy_response_multierror(
errors: &[(IoError, String)],
base_url: &str,
) -> Result<Vec<u8>, String> {
let mut out = initialise_xml_output().map_err(|e| e.to_string())?;
out.write(
XmlWEvent::start_element("D:multistatus")
.ns(WEBDAV_XML_NAMESPACE_DAV.0, WEBDAV_XML_NAMESPACE_DAV.1),
)
.map_err(|e| e.to_string())?;
out.write(XmlWEvent::start_element("D:response"))
.map_err(|e| e.to_string())?;
for (_, subp) in errors {
out.write(XmlWEvent::start_element("D:href"))
.map_err(|e| e.to_string())?;
let href = format!("{}/{}", base_url.trim_end_matches('/'), subp);
out.write(XmlWEvent::characters(&href))
.map_err(|e| e.to_string())?;
out.write(XmlWEvent::end_element())
.map_err(|e| e.to_string())?;
}
out.write(XmlWEvent::start_element("D:status"))
.map_err(|e| e.to_string())?;
out.write(XmlWEvent::characters("HTTP/1.1 507 Insufficient Storage"))
.map_err(|e| e.to_string())?;
out.write(XmlWEvent::end_element())
.map_err(|e| e.to_string())?;
out.write(XmlWEvent::end_element())
.map_err(|e| e.to_string())?;
out.write(XmlWEvent::end_element())
.map_err(|e| e.to_string())?;
Ok(out.into_inner())
}
fn set_error(res: &mut Response, status: StatusCode, title: &str, msg: &str) {
let body = error_html(title, msg, "");
res.status_code(status);
res.headers_mut().insert(
salvo::http::header::CONTENT_TYPE,
"text/html; charset=utf-8".parse().unwrap(),
);
res.headers_mut()
.insert(salvo::http::header::SERVER, USER_AGENT.parse().unwrap());
res.render(Text::Html(body));
}