diff --git a/Cargo.toml b/Cargo.toml index c7aba82..9701a3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,6 @@ edition = "2021" [dependencies] clap = "4.4" + +[workspace] +members = ["http"] diff --git a/http/Cargo.toml b/http/Cargo.toml new file mode 100644 index 0000000..ed7dbbf --- /dev/null +++ b/http/Cargo.toml @@ -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" +] diff --git a/http/src/lib.rs b/http/src/lib.rs new file mode 100644 index 0000000..0e5a228 --- /dev/null +++ b/http/src/lib.rs @@ -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 { + Ok(Self { + #[cfg(windows)] + session: winhttp_open(None).map_err(NewError::CreateWinHttpSessionFailed)?, + }) + } + + pub fn request(&self, method: Method, url: impl AsRef) -> Result { + 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 { + 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 = 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), +} diff --git a/http/src/request.rs b/http/src/request.rs new file mode 100644 index 0000000..91e5dbf --- /dev/null +++ b/http/src/request.rs @@ -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, + #[cfg(windows)] + request: crate::winhttp::Handle, // Must be dropped before connection. + #[cfg(windows)] + connection: crate::winhttp::Handle, // Must be dropped last. + phantom: PhantomData>, +} + +impl<'a> Request<'a> { + #[cfg(unix)] + pub(crate) fn new(method: Method, url: &Url) -> Result { + 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 { + 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 = 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 = 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 { + // Execute the request. + self.session + .perform() + .map_err(SendError::CurlPerformFailed)?; + + Ok(Response::new()) + } + + #[cfg(windows)] + pub fn send(self) -> Result { + 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), +} diff --git a/http/src/response.rs b/http/src/response.rs new file mode 100644 index 0000000..f440504 --- /dev/null +++ b/http/src/response.rs @@ -0,0 +1,8 @@ +/// Response of a HTTP request. +pub struct Response {} + +impl Response { + pub(crate) fn new() -> Self { + Self {} + } +} diff --git a/http/src/winhttp.rs b/http/src/winhttp.rs new file mode 100644 index 0000000..dd71824 --- /dev/null +++ b/http/src/winhttp.rs @@ -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() + ); + } + } +}