| OLD | NEW |
| 1 # This file is part of the Adblock Plus web scripts, | 1 # This file is part of the Adblock Plus web scripts, |
| 2 # Copyright (C) 2006-2016 Eyeo GmbH | 2 # Copyright (C) 2006-2016 Eyeo GmbH |
| 3 # | 3 # |
| 4 # Adblock Plus is free software: you can redistribute it and/or modify | 4 # Adblock Plus is free software: you can redistribute it and/or modify |
| 5 # it under the terms of the GNU General Public License version 3 as | 5 # it under the terms of the GNU General Public License version 3 as |
| 6 # published by the Free Software Foundation. | 6 # published by the Free Software Foundation. |
| 7 # | 7 # |
| 8 # Adblock Plus is distributed in the hope that it will be useful, | 8 # Adblock Plus is distributed in the hope that it will be useful, |
| 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of | 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 11 # GNU General Public License for more details. | 11 # GNU General Public License for more details. |
| 12 # | 12 # |
| 13 # You should have received a copy of the GNU General Public License | 13 # You should have received a copy of the GNU General Public License |
| 14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | 14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
| 15 | 15 |
| 16 """ | 16 """ |
| 17 | 17 |
| 18 Nightly builds generation script | 18 Nightly builds generation script |
| 19 ================================ | 19 ================================ |
| 20 | 20 |
| 21 This script generates nightly builds of extensions, together | 21 This script generates nightly builds of extensions, together |
| 22 with changelogs and documentation. | 22 with changelogs and documentation. |
| 23 | 23 |
| 24 """ | 24 """ |
| 25 | 25 |
| 26 import ConfigParser | 26 import ConfigParser |
| 27 import cookielib | 27 import base64 |
| 28 from datetime import datetime | 28 from datetime import datetime |
| 29 import hashlib | 29 import hashlib |
| 30 import HTMLParser | 30 import hmac |
| 31 import json | 31 import json |
| 32 import logging | 32 import logging |
| 33 import os | 33 import os |
| 34 import pipes | 34 import pipes |
| 35 import random |
| 35 import shutil | 36 import shutil |
| 36 import struct | 37 import struct |
| 37 import subprocess | 38 import subprocess |
| 38 import sys | 39 import sys |
| 39 import tempfile | 40 import tempfile |
| 40 import time | 41 import time |
| 41 from urllib import urlencode | 42 from urllib import urlencode |
| 42 import urllib2 | 43 import urllib2 |
| 43 import urlparse | 44 import urlparse |
| 44 from xml.dom.minidom import parse as parseXml | 45 from xml.dom.minidom import parse as parseXml |
| (...skipping 339 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 384 } | 385 } |
| 385 if os.path.exists(os.path.join(baseDir, changelogFile)): | 386 if os.path.exists(os.path.join(baseDir, changelogFile)): |
| 386 link['changelog'] = changelogFile | 387 link['changelog'] = changelogFile |
| 387 links.append(link) | 388 links.append(link) |
| 388 template = get_template(get_config().get('extensions', 'nightlyIndexPage
')) | 389 template = get_template(get_config().get('extensions', 'nightlyIndexPage
')) |
| 389 template.stream({'config': self.config, 'links': links}).dump(outputPath
) | 390 template.stream({'config': self.config, 'links': links}).dump(outputPath
) |
| 390 | 391 |
| 391 def uploadToMozillaAddons(self): | 392 def uploadToMozillaAddons(self): |
| 392 import urllib3 | 393 import urllib3 |
| 393 | 394 |
| 394 username = get_config().get('extensions', 'amo_username') | 395 header = { |
| 395 password = get_config().get('extensions', 'amo_password') | 396 'alg': 'HS256', # HMAC-SHA256 |
| 397 'typ': 'JWT', |
| 398 } |
| 396 | 399 |
| 397 slug = self.config.galleryID | 400 issued = int(time.time()) |
| 398 login_url = 'https://addons.mozilla.org/en-US/firefox/users/login' | 401 payload = { |
| 399 upload_url = 'https://addons.mozilla.org/en-US/developers/addon/%s/uploa
d' % slug | 402 'iss': get_config().get('extensions', 'amo_key'), |
| 400 add_url = 'https://addons.mozilla.org/en-US/developers/addon/%s/versions
/add' % slug | 403 'jti': random.random(), |
| 404 'iat': issued, |
| 405 'exp': issued + 60, |
| 406 } |
| 401 | 407 |
| 402 cookie_jar = cookielib.CookieJar() | 408 input = '.'.join([ |
| 403 opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie_jar)) | 409 base64.b64encode(json.dumps(header)), |
| 410 base64.b64encode(json.dumps(payload)) |
| 411 ]) |
| 404 | 412 |
| 405 def load_url(url, data=None): | 413 signature = hmac.new(get_config().get('extensions', 'amo_secret'), |
| 406 content_type = 'application/x-www-form-urlencoded' | 414 msg=input, |
| 407 if isinstance(data, dict): | 415 digestmod=hashlib.sha256).digest() |
| 408 if any(isinstance(v, tuple) for v in data.itervalues()): | 416 token = '.'.join([input, base64.b64encode(signature)]) |
| 409 data, content_type = urllib3.filepost.encode_multipart_formd
ata(data) | |
| 410 else: | |
| 411 data = urlencode(data.items()) | |
| 412 | 417 |
| 413 request = urllib2.Request(url, data, headers={'Content-Type': conten
t_type}) | 418 upload_url = ('https://addons.mozilla.org/api/v3/addons/{0}/' |
| 414 response = opener.open(request) | 419 'versions/{1}/').format(self.extensionID, self.version) |
| 415 try: | |
| 416 return response.read() | |
| 417 finally: | |
| 418 response.close() | |
| 419 | 420 |
| 420 class CSRFParser(HTMLParser.HTMLParser): | 421 opener = urllib2.build_opener(urllib2.HTTPHandler) |
| 421 result = None | 422 with open(self.path, 'rb') as file: |
| 422 dummy_exception = Exception() | 423 data, content_type = urllib3.filepost.encode_multipart_formdata({ |
| 424 'upload': ( |
| 425 os.path.basename(self.path), |
| 426 file.read(), |
| 427 'application/x-xpinstall' |
| 428 ) |
| 429 }) |
| 423 | 430 |
| 424 def __init__(self, data): | 431 request = urllib2.Request(upload_url, data=data) |
| 425 HTMLParser.HTMLParser.__init__(self) | 432 request.add_header('Content-Type', content_type) |
| 426 try: | 433 request.add_header('Authorization', 'JWT ' + token) |
| 427 self.feed(data) | 434 request.get_method = lambda: 'PUT' |
| 428 self.close() | |
| 429 except Exception, e: | |
| 430 if e != self.dummy_exception: | |
| 431 raise | |
| 432 | 435 |
| 433 if not self.result: | 436 try: |
| 434 raise Exception('Failed to extract CSRF token') | 437 opener.open(request).close() |
| 435 | 438 except urllib2.HTTPError as e: |
| 436 def set_result(self, value): | 439 logging.error(e.read()) |
| 437 self.result = value | 440 raise |
| 438 raise self.dummy_exception | |
| 439 | |
| 440 def handle_starttag(self, tag, attrs): | |
| 441 attrs = dict(attrs) | |
| 442 if tag == 'meta' and attrs.get('name') == 'csrf': | |
| 443 self.set_result(attrs.get('content')) | |
| 444 if tag == 'input' and attrs.get('name') == 'csrfmiddlewaretoken'
: | |
| 445 self.set_result(attrs.get('value')) | |
| 446 | |
| 447 # Extract anonymous CSRF token | |
| 448 login_page = load_url(login_url) | |
| 449 csrf_token = CSRFParser(login_page).result | |
| 450 | |
| 451 # Log in and get session's CSRF token | |
| 452 main_page = load_url( | |
| 453 login_url, | |
| 454 { | |
| 455 'csrfmiddlewaretoken': csrf_token, | |
| 456 'username': username, | |
| 457 'password': password, | |
| 458 } | |
| 459 ) | |
| 460 csrf_token = CSRFParser(main_page).result | |
| 461 | |
| 462 # Upload build | |
| 463 with open(self.path, 'rb') as file: | |
| 464 upload_response = json.loads(load_url( | |
| 465 upload_url, | |
| 466 { | |
| 467 'csrfmiddlewaretoken': csrf_token, | |
| 468 'upload': (os.path.basename(self.path), file.read(), 'applic
ation/x-xpinstall'), | |
| 469 } | |
| 470 )) | |
| 471 | |
| 472 # Wait for validation to finish | |
| 473 while not upload_response.get('validation'): | |
| 474 time.sleep(2) | |
| 475 upload_response = json.loads(load_url( | |
| 476 upload_url + '/' + upload_response.get('upload') | |
| 477 )) | |
| 478 | |
| 479 if upload_response['validation'].get('errors', 0): | |
| 480 raise Exception('Build failed AMO validation, see https://addons.moz
illa.org%s' % upload_response.get('full_report_url')) | |
| 481 | |
| 482 # Add version | |
| 483 add_response = json.loads(load_url( | |
| 484 add_url, | |
| 485 { | |
| 486 'csrfmiddlewaretoken': csrf_token, | |
| 487 'upload': upload_response.get('upload'), | |
| 488 'source': ('', '', 'application/octet-stream'), | |
| 489 'beta': 'on', | |
| 490 'supported_platforms': 1, # PLATFORM_ANY.id | |
| 491 } | |
| 492 )) | |
| 493 | 441 |
| 494 def uploadToChromeWebStore(self): | 442 def uploadToChromeWebStore(self): |
| 495 # Google APIs use HTTP error codes with error message in body. So we add | 443 # Google APIs use HTTP error codes with error message in body. So we add |
| 496 # the response body to the HTTPError to get more meaningful error messag
es. | 444 # the response body to the HTTPError to get more meaningful error messag
es. |
| 497 | 445 |
| 498 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler): | 446 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler): |
| 499 def http_error_default(self, req, fp, code, msg, hdrs): | 447 def http_error_default(self, req, fp, code, msg, hdrs): |
| 500 raise urllib2.HTTPError(req.get_full_url(), code, '%s\n%s' % (ms
g, fp.read()), hdrs, fp) | 448 raise urllib2.HTTPError(req.get_full_url(), code, '%s\n%s' % (ms
g, fp.read()), hdrs, fp) |
| 501 | 449 |
| 502 opener = urllib2.build_opener(HTTPErrorBodyHandler) | 450 opener = urllib2.build_opener(HTTPErrorBodyHandler) |
| (...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 593 | 541 |
| 594 if self.config.type == 'ie': | 542 if self.config.type == 'ie': |
| 595 self.writeIEUpdateManifest(versions) | 543 self.writeIEUpdateManifest(versions) |
| 596 | 544 |
| 597 # update index page | 545 # update index page |
| 598 self.updateIndex(versions) | 546 self.updateIndex(versions) |
| 599 | 547 |
| 600 # update nightlies config | 548 # update nightlies config |
| 601 self.config.latestRevision = self.revision | 549 self.config.latestRevision = self.revision |
| 602 | 550 |
| 603 if self.config.type == 'gecko' and self.config.galleryID and get_con
fig().get('extensions', 'amo_username'): | 551 if self.config.type == 'gecko' and self.config.galleryID and get_con
fig().has_option('extensions', 'amo_key'): |
| 604 self.uploadToMozillaAddons() | 552 self.uploadToMozillaAddons() |
| 605 elif self.config.type == 'chrome' and self.config.clientID and self.
config.clientSecret and self.config.refreshToken: | 553 elif self.config.type == 'chrome' and self.config.clientID and self.
config.clientSecret and self.config.refreshToken: |
| 606 self.uploadToChromeWebStore() | 554 self.uploadToChromeWebStore() |
| 607 finally: | 555 finally: |
| 608 # clean up | 556 # clean up |
| 609 if self.tempdir: | 557 if self.tempdir: |
| 610 shutil.rmtree(self.tempdir, ignore_errors=True) | 558 shutil.rmtree(self.tempdir, ignore_errors=True) |
| 611 | 559 |
| 612 | 560 |
| 613 def main(): | 561 def main(): |
| (...skipping 17 matching lines...) Expand all Loading... |
| 631 except Exception, ex: | 579 except Exception, ex: |
| 632 logging.error('The build for %s failed:', repo) | 580 logging.error('The build for %s failed:', repo) |
| 633 logging.exception(ex) | 581 logging.exception(ex) |
| 634 | 582 |
| 635 file = open(nightlyConfigFile, 'wb') | 583 file = open(nightlyConfigFile, 'wb') |
| 636 nightlyConfig.write(file) | 584 nightlyConfig.write(file) |
| 637 | 585 |
| 638 | 586 |
| 639 if __name__ == '__main__': | 587 if __name__ == '__main__': |
| 640 main() | 588 main() |
| OLD | NEW |