-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
40957ca
commit abd0265
Showing
6 changed files
with
347 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,3 +5,6 @@ edition = "2021" | |
|
||
[dependencies] | ||
clap = "4.4" | ||
|
||
[workspace] | ||
members = ["http"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
); | ||
} | ||
} | ||
} |