update.py 7.2 KB

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