pub const REST_HOST_ALLOWLIST: &[&str] = &[
"open-api.bingx.com",
"api.binance.com",
"dapi.binance.com",
"fapi.binance.com",
"testapi.binance.vision", "testnet.binancefuture.com",
"api-pub.bitfinex.com",
"api.bitfinex.com",
"api.bitget.com",
"www.bitstamp.net",
"api.bybit.com",
"api-testnet.bybit.com",
"api.coinbase.com",
"api.crypto.com",
"uat-api.3ona.co",
"www.deribit.com",
"test.deribit.com",
"indexer.dydx.trade",
"indexer.v4testnet.dydx.exchange",
"api.gateio.ws",
"fx-api.gateio.ws",
"api-testnet.gateapi.io", "fx-api-testnet.gateio.ws",
"api.gemini.com",
"api.sandbox.gemini.com",
"api.hbdm.com",
"api.huobi.pro",
"api-aws.huobi.pro",
"api.hyperliquid.xyz",
"api.hyperliquid-testnet.xyz",
"api.kraken.com",
"demo-futures.kraken.com", "futures.kraken.com",
"api-futures.kucoin.com",
"api-sandbox-futures.kucoin.com", "api.kucoin.com",
"openapi-sandbox.kucoin.com",
"mainnet.zklighter.elliot.ai",
"testnet.zklighter.elliot.ai",
"api.mexc.com",
"contract.mexc.com",
"www.okx.com",
"api.upbit.com",
"id-api.upbit.com",
"sg-api.upbit.com",
"th-api.upbit.com",
];
pub fn rest_host_allowlist() -> &'static [&'static str] {
REST_HOST_ALLOWLIST
}
pub fn is_allowed_rest_host(host: &str) -> bool {
if host.is_empty() {
return false;
}
let lower = host.to_ascii_lowercase();
REST_HOST_ALLOWLIST
.iter()
.any(|&allowed| allowed == lower.as_str())
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn list_is_non_empty() {
assert!(!REST_HOST_ALLOWLIST.is_empty());
}
#[test]
fn no_duplicates() {
let set: HashSet<&str> = REST_HOST_ALLOWLIST.iter().copied().collect();
assert_eq!(
set.len(),
REST_HOST_ALLOWLIST.len(),
"REST_HOST_ALLOWLIST contains duplicate entries"
);
}
#[test]
fn known_hosts_present() {
assert!(
is_allowed_rest_host("api.binance.com"),
"api.binance.com must be in allowlist"
);
assert!(
is_allowed_rest_host("api.kucoin.com"),
"api.kucoin.com must be in allowlist"
);
assert!(
is_allowed_rest_host("indexer.dydx.trade"),
"indexer.dydx.trade must be in allowlist"
);
}
#[test]
fn case_insensitive_match() {
assert!(is_allowed_rest_host("API.BINANCE.COM"));
assert!(is_allowed_rest_host("Api.Kucoin.Com"));
assert!(is_allowed_rest_host("INDEXER.DYDX.TRADE"));
}
#[test]
fn ssrf_candidates_blocked() {
assert!(!is_allowed_rest_host("169.254.169.254")); assert!(!is_allowed_rest_host("localhost"));
assert!(!is_allowed_rest_host("127.0.0.1"));
assert!(!is_allowed_rest_host("evil.com"));
assert!(!is_allowed_rest_host("api.binance.com.evil.com"));
assert!(!is_allowed_rest_host("fake-api.binance.com"));
assert!(!is_allowed_rest_host(""));
}
#[test]
fn all_entries_are_lowercase() {
for &host in REST_HOST_ALLOWLIST {
assert_eq!(
host,
host.to_ascii_lowercase(),
"Entry '{host}' is not all-lowercase — allowlist must use lowercase hostnames"
);
}
}
}