feat: log censoring (#925)
This commit is contained in:
parent
c43272683b
commit
b2384afb4e
9 changed files with 106 additions and 9 deletions
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
default: minor
|
||||
---
|
||||
|
||||
# Game logs hide account tokens, and the game output starts with the launch command ready to paste into a terminal
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4995,6 +4995,7 @@ dependencies = [
|
|||
"oneclient_db",
|
||||
"oneclient_net",
|
||||
"polyio",
|
||||
"regex",
|
||||
"reqwest 0.13.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ bytes.workspace = true
|
|||
chrono.workspace = true
|
||||
image.workspace = true
|
||||
reqwest.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_repr.workspace = true
|
||||
strum.workspace = true
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ pub(super) fn lines_from(content: &str, opts: &ReadOptions) -> Vec<LogLine> {
|
|||
lines.push(LogLine {
|
||||
number: idx + 1,
|
||||
level,
|
||||
text: raw.to_string(),
|
||||
text: super::censor(raw).into_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ pub async fn upload_log_at(
|
|||
path: &Path,
|
||||
) -> ClusterResult<MclogsUploadResponse> {
|
||||
let path = ensure_allowed(path)?;
|
||||
let mut content = read_file_string(&path).await?;
|
||||
let mut content = super::censor(&read_file_string(&path).await?).into_owned();
|
||||
|
||||
let line_count = content.lines().count();
|
||||
if line_count > MAX_LINES {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use chrono::{DateTime, Utc};
|
|||
pub use error::LogsError;
|
||||
pub use manage::{cluster_output_log, delete_log_at, list_cluster_logs, read_log_at};
|
||||
pub use mclogs::upload_log_at;
|
||||
pub use parse::parse_level;
|
||||
pub use parse::{censor, parse_level};
|
||||
|
||||
/// No launcher variant on purpose the launcher's own logs live in the log console
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,21 @@
|
|||
use std::borrow::Cow;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use super::LogLevel;
|
||||
|
||||
static SECRET: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r#"(--accessToken|--session(?:Id)?)([\s,=]+)[^\s,\[\]'"]+|(token:)[^:\s]+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*"#,
|
||||
)
|
||||
.expect("secret pattern")
|
||||
});
|
||||
|
||||
pub fn censor(line: &str) -> Cow<'_, str> {
|
||||
SECRET.replace_all(line, "${1}${2}${3}[censored]")
|
||||
}
|
||||
|
||||
pub fn parse_level(line: &str) -> Option<LogLevel> {
|
||||
let bytes = line.as_bytes();
|
||||
let mut i = 0;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use std::fmt::Write as _;
|
||||
use std::path::Path;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -5,7 +6,7 @@ use std::time::Duration;
|
|||
|
||||
use chrono::{DateTime, Utc};
|
||||
use interfrost::api::minecraft::ArgumentType;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::ClusterStage;
|
||||
|
|
@ -395,6 +396,14 @@ async fn start(
|
|||
.args(mc_args)
|
||||
.current_dir(&cwd);
|
||||
|
||||
let header = format!(
|
||||
"OneClient {} · {} · {version_name} · Java {}\n\n{}\n\n",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
cluster.name,
|
||||
java.major,
|
||||
command_line(command.as_std(), &version_info.main_class),
|
||||
);
|
||||
|
||||
let log_path = oneclient_cluster::logs::cluster_output_log(&cluster)?;
|
||||
if let Some(parent) = log_path.parent() {
|
||||
polyio::create_dir_all(parent).await.ok();
|
||||
|
|
@ -405,10 +414,15 @@ async fn start(
|
|||
// The cloned handle shares the file offset
|
||||
// so stdout and stderr interleave instead of overwriting
|
||||
let handles = match tokio::fs::File::create(&log_path).await {
|
||||
Ok(out) => match out.try_clone().await {
|
||||
Ok(err) => Ok((out.into_std().await, err.into_std().await)),
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
Ok(mut out) => {
|
||||
if let Err(err) = out.write_all(header.as_bytes()).await {
|
||||
tracing::warn!(cluster_id, error = %err, "failed to write the launch header");
|
||||
}
|
||||
match out.try_clone().await {
|
||||
Ok(err) => Ok((out.into_std().await, err.into_std().await)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
|
|
@ -752,6 +766,66 @@ fn base_command(profile: &GameSettingsProfile, java_path: &str) -> (Command, Opt
|
|||
(command, Some(program.to_string()))
|
||||
}
|
||||
|
||||
fn command_line(command: &std::process::Command, main_class: &str) -> String {
|
||||
#[cfg(not(windows))]
|
||||
const CONTINUATION: &str = " \\\n ";
|
||||
#[cfg(windows)]
|
||||
const CONTINUATION: &str = " `\n ";
|
||||
|
||||
let mut out = String::new();
|
||||
if let Some(dir) = command.get_current_dir() {
|
||||
let _ = writeln!(out, "cd {}", shell_quote(&dir.to_string_lossy()));
|
||||
}
|
||||
|
||||
for (key, value) in command.get_envs() {
|
||||
let (key, Some(value)) = (key.to_string_lossy(), value) else {
|
||||
continue;
|
||||
};
|
||||
let value = shell_quote(&value.to_string_lossy());
|
||||
#[cfg(not(windows))]
|
||||
let _ = write!(out, "{key}={value} ");
|
||||
#[cfg(windows)]
|
||||
let _ = writeln!(out, "$env:{key} = {value}");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
out += "& ";
|
||||
}
|
||||
out += &shell_quote(&command.get_program().to_string_lossy());
|
||||
|
||||
for arg in command.get_args() {
|
||||
let arg = arg.to_string_lossy();
|
||||
let arg = oneclient_cluster::logs::censor(&arg);
|
||||
out += if arg.starts_with('-') || arg == main_class {
|
||||
CONTINUATION
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
out += &shell_quote(&arg);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn shell_quote(arg: &str) -> String {
|
||||
let plain = !arg.is_empty()
|
||||
&& arg
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || b"_-./=:,+@%".contains(&b));
|
||||
if plain {
|
||||
arg.to_string()
|
||||
} else {
|
||||
format!("'{}'", arg.replace('\'', r"'\''"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn shell_quote(arg: &str) -> String {
|
||||
format!("'{}'", arg.replace('\'', "''"))
|
||||
}
|
||||
|
||||
fn apply_env(command: &mut Command, profile: &GameSettingsProfile) {
|
||||
command.env_remove("_JAVA_OPTIONS");
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ async fn emit(
|
|||
crash_watch: &CrashWatch,
|
||||
) {
|
||||
// Blank lines are kept so the console reads the way the log does
|
||||
let text = line.trim_end_matches(['\n', '\r']).to_string();
|
||||
let text = oneclient_cluster::logs::censor(line.trim_end_matches(['\n', '\r'])).into_owned();
|
||||
if let Some(recorder) = recorder {
|
||||
recorder.observe(&text).await;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue