-
-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
253 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))); | ||
} | ||
} |