| #!/usr/bin/python |
| """ |
| Background daemon to refresh OAuth access tokens. |
| Tokens are written to ~/.git-credential-cache/cookie |
| Git config variable http.cookiefile is updated. |
| |
| Runs only on Google Compute Engine. |
| """ |
| |
| import atexit |
| import contextlib |
| import cookielib |
| import json |
| import os |
| import subprocess |
| import sys |
| import time |
| import urllib2 |
| |
| REFRESH = 25 # seconds remaining when starting refresh |
| RETRY_INTERVAL = 5 # seconds between retrying a failed refresh |
| |
| META_URL = 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/' |
| SUPPORTED_SCOPES = [ |
| 'https://www.googleapis.com/auth/gerritcodereview', |
| 'https://www.googleapis.com/auth/source.full_control', |
| 'https://www.googleapis.com/auth/source.read_write', |
| 'https://www.googleapis.com/auth/source.read_only', |
| ] |
| COOKIE_JAR = None |
| |
| def read_meta(part): |
| r = urllib2.Request(META_URL + part) |
| r.add_header('Metadata-Flavor', 'Google') |
| return contextlib.closing(urllib2.urlopen(r)) |
| |
| def select_scope(): |
| with read_meta('scopes') as d: |
| avail = set(d.read().split()) |
| scopes = [s for s in SUPPORTED_SCOPES if s in avail] |
| if scopes: |
| return iter(scopes).next() |
| sys.stderr.write('error: VM must have one of these scopes:\n\n') |
| for s in SUPPORTED_SCOPES: |
| sys.stderr.write(' %s\n' % (s)) |
| sys.exit(1) |
| |
| def configure_git(): |
| global COOKIE_JAR |
| |
| dir = os.path.join(os.environ['HOME'], '.git-credential-cache') |
| COOKIE_JAR = os.path.join(dir, 'cookie') |
| |
| if os.path.exists(dir): |
| os.chmod(dir, 0700) |
| else: |
| os.mkdir(dir, 0700) |
| subprocess.call([ |
| 'git', 'config', '--global', |
| 'http.cookiefile', COOKIE_JAR |
| ]) |
| |
| def acquire_token(scope, retry): |
| while True: |
| try: |
| with read_meta('token?scopes=' + scope) as d: |
| return json.load(d) |
| except urllib2.URLError: |
| if not retry: |
| raise |
| time.sleep(RETRY_INTERVAL) |
| |
| def update_cookie(scope, retry): |
| now = int(time.time()) |
| token = acquire_token(scope, retry) |
| access_token = token['access_token'] |
| expires = now + int(token['expires_in']) |
| |
| tmp_jar = COOKIE_JAR + '.lock' |
| cj = cookielib.MozillaCookieJar(tmp_jar) |
| |
| for d in ['source.developers.google.com', '.googlesource.com']: |
| cj.set_cookie(cookielib.Cookie( |
| version = 0, |
| name = 'o', |
| value = access_token, |
| port = None, |
| port_specified = False, |
| domain = d, |
| domain_specified = True, |
| domain_initial_dot = d.startswith('.'), |
| path = '/', |
| path_specified = True, |
| secure = True, |
| expires = expires, |
| discard = False, |
| comment = None, |
| comment_url = None, |
| rest = None)) |
| |
| cj.save() |
| os.rename(tmp_jar, COOKIE_JAR) |
| return expires |
| |
| def cleanup(): |
| if COOKIE_JAR: |
| for p in [COOKIE_JAR, COOKIE_JAR + '.lock']: |
| if os.path.exists(p): |
| os.remove(p) |
| |
| def refresh_loop(scope, expires): |
| atexit.register(cleanup) |
| expires = expires - REFRESH |
| while True: |
| now = time.time() |
| expires = max(expires, now + RETRY_INTERVAL) |
| while now < expires: |
| time.sleep(expires - now) |
| now = time.time() |
| expires = update_cookie(scope, retry=True) - REFRESH |
| |
| def main(): |
| scope = select_scope() |
| configure_git() |
| expires = update_cookie(scope, retry=False) |
| |
| if '--nofork' not in sys.argv: |
| if os.fork() > 0: |
| sys.exit(0) |
| |
| os.chdir('/') |
| os.setsid() |
| os.umask(0) |
| |
| pid = os.fork() |
| if pid > 0: |
| print 'git-cookie-authdaemon PID %d' % (pid) |
| sys.exit(0) |
| |
| refresh_loop(scope, expires) |
| |
| if __name__ == '__main__': |
| main() |