From 82f50ae5c09f636094e9895c3e0d3ed1b953efe5 Mon Sep 17 00:00:00 2001 From: imengying Date: Sat, 12 Sep 2026 10:44:35 +0800 Subject: [PATCH] Fixes and improvements --- .github/workflows/release.yml | 0 README.md | 2 +- agent/agent.sh | 19 ++---------- agent/install.ps1 | 0 agent/src/live.rs | 3 +- agent/src/live_batch.rs | 0 agent/src/main.rs | 26 ++++------------- agent/src/metrics.rs | 29 +++++++++++++++++-- agent/src/remote.rs | 0 agent/src/tests.rs | 22 +++++++------- agent/src/update.rs | 0 backend/src/db/mod.rs | 0 backend/src/db/queries.rs | 0 backend/src/main.rs | 0 backend/src/notify.rs | 18 ++++++++---- backend/src/routes/admin.rs | 0 backend/src/routes/auth.rs | 0 backend/src/routes/public.rs | 0 backend/src/routes/remote.rs | 0 backend/src/websocket/agent.rs | 0 docs/README.en.md | 2 +- frontend/src/App.tsx | 0 frontend/src/AppProvider.tsx | 0 frontend/src/admin-main.tsx | 0 frontend/src/components/AdminPanel.tsx | 0 frontend/src/components/AlertRuleManager.tsx | 0 frontend/src/components/LatencyManager.tsx | 0 frontend/src/components/LoginForm.tsx | 0 frontend/src/components/NodeCard.tsx | 0 frontend/src/components/NodeDetails.tsx | 0 frontend/src/components/PasswordInput.tsx | 0 frontend/src/components/TelegramSettings.tsx | 0 frontend/src/components/TurnstileWidget.tsx | 0 frontend/src/components/admin/AboutTab.tsx | 0 frontend/src/components/admin/RemoteTab.tsx | 2 +- .../src/components/admin/SettingsTabs.tsx | 0 frontend/src/components/admin/controls.tsx | 0 frontend/src/components/admin/shared.ts | 0 frontend/src/components/useVerification.tsx | 0 frontend/src/hooks/useNodeLatency.ts | 0 frontend/src/install.test.ts | 1 - frontend/src/live.test.ts | 0 frontend/src/live.ts | 0 frontend/src/password.ts | 0 frontend/src/styles/admin.css | 0 frontend/src/styles/base.css | 0 install.ps1 | 0 scripts/smoke-test.sh | 2 +- 48 files changed, 63 insertions(+), 63 deletions(-) mode change 100755 => 100644 .github/workflows/release.yml mode change 100755 => 100644 README.md mode change 100755 => 100644 agent/install.ps1 mode change 100755 => 100644 agent/src/live_batch.rs mode change 100755 => 100644 agent/src/main.rs mode change 100755 => 100644 agent/src/metrics.rs mode change 100755 => 100644 agent/src/remote.rs mode change 100755 => 100644 agent/src/tests.rs mode change 100755 => 100644 agent/src/update.rs mode change 100755 => 100644 backend/src/db/mod.rs mode change 100755 => 100644 backend/src/db/queries.rs mode change 100755 => 100644 backend/src/main.rs mode change 100755 => 100644 backend/src/notify.rs mode change 100755 => 100644 backend/src/routes/admin.rs mode change 100755 => 100644 backend/src/routes/auth.rs mode change 100755 => 100644 backend/src/routes/public.rs mode change 100755 => 100644 backend/src/routes/remote.rs mode change 100755 => 100644 backend/src/websocket/agent.rs mode change 100755 => 100644 docs/README.en.md mode change 100755 => 100644 frontend/src/App.tsx mode change 100755 => 100644 frontend/src/AppProvider.tsx mode change 100755 => 100644 frontend/src/admin-main.tsx mode change 100755 => 100644 frontend/src/components/AdminPanel.tsx mode change 100755 => 100644 frontend/src/components/AlertRuleManager.tsx mode change 100755 => 100644 frontend/src/components/LatencyManager.tsx mode change 100755 => 100644 frontend/src/components/LoginForm.tsx mode change 100755 => 100644 frontend/src/components/NodeCard.tsx mode change 100755 => 100644 frontend/src/components/NodeDetails.tsx mode change 100755 => 100644 frontend/src/components/PasswordInput.tsx mode change 100755 => 100644 frontend/src/components/TelegramSettings.tsx mode change 100755 => 100644 frontend/src/components/TurnstileWidget.tsx mode change 100755 => 100644 frontend/src/components/admin/AboutTab.tsx mode change 100755 => 100644 frontend/src/components/admin/RemoteTab.tsx mode change 100755 => 100644 frontend/src/components/admin/SettingsTabs.tsx mode change 100755 => 100644 frontend/src/components/admin/controls.tsx mode change 100755 => 100644 frontend/src/components/admin/shared.ts mode change 100755 => 100644 frontend/src/components/useVerification.tsx mode change 100755 => 100644 frontend/src/hooks/useNodeLatency.ts mode change 100755 => 100644 frontend/src/live.test.ts mode change 100755 => 100644 frontend/src/live.ts mode change 100755 => 100644 frontend/src/password.ts mode change 100755 => 100644 frontend/src/styles/admin.css mode change 100755 => 100644 frontend/src/styles/base.css mode change 100755 => 100644 install.ps1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml old mode 100755 new mode 100644 diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 177c63d..dd50522 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ curl -fsSL https://raw.githubusercontent.com/elysia62/NodeFlare/main/agent/agent Windows 对应 `-Endpoint` / `-Token` / `-Interval` / `-Mirror` 与 `-Update` / `-Status` / `-Uninstall`。 -> **关于 Token 传递**:`-t` 会出现在进程列表中。Agent 也支持环境变量 `NODEFLARE_AGENT_TOKEN`,以及 `--token-file <路径>` 从文件读取。Linux 安装脚本会把 Token 写入 `0600` 的 `agent.env` 供服务单元读取。 +Linux 使用 systemd 时,安装脚本将 Token 直接写入服务单元的 `Environment=NODEFLARE_AGENT_TOKEN=...`;`--update` 从该服务配置读取地址、Token 和历史保存间隔。 ## 监控口径与采样 diff --git a/agent/agent.sh b/agent/agent.sh index f98ff62..f5b3a4e 100755 --- a/agent/agent.sh +++ b/agent/agent.sh @@ -8,9 +8,6 @@ STATE_ROOT="/etc/nodeflare" STATE_DIR="$STATE_ROOT/agent" SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service" OPENRC_FILE="/etc/init.d/$SERVICE_NAME" -# Holds NODEFLARE_AGENT_TOKEN for systemd; a 0600 file inside the 0750 state dir -# is readable only by root/uninstall, unlike Environment= in the unit. -TOKEN_ENV_FILE="$STATE_DIR/agent.env" temporary="" backup_agent="" backup_service="" @@ -350,10 +347,6 @@ install_agent() { mv "$temporary" "$AGENT_FILE" log "正在启动 $init_system 服务" if [ "$init_system" = "systemd" ]; then - # The token goes into a 0600 file rather than Environment=, which any local - # user can read back with `systemctl show`. - printf 'NODEFLARE_AGENT_TOKEN=%s\n' "$token" > "$TOKEN_ENV_FILE" - chmod 600 "$TOKEN_ENV_FILE" printf '%s\n' \ '[Unit]' \ 'Description=NodeFlare Agent' \ @@ -366,7 +359,7 @@ install_agent() { 'Restart=always' \ 'RestartSec=10' \ 'NoNewPrivileges=true' \ - "EnvironmentFile=$TOKEN_ENV_FILE" \ + "Environment=NODEFLARE_AGENT_TOKEN=$token" \ "Environment=NODEFLARE_STATE_DIR=$STATE_DIR" \ '' \ '[Install]' \ @@ -411,15 +404,7 @@ load_installed_agent_config() { service_exec=$(sed -n 's/^ExecStart=//p' "$SERVICE_FILE" | head -n 1) endpoint=$(printf '%s\n' "$service_exec" | sed -n 's/.* -e \([^[:space:]]*\) -i \([0-9][0-9]*\)$/\1/p') interval=$(printf '%s\n' "$service_exec" | sed -n 's/.* -e \([^[:space:]]*\) -i \([0-9][0-9]*\)$/\2/p') - token="" - if [ -f "$TOKEN_ENV_FILE" ]; then - token=$(sed -n 's/^NODEFLARE_AGENT_TOKEN=//p' "$TOKEN_ENV_FILE" | head -n 1) - fi - # Fall back to the unit file so installs created before the token moved - # into an EnvironmentFile still update cleanly. - if [ -z "$token" ]; then - token=$(sed -n 's/^Environment=NODEFLARE_AGENT_TOKEN=//p' "$SERVICE_FILE" | head -n 1) - fi + token=$(sed -n 's/^Environment=NODEFLARE_AGENT_TOKEN=//p' "$SERVICE_FILE" | head -n 1) ;; openrc) [ -f "$OPENRC_FILE" ] || fail "未找到 Agent 服务配置,请先完成安装" diff --git a/agent/install.ps1 b/agent/install.ps1 old mode 100755 new mode 100644 diff --git a/agent/src/live.rs b/agent/src/live.rs index 0c28805..2adaa37 100755 --- a/agent/src/live.rs +++ b/agent/src/live.rs @@ -574,6 +574,7 @@ pub(crate) fn live_sender_loop(endpoint: &str, token: &str, worker: LiveSenderWo stats.live_persistence_failed(); drop_socket = true; } else { + backoff.reset(); prune_live_queue(&pending, ack.persisted_through_ts); accepted_through = accepted_through.max(ack.persisted_through_ts); next_probe_at = Instant::now() + ack_persist_interval(&ack); @@ -644,7 +645,6 @@ pub(crate) fn live_sender_loop(endpoint: &str, token: &str, worker: LiveSenderWo continue; } stats.live_batch_sent(batch_len); - backoff.reset(); if let Some(timestamp) = batch_last { accepted_through = accepted_through.max(timestamp); } @@ -663,6 +663,7 @@ pub(crate) fn live_sender_loop(endpoint: &str, token: &str, worker: LiveSenderWo stats.live_persistence_failed(); drop_socket = true; } else { + backoff.reset(); prune_live_queue(&pending, ack.persisted_through_ts); accepted_through = accepted_through.max(ack.persisted_through_ts); next_probe_at = Instant::now() + ack_persist_interval(&ack); diff --git a/agent/src/live_batch.rs b/agent/src/live_batch.rs old mode 100755 new mode 100644 diff --git a/agent/src/main.rs b/agent/src/main.rs old mode 100755 new mode 100644 index d7a955e..b76a359 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -120,12 +120,8 @@ struct CliOptions { #[arg(short = 'e', value_name = "URL")] endpoint: String, /// Agent token - #[arg(short = 't', value_name = "TOKEN", conflicts_with = "token_file")] + #[arg(short = 't', value_name = "TOKEN")] token: Option, - /// Read the Agent token from a file instead of -t, which keeps it out of - /// the process list - #[arg(long = "token-file", value_name = "PATH")] - token_file: Option, /// Initial report interval in seconds (15-3600) #[arg(short = 'i', value_name = "SECONDS", default_value_t = 60)] interval: u64, @@ -455,26 +451,16 @@ fn advance_deadline(mut deadline: Instant, interval: Duration, now: Instant) -> deadline } -/// Reads an Agent token from a file, trimming surrounding whitespace (editors -/// and shell redirection usually append a trailing newline). -fn read_token_file(path: &Path) -> Result { - let contents = fs::read_to_string(path) - .map_err(|error| format!("failed to read token file {}: {error}", path.display()))?; - Ok(contents.trim().to_string()) -} - fn runtime_config(options: &CliOptions) -> Result { let interval = options.interval; if !(15..=3600).contains(&interval) { return Err("interval must be between 15 and 3600 seconds".into()); } - let token = match &options.token { - Some(token) => token.clone(), - None => match options.token_file.as_deref() { - Some(path) => read_token_file(path)?, - None => env::var("NODEFLARE_AGENT_TOKEN").unwrap_or_default(), - }, - }; + let token = options + .token + .clone() + .or_else(|| env::var("NODEFLARE_AGENT_TOKEN").ok()) + .unwrap_or_default(); let endpoint = options.endpoint.clone(); if token.is_empty() || token.len() > 512 || token.chars().any(char::is_whitespace) { return Err("token is invalid".into()); diff --git a/agent/src/metrics.rs b/agent/src/metrics.rs old mode 100755 new mode 100644 index a3cb1cb..657f33c --- a/agent/src/metrics.rs +++ b/agent/src/metrics.rs @@ -57,6 +57,8 @@ pub(crate) fn output_with_timeout( mut process: Command, timeout: Duration, ) -> Option { + #[cfg(unix)] + process.process_group(0); let mut child = process .stdin(Stdio::null()) .stdout(Stdio::piped()) @@ -75,13 +77,13 @@ pub(crate) fn output_with_timeout( match child.try_wait() { Ok(Some(status)) => break Some(status), Ok(None) if started.elapsed() >= timeout => { - let _ = child.kill(); + terminate_command_tree(&mut child); let _ = child.wait(); break None; } Ok(None) => thread::sleep(Duration::from_millis(20)), Err(_) => { - let _ = child.kill(); + terminate_command_tree(&mut child); let _ = child.wait(); break None; } @@ -97,6 +99,29 @@ pub(crate) fn output_with_timeout( }) } +fn terminate_command_tree(child: &mut Child) { + #[cfg(unix)] + { + let group = format!("-{}", child.id()); + let _ = Command::new("kill") + .args(["-KILL", "--", group.as_str()]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } + #[cfg(target_os = "windows")] + { + let _ = Command::new("taskkill") + .args(["/PID", &child.id().to_string(), "/T", "/F"]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } + let _ = child.kill(); +} + pub(crate) fn command(name: &str, args: &[&str]) -> String { let mut process = Command::new(name); process.args(args); diff --git a/agent/src/remote.rs b/agent/src/remote.rs old mode 100755 new mode 100644 diff --git a/agent/src/tests.rs b/agent/src/tests.rs old mode 100755 new mode 100644 index 00be37c..e7939e4 --- a/agent/src/tests.rs +++ b/agent/src/tests.rs @@ -430,6 +430,16 @@ fn remote_commands_are_terminated_after_the_deadline() { assert!(result.result.contains("已终止")); } +#[cfg(unix)] +#[test] +fn command_timeout_terminates_descendants_without_waiting_for_pipes() { + let mut command = std::process::Command::new("sh"); + command.args(["-c", "sleep 2 & wait"]); + let started = Instant::now(); + assert!(super::output_with_timeout(command, Duration::from_millis(50)).is_none()); + assert!(started.elapsed() < Duration::from_secs(1)); +} + #[test] fn combined_remote_output_is_bounded_at_utf8_boundaries() { let result = remote_result_text( @@ -628,18 +638,6 @@ fn parses_cli_options() { assert!(parsed.once); assert!(CliOptions::try_parse_from(["agent", "-t"]).is_err()); assert!(CliOptions::try_parse_from(["agent", "-t", "first", "-t", "second"]).is_err()); - assert!( - CliOptions::try_parse_from([ - "agent", - "-e", - "https://monitor.example.com", - "-t", - "agent-token", - "--token-file", - "/run/nodeflare/token", - ]) - .is_err() - ); assert!(CliOptions::try_parse_from(["agent", "--once", "--collect"]).is_err()); } diff --git a/agent/src/update.rs b/agent/src/update.rs old mode 100755 new mode 100644 diff --git a/backend/src/db/mod.rs b/backend/src/db/mod.rs old mode 100755 new mode 100644 diff --git a/backend/src/db/queries.rs b/backend/src/db/queries.rs old mode 100755 new mode 100644 diff --git a/backend/src/main.rs b/backend/src/main.rs old mode 100755 new mode 100644 diff --git a/backend/src/notify.rs b/backend/src/notify.rs old mode 100755 new mode 100644 index 76fdc48..51b2596 --- a/backend/src/notify.rs +++ b/backend/src/notify.rs @@ -287,9 +287,8 @@ async fn send( .json(&payload) .send() .await - .map_err(reqwest::Error::without_url)? - .error_for_status() .map_err(reqwest::Error::without_url)?; + let status = response.status(); let body = response .bytes() .await @@ -297,8 +296,8 @@ async fn send( if body.len() > 64 * 1024 { anyhow::bail!("Telegram response is too large"); } - let value: Value = serde_json::from_slice(&body)?; - if value.get("ok").and_then(Value::as_bool) != Some(true) { + let value = serde_json::from_slice::(&body).unwrap_or(Value::Null); + if !status.is_success() || value.get("ok").and_then(Value::as_bool) != Some(true) { // Surface Telegram's error_code/description (e.g. "chat not found", // "bot was blocked by the user") so admins can act on the failure; // neither field contains the bot token. @@ -306,11 +305,18 @@ async fn send( .get("description") .and_then(Value::as_str) .unwrap_or("no description"); + let status_text = if status.is_success() { + String::new() + } else { + format!(" HTTP {}", status.as_u16()) + }; match value.get("error_code").and_then(Value::as_i64) { Some(code) => { - anyhow::bail!("Telegram rejected the message (error {code}): {description}") + anyhow::bail!( + "Telegram rejected the message{status_text} (error {code}): {description}" + ) } - None => anyhow::bail!("Telegram rejected the message: {description}"), + None => anyhow::bail!("Telegram rejected the message{status_text}: {description}"), } } Ok(()) diff --git a/backend/src/routes/admin.rs b/backend/src/routes/admin.rs old mode 100755 new mode 100644 diff --git a/backend/src/routes/auth.rs b/backend/src/routes/auth.rs old mode 100755 new mode 100644 diff --git a/backend/src/routes/public.rs b/backend/src/routes/public.rs old mode 100755 new mode 100644 diff --git a/backend/src/routes/remote.rs b/backend/src/routes/remote.rs old mode 100755 new mode 100644 diff --git a/backend/src/websocket/agent.rs b/backend/src/websocket/agent.rs old mode 100755 new mode 100644 diff --git a/docs/README.en.md b/docs/README.en.md old mode 100755 new mode 100644 index acdafd5..c97fd24 --- a/docs/README.en.md +++ b/docs/README.en.md @@ -136,7 +136,7 @@ Per-platform scripts: On Windows the equivalents are `-Endpoint` / `-Token` / `-Interval` / `-Mirror` and `-Update` / `-Status` / `-Uninstall`. -> **About the token**: `-t` is visible in the process list. The agent also accepts the `NODEFLARE_AGENT_TOKEN` environment variable and `--token-file `. The Linux installer writes the token to a `0600` `agent.env` file that the service unit reads. +On Linux with systemd, the installer writes the token directly into the service unit as `Environment=NODEFLARE_AGENT_TOKEN=...`. The `--update` command reads the endpoint, token, and history interval from that service configuration. ## Metrics and sampling diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/AppProvider.tsx b/frontend/src/AppProvider.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/admin-main.tsx b/frontend/src/admin-main.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/components/AdminPanel.tsx b/frontend/src/components/AdminPanel.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/components/AlertRuleManager.tsx b/frontend/src/components/AlertRuleManager.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/components/LatencyManager.tsx b/frontend/src/components/LatencyManager.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/components/LoginForm.tsx b/frontend/src/components/LoginForm.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/components/NodeCard.tsx b/frontend/src/components/NodeCard.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/components/NodeDetails.tsx b/frontend/src/components/NodeDetails.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/components/PasswordInput.tsx b/frontend/src/components/PasswordInput.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/components/TelegramSettings.tsx b/frontend/src/components/TelegramSettings.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/components/TurnstileWidget.tsx b/frontend/src/components/TurnstileWidget.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/components/admin/AboutTab.tsx b/frontend/src/components/admin/AboutTab.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/components/admin/RemoteTab.tsx b/frontend/src/components/admin/RemoteTab.tsx old mode 100755 new mode 100644 index e8a1c62..32c1e44 --- a/frontend/src/components/admin/RemoteTab.tsx +++ b/frontend/src/components/admin/RemoteTab.tsx @@ -56,7 +56,7 @@ export function RemoteTab({ locale, servers, busy, selectTab, createRemoteTask, {remoteTasks.length > 0 ?

{ui(locale, "执行结果", "Results")}

{remoteTasksActive ? {remotePollingUntil ? : null}{remotePollingUntil ? ui(locale, "等待结果,每 2 秒自动刷新", "Waiting for results; auto-refreshing every 2s") : ui(locale, "自动刷新已暂停", "Auto-refresh paused")} : {ui(locale, "本次命令已结束", "This run has finished")}}
- {remoteTasksActive && !remotePollingUntil ?

ui(locale, "已等待 1 分钟,命令可能仍在执行。点击“刷新结果”可继续查询,暂停刷新不会停止命令。", "Waited 1 minute; the command may still be running. Click \"Refresh results\" to keep polling; pausing refresh does not stop the command.")

: null} + {remoteTasksActive && !remotePollingUntil ?

{ui(locale, "已等待 1 分钟,命令可能仍在执行。点击“刷新结果”可继续查询,暂停刷新不会停止命令。", "Waited 1 minute; the command may still be running. Click \"Refresh results\" to keep polling; pausing refresh does not stop the command.")}

: null}
{ui(locale, "本次命令", "Command")}{remoteTasks[0]?.command}
{remoteTasks.map((task) => { diff --git a/frontend/src/components/admin/SettingsTabs.tsx b/frontend/src/components/admin/SettingsTabs.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/components/admin/controls.tsx b/frontend/src/components/admin/controls.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/components/admin/shared.ts b/frontend/src/components/admin/shared.ts old mode 100755 new mode 100644 diff --git a/frontend/src/components/useVerification.tsx b/frontend/src/components/useVerification.tsx old mode 100755 new mode 100644 diff --git a/frontend/src/hooks/useNodeLatency.ts b/frontend/src/hooks/useNodeLatency.ts old mode 100755 new mode 100644 diff --git a/frontend/src/install.test.ts b/frontend/src/install.test.ts index 3c21f3f..f611eaa 100644 --- a/frontend/src/install.test.ts +++ b/frontend/src/install.test.ts @@ -422,7 +422,6 @@ describe("agent installer manual update", () => { set -eu ${functions} init_system=${init} - TOKEN_ENV_FILE=$SERVICE_CONFIG.env SERVICE_FILE=$SERVICE_CONFIG OPENRC_FILE=$SERVICE_CONFIG load_installed_agent_config diff --git a/frontend/src/live.test.ts b/frontend/src/live.test.ts old mode 100755 new mode 100644 diff --git a/frontend/src/live.ts b/frontend/src/live.ts old mode 100755 new mode 100644 diff --git a/frontend/src/password.ts b/frontend/src/password.ts old mode 100755 new mode 100644 diff --git a/frontend/src/styles/admin.css b/frontend/src/styles/admin.css old mode 100755 new mode 100644 diff --git a/frontend/src/styles/base.css b/frontend/src/styles/base.css old mode 100755 new mode 100644 diff --git a/install.ps1 b/install.ps1 old mode 100755 new mode 100644 diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 4b14db3..2dccfac 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -15,7 +15,7 @@ monitor_curl() { # --fail-with-body was added in curl 7.76; older builds (e.g. CentOS 7 ships # 7.29) would abort with an unknown-option error, so fall back to --fail. -if curl --help 2>&1 | grep -q -- '--fail-with-body'; then +if curl --help all 2>&1 | grep -q -- '--fail-with-body'; then CURL_FAIL_FLAG=--fail-with-body else CURL_FAIL_FLAG=--fail