はじめに
telnetは、汎用的な双方向通信プロトコルであり、平文で通信する。以前scpが失敗する件について記事にしたが、.bashrc等の設定次第で、RubyでNet::Telnetを使用するとき、失敗することがある。
Net::Telnetクラス
基本の使い方
Rubyのマニュアルを確認すると、
require 'net/telnet'
# リモートホスト "foobar" に接続
# タイムアウトは 10 秒
localhost = Net::Telnet.new("Host" => "localhost",
"Timeout" => 10)
# ログインし、プロンプトが出るまで待ち合わせる
telnet.login("your name", "your password") {|c| print c}
# ls コマンドを実行し、実行後、プロンプトが出るまで待ち合わせる
telnet.cmd("ls") {|c| print c}
# sleep で 5 秒
telnet.cmd("sleep 5 && echo foobar &") {|c| print c}
STDOUT.flush # <- これがないとここまで処理が来てることがわかりにくい
# 前のコマンドの出力を待ち合わせる
telnet.waitfor(/foobar\Z/) {|c| print c}
# ログインセッションの終了
telnet.cmd("exit") {|c| print c}
telnet.close
のように使用する。作成したtelnetインスタンスは、サーバから返ってきた平文を解釈するが、一番最後にプロンプト(例user @ server $
)が表示されると、処理が終了したと認識し、次の処理に移れるようになる。
使用例
irb(main):001:0> require "net/telnet"
=> true
irb(main):002:0> pop = Net::Telnet.new("Host" => "192.168.179.9")
=> #<Net::Telnet:0x00007f8e159a59e8 @options={"Host"=>"192.168.179.9", "Port"=>23, "Prompt"=>/[$%#>] \z/n, "Timeout"=>10, "Waittime"=>0, "Binmode"=>false, "Telnetmode"=>true}, @telnet_option={"SGA"=>false, "BINARY"=>false}, @sock=#<TCPSocket:fd 9, AF_INET, 192.168.179.127, 62402>>
irb(main):003:0> pop.login("hikaru", "パスワード")
=> "\nKernel 3.10.0-957.1.3.el7.x86_64 on an x86_64\nClara login: hikaru\nPassword: \nLast login: Sun Jan 13 23:18:12 from ::ffff:192.168.179.127\n[hikaru@Clara ~]$ "
irb(main):004:0> pop.cmd("ls")
=> "ls\nDesktop Downloads Music\tPublic\t Videos hikaru\nDocuments Dropbox Pictures\tTemplates cern zabbix\n[hikaru@Clara ~]$ "
irb(main):005:0> exit
プロンプトの解釈について
Net::Telnetクラスでプロンプトの解釈は、クラスをnewするときにPrompt
引数で指定でき、デフォルトだと"Prompt"=>/[$%#>] \z/n
となっている。プロンプトの書き換えは.bashrcなどでPS1=hogehoge
の形で書き換えるが、ここで指定した形とNet::TelnetのPromptがアンマッチだと、処理が終了したと認識できず、Timeoutする。
再度Net::Telnetのデフォルトを確認すると、"Prompt"=>/[$%#>] \z/n
のように、途中で半角スペースが必須となっている。そのため、サーバ側のPS1の書き換えで半角スペースを省いてしまうと、Net::TelnetがTimeoutする。
Timeoutする実行例
PS1="<\u @ \t>" # 最後に半角スペースなし
irb(main):001:0> require "net/telnet"
=> true
irb(main):002:0> pop = Net::Telnet.new("Host" => "192.168.179.9")
=> #<Net::Telnet:0x00007f9af40ae060 @options={"Host"=>"192.168.179.9", "Port"=>23, "Prompt"=>/[$%#>] \z/n, "Timeout"=>10, "Waittime"=>0, "Binmode"=>false, "Telnetmode"=>true}, @telnet_option={"SGA"=>false, "BINARY"=>false}, @sock=#<TCPSocket:fd 9, AF_INET, 192.168.179.127, 62401>>
irb(main):003:0> pop.login("hikaru", "パスワード")
Traceback (most recent call last):
5: from /Users/hikaru/.rbenv/versions/2.5.0/bin/irb:11:in `<main>'
4: from (irb):3
3: from /Users/hikaru/.rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/net-telnet-0.2.0/lib/net/telnet.rb:748:in `login'
2: from /Users/hikaru/.rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/net-telnet-0.2.0/lib/net/telnet.rb:696:in `cmd'
1: from /Users/hikaru/.rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/net-telnet-0.2.0/lib/net/telnet.rb:557:in `waitfor'
Net::ReadTimeout (timed out while waiting for more data)
Timeoutしない例
PS1="<\u @ \t> " # 最後に半角スペースアリ
irb(main):001:0> require "net/telnet"
=> true
irb(main):002:0> pop = Net::Telnet.new("Host" => "192.168.179.9")
=> #<Net::Telnet:0x00007fe15e1a8938 @options={"Host"=>"192.168.179.9", "Port"=>23, "Prompt"=>/[$%#>] \z/n, "Timeout"=>10, "Waittime"=>0, "Binmode"=>false, "Telnetmode"=>true}, @telnet_option={"SGA"=>false, "BINARY"=>false}, @sock=#<TCPSocket:fd 9, AF_INET, 192.168.179.127, 62469>>
irb(main):003:0> pop.login("hikaru", "パスワード")
=> "\nKernel 3.10.0-957.1.3.el7.x86_64 on an x86_64\nClara login: hikaru\nPassword: \nLast login: Sun Jan 13 23:18:59 from ::ffff:192.168.179.127\n<\xE6\x9C\xAC\xE7\x95\xAA hikaru 23:22:30> "
最後に
RubyのNet::Telnetを使用する場合は、上記のようにサーバ側のPS1を気にしないといけない。なお、普通のtelnetは違うルールでサーバのプロンプトを認識しているらしく、PS1の最後に半角スペースをいれなくても接続できた。
以上である。