2008-10-27

hunchentootでREST

hunchentootでログイン処理 というエントリでセッションを使ったログインのコードを作って得意げになっていたわけだが、さらに勉強を進めてみると時代は今 RESTful であるらしい。
そして RESTful な設計ではセッションプげらであるらしい。
この REST なるものがこれからの(もうすでに?)トレンドのようなのだ。

RESTアーキテクチャはHTTPの原点に戻りステートレスな構造なのでキャッシュや分散処理など昨今の多数のユーザを抱えるWebアプリのスケーラビリティとの相性の良さから俄然注目されているんだそうな。それにステートレスだとブラウザでブックマークが楽だ。これって結構重要なのではないか?
ステートレスでどうやってセッション持つんだよう、って思うかもしれないけど、目から鱗の発想の転換がそこにはあった。URLの遷移がすなわちステート遷移なんである。まそれは置いといて。

RESTではリソースとURLを直接に対応させる。そしてHTTPのプロトコルそのものが既に持っているコマンドでリソースの取得、更新、削除などを指示する。
そのURLはこんな感じになる。
http://example.com/item/10

普通にブラウザのアドレスバーに入れればid:10のitemを取得できる。idのところが可変なんですな。

これをhunchentootで実装してみよう。まさにこのための関数が用意されていた。

(create-regex-dispatcher "/item/(\\d+)?" 'item)


ところがこの関数せっかく正規表現でマッチしてるのにマッチしたグループの情報(括弧でくくった\d+のマッチ)をあとから取れないのである。なぜならitem関数のところにわたせるのは無引数関数だけだから。

こんなんじゃないといけない。
(defun item ()
...)

こういうふうにしたい。
(defun item (idstr)
...)


いや、引数で渡されなくても関数内でリクエスト文字列ともう一度マッチとれば必要な情報は得られるけど無駄だし使い勝手も悪い。
というわけで、create-regex-dispatcherのマッチグループを引数で受けられるバージョンを作ってみた。

(defun create-regex-dispatcher/var (regex page-function)
(let ((scanner (cl-ppcre:create-scanner regex)))
(lambda (request)
(multiple-value-bind (match regs)
(cl-ppcre:scan-to-strings scanner (script-name request))
(when match
(lambda ()
(apply page-function (coerce regs 'list))))))))

あとは、
(create-regex-dispatcher/var "/item/(\\d+)?" 'item)


hunchentootのデフォルトのディスパッチャが無引数関数を要求してるので、それにあわせるために無引数クロージャを作って返してるだけである。

hunchentootでRESTっていうかその準備みたいになったけど、まあ簡単に拡張できるよって事で。

2008-10-26

hunchentootでログイン処理(2) マクロ化

で、前回のログイン処理は土曜日いっぱいを使ってすごい苦労してできていた。
いま流行のRailsとか使えば5分でできちゃうような事なのであろう。
いやいや僕の場合だとRailsでも5時間はかかるぞきっと。

さて、うまく書けたログイン処理関数(login)だがまだ重大な欠陥がある。
(login)関数の最初の10行ぐらいが大事なとこ、というのは前も書いたけどまさにそれが欠陥だ。
ログインのロジックそのものは同じであっても、アプリごとにログインページのデザインは違うだろう。作るサイトによって趣味とかいろいろあるわけだし。

だから、ログイン処理と表示するページを分離しよう。そうすればログイン処理の部分だけライブラリ化してページ自体は自分で毎回定義できる。

マクロである。

(login)関数のログイン処理固有の部分をマクロとして抜き出してみた。
こんなコードになる。

;; name は定義する関数の名前。
;; auth-p はパスワード認証する関数の名前。2引数 name, pass を取る。
;; body はHTMLを生成する式でなければならない。
;; そのHTMLページはGET, POSTの両方で呼ばれ得る。
;; GETパラメタとしてfromを取りログイン処理後にそのURLにリダイレクトする。
;; POSTパラメタとしてfrom,name,pass,cancelの3つを取る。
;; セッションが有効なときに呼ばれた場合はログアウト処理をする。
;; cancelが渡された場合はすぐにfromへリダイレクトする。
;; name,passが(auth-p name pass)でTを返すならば (session-value 'user)に
;; ユーザ名を設定してセッションを作る。
;; auth-pに失敗した場合には同じページを再表示する。
(defmacro define-login-handler (name auth-p &body body)
(let ((=user= (gensym))
(=pass= (gensym)))
`(defun ,name ()
(when *session*
(remove-session *session*)
(redirect-safe (get-parameter "from")))
(let ((,=user= (post-parameter "name"))
(,=pass= (post-parameter "pass")))
(cond ((post-parameter "cancel")
(redirect-safe (post-parameter "from")))
((,auth-p ,=user= ,=pass=)
(setf (session-value 'user) ,=user=)
(redirect-safe (post-parameter "from"))))
,@body))))

新しく作ったこの(define-login-handler)がコメントでいろいろ書いたようなログイン処理に関わる面倒事をよきに処理してくれるわけだ。このマクロにはページ生成に関わる処理はなにも書かれていない。
新しく作ったといっても実際には元の(login)の最初の10行ぐらいをコピペしてきて、バッククォートで囲んで展開したいところを「,」or「,@」でプレフィックスしたぐらいだ。あとはletで導入した変数がbody部の変数使用に悪影響を与えないようgensymしたくらいか。
マクロといってもHTMLテンプレ言語を使える知識があれば難しくないと思う。

さっそくこれを使って(login)関数を書き直してみる。

(define-login-handler login auth-valid-p
(no-cache)
(setf (content-type) "text/html; charset=utf-8"
(reply-external-format) *utf-8*)
(with-html
(:html
(:head
(:title "ログイン処理"))
(:body
(:p (if *session*
(fmt "User: ~A" (session-value 'user))
(fmt "Not Login.")))
(:p (:form :method :post
"Login: "
(:input :type :text
:name "name"
:value (or (session-value 'user) ""))
:br
"Password: "
(:input :type :password
:name "pass"
:value "")
(:input :type :hidden
:name "from"
:value (get-parameter "from"))
:br
(:input :type :submit
:value "Login")
:br
(:input :type :submit
:name "cancel"
:value "Cancel")))))))

これを実行すると(login)という関数がつくられる。auth-valid-pは前回の関数そのまま。

どうでしょう。見事にページ生成の処理だけが残った。今後は新しいログインページを作る場合にもログイン処理に頭を悩ませる必要は無くなった。GET,POSTのパラメタ名といういくつかの約束を守ったページをデザインするだけで、ログイン処理そのものはマクロが書いてくれるわけだ。

hunchentootでログイン処理

OS XでのLisp処理系としてSBCLを使っていたんだけど、これはmacppcではスレッドが使えない。

いや、別に使いたいわけじゃないんだけど使いたいアプリがスレッドの実装を要求するのでしょうがないから代替となるLisp処理系を探すことになる。

で、Clozure CLを使っている。インストールして使えるようにするところまでの記録を書こうと思う。いつか。

今日はその使いたいアプリhunchentootでのログイン処理について。

hunchentootはLispで書かれたWebアプリサーバだ。Apacheは基本的にドキュメントルート配下のファイルをブラウザに送信するだけで、動的なコンテンツはCGIに依頼したりで自分では何もできない。
しかし、hunchentootはコンテンツの生成を全部Lispのプログラムでやる。プログラムはもちろん自分で書く。プログラムはhunchentootの一部として動作するのでCGIと違ってWebサーバが内部に持っているほとんどどんな情報でもその気になれば使えるわけだ。すごい。

しかも、SLIMEから関数定義を更新すれば(C-c C-c)ただちにWebサーバの動作に反映される。再起動とかリロードとかまったく不要。神の領域。違う?

このhenchentootの上に構築されたWebアプリフレームワークとしてWeblocksというものがある。あるんだけどその斬新な機構に自分の学習曲線が追いつかずよくわからんので、日本語で解説本が出るまで待つ事にしよう。
それまでは素のhunchentootを使うことにする。すなわちブラウザのリクエスト(GETとかPOSTとか)のパラメタを関数の引数とみなして手書きで作ったHTMLをだらっと返す、古き良きCGI的なモデルでWebアプリをつくる。CGIよりちょっと良くなる。このくらいの学習曲線でないとついていけない。

本題。

なにはともあれWebアプリではセッション+認証が不可欠だろうからそこからトライしてみた。
セッションについてはhunchentootが完全に面倒をみてくれるのでそれを利用して、認証つまりログイン処理を書いてみた。

ログイン処理の基本についてはこちらのサイトがとても参考になった。処理に手落ちがある場合の具体的な攻撃手順も解説されてるので、何が本質的な問題なのか納得できました。

で、レベル3を目指して書いてみたのが以下。参考になればうれしいです。


* 日本語リテラルが文字化けせず表示されることの確認。けっこう苦労するんで。
* login.html はログインページを表示する。すでにログインしていればログアウトする。
* login.html は処理後にリダイレクトして元いたページへ戻る。ログアウト時も同様。
* リダイレクトのURLをチェックして変なところには飛ばない。
* HTMLはさすがに手書きではなくCL-WHOを使って楽する。LispプログラムとHTMLテンプレート言語のシームレスな融合の美しさをご覧あれ。
* パスワードは Login: foo Password: bar です。
* (login)関数の最初の10行ぐらいが大事なとこ。(redirect-safe)と(auth-valid-p)もチェック。あとはこれらを動作させるためのサンプルにすぎない。


(defvar *this-file* (load-time-value
(or #.*compile-file-pathname* *load-pathname*)))

(defmacro with-html (&body body)
`(with-html-output-to-string (*standard-output* nil :prologue t)
,@body))

(defun menu-link ()
(with-html-output (*standard-output*)
(:p (:hr
(:a :href "/hunchentoot/test" "Back to menu")
(htm (:a :href (format nil "/hunchentoot/login.html?from=~A" (url-encode (request-uri)))
(str (if *session* " | Logout" " | Login"))))))))

(defparameter *headline*
(load-time-value
(format nil "Hunchentoot (see file ~A)"
(merge-pathnames (make-pathname :type "lisp") *this-file*))))

(defvar *utf-8* (flex:make-external-format :utf-8 :eol-style :lf))

(defvar *utf-8-file* (merge-pathnames "UTF-8-demo.html" *this-file*)
"Demo file stolen from .")

(defun auth-valid-p (user pass)
(and (equal user "foo")
(equal pass "bar")))

(defun redirect-safe (url)
(let ((url (url-decode url)))
(cond ((and (< 0 (length url))
(eql #\/ (aref url 0)))
(redirect url))
(t
(redirect "/hunchentoot/test")))))

(defun logout ()
(if *session*
(remove-session *session*))
(redirect-safe (get-parameter "from")))

(defun login ()
(if *session* (logout))
(let ((user (post-parameter "name"))
(pass (post-parameter "pass")))
(cond ((post-parameter "cancel")
(redirect-safe (post-parameter "from")))
((auth-valid-p user pass)
(setf (session-value 'user) user)
(redirect-safe (post-parameter "from"))))

(no-cache)
(setf (content-type) "text/html; charset=utf-8"
(reply-external-format) *utf-8*)
(with-html
(:html
(:head
(:title "ログイン処理"))
(:body
(:p (if *session*
(fmt "User: ~A" (session-value 'user))
(fmt "Not Login.")))
(:p (:form :method :post
"Login: "
(:input :type :text
:name "name"
:value (or (session-value 'user) user ""))
:br
"Password: "
(:input :type :password
:name "pass"
:value "")
(:input :type :hidden
:name "from"
:value (get-parameter "from"))
:br
(:input :type :submit
:value "Login")
:br
(:input :type :submit
:name "cancel"
:value "Cancel"))))))))

(defun menu ()
(setf (content-type) "text/html; charset=utf-8"
(reply-external-format) *utf-8*)
(with-html
(:html
(:head
(:link :rel "shortcut icon"
:href "/hunchentoot/test/favicon.ico" :type "image/x-icon")
(:title "Hunchentoot テスト menu ©"))
(:body
(:h2 (str *headline*))
(:p (str (script-name)))
(:p (:a :href "/hunchentoot/test/menu.html" "menu page"))
(menu-link)
))))

(setq *dispatch-table*
(nconc
(list 'dispatch-easy-handlers)
(mapcar (lambda (args)
(apply #'create-prefix-dispatcher args))
'(("/hunchentoot/test/form-test.html" form-test)
("/hunchentoot/test/menu.html" menu)
("/hunchentoot/test" menu)
("/hunchentoot/login.html" login)))
(list #'default-dispatcher)))

2008-10-16

MacでIPv6匿名アドレスを起動時に有効にする

/etc/sysctl.conf に以下のように書くとよい。(OS X 10.4で確認)

net.inet6.ip6.use_tempaddr=1


Mac OS XはFreeBSDの情報がそのまま通用する事が多いので、とりあえずFreeBSDのまねしてみるとうまくいったりする。

でもこれデフォルトでオンにしとくべき話だよね。アップル。

そういやあipfwもデフォルトで通過する設定だな。ここも全ルールを消去したらデフォルトで遮断するようにすべきではないんかい。

IPv6なサーバをたてる

IPv6リーチャブルになったので頑張ってサーバをたててみたのだよ。

しかしサーバを立ててもIPアドレスでしかアクセスできないとなると、
こんな感じ(http://[2001:5c0:a0b7::1]/)になってしまう。結局IPv4の時と同じようにDynDNSの仕組みによってお手軽にホスト名を登録できなきゃ実質使えないと気づいた。
その後、IPv6対応のDynDNSサービス(無料)を見つけたので、このエントリを書き直し。

http://dns6.org/ このサイトで提供してくれているDynDNSサービスはAAAAレコードも登録できるので、IPv6なサーバで使える!
だた、ポリシーとか利用規約らしき文章がさっぱり見つからないいたってシンプルなサイトの作り、大丈夫でしょうかね?

http://hjort.dns6.org/ 俺サーバを立ててみた。まだコンテンツは無い。

ふと思ったんだけど、IPv6は自動設定がデフォルトで付いてくるので手軽で便利なんだから、もっと上位のレイヤDNSのあたりまでも自動化を進めてほしかった。技術的な難しさはともかく。自動設定してくれると僕のような素人には内容はわからんけど多分正しいんだろうと確信できる。やはりありがたいよ。手動設定だと間違った設定だけどなんとなく動くこれでいいのかなあ、という状態になりやすいし。一旦それでも動いちまうとめんどくさいからいいや、という心理は当然働くしそしてあれなノードがインターネットにあふれかえる事になる。

2008-10-14

OS X用トンネルドライバ

go6.netでIPv6トンネルを掘る説明で忘れていたことがあった。

トンネルドライバ(tunデバイス)を使えるようにする必要がある。
OS X用TUNドライバ


OpenVPNを使うためにも必要で、以前インストールしていたのをすっかり忘れていた。

2008-10-10

Mac OS XでIPv6を使う。今すぐ

(追記:以下は2011年9月現在はもう古い情報です。参考にならないでしょう)

できるなら使おうではないか。しかもタダだ。びた一文余計に払う必要はない。
やらないなんてもったいない。
ではやってみます。

1) まずは http://go6.net/ に行く。IPv6トンネルを提供してくれるサービスをやっている。
2) 上のメニューエリアからApplications -> Downloads とクリック
3) OS X用のバイナリは無いのでGateway6 5.1 Source Codeを選んでダウンロードする。
4) ダウンロードしたファイルを展開する。
% tar xfz ~/Downloads/gw6c-5_1-RELEASE-src.tar.gz

5) 展開されて出てきたディレクトリに移動する。
% cd tspc-advanced

6) パッチする。でないと途中でこける。
tspc-advanced/include/md5.h の#ifndef _SYS_MD5_H_の行の前に一行追加する。
こんな感じ。
#include <stdint.h>  // <- この行をいれる

#ifndef _SYS_MD5_H_
#define _SYS_MD5_H_
/* MD5 context. */

7) コンパイルする。OS Xの開発環境(X Code)をインストールしてないとgccが無くて失敗するかも。
X Codeのインストールは簡単なのでやる。
% make target=darwin

8) binディレクトリに実行バイナリと設定ファイルが完成している。
設定ファイルはサンプルなのでコピーして、さっそく実行しよう。
サンプルのままだと匿名でトンネルを作るのでとりあえず試すにはいい。
% cd bin
% cp gw6c.conf.sample gw6c.conf
% sudo ./gw6c -f gw6c.conf

9) ping6が通るか試してみる。
% ping6 www.kame.net
% ping6 ipv6.2ch.net

できた?おめでとう!IPv6アドレスをゲットできたわけだ。
10) ユーザ登録をするとさらに固定アドレスをもらえる。
さらにルータモードで動作させると/48プレフィクスのグローバルアドレスをもらえるので、家庭内LANの全マシンにアドレスを配ることができる。

だめだった場合:
- UDP 3653 で外に接続にいける必要がある。
- root権限がないといけない。
- 設定ファイルでログ取得モードにしてどんなエラーが出てるのか調べてみる。

11) さらに進む。
- go6.net へユーザ登録する。
- ルータモードで動作させる。
- LANの別のマシンに自動でIPv6アドレスが設定されるのを見てにやにやする。
- http://ipv6.2ch.net/ のスレに書き込む。
といった事ができた。やってみようではないか。

12) ユーザ登録する。
そんな難しくないと思う。
一点だけ。登録するパスワードは他のシステムでも使ってるような大事なパスワードをいれてはいけない。登録完了の通知メールにパスワードが平文で書かれて返送されてくるので。
完了したらgw6c.confの以下の変数を書き換える。
userid=あなたのID
passwd=あなたのパスワード
server=broker.freenet6.net
auth_method=any


13) システム環境設定からネットワーク、詳細...のダイアログをだす。
そしてIPv6を「切」にする。ここ重要。

ルータモードで動作させるばあいには切にしないとうまく動作しなかった。
自分で自分のルータ広告を受け取ってしまうのかルーティングテーブルが変になるのだ。
理屈はわからんけどとにかく切でうまくいく。

14) gw6c.conf書き換え
ルータモードにするには以下の設定値を右辺のように書き換える。
host_type=router
if_prefix=en0

ルータモードは匿名では使えない。ユーザ登録が必要。

うまくいくと、これまでtun0だけについていたIPv6グローバルアドレスのほかにen0にもIPv6アドレスがつくはず。
そしてen0側のLANに接続されている全マシンにIPv6グローバルアドレスが設定されているはずだ。それらのマシンからもIPv6で外に出て行けるか確かめよう。

15) と言ったかんじでIPv6なネットワークはいとも簡単に使えるのだった。
固定グローバルアドレスを好きなだけ使えるというのは、新しい時代のインターネットの基本的人権となるであろう。うむ。
末端クライアントには半人前のアドレスしか無い暗黒時代それは終わり夜明けは近い。たぶん。
でもってP2P接続が簡単になりネットゲームが誰でも簡単に楽しめるようになってくれると嬉しい。広域NATとかまじひどい冗談だ。PS3でポート開放とかでググってみればごく一般のユーザの悲鳴が聞こえてくる。ネットはもっとシンプルで簡単じゃないといけない。

ここでは説明してないけど、固定アドレスであるがゆえにそしてNATが無いのでパケットフィルタのルールを書くのもとても簡単になった。匿名アドレス宛をはじくのがどうすりゃいいかわからんけど。

2008-10-02

PS3 COD4 のポート開放について調べてみた

COD4でのネット対戦については一旦満足したものの、なぜか通信が調子悪い時がある。
プレイヤー一覧が出るときにアンテナ強度みたいなのがでるよね。あれが4本たってることが多いんだけどたまーにやっぱり2本とかになってしまう。

なぜだ。ググってポート3074,3075,3095は開放してある。
ほんとに開放されてんのか?
調べようではないか。新しく手に入れたNetScreenはsyslogで詳細な通信ログが取れるのだ。
活用しない手はない。

で、調べてみた。わかったこと:
1) 3074 中から外へTCPしてる。そのデストポート
2) 3075 外から中へUDPしてる。ただしソースポート
3) 3095 使ってなさそう
追加4)いざ対戦を開始してみるとUDP:3074に接続しにきてた。これは開放の必要あり。

がーん。ググって出てきた情報どれもこれも違ってんじゃん。
1)は中から外だからポート開放とは関係なく許可済み。
3)はひととおり対戦してみたけど使う気配なし。
問題の2)が開放すべきポートの情報なんだけど、外側のソースポートが3075固定で、内側のデスティネーションポートは2216, 2951, 1450, 1594, 1636, 1666, 1718, 1731, .....とどうもランダムぽい。
4)は開放の必要あり。

1)2)は最初にCOD4をオンラインにした時に通信が発生しているので、多分これを開放しないとホストになれないのじゃないかと思う。ここでポート情報とかをマッチングサーバに登録するんだろな。
3)は保留。無視していいんじゃない?
4)は開放必須ぽい。

ううむ。デストポート側が固定になってくれてないとこれではポート開放の指定ができない。
範囲指定してどばっと開放しようにも持ってるNetScreenはグレード低い安もんだから64個しかポート開放できんのだよ。
困った。
あきらめました。

つうかゲームの開発者よ、開放すべきポートは固定にしてくれよ。あほか。
ソースポート側固定したって意味ねーんだよ。

ちなみに、PS3標準のP2P用ポートはUDP:3658で間違いなさそう。
トロステーションでちゃんとネット対戦できた。
その他のポートを開く必要はない。
そう、こういうふうに開くべきポートを固定にしてくれればいいんだよ。やればできるだろう。


PS3でCOD4ネット対戦やろうという人のためのまとめ:
1)開放するべきポートはUDP:3074。さらにこれ以外のランダムなポートで接続しにきます。
2)UDP:1024-49151まで広い範囲をすべてPS3に転送しましょう。お使いのルータがそうできるなら。(49152-65535の範囲は動的ポートで予約されてるので多分開く必要ないです。)
3)それができないルータはもうあきらめてPS3をDMZに置くかUPnP対応ルータ使うかしかない。
4)ポート開放しなくても対戦は一応できる。快適さは入った部屋しだい。たぶんホストにはなれない。フレンド呼んで対戦とかができないのだと思います。