前回、Google Cloud Storage(GCS)の署名付きURLが生成できることを確認したのですが、実際にURLを利用してファイルをアップロードした際にハマりました。

PythonでGoogle Cloud Storageの署名付きURLを作成する
https://cloudpack.media/45121

再現してみる

前回、署名付きURLを作成する際に、Content-Type を空にしていたのですが、そのままでcurl でファイルをアップロードできたものの、Webアプリからアップロードしようとすると403 エラーとなりハマっていました。

main.py_署名付きURLを作成するスクリプト

import time

import urllib

from datetime import datetime, timedelta

import os

import base64

from oauth2client.service_account import ServiceAccountCredentials

API_ACCESS_ENDPOINT = 'https://storage.googleapis.com'


def sign_url(bucket, bucket_object, method, expires_after_seconds=60):
    gcs_filename = '/%s/%s' % (bucket, bucket_object)

    # content_typeを空で生成している
    content_md5, content_type = None, None

    credentials = ServiceAccountCredentials.from_json_keyfile_name('[サービスアカウントキーのファイルパス]')
    google_access_id = credentials.service_account_email

    expiration = datetime.now() + timedelta(seconds=expires_after_seconds)
    expiration = int(time.mktime(expiration.timetuple()))

    signature_string = '\n'.join([
        method,
        content_md5 or '',
        content_type or '',
        str(expiration),
        gcs_filename])
    _, signature_bytes = credentials.sign_blob(signature_string)
    signature = base64.b64encode(signature_bytes)

    query_params = {'GoogleAccessId': google_access_id,
                    'Expires': str(expiration),
                    'Signature': signature}

    return '{endpoint}{resource}?{querystring}'.format(
        endpoint=API_ACCESS_ENDPOINT,
        resource=gcs_filename,
        querystring=urllib.parse.urlencode(query_params))


if __name__ == '__main__':
    url = sign_url('[バケット名]', 'hoge.json', 'PUT')
    print(url)

再現すると、こんな感じです。署名付きURLを生成します。

> python main.py

https://storage.googleapis.com/xxxxx/hoge.json?GoogleAccessId=xxxx%40xxxxx.iam.gserviceaccount.com&Expires=1542800155&Signature=xxx長いxxx

curl でCoontent-Type指定しないままPUTでファイルアップロードしてみます。

> curl -i -X PUT https://storage.googleapis.com/(略) \
  --upload-file "[アップロードするファイルパス]"

HTTP/1.1 100 Continue

HTTP/1.1 200 OK
X-GUploader-UploadID: AEnB2Urlfu1tTz2Hm49qPwusYhRdMEYJkTVxVOV0AX21pz5_Ixt4BrD1xspqiVcvQvHYnVDXWn-FZFKd0vk4DDE6IDiVoWh1Uw
ETag: "8d7f148d56e423e591ad9a54d46d5cb8"
x-goog-generation: 1542800499311073
x-goog-metageneration: 1
x-goog-hash: crc32c=elpi7A==
x-goog-hash: md5=jX8UjVbkI+WRrZpU1G1cuA==
x-goog-stored-content-length: 42542345
x-goog-stored-content-encoding: identity
Vary: Origin
Content-Length: 0
Date: Wed, 21 Nov 2018 11:41:39 GMT
Server: UploadServer
Content-Type: text/html; charset=UTF-8
Alt-Svc: quic=":443"; ma=2592000; v="44,43,39,35"

はい。無事にアップロードできました。
次にcurlContent-Type を指定してPUTしてみます。

> curl -i -X PUT https://storage.googleapis.com/(略) \
  --upload-file "[アップロードするファイルパス]" \
  -H 'Content-Type: application/json'

<?xml version='1.0' encoding='UTF-8'?><Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.</Message><StringToSign>PUT

application/json
1542800722
/xxxxx/hoge.json</StringToSign></Error>

はい。
does not match the signature you provided って怒られました。

仕方ないので、署名付きURLを生成するスクリプトでContent-Type を指定してみます。

一部抜粋

 # content_typeを空で生成している
    content_md5, content_type = None, 'application/json'
> curl -i -X PUT https://storage.googleapis.com/(略) \
  --upload-file "[アップロードするファイルパス]" \
  -H 'Content-Type: application/json'

HTTP/1.1 100 Continue

HTTP/1.1 200 OK
X-GUploader-UploadID: AEnB2UrSSXsLx_oUYGvy0oToDzBCs_JeSf13NCmc8PdNVpdL-SfwrRkPNxO4f64n7j3uv4UDLpOF8r4zIe3xG8pPQPTd3QvwWg
ETag: "8d7f148d56e423e591ad9a54d46d5cb8"
x-goog-generation: 1542801014953412
x-goog-metageneration: 1
x-goog-hash: crc32c=elpi7A==
x-goog-hash: md5=jX8UjVbkI+WRrZpU1G1cuA==
x-goog-stored-content-length: 42542345
x-goog-stored-content-encoding: identity
Vary: Origin
Content-Length: 0
Date: Wed, 21 Nov 2018 11:50:15 GMT
Server: UploadServer
Content-Type: text/html; charset=UTF-8
Alt-Svc: quic=":443"; ma=2592000; v="44,43,39,35"

はい。
署名付きURLでContent-Type を指定すると大丈夫でした。

まとめ

ドキュメントを見る限り必須の指定ではないものの、空文字を含む文字列が一致しないと、受け入れてくれないみたいです。悲しいTT 俺の一日返せぇ

署名付き URL | Cloud Storage ドキュメント | Google Cloud
https://cloud.google.com/storage/docs/access-control/signed-urls

必要に応じて指定。content-type を指定する場合、クライアント(ブラウザ)は、この HTTP ヘッダーを同じ値に設定する必要があります。

参考

PythonでGoogle Cloud Storageの署名付きURLを作成する
https://cloudpack.media/45121

署名付き URL | Cloud Storage ドキュメント | Google Cloud
https://cloud.google.com/storage/docs/access-control/signed-urls

元記事はこちら

Google Cloud Storageの署名付きURL作成時にContent-Type を指定せずPUT時にContent-Type を含めると403エラーになる