| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- # The contents of this file are subject to the BitTorrent Open Source License
- # Version 1.1 (the License). You may not copy or use this file, in either
- # source code or executable form, except in compliance with the License. You
- # may obtain a copy of the License at http://www.bittorrent.com/license/.
- #
- # Software distributed under the License is distributed on an AS IS basis,
- # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- # for the specific language governing rights and limitations under the
- # License.
- # written by Matt Chisholm
- import os
- import pickle
- import logging
- from BTL.translation import _
- from BTL import infohash_short
- from BTL.platform import app_name
- from BTL.exceptions import str_exc
- from BTL.ConvertedMetainfo import ConvertedMetainfo
- from BTL.bencode import bdecode
- from BTL.platform import encode_for_filesystem
- from BTL.defer import ThreadedDeferred, wrap_task
- from BTL.yielddefer import launch_coroutine
- from BTL.obsoletepythonsupport import set
- from BTL.hash import sha
- from BitTorrent import version, BTFailure
- from BitTorrent import zurllib
- from BitTorrent import GetTorrent
- from BitTorrent.platform import osx, get_temp_dir, doc_root, os_version
- from BitTorrent.MultiTorrent import TorrentAlreadyRunning
- from BitTorrent.MultiTorrent import TorrentAlreadyInQueue, UnknownInfohash
- # needed for py2exe to include the public key lib
- from Crypto.PublicKey import DSA
- from TorrentButler import TorrentButler
- from NewVersion import Version
- version_host = 'http://version.bittorrent.com/'
- download_url = 'http://www.bittorrent.com/download.html'
- DEBUG = False
- known_autoupdates = [
- (u'BitTorrent-4.2.2.exe', '66c45a77dc29aadca5f0c2d0cd6209ab6d562125'),
- (u'BitTorrent-4.20.0.exe', 'ad859d8ee38ba8590b09ab3cc1faef5e156ca6eb'),
- (u'BitTorrent-4.20.1.exe', 'd6aaf41faa0e2c8253672f3159f4cc5d8c5f850f'),
- (u'BitTorrent-4.20.2.exe', '4f726499ea54cec9c68838e950e2feebbf8dd06c'),
- (u'BitTorrent-4.20.3.1.exe', '233ca503d5d1f1d2aeb40f1ad45f03b8e962dffb'),
- (u'BitTorrent-4.20.3.2.exe', 'ee1e3ea19ded24ead480f81b7b8abbd376988b59'),
- (u'BitTorrent-4.20.3.exe', 'fa20f9e611b3b9c9cafe8c270dfc95f74d7ff385'),
- (u'BitTorrent-4.20.4.exe', 'a6c56171a44ba4273ffe3ffe868f97329f6ccbb9'),
- (u'BitTorrent-4.20.5.exe', '87a8291f7ac19dec57f8ea28c9785bdf5b4e517e'),
- (u'BitTorrent-4.20.6.exe', '697e24ba619bf4ef7d5147323c4659afdff15a20'),
- (u'BitTorrent-4.20.7.exe', 'ed25fb825c18d5555172e1b8e51c5142523f8478'),
- (u'BitTorrent-4.20.8.exe', '61d6be5f1c8a70672b6e607df7f8d0049381efc5'),
- (u'BitTorrent-4.3.6-Beta.exe', '66c45a77dc29aadca5f0c2d0cd6209ab6d562125'),
- (u'BitTorrent-4.4.0.exe', '66c45a77dc29aadca5f0c2d0cd6209ab6d562125'),
- (u'BitTorrent-4.4.1.exe', '66c45a77dc29aadca5f0c2d0cd6209ab6d562125'),
- (u'BitTorrent-4.9.3-Beta.exe', 'e04c381ced55270f012981098c9cb48819022f33'),
- (u'BitTorrent-4.9.4-Beta.exe', '082f961bc6dfffeb19eca056d99745d9fa3f3420'),
- (u'BitTorrent-4.9.5-Beta.exe', '3713e6760255cb454877e6d3cfea7b5aada45798'),
- (u'BitTorrent-4.9.6-Beta.exe', '841c8057e9ff135333eddcd42147dbe997ee4092'),
- (u'BitTorrent-4.9.7-Beta.exe', 'e7a17e56805644ced82a5c5cd9584401953b5bff'),
- (u'BitTorrent-4.9.8-Beta.exe', '885335197d45963d9616492ddff90609dc7ba979'),
- (u'BitTorrent-4.9.9-Beta.exe', '4289c4c5976c07af2058b8f97d6f48bde9be3184'),
- ]
- class AutoUpdateButler(TorrentButler):
- def __init__(self, multitorrent, rawserver,
- test_new_version=None, test_current_version=None):
- TorrentButler.__init__(self, multitorrent)
- self.runs = 0
- self.rawserver = rawserver
- self.estate = set()
- self.old_updates = set()
- self.log_root = "core.AutoUpdateButler"
- self.logger = logging.getLogger(self.log_root)
- self.installable_version = None
- self.available_version = None
- self.current_version = Version.from_str(version)
- self.debug_mode = DEBUG
- self.delay = 60*60*24
- if self.debug_mode:
- self.delay = 10
- if test_new_version:
- test_new_version = Version.from_str(test_new_version)
- self.debug_mode = True
- self.debug('__init__() turning debug on')
- def _hack_get_available(url):
- self.debug('_hack_get_available() run#%d: returning %s' % (self.runs, str(test_new_version)))
- return test_new_version
- self._get_available = _hack_get_available
- if test_current_version:
- self.debug_mode = True
- self.current_version = Version.from_str(test_current_version)
- self.version_site = version_host
- # The version URL format is:
- # http:// VERSION_SITE / OS_NAME / (LEGACY /) BETA or STABLE
- # LEGACY means that the user is on a version of an OS that has
- # been deemed "legacy", and as such the latest client version
- # for their OS version may be different than the latest client
- # version for the OS in general. For example, if we are going
- # to roll a version that requires WinXP/2K or greater, or a
- # version that requires OSX 10.5 or greater, we may maintain
- # an older version for Win98 or OSX 10.4 in OS_NAME/legacy/.
- if os.name == 'nt':
- self.version_site += 'win32/'
- if os_version not in ('XP', '2000', '2003'):
- self.version_site += 'legacy/'
- elif osx:
- self.version_site += 'osx/'
- elif self.debug_mode:
- self.version_site += 'win32/'
- self.installer_dir = self._calc_installer_dir()
- # kick it off
- self.rawserver.add_task(0, self.check_version)
- def get_auto_update_status(self):
- r = None, None
- if not self._can_install():
- # Auto-update doesn't work here, so just notify the user
- # of the new available version.
- r = self.available_version, None, self.delay
- self.available_version = None
- elif self.installable_version is not None:
- # Auto-update is done, notify the user of the version
- # ready to install.
- r = self.available_version, self.installable_version, self.delay
- self.available_version = None
- else:
- # Auto-update is in progress, don't tell the user
- # anything, and don't reset anything.
- r = None, None, None
- return r
- def butle(self):
- for i in list(self.estate):
- try:
- t = self.multitorrent.get_torrent(i)
- if t.state == 'initialized':
- self.multitorrent.start_torrent(t.infohash)
- torrent, status = self.multitorrent.torrent_status(i)
- if torrent.completed:
- self.finished(t)
- except UnknownInfohash:
- self.debug('butle() removing ' + infohash_short(i))
- self.estate.remove(i)
- self.installable_version = None
- self.available_version = None
- for v, i in list(self.old_updates):
- self.old_updates.discard((v, i))
- self.logger.warning(_("Cleaning up old autoupdate %s") % v)
- try:
- self.multitorrent.remove_torrent(i, del_files=True)
- except UnknownInfohash:
- pass
- def butles(self, torrent):
- id = (torrent.metainfo.name, torrent.infohash.encode('hex'))
- if (torrent.hidden or torrent.is_auto_update) and id in known_autoupdates:
- self.old_updates.add((torrent.metainfo.name, torrent.infohash))
- return True
- return torrent.infohash in self.estate and torrent.is_initialized()
- def started(self, torrent):
- """Only run the most recently added torrent"""
- if self.butles(torrent):
- removable = self.estate - set([torrent.infohash])
- for i in removable:
- self.estate.discard(i)
- self.multitorrent.remove_torrent(i, del_files=True)
- def finished(self, torrent):
- """Launch the auto-updater"""
- self.debug('finished() called for ' + infohash_short(torrent.infohash))
- if self.butles(torrent):
- self.debug('finished() setting installable version to ' + infohash_short(torrent.infohash))
- self.installable_version = torrent.infohash
- # Auto-update specific methods
- def debug(self, message):
- if self.debug_mode:
- self.logger.warning(message)
- def _can_install(self):
- """Return True if this OS supports auto-updates."""
- if self.debug_mode:
- return True
- if self.installer_dir is None:
- return False
- if os.name == 'nt':
- return True
- elif osx:
- return True
- else:
- return False
- def _calc_installer_name(self, available_version):
- """Figure out the name of the installer for this OS."""
- if os.name == 'nt' or self.debug_mode:
- ext = 'exe'
- elif osx:
- ext = 'dmg'
- elif os.name == 'posix':
- ext = 'tar.gz'
- else:
- return
- parts = [app_name, str(available_version)]
- if available_version.is_beta():
- parts.append('Beta')
- name = '-'.join(parts)
- name += '.' + ext
- return name
- def _calc_installer_dir(self):
- """Find a place to store the installer while it's being downloaded."""
- temp_dir = get_temp_dir()
- return temp_dir
- def _get_available(self, url):
- """Get the available version from the version site. The
- command line option --new_version X.Y.Z overrides this method
- and returns 'X.Y.Z' instead."""
- self.debug('_get_available() run#%d: hitting url %s' % (self.runs, url))
- try:
- u = zurllib.urlopen(url)
- s = u.read()
- s = s.strip()
- except:
- raise BTFailure(_("Could not get latest version from %s")%url)
- try:
- # we're luck asserts are turned off in production.
- # this assert is false for 4.20.X
- #assert len(s) == 5
- available_version = Version.from_str(s)
- except:
- raise BTFailure(_("Could not parse new version string from %s")%url)
- return available_version
- def _get_torrent(self, installer_url):
- """Get the .torrent file from the version site."""
- torrentfile = None
- try:
- torrentfile = GetTorrent.get_url(installer_url)
- except GetTorrent.GetTorrentException, e:
- self.debug('_get_torrent() run#%d: failed to download torrent file %s: %s' %
- (self.runs, installer_url, str_exc(e)))
- pass
- return torrentfile
- def _get_signature(self, installer_url):
- """Get the signature (.sign) file from the version site, and
- unpickle the signature. The sign file is a signature of the
- .torrent file created with the auto-update tool in
- auto-update/sign_file.py."""
- signature = None
- try:
- signfile = zurllib.urlopen(installer_url + '.sign')
- except:
- self.debug('_get_signature() run#%d: failed to download signfile %s.sign' %
- (self.runs, installer_url))
- pass
- else:
- try:
- signature = pickle.load(signfile)
- except:
- self.debug('_get_signature() run#%d: failed to unpickle signfile %s' %
- (self.runs, signfile))
- pass
- return signature
- def _check_signature(self, torrentfile, signature):
- """Check the torrent file's signature using the public key."""
- public_key_file = open(os.path.join(doc_root, 'public.key'), 'rb')
- public_key = pickle.load(public_key_file)
- public_key_file.close()
- h = sha(torrentfile).digest()
- return public_key.verify(h, signature)
- def check_version(self):
- """Launch the actual version check code in a coroutine since
- it needs to make three (or four, in beta) http requests, one
- disk read, and one decryption."""
- df = launch_coroutine(wrap_task(self.rawserver.external_add_task),
- self._check_version)
- def errback(e):
- self.logger.error('check_version() run #%d: ' % self.runs,
- exc_info=e.exc_info())
- df.addErrback(errback)
- def _check_version(self):
- """Actually check for an auto-update:
- 1. Check the version number from the file on the version site.
- 2. Check the stable version number from the file on the version site.
- 3. Notify the user and stop if they are on an OS with no installer.
- 4. Get the torrent file from the version site.
- 5. Get the signature from the version site.
- 6. Check the signature against the torrent file using the public key.
- 7a. Start the torrent if it's not in the client.
- 7b. Restart the torrent if it's in the client but not running.
- 8. Put the infohash of the torrent into estate so the butler knows
- to butle it.
- 9. AutoUpdateButler.started() ensures that only the most recent
- auto-update torrent is running.
- 10. AutoUpdateButler.finished() indicates the new version is available,
- the UI polls for that value later.
- Whether an auto-update was found and started or not, requeue
- the call to check_version() to run a day later. This means
- that the version check runs at startup, and once a day.
- """
- debug_prefix = '_check_version() run#%d: '%self.runs
- self.debug(debug_prefix + 'starting')
- url = self.version_site + self.current_version.name()
- df = ThreadedDeferred(wrap_task(self.rawserver.external_add_task),
- self._get_available, url, daemon=True)
- yield df
- try:
- available_version = df.getResult()
- except BTFailure, e:
- self.debug(debug_prefix + 'failed to load %s' % url)
- self._restart()
- return
- if available_version.is_beta():
- if available_version[1] != self.current_version[1]:
- available_version = self.current_version
- if self.current_version.is_beta():
- stable_url = self.version_site + 'stable'
- df = ThreadedDeferred(wrap_task(self.rawserver.external_add_task),
- self._get_available, stable_url)
- yield df
- try:
- available_stable_version = df.getResult()
- except BTFailure, e:
- self.debug(debug_prefix + 'failed to load %s' % url)
- self._restart()
- return
- if available_stable_version > available_version:
- available_version = available_stable_version
- self.debug(debug_prefix + 'got %s' % str(available_version))
- if available_version <= self.current_version:
- self.debug(debug_prefix + 'not updating old version %s' %
- str(available_version))
- self._restart()
- return
- if not self._can_install():
- self.debug(debug_prefix + 'cannot install on this os')
- self.available_version = available_version
- self._restart()
- return
- installer_name = self._calc_installer_name(available_version)
- installer_url = self.version_site + installer_name + '.torrent'
- fs_name = encode_for_filesystem(installer_name.decode('ascii'))[0]
- installer_path = os.path.join(self.installer_dir, fs_name)
- df = ThreadedDeferred(wrap_task(self.rawserver.external_add_task),
- self._get_torrent, installer_url)
- yield df
- torrentfile = df.getResult()
- df = ThreadedDeferred(wrap_task(self.rawserver.external_add_task),
- self._get_signature, installer_url)
- yield df
- signature = df.getResult()
- if torrentfile and signature:
- df = ThreadedDeferred(wrap_task(self.rawserver.external_add_task),
- self._check_signature, torrentfile, signature)
- yield df
- checked = df.getResult()
- if checked:
- self.debug(debug_prefix + 'signature verified successfully.')
- b = bdecode(torrentfile)
- metainfo = ConvertedMetainfo(b)
- infohash = metainfo.infohash
- self.available_version = available_version
- self.multitorrent.remove_auto_updates_except(infohash)
- try:
- df = self.multitorrent.create_torrent(metainfo, installer_path,
- installer_path, hidden=True,
- is_auto_update=True)
- yield df
- df.getResult()
- except TorrentAlreadyRunning:
- self.debug(debug_prefix + 'found auto-update torrent already running')
- except TorrentAlreadyInQueue:
- self.debug(debug_prefix + 'found auto-update torrent queued')
- else:
- self.debug(debug_prefix + 'starting auto-update download')
- self.debug(debug_prefix + 'adding to estate ' + infohash_short(infohash))
- self.estate.add(infohash)
- else:
- self.debug(debug_prefix + 'torrent file signature failed to verify.')
- pass
- else:
- self.debug(debug_prefix +
- 'couldn\'t get both torrentfile %s and signature %s' %
- (str(type(torrentfile)), str(type(signature))))
- self._restart()
- def _restart(self):
- """Run the auto-update check once a day, or every ten seconds
- in debug mode."""
- self.runs += 1
- self.rawserver.external_add_task(self.delay, self.check_version)
|