OCamlで簡易SSLクライアント
私は現在趣味で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らしく、ヴァリアントを利用して複雑なデータ構造を美しく定義したいんですが、いつになったらできるのでしょうか…