Left: | ||
Right: |
LEFT | RIGHT |
---|---|
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 |
(...skipping 27 matching lines...) Expand all Loading... | |
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 import uuid |
43 from urllib import urlencode | 43 from urllib import urlencode |
44 import urllib2 | 44 import urllib2 |
45 import urlparse | 45 import urlparse |
46 import zipfile | 46 import zipfile |
47 import contextlib | 47 import contextlib |
48 from xml.dom.minidom import parse as parseXml | |
48 | 49 |
49 from Crypto.PublicKey import RSA | 50 from Crypto.PublicKey import RSA |
50 from Crypto.Signature import PKCS1_v1_5 | 51 from Crypto.Signature import PKCS1_v1_5 |
51 import Crypto.Hash.SHA256 | 52 import Crypto.Hash.SHA256 |
52 | 53 |
53 from xml.dom.minidom import parse as parseXml | |
54 | |
55 from sitescripts.extensions.utils import ( | 54 from sitescripts.extensions.utils import ( |
56 compareVersions, Configuration, | 55 compareVersions, Configuration, |
57 writeAndroidUpdateManifest | 56 writeAndroidUpdateManifest, |
58 ) | 57 ) |
59 from sitescripts.utils import get_config, get_template | 58 from sitescripts.utils import get_config, get_template |
60 | 59 |
61 MAX_BUILDS = 50 | 60 MAX_BUILDS = 50 |
62 | 61 |
63 | 62 |
64 # Google and Microsoft APIs use HTTP error codes with error message in | 63 # Google and Microsoft APIs use HTTP error codes with error message in |
65 # body. So we add the response body to the HTTPError to get more | 64 # body. So we add the response body to the HTTPError to get more |
66 # meaningful error messages. | 65 # meaningful error messages. |
67 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler): | 66 class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler): |
(...skipping 28 matching lines...) Expand all Loading... | |
96 | 95 |
97 def hasChanges(self): | 96 def hasChanges(self): |
98 return self.revision != self.previousRevision | 97 return self.revision != self.previousRevision |
99 | 98 |
100 def getCurrentRevision(self): | 99 def getCurrentRevision(self): |
101 """ | 100 """ |
102 retrieves the current revision ID from the repository | 101 retrieves the current revision ID from the repository |
103 """ | 102 """ |
104 command = [ | 103 command = [ |
105 'hg', 'id', '-i', '-r', self.config.revision, '--config', | 104 'hg', 'id', '-i', '-r', self.config.revision, '--config', |
106 'defaults.id=', self.config.repository | 105 'defaults.id=', self.config.repository, |
107 ] | 106 ] |
108 return subprocess.check_output(command).strip() | 107 return subprocess.check_output(command).strip() |
109 | 108 |
110 def getCurrentBuild(self): | 109 def getCurrentBuild(self): |
111 """ | 110 """ |
112 calculates the (typically numerical) build ID for the current build | 111 calculates the (typically numerical) build ID for the current build |
113 """ | 112 """ |
114 command = ['hg', 'id', '-n', '--config', 'defaults.id=', self.tempdir] | 113 command = ['hg', 'id', '-n', '--config', 'defaults.id=', self.tempdir] |
115 build = subprocess.check_output(command).strip() | 114 build = subprocess.check_output(command).strip() |
116 return build | 115 return build |
117 | 116 |
118 def getChanges(self): | 117 def getChanges(self): |
119 """ | 118 """ |
120 retrieve changes between the current and previous ("first") revision | 119 retrieve changes between the current and previous ("first") revision |
121 """ | 120 """ |
122 command = [ | 121 command = [ |
123 'hg', 'log', '-R', self.tempdir, '-r', | 122 'hg', 'log', '-R', self.tempdir, '-r', |
124 'reverse(ancestors({}))'.format(self.config.revision), '-l', '50', | 123 'reverse(ancestors({}))'.format(self.config.revision), '-l', '50', |
125 '--encoding', 'utf-8', '--template', | 124 '--encoding', 'utf-8', '--template', |
126 '{date|isodate}\\0{author|person}\\0{rev}\\0{desc}\\0\\0', | 125 '{date|isodate}\\0{author|person}\\0{rev}\\0{desc}\\0\\0', |
127 '--config', 'defaults.log=' | 126 '--config', 'defaults.log=', |
128 ] | 127 ] |
129 result = subprocess.check_output(command).decode('utf-8') | 128 result = subprocess.check_output(command).decode('utf-8') |
130 | 129 |
131 for change in result.split('\x00\x00'): | 130 for change in result.split('\x00\x00'): |
132 if change: | 131 if change: |
133 date, author, revision, description = change.split('\x00') | 132 date, author, revision, description = change.split('\x00') |
134 yield {'date': date, 'author': author, 'revision': revision, 'de scription': description} | 133 yield {'date': date, 'author': author, 'revision': revision, 'de scription': description} |
135 | 134 |
136 def copyRepository(self): | 135 def copyRepository(self): |
137 """ | 136 """ |
(...skipping 141 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
279 os.makedirs(baseDir) | 278 os.makedirs(baseDir) |
280 | 279 |
281 # ABP for Android used to have its own update manifest format. We need t o | 280 # ABP for Android used to have its own update manifest format. We need t o |
282 # generate both that and the new one in the libadblockplus format as lon g | 281 # generate both that and the new one in the libadblockplus format as lon g |
283 # as a significant amount of users is on an old version. | 282 # as a significant amount of users is on an old version. |
284 if self.config.type == 'android': | 283 if self.config.type == 'android': |
285 newManifestPath = os.path.join(baseDir, 'update.json') | 284 newManifestPath = os.path.join(baseDir, 'update.json') |
286 writeAndroidUpdateManifest(newManifestPath, [{ | 285 writeAndroidUpdateManifest(newManifestPath, [{ |
287 'basename': self.basename, | 286 'basename': self.basename, |
288 'version': self.version, | 287 'version': self.version, |
289 'updateURL': self.updateURL | 288 'updateURL': self.updateURL, |
290 }]) | 289 }]) |
291 | 290 |
292 template = get_template(get_config().get('extensions', templateName), | 291 template = get_template(get_config().get('extensions', templateName), |
293 autoescape=autoescape) | 292 autoescape=autoescape) |
294 template.stream({'extensions': [self]}).dump(manifestPath) | 293 template.stream({'extensions': [self]}).dump(manifestPath) |
295 | 294 |
296 def writeIEUpdateManifest(self, versions): | 295 def writeIEUpdateManifest(self, versions): |
297 """ | 296 """ |
298 Writes update.json file for the latest IE build | 297 Writes update.json file for the latest IE build |
299 """ | 298 """ |
300 if len(versions) == 0: | 299 if len(versions) == 0: |
301 return | 300 return |
302 | 301 |
303 version = versions[0] | 302 version = versions[0] |
304 packageName = self.basename + '-' + version + self.config.packageSuffix | 303 packageName = self.basename + '-' + version + self.config.packageSuffix |
305 updateURL = urlparse.urljoin(self.config.nightliesURL, self.basename + ' /' + packageName + '?update') | 304 updateURL = urlparse.urljoin(self.config.nightliesURL, self.basename + ' /' + packageName + '?update') |
306 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) | 305 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) |
307 manifestPath = os.path.join(baseDir, 'update.json') | 306 manifestPath = os.path.join(baseDir, 'update.json') |
308 | 307 |
309 from sitescripts.extensions.utils import writeIEUpdateManifest as doWrit e | 308 from sitescripts.extensions.utils import writeIEUpdateManifest as doWrit e |
310 doWrite(manifestPath, [{ | 309 doWrite(manifestPath, [{ |
311 'basename': self.basename, | 310 'basename': self.basename, |
312 'version': version, | 311 'version': version, |
313 'updateURL': updateURL | 312 'updateURL': updateURL, |
314 }]) | 313 }]) |
315 | 314 |
316 for suffix in ['-x86.msi', '-x64.msi', '-gpo-x86.msi', '-gpo-x64.msi']: | 315 for suffix in ['-x86.msi', '-x64.msi', '-gpo-x86.msi', '-gpo-x64.msi']: |
317 linkPath = os.path.join(baseDir, '00latest%s' % suffix) | 316 linkPath = os.path.join(baseDir, '00latest%s' % suffix) |
318 outputPath = os.path.join(baseDir, self.basename + '-' + version + s uffix) | 317 outputPath = os.path.join(baseDir, self.basename + '-' + version + s uffix) |
319 self.symlink_or_copy(outputPath, linkPath) | 318 self.symlink_or_copy(outputPath, linkPath) |
320 | 319 |
321 def build(self): | 320 def build(self): |
322 """ | 321 """ |
323 run the build command in the tempdir | 322 run the build command in the tempdir |
324 """ | 323 """ |
325 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) | 324 baseDir = os.path.join(self.config.nightliesDirectory, self.basename) |
326 if not os.path.exists(baseDir): | 325 if not os.path.exists(baseDir): |
327 os.makedirs(baseDir) | 326 os.makedirs(baseDir) |
328 outputFile = '%s-%s%s' % (self.basename, self.version, self.config.packa geSuffix) | 327 outputFile = '%s-%s%s' % (self.basename, self.version, self.config.packa geSuffix) |
329 self.path = os.path.join(baseDir, outputFile) | 328 self.path = os.path.join(baseDir, outputFile) |
330 self.updateURL = urlparse.urljoin(self.config.nightliesURL, self.basenam e + '/' + outputFile + '?update') | 329 self.updateURL = urlparse.urljoin(self.config.nightliesURL, self.basenam e + '/' + outputFile + '?update') |
331 | 330 |
332 if self.config.type == 'android': | 331 if self.config.type == 'android': |
333 apkFile = open(self.path, 'wb') | 332 apkFile = open(self.path, 'wb') |
334 | 333 |
335 try: | 334 try: |
336 try: | 335 try: |
337 port = get_config().get('extensions', 'androidBuildPort') | 336 port = get_config().get('extensions', 'androidBuildPort') |
338 except ConfigParser.NoOptionError: | 337 except ConfigParser.NoOptionError: |
339 port = '22' | 338 port = '22' |
340 command = ['ssh', '-p', port, get_config().get('extensions', 'an droidBuildHost')] | 339 command = ['ssh', '-p', port, get_config().get('extensions', 'an droidBuildHost')] |
341 command.extend(map(pipes.quote, [ | 340 command.extend(map(pipes.quote, [ |
342 '/home/android/bin/makedebugbuild.py', '--revision', | 341 '/home/android/bin/makedebugbuild.py', '--revision', |
343 self.buildNum, '--version', self.version, '--stdout' | 342 self.buildNum, '--version', self.version, '--stdout', |
344 ])) | 343 ])) |
345 subprocess.check_call(command, stdout=apkFile, close_fds=True) | 344 subprocess.check_call(command, stdout=apkFile, close_fds=True) |
346 except: | 345 except: |
347 # clear broken output if any | 346 # clear broken output if any |
348 if os.path.exists(self.path): | 347 if os.path.exists(self.path): |
349 os.remove(self.path) | 348 os.remove(self.path) |
350 raise | 349 raise |
351 else: | 350 else: |
352 env = os.environ | 351 env = os.environ |
353 spiderMonkeyBinary = self.config.spiderMonkeyBinary | 352 spiderMonkeyBinary = self.config.spiderMonkeyBinary |
354 if spiderMonkeyBinary: | 353 if spiderMonkeyBinary: |
355 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary) | 354 env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary) |
356 | 355 |
357 command = [os.path.join(self.tempdir, 'build.py')] | 356 command = [os.path.join(self.tempdir, 'build.py')] |
358 command.extend(['build', '-t', self.config.type, '-b', | 357 command.extend(['build', '-t', self.config.type, '-b', |
359 self.buildNum]) | 358 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.
| |
360 | 359 |
361 if self.config.type not in {'gecko', 'edge'}: | 360 if self.config.type not in {'gecko', 'edge'}: |
362 command.extend(['-k', self.config.keyFile]) | 361 command.extend(['-k', self.config.keyFile]) |
363 command.append(self.path) | 362 command.append(self.path) |
364 subprocess.check_call(command, env=env) | 363 subprocess.check_call(command, env=env) |
365 | 364 |
366 if not os.path.exists(self.path): | 365 if not os.path.exists(self.path): |
367 raise Exception("Build failed, output file hasn't been created") | 366 raise Exception("Build failed, output file hasn't been created") |
368 | 367 |
369 if self.config.type not in self.downloadable_repos: | 368 if self.config.type not in self.downloadable_repos: |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
407 packageFile = self.basename + '-' + version + self.config.packageSuf fix | 406 packageFile = self.basename + '-' + version + self.config.packageSuf fix |
408 changelogFile = self.basename + '-' + version + '.changelog.xhtml' | 407 changelogFile = self.basename + '-' + version + '.changelog.xhtml' |
409 if not os.path.exists(os.path.join(baseDir, packageFile)): | 408 if not os.path.exists(os.path.join(baseDir, packageFile)): |
410 # Oops | 409 # Oops |
411 continue | 410 continue |
412 | 411 |
413 link = { | 412 link = { |
414 'version': version, | 413 'version': version, |
415 'download': packageFile, | 414 'download': packageFile, |
416 'mtime': os.path.getmtime(os.path.join(baseDir, packageFile)), | 415 'mtime': os.path.getmtime(os.path.join(baseDir, packageFile)), |
417 'size': os.path.getsize(os.path.join(baseDir, packageFile)) | 416 'size': os.path.getsize(os.path.join(baseDir, packageFile)), |
418 } | 417 } |
419 if os.path.exists(os.path.join(baseDir, changelogFile)): | 418 if os.path.exists(os.path.join(baseDir, changelogFile)): |
420 link['changelog'] = changelogFile | 419 link['changelog'] = changelogFile |
421 links.append(link) | 420 links.append(link) |
422 template = get_template(get_config().get('extensions', 'nightlyIndexPage ')) | 421 template = get_template(get_config().get('extensions', 'nightlyIndexPage ')) |
423 template.stream({'config': self.config, 'links': links}).dump(outputPath ) | 422 template.stream({'config': self.config, 'links': links}).dump(outputPath ) |
424 | 423 |
425 def read_downloads_lockfile(self): | 424 def read_downloads_lockfile(self): |
426 path = get_config().get('extensions', 'downloadLockFile') | 425 path = get_config().get('extensions', 'downloadLockFile') |
427 try: | 426 try: |
(...skipping 25 matching lines...) Expand all Loading... | |
453 for i, entry in enumerate(current[platform]): | 452 for i, entry in enumerate(current[platform]): |
454 if entry[filter_key] == filter_value: | 453 if entry[filter_key] == filter_value: |
455 del current[platform][i] | 454 del current[platform][i] |
456 if len(current[platform]) == 0: | 455 if len(current[platform]) == 0: |
457 del current[platform] | 456 del current[platform] |
458 except KeyError: | 457 except KeyError: |
459 pass | 458 pass |
460 self.write_downloads_lockfile(current) | 459 self.write_downloads_lockfile(current) |
461 | 460 |
462 def azure_jwt_signature_fnc(self): | 461 def azure_jwt_signature_fnc(self): |
463 return ('RS256', | 462 return ( |
464 lambda s, m: PKCS1_v1_5.new(s).sign(Crypto.Hash.SHA256.new(m))) | 463 'RS256', |
464 lambda s, m: PKCS1_v1_5.new(s).sign(Crypto.Hash.SHA256.new(m)), | |
465 ) | |
465 | 466 |
466 def mozilla_jwt_signature_fnc(self): | 467 def mozilla_jwt_signature_fnc(self): |
467 return ( | 468 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 'HS256', |
469 lambda s, m: hmac.new(s, msg=m, digestmod=hashlib.sha256).digest() | 470 lambda s, m: hmac.new(s, msg=m, digestmod=hashlib.sha256).digest(), |
470 ) | 471 ) |
471 | 472 |
472 def sign_jwt(self, issuer, secret, url, signature_fnc, jwt_headers={}): | 473 def sign_jwt(self, issuer, secret, url, signature_fnc, jwt_headers={}): |
473 alg, fnc = signature_fnc() | 474 alg, fnc = signature_fnc() |
474 | 475 |
475 header = {'typ': 'JWT'} | 476 header = {'typ': 'JWT'} |
476 header.update(jwt_headers) | 477 header.update(jwt_headers) |
477 header.update({'alg': alg}) | 478 header.update({'alg': alg}) |
478 | 479 |
479 issued = int(time.mktime(time.localtime())) | 480 issued = int(time.time()) |
480 expires = issued + 60 | 481 expires = issued + 60 |
481 | 482 |
482 payload = { | 483 payload = { |
483 'aud': url, | 484 'aud': url, |
484 'iss': issuer, | 485 'iss': issuer, |
485 'sub': issuer, | 486 'sub': issuer, |
486 'jti': str(uuid.uuid4()), | 487 'jti': str(uuid.uuid4()), |
487 'iat': issued, | 488 'iat': issued, |
488 'nbf': issued, | 489 'nbf': issued, |
489 'exp': expires, | 490 'exp': expires, |
(...skipping 25 matching lines...) Expand all Loading... | |
515 config = get_config() | 516 config = get_config() |
516 | 517 |
517 upload_url = ('https://addons.mozilla.org/api/v3/addons/{}/' | 518 upload_url = ('https://addons.mozilla.org/api/v3/addons/{}/' |
518 'versions/{}/').format(self.extensionID, self.version) | 519 'versions/{}/').format(self.extensionID, self.version) |
519 | 520 |
520 with open(self.path, 'rb') as file: | 521 with open(self.path, 'rb') as file: |
521 data, content_type = urllib3.filepost.encode_multipart_formdata({ | 522 data, content_type = urllib3.filepost.encode_multipart_formdata({ |
522 'upload': ( | 523 'upload': ( |
523 os.path.basename(self.path), | 524 os.path.basename(self.path), |
524 file.read(), | 525 file.read(), |
525 'application/x-xpinstall' | 526 'application/x-xpinstall', |
526 ) | 527 ), |
527 }) | 528 }) |
528 | 529 |
529 request = self.generate_mozilla_jwt_request( | 530 request = self.generate_mozilla_jwt_request( |
530 config.get('extensions', 'amo_key'), | 531 config.get('extensions', 'amo_key'), |
531 config.get('extensions', 'amo_secret'), | 532 config.get('extensions', 'amo_secret'), |
532 upload_url, | 533 upload_url, |
533 'PUT', | 534 'PUT', |
534 data, | 535 data, |
535 [('Content-Type', content_type)], | 536 [('Content-Type', content_type)], |
536 ) | 537 ) |
537 | 538 |
538 try: | 539 try: |
539 urllib2.urlopen(request).close() | 540 urllib2.urlopen(request).close() |
540 except urllib2.HTTPError as e: | 541 except urllib2.HTTPError as e: |
541 try: | 542 try: |
542 logging.error(e.read()) | 543 logging.error(e.read()) |
543 finally: | 544 finally: |
544 e.close() | 545 e.close() |
545 raise | 546 raise |
546 | 547 |
547 self.add_to_downloads_lockfile( | 548 self.add_to_downloads_lockfile( |
548 self.config.type, | 549 self.config.type, |
549 { | 550 { |
550 'buildtype': 'devbuild', | 551 'buildtype': 'devbuild', |
551 'app_id': self.extensionID, | 552 'app_id': self.extensionID, |
552 'version': self.version, | 553 'version': self.version, |
553 } | 554 }, |
554 ) | 555 ) |
555 os.remove(self.path) | 556 os.remove(self.path) |
556 | 557 |
557 def download_from_mozilla_addons(self, buildtype, version, app_id): | 558 def download_from_mozilla_addons(self, buildtype, version, app_id): |
558 config = get_config() | 559 config = get_config() |
559 iss = config.get('extensions', 'amo_key') | 560 iss = config.get('extensions', 'amo_key') |
560 secret = config.get('extensions', 'amo_secret') | 561 secret = config.get('extensions', 'amo_secret') |
561 | 562 |
562 url = ('https://addons.mozilla.org/api/v3/addons/{}/' | 563 url = ('https://addons.mozilla.org/api/v3/addons/{}/' |
563 'versions/{}/').format(app_id, version) | 564 'versions/{}/').format(app_id, version) |
564 | 565 |
565 request = self.generate_mozilla_jwt_request( | 566 request = self.generate_mozilla_jwt_request( |
566 iss, secret, url, 'GET', | 567 iss, secret, url, 'GET', |
567 ) | 568 ) |
568 response = json.load(urllib2.urlopen(request)) | 569 response = json.load(urllib2.urlopen(request)) |
569 | 570 |
570 filename = '{}-{}.xpi'.format(self.basename, version) | 571 filename = '{}-{}.xpi'.format(self.basename, version) |
571 self.path = os.path.join( | 572 self.path = os.path.join( |
572 config.get('extensions', 'nightliesDirectory'), | 573 config.get('extensions', 'nightliesDirectory'), |
573 self.basename, | 574 self.basename, |
574 filename | 575 filename, |
575 ) | 576 ) |
576 | 577 |
577 necessary = ['passed_review', 'reviewed', 'processed', 'valid'] | 578 necessary = ['passed_review', 'reviewed', 'processed', 'valid'] |
578 if all(response[x] for x in necessary): | 579 if all(response[x] for x in necessary): |
579 download_url = response['files'][0]['download_url'] | 580 download_url = response['files'][0]['download_url'] |
580 checksum = response['files'][0]['hash'] | 581 checksum = response['files'][0]['hash'] |
581 | 582 |
582 request = self.generate_mozilla_jwt_request( | 583 request = self.generate_mozilla_jwt_request( |
583 iss, secret, download_url, 'GET', | 584 iss, secret, download_url, 'GET', |
584 ) | 585 ) |
(...skipping 10 matching lines...) Expand all Loading... | |
595 if returned_checksum != checksum: | 596 if returned_checksum != checksum: |
596 logging.error('Checksum could not be verified: {} vs {}' | 597 logging.error('Checksum could not be verified: {} vs {}' |
597 ''.format(checksum, returned_checksum)) | 598 ''.format(checksum, returned_checksum)) |
598 | 599 |
599 with open(self.path, 'w') as fp: | 600 with open(self.path, 'w') as fp: |
600 fp.write(file_content) | 601 fp.write(file_content) |
601 | 602 |
602 self.update_link = os.path.join( | 603 self.update_link = os.path.join( |
603 config.get('extensions', 'nightliesURL'), | 604 config.get('extensions', 'nightliesURL'), |
604 self.basename, | 605 self.basename, |
605 filename | 606 filename, |
606 ) | 607 ) |
607 | 608 |
608 self.remove_from_downloads_lockfile(self.config.type, | 609 self.remove_from_downloads_lockfile(self.config.type, |
609 'version', | 610 'version', |
610 version) | 611 version) |
611 elif not response['passed_review'] or not response['valid']: | 612 elif not response['passed_review'] or not response['valid']: |
612 # When the review failed for any reason, we want to know about it | 613 # When the review failed for any reason, we want to know about it |
613 logging.error(json.dumps(response, indent=4)) | 614 logging.error(json.dumps(response, indent=4)) |
614 self.remove_from_downloads_lockfile(self.config.type, | 615 self.remove_from_downloads_lockfile(self.config.type, |
615 'version', | 616 'version', |
616 version) | 617 version) |
617 | 618 |
618 def uploadToChromeWebStore(self): | 619 def uploadToChromeWebStore(self): |
619 | 620 |
620 opener = urllib2.build_opener(HTTPErrorBodyHandler) | 621 opener = urllib2.build_opener(HTTPErrorBodyHandler) |
621 | 622 |
622 # use refresh token to obtain a valid access token | 623 # use refresh token to obtain a valid access token |
623 # https://developers.google.com/accounts/docs/OAuth2WebServer#refresh | 624 # https://developers.google.com/accounts/docs/OAuth2WebServer#refresh |
624 | 625 |
625 response = json.load(opener.open( | 626 response = json.load(opener.open( |
626 'https://accounts.google.com/o/oauth2/token', | 627 'https://accounts.google.com/o/oauth2/token', |
627 | 628 |
628 urlencode([ | 629 urlencode([ |
629 ('refresh_token', self.config.refreshToken), | 630 ('refresh_token', self.config.refreshToken), |
630 ('client_id', self.config.clientID), | 631 ('client_id', self.config.clientID), |
631 ('client_secret', self.config.clientSecret), | 632 ('client_secret', self.config.clientSecret), |
632 ('grant_type', 'refresh_token'), | 633 ('grant_type', 'refresh_token'), |
633 ]) | 634 ]), |
634 )) | 635 )) |
635 | 636 |
636 auth_token = '%s %s' % (response['token_type'], response['access_token'] ) | 637 auth_token = '%s %s' % (response['token_type'], response['access_token'] ) |
637 | 638 |
638 # upload a new version with the Chrome Web Store API | 639 # upload a new version with the Chrome Web Store API |
639 # https://developer.chrome.com/webstore/using_webstore_api#uploadexisitn g | 640 # https://developer.chrome.com/webstore/using_webstore_api#uploadexisitn g |
640 | 641 |
641 request = urllib2.Request('https://www.googleapis.com/upload/chromewebst ore/v1.1/items/' + self.config.devbuildGalleryID) | 642 request = urllib2.Request('https://www.googleapis.com/upload/chromewebst ore/v1.1/items/' + self.config.devbuildGalleryID) |
642 request.get_method = lambda: 'PUT' | 643 request.get_method = lambda: 'PUT' |
643 request.add_header('Authorization', auth_token) | 644 request.add_header('Authorization', auth_token) |
(...skipping 26 matching lines...) Expand all Loading... | |
670 response = json.load(opener.open(request)) | 671 response = json.load(opener.open(request)) |
671 | 672 |
672 if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons e['status']): | 673 if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in respons e['status']): |
673 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']}) | 674 raise Exception({'status': response['status'], 'statusDetail': respo nse['statusDetail']}) |
674 | 675 |
675 def generate_certificate_token_request(self, url, private_key): | 676 def generate_certificate_token_request(self, url, private_key): |
676 # Construct the token request according to | 677 # Construct the token request according to |
677 # https://docs.microsoft.com/en-us/azure/active-directory/develop/active -directory-certificate-credentials | 678 # https://docs.microsoft.com/en-us/azure/active-directory/develop/active -directory-certificate-credentials |
678 hex_val = binascii.a2b_hex(self.config.thumbprint) | 679 hex_val = binascii.a2b_hex(self.config.thumbprint) |
679 x5t = base64.urlsafe_b64encode(hex_val).decode() | 680 x5t = base64.urlsafe_b64encode(hex_val).decode() |
680 | 681 |
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 key = RSA.importKey(private_key) |
682 | 683 |
683 signed = self.sign_jwt(self.config.clientID, key, url, | 684 signed = self.sign_jwt(self.config.clientID, key, url, |
684 self.azure_jwt_signature_fnc, | 685 self.azure_jwt_signature_fnc, |
685 jwt_headers={'x5t': x5t}) | 686 jwt_headers={'x5t': x5t}) |
686 | 687 |
687 # generate oauth parameters for login.microsoft.com | 688 # generate oauth parameters for login.microsoft.com |
688 oauth_params = { | 689 oauth_params = { |
689 'grant_type': 'client_credentials', | 690 'grant_type': 'client_credentials', |
690 'client_id': self.config.clientID, | 691 'client_id': self.config.clientID, |
691 'resource': 'https://graph.windows.net', | 692 'resource': 'https://graph.windows.net', |
692 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-' | 693 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-' |
693 'type:jwt-bearer', | 694 'type:jwt-bearer', |
694 'client_assertion': signed, | 695 'client_assertion': signed, |
695 } | 696 } |
696 | 697 |
697 request = urllib2.Request(url, urlencode(oauth_params)) | 698 request = urllib2.Request(url, urlencode(oauth_params)) |
698 request.get_method = lambda: 'POST' | 699 request.get_method = lambda: 'POST' |
699 | 700 |
700 return request | 701 return request |
701 | 702 |
702 def get_windows_store_access_token(self): | 703 def get_windows_store_access_token(self): |
703 # use client certificate to obtain a valid access token | 704 # use client certificate to obtain a valid access token |
704 url = 'https://login.microsoftonline.com/{}/oauth2/token'.format( | 705 url_template = 'https://login.microsoftonline.com/{}/oauth2/token' |
705 self.config.tenantID | 706 url = url_template.format(self.config.tenantID) |
706 ) | |
707 | 707 |
708 with open(self.config.privateKey, 'r') as fp: | 708 with open(self.config.privateKey, 'r') as fp: |
709 private_key = fp.read() | 709 private_key = fp.read() |
710 | 710 |
711 opener = urllib2.build_opener(HTTPErrorBodyHandler) | 711 opener = urllib2.build_opener(HTTPErrorBodyHandler) |
712 request = self.generate_certificate_token_request(url, private_key) | 712 request = self.generate_certificate_token_request(url, private_key) |
713 | 713 |
714 with contextlib.closing(opener.open(request)) as response: | 714 with contextlib.closing(opener.open(request)) as response: |
715 data = json.load(response) | 715 data = json.load(response) |
716 auth_token = '{0[token_type]} {0[access_token]}'.format(data) | 716 auth_token = '{0[token_type]} {0[access_token]}'.format(data) |
(...skipping 25 matching lines...) Expand all Loading... | |
742 def upload_to_windows_store(self): | 742 def upload_to_windows_store(self): |
743 opener = urllib2.build_opener(HTTPErrorBodyHandler) | 743 opener = urllib2.build_opener(HTTPErrorBodyHandler) |
744 | 744 |
745 headers = {'Authorization': self.get_windows_store_access_token(), | 745 headers = {'Authorization': self.get_windows_store_access_token(), |
746 'Content-type': 'application/json'} | 746 'Content-type': 'application/json'} |
747 | 747 |
748 # Get application | 748 # Get application |
749 # https://docs.microsoft.com/en-us/windows/uwp/monetize/get-an-app | 749 # https://docs.microsoft.com/en-us/windows/uwp/monetize/get-an-app |
750 api_path = '{}/v1.0/my/applications/{}'.format( | 750 api_path = '{}/v1.0/my/applications/{}'.format( |
751 'https://manage.devcenter.microsoft.com', | 751 'https://manage.devcenter.microsoft.com', |
752 self.config.devbuildGalleryID | 752 self.config.devbuildGalleryID, |
753 ) | 753 ) |
754 | 754 |
755 request = urllib2.Request(api_path, None, headers) | 755 request = urllib2.Request(api_path, None, headers) |
756 with contextlib.closing(opener.open(request)) as response: | 756 with contextlib.closing(opener.open(request)) as response: |
757 app_obj = json.load(response) | 757 app_obj = json.load(response) |
758 | 758 |
759 # Delete existing in-progress submission | 759 # Delete existing in-progress submission |
760 # https://docs.microsoft.com/en-us/windows/uwp/monetize/delete-an-app-su bmission | 760 # https://docs.microsoft.com/en-us/windows/uwp/monetize/delete-an-app-su bmission |
761 submissions_path = api_path + '/submissions' | 761 submissions_path = api_path + '/submissions' |
762 if 'pendingApplicationSubmission' in app_obj: | 762 if 'pendingApplicationSubmission' in app_obj: |
(...skipping 126 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
889 # write update manifest | 889 # write update manifest |
890 self.writeUpdateManifest() | 890 self.writeUpdateManifest() |
891 | 891 |
892 # retire old builds | 892 # retire old builds |
893 versions = self.retireBuilds() | 893 versions = self.retireBuilds() |
894 # update index page | 894 # update index page |
895 self.updateIndex(versions) | 895 self.updateIndex(versions) |
896 | 896 |
897 # Update soft link to latest build | 897 # Update soft link to latest build |
898 baseDir = os.path.join( | 898 baseDir = os.path.join( |
899 self.config.nightliesDirectory, self.basename | 899 self.config.nightliesDirectory, self.basename, |
900 ) | 900 ) |
901 linkPath = os.path.join( | 901 linkPath = os.path.join( |
902 baseDir, '00latest' + self.config.packageSuffix | 902 baseDir, '00latest' + self.config.packageSuffix, |
903 ) | 903 ) |
904 | 904 |
905 self.symlink_or_copy(self.path, linkPath) | 905 self.symlink_or_copy(self.path, linkPath) |
906 finally: | 906 finally: |
907 # clean up | 907 # clean up |
908 if self.tempdir: | 908 if self.tempdir: |
909 shutil.rmtree(self.tempdir, ignore_errors=True) | 909 shutil.rmtree(self.tempdir, ignore_errors=True) |
910 | 910 |
911 | 911 |
912 def main(download=False): | 912 def main(download=False): |
(...skipping 23 matching lines...) Expand all Loading... | |
936 | 936 |
937 file = open(nightlyConfigFile, 'wb') | 937 file = open(nightlyConfigFile, 'wb') |
938 nightlyConfig.write(file) | 938 nightlyConfig.write(file) |
939 | 939 |
940 | 940 |
941 if __name__ == '__main__': | 941 if __name__ == '__main__': |
942 parser = argparse.ArgumentParser() | 942 parser = argparse.ArgumentParser() |
943 parser.add_argument('--download', action='store_true', default=False) | 943 parser.add_argument('--download', action='store_true', default=False) |
944 args = parser.parse_args() | 944 args = parser.parse_args() |
945 main(args.download) | 945 main(args.download) |
LEFT | RIGHT |