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)
}
}
pub struct UreqEgress {
agent: ureq::Agent,
resolver: GuardedResolver,
allow_loopback_http: bool,
nets: Arc<RwLock<PrivateNets>>,
}
fn config(tls: Option<ureq::tls::TlsConfig>) -> 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(None)
.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),
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 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()));
let tls = ureq::tls::TlsConfig::builder()
.root_certs(ureq::tls::RootCerts::Specific(Arc::new(roots)))
.build();
ureq::Agent::with_parts(
config(Some(tls)),
ureq::unversioned::transport::DefaultConnector::default(),
self.resolver.clone(),
)
}
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> {
self.get_with(&self.agent, url)
}
fn post_form(
&self,
url: &str,
authorization: &str,
body: &str,
) -> Result<(u16, String), String> {
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);
}
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);
}
self.post_with(&self.agent_trusting(ca), url, authorization, body)
}
}