RubyでSSHサーバーを立てる

RubyでSSHサーバーを立ててみます。

使うのは、[hrr_rb_ssh](https://rubygems.org/gems/hrr_rb_ssh) Gem（現時点で、バージョンは0.4.0）です。


# 使いどころ

- SSH接続を利用するツール等のテストに
- ネットワーク機器等のシミュレーターとして
- SSH Subsystemの実装に（NETCONF等）
- ユーザー権限で動かすオレオレSSHサーバーとして


# インストール

Gemは、次のようにインストールします。

Bundlerを使う場合、

```Ruby:Gemfile
gem 'hrr_rb_ssh'
gem 'hrr_rb_ssh-ed25519' # ed25519を使う場合
```

というファイルを作り、

```sh
$ bundle install
```

します。もしくは、直接

```sh
$ gem install hrr_rb_ssh
$ gem install hrr_rb_ssh-ed25519 # ed25519を使う場合
```

を実行します。


# 使ってみる

まずは、実行ユーザーのシェルにログインできるSSHサーバーを立ててみます。

ちょっと長いですが、やっていることは、

- ライブラリを読み込む
- 認証（publickey、password）情報の定義
- SSH内部処理のハンドラの定義（実際には、デフォルト実装を使用）
- TCPserverを立てて、接続を待ち受ける
- SSHサーバーサービスを開始する

です。

```ruby
require 'logger'
require 'socket'
require 'hrr_rb_ssh'
require 'hrr_rb_ssh/ed25519' # ed25519を使う場合


logger = Logger.new STDOUT
logger.level = Logger::INFO


# 公開鍵認証に、OpenSSHの~/.ssh/authorized_keysを使う
auth_publickey = HrrRbSsh::Authentication::Authenticator.new { |context|
  username = ENV['USER']
  homedir = Dir.home
  authorized_keys = HrrRbSsh::Compat::OpenSSH::AuthorizedKeys.new(File.read(File.join(homedir, '.ssh', 'authorized_keys')))
  authorized_keys.any?{ |public_key|
    context.verify username, public_key.algorithm_name, public_key.to_pem
  }
}
# 指定のパスワードで認証する
auth_password = HrrRbSsh::Authentication::Authenticator.new { |context|
  username = ENV['USER']
  password = '接続時に使うパスワード'
  context.verify username, password
}


options = {}

# 定義した認証を利用する
options['authentication_publickey_authenticator'] = auth_publickey
options['authentication_password_authenticator']  = auth_password

# SSH接続の内部処理を、デフォルト実装のハンドラで処理する
options['connection_channel_request_pty_req']       = HrrRbSsh::Connection::RequestHandler::ReferencePtyReqRequestHandler.new
options['connection_channel_request_env']           = HrrRbSsh::Connection::RequestHandler::ReferenceEnvRequestHandler.new
options['connection_channel_request_shell']         = HrrRbSsh::Connection::RequestHandler::ReferenceShellRequestHandler.new
options['connection_channel_request_exec']          = HrrRbSsh::Connection::RequestHandler::ReferenceExecRequestHandler.new
options['connection_channel_request_window_change'] = HrrRbSsh::Connection::RequestHandler::ReferenceWindowChangeRequestHandler.new


# ポート10022番で接続を待ち受ける
server = TCPServer.new 10022
io = server.accept

# SSHサーバーを起動する
server = HrrRbSsh::Server.new options, logger: logger
server.start io
```


# 独自サービスを提供する

[使ってみる](#使ってみる) では、デフォルトのハンドラを利用し、ユーザーのログインシェルをサービスとして提供しました。

ここでは、ログインシェルの代わりに、独自サービスとして、echoサーバー（処理は何も行わず、ただ文字をエコーバックするだけ）を実装してみます。

コードは、次のようになります。

```ruby
require 'logger'
require 'socket'
require 'hrr_rb_ssh'
require 'hrr_rb_ssh/ed25519' # ed25519を使う場合


logger = Logger.new STDOUT
logger.level = Logger::INFO


# 指定のパスワードで認証する
auth_password = HrrRbSsh::Authentication::Authenticator.new { |context|
  username = ENV['USER']
  password = '接続時に使うパスワード'
  context.verify username, password
}


# echoサーバーの処理を実装
conn_echo = HrrRbSsh::Connection::RequestHandler.new { |context|
  context.chain_proc { |chain|
    # SSHクライアントに、最初のメッセージを出力（context.io[1].write）する
    context.io[1].write "Start echo service\r\n"
    loop do
      # SSHクライアントからの入力（context.io[0].read）を受けて
      buf = context.io[0].readpartial(10240)
      # Ctrl+Dが入力された場合、抜ける
      break if buf.include?(0x04.chr)
      # SSHクライアントに出力（context.io[1].write）する
      context.io[1].write buf
    end
    exitstatus = 0 
    exitstatus
  } 
} 


options = {}

# 定義した認証を利用する
options['authentication_publickey_authenticator'] = auth_publickey
options['authentication_password_authenticator']  = auth_password

# shellのリクエストに対するハンドラを、echoサーバーの実装に置き換える
options['connection_channel_request_shell'] = conn_echo


# ポート10022番で接続を待ち受ける
server = TCPServer.new 10022
io = server.accept

# SSHサーバーを起動する
server = HrrRbSsh::Server.new options, logger: logger
server.start io
```

SSHクライアントからアクセスし、例えば `foobarbaz...` と入力すると、

```sh
$ ssh localhost -p 10022
Password: 接続時に使うパスワード
Start echo service
foobarbaz...
```

のように、入力がエコーバックされます。

このように、`HrrRbSsh::Connection::RequestHandler.new {|context| ...}` を定義することで、独自のサービスを提供することが可能になります。