Chromium Code Reviews| Index: tools/origin_trials/generate_token.py |
| diff --git a/tools/origin_trials/generate_token.py b/tools/origin_trials/generate_token.py |
| new file mode 100755 |
| index 0000000000000000000000000000000000000000..9a052f976ac298e926562ae21a8a9a8e41df1b0a |
| --- /dev/null |
| +++ b/tools/origin_trials/generate_token.py |
| @@ -0,0 +1,121 @@ |
| +#!/usr/bin/env python |
| +# Copyright (c) 2016 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
| +"""Utility for generating experimental API tokens |
| + |
| +usage: generate_token.py [-h] [--key-file KEY_FILE] |
| + [--expire-days EXPIRE_DAYS | |
| + --expire-timestamp EXPIRE_TIMESTAMP] |
| + origin trial_name |
| + |
| +Run "generate_token.py -h" for more help on usage. |
| +""" |
| +import argparse |
| +import base64 |
| +import re |
| +import os |
| +import sys |
| +import time |
| +import urlparse |
| + |
| +from third_party.ed25519 import ed25519 |
|
Nico
2016/01/26 17:27:46
With a snippet like e.g. https://code.google.com/p
iclelland
2016/02/12 22:00:50
I hate to hack the import path; Python has a well-
|
| + |
| +HOSTNAME_REGEX = re.compile( |
|
palmer
2016/02/11 22:03:20
http://stackoverflow.com/a/2532344 looks more corr
iclelland
2016/02/16 20:25:12
Certainly. It handles the 255 character limit, as
|
| + r"""^ |
| + # Zero or more components, where each one is either a single alphanumeric |
| + (([a-z0-9]| |
| + # Or multiple alphanumerics with internal hyphens |
| + [a-z0-9][a-z0-9-]*[a-z0-9]) |
| + # Separated by periods |
| + \.)* |
| + # And followed by a final component. |
| + ([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$""", |
| + re.IGNORECASE | re.VERBOSE |
| +) |
| + |
| +def OriginFromArgs(arg): |
|
palmer
2016/02/11 22:03:20
Nit: The function name suggests that the argument
iclelland
2016/02/12 22:00:50
It is a convention, but you're right that it's a m
|
| + """Constructs the origin for the token from the command line arguments. |
| + |
| + Returns None if this is not possible (neither a valid hostname nor a |
| + valid origin URL was provided.) |
| + """ |
| + # Does it look like a hostname? |
| + if HOSTNAME_REGEX.match(arg): |
| + return 'https://'+arg |
| + # Try to construct an origin URL from the argument |
| + origin = urlparse.urlparse(arg) |
| + if not origin or not origin.scheme or not origin.netloc: |
| + raise argparse.ArgumentTypeError("%s is not a hostname or a URL" % arg) |
| + # Strip any other components and return the URL: |
| + return urlparse.urlunparse(origin[:2] + ('', )*4) |
|
palmer
2016/02/11 22:03:20
It's a bit mysterious (to me, at least) what urlun
palmer
2016/02/11 22:03:20
Nit: consistently use spaces around operators.
iclelland
2016/02/12 22:00:50
I would, but PEP8 says, "If operators with differe
iclelland
2016/02/16 20:25:12
Unparse is just the opposite of parse -- parse bre
|
| + |
| +def ExpiryFromArgs(args): |
| + if args.expire_timestamp: |
| + return int(args.expire_timestamp) |
| + return ((int(time.time()) + (int(args.expire_days) * 86400)) * 1000) |
| + |
| +def GenerateTokenData(origin, apiName, expiry): |
| + return "{0}|{1}|{2}".format(origin, apiName, expiry) |
| + |
| +def Sign(private_key, data): |
|
palmer
2016/02/11 22:03:20
Nit: Consistently use private_key or privateKey st
iclelland
2016/02/12 22:00:50
Thanks. I missed apiName on my first pass. PEP8 st
|
| + return ed25519.signature(data, private_key[:32], private_key[32:]) |
| + |
| +def FormatToken(signature, data): |
| + return base64.b64encode(signature) + "|" + data |
|
palmer
2016/02/11 22:03:20
Nit: Use "..." or '...' strings throughout; whatev
iclelland
2016/02/12 22:00:50
Done. The standard explicitly makes no recommendat
|
| + |
| +def main(): |
| + parser = argparse.ArgumentParser( |
| + description="Generate tokens for enabling experimental APIs") |
| + parser.add_argument('origin', |
| + help="Origin for which to enable the API. This can be a " |
|
palmer
2016/02/11 22:03:20
Nit: Punctuation and conciseness. How about:
Orig
iclelland
2016/02/12 22:00:50
Done.
|
| + "bare hostname (in which case https will be " |
| + "assumed,) or a valid origin URL (hostname, " |
| + "protocol and port can all be included.)", |
| + type=OriginFromArgs) |
| + parser.add_argument('trial_name', |
| + help="Feature to enable. The current list of " |
| + "experimental feature trials can be found in " |
| + "RuntimeFeatures.in") |
| + parser.add_argument('--key-file', |
| + help='Ed25519 private key file to sign the token with', |
| + default="eftest.key") |
| + expiry_group = parser.add_mutually_exclusive_group() |
| + expiry_group.add_argument('--expire-days', |
| + help='Days from now when the token should exipire', |
| + type=int, |
| + default=42) |
| + expiry_group.add_argument('--expire-timestamp', |
| + help="Exact time (milliseconds since 1970-01-01 " |
| + "00:00:00 UTC) when the token should exipire", |
| + type=int) |
| + |
| + args = parser.parse_args() |
| + expiry = ExpiryFromArgs(args) |
| + |
| + key_file = open(os.path.expanduser(args.key_file)) |
| + private_key = key_file.read() |
|
palmer
2016/02/11 22:03:20
Maybe just read the 1st 64 bytes from the file, an
iclelland
2016/02/12 22:00:50
Yeah, the key is a binary file; it wouldn't make s
|
| + |
| + # Validate that the key file read was a proper Ed25519 key -- running the |
| + # publickey method on the first half of the key should return the second |
| + # half. |
| + if (len(private_key) != 64 or |
| + ed25519.publickey(private_key[:32]) != private_key[32:]): |
| + print("Unable to use the specified private key file.") |
| + sys.exit(1) |
| + |
| + token_data = GenerateTokenData(args.origin, args.trial_name, expiry) |
| + signature = Sign(private_key, token_data) |
| + |
| + # Verify that that the signature is correct before printing it. |
| + try: |
| + ed25519.checkvalid(signature, token_data, private_key[32:]) |
| + except Exception: |
| + print "There was an error generating the signature." |
|
palmer
2016/02/11 22:03:20
Maybe print the exception in case it has anything
iclelland
2016/02/12 22:00:50
For diagnostics, sure. The exceptions are mostly n
|
| + sys.exit(1) |
| + |
| + print FormatToken(signature, token_data) |
| + |
| +if __name__ == "__main__": |
| + main() |