Swift Vaporを使って、Githubサインイン機能とCAPTCHA検証を備えたお問い合わせフォームを構築し、Ubuntuサーバ (+Nginx) にデプロイする

私はSwiftの経験はありますが、これは私にとってSwiftでサーバーを実行する最初の数少ないプロジェクトの1つです。

​多くの個人開発者のウェブサイトを見てみたところ、すべてにコンタクトフォームがあることが分かりました。​私のサイトにはまだコンタクトフォームがなかったので、Swift Vaporを実装したのです。

この記事では、私が `Swift Vapor` を使って、アプリケーション（GithubサインインとCAPTCHA検証が必要なコンタクトフォーム）を学び始め開発した経緯について説明します。 この記事全体を通して、このアプリケーションを開発するために私がとった手順について説明します。また、Vaporの学習についても説明します。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/f9e640a0-4e75-76d9-edba-d0fd1ece2c72.png">

この記事が、`Swift Vapor`について学び始めたり、`Swift Vapor`を使ったプログラミングに関するヒントを学んだりするのに役立つことを願っています。

筆者がVaporで学習し、本稿で解説するのは以下の内容です。

- MacOSでのVaporの環境設定
- リクエストURLに従ったhttpトラフィックのルーティング
- （重要）非同期レスポンスの実現（「EventLoopFuture」を使用）
- （重要）Ubuntuシステムでのサーバの設定と展開 
- HTTPSのサポート

# 基本的な機能

このプロジェクトの設計目標は次のとおりです。

- スパムを防ぐため、ユーザーはGithub.comアカウントを使用してサインインする必要があります。​サーバーは、Github APIを使用して、ユーザーのGithubプロファイルからメールアドレスを取得します。
- ユーザーはサインインしたあと、このフォームにより、確認済みのメールアドレスの1つを連絡先情報として選択できるようになります。
- ユーザーがフォームを送信すると、`Mailgun`を通じてメールが送信されます。​ユーザーはCAPTCHAの検証も完了します。

このアプリケーションの基本的な流れは以下のとおりです。

- ユーザーが Web サイトにアクセスし、ホーム ページのボタンをクリックして Github.com のサインイン ページにリダイレクトします。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/8bf469c7-fb4f-df4a-9fad-8735352b7dec.png">

- Github がユーザーを認証し、サインイン トークンと一緒にユーザーを当社 Web サイトにリダイレクトします。
- 当社はサインイン トークンを OAuth 承認トークンと交換し、OAuth トークンを使用してメール アドレスのリストをフェッチします。正常にフェッチしたら、そのリストをセッション ストレージに保存し、ユーザーに連絡先フォームを表示します。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/da01efcb-068c-3e17-1767-765faef88397.png">


- 連絡先フォームには Captcha 認証があります。Captcha API を呼び出してユーザーがロボットでないことを確認します。
- 送信すると、サーバーがユーザー入力情報に関するメールを当社に送信します。

# Github リポジトリ

完成したコードが Github にアップロードされました。[Github リポジトリを表示するにはここをクリックしてください。](https://github.com/mszmagic/SwiftVapor-Contact-Form)

`APICredentials.swift`を更新して、GithubアプリケーションクライアントID/シークレット（ユーザーの認証用）、`Mailgun`API（メールの送信用）、`hCaptcha`アプリID/シークレット（ユーザーにCAPTCHAの検証を依頼するため）を提供する必要があります。

# `Vapor` とは

`Vapor` は Swift 言語を使ってウェブサーバーを構築できるようにするフレームワークです。Macにサーバーを開発し、Ubuntuシステムにサーバーをデプロイすることができます。

`Vapor` は動的に生成されたHTMLコンテンツをSwiftコードでレンダリングできるようにするとともに、ご自分のAPIを（入力に基づいてさまざまな応答を提供することにより）ホストできるようにします。

概して言うと、すでにモバイル開発用のSwiftをお使いであれば、サーバーおよびアプリケーション開発に対するSwift + Vaporの実用性をご理解いただける可能性があります。

# ステップ1. `Vapor` をインストールしてスタータープロジェクトを設定

`Vapor` を Mac にインストールするには、これらのコマンドをターミナルで実行することができます。

```shell
brew tap vapor/tap
brew install vapor/tap/vapor
```

Vapor アプリケーションは次をコールして作成することができます :

```shell
vapor new Hello
```

このサンプルプロジェクトでは、`Fluent`は必須ではありませんが、`Leaf`は必須です。

- `Fluent`はデータ構造の作成に役立ちます。
- また、`Leaf`は、HTMLテンプレートファイルを設計し、`Vapor`を使ってこれらのテンプレートにコンテンツをロードするのに役立つツールです。

ガイドに従ってプロジェクトを作成したら、作成したディレクトリに移って、`vapor xcode`を入力してXcodeでプロジェクトを開きます。

## 基本ファイル構造

本稿では以下を利用します。

- `.leaf`ファイル（HTMLテンプレート）がすべて入った`Views`フォルダ
- サーバの全ルーティング情報（訪問先のURL経路ごとのサーバのレスポンスを定義）が入った`routes.swift`ファイル
- サーバをコンフィギュレーションする`configure.swift`ファイル

# ステップ 2.認証を処理するため、Githubアプリケーションを作成

<details><summary>詳細に関してはこちらをクリックしてください</summary><div>

サーバーがユーザーのGithubプロフィールからメールアドレスのリストをフェッチするコンタクトフォームを作成する必要があります。それを行うには、Github.comの認証情報 (Oauth) でユーザーを認証する必要があります。

認証を処理するには、Githubアプリケーションを作成し、そのGithubアプリケーションのクライアントキーとシークレットキーを取得する必要があります。

`Settings` > `Developer Settings`、そして`GitHub Apps`を選択すると、新しいアプリケーションを作成できます。

`User authorization callback URL`という名の構成フィールドには`https://[Your domain]/github_callback`と入力します。Githubはユーザーの認証を完了すると、ログインコードとともに`https://[Your domain]/github_callback`にリダイレクトします。

記事の次のパートで、`github_callback` をデザインします。

また、GitHubアプリでユーザーのメールアドレスを取得できるようにしておきましょう。アプリのページに移動し、`Permissions & events`をクリックし、`User permissions`セクションに移動し、`Email addresses`を`Read-Only`に設定します。

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/5e13ee1f-4d38-64c2-2e7e-5da2d33e9f55.png)

</div></details>

# ステップ 3. トップページの定義

`routes.swift`ファイルで、トップページを定義します。トップページは、`index.leaf`ファイルのHTMLコードを利用します。

```swift
app.get { request in
    return request.view.render("index")
}
```

また、ウェブサイトのトップページの外観を設定するために、`index.leaf`をカスタマイズすることもできます。

<details><summary>詳細に関してはこちらをクリックしてください</summary><div>

```html
<!doctype html>
<html lang="ja">

<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
        integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
    <title>MszMagic連絡フォーム</title>
</head>

<style>
    .content {
        max-width: 500px;
        margin: auto;
    }
</style>

<body>

    <div class="content">
        <br>
        <h1>​お問い合わせフォーム</h1>
        <br>
        スパム予防のため、問い合わせフォームへとお進みいただくにはGithub.comアカウントでログインしてください。お客様の保存済みEメールアドレスへアクセスする許可のみ必要になります。当社がその他の情報を利用することはありません。
        <br><br>
        <button type="button" onclick="location.href='/start';" class="btn btn-outline-success">Githubでログイン</button>
    </div>

</body>

</html>
```

</div></details>

トップページにはボタンがあり、そのボタンをクリックすることで URL `/start` に遷移します。

# ステップ 4. Githubログインページへのユーザーのリダイレクト (`/start`)

これで、ユーザーが閲覧するとGithubの認証ページにユーザーがリダイレクトされるよう`/start`のルーティングをコンフィギュレーションできます。リダイレクトは自分の`routes.swift`ファイルで定義できます。

```swift
app.get("start") { request in
    return request.redirect(to: "https://github.com/login/oauth/authorize?client_id=\(APICredentials.github_client_id)")
}
```

## ヒント：`Response` オブジェクトの構成

`Response` オブジェクトは複数の方法で組み立てることができます。

- ユーザーが新しいウェブページにリダイレクトされるように `.redirect` を使います。

- プレーンテキスト、ヘッダ、HTTPレスポンスステータス、HTTPのバージョンで`Response`オブジェクトを生成することもできます。

```swift
static func generateResponse(text: String, status: HTTPStatus, version: HTTPVersion) -> Response {
    return .init(status: status,
                 version: version,
                 headers: ["Content-Type": "text/html; charset=utf-8"],
                 body: .init(string: text))
}
```

ワンポイント・アドバイス：Webサイトのホームページの作成手順3と同様に、HTMLテンプレートをレンダリングすることもできます。

# ステップ 5. セッションストレージを有効化する

ユーザー情報の一部を保存する必要があることから、Vaporアプリを設定し、セッションストレージを有効化する必要があります。 `configure.swift`ファイルに移動し、以下の行を追加してください

```swift
app.middleware.use(app.sessions.middleware)
```

更新済みのコード：

```swift
public func configure(_ app: Application) throws {
    app.views.use(.leaf)
    app.middleware.use(app.sessions.middleware)
    app.sessions.configuration.cookieName = "session_id"
    try routes(app)
}
```

これで、セッションデータがサーバーのメモリに保存されることになります。

`routes.swift` ファイルでは、以下のコードを利用して、ユーザーのセッションに値を保存することができます

```swift
request.session.data["github_email_addresses"] = userEmails.joined(separator: ",")
```

また、以下のコードを利用すると、セッションから値を読み取ることができます。

```swift
let emailAddressesString = request.session.data["github_email_addresses"]
```

# ステップ 6. ユーザーのメールアドレスを非同期に取得し、ユーザーのメールアドレスをVaporのセッションストレージに保存します。

Githubがユーザーを認証すると、Githubは、GitHubアプリの設定ページで指定したURLにリダイレクトします。ルーティングがそのURLに移動したときに何をするかを定義する必要があります。

GithubがログインコールバックのURLを呼び出すと、URLには`code`パラメータも用意されています。この`code`パラメータの値を取得し、それを使ってアカウント認証トークンと交換する必要があります。

以下は、HTTP URLクエリパラメータを取得するコードです。

```swift
let authCode = try? request.query.get(String.self, at: "code")
```

トークンの交換はネットワークアクティビティであり、終了までにかなり時間がかかる場合があります。従って、応答する前に`EventLoopFuture`を使い、認証トークンがダウンロードされるまで待つようVaporサーバーに求めます。

**非同期的にコードが実行されていることを確認するために私が行った手順は次の通りです。**

1. 返すデータ型`EventLoopFuture<Response>`を作成します
2. `let promise = request.eventLoop.makePromise(of: Response.self)` を使って `Promise` オブジェクトを定義します
3. 関数の最後で `return promise.futureResult` を実行します。このコードを呼び出すと、関数は結果の準備が整っている状態になるまで待機します。
4. データを取得できなかった時は、エラーメッセージとともに `promise.fail` を呼び出すことができます。
5. データの準備が整ったら、取得している結果データと一緒に `promise.succeed` を呼び出します。

ログインコードから認証トークンを取得し、その認証コードからユーザーの電子メールアドレスを取得する完成したコードは次の通りです。

```swift
app.get("github_callback") { request -> EventLoopFuture<Response> in // 1
    let promise = request.eventLoop.makePromise(of: Response.self) // 2
    if let authCode = try? request.query.get(String.self, at: "code") {
        RequestController.shared.fetchToken(loginCode: authCode) { result in
            if let fetchedToken = result.code {
                RequestController.shared.fetchEmailAddress(authToken: fetchedToken) { userEmails in
                    if userEmails.count > 0 {
                        request.session.data["github_email_addresses"] = userEmails.joined(separator: ",")
                        promise.succeed(request.redirect(to: "/form")) // 5
                    } else {
                        promise.succeed(.failedObtainEmailFromGithub(version: request.version)) // 5
                    }
                }
            } else {
                let errorMessage = result.error ?? "不明なエラー"
                promise.succeed(.generateResponse(text: errorMessage, status: .unauthorized, version: request.version)) // 5
            }
        }
    } else {
        promise.succeed(.failedObtainGithubAuthCode(version: request.version)) // 5
    }
    return promise.futureResult // 3
}
```

`RequestController.shared.fetchToken`と`fetchEmailAddress`のコードはここには含まれていません。これらの2つの機能は`NSURLSession`を使用してGitHub APIから情報をフェッチし、完了コールバックを通して結果を返します。この記事のGithubレポジトリでこれら2つの機能のコードを見つけることができます。

コードを見ると分かるように、メールアドレスがフェッチされると、コードはメールアドレスが少なくとも1つ存在するかどうかを確認します。確認をパスすると、ユーザーのメールアドレスをセッションストレージに保存してから、ユーザーを`/form` URLにリダイレクトします。

## Linuxでの`NSURLSession`の使用について

`NSURLSession`は`Foundation`フレームワークにあります。しかし、Linuxでは、このフレームワークは`FoundationNetworking`と呼ばれています。​そのため、次のようにしてフレームワークをインポートする必要があります。

```swift
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
```

# ステップ 7. 連絡先フォームを設計する（ダイナミックHTML)

ここでは、`Leaf`を利用して、フォームを設計します。

まず、`form.leaf`ファイルを作成し、そこにHTMLコードを書き込みます。詳細については、リポジトリを参照してください。HTMLコードには`#(FormOptions)`のプレースホルダーが含まれているので、すべてのユーザーのGithubEメールアドレスをピッカーに表示することができます。

次は、ルーティング・ルールの作成についてです。

```swift
app.get("form") { request -> EventLoopFuture<View> in
    if let emailAddressesString = request.session.data["github_email_addresses"] {
        var formOptionString = ""
        let emailAddresses = emailAddressesString.split(separator: ",")
        for emailAddress in emailAddresses {
            formOptionString.append("<option>\(emailAddress)</option>")
        }
        return request.view.render("form", ["FormOptions": formOptionString])
    } else {
        return request.view.render("message", ["title": "無許可", "content": "セッションには有効なメールアドレスのリストが含まれていません。"])
    }
}
```

`form.leaf` の設計においては、フォームデータは`/submit`のようなエンドポイントに送信しなければなりません。[ここをクリックして、私が `form.leaf` ファイル用に書いたコードをご覧ください。](https://github.com/mszmagic/SwiftVapor-Contact-Form/blob/master/Resources/Views/form.leaf)

## ヒント：ウェブページに表示されるコンテンツを動的に更新

これで、`Vapor`コードでウェブページのコンテンツを更新できるようになっています。

`.leaf`ファイルには、プレースホルダー（例：`#(title)`）を記入してください

`message.leaf`ファイルの例がこちらです。

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">

  <title>#(title)</title>
</head>

<body>
  <h1>#(title)</h1>
  #(content)
  <a href="/">TOP</a>
</body>
</html>
```

そして、`routes.swift`ファイルの中に、プレースホルダー用の値を定義できます。

```swift
return request.view.render("message", ["title": "無許可", "content": "セッションには有効なメールアドレスのリストが含まれていません。"])
```

# ステップ 8. `Mailgun`を統合してメール送信

これで、`Mailgun`のようなサービスを使って、メールが送信できるようになります。

[`Mailgun`のAPIにメッセージを送信するためのコードは、こちらをクリックして閲覧してください。](https://github.com/mszmagic/SwiftVapor-Contact-Form/blob/master/Sources/App/Controllers/RequestController.swift#L138)

ルーティング用には`EventLoopFuture`も用います。メール送信APIからの応答を待つためです。本記事のこれまでのセクションで説明したのとよく似ています。[そのコードは、こちらをクリックして閲覧してください。](https://github.com/mszmagic/SwiftVapor-Contact-Form/blob/master/Sources/App/routes.swift#L81)

# ステップ 9. アプリケーションをHerokuまたはUbuntuに展開します

Ubuntu 20.04 を実行した状態で小規模な AWS EC2 インスタンスを開始することを選択しました 

## Swiftをインストールします

最新のSwiftバージョンのダウンロード用のリンクはこちらから取得できます : https://swift.org/download/#releases

```bash
sudo apt-get install clang

wget https://swift.org/builds/swift-5.3.1-release/ubuntu2004/swift-5.3.1-RELEASE/swift-5.3.1-RELEASE-ubuntu20.04.tar.gz

tar xzf swift-5.3.1-RELEASE-ubuntu20.04.tar.gz

rm swift-5.3.1-RELEASE-ubuntu20.04.tar.gz

sudo mv swift-5.3.1-RELEASE-ubuntu20.04 /usr/share/swift

echo "export PATH=/usr/share/swift/usr/bin:$PATH" >> ~/.bashrc

source  ~/.bashrc
```

それからコマンドを実行することでテストをインストールできます。

```shell
swift --version
```

## Vapor toolbox をインストールします

```shell

apt-get install -y zlib1g-dev

git clone https://github.com/vapor/toolbox vapor-toolbox

cd vapor-toolbox

cd Tests

touch LinuxMain.swift

cd ../

swift build -c release --disable-sandbox

mv .build/release/vapor /usr/local/bin
```

## サーバー ファイルを複製します。

Mac 上のファイルをリモート Git サーバーにプッシュし、これらのコードを Linux サーバーのディレクトリにプルできます。この例では、コードを `/home/ubuntu/feedbackform` に複製できます。

以下のコマンドを実行して、レポジトリを複製し、サーバーを構築します。

```
cd /home/ubuntu
git clone ...
cd feedbackform
vapor build
```

また、`supervisor`アプリケーションがサーバーを素早く構築して起動できるように、`run.sh`ファイルを作成することもできます。

```shell
nano run.sh

#!/bin/bash
swift build --configuration release
.build/release/Run serve --env production --port 8080 --hostname 0.0.0.0
```

加えて、`Tests`ディレクトリーに空の`LinuxMain.swift`ファイルを作成する必要もあります。

```shell
cd Tests
touch LinuxMain.swift
```

## Vaporサーバーの起動

`./run.sh`を実行することで、Vaporサーバーを起動できます。

また、`supervisor`を使ってサーバーが自動的に実行されるように設定することもできます。

```shell
apt install supervisor

nano /etc/supervisor/conf.d/vapor_server.conf
```

`vapor_server.conf`ファイルに以下の内容を入力してください

```
[program:app_collection]
command=/home/ubuntu/feedbackform/run.sh
directory=/home/ubuntu/feedbackform
autorestart=true
user=ubuntu
```

```shell
supervisorctl reread
supervisorctl update
```

これで、ポート8080上でVaporサーバーが起動しているはずです。次に、そのポートをポート80またはポート443にプロキシ接続するために、プロキシを使う必要があります。

また、`supervisor`を使うことで、Linuxシステムが起動する度にVaporサーバーを立ち上げさせることができます。

## Nginxプロキシサーバーの設定

まず、Nginxをインストールする必要があります

```shell
sudo apt update
sudo apt install nginx
```

その後、Nginxのデフォルト設定ファイルを編集できます。

```shell
cd /etc/nginx/sites-enabled/
nano default
```

`default`ファイルの内容がこちらです。

```
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    location / {
        proxy_pass http://localhost:8080;
    }
}
```

これで、`Nginx`サーバーを再読み込みする準備が整いました。

```
sudo service nginx restart
```

# ステップ 9. HTTPSの設定

これで、`LetsEncrypt`を使ってサーバーのHTTPSを設定する準備が整いました。Nginxサーバーをプロキシとして設定したので、`DNS`を用いてドメインの所有権を検証して、手動で`Nginx`の設定ファイルを編集する方が楽かもしれません。

```shell
sudo apt-get update
sudo apt-get install python3-certbot-nginx
sudo certbot -d [Your domain] --manual --preferred-challenges dns certonly
```

その後、`/etc/nginx/sites-enabled/default` ファイルを編集することができます :

```
ssl_certificate /etc/letsencrypt/live/[Domain name]/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/[Domain name]/privkey.pem;
```

更新されたNginx構成はこのように表示されます

```
server {
    listen 443 ssl;

    ssl_certificate /etc/letsencrypt/live/[Domain name]/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/[Domain name]/privkey.pem;
    ssl_protocols TLSv1.2;

    location / {
        proxy_pass http://localhost:8080;
    }
}
```

これで、`Nginx`サーバーを再読み込みする準備が整いました。

```
sudo service nginx restart
```

---

この記事の中で、お気づきになった改善点がございましたら、訂正していただけませんでしょうか。この記事は、私のSwift Vaporに関する学習の進捗状況を記録することだけを目的としたものです。

[Qiitaには、Swift Vaporについて説明するすばらしい記事がたくさんあります。​これらの記事は、より高度なトピックを理解するのに非常に役立ちますので必ず読んでください。](https://qiita.com/search?sort=&q=Vapor+tag%3ASwift)

​この記事で説明した内容の一部は、他の記事ですでに取り上げられている場合もあります。​しかし、 Swift Vaporアプリケーションの開発とデプロイの方法を完全に理解できるために、これらのコンテンツを含めることにしました。

---

:relaxed: [Twitter @MszPro](https://twitter.com/MszPro)

:sunny: [私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。](http://mszpro.com/ioskiji/)
<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">