| Index: sitescripts/extensions/bin/createNightlies.py |
| =================================================================== |
| --- a/sitescripts/extensions/bin/createNightlies.py |
| +++ b/sitescripts/extensions/bin/createNightlies.py |
| @@ -19,24 +19,25 @@ Nightly builds generation script |
| ================================ |
| This script generates nightly builds of extensions, together |
| with changelogs and documentation. |
| """ |
| import ConfigParser |
| -import cookielib |
| +import base64 |
| from datetime import datetime |
| import hashlib |
| -import HTMLParser |
| +import hmac |
| import json |
| import logging |
| import os |
| import pipes |
| +import random |
| import shutil |
| import struct |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| from urllib import urlencode |
| import urllib2 |
| @@ -386,115 +387,61 @@ class NightlyBuild(object): |
| link['changelog'] = changelogFile |
| links.append(link) |
| template = get_template(get_config().get('extensions', 'nightlyIndexPage')) |
| template.stream({'config': self.config, 'links': links}).dump(outputPath) |
| def uploadToMozillaAddons(self): |
| import urllib3 |
| - username = get_config().get('extensions', 'amo_username') |
| - password = get_config().get('extensions', 'amo_password') |
| - |
| - slug = self.config.galleryID |
| - login_url = 'https://addons.mozilla.org/en-US/firefox/users/login' |
| - upload_url = 'https://addons.mozilla.org/en-US/developers/addon/%s/upload' % slug |
| - add_url = 'https://addons.mozilla.org/en-US/developers/addon/%s/versions/add' % slug |
| - |
| - cookie_jar = cookielib.CookieJar() |
| - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie_jar)) |
| - |
| - def load_url(url, data=None): |
| - content_type = 'application/x-www-form-urlencoded' |
| - if isinstance(data, dict): |
| - if any(isinstance(v, tuple) for v in data.itervalues()): |
| - data, content_type = urllib3.filepost.encode_multipart_formdata(data) |
| - else: |
| - data = urlencode(data.items()) |
| - |
| - request = urllib2.Request(url, data, headers={'Content-Type': content_type}) |
| - response = opener.open(request) |
| - try: |
| - return response.read() |
| - finally: |
| - response.close() |
| - |
| - class CSRFParser(HTMLParser.HTMLParser): |
| - result = None |
| - dummy_exception = Exception() |
| - |
| - def __init__(self, data): |
| - HTMLParser.HTMLParser.__init__(self) |
| - try: |
| - self.feed(data) |
| - self.close() |
| - except Exception, e: |
| - if e != self.dummy_exception: |
| - raise |
| - |
| - if not self.result: |
| - raise Exception('Failed to extract CSRF token') |
| - |
| - def set_result(self, value): |
| - self.result = value |
| - raise self.dummy_exception |
| + header = { |
| + 'alg': 'HS256', # HMAC-SHA256 |
| + 'typ': 'JWT', |
| + } |
| - def handle_starttag(self, tag, attrs): |
| - attrs = dict(attrs) |
| - if tag == 'meta' and attrs.get('name') == 'csrf': |
| - self.set_result(attrs.get('content')) |
| - if tag == 'input' and attrs.get('name') == 'csrfmiddlewaretoken': |
| - self.set_result(attrs.get('value')) |
| - |
| - # Extract anonymous CSRF token |
| - login_page = load_url(login_url) |
| - csrf_token = CSRFParser(login_page).result |
| + issued = int(time.time()) |
| + payload = { |
| + 'iss': get_config().get('extensions', 'amo_key'), |
| + 'jti': random.random(), |
| + 'iat': issued, |
| + 'exp': issued + 60, |
| + } |
| - # Log in and get session's CSRF token |
| - main_page = load_url( |
| - login_url, |
| - { |
| - 'csrfmiddlewaretoken': csrf_token, |
| - 'username': username, |
| - 'password': password, |
| - } |
| - ) |
| - csrf_token = CSRFParser(main_page).result |
| + input = '.'.join([ |
|
Sebastian Noack
2016/09/13 15:03:12
'{}.{}'.format(...) seems more appropriate rather
Wladimir Palant
2016/09/13 15:16:54
Done.
|
| + base64.b64encode(json.dumps(header)), |
| + base64.b64encode(json.dumps(payload)) |
| + ]) |
| - # Upload build |
| + signature = hmac.new(get_config().get('extensions', 'amo_secret'), |
| + msg=input, |
| + digestmod=hashlib.sha256).digest() |
| + token = '.'.join([input, base64.b64encode(signature)]) |
| + |
| + upload_url = ('https://addons.mozilla.org/api/v3/addons/{0}/' |
|
Sebastian Noack
2016/09/13 15:03:12
Nit: Note that the indices in the placeholders are
Wladimir Palant
2016/09/13 15:16:54
Done.
|
| + 'versions/{1}/').format(self.extensionID, self.version) |
| + |
| with open(self.path, 'rb') as file: |
| - upload_response = json.loads(load_url( |
| - upload_url, |
| - { |
| - 'csrfmiddlewaretoken': csrf_token, |
| - 'upload': (os.path.basename(self.path), file.read(), 'application/x-xpinstall'), |
| - } |
| - )) |
| + data, content_type = urllib3.filepost.encode_multipart_formdata({ |
| + 'upload': ( |
| + os.path.basename(self.path), |
| + file.read(), |
| + 'application/x-xpinstall' |
| + ) |
| + }) |
| - # Wait for validation to finish |
| - while not upload_response.get('validation'): |
| - time.sleep(2) |
| - upload_response = json.loads(load_url( |
| - upload_url + '/' + upload_response.get('upload') |
| - )) |
| - |
| - if upload_response['validation'].get('errors', 0): |
| - raise Exception('Build failed AMO validation, see https://addons.mozilla.org%s' % upload_response.get('full_report_url')) |
| + request = urllib2.Request(upload_url, data=data) |
| + request.add_header('Content-Type', content_type) |
| + request.add_header('Authorization', 'JWT ' + token) |
| + request.get_method = lambda: 'PUT' |
| - # Add version |
| - add_response = json.loads(load_url( |
| - add_url, |
| - { |
| - 'csrfmiddlewaretoken': csrf_token, |
| - 'upload': upload_response.get('upload'), |
| - 'source': ('', '', 'application/octet-stream'), |
| - 'beta': 'on', |
| - 'supported_platforms': 1, # PLATFORM_ANY.id |
| - } |
| - )) |
| + try: |
| + urllib2.urlopen(request).close() |
| + except urllib2.HTTPError as e: |
| + logging.error(e.read()) |
|
Sebastian Noack
2016/09/13 15:03:12
The error response should be closed as well.
Wladimir Palant
2016/09/13 15:16:54
Done.
|
| + raise |
| def uploadToChromeWebStore(self): |
| # Google APIs use HTTP error codes with error message in body. So we add |
| # the response body to the HTTPError to get more meaningful error messages. |
| class HTTPErrorBodyHandler(urllib2.HTTPDefaultErrorHandler): |
| def http_error_default(self, req, fp, code, msg, hdrs): |
| raise urllib2.HTTPError(req.get_full_url(), code, '%s\n%s' % (msg, fp.read()), hdrs, fp) |
| @@ -595,17 +542,17 @@ class NightlyBuild(object): |
| self.writeIEUpdateManifest(versions) |
| # update index page |
| self.updateIndex(versions) |
| # update nightlies config |
| self.config.latestRevision = self.revision |
| - if self.config.type == 'gecko' and self.config.galleryID and get_config().get('extensions', 'amo_username'): |
| + if self.config.type == 'gecko' and self.config.galleryID and get_config().has_option('extensions', 'amo_key'): |
| self.uploadToMozillaAddons() |
| elif self.config.type == 'chrome' and self.config.clientID and self.config.clientSecret and self.config.refreshToken: |
| self.uploadToChromeWebStore() |
| finally: |
| # clean up |
| if self.tempdir: |
| shutil.rmtree(self.tempdir, ignore_errors=True) |