| #!/usr/bin/python |
| # Copyright (C) 2012 Google Inc. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """ |
| 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 (GCE). On GCE Windows '--nofork' option |
| is needed. '--debug' option is available. |
| """ |
| |
| from __future__ import print_function |
| |
| import atexit |
| import contextlib |
| import json |
| import os |
| import platform |
| import subprocess |
| import sys |
| import time |
| |
| if sys.version_info[0] > 2: |
| # Python 3 imports |
| from urllib.request import urlopen, Request |
| from urllib.error import URLError |
| from urllib.error import HTTPError |
| import http.cookiejar as cookielib |
| else: |
| # Python 2 imports |
| from urllib2 import urlopen, Request, HTTPError, URLError |
| import cookielib |
| |
| 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/cloud-platform', |
| '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 |
| IS_WINDOWS = platform.system() == 'Windows' |
| |
| def read_meta(part): |
| r = Request(META_URL + part) |
| r.add_header('Metadata-Flavor', 'Google') |
| return contextlib.closing(urlopen(r)) |
| |
| def select_scope(): |
| with read_meta('scopes') as d: |
| avail = set(map(lambda s: s.decode('utf-8'), d.read().split())) |
| scopes = [s for s in SUPPORTED_SCOPES if s in avail] |
| if scopes: |
| return next(iter(scopes)) |
| 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 |
| |
| if IS_WINDOWS: |
| # Git for Windows reads %HOMEPATH%/.gitconfig in Command Prompt, |
| # but $HOME/.gitconfig in Cygwin. The two paths can be different. |
| # Cygwin sets env var $HOMEPATH accordingly to %HOMEPATH%. |
| # Set cookie file as %HOMEPATH%/.git-credential-cache/cookie, |
| # so it can be used in both cases. |
| if 'HOMEPATH' in os.environ: |
| homepath = os.environ['HOMEPATH'] |
| else: |
| # When launched as a scheduled task at machine startup |
| # HOMEPATH may not be set. |
| sys.stderr.write('HOMEPATH is not set.\n') |
| sys.exit(1) |
| else: |
| homepath = os.environ['HOME'] |
| dir = os.path.join(homepath, '.git-credential-cache') |
| |
| COOKIE_JAR = os.path.join(dir, 'cookie') |
| if '--debug' in sys.argv: |
| print('Cookie file: %s' % COOKIE_JAR) |
| |
| if os.path.exists(dir): |
| os.chmod(dir, 0o700) |
| else: |
| os.mkdir(dir, 0o700) |
| 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 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']) # Epoch in sec |
| |
| 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() |
| if '--debug' in sys.argv: |
| print('Updating %s.' % COOKIE_JAR) |
| print('Expires: %d, %s, in %d seconds'% ( |
| expires, time.ctime(expires), expires - now)) |
| sys.stdout.flush() |
| if IS_WINDOWS: |
| # os.rename() below on Windows will raise OSError when dst exists. |
| # See https://docs.python.org/2/library/os.html#os.rename |
| if os.path.isfile(COOKIE_JAR): |
| os.remove(COOKIE_JAR) |
| 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 IS_WINDOWS: |
| # os.fork() is not supported on Windows. |
| sys.stderr.write('Add \'--nofork\' on Windows\n') |
| sys.exit(1) |
| |
| 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() |