Skip to content

Commit 4dfa125

Browse files
authored
Add initial TLS support to dropshot (#224)
This adds a new (optional) configuration table for making Dropshot listen for HTTP-over-TLS connections instead of plain HTTP: [http_api_server] bind_address = "127.0.0.1:0" request_body_max_bytes = 1024 [http_api_server.tls] cert_file = "/path/to/certs.pem" key_file = "/path/to/key.pem" When TLS is enabled, only TLS connections will be accepted.
1 parent c51a606 commit 4dfa125

15 files changed

Lines changed: 1571 additions & 117 deletions

Cargo.lock

Lines changed: 304 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dropshot/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ keywords = ["rest", "openapi", "async"]
1111
categories = ["network-programming", "web-programming::http-server"]
1212

1313
[dependencies]
14+
async-stream = "0.3.2"
1415
async-trait = "0.1.52"
1516
base64 = "0.13.0"
1617
bytes = "1"
@@ -22,13 +23,16 @@ openapiv3 = "=1.0.0"
2223
paste = "1.0.6"
2324
percent-encoding = "2.1.0"
2425
proc-macro2 = "1.0.36"
26+
rustls = "0.20.2"
27+
rustls-pemfile = "0.2.1"
2528
serde_json = "1.0.74"
2629
serde_urlencoded = "0.7.0"
2730
slog-async = "2.4.0"
2831
slog-bunyan = "2.2.0"
2932
slog-json = "2.4.0"
3033
slog-term = "2.5.0"
3134
syn = "1.0.85"
35+
tokio-rustls = "0.23.2"
3236
toml = "0.5.6"
3337

3438
[dependencies.chrono]
@@ -69,13 +73,23 @@ features = [ "uuid" ]
6973

7074
[dev-dependencies]
7175
expectorate = "1.0.4"
76+
hyper-rustls = "0.23.0"
7277
hyper-staticfile = "0.8"
7378
lazy_static = "1.4.0"
7479
libc = "0.2.112"
7580
mime_guess = "2.0.3"
7681
subprocess = "0.2.8"
7782
tempfile = "3.3"
7883
trybuild = "1.0.53"
84+
# Used by the https examples and tests
85+
pem = "1.0"
86+
rcgen = "0.8.14"
87+
88+
[dev-dependencies.rustls]
89+
version = "0.20.2"
90+
# This is needed to use with_custom_certificate_verifier in tests
91+
# https://docs.rs/rustls/0.20.2/src/rustls/client/builder.rs.html#34-49
92+
features = [ "dangerous_configuration" ]
7993

8094
[dev-dependencies.schemars]
8195
version = "0.8.8"

dropshot/examples/https.rs

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright 2020 Oxide Computer Company
2+
3+
/*!
4+
* Example use of Dropshot with TLS enabled
5+
*/
6+
7+
use dropshot::endpoint;
8+
use dropshot::ApiDescription;
9+
use dropshot::ConfigDropshot;
10+
use dropshot::ConfigLogging;
11+
use dropshot::ConfigLoggingLevel;
12+
use dropshot::ConfigTls;
13+
use dropshot::HttpError;
14+
use dropshot::HttpResponseOk;
15+
use dropshot::HttpResponseUpdatedNoContent;
16+
use dropshot::HttpServerStarter;
17+
use dropshot::RequestContext;
18+
use dropshot::TypedBody;
19+
use schemars::JsonSchema;
20+
use serde::Deserialize;
21+
use serde::Serialize;
22+
use std::io::Write;
23+
use std::sync::atomic::AtomicU64;
24+
use std::sync::atomic::Ordering;
25+
use std::sync::Arc;
26+
use tempfile::NamedTempFile;
27+
28+
/*
29+
* This function would not be used in a normal application. It is used to
30+
* generate temporary keys and certificates for the purpose of this demo.
31+
*/
32+
fn generate_keys() -> Result<(NamedTempFile, NamedTempFile), String> {
33+
let keypair =
34+
rcgen::generate_simple_self_signed(vec!["localhost".to_string()])
35+
.map_err(|e| e.to_string())?;
36+
let cert = keypair.serialize_pem().map_err(|e| e.to_string())?;
37+
let priv_key = keypair.serialize_private_key_pem();
38+
39+
let make_temp = || {
40+
tempfile::Builder::new()
41+
.prefix("dropshot-https-example-")
42+
.rand_bytes(5)
43+
.tempfile()
44+
};
45+
46+
let mut cert_file =
47+
make_temp().map_err(|_| "failed to create cert_file")?;
48+
cert_file
49+
.write(cert.as_bytes())
50+
.map_err(|_| "failed to write cert_file")?;
51+
let mut key_file = make_temp().map_err(|_| "failed to create key_file")?;
52+
key_file
53+
.write(priv_key.as_bytes())
54+
.map_err(|_| "failed to write key_file")?;
55+
Ok((cert_file, key_file))
56+
}
57+
58+
#[tokio::main]
59+
async fn main() -> Result<(), String> {
60+
/*
61+
* Begin by generating TLS certificates and keys. A normal application would
62+
* just pass the paths to these via ConfigDropshot.
63+
*/
64+
let (cert_file, key_file) = generate_keys()?;
65+
66+
/*
67+
* We must specify a configuration with a bind address. We'll use 127.0.0.1
68+
* since it's available and won't expose this server outside the host. We
69+
* request port 0, which allows the operating system to pick any available
70+
* port.
71+
*
72+
* In addition, we'll make this an HTTPS server.
73+
*/
74+
let config_dropshot = ConfigDropshot {
75+
tls: Some(ConfigTls {
76+
cert_file: cert_file.path().to_path_buf(),
77+
key_file: key_file.path().to_path_buf(),
78+
}),
79+
..Default::default()
80+
};
81+
82+
/*
83+
* For simplicity, we'll configure an "info"-level logger that writes to
84+
* stderr assuming that it's a terminal.
85+
*/
86+
let config_logging = ConfigLogging::StderrTerminal {
87+
level: ConfigLoggingLevel::Info,
88+
};
89+
let log = config_logging
90+
.to_logger("example-basic")
91+
.map_err(|error| format!("failed to create logger: {}", error))?;
92+
93+
/*
94+
* Build a description of the API.
95+
*/
96+
let mut api = ApiDescription::new();
97+
api.register(example_api_get_counter).unwrap();
98+
api.register(example_api_put_counter).unwrap();
99+
100+
/*
101+
* The functions that implement our API endpoints will share this context.
102+
*/
103+
let api_context = ExampleContext::new();
104+
105+
/*
106+
* Set up the server.
107+
*/
108+
let server =
109+
HttpServerStarter::new(&config_dropshot, api, api_context, &log)
110+
.map_err(|error| format!("failed to create server: {}", error))?
111+
.start();
112+
113+
/*
114+
* Wait for the server to stop. Note that there's not any code to shut down
115+
* this server, so we should never get past this point.
116+
*/
117+
server.await
118+
}
119+
120+
/**
121+
* Application-specific example context (state shared by handler functions)
122+
*/
123+
struct ExampleContext {
124+
/** counter that can be manipulated by requests to the HTTP API */
125+
counter: AtomicU64,
126+
}
127+
128+
impl ExampleContext {
129+
/**
130+
* Return a new ExampleContext.
131+
*/
132+
pub fn new() -> ExampleContext {
133+
ExampleContext {
134+
counter: AtomicU64::new(0),
135+
}
136+
}
137+
}
138+
139+
/*
140+
* HTTP API interface
141+
*/
142+
143+
/**
144+
* `CounterValue` represents the value of the API's counter, either as the
145+
* response to a GET request to fetch the counter or as the body of a PUT
146+
* request to update the counter.
147+
*/
148+
#[derive(Deserialize, Serialize, JsonSchema)]
149+
struct CounterValue {
150+
counter: u64,
151+
}
152+
153+
/**
154+
* Fetch the current value of the counter.
155+
*/
156+
#[endpoint {
157+
method = GET,
158+
path = "/counter",
159+
}]
160+
async fn example_api_get_counter(
161+
rqctx: Arc<RequestContext<ExampleContext>>,
162+
) -> Result<HttpResponseOk<CounterValue>, HttpError> {
163+
let api_context = rqctx.context();
164+
165+
Ok(HttpResponseOk(CounterValue {
166+
counter: api_context.counter.load(Ordering::SeqCst),
167+
}))
168+
}
169+
170+
/**
171+
* Update the current value of the counter. Note that the special value of 10
172+
* is not allowed (just to demonstrate how to generate an error).
173+
*/
174+
#[endpoint {
175+
method = PUT,
176+
path = "/counter",
177+
}]
178+
async fn example_api_put_counter(
179+
rqctx: Arc<RequestContext<ExampleContext>>,
180+
update: TypedBody<CounterValue>,
181+
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
182+
let api_context = rqctx.context();
183+
let updated_value = update.into_inner();
184+
185+
if updated_value.counter == 10 {
186+
Err(HttpError::for_bad_request(
187+
Some(String::from("BadInput")),
188+
format!("do not like the number {}", updated_value.counter),
189+
))
190+
} else {
191+
api_context.counter.store(updated_value.counter, Ordering::SeqCst);
192+
Ok(HttpResponseUpdatedNoContent())
193+
}
194+
}

dropshot/examples/pagination-basic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ async fn main() -> Result<(), String> {
138138
let ctx = tree;
139139
let config_dropshot = ConfigDropshot {
140140
bind_address: SocketAddr::from((Ipv4Addr::LOCALHOST, port)),
141-
request_body_max_bytes: 1024,
141+
..Default::default()
142142
};
143143
let config_logging = ConfigLogging::StderrTerminal {
144144
level: ConfigLoggingLevel::Debug,

dropshot/examples/pagination-multiple-resources.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ async fn main() -> Result<(), String> {
295295
let ctx = DataCollection::new();
296296
let config_dropshot = ConfigDropshot {
297297
bind_address: SocketAddr::from((Ipv4Addr::LOCALHOST, port)),
298-
request_body_max_bytes: 1024,
298+
..Default::default()
299299
};
300300
let config_logging = ConfigLogging::StderrTerminal {
301301
level: ConfigLoggingLevel::Debug,

dropshot/examples/pagination-multiple-sorts.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ async fn main() -> Result<(), String> {
311311
let ctx = ProjectCollection::new();
312312
let config_dropshot = ConfigDropshot {
313313
bind_address: SocketAddr::from((Ipv4Addr::LOCALHOST, port)),
314-
request_body_max_bytes: 1024,
314+
..Default::default()
315315
};
316316
let config_logging = ConfigLogging::StderrTerminal {
317317
level: ConfigLoggingLevel::Debug,

dropshot/src/config.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use serde::Deserialize;
77
use serde::Serialize;
88
use std::net::SocketAddr;
9+
use std::path::PathBuf;
910

1011
/**
1112
* Configuration for a Dropshot server.
@@ -31,6 +32,11 @@ use std::net::SocketAddr;
3132
* [http_api_server]
3233
* bind_address = "127.0.0.1:12345"
3334
* request_body_max_bytes = 1024
35+
* ## Optional, to enable TLS
36+
* [http_api_server.tls]
37+
* cert_file = "/path/to/certs.pem"
38+
* key_file = "/path/to/key.pem"
39+
*
3440
*
3541
* ## ... (other app-specific config)
3642
* "##
@@ -49,13 +55,31 @@ pub struct ConfigDropshot {
4955
pub bind_address: SocketAddr,
5056
/** maximum allowed size of a request body, defaults to 1024 */
5157
pub request_body_max_bytes: usize,
58+
59+
/** If present, enables TLS with the given configuration */
60+
pub tls: Option<ConfigTls>,
61+
}
62+
63+
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
64+
pub struct ConfigTls {
65+
/** Path to a PEM file containing a certificate chain for the
66+
* server to identify itself with. The first certificate is the
67+
* end-entity certificate, and the remaining are intermediate
68+
* certificates on the way to a trusted CA.
69+
*/
70+
pub cert_file: PathBuf,
71+
/** Path to a PEM-encoded PKCS #8 file containing the private key the
72+
* server will use.
73+
*/
74+
pub key_file: PathBuf,
5275
}
5376

5477
impl Default for ConfigDropshot {
5578
fn default() -> Self {
5679
ConfigDropshot {
5780
bind_address: "127.0.0.1:0".parse().unwrap(),
5881
request_body_max_bytes: 1024,
82+
tls: None,
5983
}
6084
}
6185
}

dropshot/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
* &ConfigDropshot {
7171
* bind_address: "127.0.0.1:0".parse().unwrap(),
7272
* request_body_max_bytes: 1024,
73+
* tls: None,
7374
* },
7475
* api,
7576
* Arc::new(()),
@@ -615,6 +616,7 @@ pub use api_description::ApiEndpointParameterLocation;
615616
pub use api_description::ApiEndpointResponse;
616617
pub use api_description::OpenApiDefinition;
617618
pub use config::ConfigDropshot;
619+
pub use config::ConfigTls;
618620
pub use error::HttpError;
619621
pub use error::HttpErrorResponseBody;
620622
pub use handler::Extractor;

0 commit comments

Comments
 (0)