Merge ~pappacena/launchpad:https-mirrors into launchpad:master
- Git
- lp:~pappacena/launchpad
- https-mirrors
- Merge into master
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Thiago F. Pappacena | ||||
Approved revision: | b9738272f31434e053dfd23cd10e78bcf079a91d | ||||
Merge reported by: | Otto Co-Pilot | ||||
Merged at revision: | not available | ||||
Proposed branch: | ~pappacena/launchpad:https-mirrors | ||||
Merge into: | launchpad:master | ||||
Diff against target: |
1116 lines (+477/-53) 16 files modified
lib/lp/registry/browser/distributionmirror.py (+7/-6) lib/lp/registry/browser/tests/distributionmirror-views.txt (+40/-9) lib/lp/registry/configure.zcml (+5/-3) lib/lp/registry/interfaces/distribution.py (+4/-4) lib/lp/registry/interfaces/distributionmirror.py (+21/-3) lib/lp/registry/model/distribution.py (+7/-4) lib/lp/registry/model/distributionmirror.py (+10/-2) lib/lp/registry/scripts/distributionmirror_prober.py (+153/-7) lib/lp/registry/stories/webservice/xx-distribution-mirror.txt (+4/-0) lib/lp/registry/stories/webservice/xx-distribution.txt (+1/-0) lib/lp/registry/templates/distributionmirror-index.pt (+4/-0) lib/lp/registry/templates/distributionmirror-macros.pt (+3/-1) lib/lp/registry/tests/distributionmirror_http_server.py (+15/-7) lib/lp/registry/tests/test_distributionmirror_prober.py (+196/-3) lib/lp/scripts/utilities/importpedant.py (+3/-1) lib/lp/testing/factory.py (+4/-3) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+379387@code.launchpad.net |
Commit message
Allowing to register HTTPS mirrors (both for CD images and archives).
Description of the change
Continuing the work done by Andy Brody, this MP allows us to register HTTPS mirrors, and adjust distributino mirror prober to check the health of those types of mirrors.
Original work done by Brody is available here: Original work by Brody here: https:/
Thiago F. Pappacena (pappacena) wrote : | # |
Thiago F. Pappacena (pappacena) wrote : | # |
Another topic is proxy: the current HTTP prober supports proxy (configured at config.
Colin Watson (cjwatson) wrote : | # |
I'll answer the questions briefly without (yet) doing a code review:
* Mirrors using invalid certificates should be treated as being broken, ideally with a clear error message in the prober log saying as much. releases.ubuntu.com and nl.archive.
* I think we need to support http_proxy, because IIRC some of the relevant deployments are in environments where that's how they talk to the outside world. But this shouldn't be a problem; we already use that for HTTPS in other places (e.g. lp.bugs.
Thiago F. Pappacena (pappacena) wrote : | # |
I think it's in a good state for a review now, cjwatson. It's including:
- Brody's work to add the new fields
- The changes on prober to deal with HTTPS (using treq), including proxy (using ProxyAgent) and dealing with invalid certificates (both generic SSL errors and validation errors, like DNS mismatches).
Thiago F. Pappacena (pappacena) wrote : | # |
MP for the database patch: https:/
Colin Watson (cjwatson) wrote : | # |
Thanks for chasing this up! Looks like mostly a good job on the prober changes - just a few small things. Also, you're going to need to make at least basic changes to lib/lp/
- 27f75ce... by Thiago F. Pappacena
-
recognizing https_base_url fields on webservices doc tests
- c4c455a... by Thiago F. Pappacena
-
fixing mirrors test cases
- bb1be86... by Thiago F. Pappacena
-
fixing typo and hidding missleading field description
- e952bb1... by Thiago F. Pappacena
-
refactoring to better deal with errors on https
- 7ec4172... by Thiago F. Pappacena
-
refactoring tests to use MockPatchObject
- 331890b... by Thiago F. Pappacena
-
small refactorings for tests
Thiago F. Pappacena (pappacena) wrote : | # |
I have done the requested changes. I also added a better HTTPs error code handling, and a couple of tests.
Colin Watson (cjwatson) : | # |
- b973827... by Thiago F. Pappacena
-
fixing typo and ignoring unused variable
Preview Diff
1 | diff --git a/lib/lp/registry/browser/distributionmirror.py b/lib/lp/registry/browser/distributionmirror.py | |||
2 | index fd671a0..6ae1eb7 100644 | |||
3 | --- a/lib/lp/registry/browser/distributionmirror.py | |||
4 | +++ b/lib/lp/registry/browser/distributionmirror.py | |||
5 | @@ -1,4 +1,4 @@ | |||
7 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
8 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
9 | 3 | 3 | ||
10 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
11 | @@ -207,9 +207,9 @@ class DistributionMirrorDeleteView(LaunchpadFormView): | |||
12 | 207 | class DistributionMirrorAddView(LaunchpadFormView): | 207 | class DistributionMirrorAddView(LaunchpadFormView): |
13 | 208 | schema = IDistributionMirror | 208 | schema = IDistributionMirror |
14 | 209 | field_names = [ | 209 | field_names = [ |
18 | 210 | "display_name", "description", "whiteboard", "http_base_url", | 210 | "display_name", "description", "whiteboard", "https_base_url", |
19 | 211 | "ftp_base_url", "rsync_base_url", "speed", "country", "content", | 211 | "http_base_url", "ftp_base_url", "rsync_base_url", "speed", "country", |
20 | 212 | "official_candidate", | 212 | "content", "official_candidate", |
21 | 213 | ] | 213 | ] |
22 | 214 | invariant_context = None | 214 | invariant_context = None |
23 | 215 | 215 | ||
24 | @@ -235,6 +235,7 @@ class DistributionMirrorAddView(LaunchpadFormView): | |||
25 | 235 | content=data['content'], display_name=data['display_name'], | 235 | content=data['content'], display_name=data['display_name'], |
26 | 236 | description=data['description'], | 236 | description=data['description'], |
27 | 237 | whiteboard=data['whiteboard'], | 237 | whiteboard=data['whiteboard'], |
28 | 238 | https_base_url=data['https_base_url'], | ||
29 | 238 | http_base_url=data['http_base_url'], | 239 | http_base_url=data['http_base_url'], |
30 | 239 | ftp_base_url=data['ftp_base_url'], | 240 | ftp_base_url=data['ftp_base_url'], |
31 | 240 | rsync_base_url=data['rsync_base_url'], | 241 | rsync_base_url=data['rsync_base_url'], |
32 | @@ -279,8 +280,8 @@ class DistributionMirrorEditView(LaunchpadEditFormView): | |||
33 | 279 | schema = IDistributionMirror | 280 | schema = IDistributionMirror |
34 | 280 | field_names = [ | 281 | field_names = [ |
35 | 281 | "name", "display_name", "description", "whiteboard", | 282 | "name", "display_name", "description", "whiteboard", |
38 | 282 | "http_base_url", "ftp_base_url", "rsync_base_url", "speed", | 283 | "https_base_url", "http_base_url", "ftp_base_url", "rsync_base_url", |
39 | 283 | "country", "content", "official_candidate", | 284 | "speed", "country", "content", "official_candidate", |
40 | 284 | ] | 285 | ] |
41 | 285 | 286 | ||
42 | 286 | @property | 287 | @property |
43 | diff --git a/lib/lp/registry/browser/tests/distributionmirror-views.txt b/lib/lp/registry/browser/tests/distributionmirror-views.txt | |||
44 | index e11dbd9..f8fe4c5 100644 | |||
45 | --- a/lib/lp/registry/browser/tests/distributionmirror-views.txt | |||
46 | +++ b/lib/lp/registry/browser/tests/distributionmirror-views.txt | |||
47 | @@ -44,18 +44,19 @@ The view provides a label, page_title, and cancel_url | |||
48 | 44 | >>> print view.cancel_url | 44 | >>> print view.cancel_url |
49 | 45 | http://launchpad.test/ubuntu | 45 | http://launchpad.test/ubuntu |
50 | 46 | 46 | ||
52 | 47 | A HTTP or FTP URL is required to register a mirror. | 47 | A HTTP, HTTPS or FTP URL is required to register a mirror. |
53 | 48 | 48 | ||
54 | 49 | >>> view.field_names | 49 | >>> view.field_names |
58 | 50 | ['display_name', 'description', 'whiteboard', 'http_base_url', | 50 | ['display_name', 'description', 'whiteboard', 'https_base_url', |
59 | 51 | 'ftp_base_url', 'rsync_base_url', 'speed', 'country', 'content', | 51 | 'http_base_url', 'ftp_base_url', 'rsync_base_url', 'speed', 'country', |
60 | 52 | 'official_candidate'] | 52 | 'content', 'official_candidate'] |
61 | 53 | 53 | ||
62 | 54 | >>> form = { | 54 | >>> form = { |
63 | 55 | ... 'field.display_name': 'Illuminati', | 55 | ... 'field.display_name': 'Illuminati', |
64 | 56 | ... 'field.description': 'description', | 56 | ... 'field.description': 'description', |
65 | 57 | ... 'field.whiteboard': 'whiteboard', | 57 | ... 'field.whiteboard': 'whiteboard', |
66 | 58 | ... 'field.http_base_url': 'http://secret.me/', | 58 | ... 'field.http_base_url': 'http://secret.me/', |
67 | 59 | ... 'field.https_base_url': '', | ||
68 | 59 | ... 'field.ftp_base_url': '', | 60 | ... 'field.ftp_base_url': '', |
69 | 60 | ... 'field.rsync_base_url': '', | 61 | ... 'field.rsync_base_url': '', |
70 | 61 | ... 'field.speed': 'S128K', | 62 | ... 'field.speed': 'S128K', |
71 | @@ -93,6 +94,7 @@ not significant). | |||
72 | 93 | The same is true for a FTP URL. | 94 | The same is true for a FTP URL. |
73 | 94 | 95 | ||
74 | 95 | >>> mirror.ftp_base_url = 'ftp://now-here.me/' | 96 | >>> mirror.ftp_base_url = 'ftp://now-here.me/' |
75 | 97 | >>> bad_form['field.https_base_url'] = '' | ||
76 | 96 | >>> bad_form['field.http_base_url'] = '' | 98 | >>> bad_form['field.http_base_url'] = '' |
77 | 97 | >>> bad_form['field.ftp_base_url'] = 'ftp://now-here.me' | 99 | >>> bad_form['field.ftp_base_url'] = 'ftp://now-here.me' |
78 | 98 | >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form) | 100 | >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form) |
79 | @@ -110,14 +112,15 @@ The same is true for a rsync URL. | |||
80 | 110 | ... print error[2] | 112 | ... print error[2] |
81 | 111 | The distribution mirror ... is already registered with this URL. | 113 | The distribution mirror ... is already registered with this URL. |
82 | 112 | 114 | ||
84 | 113 | A mirror must have an ftp or http URL. | 115 | A mirror must have an ftp, HTTPS or http URL. |
85 | 114 | 116 | ||
86 | 117 | >>> bad_form['field.https_base_url'] = '' | ||
87 | 115 | >>> bad_form['field.http_base_url'] = '' | 118 | >>> bad_form['field.http_base_url'] = '' |
88 | 116 | >>> bad_form['field.ftp_base_url'] = '' | 119 | >>> bad_form['field.ftp_base_url'] = '' |
89 | 117 | >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form) | 120 | >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form) |
90 | 118 | >>> for message in view.errors: | 121 | >>> for message in view.errors: |
91 | 119 | ... print message | 122 | ... print message |
93 | 120 | A mirror must have at least an HTTP or FTP URL. | 123 | A mirror must have at least an HTTP(S) or FTP URL. |
94 | 121 | 124 | ||
95 | 122 | The URL cannot contain a fragment. | 125 | The URL cannot contain a fragment. |
96 | 123 | 126 | ||
97 | @@ -135,6 +138,34 @@ The URL cannot contain a query string. | |||
98 | 135 | ... print error[2] | 138 | ... print error[2] |
99 | 136 | URIs with query strings are not allowed. | 139 | URIs with query strings are not allowed. |
100 | 137 | 140 | ||
101 | 141 | The HTTPS URL may not have an HTTP scheme. | ||
102 | 142 | |||
103 | 143 | >>> bad_form['field.http_base_url'] = '' | ||
104 | 144 | >>> bad_form['field.https_base_url'] = 'http://secret.me/#fragement' | ||
105 | 145 | >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form) | ||
106 | 146 | >>> for error in view.errors: | ||
107 | 147 | ... print error[2] | ||
108 | 148 | The URI scheme "http" is not allowed. | ||
109 | 149 | Only URIs with the following schemes may be used: https | ||
110 | 150 | |||
111 | 151 | The HTTPS URL cannot contain a fragment. | ||
112 | 152 | |||
113 | 153 | >>> bad_form['field.http_base_url'] = '' | ||
114 | 154 | >>> bad_form['field.https_base_url'] = 'https://secret.me/#fragement' | ||
115 | 155 | >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form) | ||
116 | 156 | >>> for error in view.errors: | ||
117 | 157 | ... print error[2] | ||
118 | 158 | URIs with fragment identifiers are not allowed. | ||
119 | 159 | |||
120 | 160 | The URL cannot contain a query string. | ||
121 | 161 | |||
122 | 162 | >>> bad_form['field.http_base_url'] = '' | ||
123 | 163 | >>> bad_form['field.https_base_url'] = 'https://secret.me/?query=string' | ||
124 | 164 | >>> view = create_initialized_view(ubuntu, '+newmirror', form=bad_form) | ||
125 | 165 | >>> for error in view.errors: | ||
126 | 166 | ... print error[2] | ||
127 | 167 | URIs with query strings are not allowed. | ||
128 | 168 | |||
129 | 138 | 169 | ||
130 | 139 | Reviewing a distribution mirror | 170 | Reviewing a distribution mirror |
131 | 140 | ------------------------------- | 171 | ------------------------------- |
132 | @@ -239,9 +270,9 @@ The +edit view provides a label, page_title, and cancel_url. | |||
133 | 239 | The user can edit the mirror fields. | 270 | The user can edit the mirror fields. |
134 | 240 | 271 | ||
135 | 241 | >>> view.field_names | 272 | >>> view.field_names |
139 | 242 | ['name', 'display_name', 'description', 'whiteboard', 'http_base_url', | 273 | ['name', 'display_name', 'description', 'whiteboard', 'https_base_url', |
140 | 243 | 'ftp_base_url', 'rsync_base_url', 'speed', 'country', 'content', | 274 | 'http_base_url', 'ftp_base_url', 'rsync_base_url', 'speed', 'country', |
141 | 244 | 'official_candidate'] | 275 | 'content', 'official_candidate'] |
142 | 245 | 276 | ||
143 | 246 | >>> print mirror.ftp_base_url | 277 | >>> print mirror.ftp_base_url |
144 | 247 | None | 278 | None |
145 | diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml | |||
146 | index 9be68e0..d5875c5 100644 | |||
147 | --- a/lib/lp/registry/configure.zcml | |||
148 | +++ b/lib/lp/registry/configure.zcml | |||
149 | @@ -1,4 +1,4 @@ | |||
151 | 1 | <!-- Copyright 2009-2019 Canonical Ltd. This software is licensed under the | 1 | <!-- Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
152 | 2 | GNU Affero General Public License version 3 (see the file LICENSE). | 2 | GNU Affero General Public License version 3 (see the file LICENSE). |
153 | 3 | --> | 3 | --> |
154 | 4 | 4 | ||
155 | @@ -2046,6 +2046,7 @@ | |||
156 | 2046 | description | 2046 | description |
157 | 2047 | distribution | 2047 | distribution |
158 | 2048 | http_base_url | 2048 | http_base_url |
159 | 2049 | https_base_url | ||
160 | 2049 | ftp_base_url | 2050 | ftp_base_url |
161 | 2050 | rsync_base_url | 2051 | rsync_base_url |
162 | 2051 | enabled | 2052 | enabled |
163 | @@ -2084,8 +2085,9 @@ | |||
164 | 2084 | <require | 2085 | <require |
165 | 2085 | permission="launchpad.Edit" | 2086 | permission="launchpad.Edit" |
166 | 2086 | set_attributes="name display_name description whiteboard | 2087 | set_attributes="name display_name description whiteboard |
169 | 2087 | http_base_url ftp_base_url rsync_base_url enabled | 2088 | http_base_url https_base_url ftp_base_url |
170 | 2088 | speed country content official_candidate owner" | 2089 | rsync_base_url enabled speed country content |
171 | 2090 | official_candidate owner" | ||
172 | 2089 | attributes="official_candidate whiteboard resubmitForReview" /> | 2091 | attributes="official_candidate whiteboard resubmitForReview" /> |
173 | 2090 | <require | 2092 | <require |
174 | 2091 | permission="launchpad.Admin" | 2093 | permission="launchpad.Admin" |
175 | diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py | |||
176 | index c7ddb95..a7106f6 100644 | |||
177 | --- a/lib/lp/registry/interfaces/distribution.py | |||
178 | +++ b/lib/lp/registry/interfaces/distribution.py | |||
179 | @@ -1,4 +1,4 @@ | |||
181 | 1 | # Copyright 2009-2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
182 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
183 | 3 | 3 | ||
184 | 4 | """Interfaces including and related to IDistribution.""" | 4 | """Interfaces including and related to IDistribution.""" |
185 | @@ -491,13 +491,13 @@ class IDistributionPublic( | |||
186 | 491 | """Return the country DNS mirror for a country and content type.""" | 491 | """Return the country DNS mirror for a country and content type.""" |
187 | 492 | 492 | ||
188 | 493 | def newMirror(owner, speed, country, content, display_name=None, | 493 | def newMirror(owner, speed, country, content, display_name=None, |
190 | 494 | description=None, http_base_url=None, | 494 | description=None, http_base_url=None, https_base_url=None, |
191 | 495 | ftp_base_url=None, rsync_base_url=None, enabled=False, | 495 | ftp_base_url=None, rsync_base_url=None, enabled=False, |
192 | 496 | official_candidate=False, whiteboard=None): | 496 | official_candidate=False, whiteboard=None): |
193 | 497 | """Create a new DistributionMirror for this distribution. | 497 | """Create a new DistributionMirror for this distribution. |
194 | 498 | 498 | ||
197 | 499 | At least one of http_base_url or ftp_base_url must be provided in | 499 | At least one of {http,https,ftp}_base_url must be provided in order to |
198 | 500 | order to create a mirror. | 500 | create a mirror. |
199 | 501 | """ | 501 | """ |
200 | 502 | 502 | ||
201 | 503 | def getOCIProject(name): | 503 | def getOCIProject(name): |
202 | diff --git a/lib/lp/registry/interfaces/distributionmirror.py b/lib/lp/registry/interfaces/distributionmirror.py | |||
203 | index ce6720e..4849e7a 100644 | |||
204 | --- a/lib/lp/registry/interfaces/distributionmirror.py | |||
205 | +++ b/lib/lp/registry/interfaces/distributionmirror.py | |||
206 | @@ -1,4 +1,4 @@ | |||
208 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
209 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
210 | 3 | 3 | ||
211 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
212 | @@ -302,6 +302,12 @@ class DistroMirrorHTTPURIField(DistroMirrorURIField): | |||
213 | 302 | return getUtility(IDistributionMirrorSet).getByHttpUrl(url) | 302 | return getUtility(IDistributionMirrorSet).getByHttpUrl(url) |
214 | 303 | 303 | ||
215 | 304 | 304 | ||
216 | 305 | class DistroMirrorHTTPSURIField(DistroMirrorURIField): | ||
217 | 306 | |||
218 | 307 | def getMirrorByURI(self, url): | ||
219 | 308 | return getUtility(IDistributionMirrorSet).getByHttpsUrl(url) | ||
220 | 309 | |||
221 | 310 | |||
222 | 305 | class DistroMirrorFTPURIField(DistroMirrorURIField): | 311 | class DistroMirrorFTPURIField(DistroMirrorURIField): |
223 | 306 | 312 | ||
224 | 307 | def getMirrorByURI(self, url): | 313 | def getMirrorByURI(self, url): |
225 | @@ -349,6 +355,14 @@ class IDistributionMirror(Interface): | |||
226 | 349 | allowed_schemes=['http'], allow_userinfo=False, | 355 | allowed_schemes=['http'], allow_userinfo=False, |
227 | 350 | allow_query=False, allow_fragment=False, trailing_slash=True, | 356 | allow_query=False, allow_fragment=False, trailing_slash=True, |
228 | 351 | description=_('e.g.: http://archive.ubuntu.com/ubuntu/'))) | 357 | description=_('e.g.: http://archive.ubuntu.com/ubuntu/'))) |
229 | 358 | https_base_url = exported(DistroMirrorHTTPSURIField( | ||
230 | 359 | title=_('HTTPS URL'), required=False, readonly=False, | ||
231 | 360 | allowed_schemes=['https'], allow_userinfo=False, | ||
232 | 361 | allow_query=False, allow_fragment=False, trailing_slash=True, | ||
233 | 362 | # XXX: pappacena 2020-02-21: Add description field with a more | ||
234 | 363 | # suitable example once we have https for archive.ubuntu.com, like: | ||
235 | 364 | # description=_('e.g.: http://archive.ubuntu.com/ubuntu/') | ||
236 | 365 | )) | ||
237 | 352 | ftp_base_url = exported(DistroMirrorFTPURIField( | 366 | ftp_base_url = exported(DistroMirrorFTPURIField( |
238 | 353 | title=_('FTP URL'), required=False, readonly=False, | 367 | title=_('FTP URL'), required=False, readonly=False, |
239 | 354 | allowed_schemes=['ftp'], allow_userinfo=False, | 368 | allowed_schemes=['ftp'], allow_userinfo=False, |
240 | @@ -435,8 +449,9 @@ class IDistributionMirror(Interface): | |||
241 | 435 | 449 | ||
242 | 436 | @invariant | 450 | @invariant |
243 | 437 | def mirrorMustHaveHTTPOrFTPURL(mirror): | 451 | def mirrorMustHaveHTTPOrFTPURL(mirror): |
246 | 438 | if not (mirror.http_base_url or mirror.ftp_base_url): | 452 | if not (mirror.http_base_url or mirror.https_base_url or |
247 | 439 | raise Invalid('A mirror must have at least an HTTP or FTP URL.') | 453 | mirror.ftp_base_url): |
248 | 454 | raise Invalid('A mirror must have at least an HTTP(S) or FTP URL.') | ||
249 | 440 | 455 | ||
250 | 441 | def getSummarizedMirroredSourceSeries(): | 456 | def getSummarizedMirroredSourceSeries(): |
251 | 442 | """Return a summarized list of this distribution_mirror's | 457 | """Return a summarized list of this distribution_mirror's |
252 | @@ -614,6 +629,9 @@ class IDistributionMirrorSet(Interface): | |||
253 | 614 | def getByHttpUrl(url): | 629 | def getByHttpUrl(url): |
254 | 615 | """Return the mirror with the given HTTP URL or None.""" | 630 | """Return the mirror with the given HTTP URL or None.""" |
255 | 616 | 631 | ||
256 | 632 | def getByHttpsUrl(url): | ||
257 | 633 | """Return the mirror with the given HTTPS URL or None.""" | ||
258 | 634 | |||
259 | 617 | def getByFtpUrl(url): | 635 | def getByFtpUrl(url): |
260 | 618 | """Return the mirror with the given FTP URL or None.""" | 636 | """Return the mirror with the given FTP URL or None.""" |
261 | 619 | 637 | ||
262 | diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py | |||
263 | index 2288d0e..f78908b 100644 | |||
264 | --- a/lib/lp/registry/model/distribution.py | |||
265 | +++ b/lib/lp/registry/model/distribution.py | |||
266 | @@ -1,4 +1,4 @@ | |||
268 | 1 | # Copyright 2009-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
269 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
270 | 3 | 3 | ||
271 | 4 | """Database classes for implementing distribution items.""" | 4 | """Database classes for implementing distribution items.""" |
272 | @@ -709,7 +709,7 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements, | |||
273 | 709 | country_dns_mirror=True).one() | 709 | country_dns_mirror=True).one() |
274 | 710 | 710 | ||
275 | 711 | def newMirror(self, owner, speed, country, content, display_name=None, | 711 | def newMirror(self, owner, speed, country, content, display_name=None, |
277 | 712 | description=None, http_base_url=None, | 712 | description=None, http_base_url=None, https_base_url=None, |
278 | 713 | ftp_base_url=None, rsync_base_url=None, | 713 | ftp_base_url=None, rsync_base_url=None, |
279 | 714 | official_candidate=False, enabled=False, | 714 | official_candidate=False, enabled=False, |
280 | 715 | whiteboard=None): | 715 | whiteboard=None): |
281 | @@ -722,15 +722,17 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements, | |||
282 | 722 | return None | 722 | return None |
283 | 723 | 723 | ||
284 | 724 | urls = {'http_base_url': http_base_url, | 724 | urls = {'http_base_url': http_base_url, |
285 | 725 | 'https_base_url': https_base_url, | ||
286 | 725 | 'ftp_base_url': ftp_base_url, | 726 | 'ftp_base_url': ftp_base_url, |
287 | 726 | 'rsync_base_url': rsync_base_url} | 727 | 'rsync_base_url': rsync_base_url} |
288 | 727 | for name, value in urls.items(): | 728 | for name, value in urls.items(): |
289 | 728 | if value is not None: | 729 | if value is not None: |
290 | 729 | urls[name] = IDistributionMirror[name].normalize(value) | 730 | urls[name] = IDistributionMirror[name].normalize(value) |
291 | 730 | 731 | ||
293 | 731 | url = urls['http_base_url'] or urls['ftp_base_url'] | 732 | url = (urls['https_base_url'] or urls['http_base_url'] or |
294 | 733 | urls['ftp_base_url']) | ||
295 | 732 | assert url is not None, ( | 734 | assert url is not None, ( |
297 | 733 | "A mirror must provide either an HTTP or FTP URL (or both).") | 735 | "A mirror must provide at least one HTTP/HTTPS/FTP URL.") |
298 | 734 | dummy, host, dummy, dummy, dummy, dummy = urlparse(url) | 736 | dummy, host, dummy, dummy, dummy, dummy = urlparse(url) |
299 | 735 | name = sanitize_name('%s-%s' % (host, content.name.lower())) | 737 | name = sanitize_name('%s-%s' % (host, content.name.lower())) |
300 | 736 | 738 | ||
301 | @@ -744,6 +746,7 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements, | |||
302 | 744 | distribution=self, owner=owner, name=name, speed=speed, | 746 | distribution=self, owner=owner, name=name, speed=speed, |
303 | 745 | country=country, content=content, display_name=display_name, | 747 | country=country, content=content, display_name=display_name, |
304 | 746 | description=description, http_base_url=urls['http_base_url'], | 748 | description=description, http_base_url=urls['http_base_url'], |
305 | 749 | https_base_url=urls['https_base_url'], | ||
306 | 747 | ftp_base_url=urls['ftp_base_url'], | 750 | ftp_base_url=urls['ftp_base_url'], |
307 | 748 | rsync_base_url=urls['rsync_base_url'], | 751 | rsync_base_url=urls['rsync_base_url'], |
308 | 749 | official_candidate=official_candidate, enabled=enabled, | 752 | official_candidate=official_candidate, enabled=enabled, |
309 | diff --git a/lib/lp/registry/model/distributionmirror.py b/lib/lp/registry/model/distributionmirror.py | |||
310 | index 747a2b0..323fb26 100644 | |||
311 | --- a/lib/lp/registry/model/distributionmirror.py | |||
312 | +++ b/lib/lp/registry/model/distributionmirror.py | |||
313 | @@ -1,4 +1,4 @@ | |||
315 | 1 | # Copyright 2009-2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
316 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
317 | 3 | 3 | ||
318 | 4 | """Module docstring goes here.""" | 4 | """Module docstring goes here.""" |
319 | @@ -129,6 +129,8 @@ class DistributionMirror(SQLBase): | |||
320 | 129 | notNull=False, default=None) | 129 | notNull=False, default=None) |
321 | 130 | http_base_url = StringCol( | 130 | http_base_url = StringCol( |
322 | 131 | notNull=False, default=None, unique=True) | 131 | notNull=False, default=None, unique=True) |
323 | 132 | https_base_url = StringCol( | ||
324 | 133 | notNull=False, default=None, unique=True) | ||
325 | 132 | ftp_base_url = StringCol( | 134 | ftp_base_url = StringCol( |
326 | 133 | notNull=False, default=None, unique=True) | 135 | notNull=False, default=None, unique=True) |
327 | 134 | rsync_base_url = StringCol( | 136 | rsync_base_url = StringCol( |
328 | @@ -155,7 +157,9 @@ class DistributionMirror(SQLBase): | |||
329 | 155 | @property | 157 | @property |
330 | 156 | def base_url(self): | 158 | def base_url(self): |
331 | 157 | """See IDistributionMirror""" | 159 | """See IDistributionMirror""" |
333 | 158 | if self.http_base_url is not None: | 160 | if self.https_base_url is not None: |
334 | 161 | return self.https_base_url | ||
335 | 162 | elif self.http_base_url is not None: | ||
336 | 159 | return self.http_base_url | 163 | return self.http_base_url |
337 | 160 | else: | 164 | else: |
338 | 161 | return self.ftp_base_url | 165 | return self.ftp_base_url |
339 | @@ -677,6 +681,10 @@ class DistributionMirrorSet: | |||
340 | 677 | """See IDistributionMirrorSet""" | 681 | """See IDistributionMirrorSet""" |
341 | 678 | return DistributionMirror.selectOneBy(http_base_url=url) | 682 | return DistributionMirror.selectOneBy(http_base_url=url) |
342 | 679 | 683 | ||
343 | 684 | def getByHttpsUrl(self, url): | ||
344 | 685 | """See IDistributionMirrorSet""" | ||
345 | 686 | return DistributionMirror.selectOneBy(https_base_url=url) | ||
346 | 687 | |||
347 | 680 | def getByFtpUrl(self, url): | 688 | def getByFtpUrl(self, url): |
348 | 681 | """See IDistributionMirrorSet""" | 689 | """See IDistributionMirrorSet""" |
349 | 682 | return DistributionMirror.selectOneBy(ftp_base_url=url) | 690 | return DistributionMirror.selectOneBy(ftp_base_url=url) |
350 | diff --git a/lib/lp/registry/scripts/distributionmirror_prober.py b/lib/lp/registry/scripts/distributionmirror_prober.py | |||
351 | index 916d4ba..c633fb8 100644 | |||
352 | --- a/lib/lp/registry/scripts/distributionmirror_prober.py | |||
353 | +++ b/lib/lp/registry/scripts/distributionmirror_prober.py | |||
354 | @@ -1,4 +1,4 @@ | |||
356 | 1 | # Copyright 2009-2019 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
357 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
358 | 3 | 3 | ||
359 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
360 | @@ -12,6 +12,7 @@ import logging | |||
361 | 12 | import os.path | 12 | import os.path |
362 | 13 | from StringIO import StringIO | 13 | from StringIO import StringIO |
363 | 14 | 14 | ||
364 | 15 | import OpenSSL | ||
365 | 15 | import requests | 16 | import requests |
366 | 16 | from six.moves import http_client | 17 | from six.moves import http_client |
367 | 17 | from six.moves.urllib.parse import ( | 18 | from six.moves.urllib.parse import ( |
368 | @@ -20,14 +21,27 @@ from six.moves.urllib.parse import ( | |||
369 | 20 | urlparse, | 21 | urlparse, |
370 | 21 | urlunparse, | 22 | urlunparse, |
371 | 22 | ) | 23 | ) |
372 | 24 | from treq.client import HTTPClient as TreqHTTPClient | ||
373 | 23 | from twisted.internet import ( | 25 | from twisted.internet import ( |
374 | 24 | defer, | 26 | defer, |
375 | 25 | protocol, | 27 | protocol, |
376 | 26 | reactor, | 28 | reactor, |
377 | 27 | ) | 29 | ) |
379 | 28 | from twisted.internet.defer import DeferredSemaphore | 30 | from twisted.internet.defer import ( |
380 | 31 | CancelledError, | ||
381 | 32 | DeferredSemaphore, | ||
382 | 33 | ) | ||
383 | 34 | from twisted.internet.endpoints import HostnameEndpoint | ||
384 | 35 | from twisted.internet.ssl import VerificationError | ||
385 | 29 | from twisted.python.failure import Failure | 36 | from twisted.python.failure import Failure |
386 | 37 | from twisted.web.client import ( | ||
387 | 38 | Agent, | ||
388 | 39 | BrowserLikePolicyForHTTPS, | ||
389 | 40 | ProxyAgent, | ||
390 | 41 | ResponseNeverReceived, | ||
391 | 42 | ) | ||
392 | 30 | from twisted.web.http import HTTPClient | 43 | from twisted.web.http import HTTPClient |
393 | 44 | from twisted.web.iweb import IResponse | ||
394 | 31 | from zope.component import getUtility | 45 | from zope.component import getUtility |
395 | 32 | 46 | ||
396 | 33 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities | 47 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
397 | @@ -50,6 +64,7 @@ from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries | |||
398 | 50 | # IMPORTANT: Changing these values can cause lots of false negatives when | 64 | # IMPORTANT: Changing these values can cause lots of false negatives when |
399 | 51 | # probing mirrors, so please don't change them unless you know what you're | 65 | # probing mirrors, so please don't change them unless you know what you're |
400 | 52 | # doing. | 66 | # doing. |
401 | 67 | |||
402 | 53 | MIN_REQUEST_TIMEOUT_RATIO = 3 | 68 | MIN_REQUEST_TIMEOUT_RATIO = 3 |
403 | 54 | MIN_REQUESTS_TO_CONSIDER_RATIO = 30 | 69 | MIN_REQUESTS_TO_CONSIDER_RATIO = 30 |
404 | 55 | 70 | ||
405 | @@ -57,6 +72,9 @@ MIN_REQUESTS_TO_CONSIDER_RATIO = 30 | |||
406 | 57 | # We need to get rid of these global dicts in this module. | 72 | # We need to get rid of these global dicts in this module. |
407 | 58 | host_requests = {} | 73 | host_requests = {} |
408 | 59 | host_timeouts = {} | 74 | host_timeouts = {} |
409 | 75 | # Set of invalid certificate (host, port) tuples, to avoid doing HTTPS calls | ||
410 | 76 | # to hosts we already know they are not valid. | ||
411 | 77 | invalid_certificate_hosts = set() | ||
412 | 60 | 78 | ||
413 | 61 | MAX_REDIRECTS = 3 | 79 | MAX_REDIRECTS = 3 |
414 | 62 | 80 | ||
415 | @@ -161,6 +179,64 @@ class ProberProtocol(HTTPClient): | |||
416 | 161 | pass | 179 | pass |
417 | 162 | 180 | ||
418 | 163 | 181 | ||
419 | 182 | class HTTPSProbeFailureHandler: | ||
420 | 183 | """Handler to translate general errors into expected errors on HTTPS | ||
421 | 184 | connections.""" | ||
422 | 185 | def __init__(self, factory): | ||
423 | 186 | self.factory = factory | ||
424 | 187 | |||
425 | 188 | def handleResponse(self, response): | ||
426 | 189 | """Translates any request with return code different from 200 into | ||
427 | 190 | an error in the callback chain. | ||
428 | 191 | |||
429 | 192 | Note that other 2xx codes that are not 200 are considered errors too. | ||
430 | 193 | This behaviour is the same as seen in ProberProtocol.handleStatus, | ||
431 | 194 | for HTTP responses. | ||
432 | 195 | """ | ||
433 | 196 | status = response.code | ||
434 | 197 | if status == http_client.OK: | ||
435 | 198 | return response | ||
436 | 199 | else: | ||
437 | 200 | raise BadResponseCode(status, response) | ||
438 | 201 | |||
439 | 202 | def handleErrors(self, error): | ||
440 | 203 | """Handle exceptions in https requests. | ||
441 | 204 | """ | ||
442 | 205 | if self.isInvalidCertificateError(error): | ||
443 | 206 | invalid_certificate_hosts.add( | ||
444 | 207 | (self.factory.request_host, self.factory.request_port)) | ||
445 | 208 | reason = InvalidHTTPSCertificate( | ||
446 | 209 | self.factory.request_host, self.factory.request_port) | ||
447 | 210 | raise reason | ||
448 | 211 | if self.isTimeout(error): | ||
449 | 212 | raise ProberTimeout(self.factory.url, self.factory.timeout) | ||
450 | 213 | raise error | ||
451 | 214 | |||
452 | 215 | def isTimeout(self, error): | ||
453 | 216 | """Checks if the error was caused by a timeout. | ||
454 | 217 | """ | ||
455 | 218 | return self._isErrorFromType(error, CancelledError) | ||
456 | 219 | |||
457 | 220 | def isInvalidCertificateError(self, error): | ||
458 | 221 | """Checks if the error was caused by an invalid certificate. | ||
459 | 222 | """ | ||
460 | 223 | # It might be a raw SSL error, or a twisted-encapsulated | ||
461 | 224 | # verification error (such as DNSMismatch error when the | ||
462 | 225 | # certificate is valid for a different domain, for example). | ||
463 | 226 | return self._isErrorFromType( | ||
464 | 227 | error, OpenSSL.SSL.Error, VerificationError) | ||
465 | 228 | |||
466 | 229 | def _isErrorFromType(self, error, *types): | ||
467 | 230 | """Checks if the error was caused by any of the given types. | ||
468 | 231 | """ | ||
469 | 232 | if not isinstance(error.value, ResponseNeverReceived): | ||
470 | 233 | return False | ||
471 | 234 | for reason in error.value.reasons: | ||
472 | 235 | if reason.check(*types) is not None: | ||
473 | 236 | return True | ||
474 | 237 | return False | ||
475 | 238 | |||
476 | 239 | |||
477 | 164 | class RedirectAwareProberProtocol(ProberProtocol): | 240 | class RedirectAwareProberProtocol(ProberProtocol): |
478 | 165 | """A specialized version of ProberProtocol that follows HTTP redirects.""" | 241 | """A specialized version of ProberProtocol that follows HTTP redirects.""" |
479 | 166 | 242 | ||
480 | @@ -216,6 +292,8 @@ class ProberFactory(protocol.ClientFactory): | |||
481 | 216 | connect_port = None | 292 | connect_port = None |
482 | 217 | connect_path = None | 293 | connect_path = None |
483 | 218 | 294 | ||
484 | 295 | https_agent_policy = BrowserLikePolicyForHTTPS | ||
485 | 296 | |||
486 | 219 | def __init__(self, url, timeout=config.distributionmirrorprober.timeout): | 297 | def __init__(self, url, timeout=config.distributionmirrorprober.timeout): |
487 | 220 | # We want the deferred to be a private attribute (_deferred) to make | 298 | # We want the deferred to be a private attribute (_deferred) to make |
488 | 221 | # sure our clients will only use the deferred returned by the probe() | 299 | # sure our clients will only use the deferred returned by the probe() |
489 | @@ -225,9 +303,14 @@ class ProberFactory(protocol.ClientFactory): | |||
490 | 225 | self.timeout = timeout | 303 | self.timeout = timeout |
491 | 226 | self.timeoutCall = None | 304 | self.timeoutCall = None |
492 | 227 | self.setURL(url.encode('ascii')) | 305 | self.setURL(url.encode('ascii')) |
493 | 306 | self.logger = logging.getLogger('distributionmirror-prober') | ||
494 | 307 | |||
495 | 308 | @property | ||
496 | 309 | def is_https(self): | ||
497 | 310 | return self.request_scheme == 'https' | ||
498 | 228 | 311 | ||
499 | 229 | def probe(self): | 312 | def probe(self): |
501 | 230 | logger = logging.getLogger('distributionmirror-prober') | 313 | logger = self.logger |
502 | 231 | # NOTE: We don't want to issue connections to any outside host when | 314 | # NOTE: We don't want to issue connections to any outside host when |
503 | 232 | # running the mirror prober in a development machine, so we do this | 315 | # running the mirror prober in a development machine, so we do this |
504 | 233 | # hack here. | 316 | # hack here. |
505 | @@ -244,13 +327,46 @@ class ProberFactory(protocol.ClientFactory): | |||
506 | 244 | "host already." % self.url) | 327 | "host already." % self.url) |
507 | 245 | return self._deferred | 328 | return self._deferred |
508 | 246 | 329 | ||
509 | 330 | if (self.request_host, self.request_port) in invalid_certificate_hosts: | ||
510 | 331 | reactor.callLater( | ||
511 | 332 | 0, self.failed, InvalidHTTPSCertificateSkipped(self.url)) | ||
512 | 333 | logger.debug("Skipping %s as it doesn't have a valid HTTPS " | ||
513 | 334 | "certificate" % self.url) | ||
514 | 335 | return self._deferred | ||
515 | 336 | |||
516 | 247 | self.connect() | 337 | self.connect() |
517 | 248 | logger.debug('Probing %s' % self.url) | 338 | logger.debug('Probing %s' % self.url) |
518 | 249 | return self._deferred | 339 | return self._deferred |
519 | 250 | 340 | ||
520 | 341 | def getHttpsClient(self): | ||
521 | 342 | # Should we use a proxy? | ||
522 | 343 | if not config.launchpad.http_proxy: | ||
523 | 344 | agent = Agent( | ||
524 | 345 | reactor=reactor, contextFactory=self.https_agent_policy()) | ||
525 | 346 | else: | ||
526 | 347 | endpoint = HostnameEndpoint( | ||
527 | 348 | reactor, self.connect_host, self.connect_port) | ||
528 | 349 | agent = ProxyAgent(endpoint) | ||
529 | 350 | return TreqHTTPClient(agent) | ||
530 | 351 | |||
531 | 251 | def connect(self): | 352 | def connect(self): |
532 | 353 | """Starts the connection and sets the self._deferred to the proper | ||
533 | 354 | task. | ||
534 | 355 | """ | ||
535 | 252 | host_requests[self.request_host] += 1 | 356 | host_requests[self.request_host] += 1 |
537 | 253 | reactor.connectTCP(self.connect_host, self.connect_port, self) | 357 | if self.is_https: |
538 | 358 | treq = self.getHttpsClient() | ||
539 | 359 | self._deferred.addCallback( | ||
540 | 360 | lambda _: treq.head( | ||
541 | 361 | self.url, reactor=reactor, allow_redirects=True, | ||
542 | 362 | timeout=self.timeout)) | ||
543 | 363 | error_handler = HTTPSProbeFailureHandler(self) | ||
544 | 364 | self._deferred.addCallback(error_handler.handleResponse) | ||
545 | 365 | self._deferred.addErrback(error_handler.handleErrors) | ||
546 | 366 | reactor.callWhenRunning(self._deferred.callback, None) | ||
547 | 367 | else: | ||
548 | 368 | reactor.connectTCP(self.connect_host, self.connect_port, self) | ||
549 | 369 | |||
550 | 254 | if self.timeoutCall is not None and self.timeoutCall.active(): | 370 | if self.timeoutCall is not None and self.timeoutCall.active(): |
551 | 255 | self._cancelTimeout(None) | 371 | self._cancelTimeout(None) |
552 | 256 | self.timeoutCall = reactor.callLater( | 372 | self.timeoutCall = reactor.callLater( |
553 | @@ -269,6 +385,8 @@ class ProberFactory(protocol.ClientFactory): | |||
554 | 269 | self.connector = connector | 385 | self.connector = connector |
555 | 270 | 386 | ||
556 | 271 | def succeeded(self, status): | 387 | def succeeded(self, status): |
557 | 388 | if IResponse.providedBy(status): | ||
558 | 389 | status = str(status.code) | ||
559 | 272 | self._deferred.callback(status) | 390 | self._deferred.callback(status) |
560 | 273 | 391 | ||
561 | 274 | def failed(self, reason): | 392 | def failed(self, reason): |
562 | @@ -288,7 +406,7 @@ class ProberFactory(protocol.ClientFactory): | |||
563 | 288 | # https://bugs.squid-cache.org/show_bug.cgi?id=1758 applied. So, if | 406 | # https://bugs.squid-cache.org/show_bug.cgi?id=1758 applied. So, if |
564 | 289 | # you encounter any problems with FTP URLs you'll probably have to nag | 407 | # you encounter any problems with FTP URLs you'll probably have to nag |
565 | 290 | # the sysadmins to fix squid for you. | 408 | # the sysadmins to fix squid for you. |
567 | 291 | if scheme not in ('http', 'ftp'): | 409 | if scheme not in ('http', 'https', 'ftp'): |
568 | 292 | raise UnknownURLScheme(url) | 410 | raise UnknownURLScheme(url) |
569 | 293 | 411 | ||
570 | 294 | if scheme and host: | 412 | if scheme and host: |
571 | @@ -376,14 +494,26 @@ class ProberTimeout(ProberError): | |||
572 | 376 | 494 | ||
573 | 377 | class BadResponseCode(ProberError): | 495 | class BadResponseCode(ProberError): |
574 | 378 | 496 | ||
576 | 379 | def __init__(self, status, *args): | 497 | def __init__(self, status, response=None, *args): |
577 | 380 | ProberError.__init__(self, *args) | 498 | ProberError.__init__(self, *args) |
578 | 381 | self.status = status | 499 | self.status = status |
579 | 500 | self.response = response | ||
580 | 382 | 501 | ||
581 | 383 | def __str__(self): | 502 | def __str__(self): |
582 | 384 | return "Bad response code: %s" % self.status | 503 | return "Bad response code: %s" % self.status |
583 | 385 | 504 | ||
584 | 386 | 505 | ||
585 | 506 | class InvalidHTTPSCertificate(ProberError): | ||
586 | 507 | def __init__(self, host, port, *args): | ||
587 | 508 | super(InvalidHTTPSCertificate, self).__init__(*args) | ||
588 | 509 | self.host = host | ||
589 | 510 | self.port = port | ||
590 | 511 | |||
591 | 512 | def __str__(self): | ||
592 | 513 | return "Invalid SSL certificate when trying to probe %s:%s" % ( | ||
593 | 514 | self.host, self.port) | ||
594 | 515 | |||
595 | 516 | |||
596 | 387 | class RedirectToDifferentFile(ProberError): | 517 | class RedirectToDifferentFile(ProberError): |
597 | 388 | 518 | ||
598 | 389 | def __init__(self, orig_path, new_path, *args): | 519 | def __init__(self, orig_path, new_path, *args): |
599 | @@ -409,6 +539,14 @@ class ConnectionSkipped(ProberError): | |||
600 | 409 | "host. It will be retried on the next probing run.") | 539 | "host. It will be retried on the next probing run.") |
601 | 410 | 540 | ||
602 | 411 | 541 | ||
603 | 542 | class InvalidHTTPSCertificateSkipped(ProberError): | ||
604 | 543 | |||
605 | 544 | def __str__(self): | ||
606 | 545 | return ("Connection skipped because the server doesn't have a valid " | ||
607 | 546 | "HTTPS certificate. It will be retried on the next " | ||
608 | 547 | "probing run.") | ||
609 | 548 | |||
610 | 549 | |||
611 | 412 | class UnknownURLScheme(ProberError): | 550 | class UnknownURLScheme(ProberError): |
612 | 413 | 551 | ||
613 | 414 | def __init__(self, url, *args): | 552 | def __init__(self, url, *args): |
614 | @@ -429,7 +567,13 @@ class UnknownURLSchemeAfterRedirect(UnknownURLScheme): | |||
615 | 429 | 567 | ||
616 | 430 | class ArchiveMirrorProberCallbacks(LoggingMixin): | 568 | class ArchiveMirrorProberCallbacks(LoggingMixin): |
617 | 431 | 569 | ||
619 | 432 | expected_failures = (BadResponseCode, ProberTimeout, ConnectionSkipped) | 570 | expected_failures = ( |
620 | 571 | BadResponseCode, | ||
621 | 572 | ProberTimeout, | ||
622 | 573 | ConnectionSkipped, | ||
623 | 574 | InvalidHTTPSCertificate, | ||
624 | 575 | InvalidHTTPSCertificateSkipped, | ||
625 | 576 | ) | ||
626 | 433 | 577 | ||
627 | 434 | def __init__(self, mirror, series, pocket, component, url, log_file): | 578 | def __init__(self, mirror, series, pocket, component, url, log_file): |
628 | 435 | self.mirror = mirror | 579 | self.mirror = mirror |
629 | @@ -574,6 +718,8 @@ class MirrorCDImageProberCallbacks(LoggingMixin): | |||
630 | 574 | ProberTimeout, | 718 | ProberTimeout, |
631 | 575 | RedirectToDifferentFile, | 719 | RedirectToDifferentFile, |
632 | 576 | UnknownURLSchemeAfterRedirect, | 720 | UnknownURLSchemeAfterRedirect, |
633 | 721 | InvalidHTTPSCertificate, | ||
634 | 722 | InvalidHTTPSCertificateSkipped, | ||
635 | 577 | ) | 723 | ) |
636 | 578 | 724 | ||
637 | 579 | def __init__(self, mirror, distroseries, flavour, log_file): | 725 | def __init__(self, mirror, distroseries, flavour, log_file): |
638 | diff --git a/lib/lp/registry/stories/webservice/xx-distribution-mirror.txt b/lib/lp/registry/stories/webservice/xx-distribution-mirror.txt | |||
639 | index db17b40..3e49ebe 100644 | |||
640 | --- a/lib/lp/registry/stories/webservice/xx-distribution-mirror.txt | |||
641 | +++ b/lib/lp/registry/stories/webservice/xx-distribution-mirror.txt | |||
642 | @@ -23,6 +23,7 @@ mirrors: | |||
643 | 23 | enabled: True | 23 | enabled: True |
644 | 24 | ftp_base_url: None | 24 | ftp_base_url: None |
645 | 25 | http_base_url: u'http://archive.ubuntu.com/ubuntu/' | 25 | http_base_url: u'http://archive.ubuntu.com/ubuntu/' |
646 | 26 | https_base_url: None | ||
647 | 26 | name: u'canonical-archive' | 27 | name: u'canonical-archive' |
648 | 27 | official_candidate: True | 28 | official_candidate: True |
649 | 28 | owner_link: u'http://.../~mark' | 29 | owner_link: u'http://.../~mark' |
650 | @@ -52,6 +53,7 @@ And CD image mirrors: | |||
651 | 52 | enabled: True | 53 | enabled: True |
652 | 53 | ftp_base_url: None | 54 | ftp_base_url: None |
653 | 54 | http_base_url: u'http://releases.ubuntu.com/' | 55 | http_base_url: u'http://releases.ubuntu.com/' |
654 | 56 | https_base_url: None | ||
655 | 55 | name: u'canonical-releases' | 57 | name: u'canonical-releases' |
656 | 56 | official_candidate: True | 58 | official_candidate: True |
657 | 57 | owner_link: u'http://.../~mark' | 59 | owner_link: u'http://.../~mark' |
658 | @@ -145,6 +147,7 @@ Mirror listing admins may see all: | |||
659 | 145 | enabled: True | 147 | enabled: True |
660 | 146 | ftp_base_url: None | 148 | ftp_base_url: None |
661 | 147 | http_base_url: u'http://localhost:11375/archive-mirror/' | 149 | http_base_url: u'http://localhost:11375/archive-mirror/' |
662 | 150 | https_base_url: None | ||
663 | 148 | name: u'archive-404-mirror' | 151 | name: u'archive-404-mirror' |
664 | 149 | official_candidate: True | 152 | official_candidate: True |
665 | 150 | owner_link: u'http://.../~name12' | 153 | owner_link: u'http://.../~name12' |
666 | @@ -227,6 +230,7 @@ While others can be set with the appropriate authorization: | |||
667 | 227 | enabled: True | 230 | enabled: True |
668 | 228 | ftp_base_url: None | 231 | ftp_base_url: None |
669 | 229 | http_base_url: u'http://releases.ubuntu.com/' | 232 | http_base_url: u'http://releases.ubuntu.com/' |
670 | 233 | https_base_url: None | ||
671 | 230 | name: u'canonical-releases' | 234 | name: u'canonical-releases' |
672 | 231 | official_candidate: True | 235 | official_candidate: True |
673 | 232 | owner_link: u'http://.../~mark' | 236 | owner_link: u'http://.../~mark' |
674 | diff --git a/lib/lp/registry/stories/webservice/xx-distribution.txt b/lib/lp/registry/stories/webservice/xx-distribution.txt | |||
675 | index 1c0ed87..1486904 100644 | |||
676 | --- a/lib/lp/registry/stories/webservice/xx-distribution.txt | |||
677 | +++ b/lib/lp/registry/stories/webservice/xx-distribution.txt | |||
678 | @@ -159,6 +159,7 @@ packages matching (substring) the given text. | |||
679 | 159 | enabled: True | 159 | enabled: True |
680 | 160 | ftp_base_url: None | 160 | ftp_base_url: None |
681 | 161 | http_base_url: u'http://releases.ubuntu.com/' | 161 | http_base_url: u'http://releases.ubuntu.com/' |
682 | 162 | https_base_url: None | ||
683 | 162 | name: u'canonical-releases' | 163 | name: u'canonical-releases' |
684 | 163 | official_candidate: True | 164 | official_candidate: True |
685 | 164 | owner_link: u'http://.../~mark' | 165 | owner_link: u'http://.../~mark' |
686 | diff --git a/lib/lp/registry/templates/distributionmirror-index.pt b/lib/lp/registry/templates/distributionmirror-index.pt | |||
687 | index 78e934d..27f3139 100644 | |||
688 | --- a/lib/lp/registry/templates/distributionmirror-index.pt | |||
689 | +++ b/lib/lp/registry/templates/distributionmirror-index.pt | |||
690 | @@ -118,6 +118,10 @@ | |||
691 | 118 | <h2>Mirror location information</h2> | 118 | <h2>Mirror location information</h2> |
692 | 119 | 119 | ||
693 | 120 | <ul class="webref" id="mirror-urls"> | 120 | <ul class="webref" id="mirror-urls"> |
694 | 121 | <li tal:condition="context/https_base_url" > | ||
695 | 122 | <a tal:content="context/https_base_url" | ||
696 | 123 | tal:attributes="href context/https_base_url">https://url/</a> | ||
697 | 124 | </li> | ||
698 | 121 | <li tal:condition="context/http_base_url" > | 125 | <li tal:condition="context/http_base_url" > |
699 | 122 | <a tal:content="context/http_base_url" | 126 | <a tal:content="context/http_base_url" |
700 | 123 | tal:attributes="href context/http_base_url">http://url/</a> | 127 | tal:attributes="href context/http_base_url">http://url/</a> |
701 | diff --git a/lib/lp/registry/templates/distributionmirror-macros.pt b/lib/lp/registry/templates/distributionmirror-macros.pt | |||
702 | index 18f82af..489f770 100644 | |||
703 | --- a/lib/lp/registry/templates/distributionmirror-macros.pt | |||
704 | +++ b/lib/lp/registry/templates/distributionmirror-macros.pt | |||
705 | @@ -17,7 +17,7 @@ | |||
706 | 17 | <tbody> | 17 | <tbody> |
707 | 18 | <tal:country_and_mirrors repeat="country_and_mirrors mirrors_by_country"> | 18 | <tal:country_and_mirrors repeat="country_and_mirrors mirrors_by_country"> |
708 | 19 | <tr class="head"> | 19 | <tr class="head"> |
710 | 20 | <th colspan="2" | 20 | <th colspan="2" |
711 | 21 | tal:content="country_and_mirrors/country" /> | 21 | tal:content="country_and_mirrors/country" /> |
712 | 22 | <th tal:content="country_and_mirrors/throughput"/> | 22 | <th tal:content="country_and_mirrors/throughput"/> |
713 | 23 | <th tal:condition="show_mirror_type"> | 23 | <th tal:condition="show_mirror_type"> |
714 | @@ -35,6 +35,8 @@ | |||
715 | 35 | tal:content="mirror/title">Mirror Name</a> | 35 | tal:content="mirror/title">Mirror Name</a> |
716 | 36 | </td> | 36 | </td> |
717 | 37 | <td> | 37 | <td> |
718 | 38 | <a tal:condition="mirror/https_base_url" | ||
719 | 39 | tal:attributes="href mirror/https_base_url">https</a> | ||
720 | 38 | <a tal:condition="mirror/http_base_url" | 40 | <a tal:condition="mirror/http_base_url" |
721 | 39 | tal:attributes="href mirror/http_base_url">http</a> | 41 | tal:attributes="href mirror/http_base_url">http</a> |
722 | 40 | <a tal:condition="mirror/ftp_base_url" | 42 | <a tal:condition="mirror/ftp_base_url" |
723 | diff --git a/lib/lp/registry/tests/distributionmirror_http_server.py b/lib/lp/registry/tests/distributionmirror_http_server.py | |||
724 | index e3e0a39..d12c9b3 100644 | |||
725 | --- a/lib/lp/registry/tests/distributionmirror_http_server.py | |||
726 | +++ b/lib/lp/registry/tests/distributionmirror_http_server.py | |||
727 | @@ -1,6 +1,6 @@ | |||
728 | 1 | #!/usr/bin/python | 1 | #!/usr/bin/python |
729 | 2 | # | 2 | # |
731 | 3 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 3 | # Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
732 | 4 | # GNU Affero General Public License version 3 (see the file LICENSE). | 4 | # GNU Affero General Public License version 3 (see the file LICENSE). |
733 | 5 | 5 | ||
734 | 6 | from twisted.web.resource import Resource | 6 | from twisted.web.resource import Resource |
735 | @@ -21,10 +21,10 @@ class DistributionMirrorTestHTTPServer(Resource): | |||
736 | 21 | :error: Respond with a '500 Internal Server Error' status. | 21 | :error: Respond with a '500 Internal Server Error' status. |
737 | 22 | 22 | ||
738 | 23 | :redirect-to-valid-mirror/*: Respond with a '302 Found' status, | 23 | :redirect-to-valid-mirror/*: Respond with a '302 Found' status, |
740 | 24 | redirecting to http://localhost:%(port)s/valid-mirror/*. | 24 | redirecting to http(s)://localhost:%(port)s/valid-mirror/*. |
741 | 25 | 25 | ||
742 | 26 | :redirect-infinite-loop: Respond with a '302 Found' status, redirecting | 26 | :redirect-infinite-loop: Respond with a '302 Found' status, redirecting |
744 | 27 | to http://localhost:%(port)s/redirect-infinite-loop. | 27 | to http(s)://localhost:%(port)s/redirect-infinite-loop. |
745 | 28 | 28 | ||
746 | 29 | :redirect-unknown-url-scheme: Respond with a '302 Found' status, | 29 | :redirect-unknown-url-scheme: Respond with a '302 Found' status, |
747 | 30 | redirecting to ssh://localhost/redirect-unknown-url-scheme. | 30 | redirecting to ssh://localhost/redirect-unknown-url-scheme. |
748 | @@ -32,11 +32,13 @@ class DistributionMirrorTestHTTPServer(Resource): | |||
749 | 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' |
750 | 33 | status. | 33 | status. |
751 | 34 | """ | 34 | """ |
752 | 35 | protocol = "http" | ||
753 | 35 | 36 | ||
754 | 36 | def getChild(self, name, request): | 37 | def getChild(self, name, request): |
755 | 38 | protocol = self.protocol | ||
756 | 37 | port = request.getHost().port | 39 | port = request.getHost().port |
757 | 38 | if name == 'valid-mirror': | 40 | if name == 'valid-mirror': |
759 | 39 | leaf = DistributionMirrorTestHTTPServer() | 41 | leaf = self.__class__() |
760 | 40 | leaf.isLeaf = True | 42 | leaf.isLeaf = True |
761 | 41 | return leaf | 43 | return leaf |
762 | 42 | elif name == 'timeout': | 44 | elif name == 'timeout': |
763 | @@ -49,12 +51,14 @@ class DistributionMirrorTestHTTPServer(Resource): | |||
764 | 49 | 'than one component.') | 51 | 'than one component.') |
765 | 50 | remaining_path = request.path.replace('/%s' % name, '') | 52 | remaining_path = request.path.replace('/%s' % name, '') |
766 | 51 | leaf = RedirectingResource( | 53 | leaf = RedirectingResource( |
768 | 52 | 'http://localhost:%s/valid-mirror%s' % (port, remaining_path)) | 54 | '%s://localhost:%s/valid-mirror%s' % ( |
769 | 55 | protocol, port, remaining_path)) | ||
770 | 53 | leaf.isLeaf = True | 56 | leaf.isLeaf = True |
771 | 54 | return leaf | 57 | return leaf |
772 | 55 | elif name == 'redirect-infinite-loop': | 58 | elif name == 'redirect-infinite-loop': |
773 | 56 | return RedirectingResource( | 59 | return RedirectingResource( |
775 | 57 | 'http://localhost:%s/redirect-infinite-loop' % port) | 60 | '%s://localhost:%s/redirect-infinite-loop' % |
776 | 61 | (protocol, port)) | ||
777 | 58 | elif name == 'redirect-unknown-url-scheme': | 62 | elif name == 'redirect-unknown-url-scheme': |
778 | 59 | return RedirectingResource( | 63 | return RedirectingResource( |
779 | 60 | 'ssh://localhost/redirect-unknown-url-scheme') | 64 | 'ssh://localhost/redirect-unknown-url-scheme') |
780 | @@ -65,6 +69,11 @@ class DistributionMirrorTestHTTPServer(Resource): | |||
781 | 65 | return "Hi" | 69 | return "Hi" |
782 | 66 | 70 | ||
783 | 67 | 71 | ||
784 | 72 | class DistributionMirrorTestSecureHTTPServer(DistributionMirrorTestHTTPServer): | ||
785 | 73 | """HTTPS version of DistributionMirrorTestHTTPServer""" | ||
786 | 74 | protocol = "https" | ||
787 | 75 | |||
788 | 76 | |||
789 | 68 | class RedirectingResource(Resource): | 77 | class RedirectingResource(Resource): |
790 | 69 | 78 | ||
791 | 70 | def __init__(self, redirection_url): | 79 | def __init__(self, redirection_url): |
792 | @@ -85,4 +94,3 @@ class FiveHundredResource(Resource): | |||
793 | 85 | def render_GET(self, request): | 94 | def render_GET(self, request): |
794 | 86 | request.setResponseCode(500) | 95 | request.setResponseCode(500) |
795 | 87 | request.write('ASPLODE!!!') | 96 | request.write('ASPLODE!!!') |
796 | 88 | return NOT_DONE_YET | ||
797 | diff --git a/lib/lp/registry/tests/test_distributionmirror_prober.py b/lib/lp/registry/tests/test_distributionmirror_prober.py | |||
798 | index 39228d2..c3e363c 100644 | |||
799 | --- a/lib/lp/registry/tests/test_distributionmirror_prober.py | |||
800 | +++ b/lib/lp/registry/tests/test_distributionmirror_prober.py | |||
801 | @@ -1,4 +1,4 @@ | |||
803 | 1 | # Copyright 2009-2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
804 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
805 | 3 | 3 | ||
806 | 4 | """distributionmirror-prober tests.""" | 4 | """distributionmirror-prober tests.""" |
807 | @@ -11,6 +11,7 @@ import logging | |||
808 | 11 | import os | 11 | import os |
809 | 12 | from StringIO import StringIO | 12 | from StringIO import StringIO |
810 | 13 | 13 | ||
811 | 14 | from fixtures import MockPatchObject | ||
812 | 14 | from lazr.uri import URI | 15 | from lazr.uri import URI |
813 | 15 | import responses | 16 | import responses |
814 | 16 | from six.moves import http_client | 17 | from six.moves import http_client |
815 | @@ -28,9 +29,14 @@ from testtools.twistedsupport import ( | |||
816 | 28 | from twisted.internet import ( | 29 | from twisted.internet import ( |
817 | 29 | defer, | 30 | defer, |
818 | 30 | reactor, | 31 | reactor, |
819 | 32 | ssl, | ||
820 | 31 | ) | 33 | ) |
821 | 32 | from twisted.python.failure import Failure | 34 | from twisted.python.failure import Failure |
822 | 33 | from twisted.web import server | 35 | from twisted.web import server |
823 | 36 | from twisted.web.client import ( | ||
824 | 37 | BrowserLikePolicyForHTTPS, | ||
825 | 38 | ProxyAgent, | ||
826 | 39 | ) | ||
827 | 34 | from zope.component import getUtility | 40 | from zope.component import getUtility |
828 | 35 | from zope.security.proxy import removeSecurityProxy | 41 | from zope.security.proxy import removeSecurityProxy |
829 | 36 | 42 | ||
830 | @@ -44,6 +50,8 @@ from lp.registry.scripts.distributionmirror_prober import ( | |||
831 | 44 | BadResponseCode, | 50 | BadResponseCode, |
832 | 45 | ConnectionSkipped, | 51 | ConnectionSkipped, |
833 | 46 | InfiniteLoopDetected, | 52 | InfiniteLoopDetected, |
834 | 53 | InvalidHTTPSCertificate, | ||
835 | 54 | InvalidHTTPSCertificateSkipped, | ||
836 | 47 | LoggingMixin, | 55 | LoggingMixin, |
837 | 48 | MAX_REDIRECTS, | 56 | MAX_REDIRECTS, |
838 | 49 | MIN_REQUEST_TIMEOUT_RATIO, | 57 | MIN_REQUEST_TIMEOUT_RATIO, |
839 | @@ -65,6 +73,7 @@ from lp.registry.scripts.distributionmirror_prober import ( | |||
840 | 65 | ) | 73 | ) |
841 | 66 | from lp.registry.tests.distributionmirror_http_server import ( | 74 | from lp.registry.tests.distributionmirror_http_server import ( |
842 | 67 | DistributionMirrorTestHTTPServer, | 75 | DistributionMirrorTestHTTPServer, |
843 | 76 | DistributionMirrorTestSecureHTTPServer, | ||
844 | 68 | ) | 77 | ) |
845 | 69 | from lp.services.config import config | 78 | from lp.services.config import config |
846 | 70 | from lp.services.daemons.tachandler import TacTestSetup | 79 | from lp.services.daemons.tachandler import TacTestSetup |
847 | @@ -103,6 +112,186 @@ class HTTPServerTestSetup(TacTestSetup): | |||
848 | 103 | return os.path.join(self.root, 'distributionmirror_http_server.log') | 112 | return os.path.join(self.root, 'distributionmirror_http_server.log') |
849 | 104 | 113 | ||
850 | 105 | 114 | ||
851 | 115 | |||
852 | 116 | class LocalhostWhitelistedHTTPSPolicy(BrowserLikePolicyForHTTPS): | ||
853 | 117 | """HTTPS policy that bypasses SSL certificate check when doing requests | ||
854 | 118 | to localhost. | ||
855 | 119 | """ | ||
856 | 120 | |||
857 | 121 | def creatorForNetloc(self, hostname, port): | ||
858 | 122 | # check if the hostname is in the the whitelist, | ||
859 | 123 | # otherwise return the default policy | ||
860 | 124 | if hostname == 'localhost': | ||
861 | 125 | return ssl.CertificateOptions(verify=False) | ||
862 | 126 | return super(LocalhostWhitelistedHTTPSPolicy, self).creatorForNetloc( | ||
863 | 127 | hostname, port) | ||
864 | 128 | |||
865 | 129 | |||
866 | 130 | class TestProberHTTPSProtocolAndFactory(TestCase): | ||
867 | 131 | layer = TwistedLayer | ||
868 | 132 | run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory( | ||
869 | 133 | timeout=30) | ||
870 | 134 | |||
871 | 135 | def setUp(self): | ||
872 | 136 | super(TestProberHTTPSProtocolAndFactory, self).setUp() | ||
873 | 137 | root = DistributionMirrorTestSecureHTTPServer() | ||
874 | 138 | site = server.Site(root) | ||
875 | 139 | site.displayTracebacks = False | ||
876 | 140 | keys_path = os.path.join(config.root, "configs", "development") | ||
877 | 141 | keys = ssl.DefaultOpenSSLContextFactory( | ||
878 | 142 | os.path.join(keys_path, "launchpad.key"), | ||
879 | 143 | os.path.join(keys_path, "launchpad.crt"), | ||
880 | 144 | ) | ||
881 | 145 | self.listening_port = reactor.listenSSL(0, site, keys) | ||
882 | 146 | |||
883 | 147 | self.addCleanup(self.listening_port.stopListening) | ||
884 | 148 | |||
885 | 149 | # Change the default policy to accept localhost self-signed | ||
886 | 150 | # certificates. | ||
887 | 151 | original_probefactory_policy = ProberFactory.https_agent_policy | ||
888 | 152 | original_redirect_policy = ( | ||
889 | 153 | RedirectAwareProberFactory.https_agent_policy) | ||
890 | 154 | ProberFactory.https_agent_policy = LocalhostWhitelistedHTTPSPolicy | ||
891 | 155 | RedirectAwareProberFactory.https_agent_policy = ( | ||
892 | 156 | LocalhostWhitelistedHTTPSPolicy) | ||
893 | 157 | |||
894 | 158 | for factory in (ProberFactory, RedirectAwareProberFactory): | ||
895 | 159 | self.useFixture(MockPatchObject( | ||
896 | 160 | factory, "https_agent_policy", | ||
897 | 161 | LocalhostWhitelistedHTTPSPolicy)) | ||
898 | 162 | |||
899 | 163 | self.port = self.listening_port.getHost().port | ||
900 | 164 | |||
901 | 165 | self.urls = {'timeout': u'https://localhost:%s/timeout' % self.port, | ||
902 | 166 | '200': u'https://localhost:%s/valid-mirror' % self.port, | ||
903 | 167 | '500': u'https://localhost:%s/error' % self.port, | ||
904 | 168 | '404': u'https://localhost:%s/invalid-mirror' % self.port} | ||
905 | 169 | self.pushConfig('launchpad', http_proxy=None) | ||
906 | 170 | |||
907 | 171 | self.useFixture(MockPatchObject( | ||
908 | 172 | distributionmirror_prober, "host_requests", {})) | ||
909 | 173 | self.useFixture(MockPatchObject( | ||
910 | 174 | distributionmirror_prober, "host_timeouts", {})) | ||
911 | 175 | self.useFixture(MockPatchObject( | ||
912 | 176 | distributionmirror_prober, "invalid_certificate_hosts", set())) | ||
913 | 177 | |||
914 | 178 | def _createProberAndProbe(self, url): | ||
915 | 179 | prober = ProberFactory(url) | ||
916 | 180 | return prober.probe() | ||
917 | 181 | |||
918 | 182 | def test_timeout(self): | ||
919 | 183 | prober = ProberFactory(self.urls['timeout'], timeout=0.5) | ||
920 | 184 | d = prober.probe() | ||
921 | 185 | return assert_fails_with(d, ProberTimeout) | ||
922 | 186 | |||
923 | 187 | def test_500(self): | ||
924 | 188 | d = self._createProberAndProbe(self.urls['500']) | ||
925 | 189 | return assert_fails_with(d, BadResponseCode) | ||
926 | 190 | |||
927 | 191 | def test_notfound(self): | ||
928 | 192 | d = self._createProberAndProbe(self.urls['404']) | ||
929 | 193 | return assert_fails_with(d, BadResponseCode) | ||
930 | 194 | |||
931 | 195 | def test_config_no_https_proxy(self): | ||
932 | 196 | prober = ProberFactory(self.urls['200']) | ||
933 | 197 | self.assertThat(prober, MatchesStructure.byEquality( | ||
934 | 198 | request_scheme='https', | ||
935 | 199 | request_host='localhost', | ||
936 | 200 | request_port=self.port, | ||
937 | 201 | request_path='/valid-mirror', | ||
938 | 202 | connect_scheme='https', | ||
939 | 203 | connect_host='localhost', | ||
940 | 204 | connect_port=self.port, | ||
941 | 205 | connect_path='/valid-mirror')) | ||
942 | 206 | |||
943 | 207 | def test_RedirectAwareProber_follows_https_redirect(self): | ||
944 | 208 | url = 'https://localhost:%s/redirect-to-valid-mirror/file' % self.port | ||
945 | 209 | prober = RedirectAwareProberFactory(url) | ||
946 | 210 | self.assertEqual(prober.url, url) | ||
947 | 211 | deferred = prober.probe() | ||
948 | 212 | |||
949 | 213 | def got_result(result): | ||
950 | 214 | self.assertEqual(http_client.OK, result.code) | ||
951 | 215 | self.assertEqual( | ||
952 | 216 | 'https://localhost:%s/valid-mirror/file' % self.port, | ||
953 | 217 | result.request.absoluteURI) | ||
954 | 218 | |||
955 | 219 | return deferred.addCallback(got_result) | ||
956 | 220 | |||
957 | 221 | def test_https_prober_uses_proxy(self): | ||
958 | 222 | root = DistributionMirrorTestSecureHTTPServer() | ||
959 | 223 | site = server.Site(root) | ||
960 | 224 | proxy_listen_port = reactor.listenTCP(0, site) | ||
961 | 225 | proxy_port = proxy_listen_port.getHost().port | ||
962 | 226 | self.pushConfig( | ||
963 | 227 | 'launchpad', http_proxy='http://localhost:%s/valid-mirror/file' | ||
964 | 228 | % proxy_port) | ||
965 | 229 | |||
966 | 230 | url = 'https://localhost:%s/valid-mirror/file' % self.port | ||
967 | 231 | prober = RedirectAwareProberFactory(url) | ||
968 | 232 | self.assertEqual(prober.url, url) | ||
969 | 233 | deferred = prober.probe() | ||
970 | 234 | |||
971 | 235 | def got_result(result): | ||
972 | 236 | # We basically don't care about the result here. We just want to | ||
973 | 237 | # check that it did the request to the correct URI, | ||
974 | 238 | # and ProxyAgent was used pointing to the correct proxy. | ||
975 | 239 | agent = prober.getHttpsClient()._agent | ||
976 | 240 | self.assertIsInstance(agent, ProxyAgent) | ||
977 | 241 | self.assertEqual('localhost', agent._proxyEndpoint._hostText) | ||
978 | 242 | self.assertEqual(proxy_port, agent._proxyEndpoint._port) | ||
979 | 243 | |||
980 | 244 | self.assertEqual( | ||
981 | 245 | 'https://localhost:%s/valid-mirror/file' % self.port, | ||
982 | 246 | result.value.response.request.absoluteURI) | ||
983 | 247 | |||
984 | 248 | def cleanup(*args, **kwargs): | ||
985 | 249 | proxy_listen_port.stopListening() | ||
986 | 250 | |||
987 | 251 | # Doing the proxy checks on the error callback because the | ||
988 | 252 | # proxy is dummy and always returns 404. | ||
989 | 253 | deferred.addErrback(got_result) | ||
990 | 254 | deferred.addBoth(cleanup) | ||
991 | 255 | return deferred | ||
992 | 256 | |||
993 | 257 | def test_https_fails_on_invalid_certificates(self): | ||
994 | 258 | """Changes set back the default browser-like policy for HTTPS | ||
995 | 259 | request and make sure the request is failing due to invalid | ||
996 | 260 | (self-signed) certificate. | ||
997 | 261 | """ | ||
998 | 262 | url = 'https://localhost:%s/valid-mirror/file' % self.port | ||
999 | 263 | prober = RedirectAwareProberFactory(url) | ||
1000 | 264 | prober.https_agent_policy = BrowserLikePolicyForHTTPS | ||
1001 | 265 | self.assertEqual(prober.url, url) | ||
1002 | 266 | deferred = prober.probe() | ||
1003 | 267 | |||
1004 | 268 | def on_failure(result): | ||
1005 | 269 | self.assertIsInstance(result.value, InvalidHTTPSCertificate) | ||
1006 | 270 | self.assertIn( | ||
1007 | 271 | ("localhost", self.port), | ||
1008 | 272 | distributionmirror_prober.invalid_certificate_hosts) | ||
1009 | 273 | |||
1010 | 274 | def on_success(result): | ||
1011 | 275 | if result is not None: | ||
1012 | 276 | self.fail( | ||
1013 | 277 | "Should have raised SSL error. Got '%s' instead" % result) | ||
1014 | 278 | |||
1015 | 279 | deferred.addErrback(on_failure) | ||
1016 | 280 | deferred.addCallback(on_success) | ||
1017 | 281 | return deferred | ||
1018 | 282 | |||
1019 | 283 | def test_https_skips_invalid_certificates_hosts(self): | ||
1020 | 284 | distributionmirror_prober.invalid_certificate_hosts.add( | ||
1021 | 285 | ("localhost", self.port)) | ||
1022 | 286 | url = 'https://localhost:%s/valid-mirror/file' % self.port | ||
1023 | 287 | prober = RedirectAwareProberFactory(url) | ||
1024 | 288 | prober.https_agent_policy = BrowserLikePolicyForHTTPS | ||
1025 | 289 | self.assertEqual(prober.url, url) | ||
1026 | 290 | deferred = prober.probe() | ||
1027 | 291 | |||
1028 | 292 | return assert_fails_with(deferred, InvalidHTTPSCertificateSkipped) | ||
1029 | 293 | |||
1030 | 294 | |||
1031 | 106 | class TestProberProtocolAndFactory(TestCase): | 295 | class TestProberProtocolAndFactory(TestCase): |
1032 | 107 | 296 | ||
1033 | 108 | layer = TwistedLayer | 297 | layer = TwistedLayer |
1034 | @@ -212,7 +401,7 @@ class TestProberProtocolAndFactory(TestCase): | |||
1035 | 212 | self.assertTrue(prober.url == new_url) | 401 | self.assertTrue(prober.url == new_url) |
1036 | 213 | self.assertTrue(result == str(http_client.OK)) | 402 | self.assertTrue(result == str(http_client.OK)) |
1037 | 214 | 403 | ||
1039 | 215 | return deferred.addCallback(got_result) | 404 | return deferred.addBoth(got_result) |
1040 | 216 | 405 | ||
1041 | 217 | def test_redirectawareprober_detects_infinite_loop(self): | 406 | def test_redirectawareprober_detects_infinite_loop(self): |
1042 | 218 | prober = RedirectAwareProberFactory( | 407 | prober = RedirectAwareProberFactory( |
1043 | @@ -737,12 +926,16 @@ class TestMirrorCDImageProberCallbacks(TestCaseWithFactory): | |||
1044 | 737 | ConnectionSkipped, | 926 | ConnectionSkipped, |
1045 | 738 | RedirectToDifferentFile, | 927 | RedirectToDifferentFile, |
1046 | 739 | UnknownURLSchemeAfterRedirect, | 928 | UnknownURLSchemeAfterRedirect, |
1047 | 929 | InvalidHTTPSCertificate, | ||
1048 | 930 | InvalidHTTPSCertificateSkipped, | ||
1049 | 740 | ])) | 931 | ])) |
1050 | 741 | exceptions = [BadResponseCode(str(http_client.NOT_FOUND)), | 932 | exceptions = [BadResponseCode(str(http_client.NOT_FOUND)), |
1051 | 742 | ProberTimeout('http://localhost/', 5), | 933 | ProberTimeout('http://localhost/', 5), |
1052 | 743 | ConnectionSkipped(), | 934 | ConnectionSkipped(), |
1053 | 744 | RedirectToDifferentFile('/foo', '/bar'), | 935 | RedirectToDifferentFile('/foo', '/bar'), |
1055 | 745 | UnknownURLSchemeAfterRedirect('https://localhost')] | 936 | UnknownURLSchemeAfterRedirect('https://localhost'), |
1056 | 937 | InvalidHTTPSCertificate('localhost', 443), | ||
1057 | 938 | InvalidHTTPSCertificateSkipped("https://localhost/xx")] | ||
1058 | 746 | for exception in exceptions: | 939 | for exception in exceptions: |
1059 | 747 | failure = callbacks.ensureOrDeleteMirrorCDImageSeries( | 940 | failure = callbacks.ensureOrDeleteMirrorCDImageSeries( |
1060 | 748 | [(defer.FAILURE, Failure(exception))]) | 941 | [(defer.FAILURE, Failure(exception))]) |
1061 | diff --git a/lib/lp/scripts/utilities/importpedant.py b/lib/lp/scripts/utilities/importpedant.py | |||
1062 | index ec41baf..c7859a4 100644 | |||
1063 | --- a/lib/lp/scripts/utilities/importpedant.py | |||
1064 | +++ b/lib/lp/scripts/utilities/importpedant.py | |||
1065 | @@ -1,4 +1,4 @@ | |||
1067 | 1 | # Copyright 2009-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
1068 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1069 | 3 | 3 | ||
1070 | 4 | from __future__ import absolute_import, print_function, unicode_literals | 4 | from __future__ import absolute_import, print_function, unicode_literals |
1071 | @@ -42,6 +42,8 @@ valid_imports_not_in_all = { | |||
1072 | 42 | 'textwrap': set(['dedent']), | 42 | 'textwrap': set(['dedent']), |
1073 | 43 | 'testtools.testresult.real': set(['_details_to_str']), | 43 | 'testtools.testresult.real': set(['_details_to_str']), |
1074 | 44 | 'twisted.internet.threads': set(['deferToThreadPool']), | 44 | 'twisted.internet.threads': set(['deferToThreadPool']), |
1075 | 45 | # Even docs tell us to use this class. See docs on WebClientContextFactory. | ||
1076 | 46 | 'twisted.web.client': set(['BrowserLikePolicyForHTTPS']), | ||
1077 | 45 | 'zope.component': set( | 47 | 'zope.component': set( |
1078 | 46 | ['adapter', | 48 | ['adapter', |
1079 | 47 | 'ComponentLookupError', | 49 | 'ComponentLookupError', |
1080 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py | |||
1081 | index 85a05b6..6c0492f 100644 | |||
1082 | --- a/lib/lp/testing/factory.py | |||
1083 | +++ b/lib/lp/testing/factory.py | |||
1084 | @@ -2,7 +2,7 @@ | |||
1085 | 2 | # NOTE: The first line above must stay first; do not move the copyright | 2 | # NOTE: The first line above must stay first; do not move the copyright |
1086 | 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/. |
1087 | 4 | # | 4 | # |
1089 | 5 | # Copyright 2009-2019 Canonical Ltd. This software is licensed under the | 5 | # Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
1090 | 6 | # GNU Affero General Public License version 3 (see the file LICENSE). | 6 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1091 | 7 | 7 | ||
1092 | 8 | """Testing infrastructure for the Launchpad application. | 8 | """Testing infrastructure for the Launchpad application. |
1093 | @@ -3527,13 +3527,13 @@ class BareLaunchpadObjectFactory(ObjectFactory): | |||
1094 | 3527 | return proberecord | 3527 | return proberecord |
1095 | 3528 | 3528 | ||
1096 | 3529 | def makeMirror(self, distribution, displayname=None, country=None, | 3529 | def makeMirror(self, distribution, displayname=None, country=None, |
1098 | 3530 | http_url=None, ftp_url=None, rsync_url=None, | 3530 | http_url=None, https_url=None, ftp_url=None, rsync_url=None, |
1099 | 3531 | official_candidate=False): | 3531 | official_candidate=False): |
1100 | 3532 | """Create a mirror for the distribution.""" | 3532 | """Create a mirror for the distribution.""" |
1101 | 3533 | if displayname is None: | 3533 | if displayname is None: |
1102 | 3534 | displayname = self.getUniqueString("mirror") | 3534 | displayname = self.getUniqueString("mirror") |
1103 | 3535 | # If no URL is specified create an HTTP URL. | 3535 | # If no URL is specified create an HTTP URL. |
1105 | 3536 | if http_url is None and ftp_url is None and rsync_url is None: | 3536 | if http_url is https_url is ftp_url is rsync_url is None: |
1106 | 3537 | http_url = self.getUniqueURL() | 3537 | http_url = self.getUniqueURL() |
1107 | 3538 | # If no country is given use Argentina. | 3538 | # If no country is given use Argentina. |
1108 | 3539 | if country is None: | 3539 | if country is None: |
1109 | @@ -3547,6 +3547,7 @@ class BareLaunchpadObjectFactory(ObjectFactory): | |||
1110 | 3547 | display_name=displayname, | 3547 | display_name=displayname, |
1111 | 3548 | description=None, | 3548 | description=None, |
1112 | 3549 | http_base_url=http_url, | 3549 | http_base_url=http_url, |
1113 | 3550 | https_base_url=https_url, | ||
1114 | 3550 | ftp_base_url=ftp_url, | 3551 | ftp_base_url=ftp_url, |
1115 | 3551 | rsync_base_url=rsync_url, | 3552 | rsync_base_url=rsync_url, |
1116 | 3552 | official_candidate=official_candidate) | 3553 | official_candidate=official_candidate) |
I have a question about the prober: what should we do about mirrors using "invalid" certificates? Should we consider them or not?
I'm asking because I was thinking to mark those as not healthy, but the HTTPS version of releases.ubuntu.com and nl.archive. ubuntu. com, for example, both use a certificate that is not trusted (the domain does not match the cerfiticate).
Maybe cjwatson has an opinion about it.