fix: panel api request to store new created user

This commit is contained in:
knylbyte
2025-12-18 18:37:10 +01:00
parent b9583819a5
commit e1867cdfd0
2 changed files with 185 additions and 10 deletions
+72 -9
View File
@@ -211,6 +211,34 @@ fn build_panel_url(base_url: &str, query_params: &[(String, String)]) -> Result<
Ok(url)
}
fn sanitize_panel_api_json_for_log(value: &Value) -> Value {
match value {
Value::Array(arr) => Value::Array(arr.iter().map(sanitize_panel_api_json_for_log).collect()),
Value::Object(obj) => {
let mut out = serde_json::Map::with_capacity(obj.len());
for (k, v) in obj {
if k.eq_ignore_ascii_case("api_key") || k.eq_ignore_ascii_case("apikey") || k.eq_ignore_ascii_case("token") {
out.insert(k.clone(), Value::String("***".to_string()));
continue;
}
if k.eq_ignore_ascii_case("username") || k.eq_ignore_ascii_case("password") {
out.insert(k.clone(), Value::String("***".to_string()));
continue;
}
if k.eq_ignore_ascii_case("url") {
if let Some(s) = v.as_str() {
out.insert(k.clone(), Value::String(sanitize_sensitive_info(s).into_owned()));
continue;
}
}
out.insert(k.clone(), sanitize_panel_api_json_for_log(v));
}
Value::Object(out)
}
other => other.clone(),
}
}
async fn panel_get_json(app_state: &AppState, url: Url) -> Result<Value, TuliproxError> {
let client = app_state.http_client.load();
let sanitized = sanitize_sensitive_info(url.as_str());
@@ -227,6 +255,10 @@ async fn panel_get_json(app_state: &AppState, url: Url) -> Result<Value, Tulipro
.map_err(|e| TuliproxError::new(TuliproxErrorKind::Info, format!("panel_api read response failed: {e}")))?;
let json: Value = serde_json::from_str(&body)
.map_err(|e| TuliproxError::new(TuliproxErrorKind::Info, format!("panel_api invalid json (http {status}): {e}")))?;
let json_for_log = sanitize_panel_api_json_for_log(&json);
if let Ok(json_str) = serde_json::to_string(&json_for_log) {
debug_if_enabled!("panel_api response (http {}): {}", status, sanitize_sensitive_info(&json_str));
}
Ok(json)
}
@@ -395,10 +427,29 @@ async fn patch_source_yml_add_alias(
};
let aliases_key = serde_yaml::Value::String("aliases".to_string());
if !inp_map.contains_key(&aliases_key) {
inp_map.insert(aliases_key.clone(), serde_yaml::Value::Sequence(vec![]));
let aliases_value = inp_map
.entry(aliases_key.clone())
.or_insert_with(|| serde_yaml::Value::Sequence(vec![]));
match aliases_value {
serde_yaml::Value::Sequence(_) => {}
serde_yaml::Value::Null => {
*aliases_value = serde_yaml::Value::Sequence(vec![]);
}
serde_yaml::Value::Mapping(_) => {
// Some users might have a single alias stored as a mapping instead of a list.
// Normalize to a list so we can append safely.
let old = std::mem::replace(aliases_value, serde_yaml::Value::Sequence(vec![]));
if let serde_yaml::Value::Mapping(map) = old {
if let serde_yaml::Value::Sequence(seq) = aliases_value {
seq.push(serde_yaml::Value::Mapping(map));
}
}
}
_ => {
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "panel_api: input.aliases is not a list in source.yml");
}
}
let Some(alias_seq) = inp_map.get_mut(&aliases_key).and_then(|v| v.as_sequence_mut()) else {
let Some(alias_seq) = aliases_value.as_sequence_mut() else {
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "panel_api: input.aliases is not a list in source.yml");
};
@@ -452,12 +503,24 @@ async fn patch_source_yml_update_exp_date(
if account_name == input_name {
inp_map.insert(serde_yaml::Value::String("exp_date".to_string()), serde_yaml::Value::Number(exp_date.into()));
inp_map.insert(serde_yaml::Value::String("enabled".to_string()), serde_yaml::Value::Bool(true));
} else if let Some(aliases) = inp_map.get_mut(serde_yaml::Value::String("aliases".to_string())).and_then(|v| v.as_sequence_mut()) {
for a in aliases.iter_mut() {
let Some(a_map) = a.as_mapping_mut() else { continue; };
let a_name = a_map.get(serde_yaml::Value::String("name".to_string())).and_then(|v| v.as_str());
if a_name == Some(account_name) {
a_map.insert(serde_yaml::Value::String("exp_date".to_string()), serde_yaml::Value::Number(exp_date.into()));
} else if let Some(aliases_value) = inp_map.get_mut(serde_yaml::Value::String("aliases".to_string())) {
// Normalize aliases to a sequence if the file contains a single mapping.
if matches!(aliases_value, serde_yaml::Value::Mapping(_)) {
let old = std::mem::replace(aliases_value, serde_yaml::Value::Sequence(vec![]));
if let serde_yaml::Value::Mapping(map) = old {
if let serde_yaml::Value::Sequence(seq) = aliases_value {
seq.push(serde_yaml::Value::Mapping(map));
}
}
}
if let Some(aliases) = aliases_value.as_sequence_mut() {
for a in aliases.iter_mut() {
let Some(a_map) = a.as_mapping_mut() else { continue; };
let a_name = a_map.get(serde_yaml::Value::String("name".to_string())).and_then(|v| v.as_str());
if a_name == Some(account_name) {
a_map.insert(serde_yaml::Value::String("exp_date".to_string()), serde_yaml::Value::Number(exp_date.into()));
}
}
}
}
+113 -1
View File
@@ -325,7 +325,7 @@ where
.map_err(|err| create_tuliprox_error!(TuliproxErrorKind::Info, "Could not write file {}: {}", &path.to_str().unwrap_or("?"), err))
}
pub fn format_sources_yaml_panel_api_query_params_flow_style(yaml: &str) -> String {
fn format_sources_yaml_panel_api_query_params_flow_style_impl(yaml: &str) -> String {
let has_trailing_newline = yaml.ends_with('\n');
let lines: Vec<&str> = yaml.split_terminator('\n').collect();
let mut out: Vec<String> = Vec::with_capacity(lines.len());
@@ -417,6 +417,97 @@ pub fn format_sources_yaml_panel_api_query_params_flow_style(yaml: &str) -> Stri
}
}
fn parse_yaml_key_value_line(s: &str) -> Option<(String, String)> {
let (key, value) = s.split_once(':')?;
let key = key.trim();
if key.is_empty() {
return None;
}
let value = value.trim();
let value = if value.is_empty() { "null" } else { value };
Some((key.to_string(), value.to_string()))
}
fn format_sources_yaml_aliases_flow_style_impl(yaml: &str) -> String {
let has_trailing_newline = yaml.ends_with('\n');
let lines: Vec<&str> = yaml.split_terminator('\n').collect();
let mut out: Vec<String> = Vec::with_capacity(lines.len());
let mut aliases_indent: Option<usize> = None;
let mut i = 0usize;
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim_start();
let indent = line.len().saturating_sub(trimmed.len());
if !trimmed.is_empty() && !trimmed.starts_with('#') {
if let Some(ai) = aliases_indent {
if indent < ai || (indent == ai && trimmed != "aliases:" && !trimmed.starts_with("- ")) {
aliases_indent = None;
}
}
if trimmed == "aliases:" {
aliases_indent = Some(indent);
}
}
// Convert:
// aliases:
// - name: foo
// url: http://...
// Into:
// - { name: foo, url: http://... }
if aliases_indent.is_some() && trimmed.starts_with("- ") && !trimmed.starts_with("- {") {
let item_indent = indent;
let first = trimmed.strip_prefix("- ").unwrap_or(trimmed);
if let Some((k, v)) = parse_yaml_key_value_line(first) {
let mut parts: Vec<(String, String)> = vec![(k, v)];
let mut j = i + 1;
while j < lines.len() {
let l2 = lines[j];
let t2 = l2.trim_start();
let ind2 = l2.len().saturating_sub(t2.len());
if t2.is_empty() || t2.starts_with('#') {
break;
}
if ind2 == item_indent.saturating_add(2) && !t2.starts_with("- ") {
if let Some((k2, v2)) = parse_yaml_key_value_line(t2) {
parts.push((k2, v2));
j += 1;
continue;
}
}
break;
}
let formatted = parts
.iter()
.map(|(kk, vv)| format!("{kk}: {vv}"))
.collect::<Vec<String>>()
.join(", ");
out.push(format!("{}- {{ {} }}", " ".repeat(item_indent), formatted));
i = j;
continue;
}
}
out.push(line.to_string());
i += 1;
}
if has_trailing_newline {
out.join("\n") + "\n"
} else {
out.join("\n")
}
}
pub fn format_sources_yaml_panel_api_query_params_flow_style(yaml: &str) -> String {
let yaml = format_sources_yaml_panel_api_query_params_flow_style_impl(yaml);
format_sources_yaml_aliases_flow_style_impl(&yaml)
}
pub async fn save_api_proxy(file_path: &str, backup_dir: &str, config: &ApiProxyConfigDto) -> Result<(), TuliproxError> {
write_config_file(file_path, backup_dir, config, "api-proxy.yml").await
}
@@ -492,4 +583,25 @@ mod tests {
assert!(out.contains("- { key: type, value: m3u }"));
assert!(!out.contains("- key: api_key\n"));
}
#[test]
fn test_sources_yaml_aliases_flow_style() {
let input = r"sources:
- inputs:
- name: demo
aliases:
- name: demo-u1
url: http://line.example.invalid
username: u1
password: p1
priority: 0
max_connections: 1
exp_date: 123
";
let out = format_sources_yaml_panel_api_query_params_flow_style(input);
assert!(
out.contains("- { name: demo-u1, url: http://line.example.invalid, username: u1, password: p1, priority: 0, max_connections: 1, exp_date: 123 }"),
"out:\n{out}"
);
}
}