| Left: | ||
| Right: |
| 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-present eyeo GmbH | 2 # Copyright (C) 2006-present 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 argparse | 26 import argparse |
| 27 import ConfigParser | 27 import ConfigParser |
| 28 import binascii | |
| 28 import base64 | 29 import base64 |
| 29 import hashlib | 30 import hashlib |
| 30 import hmac | 31 import hmac |
| 31 import json | 32 import json |
| 32 import logging | 33 import logging |
| 33 import os | 34 import os |
| 34 import pipes | 35 import pipes |
| 35 import random | |
| 36 import shutil | 36 import shutil |
| 37 import struct | 37 import struct |
| 38 import subprocess | 38 import subprocess |
| 39 import sys | 39 import sys |
| 40 import tempfile | 40 import tempfile |
| 41 import time | 41 import time |
| 42 import uuid | |
| 42 from urllib import urlencode | 43 from urllib import urlencode |
| 43 import urllib2 | 44 import urllib2 |
| 44 import urlparse | 45 import urlparse |
| 45 import zipfile | 46 import zipfile |
| 46 import contextlib | 47 import contextlib |
| 47 | 48 |
| 49 from Crypto.PublicKey import RSA | |
| 50 from Crypto.Signature import PKCS1_v1_5 | |
| 51 import Crypto.Hash.SHA256 | |
| 52 | |
| 48 from xml.dom.minidom import parse as parseXml | 53 from xml.dom.minidom import parse as parseXml |
| 49 | 54 |
| 50 from sitescripts.extensions.utils import ( | 55 from sitescripts.extensions.utils import ( |
| 51 compareVersions, Configuration, | 56 compareVersions, Configuration, |
| 52 writeAndroidUpdateManifest | 57 writeAndroidUpdateManifest |
| 53 ) | 58 ) |
| 54 from sitescripts.utils import get_config, get_template | 59 from sitescripts.utils import get_config, get_template |
| 55 | 60 |
| 56 MAX_BUILDS = 50 | 61 MAX_BUILDS = 50 |
| 57 | 62 |
| (...skipping 285 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 343 if os.path.exists(self.path): | 348 if os.path.exists(self.path): |
| 344 os.remove(self.path) | 349 os.remove(self.path) |
| 345 raise | 350 raise |
| 346 else: | 351 else: |
| 347 env = os.environ | 352 env = os.environ |
| 348 spiderMonkeyBinary = self.config.spiderMonkeyBinary | 353 spiderMonkeyBinary = self.config.spiderMonkeyBinary |
| 349 if spiderMonkeyBinary: | 354 if spiderMonkeyBinary: |
| 350 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary) | 355 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary) |
| 351 | 356 |
| 352 command = [os.path.join(self.tempdir, 'build.py')] | 357 command = [os.path.join(self.tempdir, 'build.py')] |
| 353 if self.config.type == 'safari': | 358 command.extend(['build', '-t', self.config.type, '-b', |
| 354 command.extend(['-t', self.config.type, 'build']) | 359 self.buildNum]) |
|
Sebastian Noack
2018/04/16 16:13:30
Nit: The indentation is off by one space here.
tlucas
2018/04/16 16:37:44
Done.
| |
| 355 else: | |
| 356 command.extend(['build', '-t', self.config.type]) | |
| 357 command.extend(['-b', self.buildNum]) | |
| 358 | 360 |
| 359 if self.config.type not in {'gecko', 'edge'}: | 361 if self.config.type not in {'gecko', 'edge'}: |
| 360 command.extend(['-k', self.config.keyFile]) | 362 command.extend(['-k', self.config.keyFile]) |
| 361 command.append(self.path) | 363 command.append(self.path) |
| 362 subprocess.check_call(command, env=env) | 364 subprocess.check_call(command, env=env) |
| 363 | 365 |
| 364 if not os.path.exists(self.path): | 366 if not os.path.exists(self.path): |
| 365 raise Exception("Build failed, output file hasn't been created") | 367 raise Exception("Build failed, output file hasn't been created") |
| 366 | 368 |
| 367 if self.config.type not in self.downloadable_repos: | 369 if self.config.type not in self.downloadable_repos: |
| (...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 450 try: | 452 try: |
| 451 for i, entry in enumerate(current[platform]): | 453 for i, entry in enumerate(current[platform]): |
| 452 if entry[filter_key] == filter_value: | 454 if entry[filter_key] == filter_value: |
| 453 del current[platform][i] | 455 del current[platform][i] |
| 454 if len(current[platform]) == 0: | 456 if len(current[platform]) == 0: |
| 455 del current[platform] | 457 del current[platform] |
| 456 except KeyError: | 458 except KeyError: |
| 457 pass | 459 pass |
| 458 self.write_downloads_lockfile(current) | 460 self.write_downloads_lockfile(current) |
| 459 | 461 |
| 460 def generate_jwt_request(self, issuer, secret, url, method, data=None, | 462 def azure_jwt_signature_fnc(self): |
| 461 add_headers=[]): | 463 return ('RS256', |
| 462 header = { | 464 lambda s, m: PKCS1_v1_5.new(s).sign(Crypto.Hash.SHA256.new(m))) |
| 463 'alg': 'HS256', # HMAC-SHA256 | 465 |
| 464 'typ': 'JWT', | 466 def mozilla_jwt_signature_fnc(self): |
| 467 return ( | |
|
Sebastian Noack
2018/04/16 16:13:29
Nit: It looks weird to use different flavor of ind
tlucas
2018/04/16 16:37:44
Done.
| |
| 468 'HS256', | |
| 469 lambda s, m: hmac.new(s, msg=m, digestmod=hashlib.sha256).digest() | |
| 470 ) | |
| 471 | |
| 472 def sign_jwt(self, issuer, secret, url, signature_fnc, jwt_headers={}): | |
| 473 alg, fnc = signature_fnc() | |
| 474 | |
| 475 header = {'typ': 'JWT'} | |
| 476 header.update(jwt_headers) | |
| 477 header.update({'alg': alg}) | |
| 478 | |
| 479 issued = int(time.mktime(time.localtime())) | |
| 480 expires = issued + 60 | |
| 481 | |
| 482 payload = { | |
| 483 'aud': url, | |
| 484 'iss': issuer, | |
| 485 'sub': issuer, | |
| 486 'jti': str(uuid.uuid4()), | |
| 487 'iat': issued, | |
| 488 'nbf': issued, | |
| 489 'exp': expires, | |
| 465 } | 490 } |
| 466 | 491 |
| 467 issued = int(time.time()) | 492 segments = [base64.urlsafe_b64encode(json.dumps(header)), |
| 468 payload = { | 493 base64.urlsafe_b64encode(json.dumps(payload))] |
| 469 'iss': issuer, | |
| 470 'jti': random.random(), | |
| 471 'iat': issued, | |
| 472 'exp': issued + 60, | |
| 473 } | |
| 474 | 494 |
| 475 hmac_data = '{}.{}'.format( | 495 signature = fnc(secret, b'.'.join(segments)) |
| 476 base64.b64encode(json.dumps(header)), | 496 segments.append(base64.urlsafe_b64encode(signature)) |
| 477 base64.b64encode(json.dumps(payload)) | 497 return b'.'.join(segments) |
| 478 ) | |
| 479 | 498 |
| 480 signature = hmac.new(secret, msg=hmac_data, | 499 def generate_mozilla_jwt_request(self, issuer, secret, url, method, |
| 481 digestmod=hashlib.sha256).digest() | 500 data=None, add_headers=[]): |
| 482 token = '{}.{}'.format(hmac_data, base64.b64encode(signature)) | 501 signed = self.sign_jwt(issuer, secret, url, |
| 502 self.mozilla_jwt_signature_fnc) | |
| 483 | 503 |
| 484 request = urllib2.Request(url, data) | 504 request = urllib2.Request(url, data) |
| 485 request.add_header('Authorization', 'JWT ' + token) | 505 request.add_header('Authorization', 'JWT ' + signed) |
| 486 for header in add_headers: | 506 for header in add_headers: |
| 487 request.add_header(*header) | 507 request.add_header(*header) |
| 488 request.get_method = lambda: method | 508 request.get_method = lambda: method |
| 489 | 509 |
| 490 return request | 510 return request |
| 491 | 511 |
| 492 def uploadToMozillaAddons(self): | 512 def uploadToMozillaAddons(self): |
| 493 import urllib3 | 513 import urllib3 |
| 494 | 514 |
| 495 config = get_config() | 515 config = get_config() |
| 496 | 516 |
| 497 upload_url = ('https://addons.mozilla.org/api/v3/addons/{}/' | 517 upload_url = ('https://addons.mozilla.org/api/v3/addons/{}/' |
| 498 'versions/{}/').format(self.extensionID, self.version) | 518 'versions/{}/').format(self.extensionID, self.version) |
| 499 | 519 |
| 500 with open(self.path, 'rb') as file: | 520 with open(self.path, 'rb') as file: |
| 501 data, content_type = urllib3.filepost.encode_multipart_formdata({ | 521 data, content_type = urllib3.filepost.encode_multipart_formdata({ |
| 502 'upload': ( | 522 'upload': ( |
| 503 os.path.basename(self.path), | 523 os.path.basename(self.path), |
| 504 file.read(), | 524 file.read(), |
| 505 'application/x-xpinstall' | 525 'application/x-xpinstall' |
| 506 ) | 526 ) |
| 507 }) | 527 }) |
| 508 | 528 |
| 509 request = self.generate_jwt_request( | 529 request = self.generate_mozilla_jwt_request( |
| 510 config.get('extensions', 'amo_key'), | 530 config.get('extensions', 'amo_key'), |
| 511 config.get('extensions', 'amo_secret'), | 531 config.get('extensions', 'amo_secret'), |
| 512 upload_url, | 532 upload_url, |
| 513 'PUT', | 533 'PUT', |
| 514 data, | 534 data, |
| 515 [('Content-Type', content_type)] | 535 [('Content-Type', content_type)], |
| 516 ) | 536 ) |
| 517 | 537 |
| 518 try: | 538 try: |
| 519 urllib2.urlopen(request).close() | 539 urllib2.urlopen(request).close() |
| 520 except urllib2.HTTPError as e: | 540 except urllib2.HTTPError as e: |
| 521 try: | 541 try: |
| 522 logging.error(e.read()) | 542 logging.error(e.read()) |
| 523 finally: | 543 finally: |
| 524 e.close() | 544 e.close() |
| 525 raise | 545 raise |
| 526 | 546 |
| 527 self.add_to_downloads_lockfile( | 547 self.add_to_downloads_lockfile( |
| 528 self.config.type, | 548 self.config.type, |
| 529 { | 549 { |
| 530 'buildtype': 'devbuild', | 550 'buildtype': 'devbuild', |
| 531 'app_id': self.extensionID, | 551 'app_id': self.extensionID, |
| 532 'version': self.version, | 552 'version': self.version, |
| 533 } | 553 } |
| 534 ) | 554 ) |
| 535 os.remove(self.path) | 555 os.remove(self.path) |
| 536 | 556 |
| 537 def download_from_mozilla_addons(self, buildtype, version, app_id): | 557 def download_from_mozilla_addons(self, buildtype, version, app_id): |
| 538 config = get_config() | 558 config = get_config() |
| 539 iss = config.get('extensions', 'amo_key') | 559 iss = config.get('extensions', 'amo_key') |
| 540 secret = config.get('extensions', 'amo_secret') | 560 secret = config.get('extensions', 'amo_secret') |
| 541 | 561 |
| 542 url = ('https://addons.mozilla.org/api/v3/addons/{}/' | 562 url = ('https://addons.mozilla.org/api/v3/addons/{}/' |
| 543 'versions/{}/').format(app_id, version) | 563 'versions/{}/').format(app_id, version) |
| 544 | 564 |
| 545 request = self.generate_jwt_request(iss, secret, url, 'GET') | 565 request = self.generate_mozilla_jwt_request( |
| 566 iss, secret, url, 'GET', | |
| 567 ) | |
| 546 response = json.load(urllib2.urlopen(request)) | 568 response = json.load(urllib2.urlopen(request)) |
| 547 | 569 |
| 548 filename = '{}-{}.xpi'.format(self.basename, version) | 570 filename = '{}-{}.xpi'.format(self.basename, version) |
| 549 self.path = os.path.join( | 571 self.path = os.path.join( |
| 550 config.get('extensions', 'nightliesDirectory'), | 572 config.get('extensions', 'nightliesDirectory'), |
| 551 self.basename, | 573 self.basename, |
| 552 filename | 574 filename |
| 553 ) | 575 ) |
| 554 | 576 |
| 555 necessary = ['passed_review', 'reviewed', 'processed', 'valid'] | 577 necessary = ['passed_review', 'reviewed', 'processed', 'valid'] |
| 556 if all(response[x] for x in necessary): | 578 if all(response[x] for x in necessary): |
| 557 download_url = response['files'][0]['download_url'] | 579 download_url = response['files'][0]['download_url'] |
| 558 checksum = response['files'][0]['hash'] | 580 checksum = response['files'][0]['hash'] |
| 559 | 581 |
| 560 request = self.generate_jwt_request(iss, secret, download_url, | 582 request = self.generate_mozilla_jwt_request( |
| 561 'GET') | 583 iss, secret, download_url, 'GET', |
| 584 ) | |
| 562 try: | 585 try: |
| 563 response = urllib2.urlopen(request) | 586 response = urllib2.urlopen(request) |
| 564 except urllib2.HTTPError as e: | 587 except urllib2.HTTPError as e: |
| 565 logging.error(e.read()) | 588 logging.error(e.read()) |
| 566 | 589 |
| 567 # Verify the extension's integrity | 590 # Verify the extension's integrity |
| 568 file_content = response.read() | 591 file_content = response.read() |
| 569 sha256 = hashlib.sha256(file_content) | 592 sha256 = hashlib.sha256(file_content) |
| 570 returned_checksum = '{}:{}'.format(sha256.name, sha256.hexdigest()) | 593 returned_checksum = '{}:{}'.format(sha256.name, sha256.hexdigest()) |
| 571 | 594 |
| (...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 642 request.get_method = lambda: 'POST' | 665 request.get_method = lambda: 'POST' |
| 643 request.add_header('Authorization', auth_token) | 666 request.add_header('Authorization', auth_token) |
| 644 request.add_header('x-goog-api-version', '2') | 667 request.add_header('x-goog-api-version', '2') |
| 645 request.add_header('Content-Length', '0') | 668 request.add_header('Content-Length', '0') |
| 646 | 669 |
| 647 response = json.load(opener.open(request)) | 670 response = json.load(opener.open(request)) |
| 648 | 671 |
| 649 if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons e['status']): | 672 if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons e['status']): |
| 650 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']}) | 673 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']}) |
| 651 | 674 |
| 675 def generate_certificate_token_request(self, url, private_key): | |
| 676 # Construct the token request according to | |
| 677 # https://docs.microsoft.com/en-us/azure/active-directory/develop/active -directory-certificate-credentials | |
| 678 hex_val = binascii.a2b_hex(self.config.thumbprint) | |
| 679 x5t = base64.urlsafe_b64encode(hex_val).decode() | |
| 680 | |
|
Sebastian Noack
2018/04/16 16:13:30
Nit: This blank line looks redundant.
tlucas
2018/04/16 16:37:44
IMHO it doesn't, since the above two lines are not
| |
| 681 key = RSA.importKey(private_key) | |
| 682 | |
| 683 signed = self.sign_jwt(self.config.clientID, key, url, | |
| 684 self.azure_jwt_signature_fnc, | |
| 685 jwt_headers={'x5t': x5t}) | |
| 686 | |
| 687 # generate oauth parameters for login.microsoft.com | |
| 688 oauth_params = { | |
| 689 'grant_type': 'client_credentials', | |
| 690 'client_id': self.config.clientID, | |
| 691 'resource': 'https://graph.windows.net', | |
| 692 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-' | |
| 693 'type:jwt-bearer', | |
| 694 'client_assertion': signed, | |
| 695 } | |
| 696 | |
| 697 request = urllib2.Request(url, urlencode(oauth_params)) | |
| 698 request.get_method = lambda: 'POST' | |
| 699 | |
| 700 return request | |
| 701 | |
| 652 def get_windows_store_access_token(self): | 702 def get_windows_store_access_token(self): |
| 653 # use refresh token to obtain a valid access token | 703 # use client certificate to obtain a valid access token |
| 654 # https://docs.microsoft.com/en-us/azure/active-directory/active-directo ry-protocols-oauth-code#refreshing-the-access-tokens | 704 url = 'https://login.microsoftonline.com/{}/oauth2/token'.format( |
| 655 server = 'https://login.microsoftonline.com' | 705 self.config.tenantID |
| 656 token_path = '{}/{}/oauth2/token'.format(server, self.config.tenantID) | 706 ) |
| 707 | |
| 708 with open(self.config.privateKey, 'r') as fp: | |
| 709 private_key = fp.read() | |
| 657 | 710 |
| 658 opener = urllib2.build_opener(HTTPErrorBodyHandler) | 711 opener = urllib2.build_opener(HTTPErrorBodyHandler) |
| 659 post_data = urlencode([ | 712 request = self.generate_certificate_token_request(url, private_key) |
| 660 ('refresh_token', self.config.refreshToken), | 713 |
| 661 ('client_id', self.config.clientID), | |
| 662 ('client_secret', self.config.clientSecret), | |
| 663 ('grant_type', 'refresh_token'), | |
| 664 ('resource', 'https://graph.windows.net') | |
| 665 ]) | |
| 666 request = urllib2.Request(token_path, post_data) | |
| 667 with contextlib.closing(opener.open(request)) as response: | 714 with contextlib.closing(opener.open(request)) as response: |
| 668 data = json.load(response) | 715 data = json.load(response) |
| 669 auth_token = '{0[token_type]} {0[access_token]}'.format(data) | 716 auth_token = '{0[token_type]} {0[access_token]}'.format(data) |
| 670 | 717 |
| 671 return auth_token | 718 return auth_token |
| 672 | 719 |
| 673 def upload_appx_file_to_windows_store(self, file_upload_url): | 720 def upload_appx_file_to_windows_store(self, file_upload_url): |
| 674 # Add .appx file to a .zip file | 721 # Add .appx file to a .zip file |
| 675 zip_path = os.path.splitext(self.path)[0] + '.zip' | 722 zip_path = os.path.splitext(self.path)[0] + '.zip' |
| 676 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: | 723 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: |
| (...skipping 130 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 807 | 854 |
| 808 # update nightlies config | 855 # update nightlies config |
| 809 self.config.latestRevision = self.revision | 856 self.config.latestRevision = self.revision |
| 810 | 857 |
| 811 if (self.config.type == 'gecko' and | 858 if (self.config.type == 'gecko' and |
| 812 self.config.galleryID and | 859 self.config.galleryID and |
| 813 get_config().has_option('extensions', 'amo_key')): | 860 get_config().has_option('extensions', 'amo_key')): |
| 814 self.uploadToMozillaAddons() | 861 self.uploadToMozillaAddons() |
| 815 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken: | 862 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken: |
| 816 self.uploadToChromeWebStore() | 863 self.uploadToChromeWebStore() |
| 817 elif self.config.type == 'edge' and self.config.clientID and self.co nfig.clientSecret and self.config.refreshToken and self.config.tenantID: | 864 elif self.config.type == 'edge' and self.config.clientID and self.co nfig.tenantID and self.config.privateKey and self.config.thumbprint: |
| 818 self.upload_to_windows_store() | 865 self.upload_to_windows_store() |
| 819 | 866 |
| 820 finally: | 867 finally: |
| 821 # clean up | 868 # clean up |
| 822 if self.tempdir: | 869 if self.tempdir: |
| 823 shutil.rmtree(self.tempdir, ignore_errors=True) | 870 shutil.rmtree(self.tempdir, ignore_errors=True) |
| 824 | 871 |
| 825 def download(self): | 872 def download(self): |
| 826 download_info = self.read_downloads_lockfile() | 873 download_info = self.read_downloads_lockfile() |
| 827 downloads = self.downloadable_repos.intersection(download_info.keys()) | 874 downloads = self.downloadable_repos.intersection(download_info.keys()) |
| (...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 889 | 936 |
| 890 file = open(nightlyConfigFile, 'wb') | 937 file = open(nightlyConfigFile, 'wb') |
| 891 nightlyConfig.write(file) | 938 nightlyConfig.write(file) |
| 892 | 939 |
| 893 | 940 |
| 894 if __name__ == '__main__': | 941 if __name__ == '__main__': |
| 895 parser = argparse.ArgumentParser() | 942 parser = argparse.ArgumentParser() |
| 896 parser.add_argument('--download', action='store_true', default=False) | 943 parser.add_argument('--download', action='store_true', default=False) |
| 897 args = parser.parse_args() | 944 args = parser.parse_args() |
| 898 main(args.download) | 945 main(args.download) |
| OLD | NEW |