ページ

2009年9月30日水曜日

プチアプリ制作続き - FireFox3.5 のペーストボードタイプは?

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

前回:Cocoaの日々: プチアプリ制作 - SafariのURLから HTMLの<A>タグを生成する

FireFox 3.5 では使えなかった。ペーストボードタイプをダンプしてみた。

2009-09-30 12:53:10.186 AHREFGenerator[7466:10b] (
"public.url-name",
"CorePasteboardFlavorType 0x75726C6E",
"public.url",
"CorePasteboardFlavorType 0x75726C20",
"Apple URL pasteboard type",
"public.utf8-plain-text",
NSStringPboardType,
"public.html",
"Apple HTML pasteboard type",
"dyn.agk81n6xqqu",
"CorePasteboardFlavorType 0x75726C64",
"dyn.agu8y4554rf0g22n1rf0gk25bsmwa",
MozillaWildcard,
"dyn.agu8y63n2nuuha5dbrf1ca2pxqry0wkduqf31k3pcr7u1e3basv61a3k",
"NeXT plain ascii pasteboard type"
)


なるほど WebURLsWithTitlesPboardType が無い。これは Safari(Webkit)固有のものなのかもしれない。

以下は、Safar4 でのダンプ。
2009-09-30 12:53:45.268 AHREFGenerator[7466:10b] (
"dyn.agu8yc6durvwwa3xmrvw1gkdusm1044pxqyuha2pxsvw0e55bsmwca7d3sbwu",
"Apple files promise pasteboard type",
"dyn.agu8ye55trr00c6xpkvy0g7dmr71gc6x3mvy1g7cuqm10c6xenv61a3k",
BookmarkDictionaryListPboardType,
"dyn.agu8ye55trr00c6xpnr4gc7dmsr4gw25xnbbg82pwqvnhw6df",
BookmarkStatisticsPBoardType,
"dyn.agu8zs3pcnzme2641rf4guzdmsv0gn64uqm10c6xenv61a3k",
WebURLsWithTitlesPboardType,
"public.url",
"CorePasteboardFlavorType 0x75726C20",
"Apple URL pasteboard type",
"dyn.agu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu",
"public.utf8-plain-text",
NSStringPboardType,
"dyn.agu8y63n2nuuha5dbrf1ca2pxqry0wkduqf31k3pcr7u1e3basv61a3k",
"NeXT plain ascii pasteboard type",
"public.url-name",
"CorePasteboardFlavorType 0x75726C6E",
"com.apple.pasteboard.promised-file-content-type",
"com.apple.pasteboard.promised-file-url",
NSPromiseContentsPboardType,
"dyn.agu8y6y4usm1044pxqzb085xyqz1hk64uqm10c6xenv61a3k"
)



"public.url-name" というのが気になったので出力してみた。
2009-09-30 12:55:27.022 AHREFGenerator[7576:10b] public.url-name=Yahoo! JAPAN


ああ、ここにタイトルが入っていた。このタイプは Safari、Firefox 共に存在するのでタイトルを’取得するにはこちらの方がいいようだ。

- - - -
ついでにサイトのサムネイルを生成するのもいいかもしれない。


+++++ ドラッグ&ドロップ、ペーストボードの関連記事 +++++
Cocoaの日々: ドラッグ&ドロップのデータタイプ
Cocoaの日々: FireFoxから画像をドラッグ&ドロップして保存する
Cocoaの日々: Safariから画像をドラッグ&ドロップして保存する


2009年9月29日火曜日

プチアプリ制作 - SafariのURLから HTMLの<A>タグを生成する

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク




ブログを書いていて他ページのリンクを埋め込むときにいつも手で <A>タグを書いている。いい加減面倒になってきたので Safariの URLをドラッグ&ドロップするとそのページの <A>タグを生成するプチアプリを(ボケ防止も兼ねて)作ってみた。



動作イメージ:
 (1) Safari の URLをドラッグしてアプリへ落とす
 (2) URLとタイトルが埋め込まれた <A>タグが、テキストボックスに生成される

後はこれをコピーして使う。

ソースコード:AHREFGenerator-01.zip


小一時間で作成したので最小限動くだけのコード。URLを受け取るビューのコードを以下に掲載する。

DropURL.m

まずペーストボードタイプの登録。

- (void)awakeFromNib
{
[self registerForDraggedTypes:[NSArray arrayWithObjects:
    @"WebURLsWithTitlesPboardType",nil]];

}


続いてドロップの受け入れ許可。
- (NSDragOperation)draggingEntered:(id )sender {
NSPasteboard *pb = [sender draggingPasteboard];
NSArray* pb_types = [pb types];

if ([pb_types containsObject:@"WebURLsWithTitlesPboardType"]) {
return NSDragOperationCopy;
}
return NSDragOperationNone;
}


最後に<A>タグの生成。
- (BOOL)performDragOperation:(id < NSDraggingInfo >)sender
{
NSPasteboard* pb = [sender draggingPasteboard];
NSArray* pb_types = [pb types];

if ([pb_types containsObject:@"WebURLsWithTitlesPboardType"]) {
NSArray *props = [pb propertyListForType:@"WebURLsWithTitlesPboardType"];
NSString* url = [[props objectAtIndex:0] objectAtIndex:0];
NSString* title = [[props objectAtIndex:1] objectAtIndex:0];

NSString* str = [NSString stringWithFormat:
@"%@", url, title];
[text_view setString:str];
[text_view setSelectedRange:NSMakeRange(0, [str length])];

}  
return YES;
}


Safari からわたってくるペーストボードに WebURLsWithTitlesPboardType というものがあり、ここから URLと合わせてページのタイトル文字列を取得することができる。配列の入れ子となっていてこんな感じで URL とタイトルが入っている。
2009-09-29 22:54:17.723 AHREFGenerator[6530:10b] (
(
"http://www.apple.com/ipodtouch/"
),
(
"Apple - iPod touch - Music, games, apps, and more on a great iPod."
)
)


ここからindexの決めうちでURLとタイトルを取り出す。
NSString* url = [[props objectAtIndex:0] objectAtIndex:0];
NSString* title = [[props objectAtIndex:1] objectAtIndex:0];


参考:CocoaDev: WebURLsWithTitlesPboardType
#早速活用できた ;->

- - - -
NSPasteboard は Snow Leopard で結構手が入っている。新規メソッドも追加されているようだ。
Mac Dev Center: NSPasteboard Class Reference

こんな感じで大半のメソッドが 10.5以前と、10.6以降の区別がされている。


Overview には 10.6 からの新機能が紹介されている。複数のアイテムを含むことができるようになった(らしい)。


またペーストボードに対するデータの書き込みと読み出しのインターフェイスがプロトコルとして定義されることで様々なクラスをペーストボードで直接扱えるようになったようだ。このあたりは別の機会に検証してみたい。

Mac Dev Center: NSPasteboardWriting Protocol Reference Protocol Reference
Mac Dev Center: NSPasteboardReading Protocol Reference Protocol Reference

2009年9月28日月曜日

Snow Leopard で導入された NSApplicationDelegate

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

Snow Leopard から NSApplicationDelegate Protocol Reference が導入された。
NSApplicationDelegate Protocol Reference


従来、NSApplication で定義されていたメソッドが Protocol として独立して定義されるようになった。メソッドはすべて Optional 扱いとなっている。

iPhone では既に Protocol化されている:UIApplicationDelegate

- - - -
Objective-C 2.0 から Protocol で Optional 指定ができるようになったことを受けて整理されたようだ。Delegateメソッドの定義が独立することでNSApplicationの定義がすっきりし、責務も明確になって理解しやすくなった。

2009年9月27日日曜日

Programming in Objective-C 2.0 LiveLessons Bundle

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

Objective-C 関連の本を出している Stephen G.Kochan の トレーニングコースビデオ付き本。



本自体は 3,791円で売られているわけだから、差額約 3,000円で8時間のビデオが付くのはお買い得かもしれない。
Programming in Objective-C 2.0 (2nd Edition) (Developer's Library)

今時なら Podcast で提供してもらうと通勤電車などで見られて便利かもしれない。

2009年9月26日土曜日

Tips: SimpleCap から Picasa へ画像をアップロードする

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

SimpleCap では撮影した画像を任意のアプリケーションへ渡せる。この機能を使って Picasa へ画像をアップロードしてみよう。

Picasa Web Albums


※これ以降は、Picasa のアカウントを既に持っている前提で話を進める。



SimpleCap 自体はアップローダの機能は持たないので、まずは専用ソフトを入手する。

Download Picasa Web Albums Uploader


ファイルをダウンロードしてインストールすると次のアプリケーションが使えるようになる。



続いてSimpleCap の環境設定を開き、設定済みアプリケーションに登録する。


これで準備完了。


後はスクリーンショットを撮った後、ビューアで "Picasa Web Albums Uploader.app" を選択する。



認証ダイアログが表示されるので自分のユーザアカウントを入力する(初回のみ)。



アップロード用のダイアログが表示される。右下の "Upload" ボタンを押せばアップロードが始まる。繰り返しビューアで画像を選択して送ると、左側にアップロード対象の画像が溜まっていく。こうしてまとめておいて最後に一気にアップロードすることもできる。



アップロードされた画像をブラウザで見る。

2009年9月25日金曜日

WPSU(19) - WebKitで新規ウィンドウを開く(window.openオプション)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

JavaScript の window.open() にはサイズ指定の他、様々なオプションがある。

参考:とほほのJavaScriptリファレンス
ウィンドウ(window) - open()

それぞれのオプションが WebUIDelegate のどのメソッドに対応するのか見てみよう。昨日の HTMLに手を加えて検証用のHTMLを作成してみた。

openchild11.html

このページを開くと各オプションが設定できるようになっている。


"open a child window" をクリックすると、そのオプションを使い window.open() で子ウィンドウが新規に開く。



WPSU で上記HTMLページを開き、どのデリゲートメソッドが呼び出されるか見てみてる。呼出されそうなメソッドを選びログへ書き出してみる。

- (void)webView:(WebView *)sender setFrame:(NSRect)frame
{
NSLog(@"webView:setFrame: %@", NSStringFromRect(frame));
}
- (void)webView:(WebView *)sender setResizable:(BOOL)resizable
{
NSLog(@"webView:setResizable: %d", resizable);
}

- (void)webView:(WebView *)sender setStatusBarVisible:(BOOL)visible
{
NSLog(@"webView:setStatusBarVisible:visible %d", visible);
}

- (void)webView:(WebView *)sender setToolbarsVisible:(BOOL)visible
{
NSLog(@"webView:setToolbarsVisible: %d", visible);
}



結果
2009-09-25 23:00:33.235 WebPageScreenshotUtility[4726:80f] webView:setToolbarsVisible: 0
2009-09-25 23:00:33.235 WebPageScreenshotUtility[4726:80f] webView:setStatusBarVisible:visible 0
2009-09-25 23:00:33.235 WebPageScreenshotUtility[4726:80f] webView:setResizable: 1
2009-09-25 23:00:33.235 WebPageScreenshotUtility[4726:80f] webView:setFrame: {{154, 295}, {400, 300}}



まとめるとこんな感じ。
width, height => webView:setFrame:
location => 不明
toolbar => webView:setToolbarsVisible:
directories => 不明
scrollbars => 不明
menubar => 不明
resizable => webView:setResizable:
status => webView:setStatusBarVisible:


不明のものはほとんどが Safari でも無効なので、使用している JavaScriptエンジンでは対応していないのかもしれない。ただ scrollbars オプションだけは Safariでも有効だったので、この対応付けだけがわからなかった。

2009年9月24日木曜日

WPSU(18) - WebKitで新規ウィンドウを開く(リサイズ #2)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

前日のデリゲートメソッドで指定されたビューのサイズは得られるのだが、実際にビューが載るウィンドウのサイズを決めなくてはいけない。

ビューとウィンドウの位置関係はこんな感じ。




今回は左下(origin)の位置関係は変えず、サイズだけ調整することにしよう。

それぞれの初期範囲を下記とする。これは Interface Builderで配置されている位置関係となる。

ビューの初期範囲:{{vx0, vy0}, {vw0, vh0}}
ウィンドウの初期範囲:{{wx0, wy0}, {ww0, wh0}}


次に、新規ウィンドウが開いたときのそれぞれの範囲を下記とする。
ビューの範囲:{{vx, vy}, {vw, vh}}
ウィンドウの範囲:{{wx, wy}, {ww, wh}}


デリゲートメソッドで渡されるのはビューの範囲だけなので、これと初期範囲の値から、ウィンドウの範囲を求める必要がある。

まず左下の位置(origin)から。
wx = vx + ( wx0 - vx0 )
wy = vy - ( wy0 - vy0 )


次にサイズ。
ww = vw + ( ww0 - vw0 )
wh = vh + ( wh0 - vh0 )



どちらも初期状態の差分を加えるだけ。ツールバーやステータスバーの表示制御があると少し面倒にはなるが基本的な考えは変わらない。

実装を入れてみよう。

デリゲートメソッド内で外側のウィンドウのサイズを計算して設定する。ちと雑なコードだが動作をみるには十分。
- (void)webView:(WebView *)sender setFrame:(NSRect)frame
{
NSRect window_rect;
window_rect.origin = [sender convertPoint:frame.origin toView:nil];
window_rect.origin = [_main_window convertBaseToScreen:window_rect.origin];
window_rect.size = frame.size;

window_rect.origin.x += _window_rect0.origin.x - _view_rect0.origin.x;
window_rect.origin.y += _window_rect0.origin.y - _view_rect0.origin.y;
window_rect.size.width += _window_rect0.size.width - _view_rect0.size.width;
window_rect.size.height += _window_rect0.size.height - _view_rect0.size.height;
[_main_window setFrame:window_rect display:YES];

}



確認用に HTMLファイルを用意した。

openchild1.html

<html>
<head>
<title>SAMPLE: Parent window</title>
<script>
function openChildWindow() {
window.open("openchild2.html", "Child",
'location=0,toolbar=1,directories=0,scrollbars=1,menubar=0,resizable=0,width=800,height=600');
}
</script>
</head>
<body>
<a href="JavaScript:openChildWindow()">open a child window</a><br/>
<br/>
width: 800 / height: 600
</body>
</html>


JavaScriptで子ウィンドウをオープンする。


実行してみよう。

親となるHTMLを開く。


リンクを押して子ウィンドウを開く。


window.open() で指定した {800,600}のサイズで開いた。

2009年9月23日水曜日

WPSU(17) - WebKitで新規ウィンドウを開く(リサイズ)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

JavaScriptの window.open を使う場合、新規ウィンドウのサイズが指定できる。この時のサイズを得るには -[WebUIDelegate webView:setFrame:] を使う。

このデリゲートメソッドを実装しておくと、新規ウィンドウオープン時に呼出されて位置とサイズが渡される。必要ならここで新規ウィンドウのリサイズを行う。

(例)
- (void)webView:(WebView *)sender setFrame:(NSRect)frame
{
NSLog(@"%@, %@", sender, NSStringFromRect(frame));
}


2009-09-23 23:50:45.789 WebPageScreenshotUtility[3769:80f] , {{154, -5}, {550, 600}}



と、ここまで書いたが実際にはまだ動くコードを書いていない。呼出しは確認でき、それっぽい値がわたってきているので多分大丈夫だと思うが。。今日は時間切れ。

2009年9月22日火曜日

WPSU(16) - WebKitで新規ウィンドウを開く(Documentベースアプリへ移行)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

WPSU はシングルウィンドウのアプリとして作成しているため、新規ウィンドウを開くようなリンクを押しても反応しない。


Safari など普通のブラウザと同様にリンクが開くようにする。このあたりは自分で実装するよりは標準のフレームワークを使った方が楽にできる。今までのプロジェクトを一旦捨てて新しいプロジェクトへソースコードを載せ替える。

最初のテンプレートで document-based application を選択する。


後は旧プロジェクトから必要なソースコードをコピーしてプロジェクトへ加える。従来 WebController で行っていた処理は MyDocument へ載せ変えた。

その上で、リンクが押されたときの処理を追加する。

まず Interface Builder を使い、WebViewの UIDelegate を File's Owner(すなわち MyDocument)へ設定する。


次に -[WebUIDelegate webView:createWebViewWithRequest:] を MyDocumentに実装する。

MyDocument.m

- (WebView *)webView:(WebView *)sender createWebViewWithRequest:(NSURLRequest *)request
{
MyDocument* document = [[NSDocumentController sharedDocumentController] openUntitledDocumentOfType:@"DocumentType" display:YES];
[[[document webView] mainFrame] loadRequest:request];
return [document webView];
}

新規にドキュメントを作成し、その上に配置された WebView でリクエストを処理させる。


実行してみよう。



出た。

- - - -
JavaScriptを ON にするとエラーが出て強制終了してしまった。
どうも JSを使うブログパーツが原因で何か問題が起きているようだ。

2009年9月21日月曜日

Snow Leopardの Blocksその2: -[NSMutableArray sortUsingComparator:] でEXC_BAD_ACCESS

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

Blocks を試している。
今度は -[NSMutableArray sortUsingComparator:] を使ってソートをしてみる。

 NSMutableArray* array = [NSArray arrayWithObjects:
@"monkey", @"dolphin", @"elephant", nil];
[array sortUsingComparator:^(id obj1, id obj2) {
NSLog(@"%@, %@", obj1, obj2);
return [obj1 compare:obj2];
}];



何故か EXC_BAD_ACCESS が出てしまった。

コンソール出力。
2009-09-21 23:44:14.717 BlocksStudy[1401:80f] monkey, 0
2009-09-21 23:44:14.720 BlocksStudy[1401:80f] dolphin, 1
2009-09-21 23:44:14.721 BlocksStudy[1401:80f] elephant, 2
2009-09-21 23:44:14.722 BlocksStudy[1401:80f] monkey, dolphin
2009-09-21 23:44:14.723 BlocksStudy[1401:80f] monkey, elephant
2009-09-21 23:44:14.724 BlocksStudy[1401:80f] dolphin, elephant
プログラムはシグナルを受信しました:“EXC_BAD_ACCESS”。
sharedlibrary apply-load-rules all


うーむ。使い方がまずいのか?

未解決。

(9/25追記)
NSMutableArray ではなく、NSArray に対してソートをかけていたのが EXC_BAD_ACCESS の原因だった。コメントでの指摘で気がついた。あちゃー。。

以下で問題なく動作しているのが確認できた。

 NSMutableArray* array = [NSMutableArray arrayWithObjects:
@"monkey", @"dolphin", @"elephant",
@"cat", @"dog", nil];

[array sortUsingComparator:^(id obj1, id obj2) {
return [obj1 compare:obj2];
}];

NSLog(@"%@", array);


結果
2009-09-25 23:08:42.986 BlocksStudy[4831:80f] (
cat,
dog,
dolphin,
elephant,
monkey
)


Blocks なかなかいい。

2009年9月20日日曜日

Objective-Cでクロージャ? 〜 Blocks

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

Blocksをちょっと試してみた。

詳しくは ADC に記事がある。
Blocks Programming Topics


NSArray の enumerateObjectsUsingBlock: を使ってみる。

配列内のオブジェクトに対する処理をこんな感じでかける。

NSMutableArray* array = [NSArray arrayWithObjects:
@"cat", @"dog", @"monkey", @"dolphin", @"elephant", nil];

[array enumerateObjectsUsingBlock:
^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(@"%@, %d", obj, idx);
}
];


結果。
 cat, 0
dog, 1
monkey, 2
dolphin, 3
elephant, 4


おおこれは面白い。


Blocks関係の参考情報:
マルチコア時代の新機軸! Snow LeopardのGCD
Objective-C Blocks

2009年9月19日土曜日

Mac OS X SnowLeopard Release Notes - Cocoa Foundation Framework

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

Mac OS X SnowLeopard Release Notes
Cocoa Foundation Framework
をざっと見てみた。

気になる新クラスがいくつか。

NSPurgeableData
NSCache


新しく導入された Blocks 関連のメソッド。NSArray などのコレクション系クラスを中心に Blocks対応のメソッドが追加されている。

New Block-based collection enumeration methods
New Block-based collection searching methods
New Block-based sorting methods

Blocks は面白い。


後日いろいろ試してみよう。

2009年9月18日金曜日

NSScreen座標系 から CGWindow座標系へ

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

先日、Snow Leopard で CGWindowListCreateImage に CGRectInfinite を渡すと固まる場合があるというエントリで CGWindowListCreateImage() では CGRectInfinite を使わず、全スクリーンの実際の範囲を渡した方が良いということを書いた。その範囲は次のようなコードで取得できた。

 NSRect desktopRect = NSZeroRect;
for (NSScreen *screen in [NSScreen screens])
{
desktopRect = NSUnionRect(desktopRect, [screen frame]);
}


ここまではいいのだが、実はこれをCGRectへ変換してそのままCGWindowListCreateImage() へ渡してもうまくいかない。理由は簡単で NSScreen が使っている座標系(Quartz)と CGWindowListCreateImageが使っている座標系(CoreImage)の違うから。メニューバーを含むメインウィンドウの原点位置の取り方の違いもある。

この辺りは以前のエントリ(β版バグ修正 - マルチスクリーン)でメニューバーのあるスクリーンの位置による状態などの検証結果を書いた。


今回 CGWindowListCreateImage() へ渡すべきはすべてのスクリーンを含む範囲(CGRect)で、問題になるのはその開始座標となる。ただ2つの座標系間で問題になるのはY方向のみなので、これを考慮した変換を行えば良い。具体的な例から考えてみよう。

例として3つのスクリーン(ディスプレイ)が存在し、メニューバーを持つスクリーンを挟んで上に残りのスクリーンを配置したとする。

赤い点がそれぞれの座標系における全スクリーン範囲の開始座標(origin)となる。

この例の場合、NSScreenの情報を元に作成した全スクリーンの範囲は次のようになる。
{{0, -854}, {1600, 2478}}

範囲の左下が NSRect.origin 座標となる。

一方、CGWindow系では左上が原点になるので、この座標系における全スクリーンの範囲は次のようにする必要がある。
{{0, -600}, {1600, 2478}}



X座標(*)とサイズに変化はなく、Y軸だけが異なっている。この変換はどうすれば良いのか。
(*) 仮にスクリーンが左右に配置されていても2つの座標系でX座標は一致する。


これは全スクリーンの高さと、原点を位置を決めるメインスクリーン(メニューバーのあるスクリーン)の情報があれば割り出せる。

NSScreenでの全スクリーン範囲:{{sx, sy}, {sw, sh}}
メインスクリーンの範囲: {{mx, my}, {mw, mh}}
CGWindowでの最終座標:{cx, cy}


とすると

cx = sx
cy = mh - (sy + sh)

となる。

メインスクリーンの左上が原点座標になるので、NSScreen上の左上の座標 {0, 1624} を算出した上で、座標系変換とメインスクリーンの位置による補正を行ってやれば良い。
(1) 左上算出 => (sx + sh)
(2) 座標系変換 => - (sx + sh)
(2) メインスクリーン補正 => - (sx + sh) + mh


例の場合、
NSScreenでの全スクリーン範囲:{{0, -854}, {1600, 2478}}
メインスクリーンの範囲: {{0, 0}, {1600, 1024}}

なので
cy = 1024 - (-854 + 2478) = -600

となる。


この数式はスクリーンの数に依存しないので、スクリーンが上下にたくさん増えてもうまく働くと思う。


- - - - -

2つのディスプレイで試したところ、変換前はうまく撮れてなかったのが、変換コードを入れた後はきちんと撮影できた。

2009年9月17日木曜日

WPSU(15) - XCHTTPCookieStorage組み込み

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

WPSU (WebPage Screenshot Utility) の開発に戻る。先日までに開発した XCHTTPCookieStorage を組み込む。

ソースコードをコピーし、webView:resource:willSendRequest:redirectResponse:fromDataSource:dataSource 等のデリゲートを実装するだけ。



実行すると無事にログインできた(クッキーが使えた)。



クッキーのファイル((plist)もちゃんと書き出されている。


これで Safari などと独立してクッキーを管理できる様になった。


ソースコード:WebPageScreenshotUtility-03.zip

2009年9月16日水曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (14) 仕様まとめと最新コード

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

クッキーの扱いに関するルールをまとめておく。
(XCHTTPCookieStorage の仕様に相当する)

1. 基本ルール
 ・クッキーは、ドメイン、パス、名前の3つの組み合わせをキーとして扱う。
 ・クッキーを保存する場合、同一のキーが存在する場合は上書きする。


2. ドメインのルール

(1) 受け入れルール
・gTLD (global Top Level Domain) のみは受け入れる。
・国毎に規定される Effective TLD に該当するものは受け入れる。
 (リンク:Effective TLD Service
 プログラムでは effective_tld_names.dat を使用している。

・上記に加え、NSHTTPCookieAcceptPolicy に従う。
 NSHTTPCookieAcceptPolicyAlways:無条件にクッキーを受け入れる。
 NSHTTPCookieAcceptPolicyNever:無条件にクッキーを受け入れない。
 NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain:
   リクエストURLのドメインと後方一致する場合のみ受け入れる。

クッキードメイン:.xcatsan.com
リクエストURLホスト:
 www.xcatsan.com => OK
 jango.www.xcatsan.com => OK
 xcatsan.com => OK(先頭に . が無い場合も OK)


(2) 送出ルール
・クッキーのドメインがリクエストURLのホスト名と後方一致する場合に送出する。
(OK)クッキードメイン:.xcatsan.com
    リクエストURLホスト:www.xcatsan.com

・クッキーのドメインの先頭に . が付く場合、これを取り除いて一致する場合も送出する。
(OK)クッキードメイン:.www.xcatsan.com
    リクエストURLホスト:www.xcatsan.com

 

3. パスのルール

(1) 送出ルール
 ・クッキーのパスがリクエストURLのパスと前方一致する場合に送出する。
クッキーパス:/a
リクエストURLパス:
 /a/catsan.html => OK
 /a/b/c/mikeneko.html => OK
 /x/a/catsan.html => NG
 /abc/catsan.html => NG


 ・同じドメイン内で同じ名前を持つクッキーが存在する場合は、より詳細な方を返す。
例)クッキーのパスが "/acme" と "/acme/ammo" で同じ nameの Cookieが存在する場合、
 a) リクエストURLのパスが "/acme/ammo/test.html" の場合、後者を返す
 b) リクエストURLのパスが "/acme/parts/some.html" の場合、前者を返す



4. その他のルール

(1) Secure 指定がある場合は、リクエストのURLスキームが https の場合のみ該当するクッキーを送出する。
(2) 有効期限が切れたクッキーは送出しない。またファイルへ書き出さない(既にファイル内に存在する場合は削除する)。
(3) SessionOnly 指定がある場合は、ファイルへ書き出さない。


* ドメインの受け取りルールの適用例
com => NG
.com => NG
xcatsan.com => OK
.xcatsan.com => OK
www.xcatsan.com => OK
jp => NG
.jp => NG
xcatsan.jp => OK
.xcatsan.jp =>OK
co.jp => NG
.co.jp => NG
xcatsan.co.jp =>OK
www.xcatsan.co.jp =>OK
tokyo.jp => NG
.tokyo.jp => NG
chiyoda.tokyo.jp =>NG
metro.tokyo.jp => OK
www.chiyoda.tokyo.jp =>OK



- - - -
さらにソースコードに若干修正をいれた(フォルダ作成のタイミング、ファイル読み込み時のポリシー非適用など)。
最新版を掲載しておく。

ソースコード:CookieStorage-7.zip

2009年9月15日火曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (13)ドメインチェックルール調整

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

GoogleAppsへログインできることが確認できたのだが、また一つ問題に気がついた。

ログインボックスに「ログイン状態を保持する」というのがある。


チェックを入れておくと、次回ブラウザでアクセスした時に自動的にログイン状態になる。Safariではそのような動作になっていた。

ところがサンプルアプリの CookieStorageではこれが動作しない。ログイン状態は恐らくクッキーで管理されていると思われるので、なんらかの理由で CookieStorage がクッキーが受け取れていないか、あるいは送出できていないと予想される。

Safari のクッキーを眺めているとこのクッキーはどうも "HID"という名前を持つものだと分かった。


リクエストのURLと、このクッキーのドメインを照らし合わせてみると次のようになっていた。

クッキードメイン:.www.google.com
リクエストURL:www.google.com


なるほど。このケースは送出OKなのか。今の XCHTTPCookieStorage では NGにしているのでこれでは確かにクッキーが送出されない。Safari が動作し、GoogleAppsがそのような動作を期待しているということはこれが仕様なのだろう。手を入れて対応できるようにしよう。

クッキー送出判断ロジックに手を加えた。

以前のコードはこう。
XCHTTPCookieStorage.m
 int index = [domain_parts count]-1;
NSString* part = [domain_parts objectAtIndex:index];
NSString* check_domain = [NSString stringWithFormat:@".%@", part];
index--;

while (index >=0) {
part = [domain_parts objectAtIndex:index];
if (index) {
check_domain = [NSString stringWithFormat:@".%@%@", part, check_domain];
} else {
check_domain = [NSString stringWithFormat:@"%@%@", part, check_domain];
}
index--;


以前はわざわざ最後のチェックで頭に . をつけないようにしていた。例えば、リクエストURL が www.google.com の場合、送出するクッキーを探すときのドメインは次のようになっていた。
[index= 2] .com
[index= 1] .google.com
[index= 0] www.google.com


新コードはこうなった。
 int index = [domain_parts count]-1;
NSString* part = [domain_parts objectAtIndex:index];
NSString* check_domain = [NSString stringWithFormat:@".%@", part];
index--;

while (index >=-1) {
if (index >= 0) {
part = [domain_parts objectAtIndex:index];
check_domain = [NSString stringWithFormat:@".%@%@", part, check_domain];
} else {
check_domain = [check_domain substringFromIndex:1];
}
index--;


クッキーを探すときに頭に . が付くケースを追加した。
[index= 2] .com
[index= 1] .google.com
[index= 0] .www.google.com
[index=-1] www.google.com




加えて -[setCookies:forURL:mainDocumentURL:] 内の NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain チェックも頭に . が付くケースを追加した。

- (void)setCookies:(NSArray *)cookies forURL:(NSURL *)URL mainDocumentURL:(NSURL *)mainDocumentURL
{
:
(旧)NSString* url_host = [mainDocumentURL host];
(新)NSString* url_host = [NSString stringWithFormat:@".%@", [mainDocumentURL host]];

for (NSHTTPCookie *cookie in cookies) {
if (_cookie_accept_policy ==
NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain &&
![url_host hasSuffix:[cookie domain]]) {
continue;
}
[self setCookie:cookie];
}
}



実行すると GoogleApps で無事にログイン状態が保持された。


ソースコード:CookieStorage-6.zip

2009年9月14日月曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (12)バグ修正他

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

触っていると GoogleApps でのログインに失敗することが判明。
原因はパスチェックにあった。

今までは比較対象のクッキーのパスへ無条件に / をつけていた。

 if (![url_path isEqualToString:cookie_path]) {
if (![url_path hasPrefix:[cookie_path stringByAppendingString:@"/"]]) {
continue;
}
}


この場合
url_path: /a/some.com/sample.gif
cookie_path: /a/some.com

は問題ないのだが

url_path: /a/some.com/LoginAction
cookie_path: /a/some.com/ ※最後に / がつく


のケースはcookie_pathの末尾に / を重ねてしまうので、クッキー送出の対象にならなくなっていた。

そこで末尾についているケースをきちんと対処してやる。

if (![url_path isEqualToString:cookie_path]) {
if (![cookie_path hasPrefix:@"/"]) {
cookie_path = [cookie_path stringByAppendingString:@"/"];
}
if (![url_path hasPrefix:cookie_path]) {
continue;
}
}


そもそも最後に / をつけていたのは単純な後方一致だと
url_path: /a/some.com.gif
cookie_path: /a/som.com

のようなケースも拾ってしまうため。
末尾に / をつけてチェック( cookie_path: /a/some.com/ )することでこういった誤送信を防げる。


修正後、無事に GoogleAppsへログインできた。


#####

なおバグとは別に、クッキーのクリアができた方が便利なので NSHTTPCookieStorage には無い、オリジナルのメソッドを追加しておく。
- (void)clearCookies
{
for (NSMutableDictionary* cookies2 in [_cookies allValues]) {
[cookies2 removeAllObjects];
}
[_cookies removeAllObjects];
_is_modified = YES;
[self _writeCookies:nil];
}


呼び出すと、メモリとファイル両方のクッキーを削除する(ファイル自体は空のまま残す)。

2009年9月13日日曜日

Snow Leopard で CGWindowListCreateImage に CGRectInfinite を渡すと固まる場合がある

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

SimpleCap ユーザの方から Snow Leopard でスクリーンキャプチャを実行した時、タイマーが0になった後に固まってしまうとの報告をうけた。コンソール.app のログをもらったところ下記のエラーが出ているのがわかった。

SimpleCap[1199] : CGImageCreate: invalid image bits/pixel or bytes/row.


自分の所の Snow Leopard では発生しないので何らかの条件が揃うと起きるようだ。

ソースを見たところ特に問題が見つらなかったので、"snow leopard CGWindowListCreateImage" でネットを検索したところ下記の記事が見つかった。

Re: CGWindowListCreateImage fails on Snow Leopard with kCGNullWindowID

CGWindowListCreateImage() は CGRectInfinite を渡すと、自動的にスクリーン全体の大きさを割り出して CGImage を作ってくれる。この情報によると、本来 CGRectInfinite はその名の通りシステム内で無限相当の矩形範囲なのだが、Snow Leopardの 64bit化に伴い 32bitアプリでこの値を使うと「無限相当」ではなく「非常に大きな範囲」として認識されてしまうのが原因とのこと。つまり 32bitでは無限と見なせていた大きな値が、64bit化によってそこまで大きな値ではなくなったということらしい。その結果 CGWindowListCreateImage() はその巨大な矩形範囲の CGImage を生成してしまおうとする。先のエラーの場合、巨大すぎるが為に CGImage の生成に失敗したようだ。

確かに自分のMacではキャプチャで固まることは無いのだが(Leopardの時よりも)長い時間がかかる。この場合は、時間はかかったが運良く(運悪く?)画像の生成に成功していた為に問題が出ていなかったようだ。


対策は先の記事にも掲載してあった通り CGRectInfinite を使わず明示的に範囲指定すること。
(例)
NSRect desktopRect = NSZeroRect;
for (NSScreen *screen in [NSScreen screens])
{
desktopRect = NSUnionRect(desktopRect, [screen frame]);
}



SimpleCap の場合はマルチスクリーンに対応するため、専用のクラス Screen を用意しており、そこから上記と同じ範囲値を取得できる。
(旧)
CGImageRef cgimage = CGWindowListCreateImage(CGRectInfinite,
option,
[_capture_controller windowID],
kCGWindowImageDefault);


(新)
CGRect cgrect = NSRectToCGRect([[Screen defaultScreen] frame]);
CGImageRef cgimage = CGWindowListCreateImage(cgrect,
option,
[_capture_controller windowID],
kCGWindowImageDefault);



32bit->64bit移行期に発生する不具合はまだまだありそうな気もする。


※上記修正を加えた 1.1.0 は今月リリースの予定です。少しお待ちください。


- - - -
(Special Thanks )
今回の件は 感じ通信 さんからの報告で発覚しました。ありがとうございました!

2009年9月12日土曜日

自動変数を初期化しなかったのが原因で EXC_BAD_ACCESS

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

前回までのサンプルを別のMac(MacOS10.5/PowerPC)へ持っていき、ビルドして実行したところエラーを吐いて止まってしまった。

デバッガコンソール

[Session started at 2009-09-12 06:09:45 +0900.]
プログラムをデバッガに読み込み中...
GNU gdb 6.3.50-20050815 (Apple version gdb-962) (Sat Jul 26 08:17:57 UTC 2008)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "powerpc-apple-darwin".プログラムは読み込まれました。
sharedlibrary apply-load-rules all
Attaching to program: `/Users/hashi/Documents/Private/study/CookieStorage/build/Debug/CookieStorage.app/Contents/MacOS/CookieStorage', process 34578.


デバッガを使いトレースしてみると原因がわかった。
原因は単純で自動変数を初期化しなかったこと。

 NSError* error;

NSString* path = [NSString stringWithFormat:@"%@/%@",
[[NSBundle mainBundle] resourcePath], FILENAME];
NSString* contents = [NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding
error:&error];
if (error) {
NSLog(@"%@", error);
_is_loaded_file = NO;
return;
}


エラーが無いにも関わらず 未初期化状態の error を NSLog( ) で表示しようとしてエラーとなった。

Current language:  auto; currently objective-c
プログラムはシグナルを受信しました:“EXC_BAD_ACCESS”。



対処は初期化コードを書くだけ。
 NSError* error = nil;



ちょっと恥ずかしいミスだった。別の MacOSX10.6 では問題なかったのでたまたま nil になっていたようだ(もしくはコンパイルにオプションがある?)。

2009年9月11日金曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (11)完成〜クッキーの保存/読み取りを実装

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

NSHTTPCookieStorage 互換クラスの自前実装の最後。残っていたクッキーのファイルへの保存と読み取りの実装に取りかかる。

まずは書き出しから。plist 形式でクッキーの内容を保存する。
XCHTTPCookieStorage.m

- (void)_writeCookies:(NSTimer*)timer
{
if (!_is_modified) {
return;
}
NSMutableArray* write_cookies = [NSMutableArray array];

for (NSHTTPCookie* cookie in [self cookies]) {
if ([self _isWritableCookie:cookie]) {
[write_cookies addObject:[cookie properties]];
}
}
BOOL result = [write_cookies writeToFile:[self _filepath] atomically:YES];

if (result) {
_is_modified = NO;
} else {
NSLog(@"Failed to write : %@", [self _filepath]);
}

}

_is_modified はメモリ上のクッキーに追加や変更が入った場合に YESになる。-[cookies] でメモリ上に保持しているクッキーを取得し保存対象かどうかを -[_isWritableCookie:] でチェックし、最後に -[NSArray writeToFile:atomically:] で保存する。atomically:YES とすると一旦テンポラリファイルへ書き出した後、目的のファイルへコピー(上書き)する。これは Safari のクッキー調査で見た動作と同じ。

なお単一アプリからの利用しか想定していないので排他制御(ロック)は行っていない。NSTimerからの呼出しも同一スレッドなのでアプリ内でのファイル書き出し競合も通常は起らない。


保存対象かどうかの判断を行うメソッド。
- (BOOL)_isWritableCookie:(NSHTTPCookie*)cookie
{
if ([cookie isSessionOnly]) {
return NO;
}
NSDate* cookie_expires_date = [cookie expiresDate];
NSDate* date = [NSDate date];
if (cookie_expires_date &&
[date compare:cookie_expires_date] == NSOrderedDescending) {
return NO;
}

return YES;
}

セッション限りのもの、期限切れのものは保存しない。


クッキーファイルの保存場所を決定するメソッド。
- (NSString*)_filepath
{
NSString* path = [NSString stringWithFormat:@"%@/%@", [NSSearchPathForDirectoriesInDomains(NSAllLibrariesDirectory, NSUserDomainMask, YES) objectAtIndex:0], @"Cookies"];

NSError* error;
NSFileManager* fm = [NSFileManager defaultManager];
if (![fm isReadableFileAtPath:path]) {
[fm createDirectoryAtPath:path
withIntermediateDirectories:YES
attributes:nil
error:&error
];
if (error) {
NSLog(@"%@", error);
}
}

NSString* identifier = [[NSBundle mainBundle] bundleIdentifier];
NSString* filepath =
[NSString stringWithFormat:@"%@/%@.plist", path, identifier];

return filepath;
}

保存先は ~/Library/Cookies、ファイル名はアプリケーションの Identifier + ".plist" とする。保存先のフォルダが存在しない場合は作成する。

最後に -[_writeCookies:] を NSTimerに登録し、30秒毎に(非同期で)保存する。

#define SYNC_INTERVAL 30
- (id)init
{
if (self = [super init]) {
:
_timer=[[NSTimer scheduledTimerWithTimeInterval:SYNC_INTERVAL
target:self
selector:@selector(_writeCookies:)
userInfo:nil repeats:YES] retain];
:
}


これで書き出しの実装ができた。

次は読み込み。これは既存のメソッドの組み合わせで簡単にできる。
- (void)_readCookies
{
NSArray* read_cookies =
[NSArray arrayWithContentsOfFile:[self _filepath]];

if (read_cookies) {
for (NSDictionary* properties in read_cookies) {
[self setCookie:[NSHTTPCookie cookieWithProperties:properties]];
}
}
}



さて実行してみよう。

クッキーを送出するサイトを開き少し(30秒)待つとファイルが生成された。


中身をみるとクッキーが書き出されているようだ。


試しにログイン状態でアプリケーションを一旦終了し、再起動してみる。

ページを開くとログイン状態が保たれていた。ファイルへ保存したクッキーがメモリへ読み込まれきちんと送出されている。


最後にアプリ終了時にクッキーを保存するため NSApplicationWillTerminateNotification の受取を登録しておく。

- (id)init
{
if (self = [super init]) {
:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_willTerminate:)
name:NSApplicationWillTerminateNotification
object:nil];
:
}


アプリ終了直前に NSApplicationWillTerminateNotification がポストされたら下記のメソッドを呼出してクッキーを保存する。
- (void)_willTerminate:(NSNotification*)notification
{
[self _writeCookies:nil];
}




ソースコード:CookieStorage-5.zip

- - - -
ようやくクッキーのハンドリングが一段落できた(長かった)。

2009年9月10日木曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (10)クッキー受取ポリシーチェックの実装

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

永続化の前に NSHTTPCookieAcceptPolicy への対応を行っておく。

ヘッダファイル NSHTTPCookieStorage.h によれば3種類の値が存在する。

NSHTTPCookieStorage.h

/*!
@enum NSHTTPCookieAcceptPolicy
@abstract Values for the different cookie accept policies
@constant NSHTTPCookieAcceptPolicyAlways Accept all cookies
@constant NSHTTPCookieAcceptPolicyNever Reject all cookies
@constant NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain Accept cookies
only from the main document domain
*/
enum {
NSHTTPCookieAcceptPolicyAlways,
NSHTTPCookieAcceptPolicyNever,
NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain
};
typedef NSUInteger NSHTTPCookieAcceptPolicy;


Safari(4.0)のセキュリティ設定を見ると3選択があるのでこれらが対応しているのだろう。



ポリシー適用には URLが必要になるので setCookies:forURL:mainDocumentURL: でチェックを行うことにする。前回のコードにポリシー適用の条件を加えた。

XCHTTPCookieStorage.m
- (void)setCookies:(NSArray *)cookies forURL:(NSURL *)URL mainDocumentURL:(NSURL *)mainDocumentURL
{
if (_cookie_accept_policy == NSHTTPCookieAcceptPolicyNever) {
return;
}

NSString* url_host = [mainDocumentURL host];

for (NSHTTPCookie *cookie in cookies) {
if (_cookie_accept_policy ==
NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain &&
![url_host hasSuffix:[cookie domain]]) {
continue;
}
[self setCookie:cookie];
}
}


NSHTTPCookieAcceptPolicyNever は無条件に拒否、NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain の場合はドメインチェックを行うようにした。
また setCookie: にも NSHTTPCookieAcceptPolicyNever の場合の拒否ロジックを加えておいた。


デフォルト値は Safariに合わせて NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain にしておいた。

2009年9月9日水曜日

XCode3.2で App Delegate が用意されるようになった

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

XCode3.2 から Cocoaアプリケーションテンプレートを使って新規にプロジェクトを作成すると App Delegate が用意されるようになった。

以下は BlocksStudy というプロジェクトを作成した時の様子。BlocksStudyAppDelegate というクラスが自動的に用意された。


BlocksStudyAppDelegate.h

@interface BlocksStudyAppDelegate : NSObject  {
NSWindow *window;
}

@property (assign) IBOutlet NSWindow *window;

@end


NSWindow のアウトレットがプロパティとして用意されている。


BlocksStudyAppDelegate.m
@implementation BlocksStudyAppDelegate

@synthesize window;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
}

@end


applicationDidFinishLaunching: のひな形が用意されている。


InterfaceBuilder にもちゃんとインスタンスが登録されていて、File'sOwner(NSApplicationインスタンス)の delegate先に指定されている。



iPhone のテンプレートに近くなったようだ。

- - - -
検証目的なら今まで作っていた AppCotnroller は不要になるな。

2009年9月8日火曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (9)クッキー受取のドメインチェック強化

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

XCEffectiveTLDNames を使いドメインチェックを強化する。
といっても3行を加えるだけ。
NSHTTPCookieStorage.m

- (void)setCookie:(NSHTTPCookie *)cookie
{
if ([[XCEffectiveTLDNames sharedEffectiveTLDNames] isEffectiveTLDName:[cookie domain]]) {
return;
}
  :


また、クッキー送出もとのサーバのドメインがクッキーで設定されているドメインと一致するかをチェックする。これはリクエストURLが必要なので -[setCookies:forURL:mainDocumentURL:] へ書いてみた。
- (void)setCookies:(NSArray *)cookies forURL:(NSURL *)URL mainDocumentURL:(NSURL *)mainDocumentURL
{
NSString* url_host = [URL host];

for (NSHTTPCookie *cookie in cookies) {
if ([url_host hasSuffix:[cookie domain]]) {
[self setCookie:cookie];
}
}
}


クッキーのドメインがリクエストURLホストの後方一致となるかをチェックする。
(例)リクエストURLのホスト:www.xcatsan.com
  a) クッキードメインが .xcatsan.com の場合:OK
  b) クッキードメインが www.xcatsan.com の場合:OK
  c) クッキードメインが .mail.xcatsan.com の場合:NG



動作確認は、Googleのログインで成功した。大丈夫そうだ。


サンプル:CookieStorage-4.zip


- - - -
クッキー受取、送出が大体実装できた。残りは永続化の部分だな。

2009年9月7日月曜日

NSHTTPCookieのドメインに . が付く、付かない?

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

NSHTTPCookie の挙動で一つ気がついたことがある。

domain="www.xcatsan.com" のクッキーをサーバから送り、それを NSHTTPCookie へ渡すと domain=".www.xcatsan.com" となる( . ドットが頭につく)。

<NSHTTPCookie version:0 name:@"testname" value:@"testvalue"
expiresDate:@"(null)" created:@"273711615.645900" sessionOnly:TRUE
domain:@".www.xcatsan.com" path:@"/system" secure:FALSE
comment:@"(null)" commentURL:@"(null)" portList:(null)>,


そういう仕様かと思いきや他のサイトのクッキーを眺めていると頭に . が付かないものがあった。
<NSHTTPCookie version:0 name:@"GoogleAccountsLocale_session" value:@"ja"
expiresDate:@"(null)" created:@"273711677.501391" sessionOnly:TRUE
domain:@"www.google.co.jp" path:@"/accounts" secure:FALSE
comment:@"(null)" commentURL:@"(null)" portList:(null)>,


この差はなんだろうか。気になって調べてみた。
 :
 :

原因は単純でサーバ側がクッキーをセットする時に domain指定しているか、していないかで . の有無が決まる。

  • Set-Cookie: domain指定なし => 頭に.が付かない(例)"www.xcatsan.com"

  • Set-Cookie: domain指定あり => 頭に.が付く(例)".www.xcatsan.com"



分かってみれば単純だった。前者は送出元のサーバに限定されたクッキー、後者は指定ドメインに一致するクッキーとなる。

2009年9月6日日曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (8)クッキー受取のロジックを改良する

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

Mozzila が提供する effective_tld_names.dat を使う実装を行う。

effective_tld_names.dat を扱う専用のクラス "XCEffectiveTLDnames" を用意する。

XCEffectiveTLDnames.h

@interface XCEffectiveTLDNames : NSObject {

NSMutableSet* _domain_set;
NSMutableSet* _wildcard_set;
NSMutableSet* _exception_set;

BOOL _is_loaded_file;
}

+ (XCEffectiveTLDNames*)sharedEffectiveTLDNames;
- (BOOL)isEffectiveTLDName:(NSString*)domain;

@end



  • _domain_set は、jp や co.jp などを格納する。

  • _wildcard_set は、ワイルドカード指定されているドメイン(*.tokyo.jp など)を格納する(※実際に格納するのは "tokyo.jp")

  • _exception_set は、! で始まる除外指定のドメイン(!metro.tokyo.jpなど)を格納する(※実際に格納するのは "metro.tokyo.jp")




ファイルはダウンロードして XCodeプロジェクト内の Resources へ入れておく。エンコーディングは仕様により UTF-8と規定されいている。



XCEffectiveTLDNames はシングルトンとする。
XCEffectiveTLDNames.m
+ (XCEffectiveTLDNames*)sharedEffectiveTLDNames
{
static XCEffectiveTLDNames* _shared = nil;

if (!_shared) {
_shared = [[XCEffectiveTLDNames alloc] init];
[_shared _loadFile];
}
return _shared;
}


初期化時にファイルの読み込みを行う。
- (void)_loadFile
{
NSError* error;

NSString* path = [NSString stringWithFormat:@"%@/%@",
[[NSBundle mainBundle] resourcePath], FILENAME];
NSString* contents = [NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding
error:&error];
if (error) {
NSLog(@"%@", error);
_is_loaded_file = NO;
return;
}

for (NSString* line in [contents componentsSeparatedByString:@"\n"]) {
if ([line length] == 0) {
continue;
}
if ([line hasPrefix:@"//"]) {
continue;
}
if ([line hasPrefix:@"*."]) {
if ([line length] > 2) {
[_wildcard_set addObject:[line substringFromIndex:2]];
// *.tokyo.jp => tokyo.jp
}
continue;
}
if ([line hasPrefix:@"!"]) {
if ([line length] > 1) {
[_exception_set addObject:[line substringFromIndex:1]];
// !metoro.tokyo.jp => metoro.tokyo.jp
}
continue;
}
[_domain_set addObject:line];
}
_is_loaded_file = YES;

}

ちょっと大味な実装な気もするが Cocoaでファイル読み込みを扱ってみるとこんな感じになった。コメントや空行を取り除き、ワイルドカード、除外、ドメインをそれぞれ適切な NSMutableSet へ格納する。

これらを使い、クッキーで受け取れるドメインのチェックを行う。
- (BOOL)isEffectiveTLDName:(NSString*)domain
{
if ([domain hasPrefix:@"."] && [domain length] > 1) {
domain = [domain substringFromIndex:1];
}

if ([_domain_set containsObject:domain]) {
return YES;
}

if ([_exception_set containsObject:domain]) {
return NO;
}

if ([_wildcard_set containsObject:domain]) {
return YES;
}

NSRange range = [domain rangeOfString:@"."];
if (range.location == NSNotFound || (range.location+1) >= [domain length]) {
return NO;
}
NSString* check_domain = [domain substringFromIndex:range.location+1];

if ([_wildcard_set containsObject:check_domain]) {
return YES;
}

return NO;
}


最初に _domain_set, _exception_set, _wildcar_set に一致するかどうかチェックを行う。その後、ワイルドカード用にチェックを行う。Effective TLD の場合に YES を返す。実際にクッキーを受け取るかどうかはこの戻り値が NO の場合となる。

テストコードを書いて動作を確認してみよう。
- (void)_checkself
{
NSString* domain;

domain = @"com";
NSLog(@"%@: %d", domain, [self isEffectiveTLDName:domain]);

domain = @".com";
NSLog(@"%@: %d", domain, [self isEffectiveTLDName:domain]);
:
:


結果はこう(1=YES, 0=NO)。
com: 1
.com: 1
xcatsan.com: 0
.xcatsan.com: 0
www.xcatsan.com: 0
jp: 1
.jp: 1
xcatsan.jp: 0
.xcatsan.jp: 0
co.jp: 1
.co.jp: 1
xcatsan.co.jp: 0
www.xcatsan.co.jp: 0
tokyo.jp: 1
.tokyo.jp: 1
chiyoda.tokyo.jp: 1
metro.tokyo.jp: 0
www.chiyoda.tokyo.jp: 0


TLDはもちろん、ワイルドカードと除外指定もよさそうだ。
このクラスを XCHTTPCookieStorage で使おう。

なお何らかの理由でファイルの読み込みに失敗した場合は -[XCEffectiveTLDNames isEffectiveTLDName] の戻り値は常に NO が帰るのでセキュリティレベルが低下する(当たり前だが)。

- - - -
effective_tld_names.dat のサイズは 58KB程度ある。大したサイズではないが iPhone/iPod touch で実行する場合はどなんだろうか。メンテナンス性は落ちるが NSMutableSet の内容をそのまま plist へ落としておいてそれを使い回すと実行時間とメモリ効率は良くなる。

2009年9月5日土曜日

クッキーのドメイン問題への対処 ... Mozzila Effective TLD Service

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

クッキーのドメインにまつわる問題として Cookie Monster がある。

この件について調査しているのだが決定的な方法はなくブラウザの対応状況もまちまちなようだ。
クッキーについて、ドメインの指定で、...
(上記ページに参考になるリンクがいくつかある)

この問題に取り組んでいる人も居た。
ワイルド過ぎる realm のワイルドカードを何とかしたい - 前編


情報を集めていくうちに Mozzilaが Effective TLD Service なるものを提供していることがわかった。ここでは TLD だけでなく SLD(Second Level Domain) が汎用(co.jpなど)かどうかが判断できるファイルを提供している。

mxr.mozilla.org/mozilla-central/source/netwerk/dns/src/effective_tld_names.dat


jp のあたりを少し引用してみる。

// jp : http://en.wikipedia.org/wiki/.jp
// http://jprs.co.jp/en/jpdomain.html
// Submitted by registry 2008-06-11
jp
// jp organizational type names
ac.jp
ad.jp
co.jp
ed.jp
go.jp
gr.jp
lg.jp
ne.jp
or.jp
// jp geographic type names
// http://jprs.jp/doc/rule/saisoku-1.html
*.aichi.jp
*.akita.jp
*.aomori.jp
*.chiba.jp
:
:
!metro.tokyo.jp
!pref.aichi.jp
!pref.akita.jp
!pref.aomori.jp
:
:


co.jp などのSLDの他、 *.tokyo.jp などの地域型ドメインが列挙されている。
! は、ワイルドカードの除外パターンを表す。

なお前掲のサイトの検証によれば漏れている gLTD もあるようだ。

仮に利用するとしたら、クッキーにこのファイルに掲載されているドメインが指定されている場合は受け取らない、という使い方ができる。
(例)
jp => NG
.jp => NG
co.jp => NG
.co.jp => NG
xcatsan.co.jp => OK
xcatsan.jp => OK
.xcatsan.jp => OK
xcatsan.aichi.jp => NG (*.aichi.jp の指定による)
pref.aichi.jp => OK (!pref.aichi.jp の指定による)


- - - -
ライセンスは MPL Version 1.1 (Mozilla Public License) が使える。これをプログラムに埋め込んで使う方向で考えよう。

2009年9月4日金曜日

クッキーでドメイン=.co.jp を送ってみたら..

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

サーバに下記のような PHPプログラムをおいて自前実装(XCHTTPCookieStorage)でクッキー受取りの挙動を確認してみた。

<?php
setcookie("testname", "testvalue", 0, "/", ".co.jp");
?>
<html>
hello
</html>


上記 PHPへアクセスすると .co.jp のクッキーが送られてくる。


実行して見ると見事に?受け取れていた。
2009-09-04 05:17:22.750 CookieStorage[3498:80f] (
<NSHTTPCookie version:0 name:@"testname" value:@"testvalue"
expiresDate:@"(null)" created:@"273626241.352053"
sessionOnly:TRUE domain:@".co.jp" path:@"/"
secure:FALSE comment:@"(null)" commentURL:@"(null)" portList:(null)>
)


しかもリクエストURLは xxxx.co.jp ですらない(xxxx.jp というサイト)。つまりクッキー送出元のドメインのチェックも行われていない。試しにまったく無関係なサイト(例えば .yahoo.co.jp)を送出しても受け取っていた。

NSHTTPCookie がそこまできめ細やかなチェックはやらないか。クラスの役割を考えると妥当と言えば妥当だ。
やはりクッキーのドメインチェックは自分でやる必要がありそうだ。

チェックは受取時にすべきだろう。そうすれば送出しないし、ファイルなどへの保存もしない。

主なチェック項目はこんな感じ。
・送出元サーバのドメインとのチェック(無関係なドメインのクッキーは受け取らない)
・gTLDや .の数、その他のチェック

しかし .co.jp と.xcatsan.jp の区別をどうするか。最低限日本のドメインはいいとして海外はどうする?

2009年9月3日木曜日

クッキー(Cookie)のドメインについて

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

クッキーを受け取る、送る時の重要な条件の一つとしてドメインチェックがある。

例えば、http://www1.xcatsan.co.jp/ から Domain=www1.xcatsan.co.jp のクッキーを受け取った後、このサイトへアクセスする時にはこのクッキーを送る。またhttp://www2.xcatsan.co.jp/ へアクセスする時にはこのクッキーは送らない。

一方、Domain=.xcatsan.co.jp が送られてきた場合には、http://www1.xcatsan.co.jp/, http://www2.xcatsan.co.jp/ 共にこのクッキーを送る。

問題なのは Domain=.co.jp といったクッキーが送られてきた場合。このようなクッキーをもしブラウザが受け取ってしまうと、先の2つのサイトはもちろん http://www.yahoo.co.jp/ や http://www.apple.co.jp/ といった .co.jp を持つすべてのサイトに対してこのクッキーを送ってしまう。

これはセキュリティの問題と認識されていて様々なサイトでも取り上げられている。

(ややふるい情報ばかりだが)



ドメインのルールについては以前紹介した [Studyng HTTP] HTTP Cookies
DOMAIN_NAME に指定できる文字列は、トップレベルドメインが 
"com", "edu", "net", "org", "gov", "mil", "int" の場合はピリオドが2つ以上、
それ以外の場合はピリオドが3つ以上含まれていなければいけません。
従って例えば、domain=jp 等と指定する事はできません。

となっていた。

これを具体的な例に落とすと
.com => NG(.が1個)
xcatsan.com => NG(.が1個)
.xcatsan.com => OK(.が2個)
.co.jp => NG(非gTLDで .が2個)
xcatsan.co.jp => NG(非gTLDで .が2個)
.xcatsan.co.jp => OK(非gTLDで .が3個)
.xcatsan.jp => NG(非gTLDで .が2個)

となる。

ただ最近は xcatsan.jp のようなドメインも存在する。
実際 Safari4のクッキー(Cookies.plist)を見ると、.xcatsan.jp のような非gTLDでかつ . が2個のケースも許容していた。

となると .co.jp (NG) と .xcatsan.jp (OK) の区別は gTLDと . の数だけの判断だけではできなくて、TLD別に細かく判断していく必要がある(JPドメインの場合、co や ne といった定義済みドメインなのか、そうでないか、といった判断)。


うーむ。これを自前で実装するのは面倒だし、国毎のドメイン事情が変わった場合などの後々のメンテナンスも難しい。できれば ルール適用のロジックは Foudation Framework側(すなわち NSHTTPCookie)にまかせたい。

現在の自前実装では、クッキーの受取は基本的に NSHTTPCookie まかせになっている。 そこでもしここで Safari4と同等のルール(すなわち、.co.jp は NGで、.xcatsan.jp は OK)を適用しているならそれにまかせて送出時には特になにもしない(今のままの)実装としよう。


この辺りの挙動を調べてみよう。
(そもそも Safari4の挙動はどうなっているのか?)

2009年9月2日水曜日

NSHTTPCookieStorage相当のクラスを自前で実装する (7)クッキー送出のロジックを改良〜実装

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

ざっくりとした実装ができた。

仕様は以前紹介した通り。

Cocoaの日々
NSHTTPCookieStorage相当のクラスを自前で実装する (6)クッキー送出のロジックを改良


サンプル:CookieStorage-3.zip


まずはクッキー取得から。
XCHTTPCookieStorage.m

- (void)setCookie:(NSHTTPCookie *)cookie
{
NSString* key1 = [cookie domain];
NSString* key2 = [NSString stringWithFormat:@"%@/%@", [cookie path], [cookie name]];

NSMutableDictionary* cookies2 = [_cookies valueForKey:key1];
if (!cookies2) {
cookies2 = [NSMutableDictionary dictionary];
[_cookies setValue:cookies2 forKey:key1];
}
[cookies2 setValue:cookie forKey:key2];
}


ドメインをキーにして辞書を作成する。この辞書には path/name をキーとしてクッキーを格納する。


次に取得。変更がはいったポイントのみ解説する。
- (NSArray *)cookiesForURL:(NSURL *)URL
{
NSMutableArray* return_cookies = [NSMutableArray array];
NSString* url_path = [URL path];
NSString* url_domain = [URL host];
NSDate* date = [NSDate date];
BOOL is_secure = [[URL scheme] isEqualToString:@"https"];

NSArray* domain_parts = [url_domain componentsSeparatedByString:@"."];
int count = [domain_parts count];
if (count < 1) {
return return_cookies;
}

int index = [domain_parts count]-1;
NSString* part = [domain_parts objectAtIndex:index];
NSString* check_domain = [NSString stringWithFormat:@".%@", part];
index--;

while (index >=0) {
part = [domain_parts objectAtIndex:index];
if (index) {
check_domain = [NSString stringWithFormat:@".%@%@", part, check_domain];
} else {
check_domain = [NSString stringWithFormat:@"%@%@", part, check_domain];
}
index--;

NSDictionary* cookies2 = [_cookies valueForKey:check_domain];
if (!cookies2) {
continue;
}

for (NSHTTPCookie* cookie in [cookies2 allValues]) {
:
(以下、ドメインを除くチェックが続く)


渡されたリクエストURLを .(ドット)で分解し、これを順番に組み立ててチェックしていく。
(イメージ)mail.google.co.jp の場合:

.co.jp
.google.co.jp
mail.google.co.jp


最後の完全一致を除き、先頭に .(ドット)をつけて検索する。なお以前の仕様では検索順序が(詳細 >> 汎用)だったが、これを逆(汎用 >> 詳細)にした。この辺りの仕様はちょっとからないのだが、異なるドメインで同じ path+name が存在した時にはパスと同じように詳細な方を優先させた方が良いと判断してこのような順序とした。


実行してみよう。


ログインできた(クッキー取得と送出がうまくいっている、と思われる)。


- - - -
なお今のままではドメインが .co.jp を持つクッキーもマッチしてしまう。汎用ドメイン(.xcatsan.comなど)を想定してドメインの単語が2つ以上を許しているが .co.jp などはどの組織にも属さない情報となるのでセキュリティ上問題となる。ここでちゃんとチェックするか、あるいはクッキー保存時にそのようなドメインを持つクッキーを受け付けない様にする必要がある(あるいは両方)。

2009年9月1日火曜日

Webページをアクセスしている場合でも webView:resource:willSendRequest:redirectResponse:fromDataSource: のresponseにNSHTTPURLResponse以外が渡ってくることがある

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

WebViewを使ってGoogleMapsを見ていると WebResourceLoadDelegate のメソッドで例外が出ていた。




2009-09-01 12:53:21.225 CookieStorage[23839:10b]
<NSURLResponse: 0x6a680c0>, about:blank
2009-09-01 12:53:21.246 CookieStorage[23839:10b]
*** -[NSURLResponse allHeaderFields]: unrecognized selector
sent to instance 0x6a680c0
2009-09-01 12:53:21.266 CookieStorage[23839:10b]
*** WebKit discarded an uncaught exception in the
webView:resource:didReceiveResponse:fromDataSource:
delegate:
<NSInvalidArgumentException> *** -[NSURLResponse allHeaderFields]:
unrecognized selector sent to instance 0x6a680c0


プログラムではデリゲートメソッド webView:resource:willSendRequest:redirectResponse:fromDataSource:の responseを NSHTTPURLResponse でキャストしてHTTPヘッダ情報などにアクセスしている。
[(NSHTTPURLResponse*)response allHeaderFields]


ところが例外が出ているケースでは URLがどうも "about:blank" のようなものになっており、この場合は型が NSURLResponse になっているのが分かった。存在しないメソッドを呼び出していたので例外が起きていた。もしかすると以前のクラッシュはこれが原因かもしれない。


対応は responseの型をチェックし、NSHTTPURLResponse の場合のみHTTPヘッダ情報へアクセスするようにした。
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
:
}


- - - -
考えてみるとWebブラウジングしていても、リンクが http:// 以外のケース(ftp:// や feed:// など)もあるので当然想定すべきだったか。