S3 + Lambda + Cognitoを使って、簡単お問い合わせシステム構築

S3とLambdaとCognitoを使って、簡単でセキュアなお問い合わせフォームシステムを作ってみます。

**記事執筆当時、Cognitoがバージニア/アイルランドのみ利用可能だったため、今回はS3/Lambda/Congito全部バージニア(us-east-1)に揃えています（S3とLambdaはリージョン揃える必要があります）。2015年9月以降、東京リージョンでもCognitoは利用可能となっています。**

# 全体像

S3上にホスティングしているお問い合わせフォームから投稿すると、S3上にJSON形式で内容がuploadされます。ファイルがuploadされると、Lambdaがイベントフックして内容をGmailに送信する構造です。
お問い合わせ数の少ないサイトでは、これで十分でしょう。

SQLインジェクションとは無縁ですし、負荷も気にしなくて良いので、選択肢としてはありかなと思います。

# 0 準備
## 0-1 : S3バケットの作成

ホスティング用のS3バケットを作成します。仮にバケット名をxxx.example.comとし、Static Web HostingをONにします。

# 1 Cognito

フォーム投稿ボタンを押した時に、データをS3に時限でupload可能なIAM Roleを発行してもらうため、AWS Cognitoの[Security Token Service](http://docs.aws.amazon.com/ja_jp/STS/latest/UsingSTS/Welcome.html)を使います。

## 1-1: Cognitoで identity pool作成

CognitoトップからCreate identity poolをします。


Identity Pool Name: 適当なプール名を入れてください
Enable Access to Unauthenticated Identities: 認証なしで、時限IAM Roleを発行します。Amazon/Facebook/Twitterなどのアカウントを要求することも可能です。

![cognito0](https://s3-ap-northeast-1.amazonaws.com/qiita-3-shake.com/cognito_0.png)

![cognito1](https://s3-ap-northeast-1.amazonaws.com/qiita-3-shake.com/cognito_1.png)

identity poolが作成されたら、identity pool idをメモします。

![cognitox](https://s3-ap-northeast-1.amazonaws.com/qiita-3-shake.com/cognito_x.png)

## 1-2: 自動作成したIAM RoleにS3 upload権限を追加
次に自動作成したIAM Roleにs3:PutObject / s3:PutObjectAcl権限を追加します。

![cognito2](https://s3-ap-northeast-1.amazonaws.com/qiita-3-shake.com/cognito_2.png)

![cognito3](https://s3-ap-northeast-1.amazonaws.com/qiita-3-shake.com/cognito_3.png)

## 1-3: S3バケットにCORS許可

JavaScriptでS3にアクセスするため、S3に[CORS(Cross-Origin Resource Sharing)](http://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/cors.html)を許可します。

S3バケット(xxx.example.com) => Permissions => Edit CORS Configuration でCORSを設定します。

- Allowedmethod: PUT（upload）のみ許可します 
- AllowedOrigin: お問い合わせフォームのあるドメインを指定（*にしてしまうと、どこからでもcognitoで発行したSecurity TokenでS3 putができるようになります。S3 put攻撃されたくないので、ここは制限します）

```
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>xxx.example.com.s3-website-us-east-1.amazonaws.com</AllowedOrigin>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>
```

以上で、お問い合わせフォーム（xxx.example.com）からのみ、時限でs3にファイルをputできるIAM Roleが発行可能になりました。

# 2 お問い合わせフォーム本体

次にお問い合わせフォーム本体をs3にuploadしましょう。
今回は、「問い合わせタイトル」「返信メールアドレス」「本文」の3項目を投稿させます。
重要なのは、IdentityPoolIdの欄に、先ほどメモったIDをコピペすることです。

[aws-sdk.min.js](http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/browser-intro.html)と以下のindex.htmlをS3バケットにuploadします。




```html:index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="import.css">
    <script src="aws-sdk.min.js"></script>
    <title>投書画面</title>
    <script>
        var $id = function(id) { return document.getElementById(id); };
        AWS.config.region = "us-east-1";
        AWS.config.credentials = new AWS.CognitoIdentityCredentials({IdentityPoolId: "Cognitoで作成したIdentityPoolId"});
        AWS.config.credentials.get(function(err) {
            if (!err) {
                console.log("Cognito Identify Id: " + AWS.config.credentials.identityId);
            }
        });

        function uploadFile() {
            AWS.config.region = 'us-east-1';
            var s3BucketName = "xxx.example.com";
            var now = new Date();
            var obj = {"title":$id("title").value, "mail":$id("mail").value ,"contents":$id("contents").value, "date": now.toLocaleString()};
            var s3 = new AWS.S3({params: {Bucket: s3BucketName}});
            var blob = new Blob([JSON.stringify(obj, null, 2)], {type:'text/plain'});
            s3.putObject({Key: "uploads/" +now.getTime()+".txt", ContentType: "text/plain", Body: blob, ACL: "public-read"},
            function(err, data){
                if(data !== null){
                    alert("お問い合わせ完了致しました");
                }
                else{
                    alert("Upload Failed" + err.message);
                }
            });
        }
    </script>
</head>
<body>
    <div class="wrapper">
        <div id="postform">
            <form>
                <table>
                    <tr>
                        <th>件名</th>
                        <td>
                            <input id="title" type="text" name="title" class="titletext fontchange" maxlength="40" value="" />
                        </td>
                    </tr>
                    <tr>
                        <th>メールアドレス</th>
                        <td>
                            <input id="mail" type="email" name="mail" size="30" maxlength="50" />
                        </td>
                    </tr>
                    <tr>
                        <th>お問い合わせ内容</th>
                        <td>
                            <TEXTAREA id="contents" cols="40" rows="6" name="contents" class="fontchange"></TEXTAREA>
                        </td>
                    </tr>
                </table>
                <div id="button_area" class="clearfix">
                    <input onClick="uploadFile();" type="button" value="投稿" id="css_button" class="button_right" />
                </div>
            </form>
        </div>
        <! --loginform-->
    </div>
    <! --wrapper -->
</body>
</html>
```

# 3 Lambda
## 3-1 準備
以下2つのモジュールをDLします。

- [AWS-SDK](http://aws.amazon.com/jp/sdk-for-node-js/)

```
npm install aws-sdk
```

- [nodemailer](https://github.com/andris9/Nodemailer) （Gmail転送用）

```
npm install nodemailer
```

以上をインストールすると以下の通りnode_modulesディレクトリ以下に保存されます。

```
[~] ls node_modules
aws-sdk nodemailer
```

後ほど、作成するindex.jsと共にzipにしてLambdaにuploadします。

## 3-2 Lambda function

以下の感じで作成します

```js:index.js
console.log("Loading event")
var aws = require('aws-sdk');
var s3 = new aws.S3({apiVersion: '2006-03-01'});
var mailer = require('nodemailer');

var settings = {
    service: 'Gmail',
    auth: {
        user: '送信元Gmailアドレス',
        pass: 'Gmailパスワード',
        port: 25
    }
};

var smtp = mailer.createTransport(settings);

exports.handler = function(event, context) {
    console.log('Received event:', JSON.stringify(event, null, 2));
    var bucket = event.Records[0].s3.bucket.name;
    var key = event.Records[0].s3.object.key;
    s3.getObject({Bucket: bucket, Key: key},
        function(err, data) {
            if (err){
                context.done('error', 'error getting file' + err);
            } else {
                var message = JSON.parse(data.Body);
                var options = {
                    to : '送信先Gmailアドレス',
                    replyTo : message.mail,
                    subject: message.title,
                    text: ' ' + message.contents
                };
                smtp.sendMail(options, function(error, info){
                    if (error){
                        console.log('error:' + error);
                    } else {
                        console.log('Message sent:' + info.response);
                    }
                })
            }
        }
    );
};
```

以上をzipで固めてLambda functionにupします

```zip -r s3_form.zip index.js node_modules```

function名はS3mail_semdとします。（タイポ...）

3-3: 対象S3バケットのイベントフック作成

最後に対象S3バケットで、下図の通りEvent Notificationsを設定します。

![hook](https://s3-ap-northeast-1.amazonaws.com/qiita-3-shake.com/cognito_4.png)


以上でひと通り完了です。

# 確認

こんな感じで投稿すると
![mail](https://s3-ap-northeast-1.amazonaws.com/qiita-3-shake.com/mail.png)

無事Gmailで受け取れました

![reply](https://s3-ap-northeast-1.amazonaws.com/qiita-3-shake.com/reply-mail.png)

# 参考サイト
https://www.system-i-enter.com/blog/blog/2015/02/03/s3/
https://www.system-i-enter.com/blog/blog/2015/02/10/aws-lambda/
http://dev.classmethod.jp/cloud/cors-cross-origin-resource-sharing-cross-domain/
http://dev.classmethod.jp/etc/about-cors/