Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/env python | |
| 2 # Copyright (c) 2016 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 """Utility for generating experimental API tokens | |
| 7 | |
| 8 usage: generate_token.py [-h] [--key-file KEY_FILE] | |
| 9 [--expire-days EXPIRE_DAYS | | |
| 10 --expire-timestamp EXPIRE_TIMESTAMP] | |
| 11 origin trial_name | |
| 12 | |
| 13 Run "generate_token.py -h" for more help on usage. | |
| 14 """ | |
| 15 import argparse | |
| 16 import base64 | |
| 17 import re | |
| 18 import os | |
| 19 import sys | |
| 20 import time | |
| 21 import urlparse | |
| 22 | |
| 23 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-
| |
| 24 | |
| 25 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
| |
| 26 r"""^ | |
| 27 # Zero or more components, where each one is either a single alphanumeric | |
| 28 (([a-z0-9]| | |
| 29 # Or multiple alphanumerics with internal hyphens | |
| 30 [a-z0-9][a-z0-9-]*[a-z0-9]) | |
| 31 # Separated by periods | |
| 32 \.)* | |
| 33 # And followed by a final component. | |
| 34 ([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$""", | |
| 35 re.IGNORECASE | re.VERBOSE | |
| 36 ) | |
| 37 | |
| 38 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
| |
| 39 """Constructs the origin for the token from the command line arguments. | |
| 40 | |
| 41 Returns None if this is not possible (neither a valid hostname nor a | |
| 42 valid origin URL was provided.) | |
| 43 """ | |
| 44 # Does it look like a hostname? | |
| 45 if HOSTNAME_REGEX.match(arg): | |
| 46 return 'https://'+arg | |
| 47 # Try to construct an origin URL from the argument | |
| 48 origin = urlparse.urlparse(arg) | |
| 49 if not origin or not origin.scheme or not origin.netloc: | |
| 50 raise argparse.ArgumentTypeError("%s is not a hostname or a URL" % arg) | |
| 51 # Strip any other components and return the URL: | |
| 52 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
| |
| 53 | |
| 54 def ExpiryFromArgs(args): | |
| 55 if args.expire_timestamp: | |
| 56 return int(args.expire_timestamp) | |
| 57 return ((int(time.time()) + (int(args.expire_days) * 86400)) * 1000) | |
| 58 | |
| 59 def GenerateTokenData(origin, apiName, expiry): | |
| 60 return "{0}|{1}|{2}".format(origin, apiName, expiry) | |
| 61 | |
| 62 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
| |
| 63 return ed25519.signature(data, private_key[:32], private_key[32:]) | |
| 64 | |
| 65 def FormatToken(signature, data): | |
| 66 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
| |
| 67 | |
| 68 def main(): | |
| 69 parser = argparse.ArgumentParser( | |
| 70 description="Generate tokens for enabling experimental APIs") | |
| 71 parser.add_argument('origin', | |
| 72 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.
| |
| 73 "bare hostname (in which case https will be " | |
| 74 "assumed,) or a valid origin URL (hostname, " | |
| 75 "protocol and port can all be included.)", | |
| 76 type=OriginFromArgs) | |
| 77 parser.add_argument('trial_name', | |
| 78 help="Feature to enable. The current list of " | |
| 79 "experimental feature trials can be found in " | |
| 80 "RuntimeFeatures.in") | |
| 81 parser.add_argument('--key-file', | |
| 82 help='Ed25519 private key file to sign the token with', | |
| 83 default="eftest.key") | |
| 84 expiry_group = parser.add_mutually_exclusive_group() | |
| 85 expiry_group.add_argument('--expire-days', | |
| 86 help='Days from now when the token should exipire', | |
| 87 type=int, | |
| 88 default=42) | |
| 89 expiry_group.add_argument('--expire-timestamp', | |
| 90 help="Exact time (milliseconds since 1970-01-01 " | |
| 91 "00:00:00 UTC) when the token should exipire", | |
| 92 type=int) | |
| 93 | |
| 94 args = parser.parse_args() | |
| 95 expiry = ExpiryFromArgs(args) | |
| 96 | |
| 97 key_file = open(os.path.expanduser(args.key_file)) | |
| 98 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
| |
| 99 | |
| 100 # Validate that the key file read was a proper Ed25519 key -- running the | |
| 101 # publickey method on the first half of the key should return the second | |
| 102 # half. | |
| 103 if (len(private_key) != 64 or | |
| 104 ed25519.publickey(private_key[:32]) != private_key[32:]): | |
| 105 print("Unable to use the specified private key file.") | |
| 106 sys.exit(1) | |
| 107 | |
| 108 token_data = GenerateTokenData(args.origin, args.trial_name, expiry) | |
| 109 signature = Sign(private_key, token_data) | |
| 110 | |
| 111 # Verify that that the signature is correct before printing it. | |
| 112 try: | |
| 113 ed25519.checkvalid(signature, token_data, private_key[32:]) | |
| 114 except Exception: | |
| 115 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
| |
| 116 sys.exit(1) | |
| 117 | |
| 118 print FormatToken(signature, token_data) | |
| 119 | |
| 120 if __name__ == "__main__": | |
| 121 main() | |
| OLD | NEW |