CLOVER🍀

That was when it all began.

サーバーが対応しているSSL/TLSプロトコルを確認する(openssl s_client、nmap)

これは、なにをしたくて書いたもの?

サーバーが対応しているSSLTLSプロトコルを確認する方法はないかな?と思って、ちょっと調べてみました。

OpenSSLを使えば良いみたいです。

ただ、調べられるのはOpenSSLが利用できるプロトコルの範囲で、ですが。

環境

今回の環境は、こちら。Ubuntu Linux 20.04 LTSです。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.1 LTS
Release:    20.04
Codename:   focal


$ uname -srvmpio
Linux 5.4.0-54-generic #60-Ubuntu SMP Fri Nov 6 10:37:59 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

OpenSSLは、こちらのバージョンを使います。

$ openssl version
OpenSSL 1.1.1f  31 Mar 2020

お題

IPアドレス192.168.33.11のサーバーに、SSLTLSを有効にしたApacheを用意します。

ここに、別のサーバーからOpenSSLコマンドを使ってアクセスして、構築したApacheが対応しているSSLTLSのバージョンを
確認してみます。

Apacheの用意

まずは、Apacheをインストールします。

$ sudo apt install apache2

$ apache2 -v
Server version: Apache/2.4.41 (Ubuntu)
Server built:   2020-08-12T19:46:17

mod_sslを有効にして、SSLTLS用のVirtualHostも有効にします。

$ sudo a2enmod ssl
$ sudo a2ensite default-ssl
$ sudo systemctl restart apache2

確認。

$ curl -I -k https://192.168.33.11
HTTP/1.1 200 OK
Date: Wed, 18 Nov 2020 15:07:07 GMT
Server: Apache/2.4.41 (Ubuntu)
Last-Modified: Wed, 18 Nov 2020 15:04:52 GMT
ETag: "2aa6-5b462eef36f61"
Accept-Ranges: bytes
Content-Length: 10918
Vary: Accept-Encoding
Content-Type: text/html

自己署名証明書ですが、ApacheHTTPSで動作していることが確認できました。

OpenSSLクライアントで、サーバーが対応しているSSLTLSプロトコルを確認する

では、OpenSSLをクライアントとして使い、今回用意したApacheがどのSSLTLSプロトコルに対応しているか確認してみます。

確認は、openssl s_clientで行います。

-tlsXXXオプションを使うことで、使用するプロトコルを指定できます。

$ openssl s_client --help 2>&1 | grep '\-tls1'
 -tls1                      Just use TLSv1
 -tls1_1                    Just use TLSv1.1
 -tls1_2                    Just use TLSv1.2
 -tls1_3                    Just use TLSv1.3

これを利用して、以下のコマンドで指定のSSLTLSプロトコルにサーバーが対応しているかを確認できます。

$ echo | openssl s_client -connect [ホスト]:[ポート] [使用するプロトコル]

空のechoが入っているのは、これを入れない場合に入力待ちになるのを終了させるためです。

ところで、以前は-ssl2-ssl3というオプションもあったようですが、今は使えなくなっています。

$ openssl s_client -ssl2
s_client: Option unknown option -ssl2
s_client: Use -help for summary.


$ openssl s_client -ssl3
s_client: Option unknown option -ssl3
s_client: Use -help for summary.

まあ、使わないですからね…。よって確認という意味では、このあたりには使えないことになります。

とはいえ、OpenSSLで利用できる暗号化スイートを見ると、SSLv3は入っていそうですが…。

$ openssl ciphers -v ALL | perl -wnla -e 'print $F[1]' | sort -u
SSLv3
TLSv1
TLSv1.2
TLSv1.3

今回は、気にしないでおきましょう。

ここで、Apacheの設定ファイルを見て、どのSSLTLSプロトコルが指定されているのか見てみます。

$ grep -r SSLProtocol /etc/apache2/
/etc/apache2/mods-available/ssl.conf:   SSLProtocol all -SSLv3

allからSSLv3を引いたもの、ですね。

ドキュメントだけ見ると、TLS 1.0以上が使えそうな感じに見えます。

all
This is a shortcut for +SSLv3 +TLSv1'' or - when using OpenSSL 1.0.1 and later -+SSLv3 +TLSv1 +TLSv1.1 +TLSv1.2'', respectively (except for OpenSSL versions compiled with the ``no-ssl3'' configuration option, where all does not include +SSLv3).

SSLProtocol Directive

では、確認してみます。

TLS 1.3。こちらは、接続がうまくいきます。

$ echo | openssl s_client -connect 192.168.33.11:443 -tls1_3
CONNECTED(00000003)
Can't use SSL_get_servername
depth=0 CN = ubuntu2004.localdomain
verify error:num=18:self signed certificate
verify return:1
depth=0 CN = ubuntu2004.localdomain
verify return:1
---
Certificate chain
 0 s:CN = ubuntu2004.localdomain
   i:CN = ubuntu2004.localdomain
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIDADCCAeigAwIBAgIUSZu0PqgWEJGZZN1ovyCtcXLpHQQwDQYJKoZIhvcNAQEL
BQAwITEfMB0GA1UEAwwWdWJ1bnR1MjAwNC5sb2NhbGRvbWFpbjAeFw0yMDExMTgx
NTA0NDdaFw0zMDExMTYxNTA0NDdaMCExHzAdBgNVBAMMFnVidW50dTIwMDQubG9j
YWxkb21haW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDapFGUGAZv
ABuD27z9SRBFbgy0vsUAl8pVewz3qNWigVWnhPDPSbh1SvT7jjM+SHXC6iXxFllJ
EiG2SIv4cBE35RaI6XUAgwgMdxyIAHOR/IVGt+Co/JxAM8Ly2xhxzrTifT8ypxyp
TZwp4ePDPi4A5DZpLUEFuhWQd/bk482B/RcIgUmc8kflW2JWIzIqcQqjKdON6bw/
DDtw2BfP+j82m84yG/dXitTA2rU6VlbeHZGYmaJb8jEdW1vwE/be9skUpkIYNHj6
pVkKgtUFy/zaDPoixLQD3R8J7mNBQjkw1BTL+6kfSVfUD/CNLLdxYiox360s+xZD
8puR0cZNrRSbAgMBAAGjMDAuMAkGA1UdEwQCMAAwIQYDVR0RBBowGIIWdWJ1bnR1
MjAwNC5sb2NhbGRvbWFpbjANBgkqhkiG9w0BAQsFAAOCAQEAE3dWIILuPyBUX/8+
82XypovL7SrmCoCZ8xa4oRqsVFfQN76CHargkhwMIhHaFb+PLyRUOHmeHHi5UYIm
VYBDnyhmFJdYO7tI8B7MLj4BTNq2PV40tEL+MyAdX/Pwlo2oxBDGC14RGUjUatKY
O0RyRz0FjwxATDNelPhPsazmXq33r/4n4WWM5Q8YtbzkYBHSnLbqSB5/LsrvO+kW
n7enhawbUv6kVB5Ph6S+khR4mlRF5xGcp0qUTKlN2rN3UcV0GyQjegnh0IGZinED
NCVSEcec0BqHcj5eTCXzJvPxt1rXKprnSWIcJtfNFibxsP50MBOEgpEnaDcz/hY0
ZhRFFg==
-----END CERTIFICATE-----
subject=CN = ubuntu2004.localdomain

issuer=CN = ubuntu2004.localdomain

---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: RSA-PSS
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 1328 bytes and written 295 bytes
Verification error: self signed certificate
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 2048 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 18 (self signed certificate)
---
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384
    Session-ID: A708A31196A289795C7B60F4B4EC20237F2201C7B8C36326BAF453DCCA668D06
    Session-ID-ctx: 
    Resumption PSK: AE6D958DA4478714610E437A1041FC561E7917F3709A5ADC0A1EA551922A608075D661C81533800E8D1185CFE1BA2B15
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 300 (seconds)
    TLS session ticket:
    0000 - a4 ca 94 62 e0 c0 b7 ee-95 90 3c 32 46 d4 7b 03   ...b......<2F.{.
    0010 - fe 76 19 8f d1 66 18 49-ea 90 a3 70 23 d9 5f 2d   .v...f.I...p#._-
    0020 - 33 ef 50 38 9f 6b 3d ac-59 97 d5 5d 31 d2 3c 00   3.P8.k=.Y..]1.<.
    0030 - c7 8f e3 c4 86 e7 fd 77-db e4 79 ce ff 57 dd c9   .......w..y..W..
    0040 - ed e4 42 1a a0 63 ee c1-bb 15 4e dd b9 23 1b d6   ..B..c....N..#..
    0050 - 29 1c a5 3c 97 73 e5 2b-93 24 93 fe fb 4d 7a c1   )..<.s.+.$...Mz.
    0060 - a8 46 0f d4 ee 2d 4f e3-99 94 5d d0 9e 1b f2 91   .F...-O...].....
    0070 - 7f 6b d7 b2 3f 2f 92 0a-e9 36 eb 56 92 9c 2c e6   .k..?/...6.V..,.
    0080 - c2 75 3a 8f 67 36 19 34-2f 90 0a 00 9a ab ed 89   .u:.g6.4/.......
    0090 - 17 e4 c3 3d 98 f7 8d c9-26 6e 90 8d e2 a7 1a d7   ...=....&n......
    00a0 - da de 47 c4 62 e6 06 7e-bb 5a 90 01 3c 7b 08 14   ..G.b..~.Z..<{..
    00b0 - d1 f8 eb fe 4d 32 97 6f-09 21 4e 70 fa 64 a7 87   ....M2.o.!Np.d..
    00c0 - 73 61 b2 d6 6f fa 15 b9-fa ce 02 2a a8 2f 26 90   sa..o......*./&.
    00d0 - e3 6c a7 a8 2e 40 c8 a9-27 57 59 fa ab 60 8f 39   .l...@..'WY..`.9

    Start Time: 1605717514
    Timeout   : 7200 (sec)
    Verify return code: 18 (self signed certificate)
    Extended master secret: no
    Max Early Data: 0
---
read R BLOCK
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384
    Session-ID: C15FB7D0AB53DA4EC05FB4D374F4EC7AF2D75043EC63ECEA06D4EB5C30FF55BE
    Session-ID-ctx: 
    Resumption PSK: B1F62BABD2FC0971CE6808A045C23E8B420FADB9C96C902F861CB37101113B3362BF45931F400A3E50312EAF912C9132
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 300 (seconds)
    TLS session ticket:
    0000 - a4 ca 94 62 e0 c0 b7 ee-95 90 3c 32 46 d4 7b 03   ...b......<2F.{.
    0010 - 83 60 13 b1 d7 41 e4 b1-a7 c3 76 b2 72 93 4a d8   .`...A....v.r.J.
    0020 - e1 07 8f ee a8 57 15 3d-bb 4f cb c3 16 42 a8 60   .....W.=.O...B.`
    0030 - c7 8e c5 c0 d6 84 42 7c-56 0a 50 24 14 97 a1 ee   ......B|V.P$....
    0040 - 75 79 b6 d0 85 0c 4d bc-f2 69 73 3b 5b 7b 8d 87   uy....M..is;[{..
    0050 - f5 ca ad 76 0b 40 8a 23-14 a0 a1 2f d8 50 e0 b1   ...v.@.#.../.P..
    0060 - ae 23 41 17 59 46 89 96-d4 59 b6 0b 7c b7 ee 46   .#A.YF...Y..|..F
    0070 - 44 68 cf 0a 52 3c cb db-54 0c 28 25 d6 6b 24 c1   Dh..R<..T.(%.k$.
    0080 - 2a 33 d6 ab e9 5a 90 53-a4 5d 9c ce dd 89 b8 2a   *3...Z.S.].....*
    0090 - 48 de a1 be ab 9c 0d 84-7a e7 17 44 e5 89 c1 cb   H.......z..D....
    00a0 - cb 8c 03 05 4e 24 45 69-08 49 86 01 62 f6 87 d1   ....N$Ei.I..b...
    00b0 - 09 72 37 83 e5 84 2f 1b-d5 e5 02 95 fc 68 d2 c7   .r7.../......h..
    00c0 - 0f d8 09 4f 45 4c 0d 39-b8 ff 37 67 99 4b 98 98   ...OEL.9..7g.K..
    00d0 - 6c 1d c7 14 23 e6 50 23-90 27 4d 01 b2 c2 f9 ac   l...#.P#.'M.....

    Start Time: 1605717514
    Timeout   : 7200 (sec)
    Verify return code: 18 (self signed certificate)
    Extended master secret: no
    Max Early Data: 0
---
read R BLOCK
DONE

TLS 1.2。こちらもうまくいきます。

$ echo | openssl s_client -connect 192.168.33.11:443 -tls1_2
CONNECTED(00000003)
Can't use SSL_get_servername
depth=0 CN = ubuntu2004.localdomain
verify error:num=18:self signed certificate
verify return:1
depth=0 CN = ubuntu2004.localdomain
verify return:1
---
Certificate chain
 0 s:CN = ubuntu2004.localdomain
   i:CN = ubuntu2004.localdomain
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIDADCCAeigAwIBAgIUSZu0PqgWEJGZZN1ovyCtcXLpHQQwDQYJKoZIhvcNAQEL
BQAwITEfMB0GA1UEAwwWdWJ1bnR1MjAwNC5sb2NhbGRvbWFpbjAeFw0yMDExMTgx
NTA0NDdaFw0zMDExMTYxNTA0NDdaMCExHzAdBgNVBAMMFnVidW50dTIwMDQubG9j
YWxkb21haW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDapFGUGAZv
ABuD27z9SRBFbgy0vsUAl8pVewz3qNWigVWnhPDPSbh1SvT7jjM+SHXC6iXxFllJ
EiG2SIv4cBE35RaI6XUAgwgMdxyIAHOR/IVGt+Co/JxAM8Ly2xhxzrTifT8ypxyp
TZwp4ePDPi4A5DZpLUEFuhWQd/bk482B/RcIgUmc8kflW2JWIzIqcQqjKdON6bw/
DDtw2BfP+j82m84yG/dXitTA2rU6VlbeHZGYmaJb8jEdW1vwE/be9skUpkIYNHj6
pVkKgtUFy/zaDPoixLQD3R8J7mNBQjkw1BTL+6kfSVfUD/CNLLdxYiox360s+xZD
8puR0cZNrRSbAgMBAAGjMDAuMAkGA1UdEwQCMAAwIQYDVR0RBBowGIIWdWJ1bnR1
MjAwNC5sb2NhbGRvbWFpbjANBgkqhkiG9w0BAQsFAAOCAQEAE3dWIILuPyBUX/8+
82XypovL7SrmCoCZ8xa4oRqsVFfQN76CHargkhwMIhHaFb+PLyRUOHmeHHi5UYIm
VYBDnyhmFJdYO7tI8B7MLj4BTNq2PV40tEL+MyAdX/Pwlo2oxBDGC14RGUjUatKY
O0RyRz0FjwxATDNelPhPsazmXq33r/4n4WWM5Q8YtbzkYBHSnLbqSB5/LsrvO+kW
n7enhawbUv6kVB5Ph6S+khR4mlRF5xGcp0qUTKlN2rN3UcV0GyQjegnh0IGZinED
NCVSEcec0BqHcj5eTCXzJvPxt1rXKprnSWIcJtfNFibxsP50MBOEgpEnaDcz/hY0
ZhRFFg==
-----END CERTIFICATE-----
subject=CN = ubuntu2004.localdomain

issuer=CN = ubuntu2004.localdomain

---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: RSA-PSS
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 1429 bytes and written 281 bytes
Verification error: self signed certificate
---
New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-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-GCM-SHA384
    Session-ID: 21A6BDF246B2663FDDF515879D77AE2B9CA98C9E1B2E8542833CD5DC5D73950D
    Session-ID-ctx: 
    Master-Key: 8BA6EF7E41F59386515662BA92F1EEAEC8E39AB217C7D441BC8CAC3A5FEAF2B6D91A14DE909B084E8F499AE6F9F28FF5
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 300 (seconds)
    TLS session ticket:
    0000 - a4 ca 94 62 e0 c0 b7 ee-95 90 3c 32 46 d4 7b 03   ...b......<2F.{.
    0010 - 3c b3 57 b8 ff 62 f1 4f-05 89 ea 14 b1 c8 8e 16   <.W..b.O........
    0020 - 04 52 8f bf eb 6b bf 8e-a4 81 2c 87 75 c0 d1 82   .R...k....,.u...
    0030 - 7c 95 3a d3 e3 05 36 ed-aa 78 0e 86 bc 28 2f a8   |.:...6..x...(/.
    0040 - 8d ff c5 62 7b 52 42 25-08 96 4e e1 25 8d e1 8f   ...b{RB%..N.%...
    0050 - 58 4d c9 74 0c e1 bc 7f-4f e0 e2 4c 84 5c 08 3b   XM.t....O..L.\.;
    0060 - 68 29 05 9f 95 34 ac 9e-d5 c8 ac ef 52 d6 71 2b   h)...4......R.q+
    0070 - b7 f0 b5 29 71 6c 78 61-06 07 69 56 23 3c ce 3e   ...)qlxa..iV#<.>
    0080 - 3a 97 55 9e 14 ab e3 29-04 48 e0 93 97 48 55 30   :.U....).H...HU0
    0090 - 57 2b e5 0b a6 82 67 de-8b 9a 2a 59 bf 9b 68 2b   W+....g...*Y..h+
    00a0 - b1 47 0c 18 53 36 06 26-cd d6 4b 4d ba 13 e3 63   .G..S6.&..KM...c
    00b0 - 5b 73 45 f9 19 f1 26 12-b6 fa e6 3e cd cc fa 80   [sE...&....>....

    Start Time: 1605717604
    Timeout   : 7200 (sec)
    Verify return code: 18 (self signed certificate)
    Extended master secret: yes
---
DONE

TLS 1.1、TLS 1.0はうまくいきません。

## TLS 1.1
$ echo | openssl s_client -connect 192.168.33.11:443 -tls1_1
CONNECTED(00000003)
139968696386880:error:141E70BF:SSL routines:tls_construct_client_hello:no protocols available:../ssl/statem/statem_clnt.c:1112:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 0 bytes and written 7 bytes
Verification: OK
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---


## TLS 1.0
$ echo | openssl s_client -connect 192.168.33.11:443 -tls1
CONNECTED(00000003)
140670905648448:error:141E70BF:SSL routines:tls_construct_client_hello:no protocols available:../ssl/statem/statem_clnt.c:1112:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 0 bytes and written 7 bytes
Verification: OK
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---

つまり、TLS 1.2およびTLS 1.3に対応している状態ですね。

ここで、ApacheTLS 1.3のみに対応するように変更してみます。
/etc/apache2/mods-enabled/ssl.conf

        SSLProtocol TLSv1.3

再起動。

$ sudo systemctl restart apache2

確認。TLS 1.3はOKです。

$ echo | openssl s_client -connect 192.168.33.11:443 -tls1_3
CONNECTED(00000003)
Can't use SSL_get_servername
depth=0 CN = ubuntu2004.localdomain
verify error:num=18:self signed certificate
verify return:1
depth=0 CN = ubuntu2004.localdomain
verify return:1
---
Certificate chain
 0 s:CN = ubuntu2004.localdomain
   i:CN = ubuntu2004.localdomain
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIDADCCAeigAwIBAgIUSZu0PqgWEJGZZN1ovyCtcXLpHQQwDQYJKoZIhvcNAQEL
BQAwITEfMB0GA1UEAwwWdWJ1bnR1MjAwNC5sb2NhbGRvbWFpbjAeFw0yMDExMTgx
NTA0NDdaFw0zMDExMTYxNTA0NDdaMCExHzAdBgNVBAMMFnVidW50dTIwMDQubG9j
YWxkb21haW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDapFGUGAZv
ABuD27z9SRBFbgy0vsUAl8pVewz3qNWigVWnhPDPSbh1SvT7jjM+SHXC6iXxFllJ
EiG2SIv4cBE35RaI6XUAgwgMdxyIAHOR/IVGt+Co/JxAM8Ly2xhxzrTifT8ypxyp
TZwp4ePDPi4A5DZpLUEFuhWQd/bk482B/RcIgUmc8kflW2JWIzIqcQqjKdON6bw/
DDtw2BfP+j82m84yG/dXitTA2rU6VlbeHZGYmaJb8jEdW1vwE/be9skUpkIYNHj6
pVkKgtUFy/zaDPoixLQD3R8J7mNBQjkw1BTL+6kfSVfUD/CNLLdxYiox360s+xZD
8puR0cZNrRSbAgMBAAGjMDAuMAkGA1UdEwQCMAAwIQYDVR0RBBowGIIWdWJ1bnR1
MjAwNC5sb2NhbGRvbWFpbjANBgkqhkiG9w0BAQsFAAOCAQEAE3dWIILuPyBUX/8+
82XypovL7SrmCoCZ8xa4oRqsVFfQN76CHargkhwMIhHaFb+PLyRUOHmeHHi5UYIm
VYBDnyhmFJdYO7tI8B7MLj4BTNq2PV40tEL+MyAdX/Pwlo2oxBDGC14RGUjUatKY
O0RyRz0FjwxATDNelPhPsazmXq33r/4n4WWM5Q8YtbzkYBHSnLbqSB5/LsrvO+kW
n7enhawbUv6kVB5Ph6S+khR4mlRF5xGcp0qUTKlN2rN3UcV0GyQjegnh0IGZinED
NCVSEcec0BqHcj5eTCXzJvPxt1rXKprnSWIcJtfNFibxsP50MBOEgpEnaDcz/hY0
ZhRFFg==
-----END CERTIFICATE-----
subject=CN = ubuntu2004.localdomain

issuer=CN = ubuntu2004.localdomain

---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: RSA-PSS
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 1328 bytes and written 295 bytes
Verification error: self signed certificate
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 2048 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 18 (self signed certificate)
---
DONE

TLS 1.2を指定すると、動作しなくなります。

$ echo | openssl s_client -connect 192.168.33.11:443 -tls1_2
CONNECTED(00000003)
140545358013760:error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version:../ssl/record/rec_layer_s3.c:1543:SSL alert number 70
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 188 bytes
Verification: OK
---
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: 
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    Start Time: 1605717794
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
    Extended master secret: no
---

こんな感じで確認できました、と。

nmapを使って調べてみる

別のやり方として、nmapを使う方法もあるようです。

nmapをインストール。

$ sudo apt install nmap

$ nmap --version
Nmap version 7.80 ( https://nmap.org )
Platform: x86_64-pc-linux-gnu
Compiled with: liblua-5.3.3 openssl-1.1.1d nmap-libssh2-1.8.2 libz-1.2.11 libpcre-8.39 libpcap-1.9.1 nmap-libdnet-1.12 ipv6
Compiled without:
Available nsock engines: epoll poll select

ssl-enum-ciphers NSE script — Nmap Scripting Engine documentation

ssl-enum-ciphersスクリプト指定で、通信先が対応しているSSLTLSプロトコルを確認することができます。

先ほどのApacheSSLTLSを有効にしただけの状態)に対して使うと、こんな感じになります。

$ nmap -sV --script ssl-enum-ciphers -p 443 192.168.33.11
Starting Nmap 7.80 ( https://nmap.org ) at 2020-11-18 15:14 UTC
Nmap scan report for 192.168.33.11
Host is up (0.00042s latency).

PORT    STATE SERVICE  VERSION
443/tcp open  ssl/http Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
| ssl-enum-ciphers: 
|   TLSv1.2: 
|     ciphers: 
|       TLS_DHE_RSA_WITH_AES_128_CBC_SHA (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_128_CCM (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_128_CCM_8 (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_256_CBC_SHA (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_256_CCM (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_256_CCM_8 (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 (dh 2048) - A
|       TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256 (dh 2048) - A
|       TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384 (dh 2048) - A
|       TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA (dh 2048) - A
|       TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 (dh 2048) - A
|       TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA (dh 2048) - A
|       TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256 (dh 2048) - A
|       TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (dh 2048) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (secp256r1) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CCM (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CCM_8 (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_GCM_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CCM (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CCM_8 (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_GCM_SHA384 (rsa 2048) - A
|       TLS_RSA_WITH_ARIA_128_GCM_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_ARIA_256_GCM_SHA384 (rsa 2048) - A
|       TLS_RSA_WITH_CAMELLIA_128_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_CAMELLIA_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256 (rsa 2048) - A
|     compressors: 
|       NULL
|     cipher preference: client
|_  least strength: A

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 12.59 seconds

ですが、TLS 1.3には対応していません。

TLS 1.2の対応をドロップした後は、こんな感じになります。

$ nmap -sV --script ssl-enum-ciphers -p 443 192.168.33.11
Starting Nmap 7.80 ( https://nmap.org ) at 2020-11-18 15:32 UTC
Nmap scan report for 192.168.33.11
Host is up (0.00036s latency).

PORT    STATE SERVICE  VERSION
443/tcp open  ssl/http Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 12.41 seconds

OpenSSLが対応している暗号化スイートを調べる

OpenSSLのciphersで確認できます。ALLを付けないと、表示数がだいぶ減りますね。

$ openssl ciphers -v ALL
TLS_AES_256_GCM_SHA384  TLSv1.3 Kx=any      Au=any  Enc=AESGCM(256) Mac=AEAD
TLS_CHACHA20_POLY1305_SHA256 TLSv1.3 Kx=any      Au=any  Enc=CHACHA20/POLY1305(256) Mac=AEAD
TLS_AES_128_GCM_SHA256  TLSv1.3 Kx=any      Au=any  Enc=AESGCM(128) Mac=AEAD
ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AESGCM(256) Mac=AEAD
ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH     Au=RSA  Enc=AESGCM(256) Mac=AEAD
DHE-DSS-AES256-GCM-SHA384 TLSv1.2 Kx=DH       Au=DSS  Enc=AESGCM(256) Mac=AEAD
DHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=DH       Au=RSA  Enc=AESGCM(256) Mac=AEAD
ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=CHACHA20/POLY1305(256) Mac=AEAD
ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH     Au=RSA  Enc=CHACHA20/POLY1305(256) Mac=AEAD
DHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=DH       Au=RSA  Enc=CHACHA20/POLY1305(256) Mac=AEAD
ECDHE-ECDSA-AES256-CCM8 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AESCCM8(256) Mac=AEAD
ECDHE-ECDSA-AES256-CCM  TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AESCCM(256) Mac=AEAD
DHE-RSA-AES256-CCM8     TLSv1.2 Kx=DH       Au=RSA  Enc=AESCCM8(256) Mac=AEAD
DHE-RSA-AES256-CCM      TLSv1.2 Kx=DH       Au=RSA  Enc=AESCCM(256) Mac=AEAD
ECDHE-ECDSA-ARIA256-GCM-SHA384 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=ARIAGCM(256) Mac=AEAD
ECDHE-ARIA256-GCM-SHA384 TLSv1.2 Kx=ECDH     Au=RSA  Enc=ARIAGCM(256) Mac=AEAD
DHE-DSS-ARIA256-GCM-SHA384 TLSv1.2 Kx=DH       Au=DSS  Enc=ARIAGCM(256) Mac=AEAD
DHE-RSA-ARIA256-GCM-SHA384 TLSv1.2 Kx=DH       Au=RSA  Enc=ARIAGCM(256) Mac=AEAD
ADH-AES256-GCM-SHA384   TLSv1.2 Kx=DH       Au=None Enc=AESGCM(256) Mac=AEAD
ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AESGCM(128) Mac=AEAD
ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH     Au=RSA  Enc=AESGCM(128) Mac=AEAD
DHE-DSS-AES128-GCM-SHA256 TLSv1.2 Kx=DH       Au=DSS  Enc=AESGCM(128) Mac=AEAD
DHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=DH       Au=RSA  Enc=AESGCM(128) Mac=AEAD
ECDHE-ECDSA-AES128-CCM8 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AESCCM8(128) Mac=AEAD
ECDHE-ECDSA-AES128-CCM  TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AESCCM(128) Mac=AEAD
DHE-RSA-AES128-CCM8     TLSv1.2 Kx=DH       Au=RSA  Enc=AESCCM8(128) Mac=AEAD
DHE-RSA-AES128-CCM      TLSv1.2 Kx=DH       Au=RSA  Enc=AESCCM(128) Mac=AEAD
ECDHE-ECDSA-ARIA128-GCM-SHA256 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=ARIAGCM(128) Mac=AEAD
ECDHE-ARIA128-GCM-SHA256 TLSv1.2 Kx=ECDH     Au=RSA  Enc=ARIAGCM(128) Mac=AEAD
DHE-DSS-ARIA128-GCM-SHA256 TLSv1.2 Kx=DH       Au=DSS  Enc=ARIAGCM(128) Mac=AEAD
DHE-RSA-ARIA128-GCM-SHA256 TLSv1.2 Kx=DH       Au=RSA  Enc=ARIAGCM(128) Mac=AEAD
ADH-AES128-GCM-SHA256   TLSv1.2 Kx=DH       Au=None Enc=AESGCM(128) Mac=AEAD
ECDHE-ECDSA-AES256-SHA384 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AES(256)  Mac=SHA384
ECDHE-RSA-AES256-SHA384 TLSv1.2 Kx=ECDH     Au=RSA  Enc=AES(256)  Mac=SHA384
DHE-RSA-AES256-SHA256   TLSv1.2 Kx=DH       Au=RSA  Enc=AES(256)  Mac=SHA256
DHE-DSS-AES256-SHA256   TLSv1.2 Kx=DH       Au=DSS  Enc=AES(256)  Mac=SHA256
ECDHE-ECDSA-CAMELLIA256-SHA384 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=Camellia(256) Mac=SHA384
ECDHE-RSA-CAMELLIA256-SHA384 TLSv1.2 Kx=ECDH     Au=RSA  Enc=Camellia(256) Mac=SHA384
DHE-RSA-CAMELLIA256-SHA256 TLSv1.2 Kx=DH       Au=RSA  Enc=Camellia(256) Mac=SHA256
DHE-DSS-CAMELLIA256-SHA256 TLSv1.2 Kx=DH       Au=DSS  Enc=Camellia(256) Mac=SHA256
ADH-AES256-SHA256       TLSv1.2 Kx=DH       Au=None Enc=AES(256)  Mac=SHA256
ADH-CAMELLIA256-SHA256  TLSv1.2 Kx=DH       Au=None Enc=Camellia(256) Mac=SHA256
ECDHE-ECDSA-AES128-SHA256 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AES(128)  Mac=SHA256
ECDHE-RSA-AES128-SHA256 TLSv1.2 Kx=ECDH     Au=RSA  Enc=AES(128)  Mac=SHA256
DHE-RSA-AES128-SHA256   TLSv1.2 Kx=DH       Au=RSA  Enc=AES(128)  Mac=SHA256
DHE-DSS-AES128-SHA256   TLSv1.2 Kx=DH       Au=DSS  Enc=AES(128)  Mac=SHA256
ECDHE-ECDSA-CAMELLIA128-SHA256 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=Camellia(128) Mac=SHA256
ECDHE-RSA-CAMELLIA128-SHA256 TLSv1.2 Kx=ECDH     Au=RSA  Enc=Camellia(128) Mac=SHA256
DHE-RSA-CAMELLIA128-SHA256 TLSv1.2 Kx=DH       Au=RSA  Enc=Camellia(128) Mac=SHA256
DHE-DSS-CAMELLIA128-SHA256 TLSv1.2 Kx=DH       Au=DSS  Enc=Camellia(128) Mac=SHA256
ADH-AES128-SHA256       TLSv1.2 Kx=DH       Au=None Enc=AES(128)  Mac=SHA256
ADH-CAMELLIA128-SHA256  TLSv1.2 Kx=DH       Au=None Enc=Camellia(128) Mac=SHA256
ECDHE-ECDSA-AES256-SHA  TLSv1 Kx=ECDH     Au=ECDSA Enc=AES(256)  Mac=SHA1
ECDHE-RSA-AES256-SHA    TLSv1 Kx=ECDH     Au=RSA  Enc=AES(256)  Mac=SHA1
DHE-RSA-AES256-SHA      SSLv3 Kx=DH       Au=RSA  Enc=AES(256)  Mac=SHA1
DHE-DSS-AES256-SHA      SSLv3 Kx=DH       Au=DSS  Enc=AES(256)  Mac=SHA1
DHE-RSA-CAMELLIA256-SHA SSLv3 Kx=DH       Au=RSA  Enc=Camellia(256) Mac=SHA1
DHE-DSS-CAMELLIA256-SHA SSLv3 Kx=DH       Au=DSS  Enc=Camellia(256) Mac=SHA1
AECDH-AES256-SHA        TLSv1 Kx=ECDH     Au=None Enc=AES(256)  Mac=SHA1
ADH-AES256-SHA          SSLv3 Kx=DH       Au=None Enc=AES(256)  Mac=SHA1
ADH-CAMELLIA256-SHA     SSLv3 Kx=DH       Au=None Enc=Camellia(256) Mac=SHA1
ECDHE-ECDSA-AES128-SHA  TLSv1 Kx=ECDH     Au=ECDSA Enc=AES(128)  Mac=SHA1
ECDHE-RSA-AES128-SHA    TLSv1 Kx=ECDH     Au=RSA  Enc=AES(128)  Mac=SHA1
DHE-RSA-AES128-SHA      SSLv3 Kx=DH       Au=RSA  Enc=AES(128)  Mac=SHA1
DHE-DSS-AES128-SHA      SSLv3 Kx=DH       Au=DSS  Enc=AES(128)  Mac=SHA1
DHE-RSA-SEED-SHA        SSLv3 Kx=DH       Au=RSA  Enc=SEED(128) Mac=SHA1
DHE-DSS-SEED-SHA        SSLv3 Kx=DH       Au=DSS  Enc=SEED(128) Mac=SHA1
DHE-RSA-CAMELLIA128-SHA SSLv3 Kx=DH       Au=RSA  Enc=Camellia(128) Mac=SHA1
DHE-DSS-CAMELLIA128-SHA SSLv3 Kx=DH       Au=DSS  Enc=Camellia(128) Mac=SHA1
AECDH-AES128-SHA        TLSv1 Kx=ECDH     Au=None Enc=AES(128)  Mac=SHA1
ADH-AES128-SHA          SSLv3 Kx=DH       Au=None Enc=AES(128)  Mac=SHA1
ADH-SEED-SHA            SSLv3 Kx=DH       Au=None Enc=SEED(128) Mac=SHA1
ADH-CAMELLIA128-SHA     SSLv3 Kx=DH       Au=None Enc=Camellia(128) Mac=SHA1
RSA-PSK-AES256-GCM-SHA384 TLSv1.2 Kx=RSAPSK   Au=RSA  Enc=AESGCM(256) Mac=AEAD
DHE-PSK-AES256-GCM-SHA384 TLSv1.2 Kx=DHEPSK   Au=PSK  Enc=AESGCM(256) Mac=AEAD
RSA-PSK-CHACHA20-POLY1305 TLSv1.2 Kx=RSAPSK   Au=RSA  Enc=CHACHA20/POLY1305(256) Mac=AEAD
DHE-PSK-CHACHA20-POLY1305 TLSv1.2 Kx=DHEPSK   Au=PSK  Enc=CHACHA20/POLY1305(256) Mac=AEAD
ECDHE-PSK-CHACHA20-POLY1305 TLSv1.2 Kx=ECDHEPSK Au=PSK  Enc=CHACHA20/POLY1305(256) Mac=AEAD
DHE-PSK-AES256-CCM8     TLSv1.2 Kx=DHEPSK   Au=PSK  Enc=AESCCM8(256) Mac=AEAD
DHE-PSK-AES256-CCM      TLSv1.2 Kx=DHEPSK   Au=PSK  Enc=AESCCM(256) Mac=AEAD
RSA-PSK-ARIA256-GCM-SHA384 TLSv1.2 Kx=RSAPSK   Au=RSA  Enc=ARIAGCM(256) Mac=AEAD
DHE-PSK-ARIA256-GCM-SHA384 TLSv1.2 Kx=DHEPSK   Au=PSK  Enc=ARIAGCM(256) Mac=AEAD
AES256-GCM-SHA384       TLSv1.2 Kx=RSA      Au=RSA  Enc=AESGCM(256) Mac=AEAD
AES256-CCM8             TLSv1.2 Kx=RSA      Au=RSA  Enc=AESCCM8(256) Mac=AEAD
AES256-CCM              TLSv1.2 Kx=RSA      Au=RSA  Enc=AESCCM(256) Mac=AEAD
ARIA256-GCM-SHA384      TLSv1.2 Kx=RSA      Au=RSA  Enc=ARIAGCM(256) Mac=AEAD
PSK-AES256-GCM-SHA384   TLSv1.2 Kx=PSK      Au=PSK  Enc=AESGCM(256) Mac=AEAD
PSK-CHACHA20-POLY1305   TLSv1.2 Kx=PSK      Au=PSK  Enc=CHACHA20/POLY1305(256) Mac=AEAD
PSK-AES256-CCM8         TLSv1.2 Kx=PSK      Au=PSK  Enc=AESCCM8(256) Mac=AEAD
PSK-AES256-CCM          TLSv1.2 Kx=PSK      Au=PSK  Enc=AESCCM(256) Mac=AEAD
PSK-ARIA256-GCM-SHA384  TLSv1.2 Kx=PSK      Au=PSK  Enc=ARIAGCM(256) Mac=AEAD
RSA-PSK-AES128-GCM-SHA256 TLSv1.2 Kx=RSAPSK   Au=RSA  Enc=AESGCM(128) Mac=AEAD
DHE-PSK-AES128-GCM-SHA256 TLSv1.2 Kx=DHEPSK   Au=PSK  Enc=AESGCM(128) Mac=AEAD
DHE-PSK-AES128-CCM8     TLSv1.2 Kx=DHEPSK   Au=PSK  Enc=AESCCM8(128) Mac=AEAD
DHE-PSK-AES128-CCM      TLSv1.2 Kx=DHEPSK   Au=PSK  Enc=AESCCM(128) Mac=AEAD
RSA-PSK-ARIA128-GCM-SHA256 TLSv1.2 Kx=RSAPSK   Au=RSA  Enc=ARIAGCM(128) Mac=AEAD
DHE-PSK-ARIA128-GCM-SHA256 TLSv1.2 Kx=DHEPSK   Au=PSK  Enc=ARIAGCM(128) Mac=AEAD
AES128-GCM-SHA256       TLSv1.2 Kx=RSA      Au=RSA  Enc=AESGCM(128) Mac=AEAD
AES128-CCM8             TLSv1.2 Kx=RSA      Au=RSA  Enc=AESCCM8(128) Mac=AEAD
AES128-CCM              TLSv1.2 Kx=RSA      Au=RSA  Enc=AESCCM(128) Mac=AEAD
ARIA128-GCM-SHA256      TLSv1.2 Kx=RSA      Au=RSA  Enc=ARIAGCM(128) Mac=AEAD
PSK-AES128-GCM-SHA256   TLSv1.2 Kx=PSK      Au=PSK  Enc=AESGCM(128) Mac=AEAD
PSK-AES128-CCM8         TLSv1.2 Kx=PSK      Au=PSK  Enc=AESCCM8(128) Mac=AEAD
PSK-AES128-CCM          TLSv1.2 Kx=PSK      Au=PSK  Enc=AESCCM(128) Mac=AEAD
PSK-ARIA128-GCM-SHA256  TLSv1.2 Kx=PSK      Au=PSK  Enc=ARIAGCM(128) Mac=AEAD
AES256-SHA256           TLSv1.2 Kx=RSA      Au=RSA  Enc=AES(256)  Mac=SHA256
CAMELLIA256-SHA256      TLSv1.2 Kx=RSA      Au=RSA  Enc=Camellia(256) Mac=SHA256
AES128-SHA256           TLSv1.2 Kx=RSA      Au=RSA  Enc=AES(128)  Mac=SHA256
CAMELLIA128-SHA256      TLSv1.2 Kx=RSA      Au=RSA  Enc=Camellia(128) Mac=SHA256
ECDHE-PSK-AES256-CBC-SHA384 TLSv1 Kx=ECDHEPSK Au=PSK  Enc=AES(256)  Mac=SHA384
ECDHE-PSK-AES256-CBC-SHA TLSv1 Kx=ECDHEPSK Au=PSK  Enc=AES(256)  Mac=SHA1
SRP-DSS-AES-256-CBC-SHA SSLv3 Kx=SRP      Au=DSS  Enc=AES(256)  Mac=SHA1
SRP-RSA-AES-256-CBC-SHA SSLv3 Kx=SRP      Au=RSA  Enc=AES(256)  Mac=SHA1
SRP-AES-256-CBC-SHA     SSLv3 Kx=SRP      Au=SRP  Enc=AES(256)  Mac=SHA1
RSA-PSK-AES256-CBC-SHA384 TLSv1 Kx=RSAPSK   Au=RSA  Enc=AES(256)  Mac=SHA384
DHE-PSK-AES256-CBC-SHA384 TLSv1 Kx=DHEPSK   Au=PSK  Enc=AES(256)  Mac=SHA384
RSA-PSK-AES256-CBC-SHA  SSLv3 Kx=RSAPSK   Au=RSA  Enc=AES(256)  Mac=SHA1
DHE-PSK-AES256-CBC-SHA  SSLv3 Kx=DHEPSK   Au=PSK  Enc=AES(256)  Mac=SHA1
ECDHE-PSK-CAMELLIA256-SHA384 TLSv1 Kx=ECDHEPSK Au=PSK  Enc=Camellia(256) Mac=SHA384
RSA-PSK-CAMELLIA256-SHA384 TLSv1 Kx=RSAPSK   Au=RSA  Enc=Camellia(256) Mac=SHA384
DHE-PSK-CAMELLIA256-SHA384 TLSv1 Kx=DHEPSK   Au=PSK  Enc=Camellia(256) Mac=SHA384
AES256-SHA              SSLv3 Kx=RSA      Au=RSA  Enc=AES(256)  Mac=SHA1
CAMELLIA256-SHA         SSLv3 Kx=RSA      Au=RSA  Enc=Camellia(256) Mac=SHA1
PSK-AES256-CBC-SHA384   TLSv1 Kx=PSK      Au=PSK  Enc=AES(256)  Mac=SHA384
PSK-AES256-CBC-SHA      SSLv3 Kx=PSK      Au=PSK  Enc=AES(256)  Mac=SHA1
PSK-CAMELLIA256-SHA384  TLSv1 Kx=PSK      Au=PSK  Enc=Camellia(256) Mac=SHA384
ECDHE-PSK-AES128-CBC-SHA256 TLSv1 Kx=ECDHEPSK Au=PSK  Enc=AES(128)  Mac=SHA256
ECDHE-PSK-AES128-CBC-SHA TLSv1 Kx=ECDHEPSK Au=PSK  Enc=AES(128)  Mac=SHA1
SRP-DSS-AES-128-CBC-SHA SSLv3 Kx=SRP      Au=DSS  Enc=AES(128)  Mac=SHA1
SRP-RSA-AES-128-CBC-SHA SSLv3 Kx=SRP      Au=RSA  Enc=AES(128)  Mac=SHA1
SRP-AES-128-CBC-SHA     SSLv3 Kx=SRP      Au=SRP  Enc=AES(128)  Mac=SHA1
RSA-PSK-AES128-CBC-SHA256 TLSv1 Kx=RSAPSK   Au=RSA  Enc=AES(128)  Mac=SHA256
DHE-PSK-AES128-CBC-SHA256 TLSv1 Kx=DHEPSK   Au=PSK  Enc=AES(128)  Mac=SHA256
RSA-PSK-AES128-CBC-SHA  SSLv3 Kx=RSAPSK   Au=RSA  Enc=AES(128)  Mac=SHA1
DHE-PSK-AES128-CBC-SHA  SSLv3 Kx=DHEPSK   Au=PSK  Enc=AES(128)  Mac=SHA1
ECDHE-PSK-CAMELLIA128-SHA256 TLSv1 Kx=ECDHEPSK Au=PSK  Enc=Camellia(128) Mac=SHA256
RSA-PSK-CAMELLIA128-SHA256 TLSv1 Kx=RSAPSK   Au=RSA  Enc=Camellia(128) Mac=SHA256
DHE-PSK-CAMELLIA128-SHA256 TLSv1 Kx=DHEPSK   Au=PSK  Enc=Camellia(128) Mac=SHA256
AES128-SHA              SSLv3 Kx=RSA      Au=RSA  Enc=AES(128)  Mac=SHA1
SEED-SHA                SSLv3 Kx=RSA      Au=RSA  Enc=SEED(128) Mac=SHA1
CAMELLIA128-SHA         SSLv3 Kx=RSA      Au=RSA  Enc=Camellia(128) Mac=SHA1
PSK-AES128-CBC-SHA256   TLSv1 Kx=PSK      Au=PSK  Enc=AES(128)  Mac=SHA256
PSK-AES128-CBC-SHA      SSLv3 Kx=PSK      Au=PSK  Enc=AES(128)  Mac=SHA1
PSK-CAMELLIA128-SHA256  TLSv1 Kx=PSK      Au=PSK  Enc=Camellia(128) Mac=SHA256

サーバーが特定の暗号スイートに対応しているか調べる

オマケ。

以下で確認できるようですが、今回はメモとして。

$ echo | openssl s_client -connect [ホスト]:[ポート] -cipher [暗号スイート]

Vert.x Core、Vert.x Webで遊ぶ

これは、なにをしたくて書いたもの?

そろそろ、Vert.xも少し知っておいた方がいいのかなーと思いまして。

試してみようかなと。

Vert.xとは

Vert.xは、イベント駆動、ノンブロッキング、複数の言語による開発が可能なフレームワークです。

Eclipse Vert.x

複数の言語とは、Java、Kotlin、JavaScript、Groovy、RubyScalaを指します。

まだ若いフレームワークかというとそうでもなく、けっこう前から存在しています。

vert.x – Node.jsの代替フレームワーク

現在のバージョンは3.9系ですが、4が開発中です。

4ではJava、Kotlin、Groovyあたりがターゲットになるんでしょうかね?(コード例が減ってる)

Eclipse Vert.x

現在のバージョンのドキュメントは、こちらです。

Vert.x Documentation

とりあえず、始めてみましょうか。

環境

今回の環境は、こちらです。

$ java --version
openjdk 11.0.9.1 2020-11-04
OpenJDK Runtime Environment (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04)
OpenJDK 64-Bit Server VM (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.9.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-53-generic", arch: "amd64", family: "unix"

始めてみる

Vert.xには、Starterと呼ばれる初期プロジェクトを作るWebサイトがあります。

Vert.x Starter - Create new Eclipse Vert.x applications

サンプルはこちらのリポジトリにあります。

https://github.com/vert-x3/vertx-examples/tree/master

Webアプリケーションのサンプルは、こちら。

https://github.com/vert-x3/vertx-examples/tree/master/web-examples

で、どうしましょうというところですが、今回はStarterからプロジェクトを作って始めてみたいと思います。

まずは、プロジェクト用のディレクトリを作成。

$ mkdir hello-web
$ cd hello-web

Starterでプロジェクトを.tar.gzファイルで作成して、ダウンロードします。

$ curl -s -G https://start.vertx.io/starter.tar.gz \
   -d "groupId=org.littlewings" \
   -d "artifactId=hello-web" \
   -d "packageName=org.littlewings.vertx.web" \
   -d "vertxVersion=3.9.4" \
   -d "vertxDependencies=vertx-web" \
   -d "language=java" \
   -d "jdkVersion=11" \
   -d "buildTool=maven" \
   -o - | \
  tar -zxvf -

Starterのサイトにはcurlの使用例も載っていて、こちらは.zipで書かれていますが、拡張子を変更することで.tar.gz
することができます。

依存関係にはvertx-webを加え、Vert.xのバージョンは3.9.4で作成。

できあがったプロジェクトには、こんな感じのファイルが含まれています。

$ find -type f
./.editorconfig
./.mvn/wrapper/MavenWrapperDownloader.java
./.mvn/wrapper/maven-wrapper.jar
./.mvn/wrapper/maven-wrapper.properties
./pom.xml
./README.adoc
./.gitignore
./mvnw
./mvnw.cmd
./src/main/java/org/littlewings/vertx/web/MainVerticle.java
./src/test/java/org/littlewings/vertx/web/TestMainVerticle.java

少し、代表的なファイルを見てみましょう。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.littlewings</groupId>
  <artifactId>hello-web</artifactId>
  <version>1.0.0-SNAPSHOT</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
    <maven-shade-plugin.version>2.4.3</maven-shade-plugin.version>
    <maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
    <exec-maven-plugin.version>1.5.0</exec-maven-plugin.version>

    <vertx.version>3.9.4</vertx.version>
    <junit-jupiter.version>5.4.0</junit-jupiter.version>

    <main.verticle>org.littlewings.vertx.web.MainVerticle</main.verticle>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-stack-depchain</artifactId>
        <version>${vertx.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-web</artifactId>
    </dependency>

    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-junit5</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>${junit-jupiter.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>${junit-jupiter.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>${maven-compiler-plugin.version}</version>
        <configuration>
          <release>11</release>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-shade-plugin</artifactId>
        <version>${maven-shade-plugin.version}</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <transformers>
                <transformer
                  implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <manifestEntries>
                    <Main-Class>io.vertx.core.Launcher</Main-Class>
                    <Main-Verticle>${main.verticle}</Main-Verticle>
                  </manifestEntries>
                </transformer>
                <transformer
                  implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                  <resource>META-INF/services/io.vertx.core.spi.VerticleFactory</resource>
                </transformer>
              </transformers>
              <artifactSet>
              </artifactSet>
              <outputFile>${project.build.directory}/${project.artifactId}-${project.version}-fat.jar
              </outputFile>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>${maven-surefire-plugin.version}</version>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>${exec-maven-plugin.version}</version>
        <configuration>
          <mainClass>io.vertx.core.Launcher</mainClass>
          <arguments>
            <argument>run</argument>
            <argument>${main.verticle}</argument>
          </arguments>
        </configuration>
      </plugin>
    </plugins>
  </build>


</project>

Vert.xに関するバージョン全体はBOMで指定され

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-stack-depchain</artifactId>
        <version>${vertx.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

依存関係には、指定したvertx-webが含まれています。

    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-web</artifactId>
    </dependency>

Maven Shade Pluginで、単一のJARを作れるようにも構成されています。

      <plugin>
        <artifactId>maven-shade-plugin</artifactId>
        <version>${maven-shade-plugin.version}</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <transformers>
                <transformer
                  implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <manifestEntries>
                    <Main-Class>io.vertx.core.Launcher</Main-Class>
                    <Main-Verticle>${main.verticle}</Main-Verticle>
                  </manifestEntries>
                </transformer>
                <transformer
                  implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                  <resource>META-INF/services/io.vertx.core.spi.VerticleFactory</resource>
                </transformer>
              </transformers>
              <artifactSet>
              </artifactSet>
              <outputFile>${project.build.directory}/${project.artifactId}-${project.version}-fat.jar
              </outputFile>
            </configuration>
          </execution>
        </executions>
      </plugin>

起動クラスはVert.xのものを使い

                <transformer
                  implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <manifestEntries>
                    <Main-Class>io.vertx.core.Launcher</Main-Class>
                    <Main-Verticle>${main.verticle}</Main-Verticle>
                  </manifestEntries>
                </transformer>

できあがるJARファイルは、-fat.jarという名前になります、と。

              <outputFile>${project.build.directory}/${project.artifactId}-${project.version}-fat.jar
              </outputFile>

src/main/javaに含まれていたクラスは、こちら。
src/main/java/org/littlewings/vertx/web/MainVerticle.java

package org.littlewings.vertx.web;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;

public class MainVerticle extends AbstractVerticle {

  @Override
  public void start(Promise<Void> startPromise) throws Exception {
    vertx.createHttpServer().requestHandler(req -> {
      req.response()
        .putHeader("content-type", "text/plain")
        .end("Hello from Vert.x!");
    }).listen(8888, http -> {
      if (http.succeeded()) {
        startPromise.complete();
        System.out.println("HTTP server started on port 8888");
      } else {
        startPromise.fail(http.cause());
      }
    });
  }
}

Verticleってなんでしょう?

Vert.xのCoreのドキュメントに書かれていそうです。

Vert.x Core Manual - Vert.x

Vert.x Coreのドキュメントを読みつつ、作成したプロジェクトを動かしてみる

Vert.x Coreのドキュメントを、少し眺めてみます。

Vert.x Core Manual - Vert.x

Vert.x Coreは、次のような機能を提供します。

  • Writing TCP clients and servers
  • Writing HTTP clients and servers including support for WebSockets
  • The Event bus
  • Shared data - local maps and clustered distributed maps
  • Periodic and delayed actions
  • Deploying and undeploying Verticles
  • Datagram Sockets
  • DNS client
  • File system access
  • High availability
  • Native transports
  • Clustering

Vert.x Coreの機能はLow Levelなものとされていて、データベースアクセスや認証、High LevelのWeb機能などは含まれません。
そして、小さく軽量です、と。

Vert.x Coreは、今回作成したプロジェクトの場合、Vert.x Webからの推移的依存関係に含まれます。

mvn dependency:treeの結果を抜粋。

[INFO] +- io.vertx:vertx-web:jar:3.9.4:compile
[INFO] |  +- io.vertx:vertx-web-common:jar:3.9.4:compile
[INFO] |  +- io.vertx:vertx-auth-common:jar:3.9.4:compile
[INFO] |  +- io.vertx:vertx-bridge-common:jar:3.9.4:compile
[INFO] |  \- io.vertx:vertx-core:jar:3.9.4:compile
[INFO] |     +- io.netty:netty-common:jar:4.1.49.Final:compile
[INFO] |     +- io.netty:netty-buffer:jar:4.1.49.Final:compile
[INFO] |     +- io.netty:netty-transport:jar:4.1.49.Final:compile
[INFO] |     +- io.netty:netty-handler:jar:4.1.49.Final:compile
[INFO] |     |  \- io.netty:netty-codec:jar:4.1.49.Final:compile
[INFO] |     +- io.netty:netty-handler-proxy:jar:4.1.49.Final:compile
[INFO] |     |  \- io.netty:netty-codec-socks:jar:4.1.49.Final:compile
[INFO] |     +- io.netty:netty-codec-http:jar:4.1.49.Final:compile
[INFO] |     +- io.netty:netty-codec-http2:jar:4.1.49.Final:compile
[INFO] |     +- io.netty:netty-resolver:jar:4.1.49.Final:compile
[INFO] |     +- io.netty:netty-resolver-dns:jar:4.1.49.Final:compile
[INFO] |     |  \- io.netty:netty-codec-dns:jar:4.1.49.Final:compile
[INFO] |     +- com.fasterxml.jackson.core:jackson-core:jar:2.11.3:compile
[INFO] |     \- com.fasterxml.jackson.core:jackson-databind:jar:2.11.3:compile
[INFO] |        \- com.fasterxml.jackson.core:jackson-annotations:jar:2.11.3:compile

Vert.x Coreのドキュメントから、いくつか特徴や注意事項を抜き出します。

イベント駆動型なので、Vert.x側がアプリケーションを呼び出します。

Don’t call us, we’ll call you.

IOなどで、ブロックしてはいけません。

Don’t block me!

Vert.xは、イベント駆動型ではありますがシングルスレッドではなく、複数のイベントループを持ちます。

Reactor and Multi-Reactor

イベントループをブロックしないこと。

The Golden Rule - Don’t Block the Event Loop

JDBCなど、ブロックするAPIを使う場合は専用のAPI(別のスレッドプール)を介して呼び出すこと。

Running blocking code

複数の非同期処理結果を調整する場合。

Async coordination

とまあ、イベント駆動かつノンブロッキングな考えでアプリケーションを作るためのフレームワークであることが
あらためてわかります。

で、読み進めていくとVerticlesが出てきます。

Verticles

どうも、必ず使うものではないようです。

This model is entirely optional and Vert.x does not force you to create your applications in this way if you don’t want to..

Verticleは、Vert.xによってデプロイおよび実行される、コードのチャンクです。Actorモデルにおける、Actorに似たものと
考えるとよいそうな。

Vert.xインスタンスはデフォルトでCPUコア × 2のイベントループスレッドを保持し、この中で複数のVerticleインスタンス
含まれるように構成されるのだとか。また、異なるVerticleインスタンスはEvent Busを使ったメッセージ通信も可能です、と。

要するに、Vert.xにおけるデプロイの単位のように見えます。

とりあえず、今あるVerticleを動かしてみましょうか。

作成したプロジェクトをビルドしてみます。

$ mvn package -DskipTests=true

Fat JARを起動。

$ java -jar target/hello-web-1.0.0-SNAPSHOT-fat.jar
HTTP server started on port 8888
11月 15, 2020 9:35:13 午後 io.vertx.core.impl.launcher.commands.VertxIsolatedDeployer
情報: Succeeded in deploying verticle

よく見ると、Verticleをデプロイした、って書いてますね。

確認。

$ curl -i localhost:8888
HTTP/1.1 200 OK
content-type: text/plain
content-length: 18

Hello from Vert.x!

メッセージが返ってきました。Hello from〜はどこで出力しているのでしょう?

というわけで、先ほどのプログラムを見返します(抜粋)。

    vertx.createHttpServer().requestHandler(req -> {
      req.response()
        .putHeader("content-type", "text/plain")
        .end("Hello from Vert.x!");
    }).listen(8888, http -> {
      if (http.succeeded()) {
        startPromise.complete();
        System.out.println("HTTP server started on port 8888");
      } else {
        startPromise.fail(http.cause());
      }
    });

しっかり入っていますね。

ところで、この状態だとリクエストのパスに関わらずいつも同じ結果を返します。

$ curl -i localhost:8888/foo/bar
HTTP/1.1 200 OK
content-type: text/plain
content-length: 18

Hello from Vert.x!

どうやって起動しているか、今一度見返してみましょう。

Maven Shade Pluginでは、こんな設定が入っているんでした。

                <transformer
                  implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <manifestEntries>
                    <Main-Class>io.vertx.core.Launcher</Main-Class>
                    <Main-Verticle>${main.verticle}</Main-Verticle>
                  </manifestEntries>
                </transformer>

このあたりを少し追ってみます。

https://github.com/eclipse-vertx/vert.x/blob/3.9.4/src/main/java/io/vertx/core/Launcher.java

https://github.com/eclipse-vertx/vert.x/blob/3.9.4/src/main/java/io/vertx/core/impl/launcher/VertxCommandLauncher.java

https://github.com/eclipse-vertx/vert.x/blob/3.9.4/src/main/java/io/vertx/core/impl/launcher/commands/RunCommand.java

これらのクラス内で、指定されたVerticle(今回は自動生成されたもの)をデプロイしているようです。

また、デフォルトでrunというコマンドを実行しているようなので、Fat JARのヘルプを見てみます。

$ java -jar target/hello-web-1.0.0-SNAPSHOT-fat.jar --help
Usage: java -jar target/hello-web-1.0.0-SNAPSHOT-fat.jar [COMMAND] [OPTIONS]
            [arg...]

Commands:
    bare      Creates a bare instance of vert.x.
    list      List vert.x applications
    run       Runs a verticle called <main-verticle> in its own instance of
              vert.x.
    start     Start a vert.x application in background
    stop      Stop a vert.x application
    version   Displays the version.

Run 'java -jar target/hello-web-1.0.0-SNAPSHOT-fat.jar COMMAND --help' for more
information on a command.

他にもいくつかコマンドがありますね。コマンドの実装は、以下にあります。

https://github.com/eclipse-vertx/vert.x/tree/3.9.4/src/main/java/io/vertx/core/impl/launcher/commands

まあ、なんとなくわかってきたような…?

試しに、Verticleを使わずに、今のVerticleに近い処理を書いてみましょう。こんな感じでしょうか。
src/main/java/org/littlewings/vertx/web/App.java

package org.littlewings.vertx.web;

import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;

public class App {
  public static void main(String... args) {
    Vertx vertx = Vertx.vertx();
    HttpServer server = vertx.createHttpServer();

    server.requestHandler(req ->
      req
        .response()
        .putHeader("content-type", "text/plain")
        .end("Hello from Vert.x!")
    );

    server.listen(8888);

    System.out.println("HTTP server started on port 8888");
  }
}

これを直接起動して、curlで確認。

$ curl -i localhost:8888
HTTP/1.1 200 OK
content-type: text/plain
content-length: 18

Hello from Vert.x!

OKです。

なお、mvn exec:javaで実行しようとすると、生成されたpom.xmlはVerticleを実行するように構成されているので
この設定のまま-Dexec.mainClassを指定してもうまくいきません。

      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>${exec-maven-plugin.version}</version>
        <configuration>
          <mainClass>io.vertx.core.Launcher</mainClass>
          <arguments>
            <argument>run</argument>
            <argument>${main.verticle}</argument>
          </arguments>
        </configuration>
      </plugin>

ご注意を。

Vert.x Webを見る

現時点で少し使っていますが、Vert.x Webをもう少し使ってみます。

Vert.x-Web - Vert.x

コンセプトやルーティングに関するドキュメント。

Basic Vert.x-Web concepts

リクエストを処理するHandlerがあり、チェインが可能。

Handling requests and calling the next handler

If you don’t end the response in your handler, you should call next so another matching route can handle the request (if any).

ルーティングは固定のパス、前方一致、正規表現、HTTPメソッドを指定でき、パスにパラメーターを含めてキャプチャも
できます。

Routing by exact path

Routing by paths that begin with something

Capturing path parameters

Routing with regular expressions

Routing by HTTP method

HTTPボディを扱う場合。

Request body handling

では、このあたりを見つつ、自動生成されたVerticleをカスタマイズしていくとしましょう。

package org.littlewings.vertx.web;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;

public class MainVerticle extends AbstractVerticle {

  @Override
  public void start(Promise<Void> startPromise) throws Exception {

      // ここに処理を書く

  }
}

まずは、HttpServerの作成とRouterの取得。

    HttpServer server = vertx.createHttpServer();

    Router router = Router.router(vertx);

    // ここにHandlerの実装を書く

    server.requestHandler(router);

    server.listen(8888, http -> {
      if (http.succeeded()) {
        startPromise.complete();
        System.out.println("HTTP server started on port 8888");
      } else {
        startPromise.fail(http.cause());
      }
    });

ここまでを、固定の実装とします。

以降は、コメントの部分の実装を変更しつつ、以下のコマンドでJARの再作成およびプログラムの起動し直しを繰り返していると
思って読んでください。

$ mvn package -DskipTests=true && java -jar target/hello-web-1.0.0-SNAPSHOT-fat.jar

まずはRouterに特になにも設定せずHandlerを追加。

    router.route().handler(routingContext -> {
      HttpServerResponse response = routingContext.response();
      response
        .putHeader("content-type", "text/plain")
        .end("Hello from Vert.x!");
    });

自動生成された処理でも似たようなHandlerらしきものが設定されていましたが、今回はRoutingContextが渡されます。

自動生成されたコードでは、HttpServerRequestが渡されていました。なお、これはRoutingContextからも取得できます。

確認。

$ curl -i localhost:8888
HTTP/1.1 200 OK
content-type: text/plain
content-length: 18

Hello from Vert.x!

次に、Handlerを複数チェインさせてみましょう。1度Routeを取得して、ここにHandlerを足していきます。

    Route route = router.route();

    route.handler(routingContext -> {
      HttpServerResponse response = routingContext.response();

      response.putHeader("content-type", "text/plain");

      routingContext.next();
    });

    route.handler(routingContext -> {
      HttpServerResponse response = routingContext.response();

      response.end("Hello from Vert.x!");
    });

最初のHandlerRoutingContext#nextを呼び出しているところがポイントで、最後となるHandlerHttpServerResponse#end
呼び出す必要があります。

先ほどと動作は変わらないので、確認は省略。

次は、一気にパスの指定、QueryStringの取得、HTTPボディを扱うようにしてみましょう。

    Route getRoute = router.route("/echo").method(HttpMethod.GET);
    getRoute.handler(routingContext -> {
      HttpServerRequest request = routingContext.request();
      HttpServerResponse response = routingContext.response();

      String message = request.getParam("message");

      response.end(String.format("Server reply, [%s]", message));
    });

    Route postRoute = router.route("/json").method(HttpMethod.POST);
    postRoute.handler(BodyHandler.create());
    postRoute.handler(routingContext -> {
      String bodyAsString = routingContext.getBodyAsString();
      System.out.println(bodyAsString);

      JsonObject json = routingContext.getBodyAsJson();
      String message = json.getString("message");

      HttpServerResponse response = routingContext.response();

      response.end(String.format("Server reply, [%s]", message));
    });

Routeを取得する際にパスを指定し、またHTTPメソッドも指定します。

    Route getRoute = router.route("/echo").method(HttpMethod.GET);

QueryStringは、HttpServerRequest#getParamでパラメーターとして扱えるようです。

      String message = request.getParam("message");

HTTPボディを扱う場合は、1度BodyHandlerを追加して

    postRoute.handler(BodyHandler.create());

その後に、Handlerをチェインさせます。

    postRoute.handler(routingContext -> {
      String bodyAsString = routingContext.getBodyAsString();
      System.out.println(bodyAsString);

      JsonObject json = routingContext.getBodyAsJson();
      String message = json.getString("message");

      HttpServerResponse response = routingContext.response();

      response.end(String.format("Server reply, [%s]", message));
    });

今回は、HTTPボディを文字列として取得して標準出力に書き出しつつ、JSONとしても扱っています。

確認。

GETでQueryString。

$ curl -i 'localhost:8888/echo?message=Hello%20World!!'
HTTP/1.1 200 OK
content-length: 29

Server reply, [Hello World!!]

POSTでJSON送信。

$ curl -i localhost:8888/json -d '{"message": "Hello World!!"}'
HTTP/1.1 200 OK
content-length: 29

Server reply, [Hello World!!]

POSTの時は、サーバー側にこんな感じでHTTPボディの内容が出力されます。

{"message": "Hello World!!"}

OKですね。

まとめ

今回は、Vert.xのごくごく基本的な部分と、Vert.x Webを少し扱ってみました。

やや雰囲気がわかってきた感じがするので、これからちょっとずつ慣れていってみましょう。

最後に、今回いろいろ触ったVerticleの全体を載せておきます。
src/main/java/org/littlewings/vertx/web/MainVerticle.java

package org.littlewings.vertx.web;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;

public class MainVerticle extends AbstractVerticle {

  @Override
  public void start(Promise<Void> startPromise) throws Exception {
    HttpServer server = vertx.createHttpServer();

    Router router = Router.router(vertx);

    /*
    router.route().handler(routingContext -> {
      HttpServerResponse response = routingContext.response();
      response
        .putHeader("content-type", "text/plain")
        .end("Hello from Vert.x!");
    });
     */

    /*
    Route route = router.route();

    route.handler(routingContext -> {
      HttpServerResponse response = routingContext.response();

      response.putHeader("content-type", "text/plain");

      routingContext.next();
    });

    route.handler(routingContext -> {
      HttpServerResponse response = routingContext.response();

      response.end("Hello from Vert.x!");
    });
     */

    Route getRoute = router.route("/echo").method(HttpMethod.GET);
    getRoute.handler(routingContext -> {
      HttpServerRequest request = routingContext.request();
      HttpServerResponse response = routingContext.response();

      String message = request.getParam("message");

      response.end(String.format("Server reply, [%s]", message));
    });

    Route postRoute = router.route("/json").method(HttpMethod.POST);
    postRoute.handler(BodyHandler.create());
    postRoute.handler(routingContext -> {
      String bodyAsString = routingContext.getBodyAsString();
      System.out.println(bodyAsString);

      JsonObject json = routingContext.getBodyAsJson();
      String message = json.getString("message");

      HttpServerResponse response = routingContext.response();

      response.end(String.format("Server reply, [%s]", message));
    });

    server.requestHandler(router);

    server.listen(8888, http -> {
      if (http.succeeded()) {
        startPromise.complete();
        System.out.println("HTTP server started on port 8888");
      } else {
        startPromise.fail(http.cause());
      }
    });
  }
}