blob: d4fc036f9026b5f5083087729f0d4284e35c8b59 [file] [log] [blame]
#!/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.path.join(os.environ.get('HOMEDRIVE'), 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()