Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: sitescripts/extensions/bin/createNightlies.py

Issue 29751598: Issue 6291 - Use client certificate for Windows Store uploads (Closed) Base URL: https://hg.adblockplus.org/abpssembly/file/a67d8f0e66b2
Patch Set: NOISSUE rebase against https://codereview.adblockplus.org/29753611/ Created April 16, 2018, 4:57 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | sitescripts/extensions/utils.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
Vasily Kuznetsov 2018/04/17 17:43:54 Nit: this should be above together with other stdl
tlucas 2018/04/18 12:58:30 Done.
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
58 63
(...skipping 388 matching lines...) Expand 10 before | Expand all | Expand 10 after
447 try: 452 try:
448 for i, entry in enumerate(current[platform]): 453 for i, entry in enumerate(current[platform]):
449 if entry[filter_key] == filter_value: 454 if entry[filter_key] == filter_value:
450 del current[platform][i] 455 del current[platform][i]
451 if len(current[platform]) == 0: 456 if len(current[platform]) == 0:
452 del current[platform] 457 del current[platform]
453 except KeyError: 458 except KeyError:
454 pass 459 pass
455 self.write_downloads_lockfile(current) 460 self.write_downloads_lockfile(current)
456 461
457 def generate_jwt_request(self, issuer, secret, url, method, data=None, 462 def azure_jwt_signature_fnc(self):
458 add_headers=[]): 463 return (
459 header = { 464 'RS256',
460 'alg': 'HS256', # HMAC-SHA256 465 lambda s, m: PKCS1_v1_5.new(s).sign(Crypto.Hash.SHA256.new(m))
Vasily Kuznetsov 2018/04/17 17:43:54 Nit: According to the recently landed coding style
tlucas 2018/04/18 12:58:30 Done.
461 'typ': 'JWT', 466 )
467
468 def mozilla_jwt_signature_fnc(self):
469 return (
470 'HS256',
471 lambda s, m: hmac.new(s, msg=m, digestmod=hashlib.sha256).digest()
472 )
473
474 def sign_jwt(self, issuer, secret, url, signature_fnc, jwt_headers={}):
475 alg, fnc = signature_fnc()
476
477 header = {'typ': 'JWT'}
478 header.update(jwt_headers)
479 header.update({'alg': alg})
480
481 issued = int(time.time())
482 expires = issued + 60
483
484 payload = {
485 'aud': url,
486 'iss': issuer,
487 'sub': issuer,
488 'jti': str(uuid.uuid4()),
489 'iat': issued,
490 'nbf': issued,
491 'exp': expires,
462 } 492 }
463 493
464 issued = int(time.time()) 494 segments = [base64.urlsafe_b64encode(json.dumps(header)),
465 payload = { 495 base64.urlsafe_b64encode(json.dumps(payload))]
466 'iss': issuer,
467 'jti': random.random(),
468 'iat': issued,
469 'exp': issued + 60,
470 }
471 496
472 hmac_data = '{}.{}'.format( 497 signature = fnc(secret, b'.'.join(segments))
473 base64.b64encode(json.dumps(header)), 498 segments.append(base64.urlsafe_b64encode(signature))
474 base64.b64encode(json.dumps(payload)) 499 return b'.'.join(segments)
475 )
476 500
477 signature = hmac.new(secret, msg=hmac_data, 501 def generate_mozilla_jwt_request(self, issuer, secret, url, method,
478 digestmod=hashlib.sha256).digest() 502 data=None, add_headers=[]):
479 token = '{}.{}'.format(hmac_data, base64.b64encode(signature)) 503 signed = self.sign_jwt(issuer, secret, url,
504 self.mozilla_jwt_signature_fnc)
480 505
481 request = urllib2.Request(url, data) 506 request = urllib2.Request(url, data)
482 request.add_header('Authorization', 'JWT ' + token) 507 request.add_header('Authorization', 'JWT ' + signed)
483 for header in add_headers: 508 for header in add_headers:
484 request.add_header(*header) 509 request.add_header(*header)
485 request.get_method = lambda: method 510 request.get_method = lambda: method
486 511
487 return request 512 return request
488 513
489 def uploadToMozillaAddons(self): 514 def uploadToMozillaAddons(self):
490 import urllib3 515 import urllib3
491 516
492 config = get_config() 517 config = get_config()
493 518
494 upload_url = ('https://addons.mozilla.org/api/v3/addons/{}/' 519 upload_url = ('https://addons.mozilla.org/api/v3/addons/{}/'
495 'versions/{}/').format(self.extensionID, self.version) 520 'versions/{}/').format(self.extensionID, self.version)
496 521
497 with open(self.path, 'rb') as file: 522 with open(self.path, 'rb') as file:
498 data, content_type = urllib3.filepost.encode_multipart_formdata({ 523 data, content_type = urllib3.filepost.encode_multipart_formdata({
499 'upload': ( 524 'upload': (
500 os.path.basename(self.path), 525 os.path.basename(self.path),
501 file.read(), 526 file.read(),
502 'application/x-xpinstall' 527 'application/x-xpinstall'
503 ) 528 )
504 }) 529 })
505 530
506 request = self.generate_jwt_request( 531 request = self.generate_mozilla_jwt_request(
507 config.get('extensions', 'amo_key'), 532 config.get('extensions', 'amo_key'),
508 config.get('extensions', 'amo_secret'), 533 config.get('extensions', 'amo_secret'),
509 upload_url, 534 upload_url,
510 'PUT', 535 'PUT',
511 data, 536 data,
512 [('Content-Type', content_type)] 537 [('Content-Type', content_type)],
513 ) 538 )
514 539
515 try: 540 try:
516 urllib2.urlopen(request).close() 541 urllib2.urlopen(request).close()
517 except urllib2.HTTPError as e: 542 except urllib2.HTTPError as e:
518 try: 543 try:
519 logging.error(e.read()) 544 logging.error(e.read())
520 finally: 545 finally:
521 e.close() 546 e.close()
522 raise 547 raise
523 548
524 self.add_to_downloads_lockfile( 549 self.add_to_downloads_lockfile(
525 self.config.type, 550 self.config.type,
526 { 551 {
527 'buildtype': 'devbuild', 552 'buildtype': 'devbuild',
528 'app_id': self.extensionID, 553 'app_id': self.extensionID,
529 'version': self.version, 554 'version': self.version,
530 } 555 }
531 ) 556 )
532 os.remove(self.path) 557 os.remove(self.path)
533 558
534 def download_from_mozilla_addons(self, buildtype, version, app_id): 559 def download_from_mozilla_addons(self, buildtype, version, app_id):
535 config = get_config() 560 config = get_config()
536 iss = config.get('extensions', 'amo_key') 561 iss = config.get('extensions', 'amo_key')
537 secret = config.get('extensions', 'amo_secret') 562 secret = config.get('extensions', 'amo_secret')
538 563
539 url = ('https://addons.mozilla.org/api/v3/addons/{}/' 564 url = ('https://addons.mozilla.org/api/v3/addons/{}/'
540 'versions/{}/').format(app_id, version) 565 'versions/{}/').format(app_id, version)
541 566
542 request = self.generate_jwt_request(iss, secret, url, 'GET') 567 request = self.generate_mozilla_jwt_request(
568 iss, secret, url, 'GET',
569 )
543 response = json.load(urllib2.urlopen(request)) 570 response = json.load(urllib2.urlopen(request))
544 571
545 filename = '{}-{}.xpi'.format(self.basename, version) 572 filename = '{}-{}.xpi'.format(self.basename, version)
546 self.path = os.path.join( 573 self.path = os.path.join(
547 config.get('extensions', 'nightliesDirectory'), 574 config.get('extensions', 'nightliesDirectory'),
548 self.basename, 575 self.basename,
549 filename 576 filename
550 ) 577 )
551 578
552 necessary = ['passed_review', 'reviewed', 'processed', 'valid'] 579 necessary = ['passed_review', 'reviewed', 'processed', 'valid']
553 if all(response[x] for x in necessary): 580 if all(response[x] for x in necessary):
554 download_url = response['files'][0]['download_url'] 581 download_url = response['files'][0]['download_url']
555 checksum = response['files'][0]['hash'] 582 checksum = response['files'][0]['hash']
556 583
557 request = self.generate_jwt_request(iss, secret, download_url, 584 request = self.generate_mozilla_jwt_request(
558 'GET') 585 iss, secret, download_url, 'GET',
586 )
559 try: 587 try:
560 response = urllib2.urlopen(request) 588 response = urllib2.urlopen(request)
561 except urllib2.HTTPError as e: 589 except urllib2.HTTPError as e:
562 logging.error(e.read()) 590 logging.error(e.read())
563 591
564 # Verify the extension's integrity 592 # Verify the extension's integrity
565 file_content = response.read() 593 file_content = response.read()
566 sha256 = hashlib.sha256(file_content) 594 sha256 = hashlib.sha256(file_content)
567 returned_checksum = '{}:{}'.format(sha256.name, sha256.hexdigest()) 595 returned_checksum = '{}:{}'.format(sha256.name, sha256.hexdigest())
568 596
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after
639 request.get_method = lambda: 'POST' 667 request.get_method = lambda: 'POST'
640 request.add_header('Authorization', auth_token) 668 request.add_header('Authorization', auth_token)
641 request.add_header('x-goog-api-version', '2') 669 request.add_header('x-goog-api-version', '2')
642 request.add_header('Content-Length', '0') 670 request.add_header('Content-Length', '0')
643 671
644 response = json.load(opener.open(request)) 672 response = json.load(opener.open(request))
645 673
646 if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons e['status']): 674 if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons e['status']):
647 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']}) 675 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']})
648 676
677 def generate_certificate_token_request(self, url, private_key):
678 # Construct the token request according to
679 # https://docs.microsoft.com/en-us/azure/active-directory/develop/active -directory-certificate-credentials
680 hex_val = binascii.a2b_hex(self.config.thumbprint)
681 x5t = base64.urlsafe_b64encode(hex_val).decode()
682
683 key = RSA.importKey(private_key)
684
685 signed = self.sign_jwt(self.config.clientID, key, url,
686 self.azure_jwt_signature_fnc,
687 jwt_headers={'x5t': x5t})
688
689 # generate oauth parameters for login.microsoft.com
690 oauth_params = {
691 'grant_type': 'client_credentials',
692 'client_id': self.config.clientID,
693 'resource': 'https://graph.windows.net',
694 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-'
695 'type:jwt-bearer',
696 'client_assertion': signed,
697 }
698
699 request = urllib2.Request(url, urlencode(oauth_params))
700 request.get_method = lambda: 'POST'
701
702 return request
703
649 def get_windows_store_access_token(self): 704 def get_windows_store_access_token(self):
650 # use refresh token to obtain a valid access token 705 # use client certificate to obtain a valid access token
651 # https://docs.microsoft.com/en-us/azure/active-directory/active-directo ry-protocols-oauth-code#refreshing-the-access-tokens 706 url = 'https://login.microsoftonline.com/{}/oauth2/token'.format(
Vasily Kuznetsov 2018/04/17 17:43:54 Preexisting version of this was kind of a hack to
tlucas 2018/04/18 12:58:30 Done.
652 server = 'https://login.microsoftonline.com' 707 self.config.tenantID
653 token_path = '{}/{}/oauth2/token'.format(server, self.config.tenantID) 708 )
709
710 with open(self.config.privateKey, 'r') as fp:
711 private_key = fp.read()
654 712
655 opener = urllib2.build_opener(HTTPErrorBodyHandler) 713 opener = urllib2.build_opener(HTTPErrorBodyHandler)
656 post_data = urlencode([ 714 request = self.generate_certificate_token_request(url, private_key)
657 ('refresh_token', self.config.refreshToken), 715
658 ('client_id', self.config.clientID),
659 ('client_secret', self.config.clientSecret),
660 ('grant_type', 'refresh_token'),
661 ('resource', 'https://graph.windows.net')
662 ])
663 request = urllib2.Request(token_path, post_data)
664 with contextlib.closing(opener.open(request)) as response: 716 with contextlib.closing(opener.open(request)) as response:
665 data = json.load(response) 717 data = json.load(response)
666 auth_token = '{0[token_type]} {0[access_token]}'.format(data) 718 auth_token = '{0[token_type]} {0[access_token]}'.format(data)
667 719
668 return auth_token 720 return auth_token
669 721
670 def upload_appx_file_to_windows_store(self, file_upload_url): 722 def upload_appx_file_to_windows_store(self, file_upload_url):
671 # Add .appx file to a .zip file 723 # Add .appx file to a .zip file
672 zip_path = os.path.splitext(self.path)[0] + '.zip' 724 zip_path = os.path.splitext(self.path)[0] + '.zip'
673 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: 725 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
(...skipping 130 matching lines...) Expand 10 before | Expand all | Expand 10 after
804 856
805 # update nightlies config 857 # update nightlies config
806 self.config.latestRevision = self.revision 858 self.config.latestRevision = self.revision
807 859
808 if (self.config.type == 'gecko' and 860 if (self.config.type == 'gecko' and
809 self.config.galleryID and 861 self.config.galleryID and
810 get_config().has_option('extensions', 'amo_key')): 862 get_config().has_option('extensions', 'amo_key')):
811 self.uploadToMozillaAddons() 863 self.uploadToMozillaAddons()
812 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken: 864 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken:
813 self.uploadToChromeWebStore() 865 self.uploadToChromeWebStore()
814 elif self.config.type == 'edge' and self.config.clientID and self.co nfig.clientSecret and self.config.refreshToken and self.config.tenantID: 866 elif self.config.type == 'edge' and self.config.clientID and self.co nfig.tenantID and self.config.privateKey and self.config.thumbprint:
815 self.upload_to_windows_store() 867 self.upload_to_windows_store()
816 868
817 finally: 869 finally:
818 # clean up 870 # clean up
819 if self.tempdir: 871 if self.tempdir:
820 shutil.rmtree(self.tempdir, ignore_errors=True) 872 shutil.rmtree(self.tempdir, ignore_errors=True)
821 873
822 def download(self): 874 def download(self):
823 download_info = self.read_downloads_lockfile() 875 download_info = self.read_downloads_lockfile()
824 downloads = self.downloadable_repos.intersection(download_info.keys()) 876 downloads = self.downloadable_repos.intersection(download_info.keys())
(...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after
886 938
887 file = open(nightlyConfigFile, 'wb') 939 file = open(nightlyConfigFile, 'wb')
888 nightlyConfig.write(file) 940 nightlyConfig.write(file)
889 941
890 942
891 if __name__ == '__main__': 943 if __name__ == '__main__':
892 parser = argparse.ArgumentParser() 944 parser = argparse.ArgumentParser()
893 parser.add_argument('--download', action='store_true', default=False) 945 parser.add_argument('--download', action='store_true', default=False)
894 args = parser.parse_args() 946 args = parser.parse_args()
895 main(args.download) 947 main(args.download)
OLDNEW
« no previous file with comments | « no previous file | sitescripts/extensions/utils.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld