『Web API: The Good Parts』 を読んだ
- 作者: 水野貴明
- 出版社/メーカー: オライリージャパン
- 発売日: 2014/11/21
- メディア: 大型本
- この商品を含むブログ (6件) を見る
同僚から借りて読みました。 全体としては Web API の設計に少しでも携わる人間ならとりあえず読んでおいたらいいんじゃないかなーという感じです。 薄いし。
本書を読んだからと言って最高の Web API の設計ができるようになるとは思わないですが、Web API の設計をする際に知っておくべきことが一通りまとまっていて良い感じだと思いました。
学びメモ
知らなかったことや、なんとなく知ってたけど改めて調べたことなどまとめておきます。
RFC 5861 での Cache-Control
ヘッダの拡張
RFC 5861 にて、Cache-Control
ヘッダの 2 つの拡張が定義されています。
stale-while-revalidate
ディレクティブ : プロキシサーバー上でmax-age
を超えてキャッシュが切れた後も、(裏側で非同期にキャッシュの検証を行いつつ) キャッシュ済みの古いレスポンスデータを返しても良い時間を指定できる。stale-if-error
ディレクティブ : オリジンサーバーへのリクエストに失敗する場合に、キャッシュ済みの古いレスポンスデータを返してもよい時間を指定できる。
RFC 5861 の現在の state は “Informational” らしいのでどれぐらいサポートされてるのかわからないですが、「プロキシサーバー上でキャッシュが無効になったあとも、裏でキャッシュ更新しつつユーザーからのリクエストに対してはレスポンスを返してほしい」 って状況はあるので、こういうのが既存のプロキシサーバーなどでサポートされてるとアプリケーション開発時に便利に使えそうです。
本書の 4.3.6 節 「Cache-Control ヘッダ」 に記述されています。
独自のメディアタイプを作る
メディアタイプの登録プロセスの効率と柔軟性を増やすために、サブタイプのいくつかの登録 『木』 (registration “trees”) が RFC 6838 にて定義されています。 それぞれの木には facet *1 が定義されているので、新たに作る独自のメディアタイプの用途に応じて登録木を選択し、それに応じた facet を付けることになります。 (登録木によっては登録作業なども必要。)
- Standards Tree : IETF 標準に関連付けられるなどした、標準のメディアタイプのための木。 Facet はない。
- Vendor Tree : 公に使用可能な製品に結びつけられたメディアタイプのための木。 Facet は “vnd.”。 (例 : “application/vnd.ms-excel”)
- Personal or Vanity Tree : 実験的に作られたり、商業的に流通しない製品の一部だったりするメディアタイプのための木。 Facet は “prs.”。
- Unregistered x. Tree : 私的に、ローカルな環境でのみ使われるメディアタイプのための木。 Facet は “x.”。
Web API で使用されるメディアタイプを新たに独自に定義する場合は、vendor tree か personal or vanity tree のどちらかになるでしょう。
本書の 4.4.3 節 「自分でメディアタイプを定義する場合」 に書かれています。
CORS 周り
XMLHttpRequest
*2 には同一生成元ポリシー (Same Origin Policy) が適用されるため、通常は生成元の異なる URL に XMLHttpRequest
でリクエストを投げることができません。 異なる生成元へのリクエストが可能になるように考えられた仕様として CORS (Cross-Origin Resource Sharing) があります。
それ自体は知ってたのですが、
- CORS でプリフライトリクエストが投げられる条件 (
POST
メソッドでもプリフライトリクエストするとは限らない、みたいな話を聞いたことはあったけど、詳しくは知らなかった)、とか Cookie
やAuthentication
などのヘッダで認証情報をやり取りする場合は、サーバー側はAccess-Control-Allow-Credentials
ヘッダを返す必要があり、クライアント側はXMLHttpRequest#withCredentials
というプロパティをtrue
にしないといけない
など知らなかったので、いろいろあるなーと思いました。
ちなみに最近は CORS (や他の仕様) を置き換えるものとして Fetch Standard が作られてるみたいですね。
本書の 4.5 節 「同一生成元ポリシーとクロスオリジンリソース共有」 に書かれています。
セキュリティ周りの話
X-Content-Type-Options
ヘッダ
IE (どのバージョンまで? 11 も?) には Content-Type
ヘッダの値を無視してレスポンス内容のデータ形式を推定する Content Sniffing という機能があり、JSON として解釈されるべきコンテンツが HTML として解釈されて XSS 脆弱性になる、という問題があります。
IE 8 以降の場合は、サーバーがレスポンスとして次のレスポンスヘッダを返すことで Content Sniffing を無効にできます。 さらに、Firefox や Chrome、IE 9 以降では、このヘッダを付けることで JSON ハイジャック *3 (後述) の危険性を減らすことができるそうです。 (JSON を返すときは常に付けましょう。)
X-Content-Type-Options: nosniff
詳細は次のページに書かれています。
本書では、6.3.1 節 「XSS」 に書かれています。
JSON ハイジャック
JSON 形式のレスポンスを JavaScript としてブラウザに解釈させることで、別オリジンのサーバーから配信される JSON の内容を悪意ある第三者の web ページが読みだすことができてしまう、という脆弱性。 例えば、悪意ある第三者の web ページが Array
オブジェクトのコンストラクタを書き換えたうえで、配列を返す JSON ファイルを script
要素で読み込むと、JSON の中身が書き換えられた Array
オブジェクトのコンストラクタに渡されてデータが読まれる、という感じです。 (これは Firefox 2 系で発生していた脆弱性で、最近のブラウザは対策済みらしいです。)
サーバー側の対策として、以下のものがあります。
script
要素では読み込めないようにする :script
要素で読み込まれた場合には送られないヘッダフィールド (X-Requested-With
ヘッダなど) を必須にする。- JSON のレスポンスをブラウザが JS として認識しないようにする :
Content-Type: application/json
にすることはもちろん、X-Content-Type-Options: nosniff
を付ける。 - JSON のレスポンスを JS として解釈不可能にする (あるいは JS として実行されてもデータが読まれないようにする) : レスポンスの先頭に
while (true);
を付けるとか。
本書では、6.3;3 節 「JSON ハイジャック」 に書かれています。
セキュリティ周りの各種ヘッダ
X-XSS-Protection
レスポンスヘッダ : XSS を発生させそうなパターンがレスポンスに含まれている場合に、ブラウザがレスポンスをブロックする機能を有効にしたり無効にしたりできるヘッダフィールドぽい。 この機能は Chrome と Safari、IE (8 以降) がサポートしてるぽい?- ちゃんとしたドキュメントはあんまり見当たらない。
- XSS ブロックの機能はデフォルトで有効っぽい *4 から、サーバーサイドでは基本的には付けなくてもよさそう?
- 参考 : IE8 Security Part IV: The XSS Filter | IEBlog
Strict-Transport-Security
レスポンスヘッダ : ブラウザからのアクセスを HTTPS に限定させる。- http でリクエストされたときに https にリダイレクトする、という挙動だと、最初の http でのアクセスが中間者攻撃で書き換えられる恐れがある → 以前にこのヘッダが送られてきていたら、ブラウザは最初から https でアクセスする。
- RFC : RFC 6797 - HTTP Strict Transport Security (HSTS)
- 参考 : HTTP Strict Transport Security - Security | MDN
Public-Key-Pins
レスポンスヘッダ : ブラウザに暗号公開鍵とドメイン (?) を結び付けさせる → ブラウザは将来に証明書が偽造された場合に検知できるようになる。- 攻撃者により認証局が危殆化した場合に中間者攻撃されるのを防げる。
- RFC : RFC 7469 - Public Key Pinning Extension for HTTP
- 参考 : Public Key Pinning - Web security | MDN
本書の 6.5 節 「セキュリティ関係の HTTP ヘッダ」 に書かれています。
ブラウザがレスポンス内容を UTF-7 と誤認することによる XSS の成立
XSS 対策として 「<」 という文字列をエスケープするなどの方法がありますが、ブラウザにレスポンス内容の文字エンコーディングを UTF-7 だと誤認させることで、「<」 のエスケープをしていても 「<script>」 という文字列をそのままブラウザに食わせることができる、という攻撃があるとのことです。
UTF-7 では、「<」 という文字は ASCII の 「+ADw-」 として表現されるためです。 「+Adw-script+AD4-」 という文字列を UTF-7 としてブラウザに解釈させれば 「<script>」 とみなされるので、「<」 という文字のエスケープをしていてもそれをすり抜けて XSS ができることがあるようです。 (古いブラウザの脆弱性。)
最近のブラウザだと問題ないようですが、念のため対策するのであれば 「+」 をエスケープすると良いようです。
- 参考 : 第1回 UTF-7によるクロスサイトスクリプティング攻撃[前編]:本当は怖い文字コードの話|gihyo.jp … 技術評論社
- 参考 : UTF-7でXSSを発生させる10の方法 - 葉っぱ日記
本書の 6.3.1 節 「XSS」 に書かれています。
ステータスコード 429 Too Many Request
レートリミットに達した場合に返すためのステータスコードとして 429 Too Many Request が定義されているとのこと。 初めて知りました。
ヘッダフィールドの値として使用できる時刻の形式は決められてる?
本書には以下のように書かれていて、X-Rate-Limit-Reset
のようなヘッダフィールドの値として Unix タイムスタンプを使うことは問題がある、といってるんですけど実際どうなんでしょうね。 (本書 195 ページ。)
HTTP ヘッダに Unix タイムスタンプを入れるのは、実は問題があるのです。 というのは、RFC 7231 の HTTP 1.1 仕様によればヘッダに入れてよい時間の形式は以下の 3 種類に限定されているからです
多分根拠は RFC 7231 の以下の箇所だと思うんですけど、これって別にあらゆるヘッダフィールドの値として使われる時刻の形式について言っているのではなくて、HTTP-date
について言ってるだけな気もします。 わかんないですけど。
A recipient that parses a timestamp value in an HTTP header field MUST accept all three HTTP-date formats. When a sender generates a header field that contains one or more timestamps defined as HTTP-date, the sender MUST generate those timestamps in the IMF-fixdate format.
RFC 7231 (section 7.1.1.1 Date/Time Formats)
VirtualBox 5 上の Debian 8 に Guest Additions をインストールする
まあドキュメントを読めって話ではあります。 次のページにまとまっています。
背景
わりと長く VirtualBox を使ってきて、何度も Guest Additions をインストールしてきましたが、これまで Guest Additions のインストール手順をちゃんと見たことがありませんでした。
これまでは雑にインストールメディアからインストールできてたのですが、今回 VirtualBox 上の仮想マシンに Debian 8 をセットアップして Guest Additions をインストールしたらちゃんとインストールできてなかった *1 ので、改めて調べてみました。
インストール方法
最初に言った通りドキュメントに全部書かれてますので、詳細はドキュメントを読んでください。
事前準備
まず、事前準備として、dkms
パッケージをインストールします。 (Debian/Ubuntu 系の場合。)
apt-get update apt-get install dkms
(root
ユーザー以外で上記コマンドを実行する場合は sudo
してください。)
んでもってシステムを再起動します。
インストール
再起動後、ゲスト OS が起動したら、まずは Guest Additions のインストールメディア (VBoxGuestAdditions.iso
) を挿入します。 「デバイス」 メニューの 「Guest Additions CD イメージの挿入」 を選択すると、ゲスト OS にインストールメディアが (仮想的に) 挿入されます。
挿入された CD-ROM は /media/cdrom
にマウントされます。 端末を開いて /media/cdrom
に移動して、次のコマンドを実行することでインストールが開始されます。
sh ./VBoxLinuxAdditions.run
(root
ユーザー以外で実行する場合は sudo
しましょう。)
関連エントリ
VirtualBox で共有フォルダを使う
- VirtualBox 4 における共有フォルダの自動マウントの設定 - ひだまりソケットは壊れない : Guest Additions をインストールしたら共有フォルダ機能が使えます。
VirtualBox 上の Debian に Guest Additions をインストールする
dkms
パッケージをインストールする以外の方法が紹介されてますが、まあ gcc
と make
と linux-headers
の正しいバージョンが入ればなんでも良さそうです。
*1:事前準備ができてなかった
UWP アプリ 「みお×ぽん」 をリリースしました
2016 年 1 月 19 日に、Universal Windows Platform (UWP) アプリ 「みお×ぽん」 をリリースしました。 下記ページからダウンロードできます。
どんなアプリ?
株式会社インターネットイニシアティブが提供するインターネット通信サービス 「IIJmio 高速モバイル / D サービス」 の利用状況の確認と、高速通信のためのクーポンの有効・無効の切り替えを行うことができます。 Windows 10 を搭載した PC や、Windows 10 mobile 端末などで利用できます。
IIJ 公式アプリ 「みおぽん」 (Android 用 / iOS 用) の機能を Windows プラットフォームで提供するために開発しました。
既知の問題点
Chrome からの共有で onNewIntent が呼ばれない問題 (Android アプリの documentLaunchMode の話)
Android の API level 21 で導入された documentLaunchMode
に関する Activity の挙動にバグっぽいところがあって、結構扱いに困るのでまとめておきます。 「documentLaunchMode
? 関係ないや」 って思ってる人でも、外部アプリからの Intent を扱うアプリを書くときに影響されるかもしれません。
まとめ
- Activity の
documentLaunchMode
としてintoExisting
が指定されており、既存の Activity が再利用される場合は、launchMode
がstandard
であっても Activity のonNewIntent
メソッドが呼ばれるべき *1 だが、実際には呼ばれない。- バグっぽい。
- AndroidManifext.xml で
documentLaunchMode
の値を指定していなくても、外部アプリが投げる Intent にintoExisting
相当のフラグ (FLAG_ACTIVITY_NEW_DOCUMENT
フラグ) が設定されていることがあるので、外部からの Intent を受け取る Activity を書いている場合には否が応でもこの問題に悩まされる。 - とりあえずの対処法は、AndroidManifest.xml で、Activity に
launchMode="singleTop"
を指定すること。- アプリ内部で使用することを考えて
singleTop
にしづらい場合は、内部で使用する Activity と外部からの Intent を受け取るための Activity を別に定義して (単純にサブクラスを作れば良いだろう)、外部からの Intent を受け取るための Activity のlaunchMode
をsingleTop
にする、などの対応が必要。
- アプリ内部で使用することを考えて
関連する発表資料
この記事の内容に関連した話を 2015 年 9 月 30 日の 「関西モバイル研究会 #6」 で発表しました。
背景
この問題に行きついた背景です。
- 外部アプリからテキスト (
Intent.EXTRA_TEXT
) を含む Intent を受け取って処理する Activity を含むアプリを開発していた。- Chrome アプリの 「共有」 で投げられる Intent も受け取れる。
- 次のようなユーザー操作を行うと、Activity が新しい Intent を扱えなくて困った。
- Chrome でとあるページ (例として 「http://example.com/1」) で 「共有」 し、Activity を起動。 → Activity では Intent から 「http://example.com/1」 というテキストを取りだせる。
- Activity を終了せずに、アプリを切り替えて Chrome に戻る。
- 別のページ (例として 「http://example.com/2」) で再度 「共有」 し、同じ Activity を起動。 → Activity が破棄されずに残っていた場合、
onStart
メソッドやonResume
メソッドが呼ばれるが、onNewIntent
メソッドが呼ばれず、getIntent
メソッドで取得できる Intent も前に開いたときの Intent になっている。 → 新しい Intent の情報が得られない!
documentLaunchMode
の話
documentLaunchMode
とはなんぞや、という話。
- 公式ドキュメントとしては以下のページを読むとわかりやすいです。
- API level 21 で導入。
- Activity 起動時の挙動を制御するためのもの。
- これを指定することで、「最近のタスク一覧」 に同じアプリケーションの複数ドキュメントを表示できる。
- AndroidManifest.xml で指定することもできるし、
startActivity
にフラグとして渡すこともできる。
intoExisting
documentLaunchMode
の値としてintoExisting
を指定すると、既存のすべてのタスクの中から Intent のコンポーネント情報とdata
の URI が同じものが探される。- あれば、そのタスクの中身がリセットされて、Activity が再利用されて Activity の
onNewIntent
メソッドが呼ばれる。 - なければ、新しいタスクが生成される。
- あれば、そのタスクの中身がリセットされて、Activity が再利用されて Activity の
- と、ドキュメントには書かれているが、
onNewIntent
メソッドが呼ばれるのはタスクの Activity のlaunchMode
としてsingleTop
などの値が指定されている場合で、standard では呼ばれなかった。 (まじかよ……) - DocumentCentricApps というサンプルコードがあるが、サンプルコードのコメントなどはドキュメント通りの挙動を期待しているが、実際の挙動はドキュメントどおりにはならなかった。 (Nexus 5; Android 5.1.1 で確認)
バグっぽい
- ドキュメントを読む限り、
documentedLaunchMode
がintoExisting
ならば、たとえlaunchMode
がstandard
でもonNewIntent
メソッドが呼ばれてもよさそうだけど、実際には呼ばれない。 - API level 23 (Android 6.0 Marshmallow) での挙動も見てみたけどやっぱり同じだった。
- もともと
launchMode
がstandard
だとonNewIntent
メソッドは呼ばれないものなので、仕様なのかもしれないけど仕様だとしたら糞仕様だしバグならつらい。 - ていうか 『Activities launched with the
FLAG_ACTIVITY_NEW_DOCUMENT
flag must have theandroid:launchMode="standard"
attribute value』 って書かれてるし……。 - StackOverflow でこの問題について書いてる人が居た : Android API guide > Overview Screen: onNewIntent() not called with FLAG_ACTIVITY_NEW_DOCUMENT - Stack Overflow
- バグ報告した : Issue 188033 - android - onNewIntent method not called with FLAG_ACTIVITY_NEW_DOCUMENT flag - Android Open Source Project - Issue Tracker - Google Project Hosting
Chrome アプリの 「共有」 で onNewIntent
が呼ばれない問題
原因
Chrome アプリの 「共有」 (メニューの 「共有」 の右側に表示されている、最後に起動したアプリアイコンをタップ) で投げられる Intentを見てみたところ、以下のフラグが設定されていました。
FLAG_ACTIVITY_PREVIOUS_IS_TOP
FLAG_ACTIVITY_FORWARD_RESULT
FLAG_GRANT_READ_URI_PERMISSION
FLAG_ACTIVITY_NEW_DOCUMENT
(API level 21;documentLaunchMode="intoExisting"
相当のフラグ)
上述のとおり、documentLaunchMode
が intoExisting
で、launchMode
が standard
だと、古い Activity が再利用されるもの onNewIntent
メソッドが呼ばれないのです。 つらいですね。
回避策
一つの解決策としては、Activity に launchMode="singleTop"
を設定する方法があります。 これだと onNewIntent
メソッドが呼ばれるようになります。
しかし、外部からの Intent を受け取るだけじゃなくてアプリ内部からも起動される Activity の場合は launchMode="singleTop"
を設定することができないことも多いでしょう。 そういう場合は、アプリ内部からしか起動されない Activity と外部からの Intent を受け取る Activity を別のクラスにしてしまって (片方をもう一方のサブクラスとするなど)、外部からの Intent を受け取る Activity にだけ launchMode="singleTop"
を設定するなどの方法を採ることになるでしょうか。 もうちょっといい方法があれば嬉しいですが、それぐらいしか思いつきませんでした。
終わり
この問題、とにかく辛いのですがあんまり困ってる人を見かけないので、もしいい感じの回避策があるのでしたら教えてください!!!!
*1:ドキュメントを読む感じだとそうだと思われる
実機の Android 端末に対して Hierarchy Viewer を使って View の階層構造を調べる
Android アプリを開発する際に便利な Hierarchy Viewer ですが、日本語のブログ記事だと実機の Android 端末に対して Hierarchy Viewer を使う際に ViewServer を使わない方法を紹介してるものがあまり見当たらない *1 ので、ViewServer を使わない方法を紹介しておきます。
Hierarchy Viewer とは
Hierarchy Viewer は、Android アプリの UI のデバッグや最適化を行う際に便利なツールです。レイアウトの View の階層構造を表示する機能 (Web 開発でいうところの DOM インスペクタみたいな感じ) や、ディスプレイの表示内容を拡大して表示する機能 (Pixel Perfect というツール) があります。
Hierarchy Viewer を使うための準備
上記ページに書いてあります。 Android 4.1 以降の端末であれば、次の 2 つのことをすれば良いようです。
- Android 端末の開発者向けオプションを有効にする
- 開発に使用している PC の環境変数
ANDROID_HVPROTO
の値をddm
にする。
ロックされている Android 4.1 未満の端末の場合は、ViewServer を使用することになります。 エミュレータのような非ロックの Android 4.1 未満の端末なら何もする必要はなさそうです。 (エミュレータは非ロックです。)
使い方
公式のドキュメントを読むのが一番良いと思うのでリンクをはっておきます。
下記ページにて、Hierarchy Viewer の起動 (Android Studio のメニューから起動する方法) から一通りの使用方法が説明されています。
また、Hierarchy Viewer のより細かな使い方や、Pixel Perfect の使い方などが以下のページで説明されています。
関連ページ
- [Android] Hierarchy View を使ってレイアウトを見直す: スタジオプリズム㐧3ブログ
- 非rootなAndroid端末でHierarchy Viewerを使う方法 - Just for Fun
- ViewServer の導入方法が詳しく説明されています。
- Android - Viewの構造を丸見えにするHierarchy Viewerの使い方 - Qiita
- 使い方が説明されています。 日本語で使い方を見たい方はこちらを参考にするとよいでしょう。
- 実機でHierarchy Viewerを使う 電脳羊(Android Dream)/ウェブリブログ
- こちらも ViewServer を使う方法。