Gyazo + Amazon S3 に AWS Lambda を足してもう少し安全にスクリーンショット共有


[Gyazo + Amazon CloudFront で簡単☆高速スクリーンショット共有](http://d.hatena.ne.jp/naoya/20130218/1361162225)


個人用の Gyazo 環境向けに、クライアントに手を入れて直接 Dropbox や S3 へスクリーンショットをアップロードするというテクニックがある。自前サーバを管理することなく利用できて大変ナイスなものの、クライアント側で比較的重要な固有情報を含んだ状態になっているので、取り扱いが若干難しいと思った。

そこで AWS Lambda を利用して、S3 へのアップロードを行う署名付き URL の発行をそちらで行い、クライアントでは受け取った情報だけでアップロード処理が進めれるような体制を作ってみた。


## 処理の流れと役割

![a200ceb299ac1e3739b6a9dc9d80c34b.png](https://qiita-image-store.s3.amazonaws.com/0/18511/ad8471ca-e8fd-6915-3a07-627e605f698d.png "a200ceb299ac1e3739b6a9dc9d80c34b.png")


### Gyazo

スクリーンショットを撮る。
通常ならば Gyazo サーバへスクリーンショットをアップロードするところを、後述する AWS Lambda へ S3 アップロードのための署名付き URL を発行するようリクエストを送る。その後、署名付き URL を使って S3 へスクリーンショットをアップロードする。

### Amazon S3

Gyazo スクリーンショットを保存すると共に参照リクエストされたデータを表示する。

### AWS Lambda

aws-sdk を使って S3 へのファイルアップロードのための署名付き URL を発行する。

### Amazon API Gateway

上記の Lambda Function を HTTPS 経由でやりとりするためのインターフェース。



## 構築、カスタマイズ手順

### Amazon S3

画像を格納するバケットを作成する。

Gyazo はスクリーンショットを撮った次の瞬間にブラウザでその画像が表示されるのが軽快で素晴らしく、それは同等の動きになって欲しい。そこでこのバケット内オブジェクトを公開状態にするべくバケットポリシーを設定する。 (`BUCKET_NAME` の部分は実際の S3 バケット名を記述)

```S3バケットポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1462499000000",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::BUCKET_NAME/*"
        }
    ]
}
```


### AWS Identity and Access Management (IAM)

IAM で Lambda 用のロールを作る。ここでは S3 への PutObject 操作と、Lambda 自身のログの書き出し権限を付与する。(`BUCKET_NAME` の部分は実際の S3 バケット名を記述)

```Lambda用ロール
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::BUCKET_NAME",
                "arn:aws:s3:::BUCKET_NAME/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        }
    ]
}
```

### AWS Lambda

aws-sdk を使って S3 への PutObject を行うことができる署名付き URL を発行するコードを用意する。

[OSSとして公開されている Gyazo](https://github.com/gyazo/Gyazo) のサーバの振る舞いを見てみると、画像データを受け取り MD5 ハッシュダイジェストを算出してファイル名を決定したりしているが、今回の Lambda のコードでは画像データ自体は受け取らず必要データを Gyazo クライアント側から受け取って処理をする形式にした。したがって元々サーバ側でやっていた処理の一部が Gyazo クライアントへ移ることになる。

実際のコード中身は以下のとおり。(`BUCKET_NAME` の部分は実際の S3 バケット名を記述)
ファイルのハッシュダイジェストと Content-Type を受け取り内容を検証、その内容に基づいて S3 の署名付き URL を発行して返却している。
余分ながら、PNG, GIFといった画像データを扱っているので、せっかくなのでJPEG画像も受け入れ可能にしておいた。


```javascript:Lambda関数(node.js)
var aws = require('aws-sdk');
var s3 = new aws.S3();

var BUCKET = "BUCKET_NAME";
var URL_PREFIX = 'https://s3-ap-northeast-1.amazonaws.com/' + BUCKET + '/';

exports.handler = function (event, context, callback) {
    var hash = event.hash;
    var contentType = event.contentType;

    if (!hash) {
        callback('no set a file hash digest.');
        return;
    }

    if (!contentType) {
        callback('no set content type.');
        return;
    }

    var key;

    if (contentType.match(/(\.|\/)png$/i)) {
        key = hash + ".png";
    } else if (contentType.match(/(\.|\/)gif$/i)) {
        key = hash + ".gif";
    } else if (contentType.match(/(\.|\/)jpe?g$/i)) {
        key = hash + ".jpg";
    } else {
        callback('invalid content type (gif, jpg, or png)');
        return;
    }

    // console.log('Content-Type: ' + contentType);

    var params = {
        Bucket: BUCKET,
        Key: key,
        Body: '',
        ContentType: contentType,
        Expires: 60
    };
    s3.getSignedUrl('putObject', params, function (err, url) {
        if (err) {
            callback(err);
            return;
        }

        // console.log('Signed-URL: ', url);

        callback(null, {
            'signedUrl': url,
            'imageUrl': URL_PREFIX + key
        });
    });
};

```

作成した Lambda function の設定画面から「API endpoints」をタブを開いて API Gateway 経由でこの function が実行されるようにする。作成後に API Gateway 側をチェックして、この function のエンドポイント URL をチェックする。


### Gyazo クライアント

Gyazo クライアントとして振る舞いは、OSX 版の場合 `Gyazo.app/Contents/Resources/script` に ruby コードとして記述されているので、これを AWS 向けの振る舞いとなるようコードを書き換える。（元の Gyazo.app を別名にコピーした上で別アプリとして動くように）

まずはコードの先頭で、追加で必要になるモジュールを読み込むように。

```ruby:読み込みモジュール追加
require 'uri'
require 'mime/types'
require 'digest/md5'
```

続けて AWS 向けの新しい振る舞いを関数として追加する。

* 署名付きURLの発行リクエスト
* S3へのファイルアップロードリクエスト

まずは署名付き URL を発行する関数。ここでは API Gateway を通じて Lambda へリクエストすることができるエントリポイント URL を設定する必要がある。

```ruby:アップロード用署名付きURLを発行する関数を追加
@resource = "https://example.execute-api.ap-northeast-1.amazonaws.com/prod/s3_put_url"

def create_upload_url(hash, content_type)
  uri = URI.parse(@resource)
  use_ssl = uri.scheme == 'https' ? true : false

  begin
    res = Net::HTTP.start(uri.host, uri.port, :use_ssl => use_ssl) do |http|
      body = {
        "hash" => hash,
        "contentType" => content_type,
      }.to_json
      header = {
        "Content-Type" => "application/json"
      }

      http.open_timeout = 10
      http.read_timeout = 10
      http.post(uri.request_uri, body, header)
    end

    case res
    when Net::HTTPSuccess
      JSON.parse(res.body)
    else
      abort("Failed to upload url creation.\n#{res.code}: #{res.message}")
    end
  rescue => e
    abort("Failed to upload url creation.\n#{e}")
    nil
  end
end
```

もう一つの S3 へのアップロードを行う関数。発行された署名付き URL を渡して処理を行うので、S3 に関する設定はここでは登場しない。

```ruby:アップロード用署名付きURLを使ってS3へファイルを格納する関数を追加
def upload_file(url, data, content_type)
  uri = URI.parse(url)
  use_ssl = uri.scheme == 'https' ? true : false

  begin
    res = Net::HTTP.start(uri.host, uri.port, :use_ssl => use_ssl) do |http|
      req = Net::HTTP::Put.new(uri.request_uri)
      req["Content-Type"] = content_type
      req.body = data
      http.request(req)
    end

    case res
    when Net::HTTPSuccess
      res
    else
      abort("Failed to upload.\n#{res.code}: #{res.message}")
    end
  rescue => e
    abort("Failed to upload.\n#{e}")
    nil
  end
end
```


最後に、元々のアップロード処理を行っている箇所のコード以下を、下記のように前述関数を利用するコードへ書き換える。

```ruby:アップロード処理コードの変更
# upload
hash = Digest::MD5.hexdigest(imagedata)
content_type = "image/png"
urls = create_upload_url(hash, content_type)
res = upload_file(urls["signedUrl"], imagedata, content_type)

if res.code.to_i >= 400
  abort("Failed to upload.\n#{res.code}: #{res.message}")
end

IO.popen("pbcopy","r+"){|io|
  io.write urls["imageUrl"]
  io.close
}
system "open #{urls["imageUrl"]}"

```


以上で完了。
カスタマイズ版 Gyazo.app を使ってスクリーンショットを撮ってみると、ブラウザ上で表示される画像やクリップボードへコピーされる URL は S3 へアップされたものになっているはず。



## まとめ

Gyazo のデータを格納する流れをカスタマイズして、AWS Lambda で S3 アップロードのための署名付き URL を発行してスクリーンショットを格納する仕組みを紹介した。

この仕組みではクライアント（＝ Gyazo クライアント）に AWS のアクセスキー情報などが一切登場することがなく唯一 Lambda を叩く API Gateway エントリポイント URL が現れるのみなので、たとえばチームなどで利用する際にも安全にクライアントを配ることができる。