Skip to content

Commit baafb0e

Browse files
authored
Add bindings for ClientSession, plus demo program (#1)
The demo program three times connects via HTTPS to a host, makes an HTTP request, and prints the response to stdout.
2 parents 6504e68 + 56c7d73 commit baafb0e

File tree

4 files changed

+675
-0
lines changed

4 files changed

+675
-0
lines changed

Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "crustls"
3+
version = "0.1.0"
4+
authors = ["Jacob Hoffman-Andrews <[email protected]>"]
5+
description = "C-to-rustls bindings"
6+
edition = "2018"
7+
8+
[dependencies]
9+
rustls = "0.19"
10+
webpki = "0.21"
11+
libc = "0.2"
12+
env_logger = "0.8"
13+
webpki-roots = "0.21"
14+
15+
[dev_dependencies]
16+
cbindgen = "*"
17+
18+
[lib]
19+
name = "crustls"
20+
crate-type = ["staticlib"]

Makefile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
CFLAGS := -Werror -Wall -Wextra -Wpedantic
2+
LDFLAGS := -Wl,--gc-sections -lpthread -ldl
3+
4+
all: target/crustls-demo
5+
target/crustls-demo httpbin.org /headers
6+
7+
target:
8+
mkdir -p $@
9+
10+
src/lib.h: src/lib.rs
11+
cbindgen --lang C --output src/lib.h
12+
13+
target/crustls-demo: target/main.o target/debug/libcrustls.a
14+
$(CC) -o $@ $^ $(LDFLAGS)
15+
16+
target/debug/libcrustls.a: src/lib.rs Cargo.toml
17+
cargo build
18+
19+
target/main.o: src/main.c src/lib.h | target
20+
$(CC) -o $@ -c $< $(CFLAGS)
21+
22+
clean:
23+
rm -rf target

src/lib.rs

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
#![crate_type = "staticlib"]
2+
3+
use libc::{c_char, c_int, c_void, size_t, ssize_t};
4+
use std::io::{Cursor, Read, Write};
5+
use std::slice;
6+
use std::{ffi::CStr, sync::Arc};
7+
use std::{io::ErrorKind::ConnectionAborted, mem};
8+
9+
use rustls::{ClientConfig, ClientSession, Session};
10+
11+
type CrustlsResult = c_int;
12+
13+
pub const CRUSTLS_OK: c_int = 0;
14+
pub const CRUSTLS_ERROR: c_int = 1;
15+
16+
/// Create a client_config. Caller owns the memory and must free it with
17+
/// rustls_client_config_free.
18+
#[no_mangle]
19+
pub extern "C" fn rustls_client_config_new() -> *const c_void {
20+
let mut config = rustls::ClientConfig::new();
21+
config
22+
.root_store
23+
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
24+
env_logger::init();
25+
Arc::into_raw(Arc::new(config)) as *const c_void
26+
}
27+
28+
/// "Free" a client_config previously returned from rustls_client_config_new.
29+
/// Since client_config is actually an atomically reference-counted pointer,
30+
/// extant client_sessions may still hold an internal reference to the
31+
/// Rust object. However, C code must consider this pointer unusable after
32+
/// "free"ing it.
33+
/// Calling with NULL is fine. Must not be called twice with the same value.
34+
#[no_mangle]
35+
pub extern "C" fn rustls_client_config_free(config: *const c_void) {
36+
unsafe {
37+
if let Some(c) = (config as *const ClientConfig).as_ref() {
38+
// To free the client_config, we reconstruct the Arc. It should have a refcount of 1,
39+
// representing the C code's copy. When it drops, that refcount will go down to 0
40+
// and the inner ClientConfig will be dropped.
41+
let arc: Arc<ClientConfig> = Arc::from_raw(c);
42+
let strong_count = Arc::strong_count(&arc);
43+
if strong_count < 1 {
44+
eprintln!(
45+
"rustls_client_config_free: invariant failed: arc.strong_count was < 1: {}. \
46+
You must not free the same client_config multiple times.",
47+
strong_count
48+
);
49+
}
50+
} else {
51+
eprintln!("rustls_client_config_free: config was NULL");
52+
}
53+
};
54+
}
55+
56+
/// In rustls_client_config_new, we create an Arc, then call `into_raw` and return the resulting raw
57+
/// pointer to C. C can then call rustls_client_session_new multiple times using that same raw
58+
/// pointer. On each call, we need to reconstruct the Arc. But once we reconstruct the Arc, its
59+
/// reference count will be decremented on drop. We need to reference count to stay at 1, because
60+
/// the C code is holding a copy. This function turns the raw pointer back into an Arc, clones it
61+
/// to increment the reference count (which will make it 2 in this particular case), and
62+
/// mem::forgets the clone. The mem::forget prevents the reference count from being decremented when
63+
/// we exit this function, so it will stay at 2 as long as we are in Rust code. Once the caller
64+
/// drops its Arc, the reference count will go back down to 1, indicating the C code's copy.
65+
///
66+
/// Unsafety:
67+
///
68+
/// v must be a non-null pointer that resulted from previously calling `Arc::into_raw`.
69+
unsafe fn arc_with_incref_from_raw<T>(v: *const T) -> Arc<T> {
70+
let r = Arc::from_raw(v);
71+
let val = Arc::clone(&r);
72+
mem::forget(r);
73+
val
74+
}
75+
76+
/// Create a new rustls::ClientSession, and return it in the output parameter `out`.
77+
/// If this returns an error code, the memory pointed to by `session_out` remains unchanged.
78+
/// If this returns a non-error, the memory pointed to by `session_out` is modified to point
79+
/// at a valid ClientSession. The caller now owns the ClientSession and must call
80+
/// `rustls_client_session_free` when done with it.
81+
#[no_mangle]
82+
pub extern "C" fn rustls_client_session_new(
83+
config: *const c_void,
84+
hostname: *const c_char,
85+
session_out: *mut *mut c_void,
86+
) -> CrustlsResult {
87+
let hostname: &CStr = unsafe {
88+
if hostname.is_null() {
89+
eprintln!("rustls_client_session_new: hostname was NULL");
90+
return CRUSTLS_ERROR;
91+
}
92+
CStr::from_ptr(hostname)
93+
};
94+
let config: Arc<ClientConfig> = unsafe {
95+
match (config as *const ClientConfig).as_ref() {
96+
Some(c) => arc_with_incref_from_raw(c),
97+
None => {
98+
eprintln!("rustls_client_session_new: config was NULL");
99+
return CRUSTLS_ERROR;
100+
}
101+
}
102+
};
103+
let hostname: &str = match hostname.to_str() {
104+
Ok(s) => s,
105+
Err(e) => {
106+
eprintln!("converting hostname to Rust &str: {}", e);
107+
return CRUSTLS_ERROR;
108+
}
109+
};
110+
let name_ref = match webpki::DNSNameRef::try_from_ascii_str(hostname) {
111+
Ok(nr) => nr,
112+
Err(e) => {
113+
eprintln!(
114+
"turning hostname '{}' into webpki::DNSNameRef: {}",
115+
hostname, e
116+
);
117+
return CRUSTLS_ERROR;
118+
}
119+
};
120+
let client = ClientSession::new(&config, name_ref);
121+
122+
// We've succeeded. Put the client on the heap, and transfer ownership
123+
// to the caller. After this point, we must return CRUSTLS_OK so the
124+
// caller knows it is responsible for this memory.
125+
let b = Box::new(client);
126+
unsafe {
127+
*session_out = Box::into_raw(b) as *mut c_void;
128+
}
129+
130+
return CRUSTLS_OK;
131+
}
132+
133+
#[no_mangle]
134+
pub extern "C" fn rustls_client_session_wants_read(session: *const c_void) -> bool {
135+
unsafe {
136+
match (session as *const ClientSession).as_ref() {
137+
Some(cs) => cs.wants_read(),
138+
None => false,
139+
}
140+
}
141+
}
142+
143+
#[no_mangle]
144+
pub extern "C" fn rustls_client_session_wants_write(session: *const c_void) -> bool {
145+
unsafe {
146+
match (session as *const ClientSession).as_ref() {
147+
Some(cs) => cs.wants_write(),
148+
None => false,
149+
}
150+
}
151+
}
152+
153+
#[no_mangle]
154+
pub extern "C" fn rustls_client_session_process_new_packets(session: *mut c_void) -> CrustlsResult {
155+
let session: &mut ClientSession = unsafe {
156+
match (session as *mut ClientSession).as_mut() {
157+
Some(cs) => cs,
158+
None => {
159+
eprintln!("ClientSession::process_new_packets: session was NULL");
160+
return CRUSTLS_ERROR;
161+
}
162+
}
163+
};
164+
let result: CrustlsResult = match session.process_new_packets() {
165+
Ok(()) => CRUSTLS_OK,
166+
Err(e) => {
167+
eprintln!("ClientSession::process_new_packets: {}", e);
168+
CRUSTLS_ERROR
169+
}
170+
};
171+
result
172+
}
173+
174+
/// Free a client_session previously returned from rustls_client_session_new.
175+
/// Calling with NULL is fine. Must not be called twice with the same value.
176+
#[no_mangle]
177+
pub extern "C" fn rustls_client_session_free(session: *mut c_void) {
178+
unsafe {
179+
if let Some(c) = (session as *mut ClientSession).as_mut() {
180+
// Convert the pointer to a Box and drop it.
181+
Box::from_raw(c);
182+
} else {
183+
eprintln!("warning: rustls_client_config_free: config was NULL");
184+
}
185+
}
186+
}
187+
188+
/// Write plaintext bytes into the ClientSession. This acts like
189+
/// write(2). It returns the number of bytes written, or -1 on error.
190+
#[no_mangle]
191+
pub extern "C" fn rustls_client_session_write(
192+
session: *const c_void,
193+
buf: *const u8,
194+
count: size_t,
195+
) -> ssize_t {
196+
let session: &mut ClientSession = unsafe {
197+
match (session as *mut ClientSession).as_mut() {
198+
Some(cs) => cs,
199+
None => {
200+
eprintln!("ClientSession::write: session was NULL");
201+
return -1;
202+
}
203+
}
204+
};
205+
let write_buf: &[u8] = unsafe {
206+
if buf.is_null() {
207+
eprintln!("ClientSession::write: buf was NULL");
208+
return -1;
209+
}
210+
slice::from_raw_parts(buf, count as usize)
211+
};
212+
let n_written: usize = match session.write(write_buf) {
213+
Ok(n) => n,
214+
Err(e) => {
215+
eprintln!("ClientSession::write: {}", e);
216+
return -1;
217+
}
218+
};
219+
n_written as ssize_t
220+
}
221+
222+
/// Read plaintext bytes from the ClientSession. This acts like
223+
/// read(2), writing the plaintext bytes into `buf`. It returns
224+
/// the number of bytes read, or -1 on error.
225+
#[no_mangle]
226+
pub extern "C" fn rustls_client_session_read(
227+
session: *const c_void,
228+
buf: *mut u8,
229+
count: size_t,
230+
) -> ssize_t {
231+
let session: &mut ClientSession = unsafe {
232+
match (session as *mut ClientSession).as_mut() {
233+
Some(cs) => cs,
234+
None => {
235+
eprintln!("ClientSession::read: session was NULL");
236+
return -1;
237+
}
238+
}
239+
};
240+
let read_buf: &mut [u8] = unsafe {
241+
if buf.is_null() {
242+
eprintln!("ClientSession::read: buf was NULL");
243+
return -1;
244+
}
245+
slice::from_raw_parts_mut(buf, count as usize)
246+
};
247+
// Since it's *possible* for a Read impl to consume the possibly-uninitialized memory from buf,
248+
// zero it out just in case. TODO: use Initializer once it's stabilized.
249+
// https://doc.rust-lang.org/nightly/std/io/trait.Read.html#method.initializer
250+
for c in read_buf.iter_mut() {
251+
*c = 0;
252+
}
253+
let n_read: usize = match session.read(read_buf) {
254+
Ok(n) => n,
255+
// The CloseNotify TLS alert is benign, but rustls returns it as an Error. See comment on
256+
// https://docs.rs/rustls/0.19.0/rustls/struct.ClientSession.html#impl-Read.
257+
// Log it and return EOF.
258+
Err(e) if e.kind() == ConnectionAborted && e.to_string().contains("CloseNotify") => {
259+
eprintln!("ClientSession::read: CloseNotify (this is expected): {}", e);
260+
return 0;
261+
}
262+
Err(e) => {
263+
eprintln!("ClientSession::read: {}", e);
264+
return -1;
265+
}
266+
};
267+
n_read as ssize_t
268+
}
269+
270+
/// Read TLS bytes taken from a socket into the ClientSession. This acts like
271+
/// read(2). It returns the number of bytes read, or -1 on error.
272+
#[no_mangle]
273+
pub extern "C" fn rustls_client_session_read_tls(
274+
session: *const c_void,
275+
buf: *const u8,
276+
count: size_t,
277+
) -> ssize_t {
278+
let session: &mut ClientSession = unsafe {
279+
match (session as *mut ClientSession).as_mut() {
280+
Some(cs) => cs,
281+
None => {
282+
eprintln!("ClientSession::read_tls: session was NULL");
283+
return -1;
284+
}
285+
}
286+
};
287+
let input_buf: &[u8] = unsafe {
288+
if buf.is_null() {
289+
eprintln!("ClientSession::read_tls: buf was NULL");
290+
return -1;
291+
}
292+
slice::from_raw_parts(buf, count as usize)
293+
};
294+
let mut cursor = Cursor::new(input_buf);
295+
let n_read: usize = match session.read_tls(&mut cursor) {
296+
Ok(n) => n,
297+
Err(e) => {
298+
eprintln!("ClientSession::read_tls: {}", e);
299+
return -1;
300+
}
301+
};
302+
n_read as ssize_t
303+
}
304+
305+
/// Write TLS bytes from the ClientSession into a buffer. Those bytes should then be written to
306+
/// a socket. This acts like write(2). It returns the number of bytes read, or -1 on error.
307+
#[no_mangle]
308+
pub extern "C" fn rustls_client_session_write_tls(
309+
session: *const c_void,
310+
buf: *mut u8,
311+
count: size_t,
312+
) -> ssize_t {
313+
let session: &mut ClientSession = unsafe {
314+
match (session as *mut ClientSession).as_mut() {
315+
Some(cs) => cs,
316+
None => {
317+
eprintln!("ClientSession::write_tls: session was NULL");
318+
return -1;
319+
}
320+
}
321+
};
322+
let mut output_buf: &mut [u8] = unsafe {
323+
if buf.is_null() {
324+
eprintln!("ClientSession::write_tls: buf was NULL");
325+
return -1;
326+
}
327+
slice::from_raw_parts_mut(buf, count as usize)
328+
};
329+
let n_written: usize = match session.write_tls(&mut output_buf) {
330+
Ok(n) => n,
331+
Err(e) => {
332+
eprintln!("ClientSession::write_tls: {}", e);
333+
return -1;
334+
}
335+
};
336+
n_written as ssize_t
337+
}

0 commit comments

Comments
 (0)