Allow IP addresses in URLs

Ref: https://github.com/mbrubeck/agate/pull/433
Co-authored-by: oooo-ps <l.trk@tuta.io>
This commit is contained in:
Johann150 2026-04-03 18:31:25 +02:00
parent 1536c382ab
commit 2d6dac4a2f
No known key found for this signature in database
GPG key ID: 9EE6577A2A06F8F1
3 changed files with 74 additions and 18 deletions

View file

@ -9,10 +9,16 @@ Updates to dependencies are not considered notable changes for the purpose of th
This may lead to no listed changes for a version.
## [Unreleased]
Thank you to @oooo-ps for contributing to this release.
### Fixed
* Use the default port when checking for the right port.
### Added
* Accept requests with URLs containing IP addresses.
Agate will check that the IP matches the local address for TCP sockets.
Unix sockets will accept any IP address.
## [3.3.19] - 2025-09-18
### Fixed

View file

@ -172,6 +172,8 @@ If you want to serve the same content for multiple domains, you can instead disa
When one or more `--hostname`s are specified, Agate will check that the hostnames and port in request URLs match the specified hostnames and the listening ports. If Agate is behind a proxy on another port and receives a request with an URL specifying the proxy port, this port may not match one of Agate's listening ports and the request will be rejected: it is possible to disable the port check with `--skip-port-check`.
If Agate receives a request using an IP address in the URL, it will check that the IP address from the URL matches the local IP address of the TCP connection. Because Unix sockets do not have an IP address, this check cannot be performed and any IP address will be permitted via Unix sockets.
### Certificates
Agate has support for using multiple certificates with the `--certs` option. Agate will thus always require that a client uses SNI, which should not be a problem since the Gemini specification also requires SNI to be used.

View file

@ -496,9 +496,47 @@ impl RequestHandle<UnixStream> {
}
}
trait CheckHost {
fn check_host(&self, host: &url::Host) -> bool;
}
fn check_domain(domain: &str) -> bool {
if ARGS.hostnames.is_empty() {
// no hostnames -> hostname check disabled
true
} else {
ARGS.hostnames.iter().any(|x| x == &Host::Domain(domain))
}
}
impl CheckHost for TcpStream {
fn check_host(&self, host: &url::Host) -> bool {
match host {
url::Host::Ipv4(ip) => self
.local_addr()
.is_ok_and(|local| &local.ip().to_canonical() == ip),
url::Host::Ipv6(ip) => self.local_addr().is_ok_and(|local| match local.ip() {
IpAddr::V4(local) => &local.to_ipv6_mapped() == ip,
IpAddr::V6(local) => &local == ip,
}),
url::Host::Domain(domain) => check_domain(domain),
}
}
}
#[cfg(unix)]
impl CheckHost for UnixStream {
fn check_host(&self, host: &url::Host) -> bool {
match host {
url::Host::Ipv4(..) | url::Host::Ipv6(..) => true,
url::Host::Domain(domain) => check_domain(domain),
}
}
}
impl<T> RequestHandle<T>
where
T: AsyncWriteExt + AsyncReadExt + Unpin,
T: AsyncWriteExt + AsyncReadExt + Unpin + CheckHost,
{
/// Do the necessary actions to handle this request. Returns a corresponding
/// log line as Err or Ok, depending on if the request finished with or
@ -570,24 +608,34 @@ where
return Err((BAD_REQUEST, "URL contains fragment or userinfo"));
}
// correct host
let Some(domain) = url.domain() else {
return Err((BAD_REQUEST, "URL does not contain a domain"));
};
// because the gemini scheme is not special enough for WHATWG, normalize
// it ourselves
let host = Host::parse(
// normalize host
let host = match url.host() {
Some(Host::Domain(domain)) => {
// because the gemini scheme is not special enough for WHATWG,
// (re-)normalize it properly
let domain = Host::parse(
&percent_decode_str(domain)
.decode_utf8()
.or(Err((BAD_REQUEST, "Invalid URL")))?,
)
.or(Err((BAD_REQUEST, "Invalid URL")))?;
// also put the now properly normalized host back into the url
// TODO: simplify when <https://github.com/servo/rust-url/issues/586> resolved
url.set_host(Some(&host.to_string()))
url.set_host(Some(&domain.to_string()))
.expect("invalid domain?");
// do not use "contains" here since it requires the same type and does
// not allow to check for Host<&str> if the vec contains Hostname<String>
if !ARGS.hostnames.is_empty() && !ARGS.hostnames.iter().any(|h| h == &host) {
domain
}
// these match arms are needed for "converting" from Host<&str> to Host<String>
Some(Host::Ipv4(ip)) => Host::Ipv4(ip),
Some(Host::Ipv6(ip)) => Host::Ipv6(ip),
None => {
// cannot-be-a-base URLs cannot be used here
return Err((BAD_REQUEST, "URL does not contain a domain"));
}
};
// check for correct host
if !self.stream.get_ref().0.check_host(&host) {
return Err((PROXY_REQUEST_REFUSED, "Proxy request refused"));
}