From dc61b8a992ba91530625c5f3ef7fd9e5a6e43150 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sat, 28 Apr 2018 15:38:33 -0400 Subject: [PATCH] load/save secret tokens to/from env variable --- dist/chromium/publish-beta.py | 185 ++++++++++++++++++++++++++++ dist/firefox/publish-signed-beta.py | 31 +++-- 2 files changed, 208 insertions(+), 8 deletions(-) create mode 100755 dist/chromium/publish-beta.py diff --git a/dist/chromium/publish-beta.py b/dist/chromium/publish-beta.py new file mode 100755 index 000000000..feea2bb30 --- /dev/null +++ b/dist/chromium/publish-beta.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 + +import datetime +import json +import jwt +import os +import re +import requests +import shutil +import subprocess +import sys +import tempfile +import time +import zipfile + +from distutils.version import StrictVersion +from string import Template + +# - Download target (raw) uBlock0.webext.xpi from GitHub +# - This is referred to as "raw" package +# - This will fail if not a dev build +# - Modify raw package to make it self-hosted +# - This is referred to as "unsigned" package +# - Ask AMO to sign uBlock0.webext.xpi +# - Generate JWT to be used for communication with server +# - Upload unsigned package to AMO +# - Wait for a valid download URL for signed package +# - Download signed package as uBlock0.webext.signed.xpi +# - This is referred to as "signed" package +# - Upload uBlock0.webext.signed.xpi to GitHub +# - Remove uBlock0.webext.xpi from GitHub +# - Modify updates.json to point to new version +# - Commit changes to repo + +# Load/save auth secrets +ubo_secrets = dict() +if 'UBO_SECRETS' in os.environ: + ubo_secrets = json.loads(os.environ['UBO_SECRETS']) + +def input_secret(prompt, token): + if token in ubo_secrets: + prompt += ' ✔' + prompt += ': ' + value = input(prompt).strip() + if len(value) == 0: + if token not in ubo_secrets: + print('Token error:', token) + exit(1) + value = ubo_secrets[token] + elif token not in ubo_secrets or value != ubo_secrets[token]: + ubo_secrets[token] = value + os.environ['UBO_SECRETS'] = json.dumps(ubo_secrets, indent=None, separators=(',',':')) + return value + +# Find path to project root +projdir = os.path.split(os.path.abspath(__file__))[0] +while not os.path.isdir(os.path.join(projdir, '.git')): + projdir = os.path.normpath(os.path.join(projdir, '..')) + +cs_extension_id = 'cgbcahbpdhpcegmbfconppldiemgcoii' +tmpdir = tempfile.TemporaryDirectory() +raw_zip_filename = 'uBlock0.chromium.zip' +raw_zip_filepath = os.path.join(tmpdir.name, raw_zip_filename) +github_owner = 'gorhill' +github_repo = 'uBlock' + +# We need a version string to work with +if len(sys.argv) >= 2 and sys.argv[1]: + version = sys.argv[1] +else: + version = input('Github release version: ') +version.strip() +if not re.search('^\d+\.\d+\.\d+(b|rc)\d+$', version): + print('Error: Invalid version string.') + exit(1) + +# GitHub API token +github_token = input_secret('Github token', 'github_token') +github_auth = 'token ' + github_token + +# +# Get metadata from GitHub about the release +# + +# https://developer.github.com/v3/repos/releases/#get-a-single-release +print('Downloading release info from GitHub...') +release_info_url = 'https://api.github.com/repos/{0}/{1}/releases/tags/{2}'.format(github_owner, github_repo, version) +headers = { 'Authorization': github_auth, } +response = requests.get(release_info_url, headers=headers) +if response.status_code != 200: + print('Error: Release not found: {0}'.format(response.status_code)) + exit(1) +release_info = response.json() + +# +# Extract URL to raw package from metadata +# + +# Find url for uBlock0.chromium.zip +raw_zip_url = '' +for asset in release_info['assets']: + if asset['name'] == raw_zip_filename: + raw_zip_url = asset['url'] +if len(raw_zip_url) == 0: + print('Error: Release asset URL not found') + exit(1) + +# +# Download raw package from GitHub +# + +# https://developer.github.com/v3/repos/releases/#get-a-single-release-asset +print('Downloading raw zip package from GitHub...') +headers = { + 'Authorization': github_auth, + 'Accept': 'application/octet-stream', +} +response = requests.get(raw_zip_url, headers=headers) +# Redirections are transparently handled: +# http://docs.python-requests.org/en/master/user/quickstart/#redirection-and-history +if response.status_code != 200: + print('Error: Downloading raw package failed -- server error {0}'.format(response.status_code)) + exit(1) +with open(raw_zip_filepath, 'wb') as f: + f.write(response.content) +print('Downloaded raw package saved as {0}'.format(raw_zip_filepath)) + +# +# Upload to Chrome store +# + +# Auth tokens +cs_id = input_secret('Chrome store id', 'cs_id') +cs_secret = input_secret('Chrome store secret', 'cs_secret') +cs_refresh = input_secret('Chrome store refresh token', 'cs_refresh') + +print('Uploading to Chrome store...') +with open(raw_zip_filepath, 'rb') as f: + auth_url = 'https://accounts.google.com/o/oauth2/token' + auth_payload = { + 'client_id': cs_id, + 'client_secret': cs_secret, + 'grant_type': 'refresh_token', + 'refresh_token': cs_refresh, + } + auth_response = requests.post(auth_url, data=auth_payload) + if auth_response.status_code != 200: + print('Error: Auth failed -- server error {0}'.format(auth_response.status_code)) + print(auth_response.text) + exit(1) + auth_data = auth_response.json() + if 'access_token' not in auth_data: + print('Error: Auth failed -- no access token') + exit(1) + # Prepare access token + cs_auth = 'Bearer ' + auth_data['access_token'] + headers = { + 'Authorization': cs_auth, + 'x-goog-api-version': '2', + } + # Upload + print('Uploading package to the Chrome store...') + upload_url = 'https://www.googleapis.com/upload/chromewebstore/v1.1/items/{0}'.format(cs_extension_id) + upload_response = requests.put(upload_url, headers=headers, data=f) + if upload_response.status_code != 200: + print('Error: Upload failed -- server error {0}'.format(upload_response.status_code)) + print(upload_response.text) + exit(1) + print('Upload succeeded.') + f.close() + # Publish + print('Publishing package to the Chrome store...') + publish_url = 'https://www.googleapis.com/chromewebstore/v1.1/items/{0}/publish'.format(cs_extension_id) + headers = { + 'Authorization': cs_auth, + 'x-goog-api-version': '2', + 'Content-Length': '0', + } + publish_response = requests.post(publish_url, headers=headers) + if publish_response.status_code != 200: + print('Error: Chrome store publishing failed -- server error {0}'.format(publish_response.status_code)) + exit(1) + print('Extension successfully published.') + +print('All done.') diff --git a/dist/firefox/publish-signed-beta.py b/dist/firefox/publish-signed-beta.py index a33ab99f4..ce5e88b0f 100755 --- a/dist/firefox/publish-signed-beta.py +++ b/dist/firefox/publish-signed-beta.py @@ -16,6 +16,26 @@ import zipfile from distutils.version import LooseVersion from string import Template +# Load/save auth secrets +ubo_secrets = dict() +if 'UBO_SECRETS' in os.environ: + ubo_secrets = json.loads(os.environ['UBO_SECRETS']) + +def input_secret(prompt, token): + if token in ubo_secrets: + prompt += ' ✔' + prompt += ': ' + value = input(prompt).strip() + if len(value) == 0: + if token not in ubo_secrets: + print('Token error:', token) + exit(1) + value = ubo_secrets[token] + elif token not in ubo_secrets or value != ubo_secrets[token]: + ubo_secrets[token] = value + os.environ['UBO_SECRETS'] = json.dumps(ubo_secrets, indent=None, separators=(',',':')) + return value + # - Download target (raw) uBlock0.firefox.xpi from GitHub # - This is referred to as "raw" package # - This will fail if not a dev build @@ -63,11 +83,7 @@ if not re.search('^\d+\.\d+\.\d+(b|rc)\d+$', version): exit(1) # GitHub API token -# TODO: support as environment variable? (see os.environ) -github_token = input("Github token: ").strip() -if len(github_token) == 0: - print('Error: invalid GitHub token') - exit(1) +github_token = input_secret('Github token', 'github_token') github_auth = 'token ' + github_token # @@ -145,9 +161,8 @@ with zipfile.ZipFile(raw_xpi_filepath, 'r') as zipin: print('Ask AMO to sign self-hosted xpi package...') with open(unsigned_xpi_filepath, 'rb') as f: - # TODO: support use of env variables for key/secret? - amo_api_key = input("AMO API key: ").strip() - amo_secret = input("AMO API secret: ").strip() + amo_api_key = input_secret('AMO API key', 'amo_api_key') + amo_secret = input_secret('AMO API secret', 'amo_secret') amo_nonce = os.urandom(8).hex() jwt_payload = { 'iss': amo_api_key,