| 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..54e61c6448254458b3f3ce28824791bc6defac48
|
| --- /dev/null
|
| +++ b/tools/origin_trials/generate_token.py
|
| @@ -0,0 +1,141 @@
|
| +#!/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
|
| +
|
| +script_dir = os.path.dirname(os.path.realpath(__file__))
|
| +sys.path.insert(0, os.path.join(script_dir, 'third_party', 'ed25519'))
|
| +import ed25519
|
| +
|
| +
|
| +# Matches a valid DNS name label (alphanumeric plus hyphens, except at the ends,
|
| +# no longer than 63 ASCII characters)
|
| +DNS_LABEL_REGEX = re.compile(r"^(?!-)[a-z\d-]{1,63}(?<!-)$", re.IGNORECASE)
|
| +
|
| +def HostnameFromArg(arg):
|
| + """Determines whether a string represents a valid hostname.
|
| +
|
| + Returns the canonical hostname if its argument is valid, or None otherwise.
|
| + """
|
| + if not arg or len(arg) > 255:
|
| + return None
|
| + if arg[-1] == ".":
|
| + arg = arg[:-1]
|
| + if all(DNS_LABEL_REGEX.match(label) for label in arg.split(".")):
|
| + return arg.lower()
|
| +
|
| +def OriginFromArg(arg):
|
| + """Constructs the origin for the token from a command line argument.
|
| +
|
| + Returns None if this is not possible (neither a valid hostname nor a
|
| + valid origin URL was provided.)
|
| + """
|
| + # Does it look like a hostname?
|
| + hostname = HostnameFromArg(arg)
|
| + if hostname:
|
| + return "https://" + hostname + ":443"
|
| + # If not, 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)
|
| + # HTTPS or HTTP only
|
| + if origin.scheme not in ('https','http'):
|
| + raise argparse.ArgumentTypeError("%s does not use a recognized URL scheme" %
|
| + arg)
|
| + # Add default port if it is not specified
|
| + try:
|
| + port = origin.port
|
| + except ValueError:
|
| + raise argparse.ArgumentTypeError("%s is not a hostname or a URL" % arg)
|
| + if not port:
|
| + port = {"https": 443, "http": 80}[origin.scheme]
|
| + # Strip any extra components and return the origin URL:
|
| + return "{0}://{1}:{2}".format(origin.scheme, origin.hostname, port)
|
| +
|
| +def ExpiryFromArgs(args):
|
| + if args.expire_timestamp:
|
| + return int(args.expire_timestamp)
|
| + return (int(time.time()) + (int(args.expire_days) * 86400))
|
| +
|
| +def GenerateTokenData(origin, api_name, expiry):
|
| + return "{0}|{1}|{2}".format(origin, api_name, expiry)
|
| +
|
| +def Sign(private_key, data):
|
| + return ed25519.signature(data, private_key[:32], private_key[32:])
|
| +
|
| +def FormatToken(version, signature, data):
|
| + return version + "|" + base64.b64encode(signature) + "|" + data
|
| +
|
| +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 "
|
| + "either a hostname (default scheme HTTPS, default "
|
| + "port 443) or a URL.",
|
| + type=OriginFromArg)
|
| + 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 (seconds 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), mode="rb")
|
| + private_key = key_file.read(64)
|
| +
|
| + # 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, exc:
|
| + print "There was an error generating the signature."
|
| + print "(The original error was: %s)" % exc
|
| + sys.exit(1)
|
| +
|
| + # Output a properly-formatted token. Version 1 is hard-coded, as it is
|
| + # the only defined token version.
|
| + print FormatToken("1", signature, token_data)
|
| +
|
| +if __name__ == "__main__":
|
| + main()
|
|
|