mod builder;
use crate::abi::{self, FastlyStatus};
pub use builder::*;
use fastly_shared::SslVersion;
use http::HeaderValue;
use std::{str::FromStr, time::Duration};
pub(crate) const MAX_BACKEND_NAME_LEN: usize = 255;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Backend {
name: String,
}
impl Backend {
pub fn from_name(s: &str) -> Result<Self, BackendError> {
s.parse()
}
#[doc = include_str!("../docs/snippets/dynamic-backend-builder.md")]
pub fn builder(name: impl ToString, target: impl ToString) -> BackendBuilder {
BackendBuilder::new(name.to_string(), target.to_string())
}
pub fn name(&self) -> &str {
self.name.as_str()
}
pub fn into_string(self) -> String {
self.name
}
pub fn exists(&self) -> bool {
let mut exists = 0;
unsafe { abi::fastly_backend::exists(self.name.as_ptr(), self.name.len(), &mut exists) }
.result()
.map(|_| exists == 1)
.expect("fastly_backend::exists failed")
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn is_dynamic(&self) -> bool {
let mut is = 0;
unsafe { abi::fastly_backend::is_dynamic(self.name.as_ptr(), self.name.len(), &mut is) }
.result()
.map(|_| is == 1)
.expect("fastly_backend::is_dynamic failed")
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_host(&self) -> String {
let mut nwritten = 0;
let length = match unsafe {
abi::fastly_backend::get_host(
self.name.as_ptr(),
self.name.len(),
std::ptr::null_mut(),
0,
&mut nwritten,
)
} {
FastlyStatus::BUFLEN => nwritten,
status => panic!("fastly_backend::get_host returned an unexpected result: {status:?}"),
};
let mut buf = Vec::with_capacity(length);
unsafe {
abi::fastly_backend::get_host(
self.name.as_ptr(),
self.name.len(),
buf.as_mut_ptr(),
buf.capacity(),
&mut nwritten,
)
}
.result()
.expect("fastly_backend::get_host returned an unexpected result");
assert!(
nwritten <= buf.capacity(),
"fastly_backend::get_host wrote too many bytes"
);
unsafe {
buf.set_len(nwritten);
}
String::from_utf8(buf).expect("fastly_backend::get_host returns valid UTF-8 bytes")
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_host_override(&self) -> Option<HeaderValue> {
let mut nwritten = 0;
let length = match unsafe {
abi::fastly_backend::get_override_host(
self.name.as_ptr(),
self.name.len(),
std::ptr::null_mut(),
0,
&mut nwritten,
)
} {
FastlyStatus::NONE => return None,
FastlyStatus::BUFLEN => nwritten,
status => panic!(
"fastly_backend::get_override_host returned an unexpected result: {status:?}"
),
};
let mut buf = Vec::with_capacity(length);
unsafe {
abi::fastly_backend::get_override_host(
self.name.as_ptr(),
self.name.len(),
buf.as_mut_ptr(),
buf.capacity(),
&mut nwritten,
)
}
.result()
.expect("fastly_backend::get_override_host returned an unexpected result");
assert!(
nwritten <= buf.capacity(),
"fastly_backend::get_override_host wrote too many bytes"
);
unsafe {
buf.set_len(nwritten);
}
Some(
HeaderValue::try_from(buf)
.expect("fastly_backend::get_override_host returns valid header value bytes"),
)
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_port(&self) -> u16 {
let mut port = 0;
unsafe { abi::fastly_backend::get_port(self.name.as_ptr(), self.name.len(), &mut port) }
.result()
.map(|_| port)
.expect("fastly_backend::get_port returned an unexpected result")
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_connect_timeout(&self) -> Duration {
let mut timeout = 0;
unsafe {
abi::fastly_backend::get_connect_timeout_ms(
self.name.as_ptr(),
self.name.len(),
&mut timeout,
)
}
.result()
.map(|_| timeout)
.map(u64::from)
.map(Duration::from_millis)
.expect("fastly_backend::get_connect_timeout_ms returned an unexpected result")
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_first_byte_timeout(&self) -> Duration {
let mut timeout = 0;
unsafe {
abi::fastly_backend::get_first_byte_timeout_ms(
self.name.as_ptr(),
self.name.len(),
&mut timeout,
)
}
.result()
.map(|_| timeout)
.map(u64::from)
.map(Duration::from_millis)
.expect("fastly_backend::get_first_byte_timeout returned an unexpected result")
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_between_bytes_timeout(&self) -> Duration {
let mut timeout = 0;
unsafe {
abi::fastly_backend::get_between_bytes_timeout_ms(
self.name.as_ptr(),
self.name.len(),
&mut timeout,
)
}
.result()
.map(|_| timeout)
.map(u64::from)
.map(Duration::from_millis)
.expect("fastly_backend::get_between_bytes_timeout returned an unexpected result")
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_http_keepalive_time(&self) -> Duration {
let mut timeout = 0;
unsafe {
abi::fastly_backend::get_http_keepalive_time(
self.name.as_ptr(),
self.name.len(),
&mut timeout,
)
}
.result()
.map(|_| timeout)
.map(u64::from)
.map(Duration::from_millis)
.expect("fastly_backend::get_http_keepalive_time returned an unexpected result")
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_tcp_keepalive_enable(&self) -> bool {
let mut enabled = 0;
unsafe {
abi::fastly_backend::get_tcp_keepalive_enable(
self.name.as_ptr(),
self.name.len(),
&mut enabled,
)
}
.result()
.map(|_| enabled != 0)
.expect("fastly_backend::get_tcp_keepalive_enable returned an unexpected result")
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_tcp_keepalive_interval(&self) -> Duration {
let mut timeout = 0;
unsafe {
abi::fastly_backend::get_tcp_keepalive_interval(
self.name.as_ptr(),
self.name.len(),
&mut timeout,
)
}
.result()
.map(|_| timeout)
.map(u64::from)
.map(Duration::from_secs)
.expect("fastly_backend::get_tcp_keepalive_interval returned an unexpected result")
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_tcp_keepalive_probes(&self) -> u32 {
let mut count = 0;
unsafe {
abi::fastly_backend::get_tcp_keepalive_probes(
self.name.as_ptr(),
self.name.len(),
&mut count,
)
}
.result()
.map(|_| count)
.expect("fastly_backend::get_tcp_keepalive_probes returned an unexpected result")
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_tcp_keepalive_time(&self) -> Duration {
let mut timeout = 0;
unsafe {
abi::fastly_backend::get_tcp_keepalive_time(
self.name.as_ptr(),
self.name.len(),
&mut timeout,
)
}
.result()
.map(|_| timeout)
.map(u64::from)
.map(Duration::from_secs)
.expect("fastly_backend::get_tcp_keepalive_time returned an unexpected result")
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn is_ssl(&self) -> bool {
let mut is = 0;
unsafe { abi::fastly_backend::is_ssl(self.name.as_ptr(), self.name.len(), &mut is) }
.result()
.map(|_| is == 1)
.expect("fastly_backend::is_ssl returned an unexpected result")
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_ssl_min_version(&self) -> Option<SslVersion> {
let mut min = 0;
match unsafe {
abi::fastly_backend::get_ssl_min_version(self.name.as_ptr(), self.name.len(), &mut min)
}
.result()
{
Ok(_) => Some(min.try_into().unwrap()),
Err(FastlyStatus::NONE) => None,
other => panic!(
"fastly_backend::get_ssl_min_version returned an unexpected result: {other:?}"
),
}
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_ssl_max_version(&self) -> Option<SslVersion> {
let mut max = 0;
match unsafe {
abi::fastly_backend::get_ssl_max_version(self.name.as_ptr(), self.name.len(), &mut max)
}
.result()
{
Ok(_) => Some(max.try_into().unwrap()),
Err(FastlyStatus::NONE) => None,
other => panic!(
"fastly_backend::get_ssl_max_version returned an unexpected result: {other:?}"
),
}
}
}
#[derive(Copy, Clone, Debug, thiserror::Error, PartialEq, Eq)]
pub enum BackendError {
#[error("an empty string is not a valid backend")]
EmptyName,
#[error("backend names must be <= 255 characters")]
TooLong,
#[error("backend names must only contain visible ASCII characters or spaces")]
InvalidName,
}
impl FromStr for Backend {
type Err = BackendError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
validate_backend(s)?;
Ok(Self { name: s.to_owned() })
}
}
impl std::fmt::Display for Backend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name.as_str())
}
}
pub fn validate_backend(backend: &str) -> Result<(), BackendError> {
if backend.is_empty() {
Err(BackendError::EmptyName)
} else if backend.len() > MAX_BACKEND_NAME_LEN {
Err(BackendError::TooLong)
} else if backend.chars().any(is_invalid_char) {
Err(BackendError::InvalidName)
} else {
Ok(())
}
}
#[inline]
fn is_invalid_char(c: char) -> bool {
c != ' ' && !c.is_ascii_graphic()
}
#[cfg(test)]
mod validate_backend_tests {
use super::*;
#[test]
fn valid_backend_names_are_accepted() {
let valid_backend_names = [
"valid_backend_1",
"1_backend_with_leading_integer",
"backend-with-kebab-case",
"backend with spaces",
"backend.with.periods",
"123_456_789_000",
"123.456.789.000",
"tilde~backend",
"(parens-backend)",
];
for backend in valid_backend_names.iter() {
match validate_backend(backend) {
Ok(_) => {}
x => panic!("backend string \"{backend}\" yielded unexpected result: {x:?}",),
}
}
}
#[test]
fn empty_str_is_not_accepted() {
let invalid_backend = "";
match validate_backend(invalid_backend) {
Err(BackendError::EmptyName) => {}
x => panic!("unexpected result: {x:?}"),
}
}
#[test]
fn name_equal_to_character_limit_is_accepted() {
use std::iter::FromIterator;
let invalid_backend: String = String::from_iter(vec!['a'; 255]);
match validate_backend(&invalid_backend) {
Ok(_) => {}
x => panic!("unexpected result: {x:?}"),
}
}
#[test]
fn name_longer_than_character_limit_are_not_accepted() {
use std::iter::FromIterator;
let invalid_backend: String = String::from_iter(vec!['a'; 256]);
match validate_backend(&invalid_backend) {
Err(BackendError::TooLong) => {}
x => panic!("unexpected result: {x:?}"),
}
}
#[test]
fn unprintable_characters_are_not_accepted() {
let invalid_backend = "\n";
match validate_backend(invalid_backend) {
Err(BackendError::InvalidName) => {}
x => panic!("unexpected result: {x:?}"),
}
}
#[test]
fn unicode_is_not_accepted() {
let invalid_backend = "♓";
match validate_backend(invalid_backend) {
Err(BackendError::InvalidName) => {}
x => panic!("unexpected result: {x:?}"),
}
}
}