Skip to content

Commit

Permalink
feat: icap rfc3507 init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
NOOMA-42 committed Dec 29, 2024
1 parent c0f2d1e commit 07316cc
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 0 deletions.
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ members = [
"rama-http-core/tests/h2-fuzz",
"rama-http-core/tests/h2-support",
"rama-http-types",
"rama-icap",
"rama-macros",
"rama-net",
"rama-proxy",
Expand Down
16 changes: 16 additions & 0 deletions rama-icap/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "rama-icap"
version = "0.1.0"
edition = "2021"
description = "ICAP (Internet Content Adaptation Protocol) support for Rama"
license = "MIT OR Apache-2.0"
repository = "https://github.com/rama-net/rama"

[dependencies]
rama-core = { path = "../rama-core", version = "0.2.0-alpha.4" }
rama-error = { path = "../rama-error", version = "0.2.0-alpha.4" }
rama-net = { path = "../rama-net", version = "0.2.0-alpha.4" }
bytes = "1.5"
tokio = { version = "1.35", features = ["full"] }
http = "1.0"
thiserror = "1.0"
223 changes: 223 additions & 0 deletions rama-icap/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
//! ICAP (Internet Content Adaptation Protocol) implementation according to RFC 3507.
//!
//! This module provides support for the ICAP protocol, which allows HTTP messages to be
//! adapted/transformed by an ICAP server.
use std::collections::HashMap;
use thiserror::Error;

/// Default ICAP port as specified in RFC 3507
pub const DEFAULT_ICAP_PORT: u16 = 1344;

/// ICAP version string
pub const ICAP_VERSION: &str = "ICAP/1.0";

/// ICAP methods as defined in RFC 3507
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Method {
/// REQMOD - Request modification
ReqMod,
/// RESPMOD - Response modification
RespMod,
/// OPTIONS - Get server options
Options,
}

impl std::fmt::Display for Method {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Method::ReqMod => write!(f, "REQMOD"),
Method::RespMod => write!(f, "RESPMOD"),
Method::Options => write!(f, "OPTIONS"),
}
}
}

/// Section types that can appear in an ICAP message
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SectionType {
/// Request headers
ReqHdr,
/// Request body
ReqBody,
/// Response headers
ResHdr,
/// Response body
ResBody,
/// Options body
OptBody,
/// Null body
NullBody,
}

impl std::fmt::Display for SectionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SectionType::ReqHdr => write!(f, "req-hdr"),
SectionType::ReqBody => write!(f, "req-body"),
SectionType::ResHdr => write!(f, "res-hdr"),
SectionType::ResBody => write!(f, "res-body"),
SectionType::OptBody => write!(f, "opt-body"),
SectionType::NullBody => write!(f, "null-body"),
}
}
}

/// ICAP-specific errors
#[derive(Error, Debug)]
pub enum Error {
#[error("Invalid ICAP URI")]
InvalidUri,
#[error("Invalid encapsulated header format")]
InvalidEncapsulatedHeader,
#[error("Missing required section in encapsulated header")]
MissingRequiredSection,
#[error("Invalid section order in encapsulated header")]
InvalidSectionOrder,
#[error("Invalid ICAP version")]
InvalidVersion,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}

/// Represents an ICAP message (either request or response)
#[derive(Debug)]
pub struct Message {
/// Headers specific to ICAP
pub headers: HashMap<String, String>,
/// Sections contained in the message body
pub sections: HashMap<SectionType, Vec<u8>>,
}

impl Message {
/// Create a new empty ICAP message
pub fn new() -> Self {
Message {
headers: HashMap::new(),
sections: HashMap::new(),
}
}

/// Add a header to the message
pub fn add_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
self.headers.insert(name.into(), value.into());
}

/// Add a section to the message
pub fn add_section(&mut self, section_type: SectionType, content: Vec<u8>) {
self.sections.insert(section_type, content);
}
}

impl Default for Message {
fn default() -> Self {
Self::new()
}
}

#[derive(Debug, Default)]
pub struct Request<'a> {
/// The request method (REQMOD, RESPMOD, OPTIONS)
pub method: Option<&'a str>,
/// The request path/URI
pub path: Option<&'a str>,
/// ICAP version (as a number, 0 = 1.0)
pub version: Option<u8>,
/// Headers included in the request
pub headers: Vec<Header<'a>>,
/// Parsed encapsulated sections
pub encapsulated_sections: Option<HashMap<SectionType, Vec<u8>>>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Header<'a> {
/// Header name
pub name: &'a str,
/// Header value
pub value: &'a [u8],
}

/// Empty header constant for initialization
pub const EMPTY_HEADER: Header<'static> = Header { name: "", value: b"" };

impl<'a> Request<'a> {
/// Create a new Request with a pre-allocated headers array
pub fn new(headers: &'a mut [Header<'a>]) -> Request<'a> {
Request {
method: None,
path: None,
version: None,
headers: Vec::new(),
encapsulated_sections: None,
}
}

/// Parse an ICAP request from a byte slice
pub fn parse(&mut self, buf: &'a [u8]) -> std::result::Result<Status<usize>, Error> {
// Basic implementation for testing
if buf.len() < 5 {
return Ok(Status::Partial);
}

// Simple parsing for testing purposes
let mut parts = buf.split(|&b| b == b' ');

// Parse method
if let Some(method) = parts.next() {
self.method = std::str::from_utf8(method).ok();
}

// Parse path
if let Some(path) = parts.next() {
self.path = std::str::from_utf8(path).ok();
}

// Parse version
if let Some(version) = parts.next() {
if version.starts_with(b"ICAP/1.") {
self.version = Some(0);
}
}

Ok(Status::Complete(buf.len()))
}
}

/// Response status
#[derive(Debug)]
pub enum Status<T> {
/// Represents a complete result
Complete(T),
/// Represents a partial result, needing more data
Partial,
}

#[cfg(test)]
mod tests {
use super::*;

const NUM_OF_HEADERS: usize = 4;

#[test]
fn test_request_simple() {
let mut headers = vec![EMPTY_HEADER; NUM_OF_HEADERS];
let mut req = Request::new(&mut headers);
let buf = b"OPTIONS / ICAP/1.0\r\nEncapsulated:null-body=0\r\n\r\n";
let status = req.parse(buf);

assert!(matches!(status, Ok(Status::Complete(_))));
assert_eq!(req.method, Some("OPTIONS"));
assert_eq!(req.path, Some("/"));
assert_eq!(req.version, Some(0));
}

#[test]
fn test_request_partial() {
let mut headers = vec![EMPTY_HEADER; NUM_OF_HEADERS];
let mut req = Request::new(&mut headers);
let buf = b"RESP";
let status = req.parse(buf);

assert!(matches!(status, Ok(Status::Partial)));
}
}

0 comments on commit 07316cc

Please sign in to comment.