tttls1.3 0.3.0 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.rubocop.yml +3 -0
  4. data/.ruby-version +1 -1
  5. data/Gemfile +1 -0
  6. data/README.md +2 -2
  7. data/example/README.md +1 -1
  8. data/example/helper.rb +22 -0
  9. data/example/https_client.rb +4 -4
  10. data/example/https_client_using_0rtt.rb +6 -5
  11. data/example/https_client_using_ech.rb +5 -6
  12. data/example/https_client_using_grease_ech.rb +3 -5
  13. data/example/https_client_using_grease_psk.rb +8 -16
  14. data/example/https_client_using_hrr.rb +5 -4
  15. data/example/https_client_using_hrr_and_ech.rb +6 -6
  16. data/example/https_client_using_hrr_and_ticket.rb +5 -4
  17. data/example/https_client_using_status_request.rb +4 -5
  18. data/example/https_client_using_ticket.rb +5 -4
  19. data/example/https_server.rb +14 -1
  20. data/lib/tttls1.3/client.rb +205 -418
  21. data/lib/tttls1.3/connection.rb +21 -362
  22. data/lib/tttls1.3/ech.rb +410 -0
  23. data/lib/tttls1.3/endpoint.rb +276 -0
  24. data/lib/tttls1.3/message/certificate_verify.rb +1 -1
  25. data/lib/tttls1.3/message/extension/ech.rb +12 -10
  26. data/lib/tttls1.3/message/extension/signature_algorithms.rb +2 -2
  27. data/lib/tttls1.3/message/extension/supported_versions.rb +3 -3
  28. data/lib/tttls1.3/message/extension/unknown_extension.rb +2 -2
  29. data/lib/tttls1.3/server.rb +125 -63
  30. data/lib/tttls1.3/utils.rb +37 -0
  31. data/lib/tttls1.3/version.rb +1 -1
  32. data/lib/tttls1.3.rb +2 -1
  33. data/spec/client_spec.rb +21 -60
  34. data/spec/ech_spec.rb +39 -0
  35. data/spec/{connection_spec.rb → endpoint_spec.rb} +41 -49
  36. data/spec/server_spec.rb +12 -12
  37. data/tttls1.3.gemspec +1 -1
  38. metadata +8 -7
  39. data/lib/tttls1.3/hpke.rb +0 -91
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd89bebc90f5379d37e4fd3d1397168b6df9fcbfe5d1ad05f3ae852ba7d071d1
4
- data.tar.gz: 45372c096b46a5c37c9e05d22dfad8d53e24278d5d195b1b7305aa86b49bdfe9
3
+ metadata.gz: 7245602faa9087e83b3e47484aa88dc368b153e98c76fd00e36ebb1a863af6ef
4
+ data.tar.gz: c7362f39fc26763f712cdf61a29b3796686c50034d2a6431788d8dbf7dd505c9
5
5
  SHA512:
6
- metadata.gz: 6b79f03e7eff3d7f2e47e0ca715ee9559c8c2a04f3f6c4869b4c8b915905f8934bb6cf4dd776ae74bdc3e7d9c97dd3a8ee77e46298073bdffd70fc936a954421
7
- data.tar.gz: c43d3629de31d6ebd8f86d0c7a700d85a9f6e53be351eb13062c7ad0af9d1b8acc1c685f95a48e7d320b22ad1cd83979c4de3b57f07245060d6cc0dacdcca482
6
+ metadata.gz: 33168ef27007f9e6d73197ed3e1bae9b58dadfdf3042e732ddc6a05913db0aad48fb653ea7b7222716d70ebe42739f2d9dddee0ec75ee45eb33daba4be6f0229
7
+ data.tar.gz: d05c73a49e0f5d568785c3b6af3d9bc3c286b35f1110c5f31860f8cf788a9f9e019112fd8647394672c279808e20c4f02d438a58c44f5bab9af1d5fb851ef41f
@@ -13,14 +13,14 @@ jobs:
13
13
  runs-on: ubuntu-latest
14
14
  strategy:
15
15
  matrix:
16
- ruby-version: ['2.7.x', '3.0.x', '3.1.x']
16
+ ruby-version: ['3.1', '3.2', '3.3']
17
17
  steps:
18
18
  - uses: actions/checkout@v3
19
19
  - uses: docker://thekuwayama/openssl:latest
20
20
  - name: Set up Ruby
21
21
  uses: ruby/setup-ruby@v1
22
22
  with:
23
- ruby-version: ${{ matrix.ruby }}
23
+ ruby-version: ${{ matrix.ruby-version }}
24
24
  - name: Install dependencies
25
25
  run: |
26
26
  gem --version
data/.rubocop.yml CHANGED
@@ -1,6 +1,9 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.7
3
3
 
4
+ Gemspec/RequiredRubyVersion:
5
+ Enabled: false
6
+
4
7
  Style/ConditionalAssignment:
5
8
  Enabled: false
6
9
 
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.1.2
1
+ 3.2.2
data/Gemfile CHANGED
@@ -3,6 +3,7 @@
3
3
  source 'https://rubygems.org'
4
4
 
5
5
  gem 'ech_config', '~> 0.0.3'
6
+ gem 'hpke'
6
7
  gem 'logger'
7
8
  gem 'openssl'
8
9
  gem 'rake'
data/README.md CHANGED
@@ -104,8 +104,8 @@ tttls1.3 client is configurable using keyword arguments.
104
104
  | `:check_certificate_status` | Boolean | false | If needed to check certificate status, set true. |
105
105
  | `:process_certificate_status` | Proc | `TTTLS13::Client.method(:softfail_check_certificate_status)` | Proc(or Method) that checks received OCSPResponse. Its 3 arguments are OpenSSL::OCSP::Response, end-entity certificate(OpenSSL::X509::Certificate) and certificates chain(Array of Certificate) used for verification and it returns Boolean. |
106
106
  | `:compress_certificate_algorithms` | Array of TTTLS13::Message::Extension::CertificateCompressionAlgorithm constant | `ZLIB` | The compression algorithms are supported for compressing the Certificate message. |
107
- | `:ech_config` | ECHConfig | nil | ECHConfig to use ECH. If needed to use ECH, set TTTLS13::STANDARD\_CLIENT\_ECH_HPKE\_SYMMETRIC\_CIPHER\_SUITES, for example. See [ech_config](https://github.com/thekuwayama/ech_config). |
108
- | `:ech_hpke_cipher_suites` | Array of ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite | nil | If needed to use ECH, set client preference HPKE cipher suites. |
107
+ | `:ech_config` | ECHConfig | nil | ECHConfig to use ECH. See [ech_config](https://github.com/thekuwayama/ech_config). |
108
+ | `:ech_hpke_cipher_suites` | Array of ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite | nil | If needed to use ECH, set client preference HPKE cipher suites. For example, you can set TTTLS13::STANDARD\_CLIENT\_ECH_HPKE\_SYMMETRIC\_CIPHER\_SUITES. |
109
109
  | `:compatibility_mode` | Boolean | true | If needed to send ChangeCipherSpec, set true. |
110
110
  | `:sslkeylogfile` | String | nil | If needed to log SSLKEYLOGFILE, set the file path. |
111
111
  | `:loglevel` | Logger constant | Logger::WARN | If needed to print verbose, set Logger::DEBUG. |
data/example/README.md CHANGED
@@ -13,7 +13,7 @@ The examples run as follows:
13
13
  ```bash
14
14
  $ ruby https_client.rb
15
15
 
16
- $ ruby https_client.rb localhost:4433
16
+ $ ruby https_client.rb https://localhost:4433
17
17
  ```
18
18
 
19
19
  Note that `https_server.rb` requires PEM files of certificate and private key.
data/example/helper.rb CHANGED
@@ -4,6 +4,7 @@ $LOAD_PATH << __dir__ + '/../lib'
4
4
 
5
5
  require 'socket'
6
6
  require 'time'
7
+ require 'uri'
7
8
  require 'webrick'
8
9
 
9
10
  require 'http/parser'
@@ -61,3 +62,24 @@ def recv_http_response(client)
61
62
  parser << client.read until client.eof?
62
63
  buf
63
64
  end
65
+
66
+ def transcript_htmlize(transcript)
67
+ m = {
68
+ TTTLS13::CH1 => 'ClientHello',
69
+ TTTLS13::HRR => 'HelloRetryRequest',
70
+ TTTLS13::CH => 'ClientHello',
71
+ TTTLS13::SH => 'ServerHello',
72
+ TTTLS13::EE => 'EncryptedExtensions',
73
+ TTTLS13::CR => 'CertificateRequest',
74
+ TTTLS13::CT => 'Certificate',
75
+ TTTLS13::CV => 'CertificateVerify',
76
+ TTTLS13::SF => 'Finished',
77
+ TTTLS13::EOED => 'EndOfEarlyData',
78
+ TTTLS13::CCT => 'Certificate',
79
+ TTTLS13::CCV => 'CertificateVerify',
80
+ TTTLS13::CF => 'Finished'
81
+ }.map { |k, v| [k, '<details><summary>' + v + '</summary>%s</details>'] }.to_h
82
+ transcript.map do |k, v|
83
+ format(m[k], TTTLS13::Convert.obj2html(v.first))
84
+ end.join('<br>')
85
+ end
@@ -3,17 +3,17 @@
3
3
 
4
4
  require_relative 'helper'
5
5
 
6
- hostname, port = (ARGV[0] || 'localhost:4433').split(':')
6
+ uri = URI.parse(ARGV[0] || 'https://localhost:4433')
7
7
  ca_file = __dir__ + '/../tmp/ca.crt'
8
- req = simple_http_request(hostname)
8
+ req = simple_http_request(uri.host, uri.path)
9
9
 
10
- socket = TCPSocket.new(hostname, port)
10
+ socket = TCPSocket.new(uri.host, uri.port)
11
11
  settings = {
12
12
  ca_file: File.exist?(ca_file) ? ca_file : nil,
13
13
  alpn: ['http/1.1'],
14
14
  sslkeylogfile: '/tmp/sslkeylogfile.log'
15
15
  }
16
- client = TTTLS13::Client.new(socket, hostname, **settings)
16
+ client = TTTLS13::Client.new(socket, uri.host, **settings)
17
17
  client.connect
18
18
  client.write(req)
19
19
 
@@ -3,9 +3,9 @@
3
3
 
4
4
  require_relative 'helper'
5
5
 
6
- hostname, port = (ARGV[0] || 'localhost:4433').split(':')
6
+ uri = URI.parse(ARGV[0] || 'https://localhost:4433')
7
7
  ca_file = __dir__ + '/../tmp/ca.crt'
8
- req = simple_http_request(hostname)
8
+ req = simple_http_request(uri.host, uri.path)
9
9
 
10
10
  settings_2nd = {
11
11
  ca_file: File.exist?(ca_file) ? ca_file : nil,
@@ -15,7 +15,7 @@ process_new_session_ticket = lambda do |nst, rms, cs|
15
15
  return if Time.now.to_i - nst.timestamp > nst.ticket_lifetime
16
16
 
17
17
  settings_2nd[:ticket] = nst.ticket
18
- settings_2nd[:resumption_main_secret] = rms
18
+ settings_2nd[:resumption_secret] = rms
19
19
  settings_2nd[:psk_cipher_suite] = cs
20
20
  settings_2nd[:ticket_nonce] = nst.ticket_nonce
21
21
  settings_2nd[:ticket_age_add] = nst.ticket_age_add
@@ -35,14 +35,15 @@ succeed_early_data = false
35
35
  # Subsequent Handshake:
36
36
  settings_2nd
37
37
  ].each_with_index do |settings, i|
38
- socket = TCPSocket.new(hostname, port)
39
- client = TTTLS13::Client.new(socket, hostname, **settings)
38
+ socket = TCPSocket.new(uri.host, uri.port)
39
+ client = TTTLS13::Client.new(socket, uri.host, **settings)
40
40
 
41
41
  # send message using early data; 0-RTT
42
42
  client.early_data(req) if i == 1 && settings.include?(:ticket)
43
43
  client.connect
44
44
  # send message after Simple 1-RTT Handshake
45
45
  client.write(req) if i.zero? || !client.succeed_early_data?
46
+
46
47
  print recv_http_response(client)
47
48
  client.close unless client.eof?
48
49
  socket.close
@@ -5,16 +5,15 @@ require_relative 'helper'
5
5
  HpkeSymmetricCipherSuite = \
6
6
  ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
7
7
 
8
- hostname = 'crypto.cloudflare.com'
9
- port = 443
8
+ uri = URI.parse(ARGV[0] || 'https://localhost:4433')
10
9
  ca_file = __dir__ + '/../tmp/ca.crt'
11
- req = simple_http_request(hostname, '/cdn-cgi/trace')
10
+ req = simple_http_request(uri.host, uri.path)
12
11
 
13
12
  rr = Resolv::DNS.new.getresources(
14
- hostname,
13
+ uri.host,
15
14
  Resolv::DNS::Resource::IN::HTTPS
16
15
  )
17
- socket = TCPSocket.new(hostname, port)
16
+ socket = TCPSocket.new(uri.host, uri.port)
18
17
  settings = {
19
18
  ca_file: File.exist?(ca_file) ? ca_file : nil,
20
19
  alpn: ['http/1.1'],
@@ -23,7 +22,7 @@ settings = {
23
22
  TTTLS13::STANDARD_CLIENT_ECH_HPKE_SYMMETRIC_CIPHER_SUITES,
24
23
  sslkeylogfile: '/tmp/sslkeylogfile.log'
25
24
  }
26
- client = TTTLS13::Client.new(socket, hostname, **settings)
25
+ client = TTTLS13::Client.new(socket, uri.host, **settings)
27
26
  client.connect
28
27
  client.write(req)
29
28
 
@@ -5,11 +5,10 @@ require_relative 'helper'
5
5
  HpkeSymmetricCipherSuite = \
6
6
  ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
7
7
 
8
- hostname = 'crypto.cloudflare.com'
9
- port = 443
8
+ uri = URI.parse(ARGV[0] || 'https://localhost:4433')
10
9
  ca_file = __dir__ + '/../tmp/ca.crt'
11
10
 
12
- socket = TCPSocket.new(hostname, port)
11
+ socket = TCPSocket.new(uri.host, uri.port)
13
12
  settings = {
14
13
  ca_file: File.exist?(ca_file) ? ca_file : nil,
15
14
  alpn: ['http/1.1'],
@@ -17,10 +16,9 @@ settings = {
17
16
  TTTLS13::STANDARD_CLIENT_ECH_HPKE_SYMMETRIC_CIPHER_SUITES,
18
17
  sslkeylogfile: '/tmp/sslkeylogfile.log'
19
18
  }
20
- client = TTTLS13::Client.new(socket, hostname, **settings)
19
+ client = TTTLS13::Client.new(socket, uri.host, **settings)
21
20
  client.connect
22
21
 
23
22
  print client.retry_configs if client.rejected_ech?
24
-
25
23
  client.close unless client.eof?
26
24
  socket.close
@@ -5,13 +5,12 @@ require_relative 'helper'
5
5
  HpkeSymmetricCipherSuite = \
6
6
  ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
7
7
 
8
- hostname = 'crypto.cloudflare.com'
9
- port = 443
8
+ uri = URI.parse(ARGV[0] || 'https://localhost:4433')
10
9
  ca_file = __dir__ + '/../tmp/ca.crt'
11
- req = simple_http_request(hostname, '/cdn-cgi/trace')
10
+ req = simple_http_request(uri.host, uri.path)
12
11
 
13
12
  rr = Resolv::DNS.new.getresources(
14
- hostname,
13
+ uri.host,
15
14
  Resolv::DNS::Resource::IN::HTTPS
16
15
  )
17
16
  settings_2nd = {
@@ -37,16 +36,8 @@ settings_1st = {
37
36
  alpn: ['http/1.1'],
38
37
  process_new_session_ticket: process_new_session_ticket,
39
38
  ech_config: rr.first.svc_params['ech'].echconfiglist.first,
40
- ech_hpke_cipher_suites: [
41
- HpkeSymmetricCipherSuite.new(
42
- HpkeSymmetricCipherSuite::HpkeKdfId.new(
43
- TTTLS13::Hpke::KdfId::HKDF_SHA256
44
- ),
45
- HpkeSymmetricCipherSuite::HpkeAeadId.new(
46
- TTTLS13::Hpke::AeadId::AES_128_GCM
47
- )
48
- )
49
- ],
39
+ ech_hpke_cipher_suites:
40
+ TTTLS13::STANDARD_CLIENT_ECH_HPKE_SYMMETRIC_CIPHER_SUITES,
50
41
  sslkeylogfile: '/tmp/sslkeylogfile.log'
51
42
  }
52
43
 
@@ -56,10 +47,11 @@ settings_1st = {
56
47
  # Subsequent Handshake:
57
48
  settings_2nd
58
49
  ].each do |settings|
59
- socket = TCPSocket.new(hostname, port)
60
- client = TTTLS13::Client.new(socket, hostname, **settings)
50
+ socket = TCPSocket.new(uri.host, uri.port)
51
+ client = TTTLS13::Client.new(socket, uri.host, **settings)
61
52
  client.connect
62
53
  client.write(req)
54
+
63
55
  print recv_http_response(client)
64
56
  client.close unless client.eof?
65
57
  socket.close
@@ -3,19 +3,20 @@
3
3
 
4
4
  require_relative 'helper'
5
5
 
6
- hostname, port = (ARGV[0] || 'localhost:4433').split(':')
6
+ uri = URI.parse(ARGV[0] || 'https://localhost:4433')
7
7
  ca_file = __dir__ + '/../tmp/ca.crt'
8
- req = simple_http_request(hostname)
8
+ req = simple_http_request(uri.host, uri.path)
9
9
 
10
- socket = TCPSocket.new(hostname, port)
10
+ socket = TCPSocket.new(uri.host, uri.port)
11
11
  settings = {
12
12
  ca_file: File.exist?(ca_file) ? ca_file : nil,
13
13
  key_share_groups: [], # empty KeyShareClientHello.client_shares
14
14
  alpn: ['http/1.1']
15
15
  }
16
- client = TTTLS13::Client.new(socket, hostname, **settings)
16
+ client = TTTLS13::Client.new(socket, uri.host, **settings)
17
17
  client.connect
18
18
  client.write(req)
19
+
19
20
  print recv_http_response(client)
20
21
  client.close unless client.eof?
21
22
  socket.close
@@ -5,16 +5,15 @@ require_relative 'helper'
5
5
  HpkeSymmetricCipherSuite = \
6
6
  ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
7
7
 
8
- hostname = 'crypto.cloudflare.com'
9
- port = 443
8
+ uri = URI.parse(ARGV[0] || 'https://localhost:4433')
10
9
  ca_file = __dir__ + '/../tmp/ca.crt'
11
- req = simple_http_request(hostname, '/cdn-cgi/trace')
10
+ req = simple_http_request(uri.host, uri.path)
12
11
 
13
12
  rr = Resolv::DNS.new.getresources(
14
- hostname,
13
+ uri.host,
15
14
  Resolv::DNS::Resource::IN::HTTPS
16
15
  )
17
- socket = TCPSocket.new(hostname, port)
16
+ socket = TCPSocket.new(uri.host, uri.port)
18
17
  settings = {
19
18
  ca_file: File.exist?(ca_file) ? ca_file : nil,
20
19
  key_share_groups: [], # empty KeyShareClientHello.client_shares
@@ -24,9 +23,10 @@ settings = {
24
23
  TTTLS13::STANDARD_CLIENT_ECH_HPKE_SYMMETRIC_CIPHER_SUITES,
25
24
  sslkeylogfile: '/tmp/sslkeylogfile.log'
26
25
  }
27
- client = TTTLS13::Client.new(socket, hostname, **settings)
26
+ client = TTTLS13::Client.new(socket, uri.host, **settings)
28
27
  client.connect
29
28
  client.write(req)
29
+
30
30
  print recv_http_response(client)
31
31
  client.close unless client.eof?
32
32
  socket.close
@@ -3,9 +3,9 @@
3
3
 
4
4
  require_relative 'helper'
5
5
 
6
- hostname, port = (ARGV[0] || 'localhost:4433').split(':')
6
+ uri = URI.parse(ARGV[0] || 'https://localhost:4433')
7
7
  ca_file = __dir__ + '/../tmp/ca.crt'
8
- req = simple_http_request(hostname)
8
+ req = simple_http_request(uri.host, uri.path)
9
9
 
10
10
  settings_2nd = {
11
11
  ca_file: File.exist?(ca_file) ? ca_file : nil,
@@ -34,10 +34,11 @@ settings_1st = {
34
34
  # Subsequent Handshake:
35
35
  settings_2nd
36
36
  ].each do |settings|
37
- socket = TCPSocket.new(hostname, port)
38
- client = TTTLS13::Client.new(socket, hostname, **settings)
37
+ socket = TCPSocket.new(uri.host, uri.port)
38
+ client = TTTLS13::Client.new(socket, uri.host, **settings)
39
39
  client.connect
40
40
  client.write(req)
41
+
41
42
  print recv_http_response(client)
42
43
  client.close unless client.eof?
43
44
  socket.close
@@ -3,10 +3,11 @@
3
3
 
4
4
  require_relative 'helper'
5
5
 
6
- hostname, port = (ARGV[0] || 'localhost:4433').split(':')
6
+ uri = URI.parse(ARGV[0] || 'https://localhost:4433')
7
7
  ca_file = __dir__ + '/../tmp/ca.crt'
8
- req = simple_http_request(hostname)
8
+ req = simple_http_request(uri.host, uri.path)
9
9
 
10
+ socket = TCPSocket.new(uri.host, uri.port)
10
11
  process_certificate_status = lambda do |res, cert, chain|
11
12
  puts 'stapled OCSPResponse: '
12
13
  puts res.basic.status.pretty_inspect unless res.nil?
@@ -14,15 +15,13 @@ process_certificate_status = lambda do |res, cert, chain|
14
15
 
15
16
  TTTLS13::Client.softfail_check_certificate_status(res, cert, chain)
16
17
  end
17
-
18
- socket = TCPSocket.new(hostname, port)
19
18
  settings = {
20
19
  ca_file: File.exist?(ca_file) ? ca_file : nil,
21
20
  alpn: ['http/1.1'],
22
21
  check_certificate_status: true,
23
22
  process_certificate_status: process_certificate_status
24
23
  }
25
- client = TTTLS13::Client.new(socket, hostname, **settings)
24
+ client = TTTLS13::Client.new(socket, uri.host, **settings)
26
25
  client.connect
27
26
  client.write(req)
28
27
 
@@ -3,9 +3,9 @@
3
3
 
4
4
  require_relative 'helper'
5
5
 
6
- hostname, port = (ARGV[0] || 'localhost:4433').split(':')
6
+ uri = URI.parse(ARGV[0] || 'https://localhost:4433')
7
7
  ca_file = __dir__ + '/../tmp/ca.crt'
8
- req = simple_http_request(hostname)
8
+ req = simple_http_request(uri.host, uri.path)
9
9
 
10
10
  settings_2nd = {
11
11
  ca_file: File.exist?(ca_file) ? ca_file : nil,
@@ -33,10 +33,11 @@ settings_1st = {
33
33
  # Subsequent Handshake:
34
34
  settings_2nd
35
35
  ].each do |settings|
36
- socket = TCPSocket.new(hostname, port)
37
- client = TTTLS13::Client.new(socket, hostname, **settings)
36
+ socket = TCPSocket.new(uri.host, uri.port)
37
+ client = TTTLS13::Client.new(socket, uri.host, **settings)
38
38
  client.connect
39
39
  client.write(req)
40
+
40
41
  print recv_http_response(client)
41
42
  client.close unless client.eof?
42
43
  socket.close
@@ -30,7 +30,20 @@ Etc.nprocessors.times do
30
30
  parser.on_message_complete = lambda do
31
31
  if !parser.http_method.nil?
32
32
  logger.info 'Receive Request'
33
- server.write(simple_http_response('TEST'))
33
+ html = <<HTML
34
+ <!DOCTYPE html>
35
+ <html>
36
+ <head>
37
+ <meta charset="UTF-8" />
38
+ <title>tttls1.3 test server</title>
39
+ </head>
40
+ <body>
41
+ %s
42
+ </body>
43
+ </html>
44
+ HTML
45
+ html = format(html, transcript_htmlize(server.transcript))
46
+ server.write(simple_http_response(html))
34
47
  server.close
35
48
  else
36
49
  logger.warn 'Not Request'