Source release 18.7.0
This commit is contained in:
@@ -11,6 +11,39 @@
|
||||
|
||||
namespace widevine {
|
||||
namespace {
|
||||
std::string EscapeJson(const std::string& input) {
|
||||
std::string result;
|
||||
for (std::string::const_iterator c = input.begin(); c != input.end(); ++c) {
|
||||
switch (*c) {
|
||||
case '\"':
|
||||
result += "\\\"";
|
||||
break;
|
||||
case '\\':
|
||||
result += "\\\\";
|
||||
break;
|
||||
case '\b':
|
||||
result += "\\b";
|
||||
break;
|
||||
case '\f':
|
||||
result += "\\f";
|
||||
break;
|
||||
case '\n':
|
||||
result += "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
result += "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
result += "\\t";
|
||||
break;
|
||||
default:
|
||||
result += *c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string StringMapToJson(
|
||||
const std::map<std::string, std::string>& string_map) {
|
||||
std::string json = "{";
|
||||
@@ -72,7 +105,7 @@ Status WidevineFactoryExtractor::GenerateUploadRequest(std::string& request) {
|
||||
request_map["model"] = PropertiesCE::GetClientInfo().model_name;
|
||||
request_map["product"] = PropertiesCE::GetClientInfo().product_name;
|
||||
request_map["build_info"] = PropertiesCE::GetClientInfo().build_info;
|
||||
request_map["oemcrypto_build_info"] = oemcrypto_build_info;
|
||||
request_map["oemcrypto_build_info"] = EscapeJson(oemcrypto_build_info);
|
||||
request_map["bcc"] = wvutil::Base64Encode(bcc);
|
||||
std::string request_json = StringMapToJson(request_map);
|
||||
|
||||
|
||||
249
factory_upload_tool/ce/wv_upload_tool.py
Normal file → Executable file
249
factory_upload_tool/ce/wv_upload_tool.py
Normal file → Executable file
@@ -12,6 +12,7 @@ input, with one JSON string per line of input.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from http import HTTPStatus
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
@@ -20,16 +21,22 @@ import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
import webbrowser
|
||||
from google.auth.transport import requests
|
||||
from google.oauth2 import service_account
|
||||
|
||||
DEFAULT_BASE = 'https://widevine.googleapis.com/v1beta1'
|
||||
UPLOAD_PATH = '/uniqueDeviceInfo:batchUpload'
|
||||
BATCH_CHECK_PATH = '/uniqueDeviceInfo:batchCheck'
|
||||
TOKEN_CACHE_FILE = os.path.join(
|
||||
os.path.expanduser('~'), '.device_info_uploader.token')
|
||||
os.path.expanduser('~'), '.device_info_uploader.token'
|
||||
)
|
||||
|
||||
OAUTH_SERVICE_BASE = 'https://accounts.google.com/o/oauth2'
|
||||
OAUTH_AUTHN_URL = OAUTH_SERVICE_BASE + '/auth'
|
||||
OAUTH_TOKEN_URL = OAUTH_SERVICE_BASE + '/token'
|
||||
|
||||
OAUTH_SCOPES = ['https://www.googleapis.com/auth/widevine/frontend']
|
||||
|
||||
|
||||
class OAuthHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
"""HTTP Handler used to accept the oauth response when the user logs in."""
|
||||
@@ -41,17 +48,24 @@ class OAuthHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
params = dict(urllib.parse.parse_qsl(parsed_path.query))
|
||||
if 'error' in params:
|
||||
error = params['error']
|
||||
self.respond(400, error,
|
||||
f'Error received from the OAuth server: {error}.')
|
||||
self.respond(
|
||||
400, error, f'Error received from the OAuth server: {error}.'
|
||||
)
|
||||
sys.exit(-1)
|
||||
elif 'code' not in params:
|
||||
self.respond(400, 'ERROR',
|
||||
('Response from OAuth server is missing the authorization '
|
||||
f'code. Full response: "{self.path}"'))
|
||||
self.respond(
|
||||
400,
|
||||
'ERROR',
|
||||
(
|
||||
'Response from OAuth server is missing the authorization '
|
||||
f'code. Full response: "{self.path}"'
|
||||
),
|
||||
)
|
||||
sys.exit(-1)
|
||||
else:
|
||||
self.respond(200, 'Success!',
|
||||
'Success! You may close this browser window.')
|
||||
self.respond(
|
||||
200, 'Success!', 'Success! You may close this browser window.'
|
||||
)
|
||||
self.server.code = params['code']
|
||||
|
||||
def do_POST(self): # pylint: disable=invalid-name
|
||||
@@ -70,20 +84,25 @@ class OAuthHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
self.send_response(code)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
self.wfile.write(('<html>'
|
||||
f' <title>{title}</title>'
|
||||
f' <body>'
|
||||
f' <p style="font-size:24px;">{message}</p>'
|
||||
f' </body>'
|
||||
'</html>').encode('utf-8'))
|
||||
self.wfile.write(
|
||||
(
|
||||
'<html>'
|
||||
f' <title>{title}</title>'
|
||||
' <body>'
|
||||
f' <p style="font-size:24px;">{message}</p>'
|
||||
' </body>'
|
||||
'</html>'
|
||||
).encode('utf-8')
|
||||
)
|
||||
|
||||
|
||||
class LocalOAuthReceiver(http.server.HTTPServer):
|
||||
"""HTTP server that will wait for an OAuth authorization code."""
|
||||
|
||||
def __init__(self):
|
||||
super(LocalOAuthReceiver, self).__init__(('127.0.0.1', 0),
|
||||
OAuthHTTPRequestHandler)
|
||||
super(LocalOAuthReceiver, self).__init__(
|
||||
('127.0.0.1', 0), OAuthHTTPRequestHandler
|
||||
)
|
||||
self.code = None
|
||||
|
||||
def port(self):
|
||||
@@ -111,25 +130,63 @@ def parse_args():
|
||||
Returns:
|
||||
An argparse.Namespace object populated with the arguments.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description='Upload device info')
|
||||
parser = argparse.ArgumentParser(description='Widevine BCC Batch Upload/Check Tool')
|
||||
|
||||
parser.add_argument("--version", action="version", version="20240822") #yyyymmdd
|
||||
|
||||
parser.add_argument(
|
||||
'--json-csr',
|
||||
nargs='+',
|
||||
required=True,
|
||||
help='list of files containing JSON output from rkp_factory_extraction_tool'
|
||||
help='list of files containing JSON output from factory extraction tool',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--credentials', required=True, help='JSON credentials file')
|
||||
parser.add_argument('--credentials', help='JSON credentials file')
|
||||
|
||||
parser.add_argument(
|
||||
'--endpoint', default=DEFAULT_BASE, help='destination server URL')
|
||||
'--endpoint', default=DEFAULT_BASE, help='destination server URL'
|
||||
)
|
||||
|
||||
parser.add_argument('--org-name', required=True, help='orgnization name')
|
||||
|
||||
parser.add_argument(
|
||||
'--cache-token',
|
||||
action='store_true',
|
||||
help='Use a locally cached a refresh token')
|
||||
help='Use a locally cached a refresh token',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--service-credentials', help='JSON credentials file for service account'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--die-on-error',
|
||||
action='store_true',
|
||||
help='exit on error and stop uploading more CSRs',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--dryrun',
|
||||
action='store_true',
|
||||
help=(
|
||||
'Do not upload anything. Instead print out what actions would have'
|
||||
' been taken if the --dryrun flag had not been specified.'
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--check',
|
||||
action='store_true',
|
||||
required=False,
|
||||
help='Perform a batch check on the CSRs.',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--verbose',
|
||||
action='store_true',
|
||||
required=False,
|
||||
help='Print request and response details.',
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -146,6 +203,7 @@ def parse_json_csrs(filename, batches):
|
||||
line_count = 0
|
||||
for line in open(filename):
|
||||
line_count = line_count + 1
|
||||
obj = {}
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except json.JSONDecodeError as e:
|
||||
@@ -159,14 +217,17 @@ def parse_json_csrs(filename, batches):
|
||||
'name': obj['name'],
|
||||
'model': obj['model'],
|
||||
'product': obj['product'],
|
||||
'build_info': obj['build_info']
|
||||
'build_info': obj['build_info'],
|
||||
'oemcrypto_build_info': obj['oemcrypto_build_info'],
|
||||
})
|
||||
if device_metadata not in batches:
|
||||
batches[device_metadata] = []
|
||||
batches[device_metadata].append(bcc)
|
||||
except KeyError as e:
|
||||
die(f'Invalid object at {filename}:{line_count}, missing {e}')
|
||||
|
||||
if device_metadata not in batches:
|
||||
batches[device_metadata] = []
|
||||
batches[device_metadata].append(bcc)
|
||||
if line_count == 0:
|
||||
die('Empty BCC file!')
|
||||
|
||||
|
||||
def format_request_body(args, device_metadata, bccs):
|
||||
@@ -180,6 +241,17 @@ def format_request_body(args, device_metadata, bccs):
|
||||
return json.dumps(request).encode('utf-8')
|
||||
|
||||
|
||||
def format_check_request_body(args, bccs):
|
||||
"""Generate a formatted BatchCheck request buffer for the given build and CSRs."""
|
||||
request = {
|
||||
'parent': 'orgs/' + args.org_name,
|
||||
'request_id': uuid.uuid4().hex,
|
||||
'device_info': bccs,
|
||||
}
|
||||
|
||||
return json.dumps(request).encode('utf-8')
|
||||
|
||||
|
||||
def load_refresh_token():
|
||||
if not os.path.exists(TOKEN_CACHE_FILE):
|
||||
return None
|
||||
@@ -258,7 +330,8 @@ def load_and_validate_creds(credfile):
|
||||
' The given credentials do not appear to be for a locally installed\n'
|
||||
' application. Please navigate to the credentials dashboard and\n'
|
||||
' ensure that the "Type" of your client is "Desktop":\n'
|
||||
' https://console.cloud.google.com/apis/credentials')
|
||||
' https://console.cloud.google.com/apis/credentials'
|
||||
)
|
||||
|
||||
if 'installed' not in credmap:
|
||||
die(not_local_app_creds_error)
|
||||
@@ -267,10 +340,12 @@ def load_and_validate_creds(credfile):
|
||||
|
||||
expected_keys = set(['client_id', 'client_secret', 'redirect_uris'])
|
||||
if not expected_keys.issubset(creds.keys()):
|
||||
die(('ERROR: Invalid credential file.\n'
|
||||
' The given credentials do not appear to be valid. Please\n'
|
||||
' re-download the client credentials file from the dashboard:\n'
|
||||
' https://console.cloud.google.com/apis/credentials'))
|
||||
die((
|
||||
'ERROR: Invalid credential file.\n'
|
||||
' The given credentials do not appear to be valid. Please\n'
|
||||
' re-download the client credentials file from the dashboard:\n'
|
||||
' https://console.cloud.google.com/apis/credentials'
|
||||
))
|
||||
|
||||
if 'http://localhost' not in creds['redirect_uris']:
|
||||
die(not_local_app_creds_error)
|
||||
@@ -279,7 +354,23 @@ def load_and_validate_creds(credfile):
|
||||
|
||||
|
||||
def authenticate_and_fetch_token(args):
|
||||
"""Authenticate the user and fetch an OAUTH2 access token."""
|
||||
"""Authenticate and fetch an OAUTH2 access token."""
|
||||
# Auth for service account
|
||||
if args.service_credentials:
|
||||
if not os.path.exists(args.service_credentials):
|
||||
die('Service account credentials file does not exist.')
|
||||
svc_account = service_account.Credentials.from_service_account_file(
|
||||
args.service_credentials,
|
||||
scopes=OAUTH_SCOPES,
|
||||
)
|
||||
svc_account.refresh(requests.Request())
|
||||
return svc_account.token
|
||||
|
||||
# Auth for user account
|
||||
if args.credentials is None:
|
||||
die('User credentials is not provided.')
|
||||
if not os.path.exists(args.credentials):
|
||||
die('User credentials file does not exist.')
|
||||
creds = load_and_validate_creds(args.credentials)
|
||||
|
||||
access_type = 'online'
|
||||
@@ -292,10 +383,17 @@ def authenticate_and_fetch_token(args):
|
||||
httpd = LocalOAuthReceiver()
|
||||
redirect_uri = f'http://127.0.0.1:{httpd.port()}'
|
||||
url = (
|
||||
OAUTH_AUTHN_URL + '?response_type=code' + '&client_id=' +
|
||||
creds['client_id'] + '&redirect_uri=' + redirect_uri +
|
||||
'&scope=https://www.googleapis.com/auth/widevine/frontend' +
|
||||
'&access_type=' + access_type + '&prompt=select_account')
|
||||
OAUTH_AUTHN_URL
|
||||
+ '?response_type=code'
|
||||
+ '&client_id='
|
||||
+ creds['client_id']
|
||||
+ '&redirect_uri='
|
||||
+ redirect_uri
|
||||
+ '&scope=https://www.googleapis.com/auth/widevine/frontend'
|
||||
+ '&access_type='
|
||||
+ access_type
|
||||
+ '&prompt=select_account'
|
||||
)
|
||||
print('Opening your web browser to authenticate...')
|
||||
if not webbrowser.open(url, new=1, autoraise=True):
|
||||
print('Error opening the browser. Please open this link in a browser')
|
||||
@@ -312,15 +410,52 @@ def upload_batch(args, device_metadata, bccs):
|
||||
device_metadata: The build for which we're uploading CSRs
|
||||
bccs: a list of BCCs to be uploaded for the given build
|
||||
"""
|
||||
print("Uploading {} bcc(s) for build '{}'".format(len(bccs), device_metadata))
|
||||
print('Uploading {} bcc(s)'.format(len(bccs)))
|
||||
if args.verbose:
|
||||
print("Build: '{}'".format(device_metadata))
|
||||
body = format_request_body(args, device_metadata, bccs)
|
||||
print(body)
|
||||
print(args.endpoint + UPLOAD_PATH)
|
||||
request = urllib.request.Request(args.endpoint + UPLOAD_PATH)
|
||||
return batch_action_single_attempt(args, UPLOAD_PATH, body)
|
||||
|
||||
|
||||
def check_batch(args, device_metadata, bccs):
|
||||
"""Batch check all the CSRs.
|
||||
|
||||
Args:
|
||||
args: The parsed command-line arguments
|
||||
device_metadata: The build for which we're checking CSRs
|
||||
bccs: a list of BCCs to be checked for the given build
|
||||
"""
|
||||
print('Checking {} bcc(s)'.format(len(bccs)))
|
||||
if args.verbose:
|
||||
print("Build: '{}'".format(device_metadata))
|
||||
body = format_check_request_body(args, bccs)
|
||||
return batch_action_single_attempt(args, BATCH_CHECK_PATH, body)
|
||||
|
||||
|
||||
def batch_action_single_attempt(args, path, body):
|
||||
"""Batch action (upload or check existence) for all the CSRs in chunks.
|
||||
|
||||
Args:
|
||||
args: The parsed command-line arguments
|
||||
csrs: a list of CSRs to be uploaded/checked for the given build
|
||||
path: The endpoint url for the specific action
|
||||
body: The formatted request body
|
||||
"""
|
||||
if args.verbose:
|
||||
print('Request body:')
|
||||
print(body)
|
||||
print('Request target:')
|
||||
print(args.endpoint + path)
|
||||
request = urllib.request.Request(args.endpoint + path)
|
||||
request.add_header('Content-Type', 'application/json')
|
||||
request.add_header('X-GFE-SSL', 'yes')
|
||||
request.add_header('Authorization',
|
||||
'Bearer ' + authenticate_and_fetch_token(args))
|
||||
request.add_header(
|
||||
'Authorization', 'Bearer ' + authenticate_and_fetch_token(args)
|
||||
)
|
||||
if args.dryrun:
|
||||
print('dry run: would have reached to ' + request.full_url)
|
||||
return HTTPStatus.OK
|
||||
|
||||
try:
|
||||
response = urllib.request.urlopen(request, body)
|
||||
except urllib.error.HTTPError as e:
|
||||
@@ -329,18 +464,40 @@ def upload_batch(args, device_metadata, bccs):
|
||||
eprint(line.decode('utf-8').rstrip())
|
||||
sys.exit(1)
|
||||
|
||||
while chunk := response.read(1024):
|
||||
print(chunk.decode('utf-8'))
|
||||
|
||||
response_body = response.read().decode('utf-8')
|
||||
if args.verbose:
|
||||
print('Response body:')
|
||||
print(response_body)
|
||||
res = json.loads(response_body)
|
||||
if 'failedDeviceInfo' in res:
|
||||
eprint('Failed to upload/check some device info! Response body:')
|
||||
eprint(response_body)
|
||||
eprint('Failed: {} bcc(s)'.format(len(res['failedDeviceInfo'])))
|
||||
if args.die_on_error:
|
||||
sys.exit(1)
|
||||
elif 'successDeviceInfo' in res:
|
||||
print('Success: {} bcc(s)'.format(len(res['successDeviceInfo'])))
|
||||
else:
|
||||
eprint('Failed with unexpected response:')
|
||||
eprint(response_body)
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
if args.dryrun:
|
||||
print('Dry run mode enabled. Service APIs will not be called.')
|
||||
|
||||
batches = {}
|
||||
for filename in args.json_csr:
|
||||
parse_json_csrs(filename, batches)
|
||||
|
||||
if len(batches) > 1:
|
||||
print('WARNING: {} different device metadata'.format(len(batches)))
|
||||
|
||||
for device_metadata, bccs in batches.items():
|
||||
upload_batch(args, device_metadata, bccs)
|
||||
if args.check:
|
||||
check_batch(args, device_metadata, bccs)
|
||||
else:
|
||||
upload_batch(args, device_metadata, bccs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user