Merge ~pappacena/launchpad:revert-https-mirrors into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: feb7c661874d25353f24066d0e6df37e14a083a7
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:revert-https-mirrors
Merge into: launchpad:master
Diff against target: 1116 lines (+53/-477)
16 files modified
lib/lp/registry/browser/distributionmirror.py (+6/-7)
lib/lp/registry/browser/tests/distributionmirror-views.txt (+9/-40)
lib/lp/registry/configure.zcml (+3/-5)
lib/lp/registry/interfaces/distribution.py (+4/-4)
lib/lp/registry/interfaces/distributionmirror.py (+3/-21)
lib/lp/registry/model/distribution.py (+4/-7)
lib/lp/registry/model/distributionmirror.py (+2/-10)
lib/lp/registry/scripts/distributionmirror_prober.py (+7/-153)
lib/lp/registry/stories/webservice/xx-distribution-mirror.txt (+0/-4)
lib/lp/registry/stories/webservice/xx-distribution.txt (+0/-1)
lib/lp/registry/templates/distributionmirror-index.pt (+0/-4)
lib/lp/registry/templates/distributionmirror-macros.pt (+1/-3)
lib/lp/registry/tests/distributionmirror_http_server.py (+7/-15)
lib/lp/registry/tests/test_distributionmirror_prober.py (+3/-196)
lib/lp/scripts/utilities/importpedant.py (+1/-3)
lib/lp/testing/factory.py (+3/-4)
Reviewer Review Type Date Requested Status
Thiago F. Pappacena (community) Approve
Review via email: mp+379909@code.launchpad.net

Commit message

Reverting https-mirrors branch until we land the database patch.

To post a comment you must log in.
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :
Revision history for this message
Thiago F. Pappacena (pappacena) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/registry/browser/distributionmirror.py b/lib/lp/registry/browser/distributionmirror.py
index 6ae1eb7..fd671a0 100644
--- a/lib/lp/registry/browser/distributionmirror.py
+++ b/lib/lp/registry/browser/distributionmirror.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -207,9 +207,9 @@ class DistributionMirrorDeleteView(LaunchpadFormView):
207class DistributionMirrorAddView(LaunchpadFormView):207class DistributionMirrorAddView(LaunchpadFormView):
208 schema = IDistributionMirror208 schema = IDistributionMirror
209 field_names = [209 field_names = [
210 "display_name", "description", "whiteboard", "https_base_url",210 "display_name", "description", "whiteboard", "http_base_url",
211 "http_base_url", "ftp_base_url", "rsync_base_url", "speed", "country",211 "ftp_base_url", "rsync_base_url", "speed", "country", "content",
212 "content", "official_candidate",212 "official_candidate",
213 ]213 ]
214 invariant_context = None214 invariant_context = None
215215
@@ -235,7 +235,6 @@ class DistributionMirrorAddView(LaunchpadFormView):
235 content=data['content'], display_name=data['display_name'],235 content=data['content'], display_name=data['display_name'],
236 description=data['description'],236 description=data['description'],
237 whiteboard=data['whiteboard'],237 whiteboard=data['whiteboard'],
238 https_base_url=data['https_base_url'],
239 http_base_url=data['http_base_url'],238 http_base_url=data['http_base_url'],
240 ftp_base_url=data['ftp_base_url'],239 ftp_base_url=data['ftp_base_url'],
241 rsync_base_url=data['rsync_base_url'],240 rsync_base_url=data['rsync_base_url'],
@@ -280,8 +279,8 @@ class DistributionMirrorEditView(LaunchpadEditFormView):
280 schema = IDistributionMirror279 schema = IDistributionMirror
281 field_names = [280 field_names = [
282 "name", "display_name", "description", "whiteboard",281 "name", "display_name", "description", "whiteboard",
283 "https_base_url", "http_base_url", "ftp_base_url", "rsync_base_url",282 "http_base_url", "ftp_base_url", "rsync_base_url", "speed",
284 "speed", "country", "content", "official_candidate",283 "country", "content", "official_candidate",
285 ]284 ]
286285
287 @property286 @property
diff --git a/lib/lp/registry/browser/tests/distributionmirror-views.txt b/lib/lp/registry/browser/tests/distributionmirror-views.txt
index f8fe4c5..e11dbd9 100644
--- a/lib/lp/registry/browser/tests/distributionmirror-views.txt
+++ b/lib/lp/registry/browser/tests/distributionmirror-views.txt
@@ -44,19 +44,18 @@ The view provides a label, page_title, and cancel_url
44 >>> print view.cancel_url44 >>> print view.cancel_url
45 http://launchpad.test/ubuntu45 http://launchpad.test/ubuntu
4646
47A HTTP, HTTPS or FTP URL is required to register a mirror.47A HTTP or FTP URL is required to register a mirror.
4848
49 >>> view.field_names49 >>> view.field_names
50 ['display_name', 'description', 'whiteboard', 'https_base_url',50 ['display_name', 'description', 'whiteboard', 'http_base_url',
51 'http_base_url', 'ftp_base_url', 'rsync_base_url', 'speed', 'country',51 'ftp_base_url', 'rsync_base_url', 'speed', 'country', 'content',
52 'content', 'official_candidate']52 'official_candidate']
5353
54 >>> form = {54 >>> form = {
55 ... 'field.display_name': 'Illuminati',55 ... 'field.display_name': 'Illuminati',
56 ... 'field.description': 'description',56 ... 'field.description': 'description',
57 ... 'field.whiteboard': 'whiteboard',57 ... 'field.whiteboard': 'whiteboard',
58 ... 'field.http_base_url': 'http://secret.me/',58 ... 'field.http_base_url': 'http://secret.me/',
59 ... 'field.https_base_url': '',
60 ... 'field.ftp_base_url': '',59 ... 'field.ftp_base_url': '',
61 ... 'field.rsync_base_url': '',60 ... 'field.rsync_base_url': '',
62 ... 'field.speed': 'S128K',61 ... 'field.speed': 'S128K',
@@ -94,7 +93,6 @@ not significant).
94The same is true for a FTP URL.93The same is true for a FTP URL.
9594
96 >>> mirror.ftp_base_url = 'ftp://now-here.me/'95 >>> mirror.ftp_base_url = 'ftp://now-here.me/'
97 >>> bad_form['field.https_base_url'] = ''
98 >>> bad_form['field.http_base_url'] = ''96 >>> bad_form['field.http_base_url'] = ''
99 >>> bad_form['field.ftp_base_url'] = 'ftp://now-here.me'97 >>> bad_form['field.ftp_base_url'] = 'ftp://now-here.me'
100 >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form)98 >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form)
@@ -112,15 +110,14 @@ The same is true for a rsync URL.
112 ... print error[2]110 ... print error[2]
113 The distribution mirror ... is already registered with this URL.111 The distribution mirror ... is already registered with this URL.
114112
115A mirror must have an ftp, HTTPS or http URL.113A mirror must have an ftp or http URL.
116114
117 >>> bad_form['field.https_base_url'] = ''
118 >>> bad_form['field.http_base_url'] = ''115 >>> bad_form['field.http_base_url'] = ''
119 >>> bad_form['field.ftp_base_url'] = ''116 >>> bad_form['field.ftp_base_url'] = ''
120 >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form)117 >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form)
121 >>> for message in view.errors:118 >>> for message in view.errors:
122 ... print message119 ... print message
123 A mirror must have at least an HTTP(S) or FTP URL.120 A mirror must have at least an HTTP or FTP URL.
124121
125The URL cannot contain a fragment.122The URL cannot contain a fragment.
126123
@@ -138,34 +135,6 @@ The URL cannot contain a query string.
138 ... print error[2]135 ... print error[2]
139 URIs with query strings are not allowed.136 URIs with query strings are not allowed.
140137
141The HTTPS URL may not have an HTTP scheme.
142
143 >>> bad_form['field.http_base_url'] = ''
144 >>> bad_form['field.https_base_url'] = 'http://secret.me/#fragement'
145 >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form)
146 >>> for error in view.errors:
147 ... print error[2]
148 The URI scheme "http" is not allowed.
149 Only URIs with the following schemes may be used: https
150
151The HTTPS URL cannot contain a fragment.
152
153 >>> bad_form['field.http_base_url'] = ''
154 >>> bad_form['field.https_base_url'] = 'https://secret.me/#fragement'
155 >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form)
156 >>> for error in view.errors:
157 ... print error[2]
158 URIs with fragment identifiers are not allowed.
159
160The URL cannot contain a query string.
161
162 >>> bad_form['field.http_base_url'] = ''
163 >>> bad_form['field.https_base_url'] = 'https://secret.me/?query=string'
164 >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form)
165 >>> for error in view.errors:
166 ... print error[2]
167 URIs with query strings are not allowed.
168
169138
170Reviewing a distribution mirror139Reviewing a distribution mirror
171-------------------------------140-------------------------------
@@ -270,9 +239,9 @@ The +edit view provides a label, page_title, and cancel_url.
270The user can edit the mirror fields.239The user can edit the mirror fields.
271240
272 >>> view.field_names241 >>> view.field_names
273 ['name', 'display_name', 'description', 'whiteboard', 'https_base_url',242 ['name', 'display_name', 'description', 'whiteboard', 'http_base_url',
274 'http_base_url', 'ftp_base_url', 'rsync_base_url', 'speed', 'country',243 'ftp_base_url', 'rsync_base_url', 'speed', 'country', 'content',
275 'content', 'official_candidate']244 'official_candidate']
276245
277 >>> print mirror.ftp_base_url246 >>> print mirror.ftp_base_url
278 None247 None
diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml
index d5875c5..9be68e0 100644
--- a/lib/lp/registry/configure.zcml
+++ b/lib/lp/registry/configure.zcml
@@ -1,4 +1,4 @@
1<!-- Copyright 2009-2020 Canonical Ltd. This software is licensed under the1<!-- Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -2046,7 +2046,6 @@
2046 description2046 description
2047 distribution2047 distribution
2048 http_base_url2048 http_base_url
2049 https_base_url
2050 ftp_base_url2049 ftp_base_url
2051 rsync_base_url2050 rsync_base_url
2052 enabled2051 enabled
@@ -2085,9 +2084,8 @@
2085 <require2084 <require
2086 permission="launchpad.Edit"2085 permission="launchpad.Edit"
2087 set_attributes="name display_name description whiteboard2086 set_attributes="name display_name description whiteboard
2088 http_base_url https_base_url ftp_base_url2087 http_base_url ftp_base_url rsync_base_url enabled
2089 rsync_base_url enabled speed country content2088 speed country content official_candidate owner"
2090 official_candidate owner"
2091 attributes="official_candidate whiteboard resubmitForReview" />2089 attributes="official_candidate whiteboard resubmitForReview" />
2092 <require2090 <require
2093 permission="launchpad.Admin"2091 permission="launchpad.Admin"
diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py
index a7106f6..c7ddb95 100644
--- a/lib/lp/registry/interfaces/distribution.py
+++ b/lib/lp/registry/interfaces/distribution.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Interfaces including and related to IDistribution."""4"""Interfaces including and related to IDistribution."""
@@ -491,13 +491,13 @@ class IDistributionPublic(
491 """Return the country DNS mirror for a country and content type."""491 """Return the country DNS mirror for a country and content type."""
492492
493 def newMirror(owner, speed, country, content, display_name=None,493 def newMirror(owner, speed, country, content, display_name=None,
494 description=None, http_base_url=None, https_base_url=None,494 description=None, http_base_url=None,
495 ftp_base_url=None, rsync_base_url=None, enabled=False,495 ftp_base_url=None, rsync_base_url=None, enabled=False,
496 official_candidate=False, whiteboard=None):496 official_candidate=False, whiteboard=None):
497 """Create a new DistributionMirror for this distribution.497 """Create a new DistributionMirror for this distribution.
498498
499 At least one of {http,https,ftp}_base_url must be provided in order to499 At least one of http_base_url or ftp_base_url must be provided in
500 create a mirror.500 order to create a mirror.
501 """501 """
502502
503 def getOCIProject(name):503 def getOCIProject(name):
diff --git a/lib/lp/registry/interfaces/distributionmirror.py b/lib/lp/registry/interfaces/distributionmirror.py
index 4849e7a..ce6720e 100644
--- a/lib/lp/registry/interfaces/distributionmirror.py
+++ b/lib/lp/registry/interfaces/distributionmirror.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -302,12 +302,6 @@ class DistroMirrorHTTPURIField(DistroMirrorURIField):
302 return getUtility(IDistributionMirrorSet).getByHttpUrl(url)302 return getUtility(IDistributionMirrorSet).getByHttpUrl(url)
303303
304304
305class DistroMirrorHTTPSURIField(DistroMirrorURIField):
306
307 def getMirrorByURI(self, url):
308 return getUtility(IDistributionMirrorSet).getByHttpsUrl(url)
309
310
311class DistroMirrorFTPURIField(DistroMirrorURIField):305class DistroMirrorFTPURIField(DistroMirrorURIField):
312306
313 def getMirrorByURI(self, url):307 def getMirrorByURI(self, url):
@@ -355,14 +349,6 @@ class IDistributionMirror(Interface):
355 allowed_schemes=['http'], allow_userinfo=False,349 allowed_schemes=['http'], allow_userinfo=False,
356 allow_query=False, allow_fragment=False, trailing_slash=True,350 allow_query=False, allow_fragment=False, trailing_slash=True,
357 description=_('e.g.: http://archive.ubuntu.com/ubuntu/')))351 description=_('e.g.: http://archive.ubuntu.com/ubuntu/')))
358 https_base_url = exported(DistroMirrorHTTPSURIField(
359 title=_('HTTPS URL'), required=False, readonly=False,
360 allowed_schemes=['https'], allow_userinfo=False,
361 allow_query=False, allow_fragment=False, trailing_slash=True,
362 # XXX: pappacena 2020-02-21: Add description field with a more
363 # suitable example once we have https for archive.ubuntu.com, like:
364 # description=_('e.g.: http://archive.ubuntu.com/ubuntu/')
365 ))
366 ftp_base_url = exported(DistroMirrorFTPURIField(352 ftp_base_url = exported(DistroMirrorFTPURIField(
367 title=_('FTP URL'), required=False, readonly=False,353 title=_('FTP URL'), required=False, readonly=False,
368 allowed_schemes=['ftp'], allow_userinfo=False,354 allowed_schemes=['ftp'], allow_userinfo=False,
@@ -449,9 +435,8 @@ class IDistributionMirror(Interface):
449435
450 @invariant436 @invariant
451 def mirrorMustHaveHTTPOrFTPURL(mirror):437 def mirrorMustHaveHTTPOrFTPURL(mirror):
452 if not (mirror.http_base_url or mirror.https_base_url or438 if not (mirror.http_base_url or mirror.ftp_base_url):
453 mirror.ftp_base_url):439 raise Invalid('A mirror must have at least an HTTP or FTP URL.')
454 raise Invalid('A mirror must have at least an HTTP(S) or FTP URL.')
455440
456 def getSummarizedMirroredSourceSeries():441 def getSummarizedMirroredSourceSeries():
457 """Return a summarized list of this distribution_mirror's442 """Return a summarized list of this distribution_mirror's
@@ -629,9 +614,6 @@ class IDistributionMirrorSet(Interface):
629 def getByHttpUrl(url):614 def getByHttpUrl(url):
630 """Return the mirror with the given HTTP URL or None."""615 """Return the mirror with the given HTTP URL or None."""
631616
632 def getByHttpsUrl(url):
633 """Return the mirror with the given HTTPS URL or None."""
634
635 def getByFtpUrl(url):617 def getByFtpUrl(url):
636 """Return the mirror with the given FTP URL or None."""618 """Return the mirror with the given FTP URL or None."""
637619
diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
index f78908b..2288d0e 100644
--- a/lib/lp/registry/model/distribution.py
+++ b/lib/lp/registry/model/distribution.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the1# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Database classes for implementing distribution items."""4"""Database classes for implementing distribution items."""
@@ -709,7 +709,7 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
709 country_dns_mirror=True).one()709 country_dns_mirror=True).one()
710710
711 def newMirror(self, owner, speed, country, content, display_name=None,711 def newMirror(self, owner, speed, country, content, display_name=None,
712 description=None, http_base_url=None, https_base_url=None,712 description=None, http_base_url=None,
713 ftp_base_url=None, rsync_base_url=None,713 ftp_base_url=None, rsync_base_url=None,
714 official_candidate=False, enabled=False,714 official_candidate=False, enabled=False,
715 whiteboard=None):715 whiteboard=None):
@@ -722,17 +722,15 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
722 return None722 return None
723723
724 urls = {'http_base_url': http_base_url,724 urls = {'http_base_url': http_base_url,
725 'https_base_url': https_base_url,
726 'ftp_base_url': ftp_base_url,725 'ftp_base_url': ftp_base_url,
727 'rsync_base_url': rsync_base_url}726 'rsync_base_url': rsync_base_url}
728 for name, value in urls.items():727 for name, value in urls.items():
729 if value is not None:728 if value is not None:
730 urls[name] = IDistributionMirror[name].normalize(value)729 urls[name] = IDistributionMirror[name].normalize(value)
731730
732 url = (urls['https_base_url'] or urls['http_base_url'] or731 url = urls['http_base_url'] or urls['ftp_base_url']
733 urls['ftp_base_url'])
734 assert url is not None, (732 assert url is not None, (
735 "A mirror must provide at least one HTTP/HTTPS/FTP URL.")733 "A mirror must provide either an HTTP or FTP URL (or both).")
736 dummy, host, dummy, dummy, dummy, dummy = urlparse(url)734 dummy, host, dummy, dummy, dummy, dummy = urlparse(url)
737 name = sanitize_name('%s-%s' % (host, content.name.lower()))735 name = sanitize_name('%s-%s' % (host, content.name.lower()))
738736
@@ -746,7 +744,6 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
746 distribution=self, owner=owner, name=name, speed=speed,744 distribution=self, owner=owner, name=name, speed=speed,
747 country=country, content=content, display_name=display_name,745 country=country, content=content, display_name=display_name,
748 description=description, http_base_url=urls['http_base_url'],746 description=description, http_base_url=urls['http_base_url'],
749 https_base_url=urls['https_base_url'],
750 ftp_base_url=urls['ftp_base_url'],747 ftp_base_url=urls['ftp_base_url'],
751 rsync_base_url=urls['rsync_base_url'],748 rsync_base_url=urls['rsync_base_url'],
752 official_candidate=official_candidate, enabled=enabled,749 official_candidate=official_candidate, enabled=enabled,
diff --git a/lib/lp/registry/model/distributionmirror.py b/lib/lp/registry/model/distributionmirror.py
index 323fb26..747a2b0 100644
--- a/lib/lp/registry/model/distributionmirror.py
+++ b/lib/lp/registry/model/distributionmirror.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Module docstring goes here."""4"""Module docstring goes here."""
@@ -129,8 +129,6 @@ class DistributionMirror(SQLBase):
129 notNull=False, default=None)129 notNull=False, default=None)
130 http_base_url = StringCol(130 http_base_url = StringCol(
131 notNull=False, default=None, unique=True)131 notNull=False, default=None, unique=True)
132 https_base_url = StringCol(
133 notNull=False, default=None, unique=True)
134 ftp_base_url = StringCol(132 ftp_base_url = StringCol(
135 notNull=False, default=None, unique=True)133 notNull=False, default=None, unique=True)
136 rsync_base_url = StringCol(134 rsync_base_url = StringCol(
@@ -157,9 +155,7 @@ class DistributionMirror(SQLBase):
157 @property155 @property
158 def base_url(self):156 def base_url(self):
159 """See IDistributionMirror"""157 """See IDistributionMirror"""
160 if self.https_base_url is not None:158 if self.http_base_url is not None:
161 return self.https_base_url
162 elif self.http_base_url is not None:
163 return self.http_base_url159 return self.http_base_url
164 else:160 else:
165 return self.ftp_base_url161 return self.ftp_base_url
@@ -681,10 +677,6 @@ class DistributionMirrorSet:
681 """See IDistributionMirrorSet"""677 """See IDistributionMirrorSet"""
682 return DistributionMirror.selectOneBy(http_base_url=url)678 return DistributionMirror.selectOneBy(http_base_url=url)
683679
684 def getByHttpsUrl(self, url):
685 """See IDistributionMirrorSet"""
686 return DistributionMirror.selectOneBy(https_base_url=url)
687
688 def getByFtpUrl(self, url):680 def getByFtpUrl(self, url):
689 """See IDistributionMirrorSet"""681 """See IDistributionMirrorSet"""
690 return DistributionMirror.selectOneBy(ftp_base_url=url)682 return DistributionMirror.selectOneBy(ftp_base_url=url)
diff --git a/lib/lp/registry/scripts/distributionmirror_prober.py b/lib/lp/registry/scripts/distributionmirror_prober.py
index c633fb8..916d4ba 100644
--- a/lib/lp/registry/scripts/distributionmirror_prober.py
+++ b/lib/lp/registry/scripts/distributionmirror_prober.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -12,7 +12,6 @@ import logging
12import os.path12import os.path
13from StringIO import StringIO13from StringIO import StringIO
1414
15import OpenSSL
16import requests15import requests
17from six.moves import http_client16from six.moves import http_client
18from six.moves.urllib.parse import (17from six.moves.urllib.parse import (
@@ -21,27 +20,14 @@ from six.moves.urllib.parse import (
21 urlparse,20 urlparse,
22 urlunparse,21 urlunparse,
23 )22 )
24from treq.client import HTTPClient as TreqHTTPClient
25from twisted.internet import (23from twisted.internet import (
26 defer,24 defer,
27 protocol,25 protocol,
28 reactor,26 reactor,
29 )27 )
30from twisted.internet.defer import (28from twisted.internet.defer import DeferredSemaphore
31 CancelledError,
32 DeferredSemaphore,
33 )
34from twisted.internet.endpoints import HostnameEndpoint
35from twisted.internet.ssl import VerificationError
36from twisted.python.failure import Failure29from twisted.python.failure import Failure
37from twisted.web.client import (
38 Agent,
39 BrowserLikePolicyForHTTPS,
40 ProxyAgent,
41 ResponseNeverReceived,
42 )
43from twisted.web.http import HTTPClient30from twisted.web.http import HTTPClient
44from twisted.web.iweb import IResponse
45from zope.component import getUtility31from zope.component import getUtility
4632
47from lp.app.interfaces.launchpad import ILaunchpadCelebrities33from lp.app.interfaces.launchpad import ILaunchpadCelebrities
@@ -64,7 +50,6 @@ from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
64# IMPORTANT: Changing these values can cause lots of false negatives when50# IMPORTANT: Changing these values can cause lots of false negatives when
65# probing mirrors, so please don't change them unless you know what you're51# probing mirrors, so please don't change them unless you know what you're
66# doing.52# doing.
67
68MIN_REQUEST_TIMEOUT_RATIO = 353MIN_REQUEST_TIMEOUT_RATIO = 3
69MIN_REQUESTS_TO_CONSIDER_RATIO = 3054MIN_REQUESTS_TO_CONSIDER_RATIO = 30
7055
@@ -72,9 +57,6 @@ MIN_REQUESTS_TO_CONSIDER_RATIO = 30
72# We need to get rid of these global dicts in this module.57# We need to get rid of these global dicts in this module.
73host_requests = {}58host_requests = {}
74host_timeouts = {}59host_timeouts = {}
75# Set of invalid certificate (host, port) tuples, to avoid doing HTTPS calls
76# to hosts we already know they are not valid.
77invalid_certificate_hosts = set()
7860
79MAX_REDIRECTS = 361MAX_REDIRECTS = 3
8062
@@ -179,64 +161,6 @@ class ProberProtocol(HTTPClient):
179 pass161 pass
180162
181163
182class HTTPSProbeFailureHandler:
183 """Handler to translate general errors into expected errors on HTTPS
184 connections."""
185 def __init__(self, factory):
186 self.factory = factory
187
188 def handleResponse(self, response):
189 """Translates any request with return code different from 200 into
190 an error in the callback chain.
191
192 Note that other 2xx codes that are not 200 are considered errors too.
193 This behaviour is the same as seen in ProberProtocol.handleStatus,
194 for HTTP responses.
195 """
196 status = response.code
197 if status == http_client.OK:
198 return response
199 else:
200 raise BadResponseCode(status, response)
201
202 def handleErrors(self, error):
203 """Handle exceptions in https requests.
204 """
205 if self.isInvalidCertificateError(error):
206 invalid_certificate_hosts.add(
207 (self.factory.request_host, self.factory.request_port))
208 reason = InvalidHTTPSCertificate(
209 self.factory.request_host, self.factory.request_port)
210 raise reason
211 if self.isTimeout(error):
212 raise ProberTimeout(self.factory.url, self.factory.timeout)
213 raise error
214
215 def isTimeout(self, error):
216 """Checks if the error was caused by a timeout.
217 """
218 return self._isErrorFromType(error, CancelledError)
219
220 def isInvalidCertificateError(self, error):
221 """Checks if the error was caused by an invalid certificate.
222 """
223 # It might be a raw SSL error, or a twisted-encapsulated
224 # verification error (such as DNSMismatch error when the
225 # certificate is valid for a different domain, for example).
226 return self._isErrorFromType(
227 error, OpenSSL.SSL.Error, VerificationError)
228
229 def _isErrorFromType(self, error, *types):
230 """Checks if the error was caused by any of the given types.
231 """
232 if not isinstance(error.value, ResponseNeverReceived):
233 return False
234 for reason in error.value.reasons:
235 if reason.check(*types) is not None:
236 return True
237 return False
238
239
240class RedirectAwareProberProtocol(ProberProtocol):164class RedirectAwareProberProtocol(ProberProtocol):
241 """A specialized version of ProberProtocol that follows HTTP redirects."""165 """A specialized version of ProberProtocol that follows HTTP redirects."""
242166
@@ -292,8 +216,6 @@ class ProberFactory(protocol.ClientFactory):
292 connect_port = None216 connect_port = None
293 connect_path = None217 connect_path = None
294218
295 https_agent_policy = BrowserLikePolicyForHTTPS
296
297 def __init__(self, url, timeout=config.distributionmirrorprober.timeout):219 def __init__(self, url, timeout=config.distributionmirrorprober.timeout):
298 # We want the deferred to be a private attribute (_deferred) to make220 # We want the deferred to be a private attribute (_deferred) to make
299 # sure our clients will only use the deferred returned by the probe()221 # sure our clients will only use the deferred returned by the probe()
@@ -303,14 +225,9 @@ class ProberFactory(protocol.ClientFactory):
303 self.timeout = timeout225 self.timeout = timeout
304 self.timeoutCall = None226 self.timeoutCall = None
305 self.setURL(url.encode('ascii'))227 self.setURL(url.encode('ascii'))
306 self.logger = logging.getLogger('distributionmirror-prober')
307
308 @property
309 def is_https(self):
310 return self.request_scheme == 'https'
311228
312 def probe(self):229 def probe(self):
313 logger = self.logger230 logger = logging.getLogger('distributionmirror-prober')
314 # NOTE: We don't want to issue connections to any outside host when231 # NOTE: We don't want to issue connections to any outside host when
315 # running the mirror prober in a development machine, so we do this232 # running the mirror prober in a development machine, so we do this
316 # hack here.233 # hack here.
@@ -327,46 +244,13 @@ class ProberFactory(protocol.ClientFactory):
327 "host already." % self.url)244 "host already." % self.url)
328 return self._deferred245 return self._deferred
329246
330 if (self.request_host, self.request_port) in invalid_certificate_hosts:
331 reactor.callLater(
332 0, self.failed, InvalidHTTPSCertificateSkipped(self.url))
333 logger.debug("Skipping %s as it doesn't have a valid HTTPS "
334 "certificate" % self.url)
335 return self._deferred
336
337 self.connect()247 self.connect()
338 logger.debug('Probing %s' % self.url)248 logger.debug('Probing %s' % self.url)
339 return self._deferred249 return self._deferred
340250
341 def getHttpsClient(self):
342 # Should we use a proxy?
343 if not config.launchpad.http_proxy:
344 agent = Agent(
345 reactor=reactor, contextFactory=self.https_agent_policy())
346 else:
347 endpoint = HostnameEndpoint(
348 reactor, self.connect_host, self.connect_port)
349 agent = ProxyAgent(endpoint)
350 return TreqHTTPClient(agent)
351
352 def connect(self):251 def connect(self):
353 """Starts the connection and sets the self._deferred to the proper
354 task.
355 """
356 host_requests[self.request_host] += 1252 host_requests[self.request_host] += 1
357 if self.is_https:253 reactor.connectTCP(self.connect_host, self.connect_port, self)
358 treq = self.getHttpsClient()
359 self._deferred.addCallback(
360 lambda _: treq.head(
361 self.url, reactor=reactor, allow_redirects=True,
362 timeout=self.timeout))
363 error_handler = HTTPSProbeFailureHandler(self)
364 self._deferred.addCallback(error_handler.handleResponse)
365 self._deferred.addErrback(error_handler.handleErrors)
366 reactor.callWhenRunning(self._deferred.callback, None)
367 else:
368 reactor.connectTCP(self.connect_host, self.connect_port, self)
369
370 if self.timeoutCall is not None and self.timeoutCall.active():254 if self.timeoutCall is not None and self.timeoutCall.active():
371 self._cancelTimeout(None)255 self._cancelTimeout(None)
372 self.timeoutCall = reactor.callLater(256 self.timeoutCall = reactor.callLater(
@@ -385,8 +269,6 @@ class ProberFactory(protocol.ClientFactory):
385 self.connector = connector269 self.connector = connector
386270
387 def succeeded(self, status):271 def succeeded(self, status):
388 if IResponse.providedBy(status):
389 status = str(status.code)
390 self._deferred.callback(status)272 self._deferred.callback(status)
391273
392 def failed(self, reason):274 def failed(self, reason):
@@ -406,7 +288,7 @@ class ProberFactory(protocol.ClientFactory):
406 # https://bugs.squid-cache.org/show_bug.cgi?id=1758 applied. So, if288 # https://bugs.squid-cache.org/show_bug.cgi?id=1758 applied. So, if
407 # you encounter any problems with FTP URLs you'll probably have to nag289 # you encounter any problems with FTP URLs you'll probably have to nag
408 # the sysadmins to fix squid for you.290 # the sysadmins to fix squid for you.
409 if scheme not in ('http', 'https', 'ftp'):291 if scheme not in ('http', 'ftp'):
410 raise UnknownURLScheme(url)292 raise UnknownURLScheme(url)
411293
412 if scheme and host:294 if scheme and host:
@@ -494,26 +376,14 @@ class ProberTimeout(ProberError):
494376
495class BadResponseCode(ProberError):377class BadResponseCode(ProberError):
496378
497 def __init__(self, status, response=None, *args):379 def __init__(self, status, *args):
498 ProberError.__init__(self, *args)380 ProberError.__init__(self, *args)
499 self.status = status381 self.status = status
500 self.response = response
501382
502 def __str__(self):383 def __str__(self):
503 return "Bad response code: %s" % self.status384 return "Bad response code: %s" % self.status
504385
505386
506class InvalidHTTPSCertificate(ProberError):
507 def __init__(self, host, port, *args):
508 super(InvalidHTTPSCertificate, self).__init__(*args)
509 self.host = host
510 self.port = port
511
512 def __str__(self):
513 return "Invalid SSL certificate when trying to probe %s:%s" % (
514 self.host, self.port)
515
516
517class RedirectToDifferentFile(ProberError):387class RedirectToDifferentFile(ProberError):
518388
519 def __init__(self, orig_path, new_path, *args):389 def __init__(self, orig_path, new_path, *args):
@@ -539,14 +409,6 @@ class ConnectionSkipped(ProberError):
539 "host. It will be retried on the next probing run.")409 "host. It will be retried on the next probing run.")
540410
541411
542class InvalidHTTPSCertificateSkipped(ProberError):
543
544 def __str__(self):
545 return ("Connection skipped because the server doesn't have a valid "
546 "HTTPS certificate. It will be retried on the next "
547 "probing run.")
548
549
550class UnknownURLScheme(ProberError):412class UnknownURLScheme(ProberError):
551413
552 def __init__(self, url, *args):414 def __init__(self, url, *args):
@@ -567,13 +429,7 @@ class UnknownURLSchemeAfterRedirect(UnknownURLScheme):
567429
568class ArchiveMirrorProberCallbacks(LoggingMixin):430class ArchiveMirrorProberCallbacks(LoggingMixin):
569431
570 expected_failures = (432 expected_failures = (BadResponseCode, ProberTimeout, ConnectionSkipped)
571 BadResponseCode,
572 ProberTimeout,
573 ConnectionSkipped,
574 InvalidHTTPSCertificate,
575 InvalidHTTPSCertificateSkipped,
576 )
577433
578 def __init__(self, mirror, series, pocket, component, url, log_file):434 def __init__(self, mirror, series, pocket, component, url, log_file):
579 self.mirror = mirror435 self.mirror = mirror
@@ -718,8 +574,6 @@ class MirrorCDImageProberCallbacks(LoggingMixin):
718 ProberTimeout,574 ProberTimeout,
719 RedirectToDifferentFile,575 RedirectToDifferentFile,
720 UnknownURLSchemeAfterRedirect,576 UnknownURLSchemeAfterRedirect,
721 InvalidHTTPSCertificate,
722 InvalidHTTPSCertificateSkipped,
723 )577 )
724578
725 def __init__(self, mirror, distroseries, flavour, log_file):579 def __init__(self, mirror, distroseries, flavour, log_file):
diff --git a/lib/lp/registry/stories/webservice/xx-distribution-mirror.txt b/lib/lp/registry/stories/webservice/xx-distribution-mirror.txt
index 3e49ebe..db17b40 100644
--- a/lib/lp/registry/stories/webservice/xx-distribution-mirror.txt
+++ b/lib/lp/registry/stories/webservice/xx-distribution-mirror.txt
@@ -23,7 +23,6 @@ mirrors:
23 enabled: True23 enabled: True
24 ftp_base_url: None24 ftp_base_url: None
25 http_base_url: u'http://archive.ubuntu.com/ubuntu/'25 http_base_url: u'http://archive.ubuntu.com/ubuntu/'
26 https_base_url: None
27 name: u'canonical-archive'26 name: u'canonical-archive'
28 official_candidate: True27 official_candidate: True
29 owner_link: u'http://.../~mark'28 owner_link: u'http://.../~mark'
@@ -53,7 +52,6 @@ And CD image mirrors:
53 enabled: True52 enabled: True
54 ftp_base_url: None53 ftp_base_url: None
55 http_base_url: u'http://releases.ubuntu.com/'54 http_base_url: u'http://releases.ubuntu.com/'
56 https_base_url: None
57 name: u'canonical-releases'55 name: u'canonical-releases'
58 official_candidate: True56 official_candidate: True
59 owner_link: u'http://.../~mark'57 owner_link: u'http://.../~mark'
@@ -147,7 +145,6 @@ Mirror listing admins may see all:
147 enabled: True145 enabled: True
148 ftp_base_url: None146 ftp_base_url: None
149 http_base_url: u'http://localhost:11375/archive-mirror/'147 http_base_url: u'http://localhost:11375/archive-mirror/'
150 https_base_url: None
151 name: u'archive-404-mirror'148 name: u'archive-404-mirror'
152 official_candidate: True149 official_candidate: True
153 owner_link: u'http://.../~name12'150 owner_link: u'http://.../~name12'
@@ -230,7 +227,6 @@ While others can be set with the appropriate authorization:
230 enabled: True227 enabled: True
231 ftp_base_url: None228 ftp_base_url: None
232 http_base_url: u'http://releases.ubuntu.com/'229 http_base_url: u'http://releases.ubuntu.com/'
233 https_base_url: None
234 name: u'canonical-releases'230 name: u'canonical-releases'
235 official_candidate: True231 official_candidate: True
236 owner_link: u'http://.../~mark'232 owner_link: u'http://.../~mark'
diff --git a/lib/lp/registry/stories/webservice/xx-distribution.txt b/lib/lp/registry/stories/webservice/xx-distribution.txt
index 1486904..1c0ed87 100644
--- a/lib/lp/registry/stories/webservice/xx-distribution.txt
+++ b/lib/lp/registry/stories/webservice/xx-distribution.txt
@@ -159,7 +159,6 @@ packages matching (substring) the given text.
159 enabled: True159 enabled: True
160 ftp_base_url: None160 ftp_base_url: None
161 http_base_url: u'http://releases.ubuntu.com/'161 http_base_url: u'http://releases.ubuntu.com/'
162 https_base_url: None
163 name: u'canonical-releases'162 name: u'canonical-releases'
164 official_candidate: True163 official_candidate: True
165 owner_link: u'http://.../~mark'164 owner_link: u'http://.../~mark'
diff --git a/lib/lp/registry/templates/distributionmirror-index.pt b/lib/lp/registry/templates/distributionmirror-index.pt
index 27f3139..78e934d 100644
--- a/lib/lp/registry/templates/distributionmirror-index.pt
+++ b/lib/lp/registry/templates/distributionmirror-index.pt
@@ -118,10 +118,6 @@
118 <h2>Mirror location information</h2>118 <h2>Mirror location information</h2>
119119
120 <ul class="webref" id="mirror-urls">120 <ul class="webref" id="mirror-urls">
121 <li tal:condition="context/https_base_url" >
122 <a tal:content="context/https_base_url"
123 tal:attributes="href context/https_base_url">https://url/</a>
124 </li>
125 <li tal:condition="context/http_base_url" >121 <li tal:condition="context/http_base_url" >
126 <a tal:content="context/http_base_url"122 <a tal:content="context/http_base_url"
127 tal:attributes="href context/http_base_url">http://url/</a>123 tal:attributes="href context/http_base_url">http://url/</a>
diff --git a/lib/lp/registry/templates/distributionmirror-macros.pt b/lib/lp/registry/templates/distributionmirror-macros.pt
index 489f770..18f82af 100644
--- a/lib/lp/registry/templates/distributionmirror-macros.pt
+++ b/lib/lp/registry/templates/distributionmirror-macros.pt
@@ -17,7 +17,7 @@
17 <tbody>17 <tbody>
18 <tal:country_and_mirrors repeat="country_and_mirrors mirrors_by_country">18 <tal:country_and_mirrors repeat="country_and_mirrors mirrors_by_country">
19 <tr class="head">19 <tr class="head">
20 <th colspan="2"20 <th colspan="2"
21 tal:content="country_and_mirrors/country" />21 tal:content="country_and_mirrors/country" />
22 <th tal:content="country_and_mirrors/throughput"/>22 <th tal:content="country_and_mirrors/throughput"/>
23 <th tal:condition="show_mirror_type">23 <th tal:condition="show_mirror_type">
@@ -35,8 +35,6 @@
35 tal:content="mirror/title">Mirror Name</a>35 tal:content="mirror/title">Mirror Name</a>
36 </td>36 </td>
37 <td>37 <td>
38 <a tal:condition="mirror/https_base_url"
39 tal:attributes="href mirror/https_base_url">https</a>
40 <a tal:condition="mirror/http_base_url"38 <a tal:condition="mirror/http_base_url"
41 tal:attributes="href mirror/http_base_url">http</a>39 tal:attributes="href mirror/http_base_url">http</a>
42 <a tal:condition="mirror/ftp_base_url"40 <a tal:condition="mirror/ftp_base_url"
diff --git a/lib/lp/registry/tests/distributionmirror_http_server.py b/lib/lp/registry/tests/distributionmirror_http_server.py
index d12c9b3..e3e0a39 100644
--- a/lib/lp/registry/tests/distributionmirror_http_server.py
+++ b/lib/lp/registry/tests/distributionmirror_http_server.py
@@ -1,6 +1,6 @@
1#!/usr/bin/python1#!/usr/bin/python
2#2#
3# Copyright 2009-2020 Canonical Ltd. This software is licensed under the3# Copyright 2009 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).4# GNU Affero General Public License version 3 (see the file LICENSE).
55
6from twisted.web.resource import Resource6from twisted.web.resource import Resource
@@ -21,10 +21,10 @@ class DistributionMirrorTestHTTPServer(Resource):
21 :error: Respond with a '500 Internal Server Error' status.21 :error: Respond with a '500 Internal Server Error' status.
2222
23 :redirect-to-valid-mirror/*: Respond with a '302 Found' status,23 :redirect-to-valid-mirror/*: Respond with a '302 Found' status,
24 redirecting to http(s)://localhost:%(port)s/valid-mirror/*.24 redirecting to http://localhost:%(port)s/valid-mirror/*.
2525
26 :redirect-infinite-loop: Respond with a '302 Found' status, redirecting26 :redirect-infinite-loop: Respond with a '302 Found' status, redirecting
27 to http(s)://localhost:%(port)s/redirect-infinite-loop.27 to http://localhost:%(port)s/redirect-infinite-loop.
2828
29 :redirect-unknown-url-scheme: Respond with a '302 Found' status,29 :redirect-unknown-url-scheme: Respond with a '302 Found' status,
30 redirecting to ssh://localhost/redirect-unknown-url-scheme.30 redirecting to ssh://localhost/redirect-unknown-url-scheme.
@@ -32,13 +32,11 @@ class DistributionMirrorTestHTTPServer(Resource):
32 Any other path will cause the server to respond with a '404 Not Found'32 Any other path will cause the server to respond with a '404 Not Found'
33 status.33 status.
34 """34 """
35 protocol = "http"
3635
37 def getChild(self, name, request):36 def getChild(self, name, request):
38 protocol = self.protocol
39 port = request.getHost().port37 port = request.getHost().port
40 if name == 'valid-mirror':38 if name == 'valid-mirror':
41 leaf = self.__class__()39 leaf = DistributionMirrorTestHTTPServer()
42 leaf.isLeaf = True40 leaf.isLeaf = True
43 return leaf41 return leaf
44 elif name == 'timeout':42 elif name == 'timeout':
@@ -51,14 +49,12 @@ class DistributionMirrorTestHTTPServer(Resource):
51 'than one component.')49 'than one component.')
52 remaining_path = request.path.replace('/%s' % name, '')50 remaining_path = request.path.replace('/%s' % name, '')
53 leaf = RedirectingResource(51 leaf = RedirectingResource(
54 '%s://localhost:%s/valid-mirror%s' % (52 'http://localhost:%s/valid-mirror%s' % (port, remaining_path))
55 protocol, port, remaining_path))
56 leaf.isLeaf = True53 leaf.isLeaf = True
57 return leaf54 return leaf
58 elif name == 'redirect-infinite-loop':55 elif name == 'redirect-infinite-loop':
59 return RedirectingResource(56 return RedirectingResource(
60 '%s://localhost:%s/redirect-infinite-loop' %57 'http://localhost:%s/redirect-infinite-loop' % port)
61 (protocol, port))
62 elif name == 'redirect-unknown-url-scheme':58 elif name == 'redirect-unknown-url-scheme':
63 return RedirectingResource(59 return RedirectingResource(
64 'ssh://localhost/redirect-unknown-url-scheme')60 'ssh://localhost/redirect-unknown-url-scheme')
@@ -69,11 +65,6 @@ class DistributionMirrorTestHTTPServer(Resource):
69 return "Hi"65 return "Hi"
7066
7167
72class DistributionMirrorTestSecureHTTPServer(DistributionMirrorTestHTTPServer):
73 """HTTPS version of DistributionMirrorTestHTTPServer"""
74 protocol = "https"
75
76
77class RedirectingResource(Resource):68class RedirectingResource(Resource):
7869
79 def __init__(self, redirection_url):70 def __init__(self, redirection_url):
@@ -94,3 +85,4 @@ class FiveHundredResource(Resource):
94 def render_GET(self, request):85 def render_GET(self, request):
95 request.setResponseCode(500)86 request.setResponseCode(500)
96 request.write('ASPLODE!!!')87 request.write('ASPLODE!!!')
88 return NOT_DONE_YET
diff --git a/lib/lp/registry/tests/test_distributionmirror_prober.py b/lib/lp/registry/tests/test_distributionmirror_prober.py
index c3e363c..39228d2 100644
--- a/lib/lp/registry/tests/test_distributionmirror_prober.py
+++ b/lib/lp/registry/tests/test_distributionmirror_prober.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""distributionmirror-prober tests."""4"""distributionmirror-prober tests."""
@@ -11,7 +11,6 @@ import logging
11import os11import os
12from StringIO import StringIO12from StringIO import StringIO
1313
14from fixtures import MockPatchObject
15from lazr.uri import URI14from lazr.uri import URI
16import responses15import responses
17from six.moves import http_client16from six.moves import http_client
@@ -29,14 +28,9 @@ from testtools.twistedsupport import (
29from twisted.internet import (28from twisted.internet import (
30 defer,29 defer,
31 reactor,30 reactor,
32 ssl,
33 )31 )
34from twisted.python.failure import Failure32from twisted.python.failure import Failure
35from twisted.web import server33from twisted.web import server
36from twisted.web.client import (
37 BrowserLikePolicyForHTTPS,
38 ProxyAgent,
39 )
40from zope.component import getUtility34from zope.component import getUtility
41from zope.security.proxy import removeSecurityProxy35from zope.security.proxy import removeSecurityProxy
4236
@@ -50,8 +44,6 @@ from lp.registry.scripts.distributionmirror_prober import (
50 BadResponseCode,44 BadResponseCode,
51 ConnectionSkipped,45 ConnectionSkipped,
52 InfiniteLoopDetected,46 InfiniteLoopDetected,
53 InvalidHTTPSCertificate,
54 InvalidHTTPSCertificateSkipped,
55 LoggingMixin,47 LoggingMixin,
56 MAX_REDIRECTS,48 MAX_REDIRECTS,
57 MIN_REQUEST_TIMEOUT_RATIO,49 MIN_REQUEST_TIMEOUT_RATIO,
@@ -73,7 +65,6 @@ from lp.registry.scripts.distributionmirror_prober import (
73 )65 )
74from lp.registry.tests.distributionmirror_http_server import (66from lp.registry.tests.distributionmirror_http_server import (
75 DistributionMirrorTestHTTPServer,67 DistributionMirrorTestHTTPServer,
76 DistributionMirrorTestSecureHTTPServer,
77 )68 )
78from lp.services.config import config69from lp.services.config import config
79from lp.services.daemons.tachandler import TacTestSetup70from lp.services.daemons.tachandler import TacTestSetup
@@ -112,186 +103,6 @@ class HTTPServerTestSetup(TacTestSetup):
112 return os.path.join(self.root, 'distributionmirror_http_server.log')103 return os.path.join(self.root, 'distributionmirror_http_server.log')
113104
114105
115
116class LocalhostWhitelistedHTTPSPolicy(BrowserLikePolicyForHTTPS):
117 """HTTPS policy that bypasses SSL certificate check when doing requests
118 to localhost.
119 """
120
121 def creatorForNetloc(self, hostname, port):
122 # check if the hostname is in the the whitelist,
123 # otherwise return the default policy
124 if hostname == 'localhost':
125 return ssl.CertificateOptions(verify=False)
126 return super(LocalhostWhitelistedHTTPSPolicy, self).creatorForNetloc(
127 hostname, port)
128
129
130class TestProberHTTPSProtocolAndFactory(TestCase):
131 layer = TwistedLayer
132 run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory(
133 timeout=30)
134
135 def setUp(self):
136 super(TestProberHTTPSProtocolAndFactory, self).setUp()
137 root = DistributionMirrorTestSecureHTTPServer()
138 site = server.Site(root)
139 site.displayTracebacks = False
140 keys_path = os.path.join(config.root, "configs", "development")
141 keys = ssl.DefaultOpenSSLContextFactory(
142 os.path.join(keys_path, "launchpad.key"),
143 os.path.join(keys_path, "launchpad.crt"),
144 )
145 self.listening_port = reactor.listenSSL(0, site, keys)
146
147 self.addCleanup(self.listening_port.stopListening)
148
149 # Change the default policy to accept localhost self-signed
150 # certificates.
151 original_probefactory_policy = ProberFactory.https_agent_policy
152 original_redirect_policy = (
153 RedirectAwareProberFactory.https_agent_policy)
154 ProberFactory.https_agent_policy = LocalhostWhitelistedHTTPSPolicy
155 RedirectAwareProberFactory.https_agent_policy = (
156 LocalhostWhitelistedHTTPSPolicy)
157
158 for factory in (ProberFactory, RedirectAwareProberFactory):
159 self.useFixture(MockPatchObject(
160 factory, "https_agent_policy",
161 LocalhostWhitelistedHTTPSPolicy))
162
163 self.port = self.listening_port.getHost().port
164
165 self.urls = {'timeout': u'https://localhost:%s/timeout' % self.port,
166 '200': u'https://localhost:%s/valid-mirror' % self.port,
167 '500': u'https://localhost:%s/error' % self.port,
168 '404': u'https://localhost:%s/invalid-mirror' % self.port}
169 self.pushConfig('launchpad', http_proxy=None)
170
171 self.useFixture(MockPatchObject(
172 distributionmirror_prober, "host_requests", {}))
173 self.useFixture(MockPatchObject(
174 distributionmirror_prober, "host_timeouts", {}))
175 self.useFixture(MockPatchObject(
176 distributionmirror_prober, "invalid_certificate_hosts", set()))
177
178 def _createProberAndProbe(self, url):
179 prober = ProberFactory(url)
180 return prober.probe()
181
182 def test_timeout(self):
183 prober = ProberFactory(self.urls['timeout'], timeout=0.5)
184 d = prober.probe()
185 return assert_fails_with(d, ProberTimeout)
186
187 def test_500(self):
188 d = self._createProberAndProbe(self.urls['500'])
189 return assert_fails_with(d, BadResponseCode)
190
191 def test_notfound(self):
192 d = self._createProberAndProbe(self.urls['404'])
193 return assert_fails_with(d, BadResponseCode)
194
195 def test_config_no_https_proxy(self):
196 prober = ProberFactory(self.urls['200'])
197 self.assertThat(prober, MatchesStructure.byEquality(
198 request_scheme='https',
199 request_host='localhost',
200 request_port=self.port,
201 request_path='/valid-mirror',
202 connect_scheme='https',
203 connect_host='localhost',
204 connect_port=self.port,
205 connect_path='/valid-mirror'))
206
207 def test_RedirectAwareProber_follows_https_redirect(self):
208 url = 'https://localhost:%s/redirect-to-valid-mirror/file' % self.port
209 prober = RedirectAwareProberFactory(url)
210 self.assertEqual(prober.url, url)
211 deferred = prober.probe()
212
213 def got_result(result):
214 self.assertEqual(http_client.OK, result.code)
215 self.assertEqual(
216 'https://localhost:%s/valid-mirror/file' % self.port,
217 result.request.absoluteURI)
218
219 return deferred.addCallback(got_result)
220
221 def test_https_prober_uses_proxy(self):
222 root = DistributionMirrorTestSecureHTTPServer()
223 site = server.Site(root)
224 proxy_listen_port = reactor.listenTCP(0, site)
225 proxy_port = proxy_listen_port.getHost().port
226 self.pushConfig(
227 'launchpad', http_proxy='http://localhost:%s/valid-mirror/file'
228 % proxy_port)
229
230 url = 'https://localhost:%s/valid-mirror/file' % self.port
231 prober = RedirectAwareProberFactory(url)
232 self.assertEqual(prober.url, url)
233 deferred = prober.probe()
234
235 def got_result(result):
236 # We basically don't care about the result here. We just want to
237 # check that it did the request to the correct URI,
238 # and ProxyAgent was used pointing to the correct proxy.
239 agent = prober.getHttpsClient()._agent
240 self.assertIsInstance(agent, ProxyAgent)
241 self.assertEqual('localhost', agent._proxyEndpoint._hostText)
242 self.assertEqual(proxy_port, agent._proxyEndpoint._port)
243
244 self.assertEqual(
245 'https://localhost:%s/valid-mirror/file' % self.port,
246 result.value.response.request.absoluteURI)
247
248 def cleanup(*args, **kwargs):
249 proxy_listen_port.stopListening()
250
251 # Doing the proxy checks on the error callback because the
252 # proxy is dummy and always returns 404.
253 deferred.addErrback(got_result)
254 deferred.addBoth(cleanup)
255 return deferred
256
257 def test_https_fails_on_invalid_certificates(self):
258 """Changes set back the default browser-like policy for HTTPS
259 request and make sure the request is failing due to invalid
260 (self-signed) certificate.
261 """
262 url = 'https://localhost:%s/valid-mirror/file' % self.port
263 prober = RedirectAwareProberFactory(url)
264 prober.https_agent_policy = BrowserLikePolicyForHTTPS
265 self.assertEqual(prober.url, url)
266 deferred = prober.probe()
267
268 def on_failure(result):
269 self.assertIsInstance(result.value, InvalidHTTPSCertificate)
270 self.assertIn(
271 ("localhost", self.port),
272 distributionmirror_prober.invalid_certificate_hosts)
273
274 def on_success(result):
275 if result is not None:
276 self.fail(
277 "Should have raised SSL error. Got '%s' instead" % result)
278
279 deferred.addErrback(on_failure)
280 deferred.addCallback(on_success)
281 return deferred
282
283 def test_https_skips_invalid_certificates_hosts(self):
284 distributionmirror_prober.invalid_certificate_hosts.add(
285 ("localhost", self.port))
286 url = 'https://localhost:%s/valid-mirror/file' % self.port
287 prober = RedirectAwareProberFactory(url)
288 prober.https_agent_policy = BrowserLikePolicyForHTTPS
289 self.assertEqual(prober.url, url)
290 deferred = prober.probe()
291
292 return assert_fails_with(deferred, InvalidHTTPSCertificateSkipped)
293
294
295class TestProberProtocolAndFactory(TestCase):106class TestProberProtocolAndFactory(TestCase):
296107
297 layer = TwistedLayer108 layer = TwistedLayer
@@ -401,7 +212,7 @@ class TestProberProtocolAndFactory(TestCase):
401 self.assertTrue(prober.url == new_url)212 self.assertTrue(prober.url == new_url)
402 self.assertTrue(result == str(http_client.OK))213 self.assertTrue(result == str(http_client.OK))
403214
404 return deferred.addBoth(got_result)215 return deferred.addCallback(got_result)
405216
406 def test_redirectawareprober_detects_infinite_loop(self):217 def test_redirectawareprober_detects_infinite_loop(self):
407 prober = RedirectAwareProberFactory(218 prober = RedirectAwareProberFactory(
@@ -926,16 +737,12 @@ class TestMirrorCDImageProberCallbacks(TestCaseWithFactory):
926 ConnectionSkipped,737 ConnectionSkipped,
927 RedirectToDifferentFile,738 RedirectToDifferentFile,
928 UnknownURLSchemeAfterRedirect,739 UnknownURLSchemeAfterRedirect,
929 InvalidHTTPSCertificate,
930 InvalidHTTPSCertificateSkipped,
931 ]))740 ]))
932 exceptions = [BadResponseCode(str(http_client.NOT_FOUND)),741 exceptions = [BadResponseCode(str(http_client.NOT_FOUND)),
933 ProberTimeout('http://localhost/', 5),742 ProberTimeout('http://localhost/', 5),
934 ConnectionSkipped(),743 ConnectionSkipped(),
935 RedirectToDifferentFile('/foo', '/bar'),744 RedirectToDifferentFile('/foo', '/bar'),
936 UnknownURLSchemeAfterRedirect('https://localhost'),745 UnknownURLSchemeAfterRedirect('https://localhost')]
937 InvalidHTTPSCertificate('localhost', 443),
938 InvalidHTTPSCertificateSkipped("https://localhost/xx")]
939 for exception in exceptions:746 for exception in exceptions:
940 failure = callbacks.ensureOrDeleteMirrorCDImageSeries(747 failure = callbacks.ensureOrDeleteMirrorCDImageSeries(
941 [(defer.FAILURE, Failure(exception))])748 [(defer.FAILURE, Failure(exception))])
diff --git a/lib/lp/scripts/utilities/importpedant.py b/lib/lp/scripts/utilities/importpedant.py
index c7859a4..ec41baf 100644
--- a/lib/lp/scripts/utilities/importpedant.py
+++ b/lib/lp/scripts/utilities/importpedant.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2020 Canonical Ltd. This software is licensed under the1# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4from __future__ import absolute_import, print_function, unicode_literals4from __future__ import absolute_import, print_function, unicode_literals
@@ -42,8 +42,6 @@ valid_imports_not_in_all = {
42 'textwrap': set(['dedent']),42 'textwrap': set(['dedent']),
43 'testtools.testresult.real': set(['_details_to_str']),43 'testtools.testresult.real': set(['_details_to_str']),
44 'twisted.internet.threads': set(['deferToThreadPool']),44 'twisted.internet.threads': set(['deferToThreadPool']),
45 # Even docs tell us to use this class. See docs on WebClientContextFactory.
46 'twisted.web.client': set(['BrowserLikePolicyForHTTPS']),
47 'zope.component': set(45 'zope.component': set(
48 ['adapter',46 ['adapter',
49 'ComponentLookupError',47 'ComponentLookupError',
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 6c0492f..85a05b6 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -2,7 +2,7 @@
2# NOTE: The first line above must stay first; do not move the copyright2# NOTE: The first line above must stay first; do not move the copyright
3# notice to the top. See http://www.python.org/dev/peps/pep-0263/.3# notice to the top. See http://www.python.org/dev/peps/pep-0263/.
4#4#
5# Copyright 2009-2020 Canonical Ltd. This software is licensed under the5# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
6# GNU Affero General Public License version 3 (see the file LICENSE).6# GNU Affero General Public License version 3 (see the file LICENSE).
77
8"""Testing infrastructure for the Launchpad application.8"""Testing infrastructure for the Launchpad application.
@@ -3527,13 +3527,13 @@ class BareLaunchpadObjectFactory(ObjectFactory):
3527 return proberecord3527 return proberecord
35283528
3529 def makeMirror(self, distribution, displayname=None, country=None,3529 def makeMirror(self, distribution, displayname=None, country=None,
3530 http_url=None, https_url=None, ftp_url=None, rsync_url=None,3530 http_url=None, ftp_url=None, rsync_url=None,
3531 official_candidate=False):3531 official_candidate=False):
3532 """Create a mirror for the distribution."""3532 """Create a mirror for the distribution."""
3533 if displayname is None:3533 if displayname is None:
3534 displayname = self.getUniqueString("mirror")3534 displayname = self.getUniqueString("mirror")
3535 # If no URL is specified create an HTTP URL.3535 # If no URL is specified create an HTTP URL.
3536 if http_url is https_url is ftp_url is rsync_url is None:3536 if http_url is None and ftp_url is None and rsync_url is None:
3537 http_url = self.getUniqueURL()3537 http_url = self.getUniqueURL()
3538 # If no country is given use Argentina.3538 # If no country is given use Argentina.
3539 if country is None:3539 if country is None:
@@ -3547,7 +3547,6 @@ class BareLaunchpadObjectFactory(ObjectFactory):
3547 display_name=displayname,3547 display_name=displayname,
3548 description=None,3548 description=None,
3549 http_base_url=http_url,3549 http_base_url=http_url,
3550 https_base_url=https_url,
3551 ftp_base_url=ftp_url,3550 ftp_base_url=ftp_url,
3552 rsync_base_url=rsync_url,3551 rsync_base_url=rsync_url,
3553 official_candidate=official_candidate)3552 official_candidate=official_candidate)