tipi 0.40 → 0.45
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 +5 -1
- data/CHANGELOG.md +35 -0
- data/Gemfile +7 -1
- data/Gemfile.lock +55 -29
- data/README.md +184 -8
- data/Rakefile +1 -3
- 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 +16 -102
- data/df/server_utils.rb +175 -0
- data/examples/full_service.rb +13 -0
- data/examples/hello.rb +5 -0
- data/examples/http1_parser.rb +55 -0
- data/examples/http_server.js +1 -1
- data/examples/http_server.rb +15 -3
- data/examples/http_server_graceful.rb +1 -1
- data/examples/http_server_static.rb +6 -18
- 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 +315 -0
- data/lib/tipi/cli.rb +93 -0
- data/lib/tipi/config_dsl.rb +13 -13
- data/lib/tipi/configuration.rb +2 -2
- data/{e → lib/tipi/controller/bare_polyphony.rb} +0 -0
- data/lib/tipi/controller/bare_stock.rb +10 -0
- data/lib/tipi/controller/stock_http1_adapter.rb +15 -0
- data/lib/tipi/controller/web_polyphony.rb +351 -0
- data/lib/tipi/controller/web_stock.rb +631 -0
- data/lib/tipi/controller.rb +12 -0
- data/lib/tipi/digital_fabric/agent.rb +10 -8
- data/lib/tipi/digital_fabric/agent_proxy.rb +26 -12
- data/lib/tipi/digital_fabric/executive.rb +7 -3
- data/lib/tipi/digital_fabric/protocol.rb +19 -4
- data/lib/tipi/digital_fabric/request_adapter.rb +0 -4
- data/lib/tipi/digital_fabric/service.rb +84 -56
- data/lib/tipi/handler.rb +2 -2
- data/lib/tipi/http1_adapter.rb +86 -125
- data/lib/tipi/http2_adapter.rb +29 -16
- data/lib/tipi/http2_stream.rb +52 -56
- data/lib/tipi/rack_adapter.rb +2 -53
- data/lib/tipi/response_extensions.rb +2 -2
- 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 +8 -5
- 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 +12 -8
- metadata +88 -28
- data/examples/automatic_certificate.rb +0 -193
    
        data/lib/tipi/rack_adapter.rb
    CHANGED
    
    | @@ -4,69 +4,18 @@ require 'rack' | |
| 4 4 |  | 
| 5 5 | 
             
            module Tipi
         | 
| 6 6 | 
             
              module RackAdapter
         | 
| 7 | 
            -
                # Implements a rack input stream:
         | 
| 8 | 
            -
                # https://www.rubydoc.info/github/rack/rack/master/file/SPEC#label-The+Input+Stream
         | 
| 9 | 
            -
                class InputStream
         | 
| 10 | 
            -
                  def initialize(request)
         | 
| 11 | 
            -
                    @request = request
         | 
| 12 | 
            -
                  end
         | 
| 13 | 
            -
                  
         | 
| 14 | 
            -
                  def gets; end
         | 
| 15 | 
            -
                  
         | 
| 16 | 
            -
                  def read(length = nil, outbuf = nil); end
         | 
| 17 | 
            -
                  
         | 
| 18 | 
            -
                  def each(&block)
         | 
| 19 | 
            -
                    @request.each_chunk(&block)
         | 
| 20 | 
            -
                  end
         | 
| 21 | 
            -
                  
         | 
| 22 | 
            -
                  def rewind; end
         | 
| 23 | 
            -
                end
         | 
| 24 | 
            -
                
         | 
| 25 7 | 
             
                class << self
         | 
| 26 8 | 
             
                  def run(app)
         | 
| 27 9 | 
             
                    ->(req) { respond(req, app.(env(req))) }
         | 
| 28 10 | 
             
                  end
         | 
| 29 | 
            -
             | 
| 11 | 
            +
             | 
| 30 12 | 
             
                  def load(path)
         | 
| 31 13 | 
             
                    src = IO.read(path)
         | 
| 32 14 | 
             
                    instance_eval(src, path, 1)
         | 
| 33 15 | 
             
                  end
         | 
| 34 16 |  | 
| 35 | 
            -
                  RACK_ENV = {
         | 
| 36 | 
            -
                    'SCRIPT_NAME'                    => '',
         | 
| 37 | 
            -
                    'rack.version'                   => Rack::VERSION,
         | 
| 38 | 
            -
                    'SERVER_PORT'                    => '80', # ?
         | 
| 39 | 
            -
                    'rack.url_scheme'                => 'http', # ?
         | 
| 40 | 
            -
                    'rack.errors'                    => STDERR, # ?
         | 
| 41 | 
            -
                    'rack.multithread'               => false,
         | 
| 42 | 
            -
                    'rack.run_once'                  => false,
         | 
| 43 | 
            -
                    'rack.hijack?'                   => false,
         | 
| 44 | 
            -
                    'rack.hijack'                    => nil,
         | 
| 45 | 
            -
                    'rack.hijack_io'                 => nil,
         | 
| 46 | 
            -
                    'rack.session'                   => nil,
         | 
| 47 | 
            -
                    'rack.logger'                    => nil,
         | 
| 48 | 
            -
                    'rack.multipart.buffer_size'     => nil,
         | 
| 49 | 
            -
                    'rack.multipar.tempfile_factory' => nil
         | 
| 50 | 
            -
                  }
         | 
| 51 | 
            -
                  
         | 
| 52 17 | 
             
                  def env(request)
         | 
| 53 | 
            -
                     | 
| 54 | 
            -
                      h[k] = env_value_from_request(request, k)
         | 
| 55 | 
            -
                    end
         | 
| 56 | 
            -
                  end
         | 
| 57 | 
            -
             | 
| 58 | 
            -
                  HTTP_HEADER_RE = /^HTTP_(.+)$/.freeze
         | 
| 59 | 
            -
             | 
| 60 | 
            -
                  def env_value_from_request(request, key)
         | 
| 61 | 
            -
                    case key
         | 
| 62 | 
            -
                    when 'REQUEST_METHOD' then request.method
         | 
| 63 | 
            -
                    when 'PATH_INFO'      then request.path
         | 
| 64 | 
            -
                    when 'QUERY_STRING'   then request.query_string || ''
         | 
| 65 | 
            -
                    when 'SERVER_NAME'    then request.headers['host']
         | 
| 66 | 
            -
                    when 'rack.input'     then InputStream.new(request)
         | 
| 67 | 
            -
                    when HTTP_HEADER_RE   then request.headers[$1.downcase]
         | 
| 68 | 
            -
                    else                       RACK_ENV[key]
         | 
| 69 | 
            -
                    end
         | 
| 18 | 
            +
                    Qeweney.rack_env_from_request(request)
         | 
| 70 19 | 
             
                  end
         | 
| 71 20 |  | 
| 72 21 | 
             
                  def respond(request, (status_code, headers, body))
         | 
| @@ -8,8 +8,8 @@ module Tipi | |
| 8 8 |  | 
| 9 9 | 
             
                def serve_io(io, opts)
         | 
| 10 10 | 
             
                  if !opts[:stat] || opts[:stat].size >= SPLICE_CHUNKS_SIZE_THRESHOLD
         | 
| 11 | 
            -
                    @adapter.respond_from_io(self, io, opts[:headers])
         | 
| 12 | 
            -
                  else | 
| 11 | 
            +
                    @adapter.respond_from_io(self, io, opts[:headers], opts[:chunk_size] || 2**14)
         | 
| 12 | 
            +
                  else
         | 
| 13 13 | 
             
                    respond(io.read, opts[:headers] || {})
         | 
| 14 14 | 
             
                  end
         | 
| 15 15 | 
             
                end
         | 
| @@ -0,0 +1,75 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'polyphony'
         | 
| 4 | 
            +
            require 'json'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Tipi
         | 
| 7 | 
            +
              module Supervisor
         | 
| 8 | 
            +
                class << self
         | 
| 9 | 
            +
                  def run(opts)
         | 
| 10 | 
            +
                    puts "Start supervisor pid: #{Process.pid}"
         | 
| 11 | 
            +
                    @opts = opts
         | 
| 12 | 
            +
                    @controller_watcher = start_controller_watcher
         | 
| 13 | 
            +
                    supervise_loop
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def start_controller_watcher
         | 
| 17 | 
            +
                    spin do
         | 
| 18 | 
            +
                      cmd = controller_cmd
         | 
| 19 | 
            +
                      puts "Starting controller..."
         | 
| 20 | 
            +
                      pid = Kernel.spawn(*cmd)
         | 
| 21 | 
            +
                      @controller_pid = pid
         | 
| 22 | 
            +
                      puts "Controller pid: #{pid}"
         | 
| 23 | 
            +
                      _pid, status = Polyphony.backend_waitpid(pid)
         | 
| 24 | 
            +
                      puts "Controller has terminated with status: #{status.inspect}"
         | 
| 25 | 
            +
                      terminated = true
         | 
| 26 | 
            +
                    ensure
         | 
| 27 | 
            +
                      if pid && !terminated
         | 
| 28 | 
            +
                        puts "Terminate controller #{pid.inspect}"
         | 
| 29 | 
            +
                        Polyphony::Process.kill_process(pid)
         | 
| 30 | 
            +
                      end
         | 
| 31 | 
            +
                      Fiber.current.parent << pid
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def controller_cmd
         | 
| 36 | 
            +
                    [
         | 
| 37 | 
            +
                      'ruby',
         | 
| 38 | 
            +
                      File.join(__dir__, 'controller.rb'),
         | 
| 39 | 
            +
                      @opts.to_json
         | 
| 40 | 
            +
                    ]
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def supervise_loop
         | 
| 44 | 
            +
                    this_fiber = Fiber.current
         | 
| 45 | 
            +
                    trap('SIGUSR2') { this_fiber << :replace_controller }
         | 
| 46 | 
            +
                    loop do
         | 
| 47 | 
            +
                      case (msg = receive)
         | 
| 48 | 
            +
                      when :replace_controller
         | 
| 49 | 
            +
                        replace_controller
         | 
| 50 | 
            +
                      when Integer
         | 
| 51 | 
            +
                        pid = msg
         | 
| 52 | 
            +
                        if pid == @controller_pid
         | 
| 53 | 
            +
                          puts 'Detected dead controller. Restarting...'
         | 
| 54 | 
            +
                          exit!
         | 
| 55 | 
            +
                          @controller_watcher.restart
         | 
| 56 | 
            +
                        end
         | 
| 57 | 
            +
                      else
         | 
| 58 | 
            +
                        raise "Invalid message received: #{msg.inspect}"
         | 
| 59 | 
            +
                      end
         | 
| 60 | 
            +
                    end
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  def replace_controller
         | 
| 64 | 
            +
                    puts "Replacing controller"
         | 
| 65 | 
            +
                    old_watcher = @controller_watcher
         | 
| 66 | 
            +
                    @controller_watcher = start_controller_watcher
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    # TODO: we'll want to get some kind of signal from the new controller once it's ready
         | 
| 69 | 
            +
                    sleep 1
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    old_watcher.terminate(true)
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
            end
         | 
    
        data/lib/tipi/version.rb
    CHANGED
    
    
    
        data/lib/tipi/websocket.rb
    CHANGED
    
    | @@ -20,12 +20,12 @@ module Tipi | |
| 20 20 | 
             
                  @version = headers['sec-websocket-version'].to_i
         | 
| 21 21 | 
             
                  @reader = ::WebSocket::Frame::Incoming::Server.new(version: @version)
         | 
| 22 22 | 
             
                end
         | 
| 23 | 
            -
             | 
| 23 | 
            +
             | 
| 24 24 | 
             
                def recv
         | 
| 25 25 | 
             
                  if (msg = @reader.next)
         | 
| 26 26 | 
             
                    return msg.to_s
         | 
| 27 27 | 
             
                  end
         | 
| 28 | 
            -
             | 
| 28 | 
            +
             | 
| 29 29 | 
             
                  @conn.recv_loop do |data|
         | 
| 30 30 | 
             
                    @reader << data
         | 
| 31 31 | 
             
                    if (msg = @reader.next)
         | 
| @@ -48,7 +48,7 @@ module Tipi | |
| 48 48 | 
             
                    end
         | 
| 49 49 | 
             
                  end
         | 
| 50 50 | 
             
                end
         | 
| 51 | 
            -
             | 
| 51 | 
            +
             | 
| 52 52 | 
             
                OutgoingFrame = ::WebSocket::Frame::Outgoing::Server
         | 
| 53 53 |  | 
| 54 54 | 
             
                def send(data)
         | 
    
        data/lib/tipi.rb
    CHANGED
    
    | @@ -1,10 +1,13 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            require 'polyphony'
         | 
| 4 | 
            +
             | 
| 4 5 | 
             
            require_relative './tipi/http1_adapter'
         | 
| 5 6 | 
             
            require_relative './tipi/http2_adapter'
         | 
| 6 7 | 
             
            require_relative './tipi/configuration'
         | 
| 7 8 | 
             
            require_relative './tipi/response_extensions'
         | 
| 9 | 
            +
            require_relative './tipi/acme'
         | 
| 10 | 
            +
             | 
| 8 11 | 
             
            require 'qeweney/request'
         | 
| 9 12 |  | 
| 10 13 | 
             
            class Qeweney::Request
         | 
| @@ -14,7 +17,7 @@ end | |
| 14 17 | 
             
            module Tipi
         | 
| 15 18 | 
             
              ALPN_PROTOCOLS = %w[h2 http/1.1].freeze
         | 
| 16 19 | 
             
              H2_PROTOCOL = 'h2'
         | 
| 17 | 
            -
             | 
| 20 | 
            +
             | 
| 18 21 | 
             
              class << self
         | 
| 19 22 | 
             
                def serve(host, port, opts = {}, &handler)
         | 
| 20 23 | 
             
                  opts[:alpn_protocols] = ALPN_PROTOCOLS
         | 
| @@ -23,7 +26,7 @@ module Tipi | |
| 23 26 | 
             
                ensure
         | 
| 24 27 | 
             
                  server&.close
         | 
| 25 28 | 
             
                end
         | 
| 26 | 
            -
             | 
| 29 | 
            +
             | 
| 27 30 | 
             
                def listen(host, port, opts = {})
         | 
| 28 31 | 
             
                  opts[:alpn_protocols] = ALPN_PROTOCOLS
         | 
| 29 32 | 
             
                  Polyphony::Net.tcp_listen(host, port, opts).tap do |socket|
         | 
| @@ -32,7 +35,7 @@ module Tipi | |
| 32 35 | 
             
                    end
         | 
| 33 36 | 
             
                  end
         | 
| 34 37 | 
             
                end
         | 
| 35 | 
            -
             | 
| 38 | 
            +
             | 
| 36 39 | 
             
                def accept_loop(server, opts, &handler)
         | 
| 37 40 | 
             
                  server.accept_loop do |client|
         | 
| 38 41 | 
             
                    spin { client_loop(client, opts, &handler) }
         | 
| @@ -40,7 +43,7 @@ module Tipi | |
| 40 43 | 
             
                    # disregard
         | 
| 41 44 | 
             
                  end
         | 
| 42 45 | 
             
                end
         | 
| 43 | 
            -
             | 
| 46 | 
            +
             | 
| 44 47 | 
             
                def client_loop(client, opts, &handler)
         | 
| 45 48 | 
             
                  client.no_delay if client.respond_to?(:no_delay)
         | 
| 46 49 | 
             
                  adapter = protocol_adapter(client, opts)
         | 
| @@ -48,7 +51,7 @@ module Tipi | |
| 48 51 | 
             
                ensure
         | 
| 49 52 | 
             
                  client.close rescue nil
         | 
| 50 53 | 
             
                end
         | 
| 51 | 
            -
             | 
| 54 | 
            +
             | 
| 52 55 | 
             
                def protocol_adapter(socket, opts)
         | 
| 53 56 | 
             
                  use_http2 = socket.respond_to?(:alpn_protocol) &&
         | 
| 54 57 | 
             
                              socket.alpn_protocol == H2_PROTOCOL
         | 
    
        data/test/coverage.rb
    CHANGED
    
    | @@ -26,10 +26,10 @@ module Coverage | |
| 26 26 | 
             
                  @result = {}
         | 
| 27 27 | 
             
                  trace = TracePoint.new(:line) do |tp|
         | 
| 28 28 | 
             
                    next if tp.path =~ /\(/
         | 
| 29 | 
            -
             | 
| 29 | 
            +
             | 
| 30 30 | 
             
                    absolute = File.expand_path(tp.path)
         | 
| 31 31 | 
             
                    next unless LIB_FILES.include?(absolute)# =~ /^#{LIB_DIR}/
         | 
| 32 | 
            -
             | 
| 32 | 
            +
             | 
| 33 33 | 
             
                    @result[absolute] ||= relevant_lines_for_filename(absolute)
         | 
| 34 34 | 
             
                    @result[absolute][tp.lineno - 1] = 1
         | 
| 35 35 | 
             
                  end
         | 
    
        data/test/helper.rb
    CHANGED
    
    | @@ -8,32 +8,29 @@ require_relative './eg' | |
| 8 8 | 
             
            require_relative './coverage' if ENV['COVERAGE']
         | 
| 9 9 |  | 
| 10 10 | 
             
            require 'minitest/autorun'
         | 
| 11 | 
            -
            require 'minitest/reporters'
         | 
| 12 11 |  | 
| 13 12 | 
             
            require 'polyphony'
         | 
| 14 13 |  | 
| 15 14 | 
             
            ::Exception.__disable_sanitized_backtrace__ = true
         | 
| 16 15 |  | 
| 17 | 
            -
            Minitest::Reporters.use! [
         | 
| 18 | 
            -
              Minitest::Reporters::SpecReporter.new
         | 
| 19 | 
            -
            ]
         | 
| 20 | 
            -
             | 
| 21 16 | 
             
            class MiniTest::Test
         | 
| 22 17 | 
             
              def setup
         | 
| 23 | 
            -
                #  | 
| 24 | 
            -
                if Fiber.current.children.size > 0
         | 
| 25 | 
            -
                  puts "Children left: #{Fiber.current.children.inspect}"
         | 
| 26 | 
            -
                  exit!
         | 
| 27 | 
            -
                end
         | 
| 18 | 
            +
                # trace "* setup #{self.name}"
         | 
| 28 19 | 
             
                Fiber.current.setup_main_fiber
         | 
| 29 20 | 
             
                Fiber.current.instance_variable_set(:@auto_watcher, nil)
         | 
| 21 | 
            +
                Thread.current.backend.finalize
         | 
| 30 22 | 
             
                Thread.current.backend = Polyphony::Backend.new
         | 
| 31 23 | 
             
                sleep 0
         | 
| 32 24 | 
             
              end
         | 
| 33 25 |  | 
| 34 26 | 
             
              def teardown
         | 
| 35 | 
            -
                #  | 
| 27 | 
            +
                # trace "* teardown #{self.name}"
         | 
| 36 28 | 
             
                Fiber.current.shutdown_all_children
         | 
| 29 | 
            +
                if Fiber.current.children.size > 0
         | 
| 30 | 
            +
                  puts "Children left after #{self.name}: #{Fiber.current.children.inspect}"
         | 
| 31 | 
            +
                  exit!
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
                Fiber.current.instance_variable_set(:@auto_watcher, nil)
         | 
| 37 34 | 
             
              rescue => e
         | 
| 38 35 | 
             
                puts e
         | 
| 39 36 | 
             
                puts e.backtrace.join("\n")
         | 
| @@ -47,4 +44,55 @@ module Kernel | |
| 47 44 | 
             
              rescue Exception => e
         | 
| 48 45 | 
             
                e
         | 
| 49 46 | 
             
              end
         | 
| 50 | 
            -
             | 
| 47 | 
            +
             | 
| 48 | 
            +
              def trace(*args)
         | 
| 49 | 
            +
                STDOUT.orig_write(format_trace(args))
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
              def format_trace(args)
         | 
| 53 | 
            +
                if args.first.is_a?(String)
         | 
| 54 | 
            +
                  if args.size > 1
         | 
| 55 | 
            +
                    format("%s: %p\n", args.shift, args)
         | 
| 56 | 
            +
                  else
         | 
| 57 | 
            +
                    format("%s\n", args.first)
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
                else
         | 
| 60 | 
            +
                  format("%p\n", args.size == 1 ? args.first : args)
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
            end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
            class IO
         | 
| 66 | 
            +
              # Creates two mockup sockets for simulating server-client communication
         | 
| 67 | 
            +
              def self.server_client_mockup
         | 
| 68 | 
            +
                server_in, client_out = IO.pipe
         | 
| 69 | 
            +
                client_in, server_out = IO.pipe
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                server_connection = mockup_connection(server_in, server_out, client_out)
         | 
| 72 | 
            +
                client_connection = mockup_connection(client_in, client_out, server_out)
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                [server_connection, client_connection]
         | 
| 75 | 
            +
              end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
              def self.mockup_connection(input, output, output2)
         | 
| 78 | 
            +
                eg(
         | 
| 79 | 
            +
                  __parser_read_method__: ->() { :readpartial },
         | 
| 80 | 
            +
                  read:         ->(*args) { input.read(*args) },
         | 
| 81 | 
            +
                  read_loop:    ->(*args, &block) { input.read_loop(*args, &block) },
         | 
| 82 | 
            +
                  recv_loop:    ->(*args, &block) { input.read_loop(*args, &block) },
         | 
| 83 | 
            +
                  readpartial:  ->(*args) { input.readpartial(*args) },
         | 
| 84 | 
            +
                  recv:         ->(*args) { input.readpartial(*args) },
         | 
| 85 | 
            +
                  '<<':         ->(*args) { output.write(*args) },
         | 
| 86 | 
            +
                  write:        ->(*args) { output.write(*args) },
         | 
| 87 | 
            +
                  close:        -> { output.close },
         | 
| 88 | 
            +
                  eof?:         -> { output2.closed? }
         | 
| 89 | 
            +
                )
         | 
| 90 | 
            +
              end
         | 
| 91 | 
            +
            end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
            module Minitest::Assertions
         | 
| 94 | 
            +
              def assert_in_range exp_range, act
         | 
| 95 | 
            +
                msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
         | 
| 96 | 
            +
                assert exp_range.include?(act), msg
         | 
| 97 | 
            +
              end
         | 
| 98 | 
            +
            end
         | 
    
        data/test/test_http_server.rb
    CHANGED
    
    | @@ -4,38 +4,11 @@ require_relative 'helper' | |
| 4 4 | 
             
            require 'tipi'
         | 
| 5 5 |  | 
| 6 6 | 
             
            class String
         | 
| 7 | 
            -
              def  | 
| 7 | 
            +
              def crlf_lines
         | 
| 8 8 | 
             
                gsub "\n", "\r\n"
         | 
| 9 9 | 
             
              end
         | 
| 10 10 | 
             
            end
         | 
| 11 11 |  | 
| 12 | 
            -
            class IO
         | 
| 13 | 
            -
              # Creates two mockup sockets for simulating server-client communication
         | 
| 14 | 
            -
              def self.server_client_mockup
         | 
| 15 | 
            -
                server_in, client_out = IO.pipe
         | 
| 16 | 
            -
                client_in, server_out = IO.pipe
         | 
| 17 | 
            -
             | 
| 18 | 
            -
                server_connection = mockup_connection(server_in, server_out, client_out)
         | 
| 19 | 
            -
                client_connection = mockup_connection(client_in, client_out, server_out)
         | 
| 20 | 
            -
             | 
| 21 | 
            -
                [server_connection, client_connection]
         | 
| 22 | 
            -
              end
         | 
| 23 | 
            -
             | 
| 24 | 
            -
              def self.mockup_connection(input, output, output2)
         | 
| 25 | 
            -
                eg(
         | 
| 26 | 
            -
                  :read        => ->(*args) { input.read(*args) },
         | 
| 27 | 
            -
                  :read_loop   => ->(*args, &block) { input.read_loop(*args, &block) },
         | 
| 28 | 
            -
                  :recv_loop   => ->(*args, &block) { input.read_loop(*args, &block) },
         | 
| 29 | 
            -
                  :readpartial => ->(*args) { input.readpartial(*args) },
         | 
| 30 | 
            -
                  :recv        => ->(*args) { input.readpartial(*args) },
         | 
| 31 | 
            -
                  :<<          => ->(*args) { output.write(*args) },
         | 
| 32 | 
            -
                  :write       => ->(*args) { output.write(*args) },
         | 
| 33 | 
            -
                  :close       => -> { output.close },
         | 
| 34 | 
            -
                  :eof?        => -> { output2.closed? }
         | 
| 35 | 
            -
                )
         | 
| 36 | 
            -
              end
         | 
| 37 | 
            -
            end
         | 
| 38 | 
            -
             | 
| 39 12 | 
             
            class HTTP1ServerTest < MiniTest::Test
         | 
| 40 13 | 
             
              def teardown
         | 
| 41 14 | 
             
                @server&.interrupt if @server&.alive?
         | 
| @@ -60,7 +33,7 @@ class HTTP1ServerTest < MiniTest::Test | |
| 60 33 | 
             
                connection << "GET / HTTP/1.0\r\n\r\n"
         | 
| 61 34 |  | 
| 62 35 | 
             
                response = connection.readpartial(8192)
         | 
| 63 | 
            -
                expected = <<~HTTP.chomp. | 
| 36 | 
            +
                expected = <<~HTTP.chomp.crlf_lines.chomp
         | 
| 64 37 | 
             
                  HTTP/1.1 200
         | 
| 65 38 | 
             
                  Content-Length: 13
         | 
| 66 39 |  | 
| @@ -78,7 +51,7 @@ class HTTP1ServerTest < MiniTest::Test | |
| 78 51 | 
             
                connection << "GET / HTTP/1.1\r\n\r\n"
         | 
| 79 52 |  | 
| 80 53 | 
             
                response = connection.readpartial(8192)
         | 
| 81 | 
            -
                expected = <<~HTTP. | 
| 54 | 
            +
                expected = <<~HTTP.crlf_lines.chomp
         | 
| 82 55 | 
             
                  HTTP/1.1 200
         | 
| 83 56 | 
             
                  Content-Length: 13
         | 
| 84 57 |  | 
| @@ -100,7 +73,7 @@ class HTTP1ServerTest < MiniTest::Test | |
| 100 73 | 
             
                connection << "GET / HTTP/1.1\r\n\r\n"
         | 
| 101 74 | 
             
                response = connection.readpartial(8192)
         | 
| 102 75 | 
             
                assert !connection.eof?
         | 
| 103 | 
            -
                expected = <<~HTTP. | 
| 76 | 
            +
                expected = <<~HTTP.crlf_lines.chomp
         | 
| 104 77 | 
             
                  HTTP/1.1 200
         | 
| 105 78 | 
             
                  Content-Length: 2
         | 
| 106 79 |  | 
| @@ -127,7 +100,7 @@ class HTTP1ServerTest < MiniTest::Test | |
| 127 100 | 
             
                sleep 0.01
         | 
| 128 101 | 
             
                response = connection.readpartial(8192)
         | 
| 129 102 |  | 
| 130 | 
            -
                expected = <<~HTTP. | 
| 103 | 
            +
                expected = <<~HTTP.crlf_lines.chomp
         | 
| 131 104 | 
             
                  HTTP/1.1 200
         | 
| 132 105 | 
             
                  Content-Length: 13
         | 
| 133 106 |  | 
| @@ -152,7 +125,7 @@ class HTTP1ServerTest < MiniTest::Test | |
| 152 125 | 
             
                  req.finish
         | 
| 153 126 | 
             
                end
         | 
| 154 127 |  | 
| 155 | 
            -
                connection << <<~HTTP. | 
| 128 | 
            +
                connection << <<~HTTP.crlf_lines
         | 
| 156 129 | 
             
                  POST / HTTP/1.1
         | 
| 157 130 | 
             
                  Transfer-Encoding: chunked
         | 
| 158 131 |  | 
| @@ -178,7 +151,7 @@ class HTTP1ServerTest < MiniTest::Test | |
| 178 151 |  | 
| 179 152 | 
             
                response = connection.readpartial(8192)
         | 
| 180 153 |  | 
| 181 | 
            -
                expected = <<~HTTP. | 
| 154 | 
            +
                expected = <<~HTTP.crlf_lines
         | 
| 182 155 | 
             
                  HTTP/1.1 200
         | 
| 183 156 | 
             
                  Transfer-Encoding: chunked
         | 
| 184 157 |  | 
| @@ -199,7 +172,7 @@ class HTTP1ServerTest < MiniTest::Test | |
| 199 172 | 
             
                  upgrade: {
         | 
| 200 173 | 
             
                    echo: lambda do |adapter, _headers|
         | 
| 201 174 | 
             
                      conn = adapter.conn
         | 
| 202 | 
            -
                      conn << <<~HTTP. | 
| 175 | 
            +
                      conn << <<~HTTP.crlf_lines
         | 
| 203 176 | 
             
                        HTTP/1.1 101 Switching Protocols
         | 
| 204 177 | 
             
                        Upgrade: echo
         | 
| 205 178 | 
             
                        Connection: Upgrade
         | 
| @@ -219,7 +192,7 @@ class HTTP1ServerTest < MiniTest::Test | |
| 219 192 | 
             
                connection << "GET / HTTP/1.1\r\n\r\n"
         | 
| 220 193 | 
             
                response = connection.readpartial(8192)
         | 
| 221 194 | 
             
                assert !connection.eof?
         | 
| 222 | 
            -
                expected = <<~HTTP. | 
| 195 | 
            +
                expected = <<~HTTP.crlf_lines.chomp
         | 
| 223 196 | 
             
                  HTTP/1.1 200
         | 
| 224 197 | 
             
                  Content-Length: 2
         | 
| 225 198 |  | 
| @@ -227,7 +200,7 @@ class HTTP1ServerTest < MiniTest::Test | |
| 227 200 | 
             
                HTTP
         | 
| 228 201 | 
             
                assert_equal(expected, response)
         | 
| 229 202 |  | 
| 230 | 
            -
                connection << <<~HTTP. | 
| 203 | 
            +
                connection << <<~HTTP.crlf_lines
         | 
| 231 204 | 
             
                  GET / HTTP/1.1
         | 
| 232 205 | 
             
                  Upgrade: echo
         | 
| 233 206 | 
             
                  Connection: upgrade
         | 
| @@ -237,7 +210,7 @@ class HTTP1ServerTest < MiniTest::Test | |
| 237 210 | 
             
                snooze
         | 
| 238 211 | 
             
                response = connection.readpartial(8192)
         | 
| 239 212 | 
             
                assert !connection.eof?
         | 
| 240 | 
            -
                expected = <<~HTTP. | 
| 213 | 
            +
                expected = <<~HTTP.crlf_lines
         | 
| 241 214 | 
             
                  HTTP/1.1 101 Switching Protocols
         | 
| 242 215 | 
             
                  Upgrade: echo
         | 
| 243 216 | 
             
                  Connection: Upgrade
         | 
| @@ -255,7 +228,7 @@ class HTTP1ServerTest < MiniTest::Test | |
| 255 228 |  | 
| 256 229 | 
             
                connection.close
         | 
| 257 230 | 
             
                assert !done
         | 
| 258 | 
            -
             | 
| 231 | 
            +
             | 
| 259 232 | 
             
                sleep 0.01
         | 
| 260 233 | 
             
                assert done
         | 
| 261 234 | 
             
              end
         | 
| @@ -278,7 +251,7 @@ class HTTP1ServerTest < MiniTest::Test | |
| 278 251 | 
             
                count = 0
         | 
| 279 252 |  | 
| 280 253 | 
             
                connection << "GET / HTTP/1.1\r\n\r\n"
         | 
| 281 | 
            -
             | 
| 254 | 
            +
             | 
| 282 255 | 
             
                while (data = connection.read(chunk_size))
         | 
| 283 256 | 
             
                  response << data
         | 
| 284 257 | 
             
                  count += 1
         | 
| @@ -286,7 +259,7 @@ class HTTP1ServerTest < MiniTest::Test | |
| 286 259 | 
             
                end
         | 
| 287 260 |  | 
| 288 261 | 
             
                chunks = "#{chunk_size.to_s(16)}\n#{'*' * chunk_size}\n" * chunk_count
         | 
| 289 | 
            -
                expected = <<~HTTP. | 
| 262 | 
            +
                expected = <<~HTTP.crlf_lines
         | 
| 290 263 | 
             
                  HTTP/1.1 200
         | 
| 291 264 | 
             
                  Transfer-Encoding: chunked
         | 
| 292 265 |  | 
    
        data/test/test_request.rb
    CHANGED
    
    | @@ -9,33 +9,6 @@ class String | |
| 9 9 | 
             
              end
         | 
| 10 10 | 
             
            end
         | 
| 11 11 |  | 
| 12 | 
            -
            class IO
         | 
| 13 | 
            -
              # Creates two mockup sockets for simulating server-client communication
         | 
| 14 | 
            -
              def self.server_client_mockup
         | 
| 15 | 
            -
                server_in, client_out = IO.pipe
         | 
| 16 | 
            -
                client_in, server_out = IO.pipe
         | 
| 17 | 
            -
             | 
| 18 | 
            -
                server_connection = mockup_connection(server_in, server_out, client_out)
         | 
| 19 | 
            -
                client_connection = mockup_connection(client_in, client_out, server_out)
         | 
| 20 | 
            -
             | 
| 21 | 
            -
                [server_connection, client_connection]
         | 
| 22 | 
            -
              end
         | 
| 23 | 
            -
             | 
| 24 | 
            -
              def self.mockup_connection(input, output, output2)
         | 
| 25 | 
            -
                eg(
         | 
| 26 | 
            -
                  :read        => ->(*args) { input.read(*args) },
         | 
| 27 | 
            -
                  :read_loop   => ->(*args, &block) { input.read_loop(*args, &block) },
         | 
| 28 | 
            -
                  :recv_loop   => ->(*args, &block) { input.read_loop(*args, &block) },
         | 
| 29 | 
            -
                  :readpartial => ->(*args) { input.readpartial(*args) },
         | 
| 30 | 
            -
                  :recv        => ->(*args) { input.readpartial(*args) },
         | 
| 31 | 
            -
                  :<<          => ->(*args) { output.write(*args) },
         | 
| 32 | 
            -
                  :write       => ->(*args) { output.write(*args) },
         | 
| 33 | 
            -
                  :close       => -> { output.close },
         | 
| 34 | 
            -
                  :eof?        => -> { output2.closed? }
         | 
| 35 | 
            -
                )
         | 
| 36 | 
            -
              end
         | 
| 37 | 
            -
            end
         | 
| 38 | 
            -
             | 
| 39 12 | 
             
            class RequestHeadersTest < MiniTest::Test
         | 
| 40 13 | 
             
              def teardown
         | 
| 41 14 | 
             
                @server&.interrupt if @server&.alive?
         | 
| @@ -65,8 +38,8 @@ class RequestHeadersTest < MiniTest::Test | |
| 65 38 | 
             
                assert_kind_of Qeweney::Request, req
         | 
| 66 39 | 
             
                assert_equal 'blah.com', req.headers['host']
         | 
| 67 40 | 
             
                assert_equal 'bar', req.headers['foo']
         | 
| 68 | 
            -
                assert_equal ['1', ' | 
| 69 | 
            -
                assert_equal ' | 
| 41 | 
            +
                assert_equal ['1', '2', '3'], req.headers['hi']
         | 
| 42 | 
            +
                assert_equal 'GET', req.headers[':method']
         | 
| 70 43 | 
             
                assert_equal '/titi', req.headers[':path']
         | 
| 71 44 | 
             
              end
         | 
| 72 45 |  | 
    
        data/tipi.gemspec
    CHANGED
    
    | @@ -19,21 +19,25 @@ Gem::Specification.new do |s| | |
| 19 19 |  | 
| 20 20 | 
             
              s.executables   = ['tipi']
         | 
| 21 21 |  | 
| 22 | 
            -
              s.add_runtime_dependency      'polyphony',          '~>0. | 
| 23 | 
            -
              s.add_runtime_dependency      ' | 
| 24 | 
            -
              
         | 
| 25 | 
            -
              s.add_runtime_dependency      ' | 
| 26 | 
            -
              s.add_runtime_dependency      ' | 
| 22 | 
            +
              s.add_runtime_dependency      'polyphony',          '~>0.71'
         | 
| 23 | 
            +
              s.add_runtime_dependency      'ever',               '~>0.1'
         | 
| 24 | 
            +
              s.add_runtime_dependency      'qeweney',            '~>0.14'
         | 
| 25 | 
            +
              s.add_runtime_dependency      'extralite',          '~>1.2'
         | 
| 26 | 
            +
              s.add_runtime_dependency      'h1p',                '~>0.2'
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              s.add_runtime_dependency      'http-2',             '~>0.11'
         | 
| 27 29 | 
             
              s.add_runtime_dependency      'rack',               '>=2.0.8', '<2.3.0'
         | 
| 28 30 | 
             
              s.add_runtime_dependency      'websocket',          '~>1.2.8'
         | 
| 29 31 | 
             
              s.add_runtime_dependency      'acme-client',        '~>2.0.8'
         | 
| 32 | 
            +
              s.add_runtime_dependency      'localhost',          '~>1.1.4'
         | 
| 30 33 |  | 
| 31 34 | 
             
              # for digital fabric
         | 
| 32 35 | 
             
              s.add_runtime_dependency      'msgpack',            '~>1.4.2'
         | 
| 33 36 |  | 
| 34 | 
            -
              s.add_development_dependency  'rake',               '~> | 
| 35 | 
            -
              s.add_development_dependency  'localhost',          '~>1.1.4'
         | 
| 37 | 
            +
              s.add_development_dependency  'rake',               '~>13.0.6'
         | 
| 36 38 | 
             
              s.add_development_dependency  'minitest',           '~>5.11.3'
         | 
| 37 | 
            -
              s.add_development_dependency  'minitest-reporters', '~>1.4.2'
         | 
| 38 39 | 
             
              s.add_development_dependency  'simplecov',          '~>0.17.1'
         | 
| 40 | 
            +
              s.add_development_dependency  'memory_profiler',    '~>1.0.0'
         | 
| 41 | 
            +
             | 
| 42 | 
            +
              s.add_development_dependency  'cuba',               '~>3.9.3'
         | 
| 39 43 | 
             
            end
         |