susydev_jsonrpc_server_utils/
hosts.rs

1//! Host header validation.
2
3use crate::matcher::{Matcher, Pattern};
4use std::collections::HashSet;
5use std::net::SocketAddr;
6
7const SPLIT_PROOF: &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,
60			port,
61			as_string: string,
62			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.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) => allowed_hosts.iter().any(|h| h.matches(host)),
157		},
158	}
159}
160
161/// Updates given list of hosts with the address.
162pub fn update(hosts: Option<Vec<Host>>, address: &SocketAddr) -> Option<Vec<Host>> {
163	hosts.map(|current_hosts| {
164		let mut new_hosts = current_hosts.into_iter().collect::<HashSet<_>>();
165		let address = address.to_string();
166		new_hosts.insert(address.clone().into());
167		new_hosts.insert(address.replace("127.0.0.1", "localhost").into());
168		new_hosts.into_iter().collect()
169	})
170}
171
172#[cfg(test)]
173mod tests {
174	use super::{is_host_valid, Host};
175
176	#[test]
177	fn should_parse_host() {
178		assert_eq!(Host::parse("http://superstring.ch"), Host::new("superstring.ch", None));
179		assert_eq!(
180			Host::parse("http://superstring.ch:8443"),
181			Host::new("superstring.ch", Some(8443))
182		);
183		assert_eq!(
184			Host::parse("chrome-extension://124.0.0.1"),
185			Host::new("124.0.0.1", None)
186		);
187		assert_eq!(Host::parse("superstring.ch/somepath"), Host::new("superstring.ch", None));
188		assert_eq!(
189			Host::parse("127.0.0.1:8545/somepath"),
190			Host::new("127.0.0.1", Some(8545))
191		);
192	}
193
194	#[test]
195	fn should_reject_when_there_is_no_header() {
196		let valid = is_host_valid(None, &Some(vec![]));
197		assert_eq!(valid, false);
198	}
199
200	#[test]
201	fn should_reject_when_validation_is_disabled() {
202		let valid = is_host_valid(Some("any"), &None);
203		assert_eq!(valid, true);
204	}
205
206	#[test]
207	fn should_reject_if_header_not_on_the_list() {
208		let valid = is_host_valid(Some("superstring.ch"), &Some(vec![]));
209		assert_eq!(valid, false);
210	}
211
212	#[test]
213	fn should_accept_if_on_the_list() {
214		let valid = is_host_valid(Some("superstring.ch"), &Some(vec!["superstring.ch".into()]));
215		assert_eq!(valid, true);
216	}
217
218	#[test]
219	fn should_accept_if_on_the_list_with_port() {
220		let valid = is_host_valid(Some("superstring.ch:443"), &Some(vec!["superstring.ch:443".into()]));
221		assert_eq!(valid, true);
222	}
223
224	#[test]
225	fn should_support_wildcards() {
226		let valid = is_host_valid(Some("susy.web3.site:8180"), &Some(vec!["*.web3.site:*".into()]));
227		assert_eq!(valid, true);
228	}
229}