AutoUpdateButler.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. # The contents of this file are subject to the BitTorrent Open Source License
  2. # Version 1.1 (the License). You may not copy or use this file, in either
  3. # source code or executable form, except in compliance with the License. You
  4. # may obtain a copy of the License at http://www.bittorrent.com/license/.
  5. #
  6. # Software distributed under the License is distributed on an AS IS basis,
  7. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  8. # for the specific language governing rights and limitations under the
  9. # License.
  10. # written by Matt Chisholm
  11. import os
  12. import pickle
  13. import logging
  14. from BTL.translation import _
  15. from BTL import infohash_short
  16. from BTL.platform import app_name
  17. from BTL.exceptions import str_exc
  18. from BTL.ConvertedMetainfo import ConvertedMetainfo
  19. from BTL.bencode import bdecode
  20. from BTL.platform import encode_for_filesystem
  21. from BTL.defer import ThreadedDeferred, wrap_task
  22. from BTL.yielddefer import launch_coroutine
  23. from BTL.obsoletepythonsupport import set
  24. from BTL.hash import sha
  25. from BitTorrent import version, BTFailure
  26. from BitTorrent import zurllib
  27. from BitTorrent import GetTorrent
  28. from BitTorrent.platform import osx, get_temp_dir, doc_root, os_version
  29. from BitTorrent.MultiTorrent import TorrentAlreadyRunning
  30. from BitTorrent.MultiTorrent import TorrentAlreadyInQueue, UnknownInfohash
  31. # needed for py2exe to include the public key lib
  32. from Crypto.PublicKey import DSA
  33. from TorrentButler import TorrentButler
  34. from NewVersion import Version
  35. version_host = 'http://version.bittorrent.com/'
  36. download_url = 'http://www.bittorrent.com/download.html'
  37. DEBUG = False
  38. known_autoupdates = [
  39. (u'BitTorrent-4.2.2.exe', '66c45a77dc29aadca5f0c2d0cd6209ab6d562125'),
  40. (u'BitTorrent-4.20.0.exe', 'ad859d8ee38ba8590b09ab3cc1faef5e156ca6eb'),
  41. (u'BitTorrent-4.20.1.exe', 'd6aaf41faa0e2c8253672f3159f4cc5d8c5f850f'),
  42. (u'BitTorrent-4.20.2.exe', '4f726499ea54cec9c68838e950e2feebbf8dd06c'),
  43. (u'BitTorrent-4.20.3.1.exe', '233ca503d5d1f1d2aeb40f1ad45f03b8e962dffb'),
  44. (u'BitTorrent-4.20.3.2.exe', 'ee1e3ea19ded24ead480f81b7b8abbd376988b59'),
  45. (u'BitTorrent-4.20.3.exe', 'fa20f9e611b3b9c9cafe8c270dfc95f74d7ff385'),
  46. (u'BitTorrent-4.20.4.exe', 'a6c56171a44ba4273ffe3ffe868f97329f6ccbb9'),
  47. (u'BitTorrent-4.20.5.exe', '87a8291f7ac19dec57f8ea28c9785bdf5b4e517e'),
  48. (u'BitTorrent-4.20.6.exe', '697e24ba619bf4ef7d5147323c4659afdff15a20'),
  49. (u'BitTorrent-4.20.7.exe', 'ed25fb825c18d5555172e1b8e51c5142523f8478'),
  50. (u'BitTorrent-4.20.8.exe', '61d6be5f1c8a70672b6e607df7f8d0049381efc5'),
  51. (u'BitTorrent-4.3.6-Beta.exe', '66c45a77dc29aadca5f0c2d0cd6209ab6d562125'),
  52. (u'BitTorrent-4.4.0.exe', '66c45a77dc29aadca5f0c2d0cd6209ab6d562125'),
  53. (u'BitTorrent-4.4.1.exe', '66c45a77dc29aadca5f0c2d0cd6209ab6d562125'),
  54. (u'BitTorrent-4.9.3-Beta.exe', 'e04c381ced55270f012981098c9cb48819022f33'),
  55. (u'BitTorrent-4.9.4-Beta.exe', '082f961bc6dfffeb19eca056d99745d9fa3f3420'),
  56. (u'BitTorrent-4.9.5-Beta.exe', '3713e6760255cb454877e6d3cfea7b5aada45798'),
  57. (u'BitTorrent-4.9.6-Beta.exe', '841c8057e9ff135333eddcd42147dbe997ee4092'),
  58. (u'BitTorrent-4.9.7-Beta.exe', 'e7a17e56805644ced82a5c5cd9584401953b5bff'),
  59. (u'BitTorrent-4.9.8-Beta.exe', '885335197d45963d9616492ddff90609dc7ba979'),
  60. (u'BitTorrent-4.9.9-Beta.exe', '4289c4c5976c07af2058b8f97d6f48bde9be3184'),
  61. ]
  62. class AutoUpdateButler(TorrentButler):
  63. def __init__(self, multitorrent, rawserver,
  64. test_new_version=None, test_current_version=None):
  65. TorrentButler.__init__(self, multitorrent)
  66. self.runs = 0
  67. self.rawserver = rawserver
  68. self.estate = set()
  69. self.old_updates = set()
  70. self.log_root = "core.AutoUpdateButler"
  71. self.logger = logging.getLogger(self.log_root)
  72. self.installable_version = None
  73. self.available_version = None
  74. self.current_version = Version.from_str(version)
  75. self.debug_mode = DEBUG
  76. self.delay = 60*60*24
  77. if self.debug_mode:
  78. self.delay = 10
  79. if test_new_version:
  80. test_new_version = Version.from_str(test_new_version)
  81. self.debug_mode = True
  82. self.debug('__init__() turning debug on')
  83. def _hack_get_available(url):
  84. self.debug('_hack_get_available() run#%d: returning %s' % (self.runs, str(test_new_version)))
  85. return test_new_version
  86. self._get_available = _hack_get_available
  87. if test_current_version:
  88. self.debug_mode = True
  89. self.current_version = Version.from_str(test_current_version)
  90. self.version_site = version_host
  91. # The version URL format is:
  92. # http:// VERSION_SITE / OS_NAME / (LEGACY /) BETA or STABLE
  93. # LEGACY means that the user is on a version of an OS that has
  94. # been deemed "legacy", and as such the latest client version
  95. # for their OS version may be different than the latest client
  96. # version for the OS in general. For example, if we are going
  97. # to roll a version that requires WinXP/2K or greater, or a
  98. # version that requires OSX 10.5 or greater, we may maintain
  99. # an older version for Win98 or OSX 10.4 in OS_NAME/legacy/.
  100. if os.name == 'nt':
  101. self.version_site += 'win32/'
  102. if os_version not in ('XP', '2000', '2003'):
  103. self.version_site += 'legacy/'
  104. elif osx:
  105. self.version_site += 'osx/'
  106. elif self.debug_mode:
  107. self.version_site += 'win32/'
  108. self.installer_dir = self._calc_installer_dir()
  109. # kick it off
  110. self.rawserver.add_task(0, self.check_version)
  111. def get_auto_update_status(self):
  112. r = None, None
  113. if not self._can_install():
  114. # Auto-update doesn't work here, so just notify the user
  115. # of the new available version.
  116. r = self.available_version, None, self.delay
  117. self.available_version = None
  118. elif self.installable_version is not None:
  119. # Auto-update is done, notify the user of the version
  120. # ready to install.
  121. r = self.available_version, self.installable_version, self.delay
  122. self.available_version = None
  123. else:
  124. # Auto-update is in progress, don't tell the user
  125. # anything, and don't reset anything.
  126. r = None, None, None
  127. return r
  128. def butle(self):
  129. for i in list(self.estate):
  130. try:
  131. t = self.multitorrent.get_torrent(i)
  132. if t.state == 'initialized':
  133. self.multitorrent.start_torrent(t.infohash)
  134. torrent, status = self.multitorrent.torrent_status(i)
  135. if torrent.completed:
  136. self.finished(t)
  137. except UnknownInfohash:
  138. self.debug('butle() removing ' + infohash_short(i))
  139. self.estate.remove(i)
  140. self.installable_version = None
  141. self.available_version = None
  142. for v, i in list(self.old_updates):
  143. self.old_updates.discard((v, i))
  144. self.logger.warning(_("Cleaning up old autoupdate %s") % v)
  145. try:
  146. self.multitorrent.remove_torrent(i, del_files=True)
  147. except UnknownInfohash:
  148. pass
  149. def butles(self, torrent):
  150. id = (torrent.metainfo.name, torrent.infohash.encode('hex'))
  151. if (torrent.hidden or torrent.is_auto_update) and id in known_autoupdates:
  152. self.old_updates.add((torrent.metainfo.name, torrent.infohash))
  153. return True
  154. return torrent.infohash in self.estate and torrent.is_initialized()
  155. def started(self, torrent):
  156. """Only run the most recently added torrent"""
  157. if self.butles(torrent):
  158. removable = self.estate - set([torrent.infohash])
  159. for i in removable:
  160. self.estate.discard(i)
  161. self.multitorrent.remove_torrent(i, del_files=True)
  162. def finished(self, torrent):
  163. """Launch the auto-updater"""
  164. self.debug('finished() called for ' + infohash_short(torrent.infohash))
  165. if self.butles(torrent):
  166. self.debug('finished() setting installable version to ' + infohash_short(torrent.infohash))
  167. self.installable_version = torrent.infohash
  168. # Auto-update specific methods
  169. def debug(self, message):
  170. if self.debug_mode:
  171. self.logger.warning(message)
  172. def _can_install(self):
  173. """Return True if this OS supports auto-updates."""
  174. if self.debug_mode:
  175. return True
  176. if self.installer_dir is None:
  177. return False
  178. if os.name == 'nt':
  179. return True
  180. elif osx:
  181. return True
  182. else:
  183. return False
  184. def _calc_installer_name(self, available_version):
  185. """Figure out the name of the installer for this OS."""
  186. if os.name == 'nt' or self.debug_mode:
  187. ext = 'exe'
  188. elif osx:
  189. ext = 'dmg'
  190. elif os.name == 'posix':
  191. ext = 'tar.gz'
  192. else:
  193. return
  194. parts = [app_name, str(available_version)]
  195. if available_version.is_beta():
  196. parts.append('Beta')
  197. name = '-'.join(parts)
  198. name += '.' + ext
  199. return name
  200. def _calc_installer_dir(self):
  201. """Find a place to store the installer while it's being downloaded."""
  202. temp_dir = get_temp_dir()
  203. return temp_dir
  204. def _get_available(self, url):
  205. """Get the available version from the version site. The
  206. command line option --new_version X.Y.Z overrides this method
  207. and returns 'X.Y.Z' instead."""
  208. self.debug('_get_available() run#%d: hitting url %s' % (self.runs, url))
  209. try:
  210. u = zurllib.urlopen(url)
  211. s = u.read()
  212. s = s.strip()
  213. except:
  214. raise BTFailure(_("Could not get latest version from %s")%url)
  215. try:
  216. # we're luck asserts are turned off in production.
  217. # this assert is false for 4.20.X
  218. #assert len(s) == 5
  219. available_version = Version.from_str(s)
  220. except:
  221. raise BTFailure(_("Could not parse new version string from %s")%url)
  222. return available_version
  223. def _get_torrent(self, installer_url):
  224. """Get the .torrent file from the version site."""
  225. torrentfile = None
  226. try:
  227. torrentfile = GetTorrent.get_url(installer_url)
  228. except GetTorrent.GetTorrentException, e:
  229. self.debug('_get_torrent() run#%d: failed to download torrent file %s: %s' %
  230. (self.runs, installer_url, str_exc(e)))
  231. pass
  232. return torrentfile
  233. def _get_signature(self, installer_url):
  234. """Get the signature (.sign) file from the version site, and
  235. unpickle the signature. The sign file is a signature of the
  236. .torrent file created with the auto-update tool in
  237. auto-update/sign_file.py."""
  238. signature = None
  239. try:
  240. signfile = zurllib.urlopen(installer_url + '.sign')
  241. except:
  242. self.debug('_get_signature() run#%d: failed to download signfile %s.sign' %
  243. (self.runs, installer_url))
  244. pass
  245. else:
  246. try:
  247. signature = pickle.load(signfile)
  248. except:
  249. self.debug('_get_signature() run#%d: failed to unpickle signfile %s' %
  250. (self.runs, signfile))
  251. pass
  252. return signature
  253. def _check_signature(self, torrentfile, signature):
  254. """Check the torrent file's signature using the public key."""
  255. public_key_file = open(os.path.join(doc_root, 'public.key'), 'rb')
  256. public_key = pickle.load(public_key_file)
  257. public_key_file.close()
  258. h = sha(torrentfile).digest()
  259. return public_key.verify(h, signature)
  260. def check_version(self):
  261. """Launch the actual version check code in a coroutine since
  262. it needs to make three (or four, in beta) http requests, one
  263. disk read, and one decryption."""
  264. df = launch_coroutine(wrap_task(self.rawserver.external_add_task),
  265. self._check_version)
  266. def errback(e):
  267. self.logger.error('check_version() run #%d: ' % self.runs,
  268. exc_info=e.exc_info())
  269. df.addErrback(errback)
  270. def _check_version(self):
  271. """Actually check for an auto-update:
  272. 1. Check the version number from the file on the version site.
  273. 2. Check the stable version number from the file on the version site.
  274. 3. Notify the user and stop if they are on an OS with no installer.
  275. 4. Get the torrent file from the version site.
  276. 5. Get the signature from the version site.
  277. 6. Check the signature against the torrent file using the public key.
  278. 7a. Start the torrent if it's not in the client.
  279. 7b. Restart the torrent if it's in the client but not running.
  280. 8. Put the infohash of the torrent into estate so the butler knows
  281. to butle it.
  282. 9. AutoUpdateButler.started() ensures that only the most recent
  283. auto-update torrent is running.
  284. 10. AutoUpdateButler.finished() indicates the new version is available,
  285. the UI polls for that value later.
  286. Whether an auto-update was found and started or not, requeue
  287. the call to check_version() to run a day later. This means
  288. that the version check runs at startup, and once a day.
  289. """
  290. debug_prefix = '_check_version() run#%d: '%self.runs
  291. self.debug(debug_prefix + 'starting')
  292. url = self.version_site + self.current_version.name()
  293. df = ThreadedDeferred(wrap_task(self.rawserver.external_add_task),
  294. self._get_available, url, daemon=True)
  295. yield df
  296. try:
  297. available_version = df.getResult()
  298. except BTFailure, e:
  299. self.debug(debug_prefix + 'failed to load %s' % url)
  300. self._restart()
  301. return
  302. if available_version.is_beta():
  303. if available_version[1] != self.current_version[1]:
  304. available_version = self.current_version
  305. if self.current_version.is_beta():
  306. stable_url = self.version_site + 'stable'
  307. df = ThreadedDeferred(wrap_task(self.rawserver.external_add_task),
  308. self._get_available, stable_url)
  309. yield df
  310. try:
  311. available_stable_version = df.getResult()
  312. except BTFailure, e:
  313. self.debug(debug_prefix + 'failed to load %s' % url)
  314. self._restart()
  315. return
  316. if available_stable_version > available_version:
  317. available_version = available_stable_version
  318. self.debug(debug_prefix + 'got %s' % str(available_version))
  319. if available_version <= self.current_version:
  320. self.debug(debug_prefix + 'not updating old version %s' %
  321. str(available_version))
  322. self._restart()
  323. return
  324. if not self._can_install():
  325. self.debug(debug_prefix + 'cannot install on this os')
  326. self.available_version = available_version
  327. self._restart()
  328. return
  329. installer_name = self._calc_installer_name(available_version)
  330. installer_url = self.version_site + installer_name + '.torrent'
  331. fs_name = encode_for_filesystem(installer_name.decode('ascii'))[0]
  332. installer_path = os.path.join(self.installer_dir, fs_name)
  333. df = ThreadedDeferred(wrap_task(self.rawserver.external_add_task),
  334. self._get_torrent, installer_url)
  335. yield df
  336. torrentfile = df.getResult()
  337. df = ThreadedDeferred(wrap_task(self.rawserver.external_add_task),
  338. self._get_signature, installer_url)
  339. yield df
  340. signature = df.getResult()
  341. if torrentfile and signature:
  342. df = ThreadedDeferred(wrap_task(self.rawserver.external_add_task),
  343. self._check_signature, torrentfile, signature)
  344. yield df
  345. checked = df.getResult()
  346. if checked:
  347. self.debug(debug_prefix + 'signature verified successfully.')
  348. b = bdecode(torrentfile)
  349. metainfo = ConvertedMetainfo(b)
  350. infohash = metainfo.infohash
  351. self.available_version = available_version
  352. self.multitorrent.remove_auto_updates_except(infohash)
  353. try:
  354. df = self.multitorrent.create_torrent(metainfo, installer_path,
  355. installer_path, hidden=True,
  356. is_auto_update=True)
  357. yield df
  358. df.getResult()
  359. except TorrentAlreadyRunning:
  360. self.debug(debug_prefix + 'found auto-update torrent already running')
  361. except TorrentAlreadyInQueue:
  362. self.debug(debug_prefix + 'found auto-update torrent queued')
  363. else:
  364. self.debug(debug_prefix + 'starting auto-update download')
  365. self.debug(debug_prefix + 'adding to estate ' + infohash_short(infohash))
  366. self.estate.add(infohash)
  367. else:
  368. self.debug(debug_prefix + 'torrent file signature failed to verify.')
  369. pass
  370. else:
  371. self.debug(debug_prefix +
  372. 'couldn\'t get both torrentfile %s and signature %s' %
  373. (str(type(torrentfile)), str(type(signature))))
  374. self._restart()
  375. def _restart(self):
  376. """Run the auto-update check once a day, or every ten seconds
  377. in debug mode."""
  378. self.runs += 1
  379. self.rawserver.external_add_task(self.delay, self.check_version)