読者です 読者をやめる 読者になる 読者になる

理系学生日記

おまえはいつまで学生気分なのか

忍者TOOLS

lsの結果をawkすることについての注意点

technology

重箱の隅を突つくような話ですが、ls の結果を awk するのは筋が悪いと言われています。ファイルパーミッションを抜くとか、そういうときに、このような使われ方が為されますが、だいたいの場合は代替案(もうちょっと相応しいコマンド) があるので注意しましょう。 ls -l | awk とかが良くないとされるのは、ぼくの知る限り、これは次の 2 つの理由に依っています。

  1. ls -l 等で表示されるカラム順は定義されていないため、システム間で順番が異なる可能性がある
  2. ls が表示するファイル名というのは任意の文字を含み得る

1 つ目については、まぁそうだよね、という話ですが、2 つ目については具体的な例があった方が分かりやすいかもしれません。

以下のように、改行を含むファイル名というのは容易に作成することができます。

$ touch "Hello
World"
$ touch "Hello
World2"

ここで ls してみると、あたかも普通に表示できます。

$ ls -l
total 0
-rw-r--r-- 1 kiririmode staff 0  7 23 15:34 'Hello'$'\n''World'
-rw-r--r-- 1 kiririmode staff 0  7 23 15:34 'Hello'$'\n''World2'

が、ファイル名に改行コードを含むのは間違いないので、これを awk に渡してしまうと想定通りの出力にはなりません。

$ ls -l | awk '{ print $0 }'
total 0
-rw-r--r-- 1 kiririmode staff 0  7 23 15:34 Hello
World
-rw-r--r-- 1 kiririmode staff 0  7 23 15:34 Hello
World2

信頼できない入力文字列をそのまま信じてしまうとマズいという意味では、Web アプリケーションにおけるサニタイズとかと同じようなものかもしれないですね。 とはいえ、それを気にしないといけない状況っていうのは滅多にないと思いますが。

ちなみに、制御文字とかも余裕でファイル名に入れることができるので、いろいろ楽しんで人に迷惑をかけて怒られたら良いですね。

$ touch "$(echo -e "ab\ac")"

Quitter便利

software environment

Quitter よかった。

Quitter、一定時間(アプリ毎に設定可能) 使ってない特定のアプリを自動的に終了させたりしてくれるアプリで、Twitter とか切るようにして生産性を上げようっていうものなんだけど、「辞書」アプリとか Kindle とかが立ち上がったままでアプリの切り替え(Command + Tab 押下) の度にウザいなーって思ってたのが解消されて大変よろしい。 画面上から、使っていないアプリが自動的にいなくなるっていうところも良い。スッキリする。 Quitter そのものは、メニューバーに常駐するだけで、ほとんど意識しなくて良いところも良い。無意識のまま QOL が向上するのお得感ある。

PRGパターンとは何か

technology

最近はじめてフロントエンドの開発をすることになって、PRG パターンって常識だよみたいな雰囲気でウワワワワー! ってかんじになりました。義務教育ではそんなの教えてくれなかった。 そういうわけなので、涙ごしに霞むモニタと向きあいながら PRG パターンについて調べてみました。

PRG というのは、Post/Redirect/Get の略で、

  1. 二重サブミット対策
  2. セキュリティ対策

の双方の文脈で使用されるパターンのようです。 PRG パターンについての言及は、基本的にこちらの解説に到達しているようですし、PRG パターンの命名もこのページ上でされていることもありますので、まぁここを読んでいれば間違いないんじゃないでしょうか。 ]

PRG パターン

PRG パターンのない素直な実装の問題

PRG パターンの意味する Post/Redirect/Get は、画面を表示する際のクライアント (UserAgent) - サーバ間のやりとりを示しています。ここでいう画面っていうのは、「ユーザが Web サイトのページ上で入力した情報を POST メソッドで送信するときに表示する画面」のことです。 普通にサーバ側の処理を実装しようとすると、POST メソッドのリクエストを受信した後、その BODY 部に含まれる情報を元にして DB 更新等の処理を行い、結果を表示する完了画面をクライアントに返却する、って実装になります。 これを図示すると以下のような図になります。(図は wikipedia:Post/Redirect/Get より)

f:id:kiririmode:20160618170234p:plain

この画面遷移の何が問題になるかというと、入力した情報の二重サブミットです。ユーザが意図せず二重サブミットを行ってしまうことにより、二重課金や二重購入なんて事象が発生します。 このケースにおいては、たとえばユーザによる以下のような操作により、二重サブミットが発生します。

  1. 完了画面において、ユーザが画面をリロードする (F5 ボタンなど)
  2. 完了画面で「戻る」ボタンを押下して入力画面に戻った後、「進む」ボタンを押下する
  3. 完了画面で「戻る」ボタンを押下して入力画面の戻った後、「Submit」ボタンを押して入力した情報を再送信する

このうち、PRG パターンは 上 2 つに直接対応します。

PRG パターン

PRG パターンを使った UserAgent - サーバ間は以下のような通信になります。

f:id:kiririmode:20160618170301p:plain

前の図よりも複雑になっていますが、UserAgent が POST でリクエストを送信した際、サーバはそれを処理するとともに、HTTP ステータス 3xx を UserAgent に返却します。通常、302 または 303 が使用されることが多いみたいですね (理由は後述します)。 UserAgent は 3xx に従って、GET リクエストをサーバに送ります。サーバはその GET リクエストに応答する形で、完了画面を返すという流れになります。

この PRG パターンの何がポイントかというと、ユーザがブラウザのリロード等をした際にサーバに送信されるリクエストが、(ユーザが入力した情報を含んだ) POST ではなく、情報を含まない GET で行われるという点です。こういう GET だったら、重複してリクエストをされようが、単に同じ完了画面を返却してやれば良いですね。

あれ 3 番目の対策は

前述のとおり、二重サブミットを起こす方法として、

  1. 完了画面で「戻る」ボタンを押下して入力画面の戻った後、「Submit」ボタンを押して入力した情報を再送信する

ていうのもありますが、PRG パターンに関して述べられているのは、「戻る」ボタンを押下したときの画面はキャッシュを無効化しておいて、input フィールドに値を保存させないようブラウザに指示しておくことです。 この他、トークンを使う方法があり、PRG のページにも「Prevent resubmits」でトークンについて触れられています。 ただ、実際にこの手の二重サブミットを防ぐ方法は、以下のようなページを参照すれば良いんじゃないですかね。

セキュリティ対策

PRG パターンについては、上述のとおり主に二重サブミット対策として述べられることが多いですが、セキュリティ対策として用いられることがあります。 具体例として、まずは問題となるケースを示します。

  1. PC を使うユーザが、ログイン画面でログインパスワードを入力
  2. Submit ボタンを押して、ログイン成功
  3. ユーザが PC を離れる
  4. 悪意のある第三者が当該 PC・ブラウザを使用し、リロード (あるいは「戻る」ボタン→ 「進む」ボタンを押下)

これにより、3. のタイミングで例えセッションが切れていたとしても、4. で第三者がログインに成功しますし、あるいは、再送信される POST リクエストを解析すれば、パスワードを入手することが可能なケースがあります。 これも結局、秘匿すべき情報を含む POST リクエストが再送信されることが問題なので、PRG パターンを採用することでこの問題を解決することができます。

HTTPS双方向認証の環境を作る

technology java

以下のエントリで HTTPS 通信を試してみましたが、このような試験環境を作るのはわりとメンドい。 メンドいことを何度もやりたくないので、実施した内容をエントリに残しておきます。

クライアント認証を含めた HTTPS の双方向認証を行うためには、以下が必要になります。

  1. CA の作成
  2. サーバ証明書の作成
  3. クライアント証明書の作成
  4. HTTPS に対応したサーバの立ち上げ
  5. (Java の場合) KeyStore、TrustStore の作成

CA を作成する

CA が使用する秘密鍵を作成した後、証明書を作成するかたちになります。

# CA が使用する秘密鍵を ca.key として作成する 
$ openssl genrsa -out ca.key 2048
# CA の証明書を ca.crt として作成する
$ openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -subj '/CN=kiririmode CA'

サーバ証明書を作成する

サーバ証明書の作成には、CSR を作成した上で、CA に署名してもらえば良いです。

# サーバ証明書用の CSR を作成する
$ openssl genrsa -out server.key 2048
$ openssl req -new -key server.key -out server.csr -subj '/CN=server.kiririmode.com'

# CA に CSR に署名してもらい、サーバ証明書 (server.crt) を作成する
$ openssl x509 -req -days 3650 -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt

クライアント証明書を作成する

ここでは 2 つ作成してみます。やってることはサーバ証明書と同じですね。

$ openssl genrsa -out client1.key 2048
$ openssl req -new -key client1.key -out client1.csr -subj '/CN=com.kiririmode.client1'
$ openssl x509 -req -days 3650 -CA ca.crt -CAkey ca.key -CAcreateserial -in client1.csr -out client1.crt

$ openssl genrsa -out client2.key 2048
$ openssl req -new -key client2.key -out client2.csr -subj '/CN=com.kiririmode.client2'
$ openssl x509 -req -days 3650 -CA ca.crt -CAkey ca.key -CAcreateserial -in client2.csr -out client2.crt

HTTPS に対応したサーバの立ち上げ

せっかく openssl を使っているのであれば、openssl にサーバも任せてしまえば良いです。以下の 1 行だけで、HTTPS サーバが立ち上がります。 -Verify はクライアント証明を必須とするオプションです。

$ openssl s_server -accept 14433 -cert server.crt -key server.key -Verify 5 -CAfile ./ca.crt -www -debug

(Javaの場合)KeyStore、TrustStore の作成

KeyStore の作成については注意すべき点があって、クライアント証明書を keytool でそのままインポートしたとしても、秘密鍵の情報が KeyStore に格納されない。 これを何とかするためには、一度 X.509 の証明書と鍵を PKCS12 形式に変換してやってから、keytool でインポートする。

$ openssl pkcs12 -export -in client1.crt -inkey client1.key -out client1.p12 -name client1 -CAfile ca.crt -caname ca
$ keytool -importkeystore -destkeystore keystore.ks -srckeystore client1.p12 -srcstoretype PKCS12 -srcstorepass password -alias client1

$ openssl pkcs12 -export -in client2.crt -inkey client2.key -out client2.p12 -name client2 -CAfile ca.crt -caname ca
$ keytool -importkeystore -destkeystore keystore.ks -srckeystore client2.p12 -srcstoretype PKCS12 -srcstorepass password -alias client2

TrustStore については、単に作れば良いんじゃないですかね。

$ keytool -import -keystore truststore.ks -file ca.crt     -alias kiririmodeca
$ keytool -import -keystore truststore.ks -file server.crt -alias server

参考: importing an existing x509 certificate and private key in Java keystore to use in ssl - Stack Overflow

JavaのJSSEでクライアント証明書を自由に選択できるようにする

java

HTTPS で API を呼び出すっていうシーンは頻繁にあって、その API を使うには、通常何らかの認証が求められます。 認証にも色々あるんだけど、そのうちの一つが HTTPS のクライアント認証です。 普通に HTTPS 通信をしたいだけだと意識しないことも多いのですが、宛先サーバ毎にクライアント証明書を使い分ける、なんてニーズが生じたときにはこのあたりの理解をしておくことは不可避になります。

こういうのを Java でやれって言われたときに、エッどうやって実現するの、みたいなかんじだったので、調べたり実装してみて調べた結果をまとめてみます。

前提知識としては、以下のようなことが分かっていれば良いと思います。

知識編

2 つの証明書ストア (TrustStore と KeyStore)

まず、Java における SSL/TLS の実装は JSSE (Java Secure Socket Extension) によって提供されるのですが、ここで KeyStore と TrustStore という用語が出てきます。 KeyStore と TrustStore はすごく重要な概念なんですが、異常に混乱を招きがちだし、そもそも JSSE のリファレンスガイドからしてこんなの分かんねーよという説明しかされていない。なんだこれ、暗号か?

キーストアは、鍵データのデータベースです。鍵データにはさまざまな用途があり、それには認証やデータ整合性も含まれます。利用できるキーストアには様々なタイプがあり、その中にはPKCS12やOracleのJKSも含まれます。 (略) トラストストアとは、トラストの対象を決めるときに使用するキーストアです。すでに信頼しているエンティティからデータを受け取る場合、およびそのエンティティが発信元を名乗るエンティティであることを確認できる場合は、データは実際にそのエンティティから届いたものであると仮定できます。

でも、これらが分かっていないとクライアント認証だったりは実現できないし、そもそも JSSE がなかなか読み解けない。 そういうわけですから、まずは TrustStore と KeyStore を整理してみます。

SSL においては、通常、サーバ証明書によって、通信を行うサーバが「間違いなく自分の通信したいサーバであること」(逆に言えば、悪意のある第三者が用意したサーバでないこと)を確認することになります。PKI の仕組み上、このためには、サーバがクライアントにサーバ証明書を提示し、クライアントが「信頼してる機関 (CA) がこのサーバ証明書を(少なくとも間接的に)発行している」という判断が必要です。 逆にクライアント証明では、クライアントがサーバに対してクライアント証明書を提示し、サーバが「信頼してる機関 (CA) がこのクライアント証明書を(少なくとも間接的に)発行している」と判断することが必要になります。

このために、クライアント側では以下の情報を保持しておく必要があります。

  1. 自分がどの証明書(CA 証明書、サーバ証明書) を信じるのか
  2. 自分がどのクライアント証明書をサーバに提示するのか

この 1. の情報を保持しておくのが TrustStore、2. の情報を保持しておくのが KeyStore です。TrustStore は Trust、つまり自分の信頼するものが何かを蓄えるもの、KeyStore は自分が提示する秘密情報 (Key) を蓄えるもの、って考えるとちょっと覚えやすくなるかもしれません。 ちなみに、この TrustStore と KeyStore はどちらも Java でいう "KeyStore" とよばれるファイル(実際にはファイルでなくても良いですが、ファイルの形を取ることが多い) に保存されたりするのが話をややこしくする元凶なんじゃないかと思います。

まぁこのあたりは JSSE よりも キーストアとトラストストア (SSL をサポートする Java CAPS の構成) の記述もわかりやすいですね。

JSSE では、「キーストア」および「トラストストア」と呼ばれるファイルを使用します。キーストアは、アダプタでクライアント認証に使用され、トラストストアは、SSL 認証でサーバーを認証する際に使用されます。

要するに、TrustStore が相手が正しいかを検証するのに使用するもので、KeyStore は自分が正しいかを相手に伝えるのに使用するものです。どちらも証明書を保持しています。

TrustManager と KeyManager

TrustStore、KeyStore はともにストレージみたいに証明書を保存するためのものなので、それを使う人が必要です。

TrustStore を使って相手から送られてきたサーバ証明書が信頼できるかを検証する責務を負うのが TrustManager、 KeyStore を使って、どのクライアント証明書を相手に送付するかを決めるのが KeyManager です。

これらはともにインタフェースとして定義されていて、自由に実装をつくることができます。 ただ、実際にこれらの実装を自分でインスタンス化するってことは滅多になくて、これらのファクトリクラスである KeyManagerFactory や TrustManagerFactory を作成し、これらから KeyManager、TrustManager を作成するってことが大半です。

SSL についての情報を保持する SSLContext

この図は、JSSE のリファレンスガイドに登場する図なんですが、前述の TrustManager や KeyManager を使って、SSLSocket (名前の通り SSL のソケットを表現するクラス) を作る SSLSocketFactory を構築するのが SSLContext クラスです。この SSLContext は、SSL 実装に関する情報を保持しています。

f:id:kiririmode:20160612022046j:plain

クライアント認証に関する何らかの操作を実現しないといけない場合、往々にして KeyManager をカスタマイズする必要がありますが、カスタマイズした KeyManager を使ってくれ、ということを SSLContext に教える必要があります。 Apache の httpComponent などの HTTP クライアント実装も、だいたいこの SSLContext を差し替えることができるようになっているので、その差し替えによってクライアント証明書の選択とかを HTTP クライアントライブラリ側に反映させることができます。

httpComponent だと以下のようなかたちで、httpClient を作成する段階で SSLContext を自作のものに設定できます。

try (CloseableHttpClient client = HttpClients.custom().setSSLContext(sslContext).build()) {
  HttpGet getMethod = new HttpGet("https://server.kiririmode.com:14433/index.html");

  client.execute(getMethod);
}

デフォルトのクライアント証明書の選択

さて、それではクライアント証明書がデフォルト実装でどのように実装されるかを見てみました。 あくまで java.security をカスタマイズしていない前提ですが、何も意識しないで HTTPS 通信を行った場合、クライアント証明書は以下の条件に合致するものが選択されます。

  1. KeyManager が読み込む KeyStore に格納されているクライアント証明書である
  2. サーバが Certificate Request の中で送信してくる信頼する CA が発行している
  3. 鍵のアルゴリズムも合致する

複数のクライアント証明書が上記の条件に合致した場合は、最初に発見されたものが選択されます。 つまり、同じ CA が発行しているようなクライアント証明書が複数あった場合、対向先システムとか関係なしにそのうちの 1 つが選択されてしまうってことになります。 この振舞いについては、割と不都合あるケースが多いんじゃないでしょうか。

実装編

というわけで、上記のような基本知識を元にして、「クライアント証明書を対向サーバ毎に別々に選択する」っていうのを作ってみます。

クライアント証明書を自由に選択できる KeyManager

最初に作らないといけないのは、KeyManager の実装なんですが、実際にはクライアント証明書の形式としては通常 X.509 しかないはずなので、X509Keymanager インタフェースを実装する形で良いと思います。 クライアント証明書を選択するのは chooseClientAlias メソッドなので、これのみ実装を差し替えれば良い。というわけで、移譲させるようにします。

実装自体は、以下のとおりクライアント証明書を選択するのは Strategy パターンにしておきました。

@FunctionalInterface
public interface AliasSelectionStrategy {
  
  String selectAlias(String[] keyType, Principal[] issuers, Socket socket);

}
public class StrategyKeyManagger implements X509KeyManager {
  
  private X509KeyManager defaultKeyManager;
  private AliasSelectionStrategy aliasSelectionStrategy;
  
  public StrategyKeyManagger(X509KeyManager defaultKeyManager, AliasSelectionStrategy strategy) {
    this.defaultKeyManager = defaultKeyManager;
    this.aliasSelectionStrategy = strategy;
  }

  @Override
  public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
    String alias = aliasSelectionStrategy.selectAlias(keyType, issuers, socket);
    return (alias != null)? alias : defaultKeyManager.chooseClientAlias(keyType, issuers, socket);
  }

  @Override
  public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
    return defaultKeyManager.chooseServerAlias(keyType, issuers, socket);
  }
  // 以下略

SSLContext の差し替え

あとは、この KeyManager を使用して SSL 通信を行うように SSLContext を差し替えれば良いです。 これは以下のステップで実施します。

  1. デフォルトで使用される KeyManager を取得する
  2. そのうち、X509KeyManager の実装クラスを上記で作成した StrategyKeymanager に差し替える
  3. 差し替えた KeyManager で SSLContext を初期化する
  private static SSLContext getSSLContext(
      String keyStorePath, String keyStoreType, char[] keyStorePassword, AliasSelectionStrategy strategy,
      String trustStorePath, String trustStoreType, char[] trustStorePassword
      ) throws IOException, GeneralSecurityException {
    
    TrustManager[] trustManagers = getTrustManager(trustStorePath, trustStoreType, trustStorePassword);
    // 1. デフォルトで使用される KeyManager を取得する
    KeyManager[] keyManagers = getKeyManagers(keyStorePath, keyStoreType, keyStorePassword);
    
    // 2. KeyManager を差し替える
    for (int i = 0; i < keyManagers.length; i++) {
      if (keyManagers[i] instanceof X509KeyManager) {
        keyManagers[i] = new StrategyKeyManagger((X509KeyManager)keyManagers[i], strategy);
        break;
      }
    }
    
    // 3. 差し替えた KeyManager で SSLContext を初期化する
    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(keyManagers, trustManagers, null);
    return sslContext;
  }
    
  private static KeyManager[] getKeyManagers(String keyStorePath, String keyStoreType, char[] keyStorePassword) throws IOException, GeneralSecurityException {
    String algorithm = KeyManagerFactory.getDefaultAlgorithm();
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
    
    KeyStore keyStore = null;
    try (InputStream is = Files.newInputStream(Paths.get(keyStorePath))) {
      keyStore = KeyStore.getInstance(keyStoreType);
      keyStore.load(is, keyStorePassword);
    }
    
    kmf.init(keyStore, keyStorePassword);
    return kmf.getKeyManagers();
  }    

クライアント証明書の選択の例

ここでは、対向サーバのホスト名毎にクライアント証明書を切り替える実装を作ってみました。

public class HostBasedAliasSelectionStrategy implements AliasSelectionStrategy {

    private Map<String, String> aliasMap;

    public HostBasedAliasSelectionStrategy(Map<String, String> hostAliasMap) {
        this.aliasMap = hostAliasMap;
    }

    @Override
    public String selectAlias(String[] keyType, Principal[] issuers, Socket socket) {
        // 対向ホスト名のクライアント証明書用 alias が Map に登録されていれば、それを使用する
        String hostName = socket.getInetAddress().getHostName();
        if (aliasMap.containsKey(hostName)) {
            return aliasMap.get(hostName);
        }
        return null;
    }
}

組み合わせる

あとはこれらを使って、HTTPS 通信をすれば良いでしょう。

ここでは、server.kiririmode.com 宛の通信には、client1 という alias で登録されたクライアント証明書が使用されます

  public static void main(String[] args) throws Exception{
    
    // server.kiririmode.com 宛の通信には、`client1` という alias で登録されたクライアント証明書を使用する
    Map<String, String> aliasMap = Collections.singletonMap("server.kiririmode.com", "client1");
    HostBasedAliasSelectionStrategy strategy = new HostBasedAliasSelectionStrategy(aliasMap);
    
    SSLContext sslContext = getSSLContext(
        "/path/to/keystore.ks", "JKS", "password".toCharArray(), strategy,
        "/path/to/truststore.ks", "JKS", "password".toCharArray()
        );
    try (CloseableHttpClient client = HttpClients.custom().setSSLContext(sslContext).build()) {
      HttpGet getMethod = new HttpGet("https://server.kiririmode.com:14433/index.html");
    
      client.execute(getMethod);
    }

参考文献

  1. JSSEリファレンス・ガイド
  2. Difference between trustStore and keyStore in Java - SSL
  3. Article: Creating Custom Key Managers
  4. 上級JSSE開発者のためのカスタムSSL

Stubby4JにおけるJSONでの正規表現、あるいはリクエスト中の特定のフィールドでレスポンスを変更する

technology java

Stubby4J を使用したテストにおいて、API リクエストとして送る JSON の中の値によって、レスポンスを切り替えたい場合がある。 もうちょっと具体的にいうと、

  1. リクエストの JSON の中の特定のフィールドの値によってのみ、レスポンスを切り替える
  2. 他のフィールドの値は実行時まで予想できない

こういうケースでレスポンスを切り替えるのは Stubby4J (v3.3.0) ではできないということが分かったので、その原因と、何とかする方法を考えてみる。

実装上できない理由

Stubby4J では、実際に送られてきた HTTP リクエストと、設定ファイル(YAML) 上に記述されたリクエストを StubRequest#equals(Object) で突合し、合致する場合にのみ、YAML ファイル上に記述されたレスポンスを返却するという動作になっている。 この「突合」(equals) の実装は、実際の HTTP リクエストの Content-Type によって挙動を替えるようになっている。application/json の場合は、「送信されてきた JSON」と「YAML ファイル上に記述された JSON」が、JSON のフィールド順を除いて完全一致すること が条件になっている。 以下に実装を示すけど、JSONCompare を使用しているということは、HTTP リクエスト上の JSON はもちろん、YAML 上に記述した JSON も valid である必要があるので、何らかの細工 (正規表現を記述するなど) もできない。

   private boolean postBodiesMatch(final boolean isDataStorePostStubbed, final String dataStorePostBody, final String thisAssertingPostBody) {
         // snip

         } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_JSON)) {
            try {
               return JSONCompare.compareJSON(dataStorePostBody, thisAssertingPostBody, JSONCompareMode.NON_EXTENSIBLE).passed();
            } catch (JSONException e) {
               return false;
            }
         } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_XML)) {

         // snip
   }

設計思想によってできない理由

Json request body matching by vishalmanohar · Pull Request #19 · azagniotov/stubby4j · GitHub で、JSON の中の値でレスポンスを切り替えられるような Pull Request があった。 これを利用すると、以下のようにリクエストの JSON に依存したレスポンスの設定がかけるようになる。

-  request:
      method: POST
      url: /templatize/response
   response:
      -  status: 200
         headers:
            content-type: application/json
         body: >
               <#assign requestBodyJson = requestBody?eval>
               {"foo": "${requestBodyJson.foo}"}

のだけれど、この Pull Request に対しては「HTTP リクエストと YAML 上のリクエストの突合は HTTP 1.1 レベルでしか実施しない」という設計思想が語られており、Merge されないままクローズされた。 実はこのあとに Content-Type に依存した突合の実装が取り込まれていることから鑑みるに、設計思想が変化しているみたいなのだけれど…。

何とかする方法

上記の PR と同じような修正を行ったり、あるいは現行の実装で使用している JSONCompare での比較ではなく、もっと柔軟な比較 (正規表現を許容する、jsonpath で比較すべきフィールドを指定する、JSON Schema を書けるようにする、等) をできるようにする、などはできるのだけど、とりあえず実現するだけなら以下で可能になる。

$ git diff  main/java/by/stub/yaml/stubs/StubRequest.java
diff --git a/main/java/by/stub/yaml/stubs/StubRequest.java b/main/java/by/stub/yaml/stubs/StubRequest.java
index 7e6a8a8..d76d6de 100644
--- a/main/java/by/stub/yaml/stubs/StubRequest.java
+++ b/main/java/by/stub/yaml/stubs/StubRequest.java
@@ -252,12 +252,6 @@ public class StubRequest {
          final boolean isAssertingValueSet = isSet(thisAssertingPostBody);
          if (!isAssertingValueSet) {
             return false;
-         } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_JSON)) {
-            try {
-               return JSONCompare.compareJSON(dataStorePostBody, thisAssertingPostBody, JSONCompareMode.NON_EXTENSIBLE).passed();
-            }

これはかなり強引な方法で、いってみれば「JSON とか知るか!! 文字列として扱うんや!!! 」という方法である。 つまり、リクエストが valid JSON でなくても、XML や Form であってもマッチしてしまうので、YAML 上でリクエストを記述するときにかなりの注意を払わなければならない。

上記修正を行った後で、gradle build -x test でビルドして実験し、以下のような YAML で Stubby4J を立ち上げる。

- request:
    method: POST
    url: /hello-world
    post: >
      \{\s*"hello"\s*:\s*"(.*?)"\s*.*\}

  response:
    status: 200
    headers:
      content-type: application/json
    body: Hello, <% post.1 %>

これに対して、リクエストを送信すると、きちんとレスポンスを切り替えられるようになる

$ curl -H'content-type: application/json' -d "{\"hello\":\"kiririmode\", \"hoge\":\"fuga\" }" http://localhost:8882/hello-world
Hello, kiririmode
$ curl -H'content-type: application/json' -d "{\"hello\":\"world\", \"var\":\"val\" }" http://localhost:8882/hello-world
Hello, world

15分でWebSphere MQの環境を構築しメッセージの読み書きを行う

middleware technology

WebSphere MQ の実験するんだったら Docker 使うのが一番楽だと思います。 調べてたら、IBM が公式に WebSphere MQ v8.0 の Docker Image を作ってくれてて、これあったら 15 分くらいで WebSphere MQ が使える環境が整います。便利な世の中ですね。

ただし、上記イメージには、いわゆる WebSphere MQ のサンプルアプリが入っていないので、すぐ QUEUE にメッセージを PUT してみたい、みたいなことができない。 そういうわけで、fork してサンプルアプリもインストールするように Dockerfile を書き換えた。

それでは、これを使って 15 分くらいで WebSphere MQ が使える環境を整えましょう。

file-max を増やす

WebSphere MQ は、インストール時に fs.file-max を 524288 以上かどうかのチェックが入っており、デフォルト設定だと docker container の起動でコケる。 これは docker host 側の設定になるので、ここでは以下のコマンドで当該の制限値を変更しておく。

$ docker-machine ssh default sudo 'sysctl -w fs.file-max=524288'

Docker Container を立ち上げる

まずはリポジトリを clone して、Dockerfile から Docker Container を作成。

$ git clone https://github.com/kiririmode/mq-docker
$ cd mq-docker
$ docker build --tag mq ./8.0.0/

その後は、Container を立ち上げるだけです。

$ mkdir ~/work && cd ~/work
$ docker run --env LICENSE=accept --env MQ_QMGR_NAME=QM1 --publish 1414:1414 --detach mq
$ docker start <Container ID>
$ docker exec -it <Container ID> /bin/bash
$ docker exec -it <Container ID> /bin/bash
(mq:8.0)root@1a980fdb7590:/# su - mqm
$

コンテナを立ち上げた段階でキューマネージャ QM1 が作成されていますが、これは docker run 実行時の MQ_QMGR_NAME 指定によるものです。 dspmq コマンドで確認できます。

$ dspmq
QMNAME(QM1)                                               STATUS(Running)

キューマネージャを作成する

新しくキューマネージャを作成するのは crtmqm コマンド。-q オプションは、作成したキューマネージャをデフォルトキューマネージャに指定するものです。

$ crtmqm -q host1/qm1
WebSphere MQ queue manager created.
Directory '/var/mqm/qmgrs/host1&qm1' created.
The queue manager is associated with installation 'Installation1'.
Creating or replacing default objects for queue manager 'host1/qm1'.
Default objects statistics : 79 created. 0 replaced. 0 failed.
Completing setup.
Setup completed.

デフォルトキューマネージャの情報は、/var/mqm/mqs.ini に格納されています。

$ grep -A1 DefaultQueueManager /var/mqm/mqs.ini
DefaultQueueManager:
   Name=host1/qm1

キューマネージャを起動する

キューマネージャの起動は strmqm コマンドで実行します。引数にはキューマネージャ名を取るけど、デフォルトキューマネージャに対する操作の場合は省略可能です。

$ strmqm host1/qm1
WebSphere MQ queue manager 'host1/qm1' starting.
The queue manager is associated with installation 'Installation1'.
5 log records accessed on queue manager 'host1/qm1' during the log replay phase.
Log replay for queue manager 'host1/qm1' complete.
Transaction manager state recovered for queue manager 'host1/qm1'.
WebSphere MQ queue manager 'host1/qm1' started using V8.0.0.4.

ローカルキューを作成する

ローカルキューの作成は runmqsc コマンド経由でプロンプトを取って行います。実際にローカルキューを作成するのは DEFINE QLOCAL

$ runmqsc host1/qm1
(snip)
DEFINE QLOCAL('queue1') DESCR('newly defined')
     1 : DEFINE QLOCAL('queue1') DESCR('newly defined')
AMQ8006: WebSphere MQ queue created.

この確認には、DISPLAY QLOCAL を使用します。DISPLAY QLOCAL(<QNAME>) ALL を使用すると情報が多すぎるので、特定の情報だけ見たい場合は見たい属性の名前を明示的に指定してやる方が良いでしょう。

# 情報量が多い
DISPLAY QLOCAL('queue1')
     1 : DISPLAY QLOCAL('queue1')
AMQ8409: Display Queue details.
   QUEUE(queue1)                           TYPE(QLOCAL)
   ACCTQ(QMGR)                             ALTDATE(2016-05-04)
   ALTTIME(09.34.30)                       BOQNAME( )
(snip)

# 情報量を絞る
DISPLAY QLOCAL('queue1') CURDEPTH DESCR
     3 : DISPLAY QLOCAL('queue1') CURDEPTH DESCR
AMQ8409: Display Queue details.
   QUEUE(queue1)                           TYPE(QLOCAL)
   CURDEPTH(0)                             DESCR(newly defined)

ローカルキューの属性を変更する

ローカルキューの属性を変更するのは ALTER QLOCAL コマンド。

# 変更
ALTER QLOCAL('queue1') DESCR('kiririmode')
     4 : ALTER QLOCAL('queue1') DESCR('kiririmode')
AMQ8008: WebSphere MQ queue changed.

# 確認
DISPLAY QLOCAL('queue1') DESCR
     5 : DISPLAY QLOCAL('queue1') DESCR
AMQ8409: Display Queue details.
   QUEUE(queue1)                           TYPE(QLOCAL)
   DESCR(kiririmode)

メッセージを PUT してみる

WebSphere MQ にはデフォルトでサンプルプログラム群が含まれており、メッセージを PUT するサンプルとして amqsput が提供されている。 コマンドの引数にローカルキュー名を渡して起動すると、標準入力でメッセージ入力を行えるようになる。ここでは hello world! と入力してみた。入力の終了は空行で判断されるらしい。

$ amqsput queue1
Sample AMQSPUT0 start
target queue is queue1
hello world!

Sample AMQSPUT0 end

この後で、queue1 の中に入っているメッセージ長(CURDEPTH) を表示すると、1 に増えていることが確認できる。

DISPLAY QLOCAL('queue1') CURDEPTH
     2 : DISPLAY QLOCAL('queue1') CURDEPTH
AMQ8409: Display Queue details.
   QUEUE(queue1)                           TYPE(QLOCAL)
   CURDEPTH(1)

キューの中のメッセージを表示してみる

キューの中を覗くサンプルプログラムは amqsbcg です。

詳細は、キュー・マネージャーの独り言: 第5回 使って便利なサンプル・プログラム に記載があります。

amqsbcgは、指定されたキューからメッセージを読み込み、そのヘッダ情報とデータ本体を標準出力へ表示するプログラムです。ただし、amqsgetとは異なりbrowseするだけですので、メッセージ自体はキューに残ります。

amqsbcg では、メッセージの MQMD まで見ることができるのがメリットです。

$ amqsbcg queue1

AMQSBCG0 - starts here
**********************

 MQOPEN - 'queue1'


 MQGET of message number 1, CompCode:0 Reason:0
****Message descriptor****

  StrucId  : 'MD  '  Version : 2
  Report   : 0  MsgType : 8
  Expiry   : -1  Feedback : 0
  Encoding : 546  CodedCharSetId : 819
  Format : 'MQSTR   '
  Priority : 0  Persistence : 0
  MsgId : X'414D5120686F7374312F716D31202020D7E3295704220020'
  CorrelId : X'000000000000000000000000000000000000000000000000'
  BackoutCount : 0
  ReplyToQ       : '                                                '
  ReplyToQMgr    : 'host1/qm1                                       '
  ** Identity Context
  UserIdentifier : 'mqm         '
  AccountingToken :
   X'0431303030000000000000000000000000000000000000000000000000000006'
  ApplIdentityData : '                                '
  ** Origin Context
  PutApplType    : '6'
  PutApplName    : 'amqsput                     '
  PutDate  : '20160504'    PutTime  : '11590590'
  ApplOriginData : '    '

  GroupId : X'000000000000000000000000000000000000000000000000'
  MsgSeqNumber   : '1'
  Offset         : '0'
  MsgFlags       : '0'
  OriginalLength : '-1'

****   Message      ****

 length - 12 of 12 bytes

00000000:  6865 6C6C 6F20 776F 726C 6421           'hello world!    '



 No more messages
 MQCLOSE
 MQDISC

というわけで、基本的なキューへの読み書きが実現できました。

MacでIMEを切り替えるキーバインド

environment

Mac の IME として AquaSKK を使っているんですが、AquaSKK は Eclipse などとの相性が非常に悪くて、特定の文字が表示されないとか、Ctrl+J でモードを切り替えようとしたら改行コードが入力されてしまうとかで、ぼくは日に日にストレスを溜め込んでいます。 そういうわけですから、Eclipse のお供には専ら Google 日本語入力 を使用しているわけですが、この AquaSKK と Google 日本語入力が、意図せず切り替わってしまうことが多く、ぼくはメニューバーのアイコンをクリックして丁寧に IME を切り替えるという優しい運用をしていたんだけれど、そうこうしているうちにぼくのストレスは指数関数的に増えていってしまいました。

いちいちクリックするのがダルすぎるからショートカットでもないのかなと思ってネットを彷徨ってみると、Ctrl+Space で IME を切り替えられると書いている。

しかしですね、Ctrl+Space というのは Emacs を使っている人間からすると使用頻度の高いキーバインディングでして、これが使えなくなるというのは死に直結する。 Mac は Emacs キーバインドをそこかしこで採用しているくせに、なぜか Ctrl+Space を OS で奪いたがる。Spotlight でも同じ罪を犯してたぞ、おい。

これはもうダメですねと思っていたら、この Ctrl+Space って変更できるんですね。システム環境設定-キーボード-ショットカット-入力ソースで設定を変更できた。

f:id:kiririmode:20160503151204p:plain

Mac 界の方々におかれましては、大変申し訳ございませんでした。今後のご多幸をお祈りしております。

MQMDおよびMQIの整理

middleware technology

WebSphere MQ なんて聞いたこともなかったんだけど、なんかまぁ特定エリアではよく使われているらしい。 以下、WebSphere MQ と RabbitMQ とかと比較するのは比較軸としてふさわしくない気がしないでもないけど、Google Trends の結果としては以下のようなかんじ。

まぁそんな製品を勉強しなければならない状況は変わっていないので、いろいろと調べてみてる。 今読んでいるのは WebSphere MQ V6 Fundamentals。これを読んだら、全体感は掴めそうな気がする。

MQMD

個々の MQ メッセージに付与されるヘッダ。WebSphere MQ Message Descriptor の略。 代表的なヘッダフィールドには以下のようなものがある。

  • MsgType: レスポンスを不要とするメッセージであることを示すdatagramや、リクエストとそれに対する返信を示す requestreply、あるいは report など、メッセージの種別を示す。
  • Report: 通信先のシステム (キューマネージャ、あるいは、アプリケーション) に report の返信を要求することを示す。
  • ReplyToQ: 通信先のシステムに対し、「どのキューに」reply あるいは report を返信してほしいのかを示す。
  • ReplyToQMgr: ReplyToQ をホストしているキューマネージャ名を示す。通常、WebSphere MQ は、MQ メッセージが最初に PUT されたキューマネージャ名をこのフィールドに自動で書き込む。アプリケーションでの書き換えは可能。
  • MsgId: 名前のとおり、MQ メッセージの ID。アプリケーションが明示的に指定することも、WebSphere MQ に自動で生成させることも可能。
  • CorrelId: メッセージとメッセージの関連付けるフィールド。通常は、reply MQ メッセージの CorrelId フィールドに request MQ メッセージのメッセージ ID を格納することで、request-reply の対を関連付ける。
  • Persistence: MQ メッセージの永続性を示す。
  • CodedCharSetId: MQ の文脈では CCSID とも呼ばれ、文字コードセットの ID を示す。Web Sphere MQ はこの値を元にして、データを文字に変換することが「できる」 (変換可否は Format に依存し、例えばバイナリデータは変換不可)
  • Encoding: 数値データのエンディアンを示す
  • Expiry: メッセージの生存時間の指定で、1/10 秒単位で指定する。これを超えた MQ メッセージは、次に WebSphere MQ 内で参照 (原文的には "when an attempt is made to retrive or view them by an application") されたタイミングで削除される。WebSphere MQ V6.0 からは "expiry task" というのがあり、定期的にこういうメッセージを削除してくれるらしい。

Expiry については、後の参考文献に示すとおり When are expired WebSphere MQ messages removed from a queue? (Application Integration Middleware Support Blog) に気になる情報がある。Web Sphere MQ V6.0 以上の場合は内部に expiry task が存在しており、5 分毎に消してくれるらしい。コメント欄には以下の既述がある。

By default, the internal expirer task runs every 300 seconds (5 minutes). Lowering the frequency to every second could impact performance depending on your overall configuration (number of queues, etc).

MQI

MQI っていうのは、WebSphere MQ を使用した手続型のメッセージングインタフェースのことで、Message Queue Interface の略。全部で 13 の関数が定義されている。 Java や C++ のようなオブジェクト指向言語で直接使用するわけではないが、JMS 等の実装の中ではこの MQI が使われている。

MQCONN/MQCONNX

WebSphere MQ のキューマネージャとのコネクションを確立するためのインタフェース。MQCONNX は、単純に MQCONN よりも渡せる引数が多くなっているだけ。 成功すると、HConn と呼ばれるコネクションのハンドルを返却する。

MQOPEN

"Connected" な状態になったキューマネージャ上のオブジェクト (Queue を含む) にアクセスするためのインタフェース。ちなみに、キューを開く場合は、Object Descriptor (MQOD) とキュー名とともに、キューマネージャも渡すことで、ローカルキューマネージャが知っている別のキューマネージャに送ることもできる。 MQOPEN が成功した場合は、Hobj と呼ばれるオブジェクトハンドルを返却する。

MQCLOSE

まぁ閉じるんでしょう。あれを。

MQPUT

Open したキューに対し、名前の通りメッセージを PUT する。この MQPUT は、アプリケーションが接続しているキューマネージャ上のキューにメッセージを PUT できるまでブロックする。 ちなみに、Open したキューが「別のキューマネージャ上のキュー」である場合は、ローカルキューマネージャ上のトランスミッションキューに PUT できた時点で制御が戻ることになる。

MQPUT には MQMD とともに、MQPUT の振舞を定める MQPMO (PUT Message Options Structure) を渡すことができる。典型的には、syncpoint (MQ の文脈でいうトランザクション) を使うか否かを制御できる。

MQPUT1

クソみたいに名前がわかりにくいけど、このインタフェースは既述の MQOPENMQPUTMQCLOSE を行う。便利なだけでなく、効率も良い。

MQGET

これは MQGET とは逆に、Open したキューから、名前の通りメッセージを GET する。MQGET の振舞いは MQGMO (Get Message Options Structure) で指定でき、GET したメッセージをキューから消すか残すか、どういう基準でメッセージを選択するか (CorrelId 指定とか) などを既述できる。

MQBEGIN/MQCMIT/MQBACK

WebSphere MQ には、DBMS 等の外部リソースを巻き込んだトランザクションの概念として global unit of work がある。このトランザクションを開始するのが MQBEGINMQCMIT、あるいは MQBACK で終了する。

MQINC/MQSET

あんまり使わないかと思うので今のところ興味がないんだけど、open したオブジェクトの属性を参照するのが MQINC、設定するのが MQSET

MQDISK

キューマネージャとアプリケーションとの接続を切断する。MQCONN あるいは MQCONNX の逆操作。 MQDISK が呼び出されないままアプリケーションが終了し、それをキューマネージャが検知すると、キューマネージャは MQBACK をコールしてロールバックを行い、コネクションを切断する。

参考文献

GitbucketのDBからレビュー指摘を抽出する

environment technology db

GitBucket 上で Pull Req とかやりとりしているんだけど、いろいろと評価をするために、これらのレビュー情報を定量化する必要に駆られました。 こういうのを手で数えたりしていると日が暮れるし、何が生産性だ腹を切って死ねということになる。GitBucket でも当然のように DB 管理をしているので、それを取り出せれば良いでしょう。

環境を構築しよう

とりあえず試すためには GitBucket を構築して実際の DB の中身を見れば良い。こういう環境を一から構築していると日が暮れてしまって貴重な休日を浪費する。GitBucket には docker image があるので、docker で構築すれば良いでしょう。

$ docker-machine start default
$ eval $(docker-machine env)
$ docker run -d -p 8080:8080 -p 29418:29418 -v $(pwd)/data:/gitbucket takezoe/gitbucket

上のようなコマンドを実行すると、http://$(docker-machine ip):8080 にアクセスするとブラウザで gitbucket が立ち上がるし、カレントディレクトリ直下の data ディレクトリに gitbucket の DB (h2 database) である data.mv.db が出来上がる。

% ls data
data.h2.db  data.mv.db  data.trace.db  repositories  test.mv.db  tmp  version

h2 database に接続しよう

Gitbucket は Administrator の権限が付与されたユーザでログインすると、下記の画面のように「H2 Console」のメニューがでる。

f:id:kiririmode:20160501114138p:plain

このメニューをクリックすると、H2 接続の Web インタフェースが表示される。

f:id:kiririmode:20160501114358p:plain

JDBC URL に指定する場所は、Dockerfile (gitbucket-docker/Dockerfile at master · takezoe/gitbucket-docker · GitHub) を見れば分かるとおり jdbc:h2:/root/.gitbucket/data を指定すれば良い。 User NamePassword には sa と入れておけばいいです。この sa って何を意味しているんだろ。

そしたらレビュー指摘を抽出してみましょう

運用上、レビュー指摘は Pull Request に対して行われているんだけど、Pull Request に表示されるファイルに対するコメントとしての指摘と、Pull Request 自体に対する指摘 (ISSUE の Conversation としての指摘) で、コメントを格納されるテーブルが異なっている。

Pull Req 自体に対するレビュー指摘

Pull Req に対する指摘は ISSUE_COMMENT テーブルに対して格納される。 Pull Request を軸にして抽出するなら以下のようなかんじかな。

select pr.request_user_name, pr.request_repository_name, i.title, comment.commented_user_name, comment.content, comment.registered_date
from pull_request pr
  inner join issue i on  (pr.repository_name = i.repository_name and pr.issue_id = i.issue_id)
  inner join issue_comment comment on (comment.repository_name = pr.repository_name and comment.issue_id = pr.issue_id)
where comment.action = 'comment'
order by pr.request_user_name, pr.request_repository_name, comment.registered_date

f:id:kiririmode:20160501115801p:plain

ファイルに付与したレビュー指摘

ファイルに付与したレビュー指摘は、実体として「特定 commit の特定ファイルに対するコメント」として COMMIT_COMMENT テーブルに格納される。 これを利用すると、以下のような SQL で抽出できそう。

select cm.user_name, cm.repository_name, cm.commented_user_name, cm.content, cm.registered_date
from commit_comment cm
  inner join issue iss on  (cm.user_name = iss.user_name
                       and  cm.repository_name = iss.repository_name                                                   
                       and  cm.issue_id = iss.issue_id)
order by cm.user_name, cm.repository_name, registered_date

f:id:kiririmode:20160501120105p:plain