update.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. from __future__ import unicode_literals
  2. import io
  3. import json
  4. import traceback
  5. import hashlib
  6. import os
  7. import subprocess
  8. import sys
  9. from zipimport import zipimporter
  10. from .compat import (
  11. compat_open as open,
  12. compat_realpath,
  13. )
  14. from .utils import encode_compat_str
  15. from .version import __version__
  16. def rsa_verify(message, signature, key):
  17. from hashlib import sha256
  18. assert isinstance(message, bytes)
  19. byte_size = (len(bin(key[0])) - 2 + 8 - 1) // 8
  20. signature = ('%x' % pow(int(signature, 16), key[1], key[0])).encode()
  21. signature = (byte_size * 2 - len(signature)) * b'0' + signature
  22. asn1 = b'3031300d060960864801650304020105000420'
  23. asn1 += sha256(message).hexdigest().encode()
  24. if byte_size < len(asn1) // 2 + 11:
  25. return False
  26. expected = b'0001' + (byte_size - len(asn1) // 2 - 3) * b'ff' + b'00' + asn1
  27. return expected == signature
  28. def update_self(to_screen, verbose, opener):
  29. """Update the program file with the latest version from the repository"""
  30. UPDATE_URL = 'https://yt-dl.org/update/'
  31. VERSION_URL = UPDATE_URL + 'LATEST_VERSION'
  32. JSON_URL = UPDATE_URL + 'versions.json'
  33. UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537)
  34. if not isinstance(globals().get('__loader__'), zipimporter) and not hasattr(sys, 'frozen'):
  35. to_screen('It looks like you installed youtube-dl with a package manager, pip, setup.py or a tarball. Please use that to update.')
  36. return
  37. # Check if there is a new version
  38. try:
  39. newversion = opener.open(VERSION_URL).read().decode('utf-8').strip()
  40. except Exception:
  41. if verbose:
  42. to_screen(encode_compat_str(traceback.format_exc()))
  43. to_screen('ERROR: can\'t find the current version. Please try again later.')
  44. return
  45. if newversion == __version__:
  46. to_screen('youtube-dl is up-to-date (' + __version__ + ')')
  47. return
  48. # Download and check versions info
  49. try:
  50. versions_info = opener.open(JSON_URL).read().decode('utf-8')
  51. versions_info = json.loads(versions_info)
  52. except Exception:
  53. if verbose:
  54. to_screen(encode_compat_str(traceback.format_exc()))
  55. to_screen('ERROR: can\'t obtain versions info. Please try again later.')
  56. return
  57. if 'signature' not in versions_info:
  58. to_screen('ERROR: the versions file is not signed or corrupted. Aborting.')
  59. return
  60. signature = versions_info['signature']
  61. del versions_info['signature']
  62. if not rsa_verify(json.dumps(versions_info, sort_keys=True).encode('utf-8'), signature, UPDATES_RSA_KEY):
  63. to_screen('ERROR: the versions file signature is invalid. Aborting.')
  64. return
  65. version_id = versions_info['latest']
  66. def version_tuple(version_str):
  67. return tuple(map(int, version_str.split('.')))
  68. if version_tuple(__version__) >= version_tuple(version_id):
  69. to_screen('youtube-dl is up to date (%s)' % __version__)
  70. return
  71. to_screen('Updating to version ' + version_id + ' ...')
  72. version = versions_info['versions'][version_id]
  73. print_notes(to_screen, versions_info['versions'])
  74. # sys.executable is set to the full pathname of the exe-file for py2exe
  75. # though symlinks are not followed so that we need to do this manually
  76. # with help of realpath
  77. filename = compat_realpath(sys.executable if hasattr(sys, 'frozen') else sys.argv[0])
  78. if not os.access(filename, os.W_OK):
  79. to_screen('ERROR: no write permissions on %s' % filename)
  80. return
  81. # Py2EXE
  82. if hasattr(sys, 'frozen'):
  83. exe = filename
  84. directory = os.path.dirname(exe)
  85. if not os.access(directory, os.W_OK):
  86. to_screen('ERROR: no write permissions on %s' % directory)
  87. return
  88. try:
  89. urlh = opener.open(version['exe'][0])
  90. newcontent = urlh.read()
  91. urlh.close()
  92. except (IOError, OSError):
  93. if verbose:
  94. to_screen(encode_compat_str(traceback.format_exc()))
  95. to_screen('ERROR: unable to download latest version')
  96. return
  97. newcontent_hash = hashlib.sha256(newcontent).hexdigest()
  98. if newcontent_hash != version['exe'][1]:
  99. to_screen('ERROR: the downloaded file hash does not match. Aborting.')
  100. return
  101. try:
  102. with open(exe + '.new', 'wb') as outf:
  103. outf.write(newcontent)
  104. except (IOError, OSError):
  105. if verbose:
  106. to_screen(encode_compat_str(traceback.format_exc()))
  107. to_screen('ERROR: unable to write the new version')
  108. return
  109. try:
  110. bat = os.path.join(directory, 'youtube-dl-updater.bat')
  111. with open(bat, 'w') as batfile:
  112. batfile.write('''
  113. @echo off
  114. echo Waiting for file handle to be closed ...
  115. ping 127.0.0.1 -n 5 -w 1000 > NUL
  116. move /Y "%s.new" "%s" > NUL
  117. echo Updated youtube-dl to version %s.
  118. start /b "" cmd /c del "%%~f0"&exit /b"
  119. \n''' % (exe, exe, version_id))
  120. subprocess.Popen([bat]) # Continues to run in the background
  121. return # Do not show premature success messages
  122. except (IOError, OSError):
  123. if verbose:
  124. to_screen(encode_compat_str(traceback.format_exc()))
  125. to_screen('ERROR: unable to overwrite current version')
  126. return
  127. # Zip unix package
  128. elif isinstance(globals().get('__loader__'), zipimporter):
  129. try:
  130. urlh = opener.open(version['bin'][0])
  131. newcontent = urlh.read()
  132. urlh.close()
  133. except (IOError, OSError):
  134. if verbose:
  135. to_screen(encode_compat_str(traceback.format_exc()))
  136. to_screen('ERROR: unable to download latest version')
  137. return
  138. newcontent_hash = hashlib.sha256(newcontent).hexdigest()
  139. if newcontent_hash != version['bin'][1]:
  140. to_screen('ERROR: the downloaded file hash does not match. Aborting.')
  141. return
  142. try:
  143. with open(filename, 'wb') as outf:
  144. outf.write(newcontent)
  145. except (IOError, OSError):
  146. if verbose:
  147. to_screen(encode_compat_str(traceback.format_exc()))
  148. to_screen('ERROR: unable to overwrite current version')
  149. return
  150. to_screen('Updated youtube-dl. Restart youtube-dl to use the new version.')
  151. def get_notes(versions, fromVersion):
  152. notes = []
  153. for v, vdata in sorted(versions.items()):
  154. if v > fromVersion:
  155. notes.extend(vdata.get('notes', []))
  156. return notes
  157. def print_notes(to_screen, versions, fromVersion=__version__):
  158. notes = get_notes(versions, fromVersion)
  159. if notes:
  160. to_screen('PLEASE NOTE:')
  161. for note in notes:
  162. to_screen(note)