~juliank/software-properties/debian

« back to all changes in this revision

Viewing changes to softwareproperties/ppa.py

  • Committer: Julian Andres Klode
  • Date: 2015-09-15 14:05:00 UTC
  • mfrom: (656.1.282 upstream)
  • Revision ID: jak@debian.org-20150915140500-3tzxcbizu45vzmm9
Tags: 0.96.9debian1
* Merge with Ubuntu, remaining changes:
  - Drop driver handling
  - Drop Ubuntu release upgrade thing
  - Do not use dh-translations
  - Use copyright-format 1.0 for debian/copyright
* .assert_called() is not a valid mock method.  This silently passed in
  earlier versions of Python because mocks create attributes on the fly,
  however Python 3.5 throws AttributeErrors when the missing method
  starts with "assert" for exactly this reason.  Use assertTrue() on
  .called instead.
* debian/control: Bump Standards-Version to 3.9.6.
* Fix syntax errors in the KDE frontend.
* lp:~mpt/software-properties/1377742-markup:
  - Factors out Pango markup from translatable strings, to avoid problems
    like LP: #1377742. Also switches to using <small> for alert secondary
    text, rather than <b><big> for primary text.
* SoftwareProperties.py: self.options can be None, don't crash if this is
  the case.
* lp:~evarlast/software-properties/support-update:
  - add -u, --update option to apt-add-repository 
* data/gtkbuilder/dialog-add.ui: set label width, that's needed with 
  the new gtk
* Add missing dbus-x11 test dependency.
* cloudarchive: Enable support for the Kilo Ubuntu Cloud Archive on
  14.04 (LP: #1412465).
* fix autopkgtest failure
* Port kde ui to Qt 5
* list some extra known keys for translation, thanks feng-kylin 
  (lp: #1306494)
* lp:~larsu/software-properties/fix-wide-dialog:
  - dialog-cache-outofdate: make sure the label is wrapped at ~70
    chars

* lp:~brunonova/software-properties/lp1383289:
  - fix dragging a key into the list of keys in the "Authentication" 
    tab (LP: 1383289)
* lp:~brunonova/software-properties/lp1381050:
  - fix import of keys with special chars (LP: #1381050)
* If adding a PPA from a foreign distribution, use that distribution's
  current series as the codename.
* Add pyflakes to autopkgtest dependencies.
* fix autopkgtest 
* add more tests
* pyflakes cleanup
* make PPA suggestions work again by using the LP api
  directly instead of using python-launchpadlib (which is
  not available for py3)

* support distros like "ubuntu-rtm" in add-apt-repository
* cloudarchive: Teach cloud-archive: prefix which Ubuntu Cloud Archive's
  map to which Ubuntu releases, adding support for Juno on 14.04
  (LP: #1350291).
* cloudarchive: Tidy messages to have correct capitalization.
* software-properties-dbus:
  - do not crash if locale.setlocale() fails (LP: #1314660)
* software-properties-dbus: force C.utf-8 locale if C is used
  to ensure utf-8 support when reading sources.list LP: #1069019
* software-properties-kde : Work around crash in sip by skipping the
  + destructors of SIP objects. (LP: #1307170)
* Add "Additional Drivers" desktop file for the non {GNOME,Unity,KDE}
  flavors (LP: #1060543) - thanks to Unity193
* gnupg version 1.4.16 was changed not to require a trustdb with
  --trust-model set to always
* The new gtk doesn't wrap label by itself, do that for the drivers text
* data/software-properties-gtk.desktop.in:
  - Show in both GNOME control center and Unity control center (LP: #1257505)
* Stop using deprecated GObject constructors with positional arguments.
  (https://wiki.gnome.org/PyGObject/InitializerDeprecations)
* Remove unneeded setting of modified_sourceslist.  Thanks to Bruno Nova
  for the patch.

* do not crash for packages without a candidate
* debian/control: 
  - depends on python-apt-common (>= 0.9), which is the version containing 
    the trusty definition, without it you can't use add-apt-repository 
    without error (lp: #1257765)
  - depends on python-pycurl, ppa.py uses it (lp: #1249080)
* Ensure package sources are refreshed after modification
  Fixes Bug LP: #1075537 (and, indirectly, Bug LP: #782953 it seems).
* Restore the removal of a line feed from a source (LP: #1239893)
* debian/manpages/add-apt-repository.1: Documented options -m and -s of
  add-apt-repository (LP: #1229092)
* softwareproperties/gtk/SoftwarePropertiesGtk.py: don't use the 
  gtk-logout-helper command, it has been deprecated and dropped from the
  indicator-session binary, use gnome-session instead (lp: #1241210)
* support adding cloud-archive repositories using syntax like
  cloud-archive:havana (LP: #1233486)
* SECURITY UPDATE: possible privilege escalation via policykit UID lookup
  race.
  - softwareproperties/dbus/SoftwarePropertiesDBus.py: pass
    system-bus-name as a subject instead of pid so policykit can get the
    information from the system bus.
  - CVE-2013-1061

Show diffs side-by-side

added added

removed removed

Lines of Context:
29
29
import subprocess
30
30
import tempfile
31
31
 
 
32
from gettext import gettext as _
32
33
from threading import Thread
33
34
 
 
35
from softwareproperties.shortcuts import ShortcutException
 
36
 
34
37
try:
35
38
    import urllib.request
36
 
    from urllib.error import URLError
 
39
    from urllib.error import HTTPError, URLError
37
40
    import urllib.parse
38
41
    from http.client import HTTPException
39
42
    NEED_PYCURL = False
40
43
except ImportError:
41
44
    NEED_PYCURL = True
42
45
    import pycurl
 
46
    HTTPError = pycurl.error
 
47
 
43
48
 
44
49
DEFAULT_KEYSERVER = "hkp://keyserver.ubuntu.com:80/"
45
50
# maintained until 2015
46
 
LAUNCHPAD_PPA_API = 'https://launchpad.net/api/1.0/~%s/+archive/%s'
 
51
LAUNCHPAD_PPA_API = 'https://launchpad.net/api/1.0/%s/+archive/%s'
 
52
LAUNCHPAD_USER_API = 'https://launchpad.net/api/1.0/%s'
 
53
LAUNCHPAD_USER_PPAS_API = 'https://launchpad.net/api/1.0/%s/ppas'
 
54
LAUNCHPAD_DISTRIBUTION_API = 'https://launchpad.net/api/1.0/%s'
47
55
# Specify to use the system default SSL store; change to a different path
48
56
# to test with custom certificates.
49
57
LAUNCHPAD_PPA_CERT = "/etc/ssl/certs/ca-certificates.crt"
70
78
def encode(s):
71
79
    return re.sub("[^a-zA-Z0-9_-]", "_", s)
72
80
 
73
 
 
74
 
def expand_ppa_line(abrev, distro_codename):
75
 
    """ Convert an abbreviated ppa name of the form 'ppa:$name' to a
76
 
        proper sources.list line of the form 'deb ...' """
77
 
    # leave non-ppa: lines unchanged
78
 
    if not abrev.startswith("ppa:"):
79
 
        return (abrev, None)
80
 
    # FIXME: add support for dependency PPAs too (once we can get them
81
 
    #        via some sort of API, see LP #385129)
82
 
    abrev = abrev.split(":")[1]
83
 
    ppa_owner = abrev.split("/")[0]
84
 
    try:
85
 
        ppa_name = abrev.split("/")[1]
86
 
    except IndexError as e:
87
 
        ppa_name = "ppa"
88
 
    sourceslistd = apt_pkg.config.find_dir("Dir::Etc::sourceparts")
89
 
    line = "deb http://ppa.launchpad.net/%s/%s/ubuntu %s main" % (
90
 
        ppa_owner, ppa_name, distro_codename)
91
 
    filename = os.path.join(sourceslistd, "%s-%s-%s.list" % (
92
 
        encode(ppa_owner), encode(ppa_name), distro_codename))
93
 
    return (line, filename)
94
 
 
95
 
 
96
 
def get_ppa_info_from_lp(owner_name, ppa_name):
97
 
    lp_url = LAUNCHPAD_PPA_API % (owner_name, ppa_name)
 
81
def get_info_from_lp(lp_url):
98
82
    if NEED_PYCURL:
99
83
        # python2 has no cert verification so we need pycurl
100
84
        return _get_https_content_pycurl(lp_url)
102
86
        # python3 has cert verification so we can use the buildin urllib
103
87
        return _get_https_content_py3(lp_url)
104
88
 
 
89
def get_ppa_info_from_lp(owner_name, ppa):
 
90
    lp_url = LAUNCHPAD_PPA_API % (owner_name, ppa)
 
91
    return get_info_from_lp(lp_url)
 
92
 
 
93
def get_current_series_from_lp(distribution):
 
94
    lp_url = LAUNCHPAD_DISTRIBUTION_API % distribution
 
95
    return os.path.basename(get_info_from_lp(lp_url)["current_series_link"])
 
96
 
 
97
 
105
98
def _get_https_content_py3(lp_url):
106
99
    try:
107
100
        request = urllib.request.Request(str(lp_url), headers={"Accept":" application/json"})
133
126
        raise PPAException("Error reading %s: %s" % (lp_url, e), e)
134
127
    return json.loads(json_data)
135
128
 
 
129
 
 
130
def mangle_ppa_shortcut(shortcut):
 
131
    ppa_shortcut = shortcut.split(":")[1]
 
132
    user = ppa_shortcut.split("/")[0]
 
133
    if (user[0] == "~"):
 
134
        user = user[1:]
 
135
    ppa_path_objs = ppa_shortcut.split("/")[1:]
 
136
    ppa_path = []
 
137
    if (len(ppa_path_objs) < 1):
 
138
        ppa_path = ['ubuntu', 'ppa']
 
139
    elif (len(ppa_path_objs) == 1):
 
140
        ppa_path.insert(0, "ubuntu")
 
141
        ppa_path.extend(ppa_path_objs)
 
142
    else:
 
143
        ppa_path = ppa_path_objs
 
144
    ppa = "~%s/%s" % (user, "/".join(ppa_path))
 
145
    return ppa
 
146
 
136
147
def verify_keyid_is_v4(signing_key_fingerprint):
137
148
    """Verify that the keyid is a v4 fingerprint with at least 160bit"""
138
149
    return len(signing_key_fingerprint) >= 160/8
139
150
 
140
151
 
141
 
class AddPPASigningKeyThread(Thread):
 
152
class AddPPASigningKey(object):
142
153
    " thread class for adding the signing key in the background "
143
154
 
144
155
    GPG_DEFAULT_OPTIONS = ["gpg", "--no-default-keyring", "--no-options"]
145
156
 
146
157
    def __init__(self, ppa_path, keyserver=None):
147
 
        Thread.__init__(self)
148
158
        self.ppa_path = ppa_path
149
159
        self.keyserver = (keyserver if keyserver is not None
150
160
                          else DEFAULT_KEYSERVER)
151
161
 
152
 
    def run(self):
153
 
        self.add_ppa_signing_key(self.ppa_path)
154
 
 
155
162
    def _recv_key(self, keyring, secret_keyring, signing_key_fingerprint, keyring_dir):
156
163
        try:
157
164
            # double check that the signing key is a v4 fingerprint (160bit)
211
218
            return False
212
219
        return True
213
220
 
214
 
    def add_ppa_signing_key(self, ppa_path):
 
221
    def add_ppa_signing_key(self, ppa_path=None):
215
222
        """Query and add the corresponding PPA signing key.
216
223
        
217
224
        The signing key fingerprint is obtained from the Launchpad PPA page,
218
225
        via a secure channel, so it can be trusted.
219
226
        """
 
227
        if ppa_path is None:
 
228
            ppa_path = self.ppa_path
 
229
 
220
230
        def cleanup(tmpdir):
221
231
            shutil.rmtree(tmp_keyring_dir)
222
 
        owner_name, ppa_name, distro = ppa_path[1:].split('/')
 
232
 
223
233
        try:
224
 
            ppa_info = get_ppa_info_from_lp(owner_name, ppa_name)
 
234
            ppa_info = get_ppa_info(ppa_path)
225
235
        except PPAException as e:
226
236
            print(e.value)
227
237
            return False
252
262
            return False
253
263
        # and add it
254
264
        trustedgpgd = apt_pkg.config.find_dir("Dir::Etc::trustedparts")
255
 
        apt_keyring = os.path.join(trustedgpgd, "%s-%s.gpg" % (
256
 
            encode(owner_name), encode(ppa_name)))
 
265
        apt_keyring = os.path.join(trustedgpgd, "%s.gpg" % (
 
266
            encode(ppa_info["reference"][1:])))
257
267
        res = subprocess.call(["apt-key", "--keyring", apt_keyring, "add",
258
268
            tmp_keyring])
259
269
        # cleanup
261
271
        return (res == 0)
262
272
 
263
273
 
 
274
class AddPPASigningKeyThread(Thread, AddPPASigningKey):
 
275
    # This class is legacy.  There are no users inside the software-properties
 
276
    # codebase other than a test case.  It was left in case there were outside
 
277
    # users.  Internally, we've changed from having a class implement the
 
278
    # tread to explicitly launching a thread and invoking a method in it
 
279
    # see check_and_add_key_for_whitelisted_shortcut for how.
 
280
    def __init__(self, ppa_path, keyserver=None):
 
281
        Thread.__init__(self)
 
282
        AddPPASigningKey.__init__(self, ppa_path=ppa_path, keyserver=keyserver)
 
283
 
 
284
    def run(self):
 
285
        self.add_ppa_signing_key(self.ppa_path)
 
286
 
 
287
 
 
288
def _get_suggested_ppa_message(user, ppa_name):
 
289
    try:
 
290
        msg = []
 
291
        try:
 
292
            lp_user = get_info_from_lp(LAUNCHPAD_USER_API % user)
 
293
            lp_ppas = get_info_from_lp(LAUNCHPAD_USER_PPAS_API % user)
 
294
            entity_name = _("team") if lp_user["is_team"] else _("user")
 
295
            if lp_ppas["total_size"] > 0:
 
296
                # Translators: %(entity)s is either "team" or "user"
 
297
                msg.append(_("The %(entity)s named '%(user)s' has no PPA named '%(ppa)s'") % {
 
298
                        'entity' : entity_name,
 
299
                         'user' : user,
 
300
                         'ppa' : ppa_name})
 
301
                msg.append(_("Please choose from the following available PPAs:"))
 
302
                for ppa in lp_ppas["entries"]:
 
303
                    msg.append(_(" * '%(name)s':  %(displayname)s") % {
 
304
                                 'name' : ppa["name"],
 
305
                                 'displayname' : ppa["displayname"]})
 
306
            else:
 
307
                # Translators: %(entity)s is either "team" or "user"
 
308
                msg.append(_("The %(entity)s named '%(user)s' does not have any PPA") % {
 
309
                             'entity' : entity_name, 'user' : user})
 
310
            return '\n'.join(msg)
 
311
        except KeyError:
 
312
            return ''
 
313
    except ImportError:
 
314
        return _("Please check that the PPA name or format is correct.")
 
315
 
 
316
 
 
317
def get_ppa_info(shortcut):
 
318
    user = shortcut.split("/")[0]
 
319
    ppa = "/".join(shortcut.split("/")[1:])
 
320
    try:
 
321
        ret = get_ppa_info_from_lp(user, ppa)
 
322
        ret["distribution"] = ret["distribution_link"].split('/')[-1]
 
323
        ret["owner"] = ret["owner_link"].split('/')[-1]
 
324
        return ret
 
325
    except (HTTPError, Exception):
 
326
        msg = []
 
327
        msg.append(_("Cannot add PPA: 'ppa:%s/%s'.") % (
 
328
            user, ppa))
 
329
 
 
330
        # If the PPA does not exist, then try to find if the user/team
 
331
        # exists. If it exists, list down the PPAs
 
332
        raise ShortcutException('\n'.join(msg) + "\n" +
 
333
                                _get_suggested_ppa_message(user, ppa))
 
334
 
 
335
    except (ValueError, PPAException):
 
336
        raise ShortcutException(
 
337
            _("Cannot access PPA (%s) to get PPA information, "
 
338
              "please check your internet connection.") % \
 
339
              (LAUNCHPAD_PPA_API % (user, ppa)))
 
340
 
 
341
 
 
342
class PPAShortcutHandler(object):
 
343
    def __init__(self, shortcut):
 
344
        super(PPAShortcutHandler, self).__init__()
 
345
        self.shortcut = mangle_ppa_shortcut(shortcut)
 
346
        info = get_ppa_info(self.shortcut)
 
347
 
 
348
        if "private" in info and info["private"]:
 
349
            raise ShortcutException(
 
350
                _("Adding private PPAs is not supported currently"))
 
351
 
 
352
        self._info = info
 
353
 
 
354
    def info(self):
 
355
        return self._info
 
356
 
 
357
    def expand(self, codename, distro=None):
 
358
        if distro is not None and distro != self._info["distribution"]:
 
359
            # The requested PPA is for a foreign distribution.  Guess that
 
360
            # the user wants that distribution's current series.
 
361
            codename = get_current_series_from_lp(self._info["distribution"])
 
362
        debline = "deb http://ppa.launchpad.net/%s/%s/%s %s main" % (
 
363
            self._info["owner"][1:], self._info["name"],
 
364
            self._info["distribution"], codename)
 
365
        sourceslistd = apt_pkg.config.find_dir("Dir::Etc::sourceparts")
 
366
        filename = os.path.join(sourceslistd, "%s-%s-%s-%s.list" % (
 
367
            encode(self._info["owner"][1:]), encode(self._info["distribution"]),
 
368
            encode(self._info["name"]), codename))
 
369
        return (debline, filename)
 
370
 
 
371
    def should_confirm(self):
 
372
        return True
 
373
 
 
374
    def add_key(self, keyserver=None):
 
375
        apsk = AddPPASigningKey(self._info["reference"], keyserver=keyserver)
 
376
        return apsk.add_ppa_signing_key()
 
377
 
 
378
 
 
379
def shortcut_handler(shortcut):
 
380
    if not shortcut.startswith("ppa:"):
 
381
        return None
 
382
    return PPAShortcutHandler(shortcut)
 
383
 
 
384
 
264
385
if __name__ == "__main__":
265
386
    import sys
266
 
    owner_name, ppa_name = sys.argv[1].split(":")[1].split("/")
267
 
    print(get_ppa_info_from_lp(owner_name, ppa_name))
 
387
    ppa = sys.argv[1].split(":")[1]
 
388
    print(get_ppa_info(ppa))