mirror of
https://github.com/YGGverse/agate.git
synced 2026-04-08 12:35:28 +00:00
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:
parent
1536c382ab
commit
2d6dac4a2f
3 changed files with 74 additions and 18 deletions
|
|
@ -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.
|
This may lead to no listed changes for a version.
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
Thank you to @oooo-ps for contributing to this release.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* Use the default port when checking for the right port.
|
* 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
|
## [3.3.19] - 2025-09-18
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
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
|
### 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.
|
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.
|
||||||
|
|
|
||||||
72
src/main.rs
72
src/main.rs
|
|
@ -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>
|
impl<T> RequestHandle<T>
|
||||||
where
|
where
|
||||||
T: AsyncWriteExt + AsyncReadExt + Unpin,
|
T: AsyncWriteExt + AsyncReadExt + Unpin + CheckHost,
|
||||||
{
|
{
|
||||||
/// Do the necessary actions to handle this request. Returns a corresponding
|
/// 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
|
/// 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"));
|
return Err((BAD_REQUEST, "URL contains fragment or userinfo"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// correct host
|
// normalize host
|
||||||
let Some(domain) = url.domain() else {
|
let host = match url.host() {
|
||||||
return Err((BAD_REQUEST, "URL does not contain a domain"));
|
Some(Host::Domain(domain)) => {
|
||||||
};
|
// because the gemini scheme is not special enough for WHATWG,
|
||||||
// because the gemini scheme is not special enough for WHATWG, normalize
|
// (re-)normalize it properly
|
||||||
// it ourselves
|
let domain = Host::parse(
|
||||||
let host = Host::parse(
|
|
||||||
&percent_decode_str(domain)
|
&percent_decode_str(domain)
|
||||||
.decode_utf8()
|
.decode_utf8()
|
||||||
.or(Err((BAD_REQUEST, "Invalid URL")))?,
|
.or(Err((BAD_REQUEST, "Invalid URL")))?,
|
||||||
)
|
)
|
||||||
.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
|
// 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?");
|
.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>
|
domain
|
||||||
if !ARGS.hostnames.is_empty() && !ARGS.hostnames.iter().any(|h| h == &host) {
|
}
|
||||||
|
// 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"));
|
return Err((PROXY_REQUEST_REFUSED, "Proxy request refused"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue