読者です 読者をやめる 読者になる 読者になる

@テク野路ジーロード

配信タグシェアリングシステムpickvyを開発、運営開始。最近は、Googleアナリティクスを研究中

クロスサイト制約と日本語のエンコード/デコードにどハマった話し(JavaScript,Rails)

本日は遠出せずに、家でもくもくしたいsunday150です。
引越してきてから、西側窓に遮像カーテンが無くて、みえっぱなしです。
ほら、見てみて!じゃない。見ても得なことないです。

 

長文です。所謂、クロスサイトドメイン通信の穴にハマった話しです。
最初、その穴にハマったと気づかず試行錯誤し、そして、別の観点にも
ハマって右往左往した話しを書きます。

 

<はじまりはじまり>


構築していたサイトで不思議な現象が起きていました。
あるページをJavaScriptページへリダイレクトさせているのですが、
JavaScriptが動作しない、反応しないという現象です。


そのサイトはRailsで組んでいいます。仕組みとしてはこうです。
サイトのあるページ(=hogeコントローラのhelloメソッド)にアクセスが来たら、
publicフォルダのhello.jsへリダイレクトしてJavaScriptを渡します。
JavaScriptが動作して、データをサーバが受け取る、流れです。

 

しかし、上手くいかない=サーバ側にJavaScript実行結果のデータが蓄積されない

 

別にリダイレクトさせなくても、
RailsメソッドからJavaScriptコードを直接出力しても技術的には実現できるのです。
しかし、そのような実装をやると変更時に保守が面倒としか思えないので、
このような仕組みにしていました。

文章で表現すると、以下のイメージです。

私の環境=CentOS7上のFirefox(ESR, 38.1.1)で開発ツールを立ち上げて、
ネットワークモニタで、リクエストとレスポンス結果を見てみる。
こんな感じ。

f:id:sunday150:20160503163615p:plain



そのメソッド(=ページ)にアクセスすると、「302 Found」が返ってきていた。
最初に、これを怪しんだ。ネットワークモニタでは以下の赤枠。

f:id:sunday150:20160503163755p:plain

 

 「302」の意味は、「リクエストしたリソースが一時的に移動されているときに返される。」である。HTTPステータスコード - Wikipedia
今回だとリダイレクトしているという意味で、正常な動作なのである。
ネットワークモニタをもう一度確認しても、正常にリダイレクト動作してる。

f:id:sunday150:20160503164654p:plain

 

レスポンスのステータスコードが302であることは良い。
念の為、リダイレクト先にアクセスできるかも確認。
リダイレクト先のURLを直接開くと確かに、開く。

 

はて、サイトはちゃんとスクリプトを読み込んでいるのに、
スクリプトを実行していない?ということ?
実行していないのは、データをサーバへ送信するところ

 

ここで気付く。仕組みとしては、Aサイトに、Bサイトのスクリプトを読み込ませて
いることに...。あー、これは別ドメインへデータ送信しようとしているから、
「同一生成元ポリシー」同一生成元ポリシー - Wikipedia、所謂、クロスドメイン制約
にひっかかってることだな。

 

<ここから、同一生成元ポリシーやクロスドメイン制約について調べまくる>

長いので、略。

最終的に、クロスドメイン制約を超える方法として、
以下の6つが対応策としてインターネット上で見つかった。

  1. Access-Control-Allow-Originを有効にしてサーバ設定を弄る。
    (結局これは解読しきれなかった)
  2. HTML5のpostMessageを使う
    (但し、HTML5対応ブラウザ限定)
  3. JSONPを使う
  4. JQueryライブラリのjquery.xdomainajax.jsを使う
    (結局使わなかったから詳細不明)
  5. 同一ドメインサイト経由で別ドメインサイトのコンテンツを転送させる
    (意図はわかるが、今回の場合は使えない)

今回のケースに合い、簡単な「3.JSONPを使う」で行くことした。

 

やることは簡単。JQueryajax通信させるところに「dataType: 'jsonp'」を
追加すること。

コードとしては以下のイメージ

  • $.ajax({
           
    url: 'http://hogedomain/hoge/ajaxhello',
           
    type: 'POST',
           
    dataType: 'jsonp',
            data : { hellodata:xxx}
       
    });

補足:当たり前ですが、typeをPOSTにしても、動作します。

 

理解しきれず、上手く説明できませんが、これだけで取り敢えず通信試行は
しました。OK!

 

<そして、さらに、問題発生。JavaScript構文エラーがでてる>

 実はこれだけでは、私の場合は動かなかったのです。
それは、上記のdata部分には、base64エンコードした値をいれようと
していたのですが、「Error: String contains an invalid character」が出てしまったのです
JavaScriptの構文エラーですね。
Firefoxfirebug上のコンソールにこれが表示されていました。

 

何が問題かというと、データに”日本語”が含まれていたからなんです。

結論としてはjavascriptbase64エンコード部分を以下で実装しました。

  • 修正前: encode_data = btoa(data)
                           ↓
  • 修正後: encode_data = btoa(unescape(encodeURIComponent(data))))

 

<さらにさらに、問題発生>

サーバ側で「505 Internal Error」が発生。うーん。

当然と言えば当然ですが、サーバ側では受け取ったデータをデコード
しなくてはいけません。デコードできず、エラーが返ってきていました。

 

解決策の結論としては以下のコードになりました。

  • decode_data = URI.unescape(URI::escape(Base64.decode64(data)),Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")))

 

注意点としては、普通にデコードしてしまうと、半角スペースが
「+」にデコードされてしまって、上手く元の値を取れない点です。
この問題点を解消するために、Regexpで上手く変換かけています。
参考にしたサイトを以下に記載しておきます。

ruby/rails equivalent to javascript decodeURIComponent? - Stack Overflow

Ruby equivalent to JavaScript’s encodeURIComponent that produces identical output? - Stack Overflow

 

<ここまでで、やっと、データがやり取りできるようになりました>

最後の方はえいやで書いてしまったところも多かったので、
別途詳細に記載する機会を設けたいところです。

 

振り返ると以下の流れでした。

   [今回の試行錯誤の流れ]

  1. データ通信ができないことに気付く
  2. HTTPリクエストとレスポンスの結果から、何が起こっているのか把握する
    スクリプトは読み込んでいるのに、実行がされていない事実に辿り着く
  3. クロスドメイン通信について情報収集し、対応策を決定し、実装
  4. 動作が変わり、ブラウザ上で表示されるエラー内容の変化に気付く
    →クロスドメイン通信はいけそうだが、他のエラーっぽい
  5. 原因はJavaScript構文エラーであることに気付く→調べて解決
  6. しかしまだエラー。サーバ側で「505 Internal Error」。
    Railsでデコードが上手くいっていない→調べて解決。

 

長かった。1日潰したが、ウェブ技術の知識を増やせたのは良かった。
クロスドメイン通信、日本語のエンコード/デコードは今後も重要だな。

 

以上