use crate::BootTarget;
pub const PROTOCOL_VERSION_PATH: &str = "/redfish";
pub const SERVICE_ROOT_PATH: &str = "/redfish/v1/";
pub const SERVICE_ROOT_PATH_BARE: &str = "/redfish/v1";
pub const SYSTEMS_PATH: &str = "/redfish/v1/Systems";
pub fn system_path(system_id: &str) -> String {
format!("{SYSTEMS_PATH}/{system_id}")
}
pub fn virtual_media_collection_path(system_id: &str) -> String {
format!("{}/VirtualMedia", system_path(system_id))
}
pub fn virtual_media_path(system_id: &str, slot: &str) -> String {
format!("{}/{slot}", virtual_media_collection_path(system_id))
}
pub fn virtual_media_action_path(system_id: &str, slot: &str, action: &str) -> String {
format!(
"{}/Actions/VirtualMedia.{action}",
virtual_media_path(system_id, slot)
)
}
pub fn reset_path(system_id: &str) -> String {
format!("{}/Actions/ComputerSystem.Reset", system_path(system_id))
}
pub const SESSIONS_PATH: &str = "/redfish/v1/SessionService/Sessions";
pub const INSERT_MEDIA: &str = "InsertMedia";
pub const EJECT_MEDIA: &str = "EjectMedia";
pub const ACTION_RESET: &str = "#ComputerSystem.Reset";
pub const ACTION_INSERT_MEDIA: &str = "#VirtualMedia.InsertMedia";
pub const ACTION_EJECT_MEDIA: &str = "#VirtualMedia.EjectMedia";
pub mod prop {
pub const IMAGE: &str = "Image";
pub const INSERTED: &str = "Inserted";
pub const WRITE_PROTECTED: &str = "WriteProtected";
pub const IMAGE_NAME: &str = "ImageName";
pub const BOOT: &str = "Boot";
pub const BOOT_SOURCE_OVERRIDE_ENABLED: &str = "BootSourceOverrideEnabled";
pub const BOOT_SOURCE_OVERRIDE_TARGET: &str = "BootSourceOverrideTarget";
pub const BOOT_SOURCE_OVERRIDE_MODE: &str = "BootSourceOverrideMode";
pub const RESET_TYPE: &str = "ResetType";
pub const POWER_STATE: &str = "PowerState";
pub const ALLOWABLE_VALUES_SUFFIX: &str = "@Redfish.AllowableValues";
pub const TARGET: &str = "target";
}
pub fn allowable_values_key(name: &str) -> String {
format!("{name}{}", prop::ALLOWABLE_VALUES_SUFFIX)
}
pub const OVERRIDE_ONCE: &str = "Once";
pub const OVERRIDE_DISABLED: &str = "Disabled";
pub const OVERRIDE_CONTINUOUS: &str = "Continuous";
pub const OVERRIDE_ENABLED_ALLOWABLE: &[&str] =
&[OVERRIDE_DISABLED, OVERRIDE_ONCE, OVERRIDE_CONTINUOUS];
pub const RESET_ON: &str = "On";
pub const RESET_FORCE_ON: &str = "ForceOn";
pub const RESET_FORCE_OFF: &str = "ForceOff";
pub const RESET_GRACEFUL_SHUTDOWN: &str = "GracefulShutdown";
pub const RESET_FORCE_RESTART: &str = "ForceRestart";
pub const RESET_GRACEFUL_RESTART: &str = "GracefulRestart";
pub const RESET_TYPE_ALLOWABLE: &[&str] = &[
RESET_ON,
RESET_FORCE_ON,
RESET_FORCE_OFF,
RESET_GRACEFUL_SHUTDOWN,
RESET_FORCE_RESTART,
RESET_GRACEFUL_RESTART,
];
pub fn is_allowable_reset_type(reset_type: &str) -> bool {
RESET_TYPE_ALLOWABLE.contains(&reset_type)
}
pub const BOOT_TARGET_ALLOWABLE: &[&str] = &["None", "Pxe", "Cd", "Hdd", "BiosSetup"];
pub const BOOT_TARGET_NONE: &str = "None";
pub fn target_str(target: BootTarget) -> &'static str {
match target {
BootTarget::Cd => "Cd",
BootTarget::Pxe => "Pxe",
BootTarget::Hdd => "Hdd",
BootTarget::BiosSetup => "BiosSetup",
}
}
pub fn target_from_str(s: &str) -> Option<BootTarget> {
match s {
"Cd" => Some(BootTarget::Cd),
"Pxe" => Some(BootTarget::Pxe),
"Hdd" => Some(BootTarget::Hdd),
"BiosSetup" => Some(BootTarget::BiosSetup),
_ => None,
}
}
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
pub fn base64_encode(input: &[u8]) -> String {
let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
for chunk in input.chunks(3) {
let b0 = chunk[0];
let b1 = *chunk.get(1).unwrap_or(&0);
let b2 = *chunk.get(2).unwrap_or(&0);
let n = ((b0 as u32) << 16) | ((b1 as u32) << 8) | (b2 as u32);
out.push(B64[((n >> 18) & 63) as usize] as char);
out.push(B64[((n >> 12) & 63) as usize] as char);
out.push(if chunk.len() > 1 {
B64[((n >> 6) & 63) as usize] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
B64[(n & 63) as usize] as char
} else {
'='
});
}
out
}
pub fn base64_decode(input: &str) -> Option<Vec<u8>> {
let bytes = input.as_bytes();
if bytes.len() % 4 != 0 {
return None;
}
let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
for chunk in bytes.chunks(4) {
let mut n: u32 = 0;
let mut pad = 0usize;
for (i, &c) in chunk.iter().enumerate() {
let v = if c == b'=' {
if i < 2 {
return None;
}
pad += 1;
0
} else {
if pad > 0 {
return None;
}
B64.iter().position(|&t| t == c)? as u32
};
n |= v << (18 - 6 * i);
}
out.push((n >> 16) as u8);
if pad < 2 {
out.push((n >> 8) as u8);
}
if pad < 1 {
out.push(n as u8);
}
}
Some(out)
}
pub fn basic_auth_header(username: &str, password: &str) -> String {
format!("Basic {}", base64_encode(format!("{username}:{password}").as_bytes()))
}
pub fn parse_basic_auth(header: &str) -> Option<(String, String)> {
let rest = header.strip_prefix("Basic ").or_else(|| {
let (scheme, rest) = header.split_once(' ')?;
scheme.eq_ignore_ascii_case("basic").then_some(rest)
})?;
let decoded = base64_decode(rest.trim())?;
let text = String::from_utf8(decoded).ok()?;
let (user, pass) = text.split_once(':')?;
Some((user.to_string(), pass.to_string()))
}
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
pub fn insert_media_body(iso: &str) -> serde_json::Value {
serde_json::json!({
prop::IMAGE: iso,
prop::INSERTED: true,
prop::WRITE_PROTECTED: true,
})
}
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InsertMediaRequest {
pub image: String,
pub inserted: bool,
pub write_protected: bool,
}
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
pub fn read_insert_media_body(v: &serde_json::Value) -> Result<InsertMediaRequest, &'static str> {
let image = v
.get(prop::IMAGE)
.and_then(|i| i.as_str())
.ok_or(prop::IMAGE)?;
Ok(InsertMediaRequest {
image: image.to_string(),
inserted: v
.get(prop::INSERTED)
.and_then(|i| i.as_bool())
.unwrap_or(true),
write_protected: v
.get(prop::WRITE_PROTECTED)
.and_then(|i| i.as_bool())
.unwrap_or(true),
})
}
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
pub fn boot_override_body(target: BootTarget) -> serde_json::Value {
serde_json::json!({
prop::BOOT: {
prop::BOOT_SOURCE_OVERRIDE_ENABLED: OVERRIDE_ONCE,
prop::BOOT_SOURCE_OVERRIDE_TARGET: target_str(target),
}
})
}
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct BootOverrideRequest {
pub target: Option<String>,
pub enabled: Option<String>,
}
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
pub fn read_boot_override_body(v: &serde_json::Value) -> Option<BootOverrideRequest> {
let boot = v.get(prop::BOOT)?;
Some(BootOverrideRequest {
target: boot
.get(prop::BOOT_SOURCE_OVERRIDE_TARGET)
.and_then(|t| t.as_str())
.map(str::to_string),
enabled: boot
.get(prop::BOOT_SOURCE_OVERRIDE_ENABLED)
.and_then(|t| t.as_str())
.map(str::to_string),
})
}
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
pub fn reset_body(reset_type: &str) -> serde_json::Value {
serde_json::json!({ prop::RESET_TYPE: reset_type })
}
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
pub fn read_reset_body(v: &serde_json::Value) -> Result<String, &'static str> {
v.get(prop::RESET_TYPE)
.and_then(|r| r.as_str())
.map(str::to_string)
.ok_or(prop::RESET_TYPE)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resource_paths_are_the_dmtf_shapes() {
assert_eq!(system_path("System.Embedded.1"),
"/redfish/v1/Systems/System.Embedded.1");
assert_eq!(virtual_media_collection_path("437XR1138R2"),
"/redfish/v1/Systems/437XR1138R2/VirtualMedia");
assert_eq!(virtual_media_path("437XR1138R2", "CD1"),
"/redfish/v1/Systems/437XR1138R2/VirtualMedia/CD1");
assert_eq!(
virtual_media_action_path("437XR1138R2", "CD1", INSERT_MEDIA),
"/redfish/v1/Systems/437XR1138R2/VirtualMedia/CD1/Actions/VirtualMedia.InsertMedia"
);
assert_eq!(
reset_path("437XR1138R2"),
"/redfish/v1/Systems/437XR1138R2/Actions/ComputerSystem.Reset"
);
}
#[test]
fn the_paths_nest_the_way_the_odata_graph_does() {
let sys = system_path("s1");
assert!(sys.starts_with(SYSTEMS_PATH));
let vmc = virtual_media_collection_path("s1");
assert!(vmc.starts_with(&sys), "{vmc} under {sys}");
let vm = virtual_media_path("s1", "CD");
assert!(vm.starts_with(&vmc));
assert!(virtual_media_action_path("s1", "CD", INSERT_MEDIA).starts_with(&vm));
assert!(reset_path("s1").starts_with(&sys));
}
#[test]
fn boot_targets_round_trip_through_their_redfish_tokens() {
for t in [
BootTarget::Cd,
BootTarget::Pxe,
BootTarget::Hdd,
BootTarget::BiosSetup,
] {
assert_eq!(target_from_str(target_str(t)), Some(t), "{t:?}");
assert!(
BOOT_TARGET_ALLOWABLE.contains(&target_str(t)),
"{t:?} is advertised as allowable"
);
}
assert_eq!(target_from_str("Usb"), None);
assert_eq!(target_from_str(BOOT_TARGET_NONE), None);
assert_eq!(target_from_str("cd"), None, "the tokens are case-sensitive");
}
#[test]
fn the_reset_types_the_client_sends_are_the_ones_the_server_advertises() {
assert!(is_allowable_reset_type(RESET_ON));
assert!(is_allowable_reset_type(RESET_FORCE_OFF));
assert!(!is_allowable_reset_type("PowerOn"), "a plausible non-token");
assert!(!is_allowable_reset_type("on"), "case-sensitive");
assert!(!is_allowable_reset_type(""));
}
#[test]
fn allowable_values_annotation_key_is_the_odata_spelling() {
assert_eq!(
allowable_values_key(prop::RESET_TYPE),
"ResetType@Redfish.AllowableValues"
);
}
#[test]
fn base64_matches_known_vectors_and_round_trips() {
for (raw, enc) in [
(&b""[..], ""),
(&b"f"[..], "Zg=="),
(&b"fo"[..], "Zm8="),
(&b"foo"[..], "Zm9v"),
(&b"foob"[..], "Zm9vYg=="),
(&b"admin:secret"[..], "YWRtaW46c2VjcmV0"),
] {
assert_eq!(base64_encode(raw), enc);
assert_eq!(base64_decode(enc).as_deref(), Some(raw), "decode {enc}");
}
let all: Vec<u8> = (0u8..=255).collect();
assert_eq!(base64_decode(&base64_encode(&all)).unwrap(), all);
}
#[test]
fn base64_decode_refuses_junk_instead_of_skipping_it() {
assert_eq!(base64_decode("Zm9v!"), None, "bad length");
assert_eq!(base64_decode("Zm9 v"), None, "space is not in the alphabet");
assert_eq!(base64_decode("Z==="), None, "padding cannot start at index 1");
assert_eq!(base64_decode("Zm=v"), None, "padding cannot precede data");
assert_eq!(base64_decode("Zg=/"), None);
}
#[test]
fn basic_auth_round_trips_between_the_client_and_the_server_halves() {
let h = basic_auth_header("admin", "hunter2:with:colons");
assert_eq!(h, "Basic YWRtaW46aHVudGVyMjp3aXRoOmNvbG9ucw==");
assert_eq!(
parse_basic_auth(&h),
Some(("admin".into(), "hunter2:with:colons".into()))
);
assert_eq!(
parse_basic_auth("basic YWRtaW46c2VjcmV0"),
Some(("admin".into(), "secret".into()))
);
assert_eq!(parse_basic_auth("Bearer abc"), None);
assert_eq!(parse_basic_auth("Basic !!!!"), None);
assert_eq!(parse_basic_auth(&format!("Basic {}", base64_encode(b"admin"))), None);
}
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
mod bodies {
use super::*;
#[test]
fn every_action_body_the_client_writes_is_read_back_by_the_server_half() {
let insert = read_insert_media_body(&insert_media_body("https://d/x.iso")).unwrap();
assert_eq!(insert.image, "https://d/x.iso");
assert!(insert.inserted);
assert!(insert.write_protected, "the install medium is read-only");
let ovr = read_boot_override_body(&boot_override_body(BootTarget::Cd)).unwrap();
assert_eq!(ovr.target.as_deref(), Some("Cd"));
assert_eq!(ovr.enabled.as_deref(), Some(OVERRIDE_ONCE));
assert_eq!(read_reset_body(&reset_body(RESET_ON)).unwrap(), RESET_ON);
}
#[test]
fn a_body_missing_its_required_parameter_names_the_parameter() {
let e = read_insert_media_body(&serde_json::json!({ "Inserted": true })).unwrap_err();
assert_eq!(e, prop::IMAGE);
let e = read_reset_body(&serde_json::json!({})).unwrap_err();
assert_eq!(e, prop::RESET_TYPE);
assert!(read_insert_media_body(&serde_json::json!({ "Image": 7 })).is_err());
}
#[test]
fn insert_media_defaults_match_the_dmtf_parameter_defaults() {
let r = read_insert_media_body(&serde_json::json!({ "Image": "u" })).unwrap();
assert!(r.inserted && r.write_protected);
let r = read_insert_media_body(
&serde_json::json!({ "Image": "u", "Inserted": false, "WriteProtected": false }),
)
.unwrap();
assert!(!r.inserted && !r.write_protected);
}
#[test]
fn a_patch_with_no_boot_object_is_read_as_no_override_not_as_an_error() {
assert_eq!(read_boot_override_body(&serde_json::json!({})), None);
let partial = read_boot_override_body(
&serde_json::json!({ "Boot": { "BootSourceOverrideTarget": "Hdd" } }),
)
.unwrap();
assert_eq!(partial.target.as_deref(), Some("Hdd"));
assert_eq!(partial.enabled, None);
}
}
}