Added gsutil/ to depot_tools/third_party/
This is needed for https://chromiumcodereview.appspot.com/12042069/ Which uses gsutil to download objects from Google Storage based on SHA1 sums Continuation of: https://chromiumcodereview.appspot.com/12317103/ Rietveld didn't like a giant CL with all of gsutil (kept crashing on upload), The CL is being split into three parts Related: https://chromiumcodereview.appspot.com/12755026/ (gsutil/boto) https://chromiumcodereview.appspot.com/12685010/ (gsutil/gslib) BUG= Review URL: https://codereview.chromium.org/12685009 git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@188896 0039d316-1c4b-4281-b951-d872f2087c98experimental/szager/collated-output
parent
8efca395c0
commit
8d2f67235a
@ -0,0 +1,21 @@
|
|||||||
|
The fancy_urllib library was obtained from
|
||||||
|
http://googleappengine.googlecode.com/svn/trunk/python/lib/fancy_urllib/fancy_urllib/__init__.py
|
||||||
|
under the following license (http://googleappengine.googlecode.com/svn/trunk/python/LICENSE):
|
||||||
|
|
||||||
|
|
||||||
|
GOOGLE APP ENGINE SDK
|
||||||
|
=====================
|
||||||
|
Copyright 2008 Google Inc.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
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.
|
@ -0,0 +1,398 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software
|
||||||
|
# Foundation; All Rights Reserved
|
||||||
|
|
||||||
|
"""A HTTPSConnection/Handler with additional proxy and cert validation features.
|
||||||
|
|
||||||
|
In particular, monkey patches in Python r74203 to provide support for CONNECT
|
||||||
|
proxies and adds SSL cert validation if the ssl module is present.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "{frew,nick.johnson}@google.com (Fred Wulff and Nick Johnson)"
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import httplib
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
from urllib import splittype
|
||||||
|
from urllib import splituser
|
||||||
|
from urllib import splitpasswd
|
||||||
|
|
||||||
|
class InvalidCertificateException(httplib.HTTPException):
|
||||||
|
"""Raised when a certificate is provided with an invalid hostname."""
|
||||||
|
|
||||||
|
def __init__(self, host, cert, reason):
|
||||||
|
"""Constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host: The hostname the connection was made to.
|
||||||
|
cert: The SSL certificate (as a dictionary) the host returned.
|
||||||
|
"""
|
||||||
|
httplib.HTTPException.__init__(self)
|
||||||
|
self.host = host
|
||||||
|
self.cert = cert
|
||||||
|
self.reason = reason
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ('Host %s returned an invalid certificate (%s): %s\n'
|
||||||
|
'To learn more, see '
|
||||||
|
'http://code.google.com/appengine/kb/general.html#rpcssl' %
|
||||||
|
(self.host, self.reason, self.cert))
|
||||||
|
|
||||||
|
def can_validate_certs():
|
||||||
|
"""Return True if we have the SSL package and can validate certificates."""
|
||||||
|
try:
|
||||||
|
import ssl
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _create_fancy_connection(tunnel_host=None, key_file=None,
|
||||||
|
cert_file=None, ca_certs=None):
|
||||||
|
# This abomination brought to you by the fact that
|
||||||
|
# the HTTPHandler creates the connection instance in the middle
|
||||||
|
# of do_open so we need to add the tunnel host to the class.
|
||||||
|
|
||||||
|
class PresetProxyHTTPSConnection(httplib.HTTPSConnection):
|
||||||
|
"""An HTTPS connection that uses a proxy defined by the enclosing scope."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
httplib.HTTPSConnection.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
self._tunnel_host = tunnel_host
|
||||||
|
if tunnel_host:
|
||||||
|
logging.debug("Creating preset proxy https conn: %s", tunnel_host)
|
||||||
|
|
||||||
|
self.key_file = key_file
|
||||||
|
self.cert_file = cert_file
|
||||||
|
self.ca_certs = ca_certs
|
||||||
|
try:
|
||||||
|
import ssl
|
||||||
|
if self.ca_certs:
|
||||||
|
self.cert_reqs = ssl.CERT_REQUIRED
|
||||||
|
else:
|
||||||
|
self.cert_reqs = ssl.CERT_NONE
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _tunnel(self):
|
||||||
|
self._set_hostport(self._tunnel_host, None)
|
||||||
|
logging.info("Connecting through tunnel to: %s:%d",
|
||||||
|
self.host, self.port)
|
||||||
|
self.send("CONNECT %s:%d HTTP/1.0\r\n\r\n" % (self.host, self.port))
|
||||||
|
response = self.response_class(self.sock, strict=self.strict,
|
||||||
|
method=self._method)
|
||||||
|
(_, code, message) = response._read_status()
|
||||||
|
|
||||||
|
if code != 200:
|
||||||
|
self.close()
|
||||||
|
raise socket.error, "Tunnel connection failed: %d %s" % (
|
||||||
|
code, message.strip())
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = response.fp.readline()
|
||||||
|
if line == "\r\n":
|
||||||
|
break
|
||||||
|
|
||||||
|
def _get_valid_hosts_for_cert(self, cert):
|
||||||
|
"""Returns a list of valid host globs for an SSL certificate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cert: A dictionary representing an SSL certificate.
|
||||||
|
Returns:
|
||||||
|
list: A list of valid host globs.
|
||||||
|
"""
|
||||||
|
if 'subjectAltName' in cert:
|
||||||
|
return [x[1] for x in cert['subjectAltName'] if x[0].lower() == 'dns']
|
||||||
|
else:
|
||||||
|
# Return a list of commonName fields
|
||||||
|
return [x[0][1] for x in cert['subject']
|
||||||
|
if x[0][0].lower() == 'commonname']
|
||||||
|
|
||||||
|
def _validate_certificate_hostname(self, cert, hostname):
|
||||||
|
"""Validates that a given hostname is valid for an SSL certificate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cert: A dictionary representing an SSL certificate.
|
||||||
|
hostname: The hostname to test.
|
||||||
|
Returns:
|
||||||
|
bool: Whether or not the hostname is valid for this certificate.
|
||||||
|
"""
|
||||||
|
hosts = self._get_valid_hosts_for_cert(cert)
|
||||||
|
for host in hosts:
|
||||||
|
# Convert the glob-style hostname expression (eg, '*.google.com') into a
|
||||||
|
# valid regular expression.
|
||||||
|
host_re = host.replace('.', '\.').replace('*', '[^.]*')
|
||||||
|
if re.search('^%s$' % (host_re,), hostname, re.I):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
# TODO(frew): When we drop support for <2.6 (in the far distant future),
|
||||||
|
# change this to socket.create_connection.
|
||||||
|
self.sock = _create_connection((self.host, self.port))
|
||||||
|
|
||||||
|
if self._tunnel_host:
|
||||||
|
self._tunnel()
|
||||||
|
|
||||||
|
# ssl and FakeSocket got deprecated. Try for the new hotness of wrap_ssl,
|
||||||
|
# with fallback.
|
||||||
|
try:
|
||||||
|
import ssl
|
||||||
|
self.sock = ssl.wrap_socket(self.sock,
|
||||||
|
keyfile=self.key_file,
|
||||||
|
certfile=self.cert_file,
|
||||||
|
ca_certs=self.ca_certs,
|
||||||
|
cert_reqs=self.cert_reqs)
|
||||||
|
|
||||||
|
if self.cert_reqs & ssl.CERT_REQUIRED:
|
||||||
|
cert = self.sock.getpeercert()
|
||||||
|
hostname = self.host.split(':', 0)[0]
|
||||||
|
if not self._validate_certificate_hostname(cert, hostname):
|
||||||
|
raise InvalidCertificateException(hostname, cert,
|
||||||
|
'hostname mismatch')
|
||||||
|
except ImportError:
|
||||||
|
ssl = socket.ssl(self.sock,
|
||||||
|
keyfile=self.key_file,
|
||||||
|
certfile=self.cert_file)
|
||||||
|
self.sock = httplib.FakeSocket(self.sock, ssl)
|
||||||
|
|
||||||
|
return PresetProxyHTTPSConnection
|
||||||
|
|
||||||
|
|
||||||
|
# Here to end of _create_connection copied wholesale from Python 2.6"s socket.py
|
||||||
|
_GLOBAL_DEFAULT_TIMEOUT = object()
|
||||||
|
|
||||||
|
|
||||||
|
def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT):
|
||||||
|
"""Connect to *address* and return the socket object.
|
||||||
|
|
||||||
|
Convenience function. Connect to *address* (a 2-tuple ``(host,
|
||||||
|
port)``) and return the socket object. Passing the optional
|
||||||
|
*timeout* parameter will set the timeout on the socket instance
|
||||||
|
before attempting to connect. If no *timeout* is supplied, the
|
||||||
|
global default timeout setting returned by :func:`getdefaulttimeout`
|
||||||
|
is used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
msg = "getaddrinfo returns an empty list"
|
||||||
|
host, port = address
|
||||||
|
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
|
||||||
|
af, socktype, proto, canonname, sa = res
|
||||||
|
sock = None
|
||||||
|
try:
|
||||||
|
sock = socket.socket(af, socktype, proto)
|
||||||
|
if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
sock.connect(sa)
|
||||||
|
return sock
|
||||||
|
|
||||||
|
except socket.error, msg:
|
||||||
|
if sock is not None:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
raise socket.error, msg
|
||||||
|
|
||||||
|
|
||||||
|
class FancyRequest(urllib2.Request):
|
||||||
|
"""A request that allows the use of a CONNECT proxy."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
urllib2.Request.__init__(self, *args, **kwargs)
|
||||||
|
self._tunnel_host = None
|
||||||
|
self._key_file = None
|
||||||
|
self._cert_file = None
|
||||||
|
self._ca_certs = None
|
||||||
|
|
||||||
|
def set_proxy(self, host, type):
|
||||||
|
saved_type = None
|
||||||
|
|
||||||
|
if self.get_type() == "https" and not self._tunnel_host:
|
||||||
|
self._tunnel_host = self.get_host()
|
||||||
|
saved_type = self.get_type()
|
||||||
|
urllib2.Request.set_proxy(self, host, type)
|
||||||
|
|
||||||
|
if saved_type:
|
||||||
|
# Don't set self.type, we want to preserve the
|
||||||
|
# type for tunneling.
|
||||||
|
self.type = saved_type
|
||||||
|
|
||||||
|
def set_ssl_info(self, key_file=None, cert_file=None, ca_certs=None):
|
||||||
|
self._key_file = key_file
|
||||||
|
self._cert_file = cert_file
|
||||||
|
self._ca_certs = ca_certs
|
||||||
|
|
||||||
|
|
||||||
|
class FancyProxyHandler(urllib2.ProxyHandler):
|
||||||
|
"""A ProxyHandler that works with CONNECT-enabled proxies."""
|
||||||
|
|
||||||
|
# Taken verbatim from /usr/lib/python2.5/urllib2.py
|
||||||
|
def _parse_proxy(self, proxy):
|
||||||
|
"""Return (scheme, user, password, host/port) given a URL or an authority.
|
||||||
|
|
||||||
|
If a URL is supplied, it must have an authority (host:port) component.
|
||||||
|
According to RFC 3986, having an authority component means the URL must
|
||||||
|
have two slashes after the scheme:
|
||||||
|
|
||||||
|
>>> _parse_proxy('file:/ftp.example.com/')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
ValueError: proxy URL with no authority: 'file:/ftp.example.com/'
|
||||||
|
|
||||||
|
The first three items of the returned tuple may be None.
|
||||||
|
|
||||||
|
Examples of authority parsing:
|
||||||
|
|
||||||
|
>>> _parse_proxy('proxy.example.com')
|
||||||
|
(None, None, None, 'proxy.example.com')
|
||||||
|
>>> _parse_proxy('proxy.example.com:3128')
|
||||||
|
(None, None, None, 'proxy.example.com:3128')
|
||||||
|
|
||||||
|
The authority component may optionally include userinfo (assumed to be
|
||||||
|
username:password):
|
||||||
|
|
||||||
|
>>> _parse_proxy('joe:password@proxy.example.com')
|
||||||
|
(None, 'joe', 'password', 'proxy.example.com')
|
||||||
|
>>> _parse_proxy('joe:password@proxy.example.com:3128')
|
||||||
|
(None, 'joe', 'password', 'proxy.example.com:3128')
|
||||||
|
|
||||||
|
Same examples, but with URLs instead:
|
||||||
|
|
||||||
|
>>> _parse_proxy('http://proxy.example.com/')
|
||||||
|
('http', None, None, 'proxy.example.com')
|
||||||
|
>>> _parse_proxy('http://proxy.example.com:3128/')
|
||||||
|
('http', None, None, 'proxy.example.com:3128')
|
||||||
|
>>> _parse_proxy('http://joe:password@proxy.example.com/')
|
||||||
|
('http', 'joe', 'password', 'proxy.example.com')
|
||||||
|
>>> _parse_proxy('http://joe:password@proxy.example.com:3128')
|
||||||
|
('http', 'joe', 'password', 'proxy.example.com:3128')
|
||||||
|
|
||||||
|
Everything after the authority is ignored:
|
||||||
|
|
||||||
|
>>> _parse_proxy('ftp://joe:password@proxy.example.com/rubbish:3128')
|
||||||
|
('ftp', 'joe', 'password', 'proxy.example.com')
|
||||||
|
|
||||||
|
Test for no trailing '/' case:
|
||||||
|
|
||||||
|
>>> _parse_proxy('http://joe:password@proxy.example.com')
|
||||||
|
('http', 'joe', 'password', 'proxy.example.com')
|
||||||
|
|
||||||
|
"""
|
||||||
|
scheme, r_scheme = splittype(proxy)
|
||||||
|
if not r_scheme.startswith("/"):
|
||||||
|
# authority
|
||||||
|
scheme = None
|
||||||
|
authority = proxy
|
||||||
|
else:
|
||||||
|
# URL
|
||||||
|
if not r_scheme.startswith("//"):
|
||||||
|
raise ValueError("proxy URL with no authority: %r" % proxy)
|
||||||
|
# We have an authority, so for RFC 3986-compliant URLs (by ss 3.
|
||||||
|
# and 3.3.), path is empty or starts with '/'
|
||||||
|
end = r_scheme.find("/", 2)
|
||||||
|
if end == -1:
|
||||||
|
end = None
|
||||||
|
authority = r_scheme[2:end]
|
||||||
|
userinfo, hostport = splituser(authority)
|
||||||
|
if userinfo is not None:
|
||||||
|
user, password = splitpasswd(userinfo)
|
||||||
|
else:
|
||||||
|
user = password = None
|
||||||
|
return scheme, user, password, hostport
|
||||||
|
|
||||||
|
def proxy_open(self, req, proxy, type):
|
||||||
|
# This block is copied wholesale from Python2.6 urllib2.
|
||||||
|
# It is idempotent, so the superclass method call executes as normal
|
||||||
|
# if invoked.
|
||||||
|
orig_type = req.get_type()
|
||||||
|
proxy_type, user, password, hostport = self._parse_proxy(proxy)
|
||||||
|
if proxy_type is None:
|
||||||
|
proxy_type = orig_type
|
||||||
|
if user and password:
|
||||||
|
user_pass = "%s:%s" % (urllib2.unquote(user), urllib2.unquote(password))
|
||||||
|
creds = base64.b64encode(user_pass).strip()
|
||||||
|
# Later calls overwrite earlier calls for the same header
|
||||||
|
req.add_header("Proxy-authorization", "Basic " + creds)
|
||||||
|
hostport = urllib2.unquote(hostport)
|
||||||
|
req.set_proxy(hostport, proxy_type)
|
||||||
|
# This condition is the change
|
||||||
|
if orig_type == "https":
|
||||||
|
return None
|
||||||
|
|
||||||
|
return urllib2.ProxyHandler.proxy_open(self, req, proxy, type)
|
||||||
|
|
||||||
|
|
||||||
|
class FancyHTTPSHandler(urllib2.HTTPSHandler):
|
||||||
|
"""An HTTPSHandler that works with CONNECT-enabled proxies."""
|
||||||
|
|
||||||
|
def do_open(self, http_class, req):
|
||||||
|
# Intentionally very specific so as to opt for false negatives
|
||||||
|
# rather than false positives.
|
||||||
|
try:
|
||||||
|
return urllib2.HTTPSHandler.do_open(
|
||||||
|
self,
|
||||||
|
_create_fancy_connection(req._tunnel_host,
|
||||||
|
req._key_file,
|
||||||
|
req._cert_file,
|
||||||
|
req._ca_certs),
|
||||||
|
req)
|
||||||
|
except urllib2.URLError, url_error:
|
||||||
|
try:
|
||||||
|
import ssl
|
||||||
|
if (type(url_error.reason) == ssl.SSLError and
|
||||||
|
url_error.reason.args[0] == 1):
|
||||||
|
# Display the reason to the user. Need to use args for python2.5
|
||||||
|
# compat.
|
||||||
|
raise InvalidCertificateException(req.host, '',
|
||||||
|
url_error.reason.args[1])
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise url_error
|
||||||
|
|
||||||
|
|
||||||
|
# We have to implement this so that we persist the tunneling behavior
|
||||||
|
# through redirects.
|
||||||
|
class FancyRedirectHandler(urllib2.HTTPRedirectHandler):
|
||||||
|
"""A redirect handler that persists CONNECT-enabled proxy information."""
|
||||||
|
|
||||||
|
def redirect_request(self, req, *args, **kwargs):
|
||||||
|
new_req = urllib2.HTTPRedirectHandler.redirect_request(
|
||||||
|
self, req, *args, **kwargs)
|
||||||
|
# Same thing as in our set_proxy implementation, but in this case
|
||||||
|
# we"ve only got a Request to work with, so it was this or copy
|
||||||
|
# everything over piecemeal.
|
||||||
|
#
|
||||||
|
# Note that we do not persist tunneling behavior from an http request
|
||||||
|
# to an https request, because an http request does not set _tunnel_host.
|
||||||
|
#
|
||||||
|
# Also note that in Python < 2.6, you will get an error in
|
||||||
|
# FancyHTTPSHandler.do_open() on an https urllib2.Request that uses an http
|
||||||
|
# proxy, since the proxy type will be set to http instead of https.
|
||||||
|
# (FancyRequest, and urllib2.Request in Python >= 2.6 set the proxy type to
|
||||||
|
# https.) Such an urllib2.Request could result from this redirect
|
||||||
|
# if you are redirecting from an http request (since an an http request
|
||||||
|
# does not have _tunnel_host set, and thus you will not set the proxy
|
||||||
|
# in the code below), and if you have defined a proxy for https in, say,
|
||||||
|
# FancyProxyHandler, and that proxy has type http.
|
||||||
|
if hasattr(req, "_tunnel_host") and isinstance(new_req, urllib2.Request):
|
||||||
|
if new_req.get_type() == "https":
|
||||||
|
if req._tunnel_host:
|
||||||
|
# req is proxied, so copy the proxy info.
|
||||||
|
new_req._tunnel_host = new_req.get_host()
|
||||||
|
new_req.set_proxy(req.host, "https")
|
||||||
|
else:
|
||||||
|
# req is not proxied, so just make sure _tunnel_host is defined.
|
||||||
|
new_req._tunnel_host = None
|
||||||
|
new_req.type = "https"
|
||||||
|
if hasattr(req, "_key_file") and isinstance(new_req, urllib2.Request):
|
||||||
|
# Copy the auxiliary data in case this or any further redirect is https
|
||||||
|
new_req._key_file = req._key_file
|
||||||
|
new_req._cert_file = req._cert_file
|
||||||
|
new_req._ca_certs = req._ca_certs
|
||||||
|
|
||||||
|
return new_req
|
@ -0,0 +1 @@
|
|||||||
|
010822c61d38d70ac23600bc955fccf5
|
@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
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.
|
@ -0,0 +1,295 @@
|
|||||||
|
GOOGLE APP ENGINE SDK
|
||||||
|
**************************************************************************
|
||||||
|
Copyright 2008 Google Inc.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
fancy_urllib
|
||||||
|
**************************************************************************
|
||||||
|
|
||||||
|
# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software
|
||||||
|
# Foundation; All Rights Reserved
|
||||||
|
|
||||||
|
|
||||||
|
A. HISTORY OF THE SOFTWARE
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Python was created in the early 1990s by Guido van Rossum at Stichting
|
||||||
|
Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
|
||||||
|
as a successor of a language called ABC. Guido remains Python's
|
||||||
|
principal author, although it includes many contributions from others.
|
||||||
|
|
||||||
|
In 1995, Guido continued his work on Python at the Corporation for
|
||||||
|
National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
|
||||||
|
in Reston, Virginia where he released several versions of the
|
||||||
|
software.
|
||||||
|
|
||||||
|
In May 2000, Guido and the Python core development team moved to
|
||||||
|
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
|
||||||
|
year, the PythonLabs team moved to Digital Creations (now Zope
|
||||||
|
Corporation, see http://www.zope.com). In 2001, the Python Software
|
||||||
|
Foundation (PSF, see http://www.python.org/psf/) was formed, a
|
||||||
|
non-profit organization created specifically to own Python-related
|
||||||
|
Intellectual Property. Zope Corporation is a sponsoring member of
|
||||||
|
the PSF.
|
||||||
|
|
||||||
|
All Python releases are Open Source (see http://www.opensource.org for
|
||||||
|
the Open Source Definition). Historically, most, but not all, Python
|
||||||
|
releases have also been GPL-compatible; the table below summarizes
|
||||||
|
the various releases.
|
||||||
|
|
||||||
|
Release Derived Year Owner GPL-
|
||||||
|
from compatible? (1)
|
||||||
|
|
||||||
|
0.9.0 thru 1.2 1991-1995 CWI yes
|
||||||
|
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
|
||||||
|
1.6 1.5.2 2000 CNRI no
|
||||||
|
2.0 1.6 2000 BeOpen.com no
|
||||||
|
1.6.1 1.6 2001 CNRI yes (2)
|
||||||
|
2.1 2.0+1.6.1 2001 PSF no
|
||||||
|
2.0.1 2.0+1.6.1 2001 PSF yes
|
||||||
|
2.1.1 2.1+2.0.1 2001 PSF yes
|
||||||
|
2.2 2.1.1 2001 PSF yes
|
||||||
|
2.1.2 2.1.1 2002 PSF yes
|
||||||
|
2.1.3 2.1.2 2002 PSF yes
|
||||||
|
2.2.1 2.2 2002 PSF yes
|
||||||
|
2.2.2 2.2.1 2002 PSF yes
|
||||||
|
2.2.3 2.2.2 2003 PSF yes
|
||||||
|
2.3 2.2.2 2002-2003 PSF yes
|
||||||
|
2.3.1 2.3 2002-2003 PSF yes
|
||||||
|
2.3.2 2.3.1 2002-2003 PSF yes
|
||||||
|
2.3.3 2.3.2 2002-2003 PSF yes
|
||||||
|
2.3.4 2.3.3 2004 PSF yes
|
||||||
|
2.3.5 2.3.4 2005 PSF yes
|
||||||
|
2.4 2.3 2004 PSF yes
|
||||||
|
2.4.1 2.4 2005 PSF yes
|
||||||
|
2.4.2 2.4.1 2005 PSF yes
|
||||||
|
2.4.3 2.4.2 2006 PSF yes
|
||||||
|
2.5 2.4 2006 PSF yes
|
||||||
|
2.5.1 2.5 2007 PSF yes
|
||||||
|
|
||||||
|
Footnotes:
|
||||||
|
|
||||||
|
(1) GPL-compatible doesn't mean that we're distributing Python under
|
||||||
|
the GPL. All Python licenses, unlike the GPL, let you distribute
|
||||||
|
a modified version without making your changes open source. The
|
||||||
|
GPL-compatible licenses make it possible to combine Python with
|
||||||
|
other software that is released under the GPL; the others don't.
|
||||||
|
|
||||||
|
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
|
||||||
|
because its license has a choice of law clause. According to
|
||||||
|
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
|
||||||
|
is "not incompatible" with the GPL.
|
||||||
|
|
||||||
|
Thanks to the many outside volunteers who have worked under Guido's
|
||||||
|
direction to make these releases possible.
|
||||||
|
|
||||||
|
|
||||||
|
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||||
|
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||||
|
otherwise using this software ("Python") in source or binary form and
|
||||||
|
its associated documentation.
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this License Agreement, PSF
|
||||||
|
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||||
|
license to reproduce, analyze, test, perform and/or display publicly,
|
||||||
|
prepare derivative works, distribute, and otherwise use Python
|
||||||
|
alone or in any derivative version, provided, however, that PSF's
|
||||||
|
License Agreement and PSF's notice of copyright, i.e., "Copyright (c)
|
||||||
|
2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software Foundation;
|
||||||
|
All Rights Reserved" are retained in Python alone or in any derivative
|
||||||
|
version prepared by Licensee.
|
||||||
|
|
||||||
|
3. In the event Licensee prepares a derivative work that is based on
|
||||||
|
or incorporates Python or any part thereof, and wants to make
|
||||||
|
the derivative work available to others as provided herein, then
|
||||||
|
Licensee hereby agrees to include in any such work a brief summary of
|
||||||
|
the changes made to Python.
|
||||||
|
|
||||||
|
4. PSF is making Python available to Licensee on an "AS IS"
|
||||||
|
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||||
|
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||||
|
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||||
|
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
6. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
7. Nothing in this License Agreement shall be deemed to create any
|
||||||
|
relationship of agency, partnership, or joint venture between PSF and
|
||||||
|
Licensee. This License Agreement does not grant permission to use PSF
|
||||||
|
trademarks or trade name in a trademark sense to endorse or promote
|
||||||
|
products or services of Licensee, or any third party.
|
||||||
|
|
||||||
|
8. By copying, installing or otherwise using Python, Licensee
|
||||||
|
agrees to be bound by the terms and conditions of this License
|
||||||
|
Agreement.
|
||||||
|
|
||||||
|
|
||||||
|
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
|
||||||
|
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
|
||||||
|
Individual or Organization ("Licensee") accessing and otherwise using
|
||||||
|
this software in source or binary form and its associated
|
||||||
|
documentation ("the Software").
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this BeOpen Python License
|
||||||
|
Agreement, BeOpen hereby grants Licensee a non-exclusive,
|
||||||
|
royalty-free, world-wide license to reproduce, analyze, test, perform
|
||||||
|
and/or display publicly, prepare derivative works, distribute, and
|
||||||
|
otherwise use the Software alone or in any derivative version,
|
||||||
|
provided, however, that the BeOpen Python License is retained in the
|
||||||
|
Software, alone or in any derivative version prepared by Licensee.
|
||||||
|
|
||||||
|
3. BeOpen is making the Software available to Licensee on an "AS IS"
|
||||||
|
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
|
||||||
|
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
|
||||||
|
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
|
||||||
|
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
5. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
6. This License Agreement shall be governed by and interpreted in all
|
||||||
|
respects by the law of the State of California, excluding conflict of
|
||||||
|
law provisions. Nothing in this License Agreement shall be deemed to
|
||||||
|
create any relationship of agency, partnership, or joint venture
|
||||||
|
between BeOpen and Licensee. This License Agreement does not grant
|
||||||
|
permission to use BeOpen trademarks or trade names in a trademark
|
||||||
|
sense to endorse or promote products or services of Licensee, or any
|
||||||
|
third party. As an exception, the "BeOpen Python" logos available at
|
||||||
|
http://www.pythonlabs.com/logos.html may be used according to the
|
||||||
|
permissions granted on that web page.
|
||||||
|
|
||||||
|
7. By copying, installing or otherwise using the software, Licensee
|
||||||
|
agrees to be bound by the terms and conditions of this License
|
||||||
|
Agreement.
|
||||||
|
|
||||||
|
|
||||||
|
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between the Corporation for National
|
||||||
|
Research Initiatives, having an office at 1895 Preston White Drive,
|
||||||
|
Reston, VA 20191 ("CNRI"), and the Individual or Organization
|
||||||
|
("Licensee") accessing and otherwise using Python 1.6.1 software in
|
||||||
|
source or binary form and its associated documentation.
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this License Agreement, CNRI
|
||||||
|
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||||
|
license to reproduce, analyze, test, perform and/or display publicly,
|
||||||
|
prepare derivative works, distribute, and otherwise use Python 1.6.1
|
||||||
|
alone or in any derivative version, provided, however, that CNRI's
|
||||||
|
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
|
||||||
|
1995-2001 Corporation for National Research Initiatives; All Rights
|
||||||
|
Reserved" are retained in Python 1.6.1 alone or in any derivative
|
||||||
|
version prepared by Licensee. Alternately, in lieu of CNRI's License
|
||||||
|
Agreement, Licensee may substitute the following text (omitting the
|
||||||
|
quotes): "Python 1.6.1 is made available subject to the terms and
|
||||||
|
conditions in CNRI's License Agreement. This Agreement together with
|
||||||
|
Python 1.6.1 may be located on the Internet using the following
|
||||||
|
unique, persistent identifier (known as a handle): 1895.22/1013. This
|
||||||
|
Agreement may also be obtained from a proxy server on the Internet
|
||||||
|
using the following URL: http://hdl.handle.net/1895.22/1013".
|
||||||
|
|
||||||
|
3. In the event Licensee prepares a derivative work that is based on
|
||||||
|
or incorporates Python 1.6.1 or any part thereof, and wants to make
|
||||||
|
the derivative work available to others as provided herein, then
|
||||||
|
Licensee hereby agrees to include in any such work a brief summary of
|
||||||
|
the changes made to Python 1.6.1.
|
||||||
|
|
||||||
|
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
|
||||||
|
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||||
|
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||||
|
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
|
||||||
|
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
6. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
7. This License Agreement shall be governed by the federal
|
||||||
|
intellectual property law of the United States, including without
|
||||||
|
limitation the federal copyright law, and, to the extent such
|
||||||
|
U.S. federal law does not apply, by the law of the Commonwealth of
|
||||||
|
Virginia, excluding Virginia's conflict of law provisions.
|
||||||
|
Notwithstanding the foregoing, with regard to derivative works based
|
||||||
|
on Python 1.6.1 that incorporate non-separable material that was
|
||||||
|
previously distributed under the GNU General Public License (GPL), the
|
||||||
|
law of the Commonwealth of Virginia shall govern this License
|
||||||
|
Agreement only as to issues arising under or with respect to
|
||||||
|
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
|
||||||
|
License Agreement shall be deemed to create any relationship of
|
||||||
|
agency, partnership, or joint venture between CNRI and Licensee. This
|
||||||
|
License Agreement does not grant permission to use CNRI trademarks or
|
||||||
|
trade name in a trademark sense to endorse or promote products or
|
||||||
|
services of Licensee, or any third party.
|
||||||
|
|
||||||
|
8. By clicking on the "ACCEPT" button where indicated, or by copying,
|
||||||
|
installing or otherwise using Python 1.6.1, Licensee agrees to be
|
||||||
|
bound by the terms and conditions of this License Agreement.
|
||||||
|
|
||||||
|
ACCEPT
|
||||||
|
|
||||||
|
|
||||||
|
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
|
||||||
|
The Netherlands. All rights reserved.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and distribute this software and its
|
||||||
|
documentation for any purpose and without fee is hereby granted,
|
||||||
|
provided that the above copyright notice appear in all copies and that
|
||||||
|
both that copyright notice and this permission notice appear in
|
||||||
|
supporting documentation, and that the name of Stichting Mathematisch
|
||||||
|
Centrum or CWI not be used in advertising or publicity pertaining to
|
||||||
|
distribution of the software without specific, written prior
|
||||||
|
permission.
|
||||||
|
|
||||||
|
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||||
|
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
|
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
|
||||||
|
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||||
|
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
@ -0,0 +1,5 @@
|
|||||||
|
include gsutil COPYING VERSION LICENSE.third_party README setup.py pkg_util.py
|
||||||
|
recursive-include gslib *
|
||||||
|
recursive-include oauth2_plugin *
|
||||||
|
recursive-include third_party *
|
||||||
|
recursive-include boto *
|
@ -0,0 +1,38 @@
|
|||||||
|
This directory contains the Python command line tool gsutil, which Google
|
||||||
|
has released as open source, to demonstrate the Google Storage API and to
|
||||||
|
provide a tool for manipulating data in the system.
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
Gsutil requires Python 2.6 or later.
|
||||||
|
|
||||||
|
To install gsutil take the following steps:
|
||||||
|
|
||||||
|
1. Pick a place where you want to install the software. You can
|
||||||
|
install the code wherever you prefer; for brevity the instructions below
|
||||||
|
assume you want to install in $HOME/gsutil.
|
||||||
|
|
||||||
|
2. To install gsutil on Linux/Unix or MacOS, open a shell window, change
|
||||||
|
directories to where you downloaded the gsutil.tar.gz file, and do this:
|
||||||
|
% tar xfz gsutil.tar.gz -C $HOME
|
||||||
|
|
||||||
|
Then add the following line to your $HOME/.bashrc shell initialization
|
||||||
|
file:
|
||||||
|
export PATH=${PATH}:$HOME/gsutil
|
||||||
|
|
||||||
|
The next time you start a shell you should be able to run gsutil from
|
||||||
|
the command line.
|
||||||
|
|
||||||
|
3. To install gsutil on Windows, install cygwin (http://www.cygwin.com/),
|
||||||
|
with at least version 2.6.5 of Python. Once you have that, start a shell
|
||||||
|
and follow the Linux instructions above for unpacking and installing gsutil.
|
||||||
|
|
||||||
|
4. The first time you try to run gsutil, it will detect that you have no
|
||||||
|
configuration file containing your credentials, interactively prompt you,
|
||||||
|
and create the file.
|
||||||
|
|
||||||
|
After this you can use the tool. Running gsutil with with no arguments
|
||||||
|
will print a help summary.
|
||||||
|
|
||||||
|
For more information on installing and using gsutil, see
|
||||||
|
<http://code.google.com/apis/storage/docs/gsutil.html>.
|
@ -0,0 +1,19 @@
|
|||||||
|
Name: gsutil
|
||||||
|
URL: https://github.com/GoogleCloudPlatform/gsutil
|
||||||
|
Version: 3.25
|
||||||
|
License: Apache 2.0
|
||||||
|
|
||||||
|
Description:
|
||||||
|
Set of tools to allow querying, uploading, and downloading objects from
|
||||||
|
Google Storage.
|
||||||
|
|
||||||
|
Modifications:
|
||||||
|
* Removed gsutil/gslib/commands/test.py
|
||||||
|
* Removed gsutil/pkg_gen.sh
|
||||||
|
* Removed gsutil/gslib/tests/
|
||||||
|
* Moved gsutil/boto as a depot_tools third_party lib
|
||||||
|
* Moved gsutil/third_party into our own third_party directory
|
||||||
|
* Append sys.path in gsutil/gsutil to find the moved third_party modules
|
||||||
|
* Updated checksum ce71ac982f1148315e7fa65cff2f83e8 -> bf29190007bc7557c33806367ee3ce9e
|
||||||
|
|
||||||
|
Full license is in the COPYING file.
|
@ -0,0 +1,49 @@
|
|||||||
|
Package Generation Notes for gsutil
|
||||||
|
|
||||||
|
Gsutil can be distributed in one of three ways:
|
||||||
|
|
||||||
|
1. legacy mode - User unpacks archive file into a private directory tree
|
||||||
|
and maintains his/her own private copy of gsutil, boto, etc. This is the
|
||||||
|
only supported installation mode for Windows users.
|
||||||
|
|
||||||
|
2. enterprise mode - User unpacks the gsutil archive file and runs
|
||||||
|
'python setup.py install' (as root), which installs everything into
|
||||||
|
a shared directory tree (/usr/share/gsutil) with a symlink from
|
||||||
|
/usr/bin/gsutil to /usr/share/gsutil/gsutil to provide easy access to
|
||||||
|
the shared gsutil command. In enterprise mode, the software gets installed
|
||||||
|
in one shared location, which makes it easier to install, update and
|
||||||
|
manage gsutil for a community of users.
|
||||||
|
|
||||||
|
NOTE: Enterprise mode (installing gsutil via setup.py) is no longer
|
||||||
|
officially supported - unpacking the zip file into a directory is the
|
||||||
|
preferred method for installing gsutil for both shared and private
|
||||||
|
configurations.
|
||||||
|
|
||||||
|
3. rpm mode - User installs the gsutil rpm package file on a Red Hat
|
||||||
|
Linux system using the rpm command. The resulting installation image
|
||||||
|
looks precisely the same as the results of installing with enterprise
|
||||||
|
mode, i.e. a shared directory tree (/usr/share/gsutil) with a symlink
|
||||||
|
from /usr/bin/gsutil. rpm mode is intended for enterprises that want
|
||||||
|
a stable release that does not necessarily contain the latest changes.
|
||||||
|
|
||||||
|
All three modes derive their inventory from a common text file called
|
||||||
|
MANIFEST.in. If you want to add one or more new files or directories,
|
||||||
|
you only need to edit that one file and all three installation modes
|
||||||
|
will automatically inherit the change(s).
|
||||||
|
|
||||||
|
GENERATING PACKAGE FILES
|
||||||
|
|
||||||
|
First update the VERSION file and the gsutil.spec files to reflect the
|
||||||
|
new version number.
|
||||||
|
|
||||||
|
Legacy mode and enterprise mode are both embodied in the same gsutil
|
||||||
|
archive file, the only difference being that the latter entails running
|
||||||
|
one additional command after unpacking the gsutil archive file. So the
|
||||||
|
same archive file we've always distributed for gsutil will be used for
|
||||||
|
both legacy and enterprise installation modes.
|
||||||
|
|
||||||
|
For rpm mode, there's a new tool call pkg_gen.sh, which when run with no
|
||||||
|
arguments creates an rpm file at this location:
|
||||||
|
|
||||||
|
$HOME/rpmbuild/RPMS/noarch/gsutil-2.0-1.noarch.rpm
|
||||||
|
|
@ -0,0 +1,825 @@
|
|||||||
|
Release 3.25 (release-date: 2013-02-21)
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed two version-specific URI bugs:
|
||||||
|
|
||||||
|
1. gsutil cp -r gs://bucket1 gs://bucket2 would create objects in bucket2
|
||||||
|
with names corresponding to version-specific URIs in bucket1 (e.g.,
|
||||||
|
gs://bucket2/obj#1361417568482000, where the "#1361417568482000" part was
|
||||||
|
part of the object name, not the object's generation).
|
||||||
|
|
||||||
|
This problem similarly caused gsutil cp -r gs://bucket1 ./dir to create
|
||||||
|
files names corresponding to version-specific URIs in bucket1.
|
||||||
|
|
||||||
|
2. gsutil rm -a gs://bucket/obj would attempt to delete the same object
|
||||||
|
twice, getting a NoSuchKey error on the second attempt.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.24 (release-date: 2013-02-19)
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed bug that caused attempt to dupe-encode a unicode filename.
|
||||||
|
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Refactored retry logic from setmeta and chacl to use @Retry decorator.
|
||||||
|
|
||||||
|
- Moved @Retry decorator to third_party.
|
||||||
|
|
||||||
|
- Fixed flaky tests.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.23 (release-date: 2013-02-16)
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Make version-specific URI parsing more robust. This fixes a bug where
|
||||||
|
listing buckets in certain cases would result in the error
|
||||||
|
'BucketStorageUri' object has no attribute 'version_specific_uri'
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.22 (release-date: 2013-02-15)
|
||||||
|
|
||||||
|
|
||||||
|
New Features
|
||||||
|
------------
|
||||||
|
|
||||||
|
- Implemented new chacl command, which makes it easy to add and remove bucket
|
||||||
|
and object ACL grants without having to edit XML (like the older setacl
|
||||||
|
command).
|
||||||
|
|
||||||
|
- Implemented new "daisy-chain" copying mode, which allows cross-provider
|
||||||
|
copies to run without buffering to local disk, and to use resumable uploads.
|
||||||
|
This copying mode also allows copying between locations and between storage
|
||||||
|
classes, using the new gsutil cp -D option. (Daisy-chain copying is the
|
||||||
|
default when copying between providers, but must be explicitly requested for
|
||||||
|
the other cases to keep costs and performance expectations clear.)
|
||||||
|
|
||||||
|
- Implemented new perfdiag command to run a diagnostic test against
|
||||||
|
a bucket, collect system information, and report results. Useful
|
||||||
|
when working with Google Cloud Storage team to resolve questions
|
||||||
|
about performance.
|
||||||
|
|
||||||
|
- Added SIGQUIT (^\) handler, to allow breakpointing a running gsutil.
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed bug where gsutil setwebcfg signature didn't match with
|
||||||
|
HMAC authentication.
|
||||||
|
|
||||||
|
- Fixed ASCII codec decode error when constructing tracker filename
|
||||||
|
from non-7bit ASCII intput filename.
|
||||||
|
|
||||||
|
- Changed boto auth plugin framework to allow multiple plugins
|
||||||
|
supporting requested capability, which fixes gsutil exception
|
||||||
|
that used to happen where a GCE user had a service account
|
||||||
|
configured and then ran gsutil config.
|
||||||
|
|
||||||
|
- Changed Command.Apply method to be resilient to name expansion
|
||||||
|
exceptions. Before this change, if an exception was raised
|
||||||
|
during iteration of NameExpansionResult, the parent process
|
||||||
|
would immediately stop execution, causing the
|
||||||
|
_EOF_NAME_EXPANSION_RESULT to never be sent to child processes.
|
||||||
|
This resulted in the process hanging forever.
|
||||||
|
|
||||||
|
- Fixed various bugs for gsutil running on Windows:
|
||||||
|
- Fixed various places from a hard-coded '/' to os.sep.
|
||||||
|
- Fixed a bug in the cp command where it was using the destination
|
||||||
|
URI's .delim property instead of the source URI.
|
||||||
|
- Fixed a bug in the cp command's _SrcDstSame function by
|
||||||
|
simplifying it to use os.path.normpath.
|
||||||
|
- Fixed windows bug in tests/util.py _NormalizeURI function.
|
||||||
|
- Fixed ZeroDivisionError sometimes happening during unit tests
|
||||||
|
on Windows.
|
||||||
|
|
||||||
|
- Fixed gsutil rm bug that caused exit status 1 when encountered
|
||||||
|
non-existent URI.
|
||||||
|
|
||||||
|
- Fixed support for gsutil cp file -.
|
||||||
|
|
||||||
|
- Added preconditions and retry logic to setmeta command, to
|
||||||
|
enforce concurrency control.
|
||||||
|
|
||||||
|
- Fixed bug in copying subdirs to subdirs.
|
||||||
|
|
||||||
|
- Fixed cases where boto debug_level caused too much or too little
|
||||||
|
logging:
|
||||||
|
- resumable and one-shot uploads weren't showing response headers
|
||||||
|
when connection.debug > 0.
|
||||||
|
- payload was showing up in debug output when connection.debug
|
||||||
|
< 4 for streaming uploads.
|
||||||
|
|
||||||
|
- Removed XML parsing from setacl. The previous implementation
|
||||||
|
relied on loose XML handling, which could truncate what it sends
|
||||||
|
to the service, allowing invalid XML to be specified by the
|
||||||
|
user. Instead now the ACL XML is passed verbatim and we rely
|
||||||
|
on server-side schema enforcement.
|
||||||
|
|
||||||
|
- Added user-agent header to resumable uploads.
|
||||||
|
|
||||||
|
- Fixed reporting bits/s when it was really bytes/s.
|
||||||
|
|
||||||
|
- Changed so we now pass headers with API version & project ID
|
||||||
|
to create_bucket().
|
||||||
|
|
||||||
|
- Made "gsutil rm -r gs://bucket/folder" remove xyz_$folder$ object
|
||||||
|
(which is created by various GUI tools).
|
||||||
|
|
||||||
|
- Fixed bug where gsutil binary was shipped with protection 750
|
||||||
|
instead of 755.
|
||||||
|
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Reworked versioned object handling:
|
||||||
|
- Removed need for commands to specify -v option to parse
|
||||||
|
versions. Versioned URIs are now uniformly handled by all
|
||||||
|
commands.
|
||||||
|
- Refactored StorageUri parsing that had been split across
|
||||||
|
storage_uri and conveience; made versioned URIs render with
|
||||||
|
version string so StorageUri is round-trippable (boto change).
|
||||||
|
- Implemented gsutil cp -v option for printing the version-specific
|
||||||
|
URI that was just created.
|
||||||
|
- Added error detail for attempt to delete non-empty versioned
|
||||||
|
bucket. Also added versioning state to ls -L -b gs://bucket
|
||||||
|
output.
|
||||||
|
- Changed URI parsing to use pre-compiled regex's.
|
||||||
|
- Other bug fixes.
|
||||||
|
|
||||||
|
- Rewrote/deepened/improved various parts of built-in help:
|
||||||
|
- Updated 'gsutil help dev'.
|
||||||
|
- Fixed help command handling when terminal does not have the
|
||||||
|
number of rows set.
|
||||||
|
- Rewrote versioning help.
|
||||||
|
- Added gsutil help text for common 403 AccountProblem error.
|
||||||
|
- Added text to 'gsutil help dev' about legal agreement needed
|
||||||
|
with code submissions.
|
||||||
|
- Fixed various other typos.
|
||||||
|
- Updated doc for cp command regarding metadata not being
|
||||||
|
preserved when copying between providers.
|
||||||
|
- Fixed gsutil ls command documentation typo for the -L option.
|
||||||
|
- Added HTTP scheme to doc/examples for gsutil setcors command.
|
||||||
|
- Changed minimum version in documentation from 2.5 to 2.6 since
|
||||||
|
gsutil no longer works in Python 2.5.
|
||||||
|
- Cleaned up/clarify/deepen various other parts of gsutil
|
||||||
|
built-in documentation.
|
||||||
|
|
||||||
|
- Numerous improvements to testing infrastructure:
|
||||||
|
- Completely refactored infrastructure, allowing deeper testing
|
||||||
|
and more readable test code, and enabling better debugging
|
||||||
|
output when tests fail.
|
||||||
|
- Moved gslib/test_*.py unit tests to gslib/tests module.
|
||||||
|
- Made all tests (unit and integration, per-command and modules
|
||||||
|
(like naming) run from single gsutil test command.
|
||||||
|
- Moved TempDir functions from GsUtilIntegrationTestCase to
|
||||||
|
GsUtilTestCase.
|
||||||
|
- Made test runner message show the test function being run.
|
||||||
|
- Added file path support to ObjectToURI function.
|
||||||
|
- Disabled the test command if running on Python 2.6 and unittest2
|
||||||
|
is not available instead of breaking all of gsutil.
|
||||||
|
- Changed to pass GCS V2 API and project_id from boto config
|
||||||
|
if necessary in integration_testcase#CreateBucket().
|
||||||
|
- Fixed unit tests by using a GS-specific mocking class to
|
||||||
|
override the S3 provider.
|
||||||
|
- Added friendlier error message if test path munging fails.
|
||||||
|
- Fixed bug where gsutil test only cleaned up first few test files.
|
||||||
|
- Implemented setacl integration tests.
|
||||||
|
- Implemented StorageUri parsing unit tests.
|
||||||
|
- Implemented test for gsutil cp -D.
|
||||||
|
- Implemented setacl integration tests.
|
||||||
|
- Implemented tests for reading and seeking past end of file.
|
||||||
|
- Implemented and tests for it in new tests module.
|
||||||
|
- Changed cp tests that don't specify a Content-Type to check
|
||||||
|
for new binary/octet-stream default instead of server-detected
|
||||||
|
mime type.
|
||||||
|
|
||||||
|
- Changed gsutil mv to allow moving local files/dirs to the cloud.
|
||||||
|
Previously this was disallowed in the belief we should be
|
||||||
|
conservative about deleting data from local disk but there are
|
||||||
|
legitimate use cases for moving data from a local dir to the
|
||||||
|
cloud, it's clear to the user this would remove data from the
|
||||||
|
local disk, and allowing it makes the tool behavior more
|
||||||
|
consistent with what users would expect.
|
||||||
|
|
||||||
|
- Changed gsutil update command to insist on is_secure and
|
||||||
|
https_validate_certificates.
|
||||||
|
|
||||||
|
- Fixed release no longer to include extraneous boto dirs in
|
||||||
|
top-level of gsutil distribution (like bin/ and docs/).
|
||||||
|
|
||||||
|
- Changed resumable upload threshold from 1 MB to 2 MB.
|
||||||
|
|
||||||
|
- Removed leftover cloudauth and cloudreader dirs. Sample code
|
||||||
|
now lives at https://github.com/GoogleCloudPlatform.
|
||||||
|
|
||||||
|
- Updated copyright notice on code files.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.21 (release-date: 2012-12-10)
|
||||||
|
|
||||||
|
New Features
|
||||||
|
------------
|
||||||
|
|
||||||
|
- Added the ability for the cp command to continue even if there is an
|
||||||
|
error. This can be activated with the -c flag.
|
||||||
|
|
||||||
|
- Added support for specifying src args for gsutil cp on stdin (-I option)
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed gsutil test cp, which assumed it was run from gsutil install dir.
|
||||||
|
|
||||||
|
- Mods so we send generation subresource only when user requested
|
||||||
|
version parsing (-v option for cp and cat commands).
|
||||||
|
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- Updated docs about using setmeta with versioning enabled.
|
||||||
|
|
||||||
|
- Changed GCS endpoint in boto to storage.googleapis.com.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.20 (release-date: 2012-11-30)
|
||||||
|
|
||||||
|
New Features
|
||||||
|
------------
|
||||||
|
|
||||||
|
- Added a noclobber (-n) setting for the cp command. Existing objects/files
|
||||||
|
will not be overwritten when using this setting.
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed off-by-one error when reporting bytes transferred.
|
||||||
|
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- Improved versioning support for the remove command.
|
||||||
|
|
||||||
|
- Improved test runner support.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.19 (release-date: 2012-11-26)
|
||||||
|
|
||||||
|
New Features
|
||||||
|
------------
|
||||||
|
- Added support for object versions.
|
||||||
|
|
||||||
|
- Added support for storage classes (including Durable Reduced Availability).
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
- Fixed problem where cp -q prevented resumable uploads from being performed.
|
||||||
|
|
||||||
|
- Made setwebcfg and setcors tests robust wrt XML formatting variation.
|
||||||
|
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- Incorporated vapier@ mods to make version command not fail if CHECKSUM file
|
||||||
|
missing.
|
||||||
|
|
||||||
|
- Refactored gsutil such that most functionality exists in boto.
|
||||||
|
|
||||||
|
- Updated gsutil help dev instructions for how to check out source.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.18 (release-date: 2012-09-19)
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed resumable upload boundary condition when handling POST request
|
||||||
|
when server already has complete file, which resulted in an infinite
|
||||||
|
loop that consumed 100% of the CPU.
|
||||||
|
|
||||||
|
- Fixed one more place that outputted progress info when gsutil cp -q
|
||||||
|
specified (during streaming uploads).
|
||||||
|
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- Updated help text for "gsutil help setmeta" and "gsutil help metadata", to
|
||||||
|
clarify and deepen parts of the documentation.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.17 (release-date: 2012-08-17)
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed race condition when multiple threads attempt to get an OAuth2 refresh
|
||||||
|
token concurrently.
|
||||||
|
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- Implemented simplified syntax for setmeta command. The old syntax still
|
||||||
|
works but is now deprecated.
|
||||||
|
|
||||||
|
- Added help to gsutil cp -z option, to describe how to change where temp
|
||||||
|
files are written.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.16 (release-date: 2012-08-13)
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Added info to built-in help for setmeta command, to explain the syntax
|
||||||
|
needed when running from Windows.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.15 (release-date: 2012-08-12)
|
||||||
|
|
||||||
|
New Features
|
||||||
|
------------
|
||||||
|
- Implemented gsutil setmeta command.
|
||||||
|
|
||||||
|
- Made gsutil understand bucket subdir conventions used by various tools
|
||||||
|
(like GCS Manager and CloudBerry) so if you cp or mv to a subdir you
|
||||||
|
created with one of those tools it will work as expected.
|
||||||
|
|
||||||
|
- Added support for Windows drive letter-prefaced paths when using Storage
|
||||||
|
URIs.
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed performance bug when downloading a large object with Content-
|
||||||
|
Encoding:gzip, where decompression attempted to load the entire object
|
||||||
|
in memory. Also added "Uncompressing" log output if file is larger than
|
||||||
|
50M, to make it clear the download hasn't stalled.
|
||||||
|
|
||||||
|
- Fixed naming bug when performing gsutil mv from a bucket subdir to
|
||||||
|
and existing bucket subdir.
|
||||||
|
|
||||||
|
- Fixed bug that caused cross-provider copies into Google Cloud Storage to
|
||||||
|
fail.
|
||||||
|
|
||||||
|
- Made change needed to make resumable transfer progress messages not print
|
||||||
|
when running gsutil cp -q.
|
||||||
|
|
||||||
|
- Fixed copy/paste error in config file documentation for
|
||||||
|
https_validate_certificates option.
|
||||||
|
|
||||||
|
- Various typo fixes.
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- Changed gsutil to unset http_proxy environment variable if it's set,
|
||||||
|
because it confuses boto. (Proxies should instead be configured via the
|
||||||
|
boto config file.)
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.14 (release-date: 2012-07-28)
|
||||||
|
|
||||||
|
New Features
|
||||||
|
------------
|
||||||
|
- Added cp -q option, to support quiet operation from cron jobs.
|
||||||
|
|
||||||
|
- Made config command restore backed up file if there was a failure or user
|
||||||
|
hits ^C.
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed bug where gsutil cp -R from a source directory didn't generate
|
||||||
|
correct destination path.
|
||||||
|
|
||||||
|
- Fixed file handle leak in gsutil cp -z
|
||||||
|
|
||||||
|
- Fixed bug that caused cp -a option not to work when copying in the cloud.
|
||||||
|
|
||||||
|
- Fixed bug that caused '/-' to be appended to object name for streaming
|
||||||
|
uploads.
|
||||||
|
|
||||||
|
- Revert incorrect line I changed in previous CL, that attempted to
|
||||||
|
get fp from src_key object. The real fix that's needed is described in
|
||||||
|
http://code.google.com/p/gsutil/issues/detail?id=73.
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- Changed logging to print "Copying..." and Content-Type on same line;
|
||||||
|
refactored content type and log handling.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.13 (release-date: 2012-07-19)
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Included the fix to make 'gsutil config' honor BOTO_CONFIG environment
|
||||||
|
variable (which was intended to be included in Release 3.12)
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.11 (release-date: 2012-06-28)
|
||||||
|
|
||||||
|
New Features
|
||||||
|
------------
|
||||||
|
|
||||||
|
- Added support for configuring website buckets.
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed bug that caused simultaneous resumable downloads of the same source
|
||||||
|
object to use the same tracker file.
|
||||||
|
|
||||||
|
- Changed language code spec pointer from Wikipedia to loc.gov (for
|
||||||
|
Content-Language header).
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.10 (release-date: 2012-06-19)
|
||||||
|
|
||||||
|
New Features
|
||||||
|
------------
|
||||||
|
|
||||||
|
- Added support for setting and listing Content-Language header.
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed bug that caused getacl/setacl commands to get a character encoding
|
||||||
|
exception when ACL content contained content not representable in ISO-8859-1
|
||||||
|
character set.
|
||||||
|
|
||||||
|
- Fixed gsutil update not to fail under Windows exclusive file locking.
|
||||||
|
|
||||||
|
- Fixed gsutil ls -L to continue past 403 errors.
|
||||||
|
|
||||||
|
- Updated gsutil tests and also help dev with instructions on how to run
|
||||||
|
boto tests, based on recent test refactoring done to in boto library.
|
||||||
|
|
||||||
|
- Cleaned up parts of cp help text.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.9 (release-date: 2012-05-24)
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed bug that caused extra "file:/" to be included in pathnames with
|
||||||
|
doing gsutil cp -R on Windows.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.8 (release-date: 2012-05-20)
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed problem with non-ASCII filename characters not setting encoding before
|
||||||
|
attempting to hash for generating resumable transfer filename.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.7 (release-date: 2012-05-11)
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed handling of HTTPS tunneling through a proxy.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.6 (release-date: 2012-05-09)
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed bug that caused wildcards spanning directories not to work.
|
||||||
|
- Fixed bug that gsutil cp -z not to find available tmp space correctly
|
||||||
|
under Windows.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.5 (release-date: 2012-04-30)
|
||||||
|
|
||||||
|
Performance Improvement
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
- Change by Evan Worley to calculate MD5s incrementally during uploads and
|
||||||
|
downloads. This reduces overall transfer time substantially for large
|
||||||
|
objects.
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed bug where uploading and moving multiple files to a bucket subdirectory
|
||||||
|
didn't work as intended.
|
||||||
|
(http://code.google.com/p/gsutil/issues/detail?id=93).
|
||||||
|
- Fixed bug where gsutil cp -r sourcedir didn't copy to specified subdir
|
||||||
|
if there is only one file in sourcedir.
|
||||||
|
- Fixed bug where tracker file included a timestamp that caused it not to
|
||||||
|
be recognized across sessions.
|
||||||
|
- Fixed bug where gs://bucket/*/dir wildcard matches too many objects.
|
||||||
|
- Fixed documentation errors in help associated with ACLs and projects.
|
||||||
|
- Changed GCS ACL parsing to be case-insensitive.
|
||||||
|
- Changed ls to print error and exit with non-0 status when wildcard matches
|
||||||
|
nothing, to be more consistent with UNIX shell behavior.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.4 (release-date: 2012-04-06)
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed problem where resumable uploads/downloads of objects with very long
|
||||||
|
names would generate tracking files with names that exceeded local file
|
||||||
|
system limits, making it impossible to complete resumable transfers for
|
||||||
|
those objects. Solution was to build the tracking file name from a fixed
|
||||||
|
prefix, SHA1 hash of the long filename, epoch timestamp and last 16
|
||||||
|
chars of the long filename, which is guarantee to be a predicable and
|
||||||
|
reasonable length.
|
||||||
|
|
||||||
|
- Fixed minor bug in output from 'gsutil help dev' which advised executing
|
||||||
|
an inconsequential test script (test_util.py).
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.3 (release-date: 2012-04-03)
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed problem where gsutil ver and debug flags crashed when used
|
||||||
|
with newly generated boto config files.
|
||||||
|
|
||||||
|
- Fixed gsutil bug in windows path handling, and make checksumming work
|
||||||
|
across platforms.
|
||||||
|
|
||||||
|
- Fixed enablelogging to translate -b URI param to plain bucket name in REST
|
||||||
|
API request.
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.2 (release-date: 2012-03-30)
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Fixed problem where gsutil didn't convert between OS-specific directory
|
||||||
|
separators when copying individually-named files (issue 87).
|
||||||
|
|
||||||
|
- Fixed problem where gsutil ls -R didn't work right if there was a key
|
||||||
|
with a leading path (like /foo/bar/baz)
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.1 (release-date: 2012-03-20)
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Removed erroneous setting of Content-Encoding when a gzip file is uploaded
|
||||||
|
(vs running gsutil cp -z, when Content-Encoding should be set). This
|
||||||
|
error caused users to get gsutil.tar.gz file uncompressed by the user
|
||||||
|
agent (like wget) while downloading, making the file appear to be of the
|
||||||
|
wrong size/content.
|
||||||
|
|
||||||
|
- Fixed handling of gsutil help for Windows (previous code depended on
|
||||||
|
termios and fcntl libs, which are Linux/MacOS-specific).
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
Release 3.0 (release-date: 2012-03-20)
|
||||||
|
|
||||||
|
|
||||||
|
Important Notes
|
||||||
|
---------------
|
||||||
|
|
||||||
|
- Backwards-incompatible wildcard change
|
||||||
|
|
||||||
|
The '*' wildcard now only matches objects within a bucket directory. If
|
||||||
|
you have scripts that depend on being able to match spanning multiple
|
||||||
|
directories you need to use '**' instead. For example, the command:
|
||||||
|
|
||||||
|
gsutil cp gs://bucket/*.txt
|
||||||
|
|
||||||
|
will now only match .txt files in the top-level directory.
|
||||||
|
|
||||||
|
gsutil cp gs://bucket/**.txt
|
||||||
|
|
||||||
|
will match across all directories.
|
||||||
|
|
||||||
|
- gsutil ls now lists one directory at a time. If you want to list all objects
|
||||||
|
in a bucket, you can use:
|
||||||
|
|
||||||
|
gsutil ls gs://bucket/**
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
gsutil ls -R gs://bucket
|
||||||
|
|
||||||
|
|
||||||
|
New Features
|
||||||
|
------------
|
||||||
|
|
||||||
|
- Built-in help for all commands and many additional topics. Try
|
||||||
|
"gsutil help" for a list of available commands and topics.
|
||||||
|
|
||||||
|
- A new hierarchical file tree abstraction layer, which makes the flat bucket
|
||||||
|
name space look like a hierarchical file tree. This makes several things
|
||||||
|
possible:
|
||||||
|
- copying data to/from bucket sub-directories (see “gsutil help cp”).
|
||||||
|
- distributing large uploads/downloads across many machines
|
||||||
|
(see “gsutil help cp”)
|
||||||
|
- renaming bucket sub-directories (see “gsutil help mv”).
|
||||||
|
- listing individual bucket sub-directories and for listing directories
|
||||||
|
recursively (see “gsutil help ls”).
|
||||||
|
- setting ACLs for objects in a sub-directory (see “gsutil help setacl”).
|
||||||
|
|
||||||
|
- Support for per-directory (*) and recursive (**) wildcards. Essentially,
|
||||||
|
** works the way * did in previous gsutil releases, and * now behaves
|
||||||
|
consistently with how it works in command interpreters (like bash). The
|
||||||
|
ability to specify directory-only wildcards also enables a number of use
|
||||||
|
cases, such as distributing large uploads/downloads by wildcarded name. See
|
||||||
|
"gsutil help wildcards" for details.
|
||||||
|
|
||||||
|
- Support for Cross-Origin Resource Sharing (CORS) configuration. See "gsutil
|
||||||
|
help cors" for details.
|
||||||
|
|
||||||
|
- Support for multi-threading and recursive operation for setacl command
|
||||||
|
(see “gsutil help setacl”).
|
||||||
|
|
||||||
|
- Ability to use the UNIX 'file' command to do content type recognition as
|
||||||
|
an alternative to filename extensions.
|
||||||
|
|
||||||
|
- Introduction of new end-to-end test suite.
|
||||||
|
|
||||||
|
- The gsutil version command now computes a checksum of the code, to detect
|
||||||
|
corruption and local modification when assisting with technical support.
|
||||||
|
|
||||||
|
- The gsutil update command is no longer beta/experimental, and now also
|
||||||
|
supports updating from named URIs (for early/test releases).
|
||||||
|
|
||||||
|
- Changed gsutil ls -L to also print Content-Disposition header.
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- The gsutil cp -t option previously didn't work as documented, and instead
|
||||||
|
Content-Type was always detected based on filename extension. Content-Type
|
||||||
|
detection is now the default, the -t option is deprecated (to be removed in
|
||||||
|
the future), and specifying a -h Content-Type header now correctly overrides
|
||||||
|
the filename extension based handling. For details see "gsutil help
|
||||||
|
metadata".
|
||||||
|
|
||||||
|
- Fixed bug that caused multi-threaded mv command not to percolate failures
|
||||||
|
during the cp phase to the rm phase, which could under some circumstances
|
||||||
|
cause data that was not copied to be deleted.
|
||||||
|
|
||||||
|
- Fixed bug that caused gsutil to use GET for ls -L requests. It now uses HEAD
|
||||||
|
for ls -L requests, which is more efficient and faster.
|
||||||
|
|
||||||
|
- Fixed bug that caused gsutil not to preserve metadata during
|
||||||
|
copy-in-the-cloud.
|
||||||
|
|
||||||
|
- Fixed bug that prevented setacl command from allowing DisplayName's in ACLs.
|
||||||
|
|
||||||
|
- Fixed bug that caused gsutil/boto to suppress consecutive slashes in path
|
||||||
|
names.
|
||||||
|
|
||||||
|
- Fixed spec-non-compliant URI construction for resumable uploads.
|
||||||
|
|
||||||
|
- Fixed bug that caused rm -f not to work.
|
||||||
|
|
||||||
|
- Fixed UnicodeEncodeError that happened when redirecting gsutil ls output
|
||||||
|
to a file with non-ASCII object names.
|
||||||
|
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- UserAgent sent in HTTP requests now includes gsutil version number and OS
|
||||||
|
name.
|
||||||
|
|
||||||
|
- Starting with this release users are able to get individual named releases
|
||||||
|
from version-named objects: gs://pub/gsutil_<version>.tar.gz
|
||||||
|
and gs://pub/gsutil_<version>.zip. The version-less counterparts
|
||||||
|
(gs://pub/gsutil.tar.gz and gs://pub/gsutil.zip) will contain the latest
|
||||||
|
release. Also, the gs://pub bucket is now publicly readable (so, anyone
|
||||||
|
can list its contents).
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
Release 2.0 (release-date: 2012-01-13)
|
||||||
|
|
||||||
|
|
||||||
|
New Features
|
||||||
|
------------
|
||||||
|
|
||||||
|
- Support for for two new installation modes: enterprise and RPM.
|
||||||
|
Customers can now install gsutil one of three ways:
|
||||||
|
|
||||||
|
- Individual user mode (previously the only available mode): unpacking from
|
||||||
|
a gzipped tarball (gs://pub/gsutil.tar.gz) or zip file
|
||||||
|
(gs://pub/gsutil.zip) and running the gsutil command in place in the
|
||||||
|
unpacked gsutil directory.
|
||||||
|
|
||||||
|
- Enterprise mode (new): unpacking as above, and then running the setup.py
|
||||||
|
script in the unpacked gsutil directory. This allows a systems
|
||||||
|
administrator to install gsutil in a central location, using the Python
|
||||||
|
distutils facility. This mode is supported only on Linux and MacOS.
|
||||||
|
|
||||||
|
- RPM mode (new). A RedHat RPM can be built from the gsutil.spec.in file
|
||||||
|
in the unpacked gsutil directory, allowing it to be installed as part of
|
||||||
|
a RedHat build.
|
||||||
|
|
||||||
|
- Note: v2.0 is the first numbered gsutil release. Previous releases
|
||||||
|
were given timestamps for versions. Numbered releases enable downstream
|
||||||
|
package builds (like RPMs) to define dependencies more easily.
|
||||||
|
This is also the first version where we began including release notes.
|
@ -0,0 +1 @@
|
|||||||
|
3.25
|
@ -0,0 +1,382 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding=utf8
|
||||||
|
# Copyright 2010 Google Inc. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Main module for Google Cloud Storage command line tool."""
|
||||||
|
|
||||||
|
|
||||||
|
import ConfigParser
|
||||||
|
import errno
|
||||||
|
import getopt
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
third_party_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, os.path.dirname(third_party_dir))
|
||||||
|
sys.path.insert(0, third_party_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def _OutputAndExit(message):
|
||||||
|
global debug
|
||||||
|
if debug == 4:
|
||||||
|
stack_trace = traceback.format_exc()
|
||||||
|
sys.stderr.write('DEBUG: Exception stack trace:\n %s\n' %
|
||||||
|
re.sub('\\n', '\n ', stack_trace))
|
||||||
|
else:
|
||||||
|
sys.stderr.write('%s\n' % message)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _OutputUsageAndExit(command_runner):
|
||||||
|
command_runner.RunNamedCommand('help')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
debug = 0
|
||||||
|
# Before importing boto, find where gsutil is installed and include its
|
||||||
|
# boto sub-directory at the start of the PYTHONPATH, to ensure the versions of
|
||||||
|
# gsutil and boto stay in sync after software updates. This also allows gsutil
|
||||||
|
# to be used without explicitly adding it to the PYTHONPATH.
|
||||||
|
# We use realpath() below to unwind symlinks if any were used in the gsutil
|
||||||
|
# installation.
|
||||||
|
gsutil_bin_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
|
||||||
|
if not gsutil_bin_dir:
|
||||||
|
_OutputAndExit('Unable to determine where gsutil is installed. Sorry, '
|
||||||
|
'cannot run correctly without this.\n')
|
||||||
|
boto_lib_dir = os.path.join(gsutil_bin_dir, '..', 'boto')
|
||||||
|
if not os.path.isdir(boto_lib_dir):
|
||||||
|
_OutputAndExit('There is no boto library under the gsutil install directory '
|
||||||
|
'(%s).\nThe gsutil command cannot work properly when installed '
|
||||||
|
'this way.\nPlease re-install gsutil per the installation '
|
||||||
|
'instructions.' % gsutil_bin_dir)
|
||||||
|
# sys.path.insert(0, boto_lib_dir)
|
||||||
|
import boto
|
||||||
|
from boto.exception import BotoClientError
|
||||||
|
from boto.exception import InvalidAclError
|
||||||
|
from boto.exception import InvalidUriError
|
||||||
|
from boto.exception import ResumableUploadException
|
||||||
|
from boto.exception import StorageResponseError
|
||||||
|
from gslib.command_runner import CommandRunner
|
||||||
|
from gslib.exception import CommandException
|
||||||
|
from gslib.exception import ProjectIdException
|
||||||
|
from gslib import util
|
||||||
|
from gslib.util import ExtractErrorDetail
|
||||||
|
from gslib.util import HasConfiguredCredentials
|
||||||
|
from gslib.wildcard_iterator import WildcardException
|
||||||
|
|
||||||
|
# Load the gsutil version number and append it to boto.UserAgent so the value
|
||||||
|
# is set before anything instantiates boto. (If parts of boto were instantiated
|
||||||
|
# first those parts would have the old value of boto.UserAgent, so we wouldn't
|
||||||
|
# be guaranteed that all code paths send the correct user agent.)
|
||||||
|
ver_file_path = os.path.join(gsutil_bin_dir, 'VERSION')
|
||||||
|
if not os.path.isfile(ver_file_path):
|
||||||
|
raise CommandException(
|
||||||
|
'%s not found. Please reinstall gsutil from scratch' % ver_file_path)
|
||||||
|
ver_file = open(ver_file_path, 'r')
|
||||||
|
gsutil_ver = ver_file.read().rstrip()
|
||||||
|
ver_file.close()
|
||||||
|
boto.UserAgent += ' gsutil/%s (%s)' % (gsutil_ver, sys.platform)
|
||||||
|
|
||||||
|
# We don't use the oauth2 authentication plugin directly; importing it here
|
||||||
|
# ensures that it's loaded and available by default when an operation requiring
|
||||||
|
# authentication is performed.
|
||||||
|
try:
|
||||||
|
from oauth2_plugin import oauth2_plugin
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global debug
|
||||||
|
|
||||||
|
if sys.version_info[:3] < (2, 6):
|
||||||
|
raise CommandException('gsutil requires Python 2.6 or higher.')
|
||||||
|
|
||||||
|
config_file_list = _GetBotoConfigFileList()
|
||||||
|
command_runner = CommandRunner(gsutil_bin_dir, boto_lib_dir, config_file_list,
|
||||||
|
gsutil_ver)
|
||||||
|
headers = {}
|
||||||
|
parallel_operations = False
|
||||||
|
debug = 0
|
||||||
|
|
||||||
|
# If user enters no commands just print the usage info.
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
sys.argv.append('help')
|
||||||
|
|
||||||
|
# Change the default of the 'https_validate_certificates' boto option to
|
||||||
|
# True (it is currently False in boto).
|
||||||
|
if not boto.config.has_option('Boto', 'https_validate_certificates'):
|
||||||
|
if not boto.config.has_section('Boto'):
|
||||||
|
boto.config.add_section('Boto')
|
||||||
|
boto.config.setbool('Boto', 'https_validate_certificates', True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:], 'dDvh:m',
|
||||||
|
['debug', 'detailedDebug', 'version', 'help',
|
||||||
|
'header', 'multithreaded'])
|
||||||
|
except getopt.GetoptError, e:
|
||||||
|
_HandleCommandException(CommandException(e.msg))
|
||||||
|
for o, a in opts:
|
||||||
|
if o in ('-d', '--debug'):
|
||||||
|
# Passing debug=2 causes boto to include httplib header output.
|
||||||
|
debug = 2
|
||||||
|
if o in ('-D', '--detailedDebug'):
|
||||||
|
# We use debug level 3 to ask gsutil code to output more detailed
|
||||||
|
# debug output. This is a bit of a hack since it overloads the same
|
||||||
|
# flag that was originally implemented for boto use. And we use -DD
|
||||||
|
# to ask for really detailed debugging (i.e., including HTTP payload).
|
||||||
|
if debug == 3:
|
||||||
|
debug = 4
|
||||||
|
else:
|
||||||
|
debug = 3
|
||||||
|
if o in ('-?', '--help'):
|
||||||
|
_OutputUsageAndExit(command_runner)
|
||||||
|
if o in ('-h', '--header'):
|
||||||
|
(hdr_name, unused_ptn, hdr_val) = a.partition(':')
|
||||||
|
if not hdr_name:
|
||||||
|
_OutputUsageAndExit(command_runner)
|
||||||
|
headers[hdr_name] = hdr_val
|
||||||
|
if o in ('-m', '--multithreaded'):
|
||||||
|
parallel_operations = True
|
||||||
|
if debug > 1:
|
||||||
|
sys.stderr.write(
|
||||||
|
'***************************** WARNING *****************************\n'
|
||||||
|
'*** You are running gsutil with debug output enabled.\n'
|
||||||
|
'*** Be aware that debug output includes authentication '
|
||||||
|
'credentials.\n'
|
||||||
|
'*** Do not share (e.g., post to support forums) debug output\n'
|
||||||
|
'*** unless you have sanitized authentication tokens in the\n'
|
||||||
|
'*** output, or have revoked your credentials.\n'
|
||||||
|
'***************************** WARNING *****************************\n')
|
||||||
|
if debug == 2:
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
elif debug > 2:
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
command_runner.RunNamedCommand('ver')
|
||||||
|
config_items = []
|
||||||
|
try:
|
||||||
|
config_items.extend(boto.config.items('Boto'))
|
||||||
|
config_items.extend(boto.config.items('GSUtil'))
|
||||||
|
except ConfigParser.NoSectionError:
|
||||||
|
pass
|
||||||
|
sys.stderr.write('config_file_list: %s\n' % config_file_list)
|
||||||
|
sys.stderr.write('config: %s\n' % str(config_items))
|
||||||
|
else:
|
||||||
|
logging.basicConfig()
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
command_name = 'help'
|
||||||
|
else:
|
||||||
|
command_name = args[0]
|
||||||
|
|
||||||
|
# Unset http_proxy environment variable if it's set, because it confuses
|
||||||
|
# boto. (Proxies should instead be configured via the boto config file.)
|
||||||
|
if 'http_proxy' in os.environ:
|
||||||
|
if debug > 1:
|
||||||
|
sys.stderr.write(
|
||||||
|
'Unsetting http_proxy environment variable within gsutil run.\n')
|
||||||
|
del os.environ['http_proxy']
|
||||||
|
|
||||||
|
return _RunNamedCommandAndHandleExceptions(command_runner, command_name,
|
||||||
|
args[1:], headers, debug,
|
||||||
|
parallel_operations)
|
||||||
|
|
||||||
|
|
||||||
|
def _GetBotoConfigFileList():
|
||||||
|
"""Returns list of boto config files that exist."""
|
||||||
|
config_paths = boto.pyami.config.BotoConfigLocations
|
||||||
|
if 'AWS_CREDENTIAL_FILE' in os.environ:
|
||||||
|
config_paths.append(os.environ['AWS_CREDENTIAL_FILE'])
|
||||||
|
config_files = {}
|
||||||
|
for config_path in config_paths:
|
||||||
|
if os.path.exists(config_path):
|
||||||
|
config_files[config_path] = 1
|
||||||
|
cf_list = []
|
||||||
|
for config_file in config_files:
|
||||||
|
cf_list.append(config_file)
|
||||||
|
return cf_list
|
||||||
|
|
||||||
|
|
||||||
|
def _HandleUnknownFailure(e):
|
||||||
|
global debug
|
||||||
|
# Called if we fall through all known/handled exceptions. Allows us to
|
||||||
|
# print a stacktrace if -D option used.
|
||||||
|
if debug > 2:
|
||||||
|
stack_trace = traceback.format_exc()
|
||||||
|
sys.stderr.write('DEBUG: Exception stack trace:\n %s\n' %
|
||||||
|
re.sub('\\n', '\n ', stack_trace))
|
||||||
|
else:
|
||||||
|
_OutputAndExit('Failure: %s.' % e)
|
||||||
|
|
||||||
|
|
||||||
|
def _HandleCommandException(e):
|
||||||
|
if e.informational:
|
||||||
|
_OutputAndExit(e.reason)
|
||||||
|
else:
|
||||||
|
_OutputAndExit('CommandException: %s' % e.reason)
|
||||||
|
|
||||||
|
|
||||||
|
def _HandleControlC(signal_num, cur_stack_frame):
|
||||||
|
"""Called when user hits ^C so we can print a brief message instead of
|
||||||
|
the normal Python stack trace (unless -D option is used)."""
|
||||||
|
global debug
|
||||||
|
if debug > 2:
|
||||||
|
stack_trace = ''.join(traceback.format_list(traceback.extract_stack()))
|
||||||
|
_OutputAndExit('DEBUG: Caught signal %d - Exception stack trace:\n'
|
||||||
|
' %s' % (signal_num, re.sub('\\n', '\n ', stack_trace)))
|
||||||
|
else:
|
||||||
|
_OutputAndExit('Caught signal %d - exiting' % signal_num)
|
||||||
|
|
||||||
|
|
||||||
|
def _HandleSigQuit(signal_num, cur_stack_frame):
|
||||||
|
"""Called when user hits ^\, so we can force breakpoint a running gsutil."""
|
||||||
|
import pdb; pdb.set_trace()
|
||||||
|
|
||||||
|
|
||||||
|
def _RunNamedCommandAndHandleExceptions(command_runner, command_name, args=None,
|
||||||
|
headers=None, debug=0,
|
||||||
|
parallel_operations=False):
|
||||||
|
try:
|
||||||
|
# Catch ^C so we can print a brief message instead of the normal Python
|
||||||
|
# stack trace.
|
||||||
|
signal.signal(signal.SIGINT, _HandleControlC)
|
||||||
|
# Catch ^\ so we can force a breakpoint in a running gsutil.
|
||||||
|
if not util.IS_WINDOWS:
|
||||||
|
signal.signal(signal.SIGQUIT, _HandleSigQuit)
|
||||||
|
return command_runner.RunNamedCommand(command_name, args, headers, debug,
|
||||||
|
parallel_operations)
|
||||||
|
except AttributeError, e:
|
||||||
|
if str(e).find('secret_access_key') != -1:
|
||||||
|
_OutputAndExit('Missing credentials for the given URI(s). Does your '
|
||||||
|
'boto config file contain all needed credentials?')
|
||||||
|
else:
|
||||||
|
_OutputAndExit(str(e))
|
||||||
|
except BotoClientError, e:
|
||||||
|
_OutputAndExit('BotoClientError: %s.' % e.reason)
|
||||||
|
except CommandException, e:
|
||||||
|
_HandleCommandException(e)
|
||||||
|
except getopt.GetoptError, e:
|
||||||
|
_HandleCommandException(CommandException(e.msg))
|
||||||
|
except InvalidAclError, e:
|
||||||
|
_OutputAndExit('InvalidAclError: %s.' % str(e))
|
||||||
|
except InvalidUriError, e:
|
||||||
|
_OutputAndExit('InvalidUriError: %s.' % e.message)
|
||||||
|
except ProjectIdException, e:
|
||||||
|
_OutputAndExit('ProjectIdException: %s.' % e.reason)
|
||||||
|
except boto.auth_handler.NotReadyToAuthenticate:
|
||||||
|
_OutputAndExit('NotReadyToAuthenticate')
|
||||||
|
except OSError, e:
|
||||||
|
_OutputAndExit('OSError: %s.' % e.strerror)
|
||||||
|
except WildcardException, e:
|
||||||
|
_OutputAndExit(e.reason)
|
||||||
|
except StorageResponseError, e:
|
||||||
|
# Check for access denied, and provide detail to users who have no boto
|
||||||
|
# config file (who might previously have been using gsutil only for
|
||||||
|
# accessing publicly readable buckets and objects).
|
||||||
|
if e.status == 403:
|
||||||
|
if not HasConfiguredCredentials():
|
||||||
|
_OutputAndExit(
|
||||||
|
'You are attempting to access protected data with no configured '
|
||||||
|
'credentials.\nPlease see '
|
||||||
|
'http://code.google.com/apis/storage/docs/signup.html for\ndetails '
|
||||||
|
'about activating the Google Cloud Storage service and then run '
|
||||||
|
'the\n"gsutil config" command to configure gsutil to use these '
|
||||||
|
'credentials.')
|
||||||
|
elif (e.error_code == 'AccountProblem'
|
||||||
|
and ','.join(args).find('gs://') != -1):
|
||||||
|
default_project_id = boto.config.get_value('GSUtil',
|
||||||
|
'default_project_id')
|
||||||
|
acct_help_part_1 = (
|
||||||
|
"""Your request resulted in an AccountProblem (403) error. Usually this happens
|
||||||
|
if you attempt to create a bucket or upload an object without having first
|
||||||
|
enabled billing for the project you are using. To remedy this problem, please do
|
||||||
|
the following:
|
||||||
|
|
||||||
|
1. Navigate to the Google APIs console (https://code.google.com/apis/console),
|
||||||
|
and ensure the drop-down selector beneath "Google APIs" shows the project
|
||||||
|
you're attempting to use.
|
||||||
|
|
||||||
|
""")
|
||||||
|
acct_help_part_2 = '\n'
|
||||||
|
if default_project_id:
|
||||||
|
acct_help_part_2 = (
|
||||||
|
"""2. Click "Google Cloud Storage" on the left hand pane, and then check that
|
||||||
|
the value listed for "x-goog-project-id" on this page matches the project ID
|
||||||
|
(%s) from your boto config file.
|
||||||
|
|
||||||
|
""" % default_project_id)
|
||||||
|
acct_help_part_3 = (
|
||||||
|
"""Check whether there's an "!" next to Billing. If so, click Billing and then
|
||||||
|
enable billing for this project. Note that it can take up to one hour after
|
||||||
|
enabling billing for the project to become activated for creating buckets and
|
||||||
|
uploading objects.
|
||||||
|
|
||||||
|
If the above doesn't resolve your AccountProblem, please send mail to
|
||||||
|
gs-team@google.com requesting assistance, noting the exact command you ran, the
|
||||||
|
fact that you received a 403 AccountProblem error, and your project ID. Please
|
||||||
|
do not post your project ID on the public discussion forum (gs-discussion) or on
|
||||||
|
StackOverflow.
|
||||||
|
|
||||||
|
Note: It's possible to use Google Cloud Storage without enabling billing if
|
||||||
|
you're only listing or reading objects for which you're authorized, or if
|
||||||
|
you're uploading objects to a bucket billed to a project that has billing
|
||||||
|
enabled. But if you're attempting to create buckets or upload objects to a
|
||||||
|
bucket owned by your own project, you must first enable billing for that
|
||||||
|
project.""")
|
||||||
|
if default_project_id:
|
||||||
|
_OutputAndExit(acct_help_part_1 + acct_help_part_2 + '3. '
|
||||||
|
+ acct_help_part_3)
|
||||||
|
else:
|
||||||
|
_OutputAndExit(acct_help_part_1 + '2. ' + acct_help_part_3)
|
||||||
|
|
||||||
|
if not e.body:
|
||||||
|
e.body = ''
|
||||||
|
exc_name, error_detail = ExtractErrorDetail(e)
|
||||||
|
if error_detail:
|
||||||
|
_OutputAndExit('%s: status=%d, code=%s, reason=%s, detail=%s.' %
|
||||||
|
(exc_name, e.status, e.code, e.reason, error_detail))
|
||||||
|
else:
|
||||||
|
_OutputAndExit('%s: status=%d, code=%s, reason=%s.' %
|
||||||
|
(exc_name, e.status, e.code, e.reason))
|
||||||
|
except ResumableUploadException, e:
|
||||||
|
_OutputAndExit('ResumableUploadException: %s.' % e.message)
|
||||||
|
except socket.error, e:
|
||||||
|
if e.args[0] == errno.EPIPE:
|
||||||
|
# Retrying with a smaller file (per suggestion below) works because
|
||||||
|
# the library code send loop (in boto/s3/key.py) can get through the
|
||||||
|
# entire file and then request the HTTP response before the socket
|
||||||
|
# gets closed and the response lost.
|
||||||
|
message = (
|
||||||
|
"""
|
||||||
|
Got a "Broken pipe" error. This can happen to clients using Python 2.x,
|
||||||
|
when the server sends an error response and then closes the socket (see
|
||||||
|
http://bugs.python.org/issue5542). If you are trying to upload a large
|
||||||
|
object you might retry with a small (say 200k) object, and see if you get
|
||||||
|
a more specific error code.
|
||||||
|
""")
|
||||||
|
_OutputAndExit(message)
|
||||||
|
else:
|
||||||
|
_HandleUnknownFailure(e)
|
||||||
|
except Exception, e:
|
||||||
|
_HandleUnknownFailure(e)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
@ -0,0 +1,75 @@
|
|||||||
|
#
|
||||||
|
# gsutil.spec - RPM specification file for Google Cloud Storage command
|
||||||
|
# line utility (gsutil).
|
||||||
|
#
|
||||||
|
# Copyright 2011 Google Inc.
|
||||||
|
#
|
||||||
|
|
||||||
|
Name: gsutil
|
||||||
|
Version: 2.0
|
||||||
|
Release: 1%{?dist}
|
||||||
|
Summary: gsutil command line utility for Google Cloud Storage
|
||||||
|
License: ASL 2.0
|
||||||
|
Group: Development/Libraries
|
||||||
|
Url: http://code.google.com/apis/storage/docs/gsutil.html
|
||||||
|
Source0: http://gsutil.googlecode.com/files/%{name}-%{version}.zip
|
||||||
|
BuildArch: noarch
|
||||||
|
BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX)
|
||||||
|
# Dependency on boto commented out for now because initially we plan to
|
||||||
|
# bundle boto with this package, however, when we're ready to depend on
|
||||||
|
# a separate boto rpm package, this line should be uncommented.
|
||||||
|
#Requires: python-boto
|
||||||
|
|
||||||
|
%description
|
||||||
|
|
||||||
|
GSUtil is a Python application that facilitates access to Google Cloud Storage
|
||||||
|
from the command line. You can use GSUtil to do a wide range of bucket and
|
||||||
|
object management tasks, including:
|
||||||
|
|
||||||
|
- Creating and deleting buckets.
|
||||||
|
- Uploading, downloading, and deleting objects.
|
||||||
|
- Listing buckets and objects.
|
||||||
|
- Moving, copying, and renaming objects.
|
||||||
|
- Setting object and bucket ACLs.
|
||||||
|
|
||||||
|
%prep
|
||||||
|
%setup -q
|
||||||
|
|
||||||
|
%build
|
||||||
|
python setup.py build
|
||||||
|
|
||||||
|
%install
|
||||||
|
python setup.py install --skip-build --root=%{buildroot}
|
||||||
|
# Make all files and dirs in build area readable by other
|
||||||
|
# and make all directories executable by other. These steps
|
||||||
|
# are performed in support of the rpm installation mode,
|
||||||
|
# in which users with different user/group than the
|
||||||
|
# installation user/group must be able to run gsutil.
|
||||||
|
chmod -R o+r %{buildroot}/usr/share/gsutil
|
||||||
|
find %{buildroot}/usr/share/gsutil -type d | xargs chmod o+x
|
||||||
|
# Make main gsutil script readable and executable by other.
|
||||||
|
chmod o+rx %{buildroot}/usr/share/gsutil/gsutil
|
||||||
|
# Remove Python egg file, which we don't use (but setup.py insists on
|
||||||
|
# building) so we remove it here.
|
||||||
|
rm %{buildroot}/usr/local/lib/python2.6/dist-packages/gsutil-2.0.egg-info
|
||||||
|
# Remove update command, which shouldn't be used when gsutil is managed by RPM.
|
||||||
|
rm %{buildroot}/usr/share/gsutil/gslib/commands/update.py
|
||||||
|
# Create /usr/bin under buildroot and symlink gsutil so users don't
|
||||||
|
# need to add a custom directory to their PATH.
|
||||||
|
mkdir -p %{buildroot}%{_bindir}
|
||||||
|
cd %{buildroot}%{_bindir}
|
||||||
|
ln -s ../share/gsutil/gsutil gsutil
|
||||||
|
|
||||||
|
%clean
|
||||||
|
rm -rf %{buildroot}
|
||||||
|
|
||||||
|
%files
|
||||||
|
%defattr(-,root,root,-)
|
||||||
|
# Lines ending with a slash cause recursive enumeration of directory contents.
|
||||||
|
%{_bindir}/%{name}
|
||||||
|
###FILES_GO_HERE###
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* Tue Dec 10 2011 Marc Cohen <gs-team@google.com> 2.0-1
|
||||||
|
- initial version of rpm spec file for gsutil for inclusion in RHEL
|
||||||
|
|
@ -0,0 +1,22 @@
|
|||||||
|
# Copyright 2011 Google Inc. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
# copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish, dis-
|
||||||
|
# tribute, sublicense, and/or sell copies of the Software, and to permit
|
||||||
|
# persons to whom the Software is furnished to do so, subject to the fol-
|
||||||
|
# lowing conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
|
||||||
|
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
# IN THE SOFTWARE.
|
||||||
|
|
||||||
|
"""Package marker file."""
|
@ -0,0 +1,642 @@
|
|||||||
|
# Copyright 2010 Google Inc. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""An OAuth2 client library.
|
||||||
|
|
||||||
|
This library provides a client implementation of the OAuth2 protocol (see
|
||||||
|
http://code.google.com/apis/accounts/docs/OAuth2.html).
|
||||||
|
|
||||||
|
**** Experimental API ****
|
||||||
|
|
||||||
|
This module is experimental and is subject to modification or removal without
|
||||||
|
notice.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This implementation is inspired by the implementation in
|
||||||
|
# http://code.google.com/p/google-api-python-client/source/browse/oauth2client/,
|
||||||
|
# with the following main differences:
|
||||||
|
# - This library uses the fancy_urllib monkey patch for urllib to correctly
|
||||||
|
# implement SSL certificate validation.
|
||||||
|
# - This library does not assume that client code is using the httplib2 library
|
||||||
|
# to make HTTP requests.
|
||||||
|
# - This library implements caching of access tokens independent of refresh
|
||||||
|
# tokens (in the python API client oauth2client, there is a single class that
|
||||||
|
# encapsulates both refresh and access tokens).
|
||||||
|
|
||||||
|
|
||||||
|
import cgi
|
||||||
|
import datetime
|
||||||
|
import errno
|
||||||
|
from hashlib import sha1
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
from boto import cacerts
|
||||||
|
from third_party import fancy_urllib
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
# Try to import from django, should work on App Engine
|
||||||
|
from django.utils import simplejson as json
|
||||||
|
except ImportError:
|
||||||
|
# Try for simplejson
|
||||||
|
import simplejson as json
|
||||||
|
|
||||||
|
LOG = logging.getLogger('oauth2_client')
|
||||||
|
# Lock used for checking/exchanging refresh token, so multithreaded
|
||||||
|
# operation doesn't attempt concurrent refreshes.
|
||||||
|
token_exchange_lock = threading.Lock()
|
||||||
|
|
||||||
|
# SHA1 sum of the CA certificates file imported from boto.
|
||||||
|
CACERTS_FILE_SHA1SUM = 'ed024a78d9327f8669b3b117d9eac9e3c9460e9b'
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
"""Base exception for the OAuth2 module."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AccessTokenRefreshError(Error):
|
||||||
|
"""Error trying to exchange a refresh token into an access token."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationCodeExchangeError(Error):
|
||||||
|
"""Error trying to exchange an authorization code into a refresh token."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TokenCache(object):
|
||||||
|
"""Interface for OAuth2 token caches."""
|
||||||
|
|
||||||
|
def PutToken(self, key, value):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def GetToken(self, key):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class NoopTokenCache(TokenCache):
|
||||||
|
"""A stub implementation of TokenCache that does nothing."""
|
||||||
|
|
||||||
|
def PutToken(self, key, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def GetToken(self, key):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryTokenCache(TokenCache):
|
||||||
|
"""An in-memory token cache.
|
||||||
|
|
||||||
|
The cache is implemented by a python dict, and inherits the thread-safety
|
||||||
|
properties of dict.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(InMemoryTokenCache, self).__init__()
|
||||||
|
self.cache = dict()
|
||||||
|
|
||||||
|
def PutToken(self, key, value):
|
||||||
|
LOG.info('InMemoryTokenCache.PutToken: key=%s', key)
|
||||||
|
self.cache[key] = value
|
||||||
|
|
||||||
|
def GetToken(self, key):
|
||||||
|
value = self.cache.get(key, None)
|
||||||
|
LOG.info('InMemoryTokenCache.GetToken: key=%s%s present',
|
||||||
|
key, ' not' if value is None else '')
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class FileSystemTokenCache(TokenCache):
|
||||||
|
"""An implementation of a token cache that persists tokens on disk.
|
||||||
|
|
||||||
|
Each token object in the cache is stored in serialized form in a separate
|
||||||
|
file. The cache file's name can be configured via a path pattern that is
|
||||||
|
parameterized by the key under which a value is cached and optionally the
|
||||||
|
current processes uid as obtained by os.getuid().
|
||||||
|
|
||||||
|
Since file names are generally publicly visible in the system, it is important
|
||||||
|
that the cache key does not leak information about the token's value. If
|
||||||
|
client code computes cache keys from token values, a cryptographically strong
|
||||||
|
one-way function must be used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, path_pattern=None):
|
||||||
|
"""Creates a FileSystemTokenCache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path_pattern: Optional string argument to specify the path pattern for
|
||||||
|
cache files. The argument should be a path with format placeholders
|
||||||
|
'%(key)s' and optionally '%(uid)s'. If the argument is omitted, the
|
||||||
|
default pattern
|
||||||
|
<tmpdir>/oauth2client-tokencache.%(uid)s.%(key)s
|
||||||
|
is used, where <tmpdir> is replaced with the system temp dir as
|
||||||
|
obtained from tempfile.gettempdir().
|
||||||
|
"""
|
||||||
|
super(FileSystemTokenCache, self).__init__()
|
||||||
|
self.path_pattern = path_pattern
|
||||||
|
if not path_pattern:
|
||||||
|
self.path_pattern = os.path.join(
|
||||||
|
tempfile.gettempdir(), 'oauth2_client-tokencache.%(uid)s.%(key)s')
|
||||||
|
|
||||||
|
def CacheFileName(self, key):
|
||||||
|
uid = '_'
|
||||||
|
try:
|
||||||
|
# os.getuid() doesn't seem to work in Windows
|
||||||
|
uid = str(os.getuid())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return self.path_pattern % {'key': key, 'uid': uid}
|
||||||
|
|
||||||
|
def PutToken(self, key, value):
|
||||||
|
"""Serializes the value to the key's filename.
|
||||||
|
|
||||||
|
To ensure that written tokens aren't leaked to a different users, we
|
||||||
|
a) unlink an existing cache file, if any (to ensure we don't fall victim
|
||||||
|
to symlink attacks and the like),
|
||||||
|
b) create a new file with O_CREAT | O_EXCL (to ensure nobody is trying to
|
||||||
|
race us)
|
||||||
|
If either of these steps fail, we simply give up (but log a warning). Not
|
||||||
|
caching access tokens is not catastrophic, and failure to create a file
|
||||||
|
can happen for either of the following reasons:
|
||||||
|
- someone is attacking us as above, in which case we want to default to
|
||||||
|
safe operation (not write the token);
|
||||||
|
- another legitimate process is racing us; in this case one of the two
|
||||||
|
will win and write the access token, which is fine;
|
||||||
|
- we don't have permission to remove the old file or write to the
|
||||||
|
specified directory, in which case we can't recover
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: the refresh_token hash key to store.
|
||||||
|
value: the access_token value to serialize.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cache_file = self.CacheFileName(key)
|
||||||
|
LOG.info('FileSystemTokenCache.PutToken: key=%s, cache_file=%s',
|
||||||
|
key, cache_file)
|
||||||
|
try:
|
||||||
|
os.unlink(cache_file)
|
||||||
|
except:
|
||||||
|
# Ignore failure to unlink the file; if the file exists and can't be
|
||||||
|
# unlinked, the subsequent open with O_CREAT | O_EXCL will fail.
|
||||||
|
pass
|
||||||
|
|
||||||
|
flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
|
||||||
|
|
||||||
|
# Accommodate Windows; stolen from python2.6/tempfile.py.
|
||||||
|
if hasattr(os, 'O_NOINHERIT'):
|
||||||
|
flags |= os.O_NOINHERIT
|
||||||
|
if hasattr(os, 'O_BINARY'):
|
||||||
|
flags |= os.O_BINARY
|
||||||
|
|
||||||
|
try:
|
||||||
|
fd = os.open(cache_file, flags, 0600)
|
||||||
|
except (OSError, IOError), e:
|
||||||
|
LOG.warning('FileSystemTokenCache.PutToken: '
|
||||||
|
'Failed to create cache file %s: %s', cache_file, e)
|
||||||
|
return
|
||||||
|
f = os.fdopen(fd, 'w+b')
|
||||||
|
f.write(value.Serialize())
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
def GetToken(self, key):
|
||||||
|
"""Returns a deserialized access token from the key's filename."""
|
||||||
|
value = None
|
||||||
|
cache_file = self.CacheFileName(key)
|
||||||
|
try:
|
||||||
|
f = open(cache_file)
|
||||||
|
value = AccessToken.UnSerialize(f.read())
|
||||||
|
f.close()
|
||||||
|
except (IOError, OSError), e:
|
||||||
|
if e.errno != errno.ENOENT:
|
||||||
|
LOG.warning('FileSystemTokenCache.GetToken: '
|
||||||
|
'Failed to read cache file %s: %s', cache_file, e)
|
||||||
|
except Exception, e:
|
||||||
|
LOG.warning('FileSystemTokenCache.GetToken: '
|
||||||
|
'Failed to read cache file %s (possibly corrupted): %s',
|
||||||
|
cache_file, e)
|
||||||
|
|
||||||
|
LOG.info('FileSystemTokenCache.GetToken: key=%s%s present (cache_file=%s)',
|
||||||
|
key, ' not' if value is None else '', cache_file)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2Provider(object):
|
||||||
|
"""Encapsulates information about an OAuth2 provider."""
|
||||||
|
|
||||||
|
def __init__(self, label, authorization_uri, token_uri):
|
||||||
|
"""Creates an OAuth2Provider.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
label: A string identifying this oauth2 provider, e.g. "Google".
|
||||||
|
authorization_uri: The provider's authorization URI.
|
||||||
|
token_uri: The provider's token endpoint URI.
|
||||||
|
"""
|
||||||
|
self.label = label
|
||||||
|
self.authorization_uri = authorization_uri
|
||||||
|
self.token_uri = token_uri
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2Client(object):
|
||||||
|
"""An OAuth2 client."""
|
||||||
|
|
||||||
|
def __init__(self, provider, client_id, client_secret,
|
||||||
|
url_opener=None,
|
||||||
|
proxy=None,
|
||||||
|
access_token_cache=None,
|
||||||
|
datetime_strategy=datetime.datetime):
|
||||||
|
"""Creates an OAuth2Client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
provider: The OAuth2Provider provider this client will authenticate
|
||||||
|
against.
|
||||||
|
client_id: The OAuth2 client ID of this client.
|
||||||
|
client_secret: The OAuth2 client secret of this client.
|
||||||
|
url_opener: An optinal urllib2.OpenerDirector to use for making HTTP
|
||||||
|
requests to the OAuth2 provider's token endpoint. The provided
|
||||||
|
url_opener *must* be configured to validate server SSL certificates
|
||||||
|
for requests to https connections, and to correctly handle proxying of
|
||||||
|
https requests. If this argument is omitted or None, a suitable
|
||||||
|
opener based on fancy_urllib is used.
|
||||||
|
proxy: An optional string specifying a HTTP proxy to be used, in the form
|
||||||
|
'<proxy>:<port>'. This option is only effective if the url_opener has
|
||||||
|
been configured with a fancy_urllib.FancyProxyHandler (this is the
|
||||||
|
case for the default url_opener).
|
||||||
|
access_token_cache: An optional instance of a TokenCache. If omitted or
|
||||||
|
None, an InMemoryTokenCache is used.
|
||||||
|
datetime_strategy: datetime module strategy to use.
|
||||||
|
"""
|
||||||
|
self.provider = provider
|
||||||
|
self.client_id = client_id
|
||||||
|
self.client_secret = client_secret
|
||||||
|
# datetime_strategy is used to invoke utcnow() on; it is injected into the
|
||||||
|
# constructor for unit testing purposes.
|
||||||
|
self.datetime_strategy = datetime_strategy
|
||||||
|
self._proxy = proxy
|
||||||
|
|
||||||
|
self.access_token_cache = access_token_cache or InMemoryTokenCache()
|
||||||
|
|
||||||
|
self.ca_certs_file = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(cacerts.__file__)), 'cacerts.txt')
|
||||||
|
|
||||||
|
if url_opener is None:
|
||||||
|
# Check that the cert file distributed with boto has not been tampered
|
||||||
|
# with.
|
||||||
|
h = sha1()
|
||||||
|
h.update(file(self.ca_certs_file).read())
|
||||||
|
actual_sha1 = h.hexdigest()
|
||||||
|
if actual_sha1 != CACERTS_FILE_SHA1SUM:
|
||||||
|
raise Error(
|
||||||
|
'CA certificates file does not have expected SHA1 sum; '
|
||||||
|
'expected: %s, actual: %s' % (CACERTS_FILE_SHA1SUM, actual_sha1))
|
||||||
|
# TODO(Google): set user agent?
|
||||||
|
url_opener = urllib2.build_opener(
|
||||||
|
fancy_urllib.FancyProxyHandler(),
|
||||||
|
fancy_urllib.FancyRedirectHandler(),
|
||||||
|
fancy_urllib.FancyHTTPSHandler())
|
||||||
|
self.url_opener = url_opener
|
||||||
|
|
||||||
|
def _TokenRequest(self, request):
|
||||||
|
"""Make a requst to this client's provider's token endpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: A dict with the request parameteres.
|
||||||
|
Returns:
|
||||||
|
A tuple (response, error) where,
|
||||||
|
- response is the parsed JSON response received from the token endpoint,
|
||||||
|
or None if no parseable response was received, and
|
||||||
|
- error is None if the request succeeded or
|
||||||
|
an Exception if an error occurred.
|
||||||
|
"""
|
||||||
|
|
||||||
|
body = urllib.urlencode(request)
|
||||||
|
LOG.debug('_TokenRequest request: %s', body)
|
||||||
|
response = None
|
||||||
|
try:
|
||||||
|
request = fancy_urllib.FancyRequest(
|
||||||
|
self.provider.token_uri, data=body)
|
||||||
|
if self._proxy:
|
||||||
|
request.set_proxy(self._proxy, 'http')
|
||||||
|
|
||||||
|
request.set_ssl_info(ca_certs=self.ca_certs_file)
|
||||||
|
result = self.url_opener.open(request)
|
||||||
|
resp_body = result.read()
|
||||||
|
LOG.debug('_TokenRequest response: %s', resp_body)
|
||||||
|
except urllib2.HTTPError, e:
|
||||||
|
try:
|
||||||
|
response = json.loads(e.read())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return (response, e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = json.loads(resp_body)
|
||||||
|
except ValueError, e:
|
||||||
|
return (None, e)
|
||||||
|
|
||||||
|
return (response, None)
|
||||||
|
|
||||||
|
def GetAccessToken(self, refresh_token):
|
||||||
|
"""Given a RefreshToken, obtains a corresponding access token.
|
||||||
|
|
||||||
|
First, this client's access token cache is checked for an existing,
|
||||||
|
not-yet-expired access token for the provided refresh token. If none is
|
||||||
|
found, the client obtains a fresh access token for the provided refresh
|
||||||
|
token from the OAuth2 provider's token endpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
refresh_token: The RefreshToken object which to get an access token for.
|
||||||
|
Returns:
|
||||||
|
The cached or freshly obtained AccessToken.
|
||||||
|
Raises:
|
||||||
|
AccessTokenRefreshError if an error occurs.
|
||||||
|
"""
|
||||||
|
# Ensure only one thread at a time attempts to get (and possibly refresh)
|
||||||
|
# the access token. This doesn't prevent concurrent refresh attempts across
|
||||||
|
# multiple gsutil instances, but at least protects against multiple threads
|
||||||
|
# simultaneously attempting to refresh when gsutil -m is used.
|
||||||
|
token_exchange_lock.acquire()
|
||||||
|
try:
|
||||||
|
cache_key = refresh_token.CacheKey()
|
||||||
|
LOG.info('GetAccessToken: checking cache for key %s', cache_key)
|
||||||
|
access_token = self.access_token_cache.GetToken(cache_key)
|
||||||
|
LOG.debug('GetAccessToken: token from cache: %s', access_token)
|
||||||
|
if access_token is None or access_token.ShouldRefresh():
|
||||||
|
LOG.info('GetAccessToken: fetching fresh access token...')
|
||||||
|
access_token = self.FetchAccessToken(refresh_token)
|
||||||
|
LOG.debug('GetAccessToken: fresh access token: %s', access_token)
|
||||||
|
self.access_token_cache.PutToken(cache_key, access_token)
|
||||||
|
return access_token
|
||||||
|
finally:
|
||||||
|
token_exchange_lock.release()
|
||||||
|
|
||||||
|
def FetchAccessToken(self, refresh_token):
|
||||||
|
"""Fetches an access token from the provider's token endpoint.
|
||||||
|
|
||||||
|
Given a RefreshToken, fetches an access token from this client's OAuth2
|
||||||
|
provider's token endpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
refresh_token: The RefreshToken object which to get an access token for.
|
||||||
|
Returns:
|
||||||
|
The fetched AccessToken.
|
||||||
|
Raises:
|
||||||
|
AccessTokenRefreshError: if an error occurs.
|
||||||
|
"""
|
||||||
|
request = {
|
||||||
|
'grant_type': 'refresh_token',
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'refresh_token': refresh_token.refresh_token,
|
||||||
|
}
|
||||||
|
LOG.debug('FetchAccessToken request: %s', request)
|
||||||
|
|
||||||
|
response, error = self._TokenRequest(request)
|
||||||
|
LOG.debug(
|
||||||
|
'FetchAccessToken response (error = %s): %s', error, response)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
oauth2_error = ''
|
||||||
|
if response and response['error']:
|
||||||
|
oauth2_error = '; OAuth2 error: %s' % response['error']
|
||||||
|
raise AccessTokenRefreshError(
|
||||||
|
'Failed to exchange refresh token into access token; '
|
||||||
|
'request failed: %s%s' % (error, oauth2_error))
|
||||||
|
|
||||||
|
if 'access_token' not in response:
|
||||||
|
raise AccessTokenRefreshError(
|
||||||
|
'Failed to exchange refresh token into access token; response: %s' %
|
||||||
|
response)
|
||||||
|
|
||||||
|
token_expiry = None
|
||||||
|
if 'expires_in' in response:
|
||||||
|
token_expiry = (
|
||||||
|
self.datetime_strategy.utcnow() +
|
||||||
|
datetime.timedelta(seconds=int(response['expires_in'])))
|
||||||
|
|
||||||
|
return AccessToken(response['access_token'], token_expiry,
|
||||||
|
datetime_strategy=self.datetime_strategy)
|
||||||
|
|
||||||
|
def GetAuthorizationUri(self, redirect_uri, scopes, extra_params=None):
|
||||||
|
"""Gets the OAuth2 authorization URI and the specified scope(s).
|
||||||
|
|
||||||
|
Applications should navigate/redirect the user's user agent to this URI. The
|
||||||
|
user will be shown an approval UI requesting the user to approve access of
|
||||||
|
this client to the requested scopes under the identity of the authenticated
|
||||||
|
end user.
|
||||||
|
|
||||||
|
The application should expect the user agent to be redirected to the
|
||||||
|
specified redirect_uri after the user's approval/disapproval.
|
||||||
|
|
||||||
|
Installed applications may use the special redirect_uri
|
||||||
|
'urn:ietf:wg:oauth:2.0:oob' to indicate that instead of redirecting the
|
||||||
|
browser, the user be shown a confirmation page with a verification code.
|
||||||
|
The application should query the user for this code.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
redirect_uri: Either the string 'urn:ietf:wg:oauth:2.0:oob' for a
|
||||||
|
non-web-based application, or a URI that handles the callback from the
|
||||||
|
authorization server.
|
||||||
|
scopes: A list of strings specifying the OAuth scopes the application
|
||||||
|
requests access to.
|
||||||
|
extra_params: Optional dictionary of additional parameters to be passed to
|
||||||
|
the OAuth2 authorization URI.
|
||||||
|
Returns:
|
||||||
|
The authorization URI for the specified scopes as a string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
request = {
|
||||||
|
'response_type': 'code',
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'redirect_uri': redirect_uri,
|
||||||
|
'scope': ' '.join(scopes),
|
||||||
|
}
|
||||||
|
|
||||||
|
if extra_params:
|
||||||
|
request.update(extra_params)
|
||||||
|
url_parts = list(urlparse.urlparse(self.provider.authorization_uri))
|
||||||
|
# 4 is the index of the query part
|
||||||
|
request.update(dict(cgi.parse_qsl(url_parts[4])))
|
||||||
|
url_parts[4] = urllib.urlencode(request)
|
||||||
|
return urlparse.urlunparse(url_parts)
|
||||||
|
|
||||||
|
def ExchangeAuthorizationCode(self, code, redirect_uri, scopes):
|
||||||
|
"""Exchanges an authorization code for a refresh token.
|
||||||
|
|
||||||
|
Invokes this client's OAuth2 provider's token endpoint to exchange an
|
||||||
|
authorization code into a refresh token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: the authrorization code.
|
||||||
|
redirect_uri: Either the string 'urn:ietf:wg:oauth:2.0:oob' for a
|
||||||
|
non-web-based application, or a URI that handles the callback from the
|
||||||
|
authorization server.
|
||||||
|
scopes: A list of strings specifying the OAuth scopes the application
|
||||||
|
requests access to.
|
||||||
|
Returns:
|
||||||
|
A tuple consting of the resulting RefreshToken and AccessToken.
|
||||||
|
Raises:
|
||||||
|
AuthorizationCodeExchangeError: if an error occurs.
|
||||||
|
"""
|
||||||
|
request = {
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'code': code,
|
||||||
|
'redirect_uri': redirect_uri,
|
||||||
|
'scope': ' '.join(scopes),
|
||||||
|
}
|
||||||
|
LOG.debug('ExchangeAuthorizationCode request: %s', request)
|
||||||
|
|
||||||
|
response, error = self._TokenRequest(request)
|
||||||
|
LOG.debug(
|
||||||
|
'ExchangeAuthorizationCode response (error = %s): %s',
|
||||||
|
error, response)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
oauth2_error = ''
|
||||||
|
if response and response['error']:
|
||||||
|
oauth2_error = '; OAuth2 error: %s' % response['error']
|
||||||
|
raise AuthorizationCodeExchangeError(
|
||||||
|
'Failed to exchange refresh token into access token; '
|
||||||
|
'request failed: %s%s' % (str(error), oauth2_error))
|
||||||
|
|
||||||
|
if not 'access_token' in response:
|
||||||
|
raise AuthorizationCodeExchangeError(
|
||||||
|
'Failed to exchange authorization code into access token; '
|
||||||
|
'response: %s' % response)
|
||||||
|
|
||||||
|
token_expiry = None
|
||||||
|
if 'expires_in' in response:
|
||||||
|
token_expiry = (
|
||||||
|
self.datetime_strategy.utcnow() +
|
||||||
|
datetime.timedelta(seconds=int(response['expires_in'])))
|
||||||
|
|
||||||
|
access_token = AccessToken(response['access_token'], token_expiry,
|
||||||
|
datetime_strategy=self.datetime_strategy)
|
||||||
|
|
||||||
|
refresh_token = None
|
||||||
|
refresh_token_string = response.get('refresh_token', None)
|
||||||
|
|
||||||
|
token_exchange_lock.acquire()
|
||||||
|
try:
|
||||||
|
if refresh_token_string:
|
||||||
|
refresh_token = RefreshToken(self, refresh_token_string)
|
||||||
|
self.access_token_cache.PutToken(refresh_token.CacheKey(), access_token)
|
||||||
|
finally:
|
||||||
|
token_exchange_lock.release()
|
||||||
|
|
||||||
|
return (refresh_token, access_token)
|
||||||
|
|
||||||
|
|
||||||
|
class AccessToken(object):
|
||||||
|
"""Encapsulates an OAuth2 access token."""
|
||||||
|
|
||||||
|
def __init__(self, token, expiry, datetime_strategy=datetime.datetime):
|
||||||
|
self.token = token
|
||||||
|
self.expiry = expiry
|
||||||
|
self.datetime_strategy = datetime_strategy
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def UnSerialize(query):
|
||||||
|
"""Creates an AccessToken object from its serialized form."""
|
||||||
|
|
||||||
|
def GetValue(d, key):
|
||||||
|
return (d.get(key, [None]))[0]
|
||||||
|
kv = cgi.parse_qs(query)
|
||||||
|
if not kv['token']:
|
||||||
|
return None
|
||||||
|
expiry = None
|
||||||
|
expiry_tuple = GetValue(kv, 'expiry')
|
||||||
|
if expiry_tuple:
|
||||||
|
try:
|
||||||
|
expiry = datetime.datetime(
|
||||||
|
*[int(n) for n in expiry_tuple.split(',')])
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
return AccessToken(GetValue(kv, 'token'), expiry)
|
||||||
|
|
||||||
|
def Serialize(self):
|
||||||
|
"""Serializes this object as URI-encoded key-value pairs."""
|
||||||
|
# There's got to be a better way to serialize a datetime. Unfortunately,
|
||||||
|
# there is no reliable way to convert into a unix epoch.
|
||||||
|
kv = {'token': self.token}
|
||||||
|
if self.expiry:
|
||||||
|
t = self.expiry
|
||||||
|
tupl = (t.year, t.month, t.day, t.hour, t.minute, t.second, t.microsecond)
|
||||||
|
kv['expiry'] = ','.join([str(i) for i in tupl])
|
||||||
|
return urllib.urlencode(kv)
|
||||||
|
|
||||||
|
def ShouldRefresh(self, time_delta=300):
|
||||||
|
"""Whether the access token needs to be refreshed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time_delta: refresh access token when it expires within time_delta secs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the token is expired or about to expire, False if the
|
||||||
|
token should be expected to work. Note that the token may still
|
||||||
|
be rejected, e.g. if it has been revoked server-side.
|
||||||
|
"""
|
||||||
|
if self.expiry is None:
|
||||||
|
return False
|
||||||
|
return (self.datetime_strategy.utcnow()
|
||||||
|
+ datetime.timedelta(seconds=time_delta) > self.expiry)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.token == other.token and self.expiry == other.expiry
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'AccessToken(token=%s, expiry=%sZ)' % (self.token, self.expiry)
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshToken(object):
|
||||||
|
"""Encapsulates an OAuth2 refresh token."""
|
||||||
|
|
||||||
|
def __init__(self, oauth2_client, refresh_token):
|
||||||
|
self.oauth2_client = oauth2_client
|
||||||
|
self.refresh_token = refresh_token
|
||||||
|
|
||||||
|
def CacheKey(self):
|
||||||
|
"""Computes a cache key for this refresh token.
|
||||||
|
|
||||||
|
The cache key is computed as the SHA1 hash of the token, and as such
|
||||||
|
satisfies the FileSystemTokenCache requirement that cache keys do not leak
|
||||||
|
information about token values.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A hash key for this refresh token.
|
||||||
|
"""
|
||||||
|
h = sha1()
|
||||||
|
h.update(self.refresh_token)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
def GetAuthorizationHeader(self):
|
||||||
|
"""Gets the access token HTTP authorication header value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The value of an Authorization HTTP header that authenticates
|
||||||
|
requests with an OAuth2 access token based on this refresh token.
|
||||||
|
"""
|
||||||
|
return 'Bearer %s' % self.oauth2_client.GetAccessToken(self).token
|
@ -0,0 +1,374 @@
|
|||||||
|
# Copyright 2010 Google Inc. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Unit tests for oauth2_client."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import urllib2
|
||||||
|
import urlparse
|
||||||
|
from stat import S_IMODE
|
||||||
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
test_bin_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
|
||||||
|
|
||||||
|
lib_dir = os.path.join(test_bin_dir, '..')
|
||||||
|
sys.path.insert(0, lib_dir)
|
||||||
|
|
||||||
|
# Needed for boto.cacerts
|
||||||
|
boto_lib_dir = os.path.join(test_bin_dir, '..', 'boto')
|
||||||
|
sys.path.insert(0, boto_lib_dir)
|
||||||
|
|
||||||
|
import oauth2_client
|
||||||
|
|
||||||
|
LOG = logging.getLogger('oauth2_client_test')
|
||||||
|
|
||||||
|
class MockOpener:
|
||||||
|
def __init__(self):
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.open_error = None
|
||||||
|
self.open_result = None
|
||||||
|
self.open_capture_url = None
|
||||||
|
self.open_capture_data = None
|
||||||
|
|
||||||
|
def open(self, req, data=None):
|
||||||
|
self.open_capture_url = req.get_full_url()
|
||||||
|
self.open_capture_data = req.get_data()
|
||||||
|
if self.open_error is not None:
|
||||||
|
raise self.open_error
|
||||||
|
else:
|
||||||
|
return StringIO(self.open_result)
|
||||||
|
|
||||||
|
|
||||||
|
class MockDateTime:
|
||||||
|
def __init__(self):
|
||||||
|
self.mock_now = None
|
||||||
|
|
||||||
|
def utcnow(self):
|
||||||
|
return self.mock_now
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2ClientTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.opener = MockOpener()
|
||||||
|
self.mock_datetime = MockDateTime()
|
||||||
|
self.start_time = datetime.datetime(2011, 3, 1, 10, 25, 13, 300826)
|
||||||
|
self.mock_datetime.mock_now = self.start_time
|
||||||
|
self.client = oauth2_client.OAuth2Client(
|
||||||
|
oauth2_client.OAuth2Provider(
|
||||||
|
'Sample OAuth Provider',
|
||||||
|
'https://provider.example.com/oauth/provider?mode=authorize',
|
||||||
|
'https://provider.example.com/oauth/provider?mode=token'),
|
||||||
|
'clid', 'clsecret',
|
||||||
|
url_opener=self.opener, datetime_strategy=self.mock_datetime)
|
||||||
|
|
||||||
|
def testFetchAccessToken(self):
|
||||||
|
refresh_token = '1/ZaBrxdPl77Bi4jbsO7x-NmATiaQZnWPB51nTvo8n9Sw'
|
||||||
|
access_token = '1/aalskfja-asjwerwj'
|
||||||
|
self.opener.open_result = (
|
||||||
|
'{"access_token":"%s","expires_in":3600}' % access_token)
|
||||||
|
cred = oauth2_client.RefreshToken(self.client, refresh_token)
|
||||||
|
token = self.client.FetchAccessToken(cred)
|
||||||
|
|
||||||
|
self.assertEquals(
|
||||||
|
self.opener.open_capture_url,
|
||||||
|
'https://provider.example.com/oauth/provider?mode=token')
|
||||||
|
self.assertEquals({
|
||||||
|
'grant_type': ['refresh_token'],
|
||||||
|
'client_id': ['clid'],
|
||||||
|
'client_secret': ['clsecret'],
|
||||||
|
'refresh_token': [refresh_token]},
|
||||||
|
urlparse.parse_qs(self.opener.open_capture_data, keep_blank_values=True,
|
||||||
|
strict_parsing=True))
|
||||||
|
self.assertEquals(access_token, token.token)
|
||||||
|
self.assertEquals(
|
||||||
|
datetime.datetime(2011, 3, 1, 11, 25, 13, 300826),
|
||||||
|
token.expiry)
|
||||||
|
|
||||||
|
def testFetchAccessTokenFailsForBadJsonResponse(self):
|
||||||
|
self.opener.open_result = 'blah'
|
||||||
|
cred = oauth2_client.RefreshToken(self.client, 'abc123')
|
||||||
|
self.assertRaises(
|
||||||
|
oauth2_client.AccessTokenRefreshError, self.client.FetchAccessToken, cred)
|
||||||
|
|
||||||
|
def testFetchAccessTokenFailsForErrorResponse(self):
|
||||||
|
self.opener.open_error = urllib2.HTTPError(
|
||||||
|
None, 400, 'Bad Request', None, StringIO('{"error": "invalid token"}'))
|
||||||
|
cred = oauth2_client.RefreshToken(self.client, 'abc123')
|
||||||
|
self.assertRaises(
|
||||||
|
oauth2_client.AccessTokenRefreshError, self.client.FetchAccessToken, cred)
|
||||||
|
|
||||||
|
def testFetchAccessTokenFailsForHttpError(self):
|
||||||
|
self.opener.open_result = urllib2.HTTPError(
|
||||||
|
'foo', 400, 'Bad Request', None, None)
|
||||||
|
cred = oauth2_client.RefreshToken(self.client, 'abc123')
|
||||||
|
self.assertRaises(
|
||||||
|
oauth2_client.AccessTokenRefreshError, self.client.FetchAccessToken, cred)
|
||||||
|
|
||||||
|
def testGetAccessToken(self):
|
||||||
|
refresh_token = 'ref_token'
|
||||||
|
access_token_1 = 'abc123'
|
||||||
|
self.opener.open_result = (
|
||||||
|
'{"access_token":"%s",' '"expires_in":3600}' % access_token_1)
|
||||||
|
cred = oauth2_client.RefreshToken(self.client, refresh_token)
|
||||||
|
|
||||||
|
token_1 = self.client.GetAccessToken(cred)
|
||||||
|
|
||||||
|
# There's no access token in the cache; verify that we fetched a fresh
|
||||||
|
# token.
|
||||||
|
self.assertEquals({
|
||||||
|
'grant_type': ['refresh_token'],
|
||||||
|
'client_id': ['clid'],
|
||||||
|
'client_secret': ['clsecret'],
|
||||||
|
'refresh_token': [refresh_token]},
|
||||||
|
urlparse.parse_qs(self.opener.open_capture_data, keep_blank_values=True,
|
||||||
|
strict_parsing=True))
|
||||||
|
self.assertEquals(access_token_1, token_1.token)
|
||||||
|
self.assertEquals(self.start_time + datetime.timedelta(minutes=60),
|
||||||
|
token_1.expiry)
|
||||||
|
|
||||||
|
# Advance time by less than expiry time, and fetch another token.
|
||||||
|
self.opener.reset()
|
||||||
|
self.mock_datetime.mock_now = (
|
||||||
|
self.start_time + datetime.timedelta(minutes=55))
|
||||||
|
token_2 = self.client.GetAccessToken(cred)
|
||||||
|
|
||||||
|
# Since the access token wasn't expired, we get the cache token, and there
|
||||||
|
# was no refresh request.
|
||||||
|
self.assertEquals(token_1, token_2)
|
||||||
|
self.assertEquals(access_token_1, token_2.token)
|
||||||
|
self.assertEquals(None, self.opener.open_capture_url)
|
||||||
|
self.assertEquals(None, self.opener.open_capture_data)
|
||||||
|
|
||||||
|
# Advance time past expiry time, and fetch another token.
|
||||||
|
self.opener.reset()
|
||||||
|
self.mock_datetime.mock_now = (
|
||||||
|
self.start_time + datetime.timedelta(minutes=55, seconds=1))
|
||||||
|
access_token_2 = 'zyx456'
|
||||||
|
self.opener.open_result = (
|
||||||
|
'{"access_token":"%s",' '"expires_in":3600}' % access_token_2)
|
||||||
|
token_3 = self.client.GetAccessToken(cred)
|
||||||
|
|
||||||
|
# This should have resulted in a refresh request and a fresh access token.
|
||||||
|
self.assertEquals({
|
||||||
|
'grant_type': ['refresh_token'],
|
||||||
|
'client_id': ['clid'],
|
||||||
|
'client_secret': ['clsecret'],
|
||||||
|
'refresh_token': [refresh_token]},
|
||||||
|
urlparse.parse_qs(self.opener.open_capture_data, keep_blank_values=True,
|
||||||
|
strict_parsing=True))
|
||||||
|
self.assertEquals(access_token_2, token_3.token)
|
||||||
|
self.assertEquals(self.mock_datetime.mock_now + datetime.timedelta(minutes=60),
|
||||||
|
token_3.expiry)
|
||||||
|
|
||||||
|
def testGetAuthorizationUri(self):
|
||||||
|
authn_uri = self.client.GetAuthorizationUri(
|
||||||
|
'https://www.example.com/oauth/redir?mode=approve%20me',
|
||||||
|
('scope_foo', 'scope_bar'),
|
||||||
|
{'state': 'this and that & sundry'})
|
||||||
|
|
||||||
|
uri_parts = urlparse.urlsplit(authn_uri)
|
||||||
|
self.assertEquals(('https', 'provider.example.com', '/oauth/provider'),
|
||||||
|
uri_parts[:3])
|
||||||
|
|
||||||
|
self.assertEquals({
|
||||||
|
'response_type': ['code'],
|
||||||
|
'client_id': ['clid'],
|
||||||
|
'redirect_uri':
|
||||||
|
['https://www.example.com/oauth/redir?mode=approve%20me'],
|
||||||
|
'scope': ['scope_foo scope_bar'],
|
||||||
|
'state': ['this and that & sundry'],
|
||||||
|
'mode': ['authorize']},
|
||||||
|
urlparse.parse_qs(uri_parts[3]))
|
||||||
|
|
||||||
|
def testExchangeAuthorizationCode(self):
|
||||||
|
code = 'codeABQ1234'
|
||||||
|
exp_refresh_token = 'ref_token42'
|
||||||
|
exp_access_token = 'access_tokenXY123'
|
||||||
|
self.opener.open_result = (
|
||||||
|
'{"access_token":"%s","expires_in":3600,"refresh_token":"%s"}'
|
||||||
|
% (exp_access_token, exp_refresh_token))
|
||||||
|
|
||||||
|
refresh_token, access_token = self.client.ExchangeAuthorizationCode(
|
||||||
|
code, 'urn:ietf:wg:oauth:2.0:oob', ('scope1', 'scope2'))
|
||||||
|
|
||||||
|
self.assertEquals({
|
||||||
|
'grant_type': ['authorization_code'],
|
||||||
|
'client_id': ['clid'],
|
||||||
|
'client_secret': ['clsecret'],
|
||||||
|
'code': [code],
|
||||||
|
'redirect_uri': ['urn:ietf:wg:oauth:2.0:oob'],
|
||||||
|
'scope': ['scope1 scope2'] },
|
||||||
|
urlparse.parse_qs(self.opener.open_capture_data, keep_blank_values=True,
|
||||||
|
strict_parsing=True))
|
||||||
|
self.assertEquals(exp_access_token, access_token.token)
|
||||||
|
self.assertEquals(self.start_time + datetime.timedelta(minutes=60),
|
||||||
|
access_token.expiry)
|
||||||
|
|
||||||
|
self.assertEquals(self.client, refresh_token.oauth2_client)
|
||||||
|
self.assertEquals(exp_refresh_token, refresh_token.refresh_token)
|
||||||
|
|
||||||
|
# Check that the access token was put in the cache.
|
||||||
|
cached_token = self.client.access_token_cache.GetToken(
|
||||||
|
refresh_token.CacheKey())
|
||||||
|
self.assertEquals(access_token, cached_token)
|
||||||
|
|
||||||
|
|
||||||
|
class AccessTokenTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def testShouldRefresh(self):
|
||||||
|
mock_datetime = MockDateTime()
|
||||||
|
start = datetime.datetime(2011, 3, 1, 11, 25, 13, 300826)
|
||||||
|
expiry = start + datetime.timedelta(minutes=60)
|
||||||
|
token = oauth2_client.AccessToken(
|
||||||
|
'foo', expiry, datetime_strategy=mock_datetime)
|
||||||
|
|
||||||
|
mock_datetime.mock_now = start
|
||||||
|
self.assertFalse(token.ShouldRefresh())
|
||||||
|
|
||||||
|
mock_datetime.mock_now = start + datetime.timedelta(minutes=54)
|
||||||
|
self.assertFalse(token.ShouldRefresh())
|
||||||
|
|
||||||
|
mock_datetime.mock_now = start + datetime.timedelta(minutes=55)
|
||||||
|
self.assertFalse(token.ShouldRefresh())
|
||||||
|
|
||||||
|
mock_datetime.mock_now = start + datetime.timedelta(
|
||||||
|
minutes=55, seconds=1)
|
||||||
|
self.assertTrue(token.ShouldRefresh())
|
||||||
|
|
||||||
|
mock_datetime.mock_now = start + datetime.timedelta(
|
||||||
|
minutes=61)
|
||||||
|
self.assertTrue(token.ShouldRefresh())
|
||||||
|
|
||||||
|
mock_datetime.mock_now = start + datetime.timedelta(minutes=58)
|
||||||
|
self.assertFalse(token.ShouldRefresh(time_delta=120))
|
||||||
|
|
||||||
|
mock_datetime.mock_now = start + datetime.timedelta(
|
||||||
|
minutes=58, seconds=1)
|
||||||
|
self.assertTrue(token.ShouldRefresh(time_delta=120))
|
||||||
|
|
||||||
|
def testShouldRefreshNoExpiry(self):
|
||||||
|
mock_datetime = MockDateTime()
|
||||||
|
start = datetime.datetime(2011, 3, 1, 11, 25, 13, 300826)
|
||||||
|
token = oauth2_client.AccessToken(
|
||||||
|
'foo', None, datetime_strategy=mock_datetime)
|
||||||
|
|
||||||
|
mock_datetime.mock_now = start
|
||||||
|
self.assertFalse(token.ShouldRefresh())
|
||||||
|
|
||||||
|
mock_datetime.mock_now = start + datetime.timedelta(
|
||||||
|
minutes=472)
|
||||||
|
self.assertFalse(token.ShouldRefresh())
|
||||||
|
|
||||||
|
def testSerialization(self):
|
||||||
|
expiry = datetime.datetime(2011, 3, 1, 11, 25, 13, 300826)
|
||||||
|
token = oauth2_client.AccessToken('foo', expiry)
|
||||||
|
serialized_token = token.Serialize()
|
||||||
|
LOG.debug('testSerialization: serialized_token=%s' % serialized_token)
|
||||||
|
|
||||||
|
token2 = oauth2_client.AccessToken.UnSerialize(serialized_token)
|
||||||
|
self.assertEquals(token, token2)
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshTokenTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.opener = MockOpener()
|
||||||
|
self.mock_datetime = MockDateTime()
|
||||||
|
self.start_time = datetime.datetime(2011, 3, 1, 10, 25, 13, 300826)
|
||||||
|
self.mock_datetime.mock_now = self.start_time
|
||||||
|
self.client = oauth2_client.OAuth2Client(
|
||||||
|
oauth2_client.OAuth2Provider(
|
||||||
|
'Sample OAuth Provider',
|
||||||
|
'https://provider.example.com/oauth/provider?mode=authorize',
|
||||||
|
'https://provider.example.com/oauth/provider?mode=token'),
|
||||||
|
'clid', 'clsecret',
|
||||||
|
url_opener=self.opener, datetime_strategy=self.mock_datetime)
|
||||||
|
|
||||||
|
self.cred = oauth2_client.RefreshToken(self.client, 'ref_token_abc123')
|
||||||
|
|
||||||
|
def testUniqeId(self):
|
||||||
|
cred_id = self.cred.CacheKey()
|
||||||
|
self.assertEquals('0720afed6871f12761fbea3271f451e6ba184bf5', cred_id)
|
||||||
|
|
||||||
|
def testGetAuthorizationHeader(self):
|
||||||
|
access_token = 'access_123'
|
||||||
|
self.opener.open_result = (
|
||||||
|
'{"access_token":"%s","expires_in":3600}' % access_token)
|
||||||
|
|
||||||
|
self.assertEquals('Bearer %s' % access_token,
|
||||||
|
self.cred.GetAuthorizationHeader())
|
||||||
|
|
||||||
|
|
||||||
|
class FileSystemTokenCacheTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.cache = oauth2_client.FileSystemTokenCache()
|
||||||
|
self.start_time = datetime.datetime(2011, 3, 1, 10, 25, 13, 300826)
|
||||||
|
self.token_1 = oauth2_client.AccessToken('token1', self.start_time)
|
||||||
|
self.token_2 = oauth2_client.AccessToken(
|
||||||
|
'token2', self.start_time + datetime.timedelta(seconds=492))
|
||||||
|
self.key = 'token1key'
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
try:
|
||||||
|
os.unlink(self.cache.CacheFileName(self.key))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def testPut(self):
|
||||||
|
self.cache.PutToken(self.key, self.token_1)
|
||||||
|
# Assert that the cache file exists and has correct permissions.
|
||||||
|
self.assertEquals(
|
||||||
|
0600, S_IMODE(os.stat(self.cache.CacheFileName(self.key)).st_mode))
|
||||||
|
|
||||||
|
def testPutGet(self):
|
||||||
|
# No cache file present.
|
||||||
|
self.assertEquals(None, self.cache.GetToken(self.key))
|
||||||
|
|
||||||
|
# Put a token
|
||||||
|
self.cache.PutToken(self.key, self.token_1)
|
||||||
|
cached_token = self.cache.GetToken(self.key)
|
||||||
|
self.assertEquals(self.token_1, cached_token)
|
||||||
|
|
||||||
|
# Put a different token
|
||||||
|
self.cache.PutToken(self.key, self.token_2)
|
||||||
|
cached_token = self.cache.GetToken(self.key)
|
||||||
|
self.assertEquals(self.token_2, cached_token)
|
||||||
|
|
||||||
|
def testGetBadFile(self):
|
||||||
|
f = open(self.cache.CacheFileName(self.key), 'w')
|
||||||
|
f.write('blah')
|
||||||
|
f.close()
|
||||||
|
self.assertEquals(None, self.cache.GetToken(self.key))
|
||||||
|
|
||||||
|
def testCacheFileName(self):
|
||||||
|
cache = oauth2_client.FileSystemTokenCache(
|
||||||
|
path_pattern='/var/run/ccache/token.%(uid)s.%(key)s')
|
||||||
|
self.assertEquals('/var/run/ccache/token.%d.abc123' % os.getuid(),
|
||||||
|
cache.CacheFileName('abc123'))
|
||||||
|
|
||||||
|
cache = oauth2_client.FileSystemTokenCache(
|
||||||
|
path_pattern='/var/run/ccache/token.%(key)s')
|
||||||
|
self.assertEquals('/var/run/ccache/token.abc123',
|
||||||
|
cache.CacheFileName('abc123'))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
unittest.main()
|
@ -0,0 +1,110 @@
|
|||||||
|
# Copyright 2011 Google Inc. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Helper routines to facilitate use of oauth2_client in gsutil."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
import oauth2_client
|
||||||
|
|
||||||
|
GSUTIL_CLIENT_ID = '909320924072.apps.googleusercontent.com'
|
||||||
|
# Google OAuth2 clients always have a secret, even if the client is an installed
|
||||||
|
# application/utility such as gsutil. Of course, in such cases the "secret" is
|
||||||
|
# actually publicly known; security depends entirly on the secrecy of refresh
|
||||||
|
# tokens, which effectively become bearer tokens.
|
||||||
|
GSUTIL_CLIENT_NOTSOSECRET = 'p3RlpR10xMFh9ZXBS/ZNLYUu'
|
||||||
|
|
||||||
|
GOOGLE_OAUTH2_PROVIDER_LABEL = 'Google'
|
||||||
|
GOOGLE_OAUTH2_PROVIDER_AUTHORIZATION_URI = (
|
||||||
|
'https://accounts.google.com/o/oauth2/auth')
|
||||||
|
GOOGLE_OAUTH2_PROVIDER_TOKEN_URI = (
|
||||||
|
'https://accounts.google.com/o/oauth2/token')
|
||||||
|
|
||||||
|
OOB_REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
|
||||||
|
|
||||||
|
def OAuth2ClientFromBotoConfig(config):
|
||||||
|
token_cache = None
|
||||||
|
token_cache_type = config.get('OAuth2', 'token_cache', 'file_system')
|
||||||
|
|
||||||
|
if token_cache_type == 'file_system':
|
||||||
|
if config.has_option('OAuth2', 'token_cache_path_pattern'):
|
||||||
|
token_cache = oauth2_client.FileSystemTokenCache(
|
||||||
|
path_pattern=config.get('OAuth2', 'token_cache_path_pattern'))
|
||||||
|
else:
|
||||||
|
token_cache = oauth2_client.FileSystemTokenCache()
|
||||||
|
elif token_cache_type == 'in_memory':
|
||||||
|
token_cache = oauth2_client.InMemoryTokenCache()
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
"Invalid value for config option OAuth2/token_cache: %s" %
|
||||||
|
token_cache_type)
|
||||||
|
|
||||||
|
proxy = None
|
||||||
|
if (config.has_option('Boto', 'proxy')
|
||||||
|
and config.has_option('Boto', 'proxy_port')):
|
||||||
|
proxy = "%s:%s" % (config.get('Boto', 'proxy'),
|
||||||
|
config.get('Boto', 'proxy_port'))
|
||||||
|
|
||||||
|
provider_label = config.get(
|
||||||
|
'OAuth2', 'provider_label', GOOGLE_OAUTH2_PROVIDER_LABEL)
|
||||||
|
provider_authorization_uri = config.get(
|
||||||
|
'OAuth2', 'provider_authorization_uri',
|
||||||
|
GOOGLE_OAUTH2_PROVIDER_AUTHORIZATION_URI)
|
||||||
|
provider_token_uri = config.get(
|
||||||
|
'OAuth2', 'provider_token_uri', GOOGLE_OAUTH2_PROVIDER_TOKEN_URI)
|
||||||
|
|
||||||
|
client_id = config.get('OAuth2', 'client_id', GSUTIL_CLIENT_ID)
|
||||||
|
client_secret = config.get(
|
||||||
|
'OAuth2', 'client_secret', GSUTIL_CLIENT_NOTSOSECRET)
|
||||||
|
|
||||||
|
return oauth2_client.OAuth2Client(
|
||||||
|
oauth2_client.OAuth2Provider(
|
||||||
|
provider_label, provider_authorization_uri, provider_token_uri),
|
||||||
|
client_id, client_secret,
|
||||||
|
proxy=proxy, access_token_cache=token_cache)
|
||||||
|
|
||||||
|
def OAuth2ApprovalFlow(oauth2_client, scopes, launch_browser=False):
|
||||||
|
approval_url = oauth2_client.GetAuthorizationUri(OOB_REDIRECT_URI, scopes)
|
||||||
|
if launch_browser:
|
||||||
|
sys.stdout.write(
|
||||||
|
'Attempting to launch a browser with the OAuth2 approval dialog at '
|
||||||
|
'URL: %s\n\n'
|
||||||
|
'[Note: due to a Python bug, you may see a spurious error message "object is not\n'
|
||||||
|
'callable [...] in [...] Popen.__del__" which can be ignored.]\n\n' % approval_url)
|
||||||
|
else:
|
||||||
|
sys.stdout.write(
|
||||||
|
'Please navigate your browser to the following URL:\n%s\n' %
|
||||||
|
approval_url)
|
||||||
|
|
||||||
|
sys.stdout.write(
|
||||||
|
'In your browser you should see a page that requests you to authorize '
|
||||||
|
'gsutil to access\nGoogle Cloud Storage on your behalf. After you '
|
||||||
|
'approve, an authorization code will be displayed.\n\n')
|
||||||
|
if (launch_browser and
|
||||||
|
not webbrowser.open(approval_url, new=1, autoraise=True)):
|
||||||
|
sys.stdout.write(
|
||||||
|
'Launching browser appears to have failed; please navigate a browser '
|
||||||
|
'to the following URL:\n%s\n' % approval_url)
|
||||||
|
# Short delay; webbrowser.open on linux insists on printing out a message
|
||||||
|
# which we don't want to run into the prompt for the auth code.
|
||||||
|
time.sleep(2)
|
||||||
|
code = raw_input('Enter the authorization code: ')
|
||||||
|
|
||||||
|
refresh_token, access_token = oauth2_client.ExchangeAuthorizationCode(
|
||||||
|
code, OOB_REDIRECT_URI, scopes)
|
||||||
|
|
||||||
|
return refresh_token
|
||||||
|
|
@ -0,0 +1,24 @@
|
|||||||
|
from boto.auth_handler import AuthHandler
|
||||||
|
from boto.auth_handler import NotReadyToAuthenticate
|
||||||
|
import oauth2_client
|
||||||
|
import oauth2_helper
|
||||||
|
|
||||||
|
class OAuth2Auth(AuthHandler):
|
||||||
|
|
||||||
|
capability = ['google-oauth2', 's3']
|
||||||
|
|
||||||
|
def __init__(self, path, config, provider):
|
||||||
|
if (provider.name == 'google'
|
||||||
|
and config.has_option('Credentials', 'gs_oauth2_refresh_token')):
|
||||||
|
|
||||||
|
self.oauth2_client = oauth2_helper.OAuth2ClientFromBotoConfig(config)
|
||||||
|
|
||||||
|
self.refresh_token = oauth2_client.RefreshToken(
|
||||||
|
self.oauth2_client,
|
||||||
|
config.get('Credentials', 'gs_oauth2_refresh_token'))
|
||||||
|
else:
|
||||||
|
raise NotReadyToAuthenticate()
|
||||||
|
|
||||||
|
def add_auth(self, http_request):
|
||||||
|
http_request.headers['Authorization'] = \
|
||||||
|
self.refresh_token.GetAuthorizationHeader()
|
@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Utilities to facilitate maintaining one master list of package contents
|
||||||
|
# in MANIFEST.in and allow us to import that list into various packaging
|
||||||
|
# tools (e.g. rpmbuid and setup.py).
|
||||||
|
|
||||||
|
# Define the file in which we maintain package contents. Rather than
|
||||||
|
# hard-coding our package contents, to ease maintenance we read the
|
||||||
|
# manifest file to obtain the list of files and directories to include.
|
||||||
|
MANIFEST_IN = 'MANIFEST.in'
|
||||||
|
|
||||||
|
# Define input and output files for customizing the rpm package spec.
|
||||||
|
SPEC_IN = 'gsutil.spec.in'
|
||||||
|
SPEC_OUT = 'gsutil.spec'
|
||||||
|
|
||||||
|
# Root of rpmbuild tree for file enumeration in gsutil.spec file.
|
||||||
|
RPM_ROOT = '%{_datadir}/%{name}/'
|
||||||
|
|
||||||
|
def parse_manifest(files, dirs):
|
||||||
|
'''Parse contents of manifest file and append results to passed lists
|
||||||
|
of files and directories.
|
||||||
|
'''
|
||||||
|
f = open(MANIFEST_IN, 'r')
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
# Skip empty or comment lines.
|
||||||
|
if (len(line) <= 0) or (line[0] == '#'):
|
||||||
|
continue
|
||||||
|
tokens = line.split()
|
||||||
|
if len(tokens) >= 0:
|
||||||
|
if tokens[0] == 'include':
|
||||||
|
files.extend(tokens[1:])
|
||||||
|
elif tokens[0] == 'recursive-include' and tokens[2] == '*':
|
||||||
|
dirs.append(tokens[1])
|
||||||
|
else:
|
||||||
|
err = 'Unsupported type ' + tokens[0] + ' in ' + MANIFEST_IN + ' file.'
|
||||||
|
raise Exception(err)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# When executed as a separate script, create a dynamically generated rpm
|
||||||
|
# spec file. Otherwise, when loaded as a module by another script, no
|
||||||
|
# specific actions are taken, other than making utility functions available
|
||||||
|
# to the loading script.
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Running as main so generate a new rpm spec file.
|
||||||
|
files = []
|
||||||
|
dirs = []
|
||||||
|
parse_manifest(files, dirs)
|
||||||
|
fin = open(SPEC_IN, 'r')
|
||||||
|
fout = open(SPEC_OUT, 'w')
|
||||||
|
for line in fin:
|
||||||
|
if line.strip() == '###FILES_GO_HERE###':
|
||||||
|
for file in files:
|
||||||
|
fout.write(RPM_ROOT + file + '\n')
|
||||||
|
for dir in dirs:
|
||||||
|
fout.write(RPM_ROOT + dir + '/\n')
|
||||||
|
else:
|
||||||
|
fout.write(line)
|
||||||
|
fout.close()
|
||||||
|
fin.close()
|
@ -0,0 +1,30 @@
|
|||||||
|
Copyright (c) 2013, SaltyCrane
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
* Neither the name of the SaltyCrane nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived
|
||||||
|
from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,45 @@
|
|||||||
|
import time
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
|
||||||
|
def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None):
|
||||||
|
"""Retry calling the decorated function using an exponential backoff.
|
||||||
|
|
||||||
|
http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/
|
||||||
|
original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry
|
||||||
|
|
||||||
|
:param ExceptionToCheck: the exception to check. may be a tuple of
|
||||||
|
exceptions to check
|
||||||
|
:type ExceptionToCheck: Exception or tuple
|
||||||
|
:param tries: number of times to try (not retry) before giving up
|
||||||
|
:type tries: int
|
||||||
|
:param delay: initial delay between retries in seconds
|
||||||
|
:type delay: int
|
||||||
|
:param backoff: backoff multiplier e.g. value of 2 will double the delay
|
||||||
|
each retry
|
||||||
|
:type backoff: int
|
||||||
|
:param logger: logger to use. If None, print
|
||||||
|
:type logger: logging.Logger instance
|
||||||
|
"""
|
||||||
|
def deco_retry(f):
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def f_retry(*args, **kwargs):
|
||||||
|
mtries, mdelay = tries, delay
|
||||||
|
while mtries > 1:
|
||||||
|
try:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
except ExceptionToCheck, e:
|
||||||
|
msg = "%s, Retrying in %d seconds..." % (str(e), mdelay)
|
||||||
|
if logger:
|
||||||
|
logger.warning(msg)
|
||||||
|
else:
|
||||||
|
print msg
|
||||||
|
time.sleep(mdelay)
|
||||||
|
mtries -= 1
|
||||||
|
mdelay *= backoff
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return f_retry # true decorator
|
||||||
|
|
||||||
|
return deco_retry
|
Loading…
Reference in New Issue