run.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. #!/usr/bin/env python
  2. # -*- coding:utf-8 -*-
  3. """
  4. DDNS
  5. @author: New Future
  6. @modified: rufengsuixing
  7. """
  8. # nuitka-project: --product-version=0.0.0
  9. from os import path, environ, name as os_name
  10. from io import TextIOWrapper
  11. from subprocess import check_output
  12. from tempfile import gettempdir
  13. from logging import basicConfig, info, warning, error, debug
  14. import sys
  15. from util import ip
  16. from util.cache import Cache
  17. from util.config import init_config, get_config
  18. __version__ = "${BUILD_VERSION}@${BUILD_DATE}" # CI 时会被Tag替换
  19. __description__ = "automatically update DNS records to dynamic local IP [自动更新DNS记录指向本地IP]"
  20. __doc__ = """
  21. ddns[%s]
  22. (i) homepage or docs [文档主页]: https://ddns.newfuture.cc/
  23. (?) issues or bugs [问题和帮助]: https://github.com/NewFuture/DDNS/issues
  24. Copyright (c) New Future (MIT License)
  25. """ % (__version__)
  26. environ["DDNS_VERSION"] = "${BUILD_VERSION}"
  27. def is_false(value):
  28. """
  29. 判断值是否为 False
  30. 字符串 'false', 或者 False, 或者 'none';
  31. 0 不是 False
  32. """
  33. if isinstance(value, str):
  34. return value.strip().lower() in ['false', 'none']
  35. return value is False
  36. def get_ip(ip_type, index="default"):
  37. """
  38. get IP address
  39. """
  40. # CN: 捕获异常
  41. # EN: Catch exceptions
  42. value = None
  43. try:
  44. debug("get_ip(%s, %s)", ip_type, index)
  45. if is_false(index): # disabled
  46. return False
  47. elif isinstance(index, list): # 如果获取到的规则是列表,则依次判断列表中每一个规则,直到获取到IP
  48. for i in index:
  49. value = get_ip(ip_type, i)
  50. if value:
  51. break
  52. elif str(index).isdigit(): # 数字 local eth
  53. value = getattr(ip, "local_v" + ip_type)(index)
  54. elif index.startswith('cmd:'): # cmd
  55. value = str(check_output(index[4:]).strip().decode('utf-8'))
  56. elif index.startswith('shell:'): # shell
  57. value = str(check_output(
  58. index[6:], shell=True).strip().decode('utf-8'))
  59. elif index.startswith('url:'): # 自定义 url
  60. value = getattr(ip, "public_v" + ip_type)(index[4:])
  61. elif index.startswith('regex:'): # 正则 regex
  62. value = getattr(ip, "regex_v" + ip_type)(index[6:])
  63. else:
  64. value = getattr(ip, index + "_v" + ip_type)()
  65. except Exception as e:
  66. error("Failed to get %s address: %s", ip_type, e)
  67. return value
  68. def change_dns_record(dns, proxy_list, **kw):
  69. for proxy in proxy_list:
  70. if not proxy or (proxy.upper() in ['DIRECT', 'NONE']):
  71. dns.Config.PROXY = None
  72. else:
  73. dns.Config.PROXY = proxy
  74. record_type, domain = kw['record_type'], kw['domain']
  75. info("%s(%s) ==> %s [via %s]", domain, record_type, kw['ip'], proxy)
  76. try:
  77. return dns.update_record(domain, kw['ip'], record_type=record_type)
  78. except Exception as e:
  79. error("Failed to update %s record for %s: %s", record_type, domain, e)
  80. return False
  81. def update_ip(ip_type, cache, dns, proxy_list):
  82. """
  83. 更新IP
  84. """
  85. ipname = 'ipv' + ip_type
  86. domains = get_config(ipname)
  87. if not domains:
  88. return None
  89. if not isinstance(domains, list):
  90. domains = domains.strip('; ').replace(
  91. ',', ';').replace(' ', ';').split(';')
  92. index_rule = get_config('index' + ip_type, "default") # 从配置中获取index配置
  93. address = get_ip(ip_type, index_rule)
  94. if not address:
  95. error('Fail to get %s address!', ipname)
  96. return False
  97. elif cache and (address == cache[ipname]):
  98. info('%s address not changed, using cache.', ipname)
  99. return True
  100. record_type = (ip_type == '4') and 'A' or 'AAAA'
  101. update_fail = False # https://github.com/NewFuture/DDNS/issues/16
  102. for domain in domains:
  103. domain = domain.lower() # https://github.com/NewFuture/DDNS/issues/431
  104. if change_dns_record(dns, proxy_list, domain=domain, ip=address, record_type=record_type):
  105. update_fail = True
  106. if cache is not False:
  107. # 如果更新失败删除缓存
  108. cache[ipname] = update_fail and address
  109. def main():
  110. """
  111. 更新
  112. """
  113. init_config(__description__, __doc__, __version__)
  114. # Dynamicly import the dns module as configuration
  115. dns_provider = str(get_config('dns', 'dnspod').lower())
  116. dns = getattr(__import__('dns', fromlist=[dns_provider]), dns_provider)
  117. dns.Config.ID = get_config('id')
  118. dns.Config.TOKEN = get_config('token')
  119. dns.Config.TTL = get_config('ttl')
  120. basicConfig(
  121. level=get_config('log.level'),
  122. format='%(asctime)s [%(levelname)s] %(message)s',
  123. datefmt='%m-%d %H:%M:%S',
  124. filename=get_config('log.file'),
  125. )
  126. info("DDNS[ %s ] run: %s %s", __version__, os_name, sys.platform)
  127. if get_config("config"):
  128. info('loaded Config from: %s', path.abspath(get_config('config')))
  129. proxy = get_config('proxy') or 'DIRECT'
  130. proxy_list = proxy if isinstance(
  131. proxy, list) else proxy.strip(';').replace(',', ';').split(';')
  132. cache_config = get_config('cache', True)
  133. if cache_config is False:
  134. cache = cache_config
  135. elif cache_config is True:
  136. cache = Cache(path.join(gettempdir(), 'ddns.cache'))
  137. else:
  138. cache = Cache(cache_config)
  139. if cache is False:
  140. info('Cache is disabled!')
  141. elif not get_config('config_modified_time') or get_config('config_modified_time') >= cache.time:
  142. warning('Cache file is outdated.')
  143. cache.clear()
  144. else:
  145. debug('Cache is empty.')
  146. update_ip('4', cache, dns, proxy_list)
  147. update_ip('6', cache, dns, proxy_list)
  148. if __name__ == '__main__':
  149. encoding = sys.stdout.encoding
  150. if encoding is not None and encoding.lower() != 'utf-8' and hasattr(sys.stdout, 'buffer'):
  151. # 兼容windows 和部分ASCII编码的老旧系统
  152. sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
  153. sys.stderr = TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
  154. main()