| 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,62 @@ 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([ | 
| +            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}/' | 
| +                      'versions/{1}/').format(self.extensionID, self.version) | 
| + | 
| +        opener = urllib2.build_opener(urllib2.HTTPHandler) | 
| 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: | 
| +            opener.open(request).close() | 
| +        except urllib2.HTTPError as e: | 
| +            logging.error(e.read()) | 
| +            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 +543,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) | 
|  | 
|  |