普通の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_config.json
{
"sec_text" : "5ee6f7e62b8b1de083bb2617738a4ade620f9c09"
}
接続用ID設定ファイルの形式
パスワードやJabberサーバへの接続情報は別ファイルとして扱っています
Jabberクライアントの接続情報 Gmail版: gmail_config.json
{
"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"
}
Jabberクライアントの接続情報 Jabber.org版: jabber_config.json
{
"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 件のコメント:
コメントを投稿