use std::io::Read as _;
use std::net::{SocketAddr, ToSocketAddrs as _};
use std::sync::Arc;
use std::time::Duration;
use std::sync::RwLock;
use gunnar_grpc::sso::{addresses_allowed_in, url_allowed_in, Egress, PrivateNets, MAX_IDP_BODY};
const TIMEOUT: Duration = Duration::from_secs(10);
pub type Lookup = Arc<dyn Fn(&str, u16) -> std::io::Result<Vec<SocketAddr>> + Send + Sync>;
pub fn system_lookup() -> Lookup {
Arc::new(|host: &str, port: u16| (host, port).to_socket_addrs().map(|a| a.collect()))
}
#[derive(Clone)]
pub struct GuardedResolver {
lookup: Lookup,
allow_loopback: bool,
nets: Arc<RwLock<PrivateNets>>,
}
impl std::fmt::Debug for GuardedResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GuardedResolver")
.field("allow_loopback", &self.allow_loopback)
.finish_non_exhaustive()
}
}
impl GuardedResolver {
pub fn new(lookup: Lookup, allow_loopback: bool) -> Self {
Self::with_nets(lookup, allow_loopback, Arc::default())
}
pub fn with_nets(lookup: Lookup, allow_loopback: bool, nets: Arc<RwLock<PrivateNets>>) -> Self {
GuardedResolver {
lookup,
allow_loopback,
nets,
}
}
pub fn resolve_checked(&self, host: &str, port: u16) -> Result<Vec<SocketAddr>, String> {
let bare = host.trim_start_matches('[').trim_end_matches(']');
let addrs =
(self.lookup)(bare, port).map_err(|e| format!("{bare} does not resolve: {e}"))?;
let nets = self.nets.read().unwrap_or_else(|p| p.into_inner()).clone();
addresses_allowed_in(bare, &addrs, self.allow_loopback, &nets)?;
Ok(addrs)
}
}
impl ureq::unversioned::resolver::Resolver for GuardedResolver {
fn resolve(
&self,
uri: &ureq::http::Uri,
_config: &ureq::config::Config,
_timeout: ureq::unversioned::transport::NextTimeout,
) -> Result<ureq::unversioned::resolver::ResolvedSocketAddrs, ureq::Error> {
let host = uri
.host()
.ok_or_else(|| ureq::Error::BadUri(format!("{uri} names no host")))?;
let port = uri.port_u16().unwrap_or(match uri.scheme_str() {
Some("http") => 80,
_ => 443,
});
let addrs = self.resolve_checked(host, port).map_err(|why| {
ureq::Error::Io(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
why,
))
})?;
let mut out = self.empty();
for addr in addrs.into_iter().take(16) {
out.push(addr);
}
Ok(out)
}
}
#[derive(Debug, Clone, Copy)]
pub struct DoorOnly(pub SocketAddr);
impl ureq::unversioned::resolver::Resolver for DoorOnly {
fn resolve(
&self,
uri: &ureq::http::Uri,
_config: &ureq::config::Config,
_timeout: ureq::unversioned::transport::NextTimeout,
) -> Result<ureq::unversioned::resolver::ResolvedSocketAddrs, ureq::Error> {
let at = (uri.host().map(|h| h.trim_start_matches('[').trim_end_matches(']').parse::<std::net::IpAddr>().ok()), uri.port_u16());
if at != (Some(Some(self.0.ip())), Some(self.0.port())) {
return Err(ureq::Error::Io(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("{uri}: a box behind its front resolves its egress door alone ({})", self.0),
)));
}
let mut out = self.empty();
out.push(self.0);
Ok(out)
}
}
pub struct UreqEgress {
agent: ureq::Agent,
resolver: GuardedResolver,
allow_loopback_http: bool,
nets: Arc<RwLock<PrivateNets>>,
}
fn config(tls: Option<ureq::tls::TlsConfig>, proxy: Option<ureq::Proxy>) -> ureq::config::Config {
let builder = ureq::Agent::config_builder()
.redirect_auth_headers(ureq::config::RedirectAuthHeaders::Never)
.max_redirects(0)
.timeout_connect(Some(TIMEOUT))
.timeout_global(Some(TIMEOUT))
.proxy(proxy)
.http_status_as_error(false);
match tls {
Some(tls) => builder.tls_config(tls).build(),
None => builder.build(),
}
}
impl UreqEgress {
pub fn new(allow_loopback_http: bool) -> Self {
Self::with_lookup(allow_loopback_http, system_lookup())
}
pub fn with_lookup(allow_loopback_http: bool, lookup: Lookup) -> Self {
Self::with_nets(allow_loopback_http, lookup, Arc::default())
}
pub fn with_nets(allow_loopback_http: bool, lookup: Lookup, nets: Arc<RwLock<PrivateNets>>) -> Self {
let resolver = GuardedResolver::with_nets(lookup, allow_loopback_http, Arc::clone(&nets));
let agent = ureq::Agent::with_parts(
config(None, None),
ureq::unversioned::transport::DefaultConnector::default(),
resolver.clone(),
);
UreqEgress {
agent,
resolver,
allow_loopback_http,
nets,
}
}
fn nets(&self) -> PrivateNets {
self.nets.read().unwrap_or_else(|p| p.into_inner()).clone()
}
fn agent_trusting(&self, ca: &[Vec<u8>]) -> ureq::Agent {
let tls = Self::tls_trusting(ca);
ureq::Agent::with_parts(
config(Some(tls), None),
ureq::unversioned::transport::DefaultConnector::default(),
self.resolver.clone(),
)
}
fn tls_trusting(ca: &[Vec<u8>]) -> ureq::tls::TlsConfig {
let mut roots: Vec<ureq::tls::Certificate<'static>> = webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.map(|c| ureq::tls::Certificate::from_der(c.as_ref()).to_owned())
.collect();
roots.extend(ca.iter().map(|der| ureq::tls::Certificate::from_der(der).to_owned()));
ureq::tls::TlsConfig::builder()
.root_certs(ureq::tls::RootCerts::Specific(Arc::new(roots)))
.build()
}
fn through_the_door(ca: &[Vec<u8>]) -> Option<ureq::Agent> {
let door = nordisk_pairing::egress::proxy()?;
let at = nordisk_pairing::egress::door_addr(door).ok()?;
let proxy = ureq::Proxy::new(door).ok()?;
let tls = (!ca.is_empty()).then(|| Self::tls_trusting(ca));
Some(ureq::Agent::with_parts(config(tls, Some(proxy)), ureq::unversioned::transport::DefaultConnector::default(), DoorOnly(at)))
}
fn get_with(&self, agent: &ureq::Agent, url: &str) -> Result<(u16, String), String> {
url_allowed_in(url, self.allow_loopback_http, &self.nets())?;
let response = agent
.get(url)
.header(
"Accept",
"application/json, application/samlmetadata+xml, application/xml, text/xml",
)
.call()
.map_err(|e| format!("{e}"))?;
Self::read(response)
}
fn post_with(
&self,
agent: &ureq::Agent,
url: &str,
authorization: &str,
body: &str,
) -> Result<(u16, String), String> {
url_allowed_in(url, self.allow_loopback_http, &self.nets())?;
let mut request = agent
.post(url)
.header("Accept", "application/json")
.header("Content-Type", "application/x-www-form-urlencoded");
if !authorization.is_empty() {
request = request.header("Authorization", authorization);
}
let response = request.send(body).map_err(|e| format!("{e}"))?;
Self::read(response)
}
fn read(response: ureq::http::Response<ureq::Body>) -> Result<(u16, String), String> {
let status = response.status().as_u16();
let mut body = Vec::new();
response
.into_body()
.into_reader()
.take(MAX_IDP_BODY as u64 + 1)
.read_to_end(&mut body)
.map_err(|e| format!("reading the answer: {e}"))?;
if body.len() > MAX_IDP_BODY {
return Err(format!("the answer is larger than {MAX_IDP_BODY} bytes"));
}
Ok((status, String::from_utf8_lossy(&body).into_owned()))
}
}
impl Egress for UreqEgress {
fn get(&self, url: &str) -> Result<(u16, String), String> {
match Self::through_the_door(&[]) {
Some(agent) => self.get_with(&agent, url),
None => self.get_with(&self.agent, url),
}
}
fn post_form(
&self,
url: &str,
authorization: &str,
body: &str,
) -> Result<(u16, String), String> {
match Self::through_the_door(&[]) {
Some(agent) => self.post_with(&agent, url, authorization, body),
None => self.post_with(&self.agent, url, authorization, body),
}
}
fn get_trusting(&self, url: &str, ca: &[Vec<u8>]) -> Result<(u16, String), String> {
if ca.is_empty() {
return self.get(url);
}
match Self::through_the_door(ca) {
Some(agent) => self.get_with(&agent, url),
None => self.get_with(&self.agent_trusting(ca), url),
}
}
fn post_form_trusting(
&self,
url: &str,
authorization: &str,
body: &str,
ca: &[Vec<u8>],
) -> Result<(u16, String), String> {
if ca.is_empty() {
return self.post_form(url, authorization, body);
}
match Self::through_the_door(ca) {
Some(agent) => self.post_with(&agent, url, authorization, body),
None => self.post_with(&self.agent_trusting(ca), url, authorization, body),
}
}
}