1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
use super::{Backend, MAX_BACKEND_NAME_LEN};
use crate::abi::fastly_http_req::register_dynamic_backend;
use crate::experimental::GrpcBackend;
use crate::secret_store::Secret;
use fastly_shared::{FastlyStatus, SslVersion};
use fastly_sys::{BackendConfigOptions, DynamicBackendConfig};
use std::num::NonZeroU32;
use std::time::Duration;
use thiserror::Error;
/// A builder structure for generating a dynamic backend.
///
/// This structure can be constructed using either [`Backend::builder()`][Backend::builder()] or its
/// own [`new()`][Self::new()] method, and will generate a new backend for use by the program after
/// consuming the `BackendBuilder` with [`finish()`][Self::finish()].
pub struct BackendBuilder {
name: String,
target: String,
host_override: Option<String>,
connect_timeout: Option<Duration>,
first_byte_timeout: Option<Duration>,
between_bytes_timeout: Option<Duration>,
use_ssl: bool,
min_tls_version: Option<SslVersion>,
max_tls_version: Option<SslVersion>,
cert_hostname: Option<String>,
ca_cert: Option<String>,
ciphers: Option<String>,
sni_hostname: Option<String>,
pool_connections: bool,
client_certificate_info: Option<(String, Secret)>,
grpc: bool,
http_keepalive_time_ms: Option<u32>,
tcp_keepalive_enable: Option<bool>,
tcp_keepalive_interval_secs: Option<NonZeroU32>,
tcp_keepalive_probes: Option<NonZeroU32>,
tcp_keepalive_time_secs: Option<NonZeroU32>,
max_connections: Option<u32>,
max_use: Option<u32>,
max_lifetime: Option<Duration>,
prefer_ipv6: Option<bool>,
}
/// Errors that can arise from attempting to create a dynamic backend.
///
/// Perhaps the most critical of these is `Disallowed`, which will occur
/// if your service is not permitted to use dynamic backends.
#[derive(Clone, Debug, Error, PartialEq)]
pub enum BackendCreationError {
/// Timeouts for backends must be less than 2^32 milliseconds, or
/// about a month and a half.
#[error("Connect timeout too long; must be < 2^32 milliseconds")]
ConnectTimeoutTooLarge(Duration),
/// Timeouts for backends must be less than 2^32 milliseconds, or
/// about a month and a half.
#[error("First byte timeout too long; must be < 2^32 milliseconds")]
FirstByteTimeoutTooLarge(Duration),
/// Timeouts for backends must be less than 2^32 milliseconds, or
/// about a month and a half.
#[error("Between-byte timeout too long; must be < 2^32 milliseconds")]
BetweenBytesTimeoutTooLarge(Duration),
/// This service is not allowed to create dynamic backends.
///
/// If you'd like to use dynamic backends, please contact your Fastly sales agent.
#[error("Dynamic backends not supported for this service")]
Disallowed,
/// Something internal went wrong with the service at runtime; you
/// may be able to do something to react to this information.
///
/// This value is identical to the values underlying `FastlyStatus`.
#[error("Host failed with status {0:?}")]
HostError(FastlyStatus),
/// There was a problem converting the new name from the host into
/// something we could turn into a Rust `String`.
///
/// Please check the prefix you provided, if you provided one, and make sure it's reasonable.
#[error(transparent)]
EncodingError(#[from] std::string::FromUtf8Error),
/// The backend name provided was too long; please keep it to <255
/// characters.
#[error("Provided backend name too long: {0}")]
NameTooLong(String),
/// The backend name is already in use.
#[error("The provided backend name is already in use")]
NameInUse,
}
impl From<FastlyStatus> for BackendCreationError {
fn from(x: FastlyStatus) -> Self {
match x {
FastlyStatus::UNSUPPORTED => BackendCreationError::Disallowed,
FastlyStatus::ERROR => BackendCreationError::NameInUse,
_ => BackendCreationError::HostError(x),
}
}
}
impl BackendBuilder {
#[doc = include_str!("../../docs/snippets/dynamic-backend-builder.md")]
pub fn new(name: impl ToString, target: impl ToString) -> Self {
BackendBuilder {
name: name.to_string(),
target: target.to_string(),
host_override: None,
connect_timeout: None,
first_byte_timeout: None,
between_bytes_timeout: None,
// TODO: Should the default actually be to use SSL?
use_ssl: false,
min_tls_version: None,
max_tls_version: None,
cert_hostname: None,
ca_cert: None,
ciphers: None,
sni_hostname: None,
pool_connections: true,
client_certificate_info: None,
grpc: false,
http_keepalive_time_ms: None,
tcp_keepalive_enable: None,
tcp_keepalive_interval_secs: None,
tcp_keepalive_probes: None,
tcp_keepalive_time_secs: None,
max_connections: Some(200),
max_use: None,
max_lifetime: None,
prefer_ipv6: None,
}
}
/// Set a host header override when contacting this backend.
///
/// This will force the value of the "Host" header to the given string when sending out the
/// origin request. If this is not set and no header already exists, the "Host" header will
/// default to this builder's target.
///
/// For more information, see the Fastly documentation on override hosts here:
/// <https://docs.fastly.com/en/guides/specifying-an-override-host>
pub fn override_host(mut self, name: impl ToString) -> Self {
self.host_override = Some(name.to_string());
self
}
/// Set the connection timeout for this backend. Defaults to 1,000ms (1s).
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = Some(timeout);
self
}
/// Set a timeout that applies between the time of sending a request and the time the HTTP
/// headers are fully received. Defaults to 15,000ms (15s).
///
/// The name is, admittedly, confusing; it notionally refers to "first byte of the HTTP body",
/// though of course not all responses have bodies.
pub fn first_byte_timeout(mut self, timeout: Duration) -> Self {
self.first_byte_timeout = Some(timeout);
self
}
/// Set a timeout that applies any two bytes of the body.
/// Defaults to 10,000ms (10s).
pub fn between_bytes_timeout(mut self, timeout: Duration) -> Self {
self.between_bytes_timeout = Some(timeout);
self
}
/// Use SSL/TLS to connect to the backend.
///
/// When using SSL/TLS, Fastly checks the validity of the backend's
/// certificate, and fails the connection if the certificate is invalid.
/// This check is not optional: an invalid certificate will cause the
/// backend connection to fail (but read on).
///
/// By default, the validity check does not require that the certificate
/// hostname matches the hostname of your request. You can use
/// [check_certificate](Self::check_certificate) to request a check of the
/// certificate hostname.
///
/// By default, certificate validity uses a set of public certificate
/// authorities. You can specify an alternative CA using
/// [ca_certificate](Self::ca_certificate).
pub fn enable_ssl(mut self) -> Self {
self.use_ssl = true;
self
}
/// Disable SSL/TLS for this backend.
pub fn disable_ssl(mut self) -> Self {
self.use_ssl = false;
self
}
/// Set the minimum TLS version for connecting to the backend. Setting
/// this will enable SSL for the connection as a side effect.
pub fn set_min_tls_version(mut self, minimum: SslVersion) -> Self {
self.use_ssl = true;
self.min_tls_version = Some(minimum);
self
}
/// Set the maximum TLS version for connecting to the backend. Setting
/// this will enable SSL for the connection as a side effect.
pub fn set_max_tls_version(mut self, maximum: SslVersion) -> Self {
self.use_ssl = true;
self.max_tls_version = Some(maximum);
self
}
/// Define the hostname that the server certificate should declare, and
/// turn on validation during backend connections. You should enable this
/// if you are using SSL/TLS, and setting this will enable SSL for the
/// connection as a side effect.
///
/// If check_certificate is not provided (default), the server certificate's
/// hostname may have any value.
pub fn check_certificate(mut self, hostname: impl ToString) -> Self {
self.use_ssl = true;
self.cert_hostname = Some(hostname.to_string());
self
}
/// Set the CA certificate to use when checking the validity of the
/// backend. Setting this will enable SSL for the connection as a side
/// effect.
///
/// If ca_certificate is not provided (default), the backends's certificate
/// is validated using a set of public root CAs.
pub fn ca_certificate(mut self, value: impl ToString) -> Self {
self.use_ssl = true;
self.ca_cert = Some(value.to_string());
self
}
/// Set the acceptable cipher suites to use for TLS 1.0 - 1.2 connections. Setting
/// this will enable SSL for the connection as a side effect.
pub fn tls_ciphers(mut self, value: impl ToString) -> Self {
self.use_ssl = true;
self.ciphers = Some(value.to_string());
self
}
/// Set the SNI hostname for the backend connection. Setting this will
/// enable SSL for the connection as a side effect.
pub fn sni_hostname(mut self, value: impl ToString) -> Self {
self.use_ssl = true;
self.sni_hostname = Some(value.to_string());
self
}
/// Provide the given client certificate to the server as part of the SSL handshake.
//
/// Setting this will enable SSL for the connection as a side effect. Both
/// the certificate and the key to use should be in standard PEM format;
/// providing the information in another format will lead to an error. We
/// suggest that (at least the) key should be held in something like the
/// Fastly secret store for security, with the handle passed to this function
/// without unpacking it via [`Secret::plaintext`]; the certificate can
/// be held in a less secure medium.
///
/// (If it is absolutely necessary to get the key from another source, we
/// suggest the use of [`Secret::from_bytes`]).
pub fn provide_client_certificate(
mut self,
pem_certificate: impl ToString,
pem_key: Secret,
) -> Self {
self.use_ssl = true;
self.client_certificate_info = Some((pem_certificate.to_string(), pem_key));
self
}
/// Determine whether or not connections to the same backend should be pooled
/// across different sessions.
///
/// Fastly considers two backends "the same" if they're registered with the
/// same name and the exact same settings. In those cases, when pooling is
/// enabled, if Session 1 opens a connection to this backend it will be left
/// open, and can be re-used by Session 2. This can help improve backend
/// latency, by removing the need for the initial network / TLS handshake(s).
///
/// By default, pooling is enabled for dynamic backends.
pub fn enable_pooling(mut self, value: bool) -> Self {
self.pool_connections = value;
self
}
/// Configure up to how long to allow HTTP keepalive connections to remain idle in the
/// connection pool.
pub fn http_keepalive_time(mut self, value: Duration) -> Self {
self.http_keepalive_time_ms = value.as_millis().try_into().ok();
self
}
/// Configure whether or not to use TCP keepalive on the connection to the backend.
pub fn tcp_keepalive_enable(mut self, value: bool) -> Self {
self.tcp_keepalive_enable = Some(value);
self
}
/// Configure how long to wait in between each TCP keepalive probe sent to the backend.
pub fn tcp_keepalive_interval_secs(mut self, value: NonZeroU32) -> Self {
self.tcp_keepalive_interval_secs = Some(value);
self
}
/// Configure up to how many TCP keepalive probes to send to the backend before the connection
/// is considered dead.
pub fn tcp_keepalive_probes(mut self, value: NonZeroU32) -> Self {
self.tcp_keepalive_probes = Some(value);
self
}
/// Configure how long to wait after the last sent data over the TCP connection before
/// starting to send TCP keepalive probes.
pub fn tcp_keepalive_time_secs(mut self, value: NonZeroU32) -> Self {
self.tcp_keepalive_time_secs = Some(value);
self
}
/// Configure whether to prefer trying IPv6 connections first before IPv4 when a hostname
/// has both A and AAAA records.
///
/// This defaults to `true`.
pub fn prefer_ipv6(mut self, value: bool) -> Self {
self.prefer_ipv6 = Some(value);
self
}
/// Configure how many connections to allow in the connection pool for this backend.
///
/// `0` is treated as unlimited. The default is `200`.
///
/// Note that this limit is best determined experimentally, since the total number of
/// connections to the backend will depend on POP sizes, HTTP keepalive limits, and the traffic
/// patterns for individual POPs.
pub fn max_connections(mut self, value: u32) -> Self {
self.max_connections = Some(value);
self
}
/// Configure how many times an HTTP keepalive connection can be reused in a connection pool.
///
/// `0` is treated as unlimited. The default is `0`.
pub fn max_use(mut self, value: u32) -> Self {
self.max_use = Some(value);
self
}
/// Configure an upper bound for how long a pooled HTTP keepalive connection is allowed to have
/// been open before we stop trying to reuse it.
///
/// [Duration::ZERO] is treated as unlimited. The default is [Duration::ZERO].
pub fn max_lifetime(mut self, value: Duration) -> Self {
self.max_lifetime = Some(value);
self
}
/// Attempt to register this backend with runtime, returning the backend
/// for use like any other backends.
///
/// In the case that this function returns `BackendCreationError::NameInUse`,
/// users can use `Backend::from_str` as per normal to create a reference to
/// that version. (That being said, you should be careful to only use this
/// capability in situations in which you are 100% sure that this name will
/// always lead to the same place.)
pub fn finish(self) -> Result<Backend, BackendCreationError> {
let name = self.name.as_ptr();
let name_len = self.name.len();
if name_len > MAX_BACKEND_NAME_LEN {
return Err(BackendCreationError::NameTooLong(self.name));
}
let target = self.target.as_ptr();
let target_len = self.target.len();
// now that we've got all the required things ready to go, let's build our
// config structures.
let mut config_options = BackendConfigOptions::empty();
let mut config = DynamicBackendConfig::default();
if let Some(host_override) = self.host_override.as_deref() {
config.host_override = host_override.as_ptr();
config.host_override_len = host_override.len() as u32;
config_options.insert(BackendConfigOptions::HOST_OVERRIDE);
}
if let Some(connect_timeout) = self.connect_timeout {
config.connect_timeout_ms = connect_timeout
.as_millis()
.try_into()
.map_err(|_| BackendCreationError::ConnectTimeoutTooLarge(connect_timeout))?;
config_options.insert(BackendConfigOptions::CONNECT_TIMEOUT);
}
if let Some(first_byte_timeout) = self.first_byte_timeout {
config.first_byte_timeout_ms = first_byte_timeout
.as_millis()
.try_into()
.map_err(|_| BackendCreationError::FirstByteTimeoutTooLarge(first_byte_timeout))?;
config_options.insert(BackendConfigOptions::FIRST_BYTE_TIMEOUT);
}
if let Some(between_bytes_timeout) = self.between_bytes_timeout {
config.between_bytes_timeout_ms =
between_bytes_timeout.as_millis().try_into().map_err(|_| {
BackendCreationError::BetweenBytesTimeoutTooLarge(between_bytes_timeout)
})?;
config_options.insert(BackendConfigOptions::BETWEEN_BYTES_TIMEOUT);
}
if self.use_ssl {
config_options.insert(BackendConfigOptions::USE_SSL);
}
if let Some(min_tls_version) = self.min_tls_version {
config.ssl_min_version = min_tls_version as u32;
config_options.insert(BackendConfigOptions::SSL_MIN_VERSION);
}
if let Some(max_tls_version) = self.max_tls_version {
config.ssl_max_version = max_tls_version as u32;
config_options.insert(BackendConfigOptions::SSL_MAX_VERSION);
}
if let Some(hostname) = self.cert_hostname.as_deref() {
config.cert_hostname = hostname.as_ptr();
config.cert_hostname_len = hostname.len() as u32;
config_options.insert(BackendConfigOptions::CERT_HOSTNAME);
}
if let Some(string) = self.ca_cert.as_deref() {
config.ca_cert = string.as_ptr();
config.ca_cert_len = string.len() as u32;
config_options.insert(BackendConfigOptions::CA_CERT);
}
if let Some(string) = self.ciphers.as_deref() {
config.ciphers = string.as_ptr();
config.ciphers_len = string.len() as u32;
config_options.insert(BackendConfigOptions::CIPHERS);
}
if let Some(string) = self.sni_hostname.as_deref() {
config.sni_hostname = string.as_ptr();
config.sni_hostname_len = string.len() as u32;
config_options.insert(BackendConfigOptions::SNI_HOSTNAME);
}
if let Some((pem_cert, pem_key)) = self.client_certificate_info.as_ref() {
config.client_certificate = pem_cert.as_ptr();
config.client_certificate_len = pem_cert.len() as u32;
config.client_key = pem_key.underlying_handle();
config_options.insert(BackendConfigOptions::CLIENT_CERT);
}
if let Some(time) = self.http_keepalive_time_ms {
config.http_keepalive_time_ms = time;
config_options.insert(BackendConfigOptions::KEEPALIVE);
}
if let Some(intvl) = self.tcp_keepalive_interval_secs {
config.tcp_keepalive_interval_secs = intvl.get();
config_options.insert(BackendConfigOptions::KEEPALIVE);
}
if let Some(probes) = self.tcp_keepalive_probes {
config.tcp_keepalive_probes = probes.get();
config_options.insert(BackendConfigOptions::KEEPALIVE);
}
if let Some(time) = self.tcp_keepalive_time_secs {
config.tcp_keepalive_time_secs = time.get();
config_options.insert(BackendConfigOptions::KEEPALIVE);
}
if let Some(enable) = self.tcp_keepalive_enable {
config.tcp_keepalive_enable = u32::from(enable);
config_options.insert(BackendConfigOptions::KEEPALIVE);
} else if config_options.contains(BackendConfigOptions::KEEPALIVE) {
// If the other keepalive settings have been used, then default to
// requesting them.
config.tcp_keepalive_enable = u32::from(true);
}
if let Some(max_conns) = self.max_connections {
config.max_connections = max_conns;
config_options.insert(BackendConfigOptions::POOLING_LIMITS);
}
if let Some(max_use) = self.max_use {
config.max_use = max_use;
config_options.insert(BackendConfigOptions::POOLING_LIMITS);
}
if let Some(max_lifetime) = self.max_lifetime {
config.max_lifetime_ms = max_lifetime.as_millis().try_into().unwrap_or(u32::MAX);
config_options.insert(BackendConfigOptions::POOLING_LIMITS);
}
if !self.prefer_ipv6.unwrap_or(true) {
config_options.insert(BackendConfigOptions::PREFER_IPV4);
}
if !self.pool_connections {
config_options.insert(BackendConfigOptions::DONT_POOL);
}
if self.grpc {
config_options.insert(BackendConfigOptions::GRPC);
}
let basic_result = unsafe {
register_dynamic_backend(name, name_len, target, target_len, config_options, &config)
};
match basic_result {
FastlyStatus::OK => Ok(Backend { name: self.name }),
_ => {
// Keeping `self` live and intact to this point ensures we don't prematurely drop
// data that backs pointers passed into the registration hostcall. Note that it
// suffices to include this only in the non-OK branch, which allows the OK case the
// minor performance advantage of being able to move `self.name` rather than cloning
// it.
drop(self);
Err(BackendCreationError::from(basic_result))
}
}
}
}
impl GrpcBackend for BackendBuilder {
fn for_grpc(mut self, value: bool) -> Self {
self.grpc = value;
self
}
}