OLD | NEW |
1 # coding: utf-8 | 1 # coding: utf-8 |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 """Defines class Rietveld to easily access a rietveld instance. | 5 """Defines class Rietveld to easily access a rietveld instance. |
6 | 6 |
7 Security implications: | 7 Security implications: |
8 | 8 |
9 The following hypothesis are made: | 9 The following hypothesis are made: |
10 - Rietveld enforces: | 10 - Rietveld enforces: |
11 - Nobody else than issue owner can upload a patch set | 11 - Nobody else than issue owner can upload a patch set |
12 - Verifies the issue owner credentials when creating new issues | 12 - Verifies the issue owner credentials when creating new issues |
13 - A issue owner can't change once the issue is created | 13 - A issue owner can't change once the issue is created |
14 - A patch set cannot be modified | 14 - A patch set cannot be modified |
15 """ | 15 """ |
16 | 16 |
17 import copy | 17 import copy |
18 import errno | 18 import errno |
19 import json | 19 import json |
20 import logging | 20 import logging |
21 import re | 21 import re |
22 import socket | 22 import socket |
23 import ssl | 23 import ssl |
| 24 import StringIO |
24 import sys | 25 import sys |
25 import time | 26 import time |
26 import urllib | 27 import urllib |
27 import urllib2 | 28 import urllib2 |
28 import urlparse | 29 import urlparse |
29 | 30 |
30 import patch | 31 import patch |
31 | 32 |
32 from third_party import upload | 33 from third_party import upload |
33 import third_party.oauth2client.client as oa2client | 34 import third_party.oauth2client.client as oa2client |
(...skipping 368 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
402 try: | 403 try: |
403 # Sadly, upload.py calls ErrorExit() which does a sys.exit(1) on HTTP | 404 # Sadly, upload.py calls ErrorExit() which does a sys.exit(1) on HTTP |
404 # 500 in AbstractRpcServer.Send(). | 405 # 500 in AbstractRpcServer.Send(). |
405 old_error_exit = upload.ErrorExit | 406 old_error_exit = upload.ErrorExit |
406 def trap_http_500(msg): | 407 def trap_http_500(msg): |
407 """Converts an incorrect ErrorExit() call into a HTTPError exception.""" | 408 """Converts an incorrect ErrorExit() call into a HTTPError exception.""" |
408 m = re.search(r'(50\d) Server Error', msg) | 409 m = re.search(r'(50\d) Server Error', msg) |
409 if m: | 410 if m: |
410 # Fake an HTTPError exception. Cheezy. :( | 411 # Fake an HTTPError exception. Cheezy. :( |
411 raise urllib2.HTTPError( | 412 raise urllib2.HTTPError( |
412 request_path, int(m.group(1)), msg, None, None) | 413 request_path, int(m.group(1)), msg, None, StringIO.StringIO()) |
413 old_error_exit(msg) | 414 old_error_exit(msg) |
414 upload.ErrorExit = trap_http_500 | 415 upload.ErrorExit = trap_http_500 |
415 | 416 |
416 for retry in xrange(self._maxtries): | 417 for retry in xrange(self._maxtries): |
417 try: | 418 try: |
418 logging.debug('%s' % request_path) | 419 logging.debug('%s' % request_path) |
419 result = self.rpc_server.Send(request_path, **kwargs) | 420 return self.rpc_server.Send(request_path, **kwargs) |
420 # Sometimes GAE returns a HTTP 200 but with HTTP 500 as the content. | |
421 # How nice. | |
422 return result | |
423 except urllib2.HTTPError, e: | 421 except urllib2.HTTPError, e: |
424 if retry >= (self._maxtries - 1): | 422 if retry >= (self._maxtries - 1): |
425 raise | 423 raise |
426 flake_codes = [500, 502, 503] | 424 flake_codes = {500, 502, 503} |
427 if retry_on_404: | 425 if retry_on_404: |
428 flake_codes.append(404) | 426 flake_codes.add(404) |
429 if e.code not in flake_codes: | 427 if e.code not in flake_codes: |
430 raise | 428 raise |
431 except urllib2.URLError, e: | 429 except urllib2.URLError, e: |
432 if retry >= (self._maxtries - 1): | 430 if retry >= (self._maxtries - 1): |
433 raise | 431 raise |
434 | 432 |
435 def is_transient(): | 433 def is_transient(): |
436 # The idea here is to retry if the error isn't permanent. | 434 # The idea here is to retry if the error isn't permanent. |
437 # Unfortunately, there are so many different possible errors, | 435 # Unfortunately, there are so many different possible errors, |
438 # that we end up enumerating those that are known to us to be | 436 # that we end up enumerating those that are known to us to be |
439 # transient. | 437 # transient. |
440 # The reason can be a string or another exception, e.g., | 438 # The reason can be a string or another exception, e.g., |
441 # socket.error or whatever else. | 439 # socket.error or whatever else. |
442 reason_as_str = str(e.reason) | 440 reason_as_str = str(e.reason) |
443 for retry_anyway in [ | 441 for retry_anyway in ( |
444 'Name or service not known', | 442 'Name or service not known', |
445 'EOF occurred in violation of protocol', | 443 'EOF occurred in violation of protocol', |
446 'timed out']: | 444 'timed out'): |
447 if retry_anyway in reason_as_str: | 445 if retry_anyway in reason_as_str: |
448 return True | 446 return True |
449 return False # Assume permanent otherwise. | 447 return False # Assume permanent otherwise. |
450 if not is_transient(): | 448 if not is_transient(): |
451 raise | 449 raise |
452 except socket.error, e: | 450 except socket.error, e: |
453 if retry >= (self._maxtries - 1): | 451 if retry >= (self._maxtries - 1): |
454 raise | 452 raise |
455 if not 'timed out' in str(e): | 453 if not 'timed out' in str(e): |
456 raise | 454 raise |
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
521 extra_headers=None, | 519 extra_headers=None, |
522 **kwargs): | 520 **kwargs): |
523 """Send a POST or GET request to the server. | 521 """Send a POST or GET request to the server. |
524 | 522 |
525 Args: | 523 Args: |
526 request_path: path on the server to hit. This is concatenated with the | 524 request_path: path on the server to hit. This is concatenated with the |
527 value of 'host' provided to the constructor. | 525 value of 'host' provided to the constructor. |
528 payload: request is a POST if not None, GET otherwise | 526 payload: request is a POST if not None, GET otherwise |
529 timeout: in seconds | 527 timeout: in seconds |
530 extra_headers: (dict) | 528 extra_headers: (dict) |
| 529 |
| 530 Returns: the HTTP response body as a string |
| 531 |
| 532 Raises: |
| 533 urllib2.HTTPError |
531 """ | 534 """ |
532 # This method signature should match upload.py:AbstractRpcServer.Send() | 535 # This method signature should match upload.py:AbstractRpcServer.Send() |
533 method = 'GET' | 536 method = 'GET' |
534 | 537 |
535 headers = self.extra_headers.copy() | 538 headers = self.extra_headers.copy() |
536 headers.update(extra_headers or {}) | 539 headers.update(extra_headers or {}) |
537 | 540 |
538 if payload is not None: | 541 if payload is not None: |
539 method = 'POST' | 542 method = 'POST' |
540 headers['Content-Type'] = content_type | 543 headers['Content-Type'] = content_type |
541 | 544 |
542 prev_timeout = self._http.timeout | 545 prev_timeout = self._http.timeout |
543 try: | 546 try: |
544 if timeout: | 547 if timeout: |
545 self._http.timeout = timeout | 548 self._http.timeout = timeout |
546 # TODO(pgervais) implement some kind of retry mechanism (see upload.py). | |
547 url = self.host + request_path | 549 url = self.host + request_path |
548 if kwargs: | 550 if kwargs: |
549 url += "?" + urllib.urlencode(kwargs) | 551 url += "?" + urllib.urlencode(kwargs) |
550 | 552 |
551 # This weird loop is there to detect when the OAuth2 token has expired. | 553 # This weird loop is there to detect when the OAuth2 token has expired. |
552 # This is specific to appengine *and* rietveld. It relies on the | 554 # This is specific to appengine *and* rietveld. It relies on the |
553 # assumption that a 302 is triggered only by an expired OAuth2 token. This | 555 # assumption that a 302 is triggered only by an expired OAuth2 token. This |
554 # prevents any usage of redirections in pages accessed this way. | 556 # prevents any usage of redirections in pages accessed this way. |
555 | 557 |
556 # This variable is used to make sure the following loop runs only twice. | 558 # This variable is used to make sure the following loop runs only twice. |
557 redirect_caught = False | 559 redirect_caught = False |
558 while True: | 560 while True: |
559 try: | 561 try: |
560 ret = self._http.request(url, | 562 ret = self._http.request(url, |
561 method=method, | 563 method=method, |
562 body=payload, | 564 body=payload, |
563 headers=headers, | 565 headers=headers, |
564 redirections=0) | 566 redirections=0) |
565 except httplib2.RedirectLimit: | 567 except httplib2.RedirectLimit: |
566 if redirect_caught or method != 'GET': | 568 if redirect_caught or method != 'GET': |
567 logging.error('Redirection detected after logging in. Giving up.') | 569 logging.error('Redirection detected after logging in. Giving up.') |
568 raise | 570 raise |
569 redirect_caught = True | 571 redirect_caught = True |
570 logging.debug('Redirection detected. Trying to log in again...') | 572 logging.debug('Redirection detected. Trying to log in again...') |
571 self.creds.access_token = None | 573 self.creds.access_token = None |
572 continue | 574 continue |
573 break | 575 break |
574 | 576 |
| 577 if ret[0].status >= 300: |
| 578 raise urllib2.HTTPError( |
| 579 request_path, int(ret[0]['status']), ret[1], None, |
| 580 StringIO.StringIO()) |
| 581 |
575 return ret[1] | 582 return ret[1] |
576 | 583 |
577 finally: | 584 finally: |
578 self._http.timeout = prev_timeout | 585 self._http.timeout = prev_timeout |
579 | 586 |
580 | 587 |
581 class JwtOAuth2Rietveld(Rietveld): | 588 class JwtOAuth2Rietveld(Rietveld): |
582 """Access to Rietveld using OAuth authentication. | 589 """Access to Rietveld using OAuth authentication. |
583 | 590 |
584 This class is supposed to be used only by bots, since this kind of | 591 This class is supposed to be used only by bots, since this kind of |
(...skipping 152 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
737 self, issue, patchset, reason, clobber, revision, builders_and_tests, | 744 self, issue, patchset, reason, clobber, revision, builders_and_tests, |
738 master=None, category='cq'): | 745 master=None, category='cq'): |
739 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % | 746 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % |
740 (builders_and_tests, issue)) | 747 (builders_and_tests, issue)) |
741 | 748 |
742 def trigger_distributed_try_jobs( # pylint:disable=R0201 | 749 def trigger_distributed_try_jobs( # pylint:disable=R0201 |
743 self, issue, patchset, reason, clobber, revision, masters, | 750 self, issue, patchset, reason, clobber, revision, masters, |
744 category='cq'): | 751 category='cq'): |
745 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % | 752 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % |
746 (masters, issue)) | 753 (masters, issue)) |
OLD | NEW |