run.py 7.0 KB

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