fix: january ip redirects & domain resolver (#738)

* fix: properly block private ip ranges
I'm a dunce and forgot that domains do in fact resolve to ips.

* fix: reimplement max redirects

* fix: remove my debug error

* fix: actually check redirect urls
kind of the whole point of this thing.
This commit is contained in:
Tom
2026-05-06 21:33:52 -07:00
committed by GitHub
parent 21d82018cf
commit 356491e934

View File

@@ -10,9 +10,10 @@ use reqwest::{
use revolt_config::{config, report_internal_error}; use revolt_config::{config, report_internal_error};
use revolt_files::{create_thumbnail, decode_image, image_size_vec, is_valid_image, video_size}; use revolt_files::{create_thumbnail, decode_image, image_size_vec, is_valid_image, video_size};
use revolt_models::v0::{Embed, Image, ImageSize, Video}; use revolt_models::v0::{Embed, Image, ImageSize, Video};
use revolt_result::{create_error, Error, Result}; use revolt_result::{create_error, Error, Result, ToRevoltError};
use std::{ use std::{
io::{Cursor, Write}, io::{Cursor, Write},
str::FromStr,
time::Duration, time::Duration,
}; };
use url::{Host, Url}; use url::{Host, Url};
@@ -22,15 +23,7 @@ lazy_static! {
static ref CLIENT: Client = reqwest::Client::builder() static ref CLIENT: Client = reqwest::Client::builder()
.timeout(Duration::from_secs(10)) // TODO config .timeout(Duration::from_secs(10)) // TODO config
.connect_timeout(Duration::from_secs(5)) // TODO config .connect_timeout(Duration::from_secs(5)) // TODO config
.redirect(redirect::Policy::custom(|attempt| { .redirect(redirect::Policy::none())
if attempt.previous().len() > 5 { // TODO config
attempt.error("too many redirects")
} else if attempt.url().host_str() == Some("proxy.stoatusercontent.com") { // TODO config
attempt.stop()
} else {
attempt.follow()
}
}))
.build() .build()
.expect("reqwest Client"); .expect("reqwest Client");
@@ -63,25 +56,14 @@ lazy_static! {
.build(); .build();
static ref IP_BLOCKLIST: IpFilter = IpFilter::block(&[ static ref IP_BLOCKLIST: IpFilter = IpFilter::block(&[
"10.0.0.0/8", // something something modern problem require modern solutions "10.0.0.0/8",
"192.168.0.0/16", "192.168.0.0/16",
"172.16.0.0/16", "127.0.0.0/8",
"172.17.0.0/16", "172.16.0.0/12",
"172.18.0.0/16", "169.254.0.0/16",
"172.19.0.0/16", "::1",
"172.20.0.0/16", "fc00::/7"
"172.21.0.0/16", ]
"172.22.0.0/16",
"172.23.0.0/16",
"172.24.0.0/16",
"172.25.0.0/16",
"172.26.0.0/16",
"172.27.0.0/16",
"172.28.0.0/16",
"172.29.0.0/16",
"172.30.0.0/16",
"172.31.0.0/16",
"172.32.0.0/16"]
).unwrap(); ).unwrap();
} }
@@ -280,11 +262,14 @@ impl Request {
/// Send a new request to a service /// Send a new request to a service
pub async fn new(url: Url) -> Result<Request> { pub async fn new(url: Url) -> Result<Request> {
let mut url = url;
let url_host_str = url.host_str().ok_or(create_error!(ProxyError))?.to_string(); let url_host_str = url.host_str().ok_or(create_error!(ProxyError))?.to_string();
Request::url_is_blacklisted(&url).await?; Request::url_is_blacklisted(&url).await?;
let mut redirect_count = 0;
let response = CLIENT loop {
let response = CLIENT
.get(url) .get(url)
.header( .header(
"User-Agent", "User-Agent",
@@ -299,23 +284,42 @@ impl Request {
.await .await
.map_err(|_| create_error!(ProxyError))?; .map_err(|_| create_error!(ProxyError))?;
if !response.status().is_success() { if response.status().is_redirection() {
tracing::error!("{:?}", response); redirect_count += 1;
return Err(create_error!(ProxyError));
if redirect_count > 5 {
return Err(create_error!(ProxyError));
}
if let Some(location) = response.headers().get("location") {
let location = location.to_str().map_err(|_| create_error!(ProxyError))?;
url = Url::from_str(location).to_internal_error()?;
if !Request::url_is_blacklisted(&url).await? {
continue;
}
} else {
return Err(create_error!(ProxyError));
}
}
if !response.status().is_success() {
tracing::error!("{:?}", response);
return Err(create_error!(ProxyError));
}
let content_type = response
.headers()
.get(CONTENT_TYPE)
.ok_or(create_error!(ProxyError))?
.to_str()
.map_err(|_| create_error!(ProxyError))?;
let mime: mime::Mime = content_type
.parse()
.map_err(|_| create_error!(ProxyError))?;
return Ok(Request { response, mime });
} }
let content_type = response
.headers()
.get(CONTENT_TYPE)
.ok_or(create_error!(ProxyError))?
.to_str()
.map_err(|_| create_error!(ProxyError))?;
let mime: mime::Mime = content_type
.parse()
.map_err(|_| create_error!(ProxyError))?;
Ok(Request { response, mime })
} }
pub async fn new_from_str(url: &str) -> Result<Request> { pub async fn new_from_str(url: &str) -> Result<Request> {
@@ -337,7 +341,7 @@ impl Request {
Ok(Request::exists(proper_url).await) Ok(Request::exists(proper_url).await)
} }
pub async fn url_is_blacklisted(url: &Url) -> Result<()> { pub async fn url_is_blacklisted(url: &Url) -> Result<bool> {
if let Some(host) = url.host() { if let Some(host) = url.host() {
match host { match host {
Host::Ipv4(ipv4) => { Host::Ipv4(ipv4) => {
@@ -347,15 +351,38 @@ impl Request {
} }
} }
Host::Domain(domain) => { Host::Domain(domain) => {
let mut domain = domain.to_string();
let config = config().await; let config = config().await;
if config.january.blocked_domains.iter().any(|x| x == domain) {
// First step: TLDs and blocked domains
if !domain.contains(".") // lazily block TLDs
|| config.january.blocked_domains.iter().any(|x| x == &domain)
{
return Err(create_error!(InvalidOperation)); return Err(create_error!(InvalidOperation));
} }
if !domain.contains(":") {
domain += ":80";
}
// Second step: resolve the IP and check the blocklist
if let Ok(mut resolved_ip) = tokio::net::lookup_host(domain.clone()).await {
if let Some(resolved_ip) = resolved_ip.next() {
if !IP_BLOCKLIST.is_allowed(&resolved_ip.ip().to_string()) {
return Err(create_error!(InvalidOperation));
}
} else {
return Err(create_error!(InvalidOperation));
}
} else {
return Err(create_error!(ProxyError));
}
} }
_ => (), _ => (),
} }
}; };
Ok(()) Ok(false)
} }
} }