mirror of
https://codeberg.org/icewind/rfc7239.git
synced 2026-06-03 16:44:10 +02:00
initial implementation
This commit is contained in:
parent
5b886f3466
commit
e7dbbd847c
4 changed files with 469 additions and 7 deletions
33
.github/workflows/rust.yml
vendored
Normal file
33
.github/workflows/rust.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
- uses: Swatinem/rust-cache@v1
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: check
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Tests
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
- uses: Swatinem/rust-cache@v1
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
|
@ -3,7 +3,10 @@ name = "rfc7239"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Robin Appelman <robin@icewind.nl>"]
|
authors = ["Robin Appelman <robin@icewind.nl>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
description = "Parser for rfc7239 formatted Forwarded headers"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
license = "MIT OR Apache-2.0"
|
||||||
|
repository = "https://github.com/icewind1991/rfc7239"
|
||||||
|
documentation = "https://docs.rs/rfc7239"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
uncased = "0.9.3"
|
||||||
21
README.md
Normal file
21
README.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# rfc7239
|
||||||
|
|
||||||
|
Parser for [rfc7239] formatted `Forwarded` headers.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use rfc7239::parse;
|
||||||
|
|
||||||
|
// get the header value from your favorite http server library
|
||||||
|
let header_value = "for=192.0.2.60;proto=http;by=203.0.113.43,for=192.168.10.10";
|
||||||
|
|
||||||
|
for node_result in parse(header_value) {
|
||||||
|
let node = node_result?;
|
||||||
|
if let Some(forwarded_for) = node.forwarded_for {
|
||||||
|
println!("Forwarded by {}", forwarded_for)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[rfc7239]: https://tools.ietf.org/html/rfc7239
|
||||||
413
src/lib.rs
413
src/lib.rs
|
|
@ -1,7 +1,412 @@
|
||||||
#[cfg(test)]
|
//! Parser for [rfc7239] formatted `Forwarded` headers.
|
||||||
mod tests {
|
//!
|
||||||
|
//! ## Usage
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use rfc7239::parse;
|
||||||
|
//! # use std::error::Error;
|
||||||
|
//!
|
||||||
|
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
//! // get the header value from your favorite http server library
|
||||||
|
//! let header_value = "for=192.0.2.60;proto=http;by=203.0.113.43,for=192.168.10.10";
|
||||||
|
//!
|
||||||
|
//! for node_result in parse(header_value) {
|
||||||
|
//! let node = node_result?;
|
||||||
|
//! if let Some(forwarded_for) = node.forwarded_for {
|
||||||
|
//! println!("Forwarded by {}", forwarded_for)
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! [rfc7239]: https://tools.ietf.org/html/rfc7239
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use uncased::UncasedStr;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum RfcError {
|
||||||
|
InvalidIdentifier,
|
||||||
|
InvalidPort,
|
||||||
|
UnknownParameter,
|
||||||
|
MalformedParameter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for RfcError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let str = match self {
|
||||||
|
RfcError::InvalidIdentifier => "Invalid node identifier",
|
||||||
|
RfcError::InvalidPort => "Invalid node port",
|
||||||
|
RfcError::UnknownParameter => "Unknown parameter name",
|
||||||
|
RfcError::MalformedParameter => "Parameter doesn't consist of key value pair",
|
||||||
|
};
|
||||||
|
write!(f, "{}", str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for RfcError {}
|
||||||
|
|
||||||
|
/// Parse an rfc7239 header value into a list of forwarded nodes
|
||||||
|
pub fn parse(header_value: &str) -> impl Iterator<Item = Result<Forwarded, RfcError>> {
|
||||||
|
header_value.split(',').map(str::trim).map(Forwarded::parse)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_works() {
|
fn test_parse() {
|
||||||
assert_eq!(2 + 2, 4);
|
assert_eq!(
|
||||||
|
parse("for=192.0.2.60;proto=http;by=203.0.113.43,for=192.168.10.10")
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.unwrap(),
|
||||||
|
vec![
|
||||||
|
Forwarded {
|
||||||
|
forwarded_for: Some(NodeIdentifier::parse("192.0.2.60").unwrap()),
|
||||||
|
forwarded_by: Some(NodeIdentifier::parse("203.0.113.43").unwrap()),
|
||||||
|
protocol: Some("http"),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Forwarded {
|
||||||
|
forwarded_for: Some(NodeIdentifier::parse("192.168.10.10").unwrap()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq)]
|
||||||
|
pub struct Forwarded<'a> {
|
||||||
|
pub forwarded_for: Option<NodeIdentifier<'a>>,
|
||||||
|
pub forwarded_by: Option<NodeIdentifier<'a>>,
|
||||||
|
pub host: Option<&'a str>,
|
||||||
|
pub protocol: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Forwarded<'a> {
|
||||||
|
fn parse(forward: &'a str) -> Result<Self, RfcError> {
|
||||||
|
let mut result = Forwarded::default();
|
||||||
|
|
||||||
|
let parts = forward.split(';');
|
||||||
|
|
||||||
|
for part in parts {
|
||||||
|
if let Some(i) = part.find('=') {
|
||||||
|
let param = UncasedStr::new(&part[..i]);
|
||||||
|
let value = &part[i + 1..];
|
||||||
|
if param == "by" {
|
||||||
|
result.forwarded_by = Some(NodeIdentifier::parse(value.trim_matches('"'))?);
|
||||||
|
}
|
||||||
|
if param == "for" {
|
||||||
|
result.forwarded_for = Some(NodeIdentifier::parse(value.trim_matches('"'))?);
|
||||||
|
}
|
||||||
|
if param == "host" {
|
||||||
|
result.host = Some(value);
|
||||||
|
}
|
||||||
|
if param == "proto" {
|
||||||
|
result.protocol = Some(value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(RfcError::MalformedParameter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for Forwarded<'a> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut needs_delim = false;
|
||||||
|
if let Some(ident) = &self.forwarded_for {
|
||||||
|
if ident.display_needs_quote() {
|
||||||
|
write!(f, "for=\"{}\"", ident)?;
|
||||||
|
} else {
|
||||||
|
write!(f, "for={}", ident)?;
|
||||||
|
}
|
||||||
|
needs_delim = true;
|
||||||
|
}
|
||||||
|
if let Some(ident) = &self.forwarded_by {
|
||||||
|
if needs_delim {
|
||||||
|
write!(f, ";")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ident.display_needs_quote() {
|
||||||
|
write!(f, "by=\"{}\"", ident)?;
|
||||||
|
} else {
|
||||||
|
write!(f, "by={}", ident)?;
|
||||||
|
}
|
||||||
|
needs_delim = true;
|
||||||
|
}
|
||||||
|
if let Some(ident) = &self.host {
|
||||||
|
if needs_delim {
|
||||||
|
write!(f, ";")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "host={}", ident)?;
|
||||||
|
needs_delim = true;
|
||||||
|
}
|
||||||
|
if let Some(ident) = &self.protocol {
|
||||||
|
if needs_delim {
|
||||||
|
write!(f, ";")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "proto={}", ident)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_forwarded() {
|
||||||
|
assert_eq!(
|
||||||
|
Forwarded {
|
||||||
|
forwarded_for: Some(NodeIdentifier::parse("1.2.3.4").unwrap()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Forwarded::parse("for=1.2.3.4").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Forwarded {
|
||||||
|
forwarded_for: Some(NodeIdentifier::parse("1.2.3.4").unwrap()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Forwarded::parse("For=1.2.3.4").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Forwarded {
|
||||||
|
forwarded_for: Some(NodeIdentifier::parse("1.2.3.4").unwrap()),
|
||||||
|
forwarded_by: Some(NodeIdentifier::parse("[1::1]:80").unwrap()),
|
||||||
|
host: Some("foo"),
|
||||||
|
protocol: Some("https")
|
||||||
|
},
|
||||||
|
Forwarded::parse("for=1.2.3.4;by=\"[1::1]:80\";host=foo;proto=https").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display_forwarded() {
|
||||||
|
assert_eq!(
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
Forwarded {
|
||||||
|
forwarded_for: Some(NodeIdentifier::parse("1.2.3.4").unwrap()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"for=1.2.3.4"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
Forwarded {
|
||||||
|
forwarded_for: Some(NodeIdentifier::parse("1.2.3.4").unwrap()),
|
||||||
|
forwarded_by: Some(NodeIdentifier::parse("[1::1]:80").unwrap()),
|
||||||
|
host: Some("foo"),
|
||||||
|
protocol: Some("https")
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"for=1.2.3.4;by=\"[1::1]:80\";host=foo;proto=https"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub struct NodeIdentifier<'a> {
|
||||||
|
pub name: NodeName<'a>,
|
||||||
|
pub port: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> NodeIdentifier<'a> {
|
||||||
|
fn parse(name: &'a str) -> Result<Self, RfcError> {
|
||||||
|
match (name.rfind(':'), name.rfind(']')) {
|
||||||
|
(Some(delim), Some(ip6_end)) if delim > ip6_end => {
|
||||||
|
Self::parse_name_port(&name[0..delim], Some(&name[delim + 1..]))
|
||||||
|
}
|
||||||
|
(Some(delim), None) => Self::parse_name_port(&name[..delim], Some(&name[delim + 1..])),
|
||||||
|
_ => Self::parse_name_port(name, None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_name_port(name: &'a str, port: Option<&str>) -> Result<Self, RfcError> {
|
||||||
|
Ok(NodeIdentifier {
|
||||||
|
name: NodeName::parse(name)?,
|
||||||
|
port: port
|
||||||
|
.map(u16::from_str)
|
||||||
|
.transpose()
|
||||||
|
.map_err(|_| RfcError::InvalidPort)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// values containing `:` or `[]` characters need to be quoted
|
||||||
|
fn display_needs_quote(&self) -> bool {
|
||||||
|
self.port.is_some() || matches!(self.name, NodeName::Ip(IpAddr::V6(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for NodeIdentifier<'a> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self.port {
|
||||||
|
Some(port) => write!(f, "{}:{}", self.name, port),
|
||||||
|
None => write!(f, "{}", self.name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_node_identifier() {
|
||||||
|
assert_eq!(
|
||||||
|
NodeIdentifier {
|
||||||
|
name: NodeName::Ip("1.2.3.4".parse().unwrap()),
|
||||||
|
port: None
|
||||||
|
},
|
||||||
|
NodeIdentifier::parse("1.2.3.4").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
NodeIdentifier {
|
||||||
|
name: NodeName::Ip("1.2.3.4".parse().unwrap()),
|
||||||
|
port: Some(8080)
|
||||||
|
},
|
||||||
|
NodeIdentifier::parse("1.2.3.4:8080").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
NodeIdentifier {
|
||||||
|
name: NodeName::Ip("2001:db8:cafe::17".parse().unwrap()),
|
||||||
|
port: Some(8080)
|
||||||
|
},
|
||||||
|
NodeIdentifier::parse("[2001:db8:cafe::17]:8080").unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
NodeIdentifier::parse("unknown:99999").unwrap_err(),
|
||||||
|
RfcError::InvalidPort
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display_node_identifier() {
|
||||||
|
assert_eq!(
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
NodeIdentifier {
|
||||||
|
name: NodeName::Ip("1.2.3.4".parse().unwrap()),
|
||||||
|
port: None
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"1.2.3.4"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
NodeIdentifier {
|
||||||
|
name: NodeName::Ip("1.2.3.4".parse().unwrap()),
|
||||||
|
port: Some(8080)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"1.2.3.4:8080"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
NodeIdentifier {
|
||||||
|
name: NodeName::Ip("2001:db8:cafe::17".parse().unwrap()),
|
||||||
|
port: Some(8080)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"[2001:db8:cafe::17]:8080"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum NodeName<'a> {
|
||||||
|
Ip(IpAddr),
|
||||||
|
Unknown,
|
||||||
|
Obfuscated(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> NodeName<'a> {
|
||||||
|
fn parse(name: &'a str) -> Result<Self, RfcError> {
|
||||||
|
match name {
|
||||||
|
"unknown" => Ok(NodeName::Unknown),
|
||||||
|
obfuscated if obfuscated.starts_with("_") => {
|
||||||
|
if obfuscated
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_alphanumeric() || c == '.' || c == '_')
|
||||||
|
{
|
||||||
|
Ok(NodeName::Obfuscated(obfuscated))
|
||||||
|
} else {
|
||||||
|
Err(RfcError::InvalidIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ip6 if ip6.starts_with('[') && ip6.ends_with(']') => ip6[1..ip6.len() - 1]
|
||||||
|
.parse()
|
||||||
|
.map(IpAddr::V6)
|
||||||
|
.map(NodeName::Ip)
|
||||||
|
.map_err(|_| RfcError::InvalidIdentifier),
|
||||||
|
ip4 => ip4
|
||||||
|
.parse()
|
||||||
|
.map(IpAddr::V4)
|
||||||
|
.map(NodeName::Ip)
|
||||||
|
.map_err(|_| RfcError::InvalidIdentifier),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for NodeName<'a> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
NodeName::Ip(IpAddr::V4(ip)) => write!(f, "{}", ip),
|
||||||
|
NodeName::Ip(IpAddr::V6(ip)) => write!(f, "[{}]", ip),
|
||||||
|
NodeName::Unknown => {
|
||||||
|
write!(f, "unknown")
|
||||||
|
}
|
||||||
|
NodeName::Obfuscated(name) => write!(f, "{}", name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_node_name() {
|
||||||
|
assert_eq!(
|
||||||
|
NodeName::Ip("1.2.3.4".parse().unwrap()),
|
||||||
|
NodeName::parse("1.2.3.4").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
NodeName::Ip("2001:db8:cafe::17".parse().unwrap()),
|
||||||
|
NodeName::parse("[2001:db8:cafe::17]").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(NodeName::Unknown, NodeName::parse("unknown").unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
NodeName::Obfuscated("_FOO"),
|
||||||
|
NodeName::parse("_FOO").unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// obfuscated identifiers must be _[a-zA-Z0-9_.]*
|
||||||
|
assert!(matches!(
|
||||||
|
NodeName::parse("_FOO-INVALID").unwrap_err(),
|
||||||
|
RfcError::InvalidIdentifier
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
NodeName::parse("FOO").unwrap_err(),
|
||||||
|
RfcError::InvalidIdentifier
|
||||||
|
));
|
||||||
|
|
||||||
|
// ip6 identifiers must be surrounded with []
|
||||||
|
assert!(matches!(
|
||||||
|
NodeName::parse("2001:db8:cafe::17").unwrap_err(),
|
||||||
|
RfcError::InvalidIdentifier
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display_node_name() {
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", NodeName::Ip("1.2.3.4".parse().unwrap())),
|
||||||
|
"1.2.3.4"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", NodeName::Ip("2001:db8:cafe::17".parse().unwrap())),
|
||||||
|
"[2001:db8:cafe::17]"
|
||||||
|
);
|
||||||
|
assert_eq!(format!("{}", NodeName::Unknown), "unknown");
|
||||||
|
assert_eq!(format!("{}", NodeName::Obfuscated("_FOO")), "_FOO");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue