OCamlで簡易SSLクライアント

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

また、このメールクライアントを含め「OCaml縛り」中ですので、ちょっとしたツールなどはPerlRubyでなく、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らしく、ヴァリアントを利用して複雑なデータ構造を美しく定義したいんですが、いつになったらできるのでしょうか…