rs_jsonrpc_server_utils/
hosts.rs

1//! Host header validation.
2
3use std::collections::HashSet;
4use std::net::SocketAddr;
5use matcher::{Matcher, Pattern};
6
7const SPLIT_PROOF: &'static str = "split always returns non-empty iterator.";
8
9/// Port pattern
10#[derive(Clone, Hash, PartialEq, Eq, Debug)]
11pub enum Port {
12	/// No port specified (default port)
13	None,
14	/// Port specified as a wildcard pattern
15	Pattern(String),
16	/// Fixed numeric port
17	Fixed(u16)
18}
19
20impl From<Option<u16>> for Port {
21	fn from(opt: Option<u16>) -> Self {
22		match opt {
23			Some(port) => Port::Fixed(port),
24			None => Port::None,
25		}
26	}
27}
28
29impl From<u16> for Port {
30	fn from(port: u16) -> Port {
31		Port::Fixed(port)
32	}
33}
34
35/// Host type
36#[derive(Clone, Hash, PartialEq, Eq, Debug)]
37pub struct Host {
38	hostname: String,
39	port: Port,
40	as_string: String,
41	matcher: Matcher,
42}
43
44impl<T: AsRef<str>> From<T> for Host {
45	fn from(string: T) -> Self {
46		Host::parse(string.as_ref())
47	}
48}
49
50impl Host {
51	/// Creates a new `Host` given hostname and port number.
52	pub fn new<T: Into<Port>>(hostname: &str, port: T) -> Self {
53		let port = port.into();
54		let hostname = Self::pre_process(hostname);
55		let string = Self::to_string(&hostname, &port);
56		let matcher = Matcher::new(&string);
57
58		Host {
59			hostname: hostname,
60			port: port,
61			as_string: string,
62			matcher: matcher,
63		}
64	}
65
66	/// Attempts to parse given string as a `Host`.
67	/// NOTE: This method always succeeds and falls back to sensible defaults.
68	pub fn parse(hostname: &str) -> Self {
69		let hostname = Self::pre_process(hostname);
70		let mut hostname = hostname.split(':');
71		let host = hostname.next().expect(SPLIT_PROOF);
72		let port = match hostname.next() {
73			None => Port::None,
74			Some(port) => match port.clone().parse::<u16>().ok() {
75				Some(num) => Port::Fixed(num),
76				None => Port::Pattern(port.into()),
77			}
78		};
79
80		Host::new(host, port)
81	}
82
83	fn pre_process(host: &str) -> String {
84		// Remove possible protocol definition
85		let mut it = host.split("://");
86		let protocol = it.next().expect(SPLIT_PROOF);
87		let host = match it.next() {
88			Some(data) => data,
89			None => protocol,
90		};
91
92		let mut it = host.split('/');
93		it.next().expect(SPLIT_PROOF).to_lowercase()
94	}
95
96	fn to_string(hostname: &str, port: &Port) -> String {
97		format!(
98			"{}{}",
99			hostname,
100			match *port {
101				Port::Fixed(port) => format!(":{}", port),
102				Port::Pattern(ref port) => format!(":{}", port),
103				Port::None => "".into(),
104			},
105		)
106	}
107}
108
109impl Pattern for Host {
110	fn matches<T: AsRef<str>>(&self, other: T) -> bool {
111		self.matcher.matches(other)
112	}
113}
114
115impl ::std::ops::Deref for Host {
116	type Target = str;
117	fn deref(&self) -> &Self::Target {
118		&self.as_string
119	}
120}
121
122/// Specifies if domains should be validated.
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub enum DomainsValidation<T> {
125	/// Allow only domains on the list.
126	AllowOnly(Vec<T>),
127	/// Disable domains validation completely.
128	Disabled,
129}
130
131impl<T> Into<Option<Vec<T>>> for DomainsValidation<T> {
132	fn into(self) -> Option<Vec<T>> {
133		use self::DomainsValidation::*;
134		match self {
135			AllowOnly(list) => Some(list),
136			Disabled => None,
137		}
138	}
139}
140
141impl<T> From<Option<Vec<T>>> for DomainsValidation<T> {
142	fn from(other: Option<Vec<T>>) -> Self {
143		match other {
144			Some(list) => DomainsValidation::AllowOnly(list),
145			None => DomainsValidation::Disabled,
146		}
147	}
148}
149
150/// Returns `true` when `Host` header is whitelisted in `allowed_hosts`.
151pub fn is_host_valid(host: Option<&str>, allowed_hosts: &Option<Vec<Host>>) -> bool {
152	match allowed_hosts.as_ref() {
153		None => true,
154		Some(ref allowed_hosts) => match host {
155			None => false,
156			Some(ref host) => {
157				allowed_hosts.iter().any(|h| h.matches(host))
158			}
159		}
160	}
161}
162
163/// Updates given list of hosts with the address.
164pub fn update(hosts: Option<Vec<Host>>, address: &SocketAddr) -> Option<Vec<Host>> {
165	hosts.map(|current_hosts| {
166		let mut new_hosts = current_hosts.into_iter().collect::<HashSet<_>>();
167		let address = address.to_string();
168		new_hosts.insert(address.clone().into());
169		new_hosts.insert(address.replace("127.0.0.1", "localhost").into());
170		new_hosts.into_iter().collect()
171	})
172}
173
174#[cfg(test)]
175mod tests {
176	use super::{Host, is_host_valid};
177
178	#[test]
179	fn should_parse_host() {
180		assert_eq!(Host::parse("http://superstring.ch"), Host::new("superstring.ch", None));
181		assert_eq!(Host::parse("http://superstring.ch:8443"), Host::new("superstring.ch", Some(8443)));
182		assert_eq!(Host::parse("chrome-extension://124.0.0.1"), Host::new("124.0.0.1", None));
183		assert_eq!(Host::parse("superstring.ch/somepath"), Host::new("superstring.ch", None));
184		assert_eq!(Host::parse("127.0.0.1:8545/somepath"), Host::new("127.0.0.1", Some(8545)));
185	}
186
187	#[test]
188	fn should_reject_when_there_is_no_header() {
189		let valid = is_host_valid(None, &Some(vec![]));
190		assert_eq!(valid, false);
191	}
192
193	#[test]
194	fn should_reject_when_validation_is_disabled() {
195		let valid = is_host_valid(Some("any"), &None);
196		assert_eq!(valid, true);
197	}
198
199	#[test]
200	fn should_reject_if_header_not_on_the_list() {
201		let valid = is_host_valid(Some("superstring.ch"), &Some(vec![]));
202		assert_eq!(valid, false);
203	}
204
205	#[test]
206	fn should_accept_if_on_the_list() {
207		let valid = is_host_valid(
208			Some("superstring.ch"),
209			&Some(vec!["superstring.ch".into()]),
210		);
211		assert_eq!(valid, true);
212	}
213
214	#[test]
215	fn should_accept_if_on_the_list_with_port() {
216		let valid = is_host_valid(
217			Some("superstring.ch:443"),
218			&Some(vec!["superstring.ch:443".into()]),
219		);
220		assert_eq!(valid, true);
221	}
222
223	#[test]
224	fn should_support_wildcards() {
225		let valid = is_host_valid(
226			Some("s.web3.site:8180"),
227			&Some(vec!["*.web3.site:*".into()]),
228		);
229		assert_eq!(valid, true);
230	}
231}