AWS LambdaでWebページをギャル文字にしてみた

[前回](http://qiita.com/bakenezumi/items/4026bbd4117be598a251)の続き

AWS LamdbaとAPI GatewayでWebの中間層が簡単に作れることがわかったのでちょっと遊んでみることにした
言語は今回もScalaで実装

#今回の仕様
ブラウザからプロキシっぽいAPI Gateway経由で任意のWebページのURLにアクセスすると内容をギャル文字に変換して表示する

`galogo-filter(url) -> ギャル文字化されたHTMLページ`

#ギャル文字とは
galmoji("こんにちは") == "〓ω(ﾆ干l￡"

ちょっと昔に流行ったらしい
詳細は[Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%AE%E3%83%A3%E3%83%AB%E6%96%87%E5%AD%97)参照
意外と深い
面倒なのでひらがなだけやる

#単純なプロキシの作成
`proxy(url)`

渡されたurlをGETしてそのまま返すとどうなるか

```scala:src/main/scala/filter/HtmlFilter.scala
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にて公開

![image](https://qiita-image-store.s3.amazonaws.com/0/35910/12c51c6d-ba44-691a-f4f0-dd2a2eff7abc.png)

前回と同様の上記修正をしてprodに再デプロイ

ブラウザから前回記事のURLをターゲットにアクセスしてみる

`https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/prod/proxy?url=http://qiita.com/bakenezumi/items/4026bbd4117be598a251`
(hogehogeは内緒)

![image](https://qiita-image-store.s3.amazonaws.com/0/35910/41ecd835-c859-4f6f-680b-009adb04dac6.png)

失敗

![image](https://qiita-image-store.s3.amazonaws.com/0/35910/d4720c46-9624-3105-7eb2-453bf4106bf3.png)

Content-Typeが `application/json`になってる
今までさわってなかった「メソッドレスポンス」でhttpヘッダーをいじれるようなので修正する

![image](https://qiita-image-store.s3.amazonaws.com/0/35910/a8f4f30c-e706-48ef-a6b5-7073193ff583.png)

再デプロイ後

![image](https://qiita-image-store.s3.amazonaws.com/0/35910/4ac28d15-a8ae-dcfc-551b-09e4988f9a38.png)

text/htmlにはなったけどくずれた
ですよね..

#他リソースへの参照の相対パスを絶対パスに変換
`absolute-filter(url)`
画像、js、cssといった他リソースへの参照方法には絶対参照と相対参照の２つの方法があるが、

- 絶対参照: `<link href="http:/hogehoge.com/fugafuga.css" />`
- 相対参照: `<link href="/fugafuga.css" />`

相対参照の場合今回のフィルタを通すとホストがAWSのAPIGatewayになるため、あたりまえだが見つからない
なので全てのリンクを絶対参照に直してやる必要がある

正規表現を駆使して頑張ろうかとも思ったが、
「Jsoup」というJavaのライブラリがうまい具合にやってくれるということをGoogle先生が教えてくれたのでそれを使った

```scala:src/main/scala/filter/HtmlFilter.scala(追加)
  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`

![image](https://qiita-image-store.s3.amazonaws.com/0/35910/f93d30c7-c30d-c8a6-bf08-e9249378a0bd.png)

ファビコンはでたけど内容は変わらず
まだ足りない
なんか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)`の修正
めげずにがんばった

```scala:src/main/scala/filter/HtmlFilter.scala(修正)
  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さんが[メモってた](http://d.hatena.ne.jp/xuwei/20110207/1297075872)

```scala:src/main/scala/filter/HtmlFilter.scala(修正)
  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
    }
  }
```
どきどきしながら再表示

![image](https://qiita-image-store.s3.amazonaws.com/0/35910/030e85ca-f85c-6536-4ecf-7795a62504aa.png)

ロゴがでた！
ちがう、そうじゃない

#CSS用のプロキシ準備
`css-proxy(url)`の追加
`absolute-filter(url)`の修正

CSSはどうもContent-Typeを`text/css`で返さないといけないようなのでそれようのAPIGateway「css-proxy」を追加し(Lamdaはproxyをそのまま利用)、それに合わせabsolute-filterのLambdaを修正

```scala:src/main/scala/filter/HtmlFilter.scala(修正)
  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
  }
```
再表示

![image](https://qiita-image-store.s3.amazonaws.com/0/35910/4505f69d-076e-e5f2-5f3f-cc0645cc9ec0.png)

**やっと出た！！**
ふー、、疲れた

#ギャル文字変換
`galgo-filter(url)`

ここまでできたらギャル文字への変換は簡単だった

```scala:src/main/scala/filter/HtmlFilter.scala(追加)
  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`
にアクセス！

![image](https://qiita-image-store.s3.amazonaws.com/0/35910/1a6704d2-e90b-d242-d086-f09af916186a.png)


**読めないけど成功したっぽい！**

ギャル文字化されたページのリンクをたどるとそこから先もギャル文字

![image](https://qiita-image-store.s3.amazonaws.com/0/35910/6661c00d-7b71-120c-f551-0f1523e29779.png)

コード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`

![image](https://qiita-image-store.s3.amazonaws.com/0/35910/629f54ed-e409-149e-3b55-ca047cb66941.png)