Merge lp:~vila/bzr/186920-lp-proxy into lp:bzr
- 186920-lp-proxy
- Merge into bzr.dev
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~vila/bzr/186920-lp-proxy |
Merge into: | lp:bzr |
Diff against target: |
430 lines 6 files modified
NEWS (+3/-0) bzrlib/plugins/launchpad/__init__.py (+12/-23) bzrlib/plugins/launchpad/lp_registration.py (+25/-4) bzrlib/plugins/launchpad/test_lp_directory.py (+144/-1) bzrlib/tests/test_http.py (+55/-1) bzrlib/transport/http/_urllib2_wrappers.py (+19/-7) |
To merge this branch: | bzr merge lp:~vila/bzr/186920-lp-proxy |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
John A Meinel | Pending | ||
Review via email: mp+14238@code.launchpad.net |
This proposal supersedes a proposal from 2009-10-30.
Commit message
Description of the change
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal | # |
John A Meinel (jameinel) wrote : Posted in a previous version of this proposal | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Vincent Ladeuil wrote:
> Vincent Ladeuil has proposed merging lp:~vila/bzr/186920-lp-proxy into lp:bzr.
>
> Requested reviews:
> bzr-core (bzr-core)
>
>
> Thanks to Gordon Tyler for starting a proof-of-concept implementation, I've finally
> been able to fix bug #186920 et all (the one about lp URLS that can't be
> resolved behind proxies).
>
> This patch implement an XMLRPC transport class using our _urllib2_wrappers (the fundations
> for the urllib http client), that gives us proxy support for free.
>
> I still haven't implemented a true proxy test server but I've added tests that exercise
> the full urllib http stack.
>
+class Transport(
+
+ def __init__(self, scheme, use_datetime=0):
^- Shouldn't we have our own name here? We *can* re-use the existing
name, but it is potentially confusing.
Especially given that we have a "Transport" hierarchy already (and not
to mention that so does Paramiko), I'd like to at least call it
"XMLRPCTransport".
Also, it seems that Gordon responded that there was a problem with not
passing the original port to the proxy? Did you fix that in this? Or did
his comment come after the merge proposal?
I assume that the http/https trick is this line:
+ self._opener = _urllib2_
(Opener supposedly creates the right instance of HTTP or HTTPS based on
the url/the url of the proxy?) I assume that is why you don't need:
- - if uri_type == 'https':
- - transport = xmlrpclib.
- - else:
- - transport = xmlrpclib.
+ transport = Transport(uri_type)
anymore.
+
+ self.wfile.
+
+class PreCannedServer
^- bit of whitespace here
Otherwise:
review: approve
John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAkr
VZ0An3B1OXrswFO
=Vmrj
-----END PGP SIGNATURE-----
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal | # |
>>>>> "jam" == John A Meinel <email address hidden> writes:
jam> +class Transport(
jam> +
jam> + def __init__(self, scheme, use_datetime=0):
jam> ^- Shouldn't we have our own name here? We *can* re-use
jam> the existing name, but it is potentially confusing.
jam> Especially given that we have a "Transport" hierarchy
jam> already (and not to mention that so does Paramiko), I'd
jam> like to at least call it "XMLRPCTransport".
Sure, done.
jam> Also, it seems that Gordon responded that there was a
jam> problem with not passing the original port to the proxy?
jam> Did you fix that in this? Or did his comment come after
jam> the merge proposal?
His comment went after the mp, I've fixed it since so I'll
resubmit.
jam> I assume that the http/https trick is this line:
jam> + self._opener = _urllib2_
jam> (Opener supposedly creates the right instance of HTTP or HTTPS based on
jam> the url/the url of the proxy?) I assume that is why you don't need:
jam> - if uri_type == 'https':
jam> - transport = xmlrpclib.
jam> - else:
jam> - transport = xmlrpclib.
jam> + transport = Transport(uri_type)
jam> anymore.
Correct. I could have refactored even more aggressively (to pass
the full service url and set the agent) but I wanted to keep the
patch as minimal as possible.
Vincent
Preview Diff
1 | === modified file 'NEWS' |
2 | --- NEWS 2009-10-30 00:00:37 +0000 |
3 | +++ NEWS 2009-10-30 21:10:26 +0000 |
4 | @@ -36,6 +36,9 @@ |
5 | fails intermittently with "second push failed to complete". |
6 | (Andrew Bennetts, #437626) |
7 | |
8 | +* Launchpad urls can now be resolved from behind proxies. |
9 | + (Gordon Tyler, Vincent Ladeuil, #198920) |
10 | + |
11 | * PreviewTree file names are not limited by the encoding of the temp |
12 | directory's filesystem. (Aaron Bentley, #436794) |
13 | |
14 | |
15 | === modified file 'bzrlib/plugins/launchpad/__init__.py' |
16 | --- bzrlib/plugins/launchpad/__init__.py 2009-07-06 13:00:23 +0000 |
17 | +++ bzrlib/plugins/launchpad/__init__.py 2009-10-30 21:10:26 +0000 |
18 | @@ -262,30 +262,19 @@ |
19 | _register_directory() |
20 | |
21 | |
22 | -def test_suite(): |
23 | - """Called by bzrlib to fetch tests for this plugin""" |
24 | - from unittest import TestSuite, TestLoader |
25 | - from bzrlib.plugins.launchpad import ( |
26 | - test_account, |
27 | - test_lp_directory, |
28 | - test_lp_login, |
29 | - test_lp_open, |
30 | - test_lp_service, |
31 | - test_register, |
32 | - ) |
33 | +def load_tests(basic_tests, module, loader): |
34 | + testmod_names = [ |
35 | + 'test_account', |
36 | + 'test_register', |
37 | + 'test_lp_directory', |
38 | + 'test_lp_login', |
39 | + 'test_lp_open', |
40 | + 'test_lp_service', |
41 | + ] |
42 | + basic_tests.addTest(loader.loadTestsFromModuleNames( |
43 | + ["%s.%s" % (__name__, tmn) for tmn in testmod_names])) |
44 | + return basic_tests |
45 | |
46 | - loader = TestLoader() |
47 | - suite = TestSuite() |
48 | - for module in [ |
49 | - test_account, |
50 | - test_register, |
51 | - test_lp_directory, |
52 | - test_lp_login, |
53 | - test_lp_open, |
54 | - test_lp_service, |
55 | - ]: |
56 | - suite.addTests(loader.loadTestsFromModule(module)) |
57 | - return suite |
58 | |
59 | _launchpad_help = """Integration with Launchpad.net |
60 | |
61 | |
62 | === modified file 'bzrlib/plugins/launchpad/lp_registration.py' |
63 | --- bzrlib/plugins/launchpad/lp_registration.py 2009-07-04 16:22:16 +0000 |
64 | +++ bzrlib/plugins/launchpad/lp_registration.py 2009-10-30 21:10:26 +0000 |
65 | @@ -31,6 +31,7 @@ |
66 | errors, |
67 | __version__ as _bzrlib_version, |
68 | ) |
69 | +from bzrlib.transport.http import _urllib2_wrappers |
70 | |
71 | # for testing, do |
72 | ''' |
73 | @@ -53,6 +54,29 @@ |
74 | errors.BzrError.__init__(self, url=url) |
75 | |
76 | |
77 | +class XMLRPCTransport(xmlrpclib.Transport): |
78 | + |
79 | + def __init__(self, scheme, use_datetime=0): |
80 | + xmlrpclib.Transport.__init__(self, use_datetime=use_datetime) |
81 | + self._scheme = scheme |
82 | + self._opener = _urllib2_wrappers.Opener() |
83 | + self.verbose = 0 |
84 | + |
85 | + def request(self, host, handler, request_body, verbose=0): |
86 | + self.verbose = verbose |
87 | + url = self._scheme + "://" + host + handler |
88 | + request = _urllib2_wrappers.Request("POST", url, request_body) |
89 | + # FIXME: _urllib2_wrappers will override user-agent with its own |
90 | + # request.add_header("User-Agent", self.user_agent) |
91 | + request.add_header("Content-Type", "text/xml") |
92 | + |
93 | + response = self._opener.open(request) |
94 | + if response.code != 200: |
95 | + raise xmlrpclib.ProtocolError(host + handler, response.code, |
96 | + response.msg, response.info()) |
97 | + return self.parse_response(response) |
98 | + |
99 | + |
100 | class LaunchpadService(object): |
101 | """A service to talk to Launchpad via XMLRPC. |
102 | |
103 | @@ -90,10 +114,7 @@ |
104 | self._lp_instance = lp_instance |
105 | if transport is None: |
106 | uri_type = urllib.splittype(self.service_url)[0] |
107 | - if uri_type == 'https': |
108 | - transport = xmlrpclib.SafeTransport() |
109 | - else: |
110 | - transport = xmlrpclib.Transport() |
111 | + transport = XMLRPCTransport(uri_type) |
112 | transport.user_agent = 'bzr/%s (xmlrpclib/%s)' \ |
113 | % (_bzrlib_version, xmlrpclib.__version__) |
114 | self.transport = transport |
115 | |
116 | === modified file 'bzrlib/plugins/launchpad/test_lp_directory.py' |
117 | --- bzrlib/plugins/launchpad/test_lp_directory.py 2009-03-23 14:59:43 +0000 |
118 | +++ bzrlib/plugins/launchpad/test_lp_directory.py 2009-10-30 21:10:26 +0000 |
119 | @@ -16,10 +16,12 @@ |
120 | |
121 | """Tests for directory lookup through Launchpad.net""" |
122 | |
123 | +import os |
124 | import xmlrpclib |
125 | |
126 | from bzrlib import ( |
127 | errors, |
128 | + tests, |
129 | ) |
130 | from bzrlib.branch import Branch |
131 | from bzrlib.directory_service import directories |
132 | @@ -28,10 +30,38 @@ |
133 | TestCaseWithMemoryTransport |
134 | ) |
135 | from bzrlib.transport import get_transport |
136 | -from bzrlib.plugins.launchpad import _register_directory |
137 | +from bzrlib.plugins.launchpad import ( |
138 | + _register_directory, |
139 | + lp_registration, |
140 | + ) |
141 | from bzrlib.plugins.launchpad.lp_directory import ( |
142 | LaunchpadDirectory) |
143 | from bzrlib.plugins.launchpad.account import get_lp_login |
144 | +from bzrlib.tests import ( |
145 | + http_server, |
146 | + http_utils, |
147 | + ) |
148 | + |
149 | + |
150 | +def load_tests(standard_tests, module, loader): |
151 | + result = loader.suiteClass() |
152 | + t_tests, remaining_tests = tests.split_suite_by_condition( |
153 | + standard_tests, tests.condition_isinstance(( |
154 | + TestXMLRPCTransport, |
155 | + ))) |
156 | + transport_scenarios = [ |
157 | + ('http', dict(server_class=PreCannedHTTPServer,)), |
158 | + ] |
159 | + if tests.HTTPSServerFeature.available(): |
160 | + transport_scenarios.append( |
161 | + ('https', dict(server_class=PreCannedHTTPSServer,)), |
162 | + ) |
163 | + tests.multiply_tests(t_tests, transport_scenarios, result) |
164 | + |
165 | + # No parametrization for the remaining tests |
166 | + result.addTests(remaining_tests) |
167 | + |
168 | + return result |
169 | |
170 | |
171 | class FakeResolveFactory(object): |
172 | @@ -190,3 +220,116 @@ |
173 | transport = get_transport('lp:///apt') |
174 | branch = Branch.open_from_transport(transport) |
175 | self.assertEqual(target_branch.base, branch.base) |
176 | + |
177 | + |
178 | +class PredefinedRequestHandler(http_server.TestingHTTPRequestHandler): |
179 | + """Request handler for a unique and pre-defined request. |
180 | + |
181 | + The only thing we care about here is that we receive a connection. But |
182 | + since we want to dialog with a real http client, we have to send it correct |
183 | + responses. |
184 | + |
185 | + We expect to receive a *single* request nothing more (and we won't even |
186 | + check what request it is), the tests will recognize us from our response. |
187 | + """ |
188 | + |
189 | + def handle_one_request(self): |
190 | + tcs = self.server.test_case_server |
191 | + requestline = self.rfile.readline() |
192 | + headers = self.MessageClass(self.rfile, 0) |
193 | + if requestline.startswith('POST'): |
194 | + # The body should be a single line (or we don't know where it ends |
195 | + # and we don't want to issue a blocking read) |
196 | + body = self.rfile.readline() |
197 | + |
198 | + self.wfile.write(tcs.canned_response) |
199 | + |
200 | + |
201 | +class PreCannedServerMixin(object): |
202 | + |
203 | + def __init__(self): |
204 | + super(PreCannedServerMixin, self).__init__( |
205 | + request_handler=PredefinedRequestHandler) |
206 | + # Bytes read and written by the server |
207 | + self.bytes_read = 0 |
208 | + self.bytes_written = 0 |
209 | + self.canned_response = None |
210 | + |
211 | + |
212 | +class PreCannedHTTPServer(PreCannedServerMixin, http_server.HttpServer): |
213 | + pass |
214 | + |
215 | + |
216 | +if tests.HTTPSServerFeature.available(): |
217 | + from bzrlib.tests import https_server |
218 | + class PreCannedHTTPSServer(PreCannedServerMixin, https_server.HTTPSServer): |
219 | + pass |
220 | + |
221 | + |
222 | +class TestXMLRPCTransport(tests.TestCase): |
223 | + |
224 | + # set by load_tests |
225 | + server_class = None |
226 | + |
227 | + def setUp(self): |
228 | + tests.TestCase.setUp(self) |
229 | + self.server = self.server_class() |
230 | + self.server.setUp() |
231 | + # Ensure we don't clobber env |
232 | + self._captureVar('BZR_LP_XMLRPC_URL', None) |
233 | + |
234 | + def tearDown(self): |
235 | + self.server.tearDown() |
236 | + tests.TestCase.tearDown(self) |
237 | + |
238 | + def set_canned_response(self, server, path): |
239 | + response_format = '''HTTP/1.1 200 OK\r |
240 | +Date: Tue, 11 Jul 2006 04:32:56 GMT\r |
241 | +Server: Apache/2.0.54 (Fedora)\r |
242 | +Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r |
243 | +ETag: "56691-23-38e9ae00"\r |
244 | +Accept-Ranges: bytes\r |
245 | +Content-Length: %(length)d\r |
246 | +Connection: close\r |
247 | +Content-Type: text/plain; charset=UTF-8\r |
248 | +\r |
249 | +<?xml version='1.0'?> |
250 | +<methodResponse> |
251 | +<params> |
252 | +<param> |
253 | +<value><struct> |
254 | +<member> |
255 | +<name>urls</name> |
256 | +<value><array><data> |
257 | +<value><string>bzr+ssh://bazaar.launchpad.net/%(path)s</string></value> |
258 | +<value><string>http://bazaar.launchpad.net/%(path)s</string></value> |
259 | +</data></array></value> |
260 | +</member> |
261 | +</struct></value> |
262 | +</param> |
263 | +</params> |
264 | +</methodResponse> |
265 | +''' |
266 | + length = 334 + 2 * len(path) |
267 | + server.canned_response = response_format % dict(length=length, |
268 | + path=path) |
269 | + |
270 | + def do_request(self, server_url): |
271 | + os.environ['BZR_LP_XMLRPC_URL'] = self.server.get_url() |
272 | + service = lp_registration.LaunchpadService() |
273 | + resolve = lp_registration.ResolveLaunchpadPathRequest('bzr') |
274 | + result = resolve.submit(service) |
275 | + return result |
276 | + |
277 | + def test_direct_request(self): |
278 | + self.set_canned_response(self.server, '~bzr-pqm/bzr/bzr.dev') |
279 | + result = self.do_request(self.server.get_url()) |
280 | + urls = result.get('urls', None) |
281 | + self.assertIsNot(None, urls) |
282 | + self.assertEquals( |
283 | + ['bzr+ssh://bazaar.launchpad.net/~bzr-pqm/bzr/bzr.dev', |
284 | + 'http://bazaar.launchpad.net/~bzr-pqm/bzr/bzr.dev'], |
285 | + urls) |
286 | + # FIXME: we need to test with a real proxy, I can't find a way so simulate |
287 | + # CONNECT without leaving one server hanging the test :-/ Since that maybe |
288 | + # related to the leaking tests problems, I'll punt for now -- vila 20091030 |
289 | |
290 | === modified file 'bzrlib/tests/test_http.py' |
291 | --- bzrlib/tests/test_http.py 2009-09-17 11:54:41 +0000 |
292 | +++ bzrlib/tests/test_http.py 2009-10-30 21:10:26 +0000 |
293 | @@ -1956,7 +1956,7 @@ |
294 | pass |
295 | |
296 | |
297 | -class TestActivity(tests.TestCase): |
298 | +class TestActivityMixin(object): |
299 | """Test socket activity reporting. |
300 | |
301 | We use a special purpose server to control the bytes sent and received and |
302 | @@ -2100,3 +2100,57 @@ |
303 | code, f = t._post('abc def end-of-body\n') |
304 | self.assertEqual('lalala whatever as long as itsssss\n', f.read()) |
305 | self.assertActivitiesMatch() |
306 | + |
307 | + |
308 | +class TestActivity(tests.TestCase, TestActivityMixin): |
309 | + |
310 | + def setUp(self): |
311 | + tests.TestCase.setUp(self) |
312 | + self.server = self._activity_server(self._protocol_version) |
313 | + self.server.setUp() |
314 | + self.activities = {} |
315 | + def report_activity(t, bytes, direction): |
316 | + count = self.activities.get(direction, 0) |
317 | + count += bytes |
318 | + self.activities[direction] = count |
319 | + |
320 | + # We override at class level because constructors may propagate the |
321 | + # bound method and render instance overriding ineffective (an |
322 | + # alternative would be to define a specific ui factory instead...) |
323 | + self.orig_report_activity = self._transport._report_activity |
324 | + self._transport._report_activity = report_activity |
325 | + |
326 | + def tearDown(self): |
327 | + self._transport._report_activity = self.orig_report_activity |
328 | + self.server.tearDown() |
329 | + tests.TestCase.tearDown(self) |
330 | + |
331 | + |
332 | +class TestNoReportActivity(tests.TestCase, TestActivityMixin): |
333 | + |
334 | + def setUp(self): |
335 | + tests.TestCase.setUp(self) |
336 | + # Unlike TestActivity, we are really testing ReportingFileSocket and |
337 | + # ReportingSocket, so we don't need all the parametrization. Since |
338 | + # ReportingFileSocket and ReportingSocket are wrappers, it's easier to |
339 | + # test them through their use by the transport than directly (that's a |
340 | + # bit less clean but far more simpler and effective). |
341 | + self.server = ActivityHTTPServer('HTTP/1.1') |
342 | + self._transport=_urllib.HttpTransport_urllib |
343 | + |
344 | + self.server.setUp() |
345 | + |
346 | + # We override at class level because constructors may propagate the |
347 | + # bound method and render instance overriding ineffective (an |
348 | + # alternative would be to define a specific ui factory instead...) |
349 | + self.orig_report_activity = self._transport._report_activity |
350 | + self._transport._report_activity = None |
351 | + |
352 | + def tearDown(self): |
353 | + self._transport._report_activity = self.orig_report_activity |
354 | + self.server.tearDown() |
355 | + tests.TestCase.tearDown(self) |
356 | + |
357 | + def assertActivitiesMatch(self): |
358 | + # Nothing to check here |
359 | + pass |
360 | |
361 | === modified file 'bzrlib/transport/http/_urllib2_wrappers.py' |
362 | --- bzrlib/transport/http/_urllib2_wrappers.py 2009-08-19 16:33:39 +0000 |
363 | +++ bzrlib/transport/http/_urllib2_wrappers.py 2009-10-30 21:10:26 +0000 |
364 | @@ -80,10 +80,13 @@ |
365 | self.filesock = filesock |
366 | self._report_activity = report_activity |
367 | |
368 | + def report_activity(self, size, direction): |
369 | + if self._report_activity: |
370 | + self._report_activity(size, direction) |
371 | |
372 | def read(self, size=1): |
373 | s = self.filesock.read(size) |
374 | - self._report_activity(len(s), 'read') |
375 | + self.report_activity(len(s), 'read') |
376 | return s |
377 | |
378 | def readline(self): |
379 | @@ -93,7 +96,7 @@ |
380 | # don't *need* the size parameter we'll stay with readline(self) |
381 | # -- vila 20090209 |
382 | s = self.filesock.readline() |
383 | - self._report_activity(len(s), 'read') |
384 | + self.report_activity(len(s), 'read') |
385 | return s |
386 | |
387 | def __getattr__(self, name): |
388 | @@ -106,13 +109,17 @@ |
389 | self.sock = sock |
390 | self._report_activity = report_activity |
391 | |
392 | + def report_activity(self, size, direction): |
393 | + if self._report_activity: |
394 | + self._report_activity(size, direction) |
395 | + |
396 | def sendall(self, s, *args): |
397 | self.sock.sendall(s, *args) |
398 | - self._report_activity(len(s), 'write') |
399 | + self.report_activity(len(s), 'write') |
400 | |
401 | def recv(self, *args): |
402 | s = self.sock.recv(*args) |
403 | - self._report_activity(len(s), 'read') |
404 | + self.report_activity(len(s), 'read') |
405 | return s |
406 | |
407 | def makefile(self, mode='r', bufsize=-1): |
408 | @@ -219,8 +226,7 @@ |
409 | # we want to warn. But not below a given thresold. |
410 | _range_warning_thresold = 1024 * 1024 |
411 | |
412 | - def __init__(self, |
413 | - report_activity=None): |
414 | + def __init__(self, report_activity=None): |
415 | self._response = None |
416 | self._report_activity = report_activity |
417 | self._ranges_received_whole_file = None |
418 | @@ -360,7 +366,13 @@ |
419 | |
420 | def set_proxy(self, proxy, type): |
421 | """Set the proxy and remember the proxied host.""" |
422 | - self.proxied_host = self.get_host() |
423 | + # We need to set the default port ourselves way before it gets set |
424 | + # in the HTTP[S]Connection object at build time. |
425 | + if self.type == 'https': |
426 | + conn_class = HTTPSConnection |
427 | + else: |
428 | + conn_class = HTTPConnection |
429 | + self.proxied_host = '%s:%s' % (self.get_host(), conn_class.default_port) |
430 | urllib2.Request.set_proxy(self, proxy, type) |
431 | |
432 |
Thanks to Gordon Tyler for starting a proof-of-concept implementation, I've finally
been able to fix bug #186920 et all (the one about lp URLS that can't be
resolved behind proxies).
This patch implement an XMLRPC transport class using our _urllib2_wrappers (the fundations
for the urllib http client), that gives us proxy support for free.
I still haven't implemented a true proxy test server but I've added tests that exercise
the full urllib http stack.