Skip to content

Commit 8265db1

Browse files
Client: Add https support
1 parent 1e69f61 commit 8265db1

File tree

8 files changed

+129
-43
lines changed

8 files changed

+129
-43
lines changed

crates/client/Cargo.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "httpcli"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
edition = "2024"
55
description = "Http client"
66
authors = ["Saúl Valdelvira <saul@saulv.es>"]
@@ -13,3 +13,14 @@ path = "src/main.rs"
1313

1414
[dependencies]
1515
http.workspace = true
16+
webpki-roots = { version = "1.0.1", optional = true }
17+
18+
[dependencies.rustls]
19+
version = ">=0.23.28"
20+
optional = true
21+
default-features = false
22+
features = ["std", "aws_lc_rs"]
23+
24+
[features]
25+
default = ["tls"]
26+
tls = ["dep:rustls", "dep:webpki-roots", "http/tls"]

crates/client/src/config.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ pub enum OutFile {
99
GetFromUrl,
1010
}
1111

12+
#[derive(Clone, Debug)]
13+
pub enum HttpType {
14+
Http,
15+
Https,
16+
}
17+
1218
#[derive(Clone, Debug)]
1319
pub struct ClientConfig {
1420
pub url: String,
@@ -17,6 +23,7 @@ pub struct ClientConfig {
1723
pub port: u16,
1824
pub user_agent: String,
1925
pub out_file: OutFile,
26+
pub http_type: HttpType,
2027
}
2128

2229
impl ClientConfig {
@@ -54,7 +61,13 @@ impl ClientConfig {
5461
}
5562

5663
let mut host = conf.url.as_str();
57-
host = conf.url.strip_prefix("http://").unwrap_or(host);
64+
if let Some(url) = conf.url.strip_prefix("http://") {
65+
host = url;
66+
}
67+
if let Some(url) = conf.url.strip_prefix("https://") {
68+
host = url;
69+
conf.http_type = HttpType::Https;
70+
}
5871

5972
let mut url = "/";
6073

@@ -66,6 +79,11 @@ impl ClientConfig {
6679
if let Some(i) = host.find(':') {
6780
conf.port = host[i + 1..].parse()?;
6881
host = &host[..i];
82+
} else {
83+
conf.port = match conf.http_type {
84+
HttpType::Http => 80,
85+
HttpType::Https => 443,
86+
}
6987
}
7088

7189
conf.host = host.to_string();
@@ -82,13 +100,6 @@ impl ClientConfig {
82100
fn help() -> ! {
83101
println!(
84102
"\
85-
http-client: Copyright (C) 2025 Saúl Valdelvira
86-
87-
This program is free software: you can redistribute it and/or modify it
88-
under the terms of the GNU General Public License as published by the
89-
Free Software Foundation, version 3.
90-
Use http-client --license to read a copy of the GPL v3
91-
92103
USAGE: http-client [--method <HTTP Method>] [--host <hostname>]
93104
[--user-agent <User Agent>] [-O] [-o <output-file>]
94105
PARAMETERS:
@@ -137,6 +148,7 @@ impl Default for ClientConfig {
137148
host: String::new(),
138149
user_agent: "http-client".to_string(),
139150
out_file: OutFile::Stdout,
151+
http_type: HttpType::Http,
140152
}
141153
}
142154
}

crates/client/src/main.rs

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,64 @@
11
use std::{
22
env,
33
fs::File,
4+
io,
45
io::{Write, stdout},
56
net::{TcpStream, ToSocketAddrs},
67
process,
78
};
89

9-
use http::{HttpMethod, HttpRequest};
10+
use http::{HttpMethod, HttpRequest, HttpResponse};
1011
mod config;
1112
use config::ClientConfig;
1213

14+
use crate::config::HttpType;
15+
1316
fn open_file(fname: &str) -> Box<dyn Write> {
1417
Box::new(File::create(fname).unwrap_or_else(|_| {
1518
eprintln!("Couldn't create file: {fname}");
1619
process::exit(1);
1720
}))
1821
}
1922

23+
#[cfg(not(feature = "tls"))]
24+
#[inline]
25+
fn send_request(
26+
tcp: TcpStream,
27+
_http_type: HttpType,
28+
_host: String,
29+
req: HttpRequest,
30+
) -> http::Result<HttpResponse> {
31+
req.send_to(tcp)
32+
}
33+
34+
#[cfg(feature = "tls")]
35+
fn send_request(
36+
tcp: TcpStream,
37+
http_type: HttpType,
38+
host: String,
39+
req: HttpRequest,
40+
) -> http::Result<HttpResponse> {
41+
use std::sync::Arc;
42+
43+
if matches!(http_type, HttpType::Https) {
44+
let root_store = rustls::RootCertStore {
45+
roots: webpki_roots::TLS_SERVER_ROOTS.into(),
46+
};
47+
let mut config = rustls::ClientConfig::builder()
48+
.with_root_certificates(root_store)
49+
.with_no_client_auth();
50+
51+
config.key_log = Arc::new(rustls::KeyLogFile::new());
52+
53+
let conn =
54+
rustls::ClientConnection::new(Arc::new(config), host.try_into().unwrap()).unwrap();
55+
let tls = rustls::StreamOwned::new(conn, tcp);
56+
req.send_to(tls)
57+
} else {
58+
req.send_to(tcp)
59+
}
60+
}
61+
2062
pub fn main() -> http::Result<()> {
2163
let conf = ClientConfig::parse(env::args().skip(1)).unwrap_or_else(|err| {
2264
eprintln!("ERROR: {err}");
@@ -34,19 +76,21 @@ pub fn main() -> http::Result<()> {
3476
.url
3577
.split('/')
3678
.filter(|s| !s.is_empty())
37-
.last()
79+
.next_back()
3880
.unwrap_or(&conf.host);
3981
open_file(fname)
4082
}
4183
};
4284

4385
let req = HttpRequest::builder()
44-
.method(HttpMethod::GET)
45-
.url(conf.url)
86+
.method(conf.method)
87+
.url(conf.url.clone().into_boxed_str())
4688
.version(1.1)
47-
.header("Host", conf.host)
89+
.header("Host", conf.host.clone().into_boxed_str())
4890
.header("Accept", "*/*")
4991
.header("User-Agent", "http-client")
92+
.header("Connection", "close")
93+
.header("Accept-Encoding", "identity")
5094
.build()
5195
.unwrap();
5296

@@ -57,14 +101,32 @@ pub fn main() -> http::Result<()> {
57101
process::exit(1);
58102
}
59103
};
60-
let mut result = req.send_to(tcp).unwrap_or_else(|err| {
61-
eprint!("ERROR: {err}");
62-
process::exit(1);
63-
});
104+
105+
let mut result =
106+
send_request(tcp, conf.http_type, conf.host.clone(), req).unwrap_or_else(|err| {
107+
eprint!("ERROR: {err}");
108+
process::exit(1);
109+
});
110+
111+
if matches!(conf.method, HttpMethod::HEAD) {
112+
println!("Headers");
113+
for (k, v) in result.headers() {
114+
println!("{k}: {v}");
115+
}
116+
return Ok(());
117+
}
64118

65119
match result.write_to(&mut out) {
66120
Ok(_) => { /* eprintln!("\n\n{n} bytes transfered") */ }
67-
Err(err) => eprintln!("\n\nERROR: {err}"),
121+
Err(err) => {
122+
match err.kind() {
123+
/* Ignore this error kind
124+
* https://docs.rs/rustls/latest/rustls/manual/_03_howto/index.html#unexpected-eof
125+
* */
126+
io::ErrorKind::UnexpectedEof => {}
127+
_ => eprintln!("\n\nERROR: {err}"),
128+
}
129+
}
68130
}
69131

70132
Ok(())

crates/http/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "http-utils"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
edition = "2024"
55
description = "Http utils"
66
authors = ["Saúl Valdelvira <saul@saulv.es>"]

crates/http/src/request/mod.rs

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -265,24 +265,21 @@ impl HttpRequest {
265265
///
266266
/// # Errors
267267
/// If, while reading or writing, some io Error is found
268-
pub fn read_body(&mut self, writer: &mut dyn Write) -> Result<()> {
269-
const CHUNK_SIZE: usize = 1024;
270-
let mut buf: [u8; CHUNK_SIZE] = [0; CHUNK_SIZE];
271-
let len = self.content_length();
272-
let n = len / CHUNK_SIZE;
273-
let remainder = len % CHUNK_SIZE;
274-
275-
for _ in 0..n {
276-
self.stream.read_exact(&mut buf)?;
277-
writer.write_all(&buf)?;
278-
}
268+
pub fn read_body(&mut self, out: &mut dyn Write) -> Result<usize> {
269+
let mut total = 0;
270+
loop {
271+
let slice = self.stream.fill_buf()?;
272+
if slice.is_empty() {
273+
break;
274+
}
275+
out.write_all(slice)?;
279276

280-
if remainder > 0 {
281-
self.stream.read_exact(&mut buf[0..remainder])?;
282-
writer.write_all(&buf[0..remainder])?;
277+
let len = slice.len();
278+
self.stream.consume(len);
279+
total += len;
283280
}
284-
285-
Ok(())
281+
out.flush()?;
282+
Ok(total)
286283
}
287284
/// Respond to the request without a body
288285
///

crates/http/src/response/mod.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,14 +159,17 @@ impl HttpResponse {
159159
}
160160

161161
pub fn write_to(&mut self, out: &mut dyn io::Write) -> io::Result<usize> {
162-
let mut buf = [0_u8; 1024];
163162
let mut total = 0;
164-
while let Ok(n) = self.stream.read(&mut buf) {
165-
out.write_all(&buf[0..n])?;
166-
total += n;
167-
if n == 0 {
163+
loop {
164+
let slice = self.stream.fill_buf()?;
165+
if slice.is_empty() {
168166
break;
169167
}
168+
out.write_all(slice)?;
169+
170+
let len = slice.len();
171+
self.stream.consume(len);
172+
total += len;
170173
}
171174
out.flush()?;
172175
Ok(total)

crates/server/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ jsonrs = { package = "jsonrs", version = ">=0.1.4", git = "https://github.com/sa
1717
delay_init = { package = "delay_init" , version = ">=0.2.0", git = "https://github.com/saulvaldelvira/delay-init" }
1818
base64 = { package = "rb64", version = ">=0.1.0", git = "https://github.com/saulvaldelvira/rb64" }
1919
url = { package = "url-utils", version = ">=0.1.0", path = "../url" }
20-
rustls = "0.23.28"
21-
webpki-roots = "1.0.1"
2220

2321
[features]
2422
default = ["full"]

crates/server/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ use std::{
5757
io::{self, BufRead, BufReader},
5858
net::{TcpListener, TcpStream},
5959
sync::Arc,
60+
thread,
6061
time::{Duration, Instant},
6162
};
6263

@@ -117,6 +118,7 @@ fn handle_connection(
117118
if connection.is_some_and(|conn| conn == "keep-alive") && keep_alive {
118119
let start = Instant::now();
119120
let mut n = 1;
121+
log_info!("[{:?}] Start keep alive", thread::current().id());
120122
while start.elapsed() < keep_alive_timeout && n < keep_alive_requests {
121123
let offset = keep_alive_timeout - start.elapsed();
122124

@@ -134,6 +136,7 @@ fn handle_connection(
134136
break;
135137
}
136138
}
139+
log_info!("[{:?}] End keep alive", thread::current().id());
137140
}
138141
Ok(())
139142
}

0 commit comments

Comments
 (0)