メールアドレスの正規表現

たまにメールアドレスの形式を正規表現で表すのは不可能とかというのを目にするのですが、そんなことはありません。入れ子がなければたいていの文字列の形式は正規表現で表すことができます。

ということで、RFC5321, 5322 からメールアドレスの正規表現を書いてみました。

/\A([0-9a-z!\#$%&'*+\-\/=?^_`{|}~]+(\.[0-9a-z!\#$%&'*+\-\/=?^_`{|}~]+)*|\"([\x20\x21\x23-\x5b\x5d-\x7e]|\\[\x20-\x7e])*\")@[0-9a-z]([0-9a-z-]*[0-9a-z])?(\.[0-9a-z]([0-9a-z-]*[0-9a-z])?)*\z/i

ちょっと長いですけど、最近の Ruby だと (?<hoge>)\g<hoge> を使うことで、同じ正規表現の繰り返しを簡単に書くことができるので、それを使って書きなおしてみます。

/\A((?<atom>[0-9a-z!\#$%&'*+\-\/=?^_`{|}~]+)(\.\g<atom>)*|\"([\x20\x21\x23-\x5b\x5d-\x7e]|\\[\x20-\x7e])*\")@(?<sub_domain>[0-9a-z]([0-9a-z-]*[0-9a-z])?)(\.\g<sub_domain>)*\z/i

\g を使えば入れ子も表現することができるみたいですが、やったことはありません。

わかりにくいので改行&インデントしてコメントを入れて見ました。正規表現リテラルに x オプションを入れるとこのような表記ができます。

\g や x オプションが Ruby 以外で使えるかどうかは知りません。

/\A
  # local-part
  (  # dot-atom
     (?<atom>[0-9a-z!\#$%&'*+\-\/=?^_`{|}~]+)
     (\.\g<atom>)*
  |  # quoted-string
    \"([\x20\x21\x23-\x5b\x5d-\x7e]
       |\\[\x20-\x7e])*\"
  )@
  # domain
  (?<sub_domain>[0-9a-z]([0-9a-z-]*[0-9a-z])?)
  (\.\g<sub_domain>)*
\z/ix

これで少しはわかりやすくなったとは思いますが、さすがに暗記するのは難しいです。

ということで、この正規表現を印刷したTシャツやマグカップなどを作ってみました。これがあればいつでもメールアドレス形式のチェックができますね!

[追記]

「入れ子がなければたいていの文字列の形式は正規表現で表すことができます」と書いたんですけど、文字数とかの制約も表せませんでした。

RFC5321 では local-part の最大長は 64, domain の最大長は 255, メールアドレス全体の最大長は 256 という制約がありますが、これは正規表現で表すことはできません。

…と思ったのですが、Twitter で教えてもらって、lookahead (?=...) を使えば文字数制限も指定することができました。

sub-domain が最大63文字というのにも対応してます。

/\A
  # 全体で256文字以下
  (?=.{,256}\z)
  # local-partは64文字以下でdomainは255文字以下
  (?=.{,64}@.{,255}\z)
  # local-part
  (  # dot-atom
     (?<atom>[0-9a-z!\#$%&'*+\-\/=?^_`{|}~]+)
     (\.\g<atom>)*
  |  # quoted-string
    \"([\x09\x20\x21\x23-\x5b\x5d-\x7e]
       |\\[\x09\x20-\x7e])*\"
  )@
  # domain
  (?<sub_domain>[0-9a-z]([0-9a-z-]{,61}[0-9a-z])?)
  (\.\g<sub_domain>)*
\z/ix

しかし、こんなネタで何故かブクマが200超えてるんですけど、世の中何がウケるかわかりませんな。

反応している人はだいたい、

  1. ヘッダの From や To フィールドをパースしたい人
  2. メールアドレス登録時にバリデーションしたい人
  3. 日本語メールアドレスは?
  4. Tシャツいかす!

に分類できるみたいです。

ちなみに私はTシャツのネタ以上のことは考えてなかったので 4 です。

コメント(CFWS)とか表示名(display-name)が気になる人はきっと 1 なんでしょう。

ふつうにメールアドレスと言った時には、コメントや表示名は含まないと思うので、今回はそれは考慮しませんでした。

ヘッダの From や To フィールドをパースしようと思ったら流石に正規表現じゃ無理だと思います。コメントは入れ子だし、グループ(group)もありますし。

ケータイにありがちなピリオドの連続とかの変なアドレスが気になる人は 2 ですね。

私も以前は「ピリオド連続ローカルパートとか死ねばいいのに」と思ってたのですが、最近は考えが変わりました。

ローカルパートに特殊な記号とかピリオドの連続を含めたい場合は「"」で括れば可能です。でもそれはプロトコルの都合なので、人間様がそれに合わせてやる必要はありません。アプリが頑張ればいいんです。

たとえば、「@」の左側をローカルパートとみなして、ローカルパートに「"」で括らないと使えないような文字の並びがあれば、アプリ側で quoted-string 形式に変換してやればいいだけの話です。

日本語メールアドレスは RFC 6530〜6533 あたりを見ればいいんでしょうけど、まだちゃんと読んでません。まだ当分は普及しないんじゃないかと思ってます。

Tシャツとその他のグッズは suzuri で作りました。suzuri はお気楽でいいですね。suzuri についてはまた今度書きます。

Dockerイメージを作る (NSEG#54)

NSEGの第54回勉強会で「Dockerイメージを作る」という発表をしました。

(SlideShare にもアップロードしました)

スライドにも書きましたが、前回以降にやったことの自分用のまとめみたいな感じで、世間的に目新しい話題は特に無いです。

今回も会場はケイケンさんの会議室でした。前の日まで参加申し込み5人だったのですが、当日の参加者は10人でした。

他の人の発表

@hiro345さんの Frontend framework and Template

先週末の HTML5時代のフロントエンド開発入門でも思ったんですけど、最近のフロントまわりの技術にはさっぱりついていけてなくて良くないです。

@earth2001yさんの「昨日、ぺちゃくちゃで NSEG を喋りました」

定期的に開催されている ぺちゃくちゃないとNAGANOの話。 ぺちゃくちゃないとNAGANOは、スライドは20枚固定で 20秒/枚 で勝手にスライドが切り替わっていて、それに合わせてしゃべるという珍しい形式のプレゼンです。発表難しそう。

@office_krtecさんの「失敗して学ぶ炎上プロジェクトからの生還(案)」

SIerの闇の話。失敗し続けてもオフショアに発注し続ける会社と、炎上案件の火消しをし続ける office_krtec さんの話でした。

オフショアを使わないのが一番いいと誰もがわかりながらも、政治的な理由で使わざるを得ないというのが涙を誘います。もう、お金払うからオフショアの人には何もしないでもらって、こっち側でちゃんと作った方が結果的に安くつくんじゃないかとも思いました。

@_iwateさんの「日本一雲に近い Azure User Group の話」

9/27 に JAZUG4周年記念! どこよりも雲に近い長野でするクラウドの話 - Azureしなの | Doorkeeper というイベントをやるという話です。 あと隔週で水曜日にGEEKLAB. NAGANOで「Azure もくもく会」が開催されているとのこと。興味のある人は行ってみるのがいいと思います。

@nabetaroさんの「deb パッケージを再構築」

既存の deb パッケージを流用して、新しい deb パッケージを作る方法の解説でした。pdebuild 便利そうです。

次回

次回の日程は未定です。9月の中頃の土日だと思われます。

懇親会

C-one の地下の「だんまや水産」に行きました。

相手がいないのに ESTABLISHED になってる TCP ポート

最近 ParallelServer というライブラリを作ったのですが、その最中に奇妙な状態になってる TCP ポートを見つけたので、メモっておきます。

Ruby では TCP サーバーは次のような感じで作ることができます。お手軽ですね。

require 'socket'

Socket.tcp_server_loop(12345) do |socket, client_addr|
  socket.puts "Your IP address: #{client_addr.ip_address}"
  name = socket.gets
  socket.puts "Hello, #{name}"
  socket.close
end

これは 12345 ポートでクライアントからの接続を待ち、接続されたらクライアントのIPアドレスとクライアントからの入力をクライアントに送信して切断するだけの簡単なプログラムです。

~% nc -v localhost 12345
Connection to localhost 12345 port [tcp/*] succeeded!
Your IP address: 127.0.0.1
hogehoge
Hello, hogehoge
~% 

サーバープログラム起動直後の 12345 ポートの状態は次のようになります。

~% netstat -an | grep 12345
tcp        0      0 0.0.0.0:12345           0.0.0.0:*               LISTEN     
tcp6       0      0 :::12345                :::*                    LISTEN     

IPv4 と IPv6 の両方で LISTEN 状態です。

クライアントから接続すると次のように ESTABLISHED になります。

~% netstat -an | grep 12345
tcp        0      0 0.0.0.0:12345           0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:12345         127.0.0.1:56863         ESTABLISHED
tcp        0      0 127.0.0.1:56863         127.0.0.1:12345         ESTABLISHED
tcp6       0      0 :::12345                :::*                    LISTEN     

サーバーの 12345 とクライアントの 56863 が接続されている状態です。 同じサーバー内で接続しているため、1つの接続について、サーバーからみた接続とクライアントから見た接続の2行出力されています。

このように普通は接続が確立したらサーバーとクライアントの両方が ESTABLISHED になります。

ここで次のクライアントプログラムを動かしてみます。

require 'socket'

sockets = []
200.times do |i|
  p i
  sockets.push TCPSocket.new("localhost", 12345)
end
sleep

サーバーに 200接続したまま何もしないプログラムです。

サーバーは最初の接続からのクライアントの入力を待つので、2個め以降の接続を処理できません。

普通のサーバープログラムは、そんなことにならないように、クライアントからの接続を受け付けた時に fork したりスレッドを作ったりするのですが、ここでは意図的にこのようにしてます。

この状態で netstat を見ると、サーバー側にいくつか SYN_RECV 状態のポートがあります。

tcp        0      0 127.0.0.1:12345         127.0.0.1:36807         ESTABLISHED
tcp        0      0 127.0.0.1:12345         127.0.0.1:36930         SYN_RECV       ← これ
tcp        0      0 127.0.0.1:12345         127.0.0.1:36953         SYN_RECV       ← これ
tcp        0      0 127.0.0.1:36807         127.0.0.1:12345         ESTABLISHED
tcp        0      0 127.0.0.1:36930         127.0.0.1:12345         ESTABLISHED    ← これ
tcp        0      0 127.0.0.1:36953         127.0.0.1:12345         ESTABLISHED    ← これ

クライアントポート 36807 はちゃんと対向するサーバーが ESTABLISHED になっているのですが、36930, 36953 ポートは SYN_RECV になっています。

しばらくすると、この SYN_RECV 状態のポートはなくなります。といっても ESTABLISHED になったわけではなく、消えてなくなってしまいます。

tcp        0      0 127.0.0.1:12345         127.0.0.1:36807         ESTABLISHED
tcp        0      0 127.0.0.1:36807         127.0.0.1:12345         ESTABLISHED
tcp        0      0 127.0.0.1:36930         127.0.0.1:12345         ESTABLISHED    ← これ
tcp        0      0 127.0.0.1:36953         127.0.0.1:12345         ESTABLISHED    ← これ

クライアントは ESTABLISHED だと思っているのに接続相手のサーバーはいません。

サーバープログラムを停止しても状態は変わりません。 クライアントプログラムを停止しない限り残ったままです。

tcp        0      0 127.0.0.1:36930         127.0.0.1:12345         ESTABLISHED    ← これ
tcp        0      0 127.0.0.1:36953         127.0.0.1:12345         ESTABLISHED    ← これ

このように 200 くらい接続すると SYN_RECV が確認できるのですけど、どうやらこれは listen のバックログの値に関連してるようです。

Socket.tcp_server_loop は内部で listen(Socket::SOMAXCONN) しています。手元の Linux では SOMAXCONN は 128 になっていました。

もっと単純に次のようなサーバープログラムで試してみました。

require 'socket'

TCPServer.new(12345).listen(5)
sleep

このようにバックログを 5 にすると、クライアント接続が 10 くらいでも再現できます。

なお、listen(Socket::SOMAXCONN) しているのは Ruby 2.0 以降です。Ruby 1.9.3 では listen(5) としているので、すぐに発生します。

で、結局こんなことになってしまう理由はわかりませんでした。

クライアントが ESTABLISHED だと思ってるのに SYN_RECV になってるのもわからないし、相手がいないのに ESTABLISHED になってるソケットが存在してるのもわかりません。 ネットワークの向こうの別のサーバであればパケット落ちとかでこのような状況になるのもわかるのですけど。

バックログを超えたら SYN_RECV にならなくてもいいと思うんですけど…。 TCP/IP はそういうもんなんですかね。

[追記]

Facebook の方で色々教えてもらいました。

OS X では発生しないようです。うすうす感じていたのですがやはり Linux の実装の問題のようです。

教えてもらったブログ http://veithen.blogspot.jp/2014/01/how-tcp-backlog-works-in-linux.html に答えがありました。

BSD では接続用のキューが1つで、キューがいっぱいの場合はクライアントからの SYN を破棄するだけなので、クライアントが ESTABLISHED になることはありません。私の知ってる TCP の動きです。

Linux では SYN キューと accept キューの2つがあるようで、SYN キューに入った時に SYN ACK を返し SYN_RECV 状態になり、accept キューに入った時に ESTABLISHED になるようです。

サーバープログラムが accept しなければ、accept キューがいっぱいになり SYN キューも掃けません。SYN キューにたまったものは一定時間たつと削除されるようです。

SYN キューの大きさは、sysctl net.ipv4.tcp_max_syn_backlog で、accept キューの大きさはプログラムから指定する listen のバックログです。

sysctl -w net.ipv4.tcp_max_syn_backlog=0 にして試してみたら BSD の動きに近くなりました。ただし 0 に設定しても1個は SYN_RECV になってしまうようです。

また sysctl -w net.ipv4.tcp_abort_on_overflow=1 にすると accept キューがあふれた場合、クライアントからの SYN に RST を返すようです。この場合はクライアントプログラムは connect(2) に対して ECONNRESET エラーが返ります。

RST じゃなくて SYN を無視するようになれば BSD と同じになるんじゃないかと思ったのですが、そういうパラメータは無さそうでした。

まあ別に BSD と同じ動きにしたかったわけではなく、ただ疑問に思っただけだったので、それは解決したのでもういいです。

#NSEG 第53回勉強会

NSEG 第53回勉強会に参加しました。 今回も会場はケイケンさんです。

今回のテーマは JavaScript でした。

自分が JavaScript を触り始めたのは結構前なんですけど、未だにちゃんと書ける自信がありません。

「モダンな」JavaScript の書き方 / 春原さん

  • HTML と JavaScript を分離しよう
  • 名前空間を汚さないように
  • jQuery を使おう

という話でした。

OpenLayersで地図表示 / 鍋太郎さん

スライド

HTMLで地図を表示できるライブラリの話でした。お手軽に使えそうです。

TypeScript ではじめる AltJS / 谷口さん

各種 AltJS の特徴と TypeScript の話でした。

TypeScript 良さそうです。

クソゲーに最適な JSフレームワーク、 作りました。 / ほくさん

スライド

クソゲーWebアプリを簡単に作れる自作フレームワークの「ほくじー」の紹介でした。

デモが見れます http://hoku.in/hokug/

NSEG新Webサイトについて / まちださん

スライド

新Webサイト → http://nseg-jp.github.io/w/

うぉー、ちょーカッコいいですね。

スライドもカッコいいです。reveal.js という HTML のプレゼンツールっぽいです。

懇親会

懇親会は長野駅前の「やきとり道場 翠や」でした。

ハイボールが200円で安い!

JAWS-UG 長野支部 第1回勉強会

先月、JAWS-UG 長野支部が発足しましたが、その第1回勉強会があったので参加してきました。

会場は NSEG にも良く会場を提供して頂いてるケイケンさんの会議室でした。長野駅から徒歩数分で便利です。電源も WiFi もあってすばらしいです。

AWS のアカウント取得からウェブサーバーを起動するまでのハンズオンでした。

AWS はまったく使ったことがなかったので良い機会でした。

感想等

  • EC2は簡単だった。普通の VPS っぽい。
  • サービス多すぎてよくわからない。
  • 1年間無料で使えるのは知らなかった。…けど何がどれくらい無料なのか良くわからなかった。
  • 日本語のページもあるけど、基本は英語らしい。
  • ユーザー登録フォームが日本語だったので、日本語で氏名と住所入れてみた。登録できたけど、あとでプロフィール見てみたら文字化けしてた。
  • reboot してみたけど IPアドレスは変わらなかった(たまたま?)。
  • stop & start したら IPアドレス変わった。ディスクイメージは残ってた。なぜか消えると思ってた。EBS というのが関係してる? よくわからない。
  • 固定IPアドレスは1インスタンスにつき1個無料らしい。インスタンスに紐付けないままでいると有料らしい。

よくわかってないからかもしれませんけど、下手すると膨大な料金が課金されそうで怖いです。 なんとなく、さくらのクラウドの方が安心感あります。さくらのクラウドも使ったことないのですけど。

懇親会の会場はすぐ近くの COLORFUL でした。大画面プロジェクターでサッカーの試合が上映されてました。オシャレ空間でおっさんだらけの懇親会。

グラスは長野のサッカークラブのAC長野パルセイロのマークが入ってました。

f:id:tmtms:20140706181404j:plain

スクロールバーの矢印を表示する

最近は Xubuntu を使ってますが、テーマによるのかもしれませんが、スクロールバーの端の矢印が表示されなくなってて不便なので表示する方法を調べました。

Gtk3

$HOME/.config/gtk-3.0/gtk.css を次の内容で作成

.scrollbar {
    -GtkScrollbar-has-backward-stepper: 1;
    -GtkScrollbar-has-forward-stepper: 1;
}

Gtk2

個人ごとに設定する方法はわかりませんでした。

/usr/share/themes/テーマ名/gtk-2.0/gtkrc の内容を変更

        GtkScrollbar            ::has-backward-stepper                  = 1
        GtkScrollbar            ::has-forward-stepper                   = 1
...
        GtkScrollbar::stepper-size      = 13

Ruby の Timeout.timeout に例外クラスを指定する時の注意

ちょっとハマったのでメモ。

Ruby の Timeout ライブラリを使うと、一定の時間が過ぎても終わらない処理を中断することができます。

require 'timeout'

def hoge
  sleep
end

def main
  Timeout.timeout(3) do
    hoge
  end
rescue Timeout::Error => e
  p 'main: timeout', e.backtrace.first
end

main

このスクリプトを実行すると3秒たってから終了します。

% ruby t1.rb
"main: timeout"
"t1.rb:4:in `sleep'"

バックトレースから 4行目の sleep 中でタイムアウトが発生したことがわかります。

次に Timeout.timeout を入れ子にしてみます。

require 'timeout'

def hoge
  Timeout.timeout(5) do
    sleep
  end
rescue Timeout::Error => e
  p 'hoge: timeout', e.backtrace.first
end

def main
  Timeout.timeout(3) do
    hoge
  end
rescue Timeout::Error => e
  p 'main: timeout', e.backtrace.first
end

main

main 中のタイムアウトは3秒で、hoge 中のタイムアウトは5秒です。 ですので sleep 実行中に main のタイムアウトが発生します。 この場合 main と hoge のどちらの rescue で Timeout::Error をキャッチできるのでしょうか。 プログラムを見ると sleep に近い hoge の rescue でキャッチできそうに思えますが、実行してみると次のようになります。

% ruby t2.rb
"main: timeout"
"t2.rb:5:in `sleep'"

正解は main の rescue でした。

使い勝手としてはこの方が望ましいでしょう。外側で指定されたタイムアウトが切れた場合に内側のタイムアウト処理が動いてしまっては混乱してしまいますし、使用しているライブラリが内部で Timeout を使用しているかどうかを調べないといけないというのは大変です。

Timeout ライブラリが内部でうまいことやって、利用者にとって自然な振る舞いになるような仕組みになっています。

ところで Timeout.timeout にはタイムアウト時に発生する例外クラスを指定することができます。 何も指定しないと上記のように Timeout::Error が発生します。

ところが、Timeout::Error を渡してみると動きが異なります。

require 'timeout'

def hoge
  Timeout.timeout(5) do
    sleep
  end
rescue Timeout::Error => e
  p 'hoge: timeout', e.backtrace.first
end

def main
  Timeout.timeout(3, Timeout::Error) do
    hoge
  end
rescue Timeout::Error => e
  p 'main: timeout', e.backtrace.first
end

main
% ruby t3.rb
"hoge: timeout"
"t3.rb:5:in `sleep'"

hoge の rescue が実行されてしまいました。

Timeout.timeout に例外クラスを指定した場合は、「Timeout ライブラリが内部でうまいことやってる仕組み」が働かず、そのまま指定した例外が発生するためです。

Timeout.timeout を入れ子にしてなくても、次のプログラムを実行すると、

require 'timeout'

def hoge
  sleep
rescue
  p '何か失敗した!'
end

class OreOreTimeout < StandardError
end

def main
  Timeout.timeout(3, OreOreTimeout) do
    hoge
  end
rescue OreOreTimeout
  p 'タイムアウト!'
end

main

「タイムアウト!」ではなく「何か失敗した!」が表示されます。

% ruby t4.rb
"何か失敗した!"

OreOreTimeout は StandardError のサブクラスなので rescue で拾われてしまうためです。 StandardError ではなく Exception のサブクラスにすれば rescue で拾われないため、「タイムアウト!」になります。

Timeout.timeout に例外クラスを指定する場合は注意しましょう。