非公開のS3に期間限定でアップロードしてもらうためのシンプルな運用

S3は非常に便利かつ、そのセキュリティの高さからセキュアな用途にも使われつつあります。

一方でファイルの交換でDropboxやGoogleDriveを禁止しているところは依然として多いです。クリック一つで、世界中に公開されてしまうというのは、運用上のリスクとも言えそうです。

とはいえ、大容量のファイルを「受け取りたい」というだけのニーズに答えれないのも辛い。

## 想定するシナリオ

- 自社はプライベートなS3バケットに向けた一時的なアップロード用のHTMLフォームを作成
- 自社から取引先にアップロードフォームをメールで送信
- 取引先は、アップロードフォームを開いてファイルをアップロード
- 自社はプライベートなS3バケットからデータを取り出す

という流れです。

## サンプルソース

前提事項として、AWSのアクセスキーとシークレットキーはの環境変数で設定しているものとします。AWSのアクセスキーなどは、切り替えまで考えると、環境変数で設定するのが一番手軽だと思います（ファイルだとどうしてもうっかりコミットしてしまう）


```bash
export AWS_ACCESS_KEY_ID=XXXX
export AWS_SECRET_ACCESS_KEY=XXXX
export AWS_DEFAULT_REGION=ap-northeast-1
```

```uploadform.rb
require 'base64'
require 'openssl'
require 'digest/sha1'
require 'time'


# 各種設定
bucket_name = "バケット名" #必ず変える
upload_dir = "uploads/" #アップロード用ディレクトリは予め作っておく
redirect_url = "http://qiita.com/" #アップロードが完了した時のリダイレクト先
expiration = (Time.now + 60*60*24*7).utc.iso8601 # 1week
upload_max = 1024 * 1024 * 1024 #1G
 
#各種アップロードの制限をpolicyとして記載する
policy_document = <<EOS
{"expiration": "#{expiration}",
  "conditions": [ 
    {"bucket": "#{bucket_name}"}, 
    ["starts-with", "$key", "#{upload_dir}"],
    {"acl": "private"},
    {"success_action_redirect": "#{redirect_url}"},
    ["starts-with", "$Content-Type", ""],
    ["content-length-range", 0, #{upload_max}]
  ]
}
EOS

# policyに対してsignatureを生成することで、ユーザ側で設定を変更できないようにする
policy = Base64.encode64(policy_document).gsub("\n","")
signature = Base64.encode64(
    OpenSSL::HMAC.digest(
        OpenSSL::Digest::Digest.new('sha1'), 
        ENV['AWS_SECRET_ACCESS_KEY'], policy)
    ).gsub("\n","")

#アップロードフォームを作成（ポリシーとシグネチャもポストするからフォームのパラメータは変えれない）
html = <<EOS
<html>
  <head>
    <title>S3 POST Form</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
 
  <body>
  一週間限定のアップロードフォームです。最大1Gまでアップロードできます。<br/>
  アップロードが終わったら担当者にご連絡ください
    <form action="https://#{bucket_name}.s3.amazonaws.com/" method="post" enctype="multipart/form-data">
      <input type="hidden" name="key" value="uploads/${filename}">
      <input type="hidden" name="AWSAccessKeyId" value="#{ENV['AWS_ACCESS_KEY_ID']}">
      <input type="hidden" name="acl" value="private">
      <input type="hidden" name="success_action_redirect" value="#{redirect_url}">
      <input type="hidden" name="policy" value="#{policy}">
      <input type="hidden" name="signature" value="#{signature}">
      <input type="hidden" name="Content-Type" value="">
      <input name="file" type="file" style="margin:10px;">
      <br>
      <input type="submit" value="Upload" style="margin:10px;"> 
    </form>
  </body>
</html>
EOS

puts html
```

## 実行
```bash
$ ruby uploadform.rb >　upload.html
```
![SS 2017-06-04 11.14.07.png](https://qiita-image-store.s3.amazonaws.com/0/53489/39542d4e-2cb5-7fec-df81-93c02f3ae504.png)


## 解説
### 他の案
s3にはpresingedurlという仕組みもあり、オブジェクト単位で、期間限定URLを生成する仕組みがS3にはあります。aws cli からもpresingedurlは生成できるのですが、基本はGETリクエストです。つまり期間限定のダウンロードURLの生成が基本的な機能のです。

SDKを使えば、PUTリクエストに対するpresingedurlも生成できるのですが、先にファイル名まで指定しなくてはなりません。できればユーザがアップロードするファイル名でS3に保存したいです。そのあたりが扱いにくいです。

### 真のサーバーレス
Lambdaとかでサーバーレスのフォームを作るとかってのもあるし、JSだけで動くS3アップロードフォームの例もあるのですが、サーバーレスというからには、アップロード用の画面さえもWebサーバを用意したくない（これはただの意地）。httpで何かを公開するということ時点で、企業によっては、何らかのレビューを通らないと行けないケースは多いかと思います。

なので、ローカルのHTMLで完結したく、ローカルのHTMLだとJavaScriptが動かないケースもあり、JSなしアップロードフォームを都度作るという方式にしました。

### 参考URL
http://dev.classmethod.jp/cloud/aws-s3-direct-upload/