tipi 0.41 → 0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/test.yml +3 -1
- data/.gitignore +3 -1
- data/CHANGELOG.md +34 -0
- data/Gemfile +7 -1
- data/Gemfile.lock +53 -33
- data/README.md +184 -8
- data/Rakefile +1 -7
- data/benchmarks/bm_http1_parser.rb +85 -0
- data/bin/benchmark +37 -0
- data/bin/h1pd +6 -0
- data/bin/tipi +3 -21
- data/bm.png +0 -0
- data/df/agent.rb +1 -1
- data/df/sample_agent.rb +2 -2
- data/df/server.rb +3 -1
- data/df/server_utils.rb +48 -46
- data/examples/full_service.rb +13 -0
- data/examples/hello.rb +5 -0
- data/examples/hello.ru +3 -3
- data/examples/http1_parser.rb +10 -8
- data/examples/http_server.js +1 -1
- data/examples/http_server.rb +4 -1
- data/examples/http_server_graceful.rb +1 -1
- data/examples/https_server.rb +41 -15
- data/examples/rack_server_forked.rb +26 -0
- data/examples/rack_server_https_forked.rb +1 -1
- data/examples/servername_cb.rb +37 -0
- data/examples/websocket_demo.rb +1 -1
- data/lib/tipi/acme.rb +320 -0
- data/lib/tipi/cli.rb +93 -0
- data/lib/tipi/config_dsl.rb +13 -13
- data/lib/tipi/configuration.rb +2 -2
- data/lib/tipi/controller/bare_polyphony.rb +0 -0
- data/lib/tipi/controller/bare_stock.rb +10 -0
- data/lib/tipi/controller/extensions.rb +37 -0
- data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
- data/lib/tipi/controller/web_polyphony.rb +353 -0
- data/lib/tipi/controller/web_stock.rb +635 -0
- data/lib/tipi/controller.rb +12 -0
- data/lib/tipi/digital_fabric/agent.rb +5 -5
- data/lib/tipi/digital_fabric/agent_proxy.rb +15 -8
- data/lib/tipi/digital_fabric/executive.rb +7 -3
- data/lib/tipi/digital_fabric/protocol.rb +3 -3
- data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
- data/lib/tipi/digital_fabric/service.rb +17 -18
- data/lib/tipi/handler.rb +2 -2
- data/lib/tipi/http1_adapter.rb +85 -124
- data/lib/tipi/http2_adapter.rb +29 -16
- data/lib/tipi/http2_stream.rb +52 -57
- data/lib/tipi/rack_adapter.rb +2 -2
- data/lib/tipi/response_extensions.rb +1 -1
- data/lib/tipi/supervisor.rb +75 -0
- data/lib/tipi/version.rb +1 -1
- data/lib/tipi/websocket.rb +3 -3
- data/lib/tipi.rb +9 -7
- data/test/coverage.rb +2 -2
- data/test/helper.rb +60 -12
- data/test/test_http_server.rb +14 -41
- data/test/test_request.rb +2 -29
- data/tipi.gemspec +10 -10
- metadata +80 -54
- data/examples/automatic_certificate.rb +0 -193
- data/ext/tipi/extconf.rb +0 -12
- data/ext/tipi/http1_parser.c +0 -534
- data/ext/tipi/http1_parser.h +0 -18
- data/ext/tipi/tipi_ext.c +0 -5
- data/lib/tipi/http1_adapter_new.rb +0 -293
    
        data/df/server_utils.rb
    CHANGED
    
    | @@ -7,24 +7,7 @@ require 'tipi/digital_fabric/executive' | |
| 7 7 | 
             
            require 'json'
         | 
| 8 8 | 
             
            require 'fileutils'
         | 
| 9 9 | 
             
            require 'time'
         | 
| 10 | 
            -
             | 
| 11 | 
            -
            module ::Kernel
         | 
| 12 | 
            -
              def trace(*args)
         | 
| 13 | 
            -
                STDOUT.orig_write(format_trace(args))
         | 
| 14 | 
            -
              end
         | 
| 15 | 
            -
             | 
| 16 | 
            -
              def format_trace(args)
         | 
| 17 | 
            -
                if args.first.is_a?(String)
         | 
| 18 | 
            -
                  if args.size > 1
         | 
| 19 | 
            -
                    format("%s: %p\n", args.shift, args)
         | 
| 20 | 
            -
                  else
         | 
| 21 | 
            -
                    format("%s\n", args.first)
         | 
| 22 | 
            -
                  end
         | 
| 23 | 
            -
                else
         | 
| 24 | 
            -
                  format("%p\n", args.size == 1 ? args.first : args)
         | 
| 25 | 
            -
                end
         | 
| 26 | 
            -
              end
         | 
| 27 | 
            -
            end
         | 
| 10 | 
            +
            require 'polyphony/extensions/debug'
         | 
| 28 11 |  | 
| 29 12 | 
             
            FileUtils.cd(__dir__)
         | 
| 30 13 |  | 
| @@ -65,8 +48,10 @@ def listen_http | |
| 65 48 | 
             
                    # log("Done with HTTP connection", client: client)
         | 
| 66 49 | 
             
                    @service.decr_connection_count
         | 
| 67 50 | 
             
                  end
         | 
| 68 | 
            -
                rescue  | 
| 69 | 
            -
                   | 
| 51 | 
            +
                rescue Polyphony::BaseException
         | 
| 52 | 
            +
                  raise
         | 
| 53 | 
            +
                rescue Exception => e
         | 
| 54 | 
            +
                  log 'HTTP accept (unknown) error', error: e, backtrace: e.backtrace
         | 
| 70 55 | 
             
                end
         | 
| 71 56 | 
             
              end
         | 
| 72 57 | 
             
            end
         | 
| @@ -74,15 +59,19 @@ end | |
| 74 59 | 
             
            CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\n[^-]+-----END CERTIFICATE-----\n)/.freeze
         | 
| 75 60 |  | 
| 76 61 | 
             
            def listen_https
         | 
| 77 | 
            -
              spin( | 
| 62 | 
            +
              spin(:https_listener) do
         | 
| 78 63 | 
             
                private_key = OpenSSL::PKey::RSA.new IO.read('../../reality/ssl/privkey.pem')
         | 
| 79 64 | 
             
                c = IO.read('../../reality/ssl/cacert.pem')
         | 
| 80 65 | 
             
                certificates = c.scan(CERTIFICATE_REGEXP).map { |p|  OpenSSL::X509::Certificate.new(p.first) }
         | 
| 81 66 | 
             
                ctx = OpenSSL::SSL::SSLContext.new
         | 
| 67 | 
            +
                ctx.security_level = 0
         | 
| 82 68 | 
             
                cert = certificates.shift
         | 
| 83 69 | 
             
                log "SSL Certificate expires: #{cert.not_after.inspect}"
         | 
| 84 70 | 
             
                ctx.add_certificate(cert, private_key, certificates)
         | 
| 85 | 
            -
                ctx.ciphers = 'ECDH+aRSA'
         | 
| 71 | 
            +
                # ctx.ciphers = 'ECDH+aRSA'
         | 
| 72 | 
            +
                ctx.max_version = OpenSSL::SSL::TLS1_3_VERSION
         | 
| 73 | 
            +
                ctx.min_version = OpenSSL::SSL::SSL3_VERSION
         | 
| 74 | 
            +
                ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
         | 
| 86 75 |  | 
| 87 76 | 
             
                # TODO: further limit ciphers
         | 
| 88 77 | 
             
                # ref: https://github.com/socketry/falcon/blob/3ec805b3ceda0a764a2c5eb68cde33897b6a35ff/lib/falcon/environments/tls.rb
         | 
| @@ -99,7 +88,9 @@ def listen_https | |
| 99 88 | 
             
                server = Polyphony::Net.tcp_listen('0.0.0.0', 10443, opts)
         | 
| 100 89 | 
             
                id = 0
         | 
| 101 90 | 
             
                loop do
         | 
| 102 | 
            -
                  client = server.accept
         | 
| 91 | 
            +
                  client = server.accept rescue nil
         | 
| 92 | 
            +
                  next unless client
         | 
| 93 | 
            +
             | 
| 103 94 | 
             
                  # log('Accept HTTPS client connection', client: client)
         | 
| 104 95 | 
             
                  spin("https#{id += 1}") do
         | 
| 105 96 | 
             
                    @service.incr_connection_count
         | 
| @@ -110,10 +101,12 @@ def listen_https | |
| 110 101 | 
             
                    # log("Done with HTTP connection", client: client)
         | 
| 111 102 | 
             
                    @service.decr_connection_count
         | 
| 112 103 | 
             
                  end
         | 
| 113 | 
            -
                rescue OpenSSL::SSL::SSLError, SystemCallError, TypeError => e
         | 
| 114 | 
            -
             | 
| 115 | 
            -
                rescue  | 
| 116 | 
            -
                   | 
| 104 | 
            +
                # rescue OpenSSL::SSL::SSLError, SystemCallError, TypeError => e
         | 
| 105 | 
            +
                #   log('HTTPS accept error', error: e)
         | 
| 106 | 
            +
                rescue Polyphony::BaseException
         | 
| 107 | 
            +
                  raise
         | 
| 108 | 
            +
                rescue Exception => e
         | 
| 109 | 
            +
                  log 'HTTPS listener error: ', error: e, backtrace: e.backtrace
         | 
| 117 110 | 
             
                end
         | 
| 118 111 | 
             
              end
         | 
| 119 112 | 
             
            end
         | 
| @@ -126,13 +119,16 @@ def listen_unix | |
| 126 119 | 
             
                socket = UNIXServer.new(UNIX_SOCKET_PATH)
         | 
| 127 120 |  | 
| 128 121 | 
             
                id = 0
         | 
| 129 | 
            -
                 | 
| 122 | 
            +
                loop do
         | 
| 123 | 
            +
                  client = socket.accept
         | 
| 130 124 | 
             
                  # log('Accept Unix connection', client: client)
         | 
| 131 125 | 
             
                  spin("unix#{id += 1}") do
         | 
| 132 126 | 
             
                    Tipi.client_loop(client, {}) { |req| @service.http_request(req, true) }
         | 
| 133 127 | 
             
                  end
         | 
| 134 | 
            -
                rescue  | 
| 135 | 
            -
                   | 
| 128 | 
            +
                rescue Polyphony::BaseException
         | 
| 129 | 
            +
                  raise
         | 
| 130 | 
            +
                rescue Exception => e
         | 
| 131 | 
            +
                  log 'Unix accept error', error: e, backtrace: e.backtrace
         | 
| 136 132 | 
             
                end
         | 
| 137 133 | 
             
              end
         | 
| 138 134 | 
             
            end
         | 
| @@ -141,33 +137,39 @@ def listen_df | |
| 141 137 | 
             
              spin(:df_listener) do
         | 
| 142 138 | 
             
                opts = {
         | 
| 143 139 | 
             
                  reuse_addr:  true,
         | 
| 140 | 
            +
                  reuse_port:  true,
         | 
| 144 141 | 
             
                  dont_linger: true,
         | 
| 145 142 | 
             
                }
         | 
| 146 143 | 
             
                log('Listening for DF connections on localhost:4321')
         | 
| 147 144 | 
             
                server = Polyphony::Net.tcp_listen('0.0.0.0', 4321, opts)
         | 
| 148 145 |  | 
| 149 146 | 
             
                id = 0
         | 
| 150 | 
            -
                 | 
| 147 | 
            +
                loop do
         | 
| 148 | 
            +
                  client = server.accept
         | 
| 151 149 | 
             
                  # log('Accept DF connection', client: client)
         | 
| 152 150 | 
             
                  spin("df#{id += 1}") do
         | 
| 153 151 | 
             
                    Tipi.client_loop(client, {}) { |req| @service.http_request(req, true) }
         | 
| 154 152 | 
             
                  end
         | 
| 155 | 
            -
                rescue  | 
| 156 | 
            -
                   | 
| 153 | 
            +
                rescue Polyphony::BaseException
         | 
| 154 | 
            +
                  raise
         | 
| 155 | 
            +
                rescue Exception => e
         | 
| 156 | 
            +
                  log 'DF accept (unknown) error', error: e, backtrace: e.backtrace
         | 
| 157 157 | 
             
                end
         | 
| 158 158 | 
             
              end
         | 
| 159 159 | 
             
            end
         | 
| 160 160 |  | 
| 161 | 
            -
             | 
| 162 | 
            -
             | 
| 163 | 
            -
             | 
| 164 | 
            -
             | 
| 165 | 
            -
             | 
| 166 | 
            -
             | 
| 167 | 
            -
             | 
| 168 | 
            -
             | 
| 169 | 
            -
             | 
| 170 | 
            -
             | 
| 171 | 
            -
             | 
| 172 | 
            -
             | 
| 173 | 
            -
             | 
| 161 | 
            +
            if ENV['TRACE'] == '1'
         | 
| 162 | 
            +
              Thread.backend.trace_proc = proc do |event, fiber, value, pri|
         | 
| 163 | 
            +
                fiber_id = fiber.tag || fiber.inspect
         | 
| 164 | 
            +
                case event
         | 
| 165 | 
            +
                when :fiber_schedule
         | 
| 166 | 
            +
                  log format("=> %s %s %s %s", event, fiber_id, value.inspect, pri ? '(priority)' : '')
         | 
| 167 | 
            +
                when :fiber_run
         | 
| 168 | 
            +
                  log format("=> %s %s %s", event, fiber_id, value.inspect)
         | 
| 169 | 
            +
                when :fiber_create, :fiber_terminate
         | 
| 170 | 
            +
                  log format("=> %s %s", event, fiber_id)
         | 
| 171 | 
            +
                else
         | 
| 172 | 
            +
                  log format("=> %s", event)
         | 
| 173 | 
            +
                end
         | 
| 174 | 
            +
              end
         | 
| 175 | 
            +
            end
         | 
| @@ -0,0 +1,13 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'bundler/setup'
         | 
| 4 | 
            +
            require 'tipi'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            ::Exception.__disable_sanitized_backtrace__ = true
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            certificate_db_path = File.expand_path('certificate_store.db', __dir__)
         | 
| 9 | 
            +
            certificate_store = Tipi::ACME::SQLiteCertificateStore.new(certificate_db_path)
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            Tipi.full_service(
         | 
| 12 | 
            +
              certificate_store: certificate_store
         | 
| 13 | 
            +
            ) { |req| req.respond('Hello, world!') }
         | 
    
        data/examples/hello.rb
    ADDED
    
    
    
        data/examples/hello.ru
    CHANGED
    
    
    
        data/examples/http1_parser.rb
    CHANGED
    
    | @@ -31,23 +31,25 @@ f = spin do | |
| 31 31 | 
             
                break unless headers
         | 
| 32 32 | 
             
                trace headers
         | 
| 33 33 |  | 
| 34 | 
            -
                body = parser.read_body | 
| 34 | 
            +
                body = parser.read_body
         | 
| 35 35 | 
             
                trace "body: #{body ? body.bytesize : 0} bytes"
         | 
| 36 | 
            +
                trace body if body && body.bytesize < 80
         | 
| 36 37 | 
             
              end
         | 
| 37 38 | 
             
            end
         | 
| 38 39 |  | 
| 39 40 | 
             
            o << "GET /a HTTP/1.1\r\n\r\n"
         | 
| 40 | 
            -
             | 
| 41 | 
            +
             | 
| 42 | 
            +
            # o << "GET /a HTTP/1.1\r\nContent-Length: 0\r\n\r\n"
         | 
| 41 43 |  | 
| 42 44 | 
             
            # o << "GET / HTTP/1.1\r\nHost: localhost:10080\r\nUser-Agent: curl/7.74.0\r\nAccept: */*\r\n\r\n"
         | 
| 43 45 |  | 
| 44 | 
            -
             | 
| 46 | 
            +
            o << "post /?q=time&blah=blah HTTP/1\r\nTransfer-Encoding: chunked\r\n\r\na\r\nabcdefghij\r\n0\r\n\r\n"
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            data = " " * 4000000
         | 
| 49 | 
            +
            o << "get /?q=time HTTP/1.1\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
         | 
| 45 50 |  | 
| 46 | 
            -
             | 
| 47 | 
            -
            # o << "get /?q=time HTTP/1.1\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
         | 
| 51 | 
            +
            o << "get /?q=time HTTP/1.1\r\nCookie: foo\r\nCookie: bar\r\n\r\n"
         | 
| 48 52 |  | 
| 49 | 
            -
             | 
| 53 | 
            +
            o.close
         | 
| 50 54 |  | 
| 51 | 
            -
            # o.close
         | 
| 52 | 
            -
             
         | 
| 53 55 | 
             
            f.await
         | 
    
        data/examples/http_server.js
    CHANGED
    
    
    
        data/examples/http_server.rb
    CHANGED
    
    | @@ -14,7 +14,7 @@ puts 'Listening on port 10080...' | |
| 14 14 | 
             
            # GC.disable
         | 
| 15 15 | 
             
            # Thread.current.backend.idle_gc_period = 60
         | 
| 16 16 |  | 
| 17 | 
            -
            spin_loop(interval: 10) { p Thread. | 
| 17 | 
            +
            spin_loop(interval: 10) { p Thread.backend.stats }
         | 
| 18 18 |  | 
| 19 19 | 
             
            spin_loop(interval: 10) do
         | 
| 20 20 | 
             
              GC.compact
         | 
| @@ -29,6 +29,9 @@ spin do | |
| 29 29 | 
             
                  sleep 1
         | 
| 30 30 | 
             
                  req.send_chunk("bar\n")
         | 
| 31 31 | 
             
                  req.finish
         | 
| 32 | 
            +
                elsif req.path == '/upload'
         | 
| 33 | 
            +
                  body = req.read
         | 
| 34 | 
            +
                  req.respond("Body: #{body.inspect} (#{body.bytesize} bytes)")
         | 
| 32 35 | 
             
                else
         | 
| 33 36 | 
             
                  req.respond("Hello world!\n")
         | 
| 34 37 | 
             
                end
         | 
    
        data/examples/https_server.rb
    CHANGED
    
    | @@ -10,24 +10,50 @@ authority = Localhost::Authority.fetch | |
| 10 10 | 
             
            opts = {
         | 
| 11 11 | 
             
              reuse_addr:     true,
         | 
| 12 12 | 
             
              dont_linger:    true,
         | 
| 13 | 
            -
              secure_context: authority.server_context
         | 
| 14 13 | 
             
            }
         | 
| 15 14 |  | 
| 16 15 | 
             
            puts "pid: #{Process.pid}"
         | 
| 17 16 | 
             
            puts 'Listening on port 1234...'
         | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
                 | 
| 17 | 
            +
             | 
| 18 | 
            +
            ctx = authority.server_context
         | 
| 19 | 
            +
            server = Polyphony::Net.tcp_listen('0.0.0.0', 1234, opts)
         | 
| 20 | 
            +
            loop do
         | 
| 21 | 
            +
              socket = server.accept
         | 
| 22 | 
            +
              client = OpenSSL::SSL::SSLSocket.new(socket, ctx)
         | 
| 23 | 
            +
              client.sync_close = true
         | 
| 24 | 
            +
              spin do
         | 
| 25 | 
            +
                state = {}
         | 
| 26 | 
            +
                accept_thread = Thread.new do
         | 
| 27 | 
            +
                  puts "call client accept"
         | 
| 28 | 
            +
                  client.accept
         | 
| 29 | 
            +
                  state[:result] = :ok
         | 
| 30 | 
            +
                rescue Exception => e
         | 
| 31 | 
            +
                  puts error: e
         | 
| 32 | 
            +
                  state[:result] = e
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
                "wait for accept thread"
         | 
| 35 | 
            +
                accept_thread.join
         | 
| 36 | 
            +
                "accept thread done"
         | 
| 37 | 
            +
                if state[:result].is_a?(Exception)
         | 
| 38 | 
            +
                  puts "Exception in SSL handshake: #{state[:result].inspect}"
         | 
| 39 | 
            +
                  next
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
                Tipi.client_loop(client, opts) do |req|
         | 
| 42 | 
            +
                  p path: req.path
         | 
| 43 | 
            +
                  if req.path == '/stream'
         | 
| 44 | 
            +
                    req.send_headers('Foo' => 'Bar')
         | 
| 45 | 
            +
                    sleep 0.5
         | 
| 46 | 
            +
                    req.send_chunk("foo\n")
         | 
| 47 | 
            +
                    sleep 0.5
         | 
| 48 | 
            +
                    req.send_chunk("bar\n", done: true)
         | 
| 49 | 
            +
                  elsif req.path == '/upload'
         | 
| 50 | 
            +
                    body = req.read
         | 
| 51 | 
            +
                    req.respond("Body: #{body.inspect} (#{body.bytesize} bytes)")
         | 
| 52 | 
            +
                  else
         | 
| 53 | 
            +
                    req.respond("Hello world!\n")
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
              ensure
         | 
| 57 | 
            +
                client ? client.close : socket.close
         | 
| 28 58 | 
             
              end
         | 
| 29 | 
            -
              # req.send_headers
         | 
| 30 | 
            -
              # req.send_chunk("Method: #{req.method}\n")
         | 
| 31 | 
            -
              # req.send_chunk("Path: #{req.path}\n")
         | 
| 32 | 
            -
              # req.send_chunk("Query: #{req.query.inspect}\n", done: true)
         | 
| 33 59 | 
             
            end
         | 
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'bundler/setup'
         | 
| 4 | 
            +
            require 'tipi'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            app_path = ARGV.first || File.expand_path('./config.ru', __dir__)
         | 
| 7 | 
            +
            unless File.file?(app_path)
         | 
| 8 | 
            +
              STDERR.puts "Please provide rack config file (there are some in the examples directory.)"
         | 
| 9 | 
            +
              exit!
         | 
| 10 | 
            +
            end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            app = Tipi::RackAdapter.load(app_path)
         | 
| 13 | 
            +
            opts = { reuse_addr: true, dont_linger: true }
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            server = Tipi.listen('0.0.0.0', 1234, opts)
         | 
| 16 | 
            +
            puts 'listening on port 1234'
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            child_pids = []
         | 
| 19 | 
            +
            4.times do
         | 
| 20 | 
            +
              child_pids << Polyphony.fork do
         | 
| 21 | 
            +
                puts "forked pid: #{Process.pid}"
         | 
| 22 | 
            +
                server.each(&app)
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            child_pids.each { |pid| Thread.current.backend.waitpid(pid) }
         | 
| @@ -0,0 +1,37 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'openssl'
         | 
| 4 | 
            +
            require 'fiber'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            ctx = OpenSSL::SSL::SSLContext.new
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            f = Fiber.new { |peer| loop { p peer: peer; _name, peer = peer.transfer nil } }
         | 
| 9 | 
            +
            ctx.servername_cb = proc { |_socket, name|
         | 
| 10 | 
            +
              p servername_cb: name
         | 
| 11 | 
            +
              f.transfer([name, Fiber.current]).tap { |r| p result: r }
         | 
| 12 | 
            +
            }
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            socket = Socket.new(:INET, :STREAM).tap do |s|
         | 
| 15 | 
            +
              s.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
         | 
| 16 | 
            +
              s.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
         | 
| 17 | 
            +
              s.bind(Socket.sockaddr_in(12345, '0.0.0.0'))
         | 
| 18 | 
            +
              s.listen(Socket::SOMAXCONN)
         | 
| 19 | 
            +
            end
         | 
| 20 | 
            +
            server = OpenSSL::SSL::SSLServer.new(socket, ctx)
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            Thread.new do
         | 
| 23 | 
            +
              sleep 0.5
         | 
| 24 | 
            +
              socket = TCPSocket.new('127.0.0.1', 12345)
         | 
| 25 | 
            +
              client = OpenSSL::SSL::SSLSocket.new(socket)
         | 
| 26 | 
            +
              client.hostname = 'example.com'
         | 
| 27 | 
            +
              p client: client
         | 
| 28 | 
            +
              client.connect
         | 
| 29 | 
            +
            rescue => e
         | 
| 30 | 
            +
              p client_error: e
         | 
| 31 | 
            +
            end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            while true
         | 
| 34 | 
            +
              conn = server.accept
         | 
| 35 | 
            +
              p accepted: conn
         | 
| 36 | 
            +
              break
         | 
| 37 | 
            +
            end
         |