2012/12/22

iPhone, Android用のProxyをたててみる - 3

Hiroaki's blog: iPhone, Android用のProxyをたててみる - 2の続き。Privoxyの設定編。

configの設定
Privoxyの設定はconfigファイルで行う。コメントを除いた設定ファイルは、こんな感じ。
confdir /usr/local/etc/privoxy
logdir /var/log/privoxy
actionsfile match-all.action # Actions that are applied to all sites and maybe overruled later on.
actionsfile user.action      # User customizations
filterfile default.filter
filterfile user.filter      # User customizations
logfile logfile
debug      1 # Log the destination for each request Privoxy let through.
debug   1024 # Log the destination for requests Privoxy didn't let through, and the reason why.
debug   4096 # Startup banner and warnings
listen-address  :8118
toggle  1
enable-remote-toggle  0
enable-remote-http-toggle  0
enable-edit-actions 0
enforce-blocks 0
buffer-limit 4096
forward /       localhost:8080
forward :443    .
forwarded-connect-retries  0
accept-intercepted-requests 1
allow-cgi-request-crunching 0
split-large-forms 0
keep-alive-timeout 50
connection-sharing 1
socket-timeout 300

enable-compression 1
compression-level 9
actionsfile、filterfileは後ろで説明することにして、重要なのは、toggle 1。これで、Privoxyによるコンテンツの書き換えを有効にする。

actionsfile
「どんな操作を行うか」を定義したのが、actionsfile。柔軟に設定できるようになっているが、それがかえって難しく感じさせている気がする。というわけで、ここではよく使いそうな2つのパターンを説明する。
{+block{理由}}
適用させるドメイン or URLのpath
 :
ブロック。リクエストを中継しない。アクセスを禁止させたいドメインなどの指定に使う。
{+filter{フィルタ名}}
適用させるドメイン or URLのpath
 :
コンテンツを書き換えるときに使う。例えば、iOSではフラッシュを見ることができないため、コンテンツに埋め込まれているフラッシュを除去するときに使う。

filterfile
actionsfileで指定するfilterを定義する。コンテンツの書き換え方を記述する。

match-all.actionは、すべてのURLに対して適用するactionを書く。そうでなくてもよいのだが、ファイル名と内容を合わせておいたほうがよいだろう。これには手を加えずに、user.actionに記述するほうがよいかも。
default.filterは、あらかじめ定義されているフィルタ。フィルタは、定義しただけでは適用されないため、ここで読み込んでおいても問題ないだろう。
自分の設定は、user.actionuser.filterに記載する。広告除去 - Privoxy Wikiなどが参考になる。

ここまでやれば、自分の興味のないページへのアクセスや、端末の関係で見れないデータをダウンロードしてくることなど、無駄な処理をずいぶん省けると思う。データ転送量での速度制限が当たり前のように行われているので、こうやって自衛するのも有効なはず。


2012/12/15

iPhone, Android用のProxyをたててみる - 2

Hiroaki's blog: iPhone, Android用のProxyをたててみる - 1の続き。今回は、Apache Web Server編。

今回の概要
  • Apache Web ServerでCache Serverの設定を行う。
  • 画像圧縮のscriptを用意する。
  • Privoxyの上位Proxyとして、Apache Web Serverを指定する。

Apache Web ServerでCache Serverの設定
Apache HTTP サーバ バージョン 2.2 ドキュメント - Apache HTTP サーバを読む。以上。
なんだけど、画像圧縮とかやるとなると、サンプルが欲しくなる。
<IfModule !mod_proxy.c>
LoadModule proxy_module libexec/apache22/mod_proxy.so
LoadModule proxy_http_module libexec/apache22/mod_proxy_http.so
LoadModule proxy_connect_module libexec/apache22/mod_proxy_connect.so
LoadModule proxy_ftp_module libexec/apache22/mod_proxy_ftp.so
</IfModule>
<IfModule !mod_deflate.c>
LoadModule deflate_module libexec/apache22/mod_deflate.so
</IfModule>
<IfModule !mod_ext_filter.c>
LoadModule ext_filter_module libexec/apache22/mod_ext_filter.so
</IfModule>

<IfModule mod_ext_filter.c>
ExtFilterDefine gif-filter \
    mode=output intype=image/gif \
    cmd="/usr/local/libexec/airHproxy/gif-filter.sh"
ExtFilterDefine jpeg-filter \
    mode=output intype=image/jpeg \
    cmd="/usr/local/libexec/airHproxy/jpeg-filter.sh"
ExtFilterDefine png-filter \
    mode=output intype=image/png \
    cmd="/usr/local/libexec/airHproxy/png-filter.sh"
<IfDefine DEBUG>
ExtFilterDefine debug-before \
    mode=output \
    cmd="/usr/bin/tee /tmp/filter.before"
ExtFilterDefine debug-after \
    mode=output ftype=21 \
    cmd="/usr/bin/tee /tmp/filter.after"
</IfDefine>
</IfModule>

<IfModule !mod_cache.c>
LoadModule cache_module libexec/apache22/mod_cache.so
<IfModule !mod_mem_cache.c>
LoadModule disk_mem_module libexec/apache22/mod_mem_cache.so
</IfModule>
</IfModule>

<IfModule mod_cache.c>
CacheMaxExpire 108000
CacheLastModifiedFactor 0.1
CacheDefaultExpire 3600
CacheIgnoreNoLastMod On
<IfModule mod_disk_cache.c>
CacheRoot "/var/cache/apache22"
CacheDirLevels 5
CacheDirLength 3
</IfModule>
<IfModule mod_mem_cache.c>
CacheEnable mem /
MCacheSize 512000
</IfModule>
</IfModule>
Listen *:8080
<VirtualHost *:8080>
    ServerName proxy.example.jp
    DocumentRoot /var/empty
    CustomLog /var/log/httpd/httpd-proxy-access.log combined
    ErrorLog /var/log/httpd/httpd-proxy-error.log
    #DeflateFilterNote Ratio ratio
    #LogFormat "%h %l %u %t \"%r\" %>s %b/%{ratio}n%%" common_deflate
    #CustomLog /var/log/httpd/proxy_access.log common_deflate

    <Location />
        Deny from all
    </Location>

    ProxyRequests On
    ProxyVia On
    #ProxyRemote * http://127.0.0.1:3128
    NoProxy www.example.jp
    <Proxy *>
        Order deny,allow
        Deny from all
        Allow from 127.0.0.1

        AddDefaultCharset Off

        <IfModule mod_ext_filter.c>
        <IfDefine !DEBUG>
        SetOutputFilter html-filter;gif-filter;png-filter;jpeg-filter
        </IfDefine>
        <IfDefine DEBUG>
        SetOutputFilter debug-before;html-filter;gif-filter;png-filter;jpeg-filter;DEFLATE;debug-after
        ExtFilterOptions DebugLevel=9 LogStderr
        </IfDefine>
        </IfModule>
    </Proxy>
</VirtualHost>

これを/etc/httpd/conf.d/proxy.conf として保存(CentOS 5なので)。設定を反映させる方法は略。
CloudCoreだとメモリ2GのVPSなので、キャッシュはメモリに取る設定にしてみた。gif、jpeg、pngについては、外部フィルタを呼び出す形。設定を晒してみて思ったけど、deflateの設定はいらなさそうだなぁ。

画像圧縮のscript
Air H" を使っていたときに用意していた設定を流用。どこかのサイトにあったのを持ってきたものだったはずなのだが、原典は不明。

/usr/local/libexec/airHproxy/gif-filter.sh
#!/bin/sh
PATH=/usr/local/bin:/usr/bin:/bin
export PATH

cd `dirname $0`
. ./common-settings
# -resize WxH> とすると WxH のサイズからはみでる大きさの画像だけ、
# そのサイズに収まるように縮小する。範囲内ならサイズ変更しない。

convert +adjoin gif:- gif:- | convert $opt -colors 32 +dither -colorspace Transparent +profile "*" gif:- gif:-

で、common-settingsの内容は
#
case "$HTTP_USER_AGENT" in
*AH-K3001V*) opt="-resize 240x320>";;
*iPod*)
*Darwin*)
        opt="-resize 640x960> -unsharp 0"
        ;;
*SC-02B*)
*FlipboardProxy*)
        opt="-resize 480x800> -unsharp 0"
        ;;
esac
jpegやpngも同じ~と思ったら、画像フォーマットによってconvertのオプションが微妙に異なる。

/usr/local/libexec/airHproxy/jpeg-filter.sh
#!/bin/sh
PATH=/usr/local/bin:/usr/bin:/bin
export PATH

cd `dirname $0`
. ./common-settings

exec convert $opt -quality 85 +profile "*" jpeg:- jpeg:-
/usr/local/libexec/airHproxy/png-filter.sh
#!/bin/sh
PATH=/usr/local/bin:/usr/bin:/bin
export PATH

cd `dirname $0`
. ./common-settings

exec convert $opt -colors 32 +dither -colorspace Transparent -quality 99 +profile "*" png:- png:-


Privoxyの設定
上位のproxyを設定するのは、configファイル中のforward。
forward /       localhost:8080
forward :443    .
のようにする。SSLはproxyを通す必要がないので、.を指定する。configのコメントにもあるように、アクセス先によって使用するproxyを変えたり、proxyを使わなかったりということができる。

これで、端末側の設定がいらない、画像圧縮を行うproxyができあがり。でも、せっかくPrivoxyを使っているので、次回は広告を除去してみる。


2012/12/08

iPhone, Android用のProxyをたててみる - 1

iPod Touch(iPhone/iPad)とAndroid向けに、Proxy Serverを立てて、広告の除去や画像の圧縮をやるようにしてみた。

透過Proxyにする

Android 2.xも対象にする。これはProxyの設定がないので、設定なしで使える透過Proxyとする。
透過Proxyを通すため、VPNを必須とする。つまり、VPN接続したとき、透過Proxyを通るようにする。
CentOS 5で構築してみる。透過ProxyにはPrivoxy を使う。画像圧縮には、The Apache HTTP Server Projectのmod_ext_filterを使って、ImageMagickを呼び出す。

Privoxyのインストールには注意が必要

VPNについては、Hiroaki's blog: Amazon EC2にVPN Serverをたてる(L2TP編)などを参照、ということで、省略。

Privoxyはrpmで入れてOK。と思ったら、古いバージョンだと透過Proxyの機能がない。設定ファイルに

accept-intercepted-requests 1
と書いて起動できなかったら、バージョンが古いor透過Proxyを無効にしてコンパイルされている。この場合、一度rpmで、実行時に必要となるライブラリもろともインストール。/etc/rc.d/init.d/privoxy を適当な場所にコピーしてから、アンインストールし、最新版をソースから入れ直す。
./configure && make && make installという標準パターンでOKなのだが、透過Proxyを使いたいのと、どうせなら圧縮もさせたいので、
./configure --enable-accept-filter --enable-compression --with-user=privoxy --with-group=privoxy
とする。詳しくは、./configure --help を。

installが終わったら、コピーしておいた /etc/rc.d/init.d/privoxy を元に戻して、中身を確認。インストール先が /usr/local になっているので、PATHやP_CONF_FILEの値を修正しておく。そうそう、ユーザーとグループも、それぞれprivoxyというのが必要(というか、./configureの時に--with-user=と--with-group=で指定した奴が必要)なので、確認してなければ作っておく(詳細は略)。

Privoxyの設定

まずは、透過Proxyが動くようにする。apacheでの画像圧縮は、その次。

Privoxyの設定ファイルはいくつかあるが、/usr/local/etc/privoxy/config が親玉。非常に長いので圧倒されてしまうが、コメント行を取っ払ってしまうと、20行ちょっと。取り合えず動かすために

  • toggle 1 → toggle 0
  • #debug 1 と #debug 4096 と #debug 8192 の行の先頭の#を取る
  • accept-intercepted-requests 0 → accept-intercepted-requests 1
と直したら、service privoxy start とやって起動させてみる。ログは/var/log/privoxy/logfileに出力されるので、ユーザprivoxyで覗いてみる。ちゃんと起動できていればPort 8118でListenしているはずなので、netstatで確認。/var/run/privoxy.pid が書けなくても起動しないので、ディレクトリのアクセス権限にも注意。

透過Proxyの設定

あと一息。
透過Proxyにするためには、本来まっすぐ進むはずのパケットを無理やり回り道させるような設定が必要になるので、iptableに設定を追加する。

iptables -A PREROUTING -s 192.0.2.0/24 -p tcp --dport 80 -j REDIRECT --to-ports 8118
iptables -A POSTROUTING -o eth0 -s 192.0.2.0/24 -j MASQUERADE
のような設定。192.0.2.0は、VPNでiPhone/Androidに割り当てるIPアドレス、eth0は、透過Proxyを動かしているマシンからパケットが出て行くときのInterfaceなので、使用環境に合わせて変更すること。

これで、画像圧縮はしないけど、透過Proxyとして動作する。iPhone/AndroidからVPN接続して、いくつかサイトを見てみて、 /var/log/privoxy/logfile にアクセス先が出力されていれば、OK。



2012/11/29

10分で理解するApache Web Serverのログに関する小ネタ

その1 ログにhttpでアクセスできるようにする

Hiroaki's blog: IISのアクセスログにhttpでアクセスするのApache版。
  • ログディレクトリを公開する
  • ディレクトリ、ファイルのアクセス権を調整する
の2つをやれば、OK。
多くの場合、ログは /var/log/httpd に書き出されているので、設定はこんな感じ
Alias /log/ "/var/log/httpd/"
<Directory "/var/log/httpd/">
    Order deny,allow
    Deny from all
    Allow from アクセスを許可するネットワーク・ホスト
</Directory>
たとえば、これをlog.confという名前でファイルに保存して、CentOSなら /etc/httpd/conf.d、FreeBSDなら /usr/local/etc/apache22/Includes に置いておけば、Apache起動時に読み込んでくれる。ディレクトリの一覧は不要でしょ?
/var/log/httpd のアクセス権って、所有者(root)しか許可されていないので、chmod og+r+x とかして緩めてあげないと、403エラーになっちゃう。俗に言うシェルアカウントを発行しているようなサーバでは、緩めて問題ないか、よーくよーく考えること。

アクセスは、http://サーバ名/log/ログファイル名 とすればOK。
パスワードのないsshのキーを作って専用アカウントに設定して…なんてしなくても、これなら手軽にアクセスログを取得できるでしょ?

その2 /server-status へのアクセスをログに残さない

MuninでApacheを監視すると、定期的に /server-status にアクセスする。のだが、チリも積もれば山となる。1,2ヶ月様子を見ても変なアクセスがないようであれば、ログに残さなくていいよね?ってことで、特定のURLへのアクセスをログに載せない設定。ちゃーんとマニュアルのログファイルのところに書いてあるんだが、ぜんぜん気がつかなかったよ。
SetEnvIf Request_URI "^/server-status" dontlog
CustomLog /var/log/httpd-access.log combined env=!dontlog
SetEnvIfを使っているから、「特定のサーバからのアクセスはログに残さない」なんてのも可。



2012/11/17

iPod Touchともろもろ購入

今更ながら、iPod Touch購入。最新ではなく、1個前の第4世代、しかも、工場修理品って奴。おもしろそう!ってアプリがiPhone/iPod Touch用だったりして試せないのは残念だし、初代iPadは居間に置いていて引き上げられないし。ぷららモバイルの契約があと1年残っているので、iPhoneも選択肢から除外。

で、中国から届くのを待っている間、「やっぱり液晶保護フィルムいるよね?」というわけで、ググって評判のよさそうな【送料無料】レイ・アウト 4th iPod touch用防指紋光沢保護フィルム RT-T4F/CR 【jan 457129798...を購入。
iPod Touchが届いて裏を見たら、ピッカピカに磨いてあった。「傷、目立つよね?」というわけで、あわてて今度はケースをググったり友達に訊いたりして情報収集。候補をアマゾンで見ていたら、もうちょっとCPのよい商品が紹介されていた。ので、それ、amix iPod touch4専用ハードケース Protection fix shell for iPod touch4 (クリア) に決定。充電ケーブルも一緒に購入すると割引ということだったので、まとめて購入。

ケースの梱包が不安って声があったけど、ケーブルと一緒に買ったためか、透明の汎用のプラスチックケースに並べられて梱包されていた。保護フィルムは2枚入っていたから、汚れてきたら張替えだな。



2012/11/10

WifiをチェックするAndroidアプリを作った

しばらく放置になっていたAndroidアプリを、形にした。
chkConnectは、定期的にWeb Pageにアクセスできるかどうかをチェックするアプリ。アクセスできなければWifiを切断する。再接続はAndroidまかせ。これで、アンテナから離れた席に座ってしまったから、いちいち手動でWifiをoffにするなんて手間ともおさらば。

間隔を設定するとはいえ、ちょくちょくWeb Pageにアクセスするから、電池はそれなりに喰ってしまう。一応、Wifiがoffになっているのを検知すればチェックはやめるし、画面が消えていたときの間隔も別に設定できるので、自分は便利に使えているのだが。
というわけで、Google Playで公開する前に人柱募集モードで公開。


Appleはリスクを背負って作っている

Appleはリスクを背負っているから、妥協せずに細部までこだわるし、それがユーザの満足に繋がっているのだ。
アップルのデザイン ジョブズは“究極”をどう生み出したのかは、対サムソンの部分はApple贔屓の、サムソンの印象が悪くなるような言葉の選択があるものの、どうしてAppleにはできて日本の電機メーカーにはできないかの理由(の一部)が明らかにされていると思う。

iPhoneやiPadを始めとする(物理的な)製品へのこだわり。Apple自らが製造機器を購入し、加工工場に貸し出すというのは驚いた。が、確かに、このスタイルでなければ製品に対する想いを型にはできない。加工工場がボトルネックになってしまうからだ。想いを具現化するために妥協はしない。だから、製品を購入したユーザーも満足し、次の製品により大きな期待をするのだろう。

自分が欲しい物を妥協せずに作る。だから、ボタンを押した感触や、操作感も妥協せず、製品として、サービスとして、統一されている。デザインは見てくれではなく、統一されたUX(ユーザー体験)だと考えているからこそ、デザイン重視といっても薄っぺらい製品にはならないのだ。
そして、「自分=開発者」ではなく、「自分=製品を使うユーザー」であるから、誰が使うんだ?というような製品にはならない。そんな、ターゲット不在の製品を作っている日本のメーカー。現場の問題ではなく、そんな製品企画にGoを出す経営陣が問題だろう。経営陣こそボーナスの一部を自社製品の現物支給とし、家族の容赦無い批判を受けて、ユーザーの目線を学ぶべきではないのか?

「いいものが売れる」なんてのは、作り手の思い上がりなのだ。現実はもっと簡単で、「欲しがるものが売れる」のだ。だから、小耳に挟んで、お店で見てみて触ってみて、買って帰って箱から出して、使ってみる、といった一連の行動に対して、「欲しかったのはこれでしょ?」とアピールすることが重要なのだ。「そうそう、これが欲しかったんだよ」と満足できたものが、その人にとっての「いいもの」であり、「いいもの」とはそういう意味なのだ。そして、「いいもの」を伝えるために、Appleはユーザーが製品を買うお店の「デザイン」や、箱から出すときの演出までこだわった。だから、製品がこれだけ熱狂的に受け入れられているのだろう。

最後に、購入したのは第一版第一刷だけど、ちょっと校正がという部分があった。全く同じ文章が二箇所にあったのだけど、もう直っているのかな?

【第1章】ジョブズにとってデザインとは何か?
 アップルのデザイン活性型経営
 コラム1 ジョブズだからできた究極のシンプル

【第2章】分解して分かるアップルデザインの真髄
 分解・解剖!iPhone4S の内部構造
 アルミニウムの使いこなし方に驚く!
 樹脂にも独自の工夫を加えるアップル

【第3章】触れてうっとり、インターフェースの秘密
 ジョブズ自身が未来のユーザー役に
 識者に聞くアップルのインターフェース
  (1)長谷川踏太「広告以上にブランドメッセージを伝えている」
  (2)増井俊之「開発現場にも届く、強いリーダーシップを」

【第4章】アップルストアに挑んだ日本人デザイナー
 感動を共有するスペースデザイン
 コラム2:デザインのためなら流通でも戦う
 コラム3:量販店的な売り方に満足しないなら

【第5章】アップルの広告・グラフィックデザイン
 ジョブズのセンスが光るもう1つの歴史
 コラム4:時代を経ても変わらぬアップルの広告
 ショートインタビュー 猪子寿之「ネットワーク中心の社会を見据えた思想」
 コラム5:750 枚のタートルネック・シャツ

【第6章】革命の始まりはiMacだった
 インタビュー ジョナサン・アイブin 1999
 ショートインタビュー 藤崎圭一郎「体験を変えたアップルのデザイン」

【第7章】アップルが争っても守りたいデザイン
 GALAXY は果たして模倣なのか?
 サムスンの逆襲!
 第1ラウンドは痛み分けの結果に
 こんなにある!ジョブズ名義のデザイン特許
 ショートインタビュー 山中俊治「モノが実現するユートピア」

【第8章】ジョブズが夢見た未来のデザイン
 アップルTVとリモコンさえあれば…
 ドッキングステーションにこだわる理由
 ユニバーサルドックも変形する
 ジェスチャー入力でボタン不要の世界へ
 スタイラスペン入力もあり!
 ショートインタビュー 坂井直樹「iCar は生まれるか?」
 コラム6:iCloudとSiriに見るアップルの夢


2012/10/28

DVDイメージを一括mount

DVDのコピーが違法行為なので、意味がないけど。
DVDイメージコピーをDLNAサーバに突っ込んでおくと、いちいちDVDを入れ替えなくてもDLNA機器のリモコン操作だけで次々見れるので、快適。というわけで、DVDイメージを一括でmount/unmountするscript。FreeBSD用。むかーしむかーしにイメージを作っていたという人にしか役に立たないけど。
ネットで視聴なら、こんなことしなくてもリモコン操作だけでラクラクだけどね。

一括マウント。マウント先は/media。DVD毎にフォルダを分けてイメージを作成したと想定。

#!/usr/local/bin/bash
FILES=$(/usr/bin/locate ISO | /usr/bin/grep '^/int.*ISO$')

for isofile in $FILES
do
  devnum=$(/sbin/mdconfig -a -t vnode -f "${isofile}")
  dir=$(/usr/bin/dirname ${isofile}|/usr/bin/sed 's/\.ISO$//')
  name=$(/usr/bin/basename $dir)
  /bin/mkdir /media/$name > /dev/null 2>&1
  /sbin/mount -r -t cd9660 /dev/$devnum /media/$name
done

一括アンマウントは

#!/usr/local/bin/bash
DEVS=$(/bin/ls /dev/md* |/usr/bin/grep -v ctl)

for target in $DEVS
do
  num=$(/bin/echo $target|/usr/bin/sed 's/^.*md//')
  /sbin/umount $target
  /sbin/mdconfig -d -u $num
done
/bin/rmdir /media/*

スクリプトは可読性を高めるため、bash用の拡張構文を使用。たいしたことはやっていないので、/bin/sh の範囲でも書けるけど、仕事でメンテナンス性の高いコードを書けるよう、普段から練習。


クックブックは電子書籍の方が便利だと思う。O'Reilly Japan - bashクックブック



2012/10/27

メジャーアップデートしたMunin

Muninがアップデートして、2.0になった。FreeBSDのportsにも反映されたので、アップデート。
Node(監視される側)は一部1.4.6のままだけど、情報は問題なく取得できている。
何より嬉しいのは、ちゃんとIPv6に対応していること。munin.conf に address を書くとき、[]で囲むようにすればOK。というわけで、v6パッチのお役目終了。


2012/10/21

Software Design 11月号

第2特集はMunin。仕事でもプライベートでも、サーバのリソース監視で絶賛活躍中。監視対象となるリソースを接続or動作させている状態でMuninをインストールすると、リソース監視の設定をある程度やってくれる。というか、多くの場合は、それでOK。監視のための設定コストを低く始められるので、取り敢えず始めて、見たいものが出てきたら順次追加、というパターンでOK。死活監視としての利用はちょっと微妙なので、Monitという別のソフトを利用している。

そういえば、日本Muninユーザ会というのができたんだそうだ。
Munin.jp - Munin User Group Japan



2012/10/13

最近買った本

ガベージコレクションのアルゴリズムと実装 まぁ、マニアックといえばマニアックかもしれないけど。どんな仕組みで動いているのか知っていて損はないし、そういった奥行きが足腰の強さに現れてくるのではないか?


リバースエンジニアリング ―Pythonによるバイナリ解析技法 (Art Of Reversing)こっちもまぁマニアックだけど、上とはまたちょっと違ったレイヤー。どんな動きをしているのか、出来上がっているものを調べる側。




2012/09/30

FreeBSDのVPN ServerでIPv6をバラまく

Hiroaki's blog: LinuxのVPN ServerでIPv6をバラまくと同じことをFreeBSDでやってみる。

L2TP/IPsecを使うので、Kernelのパラメータを修正してbuild。これはIPsecを使おうとしたときには毎度おなじみの話なので、ここでは省略。詳しくは、[FreeBSD] L2TP/IPsecサーバを立てる | 雑記帳などを参照。
次。mpd5の話。インストールについては、L2TP/IPsecをやるときと同じなので、略。IPv6をばら撒くための、設定について。IPv6 Part 6: Configuring An IPv6 Network Assigned Over PPPoE Using FreeBSD » mmacleod.caがとても参考になる。
/usr/local/etc/mpd5/mpd.conf の内容。
startup:
        set user kanri irnak admin
        set console self 127.0.0.1
        set console open
        log +ipcp +ipv6cp +lcp +link +auth +ecp +ccp
        set console enable logging

default:
        load l2tp_server

l2tp_server:
        create bundle template B4
        set bundle enable ipv6cp
        set iface idle 1800
        set iface mtu 1454
        set iface enable tcpmssfix
        set iface up-script /usr/local/etc/mpd5/mpd.linkup
        set iface down-script /usr/local/etc/mpd5/mpd.linkdown
        create link template L4 l2tp
        set link action bundle B4
        set link enable multilink
        set auth enable system-acct
        set link keep-alive 10 60
        set link disable chap eap
        set link enable chap-msv2
        set link mtu 1454
        set link mru 1454
        set link enable incoming
ポイントは、ipv6cpを有効にしていること。IPv4の割り当ては行わないので、ipcpは記述しない。
つながったらroutingやfirewallの設定の変更が必要になるので、それらは up-sciptで指定した /usr/local/etc/mpd5/mpd.linkup に書く。
1つ目の引数がInterface(ng0とかng1とか)なので、
/sbin/ifconfig $1 inet6 割り当てるアドレス prefixlen 割り当てるprefix alias
とかしてあげればOK。ルーティングは適当に。例えば、P2Pだからってprefixを長くしてあげると、アドレスが一番マッチしたルートを使おうとするので、ルーティングを意識しなくてもよきに計らってくれたりする。
同様に、切断されたときの処理を down-scriptで指定した /usr/local/etc/mpd5/mpd.linkdown に書く。

クライアントのMac OSX側。こちらは、Hiroaki's blog: LinuxのVPN ServerでIPv6をバラまくと同じ。


Wifiスポットなど、まだIPv6を割り当ててくれないサービスも多く、6to4が通らないことも多いので、こういう無理やりな方法がまだまだ必要。


2012/09/17

OSXでDNS Serverを変更する

Mt. Lion(に限らず、OSX)では、/etc/resolv.conf は飾りなので、書き換えたところで実質的には意味が無い。Configure DNS lookups from the terminal - Mac OS X HintsにDNS Serverの変更方法が説明されている。それを参考に作ったスクリプトが、これ。
#!/bin/bash
if [ $# -lt 1 ]; then
    echo "$(/usr/bin/basename $0) DNSServer [DNSServer ...]"
    exit 1
fi
key=$(echo 'show State:/Network/Global/IPv4' | /usr/sbin/scutil | /usr/bin/awk '/PrimaryService/{print $3;}')
domain=$(echo "show State:/Network/Service/${key}/DNS" | /usr/sbin/scutil | /usr/bin/awk '/DomainName/{print $3;}')

/usr/sbin/scutil <<EOF
open
d.init
d.add ServerAddresses * $*
d.add DomainName ${domain:-localdomain}
set State:/Network/Service/${key}/DNS
quit
EOF

って、上記記事のコメントにも同じようなスクリプトが載っていたorz。まぁ、こっちは、割り当てられたDNS ServerがAAAA filterしちゃっているときに変更するのが目的なので、ドメイン名は既に割り当てられているものを使うようにしている。


2012/08/27

IPsecでNASへのバックアップ通信を暗号化する(NAS側)

Hiroaki's blog: IPsecでNASへのバックアップ通信を暗号化するの続き。AFP(Netatalk)の動いている、バックアップデータを受け取るサーバ側の設定。FreeBSD 7.3 + IPsec Toolsで。

kernelの再構築や、IPsec Toolsのインストールは略。
まずは/usr/local/etc/racoon/racoon.conf の内容。
# $Id: racoon.conf,v 1.3 2012/08/25 04:47:32 root Exp root $
#
# $KAME: racoon.conf.in,v 1.18 2001/08/16 06:33:40 itojun Exp $

# "path" must be placed before it should be used.
# You can overwrite which you defined, but it should not use due to confusing.
path include "/usr/local/etc/racoon" ;
#include "remote.conf" ;

# search this file for pre_shared_key with various ID key.
path pre_shared_key "/usr/local/etc/racoon/psk.txt" ;

# racoon will look for certificate file in the directory,
# if the certificate/certificate request payload is received.
path certificate "/usr/local/etc/cert" ;

# "log" specifies logging level.  It is followed by either "notify", "debug"
# or "debug2".
log info;

# "padding" defines some parameter of padding.  You should not touch these.
padding
{
        maximum_length 20;      # maximum padding length.
        randomize off;          # enable randomize length.
        strict_check off;       # enable strict check.
        exclusive_tail off;     # extract last one octet.
}

# if no listen directive is specified, racoon will listen to all
# available interface addresses.
listen
{
        #isakmp ::1 [7000];
        #isakmp 202.249.11.124 [500];
        #admin [7002];          # administrative's port by kmpstat.
        #strict_address;        # required all addresses must be bound.
#       isakmp IPv4アドレス [500];
        isakmp IPv6アドレス [500];
}

# Specification of default various timer.
timer
{
        # These value can be changed per remote node.
        counter 10;             # maximum trying count to send.
        interval 3 sec; # maximum interval to resend.
        persend 1;              # the number of packets per a send.

        # timer for waiting to complete each phase.
        phase1 30 sec;
        phase2 30 sec;
}

remote anonymous
{
        exchange_mode aggressive, main;
        doi             ipsec_doi;
        situation       identity_only;
        generate_policy on;
        proposal_check  obey;
        dpd_delay       20;
        passive         on;
        support_proxy   on;
        ike_frag        on;
        verify_identifier off;
    proposal {
            authentication_method rsasig;
            encryption_algorithm 3des;
            hash_algorithm sha1;
            dh_group 2;
    }
    proposal {
        encryption_algorithm    aes;
        hash_algorithm          sha256;
        authentication_method   pre_shared_key;
        dh_group                modp1024;
    }
    proposal {
        encryption_algorithm    3des;
        hash_algorithm          sha256;
        authentication_method   pre_shared_key;
        dh_group                modp1024;
    }
   proposal {
      authentication_method pre_shared_key;
      hash_algorithm sha1;
      encryption_algorithm aes 256;
      lifetime time 3600 sec;
      dh_group 2;
   }
   proposal {
      authentication_method pre_shared_key;
      hash_algorithm sha1;
      encryption_algorithm aes;
      lifetime time 3600 sec;
      dh_group 2;
   }
   

   proposal_check strict;
}

# Phase 2 proposal (for IPsec SA)
sainfo anonymous
{
        pfs_group 2;
        lifetime time 10 minutes;
        encryption_algorithm aes, rijndael, 3des;
        authentication_algorithm hmac_sha1;
        compression_algorithm deflate;
}
client側のIPアドレスは固定ではないため、anonymousで定義する必要がある。remote {} も sainfo{} も、client側とパラメータをそろえること。proposalは一致するものがあればよい。

次。共有キー。psk.txtは
メールアドレス 共有キー

とする。メールアドレスは、client側のconfファイルのremote{}で指定したメールアドレスとする。aggressive modeで接続してくるため、IDとしてIPアドレス以外のものも使用できるのだ。クライアント側でmy_identifier  user_fqdnとしているので、同じメールアドレスを使う。

最後に、どの通信を暗号化対象にするかを /usr/local/etc/racoon/setkey.conf に指定する。
# $Id: setkey.conf,v 1.2 2012/08/25 11:53:25 root Exp $
flush;
spdflush;
spdadd ::1 ::1 any -P in none;
spdadd ::1 ::1 any -P out none;
spdadd NASのあるネットワーク/マスク[0] NASのアドレス/0[548] any -P in none;
spdadd NASのアドレス/0[548] NASのあるネットワーク/マスク/マスク[0] any -P out none;
spdadd ::/0[0] NASのアドレス/0[548] any -P in ipsec esp/transport//require;
spdadd NASのアドレス/0[548] ::/0[0] any -P out ipsec esp/transport//require;
noneで終わっている行は、暗号化を行わない指定。自分自身と通信する場合、自分と同じネットワーク内と通信する場合は、暗号化を行わないようにする。
暗号化を行う条件は、ちょうどクライアント側とin/outを逆にした関係になる。

これでOK。動作確認。Wiresharkなどでパケットをキャプチャしながら、クライアント側で
telnet NASのアドレス 548
とする。IKASMPのパケットの後ESPのパケットになり、かつ、telnetが接続できていればOK。

racoon.conf でlogをdebugにすると、内容が膨大で接続の確認には向かない。tcpdumpやwiresharkでパケットをキャプチャして、どこまで進んでいるかを確認しながら設定を見直すのが現実的。
Phase1が確立していなければ、remote {} の部分とpsk.txtを確認。Phase2であればsainfo。また、暗号化されずにtcp/548に接続しに行っているようであれば、setkeyに喰わせるファイルを確認。firewallでrejectされていることもあるので、設定を見直すことも必要。UDP 500/4500とプロトコル番号50/51。
ログやtcpdumpの出力は意外と見かけないのが、また切り分けを難しくしていると思う。
が参考になる。
また、他に参考にしたページがいくつか。



2012/08/26

IPsecでNASへのバックアップ通信を暗号化する

外出先であっても時間がくればTimeMachineが動いてバックアップを行うので、ネットワークに繋がっているのであれば、普段使っているNASに取って欲しいところ。
この時、バックアップで使うAFPというプロトコルは暗号化を行わないので、別の手段で暗号化を行う必要がある。そこで、IPsecで暗号化を行なってみる。

  • IPv6での通信とする
  • NASのあるネットワークにいる場合、暗号化は行わない
  • 事前共有キーによる暗号化

の3点を条件とする。

  • クライアント側はMountain Lion。racoonを使う。
  • サーバ側はFreeBSD。racoonを使う。

直接設定ファイルを書くことによって、「AFPだけ暗号化する」ようにする。
お約束だが、内容の真偽も含めて、自己責任で。最悪、どこでもMy Macが動かなくなることもあるので。

Mountain Lion側。
/var/run/racoon/ に動的生成した設定ファイルを置くようになっている様子だが、消されそうなので、/etc/racoon/remote/ を作って、そこに置くことにする。
そこに置いた設定ファイルが取り込まれるよう、/etc/racoon/racoon.conf に手を加える。racoonはどこでもMy Macで使っているので、ミスがあると動かなくなるので注意。
racoon.confの最後に
include "/var/run/racoon/*.conf" ;
という行があるので、その手前(上の行)に
include "/etc/racoon/remote/*.conf" ;
を加える。
次に、/etc/racoon/remote/ に、NASに接続するための設定ファイルを作成する。ファイル名は.confで終わっていればなんでも良いので、例えば、NASのhost名にする。
中身は
remote NASのIPアドレス {
   exchange_mode aggressive;
   doi ipsec_doi;
   situation identity_only;
   verify_identifier off;
   my_identifier  user_fqdn       "メールアドレス";
   initial_contact off;
   support_proxy on;
   proposal_check obey;
   
    proposal {
        encryption_algorithm    aes;
        hash_algorithm          sha256;
        authentication_method   pre_shared_key;
        dh_group                modp1024;
    }
    proposal {
        encryption_algorithm    aes;
        hash_algorithm          sha1;
        authentication_method   pre_shared_key;
        dh_group                modp1024;
    }
    proposal {
        encryption_algorithm    3des;
        hash_algorithm          sha256;
        authentication_method   pre_shared_key;
        dh_group                modp1024;
    }
    proposal {
        encryption_algorithm    3des;
        hash_algorithm          sha1;
        authentication_method   pre_shared_key;
        dh_group                modp1024;
    }
 
   proposal {
      authentication_method pre_shared_key;
      hash_algorithm sha1;
      encryption_algorithm aes 256;
      lifetime time 3600 sec;
      dh_group 2;
   }
   
   proposal {
      authentication_method pre_shared_key;
      hash_algorithm md5;
      encryption_algorithm aes 256;
      lifetime time 3600 sec;
      dh_group 2;
   }
    proposal {
      authentication_method pre_shared_key;
      hash_algorithm sha1;
      encryption_algorithm aes;
      lifetime time 3600 sec;
      dh_group 2;
   }
   
   proposal {
      authentication_method pre_shared_key;
      hash_algorithm md5;
      encryption_algorithm aes;
      lifetime time 3600 sec;
      dh_group 2;
   }
   
   proposal {
      authentication_method pre_shared_key;
      hash_algorithm sha1;
      encryption_algorithm 3des;
      lifetime time 3600 sec;
      dh_group 2;
   }
   
   proposal {
      authentication_method pre_shared_key;
      hash_algorithm md5;
      encryption_algorithm 3des;
      lifetime time 3600 sec;
      dh_group 2;
   }
}
とする。
ポイントはexchange_mode。これをaggressiveだけにしているので、Phase 1はaggressive modeで折衝が開始される。そのため、事前共有キーファイルでの相手のIDとしてIP Address以外のものも使えるようになる。 受け側のracoonでは相手のIDとして*(任意の文字列)が使えないため、割り当てられるAddressが固定ではないモバイル環境からアクセスする場合、メールアドレスなど、IP Address以外のものを指定する必要があるため、aggressive modeは必須となる。
通常であれば、この後にsainfoを定義するのだが、既に定義されているのだ。/var/run/racoon/anonymous.conf がそれ。
sainfo の部分はこんな感じ
sainfo anonymous { 
  pfs_group 2;
  lifetime time 10 min;
  encryption_algorithm aes;
  authentication_algorithm hmac_sha1;
  compression_algorithm deflate;
}
ここまでで、暗号化するときの各種パラメータを定義した。後は、どの通信を暗号化するのかを定義する。これは、「NASにバックアップする通信」なので、NASに送る奴とNASから返ってくる奴とを定義する。
例えば、/etc/racoon/NASのhost名.key というファイルに
spdadd NASのいるネットワーク/マスク[0] NASのアドレス[548] any -P out none;
spdadd NASのアドレス[548] NASのいるネットワーク/マスク[0] any -P in none;
spdadd ::/0[0] NASのアドレス[548] any -P out ipsec esp/transport//require;
spdadd NASのアドレス[548] ::/0[0] any -P in ipsec esp/transport//require;
と書く。これをsetkey -f で喰わせてやればよいのだが、マシンを起動するたびに忘れずに実行する必要があるので、例えば、rootでcrontab -eして
@reboot /usr/sbin/setkey -f /etc/racoon/NASのhost名.key
と書いておく。

最後に、事前共有キー。/etc/racoon/psk.txt が既にあるので、同じように、
NASのアドレス 事前共有キー
の1行を追加しておく。

サーバに繋ぐ前に、一度Macを再起動させて問題なさそうなことを確認しておく。
再起動してログインしたら、アプリケーション→ユーティリティ→コンソール.app を開く。左側のログリストで「すべてのメッセージ」を選択し、右側に表示されるracoonのメッセージを確認する。問題なければ、「IPSec Phase2 established」の行が表示されているはず。何らかのエラーが出ているようであれば、/etc/racoon/racoon.conf および /etc/racoon/remote/*.conf の中身を確認して修正、racoonの再起動(わからなければ、Macの再起動)。放置しておくと、どこでもMy Macで接続できないことがある!

サーバ側の設定についてはまた今度。


2012/08/19

LinuxのVPN ServerでIPv6をバラまく

IPv4アドレスが枯渇したと言われているが、3Gや公衆のWifiスポットで未だにIPv6アドレスが割り当てられることはない。しかも、割り当てられるIPv4アドレスがPrivateアドレスで、6to4などのtunnelが使えなかったりする。
割り当てられないのであれば、自分で割り当てよう!ということで、L2TPで繋いだVPNでIPv6を流してみる。

今回は、IPv6アドレスを割り当てられているLinux(CloudCoreのCentOS 5.8)に対してOSX(Mountain Lion)からVPN接続し、踊る亀を見られるようにする。

まずはLinux側。すでに何らかの形で固定のIPv6アドレスを、/64よりも大きなブロックで割り当てていることが前提。6to4で割り当てられるのは/48なので、それでもOK。
最初は普通にL2TPのサーバにする。さくらのVPSでiPhone用の野良WiFi通信傍受対策のL2TP/IPsec(VPN)を設定したメモ(CentOS5) - nori_no のメモなどを参考に。openswanもxl2tpdもrpmがあったので、それでインストール。とりあえずはIPv4のアドレスを割り当てるようにする。

次に、Mountain Lion側。システム環境設定→ネットワーク で、新しいサービスを追加する。
  • インターフェイス:VPN
  • VPN タイプ:L2TP over IPSec
  • サービス名:適当に
さらに
  • サーバアドレス:Linuxのホスト名またはIPv4アドレス
  • アカウント名:Linuxの/etc/ppp/chap-secrets に書いたclientの名前
とし、認証設定ボタンを押下。
  • コンピュータ認証:/etc/ipsec.secret に書いたパスワード
  • パスワード:/etc/ppp/chap-secrets に書いたパスワード
をセットして、OK。
適用ボタンを押して設定を保存したら、接続ボタンを押して、繋がるかどうか確認。詳細...ボタンを押して、TCP/IPの欄でIPv4 アドレスやサブネットマスクなどが割り当てられていることを確認する。
  • Linux側では、UDPの1701, 500, 4500が通信できるようにiptablesが設定されていること。
  • 設定に問題ないようであれば、ipsecを再起動してみる(sudo /sbin/service ipsec restart)
  • Mountain Lion側では、パスワードを正しく設定していること。
が確認ポイント。IPv4で使う訳ではないので、NATやルーティングは設定不要。

ここまでできたら、IPv6用の設定に移る。
Linux側の /etc/ppp/options.xl2tpd の最後に、

+ipv6
ipv6cp-accept-local
を追加。 /etc/sysconfig/network-scripts にifcfg-ppp0 というファイル(ppp1やppp2かもしれないが、通常は0でOK)を新たに作る。中身は

IPV6INIT=yes
IPV6ADDR=受け側のIPv6アドレス/64
とする。

次。/etc/ppp/ipv6-up がバグっているような気がしてならない。ので、ちょっと手を入れる。
47行目前後にある

CONFIG=$REALDEVICE


CONFIG=$LOGDEVICE
にする。
そして、/etc/ppp にipv6-up.local というファイルを作る。中身は

#!/bin/bash

PREFIX="割り振るIPv6のネットワーク"
/sbin/ip6tables -P FORWARD ACCEPT
/sbin/ip6tables -A FORWARD -i $1 -j ACCEPT 
/sbin/ip6tables -A INPUT -i $1 -j ACCEPT 
とする。ifcfg-ppp0で/64としているので、PREFIXは::/64 で終わるアドレスとなる。このファイルに実行権限をつけることを忘れないこと。これは、IPv6アドレスが割り当てられた時に実行される。

Mountain Lion側は、システム環境設定→ネットワーク でさっき作ったサービスを選択し、詳細... のTCP/IP タブを選択。IPv4 の構成を「切」にする。
また、/etc/ppp/ipv6-up というファイルを作り、実行権限を付けておく。中身は

#!/bin/sh
/sbin/ifconfig $1 inet6 add 割り当てるIPv6アドレス/64
/sbin/route add -inet6 default $4%$1
/sbin/route change -inet6 default $4%$1
とする。Linux側で最後を::1として、Mountain Lion側では::2とするとわかりやすいかも。
これで設定はOK。作った「サービス」に繋いだ後、踊る亀にアクセスして、亀の絵がアニメーションしていればIPv6で接続できている。


本来であれば、Linux側でradvdを動かしていればMountain Lion側のIPv6アドレスは自動で割り当てられるのだが、 IPv6 Address on L2TP VPN: Apple Support Communitiesで指摘されているように、アドレスの割当が行われない。そのため、/etc/ppp/ipv6-up を用意して手動で割当を行っている。


2012/08/11

もうちょっと数当てゲーム

Hiroaki's blog: もう一回 数当てゲームで残した乱数の部分を見てみる。
isUsedNum :: Char->String->Int
isUsedNum _ [] = 0
isUsedNum a (x:xs)
             | a == x    = 1 + isUsedNum a xs
             | otherwise = isUsedNum a xs

isSuitableNum :: String->Bool
isSuitableNum [] = True
isSuitableNum (x:xs) = isUsedNum x xs == 0 && isSuitableNum xs

normNum :: String->String
normNum xs
        | (l < 4)       = normNum $ "0" ++ xs
        | otherwise     = xs
        where l = length xs

targetNum :: [Int]->String
targetNum (x:xs)
          | (isSuitableNum strX) = strX
          | otherwise            = targetNum xs
          where strX = normNum $ show x

main = do
     r <- getStdRandom $ randomR (0, 99)
     let t = targetNum $ randomRs (0,9999) (mkStdGen r)
  • 乱数で4ケタの数字を求める。この時、数字はいくつも生成してリストにしておく
  • リストの頭から数字を1つ取り出し、数当てゲームで使える数字だったら採用。違ったら、リストの頭の数字を捨てて、リストの頭から数字を取りだすところからやり直し。→ targetNum
  • 採用した数字が3ケタだったら、頭に0をつける。→ normNum
  • 数当てゲームで使えるかどうかは isSuitableNum。一桁ずつ取り出して、既に使っているかどうかを順番にチェック→isUsedNum
isUsedNumって、elem使えばもっと簡単になるよね。あと、「数字がダブらないこと」という条件があるから、求める4ケタの数字は0123~9876になる。もっとも、これはコメント入れておかないと混乱することになるけど。

では、数当て問題 - Hit & Blow -: ツムジのひとりごとのコードを拝見。

-- xs からランダムに重複しない n 個の要素を取り出す
randomPicUp :: Eq a => Int -> [a] -> IO [a]
randomPicUp n xs = loop n (length xs - 1) xs []
  where
    loop 0 _ _ prd = return prd
    loop c m xs prd = do
      g <- newStdGen
      let (i, _) = randomR (0, m) g
      let x = xs !! i
      loop (c - 1) (m - 1) (delete x xs) (x : prd)

main :: IO ()
main = do
  target <- randomPicUp 4 ['0'..'9']
大きな違いは、
  • 0~9から数字を1個選ぶ。これを4回行って、4けたの数字とする
という考え方だろう。randomPicUpの中にある、loop c m xs prdがそれだ。
  • cはループカウンタかな?xsは選び出す数字の候補で、最初は0~9だけど、選ばれた数字を除外していく。mは選び出す数字の候補の数のようだから、xsの長さから求めればいいよね。
  • prdは選んだ数字のリスト。これを数えるようにすれば、cはいらなくなるかな?
とはいえ、ここまでシンプルに書けるのね~。


2012/08/09

もう一回 数当てゲーム

数当て問題 - Hit & Blow -: ツムジのひとりごとで取り上げていただいたので、自分のコードと比べてみる。

このゲームの肝であるHitとBlowを求める処理のうち、まずはBlowから。
Blowは、「数字として当たっているし、場所(桁)も当たり」ということ。

countBlow :: String->String->Int
countBlow [] _ = 0
countBlow _ [] = 0
countBlow (x:xs) (y:ys)
          | x == y    = 1 + countBlow xs ys
          | otherwise = countBlow xs ys
  • 文字列を2つ受け取って、それぞれの先頭を比較する。一致していたら、戻り値に1を加える。
  • 文字列が終わりでなかったら、比較しなかった残りの文字列それぞれを引数にして、自分を呼び出す。
  • 受け取った文字列のうち、どちらかが空だったら、0を返す
で、それを
  • 文字列を2つ受け取って、それぞれの桁について、同じ数字だったらTrue、違ったらFalseにする→受け取った文字列と同じ長さの、Boolのリストができる
  • そのBoolのリストのうち、Trueの数がBlow
と考える。
そうすると、文字(Char)のリストである文字列を2つ受け取って、それぞれの要素に対して関数を適用する→zipWithの出番
また、BoolのリストでTrueを数えるのは
  • filterでリストのうちTrueのものだけにする
  • それからリストの要素数を数える
とすればよいので、

countTrue :: [Bool] -> Int
countTrue = length . filter (== True)

となる。
結局、Blowは

countBlow :: String -> String -> Int
countBlow target = countTrue . zipWith (==) target

で求められると。


次。Hit。
Hitは、「数字としては当たっているけど、場所(桁)は違う」ということ。
単純に「数字として当たっている」ものを数えてしまうと、Blowもカウントしてしまう。でも、Blowも求めるのだから、「数字として当たりの数」- Blow として求めればよい。
ということで、どうやって「数字として当たりの数」を求めるか?

isUsedNum :: Char->String->Int
isUsedNum _ [] = 0
isUsedNum a (x:xs)
             | a == x    = 1 + isUsedNum a xs
             | otherwise = isUsedNum a xs

countUsedNum :: String->String->Int
countUsedNum [] _ = 0
countUsedNum (x:xs) ys = isUsedNum x ys + countUsedNum xs ys

うーん、回りくどい。2つの文字列を引数として受け取って、1つ目の文字列を1文字ずつ、2つ目の文字列の構成要素か見ている(isUsedNum)。これも、文字列を1文字ずつ比較しているから、遅くなるはず。
こっちも、Blowと同様、「構成要素であればTrue」のリストを作って、最後にTrueの数を数えればいいわけだ。

countHit :: String -> String -> Int
countHit target = countTrue . map (`elem` target)

mapが肝かな?関数とリストを受け取って、リストの1つ1つの要素に対して関数を適用する(関数で処理する)。だから、mapの結果のリストって、与えたリストと同じ長さになる
で、mapにどんな関数を与えるかというと、target の構成要素かどうかを返す関数を与える。あとは、Trueの数を数えればOK。


リストの使い方がうまくない。まぁ、もっといろいろプログラム書いて慣れないとね。


2012/08/05

数あてゲームのバグをつぶす

Hiroaki's blog: Haskellで数あてゲームを書いてみる
にはバグがある。
当てる数を0以上9999以下の乱数で求めているのだが、この数字を文字列にする際、頭の0が失われるのだ。
だから、求める数字が4つにならないことがある。また、数が100未満の場合、0が複数回使われていることをチェックできないのだ。
というわけで、ゲームのルールに従っている数字かどうかをチェックする前に、4桁になるような処理を追加してみた。

import System.Random
import Control.Monad(when)

isUsedNum :: Char->String->Int
isUsedNum _ [] = 0
isUsedNum a (x:xs)
             | a == x    = 1 + isUsedNum a xs
             | otherwise = isUsedNum a xs

countUsedNum :: String->String->Int
countUsedNum [] _ = 0
countUsedNum (x:xs) ys = isUsedNum x ys + countUsedNum xs ys

countBlow :: String->String->Int
countBlow [] _ = 0
countBlow _ [] = 0
countBlow (x:xs) (y:ys)
          | x == y    = 1 + countBlow xs ys
          | otherwise = countBlow xs ys

resultString :: String->String->(Bool, String)
resultString xs ys =
             (endFlag, show hit ++ " hit(s) " ++ show blow ++ " blow(es).")
             where blow = countBlow xs ys
                   hit = countUsedNum xs ys - blow
                   endFlag = blow == 4

isSuitableNum :: String->Bool
isSuitableNum [] = True
isSuitableNum (x:xs) = isUsedNum x xs == 0 && isSuitableNum xs

checkNum :: String->IO ()
checkNum compNum = do
         putStrLn "Number ? "
         guessNum <- getLine
         when (not $ null guessNum) $ do
              let (flag, msg) = resultString compNum guessNum
              putStrLn msg
              when (not flag) $ checkNum compNum
         return ()

normNum :: String->String
normNum xs
        | (l < 4)       = normNum $ "0" ++ xs
        | otherwise     = xs
        where l = length xs

targetNum :: [Int]->String
targetNum (x:xs)
          | (isSuitableNum strX) = strX
          | otherwise            = targetNum xs
          where strX = normNum $ show x

main = do
     r <- getStdRandom $ randomR (0, 99)
     let t = targetNum $ randomRs (0,9999) (mkStdGen r)
     checkNum t
     putStrLn t


2012/07/29

Haskellで数あてゲームを書いてみる

言語は実際にプログラムを書いてみないと身に付かない。ということで、昔、ポケコンでよく見かけた数あてゲームをHaskellで書いてみた。
コンピュータが生成した数字をあてる。
コンピュータは、予想した数に対して
 Hit: 数字はあっているが、場所が異なる数字の数
 Blow:数字も場所もあっている数字の数
の2種類のヒントを返す

import System.Random
import Control.Monad(when)

isUsedNum :: Char->String->Int
isUsedNum _ [] = 0
isUsedNum a (x:xs)
             | a == x    = 1 + isUsedNum a xs
             | otherwise = isUsedNum a xs

countUsedNum :: String->String->Int
countUsedNum [] _ = 0
countUsedNum (x:xs) ys = isUsedNum x ys + countUsedNum xs ys

countBlow :: String->String->Int
countBlow [] _ = 0
countBlow _ [] = 0
countBlow (x:xs) (y:ys)
          | x == y    = 1 + countBlow xs ys
          | otherwise = countBlow xs ys

resultString :: String->String->(Bool, String)
resultString xs ys =
             (endFlag, show hit ++ " hit(s) "
                                ++ show blow ++ " blow(s).")
             where blow = countBlow xs ys
                   hit = countUsedNum xs ys - blow
                   endFlag = blow == 4

isSuitableNum :: String->Bool
isSuitableNum [] = True
isSuitableNum (x:xs) = isUsedNum x xs == 0 && isSuitableNum xs

checkNum :: String->IO ()
checkNum compNum = do
         putStr "Number ? "
         guessNum <- getLine
         when (not $ null guessNum) $ do
              let (flag, msg) = resultString compNum guessNum
              putStrLn msg
              when (not flag) $ checkNum compNum
         return ()

targetNum :: [Int]->String
targetNum (x:xs)
          | (isSuitableNum strX) = strX
          | otherwise            = targetNum xs
          where strX = show x

main = do
     r <- getStdRandom $ randomR (0, 99)
     let t = targetNum $ randomRs (0,9999) (mkStdGen r)
     checkNum t
     putStrLn t

乱数はすごいHaskellたのしく学ぼう! の通りにはいかなかった。パッケージングが変更になったんだそうだ。
cabal install random
としてパッケージを追加。また、使い方はRandom モジュールの使い方 : tnomuraのブログが参考になった。