use std::{
borrow::Cow,
fs::File,
io::{self, ErrorKind, Read, Write, stdout},
net::{TcpStream, ToSocketAddrs},
str::from_utf8,
sync::{
Mutex,
atomic::{AtomicBool, AtomicI32, Ordering},
},
};
use crate::{
error::{XmlErrorDomain, XmlParserErrors},
io::__xml_ioerr,
uri::XmlURI,
};
const XML_NANO_HTTP_MAX_REDIR: usize = 10;
const XML_NANO_HTTP_CHUNK: usize = 4096;
const XML_NANO_HTTP_WRITE: usize = 1;
const XML_NANO_HTTP_READ: usize = 2;
#[derive(Debug)]
pub struct XmlNanoHTTPCtxt {
protocol: Option<Cow<'static, str>>,
hostname: Option<Cow<'static, str>>,
port: i32,
path: Option<Cow<'static, str>>,
query: Option<Cow<'static, str>>,
socket: Option<TcpStream>,
state: i32,
out: Vec<u8>,
outptr: usize,
input: Vec<u8>,
content: usize,
inptr: usize,
inrptr: usize,
inlen: usize,
last: i32,
return_value: i32,
version: i32,
content_length: i32,
content_type: Option<Cow<'static, str>>,
location: Option<Cow<'static, str>>,
auth_header: Option<Cow<'static, str>>,
encoding: Option<Cow<'static, str>>,
mime_type: Option<Cow<'static, str>>,
}
impl XmlNanoHTTPCtxt {
#[doc(alias = "xmlNanoHTTPOpen")]
pub fn open(
url: &str,
content_type: &mut Option<Cow<'static, str>>,
) -> io::Result<XmlNanoHTTPCtxt> {
xml_nanohttp_method(url, None, None, content_type, None)
}
#[doc(alias = "xmlNanoHTTPReturnCode")]
pub fn return_code(&self) -> i32 {
self.return_value
}
#[doc(alias = "xmlNanoHTTPMimeType")]
pub fn mime_type(&self) -> Option<&str> {
self.mime_type.as_deref()
}
#[doc(alias = "xmlNanoHTTPEncoding")]
pub fn encoding(&self) -> Option<&str> {
self.encoding.as_deref()
}
#[doc(alias = "xmlNanoHTTPRedir")]
pub fn redirection(&self) -> Option<&str> {
self.location.as_deref()
}
}
static INITIALIZED: AtomicBool = AtomicBool::new(false);
static PROXY: Mutex<String> = Mutex::new(String::new());
static PROXY_PORT: AtomicI32 = AtomicI32::new(0);
#[doc(alias = "xmlNanoHTTPInit")]
pub fn xml_nanohttp_init() {
if INITIALIZED.load(Ordering::Acquire) {
return;
}
let lock = PROXY.lock().unwrap();
if lock.is_empty() {
drop(lock);
PROXY_PORT.store(80, Ordering::Relaxed);
if std::env::var("no_proxy")
.ok()
.filter(|e| e == "*")
.is_some()
{
INITIALIZED.store(true, Ordering::Release);
}
if let Ok(env) = std::env::var("http_proxy") {
xml_nanohttp_scan_proxy(&env);
INITIALIZED.store(true, Ordering::Release);
}
if let Ok(env) = std::env::var("HTTP_PROXY") {
xml_nanohttp_scan_proxy(&env);
INITIALIZED.store(true, Ordering::Release);
}
}
INITIALIZED.store(true, Ordering::Release);
}
#[doc(alias = "xmlNanoHTTPCleanup")]
pub fn xml_nanohttp_cleanup() {
let mut p = PROXY.lock().unwrap();
p.clear();
INITIALIZED.store(false, Ordering::Relaxed);
}
#[doc(alias = "xmlNanoHTTPScanProxy")]
pub fn xml_nanohttp_scan_proxy(url: &str) {
let mut p = PROXY.lock().unwrap();
p.clear();
PROXY_PORT.store(0, Ordering::Relaxed);
let Some(uri) = XmlURI::parse(url)
.filter(|uri| uri.scheme.as_deref() == Some("http") && uri.server.is_some())
else {
__xml_ioerr(
XmlErrorDomain::XmlFromHTTP,
XmlParserErrors::XmlHTTPUrlSyntax,
Some("Syntax Error\n"),
);
return;
};
let host = uri.server.as_deref().unwrap();
p.push_str(host);
if let Some(port) = uri.port {
PROXY_PORT.store(port as i32, Ordering::Release);
}
}
#[doc(alias = "xmlNanoHTTPRecv")]
fn xml_nanohttp_recv(ctxt: &mut XmlNanoHTTPCtxt) -> io::Result<usize> {
let Some(stream) = ctxt.socket.as_mut() else {
return Err(io::Error::new(ErrorKind::Other, "Socket is invalid."));
};
while ctxt.state & XML_NANO_HTTP_READ as i32 != 0 {
if ctxt.input.is_empty() {
ctxt.input = vec![0; 65000];
ctxt.inlen = 65000;
ctxt.inptr = 0;
ctxt.content = 0;
ctxt.inrptr = 0;
}
if ctxt.inrptr > XML_NANO_HTTP_CHUNK {
let delta = ctxt.inrptr;
assert!(ctxt.inptr >= ctxt.inrptr);
let len = ctxt.inptr - ctxt.inrptr;
ctxt.input.copy_within(ctxt.inrptr..ctxt.inrptr + len, 0);
ctxt.inrptr -= delta;
ctxt.content -= delta;
ctxt.inptr -= delta;
}
if ctxt.inlen < ctxt.inptr + XML_NANO_HTTP_CHUNK {
ctxt.inlen *= 2;
ctxt.input.resize(ctxt.inlen, 0);
}
match stream.read(&mut ctxt.input[ctxt.inptr..ctxt.inptr + XML_NANO_HTTP_CHUNK]) {
Ok(len @ 1..) => {
ctxt.inptr += len;
ctxt.last = len as i32;
return Ok(len);
}
Ok(0) => {
return Ok(0);
}
Err(e) => {
match e.kind() {
ErrorKind::WouldBlock | ErrorKind::TimedOut => {
}
ErrorKind::ConnectionReset | ErrorKind::ConnectionAborted => {
return Ok(0);
}
_ => {
__xml_ioerr(
XmlErrorDomain::XmlFromHTTP,
XmlParserErrors::default(),
Some("recv failed\n"),
);
return Err(e);
}
}
}
}
}
Ok(0)
}
#[doc(alias = "xmlNanoHTTPFetchContent")]
fn xml_nanohttp_fetch_content(ctxt: &mut XmlNanoHTTPCtxt, ptr: &mut usize, len: &mut usize) -> i32 {
let mut rcvd_lgth = ctxt.inptr - ctxt.content;
while let Some(cur_lgth) = xml_nanohttp_recv(ctxt).ok().filter(|&len| len > 0) {
rcvd_lgth += cur_lgth;
if ctxt.content_length > 0 && rcvd_lgth >= ctxt.content_length as usize {
break;
}
}
*ptr = ctxt.content;
*len = rcvd_lgth;
if (ctxt.content_length > 0 && rcvd_lgth < ctxt.content_length as usize) || rcvd_lgth == 0 {
-1
} else {
0
}
}
#[doc(alias = "xmlNanoHTTPFetch")]
pub fn xml_nanohttp_fetch(
url: &str,
filename: &str,
content_type: &mut Option<Cow<'static, str>>,
) -> io::Result<()> {
let mut len = 0;
let mut ctxt = XmlNanoHTTPCtxt::open(url, content_type)?;
let mut writer: Box<dyn Write> = if filename == "-" {
Box::new(stdout())
} else {
let file = File::options()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(filename)
.inspect_err(|_| *content_type = None)?;
Box::new(file)
};
let mut buf = 0;
xml_nanohttp_fetch_content(&mut ctxt, &mut buf, &mut len);
if len > 0 {
writer.write_all(&ctxt.input[buf..buf + len])?;
}
Ok(())
}
#[doc(alias = "xmlNanoHTTPMethod")]
pub fn xml_nanohttp_method(
url: &str,
method: Option<&str>,
input: Option<&str>,
content_type: &mut Option<Cow<'static, str>>,
headers: Option<&str>,
) -> io::Result<XmlNanoHTTPCtxt> {
xml_nanohttp_method_redir(url, method, input, content_type, &mut None, headers)
}
#[doc(alias = "xmlNanoHTTPScanURL")]
fn xml_nanohttp_scan_url(ctxt: &mut XmlNanoHTTPCtxt, url: &str) {
ctxt.protocol = None;
ctxt.hostname = None;
ctxt.path = None;
ctxt.query = None;
let Some(uri) = XmlURI::parse(url).filter(|uri| uri.scheme.is_some() && uri.server.is_some())
else {
return;
};
ctxt.protocol = uri.scheme.to_owned();
let host = uri.server.as_deref().unwrap();
if let Some(host) = host
.strip_prefix('[')
.and_then(|host| host.strip_suffix(']'))
.filter(|host| !host.is_empty())
{
ctxt.hostname = Some(host.to_owned().into());
} else {
ctxt.hostname = Some(host.to_owned().into());
}
ctxt.path = uri.path.to_owned();
ctxt.query = uri.query.to_owned();
if let Some(port) = uri.port {
ctxt.port = port as i32;
}
}
#[doc(alias = "xmlNanoHTTPNewCtxt")]
fn xml_nanohttp_new_ctxt(url: &str) -> XmlNanoHTTPCtxt {
let mut ret = XmlNanoHTTPCtxt {
protocol: None,
hostname: None,
port: 0,
path: None,
query: None,
socket: None,
state: 0,
out: vec![],
outptr: 0,
input: vec![],
content: 0,
inptr: 0,
inrptr: 0,
inlen: 0,
last: 0,
return_value: 0,
version: 0,
content_length: 0,
content_type: None,
location: None,
auth_header: None,
encoding: None,
mime_type: None,
};
ret.port = 80;
ret.return_value = 0;
ret.content_length = -1;
xml_nanohttp_scan_url(&mut ret, url);
ret
}
#[doc(alias = "xmlNanoHTTPHostnameMatch")]
fn xml_nanohttp_hostname_match(pattern: &str, hostname: &str) -> bool {
if pattern.is_empty() {
return false;
}
let pattern = if let Some(pattern) = pattern.strip_prefix('.') {
pattern
} else {
pattern
};
let mut pc = pattern.chars().rev();
let mut hc = hostname.chars().rev();
for (p, h) in pc.by_ref().zip(hc.by_ref()) {
if !p.eq_ignore_ascii_case(&h) {
return false;
}
}
pc.next().is_none() && matches!(hc.next(), None | Some('.'))
}
#[doc(alias = "xmlNanoHTTPBypassProxy")]
fn xml_nanohttp_bypass_proxy(hostname: &str) -> bool {
if let Ok(env) = std::env::var("no_proxy") {
return env
.split(',')
.map(|e| e.trim())
.any(|e| xml_nanohttp_hostname_match(e, hostname));
}
false
}
#[doc(alias = "xmlNanoHTTPConnectHost")]
fn xml_nanohttp_connect_host(host: &str, port: i32) -> io::Result<TcpStream> {
let host = format!("{host}:{port}");
for addr in host.to_socket_addrs()? {
if let Ok(stream) = TcpStream::connect(addr) {
stream.set_nonblocking(true)?;
return Ok(stream);
}
}
Err(io::Error::new(
ErrorKind::InvalidInput,
"Cannot convert `host` to socket addrs.",
))
}
#[doc(alias = "xmlNanoHTTPSend")]
fn xml_nanohttp_send(ctxt: &mut XmlNanoHTTPCtxt, mut buf: &[u8]) -> i32 {
let mut total_sent = 0;
if ctxt.state & XML_NANO_HTTP_WRITE as i32 != 0 {
let Some(socket) = ctxt.socket.as_mut() else {
__xml_ioerr(
XmlErrorDomain::XmlFromHTTP,
XmlParserErrors::default(),
Some("send failed\n"),
);
if total_sent == 0 {
total_sent = -1;
}
return total_sent;
};
while !buf.is_empty() {
match socket.write(buf) {
Ok(len) => {
total_sent += len as i32;
buf = &buf[len..];
}
Err(e) => match e.kind() {
ErrorKind::WouldBlock | ErrorKind::TimedOut => {
continue;
}
_ => {
__xml_ioerr(
XmlErrorDomain::XmlFromHTTP,
XmlParserErrors::default(),
Some("send failed\n"),
);
if total_sent == 0 {
total_sent = -1;
}
break;
}
},
}
}
}
total_sent
}
#[doc(alias = "xmlNanoHTTPReadLine")]
fn xml_nanohttp_read_line(ctxt: &mut XmlNanoHTTPCtxt) -> Option<Vec<u8>> {
let mut buf: [u8; 4096] = [0; 4096];
let mut bp = 0;
while bp < buf.len() {
if ctxt.inrptr == ctxt.inptr {
match xml_nanohttp_recv(ctxt) {
Ok(0) => {
if bp == 0 {
return None;
}
return Some(buf[..bp].to_vec());
}
Err(_) => return None,
_ => {}
}
}
buf[bp] = ctxt.input[ctxt.inrptr];
ctxt.inrptr += 1;
if buf[bp] == b'\n' {
return Some(buf[..bp].to_vec());
}
if buf[bp] != b'\r' {
bp += 1;
}
}
Some(buf.to_vec())
}
#[doc(alias = "xmlNanoHTTPScanAnswer")]
fn xml_nanohttp_scan_answer(ctxt: &mut XmlNanoHTTPCtxt, line: &str) {
if let Some(line) = line.strip_prefix("HTTP/") {
let mut version = 0;
let mut ret = 0;
let mut cur = line.chars().peekable();
while let Some(c) = cur.next_if(char::is_ascii_digit) {
version *= 10;
version += c as i32 - b'0' as i32;
}
if cur.next_if(|&c| c == '.').is_some() {
if let Some(c) = cur.next_if(char::is_ascii_digit) {
version *= 10;
version += c as i32 - b'0' as i32;
}
while cur.next_if(char::is_ascii_digit).is_some() {}
} else {
version *= 10;
}
if !matches!(cur.peek(), Some(&' ') | Some(&'\t')) {
return;
}
while cur.next_if(|&c| c == ' ' || c == '\t').is_some() {}
if cur.peek().filter(|c| !c.is_ascii_digit()).is_none() {
return;
}
while let Some(c) = cur.next_if(char::is_ascii_digit) {
ret *= 10;
ret += c as i32 - b'0' as i32;
}
if !matches!(cur.peek(), Some(&'\0' | &' ' | &'\t')) {
return;
}
ctxt.return_value = ret;
ctxt.version = version;
} else if let Some(mut line) = line.strip_prefix("Content-Type:") {
line = line.trim_start_matches([' ', '\t']);
let base = line;
ctxt.content_type = Some(base.to_owned().into());
if let Some((mime, _)) = base.split_once(['\0', ' ', '\t', ';', ',']) {
ctxt.mime_type = Some(mime.to_owned().into());
} else {
ctxt.mime_type = Some(base.to_owned().into());
}
if let Some(index) = base.find("charset=") {
let charset = base[index..].strip_prefix("charset=").unwrap();
let charset = charset
.split_once(['\0', ' ', '\t', ';', ','])
.unwrap_or((charset, ""))
.0;
ctxt.encoding = Some(charset.to_owned().into());
}
} else if let Some(mut line) = line.strip_prefix("ContentType:") {
line = line.trim_start_matches([' ', '\t']);
let base = line;
ctxt.content_type = Some(base.to_owned().into());
if let Some((mime, _)) = base.split_once(['\0', ' ', '\t', ';', ',']) {
ctxt.mime_type = Some(mime.to_owned().into());
} else {
ctxt.mime_type = Some(base.to_owned().into());
}
if let Some(index) = base.find("charset=") {
let charset = base[index..].strip_prefix("charset=").unwrap();
let charset = charset
.split_once(['\0', ' ', '\t', ';', ','])
.unwrap_or((charset, ""))
.0;
ctxt.encoding = Some(charset.to_owned().into());
}
} else if let Some(mut line) = line.strip_prefix("Location:") {
line = line.trim_start_matches([' ', '\t']);
if line.starts_with('/') {
let loc = format!("http://{}{line}", ctxt.hostname.as_deref().unwrap_or(""));
ctxt.location = Some(loc.into());
} else {
ctxt.location = Some(line.to_owned().into());
}
} else if let Some(mut line) = line.strip_prefix("WWW-Authenticate:") {
line = line.trim_start_matches([' ', '\t']);
ctxt.auth_header = Some(line.to_owned().into());
} else if let Some(mut line) = line.strip_prefix("Proxy-Authenticate:") {
line = line.trim_start_matches([' ', '\t']);
ctxt.auth_header = Some(line.to_owned().into());
} else if let Some(mut line) = line.strip_prefix("Content-Length:") {
line = line.trim();
ctxt.content_length = line.parse().unwrap_or(0);
}
}
#[doc(alias = "xmlNanoHTTPMethodRedir")]
pub fn xml_nanohttp_method_redir(
url: &str,
method: Option<&str>,
input: Option<&str>,
content_type: &mut Option<Cow<'static, str>>,
redir: &mut Option<String>,
headers: Option<&str>,
) -> io::Result<XmlNanoHTTPCtxt> {
let mut ctxt;
let mut nb_redirects = 0;
let mut redir_url = None::<String>;
let method = method.unwrap_or("GET");
xml_nanohttp_init();
'retry: loop {
if let Some(redir_url) = redir_url.as_deref() {
ctxt = xml_nanohttp_new_ctxt(redir_url);
ctxt.location = Some(redir_url.to_owned().into());
} else {
ctxt = xml_nanohttp_new_ctxt(url);
}
if ctxt.protocol != Some("http".into()) {
__xml_ioerr(
XmlErrorDomain::XmlFromHTTP,
XmlParserErrors::XmlHTTPUrlSyntax,
Some("Not a valid HTTP URI"),
);
return Err(io::Error::new(
ErrorKind::InvalidInput,
"Not a valid HTTP URI",
));
}
let Some(hostname) = ctxt.hostname.as_ref() else {
__xml_ioerr(
XmlErrorDomain::XmlFromHTTP,
XmlParserErrors::XmlHTTPUnknownHost,
Some("Failed to identify host in URI"),
);
return Err(io::Error::new(
ErrorKind::AddrNotAvailable,
"Failed to identify host in URI",
));
};
let proxy = PROXY.lock().unwrap();
let use_proxy = !proxy.is_empty() && !xml_nanohttp_bypass_proxy(hostname.as_ref());
let (mut blen, ret) = if use_proxy {
let blen = hostname.len() * 2 + 16;
let ret = xml_nanohttp_connect_host(&proxy, PROXY_PORT.load(Ordering::Relaxed) as _)?;
(blen, ret)
} else {
let blen = hostname.len();
let ret = xml_nanohttp_connect_host(hostname.as_ref(), ctxt.port)?;
(blen, ret)
};
ctxt.socket = Some(ret);
if input.is_some() {
blen += 36;
}
if let Some(headers) = headers.as_ref() {
blen += headers.len() + 2;
}
if let Some(content_type) = content_type.as_deref() {
blen += content_type.len() + 16;
}
if let Some(query) = ctxt.query.as_deref() {
blen += query.len() + 1;
}
blen += method.len() + ctxt.path.as_ref().map_or(0, |s| s.len()) + 24;
if ctxt.port != 80 {
if use_proxy {
blen += 17;
} else {
blen += 11;
}
}
let mut bp = Vec::with_capacity(blen);
if use_proxy {
let path = ctxt.path.as_ref().unwrap();
if ctxt.port != 80 {
write!(bp, "{method} http://{hostname}:{}{path}", ctxt.port)?;
} else {
write!(bp, "{method} http://{hostname}{path}")?;
}
} else {
let path = ctxt.path.as_ref().unwrap();
write!(bp, "{method} {path}")?;
}
if let Some(query) = ctxt.query.as_deref() {
write!(bp, "?{query}")?;
}
if ctxt.port == 80 {
write!(bp, " HTTP/1.0\r\nHost: {hostname}\r\n")?;
} else {
write!(bp, " HTTP/1.0\r\nHost: {hostname}:{}\r\n", ctxt.port)?;
}
if let Some(content_type) = content_type.as_deref() {
write!(bp, "Content-Type: {content_type}\r\n")?;
}
if let Some(headers) = headers.as_ref() {
write!(bp, "{headers}")?;
}
if let Some(input) = input.as_ref() {
write!(bp, "Content-Length: {}\r\n\r\n", input.len())?;
} else {
write!(bp, "\r\n")?;
}
ctxt.outptr = 0;
ctxt.state = XML_NANO_HTTP_WRITE as _;
xml_nanohttp_send(&mut ctxt, &bp);
ctxt.out = bp;
if let Some(input) = input {
xml_nanohttp_send(&mut ctxt, input.as_bytes());
}
ctxt.state = XML_NANO_HTTP_READ as _;
while let Some(p) = xml_nanohttp_read_line(&mut ctxt) {
if p.is_empty() {
ctxt.content = ctxt.inrptr;
break;
}
xml_nanohttp_scan_answer(&mut ctxt, from_utf8(&p).unwrap());
}
if let Some(location) = ctxt
.location
.as_deref()
.filter(|_| ctxt.return_value >= 300 && ctxt.return_value < 400)
.map(|s| s.to_owned())
{
while xml_nanohttp_recv(&mut ctxt)
.ok()
.filter(|&len| len > 0)
.is_some()
{}
if nb_redirects < XML_NANO_HTTP_MAX_REDIR as i32 {
nb_redirects += 1;
redir_url = Some(location);
continue 'retry;
}
return Err(io::Error::new(ErrorKind::NotFound, "Too many redirects"));
}
break;
}
*content_type = ctxt.content_type.clone();
*redir = redir_url;
Ok(ctxt)
}
#[doc(alias = "xmlNanoHTTPOpenRedir")]
pub fn xml_nanohttp_open_redir(
url: &str,
content_type: &mut Option<Cow<'static, str>>,
redir: &mut Option<String>,
) -> io::Result<XmlNanoHTTPCtxt> {
xml_nanohttp_method_redir(url, None, None, content_type, redir, None)
}
#[doc(alias = "xmlNanoHTTPAuthHeader")]
pub fn xml_nanohttp_auth_header(ctxt: &mut XmlNanoHTTPCtxt) -> Option<String> {
ctxt.auth_header.as_deref().map(|s| s.to_owned())
}
#[doc(alias = "xmlNanoHTTPContentLength")]
pub fn xml_nanohttp_content_length(ctxt: &mut XmlNanoHTTPCtxt) -> i32 {
ctxt.content_length
}
#[doc(alias = "xmlNanoHTTPRead")]
pub fn xml_nanohttp_read(ctxt: &mut XmlNanoHTTPCtxt, dest: &mut [u8]) -> usize {
if dest.is_empty() {
return 0;
}
let mut len = dest.len();
while ctxt.inptr - ctxt.inrptr < len {
if xml_nanohttp_recv(ctxt)
.ok()
.filter(|&len| len > 0)
.is_none()
{
break;
}
}
if ctxt.inptr - ctxt.inrptr < len {
len = ctxt.inptr - ctxt.inrptr;
}
dest[..len].copy_from_slice(&ctxt.input[ctxt.inrptr..ctxt.inrptr + len]);
ctxt.inrptr += len;
len
}
#[doc(alias = "xmlNanoHTTPSave")]
#[cfg(feature = "libxml_output")]
pub fn xml_nanohttp_save(ctxt: &mut XmlNanoHTTPCtxt, filename: &str) -> i32 {
use std::{fs::Permissions, os::unix::fs::PermissionsExt};
let mut len = 0;
let mut ret = 0;
let mut writer: Box<dyn Write> = if filename == "-" {
Box::new(stdout())
} else {
let Ok(file) = File::options()
.write(true)
.read(true)
.create(true)
.truncate(true)
.open(filename)
else {
return -1;
};
file.set_permissions(Permissions::from_mode(0o666)).ok();
Box::new(file)
};
let mut buf = 0;
xml_nanohttp_fetch_content(ctxt, &mut buf, &mut len);
if len > 0 && writer.write_all(&ctxt.input[buf..]).is_err() {
ret = -1;
}
ret
}
impl Read for XmlNanoHTTPCtxt {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let mut len = buf.len();
while self.inptr - self.inrptr < len {
if xml_nanohttp_recv(self)
.ok()
.filter(|&len| len > 0)
.is_none()
{
break;
}
}
if self.inptr - self.inrptr < len {
len = self.inptr - self.inrptr;
}
buf[..len].copy_from_slice(&self.input[self.inrptr..self.inrptr + len]);
self.inrptr += len;
Ok(len)
}
}