tl;dr

以前に以下のような記事を書きました.

これは 何か FTP いにしへのスクリプト いにしへの確認 access.yaml 確認 以上 これは 初老丸アドベントカレンダー 2 日目の記事です。 qiita.com ということで、小ネタ大ネタを交えて 12/25 まで張り切っていきましょう。 何か FTP それはファイルを手軽に転送出来...

inokara.hateblo.jp

この時には自前の Ruby スクリプトを使って, ftp ユーザーの振る舞い (ログイン出来るか, chroot になっているか, 読み書き, 削除出来るか等) をチェックしていました.

今回, 以下の Gem を拡張して Rspec を介してチェック出来るようにしてみました.

Test FTP Protocol with Rspec. Contribute to hilolih/rspec-ftp development by creating an account on GitHub.

github.com

尚, 自分がチェックしたかった振る舞いに対応するマッチャーが実装されていなかったので, 追加で実装してプルリクエストしています.

Hi, hilolihI added some matchers (I needed it.).Please confirm.Regards.Yohei

github.com

マージされると嬉しいなあ.

FTP ユーザーの振る舞いをテストする

なぜ, FTP ユーザーの振る舞いをテストしたいのか

そもそも, FTP のようないにしへの技術を未だに利用しているのかと突っ込まれそうな気がしていますが, 実際のところ FTP を利用したいというニーズはあります. その中でサーバーの構築というよりは FTP を利用するユーザーの追加や削除という作業に多くの時間を割かれます. 当然, これらの作業はコード化していますが, 作成した FTP ユーザーが正しくログイン出来るか, ファイルの追加や削除は行えるか, 意図しないディレクトリへのアクセスは許可されていないか等を確認した上で依頼主にエビデンスとして共有する必要があると考えています. また, この確認を自作のスクリプトではなく, 既存のテストフレームワーク上で実行することで汎用性を高め, 自作スクリプトという属人化しやすい部分を廃していくことを目的としています.

ということで, 今回は rspec-ftp を利用して FTP サーバーに作成したユーザーをテストする環境を以下のサンプルに用意してみましたので, これを利用して FTP ユーザーの振る舞いテストの雰囲気を紹介致します.

サンプルはこちらから

rspec-ftp を利用して FTP ユーザーの振る舞いをテストするサンプル. Contribute to inokappa/rspec-ftp-sample development by creating an account on GitHub.

github.com

サンプル実行

想定する FTP サーバー, FTP ユーザー

  • FTP サーバーは vsftpd を利用
  • FTP サーバーの IP アドレスは 172.26.0.6 (コンテナ間は vsftpd-server という名前でアクセスすることが出来る)
  • パッシブモードで起動し, パッシブポートは 21200 から 21210 ポートを利用
  • FTP ユーザー名は ftpuser, FTP ユーザーパスワードは supersecret

環境構築

docker-compose up -d

以下のように vsftpd サーバーと Ruby 環境が 3 環境起動します.

$ docker-compose up -d
Creating network "rspec-ftp-sample_my_sample_net" with driver "bridge"
Creating rspec-ruby23  ... done
Creating rspec-ruby25  ... done
Creating vsftpd-server ... done
Creating rspec-ruby24  ... done

ユーザー名, パスワードを secret.yml に定義する

以下のように secret.yml を定義します.

vsftpd-server:
  users:
    - username: ftpuser
      password: supersecret

フォーマットは以下の通りです.

IP アドレス又はホスト名:
  users:
    - username: ユーザー名
    - password: パスワード

このファイルはリポジトリにうっかりアップしてしまわないように .gitignore に登録しておくと良いでしょう.

$ cat .gitignore
secret.yml

テストを実行する…その前に

テストは rake コマンドを介して実行することを想定している為, rake -T を実行してタスクの一覧を確認しておきます.

bundle exec rake -T

以下のように出力されることを確認します.

$ docker-compose exec rspec-ruby25 bundle exec rake -T
rake ftpcheck:vsftpd-server:ftpuser  # Run ftpcheck to vsftpd-server by ftpuser

気を取り直して, テスト実行

以下のように rake コマンドを利用してテストを実行します. 一応, Ruby のバージョンに応じて以下のようにコマンドが別れています.

# Ruby 2.5.x 環境で実行する
docker-compose exec rspec-ruby25 bundle exec rake ftpcheck:vsftpd-server:ftpuser

# Ruby 2.4.x 環境で実行する
docker-compose exec rspec-ruby24 bundle exec rake ftpcheck:vsftpd-server:ftpuser

# Ruby 2.3.x 環境で実行する
docker-compose exec rspec-ruby23 bundle exec rake ftpcheck:vsftpd-server:ftpuser

以下のように出力されることを確認します.

$ docker-compose exec rspec-ruby25 bundle exec rake ftpcheck:vsftpd-server:ftpuser
/usr/local/bin/ruby -I/usr/local/bundle/gems/rspec-core-3.8.0/lib:/usr/local/bundle/gems/rspec-support-3.8.0/lib /usr/local/bundle/gems/rspec-core-3.8.0/exe/rspec spec/ftp_spec.rb

#be_accessible (real server)
  can login valid user and password

#be_chroot (real server)
  check chroot enabled

#be_writable (real server)
  check writable with active mode

#be_removable (real server)
  check removable

Finished in 0.09052 seconds (files took 0.18992 seconds to load)
4 examples, 0 failures

# Summary by Type or Subfolder

| Type or Subfolder  | Example count | Duration (s) | Average per example (s) |
|--------------------|---------------|--------------|-------------------------|
| ./spec/ftp_spec.rb | 4             | 0.07874      | 0.01968                 |


# Summary by File

| File               | Example count | Duration (s) | Average per example (s) |
|--------------------|---------------|--------------|-------------------------|
| ./spec/ftp_spec.rb | 4             | 0.07874      | 0.01968                 |

いい感じでテストが PASS しました. ちなみに, chroot が適切に設定されていない場合には…

$ docker-compose exec rspec-ruby25 bundle exec rake ftpcheck:xxx.xxx.xxx.xxx:user1
...
Failures:

  1) #be_chroot (real server) check chroot enabled
     Failure/Error: expect(ENV['TARGET_HOST']).to be_chroot.user(property['username']).pass(property['password'])
       expected "xxx.xxx.xxx.xxx" to be chroot
     # ./spec/ftp_spec.rb:11:in `block (2 levels) in <top (required)>'

Finished in 2.06 seconds (files took 0.20194 seconds to load)
4 examples, 1 failure
...

上記ようにテストはものの見事に失敗します. この時に vsftpd.log を確認すると以下のように上位ディレクトリを参照してしまっていることが確認出来ます.

...
Sat Oct  6 15:02:41 2018 [pid 1410] [user1] FTP command: Client "111.222.333.444", "CWD ../"
Sat Oct  6 15:02:41 2018 [pid 1410] [user1] FTP response: Client "111.222.333.444", "250 Directory successfully changed."
Sat Oct  6 15:02:41 2018 [pid 1410] [user1] FTP command: Client "111.222.333.444", "PWD"
Sat Oct  6 15:02:41 2018 [pid 1410] [user1] FTP response: Client "111.222.333.444", "257 "/home""
Sat Oct  6 15:02:41 2018 [pid 1412] CONNECT: Client "111.222.333.444"
...

意図した通りです. これをずーっとやりたかったんです.

余談

余談という言い方もアレですが

実装するにあたって色々と勉強になったことのメモを書いていきます. 色々と気付きがあったので, 思い出したら追記していきたいと思います.

FTP について

  • アクティブモードとパッシブモードについて理解が曖昧だったので, 改めてこれらについて理解を深める良い機会になった
    • TCP 20 番ポートはデータ転送用, TCP 21 番ポートは制御用
    • アクティブモードはサーバーが TCP 20 番ポートに対してデータの転送を行う
    • パッシブモードはデータ転送ポートは不定で, サーバー側から明示的に範囲を指定された中のポートに対してデータ転送を行う

下図の解説が解りやすかった為, 引用させて頂きました.

インターネット・プロトコル詳説(11):FTP(File Transfer Protocol)~後編 より引用.

抹茶の追加

以前にも rspec のカスタムマッチャの追加方法は勉強しました.

tl;dr 茶器 awspec でのカスタムマッチャ 抹茶を点てる RSpec::Matchers.define による抹茶の定義 chain を使って, マッチャを拡張する fail 時のメッセージを定義する ヘルパーメソッドを利用する 以上 参考 tl;dr awspec のコードを見ていて, どんな風に独自のマッ...

inokara.hateblo.jp

Rspec::Matchersdefine メソッド内に処理を書いていきます. また, メソッドチェーンは chain メソッド内にチェーンしたいメソッドを定義していきます.

Specinfra の property と set_property を利用する

FTP ユーザー名, パスワードの情報を secret.yml を切り出しておいて, テストで利用する方法の一つとして, Serverspec (厳密には 科っspecinfra) の tips で紹介されている property と set_property メソッドを spec_helper 内で利用してみました.

require 'rspec'
require 'rspec-ftp'
require 'specinfra/properties'
require 'yaml'

def property
  Specinfra::Properties.instance.properties
end

def set_property(prop)
  Specinfra::Properties.instance.properties(prop)
end

properties = YAML.load_file('secret.yml')
host = ENV['TARGET_HOST']
ftpuser = ENV['FTP_USER']
set_property properties[host]['users'].first {|u| u['username'] == ftpuser }

Travis CI で docker-compose を使う

元々の rspec-ftp にもテストは書かれていましたが, 今回いくつかのマッチャを追加するにあたり, Docker コンテナで立てた FTP サーバーに対してテストを実行したいと考えました. このような場合, docker-compose を利用するのが良いと考えていますが, 今まで Travis CI で docker-compose を使えることを知りませんでした.

ところが, 確認したところ, ずいぶん前から docker-compose は利用可能な状態になっていたことが判ったので, 以下のような docker-compose.yml を作成してテストを行うようにしました. 特に Travis CI で実行する為に特別な設定は行っておらず, 手元の端末でも docker-compose up -d で一発起動します.

version: "2"
services:
    vsftpd-server:
      image: fikipollo/vsftpd
      container_name: vsftpd-server
      environment:
        - FTP_USER=ftpuser
        - FTP_PASS=supersecret
        - ONLY_UPLOAD=NO
        - PASV_ENABLE=YES
        - PASV_ADDRESS=172.26.0.6
        - PASV_MIN=21200
        - PASV_MAX=21210
      ports:
        - "21:21"
        - "21200-21210:21200-21210"
      networks:
        my_sample_net:
          ipv4_address: 172.26.0.6
... 略 ...
    rspec-ruby23:
      image: ruby:2.3
      build: ./spec/docker/rspec
      container_name: rspec-ruby23
      volumes:
        - .:/work
      command: tail -f /dev/null
      networks:
        my_sample_net:
          ipv4_address: 172.26.0.3
networks:
  my_sample_net:
    driver: bridge
    ipam:
     driver: default
     config:
       - subnet: 172.26.0.0/16
         gateway: 172.26.0.1

networks を利用しているのは, vsftpd で PASV_ADDRESS の IP アドレスをコンテナの IP アドレスに固定する必要があった為です.

引き続き, Travis CI で docker-compose を使いつつ, 複数のコマンドを並列して実行する

複数の Ruby バージョンを使って並列してテストを走らせる為には, matrix を利用して, 環境変数 TEST_TARGET に対象の Ruby バージョンコンテナ名を入れてあげれば良いようです. 以下は実際に利用している .travis.yml です.

sudo: required
matrix:
  include:
  - name: "Ruby 2.5"
    env: TEST_TARGET=rspec-ruby25
  - name: "Ruby 2.4"
    env: TEST_TARGET=rspec-ruby24
  - name: "Ruby 2.3"
    env: TEST_TARGET=rspec-ruby23
services:
  - docker
before_install:
  - docker-compose build ${TEST_TARGET}
  - docker-compose up -d
install:
before_script:
script:
  - docker-compose exec ${TEST_TARGET} rspec
after_script:
notifications:

これを利用することで, 下図のように複数の Ruby バージョンにおいて並列でテストが走ることを確認しました.

以上

FTP という由緒正しいいにしへの技術が今後も使われ続けるのであれば, rspec-ftp を通して FTP について理解を深めていく必要があると感じました. また, 今回のようにインフラの振る舞いをテストするツールとしては infrataster と連携 (もしくはプラグイン化) 出来ないか考えてみたいと思います.

元記事はこちら

FTP ユーザーの振る舞いをテストをする rspec-ftp を試した + 抹茶を追加しました