Skip to content

Commit

Permalink
Initializes HTTP client
Browse files Browse the repository at this point in the history
  • Loading branch information
ultimaweapon committed Mar 18, 2024
1 parent 40957ca commit abd0265
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ edition = "2021"

[dependencies]
clap = "4.4"

[workspace]
members = ["http"]
21 changes: 21 additions & 0 deletions http/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "http"
version = "0.1.0"
edition = "2021"

[dependencies]
http = "1.1.0"
thiserror = "1.0.57"
url = "2.5.0"

[target.'cfg(unix)'.dependencies]
curl = "0.4.46"

[target.'cfg(windows)'.dependencies.windows-sys]
version = "0.52.0"
features = [
"Win32",
"Win32_Foundation",
"Win32_Networking",
"Win32_Networking_WinHttp"
]
104 changes: 104 additions & 0 deletions http/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
pub use self::request::*;
pub use self::response::*;
use http::Method;
use thiserror::Error;
use url::Url;

mod request;
mod response;
#[cfg(windows)]
mod winhttp;

/// Struct to construct [`Request`] objects.
pub struct HttpClient {
#[cfg(windows)]
session: winhttp::Handle,
}

impl HttpClient {
pub fn new() -> Result<Self, NewError> {
Ok(Self {
#[cfg(windows)]
session: winhttp_open(None).map_err(NewError::CreateWinHttpSessionFailed)?,
})
}

pub fn request(&self, method: Method, url: impl AsRef<Url>) -> Result<Request, RequestError> {
let url = url.as_ref();

if !matches!(url.scheme(), "http" | "https") {
return Err(RequestError::NotHttp);
}

Request::new(
method,
url,
#[cfg(windows)]
&self.session,
)
}

#[cfg(windows)]
fn winhttp_open(agent: Option<&str>) -> Result<winhttp::Handle, std::io::Error> {
use std::ptr::null;
use windows_sys::Win32::Networking::WinHttp::{
WinHttpOpen, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
};

// Encode agent.
let agent = agent.map(|v| {
let mut v: Vec<u16> = v.encode_utf16().collect();
v.push(0);
v
});

// Create WinHTTP session.
let session = unsafe {
WinHttpOpen(
agent.map(|v| v.as_ptr()).unwrap_or(null()),
WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
null(),
null(),
0,
)
};

if session.is_null() {
Err(std::io::Error::last_os_error())
} else {
Ok(winhttp::Handle::new(session))
}
}
}

#[cfg(windows)]
unsafe impl Send for HttpClient {}

#[cfg(windows)]
unsafe impl Sync for HttpClient {}

/// Represents an error when [`HttpClient::new()`] fails.
#[derive(Debug, Error)]
pub enum NewError {
#[cfg(windows)]
#[error("couldn't create WinHTTP session")]
CreateWinHttpSessionFailed(#[source] std::io::Error),
}

/// Represents an error when [`HttpClient::request()`] fails.
#[derive(Debug, Error)]
pub enum RequestError {
#[error("the URL is not a HTTP URL")]
NotHttp,

#[error("the specified method is not supported")]
UnsupportedMethod,

#[cfg(windows)]
#[error("WinHttpConnect was failed")]
WinHttpConnectFailed(#[source] std::io::Error),

#[cfg(windows)]
#[error("WinHttpOpenRequest was failed")]
WinHttpOpenRequestFailed(#[source] std::io::Error),
}
181 changes: 181 additions & 0 deletions http/src/request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
use crate::{HttpClient, RequestError, Response};
use http::Method;
use std::{marker::PhantomData, rc::Rc};
use thiserror::Error;
use url::Url;

/// HTTP request.
pub struct Request<'a> {
#[cfg(unix)]
session: curl::easy::Easy2<Handler>,
#[cfg(windows)]
request: crate::winhttp::Handle, // Must be dropped before connection.
#[cfg(windows)]
connection: crate::winhttp::Handle, // Must be dropped last.
phantom: PhantomData<Rc<&'a HttpClient>>,
}

impl<'a> Request<'a> {
#[cfg(unix)]
pub(crate) fn new(method: Method, url: &Url) -> Result<Self, RequestError> {
use curl::easy::Easy2;

// Remove username and password.
let mut url = url.clone();

url.set_username("").unwrap();
url.set_password(None).unwrap();

// Create CURL session.
let mut session = Easy2::new(Handler {});

session.url(url.as_str()).unwrap();
session.follow_location(true).unwrap();

match method {
Method::DELETE | Method::PATCH => session.custom_request(method.as_str()).unwrap(),
Method::GET => session.get(true).unwrap(),
Method::POST => session.post(true).unwrap(),
Method::PUT => session.put(true).unwrap(),
_ => return Err(RequestError::UnsupportedMethod),
}

Ok(Self {
session,
phantom: PhantomData,
})
}

#[cfg(windows)]
pub(crate) fn new(
method: Method,
url: &Url,
session: &'a crate::winhttp::Handle,
) -> Result<Self, RequestError> {
use std::io::Error;
use std::ptr::null;
use windows_sys::w;
use windows_sys::Win32::Networking::WinHttp::{
WinHttpConnect, WinHttpOpenRequest, WINHTTP_FLAG_ESCAPE_DISABLE,
WINHTTP_FLAG_ESCAPE_DISABLE_QUERY, WINHTTP_FLAG_SECURE,
};

// Get method.
let method = match method {
Method::DELETE => w!("DELETE"),
Method::GET => w!("GET"),
Method::PATCH => w!("PATCH"),
Method::POST => w!("POST"),
Method::PUT => w!("PUT"),
_ => return Err(RequestError::UnsupportedMethod),
};

// Is it possible for HTTP URL without host?
let host = url.host_str().unwrap();
let port = url.port_or_known_default().unwrap();
let secure = if url.scheme() == "https" {
WINHTTP_FLAG_SECURE
} else {
0
};

// Encode host.
let mut host: Vec<u16> = host.encode_utf16().collect();

host.push(0);

// Create connection handle.
let connection = unsafe { WinHttpConnect(session.get(), host.as_ptr(), port, 0) };

if connection.is_null() {
return Err(RequestError::WinHttpConnectFailed(Error::last_os_error()));
}

// Concat path and query.
let connection = unsafe { crate::winhttp::Handle::new(connection) };
let mut path = url.path().to_owned();

if let Some(v) = url.query() {
path.push('?');
path.push_str(v);
}

// Encode path.
let mut path: Vec<u16> = path.encode_utf16().collect();

path.push(0);

// Setup accept list.
let mut accept: Vec<*const u16> = Vec::new();

accept.push(w!("*/*"));
accept.push(null());

// Create request handle.
let request = unsafe {
WinHttpOpenRequest(
connection.get(),
method,
path.as_ptr(),
null(),
null(),
accept.as_ptr(),
WINHTTP_FLAG_ESCAPE_DISABLE | WINHTTP_FLAG_ESCAPE_DISABLE_QUERY | secure,
)
};

if request.is_null() {
Err(RequestError::WinHttpOpenRequestFailed(
Error::last_os_error(),
))
} else {
Ok(Self {
request: unsafe { crate::winhttp::Handle::new(request) },
connection,
})
}
}

#[cfg(unix)]
pub fn send(self) -> Result<Response, SendError> {
// Execute the request.
self.session
.perform()
.map_err(SendError::CurlPerformFailed)?;

Ok(Response::new())
}

#[cfg(windows)]
pub fn send(self) -> Result<Response, SendError> {
use std::io::Error;
use std::ptr::null;
use windows_sys::Win32::Foundation::FALSE;
use windows_sys::Win32::Networking::WinHttp::WinHttpSendRequest;

if unsafe { WinHttpSendRequest(self.request.get(), null(), 0, null(), 0, 0, 0) } == FALSE {
return Err(SendError::WinHttpSendRequestFailed(Error::last_os_error()));
}

Ok(Response::new())
}
}

/// An implementation of [`curl::easy::Handler`].
#[cfg(unix)]
struct Handler {}

#[cfg(unix)]
impl curl::easy::Handler for Handler {}

/// Represents an error when [`Request::send()`] fails.
#[derive(Debug, Error)]
pub enum SendError {
#[cfg(unix)]
#[error("curl_easy_perform was failed")]
CurlPerformFailed(#[source] curl::Error),

#[cfg(windows)]
#[error("WinHttpSendRequest was failed")]
WinHttpSendRequestFailed(#[source] std::io::Error),
}
8 changes: 8 additions & 0 deletions http/src/response.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// Response of a HTTP request.
pub struct Response {}

impl Response {
pub(crate) fn new() -> Self {
Self {}
}
}
30 changes: 30 additions & 0 deletions http/src/winhttp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use std::ffi::c_void;
use std::io::Error;
use windows_sys::Win32::Foundation::FALSE;
use windows_sys::Win32::Networking::WinHttp::WinHttpCloseHandle;

/// Encapsulate a WinHTTP handle.
pub struct Handle(*mut c_void);

impl Handle {
/// # Safety
/// `raw` must be a valid WinHTTP handle.
pub(crate) unsafe fn new(raw: *mut c_void) -> Self {
Self(raw)
}

pub fn get(&self) -> *mut c_void {
self.0
}
}

impl Drop for Handle {
fn drop(&mut self) {
if unsafe { WinHttpCloseHandle(self.0) } == FALSE {
panic!(
"WinHttpCloseHandle() was failed: {}",
Error::last_os_error()
);
}
}
}

0 comments on commit abd0265

Please sign in to comment.