Windows環境で作られたzip書庫のファイル名化けを解消する方法

Linux で人から渡された zip 書庫を解凍すると、ファイル名が文字化けしている事が良くあります。

zip 書庫は、格納するファイルの文字コードをそのまま保存しており、Windows 環境で作った日本語ファイル名には、文字コード CP932 が使用されるため、UTF8 が使用されている最近の Linux 環境では文字化けするのは当然です。

このようなファイルは、convmv コマンドでファイル名の変換を試してみます。

$ unzip sample1.zip 
Archive:  sample1.zip
 extracting: ?V?????t?H???_/?V?K?e?L?X?g ?h?L???????g.txt 
$ convmv -r -f cp932 -t utf8 *
Your Perl version has fleas #37757 #49830 
Starting a dry run without changes...
mv "?V?????t?H???_/?V?K?e?L?X?g ?h?L???????g.txt"	"?V?????t?H???_/新規テキスト ドキュメント.txt"
mv "./?V?????t?H???_"	"./新しいフォルダ"
No changes to your files done. Use --notest to finally rename the files.

うまく変換されているようなら、--notest オプションを付けて変換を実行します。

全ての場合でこの方法でうまくいくならそれでいいのですが、一部の zip 書庫については、この方法でもうまくいかないことがあります。

$ unzip sample2.zip 
Archive:  sample2.zip
 extracting: ?V?????t?H???_/?V?K?e?L?X?g ?h?L???????g.txt  
honda@galeon:/tmp$ convmv -r -f cp932 -t utf8 *
Your Perl version has fleas #37757 #49830 
Starting a dry run without changes...
mv "?V?????t?H???_/?V?K?e?L?X?g ?h?L???????g.txt"	"?V?????t?H???_/ノV?K稙祗禮稟 禀祗籵紆糜稟.txt"
mv "./?V?????t?H???_"	"./ノV鮹鴣穰礬糀秬"
No changes to your files done. Use --notest to finally rename the files.

これは、unzip コマンドが解凍を実施する際に、ある条件*1でファイル名の変換を実施しているが原因のようです。

上のリンク先の方は、ファイル名変換を実行する関数を無効にした unzip コマンドを再コンパイルすることで対処されています。他にも debian だと、unzip-cp932 というパッケージ*2を利用する方法もあります。

自分の場合は、jar コマンドで解凍を行って対処しています。jar 形式は zip 書庫がそのまま使用されていることが多く、jar コマンドは zip 書庫の圧縮/解凍をすることも可能です。ちなみにオプションは、tar コマンドとほとんど同じです。

$ jar xvf sample2.zip 
   created: ?V?????t?H???_/
 extracted: ?V?????t?H???_/?V?K?e?L?X?g ?h?L???????g.txt

最近のディストリビューションjava パッケージと共に jar コマンドが公式に提供されていることが多いので、unzip コマンドを再コンパイルしたり、パッケージを独自に用意する必要がないのが、この方法のメリットです。

デメリットは、unzip を利用している file-roller 等では、問題が残ったままになることが挙げられますが、最終的に文字化けを解消するには、convmv コマンドの使用が必要なので、はじめから CUI で解凍を行うのが良さそうな気がします。

*1:使用する圧縮ソフトが影響するようです。

*2:http://debian.fam.cx/index.php?sarge%2FJapanese#content_1_70

Vimからはてなフォトライフに画像を投稿する

Vimからはてなフォトライフに画像を投稿するvimスクリプトを書いてみました。hatena.vimに追加して使用してください。画像を投稿すると、その画像のfotolife記法がVimの本文に追加されます。「Vimからはてなハイクに投稿する」も合わせてどうぞ。

画像の投稿には、はてなフォトライフAtomAPIを使用しました。WSSE認証ヘッダはシェルスクリプトでは難しそうでしたので、こちらの記事を元にrubyで生成しています。AtomAPIを使っているためcookieが使用できず、画像投稿時には毎回パスワードを要求されます。

動作には、rubyhatena.vimが使用するcurlコマンドの他に、fileコマンドが必要です。

" WSSE認証ヘッダの生成
function s:WSSECreds(user, pass)
    let creds = system('ruby -r "digest/sha1" -r "time" -e "' .
                \'nonce = Array.new(10){ rand(0x100000000) }.pack(%!I*!);' .
                \'nonce_base64 = [nonce].pack(%!m!).chomp;' .
                \'now = Time.now.utc.iso8601;' .
                \'digest = [Digest::SHA1.digest(nonce + now + %!' . a:pass . '!)].pack(%!m!).chomp;' .
                \'print %!UsernameToken !' .
                \'+ %!Username=\"%s\", ! % %!' . a:user . '!' .
                \'+ %!PasswordDigest=\"%s\", ! % digest' .
                \'+ %!Nonce=\"%s\", ! % nonce_base64' .
                \'+ %!Created=\"%s\"! % now;' .
                \'"')
    if v:shell_error
        return
    else
        return creds
    endif
endfunction

" 指定したファイルをはてなフォトライフに投稿する
" :Usage
"   :FotolifePost <filename>
command! -nargs=1 -complete=file FotolifePost   call <SID>FotolifePost('<args>')

function s:FotolifePost(filename)

    let mimetype = system('file -bi "' .  a:filename . '"')
    if v:shell_error
        echo "指定したファイルがありません"
        return
    endif

    let tmpfile = tempname()
    let title = input('タイトル: ')

    " リクエストXMLの作成
    let result_encode = system('ruby -r "base64" -e "' .
                \'request_xml = %!<entry xmlns=\"http://purl.org/atom/ns#\">\n' .
                \'<title>' . title . '</title>\n' .
                \'<content mode=\"base64\" type=\"' . mimetype . '\">\n' .
                \'%s</content>\n' .
                \'</entry>!;' .
                \'open(%!' . tmpfile . '!, %!w!){|io|' .
                \'io << request_xml % Base64.encode64(open(%!' . a:filename .'!).read)' .
                \'}' .
                \'"')

    if v:shell_error
        echo "Base64 エンコードに失敗しました"
        return
    endif

    let password = inputsecret('Password: ')
    let header_wsse = 'X-WSSE: ' . s:WSSECreds(g:hatena_user, password)
    let endpoint = 'http://f.hatena.ne.jp/atom/post'
    let header_ac = 'Accept: application/x.atom+xml, application/xml, text/xml, */*'
    let header_cont = 'Content-Type: application/x.atom+xml; charset="utf-8"'


    let request = s:curl_cmd . ' -f --header "' . header_cont .
                \'" --header "' . header_ac . '" --header "' . header_wsse .
                \'" --data "@' . tmpfile .
                \'" "' . endpoint . '"'

    echo "画像のアップロード中です..."

    " 画像の投稿を実行
    let ret = system(request)

    if v:shell_error
        echo "画像の投稿に失敗しました"
        return
    endif

    let imgsyntax = matchstr(ret, '<hatena:syntax>\zs[^>]*\ze</hatena:syntax>')
    " 投稿した画像の記法を本文に追加
    let ret = append( line('.'), imgsyntax)

    if strlen(imgsyntax) != 0
        echo imgsyntax . " の投稿に成功しました"
    else
        echo "画像の投稿に失敗しました"
    endif

    return

endfunction

実行方法は次のとおり。

:FotolifePost <filename>

写真に位置情報を記録する

半年程前にハンディ GPSGarmin eTrex Vista HCx (英語版) を購入しました。元々は山登りの為に買ったんですが、最近は写真に位置情報を追加するために使うことが多いです。今回は、Linux で写真に位置情報を記録する方法について説明します。

自分が買った eTrex シリーズは、最近の Linux カーネルにはドライバが収録されていて、USB ケーブルで繋ぐと USB/シリアル変換デバイスとして認識されます。

以下は、GPS を繋いだときの /var/log/messages です。GPS が /dev/ttyUSB0 として認識されたことが分かります。

May 31 18:46:17 galeon kernel: usb 1-3: new full speed USB device using ohci_hcd and address 4
May 31 18:46:17 galeon kernel: usb 1-3: configuration #1 chosen from 1 choice
May 31 18:46:18 galeon kernel: usbcore: registered new interface driver usbserial
May 31 18:46:18 galeon kernel: drivers/usb/serial/usb-serial.c: USB Serial support registered for generic
May 31 18:46:18 galeon kernel: usbcore: registered new interface driver usbserial_generic
May 31 18:46:18 galeon kernel: drivers/usb/serial/usb-serial.c: USB Serial Driver core
May 31 18:46:18 galeon kernel: drivers/usb/serial/usb-serial.c: USB Serial support registered for Garmin GPS usb/tty
May 31 18:46:18 galeon kernel: garmin_gps 1-3:1.0: Garmin GPS usb/tty converter detected
May 31 18:46:18 galeon kernel: usb 1-3: Garmin GPS usb/tty converter now attached to ttyUSB0
May 31 18:46:18 galeon kernel: usbcore: registered new interface driver garmin_gps
May 31 18:46:18 galeon kernel: drivers/usb/serial/garmin_gps.c: garmin gps driver v0.31

この /dev/ttyUSB0 は、Debian では dialout グループのデバイスとして認識されます。

$ ls -la /dev/ttyUSB0 
crw-rw---- 1 root dialout 188, 0 2008-05-31 19:16 /dev/ttyUSB0

そのため、一般ユーザで GPS を使用するには、使用するユーザを dialout グループに加えておきます。

# usermod -a -G dialout h2onda

GPS から移動経路の情報(トラック)を抽出するには、GPSBabel を使用します。

GPSBabel 自体は WindowsMac にも対応した OSS で、GPS データ形式の変換や GPS からの情報の読み書きを行うソフトです。

トラックの抽出は、以下のコマンドで実行します。

$ gpsbabel -t -i garmin -f /dev/ttyUSB0 -o gpx -F track.gpx
オプション 説明
-t トラックの抽出を行う。他にはルート(-r)、ウェイポイント(-w)などが指定可能。
-i 入力ファイルタイプを指定。今回は Garmin GPS を入力として使用するので garmin を指定。
-f 入力ファイル名。今回は /dev/ttyUSB0 を指定。
-o 出力ファイルタイプを指定。今回は gpx を指定。
-F 出力ファイル名。

これで track.gpx として、トラック情報を取り出せます。GPSBabel は Google Earth で使われている KML 形式など、色々などファイルフォーマットを使用できますが、今回、位置情報を写真に記録する際に使用する gpscorrelate が gpx のみに対応しているので、gpx 形式を使用します。

gpscorrelate は、位置情報記録のための Linux 用プログラムです。debian testing だと、aptitude コマンドでインストールできますが、他のディストリビューションだとソースコードからインストールする必要があるでしょう。

gpscorrelate コマンドを実行する際は、先ほど取り出したトラックファイルや、UTC からの時差*1などのオプションを指定して実行します。

$ gpscorrelate -g track.gpx -M -z +9 *.jpg
オプション 説明
-g 使用するトラックファイル(GPX形式)を指定する。
-M 画像の変更日時(mtime)を修正しない。
-z GPSデータの時刻がUTCで記録されている場合に時間を修正する。日本では+9。
-t 位置情報が記録できなかった期間についても補完を行う。
-r 位置情報を削除する。

gpscorrelate は gpscorrelate-gui という GUI プログラムも同梱されているので、そちらを使用することも可能です。

さて、これで位置情報が記録されるわけですが、GPS が衛星を捕捉できなかった期間に撮られた写真については、位置情報は記録されません。もし、捕捉できていない期間の写真にも位置情報を追加する場合は、-t オプションを追加します。

$ gpscorrelate -g track.gpx -t -M -z +9 *.jpg

これでGPSが位置を記録していない期間についても位置情報を追加してくれますが、単純に線形補完で位置を決定しているため、かなり誤差の大きい位置情報が記録されることがあるので注意しましょう。

誤差が大きい場合等、写真についた位置情報を削除したい場合は -r オプションで gpscorrelate を使用します。

$ gpscorrelate -M -r *.jpg

こうして位置情報を追加した写真は、Panoramioなどの位置情報に関連したサービスを使う際に便利です。

*1:eTrexUTCで時刻を記録しています。

Linux ソフト RAID1 のハードディスクを取り替える

これまで自宅で使ってた Linux マシンは、ソフト RAID 機能を使って 500GB の HDD 2本で RAID1 アレイを構築していましたが、最近空き容量が少なくなってきたので 1TBの HDD で構築しなおすことにしました。

注意事項

以下のメモは、Debian testing (2008/05/25現在。カーネルは 2.6.24-1-amd64) での作業履歴です。
他のディストリビューションではそのまま使えない可能性があります。参考にする場合は自己責任でお願いします。

前提とする環境

これまでの RAID 構成は以下のとおり。

# cat /proc/mdstat
Personalities : [raid1]
md0 : active raid1 sda1[0] sdb1[1]
      16008640 blocks [2/2] [UU]

md1 : active raid1 sda3[0] sdb3[1]
      466366848 blocks [2/2] [UU]

パーティション構成は、16GB 領域(/dev/sd[ab]1)をシステム用(/)に確保して /dev/md0 に、スワップパーティション(/dev/sd[ab]2)を適当に確保、残り(/dev/sd[ab]3)をデータ用(/data)に /dev/md1 としています。

また、今回の作業方法としては、ハードディスクを一つずつ取り替える方法を使用してます。

準備

とりあえず、万一に備えて knoppix とかLinuxディストリビューションのインストールCDなど、レスキューCDとして使用できるものを用意しておきます。

また、/proc/mdstat を見て、RAID1 アレイの sync が完了しているかどうかを確認しておきます。

sync が完了しているかどうかは、mdadm コマンドでも確認することが出来ます。recovery や resync が実行されている状態では次のコマンドは復帰せず、ブロックされます。

# mdadm --wait /dev/md0

1. /dev/sdb をアレイから取り除く

2番目のハードディスク /dev/sdb に関連するパーティションをアレイから除去します。まず mdadm コマンドの --fail オプションで sync されないようにして、remove オプションでアレイから取り除きます。

# mdadm /dev/md0 --manage --fail /dev/sdb1
# mdadm /dev/md0 --manage --remove /dev/sdb1

残りのアレイ(/dev/md1)に関しても同様に作業を行って、/dev/sdb に属するパーティションは全てアレイから取り除きます。

# mdadm /dev/md1 --manage --fail /dev/sdb3
# mdadm /dev/md1 --manage --remove /dev/sdb3

2. シャットダウンとハードディスク取り替える

一旦電源を落として、/dev/sdb の HDD を新しいものに取り替えます。

3. 新しい /dev/sdb のパーティションを設定する

マシンの電源を入れて、新しい HDD が認識されていることを確認したら、/dev/sdb のパーティション設定を行います。

/dev/sda のパーティション情報を参考にしつつ、RAID1 用パーティションが古いものよりサイズが大きくなるように、fdisk コマンドで新しい /dev/sdb のパーティション設定を行います。今回は、システム用の /dev/sdb1 とスワップ用の /dev/sdb2 は前回と同じ、残りは全てデータ用の /dev/sdb3 とします。

# fdisk /dev/sdb

なお、ディスクのサイズやジオメトリが全く変わらないものに取り替える場合は、sfdisk コマンドを使って、/dev/sda のパーティション情報をそのまま /dev/sdb に適用させることが出来ます。今回は /dev/sdb1 と /dev/sdb2 は以前と同じ容量を使用するので、sfdisk でパーティションを設定してから、fdisk で修正しました。

# sfdisk -d /dev/sda | sfdisk /dev/sdb

ただ、sfdisk コマンドはパーティション情報を書き換える前に確認がないので、入力間違いが致命的な問題を引き起こす可能性があります。安全を重視するなら、一旦パーティション情報を USB メモリなどにバックアップしておくなり、fdisk コマンドを使うなりしたほうが良いでしょう。

パーティションの編集が終わったら、partprobe コマンド*1を実行して、カーネルに対して /dev/sdb のパーティションを再認識させておきます。

# partprobe /dev/sdb

4. 新しい /dev/sdbをアレイに追加する

新しく作成したパーティションRAIDアレイに追加します。

# mdadm /dev/md0 --manage --add /dev/sdb1

追加と同時にアレイの recovery が開始されます。recovery 中は常に I/O が行われているため、システムはかなり重くなります。気長に待ちましょう。

ちなみに --wait オプションを利用すると、recovery が終了したことを通知させることが可能です。

# mdadm --wait /dev/md0 && mplayer /usr/share/sounds/KDE_Notify.wav

他のアレイについても、同様に追加します。

# mdadm /dev/md1 --manage --add /dev/sdb3
# mdadm --wait /dev/md1 && mplayer /usr/share/sounds/KDE_Notify.wav

5. /dev/sdb に grub をインストールする

/dev/sdb が新しいディスクに取り替えられたので、次は /dev/sda を取り替えることになりますが、このときに問題になるのが、ブートローダをどうするか。

普通の Linux システムだと、/dev/sda のブートセクタに grub がインストールされているので、/dev/sda を新しい HDD と取り替えると当然マシンが起動しなくなってしまいます。直接 grub を利用できる起動用ディスクがあればそれを使えば良いですが、今回は /dev/sdb に grub をインストールして、/dev/sdb から起動するようにしてみます。

しかし、/dev/sdb のブートセクタへの grub のインストールは、通常使われる grub-install コマンドを使ってもうまく行かない*2ので、grub コマンドを実行して、grub シェルに入ってから、grub 組み込みの install コマンドを使用します。

# grub
grub> install /boot/grub/stage1 (hd1) /boot/grub/stage2

この方法でインストールすると、grub メニュー起動時のみ、/dev/sdb が (hd0) と認識されるようで、menu.lst を編集する必要もありませんでした。

6. /dev/sda を取り替える

/dev/sda の取り替えは /dev/sdb の時と流れそのものは変わりませんが、/dev/sdb となっていたところが /dev/sda となるので、入力ミスには気をつけましょう。

まず、failオプションとremove オプションでアレイから除去。

# mdadm /dev/md0 --manage --fail /dev/sda1
# mdadm /dev/md0 --manage --remove /dev/sda1
# mdadm /dev/md1 --manage --fail /dev/sda3
# mdadm /dev/md1 --manage --remove /dev/sda3

一旦システムを停止させて /dev/sda を取り替えて起動します。5.の設定がうまく行っていれば、問題なく起動するはず。うまく行かなかった場合は、準備したレスキューCDなどから立ち上げます。

新しい /dev/sda が認識されていることを確認したら、パーティション設定を行います。この時点で /dev/sda と /dev/sdb が両方新しい HDD に取り替えられ、同じディスクになっているので、sfdisk コマンドでそのままパーティションを設定できます。

# sfdisk -d /dev/sdb | sfdisk /dev/sda
# partprobe /dev/sda

パーティションをアレイに追加して、sync が完了するのを待ちましょう。

# mdadm /dev/md0 --manage --add /dev/sda1
# mdadm --wait /dev/md0 && mplayer /usr/share/sounds/KDE_Notify.wav
# mdadm /dev/md1 --manage --add /dev/sda3
# mdadm --wait /dev/md1 && mplayer /usr/share/sounds/KDE_Notify.wav

新しい /dev/sda のブートセクタは当然ながら空なので、grub をインストールするのを忘れずに。/dev/sda に対しては、grub-install コマンドで大丈夫です。

# grub-install /dev/sda

7. アレイとファイルシステムを拡大する

ここまでの作業でディスクの入れ替えは完了しました。ただし、パーティションサイズを増やした /dev/md1 は、アレイとファイルシステムは以前と同じ大きさのまま(今回の場合、400GB程度)となっているので、これを拡大させます。

RAID アレイの拡大は、--grow オプションを使用します。

# mdadm --grow /dev/md1 --size=max

ext3 ファイルシステムの拡大は、resize2fs コマンドで行います。
こちらはそれなりに時間がかかります。

# resize2fs -p /dev/md1
resize2fs 1.40.8 (13-Mar-2008)
Filesystem at /dev/md1 is mounted on /mnt/md1; on-line resizing required
old desc_blocks = 28, new_desc_blocks = 57
Performing an on-line resize of /dev/md1 to 238685712 (4k) blocks.
The filesystem on /dev/md1 is now 238685712 blocks long.

なおファイルシステムの拡大を行うと、RAID 1 アレイの resync が自動的に実行されます。これの完了をもって、作業は終了となります。

# cat /proc/mdstat
Personalities : [raid1]
md0 : active raid1 sda1[0] sdb1[1]
      16008640 blocks [2/2] [UU]

md1 : active raid1 sda3[0] sdb3[1]
      954742848 blocks [2/2] [UU]

補足

作業中のミスによってデータが消えた場合は、RAID1だと取り外したディスクからデータをサルベージすることができます。以下のようにマウントして、データを拾いましょう。

# mount -t ext3 -o ro /dev/sdc3 /mnt/sdc3

*1:parted パッケージに収録されている

*2:ブート時にstage1の読み込みに失敗する

/etc/shadow から Basic認証パスワードファイルを作成する

apache の Basic 認証に使用するパスワードファイルは、通常 htpasswd コマンドで作成しますが、Linux などで使用されている /etc/shadow ファイルから作成することも可能です。

例えば、h2onda ユーザのパスワードを Basic 認証で使用するには、以下のコマンドでユーザ名とハッシュ化されたパスワードを抽出し、パスワードファイルに追加します。

# cut -d : -f 1,2 /etc/shadow |grep ^h2onda >> htpasswd_file

この方法が使用出来るのは、Basic 認証の際に読み込んだハッシュ化パスワード文字列が、apache 独自の MD5*1 あるいは SHA*2 ハッシュ化パスワードではないと判別されると、Unix のログインパスワードの暗号化に使用されている crypt() 関数で処理されるためです。

しかし、この方法は Basic 認証とシステムのログインに使用しているパスワードを簡単に統一することが出来て便利ではありますが、そもそも /etc/passwd と /etc/shadow がなぜ分離されるようになったのかということを考えると、Basic 認証パスワードファイルに /etc/shadow と同じ中身が書かれているというのは、セキュリティの面で問題のある方法だということは意識しておくべきでしょう*3

*1:ハッシュ化パスワードが"$apr1$"から始まる文字列の場合。htpasswd -m コマンドで作成される。

*2:ハッシュ化パスワードが"{SHA}"から始まる文字列の場合。htpasswd -s コマンドで作成される。

*3:そもそも Basic 認証が…とか、htpasswd がデフォルトで使うのが DES アルゴリズムだとかいう問題もありますが…。

OpenSSH を rssh で sftp 専用にする

ネットワーク上でファイルの転送を行う用途には、昔から ftp が使用されてきました。しかし ftp にはパスワードやデータのやりとりが暗号化されていないというセキュリティ上の問題があるため、最近では anonymous ftp 以外の用途では使用されなくなってきていて、その代わりとして SSH を使用する scp や sftp が使用されることがあります。

ただ、SSH は元々 Telnet の代わりにリモートシェルを扱うためのプロトコルとして作られたため、ファイル転送機能だけが必要な場合は、本来のリモートシェルの機能がセキュリティの面で問題となります*1

そこで、シェルを使用させずに scp や sftp のみ許可するために作られたのが rssh (日本語ページ)です。

rssh は制限付きシェルとして実装されており、OpenSSH を直接変更する必要がありません。また chroot も可能なので、安全にファイル転送サービスを提供することができます*2

以下は、Debian lenny 環境において sftp,scp サービスを chroot で提供する場合の設定例です。

許可するファイル転送サービスの設定

設定ファイル /etc/rssh.conf でコメントアウトされている allow* のうち、許可するサービスを有効にします。

alloscp
allosftp
#allowcvs
#allowdist
#allowsync

umask の設定

umask はデフォルトで 022 が設定されています。必要があれば設定ファイルを変更します。

umask 022

chroot の設定

chroot を行うパスを設定ファイルに記入します。

chrootpath = "/srv/rssh_root"

chroot 環境で最小限必要なバイナリやライブラリ等のコピーは、rssh に添付されている mkchroot.sh が行ってくれます。

# mkdir /srv/rssh_root
# /usr/share/doc/rssh/examples/mkchroot.sh /srv/rssh_root

chroot 環境のログが取得できるように、syslogd に追加のソケットを指定します。

# syslogd -a /srv/rssh_root/dev/log

ユーザ毎の設定

ユーザ毎に、許可するサービス・umask・chroot パスを設定することも可能です。以下は、h2onda ユーザに scp,sftp を許可し、/srv/rssh_root に chroot する設定です。詳細は man rssh.conf を参照。

user = "h2onda:022:00011:/srv/rssh_root"

ユーザの作成

ユーザを作成します。ホームディレクトリが chroot パスの配下に、シェルを rssh に設定します。以下は Debian でのユーザ追加の例なので、Red Hat 等では適宜変更してください。

# adduser --home /srv/rssh_root/home/h2onda --shell /usr/bin/rssh h2onda


これで、h2onda ユーザは chroot で隔離され、scp と sftp のみが実行可能になります。

ただ、自分が試した Debian lenny の rssh パッケージは、ログイン時に chroot を実行する rssh_chroot_helper コマンドに suid ビットが立っていなかったため、ログイン直後にセッションが切断される問題が発生しました。他の環境で発生するかどうかは分かりませんが、ご注意を。

# chmod u+s /usr/lib/rssh/rssh_chroot_helper


また、本家のページにも書かれていますが、Windows でよく使用される WinSCP は、scp 実行時に内部で別のコマンドも実行しているらしく、rssh を使用した環境では使用できないようです。sftp モードで使用するか、Filezilla を使いましょう。

*1:商用 SSH には、scp や sftp のみを使用するための機能があるようですが。

*2:そういえば、OpenSSH Chroot Patchなんてものもありましたね…。

Vimからはてなハイクに投稿する

vimスクリプトに興味があったので、試しにhatena.vimはてなハイク対応に改造してみました。

以下のコードを plugin/hatena.vim に追加すると、はてなハイクに投稿可能になります。

" はてなにログインし、ハイクを編集する
" Usage:
"   :HaikuEdit [<keyword>]
command! -nargs=? HaikuEdit            call <SID>HaikuEdit('<args>')

" :HaikuEdit で開いたバッファの内容をはてなに送信し、ハイクを投稿する
" Usage:
"   :HaikuPost
"command! -nargs=? HaikuPost            call <SID>HaikuPost(<args>)

function! s:HaikuEdit(...) " 編集する

    " ログイン
    if !exists('b:hatena_login_info')
        let hatena_login_info = s:HatenaLogin()
        if !len(hatena_login_info)
            return
        endif
    else
        let hatena_login_info = b:hatena_login_info
    endif

    let [base_url, user, cookie_file] = hatena_login_info
    let base_url = "http://h.hatena.ne.jp"


    "  キーワードを取得
    if a:0 > 0
        let word = a:1
    else
        let word = input('Keyword: ')
    endif

    if !strlen(word)
        echoerr 'キーワードを入力してください'
        return
    endif

    " セッション(編集バッファ)を作成
    let tmpfile = tempname()
    execute g:hatena_edit_command tmpfile
    setlocal noswapfile
    let &fileencoding = 'utf-8'

    let content = system(s:curl_cmd . ' "' . base_url . '" -b "' . cookie_file . '"')
    let b:rkm = matchstr(content, 'name="rkm"\s*value="\zs[^"]*\ze"')

    if !strlen(b:rkm)
        echoerr 'ログインできませんでした'
        if exists('s:user')
            unlet s:user
        endif
        return
    endif

    let b:word = word

    autocmd BufWritePost <buffer> call s:HaikuPost() | autocmd! BufWritePost <buffer>
    autocmd WinLeave <buffer> let &titlestring = b:prev_titlestring
    autocmd WinEnter <buffer> let &titlestring = 'Haiku!: ' . b:word . ' [' . b:hatena_login_info[1] . ']'

endfunction

function! s:HaikuPost(...) " 投稿する

    if !exists('b:word') || !exists('b:rkm')
        echoerr ':HaikuEdit してから :HaikuPost して下さい'
        return
    endif

    " ログイン
    if !exists('b:hatena_login_info')
        let hatena_login_info = s:HatenaLogin()
        if !len(hatena_login_info)
            return
        endif
    else
        let hatena_login_info = b:hatena_login_info
    endif

    let [base_url, user, cookie_file] = hatena_login_info
    let base_url = "http://h.hatena.ne.jp"

    if &modified
        write
    endif
    let body_file = expand('%')

    let post_data = ' -F rkm=' . b:rkm . ' -F "word=' . b:word
                    \ . '" -F "location=/' . user . '/" -F "body=<'
                    \ . body_file . '"'

    " ポスト
    let result = system(s:curl_cmd . ' ' . base_url . '/entry -b "' . cookie_file . '"' . post_data . ' -D -')

    if v:shell_error
        echo '投稿に失敗しました'
    else
        echo '投稿しました'
    endif

endfunction

まあ、ほぼ日記投稿部分からのコピぺだったりしますが。

それでも、バッファ変数(b:*)の有効範囲を理解してなかったので、b:wordを定義する位置がおかしくてしばらくハマりました。

あと、

:HaikuEdit キーワード

という使い方が出来るようにしようと思ったんですが、キーワードが日本語だとエラーになるので、引数を無効にしてプロンプトからの入力のみにしてます。一応ダブルクォーテーションで囲っておくとうまく行くのは分かってますが、いちいちそうするのも面倒なので。

追記

Vimからはてなフォトライフに画像を投稿するvimスクリプトを書いているときに、HaikuEditの引数がうまく動作しなかった原因に気付いたので、本文のスクリプトを修正しておきました。command登録時に引数をシングルクォーテーションで囲っておくと引数が文字列として渡されるようです。