はじめに
今日(2015/12/16)から冬休みという方も多いのではないのでしょうか?
冬休みの間 serverless を触りたい!ひまだひまだ!!!!
という方は シズオカアプリコンテスト に参加してみては
いかがでしょうか?
参加申し込みをするだけで $50分のクーポン が貰えますよ!
今回作ったアプリケーションもこのシズオカアプリコンテストの出品作品です。
なお、リポジトリは以下で公開しています。(PRお待ちしております)
https://github.com/kengos/is_it_phishing
フィッシングサイトをブロックしたい
じぶん銀行 では 12/21 にこんな注意喚起をしています。
不審なメールのリンク先には絶対にアクセスしないようご注意ください。
万が一、不審なメールのリンク先にアクセスしてしまった場合、偽サイトでパスワードや確認番号表の数字の入力を求められても絶対に入力しないでください。
http://www.jibunbank.co.jp/announcement/2015/1221_01.html
そんなこと言われても、自分自身の注意だけだと限界がありますよね。
どうせなら機械的にブロックできる仕組みを用意したいなと考えました。
どうやってブロックするのか?
(日本を対象とした)フィッシングサイトをブロックするには以下の方法が良さそうだなと考えました。
- whois の登録情報中に CN, TWなどが含まれている
- whois の登録情報の メールアドレスが フリーのメールアドレスだ
- google に インデックスされていない(違法サイトなので当然)
- 中国の無料のアクセス解析ツールが仕込まれている(51.la)
- 文章中の日本語が怪しい
今回は「文章中の日本語が怪しい」以外を組み込んだアプリを作ってみることにしました。
(めんどくさい + サンプル探すのが大変)
また、DNS汚染などでの被害もあるようなので、
別環境から正引きしたIPアドレスとクライアントがアクセスしているIPアドレスが同一かどうかの
チェックも含めてみることにしました。
こんなイメージとなります
技術的なお話
ChromeExtensionの仕組み
content/main.js
クライアントがHTMLを読み込むと以下が実行され, background/main.js に対してメッセージが送信されます。
chrome.runtime.sendMessage({
location: window.location,
// scripts には HTML中のscriptタグ, iframeタグの src属性の中身が入っている
scripts: scripts.concat(iframes)
});
background/main.js
background/main.js は content/main.jsが送ってくるメッセージを以下のように待ち受けています
メッセージを受信すると
- ScoreBoard というクラスに対して送信されてきた
request
オブジェクトを渡します - ScoreBoard の結果を計算します(非同期)
- ScoreBoard が完了した場合に
chrome.tabs.sendMessage
で送信してきたタブ(sender.tab.id
)に対して結果を返します
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
var board = new ScoreBoard(request);
board.calculate();
var timer = setInterval(function() {
if(board.complete()) {
clearInterval(timer);
chrome.tabs.sendMessage(sender.tab.id, {request: request, sender: sender, score: board.getScore(), messages: board.getMessages()});
}
}, 100);
});
クライアント側のHTML書き換え
content/main.js は以下でbackground/main.js が送信してくるメッセージを待ち受けています
chrome.runtime.onMessage.addListener(function(response, sender, sendResponse) {
console.log(response);
if (response.score < 80) {
return;
}
replaceDocument();
// Document書き換え
var ulTag = document.querySelector('#phishing-injection-messages');
response.messages.forEach(function(message) {
var liTag = document.createElement('li');
liTag.innerHTML = message;
ulTag.appendChild(liTag);
});
});
response
は以下の形式のデータが入っています。(これは実際のフィッシングサイトで得られた結果です)
{
score: 80,
messages: [
"js.users.51.la の javascript/iframe が呼び出されています。",
"Googleの検索エンジンに登録されていません。"
]
}
score
が 80以上の場合はイカの画像のように messagesの中身とともに画面に表示します。
(既存のHTMLは全て非表示にしています)
script, iframe の src=xxxx
に対して行う処理
この処理では 該当ページ中に含まれている script src=xxxx
, iframe src=xxxx
のすべての値を
クライアント側から送信してもらい、それらをブラックリスト方式で判定しています。
実装箇所: background/hostname_validator.js
受け取った src/iframe 中の src の配列が hostnames
です
RegExpを使用して、test
をしているだけです。
ブラックリストに含まれていた場合は該当の hostname を this.errorHosts
に格納しています
this.buildResult()
で スコア と クライアント側に表示するメッセージを生成しています
validate: function(hostnames) {
var reg = new RegExp(HostnameValidator.ERROR_HOSTS.join('|'));
this.errorHosts = [].filter.call((hostnames || []), function(hostname) {
return reg.test(hostname);
});
this.buildResult();
return this.errorHosts.length == 0;
}
whois 処理
AWS API Gatewayとの通信が発生する部分です
ChromeExtension側の処理
実装箇所: background/whois_validator.js
Ajaxを利用して APIGatawayと通信をしています。
APIGataway側からのレスポンスを元に クライアント側に送信するスコア, メッセージを生成します
// hostname = window.location.hostname
lookup: function(hostname) {
var self = this;
$.ajax({
type: 'GET',
url: WhoisValidator.API_END_POINT,
data: {'hostname': hostname},
dataType: 'json'
}).done(function(json) {
self.setCache(hostname, json);
self.response = json;
}).fail(function(json) {
self.response = null;
}).always(function() {
self.buildResult();
});
}
AWS APIGateway, AWS lambda 側の処理
APIGateway/lambda: dns/whois
ビジネスロジック: dns/lib/whois.js
外部ライブラリとして node-whois
, change-case
を利用しています
event.hostname
に /dns/whois?hostname=xxxx
の xxxx
の部分が設定されてきますので、
その値を whois.lookup
で検索しています
この結果は通常のコンソールで行う whois
と同様の結果が返ってきますので、適当に整形してjson形式でレスポンスを返します
レスポンスを返す際には return cb(null, {})
とすれば良いらしく、
{}
の箇所に返したい任意のjson文字列をいれるだけでした
module.exports.respond = function(event, cb) {
if(event.hostname === '') {
return cb(new Error("hostname is required"), null);
}
whois.lookup(event.hostname, function(err, whoisText) {
if (err) {
return cb(err, null);
}
var response = {};
if(event.hostname.match(/\.jp$/)) {
response['type'] = 0;
response['text'] = whoisText;
} else {
var lines = whoisText.split(/\r\n|\r|\n/).filter(function(line, index) {
return String(line).trim().match(': ');
});
lines.forEach(function(line) {
var parts = line.split(": ");
var key = changeCase.camelCase(parts[0]);
response[key] = parts[1];
});
}
return cb(null, response);
});
};
DNS正引き 処理
ChromeExtension側の処理
実装箇所: background/ip_validator.js
Ajaxを利用して APIGatawayと通信をしています。
APIGataway側からのレスポンスを元に クライアント側に送信するスコア, メッセージを生成します
// hostname = window.location.hostname
lookup: function(hostname) {
var self = this;
$.ajax({
type: 'GET',
url: IpValidator.API_END_POINT,
data: {'hostname': hostname},
dataType: 'json'
}).done(function(json) {
self.setCache(hostname, json);
self.response = json;
}).fail(function(json) {
self.response = null;
}).always(function() {
self.buildResult(hostname);
});
}
APIGateway, lambda側の処理
APIGatewayの処理実装箇所: whois/ip
lambda(ビジネスロジック): dns/lib/ip.js
nodeの標準機能で hostname => ipaddress変換ができるので、以下のようになります
var dns = require('dns');
module.exports.respond = function(event, cb) {
if(event.hostname === '') {
return cb(new Error("hostname is required"), null);
}
dns.resolve(event.hostname, function (err, addresses) {
if (err) {
return cb(err, null);
}
return cb(null, addresses);
});
};
呼び出すと以下のようなレスポンスが返ってきます
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 18
Connection: keep-alive
Date: Sun, 27 Dec 2015 03:52:56 GMT
x-amzn-RequestId: 506bf61b-ac4d-11e5-95e3-63eadd8da545
X-Cache: Miss from cloudfront
Via: 1.1 61d0a31bb18a85e2e5a9c2cc54beb3e5.cloudfront.net (CloudFront)
X-Amz-Cf-Id: BKLP2gP0ls54Y8Rcw7xZ3mMu216AfErzrxc7_v-0XUYC9PZJkvqqiw==
["216.58.220.195"]
cb(new Error('hostname is requires'), null)
が実行されると次のようなメッセージになります
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 213
Connection: keep-alive
Date: Sun, 27 Dec 2015 03:54:16 GMT
x-amzn-RequestId: 7fc898b0-ac4d-11e5-b91c-ab7dacc15141
X-Cache: Miss from cloudfront
Via: 1.1 65fe4947494affb6443dd314059f54c8.cloudfront.net (CloudFront)
X-Amz-Cf-Id: a-4so43Jp1j9iIuDoSUma1mScWIvgGDWKEGgwh4v6Fn03NtFoDLk-w==
{"errorMessage":"hostname is required","errorType":"Error","stackTrace":["Object.module.exports.respond (/var/task/modules/dns/lib/ip.js:5:15)","module.exports.handler (/var/task/modules/dns/ip/handler.js:19:7)"]}
Googleへの検索リクエスト
lambda側で処理を書こうかなとも思ったのですが、リクエストIPを分散したほうがよさ気だったので
クライアント側からリクエスト送信をしています
background/google_validator.js
(Deprecatedな)GoogleSearchApiを利用しています
検索クエリを site:hostname
とすることで インデックスに登録されているかを調べています
// hostname = window.location.hostname
lookup: function(hostname) {
var self = this;
$.ajax({
type: 'GET',
url: 'http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=site:' + hostname,
dataType : "json"
}).done(function(response){
self.setCache(hostname, response);
self.response = response;
}).fail(function(response){
self.response = null;
}).always(function() {
self.buildResult();
});
}
AWS serverless(APIGateway/Lambda) について
Servelessのデプロイ
serveless function deploy
: Lambdaのデプロイを行います
serveless endpoint deploy
: APIGatewayのデプロイを行います
deployするfunction, endopointを選択し Deploy
を選択するとDeployできます
なお、-a
オプションですべてのデプロイができ -s [stage名]
で指定したstageのfunction/endpointのデプロイが出来ます
(今回はdevelopment stageのみしか用意していないので聞かれていません)
AWSの料金は?
開発時点ではこんな感じのリクエスト数でした
serverlessを使った際にAWS上で確認するコンソールは?
- Cloud Formation: serverlessの構成管理
- AWS Lambda: APIGatewayから呼び出されるビジネスロジック郡
- API Gateway: 外部とのインターフェース
- S3: 実態ファイル
- CloudWatch: AWS Lambdaのログなどはここから
- Billing
- IAM
S3
serverless.[region].[app名]/Serverless/[app名]/[stage] 以下に配置されます
lambdas以下にはLambdaのFunctionがRevisionごとに配置されます
AWS Lambda
Functionごとに以下のように配置されます
各Functionの中身はRevision管理され以下のようになります
AWS APIGateway
CloudWatch
以下は cb(new Error('hostname is required'), null)
の際のログです
serverless でハマったこと
リクエストパラメータのもらい方
各々のAPIは次のように受け取ることにしました。
whois: /dns/whois?hostname=xxxx
DNS正引き: /dns/ip?hostname=xxxx
hostname=xxxx
の設定をどのようにすればいいのかにハマりました。
結論的には以下の設定を入れてdeployするだけでした。
"requestParameters": {
"integration.request.querystring.integrationQueryParam": "method.request.querystring.hostname"
},
"requestTemplates": {
"application/json": "{\"hostname\":\"$input.params('hostname')\"}"
},
{
"whois": {
"hostname": "$input.params('hostname')"
}
}
最後に(Help me!)
◯ whois について .comなどはうまく取れるのですが取れないものも多く困っています
予算的な都合で公開API等は使いたくないのでなにかいい方法を知っている方教えて下さい。
◯文書内の怪しい日本語チェックをしてみたいです。詳しい人(ry
かなり雑な作りですが、以下からダウンロードできます($5払いました...)