私はSwiftの経験はありますが、これは私にとってSwiftでサーバーを実行する最初の数少ないプロジェクトの1つです。
多くの個人開発者のウェブサイトを見てみたところ、すべてにコンタクトフォームがあることが分かりました。私のサイトにはまだコンタクトフォームがなかったので、Swift Vaporを実装したのです。
この記事では、私が Swift Vapor
を使って、アプリケーション(GithubサインインとCAPTCHA検証が必要なコンタクトフォーム)を学び始め開発した経緯について説明します。 この記事全体を通して、このアプリケーションを開発するために私がとった手順について説明します。また、Vaporの学習についても説明します。
この記事が、Swift Vapor
について学び始めたり、Swift Vapor
を使ったプログラミングに関するヒントを学んだりするのに役立つことを願っています。
筆者がVaporで学習し、本稿で解説するのは以下の内容です。
- MacOSでのVaporの環境設定
- リクエストURLに従ったhttpトラフィックのルーティング
- (重要)非同期レスポンスの実現(「EventLoopFuture」を使用)
- (重要)Ubuntuシステムでのサーバの設定と展開
- HTTPSのサポート
基本的な機能
このプロジェクトの設計目標は次のとおりです。
- スパムを防ぐため、ユーザーはGithub.comアカウントを使用してサインインする必要があります。サーバーは、Github APIを使用して、ユーザーのGithubプロファイルからメールアドレスを取得します。
- ユーザーはサインインしたあと、このフォームにより、確認済みのメールアドレスの1つを連絡先情報として選択できるようになります。
- ユーザーがフォームを送信すると、
Mailgun
を通じてメールが送信されます。ユーザーはCAPTCHAの検証も完了します。
このアプリケーションの基本的な流れは以下のとおりです。
- ユーザーが Web サイトにアクセスし、ホーム ページのボタンをクリックして Github.com のサインイン ページにリダイレクトします。
- Github がユーザーを認証し、サインイン トークンと一緒にユーザーを当社 Web サイトにリダイレクトします。
- 当社はサインイン トークンを OAuth 承認トークンと交換し、OAuth トークンを使用してメール アドレスのリストをフェッチします。正常にフェッチしたら、そのリストをセッション ストレージに保存し、ユーザーに連絡先フォームを表示します。
- 連絡先フォームには Captcha 認証があります。Captcha API を呼び出してユーザーがロボットでないことを確認します。
- 送信すると、サーバーがユーザー入力情報に関するメールを当社に送信します。
Github リポジトリ
完成したコードが Github にアップロードされました。Github リポジトリを表示するにはここをクリックしてください。
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 にインストールするには、これらのコマンドをターミナルで実行することができます。
brew tap vapor/tap
brew install vapor/tap/vapor
Vapor アプリケーションは次をコールして作成することができます :
vapor new Hello
このサンプルプロジェクトでは、Fluent
は必須ではありませんが、Leaf
は必須です。
-
Fluent
はデータ構造の作成に役立ちます。 - また、
Leaf
は、HTMLテンプレートファイルを設計し、Vapor
を使ってこれらのテンプレートにコンテンツをロードするのに役立つツールです。
ガイドに従ってプロジェクトを作成したら、作成したディレクトリに移って、vapor xcode
を入力してXcodeでプロジェクトを開きます。
基本ファイル構造
本稿では以下を利用します。
-
.leaf
ファイル(HTMLテンプレート)がすべて入ったViews
フォルダ - サーバの全ルーティング情報(訪問先のURL経路ごとのサーバのレスポンスを定義)が入った
routes.swift
ファイル - サーバをコンフィギュレーションする
configure.swift
ファイル
ステップ 2.認証を処理するため、Githubアプリケーションを作成
詳細に関してはこちらをクリックしてください
サーバーがユーザーの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
に設定します。
ステップ 3. トップページの定義
routes.swift
ファイルで、トップページを定義します。トップページは、index.leaf
ファイルのHTMLコードを利用します。
app.get { request in
return request.view.render("index")
}
また、ウェブサイトのトップページの外観を設定するために、index.leaf
をカスタマイズすることもできます。
詳細に関してはこちらをクリックしてください
<!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>
トップページにはボタンがあり、そのボタンをクリックすることで URL /start
に遷移します。
ステップ 4. Githubログインページへのユーザーのリダイレクト (/start
)
これで、ユーザーが閲覧するとGithubの認証ページにユーザーがリダイレクトされるよう/start
のルーティングをコンフィギュレーションできます。リダイレクトは自分のroutes.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
オブジェクトを生成することもできます。
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
ファイルに移動し、以下の行を追加してください
app.middleware.use(app.sessions.middleware)
更新済みのコード:
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
ファイルでは、以下のコードを利用して、ユーザーのセッションに値を保存することができます
request.session.data["github_email_addresses"] = userEmails.joined(separator: ",")
また、以下のコードを利用すると、セッションから値を読み取ることができます。
let emailAddressesString = request.session.data["github_email_addresses"]
ステップ 6. ユーザーのメールアドレスを非同期に取得し、ユーザーのメールアドレスをVaporのセッションストレージに保存します。
Githubがユーザーを認証すると、Githubは、GitHubアプリの設定ページで指定したURLにリダイレクトします。ルーティングがそのURLに移動したときに何をするかを定義する必要があります。
GithubがログインコールバックのURLを呼び出すと、URLにはcode
パラメータも用意されています。このcode
パラメータの値を取得し、それを使ってアカウント認証トークンと交換する必要があります。
以下は、HTTP URLクエリパラメータを取得するコードです。
let authCode = try? request.query.get(String.self, at: "code")
トークンの交換はネットワークアクティビティであり、終了までにかなり時間がかかる場合があります。従って、応答する前にEventLoopFuture
を使い、認証トークンがダウンロードされるまで待つようVaporサーバーに求めます。
非同期的にコードが実行されていることを確認するために私が行った手順は次の通りです。
- 返すデータ型
EventLoopFuture<Response>
を作成します -
let promise = request.eventLoop.makePromise(of: Response.self)
を使ってPromise
オブジェクトを定義します - 関数の最後で
return promise.futureResult
を実行します。このコードを呼び出すと、関数は結果の準備が整っている状態になるまで待機します。 - データを取得できなかった時は、エラーメッセージとともに
promise.fail
を呼び出すことができます。 - データの準備が整ったら、取得している結果データと一緒に
promise.succeed
を呼び出します。
ログインコードから認証トークンを取得し、その認証コードからユーザーの電子メールアドレスを取得する完成したコードは次の通りです。
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
と呼ばれています。そのため、次のようにしてフレームワークをインポートする必要があります。
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
ステップ 7. 連絡先フォームを設計する(ダイナミックHTML)
ここでは、Leaf
を利用して、フォームを設計します。
まず、form.leaf
ファイルを作成し、そこにHTMLコードを書き込みます。詳細については、リポジトリを参照してください。HTMLコードには#(FormOptions)
のプレースホルダーが含まれているので、すべてのユーザーのGithubEメールアドレスをピッカーに表示することができます。
次は、ルーティング・ルールの作成についてです。
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
ファイル用に書いたコードをご覧ください。
ヒント:ウェブページに表示されるコンテンツを動的に更新
これで、Vapor
コードでウェブページのコンテンツを更新できるようになっています。
.leaf
ファイルには、プレースホルダー(例:#(title)
)を記入してください
message.leaf
ファイルの例がこちらです。
<!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
ファイルの中に、プレースホルダー用の値を定義できます。
return request.view.render("message", ["title": "無許可", "content": "セッションには有効なメールアドレスのリストが含まれていません。"])
ステップ 8. Mailgun
を統合してメール送信
これで、Mailgun
のようなサービスを使って、メールが送信できるようになります。
Mailgun
のAPIにメッセージを送信するためのコードは、こちらをクリックして閲覧してください。
ルーティング用にはEventLoopFuture
も用います。メール送信APIからの応答を待つためです。本記事のこれまでのセクションで説明したのとよく似ています。そのコードは、こちらをクリックして閲覧してください。
ステップ 9. アプリケーションをHerokuまたはUbuntuに展開します
Ubuntu 20.04 を実行した状態で小規模な AWS EC2 インスタンスを開始することを選択しました
Swiftをインストールします
最新のSwiftバージョンのダウンロード用のリンクはこちらから取得できます : https://swift.org/download/#releases
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
それからコマンドを実行することでテストをインストールできます。
swift --version
Vapor toolbox をインストールします
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
ファイルを作成することもできます。
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
ファイルを作成する必要もあります。
cd Tests
touch LinuxMain.swift
Vaporサーバーの起動
./run.sh
を実行することで、Vaporサーバーを起動できます。
また、supervisor
を使ってサーバーが自動的に実行されるように設定することもできます。
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
supervisorctl reread
supervisorctl update
これで、ポート8080上でVaporサーバーが起動しているはずです。次に、そのポートをポート80またはポート443にプロキシ接続するために、プロキシを使う必要があります。
また、supervisor
を使うことで、Linuxシステムが起動する度にVaporサーバーを立ち上げさせることができます。
Nginxプロキシサーバーの設定
まず、Nginxをインストールする必要があります
sudo apt update
sudo apt install nginx
その後、Nginxのデフォルト設定ファイルを編集できます。
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
の設定ファイルを編集する方が楽かもしれません。
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について説明するすばらしい記事がたくさんあります。これらの記事は、より高度なトピックを理解するのに非常に役立ちますので必ず読んでください。
この記事で説明した内容の一部は、他の記事ですでに取り上げられている場合もあります。しかし、 Swift Vaporアプリケーションの開発とデプロイの方法を完全に理解できるために、これらのコンテンツを含めることにしました。