komamitsu

OCamlで簡易SSLクライアント

2008-05-06 10:59:22

私は現在、プライベートでOCamlという言語にまったりと触れているのですが、その一環として「GMailにアクセスできるメールクライアント」を作成中です。「そのアプリ、本当に欲しいの?」と冷静に自問すると、本当のところそんなに欲しくないのですが、何かの言語を覚える際には、強制的にその言語で何かを作らせるのが私のやり方です。鬼軍曹のようです。

また、このメールクライアントを含め「OCaml縛り」中ですので、ちょっとしたツールなどはPerlやRubyでなく、OCamlで作成することになっています。つらいです。コンパイルエラーがとれません。標準ライブラリが結構物足りないです。Rubyが恋し…

気を取り直して、今回は練習としてOCamlで簡易SSLクライアントを作ってみようかと思います。

まず、OpenSSLを使うためのパッケージを入れます。FreeBSDの場合、security/ocaml-sslというportsを入れれば良いようです。ちなみにRuby1.8/1.9だと標準で入ってるんですよね(ruby/1.X/i386-freebsd6/openssl.so)、Ruby便利だ…

で、ocaml-sslを入れて、/usr/ports/security/ocaml-ssl/work/ocaml-ssl-0.4.1/docやexamplesを眺めた後、トップレベルで実験してみます(対話的に。#がプロンプトになります)。

# #use "topfind";;
- : unit = ()
Findlib has been successfully loaded. Additional directives:
#require "package";; to load a package
#list;; to list the available packages
#camlp4o;; to load camlp4 (standard syntax)
#camlp4r;; to load camlp4 (revised syntax)
#predicates "p,q,...";; to set these predicates
Topfind.reset();; to force that packages will be reloaded
#thread;; to enable threads
- : unit = ()
# #require "ssl";;
/usr/local/lib/ocaml/unix.cma: loaded
/usr/local/lib/ocaml/site-lib/ssl: added to search path
/usr/local/lib/ocaml/site-lib/ssl/ssl.cma: loaded
# open Unix;;
# open Ssl;;
# init ();;
- : unit = ()
# let hostent = gethostbyname "pop.gmail.com";;
val hostent : Unix.host_entry =
{h_name = "gmail-pop.l.google.com"; h_aliases = [|"pop.gmail.com"|];
h_addrtype = PF_INET; h_addr_list = [|; |]}
# Array.length hostent.h_addr_list;;
- : int = 2
# let addr = hostent.h_addr_list.(0);;
val addr : Unix.inet_addr =
# string_of_inet_addr addr;;
- : string = "209.85.199.111"
# let sock = Ssl.open_connection SSLv2 (ADDR_INET(addr, 995));;
val sock : Ssl.socket =

うむ、とりあえずSSL接続は成功したみたい。

と、すんなり成功したっぽく書いてスマートな風を装っているんだけど、実際のところ、Ssl.initを呼び忘れていて、

# let sock = Ssl.open_connection SSLv2 (ADDR_INET(addr, 995));;
Exception: Ssl.Context_error.
# get_error_string ();;
- : string = "error:140A90A1:lib(20):func(169):reason(161)"

$ openssl errstr 140A90A1
error:140A90A1:SSL routines:SSL_CTX_new:library has no ciphers

というエラーに悩まされていたので、/usr/ports/security/ocaml-ssl/work/ocaml-ssl-0.4.1/srcのssl_stubs.cとssl.mlを熟読したり、Cでlibssl.soを使って接続するサンプルを書いて挙動を比べてみたりと泥臭くやっていたのでした。Ssl.initの中で呼んでいるSSL_library_init()が呼ばれていなかったのでエラーになっていたようです。情けない…

とりあえず、ocaml-sslライブラリが使えることを確認したので、OCamlの練習がてらSSLクライアントを作ってみます。ファイル名は適当にmailan.mlとかにしてます。

open Unix

let ssl_conn host port cafile =
Ssl.init () ;
let hostent =
try gethostbyname host with
| Not_found -> failwith "gethostbyname" in
let addr = hostent.h_addr_list.(0) in
let sockaddr = ADDR_INET(addr, port) in
let ssl = (
let ctx = Ssl.create_context Ssl.SSLv23 Ssl.Client_context in
Ssl.load_verify_locations ctx cafile "";
Ssl.open_connection_with_context ctx sockaddr) in
Ssl.verify ssl;
ssl

let stdout_from_ssl ssl =
let bufsize = 1024 in
let buf = String.create bufsize in
let rec loop () =
let rlen =
try Ssl.read ssl buf 0 bufsize with
| Ssl.Read_error x -> 0 in
if rlen = 0 then Thread.exit ();
print_string (String.sub buf 0 rlen);
Pervasives.flush Pervasives.stdout;
loop () in
loop ()

let stdin_to_ssl ssl =
let bufsize = 1024 in
let buf = String.create bufsize in
let rec loop () =
let wlen = Unix.read Unix.stdin buf 0 bufsize in
if String.sub buf 0 4 = "QUIT" then ()
else (Ssl.output_string ssl (String.sub buf 0 wlen);
loop ()) in
loop ()

let main () =
let host = "pop.gmail.com" in
let port = 995 in
let cafile = "/usr/ports/security/ca-roots/files/ca-root.crt" in
let ssl = ssl_conn host port cafile in
let _ = Thread.create stdout_from_ssl ssl in
let _ = stdin_to_ssl ssl in
Ssl.shutdown_connection ssl
;;

let () = main ()

ところどころやっつけ仕事になっていますが、雰囲気をつかむためなので気にしないことにします。
で、以下のようなMakefileを作ってmakeと叩くと出来上がり。

mailan: mailan.ml
ocamlfind c -package ssl -thread -linkpkg -o $@ $?

ちなみにMakefileはOCamlMakefileをincludeしちゃうと簡単になるらしいのですが、今回は練習なのでべた書きで。

では、起動してみます。

$ ./mailan
+OK Gpop ready for requests from xxx.xxx.xxx.xxx xxxxxxxxxxxxxxxx.0
USER xxxxxxxx
+OK send PASS
PASS xxxxxxxx
+OK Welcome.
LIST
+OK 16 messages (66847 bytes)
1 1831
2 2175
3 1829
4 1828
5 2061
6 12662
7 3448
8 1912
9 2887
10 3465
11 3546
12 1923
13 3548
14 5162
15 4858
16 13712
.
RETR 3
+OK message follows
Delivered-To: xxxxxxxx@gmail.com


.
QUIT
$

とりあえず動きました。SSL接続周りは何とかなりそうな雰囲気です。後はPOP3/SMTPプロトコル用ライブラリを含むocamlnetを調べてみようかと思います。でも、もうこの辺で良いんじゃないか?と思う自分もいます。

OCamlらしく、ヴァリアントを利用して複雑なデータ構造を美しく定義したいんですが、いつになったらできるのでしょうか…

 

※このエントリは ブロガーにより投稿されたものです。朝日インタラクティブ および ZDNet Japan編集部の見解・意向を示すものではありません。
  • 2件のコメント
#1 ogasawara@itpl   2008-05-15 11:27:07
初めてコードを拝見しましたが、すばらしいコードだと思います。
OCamlプログラマとしてあえてコメントさせて頂くと、接続先やポート番号、証明書のパスなどをレコード型としてまとめた型を定義してやると、よりスマートかもしれないです。

このような紹介文が増えていく事を願っております。
#2 komamitsu   2008-05-15 22:40:19
ogasawaraさん

> 初めてコードを拝見しましたが、すばらしいコードだと思います。

現在、激しく照れております… :-)
ありがとうございます。

> レコード型としてまとめた型を定義してやると、よりスマートかもしれないです。

なるほど、そうですねぇ。

どうも、レコード型は「フィールド名が衝突して隠れてしまう」仕様が気になってしまい、
まだ、きちんと使ったことがないんですよねぇ(タプルとオブジェクトで凌ごうかと…)

でも、食わず嫌いは良くないので、これを機に使ってみようと思います。
  • 新着記事
  • 特集
  • ブログ