mod builder;
#[cfg(target_env = "p1")]
use crate::abi::{self, FastlyStatus};
pub use builder::*;
use fastly_shared::SslVersion;
#[cfg(not(target_env = "p1"))]
use fastly_sys::service0_1_0::fastly::compute as wit;
use http::HeaderValue;
use std::{str::FromStr, time::Duration};
pub(crate) const MAX_BACKEND_NAME_LEN: usize = 255;
#[derive(Debug)]
#[cfg_attr(target_env = "p1", derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd))]
pub struct Backend {
name: String,
#[cfg(not(target_env = "p1"))]
handle: wit::backend::Backend,
}
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
}
#[cfg(not(target_env = "p1"))]
pub fn as_handle(&self) -> &wit::backend::Backend {
&self.handle
}
#[cfg(not(target_env = "p1"))]
pub fn from_handle(handle: wit::backend::Backend) -> Self {
Self {
name: handle.get_name(),
handle,
}
}
pub fn exists(&self) -> bool {
#[cfg(target_env = "p1")]
{
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")
}
#[cfg(not(target_env = "p1"))]
{
true
}
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn is_dynamic(&self) -> bool {
#[cfg(target_env = "p1")]
{
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")
}
#[cfg(not(target_env = "p1"))]
{
self.handle
.is_dynamic()
.expect("fastly:compute/backend.[method]backend.is-dynamic failed")
}
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_host(&self) -> String {
#[cfg(target_env = "p1")]
{
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")
}
#[cfg(not(target_env = "p1"))]
{
self.handle.get_host(u64::MAX).expect(
"fastly:compute/backend.[method]backend.get-host returned an unexpected result",
)
}
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_host_override(&self) -> Option<HeaderValue> {
#[cfg(target_env = "p1")]
{
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"),
)
}
#[cfg(not(target_env = "p1"))]
{
self.handle.get_override_host(u64::MAX)
.expect("fastly:compute/backend.[method]backend.get-override-host returned an unexpected result")
.map(|override_host| HeaderValue::try_from(override_host)
.expect("fastly:compute/backend.[method]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 {
#[cfg(target_env = "p1")]
{
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")
}
#[cfg(not(target_env = "p1"))]
{
self.handle.get_port().expect(
"fastly:compute/backend.[method]backend.get-port returned an unexpected result",
)
}
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_connect_timeout(&self) -> Duration {
#[cfg(target_env = "p1")]
{
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")
}
#[cfg(not(target_env = "p1"))]
{
Duration::from_millis(u64::from(self.handle.get_connect_timeout_ms()
.expect("fastly:compute/backend.[method]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 {
#[cfg(target_env = "p1")]
{
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")
}
#[cfg(not(target_env = "p1"))]
{
Duration::from_millis(u64::from(self.handle.get_first_byte_timeout_ms()
.expect("fastly:compute/backend.[method]backend.get-first-byte-timeout-ms returned an unexpected result")))
}
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_between_bytes_timeout(&self) -> Duration {
#[cfg(target_env = "p1")]
{
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")
}
#[cfg(not(target_env = "p1"))]
{
Duration::from_millis(u64::from(self.handle.get_between_bytes_timeout_ms()
.expect("fastly:compute/backend.[method]backend.get-between-bytes-timeout-ms returned an unexpected result")))
}
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_http_keepalive_time(&self) -> Duration {
#[cfg(target_env = "p1")]
{
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")
}
#[cfg(not(target_env = "p1"))]
{
Duration::from_millis(u64::from(self.handle.get_tcp_keepalive_time()
.expect("fastly:compute/backend.[method]backend.get-tcp-keepalive-time returned an unexpected result")))
}
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_tcp_keepalive_enable(&self) -> bool {
#[cfg(target_env = "p1")]
{
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")
}
#[cfg(not(target_env = "p1"))]
{
self.handle.get_tcp_keepalive_enable()
.expect("fastly:compute/backend.[method]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 {
#[cfg(target_env = "p1")]
{
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")
}
#[cfg(not(target_env = "p1"))]
{
Duration::from_secs(u64::from(self.handle.get_tcp_keepalive_interval()
.expect("fastly:compute/backend.[method]backend.get-tcp-keepalive-internal returned an unexpected result")))
}
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_tcp_keepalive_probes(&self) -> u32 {
#[cfg(target_env = "p1")]
{
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")
}
#[cfg(not(target_env = "p1"))]
{
self.handle.get_tcp_keepalive_probes()
.expect("fastly:compute/backend.[method]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 {
#[cfg(target_env = "p1")]
{
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")
}
#[cfg(not(target_env = "p1"))]
{
Duration::from_secs(u64::from(self.handle.get_tcp_keepalive_time()
.expect("fastly:compute/backend.[method]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 {
#[cfg(target_env = "p1")]
{
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")
}
#[cfg(not(target_env = "p1"))]
{
self.handle.is_tls().expect(
"fastly:compute/backend.[method]backend.is-tls returned an unexpected result",
)
}
}
#[doc = include_str!("../docs/snippets/panics-backend-must-exist.md")]
pub fn get_ssl_min_version(&self) -> Option<SslVersion> {
#[cfg(target_env = "p1")]
{
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!("backend::get_tls_min_version returned an unexpected result: {other:?}")
}
}
}
#[cfg(not(target_env = "p1"))]
{
match self.handle.get_tls_min_version() {
Ok(Some(0x0301)) => Some(SslVersion::TLS1),
Ok(Some(0x0302)) => Some(SslVersion::TLS1_1),
Ok(Some(0x0303)) => Some(SslVersion::TLS1_2),
Ok(Some(0x0304)) => Some(SslVersion::TLS1_3),
Ok(None) => None,
other => panic!(
"fastly:compute/backend.[method]backend.get-tls-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> {
#[cfg(target_env = "p1")]
{
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:?}"
),
}
}
#[cfg(not(target_env = "p1"))]
{
match self.handle.get_tls_max_version() {
Ok(Some(0x0301)) => Some(SslVersion::TLS1),
Ok(Some(0x0302)) => Some(SslVersion::TLS1_1),
Ok(Some(0x0303)) => Some(SslVersion::TLS1_2),
Ok(Some(0x0304)) => Some(SslVersion::TLS1_3),
Ok(None) => None,
other => panic!(
"fastly:compute/backend.[method]backend.get-tls-max-version returned an unexpected result: {other:?}"
)
}
}
}
}
#[cfg(not(target_env = "p1"))]
impl Clone for Backend {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
handle: wit::backend::Backend::open(&self.name).unwrap(),
}
}
}
#[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)?;
#[cfg(target_env = "p1")]
{
Ok(Self { name: s.to_owned() })
}
#[cfg(not(target_env = "p1"))]
{
let handle = wit::backend::Backend::open(s).map_err(|err| match err {
wit::types::OpenError::NameTooLong => BackendError::TooLong,
_ => BackendError::InvalidName,
})?;
Ok(Self {
name: s.to_owned(),
handle,
})
}
}
}
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:?}"),
}
}
}