ClamAVによるアップロードファイルのウィルスチェック

とあるRailsアプリケーションでアップロードされたファイルをウィルスチェックしたいというのがあって、実装したのでメモ。

方針

ClamAVというオープンソースのアンチウィルスエンジンがあるのでこれを使う。各種言語にクライアントライブラリ(Rubyclamav-clientというgem)があったりして便利。

このアプリケーションはDockerで動かしているので、ClamAVのコンテナを立ててデーモンを動かし、ファイルアップロード時にアップロードされたファイルをClamAVのデーモンに渡してウィルス判定されたらバリデーションエラーになるようにする。

また、ファイルアップロードには shrine という gem を使っている。

実装

環境

docker-compose.yml:

---
version: '3.5'
services:
  clamav:
    image: mkodockx/docker-clamav
    ports:
      - 3310
    volumes:
      - clamav:/var/lib/clamav
      - ./docker/clamav/clamd.conf:/etc/clamd/clamd.conf:ro
      - ./docker/clamav/freshclam.conf:/etc/clamd/freshclam.conf:ro

  app:
    build:
      context: .
      dockerfile: docker/app/Dockerfile
      target: base
    command: >
      bash -c "
        rm -f tmp/pids/server.pid &&
        bundle exec rails s -p 3000 -b '0.0.0.0'"
    depends_on:
      - clamav
    volumes:
      - .:/app
    ports:
      - ${DOCKER_HOST_APP_PORT:-3000}:3000
    environment:
      CLAMD_TCP_HOST: clamav
      CLAMD_TCP_PORT: 3310

volumes:
  clamav:
    driver: local

clamd.conf:

# Daemon
Foreground true
LocalSocket /var/run/clamav/clamd.ctl
FixStaleSocket true
LocalSocketGroup clamav
LocalSocketMode 666
TCPSocket 3310

# Basic
User clamav
ScanMail true
ScanArchive true
ArchiveBlockEncrypted false
MaxDirectoryRecursion 15
FollowDirectorySymlinks false
FollowFileSymlinks false
ReadTimeout 180
MaxThreads 12
MaxConnectionQueueLength 15

# Log
LogFile /dev/stdout
LogSyslog false
LogRotate false
LogFacility LOG_LOCAL6
LogClean false
LogVerbose false
LogTime true
LogFileUnlock false
LogFileMaxSize 0

# Detail
PreludeEnable no
PreludeAnalyzerName ClamAV
DatabaseDirectory /var/lib/clamav
OfficialDatabaseOnly false
SelfCheck 3600
Debug false
ScanPE true
MaxEmbeddedPE 10M
ScanOLE2 true
ScanPDF true
ScanHTML true
MaxHTMLNormalize 10M
MaxHTMLNoTags 2M
MaxScriptNormalize 5M
MaxZipTypeRcg 1M
ScanSWF true
DetectBrokenExecutables false
ExitOnOOM false
LeaveTemporaryFiles false
AlgorithmicDetection true
ScanELF true
IdleTimeout 30
CrossFilesystems true
PhishingSignatures true
PhishingScanURLs true
PhishingAlwaysBlockSSLMismatch false
PhishingAlwaysBlockCloak false
PartitionIntersection false
DetectPUA false
ScanPartialMessages false
HeuristicScanPrecedence false
StructuredDataDetection false
CommandReadTimeout 5
SendBufTimeout 200
MaxQueue 100
ExtendedDetectionInfo true
OLE2BlockMacros false
ScanOnAccess false
AllowAllMatchScan true
ForceToDisk false
DisableCertCheck false
DisableCache false
MaxScanSize 100M
MaxFileSize 25M
MaxRecursion 16
MaxFiles 10000
MaxPartitions 50
MaxIconsPE 100
PCREMatchLimit 10000
PCRERecMatchLimit 5000
PCREMaxFileSize 25M
ScanXMLDOCS true
ScanHWP3 true
MaxRecHWP3 16
StreamMaxLength 25M
Bytecode true
BytecodeSecurity TrustSigned
BytecodeTimeout 60000

freshclam.conf:

# Daemon
Foreground true

# Basic
DatabaseOwner clamav
Debug false
MaxAttempts 5
DatabaseDirectory /var/lib/clamav
DNSDatabaseInfo current.cvd.clamav.net
ConnectTimeout 30
ReceiveTimeout 30
TestDatabases yes
ScriptedUpdates yes
CompressLocalDatabase no
SafeBrowsing false
Bytecode true
NotifyClamd /etc/clamav/clamd.conf

# Log
UpdateLogFile /dev/stdout
LogVerbose false
LogSyslog false
LogFacility LOG_LOCAL6
LogFileMaxSize 0
LogRotate false
LogTime true

# Check
Checks 24
DatabaseMirror db.local.clamav.net
DatabaseMirror database.clamav.net

Rails

Gemfile:

...

# Upload file
gem 'shrine'

# Antivirus
gem 'clamav-client', require: 'clamav/client'

config/initializers/clamav.rb:

class << Rails.application
  def clamav
    Thread.current['clamav'] ||= ClamAV::Client.new(
      ClamAV::Connection.new(
        socket: TCPSocket.new(Settings.clamav.tcp_host, Settings.clamav.tcp_port),
        wrapper: ClamAV::Wrappers::NewLineWrapper.new
      )
    )
  end
end

app/uploaders/basic_uploader.rb:

class BasicUploader < Shrine
  plugin :validation_helpers

  Attacher.validate do
    result = Rails.application.clamav.execute(ClamAV::Commands::InstreamCommand.new(get))
    errors << :has_virus if result.virus_name.present?
  end
end

app/models/hoge_model.rb:

class HogeModel < ApplicationRecord
  include BasicUploader::Attachment.new(:file)
end

テスト

EICARがウィルスチェック用の無害なテストウィルスを配布しているのでこれを使う。

$ docker-compose exec app rails c
irb(main):001:0> model = HogeModel.new(file: Rails.root.join('fixtures/antivirus/eicar.com').open)
irb(main):002:0> model.valid?
irb(main):003:0> model.errors.details
{:file=>[{:error=>:has_virus}]}
irb(main):004:0> model = HogeModel.new(file: Rails.root.join('REDME.md').open)
irb(main):005:0> model.valid?
irb(main):006:0> model.errors.details
{}

git revert したブランチを再度マージする方法

先日あやまって、topic branch を master にマージしてしまったので、 マージコミットを revert しました。のちほど再度 merge しようとしたのですがうまくマージされませんでした。この場合は、git revert したときにできた revert commit に対して git revert するようです。

$ git marge hoge # あやまって hoge branch をマージ
...

$ git log # merge commit が作成される

commit <merge-commit-hash>
Merge: aaaa bbbb
Author: Foo Baz <foo@baz.com>
Date:   Tue Sep 4 17:19:00 2018 +0900

    Merge branch 'hoge'

...
$ git revert -m 1 <merge-commit-hash> # この merge commit を revert
...

$ git log # revert commit が作成され revert された

commit <revert-commit-hash>
Author: Foo Baz <foo@baz.com>
Date:   Tue Sep 4 17:29:00 2018 +0900

    Revert "Merge branch 'hoge'"

    This reverts commit <merge-commit-hash>, reversing
    changes made to cccc.

....

$ git revert <revert-commit-hash> # 再度マージするため revert commit を revert
...

$ git log # revert commit の revert commit が生成され無事マージされた

commit eeee
Author: Foo Baz <foo@baz.com>
Date:   Tue Sep 4 17:39:00 2018 +0900

    Revert "Revert "Merge branch 'hoge'""

    This reverts commit <revert-commit-hash>.

...

参考

UniRx を試してみた

Unity でイベント処理などが複雑になってきたので、どうにかできないかと思い UniRx を試しました。

github.com

UniRx とは イベントを時系列に並んだシーケンスと捉え、それらを filter したり map したりすることでイベント処理を行うものです。

たとえば、Spaceキーが入力されたときに、プレイヤーをジャンプさせるといったことをする場合は、通常は以下のように Update() でキーが入力されたか判定し、その結果のフラグをプロパティに保持しておき、FixedUpdate() でフラグが立っていれば RidgedBody を操作するといったことをします。

using System;
using UnityEngine;

public class JumpBehaviourScript : MonoBehaviour
{
    private Boolean isEnteredSpaceKey;
    private Rigidbody rigidBody;

    private void Start()
    {
        isEnteredSpaceKey = false;
        rigidBody = GetComponent<Rigidbody>();
    }

    private void Update()
    {
        isEnteredSpaceKey = Input.GetKey(KeyCode.Space);
    }

    private void FixedUpdate()
    {
        if (isEnteredSpaceKey) {
            rigidBody.AddForce(Vector3.up, ForceMode.VelocityChange);
        }
    }
}

この方法では、キーが押されたかの判定とその時の動作が別の場所にあるため、キー入力が多くなってくると見通しが悪くなってきます。

ここで、UniRx を用いると以下のように記述できます。

using UniRx;
using UniRx.Triggers;
using UnityEngine;

public class JumpBehaviourScript : MonoBehaviour
{
    private void Start()
    {
        var rididBody = GetComponent<Rigidbody>();

        this.UpdateAsObservable()
            .Where(_ => Input.GetKey(KeyCode.Space))
            .BatchFrame(0, FrameCountType.FixedUpdate)
            .Subscribe(_ => rididBody.AddForce(Vector3.up, ForceMode.VelocityChange));
    }
}

これは、 UpdateAsObservable()Update() のタイミングでイベントが発生するというストリームを生成します。そして .Where(_ => Input.GetKey(KeyCode.Space)) でキーが押されたときだけ filter します。 そのあとに、 BatchFrame(0, FrameCountType.FixedUpdate) でバッファにイベントをためておき、FixedUpdate() のタイミングでポップします。最後に Subscribe(_ => rididBody.AddForce(Vector3.up, ForceMode.VelocityChange)) でジャンプする動作を行います。

このように、UniRx を用いることで、キー入力判定からジャンプの動作までを一つのストリームで簡潔に記述できます。

そのほかにも、MVPパターンのReactive実装のパターンである MV(R)P パターンの実装など Unity において様々なところで活躍しそうです。

UniRx については以下が詳しいです。

www.slideshare.net qiita.com

また、以下に調査時のサンプルコードがあります。

github.com

楽譜描画ライブラリVexFlowを使ってみた

最近、楽譜学習アプリを作っており楽譜を描画する必要があるので、なにか便利なライブラリがないか調べていたら VexFlowというJavaScript製のライブラリがあることを知りました。

f:id:mrk21:20180603232441p:plain

このライブラリは、JavaScriptで五線や音符を操作するAPIを呼び出すことでSVGもしくはHTML5 Canvasを用いて描画するようです。

たとえば、以下のようなコードを書くと次のような楽譜がSVGで描画されます。

import { Flow as VF } from 'vexflow';

const div = document.getElementById('container');
const renderer = new VF.Renderer(div, VF.Renderer.Backends.SVG);
renderer.resize(500, 200);

const context = renderer.getContext();
const stave = new VF.Stave(10, 40, 400);

stave.addClef("treble").addTimeSignature("4/4");
stave.setContext(context).draw();

const notes = [
  new VF.StaveNote({clef: "treble", keys: ["c/4"], duration: "q" }),
  new VF.StaveNote({clef: "treble", keys: ["d/4"], duration: "q" }),
  new VF.StaveNote({clef: "treble", keys: ["b/4"], duration: "qr" }),
  new VF.StaveNote({clef: "treble", keys: ["c/4", "e/4", "g/4"], duration: "q" })
];

const voice = new VF.Voice({num_beats: 4,  beat_value: 4});
voice.addTickables(notes);

const formatter = new VF.Formatter().joinVoices([voice]).format([voice], 400);
voice.draw(context, stave);

f:id:mrk21:20180603232247p:plain

静的な楽譜だけではなくて、アニメーションやインタラクティブなものも作れます。SVGで描画した場合はアニメーションはCSSで設定できるようです。また描画要素をグループ化できるのでわりと複雑なこともできそうです。


音符の動的追加

f:id:mrk21:20180603232904g:plain


音符のアニメーション

f:id:mrk21:20180604001147g:plain

楽譜ファイルフォーマットのMusicXMLにも対応しているようなので、楽譜を描画したい場合はこのライブラリを使えば良さそうです。

なお、調査する上で書いたコードは以下にあります。

github.com

GitHubのContributionsを埋めるゲームを全クリした

去年の4月から1日1コミットを目標にしていましたが、先日全部埋めることができました。

f:id:mrk21:20150415112811p:plain

正直いって結構しんどかったですが、常に進捗を意識できたのと、コミットの単位を短くなるようにできたのは良かったと思います。

しかし、体調があまり良くない時は正直休んだほうがいいし、コミットすることが一番大事みたいになりがちで、パフォーマンスなどを考えると良いとはいえないことも確かです。

現在もこの目標を継続中ですが、今後はやめるかもしれません。

WebMock のしくみを調査

前回のエントリーで cpp-netlib の client をモックにすげ替えることに成功しましたが、 任意のレスポンスを返すためにはどのようしたらいいか、という点は述べていませんでした。 このモックは、RubyWebMock のような設計にしようと考えているので、 WebMock がどのような設計になっているのかを調査しました。なお、調査した WebMock のバージョンは 1.20.4 となります。

WebMock 概要

まず、WebMock を用いて http://www.example.com/ にGETメソッドでアクセスした場合に、メッセージボディが "test" であるレスポンスを返すためには以下のようにします。

require 'net/http'
require 'webmock'

webmock::api.stub_request(:get, 'www.example.com').to_return(body: "test")

client = Net::HTTP.start("www.example.com")
response = client.get("/")
puts response.body # test

これにより、Net::HTTPhttp://www.example.com/ にアクセスしなくなり、かわりにモックで設定したレスポンスを返すようになります。

モックオブジェクトの生成と管理

ここで、モックオブジェクトの生成を行う WebMock::API::stub_request() メソッドは何をしているのでしょうか? このメソッドの定義は、以下のようになっています。

webmock/api.rb

module WebMock
  module API
    # ...
    def stub_request(method, uri)
      WebMock::StubRegistry.instance.
        register_request_stub(WebMock::RequestStub.new(method, uri))
    end
    # ...
  end
end

まず、WebMock::RequestStub というモックオブジェクトを生成します。 このオブジェクトでは、to_return() というレスポンスを設定するメソッドや、 with() メソッドというリクエストの条件を設定するメソッドが定義されています。 また、リクエストの条件は request_pattern プロパティに WebMock::RequestPattern というオブジェクトが保持されています。

webmock/request_stub.rb

module WebMock
  class RequestStub
    attr_accessor :request_pattern
    
    def initialize(method, uri)
      @request_pattern = RequestPattern.new(method, uri)
      # ...
    end
    
    def with(params = {}, &block)
      @request_pattern.with(params, &block)
      # ...
    end
    
    def to_return(*response_hashes, &block)
      # ...
    end
    # ...
  end
end

次に、WebMock::StubRegistry というモックオブジェクトを管理するオブジェクトの register_request_stub() メソッドに生成したモックオブジェクトを渡しています。 このメソッドでは渡された WebMock::RequestStub オブジェクトを request_stubs プロパティに追加します。request_stubs に保持されているモックオブジェクトは必要に応じて探索されます。

webmock/stub_registry.rb

module WebMock
  class StubRegistry
    include Singleton
    
    attr_accessor :request_stubs
    
    def initialize
      reset!
    end
    # ...
    def reset!
      self.request_stubs = []
    end
    # ...
    def register_request_stub(stub)
      request_stubs.insert(0, stub)
      stub
    end
    # ...
  end
end

モックオブジェクトの探索と実行

では Net::HTTP がリクエストを送るときは、どのような処理になっているのでしょうか?

WebMockは require されると、Net::HTTPWebMock::HttpLibAdapters::NetHttpAdapter@webMockNetHTTP に置き換えます。そして、Net::HTTP#request() メソッドが呼ばれた時に以下の動作をするようになります。

webmock/http_lib_adapters/net_http.rb

module WebMock
  module HttpLibAdapters
    class NetHttpAdapter < HttpLibAdapter
      # ...
      @webMockNetHTTP = Class.new(Net::HTTP) do
        # ...
        def request(request, body = nil, &block)
          request_signature = WebMock::NetHTTPUtility.request_signature_from_request(self, request, body)
          # ...
          if webmock_response = WebMock::StubRegistry.instance.response_for_request(request_signature)
            # ...
          elsif WebMock.net_connect_allowed?(request_signature.uri)
            # ...
          else
            raise WebMock::NetConnectNotAllowedError.new(request_signature)
          end
        end
        # ...
      end
      # ...
    end
  end
end

まず、WebMock::NetHTTPUtility::request_signature_from_request() メソッドにより WebMock::RequestSignature オブジェクト request_signature を生成します。このオブジェクトは、各アダプターのリクエストを正規化し保持するためのものです。

つぎに、生成した request_signature オブジェクトを WebMock::StubRegistry#response_for_request() メソッドに渡し、 内部で request_stub_for() メソッドを呼び出します。 そして、request_signature オブジェクトに一致するモックオブジェクトを request_stubs の中から各モックオブジェクトの request_pattern プロパティの matches?() メソッドを用いて線形探索します。

最後に、探索したモックオブジェクトに設定されているレスポンスを返します。このとき、一致するモックオブジェクトが存在しなければ、例外を投げます。ただし、WebMock::Config#allow_net_connecttrue の時は、実際にHTTPアクセスをします。

webmock/stub_registry.rb

module WebMock
  class StubRegistry
    # ...
    def response_for_request(request_signature)
      stub = request_stub_for(request_signature)
      stub ? evaluate_response_for_request(stub.response, request_signature) : nil
    end
    
    private
    
    def request_stub_for(request_signature)
      (global_stubs + request_stubs).detect { |registered_request_stub|
        registered_request_stub.request_pattern.matches?(request_signature)
      }
    end
    
    def evaluate_response_for_request(response, request_signature)
      response.dup.evaluate(request_signature)
    end
  end
end

WebMock::RequestPattern について

上記でモックオブジェクトの探索方法について述べましたが、具体的にはどのように request_signature に一致しているか判定しているのでしょうか?

WebMock::RequestPattern の定義は下記のようになっています。

webmock/request_pattern.rb

module WebMock
  class RequestPattern
    attr_reader :method_pattern, :uri_pattern, :body_pattern, :headers_pattern
    
    def initialize(method, uri, options = {})
      @method_pattern  = MethodPattern.new(method)
      @uri_pattern     = create_uri_pattern(uri)
      @body_pattern    = nil
      @headers_pattern = nil
      @with_block      = nil
      assign_options(options)
    end
    
    def with(options = {}, &block)
      raise ArgumentError.new('#with method invoked with no arguments. Either options hash or block must be specified.') if options.empty? && !block_given?
      assign_options(options)
      @with_block = block
      self
    end
    
    def matches?(request_signature)
      content_type = request_signature.headers['Content-Type'] if request_signature.headers
      content_type = content_type.split(';').first if content_type
      @method_pattern.matches?(request_signature.method) &&
        @uri_pattern.matches?(request_signature.uri) &&
        (@body_pattern.nil? || @body_pattern.matches?(request_signature.body, content_type || "")) &&
        (@headers_pattern.nil? || @headers_pattern.matches?(request_signature.headers)) &&
        (@with_block.nil? || @with_block.call(request_signature))
    end
    # ...
  end
  
  class MethodPattern
    # ...
  end
  
  class URIPattern
    # ...
  end
  
  class BodyPattern
    # ...
  end
  
  class HeadersPattern
    # ...
  end
end

これを見ると、まずオブジェクト生成時の対象HTTPメソッド・URL、with() メソッドで設定された条件(リクエストHTTPヘッダの有無)をもとにパターンを表すオブジェクトを生成し保持します。

そして matches?() メソッドで、これらパターンすべてにマッチする時に一致したと判断し、true を返します。

さいごに

というわけで、WebMock のしくみがだいたいわかったので、これを参考に設計していきます。

cpp-netlib にモックがほしい

C++でネットワークプログラミングをするときは、cpp-netlib という Boost.Asio をベース としたライブラリを使っているのですが、モックがほしいことがあります。 例えば下記のコードで、http://www.boost.org に実際にアクセスするのではなく任意のレスポンスを返したい。cpp-netlib のモックライブラリがないか探しましたが、どうやら無いようなので作成する ことにしました。なお、cpp-netlib のバージョンは 0.11.1RC2 となります。

main.cpp

#include <iostream>
#include <boost/network/protocol/http/client.hpp>

int main() {
    namespace network = boost::network;
    namespace http = network::http;
    using tag = http::tags::http_async_8bit_tcp_resolve;
    using client_type = http::basic_client<tag,1,1>;
    
    client_type::request request("http://www.boost.org");
    request << network::header("Connection","close");
    client_type client;
    client_type::response response = client.get(request);
    
    std::cout << "#response" << std::endl;
    std::cout << http::status(response) << std::endl;
    std::cout << http::body(response) << std::endl;
    
    for (auto && h: http::headers(response)) {
        std::cout << h.first << ": " << h.second << std::endl;
    }
    
    return 0;
}

boost::network::http::basic_client の調査

まず、http::basic_client では、http::basic_client_facade を継承していて、get() などのメンバ関数はこのクラスで定義されています。 これらのメンバ関数ではネットワークアクセスの詳細は http::basic_client_impl クラスの request_skeleton() メンバ関数に委譲されています。

cpp-netlib/boost/network/protocol/http/client.hpp

template <class Tag, unsigned version_major, unsigned version_minor>
struct basic_client : basic_client_facade<Tag, version_major, version_minor> {
    // ...
};

cpp-netlib/boost/network/protocol/http/client/facade.hpp

template <class Tag, unsigned version_major, unsigned version_minor>
struct basic_client_facade {
    // ...
    typedef basic_client_impl<Tag, version_major, version_minor> pimpl_type;
    // ...
    response get(
        request const & request,
        body_callback_function_type body_handler = body_callback_function_type()
    ) {
        return pimpl->request_skeleton(
            request,
            "GET",
            true,
            body_handler,
            body_generator_function_type()
        );
    }
    // ...
protected:
    boost::shared_ptr<pimpl_type> pimpl;
    // ...
};

次に、basic_client_impl クラスは http::impl::client_base::type クラスを継承していて、Tag テンプレート引数により特殊化して実装を分けています。 例えば、Taghttp::tags::http_async_8bit_tcp_resolve の場合は、http::impl::async_client が選択されます。request_skeleton() はこの client_base::type クラスで定義されます。

cpp-netlib/boost/network/protocol/http/client/pimpl.hpp

namespace impl {
    // ...
    template <class Tag, unsigned version_major, unsigned version_minor,
        class Enable = void>
    struct client_base {
        typedef unsupported_tag<Tag> type;
    };
    
    template <class Tag, unsigned version_major, unsigned version_minor>
    struct client_base<Tag, version_major, version_minor,
        typename enable_if<is_async<Tag> >::type> {
        typedef async_client<Tag, version_major, version_minor> type;
    };
    
    template <class Tag, unsigned version_major, unsigned version_minor>
    struct client_base<Tag, version_major, version_minor,
        typename enable_if<is_sync<Tag> >::type> {
        typedef sync_client<Tag, version_major, version_minor> type;
    };
}

template <class Tag, unsigned version_major, unsigned version_minor>
struct basic_client_impl :
    impl::client_base<Tag, version_major, version_minor>::type {
    // ...
};

cpp-netlib/boost/network/protocol/http/client/async_impl.hpp

namespace impl {
template <class Tag, unsigned version_major, unsigned version_minor>
struct async_client : connection_policy<Tag, version_major, version_minor>::type {
    // ...
    basic_response<Tag> const request_skeleton(
        basic_request<Tag> const & request_,
        string_type const & method,
        bool get_body,
        body_callback_function_type callback,
        body_generator_function_type generator
    );
    // ...
};

最後に、client_baseクラスを特殊化するにあたって、Tag がどのように定義されているかを調べます。 Tag の定義は http::tagstags にあります。 これらを見ると、非同期通信かどうかTPCかどうかなどの特性を型として定義し、型リストを用いて組み合わせ、BOOST_NETWORK_DEFINE_TAG マクロで型リストの各型をすべて継承する型を作成しているようです。

つまり、mock という特性型を作成し、http_mock_8bit_tcp_resolve というタグ型を作り、client_base を特殊化し、mock_client というクライアント型を作成すれば良さそうです。

cpp-netlib/boost/network/protocol/http/tags.hpp

struct http {
  typedef mpl::true_::type is_http;
};
// ...
typedef mpl::vector<http, client, simple, async, tcp, default_string>
    http_async_8bit_tcp_resolve_tags;
// ...
BOOST_NETWORK_DEFINE_TAG(http_async_8bit_tcp_resolve);
// ...

cpp-netlib/boost/network/tags.hpp

// ...
struct async {
  typedef mpl::true_::type is_async;
};
// ...
template <class Tag>
struct components;

// Tag Definition Macro Helper
#ifndef BOOST_NETWORK_DEFINE_TAG
#define BOOST_NETWORK_DEFINE_TAG(name)                                        \
  struct name                                                                 \
      : mpl::inherit_linearly<name##_tags,                                    \
                              mpl::inherit<mpl::placeholders::_1,             \
                                           mpl::placeholders::_2> >::type {}; \
  template <>                                                                 \
  struct components<name> {                                                   \
    typedef name##_tags type;                                                 \
  };
#endif  // BOOST_NETWORK_DEFINE_TAG
// ...

モックの作成

まず、タグ型を定義します。

mock.hpp

namespace boost { namespace network {
namespace tags {
    struct mock {
        using is_mock = mpl::true_::type;
    };
}
namespace http { namespace tags {
    using http_mock_8bit_tcp_resolve_tags =
        mpl::vector<http, client, simple, mock, tcp, default_string>;
    
    BOOST_NETWORK_DEFINE_TAG(http_mock_8bit_tcp_resolve);
}}
}

次に、これを用いて client_base を特殊化します。

mock.hpp

namespace boost { namespace network {
    template <class Tag, class Enable = void>
    struct is_mock : mpl::false_ {};
    
    template <class Tag>
    struct is_mock<Tag, typename enable_if<typename Tag::is_mock>::type> : mpl::true_ {};
    
namespace http { namespace impl {
    template <class Tag, unsigned version_major, unsigned version_minor>
    struct client_base<Tag, version_major, version_minor,
        typename enable_if<is_mock<Tag>>::type>
    {
        using type = mock_client<Tag, version_major, version_minor>;
    };
}}
}}

最後に、mock_client を作成し、request_skeleton() では適当なレスポンスを作成し返すようにします。

mock.hpp

// ...
namespace boost { namespace network { namespace http { namespace impl {
    template <class Tag, unsigned version_major, unsigned version_minor>
    struct mock_client {
        using resolver_type = typename resolver<Tag>::type;
        using string_type = typename string<Tag>::type;
        using body_callback_function_type = function<
            void (iterator_range<char const *> const &, system::error_code const &)>;
        using body_generator_function_type = function<bool(string_type &)>;
        
        mock_client(
            bool cache_resolved,
            bool follow_redirect,
            bool always_verify_peer,
            int timeout,
            boost::shared_ptr<boost::asio::io_service> service,
            optional<string_type> const & certificate_filename,
            optional<string_type> const & verify_path,
            optional<string_type> const & certificate_file,
            optional<string_type> const & private_key_file
        ) {}
        
        void wait_complete() {}
        
        basic_response<Tag> const request_skeleton(
            basic_request<Tag> const & request,
            string_type const & method,
            bool get_body,
            body_callback_function_type callback,
            body_generator_function_type generator
        ) {
            std::cout << "#request" << std::endl;
            std::cout << method << std::endl;
            std::cout << request.uri().string() << std::endl;
            for (auto && h: network::headers(request)) {
                std::cout << h.first << ": " << h.second << std::endl;
            }
            
            basic_response<Tag> result;
            result
                << http::status(200)
                << network::header("Content-Type","text/plane")
                << network::header("Content-Length","4")
                << network::body("test");
            return result;
        }
    };
}}}}

これで、モックができました。実際に使用してみます。

main.cpp

#include <iostream>
#include <boost/network/protocol/http/client.hpp>
#include "mock.hpp"

int main() {
    namespace network = boost::network;
    namespace http = network::http;
#if 1
    using tag = http::tags::http_mock_8bit_tcp_resolve;
#else
    using tag = http::tags::http_async_8bit_tcp_resolve;
#endif
    using client_type = http::basic_client<tag,1,1>;
    
    client_type::request request("http://www.boost.org");
    request << network::header("Connection","close");
    client_type client;
    client_type::response response = client.get(request);
    
    std::cout << "#response" << std::endl;
    std::cout << http::status(response) << std::endl;
    std::cout << http::body(response) << std::endl;
    
    for (auto && h: http::headers(response)) {
        std::cout << h.first << ": " << h.second << std::endl;
    }
    
    return 0;
}

実行結果

$ ./main
#request
GET
http://www.boost.org
Connection: close
#response
200
test
Content-Length: 4
Content-Type: text/plane

どうやらうまくいったようです。あとは任意のレスポンスを注入する機能を実装すればOKです。