普通のJabberクライアントをRubyで作成するためには、 xmpp4rライブラリを使って数行のコードで完結します。
今回は外部のサーバにコードを配置するので、少し要件を加える事にしました。
- プログラマとパスワードを管理する人間は別であるという前提が存在する
- JabberIDのパスワードはスクリプトに埋め込まない
- パスワードが書かれたファイル単体が流出しても問題ないよう暗号化する
- 各ファイルは別の名前に変更して使う事ができるようにする
さらにTest::Unitフレームワークでテストができるように、コードの共通部分はモジュール(module YaJabber)の中に埋め込んでいます。
ファイルの構造
今回はRuby 1.9.2で標準ライブラリに加わっている点を考えて、ファイルフォーマットにJSONを使用しました。
ディレクトリ構造は以下のとおりで、各ファイルへは相対パスでアクセスするようになっています。
./sendmsg2j.rb ## コマンド本体 conf/sec_config.json ## デフォルトの共通鍵ファイル conf/config.json ## デフォルトの設定ファイル conf/gmail_config.json ## Gmail接続用見本 conf/jabber_config.json ## Jabber.org接続用見本 lib/xmpp4r ## jabberクライアント用ライブラリ
共通鍵ファイルの形式
{
"sec_text" : "5ee6f7e62b8b1de083bb2617738a4ade620f9c09"
}
接続用ID設定ファイルの形式
パスワードやJabberサーバへの接続情報は別ファイルとして扱っています
{
"user_name":"xxxxx@gmail.com/Batch",
"user_pass":"357150946dd3950f96af22f2f639bc4d",
"user_salt":"4e6c13d4aac544b2",
"server_name":"talk.google.com",
"server_port":"5222",
"remote_user":"xxxxx@gmail.com/Home",
"msg_subject":"Post message from VPS"
}
{
"user_name":"xxxxx@jabber.org/Batch",
"user_pass":"fcf53684962bc19f273e5a7c9bad257f",
"user_salt":"bf6123bf1e2c52b6",
"server_name":"jabber.org",
"server_port":"5222",
"remote_user":"xxxxx@jabber.org/Home",
"msg_subject":"Post message from VPS"
}
この2つの設定ファイルを送信したいIDによってコマンドオプションで設定ファイルを切り替えるか、デフォルトの設定ファイル config.json を上書きして使っています。
想定する使い方
初期化時のシナリオ
sec_config.jsonファイルの"sec_text" : の右辺を任意の文字列で上書きする。
{ "sec_text" : "この中を任意の文字列で書き換える" }
パスワード設定/変更時のシナリオ
- 新しいパスワードを準備する
- ./sendmsg2j.rb -eを実行する
- パスワードを入力し、出力された2行をconfig.jsonファイルの内容と差し替える
- テスト用メッセージを送信する
メッセージの送信のシナリオ
送りたいメッセージは標準入力から読み込むので、コマンドの出力などをそのまま送信できます。
echo "message" | ./sendmsg2j.rb
外部ファイルからメッセージを読み込む場合はリダイレクトで行ないます。
./sendmsg2j.rb < message_file.txt
設定ファイルの変更方法
'-c'オプションを使って、別のID設定情報を使う。
echo "message" | ./sendmsg2j.rb -c conf/gmail_config.json
共通鍵ファイル名を変更したい
'-s'オプションを使って、別の共有鍵データファイルを使う。
echo "message" | ./sendmsg2j.rb -c conf/gmail_config.json
まとめ
Jabberクライアント自体はちゃんと動いているのと、オンラインでいるユーザにメッセージを送信する手段としてはおもしろいと思います。
重要な情報であればメールと組み合せれば、メールサーバの滞留などに対する回避策にもなるでしょう。
問題だと感じたのは、設定ファイルの形式です。 けれど、これは別の話なので記事を分けました。
Appendix. スクリプト本体
いろいろコードをいじったので無駄に焼け太った感じがあります。
module内部のメソッドは関数型言語のようなイメージですが、綺麗に切り取られた印象もなく、かなりごちゃごちゃしています。
パスワードファイルとの分離のためにこうなってしまっただけで、xmpp4rの使い方としては特別な事はしていないのでご注意ください。
#!/usr/local/bin/ruby # -*- coding: utf-8 -*- # #= Yadiary Jabber Client Script # # This script assumes the following directory structures. # # ./sendmsg2j.rb ## main and module file # ./conf/sec_config.json ## the '-s' option changes the file name. # ./conf/config.json ## the '-c' option changes the file name. # # This is tested with talk.google.org and jabber.org servers. # #== Encryption mode # # Your jabber password should be encrypted this script at first. # # ./sendmsg2j.rb -e [-s <sec_config.json>] # # After enter your password, then the script shows you 'user_pass' and 'user_salt' parameters. # These parameters are copied to your 'config.json' file. # The data structure of the 'config.json' file will be described later. # #== Send message mode # The message body is read by the standard input stream by following command line. # ./sendmsg2j.rb [-s <sec_config.json>] [-c <config.json>] # # In practical use, you will use this script with following idioms. # # 1. ./sendmsg2j.rb [-s <sec_config.json>] [-c <config.json>] < message_file # 2. echo message | ./sendmsg2j.rb [-s <sec_config.json>] [-c <config.json>] # # First case (#1), the 'message_file' contains the entire message body. # Last case (#2), the 'message' string is your choice. # #== Unit Tests # The 'unittest/test_sendmsg2j.rb' is the executable Test::Unit script file. # It will test all methods except # #== 'sec_config.json file # The 'sec_config.json' file contains the essential password string. # It should have the following data structure. # # { "sec_text" : ".. password string like sha1sum hash value ..." } # # You can change the filename and location by the '-s' option. # #== 'config.json file # The 'config.json' file is the following data structure. # { # "user_name":"daemon@example.org/Batch", # "user_pass":"5208502be38983bbf6847c0f7554f7f7", # "user_salt":"63c9240d88705faf", # "server_name":"example.org", # "server_port":"5222", # "remote_user":"user01@example.org/Home", # "msg_subject":"post message from daemon@example.org/Batch" # } # You can change the filename and location by the '-c' option. # $:.unshift File.dirname($0) $:.unshift File.join([File.dirname($0),"lib"]) require 'optparse' require 'xmpp4r' require 'json' # # This module is for Yadiary Jabber Client. # # These methods should be tested by the unittest/test_sendmsg2j.rb script. module YaJabber # It should have the following data structure. # # { "sec_text" : ".. password string like sha1sum hash value ..." } # SEC_CONFIG_FILE = File.join([File.dirname($0), "conf", "sec_config.json"]) # The 'config.json' file is the following data structure. # { # "user_name":"daemon@example.org/Batch", # "user_pass":"5208502be38983bbf6847c0f7554f7f7", # "user_salt":"63c9240d88705faf", # "server_name":"example.org", # "server_port":"5222", # "remote_user":"user01@example.org/Home", # "msg_subject":"post message from daemon@example.org/Batch" # } # CONFIG_FILE = File.join([File.dirname($0), "conf", "config.json"]) # A wrapper method to read json file. # # Example: # conf = json_read(SEC_CONFIG_FILE) # def json_read(file) conf = nil begin conf = JSON.parse(open(file).read) rescue conf = nil end return conf end # # Get your password string from the given json file. # # Example: # pass = get_password(sec_conf_file) # def get_password(sec_conf_file) # sec_conf_file - test conf = json_read(sec_conf_file) res = "" label = 'sec_text' res = conf[label] if conf.kind_of?(Hash) and conf.has_key?(label) return res end # # Example: # plain_text = STDIN.gets.strip # enc_text,salt = encrypt(pass, plain_text) # def encrypt(pass, plain_text) begin salt = OpenSSL::Random.random_bytes(8) enc = OpenSSL::Cipher::Cipher.new('aes-256-cbc') enc.encrypt enc.pkcs5_keyivgen(pass.to_s, salt.to_s, 1) e_text = enc.update(plain_text) + enc.final return e_text.unpack("H*").join, salt.unpack("H*").join rescue p $! return "","" end end # # Example: # user_rawpass = decrypt(pass, enc_text, salt) # def decrypt(pass, text, salt) begin n_salt = [salt].pack("H*") n_text = [text].pack("H*") enc = OpenSSL::Cipher::Cipher.new('aes-256-cbc') enc.decrypt enc.pkcs5_keyivgen(pass.to_s, n_salt.to_s, 1) return enc.update(n_text) + enc.final rescue p $! return "" end end # # It returns the existing filename. # # Example: # begin # @sec_conf_file = check_file(@option['sec_conf_file'], SEC_CONFIG_FILE) # rescue # printf("[error] security conf file '%s' not found.\n", @sec_conf_file) # exit(1) # end # def check_file(candidate_file=nil, default_file=nil) conf_file = candidate_file conf_file = default_file if conf_file == nil or not FileTest.exist?(conf_file) raise if not FileTest.exist?(conf_file) return conf_file end # # This is a domain specific method. # The 'conf' argument must be the CONFIG_FILE format. # def check_conf(conf={}) ## remove empty value conf.reject! { |k,v| true if v == nil or v == "" } ## set the default value for optional parameters conf['server_name'] = "jabber.org" if not conf.has_key?('server_name') conf['server_port'] = "5222" if not conf.has_key?('server_port') conf['msg_subject'] = format("Messages from %s", conf['user_name']) if not conf.has_key?('msg_subject') ## check the config key/value pairs. if not conf.has_key?('user_name') or not conf.has_key?('user_pass') or not conf.has_key?('user_salt') or not conf.has_key?('remote_user') return false ## means BAD end return true ## means GOOD end # parse command line options def option_parser res = { "encrypt_mode" => false, "conf_file" => nil, "sec_conf_file" => nil } OptionParser.new do |opts| opts.banner = 'Usage: ' + File.basename($0) + '\tinput: read messages from stdin.' opts.separator '' opts.on('-e', '--encrypt', 'Interactive password encrypt mode') { res['encrypt_mode'] = true } opts.on('-c', '--config filename', 'Set a config file') do |c| res['conf_file'] = c if FileTest.exist?(c) end opts.on('-s', '--sec_config filename', 'Set a security config file') do |c| res['sec_conf_file'] = c if FileTest.exist?(c) end opts.on_tail('-h', '--help', 'Show this message') { puts opts exit } opts.parse!(ARGV) end return res end # aggreate method which will send a message. def send_message(user_name, user_pass, remote_user,msg_subject, server_name, server_port) client = Jabber::Client.new(Jabber::JID.new(user_name)) client.connect(server_name, server_port) client.auth(user_pass) body = STDIN.readlines.join m = Jabber::Message.new(remote_user, body).set_type(:normal).set_id('1').set_subject(msg_subject) client.send(m) client.close end end ########## ## main ## ########## if $0 == __FILE__ include YaJabber ################################### ## load encrypt/decrypt password ## ################################### @option = option_parser begin @sec_conf_file = check_file(@option['sec_conf_file'], SEC_CONFIG_FILE) rescue printf("[error] security conf file '%s' not found.\n", @sec_conf_file) exit(1) end @crypt_pass = get_password(@sec_conf_file) if @crypt_pass.empty? printf("[error] Your security config file, %s, doesn't have the 'sec_text' parameter or non-empty value.\n", @sec_conf_file) printf(" Please check your security config file.\n") exit(1) end ##################### ## encryption mode ## ##################### if @option['encrypt_mode'] ## confirm the input print "** password encryption mode **\n" print "enter string: " line = STDIN.gets.strip if line.empty? print "Please enter the non-empty string as your password.\n" exit end ## ok, let's start. enc_text,salt = encrypt(@crypt_pass, line) if enc_text.empty? or salt.empty? printf(<<-EOF, @sec_conf_file) Please check your "sec_text" parameter in the security config json file, %s. EOF else printf(<<-EOF, enc_text, salt) "user_pass":"%s", "user_salt":"%s", exit. EOF end exit end ################# ## normal mode ## ################# ## load config file begin @conf_file = check_file(@option['conf_file'], CONFIG_FILE) rescue printf("[error] conf file, %s, not found.\n", @conf_file) exit(1) end conf = json_read(@conf_file) if not check_conf(conf) printf(<<-EOF, @conf_file) [error] Config key/value pairs are not enough to start. The config file %s should have like following values. { "user_name":"sysuser01@jabber.org/Batch", "user_pass":"19570e028a958d0cbc91add9c1516e05", "user_salt":"e42489150b1bed52", "remote_user":"user01@jabber.org/Home", "server_name":"jabber.org (option)", "server_port":"5222 (option)", "msg_subject":"System message from sysuser01@jabber.org/Batch. (option)" } EOF exit(1) end ################## ## send message ## ################## user_pass = decrypt(@crypt_pass, conf['user_pass'], conf['user_salt']) if user_pass.empty? printf("[error] Please check 'user_pass' or 'user_salt' fields on config file, %s\n", @conf_file) exit(1) end send_message(conf['user_name'], user_pass, conf['remote_user'], conf['msg_subject'], conf['server_name'], conf['server_port']) end ################# ## end of main ## #################
0 件のコメント:
コメントを投稿