file.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. # -*- coding:utf-8 -*-
  2. """
  3. Configuration file loader for DDNS. supports both JSON and AST parsing.
  4. @author: NewFuture
  5. """
  6. from ast import literal_eval
  7. from json import loads as json_decode, dumps as json_encode
  8. from sys import stderr, stdout
  9. from ..util.comment import remove_comment
  10. from ..util.http import request
  11. from ..util.fileio import read_file, write_file
  12. def _process_multi_providers(config):
  13. # type: (dict) -> list[dict]
  14. """Process v4.1 providers format and return list of configs."""
  15. result = []
  16. # 提取全局配置(除providers之外的所有配置)
  17. global_config = _flatten_single_config(config, exclude_keys=["providers"], preserve_keys=["extra"])
  18. # 检查providers和dns字段不能同时使用
  19. if global_config.get("dns"):
  20. stderr.write("Error: 'providers' and 'dns' fields cannot be used simultaneously in config file!\n")
  21. raise ValueError("providers and dns fields conflict")
  22. # 为每个provider创建独立配置
  23. for provider_config in config["providers"]:
  24. # 验证provider必须有provider字段
  25. if not provider_config.get("provider"):
  26. stderr.write("Error: Each provider must have a 'provider' field!\n")
  27. raise ValueError("provider missing provider field")
  28. flat_config = global_config.copy() # 从全局配置开始
  29. provider_flat = _flatten_single_config(provider_config, exclude_keys=["provider"], preserve_keys=["extra"])
  30. flat_config["dns"] = provider_config.get("provider")
  31. flat_config.update(provider_flat)
  32. result.append(flat_config)
  33. return result
  34. def _flatten_single_config(config, exclude_keys=None, preserve_keys=None):
  35. # type: (dict, list[str]|None, list[str]|None) -> dict
  36. """
  37. Flatten a single config object with optional key exclusion and preservation.
  38. Args:
  39. config: Configuration dictionary to flatten
  40. exclude_keys: Keys to completely exclude from result
  41. preserve_keys: Keys to keep as nested dicts without flattening
  42. """
  43. if exclude_keys is None:
  44. exclude_keys = []
  45. if preserve_keys is None:
  46. preserve_keys = []
  47. flat_config = {}
  48. for k, v in config.items():
  49. if k in exclude_keys:
  50. continue
  51. if k in preserve_keys:
  52. # Keep as-is without flattening
  53. flat_config[k] = v
  54. elif isinstance(v, dict):
  55. for subk, subv in v.items():
  56. flat_config["{}_{}".format(k, subk)] = subv
  57. else:
  58. flat_config[k] = v
  59. return flat_config
  60. def load_config(config_path, proxy=None, ssl="auto"):
  61. # type: (str, list[str] | None, bool | str) -> dict|list[dict]
  62. """
  63. 加载配置文件并返回配置字典或配置字典数组。
  64. 支持本地文件和远程HTTP(S) URL。
  65. 对于单个对象返回dict,对于数组返回list[dict]。
  66. 优先尝试JSON解析,失败后尝试AST解析。
  67. Args:
  68. config_path (str): 配置文件路径或HTTP(S) URL
  69. proxy (list[str] | None): 代理列表,仅用于HTTP或HTTPS请求
  70. ssl (bool | str): SSL验证配置,仅用于HTTPS请求
  71. Returns:
  72. dict|list[dict]: 配置字典或配置字典数组
  73. Raises:
  74. Exception: 当配置文件加载失败时抛出异常
  75. """
  76. try:
  77. # 检查是否为远程URL
  78. if "://" in config_path:
  79. # 使用HTTP请求获取远程配置
  80. response = request("GET", config_path, proxies=proxy, verify=ssl, retries=3)
  81. if (response.status not in (200, None)) or not response.body:
  82. stderr.write("Failed to load {}: HTTP {} {}\n".format(config_path, response.status, response.reason))
  83. stderr.write("Response body: %s\n" % response.body)
  84. raise Exception("HTTP {}: {}".format(response.status, response.reason))
  85. content = response.body
  86. else:
  87. # 本地文件加载
  88. content = read_file(config_path)
  89. # 移除注释后尝试JSON解析
  90. content_without_comments = remove_comment(content)
  91. try:
  92. config = json_decode(content_without_comments)
  93. except (ValueError, SyntaxError) as json_error:
  94. # JSON解析失败,尝试AST解析
  95. try:
  96. config = literal_eval(content)
  97. stdout.write("Successfully loaded config file with AST parser: %s\n" % config_path)
  98. except (ValueError, SyntaxError) as ast_error:
  99. if config_path.endswith(".json"):
  100. stderr.write("JSON parsing failed for %s\n" % (config_path))
  101. raise json_error
  102. stderr.write(
  103. "Both JSON and AST parsing failed for %s\nJSON Error: %s\nAST Error: %s\n"
  104. % (config_path, json_error, ast_error)
  105. )
  106. raise ast_error
  107. except Exception as e:
  108. stderr.write("Failed to load config file `%s`: %s\n" % (config_path, e))
  109. raise
  110. # 处理配置格式:v4.1 providers格式或单个对象
  111. if "providers" in config and isinstance(config["providers"], list):
  112. return _process_multi_providers(config)
  113. else:
  114. return _flatten_single_config(config, preserve_keys=["extra"])
  115. def save_config(config_path, config):
  116. # type: (str, dict) -> bool
  117. """
  118. 保存配置到文件。
  119. Args:
  120. config_path (str): 配置文件路径
  121. config (dict): 配置字典
  122. Returns:
  123. bool: 保存成功返回True
  124. Raises:
  125. Exception: 保存失败时抛出异常
  126. """
  127. # 补全默认配置
  128. config = {
  129. "$schema": "https://ddns.newfuture.cc/schema/v4.1.json",
  130. "dns": config.get("dns", "debug"),
  131. "id": config.get("id", "YOUR ID or EMAIL for DNS Provider"),
  132. "token": config.get("token", "YOUR TOKEN or KEY for DNS Provider"),
  133. "ipv4": config.get("ipv4", ["ddns.newfuture.cc"]),
  134. "index4": config.get("index4", ["default"]),
  135. "ipv6": config.get("ipv6", []),
  136. "index6": config.get("index6", []),
  137. "ttl": config.get("ttl", 600),
  138. "line": config.get("line"),
  139. "proxy": config.get("proxy", []),
  140. "cache": config.get("cache", True),
  141. "ssl": config.get("ssl", "auto"),
  142. "log": {
  143. "file": config.get("log_file"),
  144. "level": config.get("log_level", "INFO"),
  145. "format": config.get("log_format"),
  146. "datefmt": config.get("log_datefmt"),
  147. },
  148. }
  149. try:
  150. content = json_encode(config, indent=2, ensure_ascii=False)
  151. # Python 2 兼容性:检查是否需要解码
  152. if hasattr(content, "decode"):
  153. content = content.decode("utf-8") # type: ignore
  154. write_file(config_path, content)
  155. return True
  156. except Exception:
  157. stderr.write("Cannot open config file to write: `%s`!\n" % config_path)
  158. raise