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: Created April 13, 2018, 12:56 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
30 import datetime
29 import hashlib 31 import hashlib
30 import hmac 32 import hmac
31 import json 33 import json
32 import logging 34 import logging
33 import os 35 import os
34 import pipes 36 import pipes
35 import random 37 import random
36 import shutil 38 import shutil
37 import struct 39 import struct
38 import subprocess 40 import subprocess
39 import sys 41 import sys
40 import tempfile 42 import tempfile
41 import time 43 import time
44 import uuid
42 from urllib import urlencode 45 from urllib import urlencode
43 import urllib2 46 import urllib2
44 import urlparse 47 import urlparse
45 import zipfile 48 import zipfile
46 import contextlib 49 import contextlib
47 50
51 from Crypto.PublicKey import RSA
52 from Crypto.Signature import PKCS1_v1_5
53 import Crypto.Hash.SHA256
54
48 from xml.dom.minidom import parse as parseXml 55 from xml.dom.minidom import parse as parseXml
49 56
50 from sitescripts.extensions.utils import ( 57 from sitescripts.extensions.utils import (
51 compareVersions, Configuration, 58 compareVersions, Configuration,
52 writeAndroidUpdateManifest 59 writeAndroidUpdateManifest
53 ) 60 )
54 from sitescripts.utils import get_config, get_template 61 from sitescripts.utils import get_config, get_template
55 62
56 MAX_BUILDS = 50 63 MAX_BUILDS = 50
57 64
(...skipping 285 matching lines...) Expand 10 before | Expand all | Expand 10 after
343 if os.path.exists(self.path): 350 if os.path.exists(self.path):
344 os.remove(self.path) 351 os.remove(self.path)
345 raise 352 raise
346 else: 353 else:
347 env = os.environ 354 env = os.environ
348 spiderMonkeyBinary = self.config.spiderMonkeyBinary 355 spiderMonkeyBinary = self.config.spiderMonkeyBinary
349 if spiderMonkeyBinary: 356 if spiderMonkeyBinary:
350 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary) 357 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary)
351 358
352 command = [os.path.join(self.tempdir, 'build.py')] 359 command = [os.path.join(self.tempdir, 'build.py')]
353 if self.config.type == 'safari': 360 if self.config.type in {'safari', 'edge'}:
tlucas 2018/04/13 13:06:04 The branch 'edge' is built from still uses the old
Sebastian Noack 2018/04/14 02:47:30 I would rather see a dependency update landing in
tlucas 2018/04/14 08:55:46 Yeah, i agree. @Ollie - what do you think about th
Sebastian Noack 2018/04/14 09:40:54 Alternatively, we could add a hack to build.py:
Oleksandr 2018/04/16 04:37:07 I would rather merge `master` into `edge` first. T
Sebastian Noack 2018/04/16 05:45:50 Wouldn't this rather be a reason to go with my sug
Sebastian Noack 2018/04/16 05:47:19 Sorry, I meant "edge" (not "next").
tlucas 2018/04/16 10:06:07 All of the above does IMHO encourage a workaround
tlucas 2018/04/16 10:25:14 https://codereview.adblockplus.org/29753557/ https
tlucas 2018/04/16 14:50:09 Done.
354 command.extend(['-t', self.config.type, 'build']) 361 command.extend(['-t', self.config.type, 'build'])
355 else: 362 else:
356 command.extend(['build', '-t', self.config.type]) 363 command.extend(['build', '-t', self.config.type])
357 command.extend(['-b', self.buildNum]) 364 command.extend(['-b', self.buildNum])
358 365
359 if self.config.type not in {'gecko', 'edge'}: 366 if self.config.type not in {'gecko', 'edge'}:
360 command.extend(['-k', self.config.keyFile]) 367 command.extend(['-k', self.config.keyFile])
361 command.append(self.path) 368 command.append(self.path)
362 subprocess.check_call(command, env=env) 369 subprocess.check_call(command, env=env)
363 370
(...skipping 278 matching lines...) Expand 10 before | Expand all | Expand 10 after
642 request.get_method = lambda: 'POST' 649 request.get_method = lambda: 'POST'
643 request.add_header('Authorization', auth_token) 650 request.add_header('Authorization', auth_token)
644 request.add_header('x-goog-api-version', '2') 651 request.add_header('x-goog-api-version', '2')
645 request.add_header('Content-Length', '0') 652 request.add_header('Content-Length', '0')
646 653
647 response = json.load(opener.open(request)) 654 response = json.load(opener.open(request))
648 655
649 if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons e['status']): 656 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']}) 657 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']})
651 658
659 def generate_certificate_token_request(self, url, private_key):
660 # Construct the token request according to
661 # https://docs.microsoft.com/en-us/azure/active-directory/develop/active -directory-certificate-credentials
662 def base64url_encode(data):
663 return base64.urlsafe_b64encode(data).replace(b'=', b'')
Sebastian Noack 2018/04/14 02:47:30 How about .rstrip(b'=')?
tlucas 2018/04/14 08:55:46 see below
tlucas 2018/04/16 14:50:09 FWIW, there's no stripping / replacing done anymor
664
665 segments = []
666
667 hex_val = binascii.a2b_hex(self.config.thumbprint)
668 x5t = base64.urlsafe_b64encode(hex_val).decode()
669
670 now = datetime.datetime.now()
Sebastian Noack 2018/04/14 02:47:30 It seems you are relying on the fact that our serv
tlucas 2018/04/14 08:55:46 I'm actually thinking about refactoring parts of t
tlucas 2018/04/16 14:50:09 Done, almost: mktime() expects a time_struct in lo
Sebastian Noack 2018/04/16 16:13:29 You are right. But time.time() gives the same resu
tlucas 2018/04/16 16:50:24 Done.
671 minutes = datetime.timedelta(0, 0, 0, 0, 10)
672 expires = now + minutes
673
674 # generate the full jwt body
675 jwt_payload = {
676 'aud': url,
677 'iss': self.config.clientID,
678 'sub': self.config.clientID,
679 'nbf': int(time.mktime(now.timetuple())),
680 'exp': int(time.mktime(expires.timetuple())),
681 'jti': str(uuid.uuid4()),
682 }
683
684 jwt_headers = {'typ': 'JWT', 'alg': 'RS256', 'x5t': x5t}
685
686 # sign the jwt body with the given private key
687 key = RSA.importKey(private_key)
688
689 segments.append(base64url_encode(json.dumps(jwt_headers)))
690 segments.append(base64url_encode(json.dumps(jwt_payload)))
Sebastian Noack 2018/04/14 02:47:30 Since we don't append to segments above, how about
tlucas 2018/04/14 08:55:46 Acknowledged.
tlucas 2018/04/16 14:50:09 Done.
691
692 body = b'.'.join(segments)
693 signature = PKCS1_v1_5.new(key).sign(Crypto.Hash.SHA256.new(body))
694
695 segments.append(base64url_encode(signature))
696 signed_jwt = b'.'.join(segments)
697
698 # generate oauth parameters for login.microsoft.com
699 oauth_params = {
700 'grant_type': 'client_credentials',
701 'client_id': self.config.clientID,
702 'resource': 'https://graph.windows.net',
703 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-'
704 'type:jwt-bearer',
705 'client_assertion': signed_jwt,
706 }
707
708 request = urllib2.Request(url, urlencode(oauth_params))
709 request.get_method = lambda: 'POST'
710
711 return request
712
652 def get_windows_store_access_token(self): 713 def get_windows_store_access_token(self):
653 # use refresh token to obtain a valid access token 714 # 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 715 url = 'https://login.microsoftonline.com/{}/oauth2/token'.format(
655 server = 'https://login.microsoftonline.com' 716 self.config.tenantID
656 token_path = '{}/{}/oauth2/token'.format(server, self.config.tenantID) 717 )
718
719 with open(self.config.privateKey, 'r') as fp:
720 private_key = fp.read()
657 721
658 opener = urllib2.build_opener(HTTPErrorBodyHandler) 722 opener = urllib2.build_opener(HTTPErrorBodyHandler)
659 post_data = urlencode([ 723 request = self.generate_certificate_token_request(url, private_key)
660 ('refresh_token', self.config.refreshToken), 724
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: 725 with contextlib.closing(opener.open(request)) as response:
668 data = json.load(response) 726 data = json.load(response)
669 auth_token = '{0[token_type]} {0[access_token]}'.format(data) 727 auth_token = '{0[token_type]} {0[access_token]}'.format(data)
670 728
671 return auth_token 729 return auth_token
672 730
673 def upload_appx_file_to_windows_store(self, file_upload_url): 731 def upload_appx_file_to_windows_store(self, file_upload_url):
674 # Add .appx file to a .zip file 732 # Add .appx file to a .zip file
675 zip_path = os.path.splitext(self.path)[0] + '.zip' 733 zip_path = os.path.splitext(self.path)[0] + '.zip'
676 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: 734 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
(...skipping 130 matching lines...) Expand 10 before | Expand all | Expand 10 after
807 865
808 # update nightlies config 866 # update nightlies config
809 self.config.latestRevision = self.revision 867 self.config.latestRevision = self.revision
810 868
811 if (self.config.type == 'gecko' and 869 if (self.config.type == 'gecko' and
812 self.config.galleryID and 870 self.config.galleryID and
813 get_config().has_option('extensions', 'amo_key')): 871 get_config().has_option('extensions', 'amo_key')):
814 self.uploadToMozillaAddons() 872 self.uploadToMozillaAddons()
815 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken: 873 elif self.config.type == 'chrome' and self.config.clientID and self. config.clientSecret and self.config.refreshToken:
816 self.uploadToChromeWebStore() 874 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: 875 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() 876 self.upload_to_windows_store()
819 877
820 finally: 878 finally:
821 # clean up 879 # clean up
822 if self.tempdir: 880 if self.tempdir:
823 shutil.rmtree(self.tempdir, ignore_errors=True) 881 shutil.rmtree(self.tempdir, ignore_errors=True)
824 882
825 def download(self): 883 def download(self):
826 download_info = self.read_downloads_lockfile() 884 download_info = self.read_downloads_lockfile()
827 downloads = self.downloadable_repos.intersection(download_info.keys()) 885 downloads = self.downloadable_repos.intersection(download_info.keys())
(...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after
889 947
890 file = open(nightlyConfigFile, 'wb') 948 file = open(nightlyConfigFile, 'wb')
891 nightlyConfig.write(file) 949 nightlyConfig.write(file)
892 950
893 951
894 if __name__ == '__main__': 952 if __name__ == '__main__':
895 parser = argparse.ArgumentParser() 953 parser = argparse.ArgumentParser()
896 parser.add_argument('--download', action='store_true', default=False) 954 parser.add_argument('--download', action='store_true', default=False)
897 args = parser.parse_args() 955 args = parser.parse_args()
898 main(args.download) 956 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