init
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
use axum::{
|
||||
extract::ConnectInfo,
|
||||
http::{header, Request, StatusCode},
|
||||
};
|
||||
use futures_util::StreamExt;
|
||||
use ipnet::IpNet;
|
||||
use minecraft_log_viewer::{
|
||||
auth::{client_ip::ClientIpPolicy, whitelist::Whitelist, AuthorizationService},
|
||||
config::Config,
|
||||
http::{router, AppState},
|
||||
logs::index::{HistoryIndex, HistoryStore},
|
||||
};
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
sync::{atomic::AtomicUsize, Arc},
|
||||
};
|
||||
use tokio::sync::broadcast;
|
||||
use tower::ServiceExt;
|
||||
|
||||
async fn app() -> (
|
||||
axum::Router,
|
||||
tempfile::TempDir,
|
||||
broadcast::Sender<minecraft_log_viewer::logs::follower::LiveMessage>,
|
||||
) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let logs = dir.path().join("logs");
|
||||
std::fs::create_dir(&logs).unwrap();
|
||||
let latest = logs.join("latest.log");
|
||||
std::fs::write(&latest, "[12:00:00] Alex[/203.0.113.4:51234] logged in\n").unwrap();
|
||||
let whitelist_path = dir.path().join("whitelist.json");
|
||||
let whitelist_json = br#"[{"uuid":"123e4567-e89b-12d3-a456-426614174000","name":"Alex"}]"#;
|
||||
std::fs::write(&whitelist_path, whitelist_json).unwrap();
|
||||
let config = Arc::new(Config {
|
||||
listen_addr: "127.0.0.1:0".parse().unwrap(),
|
||||
data_dir: dir.path().to_path_buf(),
|
||||
log_dir: logs.clone(),
|
||||
latest_log: latest.clone(),
|
||||
whitelist: whitelist_path,
|
||||
trust_proxy: true,
|
||||
trusted_proxy_cidrs: vec![
|
||||
"10.0.0.0/8".parse().unwrap(),
|
||||
"127.0.0.1/32".parse().unwrap(),
|
||||
],
|
||||
client_ip_header: "x-forwarded-for".into(),
|
||||
public_origin: "https://logs.example".into(),
|
||||
initial_log_lines: 100,
|
||||
max_history_lines: 100,
|
||||
archive_cache_max_bytes: 1024,
|
||||
archive_cache_max_files: 2,
|
||||
max_archive_bytes: 4096,
|
||||
ws_queue_capacity: 8,
|
||||
max_ws_connections: 2,
|
||||
redact_player_ips: true,
|
||||
});
|
||||
let auth = AuthorizationService::new(latest, Whitelist::parse(whitelist_json).unwrap());
|
||||
auth.rebuild().await.unwrap();
|
||||
let history = HistoryStore::new(HistoryIndex::scan(&logs).unwrap(), 1024, 2, 4096, true);
|
||||
let (live_tx, _) = broadcast::channel(8);
|
||||
let state = AppState {
|
||||
config,
|
||||
auth,
|
||||
history,
|
||||
live_tx: live_tx.clone(),
|
||||
ip_policy: ClientIpPolicy {
|
||||
trust_proxy: true,
|
||||
trusted_proxies: vec![
|
||||
"10.0.0.0/8".parse::<IpNet>().unwrap(),
|
||||
"127.0.0.1/32".parse().unwrap(),
|
||||
],
|
||||
header: "x-forwarded-for".into(),
|
||||
},
|
||||
ws_count: Arc::new(AtomicUsize::new(0)),
|
||||
};
|
||||
(router(state), dir, live_tx)
|
||||
}
|
||||
|
||||
fn request(peer: &str) -> Request<axum::body::Body> {
|
||||
let mut request = Request::builder()
|
||||
.uri("/api/logs/recent")
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap();
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(ConnectInfo(peer.parse::<SocketAddr>().unwrap()));
|
||||
request
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authorized_direct_ip_receives_redacted_logs() {
|
||||
let (app, _dir, _tx) = app().await;
|
||||
let response = app.oneshot(request("203.0.113.4:5000")).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(response.into_body(), 8192)
|
||||
.await
|
||||
.unwrap();
|
||||
let text = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(text.contains("IP REDACTED"));
|
||||
assert!(!text.contains("203.0.113.4"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn untrusted_peer_cannot_spoof_an_authorized_forwarded_ip() {
|
||||
let (app, _dir, _tx) = app().await;
|
||||
let mut request = request("192.0.2.9:5000");
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("x-forwarded-for", "203.0.113.4".parse().unwrap());
|
||||
let response = app.oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unexpected_browser_origin_is_rejected() {
|
||||
let (app, _dir, _tx) = app().await;
|
||||
let mut request = request("203.0.113.4:5000");
|
||||
request
|
||||
.headers_mut()
|
||||
.insert(header::ORIGIN, "https://evil.example".parse().unwrap());
|
||||
let response = app.oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authorized_websocket_receives_ordered_batches() {
|
||||
use minecraft_log_viewer::logs::{follower::LiveMessage, LogLine};
|
||||
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
|
||||
let (app, _dir, tx) = app().await;
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let server = tokio::spawn(async move {
|
||||
axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
let mut request = format!("ws://{address}/api/live")
|
||||
.into_client_request()
|
||||
.unwrap();
|
||||
request
|
||||
.headers_mut()
|
||||
.insert(header::ORIGIN, "https://logs.example".parse().unwrap());
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("x-forwarded-for", "203.0.113.4".parse().unwrap());
|
||||
let (mut socket, _) = tokio_tungstenite::connect_async(request).await.unwrap();
|
||||
let hello = socket.next().await.unwrap().unwrap().into_text().unwrap();
|
||||
assert!(hello.contains("hello"));
|
||||
tx.send(LiveMessage::LogLines {
|
||||
lines: vec![
|
||||
LogLine {
|
||||
id: "1".into(),
|
||||
timestamp: None,
|
||||
text: "first".into(),
|
||||
source: "latest.log".into(),
|
||||
},
|
||||
LogLine {
|
||||
id: "2".into(),
|
||||
timestamp: None,
|
||||
text: "second".into(),
|
||||
source: "latest.log".into(),
|
||||
},
|
||||
],
|
||||
})
|
||||
.unwrap();
|
||||
let batch = socket.next().await.unwrap().unwrap().into_text().unwrap();
|
||||
assert!(batch.find("first").unwrap() < batch.find("second").unwrap());
|
||||
server.abort();
|
||||
}
|
||||
Reference in New Issue
Block a user