| 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 |