HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

mapのoperator[]の罠

mapを使ったあるプログラムを書いていて、やたらとメモリを食うので、何だろうといろいろ試行錯誤しながら調べてみたら、どうもmapの使い方に問題があるのではないかということが分かってきた。

mapのリファレンスを探して読んでみると、以下のようなことが書いてある。

https://cpprefjp.github.io/reference/map/map/op_at.html

戻り値

キーxに対応する値を返す。対応する要素が存在しない場合は、要素をデフォルト構築して参照を返す。

「デフォルト構築」ってなんぞや?・・・

そこで、(いろいろすっ飛ばして)検証プログラムを書いてみた。

#include <iostream>
#include <map>

using namespace std;

int main(int argc, char** argv) {
    map<int, int*> m;

    int a = 1;
    int b = 2;

    // (1)
    {
        map<int, int*>::iterator it = m.find(1);
        cout << "(1) " << (it == m.end()) << endl;
    }

    // (2)
    {
        int* p = m[1];
        cout << "(2) " << (p == NULL) << endl;
    }

    // (3)
    {
        map<int, int*>::iterator it = m.find(1);
        cout << "(3) " << (it == m.end()) << ":" << (it->second == NULL) << endl;
    }

    // (4)
    {
        m[1] = &a;
        map<int, int*>::iterator it = m.find(1);
        cout << "(4) " << (it == m.end()) << ":" << (it->second == NULL) << ":" << (it->second == &a) << endl;
    }

    return EXIT_SUCCESS;
}

出力は以下のようになる。

(1) 1
(2) 1
(3) 0:1
(4) 0:0:1

(1)ではitm.end()に等しいのに、(2)でm[1]をやった後に(3)でもう一度findを使うと、今度はm.end()と等しくならなくなっている、つまり、mapの中に何かしらのエントリが存在する状態になっているようだ。

(4)はついでだが、m[1]に何かを代入すると、当然ながらその値がmapに入ることになる。

これは罠だなぁ‥覚えておこう‥

json_encode関数のJSON_UNESCAPED_UNICODEオプションの指定方法

json_encode関数はデフォルトではUnicode文字列がエスケープされるのだが、PHP 5.4より以前ではエスケープを避ける手段がないので、以下のようなコードを書くと、出力を人がパッと見ても読めない。

<?php

ini_set('display_errors', 'on');
date_default_timezone_set('Asia/Tokyo');

set_include_path('../smarty-2.6.28/libs/');

require_once('Smarty.class.php');

$smarty = new Smarty();

$smarty->template_dir = 'smarty/templates/';
$smarty->compile_dir  = 'smarty/templates_c/';
$smarty->config_dir   = 'smarty/configs/';
$smarty->cache_dir    = 'smarty/cache/';

$ary = array(
    '名前' => 'ボブ',
    'サロゲートペア' => '𠮷',
);
$smarty->assign('strs_json', json_encode($ary));
$smarty->assign('strs_ary', $ary);

$smarty->display('index.tpl');
PHPバージョン:{$smarty.const.PHP_VERSION}
<br />
<pre>
{$strs_json}
</pre>
<pre>
{$strs_ary|@json_encode}
</pre>
  • 出力
PHPバージョン:5.3.3
<br />
<pre>
{"\u540d\u524d":"\u30dc\u30d6","\u30b5\u30ed\u30b2\u30fc\u30c8\u30da\u30a2":"\ud842\udfb7"}
</pre>
<pre>
{"\u540d\u524d":"\u30dc\u30d6","\u30b5\u30ed\u30b2\u30fc\u30c8\u30da\u30a2":"\ud842\udfb7"}
</pre>

一方、PHP 5.4からは、JSON_UNESCAPED_UNICODEというオプションが指定できるようになった。

‥のだが、Smartyではどう指定するのかをちょっと悩んだのでメモ。

答えは、以下のように書けばよい。

<?php

ini_set('display_errors', 'on');
date_default_timezone_set('Asia/Tokyo');

set_include_path('../smarty-2.6.28/libs/');

require_once('Smarty.class.php');

$smarty = new Smarty();

$smarty->template_dir = 'smarty/templates/';
$smarty->compile_dir  = 'smarty/templates_c/';
$smarty->config_dir   = 'smarty/configs/';
$smarty->cache_dir    = 'smarty/cache/';

$ary = array(
    '名前' => 'ボブ',
    'サロゲートペア' => '𠮷',
);
$smarty->assign('strs_json', json_encode($ary, JSON_UNESCAPED_UNICODE));
$smarty->assign('strs_ary', $ary);

$smarty->display('index.tpl');
PHPバージョン:{$smarty.const.PHP_VERSION}
<br />
<pre>
{$strs_json}
</pre>
<pre>
{$strs_ary|@json_encode:$smarty.const.JSON_UNESCAPED_UNICODE}
</pre>
  • 出力
PHPバージョン:5.4.16
<br />
<pre>
{"名前":"ボブ","サロゲートペア":"𠮷"}
</pre>
<pre>
{"名前":"ボブ","サロゲートペア":"𠮷"}
</pre>

Javaでの和暦対応の罠

ふと、Javaで和暦対応してたよなぁと思い出し、検索して見つけた以下のサイトのプログラムを元に、とあることを検証してみた。

それは、「Calendar#setで西暦年をセットして和暦でフォーマットして出力したら、西暦年に対応する元号が分かるじゃん」というもの。

早速書いてみる。

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;

public class Test1 {
    public static void main(String[] args) {
        Locale locale = new Locale("ja", "JP", "JP");
        Calendar cal = Calendar.getInstance(locale);
        System.out.println(cal.getClass().getName());

        int[] years = { 1900, 1925, 1950, 1975, 2000 };
        for (int year : years) {
            cal.set(Calendar.YEAR, year);
            DateFormat format = new SimpleDateFormat("GGGGy年M月d日", locale);
            System.out.println(year + ": " + format.format(cal.getTime()));
        }
    }
}

いざ実行。

$ javac Test1.java && java Test
java.util.JapaneseImperialCalendar
1900: 平成1900年3月11日
1925: 平成1925年3月11日
1950: 平成1950年3月11日
1975: 平成1975年3月11日
2000: 平成2000年3月11日
$ 

・・・えっ!?‥「cal.set(Calendar.YEAR, year)」って「和暦での年数をセットしないといけないの?」‥ どうやらそうらしい。

というわけでプログラムをちょこちょこいじりながら調べてみたら、「Calendar cal = Calendar.getInstance(locale)」の部分が良くないらしい。

書き直したプログラム。

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;

public class Test2 {
    public static void main(String[] args) {
        Locale locale = new Locale("ja", "JP", "JP");
        Calendar cal = Calendar.getInstance();
        System.out.println(cal.getClass().getName());

        int[] years = { 1900, 1925, 1950, 1975, 2000 };
        for (int year : years) {
            cal.set(Calendar.YEAR, year);
            DateFormat format = new SimpleDateFormat("GGGGy年M月d日", locale);
            System.out.println(year + ": " + format.format(cal.getTime()));
        }
    }
}

実行。

$ javac Test2.java && java Test2
java.util.GregorianCalendar
1900: 明治33年3月11日
1925: 大正14年3月11日
1950: 昭和25年3月11日
1975: 昭和50年3月11日
2000: 平成12年3月11日
$ 

おぉ、今度は期待通り。


ということはだよ、例えば「明治33年」の「33」という年数を素直にセットしたい場合はできないってことじゃん‥

ということで、最初のプログラム(Test1.java)の出力で得られた「java.util.JapaneseImperialCalendar」のソースを見てみる。(CentOS 7のjava-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64)

class JapaneseImperialCalendar extends Calendar {
    /*
     * Implementation Notes
     *
     * This implementation uses
     * sun.util.calendar.LocalGregorianCalendar to perform most of the
     * calendar calculations. LocalGregorianCalendar is configurable
     * and reads <JRE_HOME>/lib/calendars.properties at the start-up.
     */

    /**
     * The ERA constant designating the era before Meiji.
     */
    public static final int BEFORE_MEIJI = 0;

    /**
     * The ERA constant designating the Meiji era.
     */
    public static final int MEIJI = 1;

    /**
     * The ERA constant designating the Taisho era.
     */
    public static final int TAISHO = 2;

    /**
     * The ERA constant designating the Showa era.
     */
    public static final int SHOWA = 3;

    /**
     * The ERA constant designating the Heisei era.
     */
    public static final int HEISEI = 4;

パッケージプライベートなクラスなので直接の参照はできないが、明治、大正、昭和、平成に対応するCalendar.ERAの定数らしきが定義されている。というわけで試してみる。

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Locale;

public class Test3 {
    public static void main(String[] args) {
        Locale locale = new Locale("ja", "JP", "JP");
        Calendar cal = Calendar.getInstance(locale);
        System.out.println(cal.getClass().getName());

        Map<Integer, Integer> years = new HashMap<>();
        years.put(1, 33); // 明治33年
        years.put(2, 14); // 大正14年
        years.put(3, 25); // 昭和25年
        years.put(4, 12); // 平成12年
        for (Map.Entry<Integer, Integer> entry : years.entrySet()) {
            int era = entry.getKey();
            int year = entry.getValue();
            cal.set(Calendar.ERA, era);
            cal.set(Calendar.YEAR, year);
            DateFormat format = new SimpleDateFormat("GGGGy年M月d日", locale);
            System.out.println(year + ": " + format.format(cal.getTime()));
        }
    }
}

実行してみる。

$ javac Test3.java && java Test3
java.util.JapaneseImperialCalendar
33: 明治33年3月11日
14: 大正14年3月11日
25: 昭和25年3月11日
12: 平成12年3月11日
$ 

おぉ、できた、できた、定数の値の実装が変わったらアウトだがなw‥

サーバー名の末尾にドットを付けたリクエストを送ると予期せぬ応答が返ってくる問題

前置き

以下の2つの環境での挙動が違うことに悩んでいた。

何をしたときの挙動かというと、

$ echo '' | openssl s_client -connect localhost:8443 -servername hhelibex.local. -showcerts

のように、サーバー名の末尾にドットがついたリクエストを送ったときにSSL/TLSハンドシェイクに失敗する場合としない場合があるということ。 つまり、失敗する場合には以下のようなレスポンスが返ってくる。

CONNECTED(00000003)
139918566537120:error:140773F2:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert unexpected message:s23_clnt.c:769:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 324 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : 0000
    Session-ID: 
    Session-ID-ctx: 
    Master-Key: 
    Key-Arg   : None
    Krb5 Principal: None
    PSK identity: None
    PSK identity hint: None
    Start Time: 1520436816
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---

成功する場合には、以下のようにCipherに適切な文字列が返ってくるし、ドットを付けない場合も同様に成功する。また、CentOS 6環境のJDKをOpenJDK 1.7.0に切り替えてみても成功するようになる。

(省略)
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384
(省略)
---
DONE

そこで、Java 7とJava 8で何かが変わったのだろうと思い、その部分を特定するためにTomcat 6のソースを眺めてみたりしてものすごく遠回りしたのだが、「もしかして、根本的なところで仕様が変わっているんじゃね?」と思い始め、別の環境(CentOS 7)で簡単なサーバープログラムを動かしてみたら何か分かるかもということで試してみた。

事前準備

まず、使用するJDK環境を2つ用意する。OpenJDK 1.7.0とOpenJDK 1.8.0。インストールの手順は省略する。

で、alternativesで切り替えて挙動を見る。

事前準備としては、コンパイラ(javac)はOpenJDK 1.7.0にしておき、インタプリタは必要に応じて切り替える。最初はOpenJDK 1.7.0にしておく。

$ sudo alternatives --config javac

2 プログラムがあり 'javac' を提供します。

  選択       コマンド
-----------------------------------------------
*+ 1           java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64/bin/javac)
   2           java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.171-2.6.13.0.el7_4.x86_64/bin/javac)

Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:2
$ sudo alternatives --config java

2 プログラムがあり 'java' を提供します。

  選択       コマンド
-----------------------------------------------
*+ 1           java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64/jre/bin/java)
   2           java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.171-2.6.13.0.el7_4.x86_64/jre/bin/java)

Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:2
$ javac -version
javac 1.7.0_171
$ java -version
java version "1.7.0_171"
OpenJDK Runtime Environment (rhel-2.6.13.0.el7_4-x86_64 u171-b01)
OpenJDK 64-Bit Server VM (build 24.171-b01, mixed mode)
$ 

次に、サーバー証明書を適当に作っておく。

$ openssl genrsa -aes256 2048 -out server.key
Generating RSA private key, 2048 bit long modulus
....................................+++
......................+++
e is 65537 (0x10001)
Enter pass phrase: test
Verifying - Enter pass phrase: test
-----BEGIN RSA PRIVATE KEY-----
(省略)
-----END RSA PRIVATE KEY-----
$ openssl rsa -in server.key -out server.key 
writing RSA key
$ openssl req -new -sha256 -key server.key -out server.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:JP
State or Province Name (full name) []:Hokkaido
Locality Name (eg, city) [Default City]:Sapporo
Organization Name (eg, company) [Default Company Ltd]:HHeLiBeX Ltd.
Organizational Unit Name (eg, section) []:
Common Name (eg, your name or your server's hostname) []:hhelibex.local
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
$ openssl x509 -in server.csr -out server.crt -req -signkey server.key -days 365
Signature ok
subject=/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local
Getting Private key
$ openssl pkcs12 -export -inkey server.key -in server.crt -out server.p12
Enter Export Password: changeit
Verifying - Enter Export Password: changeit
$ 

サーバープログラム

以下のような簡単なプログラムを作って起動する。テストだから、必要なパラメータはすべてべた書きだがご容赦を。

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyStore;

import javax.net.ServerSocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;

public class SSLServer {
    public static void main(String[] args) {
        try {
            String keyStoreFile = "server.p12";
            char[] keyStorePassword = "changeit".toCharArray();

            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            keyStore.load(new FileInputStream(keyStoreFile), keyStorePassword);

            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
            kmf.init(keyStore, keyStorePassword);

            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(kmf.getKeyManagers() , null , null);
            ServerSocketFactory ssf = sslContext.getServerSocketFactory();
            ServerSocket serverSocket  = ssf.createServerSocket(8443);

            while (true) {
                System.out.println("--------" + System.getProperty("java.version") + "--------");
                System.out.println("Waiting for SSL connection");

                Socket socket = null;
                BufferedReader in = null;
                BufferedWriter out = null;
                try {
                    socket = serverSocket.accept();
                    if (socket == null) {
                        System.out.println("Client socket is null, ignored.");
                        continue;
                    }
                    System.out.println("Accepted.");

                    in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

                    String msg = in.readLine();
                    System.out.println("Message from client: " + msg);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (in != null) {
                        try { in.close(); } catch (IOException e) { e.printStackTrace(); }
                    }
                    if (out != null) {
                        try { out.close(); } catch (IOException e) { e.printStackTrace(); }
                    }
                    if (socket != null) {
                        socket.close();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

接続テスト(サーバー:OpenJDK 1.7.0)

  • 起動
$ sudo alternatives --config java

2 プログラムがあり 'java' を提供します。

  選択       コマンド
-----------------------------------------------
*  1           java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64/jre/bin/java)
 + 2           java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.171-2.6.13.0.el7_4.x86_64/jre/bin/java)

Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:2
$ java SSLServer
--------1.7.0_171--------
Waiting for SSL connection
  • クライアントから接続
$ echo "Hello World" | openssl s_client -connect localhost:8443 -servername hhelibex.local. -showcerts
CONNECTED(00000003)
depth=0 C = JP, ST = Hokkaido, L = Sapporo, O = HHeLiBeX Ltd., CN = hhelibex.local
verify error:num=18:self signed certificate
verify return:1
depth=0 C = JP, ST = Hokkaido, L = Sapporo, O = HHeLiBeX Ltd., CN = hhelibex.local
verify return:1
---
Certificate chain
 0 s:/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local
   i:/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local
-----BEGIN CERTIFICATE-----
MIIDQjCCAioCCQC3i2D7IVfOsTANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJK
UDERMA8GA1UECAwISG9ra2FpZG8xEDAOBgNVBAcMB1NhcHBvcm8xFjAUBgNVBAoM
DUhIZUxpQmVYIEx0ZC4xFzAVBgNVBAMMDmhoZWxpYmV4LmxvY2FsMB4XDTE4MDMw
NzE2MjU1NloXDTE5MDMwNzE2MjU1NlowYzELMAkGA1UEBhMCSlAxETAPBgNVBAgM
CEhva2thaWRvMRAwDgYDVQQHDAdTYXBwb3JvMRYwFAYDVQQKDA1ISGVMaUJlWCBM
dGQuMRcwFQYDVQQDDA5oaGVsaWJleC5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAM2I/AxPvGX4Pu9BqwmP0XoAAXpCYIrRNNa+bw1irLHQ82o+
J1bEGev9x6/jYhA+L7AYHMGnE6gbg1azrxyc06OdzN5X6OTC9xia6S7+LP3Ar8P6
c3BURqoU7TWOZdT7/KmPfgPC/uNty0lgA1U74PsdSMDE6VPU/MVyRoezsDLiXPrV
p9eve9bXHiuRF1G+Y0lO3Ym3fGilIZa7HEEqeVLTrKWmS2odOlIT/t8VAfCKBds9
nd38hwPThB0k9F6fhkUgwDeEZZdXXtMh+UNKEHdkI/VGji6uL72sAnwTc1H2VoAc
srKcdmzAWh8Kj9ynVenoAjWUTD4LV3L/JF76PLcCAwEAATANBgkqhkiG9w0BAQsF
AAOCAQEAgpx32ml5YAacemf63fk9fNS7czjUZqvHBtfR4B6Vl+nmwHnIOazjSgRz
WRv86ZnP9t2bh5myhFZtg47BLI6gW8Ca4a92lehz1Cl6tB5sqbBk0vm3Jd78SzIV
T1Kxx9CgOEEgT54WuLyRpuaQwICQrKZgWWysCojRtiK/7tZ0amCW8CMfakLdvAaW
NF3814ZPS5AiIWrxaQ4XVEw2kB8yGapHO9dLMO81Jr9xpzjG2ENMR7aAxzPcdFvE
HJmIAyNYN8e6J/KcYAlAGz3Jo+ppcqn3De2GlFWwvPKiuVrDTeuM9r68u+rRFrmA
wTxbkeP/rGxXRzG3G2VQmgY7rtBI6A==
-----END CERTIFICATE-----
---
Server certificate
subject=/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local
issuer=/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local
---
No client certificate CA names sent
Peer signing digest: SHA512
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 1378 bytes and written 495 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES256-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-SHA384
    Session-ID: 5AA01ABEC478A8640AF9BFA21DB39BD751CC7BD651F29EFCEA4A14F91B2D7FC8
    Session-ID-ctx: 
    Master-Key: 47C8AF2C33540069495BD8372059306B8411D19F684007DFF8E88BEE5653690CCE5C010145C58D54962A14C29418F89D
    Key-Arg   : None
    Krb5 Principal: None
    PSK identity: None
    PSK identity hint: None
    Start Time: 1520442046
    Timeout   : 300 (sec)
    Verify return code: 18 (self signed certificate)
---
DONE
$
  • サーバー側でのメッセージ出力

メッセージは、起動時からの続き。

Accepted.
Message from client: Hello World
--------1.7.0_171--------
Waiting for SSL connection
^C
$ 

サーバー証明書が問題なく取得できた。クライアントからのメッセージ「Hello World」もちゃんと送られている。

接続テスト(サーバー:OpenJDK 1.8.0)

  • 起動
$ sudo alternatives --config java

2 プログラムがあり 'java' を提供します。

  選択       コマンド
-----------------------------------------------
*  1           java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64/jre/bin/java)
 + 2           java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.171-2.6.13.0.el7_4.x86_64/jre/bin/java)

Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:1
$ java SSLServer
--------1.8.0_151--------
Waiting for SSL connection
  • クライアントから接続
$ echo "Hello World" | openssl s_client -connect localhost:8443 -servername hhelibex.local. -showcerts
CONNECTED(00000003)
140566511749024:error:140773F2:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert unexpected message:s23_clnt.c:769:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 313 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : 0000
    Session-ID: 
    Session-ID-ctx: 
    Master-Key: 
    Key-Arg   : None
    Krb5 Principal: None
    PSK identity: None
    PSK identity hint: None
    Start Time: 1520441888
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---
$ 
  • サーバー側でのメッセージ出力

メッセージは、起動時からの続き。

Accepted.
javax.net.ssl.SSLProtocolException: Illegal server name, type=host_name(0), name=hhelibex.local., value=68:68:65:6c:69:62:65:78:2e:6c:6f:63:61:6c:2e
        at sun.security.ssl.ServerNameExtension.<init>(ServerNameExtension.java:143)
        at sun.security.ssl.HelloExtensions.<init>(HelloExtensions.java:78)
        at sun.security.ssl.HandshakeMessage$ClientHello.<init>(HandshakeMessage.java:245)
        at sun.security.ssl.ServerHandshaker.processMessage(ServerHandshaker.java:220)
        at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1026)
        at sun.security.ssl.Handshaker.process_record(Handshaker.java:961)
        at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1072)
        at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1385)
        at sun.security.ssl.SSLSocketImpl.readDataRecord(SSLSocketImpl.java:938)
        at sun.security.ssl.AppInputStream.read(AppInputStream.java:105)
        at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at java.io.BufferedReader.fill(BufferedReader.java:161)
        at java.io.BufferedReader.readLine(BufferedReader.java:324)
        at java.io.BufferedReader.readLine(BufferedReader.java:389)
        at SSLServer.main(SSLServer.java:50)
Caused by: java.lang.IllegalArgumentException: Server name value of host_name cannot have the trailing dot
        at javax.net.ssl.SNIHostName.checkHostName(SNIHostName.java:319)
        at javax.net.ssl.SNIHostName.<init>(SNIHostName.java:183)
        at sun.security.ssl.ServerNameExtension.<init>(ServerNameExtension.java:137)
        ... 17 more
--------1.8.0_151--------
Waiting for SSL connection
^C
$ 

今度は例外が発生し、接続が切られている。もちろん、クライアント側では証明書の取得はできず。

検証

失敗したOpenJDK 1.8.0の場合の例外メッセージを見てみると、以下のようなメッセージが読み取れる。

  • 「Illegal server name, ・・・」
  • 「Server name value of host_name cannot have the trailing dot」

そこで、例外メッセージにあるjavax.net.ssl.SNIHostNameを見てみる。

すると、こんなことが書いてある。

TLS拡張(RFC 6066)のセクション3「Server Name Indication」で説明されているように、「HostName」には、クライアントが理解できるサーバーの完全修飾DNSホスト名が含まれます。ホスト名のエンコードされたサーバー名の値は、ASCIIエンコーディングを使用したドットで終わらないバイト文字列として表現されます。

また、

導入されたバージョン
1.8

「ドットで終わらないバイト文字列」!!

なるほど、RFC 6066がJava 8から実装され、ドットで終わるホスト名が許されなくなったということらしい。

これで冒頭の疑問が解ける。成功する方はJava 7なのでドットで終わるホスト名がまだ許されていたが、失敗する方はJava 8からのRFC 6066の実装によってドットで終わるホスト名が許されなくなった。

通常はドットで終わるホスト名でアクセスすることはないだろうから問題になることは少ないのだろうが、これは罠だなぁ‥ともあれ、疑問が解消されてよかった‥

参考

mb_encode_mimeheader/mb_decode_mimeheaderする際には内部文字エンコーディングに注意

マニュアルをちゃんと読むと書いてあるのだが。

前者のmb_encode_mimeheader()は、以下のように書いてある。

パラメータ

str
 エンコードする文字列。 mb_internal_encoding() と同じエンコーディングにしなければいけません。

つまり、マルチバイト文字を扱う限り、mb_internal_encoding()による内部文字エンコーディングの設定が必須なのだ。

ついでに、後者のmb_decode_mimeheader()のマニュアルも見てみる。

返り値

内部文字エンコーディングでデコードされた文字列を返します。

「内部文字エンコーディング」と明記してある。

というわけで、ちょっと試してみるために、以下のようなシナリオに沿ったプログラムを書いてみる。

共通部品/処理

<?php

/*
 * コマンド「od -c」を模したダンプを出力する関数。
 */
function od($str) {
    for ($i = 0; $i < strlen($str); ++$i) {
        if ($i % 16 === 0) {
            if ($i > 0) {
                echo PHP_EOL;
            }
            printf("%08o", $i);
        }
        $s = substr($str, $i, 1);
        if (ctype_graph($s) || $s === " ") {
            printf(" %3s", $s);
        } else {
            printf(" %03o", ord($s));
        }
    }
    if ($i % 16 !== 15) {
        echo PHP_EOL;
    }
    printf("%08o\n", $i);
}
<?php

// ** このファイルはもちろん「UTF-8」で保存する **

// 初期設定
mb_internal_encoding("UTF-8");

// メールで送る文字列の件名
$str = "あいうえおかきくけこさしすせそたちつてとなにぬねの";

シナリオ1

ソースコードは以下の通り。

<?php

include("od.php");
include("init.php");

// ** このファイルはもちろん「UTF-8」で保存する **

// 文字列の文字エンコーディング変換
$convStr = mb_convert_encoding($str, "ISO-2022-JP", "UTF-8");

// エンコード処理
$encStr = mb_encode_mimeheader($convStr, "ISO-2022-JP");
echo "// エンコードされた文字列" . PHP_EOL;
var_dump($encStr);

echo PHP_EOL;

// デコード処理
$decStr = mb_decode_mimeheader($encStr);
echo "// デコードされた文字列" . PHP_EOL;
var_dump($decStr);
od($decStr);

実行結果。

// エンコードされた文字列
string(115) "=?ISO-2022-JP?B?GyRCJCIkJCQmJCgkKiQrJC0kLyQxJDMkNSQ3JDkkOyQ9JD8kQSREJEYk?=
 =?ISO-2022-JP?B?SCRKJEskTCRNJE4bKEI=?="

// デコードされた文字列
string(68) "あいうえおかきくけこさしすせそたちつてH$J$K$L$M$N"
00000000 343 201 202 343 201 204 343 201 206 343 201 210 343 201 212 343
00000020 201 213 343 201 215 343 201 217 343 201 221 343 201 223 343 201
00000040 225 343 201 227 343 201 231 343 201 233 343 201 235 343 201 237
00000060 343 201 241 343 201 244 343 201 246   H   $   J   $   K   $   L
00000100   $   M   $   N
00000104

あれ、文字化けした。

多分、mb_decode_mimeheader()でデコードするときに以下のような処理をしているのだろうと思われる。

<?php
mb_internal_encoding("UTF-8");
var_dump(
    mb_convert_encoding(base64_decode("GyRCJCIkJCQmJCgkKiQrJC0kLyQxJDMkNSQ3JDkkOyQ9JD8kQSREJEYk"), mb_internal_encoding(), "ISO-2022-JP")
    . mb_convert_encoding(base64_decode("SCRKJEskTCRNJE4bKEI="), mb_internal_encoding(), "ISO-2022-JP"));
string(68) "あいうえおかきくけこさしすせそたちつてH$J$K$L$M$N"

同じ結果になった。

というわけで、これではダメ。

シナリオ2

ソースコードは以下の通り。

<?php

include("od.php");
include("init.php");

// ** このファイルはもちろん「UTF-8」で保存する **

// 文字列の文字エンコーディング変換
$convStr = mb_convert_encoding($str, "ISO-2022-JP", "UTF-8");

// 内部文字エンコーディングを変更
$origInternalEncoding = mb_internal_encoding();
mb_internal_encoding("ISO-2022-JP");

// エンコード処理
$encStr = mb_encode_mimeheader($convStr, "ISO-2022-JP");
echo "// エンコードされた文字列" . PHP_EOL;
var_dump($encStr);

// 内部文字エンコーディングを戻す
mb_internal_encoding($origInternalEncoding);

echo PHP_EOL;

// 内部文字エンコーディングを変更
mb_internal_encoding("ISO-2022-JP");

// デコード処理
$decStr = mb_decode_mimeheader($encStr);
echo "// デコードされた文字列" . PHP_EOL;
var_dump($decStr);
od($decStr);

// 内部文字エンコーディングを戻す
mb_internal_encoding($origInternalEncoding);

実行結果。

// エンコードされた文字列
string(123) "=?ISO-2022-JP?B?GyRCJCIkJCQmJCgkKiQrJC0kLyQxJDMkNSQ3JDkkOyQ9JD8kQSREGyhC?=
 =?ISO-2022-JP?B?GyRCJEYkSCRKJEskTCRNJE4bKEI=?="

// デコードされた文字列
string(56) "あいうえおかきくけこさしすせそたちつてとなにぬねの"
00000000 033   $   B   $   "   $   $   $   &   $   (   $   *   $   +   $
00000020   -   $   /   $   1   $   3   $   5   $   7   $   9   $   ;   $
00000040   =   $   ?   $   A   $   D   $   F   $   H   $   J   $   K   $
00000060   L   $   M   $   N 033   (   B
00000070

一見よさそうだが、ダンプを見ると、「ISO-2022-JP」になっている。システムの内部文字エンコーディングは「UTF-8」だったはずだ。

これもダメ。

シナリオ3

ソースコードは以下の通り。

<?php

include("od.php");
include("init.php");

// ** このファイルはもちろん「UTF-8」で保存する **

// 文字列の文字エンコーディング変換
$convStr = mb_convert_encoding($str, "ISO-2022-JP", "UTF-8");

// 内部文字エンコーディングを変更
$origInternalEncoding = mb_internal_encoding();
mb_internal_encoding("ISO-2022-JP");

// エンコード処理
$encStr = mb_encode_mimeheader($convStr, "ISO-2022-JP");
echo "// エンコードされた文字列" . PHP_EOL;
var_dump($encStr);

// 内部文字エンコーディングを戻す
mb_internal_encoding($origInternalEncoding);

echo PHP_EOL;

// デコード処理
$decStr = mb_decode_mimeheader($encStr);
echo "// デコードされた文字列" . PHP_EOL;
var_dump($decStr);
od($decStr);

実行結果。

// エンコードされた文字列
string(123) "=?ISO-2022-JP?B?GyRCJCIkJCQmJCgkKiQrJC0kLyQxJDMkNSQ3JDkkOyQ9JD8kQSREGyhC?=
 =?ISO-2022-JP?B?GyRCJEYkSCRKJEskTCRNJE4bKEI=?="

// デコードされた文字列
string(75) "あいうえおかきくけこさしすせそたちつてとなにぬねの"
00000000 343 201 202 343 201 204 343 201 206 343 201 210 343 201 212 343
00000020 201 213 343 201 215 343 201 217 343 201 221 343 201 223 343 201
00000040 225 343 201 227 343 201 231 343 201 233 343 201 235 343 201 237
00000060 343 201 241 343 201 244 343 201 246 343 201 250 343 201 252 343
00000100 201 253 343 201 254 343 201 255 343 201 256
00000113

よさそうである。文字化けしていないし、ダンプを見ても「UTF-8」になっている。念のため確認で以下を実行してみた(コンソールの文字エンコーディングUTF-8)。

$ echo -n 'あいうえおかきくけこさしすせそたちつてとなにぬねの' | od -c
0000000 343 201 202 343 201 204 343 201 206 343 201 210 343 201 212 343
0000020 201 213 343 201 215 343 201 217 343 201 221 343 201 223 343 201
0000040 225 343 201 227 343 201 231 343 201 233 343 201 235 343 201 237
0000060 343 201 241 343 201 244 343 201 246 343 201 250 343 201 252 343
0000100 201 253 343 201 254 343 201 255 343 201 256
0000113
$ 

同じ結果だ。

まとめ

というわけで、まとめると、以下のようにして各関数を呼び出さないといけない。

各言語で正規表現「^」「$」「\A」「\z」を試してみる

ということで、あちこちから突っ込みが来ないことを祈りつつ(謎)、手元にある各言語でテストプログラムを書いてみたメモ。

入力は以下のような文字列。

abc
123
*+=

最大4つのパターンを試すが、すべてのパターンでマッチすると、

1234

のように出力される。逆に、マッチしないパターンや、そもそも存在しないマッチ方法の場合は「0」や「-」をそれぞれ出力する。

環境

手元にあるものということで、環境は以下のものに限定する。

Java

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {
    private static boolean matches(String str, String pattern, int flags) {
        Pattern p = Pattern.compile(pattern, flags);
        Matcher m = p.matcher(str);
        return m.matches();
    }

    public static void main(String[] args) {
        try (Reader in = new InputStreamReader(System.in);
            PrintWriter out = new PrintWriter(System.out)
        ) {
            char[] buf = new char[1024];
            int len = in.read(buf);
            String s = new String(buf, 0, len);
//          s = s.trim();

            if (matches(s, "^[0-9]+$", 0)) {
                out.print("1");
            } else {
                out.print("0");
            }

            if (matches(s, "^[0-9]+$", Pattern.MULTILINE)) {
                out.print("2");
            } else {
                out.print("0");
            }

            if (matches(s, "\\A[0-9]+\\z", 0)) {
                out.print("3");
            } else {
                out.print("0");
            }

            if (matches(s, "\\A[0-9]+\\z", Pattern.MULTILINE)) {
                out.print("4");
            } else {
                out.print("0");
            }

            out.println();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

出力。

0000

どのパターンでもマッチしない。完全にマッチしないとダメなようだ。

C

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <regex.h>

int matches(const char* str, const char* pattern, int flags) {
    regex_t rb;
    if (regcomp(&rb, pattern, flags)) {
        perror(pattern);
        exit(1);
    }

    regmatch_t rm;
    int res;
    if (!regexec(&rb, str, 1, &rm, 0)) {
        res = 1;
    } else {
        res = 0;
    }

    regfree(&rb);

    return res;
}

int main(int argc, char** argv) {
    char str[1024];
    memset(str, '\0', sizeof(str));

    fread(str, sizeof(str), sizeof(char), stdin);
//  while (str[strlen(str) - 1] == '\n' || str[strlen(str) - 1] == '\r') {
//      str[strlen(str) - 1] = '\0';
//  }

    if (matches(str, "^[0-9]+$", REG_EXTENDED)) {
        printf("1");
    } else {
        printf("0");
    }

    // そもそも複数行モードが無い
    printf("-");

    // 文字列の先頭・末尾という正規表現が無い
    printf("-");

    // そもそも複数行モードが無い
    printf("-");

    printf("\n");

    return 0;
}

出力。

0---

まぁ、C言語は仕方がない。パターンが1つしかないので。

C++

#include <iostream>
#include <locale>
#include <string>
#include <boost/regex.hpp>

using namespace std;

bool matches(string str, const char* pattern, boost::match_flag_type flags) {
    boost::regex re(pattern);

    boost::smatch sm;
    return boost::regex_search(str, sm, re, flags);
}

int main(int argc, char** argv) {
    istreambuf_iterator<char> it(cin);
    istreambuf_iterator<char> last;
    string str(it, last);
//  str.erase(str.find_last_not_of("\r\n") + 1);

    if (matches(str, "^[0-9]+$", boost::regex_constants::match_single_line)) {
        cout << "1";
    } else {
        cout << "0";
    }

    if (matches(str, "^[0-9]+$", boost::regex_constants::match_default)) {
        cout << "2";
    } else {
        cout << "0";
    }

    if (matches(str, "\\A[0-9]+\\z", boost::regex_constants::match_single_line)) {
        cout << "3";
    } else {
        cout << "0";
    }

    if (matches(str, "\\A[0-9]+\\z", boost::regex_constants::match_default)) {
        cout << "4";
    } else {
        cout << "0";
    }

    cout << endl;

    return EXIT_SUCCESS;
}

出力。

0200

これがうわさに聞く、複数行モードで「^」「$」を使うと部分文字列にマッチするというものか。

最初のパターンでわざわざ「boost::regex_constants::match_single_line」をフラグに指定していることから分かるように、C++(Boost)のデフォルトは複数行モードのようだ。

PHP

<?php

$s = file_get_contents('php://stdin');
//$s = trim($s);

if (preg_match("/^[0-9]+$/", $s)) {
    echo '1';
} else {
    echo '0';
}

if (preg_match("/^[0-9]+$/m", $s)) {
    echo '2';
} else {
    echo '0';
}

if (preg_match("/\A[0-9]+\z/", $s)) {
    echo '3';
} else {
    echo '0';
}

if (preg_match("/\A[0-9]+\z/m", $s)) {
    echo '4';
} else {
    echo '0';
}

echo PHP_EOL;

出力。

0200

同様に、複数行モードだと「^」「$」を使うと部分文字列にマッチする。

Python 2 / 3

import sys
import re

s = sys.stdin.read()
#s = s.strip()

if re.search(r'^[0-9]+$', s):
    sys.stdout.write('1')
else:
    sys.stdout.write('0')

if re.search(r'^[0-9]+$', s, re.MULTILINE):
    sys.stdout.write('2')
else:
    sys.stdout.write('0')

if re.search(r'\A[0-9]+\Z', s):
    sys.stdout.write('3')
else:
    sys.stdout.write('0')

if re.search(r'\A[0-9]+\Z', s, re.MULTILINE):
    sys.stdout.write('4')
else:
    sys.stdout.write('0')

sys.stdout.write("\n")

出力。

0200

同様に、複数行モードだと「^」「$」を使うと部分文字列にマッチする。

なお、文字列の末尾を表す正規表現が「\z」ではなく「\Z」となることに注意。

Ruby

s = STDIN.read
#s.chomp!

# 単一行モードが無いので。
print "-"

if s.match(/^[0-9]+$/)
    print "2"
else
    print "0"
end

# 単一行モードが無いので。
print "-"

if s.match(/\A[0-9]+\z/)
    print "4"
else
    print "0"
end

print "\n"

出力。

-2-0

調べた限りでは複数行モードしかなかったので、複数行モードのみの出力。

確かに、「^」「$」で部分文字列にマッチする。

Perl

my $s;
{
    local $/ = undef;
    $s = <STDIN>;
}
#chomp($s);

if ($s =~ /^[0-9]+$/) {
    print '1';
} else {
    print '0';
}

if ($s =~ /^[0-9]+$/m) {
    print '2';
} else {
    print '0';
}

if ($s =~ /\A[0-9]+\z/) {
    print '3';
} else {
    print '0';
}

if ($s =~ /\A[0-9]+\z/m) {
    print '4';
} else {
    print '0';
}

print "\n";

出力。

0200

PHPと同様に、mフラグを付けてやると複数行モードで、「^」「$」を使用すると部分文字列にマッチする。

Go

package main

import (
    "fmt"
    "os"
    "regexp"
    "bufio"
    "io/ioutil"
//  "strings"
)

func main() {
    stdin := bufio.NewReader(os.Stdin)
    b, _ := ioutil.ReadAll(stdin)
    s := string(b)
//  s = strings.Trim(s, "\r\n")

    {
        m := regexp.MustCompile(`^[0-9]+$`)
        if m.MatchString(s) {
            fmt.Print("1")
        } else {
            fmt.Print("0")
        }
    }

    {
        m := regexp.MustCompile(`(?m)^[0-9]+$`)
        if m.MatchString(s) {
            fmt.Print("2")
        } else {
            fmt.Print("0")
        }
    }

    {
        m := regexp.MustCompile(`\A[0-9]+\z`)
        if m.MatchString(s) {
            fmt.Print("3")
        } else {
            fmt.Print("0")
        }
    }

    {
        m := regexp.MustCompile(`(?m)\A[0-9]+\z`)
        if m.MatchString(s) {
            fmt.Print("4")
        } else {
            fmt.Print("0")
        }
    }

    fmt.Println()
}

出力。

0200

PHPと同様に、mフラグを付けてやると複数行モードで、「^」「$」を使用すると部分文字列にマッチする。

まとめ

表にまとめると、以下のような感じか。マッチするケースに「○」、マッチしないケースに「×」を入れている。存在しないパターンは「-」としている。

  • (1) 単一行モードで、「^」「$」を使ったパターン
  • (2) 複数行モードで、「^」「$」を使ったパターン
  • (3) 単一行モードで、「\A」「\z」を使ったパターン
  • (4) 複数行モードで、「\A」「\z」を使ったパターン
(1) (2) (3) (4)
Java × × × ×
C ×
C++ × × ×
PHP × × ×
Python 2 / 3 × × ×
Ruby ×
Perl × × ×
Go × × ×

自分も「^」「$」をついつい使ってしまっていたので、気に留めておくことにしよう。

各言語で部分文字列を取得してみる

各言語で入力された文字列の部分文字列を取得するプログラムを書いてみたメモ。

要件は以下の通り。

  • 標準入力から、1行の文字列が与えられる
  • 入力文字列の部分文字列「[2, 4)」(つまり2~3文字目からなる文字列)を抽出
  • 標準出力に、抽出した文字列を出力

環境

手元にあるものということで、環境は以下のものに限定する。

Java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;

public class Main {
    /**
     * サロゲートを考慮したsubstring
     */
    private static String substring(String s, int startIndex, int endIndex) {
        StringBuilder sb = new StringBuilder();

        if (startIndex < 0) {
            throw new StringIndexOutOfBoundsException(startIndex);
        }
        int cpCount = s.codePointCount(0, s.length());
        if (cpCount < endIndex) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - startIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }

        int idx = 0;
        for (int i = 0; i < s.length() && idx < endIndex; ++i) {
            char ch1 = s.charAt(i);
            if (startIndex <= idx && idx < endIndex) {
                sb.append(ch1);
            }
            if (Character.isSurrogate(ch1)) {
                char ch2 = s.charAt(++i);
                if (startIndex <= idx && idx < endIndex) {
                    sb.append(ch2);
                }
            }
            ++idx;
        }

        return sb.toString();
    }

    public static void main(String[] args) {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
            PrintWriter out = new PrintWriter(System.out)
        ) {
            String s = in.readLine();

            out.println(substring(s, 1, 3));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

サロゲートペアを考慮すると、Javaでは2つのchar値でサロゲートペアを表すことになるので、部分文字列を抽出する処理に一番手間がかかった。

C

#include <stdio.h>
#include <string.h>
#include <locale.h>
#include <wchar.h>
#include <stdlib.h>

int main(int argc, char** argv) {
    setlocale(LC_ALL, "ja_JP.UTF-8");

    char str[1024];

    fgets(str, sizeof(str), stdin);
    while (str[strlen(str) - 1] == '\n' || str[strlen(str) - 1] == '\r') {
        str[strlen(str) - 1] = '\0';
    }

    wchar_t buf[1024];
    const char* p = str;
    mbsrtowcs(buf, &p, sizeof(buf), NULL);

    wchar_t wstr[3];
    memset(wstr, 0, sizeof(wstr));
    // 「2」は言うまでもなく、indexではなくlength
    wcsncpy(wstr, &buf[1], 2);
    fwprintf(stdout, L"%ls\n", wstr);

    return 0;
}

C++

#include <iostream>
#include <locale>
#include <string>
#include <boost/regex.hpp>

using namespace std;

int main(int argc, char** argv) {
    setlocale(LC_ALL, "ja_JP.UTF-8");
    wcout.imbue(locale("japanese"));

    wstring str;
    getline(wcin, str);

    // 「2」はindexではなくlengthであることに注意
    str = str.substr(1, 2);
    wcout << str << endl;

    return EXIT_SUCCESS;
}

PHP

<?php

$str = file_get_contents('php://stdin');
$str = preg_replace("/[\r\n]/", '', $str);

// 「2」はindexではなくlengthであることに注意
echo mb_substr($str, 1, 2, 'UTF-8') . PHP_EOL;

Python 2

import sys

s = sys.stdin.readline()
ustr = unicode(s, 'UTF-8')
ustr = ustr.replace('\n', '')
ustr = ustr.replace('\r', '')

print ustr[1:3].encode('UTF-8')

Python 3

import sys

b = sys.stdin.buffer.readline()
s = str(b, 'UTF-8')
s = s.replace('\n', '')
s = s.replace('\r', '')

print(s[1:3])

Ruby

str = STDIN.gets
str.chomp!()

# 「2」はindexではなくlengthであることに注意
print str[1, 2],"\n"

Perl

use Encode;

my $str = readline(STDIN);
chomp($str);

my $ustr = decode('UTF-8', $str);
# 「2」はindexではなくlengthであることに注意
print encode('UTF-8', substr($ustr, 1, 2)),"\n";

Go

package main

import (
    "fmt"
    "os"
    "io"
    "bufio"
)

func ReadLine(reader *bufio.Reader) (s string, err error) {
    prefix := false
    buf := make([]byte, 0)
    var line []byte
    for {
        line, prefix, err = reader.ReadLine()
        if err == io.EOF {
            return
        }
        buf = append(buf, line...)
        if prefix {
            continue
        }
        s = string(buf)
        return
    }
}

func main() {
    stdin := bufio.NewReader(os.Stdin)
    s, _ := ReadLine(stdin)

    runes := []rune(s)
    fmt.Println(string(runes[1:3]))
}

bash

#! /bin/bash

IFS= read s

echo "${s}" | sed -e 's/^.\(..\).*$/\1/g'

Awk

{
    gsub(/[\r\n]/, "");
    # 第3パラメータの「2」はindexではなくlengthであることに注意
    print substr($0, 2, 2);
}