前回の続き
AWS LamdbaとAPI GatewayでWebの中間層が簡単に作れることがわかったのでちょっと遊んでみることにした
言語は今回もScalaで実装
#今回の仕様
ブラウザからプロキシっぽいAPI Gateway経由で任意のWebページのURLにアクセスすると内容をギャル文字に変換して表示する
galogo-filter(url) -> ギャル文字化されたHTMLページ
#ギャル文字とは
galmoji("こんにちは") == "〓ω(ニ干l£"
ちょっと昔に流行ったらしい
詳細はWikipedia参照
意外と深い
面倒なのでひらがなだけやる
#単純なプロキシの作成
proxy(url)
渡されたurlをGETしてそのまま返すとどうなるか
package filter
trait HtmlFilterBase {
import io.Source
// テスト時はオーバーライドできるように(ファイルからとかプロキシ使うときとか)
def fromURL(url: String, encode: String = "UTF-8") = Source.fromURL(url, encode)
def proxy(url: String) = {
fromURL(url).mkString
}
}
class HtmlFilter extends HtmlFilterBase
注 : ラムダ関数の最終引数のcom.amazonaws.services.lambda.runtime.Contextは省いてもよいようなので今回から省いた。要るときだけつける
上記コードをAWS Lambdaにデプロイし、APIGatewayにて公開
前回と同様の上記修正をしてprodに再デプロイ
ブラウザから前回記事のURLをターゲットにアクセスしてみる
https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/prod/proxy?url=http://qiita.com/bakenezumi/items/4026bbd4117be598a251
(hogehogeは内緒)
失敗
Content-Typeが application/json
になってる
今までさわってなかった「メソッドレスポンス」でhttpヘッダーをいじれるようなので修正する
再デプロイ後
text/htmlにはなったけどくずれた
ですよね..
#他リソースへの参照の相対パスを絶対パスに変換
absolute-filter(url)
画像、js、cssといった他リソースへの参照方法には絶対参照と相対参照の2つの方法があるが、
- 絶対参照:
<link href="http:/hogehoge.com/fugafuga.css" />
- 相対参照:
<link href="/fugafuga.css" />
相対参照の場合今回のフィルタを通すとホストがAWSのAPIGatewayになるため、あたりまえだが見つからない
なので全てのリンクを絶対参照に直してやる必要がある
正規表現を駆使して頑張ろうかとも思ったが、
「Jsoup」というJavaのライブラリがうまい具合にやってくれるということをGoogle先生が教えてくれたのでそれを使った
def absoluteFilter(url: String) = {
import scala.collection.JavaConverters._
import org.jsoup.Jsoup
val doc = Jsoup.connect(url).get
for (e <- doc.select("link").asScala) e.attr("href", e.absUrl("href"))
for (e <- doc.select("script").asScala) e.attr("src", e.absUrl("src"))
for (e <- doc.select("a").asScala) e.attr("href", e.absUrl("href"))
for (e <- doc.select("img").asScala) e.attr("src", e.absUrl("src"))
doc.toString
}
Scalaである必要がないという突っ込みは無しで
Lambda上げてAPI Gatewayとつないでさっきと同じ設定をしてデプロイ
https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/prod/absolute-filter?url=http://qiita.com/bakenezumi/items/4026bbd4117be598a251
ファビコンはでたけど内容は変わらず
まだ足りない
なんかChromeが怒ってる
曰く
Mixed Content: The page at 'https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/prod/proxy?url=http://qiita.com/bakenezumi/items/4026bbd4117be598a251' was loaded over HTTPS, but requested an insecure stylesheet 'http://cdn.qiita.com/assets/application-e985a098a72f1b0e009b75feaa8a3df6df3339d3a8b80b7b6d518b66da3c5b44.css'. This request has been blocked; the content must be served over HTTPS.
httpsのサイトからhttpのサイトへのスクリプト、及びスタイルシートのリクエストは危険だからできないとのこと
そりゃそうだ
#クロスサイト対応
absolute-filter(url)
の修正
めげずにがんばった
val proxyUrl = "./proxy?url="
def absoluteFilter(url: String) = {
import scala.collection.JavaConverters._
import org.jsoup.Jsoup
val doc = Jsoup.connect(url).get
// クロスサイト回避のためにproxyを介す
for (e <- doc.select("link").asScala) if (e.attr("href") != "") e.attr("href", proxyUrl + e.absUrl("href"))
// クロスサイト回避のためにproxyを介す
for (e <- doc.select("script").asScala) if (e.attr("src") != "") e.attr("src", proxyUrl + e.absUrl("src"))
for (e <- doc.select("a").asScala) e.attr("href", proxyUrl + e.absUrl("href"))
for (e <- doc.select("img").asScala) e.attr("src", e.absUrl("src"))
doc.toString
}
ようは、他リソースへの絶対リンクに当記事最初のproxyラムダをかますことで無理やり同一サイトとみなさせるようにした
<link href="./proxy?url=http:/hogehoge.com/fugafuga.css" />
proxyとこのabsolute-filterは同一のAPI nameに配置しなければならないことに注意
異なるAPI nameだとhost名が変わる
んで、再表示
結果、、エラーは減ったけど出力変わらず、、(画像省略)
一部のcssとjsの内容が、
{errorMessage=Input length = 1, errorType=java.nio.charset.MalformedInputException, stackTrace=["java.nio.charset.CoderResult.throwException(CoderResult.java:281)","sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:339)","sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)","java.io.InputStreamReader.read(InputStreamReader.java:184)","java.io.BufferedReader.read1(BufferedReader.java:210)","java.io.BufferedReader.read(BufferedReader.java:286)","java.io.Reader.read(Reader.java:140)","scala.io.BufferedSource.mkString(BufferedSource.scala:96)","filter.HtmlFilterBase$class.proxy(HtmlFilter.scala:9)","filter.HtmlFilter.proxy(HtmlFilter.scala:14)","sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)","sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)","sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)","java.lang.reflect.Method.invoke(Method.java:498)"]}
こんなラムダからのエラースタックトレースJSONが返ってきてることがわかる
#proxyのgzip対応
proxy(url)
の(修正)
エラーになったjsの実コンテンツを確認するとgzip化されてた
なので解凍する
Encodingの見方がよくわからなかったので失敗したら解凍するようにした
解凍の仕方はxuweiさんがメモってた
def fromURL(url: String, encode: String = "UTF-8") = Source.fromURL(url, encode)
def fromGzip(url: String, encode:String = "UTF-8" ) = {
import java.net.URL
import java.io.BufferedInputStream
import java.util.zip.GZIPInputStream
Source.fromInputStream(
new GZIPInputStream(
new BufferedInputStream(
new URL(url).openStream()
)
), encode)
}
def proxy(url: String) = {
try {
fromURL(url).mkString
} catch {
case _: java.nio.charset.MalformedInputException => fromGzip(url).mkString
case e => throw e
}
}
どきどきしながら再表示
ロゴがでた!
ちがう、そうじゃない
#CSS用のプロキシ準備
css-proxy(url)
の追加
absolute-filter(url)
の修正
CSSはどうもContent-Typeをtext/css
で返さないといけないようなのでそれようのAPIGateway「css-proxy」を追加し(Lamdaはproxyをそのまま利用)、それに合わせabsolute-filterのLambdaを修正
val proxyUrl = "./proxy?url="
val cssProxyUrl = "./css-proxy?url="
def absoluteFilter(url: String) = {
import scala.collection.JavaConverters._
import org.jsoup.Jsoup
val doc = Jsoup.connect(url).get
// クロスサイト回避のためにproxyを介す
for (e <- doc.select("link").asScala) if (e.attr("href") != "") e.attr("href", cssProxyUrl + e.absUrl("href"))
// クロスサイト回避のためにproxyを介す
for (e <- doc.select("script").asScala) if (e.attr("src") != "") e.attr("src", proxyUrl + e.absUrl("src"))
for (e <- doc.select("a").asScala) e.attr("href", e.absUrl("href"))
for (e <- doc.select("img").asScala) e.attr("src", e.absUrl("src"))
doc.toString
}
再表示
やっと出た!!
ふー、、疲れた
#ギャル文字変換
galgo-filter(url)
ここまでできたらギャル文字への変換は簡単だった
val galMap = Map(
('あ' -> "ぁ"), ('い' -> "レヽ"), ('う' -> "ぅ"), ('え' -> "ぇ"), ('お' -> "ぉ"),
...
)
val galgoUrl = "./galgo-filter?url="
def galgoFilter(url: String) = {
import scala.collection.JavaConverters._
import org.jsoup.Jsoup
val doc = Jsoup.connect(url).get
// クロスサイト回避のためにproxyを介す
for (e <- doc.select("link").asScala) if (e.attr("href") != "") e.attr("href", cssProxyUrl + e.absUrl("href"))
// クロスサイト回避のためにproxyを介す
for (e <- doc.select("script").asScala) if (e.attr("src") != "") e.attr("src", proxyUrl + e.absUrl("src"))
// リンク先もギャル語(似非再帰 モナドっぽい)
for (e <- doc.select("a").asScala) e.attr("href", galgoUrl + e.absUrl("href"))
// イメージは遅そうだからそのまま
for (e <- doc.select("img").asScala) e.attr("src", e.absUrl("src"))
doc.toString.flatMap(c => galMap.getOrElse(c, c.toString))
}
Lambda作ってAPIGateway設定&公開
https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/prod/galgo-filter?url=http://qiita.com/bakenezumi/items/4026bbd4117be598a251
にアクセス!
読めないけど成功したっぽい!
ギャル文字化されたページのリンクをたどるとそこから先もギャル文字
コード7行目のコメントに注目
#今回のコード
https://github.com/bakenezumi/aws-filter
#所感
Webセキュリティについて考えさせられた
変なサイトから不用意にメジャーサイトのリンク先に飛ぶのは危険
URLは絶対確認しようね!
あとそろそろ手作業がきついので自動化したい
#蛇足
前回のjson変換との組み合わせもいけた
https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/prod/htmlToJson?url=https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/prod/galgo-filter?url=http://qiita.com/bakenezumi/items/4026bbd4117be598a251