1 # -*- coding: utf-8 -*-
2 # Copyright 2014 Google Inc. All Rights Reserved.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 """Implementation of Url Signing workflow.
17 see: https://developers.google.com/storage/docs/accesscontrol#Signed-URLs)
20 from __future__
import absolute_import
24 from datetime
import datetime
25 from datetime
import timedelta
31 from apitools
.base
.py
.exceptions
import HttpError
32 from apitools
.base
.py
.http_wrapper
import MakeRequest
33 from apitools
.base
.py
.http_wrapper
import Request
35 from gslib
.command
import Command
36 from gslib
.command_argument
import CommandArgument
37 from gslib
.cs_api_map
import ApiSelector
38 from gslib
.exception
import CommandException
39 from gslib
.storage_url
import ContainsWildcard
40 from gslib
.storage_url
import StorageUrlFromString
41 from gslib
.util
import GetNewHttp
42 from gslib
.util
import NO_MAX
43 from gslib
.util
import UTF8
47 # pylint: disable=C6204
48 from OpenSSL
.crypto
import load_pkcs12
49 from OpenSSL
.crypto
import sign
58 gsutil signurl [-c] [-d] [-m] [-p] pkcs12-file url...
61 _DETAILED_HELP_TEXT
= ("""
67 The signurl command will generate signed urls that can be used to access
68 the specified objects without authentication for a specific period of time.
70 Please see the `Signed URLs documentation
71 <https://developers.google.com/storage/docs/accesscontrol#Signed-URLs>`_ for
72 background about signed URLs.
74 Multiple gs:// urls may be provided and may contain wildcards. A signed url
75 will be produced for each provided url, authorized
76 for the specified HTTP method and valid for the given duration.
78 Note: Unlike the gsutil ls command, the signurl command does not support
79 operations on sub-directories. For example, if you run the command:
81 gsutil signurl <private-key-file> gs://some-bucket/some-object/
83 The signurl command uses the private key for a service account (the
84 '<private-key-file>' argument) to generate the cryptographic
85 signature for the generated URL. The private key file must be in PKCS12
86 format. The signurl command will prompt for the passphrase used to protect
87 the private key file (default 'notasecret'). For more information
88 regarding generating a private key for use with the signurl command please
89 see the `Authentication documentation.
90 <https://developers.google.com/storage/docs/authentication#generating-a-private-key>`_
92 gsutil will look up information about the object "some-object/" (with a
93 trailing slash) inside bucket "some-bucket", as opposed to operating on
94 objects nested under gs://some-bucket/some-object. Unless you actually
95 have an object with that name, the operation will fail.
98 -m Specifies the HTTP method to be authorized for use
99 with the signed url, default is GET.
101 -d Specifies the duration that the signed url should be valid
102 for, default duration is 1 hour.
104 Times may be specified with no suffix (default hours), or
105 with s = seconds, m = minutes, h = hours, d = days.
107 This option may be specified multiple times, in which case
108 the duration the link remains valid is the sum of all the
111 -c Specifies the content type for which the signed url is
114 -p Specify the keystore password instead of prompting.
118 Create a signed url for downloading an object valid for 10 minutes:
120 gsutil signurl -d 10m <private-key-file> gs://<bucket>/<object>
122 Create a signed url for uploading a plain text file via HTTP PUT:
124 gsutil signurl -m PUT -d 1h -c text/plain <private-key-file> \\
127 To construct a signed URL that allows anyone in possession of
128 the URL to PUT to the specified bucket for one day, creating
129 any object of Content-Type image/jpg, run:
131 gsutil signurl -m PUT -d 1d -c image/jpg <private-key-file> \\
138 def _DurationToTimeDelta(duration
):
139 r
"""Parses the given duration and returns an equivalent timedelta."""
141 match
= re
.match(r
'^(\d+)([dDhHmMsS])?$', duration
)
143 raise CommandException('Unable to parse duration string')
145 duration
, modifier
= match
.groups('h')
146 duration
= int(duration
)
147 modifier
= modifier
.lower()
150 ret
= timedelta(days
=duration
)
151 elif modifier
== 'h':
152 ret
= timedelta(hours
=duration
)
153 elif modifier
== 'm':
154 ret
= timedelta(minutes
=duration
)
155 elif modifier
== 's':
156 ret
= timedelta(seconds
=duration
)
161 def _GenSignedUrl(key
, client_id
, method
, md5
,
162 content_type
, expiration
, gcs_path
):
163 """Construct a string to sign with the provided key and returns \
166 tosign
= ('{0}\n{1}\n{2}\n{3}\n/{4}'
167 .format(method
, md5
, content_type
,
168 expiration
, gcs_path
))
169 signature
= base64
.b64encode(sign(key
, tosign
, 'RSA-SHA256'))
171 final_url
= ('https://storage.googleapis.com/{0}?'
172 'GoogleAccessId={1}&Expires={2}&Signature={3}'
173 .format(gcs_path
, client_id
, expiration
,
174 urllib
.quote_plus(str(signature
))))
179 def _ReadKeystore(ks_contents
, passwd
):
180 ks
= load_pkcs12(ks_contents
, passwd
)
181 client_id
= (ks
.get_certificate()
183 .CN
.replace('.apps.googleusercontent.com',
184 '@developer.gserviceaccount.com'))
189 class UrlSignCommand(Command
):
190 """Implementation of gsutil url_sign command."""
192 # Command specification. See base class for documentation.
193 command_spec
= Command
.CreateCommandSpec(
195 command_name_aliases
=['signedurl', 'queryauth'],
196 usage_synopsis
=_SYNOPSIS
,
199 supported_sub_args
='m:d:c:p:',
201 provider_url_ok
=False,
203 gs_api_support
=[ApiSelector
.XML
, ApiSelector
.JSON
],
204 gs_default_api
=ApiSelector
.JSON
,
206 CommandArgument
.MakeNFileURLsArgument(1),
207 CommandArgument
.MakeZeroOrMoreCloudURLsArgument()
210 # Help specification. See help_provider.py for documentation.
211 help_spec
= Command
.HelpSpec(
213 help_name_aliases
=['signedurl', 'queryauth'],
214 help_type
='command_help',
215 help_one_line_summary
='Create a signed url',
216 help_text
=_DETAILED_HELP_TEXT
,
217 subcommand_help_text
={},
220 def _ParseAndCheckSubOpts(self
):
221 # Default argument values
227 for o
, v
in self
.sub_opts
:
229 if delta
is not None:
230 delta
+= _DurationToTimeDelta(v
)
232 delta
= _DurationToTimeDelta(v
)
240 self
.RaiseInvalidArgumentException()
243 delta
= timedelta(hours
=1)
245 expiration
= calendar
.timegm((datetime
.utcnow() + delta
).utctimetuple())
246 if method
not in ['GET', 'PUT', 'DELETE', 'HEAD']:
247 raise CommandException('HTTP method must be one of [GET|HEAD|PUT|DELETE]')
249 return method
, expiration
, content_type
, passwd
251 def _ProbeObjectAccessWithClient(self
, key
, client_id
, gcs_path
):
252 """Performs a head request against a signed url to check for read access."""
254 signed_url
= _GenSignedUrl(key
, client_id
, 'HEAD', '', '',
255 int(time
.time()) + 10, gcs_path
)
259 req
= Request(signed_url
, 'HEAD')
260 response
= MakeRequest(h
, req
)
262 if response
.status_code
not in [200, 403, 404]:
263 raise HttpError(response
)
265 return response
.status_code
266 except HttpError
as e
:
267 raise CommandException('Unexpected response code while querying'
268 'object readability ({0})'.format(e
.message
))
270 def _EnumerateStorageUrls(self
, in_urls
):
273 for url_str
in in_urls
:
274 if ContainsWildcard(url_str
):
275 ret
.extend([blr
.storage_url
for blr
in self
.WildcardIterator(url_str
)])
277 ret
.append(StorageUrlFromString(url_str
))
281 def RunCommand(self
):
282 """Command entry point for signurl command."""
284 raise CommandException(
285 'The signurl command requires the pyopenssl library (try pip '
286 'install pyopenssl or easy_install pyopenssl)')
288 method
, expiration
, content_type
, passwd
= self
._ParseAndCheckSubOpts
()
289 storage_urls
= self
._EnumerateStorageUrls
(self
.args
[1:])
292 passwd
= getpass
.getpass('Keystore password:')
294 ks
, client_id
= _ReadKeystore(open(self
.args
[0], 'rb').read(), passwd
)
296 print 'URL\tHTTP Method\tExpiration\tSigned URL'
297 for url
in storage_urls
:
298 if url
.scheme
!= 'gs':
299 raise CommandException('Can only create signed urls from gs:// urls')
301 gcs_path
= url
.bucket_name
303 # Need to url encode the object name as Google Cloud Storage does when
304 # computing the string to sign when checking the signature.
305 gcs_path
= '{0}/{1}'.format(url
.bucket_name
,
306 urllib
.quote(url
.object_name
.encode(UTF8
)))
308 final_url
= _GenSignedUrl(ks
.get_privatekey(), client_id
,
309 method
, '', content_type
, expiration
,
312 expiration_dt
= datetime
.fromtimestamp(expiration
)
314 print '{0}\t{1}\t{2}\t{3}'.format(url
.url_string
.encode(UTF8
), method
,
316 .strftime('%Y-%m-%d %H:%M:%S')),
317 final_url
.encode(UTF8
))
319 response_code
= self
._ProbeObjectAccessWithClient
(ks
.get_privatekey(),
322 if response_code
== 404 and method
!= 'PUT':
324 msg
= ('Bucket {0} does not exist. Please create a bucket with '
325 'that name before a creating signed URL to access it.'
328 msg
= ('Object {0} does not exist. Please create/upload an object '
329 'with that name before a creating signed URL to access it.'
332 raise CommandException(msg
)
333 elif response_code
== 403:
335 '%s does not have permissions on %s, using this link will likely '
336 'result in a 403 error until at least READ permissions are granted',