fastly/backend/
builder.rs

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