#!/usr/bin/env python2 # vim:fileencoding=utf-8 from __future__ import (unicode_literals, division, absolute_import, print_function) __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' import sys, os, shutil, subprocess, re, platform, signal, tempfile, hashlib, errno import ssl, socket, stat from contextlib import closing is64bit = platform.architecture()[0] == '64bit' DLURL = 'https://calibre-ebook.com/dist/linux'+('64' if is64bit else '32') DLURL = os.environ.get('CALIBRE_INSTALLER_LOCAL_URL', DLURL) py3 = sys.version_info[0] > 2 enc = getattr(sys.stdout, 'encoding', 'utf-8') or 'utf-8' if enc.lower() == 'ascii': enc = 'utf-8' calibre_version = signature = None urllib = __import__('urllib.request' if py3 else 'urllib', fromlist=1) has_ssl_verify = hasattr(ssl, 'create_default_context') if py3: unicode = str raw_input = input from urllib.parse import urlparse import http.client as httplib encode_for_subprocess = lambda x:x else: from future_builtins import map from urlparse import urlparse import httplib def encode_for_subprocess(x): if isinstance(x, unicode): x = x.encode(enc) return x class TerminalController: # {{{ BOL = '' #: Move the cursor to the beginning of the line UP = '' #: Move the cursor up one line DOWN = '' #: Move the cursor down one line LEFT = '' #: Move the cursor left one char RIGHT = '' #: Move the cursor right one char # Deletion: CLEAR_SCREEN = '' #: Clear the screen and move to home position CLEAR_EOL = '' #: Clear to the end of the line. CLEAR_BOL = '' #: Clear to the beginning of the line. CLEAR_EOS = '' #: Clear to the end of the screen # Output modes: BOLD = '' #: Turn on bold mode BLINK = '' #: Turn on blink mode DIM = '' #: Turn on half-bright mode REVERSE = '' #: Turn on reverse-video mode NORMAL = '' #: Turn off all modes # Cursor display: HIDE_CURSOR = '' #: Make the cursor invisible SHOW_CURSOR = '' #: Make the cursor visible # Terminal size: COLS = None #: Width of the terminal (None for unknown) LINES = None #: Height of the terminal (None for unknown) # Foreground colors: BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = '' # Background colors: BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = '' BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = '' _STRING_CAPABILITIES = """ BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1 CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0 HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split() _COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split() _ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split() def __init__(self, term_stream=sys.stdout): # Curses isn't available on all platforms try: import curses except: return # If the stream isn't a tty, then assume it has no capabilities. if not hasattr(term_stream, 'isatty') or not term_stream.isatty(): return # Check the terminal type. If we fail, then assume that the # terminal has no capabilities. try: curses.setupterm() except: return # Look up numeric capabilities. self.COLS = curses.tigetnum('cols') self.LINES = curses.tigetnum('lines') # Look up string capabilities. for capability in self._STRING_CAPABILITIES: (attrib, cap_name) = capability.split('=') setattr(self, attrib, self._escape_code(self._tigetstr(cap_name))) # Colors set_fg = self._tigetstr('setf') if set_fg: if not isinstance(set_fg, bytes): set_fg = set_fg.encode('utf-8') for i,color in zip(range(len(self._COLORS)), self._COLORS): setattr(self, color, self._escape_code(curses.tparm((set_fg), i))) set_fg_ansi = self._tigetstr('setaf') if set_fg_ansi: if not isinstance(set_fg_ansi, bytes): set_fg_ansi = set_fg_ansi.encode('utf-8') for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS): setattr(self, color, self._escape_code(curses.tparm((set_fg_ansi), i))) set_bg = self._tigetstr('setb') if set_bg: if not isinstance(set_bg, bytes): set_bg = set_bg.encode('utf-8') for i,color in zip(range(len(self._COLORS)), self._COLORS): setattr(self, 'BG_'+color, self._escape_code(curses.tparm((set_bg), i))) set_bg_ansi = self._tigetstr('setab') if set_bg_ansi: if not isinstance(set_bg_ansi, bytes): set_bg_ansi = set_bg_ansi.encode('utf-8') for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS): setattr(self, 'BG_'+color, self._escape_code(curses.tparm((set_bg_ansi), i))) def _escape_code(self, raw): if not raw: raw = '' if not isinstance(raw, unicode): raw = raw.decode('ascii') return raw def _tigetstr(self, cap_name): # String capabilities can include "delays" of the form "$<2>". # For any modern terminal, we should be able to just ignore # these, so strip them out. import curses if isinstance(cap_name, bytes): cap_name = cap_name.decode('utf-8') cap = self._escape_code(curses.tigetstr(cap_name)) return re.sub(r'\$<\d+>[/*]?', b'', cap) def render(self, template): return re.sub(r'\$\$|\${\w+}', self._render_sub, template) def _render_sub(self, match): s = match.group() if s == '$$': return s else: return getattr(self, s[2:-1]) class ProgressBar: BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}\n' HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n' def __init__(self, term, header): self.term = term if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL): raise ValueError("Terminal isn't capable enough -- you " "should use a simpler progress display.") self.width = self.term.COLS or 75 self.bar = term.render(self.BAR) self.header = self.term.render(self.HEADER % header.center(self.width)) self.cleared = 1 # : true if we haven't drawn the bar yet. def update(self, percent, message=''): out = (sys.stdout.buffer if py3 else sys.stdout) if self.cleared: out.write(self.header.encode(enc)) self.cleared = 0 n = int((self.width-10)*percent) msg = message.center(self.width) msg = (self.term.BOL + self.term.UP + self.term.CLEAR_EOL + (self.bar % (100*percent, '='*n, '-'*(self.width-10-n))) + self.term.CLEAR_EOL + msg).encode(enc) out.write(msg) out.flush() def clear(self): out = (sys.stdout.buffer if py3 else sys.stdout) if not self.cleared: out.write((self.term.BOL + self.term.CLEAR_EOL + self.term.UP + self.term.CLEAR_EOL + self.term.UP + self.term.CLEAR_EOL).encode(enc)) self.cleared = 1 out.flush() # }}} def prints(*args, **kwargs): # {{{ f = kwargs.get('file', sys.stdout.buffer if py3 else sys.stdout) end = kwargs.get('end', b'\n') enc = getattr(f, 'encoding', 'utf-8') or 'utf-8' if isinstance(end, unicode): end = end.encode(enc) for x in args: if isinstance(x, unicode): x = x.encode(enc) f.write(x) f.write(b' ') f.write(end) if py3 and f is sys.stdout.buffer: f.flush() # }}} class Reporter: # {{{ def __init__(self, fname): try: self.pb = ProgressBar(TerminalController(), 'Downloading '+fname) except ValueError: prints('Downloading', fname) self.pb = None self.last_percent = 0 def __call__(self, blocks, block_size, total_size): percent = (blocks*block_size)/float(total_size) if self.pb is None: if percent - self.last_percent > 0.05: self.last_percent = percent prints('Downloaded {0:%}'.format(percent)) else: try: self.pb.update(percent) except: import traceback traceback.print_exc() # }}} # Downloading {{{ def clean_cache(cache, fname): for x in os.listdir(cache): if fname not in x: os.remove(os.path.join(cache, x)) def check_signature(dest, signature): if not os.path.exists(dest): return None m = hashlib.sha512() with open(dest, 'rb') as f: raw = f.read() m.update(raw) if m.hexdigest().encode('ascii') == signature: return raw class URLOpener(urllib.FancyURLopener): def http_error_206(self, url, fp, errcode, errmsg, headers, data=None): ''' 206 means partial content, ignore it ''' pass def do_download(dest): prints('Will download and install', os.path.basename(dest)) reporter = Reporter(os.path.basename(dest)) offset = 0 urlopener = URLOpener() if os.path.exists(dest): offset = os.path.getsize(dest) # Get content length and check if range is supported rq = urllib.urlopen(DLURL) headers = rq.info() size = int(headers['content-length']) accepts_ranges = headers.get('accept-ranges', None) == 'bytes' mode = 'wb' if accepts_ranges and offset > 0: rurl = rq.geturl() mode = 'ab' rq.close() urlopener.addheader('Range', 'bytes=%s-'%offset) rq = urlopener.open(rurl) with open(dest, mode) as f: while f.tell() < size: raw = rq.read(8192) if not raw: break f.write(raw) reporter(f.tell(), 1, size) rq.close() if os.path.getsize(dest) < size: print ('Download failed, try again later') raise SystemExit(1) prints('Downloaded %s bytes'%os.path.getsize(dest)) def download_tarball(): fname = 'calibre-%s-i686.%s'%(calibre_version, 'txz') if is64bit: fname = fname.replace('i686', 'x86_64') tdir = tempfile.gettempdir() cache = os.path.join(tdir, 'calibre-installer-cache') if not os.path.exists(cache): os.makedirs(cache) clean_cache(cache, fname) dest = os.path.join(cache, fname) raw = check_signature(dest, signature) if raw is not None: print ('Using previously downloaded', fname) return raw cached_sigf = dest +'.signature' cached_sig = None if os.path.exists(cached_sigf): with open(cached_sigf, 'rb') as sigf: cached_sig = sigf.read() if cached_sig != signature and os.path.exists(dest): os.remove(dest) try: with open(cached_sigf, 'wb') as f: f.write(signature) except IOError as e: if e.errno != errno.EACCES: raise print ('The installer cache directory has incorrect permissions.' ' Delete %s and try again.'%cache) raise SystemExit(1) do_download(dest) prints('Checking downloaded file integrity...') raw = check_signature(dest, signature) if raw is None: os.remove(dest) print ('The downloaded files\' signature does not match. ' 'Try the download again later.') raise SystemExit(1) return raw # }}} # Get tarball signature securely {{{ def get_proxies(debug=True): proxies = urllib.getproxies() for key, proxy in list(proxies.items()): if not proxy or '..' in proxy: del proxies[key] continue if proxy.startswith(key+'://'): proxy = proxy[len(key)+3:] if key == 'https' and proxy.startswith('http://'): proxy = proxy[7:] if proxy.endswith('/'): proxy = proxy[:-1] if len(proxy) > 4: proxies[key] = proxy else: prints('Removing invalid', key, 'proxy:', proxy) del proxies[key] if proxies and debug: prints('Using proxies:', repr(proxies)) return proxies class HTTPError(ValueError): def __init__(self, url, code): msg = '%s returned an unsupported http response code: %d (%s)' % ( url, code, httplib.responses.get(code, None)) ValueError.__init__(self, msg) self.code = code self.url = url class CertificateError(ValueError): pass def _dnsname_match(dn, hostname, max_wildcards=1): """Matching according to RFC 6125, section 6.4.3 http://tools.ietf.org/html/rfc6125#section-6.4.3 """ pats = [] if not dn: return False parts = dn.split(r'.') leftmost, remainder = parts[0], parts[1:] wildcards = leftmost.count('*') if wildcards > max_wildcards: # Issue #17980: avoid denials of service by refusing more # than one wildcard per fragment. A survery of established # policy among SSL implementations showed it to be a # reasonable choice. raise CertificateError( "too many wildcards in certificate DNS name: " + repr(dn)) # speed up common case w/o wildcards if not wildcards: return dn.lower() == hostname.lower() # RFC 6125, section 6.4.3, subitem 1. # The client SHOULD NOT attempt to match a presented identifier in which # the wildcard character comprises a label other than the left-most label. if leftmost == '*': # When '*' is a fragment by itself, it matches a non-empty dotless # fragment. pats.append('[^.]+') elif leftmost.startswith('xn--') or hostname.startswith('xn--'): # RFC 6125, section 6.4.3, subitem 3. # The client SHOULD NOT attempt to match a presented identifier # where the wildcard character is embedded within an A-label or # U-label of an internationalized domain name. pats.append(re.escape(leftmost)) else: # Otherwise, '*' matches any dotless string, e.g. www* pats.append(re.escape(leftmost).replace(r'\*', '[^.]*')) # add the remaining fragments, ignore any wildcards for frag in remainder: pats.append(re.escape(frag)) pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) return pat.match(hostname) def match_hostname(cert, hostname): """Verify that *cert* (in decoded format as returned by SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 rules are followed, but IP addresses are not accepted for *hostname*. CertificateError is raised on failure. On success, the function returns nothing. """ if not cert: raise ValueError("empty or no certificate") dnsnames = [] san = cert.get('subjectAltName', ()) for key, value in san: if key == 'DNS': if _dnsname_match(value, hostname): return dnsnames.append(value) if not dnsnames: # The subject is only checked when there is no dNSName entry # in subjectAltName for sub in cert.get('subject', ()): for key, value in sub: # XXX according to RFC 2818, the most specific Common Name # must be used. if key == 'commonName': if _dnsname_match(value, hostname): return dnsnames.append(value) if len(dnsnames) > 1: raise CertificateError("hostname %r " "doesn't match either of %s" % (hostname, ', '.join(map(repr, dnsnames)))) elif len(dnsnames) == 1: # python 2.7.2 does not read subject alt names thanks to this # bug: http://bugs.python.org/issue13034 # And the utter lunacy that is the linux landscape could have # any old version of python whatsoever with or without a hot fix for # this bug. Not to mention that python 2.6 may or may not # read alt names depending on its patchlevel. So we just bail on full # verification if the python version is less than 2.7.3. # Linux distros are one enormous, honking disaster. if sys.version_info[:3] < (2, 7, 3) and dnsnames[0] == 'calibre-ebook.com': return raise CertificateError("hostname %r " "doesn't match %r" % (hostname, dnsnames[0])) else: raise CertificateError("no appropriate commonName or " "subjectAltName fields were found") if has_ssl_verify: class HTTPSConnection(httplib.HTTPSConnection): def __init__(self, ssl_version, *args, **kwargs): kwargs['context'] = ssl.create_default_context(cafile=kwargs.pop('cert_file')) httplib.HTTPSConnection.__init__(self, *args, **kwargs) else: class HTTPSConnection(httplib.HTTPSConnection): def __init__(self, ssl_version, *args, **kwargs): httplib.HTTPSConnection.__init__(self, *args, **kwargs) self.calibre_ssl_version = ssl_version def connect(self): """Connect to a host on a given (SSL) port, properly verifying the SSL certificate, both that it is valid and that its declared hostnames match the hostname we are connecting to.""" if hasattr(self, 'source_address'): sock = socket.create_connection((self.host, self.port), self.timeout, self.source_address) else: # python 2.6 has no source_address sock = socket.create_connection((self.host, self.port), self.timeout) if self._tunnel_host: self.sock = sock self._tunnel() self.sock = ssl.wrap_socket(sock, cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.cert_file, ssl_version=self.calibre_ssl_version) getattr(ssl, 'match_hostname', match_hostname)(self.sock.getpeercert(), self.host) CACERT = b'''\ -----BEGIN CERTIFICATE----- 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 nR0= -----END CERTIFICATE----- ''' def get_https_resource_securely(url, timeout=60, max_redirects=5, ssl_version=None): ''' Download the resource pointed to by url using https securely (verify server certificate). Ensures that redirects, if any, are also downloaded securely. Needs a CA certificates bundle (in PEM format) to verify the server's certificates. ''' if ssl_version is None: try: ssl_version = ssl.PROTOCOL_TLSv1_2 except AttributeError: ssl_version = ssl.PROTOCOL_TLSv1 # old python with tempfile.NamedTemporaryFile(prefix='calibre-ca-cert-') as f: f.write(CACERT) f.flush() p = urlparse(url) if p.scheme != 'https': raise ValueError('URL %s scheme must be https, not %r' % (url, p.scheme)) hostname, port = p.hostname, p.port proxies = get_proxies() has_proxy = False for q in ('https', 'http'): if q in proxies: try: h, po = proxies[q].rpartition(':')[::2] po = int(po) if h: hostname, port, has_proxy = h, po, True break except Exception: # Invalid proxy, ignore pass c = HTTPSConnection(ssl_version, hostname, port, cert_file=f.name, timeout=timeout) if has_proxy: c.set_tunnel(p.hostname, p.port) with closing(c): c.connect() # This is needed for proxy connections path = p.path or '/' if p.query: path += '?' + p.query c.request('GET', path) response = c.getresponse() if response.status in (httplib.MOVED_PERMANENTLY, httplib.FOUND, httplib.SEE_OTHER): if max_redirects <= 0: raise ValueError('Too many redirects, giving up') newurl = response.getheader('Location', None) if newurl is None: raise ValueError('%s returned a redirect response with no Location header' % url) return get_https_resource_securely( newurl, timeout=timeout, max_redirects=max_redirects-1, ssl_version=ssl_version) if response.status != httplib.OK: raise HTTPError(url, response.status) return response.read() # }}} def extract_tarball(raw, destdir): prints('Extracting application files...') with open('/dev/null', 'w') as null: p = subprocess.Popen( list(map(encode_for_subprocess, ['tar', 'xJof', '-', '-C', destdir])), stdout=null, stdin=subprocess.PIPE, close_fds=True, preexec_fn=lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL)) p.stdin.write(raw) p.stdin.close() if p.wait() != 0: prints('Extracting of application files failed with error code: %s' % p.returncode) raise SystemExit(1) def get_tarball_info(): global signature, calibre_version print ('Downloading tarball signature securely...') raw = get_https_resource_securely('https://code.calibre-ebook.com/tarball-info/' + ('x86_64' if is64bit else 'i686')) signature, calibre_version = raw.rpartition(b'@')[::2] if not signature or not calibre_version: raise ValueError('Failed to get install file signature, invalid signature returned') calibre_version = calibre_version.decode('utf-8') def download_and_extract(destdir): get_tarball_info() raw = download_tarball() if os.path.exists(destdir): shutil.rmtree(destdir) os.makedirs(destdir) print('Extracting files to %s ...'%destdir) extract_tarball(raw, destdir) def check_version(): global calibre_version if calibre_version == '%version': calibre_version = urllib.urlopen('http://code.calibre-ebook.com/latest').read() def run_installer(install_dir, isolated, bin_dir, share_dir): destdir = os.path.abspath(os.path.expanduser(install_dir or '/opt')) if destdir == '/usr/bin': prints(destdir, 'is not a valid install location. Choose', end='') prints('a location like /opt or /usr/local') return 1 destdir = os.path.realpath(os.path.join(destdir, 'calibre')) if os.path.exists(destdir): if not os.path.isdir(destdir): prints(destdir, 'exists and is not a directory. Choose a location like /opt or /usr/local') return 1 print ('Installing to', destdir) download_and_extract(destdir) if not isolated: pi = [os.path.join(destdir, 'calibre_postinstall')] if bin_dir is not None: pi.extend(['--bindir', bin_dir]) if share_dir is not None: pi.extend(['--sharedir', share_dir]) subprocess.call(pi) prints('Run "calibre" to start calibre') else: prints('Run "%s/calibre" to start calibre' % destdir) return 0 def check_umask(): # A bad umask can cause system breakage because of bugs in xdg-mime # See https://www.mobileread.com/forums/showthread.php?t=277803 mask = os.umask(18) # 18 = 022 os.umask(mask) forbid_user_read = mask & stat.S_IRUSR forbid_group_read = mask & stat.S_IRGRP forbid_other_read = mask & stat.S_IROTH if forbid_user_read or forbid_group_read or forbid_other_read: prints( 'WARNING: Your current umask disallows reading of files by some users,' ' this can cause system breakage when running the installer because' ' of bugs in common system utilities.' ) sys.stdin = open('/dev/tty') # stdin is a pipe from wget while True: q = raw_input('Should the installer (f)ix the umask, (i)gnore it or (a)bort [f/i/a Default is abort]: ') or 'a' if q in 'f i a'.split(): break prints('Response', q, 'not understood') if q == 'f': mask = mask & ~stat.S_IRUSR & ~stat.S_IRGRP & ~stat.S_IROTH os.umask(mask) prints('umask changed to: {:03o}'.format(mask)) elif q == 'i': prints('Ignoring bad umask and proceeding anyway, you have been warned!') else: raise SystemExit('The system umask is unsuitable, aborting') def main(install_dir=None, isolated=False, bin_dir=None, share_dir=None, ignore_umask=False): if not ignore_umask and not isolated: check_umask() machine = os.uname()[4] if machine and machine.lower().startswith('arm'): raise SystemExit( 'You are running on an ARM system. The calibre binaries are only' ' available for x86 systems. You will have to compile from' ' source.') run_installer(install_dir, isolated, bin_dir, share_dir) try: __file__ from_file = True except NameError: from_file = False if __name__ == '__main__' and from_file: main()