[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: noteflakes\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Tests\n\non: [push, pull_request]\n\njobs:\n  build:\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, macos-latest]\n        ruby: ['3.2', '3.3', '3.4', 'head']\n\n    name: >-\n      ${{matrix.os}}, ${{matrix.ruby}}\n\n    runs-on: ${{matrix.os}}\n\n    # env:\n    #   POLYPHONY_LIBEV: \"1\"\n\n    steps:\n    - name: Setup machine\n      uses: actions/checkout@v1\n    - name: Setup Ruby\n      uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: ${{matrix.ruby}}\n        bundler-cache: true # 'bundle install' and cache\n        cache-version: 2\n    - name: Run tests\n      run:  bundle exec rake test\n"
  },
  {
    "path": ".gitignore",
    "content": "*.gem\n*.rbc\n/.config\n/coverage/\n/InstalledFiles\n/pkg/\n/spec/reports/\n/spec/examples.txt\n/test/tmp/\n/test/version_tmp/\n/tmp/\n\n# Used by dotenv library to load environment variables.\n# .env\n\n# Ignore Byebug command history file.\n.byebug_history\n\n## Specific to RubyMotion:\n.dat*\n.repl_history\nbuild/\n*.bridgesupport\nbuild-iPhoneOS/\nbuild-iPhoneSimulator/\n\n## Specific to RubyMotion (use of CocoaPods):\n#\n# We recommend against adding the Pods directory to your .gitignore. However\n# you should judge for yourself, the pros and cons are mentioned at:\n# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control\n#\n# vendor/Pods/\n\n## Documentation cache and generated files:\n/.yardoc/\n/_yardoc/\n/doc/\n/rdoc/\n\n## Environment normalization:\n/.bundle/\n/vendor/bundle\n/lib/bundler/man/\n\n# for a library or gem, you might want to ignore these files since the code is\n# intended to run in multiple environments; otherwise, check them in:\n# Gemfile.lock\n# .ruby-version\n# .ruby-gemset\n\n# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:\n.rvmrc\n\n# Used by RuboCop. Remote config files pulled in from inherit_from directive.\n# .rubocop-https?--*\nlog\nlog.*\n\nlib/tipi_ext*\nexamples/certificate_store.db\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## 0.56 2025-10-21\n\n- Update localhost, acme-client, websocket, http-2, extralite, qeweney, rack, bundler dependencies (#35)\n- Drop support for Ruby 3.1 (#35)\n- Add support for Ruby 3.3 and 3.4 (#35)\n\n\n## 0.55 2023-07-29\n\n- Simplify HTTP/1 exception handling\n- Update H1P dependency\n- Update Extralite dependency (#28)\n\n## 0.54 2023-05-28\n\n- Use `H1P.send_response` for sending response\n- Update Polyphony and H1P versions\n\n## 0.53 2022-10-04\n\n- Disregard `SystemCallError` in `Tipi.client_loop`\n\n## 0.52 2022-03-03\n\n- Treat HTTP/2 headers as immutable\n\n## 0.51 2022-02-28\n\n- Update dependencies\n\n## 0.50 2022-02-10\n\n- Update Qeweney\n\n## 0.49 2022-02-07\n\n- Update Polyphony\n\n## 0.48 2022-02-04\n\n- Update dependencies\n- Fix variable name in `Tipi.verify_path` (#16) - thanks @dm1try\n\n## 0.47 2202-02-03\n\n- Update H1P dependency\n\n## 0.46 2022-02-01\n\n- Allow setting valid hosts\n- Change interface of Qeweney apps to use #run (#15)\n- Close server listener before terminating connections\n\n## 0.45 2021-10-25\n\n- Remove `http_parser.rb` dependency (#14) - thanks @SwagDevOps\n- Use `argv` argument in `Tipi.opts_from_argv` (#13) - thanks @SwagDevOps\n- Ignore `ArgumentError` in `#parse_headers`\n\n## 0.44 2021-09-29\n\n- Implement compatibility mode for HTTP/1 (WIP)\n- Add option parsing for CLI tool\n- Implement supervisor-controller-worker model in CLI tool\n\n## 0.43 2021-08-20\n\n- Extract HTTP/1 parser into a separate gem:\n  [H1P](https://github.com/digital-fabric/h1p)\n\n## 0.42 2021-08-16\n\n- HTTP/1 parser:  disable UTF-8 parsing for all but header values\n- Add support for parsing HTTP/1 from callable source\n- Introduce full_service API for automatic HTTPS\n- Introduce automatic SSL certificate provisioning\n- Improve handling of exceptions\n- Various fixes to DF service and agent pxoy\n- Fix upgrading to HTTP2 with a request body\n- Switch to new HTTP/1 parser\n\n## 0.41 2021-07-26\n\n- Fix Rack adapter (#11)\n- Introduce experimental HTTP/1 parser\n- More work on DF server\n- Allow setting chunk size in `#respond_from_io`\n\n## 0.40 2021-06-24\n\n- Implement serving static files using splice_chunks (nice performance boost for\n  files bigger than 1M)\n- Call shutdown before closing socket\n- Fix examples (thanks @timhatch!)\n\n## 0.39 2021-06-20\n\n- More work on DF server\n- Fix HTTP2StreamHandler#send_headers\n- Various fixes to HTTP/2 adapter\n- Fix host detection for HTTP/2 connections\n- Fix HTTP/1 adapter #respond with nil body\n- Fix HTTP1Adapter#send_headers\n\n## 0.38 2021-03-09\n\n- Don't use chunked transfer encoding for non-streaming responses\n\n## 0.37.2 2021-03-08\n\n- Fix header formatting when header value is an array\n\n## 0.37 2021-02-15\n\n- Update upgrade mechanism to work with updated Qeweney API\n\n## 0.36 2021-02-12\n\n- Use `Qeweney::Status` constants\n\n## 0.35 2021-02-10\n\n- Extract Request class into separate [qeweney](https://github.com/digital-fabric/qeweney) gem\n\n## 0.34 2021-02-07\n\n- Implement digital fabric service and agents\n- Add multipart and urlencoded form data parsing\n- Improve request body reading behaviour\n- Add more `Request` information methods\n- Add access to connection for HTTP2 requests\n- Allow calling `Request#send_chunk` with empty chunk\n- Add support for handling protocol upgrades from within request handler\n\n## 0.33 2020-11-20\n\n- Update code for Polyphony 0.47.5\n- Add support for Rack::File body to Tipi::RackAdapter\n\n## 0.32 2020-08-14\n\n- Respond with array of strings instead of concatenating for HTTP 1\n- Use read_loop instead of readpartial\n- Fix http upgrade test\n\n## 0.31 2020-07-28\n\n- Fix websocket server code\n- Implement configuration layer (WIP)\n- Improve performance of rack adapter\n\n## 0.30 2020-07-15\n\n- Rename project to Tipi\n- Rearrange source code\n- Remove HTTP client code (to be developed eventually into a separate gem)\n- Fix header rendering in rack adapter (#2)\n\n## 0.29 2020-07-06\n\n- Use IO#read_loop\n\n## 0.28 2020-07-03\n\n- Update with API changes from Polyphony >= 0.41\n\n## 0.27 2020-04-14\n\n- Remove modulation dependency\n\n## 0.26 2020-03-03\n\n- Fix `Server#listen`\n\n## 0.25 2020-02-19\n\n- Ensure server socket is closed upon stopping loop\n- Fix `Request#format_header_lines`\n\n## 0.24 2020-01-08\n\n- Move HTTP to separate polyphony-http gem\n\nFor earlier changes look at the Polyphony changelog.\n"
  },
  {
    "path": "Gemfile",
    "content": "source 'https://rubygems.org'\n\ngemspec\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Sharon Rosner\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><img src=\"docs/tipi-logo.png\" /></p>\n\n# Tipi - the All-in-one Web Server for Ruby Apps\n\n[![Gem Version](https://badge.fury.io/rb/tipi.svg)](http://rubygems.org/gems/tipi)\n[![Tipi Test](https://github.com/digital-fabric/tipi/workflows/Tests/badge.svg)](https://github.com/digital-fabric/tipi/actions?query=workflow%3ATests)\n[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/digital-fabric/tipi/blob/master/LICENSE)\n\n## What is Tipi?\n\nTipi is an integrated, feature-complete HTTP/S server for Ruby applications.\nTipi is built on top of\n[Polyphony](https://github.com/digital-fabric/polyphony), a robust,\nhigh-performance library for building highly-concurrent applications in Ruby.\nTipi can be used to serve any Rack application or set of static files directly\nwithout having to employ a reverse-proxy such as Nginx.\n\n## Features\n\n* High-performance, highly concurrent web server based on\n  [Polyphony](https://github.com/digital-fabric/polyphony)\n* Full support for HTTP/1, HTTP/2, WebSocket protocols\n* Built-in SSL termination for secure, encrypted connections\n* **Automatic SSL certificates** using ACME providers such as Let's Encrypt (WIP)\n* Automatic ALPN protocol selection for serving HTTP/2\n* Request and response body streaming for efficient downloads and uploads\n* Full support for Rack-based apps\n\n## Benchmarks\n\n> Caveat emptor: the following results were obtained with an ad-hoc, manual\n> process. I am not really familiar with the servers I compared Tipi against,\n> and I ran them in their default configuration (apart from setting the number\n> of workers). Take these results with a bunch of salt.\n\n<img src=\"bm.png\" style=\"width: 480px\">\n\n| |Tipi|Puma|Falcon|Unicorn|\n|-|---:|---:|-----:|------:|\n|HTTP/1.1|138629|34573|40714|7438|\n|HTTPS/2|56762|n/a|34226|n/a|\n\n### Methodology\n\n- All servers ran the same \"Hello world\" [Rack\n  application](https://github.com/digital-fabric/tipi/blob/master/examples/hello.ru)\n- Each server was run with 4 forked worker processes:\n  - Tipi: `tipi -w4 -flocalhost:10080:10443 examples/hello.ru`\n  - [Puma](https://github.com/puma/puma): `puma -w 4 examples/hello.ru`\n  - [Falcon](https://github.com/socketry/falcon/): `falcon -n 4 -b http://localhost:9292/ -c examples/hello.ru`\n  - [Unicorn](https://yhbt.net/unicorn/): `unicorn -c u.conf examples/hello.ru`\n    with the configuration file containing the directive `worker_processes 4`\n- The benchmark results were obtained using `wrk -d60 -t4 -c64 <url>`\n- All servers were run on Ruby 2.7.2p137\n- Machine specs: i5-8350U@1.7GHzx8 CPU, 8GB of RAM, running Linux kernel version 5.13.7\n- Puma does not support HTTP/2.\n- As far as I could tell Unicorn does not support SSL termination.\n\n## Running Tipi\n\nTo run Tipi, run the included `tipi` command. Alternatively you can add tipi as\na dependency to your Gemfile, then run `bundle exec tipi`. By default \n\nTipi can be used to drive Rack apps or alternatively any app using the\n[Qeweney](https://github.com/digital-fabric/qeweney) request-response interface.\n\n### Running Rack apps\n\nUse the `tipi` command to start your app:\n\n```bash\n$ bundle exec tipi myapp.ru\n```\n\n### Running Qeweney apps\n\n```bash\n$ bundle exec tipi myapp.rb\n```\n\nThe app script file should define an `app` method that returns a proc/lambda\ntaking a single `Qeweney::Request` argument. Here's an example:\n\n```ruby\n# frozen_string_literal: true\n\ndef app\n  ->(req) { req.respond('Hello, world!', 'Content-Type' => 'text/plain') }\nend\n```\n\n## Setting server listening options\n\nBy default, Tipi serves plain HTTP on port 1234, but you can easily change that\nby providing command line options as follows:\n\n### HTTP\n\nTo listen for plain HTTP, use the `-l`/`--listen` option and specify a port\nnumber:\n\n```bash\n$ bundle exec tipi -l9292 myapp.ru\n```\n\n### HTTPS\n\nTo listen for HTTPS connections, use the `-s`/`--secure` option and specify a\nhost name and a port:\n\n```bash\n$ bundle exec tipi -sexample.com:9292 myapp.ru\n```\n\n### Full service listening\n\nThe Tipi full service listens for both HTTP and HTTPS and supports automatic\ncertificate provisioning. To use the full service, use the `-f`/`--full` option,\nand specify the domain name, the HTTP port, and the HTTPS port, e.g.:\n\n```bash\n$ bundle exec tipi -fmysite.org:10080:10443 myapp.ru\n\n#If serving multiple domains, you can use * as place holder\n$ bundle exec tipi -f*:10080:10443 myapp.ru\n```\n\nIf `localhost` is specified as the domain, Tipi will automatically generate a\nlocalhost certificate.\n\n## Concurrency settings\n\nBy default, the `tipi` command starts a single controller and uses\n[Polyphony](https://github.com/digital-fabric/polyphony) to run each connection\non its own fiber. This means that you will have a single process running on a\nsingle thread (on a single CPU core). In order to parallelize your app and\nemploy multiple CPU cores, you can tell Tipi to fork multiple worker processes\nto run your app. The number of workers is controlled using the `-w`/`--workers`\noption:\n\n```bash\n# fork 4 worker processes\n$ bundle exec tipi -w4 myapp.ru\n```\n\nYou can also set Tipi to spawn multiple threads in each worker when in\ncompatibility mode (see below.)\n\n## Compatibility mode\n\n> Note: compatibility mode is still being developed, and currently only supports\n> HTTP/1 connections.\n\nIn some apps, using Polyphony is not possible, due to incompatibilities between\nit and other third-party dependencies. In order to be able to run these apps,\nTipi provides a compatibility mode that does not use Polyphony for concurrency,\nbut instead uses a thread-per-connection concurrency model. You can also fork\nmultiple workers, each running multiple threads, if so desired. Note that the\nconcurrency level is the maximum number workers multiplied by the number of\nthreads per worker:\n\n```\nconcurrency = worker_count * threads_per_worker\n```\n\nTo run Tipi in compatibility mode, use the `-c`/`--compatibility` option, e.g.:\n\n```bash\n# 4 workers * 8 threads = 32 max concurrency\n$ bundle exec tipi -c -w4 -t8 myapp.ru\n```\n\n## Worker process supervision\n\nTipi employs a supervisor-controller-worker process supervision model, which\nminimizes the memory consumption of forked workers, and which facilitates\ngraceful reloading after updating the application code.\n\nThis supervision model is made of three levels:\n\n- Supervisor - Starts and stops the controller process\n- Controller - loads the application code and forks workers\n- Worker - listens for connections, handles incoming requests\n\n(If the worker count is 1, the Controller and Worker roles are merged into a\nsingle process.)\n\nThis model allows Tipi to fork workers after loading the app code, and use a\nmuch simpler way to perform graceful restarts:\n\n- The supervisor starts a new controller process (which may fork one or more\n  worker processes).\n- Sleep for a certain amount of time (currently 1 second.)\n- Stop the old controller process.\n- Each worker process is gracefully stopped and allowed to finish all pending\n  requests, then shutdown all open connections.\n  \n## Performing a graceful restart\n\nA graceful restart performed by sending `SIGUSR2` to the supervisor process.\n\n## Documentation\n\nDocumentation for Tipi's API is coming soon...\n\n"
  },
  {
    "path": "Rakefile",
    "content": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rake/clean\"\n\ntask :default => [:test]\n\ntask :test do\n  exec 'ruby test/run.rb'\nend\n"
  },
  {
    "path": "TODO.md",
    "content": "## Rethink design\n\n- Remove DF code\n- Remove non-Polyphony code\n\n# Miscellaneous\n\n- Try using `TCP_DEFER_ACCEPT` with Polyphony on io_uring - does it provide any\n  performance benefit?\n\n# What about HTTP/2?\n\nIt would be a nice exercise in converting a callback-based API to a blocking\none:\n\n```ruby\nparser = Tipi::HTTP2::Parser.new(socket)\nparser.each_stream(socket) do |stream|\n  spin { handle_stream(stream) }\nend\n```\n\n\n# Roadmap\n\n- Improve Rack spec compliance, add tests\n- Homogenize HTTP 1 and HTTP 2 headers - downcase symbols\n\n- Use `http-2-next` instead of `http-2` for http/2\n  - https://gitlab.com/honeyryderchuck/http-2-next\n  - Open an issue there, ask what's the difference between the two gems?\n\n## 0.38\n\n- Add more poly CLI commands and options:\n\n  - serve static files from given directory\n  - serve from rack up file\n  - serve both http and https\n  - use custom certificate files for SSL\n  - set host address to bind to\n  - set port to bind to\n  - set forking process count\n\n## 0.39 Working Sinatra application\n\n- app with database access (postgresql)\n- benchmarks!\n"
  },
  {
    "path": "benchmarks/bm_http1_parser.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\n\nHTTP_REQUEST = \"GET /foo HTTP/1.1\\r\\nHost: example.com\\r\\nAccept: */*\\r\\n\\r\\n\"\n\ndef measure_time_and_allocs\n  4.times { GC.start }\n  GC.disable\n\n  t0 = Time.now\n  a0 = object_count\n  yield\n  t1 = Time.now\n  a1 = object_count\n  [t1 - t0, a1 - a0]\nensure\n  GC.enable\nend\n\ndef object_count\n  count = ObjectSpace.count_objects\n  count[:TOTAL] - count[:FREE]\nend\n\ndef benchmark_other_http1_parser(iterations)\n  STDOUT << \"http_parser.rb: \"\n  require 'http_parser.rb'\n\n  i, o = IO.pipe\n  parser = Http::Parser.new\n  done = false\n  headers = nil\n  parser.on_headers_complete = proc do |h|\n    headers = h\n    headers[':method'] = parser.http_method\n    headers[':path'] = parser.request_url\n  end\n  parser.on_message_complete = proc { done = true }\n\n  elapsed, allocated = measure_time_and_allocs do\n    iterations.times do\n      o << HTTP_REQUEST\n      done = false\n      while !done\n        msg = i.readpartial(4096)\n        parser << msg\n      end\n    end\n  end\n  puts(format('elapsed: %f, allocated: %d (%f/req), rate: %f ips', elapsed, allocated, allocated.to_f / iterations, iterations / elapsed))\nend\n\ndef benchmark_tipi_http1_parser(iterations)\n  STDOUT << \"tipi parser: \"\n  require_relative '../lib/tipi_ext'\n  i, o = IO.pipe\n  reader = proc { |len| i.readpartial(len) }\n  parser = Tipi::HTTP1Parser.new(reader)\n\n  elapsed, allocated = measure_time_and_allocs do\n    iterations.times do\n      o << HTTP_REQUEST\n      headers = parser.parse_headers\n    end\n  end\n  puts(format('elapsed: %f, allocated: %d (%f/req), rate: %f ips', elapsed, allocated, allocated.to_f / iterations, iterations / elapsed))\nend\n\ndef fork_benchmark(method, iterations)\n  pid = fork do\n    send(method, iterations)\n  rescue Exception => e\n    p e\n    p e.backtrace\n    exit!\n  end\n  Process.wait(pid)\nend\n\nx = 500000\n# fork_benchmark(:benchmark_other_http1_parser, x)\n# fork_benchmark(:benchmark_tipi_http1_parser, x)\n\nbenchmark_tipi_http1_parser(x)"
  },
  {
    "path": "bin/benchmark",
    "content": "#!/usr/bin/env ruby\n\nrequire 'bundler/setup'\nrequire 'polyphony'\n\ndef parse_latency(latency)\n  m = latency.match(/^([\\d\\.]+)(us|ms|s)$/)\n  return nil unless m\n\n  value = m[1].to_f\n  case m[2]\n  when 's' then value\n  when 'ms' then value / 1000\n  when 'us' then value / 1000000\n  end\nend\n\ndef parse_wrk_results(results)\n  lines = results.lines\n  latencies = lines[3].strip.split(/\\s+/)\n  throughput = lines[6].strip.split(/\\s+/)\n\n  {\n    latency_avg:  parse_latency(latencies[1]),\n    latency_max:  parse_latency(latencies[3]),\n    rate:         throughput[1].to_f\n  }\nend\n\ndef run_wrk(duration: 10, threads: 2, connections: 10, url: )\n  `wrk -d#{duration} -t#{threads} -c#{connections} #{url}`\nend\n\n[8, 64, 256, 512].each do |c|\n  puts \"connections: #{c}\"\n  p parse_wrk_results(run_wrk(duration: 10, threads: 4, connections: c, url: \"http://localhost:10080/\"))\nend\n"
  },
  {
    "path": "bin/h1pd",
    "content": "#!/usr/bin/env bash\n\nset -e\nrake compile\nruby test/test_http1_parser.rb\nruby benchmarks/bm_http1_parser.rb\n"
  },
  {
    "path": "bin/tipi",
    "content": "#!/usr/bin/env ruby\n\nrequire 'bundler/setup'\nrequire 'tipi/cli'\n\ntrap('SIGINT') { exit }\n\nTipi::CLI.start\n"
  },
  {
    "path": "df/agent.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'polyphony'\nrequire 'json'\nrequire 'tipi/digital_fabric/protocol'\nrequire 'tipi/digital_fabric/agent'\n\nProtocol = DigitalFabric::Protocol\n\nclass SampleAgent < DigitalFabric::Agent\n  def initialize(id, server_url)\n    @id = id\n    super(server_url, { host: \"#{id}.realiteq.net\" }, 'foobar')\n    @name = \"agent-#{@id}\"\n  end\n\n  def http_request(req)\n    return streaming_http_request(req) if req.path == '/streaming'\n    return form_http_request(req) if req.path == '/form'\n\n    req.respond({ id: @id, time: Time.now.to_i }.to_json)\n  end\n\n  def streaming_http_request(req)\n    req.send_headers({ 'Content-Type': 'text/json' })\n\n    60.times do\n      sleep 1\n      do_some_activity\n      req.send_chunk({ id: @id, time: Time.now.to_i }.to_json)\n    end\n\n    req.finish\n  rescue Polyphony::Terminate\n    req.respond(' * shutting down *') if Fiber.current.graceful_shutdown?\n  rescue Exception => e\n    p e\n    puts e.backtrace.join(\"\\n\")\n  end\n\n  def form_http_request(req)\n    body = req.read\n    form_data = Tipi::Request.parse_form_data(body, req.headers)\n    req.respond({ form_data: form_data, headers: req.headers }.to_json, { 'Content-Type': 'text/json' })\n  end\n\n  def do_some_activity\n    File.open('/tmp/df-test.log', 'a+') { |f| sleep rand; f.puts \"#{Time.now} #{@name} #{generate_data(2**8)}\" }\n  end\n\n  def generate_data(length)\n    charset = Array('A'..'Z') + Array('a'..'z') + Array('0'..'9')\n    Array.new(length) { charset.sample }.join\n  end\nend\n\n# id = ARGV[0]\n# puts \"Starting agent #{id} pid: #{Process.pid}\"\n\n# spin_loop(interval: 60) { GC.start }\n# SampleAgent.new(id, '/tmp/df.sock').run\n# SampleAgent.new(id, 'localhost:4411').run"
  },
  {
    "path": "df/etc_benchmark.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'securerandom'\n\ndef generate\n  SecureRandom.uuid\nend\n\ncount = 100000\n\nGC.disable\nt0 = Time.now\ncount.times { generate }\nelapsed = Time.now - t0\nputs \"rate: #{count / elapsed}/s\""
  },
  {
    "path": "df/multi_agent_supervisor.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'polyphony'\nrequire 'json'\n\nrequire 'fileutils'\nFileUtils.cd(__dir__)\n\nrequire_relative 'agent'\n\nclass AgentManager\n  def initialize\n    @running_agents = {}\n    @pending_actions = Queue.new\n    @processor = spin_loop { process_pending_action }\n  end\n\n  def process_pending_action\n    action = @pending_actions.shift\n    case action[:kind]\n    when :start\n      start_agent(action[:spec])\n    when :stop\n      stop_agent(action[:spec])\n    end\n  end\n\n  def start_agent(spec)\n    return if @running_agents[spec]\n\n    @running_agents[spec] = spin do\n      while true\n        launch_agent_from_spec(spec)\n        sleep 1\n      end\n    ensure\n      @running_agents.delete(spec)\n    end\n  end\n\n  def stop_agent(spec)\n    fiber = @running_agents[spec]\n    return unless fiber\n\n    fiber.terminate\n    fiber.await\n  end\n\n  def update\n    return unless @pending_actions.empty?\n\n    current_specs = @running_agents.keys\n    updated_specs = agent_specs\n\n    to_start = updated_specs - current_specs\n    to_stop = current_specs - current_specs\n\n    to_start.each { |s| @pending_actions << { kind: :start, spec: s } }\n    to_stop.each { |s| @pending_actions << { kind: :stop, spec: s } }\n  end\n\n  def run\n    every(2) { update }\n  end\nend\n\nclass RealityAgentManager < AgentManager\n  def agent_specs\n    (1..400).map { |i| { id: i } }\n  end\n\n  def launch_agent_from_spec(spec)\n    # Polyphony::Process.watch(\"ruby agent.rb #{spec[:id]}\")\n    Polyphony::Process.watch do\n      spin_loop(interval: 60) { GC.start }\n      agent = SampleAgent.new(spec[:id], '/tmp/df.sock')\n      puts \"Starting agent #{spec[:id]} pid: #{Process.pid}\"\n      agent.run\n    end\n  end\nend\n\nputs \"Agent manager pid: #{Process.pid}\"\n\nmanager = RealityAgentManager.new\nmanager.run\n"
  },
  {
    "path": "df/multi_client.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'polyphony'\nrequire 'http/parser'\n\nclass Client\n  def initialize(id, host, port, http_host, interval)\n    @id = id\n    @host = host\n    @port = port\n    @http_host = http_host\n    @interval = interval.to_f\n    @interval_delta = @interval / 2\n  end\n\n  def run\n    while true\n      connect && issue_requests\n      sleep 5\n    end\n  end\n\n  def connect\n    @socket = Polyphony::Net.tcp_connect(@host, @port)\n  rescue SystemCallError\n    false\n  end\n\n  REQUEST = <<~HTTP\n  GET / HTTP/1.1\n  Host: %s\n\n  HTTP\n\n  def issue_requests\n    @parser = Http::Parser.new\n    @parser.on_message_complete = proc { @got_reply = true }\n    @parser.on_body = proc { |chunk| @response = chunk }\n\n    while true\n      do_request\n      sleep rand((@interval - @interval_delta)..(@interval + @interval_delta))\n    end\n  rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNREFUSED => e\n    # fail quitely\n  ensure\n    @parser = nil\n  end\n\n  def do_request\n    @got_reply = nil\n    @response = nil\n    @socket << format(REQUEST, @http_host)\n    wait_for_response\n    # if @parser.status_code != 200\n    #   puts \"Got status code #{@parser.status_code} from #{@http_host} => #{@parser.headers && @parser.headers['X-Request-ID']}\"\n    # end\n    # puts \"#{Time.now} [client-#{@id}] #{@http_host} => #{@response || '<error>'}\"\n  end\n\n  def wait_for_response\n    @socket.recv_loop do |data|\n      @parser << data\n      return @response if @got_reply\n    end\n  end\nend\n\ndef spin_client(id, host)\n  spin do\n    client = Client.new(id, 'localhost', 4411, host, 30)\n    client.run\n  end\nend\n\nspin_loop(interval: 60) { GC.start }\n\n10000.times { |id| spin_client(id, \"#{rand(1..400)}.realiteq.net\") }\n\ntrap('SIGINT') { exit! }\n\nputs \"Multi client pid: #{Process.pid}\"\nsleep\n"
  },
  {
    "path": "df/routing_benchmark.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'polyphony'\nrequire 'tipi/digital_fabric'\n\nclass FakeAgent\n  def initialize(idx)\n    @idx = idx\n  end\nend\n\ndef setup_df_service_with_agents(agent_count)\n  server = DigitalFabric::Service.new\n  agent_count.times do |i|\n    server.mount({path: \"/#{i}\"}, FakeAgent.new(i))\n  end\n  server\nend\n\ndef benchmark_route_compilation(agent_count, iterations)\n  service = setup_df_service_with_agents(agent_count)\n  t0 = Time.now\n  iterations.times { service.compile_agent_routes }\n  elapsed = Time.now - t0\n  puts \"route_compilation: #{agent_count} => #{elapsed / iterations}s (#{1/(elapsed / iterations)} ops/sec)\"\nend\n\nclass FauxRequest\n  def initialize(agent_count)\n    @agent_count = agent_count\n  end\n\n  def headers\n    { ':path' => \"/#{rand(@agent_count)}\"}\n  end\nend\n\ndef benchmark_find_agent(agent_count, iterations)\n  service = setup_df_service_with_agents(agent_count)\n  t0 = Time.now\n  request = FauxRequest.new(agent_count)\n  iterations.times do\n    agent = service.find_agent(request)\n  end\n  elapsed = Time.now - t0\n  puts \"routing: #{agent_count} => #{elapsed / iterations}s (#{1/(elapsed / iterations)} ops/sec)\"\nend\n\ndef benchmark\n  benchmark_route_compilation(100, 1000)\n  benchmark_route_compilation(500,  200)\n  benchmark_route_compilation(1000, 100)\n\n  benchmark_find_agent(100, 1000)\n  benchmark_find_agent(500,  200)\n  benchmark_find_agent(1000, 100)\nend\n\nbenchmark"
  },
  {
    "path": "df/sample_agent.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'polyphony'\nrequire 'json'\nrequire 'tipi/digital_fabric/protocol'\nrequire 'tipi/digital_fabric/agent'\n\nProtocol = DigitalFabric::Protocol\n\nclass SampleAgent < DigitalFabric::Agent\n  HTML_WS = IO.read(File.join(__dir__, 'ws_page.html'))\n  HTML_SSE = IO.read(File.join(__dir__, 'sse_page.html'))\n\n  def http_request(req)\n    path = req.headers[':path']\n    case path\n    when '/agent'\n      send_df_message(Protocol.http_response(\n        req['id'],\n        'Hello, world!',\n        {},\n        true\n      ))\n    when '/agent/ws'\n      send_df_message(Protocol.http_response(\n        req['id'],\n        HTML_WS,\n        { 'Content-Type' => 'text/html' },\n        true\n      ))\n    when '/agent/sse'\n      send_df_message(Protocol.http_response(\n        req['id'],\n        HTML_SSE,\n        { 'Content-Type' => 'text/html' },\n        true\n      ))\n    when '/agent/sse/events'\n      stream_sse_response(req)\n    else\n      send_df_message(Protocol.http_response(\n        req['id'],\n        nil,\n        { ':status' => 400 },\n        true\n      ))\n    end\n\n  end\n\n  def ws_request(req)\n    send_df_message(Protocol.ws_response(req['id'], {}))\n\n    10.times do\n      sleep 1\n      send_df_message(Protocol.ws_data(req['id'], Time.now.to_s))\n    end\n    send_df_message(Protocol.ws_close(req['id']))\n  end\n\n  def stream_sse_response(req)\n    send_df_message(Protocol.http_response(\n      req['id'],\n      nil,\n      { 'Content-Type' => 'text/event-stream' },\n      false\n    ))\n    10.times do\n      sleep 1\n      send_df_message(Protocol.http_response(\n        req['id'],\n        \"data: #{Time.now}\\n\\n\",\n        nil,\n        false\n      ))\n    end\n    send_df_message(Protocol.http_response(\n      req['id'],\n      \"retry: 0\\n\\n\",\n      nil,\n      true\n    ))\n  end\n\nend\n\nagent = SampleAgent.new('127.0.0.1', 4411, { path: '/agent' })\nagent.run\n"
  },
  {
    "path": "df/server.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative 'server_utils'\n\nlisteners = [\n  listen_http,\n  listen_https,\n  listen_unix\n]\n\nspin_loop(interval: 60) { GC.compact } if GC.respond_to?(:compact)\n\nbegin\n  log('Starting DF server')\n  Fiber.await(*listeners)\nrescue Interrupt\n  log('Got SIGINT, shutting down gracefully')\n  @service.graceful_shutdown\nrescue SystemExit\n  # ignore\nrescue Exception => e\n  log(\"Uncaught exception\", error: e, source: e.source_fiber, raising: e.raising_fiber, backtrace: e.backtrace)\nensure\n  log('DF server stopped')\nend\n"
  },
  {
    "path": "df/server_utils.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\nrequire 'tipi/digital_fabric'\nrequire 'tipi/digital_fabric/executive'\nrequire 'json'\nrequire 'fileutils'\nrequire 'time'\nrequire 'polyphony/extensions/debug'\n\nFileUtils.cd(__dir__)\n\n@service = DigitalFabric::Service.new(token: 'foobar')\n@executive = DigitalFabric::Executive.new(@service, { host: '@executive.realiteq.net' })\n\n@pid = Process.pid\n\ndef log(msg, **ctx)\n  text = format(\n    \"%s (%d) %s\\n\",\n    Time.now.strftime('%Y-%m-%d %H:%M:%S.%3N'),\n    @pid,\n    msg\n  )\n  STDOUT.orig_write text\n  return if ctx.empty?\n\n  ctx.each { |k, v| STDOUT.orig_write format(\"  %s: %s\\n\", k, v.inspect) }\nend\n\ndef listen_http\n  spin(:http_listener) do\n    opts = {\n      reuse_addr:  true,\n      dont_linger: true,\n    }\n    log('Listening for HTTP on localhost:10080')\n    server = Polyphony::Net.tcp_listen('0.0.0.0', 10080, opts)\n    id = 0\n    loop do\n      client = server.accept\n      # log(\"Accept HTTP connection\", client: client)\n      spin(\"http#{id += 1}\") do\n        @service.incr_connection_count\n        Tipi.client_loop(client, opts) { |req| @service.http_request(req) }\n      ensure\n        # log(\"Done with HTTP connection\", client: client)\n        @service.decr_connection_count\n      end\n    rescue Polyphony::BaseException\n      raise\n    rescue Exception => e\n      log 'HTTP accept (unknown) error', error: e, backtrace: e.backtrace\n    end\n  end\nend\n\nCERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\\n[^-]+-----END CERTIFICATE-----\\n)/.freeze\n\ndef listen_https\n  spin(:https_listener) do\n    private_key = OpenSSL::PKey::RSA.new IO.read('../../reality/ssl/privkey.pem')\n    c = IO.read('../../reality/ssl/cacert.pem')\n    certificates = c.scan(CERTIFICATE_REGEXP).map { |p|  OpenSSL::X509::Certificate.new(p.first) }\n    ctx = OpenSSL::SSL::SSLContext.new\n    ctx.security_level = 0\n    cert = certificates.shift\n    log \"SSL Certificate expires: #{cert.not_after.inspect}\"\n    ctx.add_certificate(cert, private_key, certificates)\n    # ctx.ciphers = 'ECDH+aRSA'\n    ctx.max_version = OpenSSL::SSL::TLS1_3_VERSION\n    ctx.min_version = OpenSSL::SSL::SSL3_VERSION\n    ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE\n\n    # TODO: further limit ciphers\n    # ref: https://github.com/socketry/falcon/blob/3ec805b3ceda0a764a2c5eb68cde33897b6a35ff/lib/falcon/environments/tls.rb\n    # ref: https://github.com/socketry/falcon/blob/3ec805b3ceda0a764a2c5eb68cde33897b6a35ff/lib/falcon/tls.rb\n\n    opts = {\n      reuse_addr:     true,\n      dont_linger:    true,\n      secure_context: ctx,\n      alpn_protocols: Tipi::ALPN_PROTOCOLS\n    }\n\n    log('Listening for HTTPS on localhost:10443')\n    server = Polyphony::Net.tcp_listen('0.0.0.0', 10443, opts)\n    id = 0\n    loop do\n      client = server.accept rescue nil\n      next unless client\n\n      # log('Accept HTTPS client connection', client: client)\n      spin(\"https#{id += 1}\") do\n        @service.incr_connection_count\n        Tipi.client_loop(client, opts) { |req| @service.http_request(req) }\n      rescue => e\n        log('Error while handling HTTPS client', client: client, error: e, backtrace: e.backtrace)\n      ensure\n        # log(\"Done with HTTP connection\", client: client)\n        @service.decr_connection_count\n      end\n    # rescue OpenSSL::SSL::SSLError, SystemCallError, TypeError => e\n    #   log('HTTPS accept error', error: e)\n    rescue Polyphony::BaseException\n      raise\n    rescue Exception => e\n      log 'HTTPS listener error: ', error: e, backtrace: e.backtrace\n    end\n  end\nend\n\nUNIX_SOCKET_PATH = '/tmp/df.sock'\ndef listen_unix\n  spin(:unix_listener) do\n    log(\"Listening on #{UNIX_SOCKET_PATH}\")\n    FileUtils.rm(UNIX_SOCKET_PATH) if File.exists?(UNIX_SOCKET_PATH)\n    socket = UNIXServer.new(UNIX_SOCKET_PATH)\n\n    id = 0\n    loop do\n      client = socket.accept\n      # log('Accept Unix connection', client: client)\n      spin(\"unix#{id += 1}\") do\n        Tipi.client_loop(client, {}) { |req| @service.http_request(req, true) }\n      end\n    rescue Polyphony::BaseException\n      raise\n    rescue Exception => e\n      log 'Unix accept error', error: e, backtrace: e.backtrace\n    end\n  end\nend\n\ndef listen_df\n  spin(:df_listener) do\n    opts = {\n      reuse_addr:  true,\n      reuse_port:  true,\n      dont_linger: true,\n    }\n    log('Listening for DF connections on localhost:4321')\n    server = Polyphony::Net.tcp_listen('0.0.0.0', 4321, opts)\n\n    id = 0\n    loop do\n      client = server.accept\n      # log('Accept DF connection', client: client)\n      spin(\"df#{id += 1}\") do\n        Tipi.client_loop(client, {}) { |req| @service.http_request(req, true) }\n      end\n    rescue Polyphony::BaseException\n      raise\n    rescue Exception => e\n      log 'DF accept (unknown) error', error: e, backtrace: e.backtrace\n    end\n  end\nend\n\nif ENV['TRACE'] == '1'\n  Thread.backend.trace_proc = proc do |event, fiber, value, pri|\n    fiber_id = fiber.tag || fiber.inspect\n    case event\n    when :schedule\n      log format(\"=> %s %s %s %s\", event, fiber_id, value.inspect, pri ? '(priority)' : '')\n    when :unblock\n      log format(\"=> %s %s %s\", event, fiber_id, value.inspect)\n    when :spin, :terminate\n      log format(\"=> %s %s\", event, fiber_id)\n    else\n      log format(\"=> %s\", event)\n    end\n  end\nend\n"
  },
  {
    "path": "df/sse_page.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n  <title>SSE Client</title>\n</head>\n<body>\n  <h1>SSE Client</h1>\n  <script>\n    var connect = function () {\n      console.log(\"connecting...\");\n      var eventSource = new EventSource(\"/agent/sse/events\");\n\n      eventSource.addEventListener('open', function(e) {\n        console.log(\"connected\");\n        document.querySelector('#status').innerText = 'connected';\n        return false;\n      }, false);\n\n      eventSource.addEventListener('message', function(e) {\n        document.querySelector('#msg').innerText = e.data;\n      }, false);\n    };\n\n    window.onload = connect;\n  </script>\n  <h1 id=\"status\">disconnected</h1>\n  <h1 id=\"msg\"></h1>\n</body>\n</html>"
  },
  {
    "path": "df/stress.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'polyphony'\nrequire 'fileutils'\n\nFileUtils.cd(__dir__)\n\ndef monitor_process(cmd)\n  while true\n    puts \"Starting #{cmd}\"\n    Polyphony::Process.watch(cmd)\n    sleep 5\n  end\nend\n\nputs \"pid: #{Process.pid}\"\nputs 'Starting stress test'\n\nspin { monitor_process('ruby server.rb') }\nspin { monitor_process('ruby multi_agent_supervisor.rb') }\nspin { monitor_process('ruby multi_client.rb') }\n\nsleep\n"
  },
  {
    "path": "df/ws_page.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n  <title>Websocket Client</title>\n</head>\n<body>\n  <h1>WebSocket Client</h1>\n  <script>\n    var connect = function () {\n      console.log(\"connecting...\")\n      var exampleSocket = new WebSocket(\"wss://dev.realiteq.net/agent\");\n\n      exampleSocket.onopen = function (event) {\n        console.log(\"connected\");\n        document.querySelector('#status').innerText = 'connected';\n        exampleSocket.send(\"Can you hear me?\");\n      };\n      exampleSocket.onclose = function (event) {\n        console.log(\"disconnected\");\n        document.querySelector('#status').innerText = 'disconnected';\n        setTimeout(function () {\n          // exampleSocket.removeAllListeners();\n          connect();\n        }, 1000);\n      }\n      exampleSocket.onmessage = function (event) {\n        console.log(\"got message\", event.data);\n        document.querySelector('#msg').innerText = event.data;\n        console.log(event.data);\n      }\n    };\n\n    window.onload = connect;\n  </script>\n  <h1 id=\"status\">disconnected</h1>\n  <h1 id=\"msg\"></h1>\n</body>\n</html>"
  },
  {
    "path": "docs/README.md",
    "content": "# Polyphony - Easy Concurrency for Ruby\n\n> Polyphony \\| pəˈlɪf\\(ə\\)ni \\|\n> 1. _Music_ the style of simultaneously combining a number of parts, each\n>    forming an individual melody and harmonizing with each other.\n> 2. _Programming_ a Ruby gem for concurrent programming focusing on performance\n>    and developer happiness.\n\nPolyphony is a library for building concurrent applications in Ruby. Polyphony\nharnesses the power of [Ruby fibers](https://ruby-doc.org/core-2.5.1/Fiber.html)\nto provide a cooperative, sequential coprocess-based concurrency model. Under\nthe hood, Polyphony uses [libev](https://github.com/enki/libev) as a\nhigh-performance event reactor that provides timers, I/O watchers and other\nasynchronous event primitives.\n\nPolyphony makes it possible to use normal Ruby built-in classes like `IO`, and\n`Socket` in a concurrent fashion without having to resort to threads. Polyphony\ntakes care of context-switching automatically whenever a blocking call like\n`Socket#accept` or `IO#read` is issued.\n\n## Features\n\n* **Full-blown, integrated, high-performance HTTP 1 / HTTP 2 / WebSocket server\n  with TLS/SSL termination, automatic ALPN protocol selection, and body\n  streaming**.\n* Co-operative scheduling of concurrent tasks using Ruby fibers.\n* High-performance event reactor for handling I/O events and timers.\n* Natural, sequential programming style that makes it easy to reason about concurrent code.\n* Abstractions and constructs for controlling the execution of concurrent code:\n  coprocesses, supervisors, throttling, resource pools etc.\n* Code can use native networking classes and libraries, growing support for\n  third-party gems such as `pg` and `redis`.\n* Use stdlib classes such as `TCPServer` and `TCPSocket` and `Net::HTTP`.\n* HTTP 1 / HTTP 2 client agent with persistent connections.\n* Competitive performance and scalability characteristics, in terms of both\n  throughput and memory consumption.\n\n## Prior Art\n\nPolyphony draws inspiration from the following, in no particular order:\n\n* [nio4r](https://github.com/socketry/nio4r/) and\n  [async](https://github.com/socketry/async) (Polyphony's C-extension code is\n  largely a spinoff of\n  [nio4r's](https://github.com/socketry/nio4r/tree/master/ext))\n* [EventMachine](https://github.com/eventmachine/eventmachine)\n* [Trio](https://trio.readthedocs.io/)\n* [Erlang supervisors](http://erlang.org/doc/man/supervisor.html) (and actually,\n  Erlang in general)\n\n## Going further\n\nTo learn more about using Polyphony to build concurrent applications, read the\ntechnical overview below, or look at the [included\nexamples](https://github.com/digital-fabric/polyphony/tree/9e0f3b09213156bdf376ef33684ef267517f06e8/examples/README.md).\nA thorough reference is forthcoming.\n\n## Contributing to Polyphony\n\nIssues and pull requests will be gladly accepted. Please use the git repository\nat https://github.com/digital-fabric/polyphony as your primary point of\ndeparture for contributing."
  },
  {
    "path": "examples/cuba.ru",
    "content": "# frozen_string_literal: true\n\nrequire 'cuba'\nrequire 'cuba/safe'\nrequire 'delegate' # See https://github.com/rack/rack/pull/1610\n\nCuba.use Rack::Session::Cookie, secret: '__a_very_long_string__'\n\nCuba.plugin Cuba::Safe\n\nCuba.define do\n  on get do\n    on 'hello' do\n      res.write 'Hello world!'\n    end\n\n    on root do\n      res.redirect '/hello'\n    end\n  end\nend\n\nrun Cuba\n"
  },
  {
    "path": "examples/full_service.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\n\n::Exception.__disable_sanitized_backtrace__ = true\n\ncertificate_db_path = File.expand_path('certificate_store.db', __dir__)\ncertificate_store = Tipi::ACME::SQLiteCertificateStore.new(certificate_db_path)\n\nTipi.full_service(\n  certificate_store: certificate_store\n) { |req| req.respond('Hello, world!') }\n"
  },
  {
    "path": "examples/hanami-api.ru",
    "content": "# frozen_string_literal: true\n\nrequire 'hanami/api'\n\nclass ExampleApi < Hanami::API\n  get '/hello' do\n    'Hello world!'\n  end\n\n  get '/' do\n    redirect '/hello'\n  end\n\n  get '/404' do\n    404\n  end\n\n  get '/500' do\n    500\n  end\nend\n\nrun ExampleApi.new\n"
  },
  {
    "path": "examples/hello.rb",
    "content": "# frozen_string_literal: true\n\nrun { |req|\n  req.respond('Hello, world!')\n}\n"
  },
  {
    "path": "examples/hello.ru",
    "content": "# frozen_string_literal: true\n\nrun lambda { |env|\n  [\n    200,\n    {\"Content-Type\" => \"text/plain\"},\n    [\"Hello, world!\"]\n  ]\n}\n"
  },
  {
    "path": "examples/http1_parser.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'polyphony'\nrequire_relative '../lib/tipi_ext'\n\ni, o = IO.pipe\n\nmodule ::Kernel\n  def trace(*args)\n    STDOUT.orig_write(format_trace(args))\n  end\n\n  def format_trace(args)\n    if args.first.is_a?(String)\n      if args.size > 1\n        format(\"%s: %p\\n\", args.shift, args)\n      else\n        format(\"%s\\n\", args.first)\n      end\n    else\n      format(\"%p\\n\", args.size == 1 ? args.first : args)\n    end\n  end\nend\n\nf = spin do\n  parser = Tipi::HTTP1Parser.new(i)\n  while true\n    trace '*' * 40\n    headers = parser.parse_headers\n    break unless headers\n    trace headers\n\n    body = parser.read_body\n    trace \"body: #{body ? body.bytesize : 0} bytes\"\n    trace body if body && body.bytesize < 80\n  end\nend\n\no << \"GET /a HTTP/1.1\\r\\n\\r\\n\"\n\n# o << \"GET /a HTTP/1.1\\r\\nContent-Length: 0\\r\\n\\r\\n\"\n\n# o << \"GET / HTTP/1.1\\r\\nHost: localhost:10080\\r\\nUser-Agent: curl/7.74.0\\r\\nAccept: */*\\r\\n\\r\\n\"\n\no << \"post /?q=time&blah=blah HTTP/1\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\na\\r\\nabcdefghij\\r\\n0\\r\\n\\r\\n\"\n\ndata = \" \" * 4000000\no << \"get /?q=time HTTP/1.1\\r\\nContent-Length: #{data.bytesize}\\r\\n\\r\\n#{data}\"\n\no << \"get /?q=time HTTP/1.1\\r\\nCookie: foo\\r\\nCookie: bar\\r\\n\\r\\n\"\n\no.close\n\nf.await\n"
  },
  {
    "path": "examples/http_request_ws_server.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\nrequire 'tipi/websocket'\n\ndef ws_handler(conn)\n  timer = spin_loop(interval: 1) do\n    conn << Time.now.to_s\n  end\n  while (msg = conn.recv)\n    conn << \"you said: #{msg}\"\n  end\nensure\n  timer.stop\nend\n\nopts = {\n  reuse_addr:  true,\n  dont_linger: true,\n}\n\nHTML = IO.read(File.join(__dir__, 'ws_page.html'))\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 4411...'\n\nTipi.serve('0.0.0.0', 4411, opts) do |req|\n  if req.upgrade_protocol == 'websocket'\n    conn = req.upgrade_to_websocket\n    ws_handler(conn)\n  else\n    req.respond(HTML, 'Content-Type' => 'text/html')\n  end\nend\n"
  },
  {
    "path": "examples/http_server.js",
    "content": "// For the sake of comparing performance, here's a node.js-based HTTP server\n// doing roughly the same thing as http_server. Preliminary benchmarking shows\n// the ruby version has a throughput (req/s) of about 2/3 of the JS version.\n\nconst http = require('http');\n\nconst MSG = 'Hello World';\n\nconst server = http.createServer((req, res) => {\n  // let requestCopy = {\n  //   method: req.method,\n  //   request_url: req.url,\n  //   headers: req.headers\n  // };\n\n  // res.writeHead(200, { 'Content-Type': 'application/json' });\n  // res.end(JSON.stringify(requestCopy));\n\n  res.writeHead(200);\n  res.end(MSG)\n});\n\nserver.listen(1235);\nconsole.log('Listening on port 1235');\n"
  },
  {
    "path": "examples/http_server.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\n\nopts = {\n  reuse_addr:  true,\n  dont_linger: true\n}\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 10080...'\n\n# GC.disable\n# Thread.current.backend.idle_gc_period = 60\n\nspin_loop(interval: 10) { p Thread.backend.stats }\n\nspin_loop(interval: 10) do\n  GC.compact\nend\n\nspin do\n  Tipi.serve('0.0.0.0', 10080, opts) do |req|\n    if req.path == '/stream'\n      req.send_headers('Foo' => 'Bar')\n      sleep 1\n      req.send_chunk(\"foo\\n\")\n      sleep 1\n      req.send_chunk(\"bar\\n\")\n      req.finish\n    elsif req.path == '/upload'\n      body = req.read\n      req.respond(\"Body: #{body.inspect} (#{body.bytesize} bytes)\")\n    else\n      req.respond(\"Hello world!\\n\")\n    end\n#    p req.transfer_counts\n  end\n  p 'done...'\nend.await\n"
  },
  {
    "path": "examples/http_server_forked.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\n\n::Exception.__disable_sanitized_backtrace__ = true\n\nopts = {\n  reuse_addr:  true,\n  reuse_port: true,\n  dont_linger: true\n}\n\nserver = Tipi.listen('0.0.0.0', 1234, opts)\n\nchild_pids = []\n8.times do\n  pid = Polyphony.fork do\n    puts \"forked pid: #{Process.pid}\"\n    server.each do |req|\n      req.respond(\"Hello world! from pid: #{Process.pid}\\n\")\n    end\n  rescue Interrupt\n  end\n  child_pids << pid\nend\n\nputs 'Listening on port 1234'\n\ntrap('SIGINT') { exit! }\n\nchild_pids.each { |pid| Thread.current.backend.waitpid(pid) }\n"
  },
  {
    "path": "examples/http_server_form.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\n\nopts = {\n  reuse_addr:  true,\n  dont_linger: true\n}\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 4411...'\n\nspin do\n  Tipi.serve('0.0.0.0', 4411, opts) do |req|\n    body = req.read\n    body2 = req.read\n    req.respond(\"body: #{body} (body2: #{body2.inspect})\\n\")\n  rescue Exception => e\n    p e\n  end\n  p 'done...'\nend.await\n"
  },
  {
    "path": "examples/http_server_graceful.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'polyphony'\nrequire 'tipi'\n\nopts = {\n  reuse_addr:  true,\n  dont_linger: true\n}\n\nserver = spin do\n  Tipi.serve('0.0.0.0', 1234, opts) do |req|\n    req.respond(\"Hello world!\\n\")\n  end\nend\n\ntrap('SIGHUP') do\n  puts 'got hup'\n  server.interrupt\nend\n\nputs \"pid: #{Process.pid}\"\nputs 'Send HUP to stop gracefully'\nputs 'Listening on port 1234...'\n\nsuspend\n"
  },
  {
    "path": "examples/http_server_routes.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\n\nopts = {\n  reuse_addr:  true,\n  dont_linger: true\n}\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 4411...'\n\napp = Tipi.route do |req|\n  req.on 'stream' do\n    req.send_headers('Foo' => 'Bar')\n    sleep 1\n    req.send_chunk(\"foo\\n\")\n    sleep 1\n    req.send_chunk(\"bar\\n\")\n    req.finish\n  end\n  req.default do\n    req.respond(\"Hello world!\\n\")\n  end\nend\n\ntrap('INT') { exit! }\nTipi.serve('0.0.0.0', 4411, opts, &app)\n"
  },
  {
    "path": "examples/http_server_simple.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 1234...'\n\nTipi.serve('0.0.0.0', 1234) do |req|\n  req.respond(\"Hello world!\\n\")\nend\n"
  },
  {
    "path": "examples/http_server_static.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\nrequire 'fileutils'\n\nopts = {\n  reuse_addr:  true,\n  dont_linger: true\n}\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 4411...'\n\nroot_path = FileUtils.pwd\n\ntrap('INT') { exit! }\n\nTipi.serve('0.0.0.0', 4411, opts) do |req|\n  path = File.join(root_path, req.path)\n  if File.file?(path)\n    req.serve_file(path)\n  else\n    req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)\n  end\nend\n"
  },
  {
    "path": "examples/http_server_throttled.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\n\n$throttler = Polyphony::Throttler.new(1000)\nopts = { reuse_addr: true, dont_linger: true }\nserver = spin do\n  Tipi.serve('0.0.0.0', 1234, opts) do |req|\n    $throttler.call { req.respond(\"Hello world!\\n\") }\n  end\nend\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 1234...'\nserver.await\n"
  },
  {
    "path": "examples/http_server_throttled_accept.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\n\n::Exception.__disable_sanitized_backtrace__ = true\n\nopts = {\n  reuse_addr:     true,\n  reuse_port:     true,\n  dont_linger:    true\n}\n\nserver = Tipi.listen('0.0.0.0', 1234, opts)\n\nputs 'Listening on port 1234'\n\nthrottler = Polyphony::Throttler.new(interval: 5)\nserver.accept_loop do |socket|\n  throttler.call do\n    spin { Tipi.client_loop(socket, opts) { |req| req.respond(\"Hello world!\\n\") } }\n  end\nend\n"
  },
  {
    "path": "examples/http_server_timeout.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\n\nopts = {\n  reuse_addr:  true,\n  dont_linger: true\n}\n\ndef timeout_handler(timeout, &handler)\n  ->(req) do\n    cancel_after(timeout) { handler.(req) }\n  rescue Polyphony::Cancel\n    req.respond(\"timeout\\n\", ':status' => 502)\n  end\nend\n\nsleep 0\n\nspin do\n  Tipi.serve(\n    '0.0.0.0',\n    1234,\n    opts,\n    &timeout_handler(0.1) do |req|\n      sleep rand(0.01..0.2)\n      req.respond(\"Hello timeout world!\\n\")\n    end\n  )\nend\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 1234...'\nsuspend"
  },
  {
    "path": "examples/http_unix_socket_server.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\n\npath = '/tmp/tipi.sock'\n\nputs \"pid: #{Process.pid}\"\nputs \"Listening on #{path}\"\n\nFileUtils.rm(path) rescue nil\nsocket = UNIXServer.new(path)\nTipi.accept_loop(socket, {}) do |req|\n  req.respond(\"Hello world!\\n\")\nrescue Exception => e\n  p e\nend\n"
  },
  {
    "path": "examples/http_ws_server.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\nrequire 'tipi/websocket'\n\ndef ws_handler(conn)\n  timer = spin_loop(interval: 1) do\n    conn << Time.now.to_s\n  end\n  while (msg = conn.recv)\n    conn << \"you said: #{msg}\"\n  end\nrescue Exception => e\n  p e\nensure\n  timer.stop\nend\n\nopts = {\n  reuse_addr:  true,\n  dont_linger: true,\n  upgrade:     {\n    websocket: Tipi::Websocket.handler(&method(:ws_handler))\n  }\n}\n\nHTML = IO.read(File.join(__dir__, 'ws_page.html'))\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 4411...'\n\nTipi.serve('0.0.0.0', 4411, opts) do |req|\n  req.respond(HTML, 'Content-Type' => 'text/html')\nend\n"
  },
  {
    "path": "examples/https_server.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\nrequire 'localhost/authority'\n\n::Exception.__disable_sanitized_backtrace__ = true\n\nauthority = Localhost::Authority.fetch\nopts = {\n  reuse_addr:     true,\n  dont_linger:    true,\n}\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 1234...'\n\nctx = authority.server_context\nserver = Polyphony::Net.tcp_listen('0.0.0.0', 1234, opts)\nloop do\n  socket = server.accept\n  client = OpenSSL::SSL::SSLSocket.new(socket, ctx)\n  client.sync_close = true\n  spin do\n    state = {}\n    accept_thread = Thread.new do\n      puts \"call client accept\"\n      client.accept\n      state[:result] = :ok\n    rescue Exception => e\n      puts error: e\n      state[:result] = e\n    end\n    \"wait for accept thread\"\n    accept_thread.join\n    \"accept thread done\"\n    if state[:result].is_a?(Exception)\n      puts \"Exception in SSL handshake: #{state[:result].inspect}\"\n      next\n    end\n    Tipi.client_loop(client, opts) do |req|\n      p path: req.path\n      if req.path == '/stream'\n        req.send_headers('Foo' => 'Bar')\n        sleep 0.5\n        req.send_chunk(\"foo\\n\")\n        sleep 0.5\n        req.send_chunk(\"bar\\n\", done: true)\n      elsif req.path == '/upload'\n        body = req.read\n        req.respond(\"Body: #{body.inspect} (#{body.bytesize} bytes)\")\n      else\n        req.respond(\"Hello world!\\n\")\n      end\n    end\n  ensure\n    client ? client.close : socket.close\n  end\nend\n"
  },
  {
    "path": "examples/https_server_forked.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\nrequire 'localhost/authority'\n\n::Exception.__disable_sanitized_backtrace__ = true\n\nauthority = Localhost::Authority.fetch\nopts = {\n  reuse_addr:     true,\n  dont_linger:    true,\n  secure_context: authority.server_context\n}\n\nserver = Tipi.listen('0.0.0.0', 1234, opts)\n\nputs 'Listening on port 1234'\n\nchild_pids = []\n4.times do\n  pid = Polyphony.fork do\n    puts \"forked pid: #{Process.pid}\"\n    server.each do |req|\n      req.respond(\"Hello world!\\n\")\n    end\n  rescue Interrupt\n  end\n  child_pids << pid\nend\n\nchild_pids.each { |pid| Thread.current.backend.waitpid(pid) }\n"
  },
  {
    "path": "examples/https_wss_server.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\nrequire 'tipi/websocket'\nrequire 'localhost/authority'\n\ndef ws_handler(conn)\n  timer = spin do\n    throttled_loop(1) do\n      conn << Time.now.to_s\n    rescue StandardError\n      nil\n    end\n  end\n  while (msg = conn.recv)\n    puts \"msg: #{msg}\"\n    # conn << \"you said: #{msg}\"\n  end\nensure\n  timer.stop\nend\n\nauthority = Localhost::Authority.fetch\nopts = {\n  reuse_addr:     true,\n  dont_linger:    true,\n  secure_context: authority.server_context,\n  upgrade:        {\n    websocket: Tipi::Websocket.handler(&method(:ws_handler))\n  }\n}\n\nHTML = IO.read(File.join(__dir__, 'wss_page.html'))\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 1234...'\nTipi.serve('0.0.0.0', 1234, opts) do |req|\n  req.respond(HTML, 'Content-Type' => 'text/html')\nend\n"
  },
  {
    "path": "examples/rack_server.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\n\napp_path = ARGV.first || File.expand_path('./config.ru', __dir__)\nunless File.file?(app_path)\n  STDERR.puts \"Please provide rack config file (there are some in the examples directory.)\"\n  exit!\nend\n\napp = Tipi::RackAdapter.load(app_path)\nopts = { reuse_addr: true, dont_linger: true }\n\nputs 'listening on port 1234'\nputs \"pid: #{Process.pid}\"\nTipi.serve('0.0.0.0', 1234, opts, &app)\n"
  },
  {
    "path": "examples/rack_server_forked.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\n\napp_path = ARGV.first || File.expand_path('./config.ru', __dir__)\nunless File.file?(app_path)\n  STDERR.puts \"Please provide rack config file (there are some in the examples directory.)\"\n  exit!\nend\n\napp = Tipi::RackAdapter.load(app_path)\nopts = { reuse_addr: true, dont_linger: true }\n\nserver = Tipi.listen('0.0.0.0', 1234, opts)\nputs 'listening on port 1234'\n\nchild_pids = []\n4.times do\n  child_pids << Polyphony.fork do\n    puts \"forked pid: #{Process.pid}\"\n    server.each(&app)\n  end\nend\n\nchild_pids.each { |pid| Thread.current.backend.waitpid(pid) }"
  },
  {
    "path": "examples/rack_server_https.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\nrequire 'localhost/authority'\n\napp_path = ARGV.first || File.expand_path('./config.ru', __dir__)\napp = Tipi::RackAdapter.load(app_path)\n\nauthority = Localhost::Authority.fetch\nopts = {\n  reuse_addr:     true,\n  dont_linger:    true,\n  secure_context: authority.server_context\n}\n\nputs 'listening on port 1234'\nputs \"pid: #{Process.pid}\"\nTipi.serve('0.0.0.0', 1234, opts, &app)\n"
  },
  {
    "path": "examples/rack_server_https_forked.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\nrequire 'localhost/authority'\n\napp_path = ARGV.first || File.expand_path('./config.ru', __dir__)\napp = Tipi::RackAdapter.load(app_path)\n\nauthority = Localhost::Authority.fetch\nopts = {\n  reuse_addr:     true,\n  reuse_port:     true,\n  dont_linger:    true,\n  secure_context: authority.server_context\n}\nserver = Tipi.listen('0.0.0.0', 1234, opts)\nputs 'Listening on port 1234'\n\nchild_pids = []\n4.times do\n  child_pids << Polyphony.fork do\n    puts \"forked pid: #{Process.pid}\"\n    server.each(&app)\n  end\nend\n\nchild_pids.each { |pid| Thread.current.backend.waitpid(pid) }\n"
  },
  {
    "path": "examples/routing_server.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\n\nopts = {\n  reuse_addr:  true,\n  dont_linger: true\n}\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 4411...'\n\napp = Tipi.route do |r|\n\n  r.on_root do\n    r.redirect '/hello'\n  end\n  r.on 'hello' do\n    r.on_get 'world' do\n      r.respond 'Hello world'\n    end\n    r.on_get do\n      r.respond 'Hello'\n    end\n    r.on_post do\n      puts 'Someone said Hello'\n      r.redirect '/'\n    end\n  end\nend\n\nspin do\n  Tipi.serve('0.0.0.0', 4411, opts, &app)\nend.await\n"
  },
  {
    "path": "examples/servername_cb.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'openssl'\nrequire 'fiber'\n\nctx = OpenSSL::SSL::SSLContext.new\n\nf = Fiber.new { |peer| loop { p peer: peer; _name, peer = peer.transfer nil } }\nctx.servername_cb = proc { |_socket, name|\n  p servername_cb: name\n  f.transfer([name, Fiber.current]).tap { |r| p result: r }\n}\n\nsocket = Socket.new(:INET, :STREAM).tap do |s|\n  s.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)\n  s.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)\n  s.bind(Socket.sockaddr_in(12345, '0.0.0.0'))\n  s.listen(Socket::SOMAXCONN)\nend\nserver = OpenSSL::SSL::SSLServer.new(socket, ctx)\n\nThread.new do\n  sleep 0.5\n  socket = TCPSocket.new('127.0.0.1', 12345)\n  client = OpenSSL::SSL::SSLSocket.new(socket)\n  client.hostname = 'example.com'\n  p client: client\n  client.connect\nrescue => e\n  p client_error: e\nend\n\nwhile true\n  conn = server.accept\n  p accepted: conn\n  break\nend\n"
  },
  {
    "path": "examples/source.rb",
    "content": "# frozen_string_literal: true\n\nrun { |req|\n  req.serve_file(__FILE__)\n}\n"
  },
  {
    "path": "examples/streaming.rb",
    "content": "# frozen_string_literal: true\n\nrun { |req|\n  req.send_headers('Content-Type' => 'text/event-stream')\n  10.times { |i|\n    sleep 0.1\n    req.send_chunk(\"data: #{i.to_s * 40}\\n\")\n  }\n  req.finish\n}\n"
  },
  {
    "path": "examples/websocket_client.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'polyphony'\nrequire 'websocket'\n\n::Exception.__disable_sanitized_backtrace__ = true\n\nclass WebsocketClient\n  def initialize(url, headers = {})\n    @socket = TCPSocket.new('127.0.0.1', 1234)\n    do_handshake(url, headers)\n  end\n\n  def do_handshake(url, headers)\n    handshake = WebSocket::Handshake::Client.new(url: url, headers: headers)\n    @socket << handshake.to_s\n    @socket.read_loop do |data|\n      handshake << data\n      break if handshake.finished?\n    end\n    raise 'Websocket handshake failed' unless handshake.valid?\n    @version = handshake.version\n\n    @reader = WebSocket::Frame::Incoming::Client.new(version: @version)\n  end\n\n  def receive\n    @socket.read_loop do |data|\n      @reader << data\n      parsed = @reader.next\n      return parsed if parsed\n    end\n  end\n\n  def send(data)\n    frame = WebSocket::Frame::Outgoing::Client.new(\n      version: @version,\n      data: data,\n      type: :text\n    )\n    @socket << frame.to_s\n  end\n  alias_method :<<, :send\n\n  def close\n    @socket.close\n  end\nend\n\n\n(1..3).each do |i|\n  spin do\n    client = WebsocketClient.new('ws://127.0.0.1:1234/', { Cookie: \"SESSIONID=#{i * 10}\" })\n    (1..3).each do |j|\n      sleep rand(0.2..0.5)\n      client.send \"Hello from client #{i} (#{j})\"\n      puts \"server reply: #{client.receive}\"\n    end\n    client.close\n  end\nend\n\nsuspend\n"
  },
  {
    "path": "examples/websocket_demo.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\nrequire 'tipi/websocket'\n\nclass WebsocketClient\n  def initialize(url, headers = {})\n    @socket = TCPSocket.new('127.0.0.1', 1234)\n    do_handshake(url, headers)\n  end\n\n  def do_handshake(url, headers)\n    handshake = WebSocket::Handshake::Client.new(url: url, headers: headers)\n    @socket << handshake.to_s\n    @socket.read_loop do |data|\n      handshake << data\n      break if handshake.finished?\n    end\n    raise 'Websocket handshake failed' unless handshake.valid?\n    @version = handshake.version\n\n    @reader = WebSocket::Frame::Incoming::Client.new(version: @version)\n  end\n\n  def receive\n    parsed = @reader.next\n    return parsed if parsed\n\n    @socket.read_loop do |data|\n      @reader << data\n      parsed = @reader.next\n      return parsed if parsed\n    end\n  end\n\n  def send(data)\n    frame = WebSocket::Frame::Outgoing::Client.new(\n      version: @version,\n      data: data,\n      type: :text\n    )\n    @socket << frame.to_s\n  end\n  alias_method :<<, :send\n\n  def close\n    @socket.close\n  end\nend\n\nserver = spin do\n  websocket_handler = Tipi::Websocket.handler do |conn|\n    while (msg = conn.recv)\n      conn << \"you said: #{msg}\"\n    end\n  end\n\n  opts = { upgrade: { websocket: websocket_handler } }\n  puts 'Listening on port http://127.0.0.1:1234/'\n  Tipi.serve('0.0.0.0', 1234, opts) do |req|\n    req.respond(\"Hello world!\\n\")\n  end\nend\n\nsleep 0.01 # wait for server to start\n\nclients = (1..3).map do |i|\n  spin do\n    client = WebsocketClient.new('ws://127.0.0.1:1234/', { Cookie: \"SESSIONID=#{i * 10}\" })\n    (1..3).each do |j|\n      sleep rand(0.2..0.5)\n      client.send \"Hello from client #{i} (#{j})\"\n      puts \"server reply: #{client.receive}\"\n    end\n    client.close\n  end\nend\n\nFiber.await(*clients)\n"
  },
  {
    "path": "examples/websocket_secure_server.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\nrequire 'localhost/authority'\n\ndef ws_handler(conn)\n  while (msg = conn.recv)\n    conn << \"you said: #{msg}\"\n  end\nend\n\nauthority = Localhost::Authority.fetch\nopts = {\n  reuse_addr:     true,\n  dont_linger:    true,\n  upgrade:        {\n    websocket: Polyphony::Websocket.handler(&method(:ws_handler))\n  },\n  secure_context: authority.server_context\n}\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 1234...'\nTipi.serve('0.0.0.0', 1234, opts) do |req|\n  req.respond(\"Hello world!\\n\")\nend\n"
  },
  {
    "path": "examples/websocket_server.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'tipi'\nrequire 'tipi/websocket'\n\ndef ws_handler(conn)\n  while (msg = conn.recv)\n    conn << \"you said: #{msg}\"\n  end\nend\n\nopts = {\n  reuse_addr:  true,\n  dont_linger: true,\n  upgrade:     {\n    websocket: Tipi::Websocket.handler(&method(:ws_handler))\n  }\n}\n\nputs \"pid: #{Process.pid}\"\nputs 'Listening on port 1234...'\nTipi.serve('0.0.0.0', 1234, opts) do |req|\n  req.respond(\"Hello world!\\n\")\nend\n"
  },
  {
    "path": "examples/ws_page.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n  <title>Websocket Client</title>\n</head>\n<body>\n  <script>\n    var connect = function () {\n      var exampleSocket = new WebSocket(\"ws://localhost:4411/\");\n\n      exampleSocket.onopen = function (event) {\n        document.querySelector('#status').innerText = 'connected';\n        exampleSocket.send(\"Can you hear me?\");\n      };\n      exampleSocket.onclose = function (event) {\n        document.querySelector('#status').innerText = 'disconnected';\n        setTimeout(function () {\n          // exampleSocket.removeAllListeners();\n          connect();\n        }, 1000);\n      }\n      exampleSocket.onmessage = function (event) {\n        document.querySelector('#msg').innerText = event.data;\n        console.log(event.data);\n      }\n    };\n\n    connect();\n  </script>\n  <h1 id=\"status\">disconnected</h1>\n  <h1 id=\"msg\"></h1>\n</body>\n</html>\n"
  },
  {
    "path": "examples/wss_page.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n  <title>Websocket Client</title>\n</head>\n<body>\n  <script>\n    var connect = function () {\n      var exampleSocket = new WebSocket(\"/\");\n\n      exampleSocket.onopen = function (event) {\n        document.querySelector('#status').innerText = 'connected';\n        exampleSocket.send(\"Can you hear me?\");\n      };\n      exampleSocket.onclose = function (event) {\n        console.log('onclose');\n        document.querySelector('#status').innerText = 'disconnected';\n        setTimeout(function () {\n          // exampleSocket.removeAllListeners();\n          connect();\n        }, 1000);\n      }\n      exampleSocket.onmessage = function (event) {\n        document.querySelector('#msg').innerText = event.data;\n        console.log(event.data);\n      }\n    };\n\n    connect();\n  </script>\n  <h1 id=\"status\">disconnected</h1>\n  <h1 id=\"msg\"></h1>\n</body>\n</html>"
  },
  {
    "path": "examples/zlib-bench.rb",
    "content": "# frozen_string_literal: true\n\nFILE_SIZES = {\n  's.tmp'  => 2**10,\n  'm.tmp'  => 2**17,\n  'l.tmp'  => 2**20,\n  'xl.tmp' => 2**24\n}\n\ndef create_files\n  FILE_SIZES.each { |fn, size|\n    IO.write(File.join('/tmp', fn), '*' * size)\n  }\nend\n\ncreate_files\n\nrun { |req|\n  file_path = File.join('/tmp', req.path)\n  if File.file?(file_path)\n    req.serve_file(file_path)\n  else\n    req.respond(nil, ':status' => 404)\n  end\n}\n"
  },
  {
    "path": "lib/tipi/acme.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'openssl'\nrequire 'acme-client'\nrequire 'localhost/authority'\n\nmodule Tipi\n  module ACME\n    class Error < StandardError\n    end\n\n    class CertificateManager\n      def initialize(master_ctx:, store:, challenge_handler:, valid_hosts:)\n        @master_ctx = master_ctx\n        @store = store\n        @challenge_handler = challenge_handler\n        @valid_hosts = valid_hosts\n        @contexts = {}\n        @requests = Polyphony::Queue.new\n        @worker = spin { run }\n        setup_sni_callback\n      end\n\n      ACME_CHALLENGE_PATH_REGEXP = /\\/\\.well\\-known\\/acme\\-challenge/.freeze\n\n      def challenge_routing_app(app)\n        ->(req) do\n          (req.path =~ ACME_CHALLENGE_PATH_REGEXP ? @challenge_handler : app)\n            .(req)\n        rescue => e\n          puts \"Error while handling request: #{e.inspect} (headers: #{req.headers})\"\n          req.respond(nil, ':status' => Qeweney::Status::BAD_REQUEST)\n        end\n      end\n\n      IP_REGEXP = /^\\d+\\.\\d+\\.\\d+\\.\\d+$/\n\n      def setup_sni_callback\n        @master_ctx.servername_cb = proc { |_socket, name| get_ctx(name) }\n      end\n\n      def get_ctx(name)\n        state = { ctx: nil }\n\n        if @valid_hosts\n          return nil unless @valid_hosts.include?(name)\n        end\n\n        ready_ctx = @contexts[name]\n        return ready_ctx if ready_ctx\n        return @master_ctx if name =~ IP_REGEXP\n\n        @requests << [name, state]\n        wait_for_ctx(state)\n        # Eventually we might want to return an error returned in\n        # state[:error]. For the time being we handle errors by returning the\n        # master context\n        state[:ctx] || @master_ctx\n      rescue => e\n        @master_ctx\n      end\n\n      MAX_WAIT_FOR_CTX_DURATION = 30\n\n      def wait_for_ctx(state)\n        t0 = Time.now\n        period = 0.00001\n        while !state[:ctx] && !state[:error]\n          orig_sleep period\n          if period < 0.1\n            period *= 2\n          elsif Time.now - t0 > MAX_WAIT_FOR_CTX_DURATION\n            raise \"Timeout waiting for certificate provisioning\"\n          end\n        end\n      end\n\n      def run\n        loop do\n          name, state = @requests.shift\n          state[:ctx] = get_context(name)\n        rescue => e\n          state[:error] = e if state\n        end\n      end\n\n      LOCALHOST_REGEXP = /\\.?localhost$/.freeze\n\n      def get_context(name)\n        @contexts[name] = setup_context(name)\n      end\n\n      def setup_context(name)\n        ctx = provision_context(name)\n        transfer_ctx_settings(ctx)\n        ctx\n      end\n\n      def provision_context(name)\n        return localhost_context if name =~ LOCALHOST_REGEXP\n\n        info = get_certificate(name)\n        ctx = OpenSSL::SSL::SSLContext.new\n        chain = parse_certificate(info[:certificate])\n        cert = chain.shift\n        ctx.add_certificate(cert, info[:private_key], chain)\n        ctx\n      end\n\n      def transfer_ctx_settings(ctx)\n        ctx.alpn_protocols = @master_ctx.alpn_protocols\n        ctx.alpn_select_cb =  @master_ctx.alpn_select_cb\n        ctx.ciphers = @master_ctx.ciphers\n      end\n\n      CERTIFICATE_REGEXP = /(-----BEGIN CERTIFICATE-----\\n[^-]+-----END CERTIFICATE-----\\n)/.freeze\n\n      def parse_certificate(certificate)\n        certificate\n          .scan(CERTIFICATE_REGEXP)\n          .map { |p|  OpenSSL::X509::Certificate.new(p.first) }\n      end\n\n      def get_expired_stamp(certificate)\n        chain = parse_certificate(certificate)\n        cert = chain.shift\n        cert.not_after\n      end\n\n      def get_certificate(name)\n        entry = @store.get(name)\n        return entry if entry\n\n        provision_certificate(name).tap do |entry|\n          @store.set(name, **entry)\n        end\n      end\n\n      def localhost_context\n        @localhost_authority ||= Localhost::Authority.fetch\n        @localhost_authority.server_context\n      end\n\n      def private_key\n        @private_key ||= OpenSSL::PKey::RSA.new(4096)\n      end\n\n      ACME_DIRECTORY = 'https://acme-v02.api.letsencrypt.org/directory'\n\n      def acme_client\n        @acme_client ||= setup_acme_client\n      end\n\n      def setup_acme_client\n        client = Acme::Client.new(\n          private_key: private_key,\n          directory: ACME_DIRECTORY\n        )\n        account = client.new_account(\n          contact: 'mailto:info@noteflakes.com',\n          terms_of_service_agreed: true\n        )\n        client\n      end\n\n      def provision_certificate(name)\n        order = acme_client.new_order(identifiers: [name])\n        authorization = order.authorizations.first\n        challenge = authorization.http\n\n        @challenge_handler.add(challenge)\n        challenge.request_validation\n        while challenge.status == 'pending'\n          sleep(0.25)\n          challenge.reload\n        end\n        raise ACME::Error, \"Invalid CSR\" if challenge.status == 'invalid'\n\n        private_key = OpenSSL::PKey::RSA.new(4096)\n        csr = Acme::Client::CertificateRequest.new(\n          private_key: private_key,\n          subject: { common_name: name }\n        )\n        order.finalize(csr: csr)\n        while order.status == 'processing'\n          sleep(0.25)\n          order.reload\n        end\n        certificate = begin\n          order.certificate(force_chain: 'DST Root CA X3')\n        rescue Acme::Client::Error::ForcedChainNotFound\n          order.certificate\n        end\n        expired_stamp = get_expired_stamp(certificate)\n        puts \"Certificate for #{name} expires: #{expired_stamp.inspect}\"\n\n        {\n          private_key: private_key,\n          certificate: certificate,\n          expired_stamp: expired_stamp\n        }\n      end\n    end\n\n    class HTTPChallengeHandler\n      def initialize\n        @challenges = {}\n      end\n\n      def add(challenge)\n        path = \"/.well-known/acme-challenge/#{challenge.token}\"\n        @challenges[path] = challenge\n      end\n\n      def remove(challenge)\n        path = \"/.well-known/acme-challenge/#{challenge.token}\"\n        @challenges.delete(path)\n      end\n\n      def call(req)\n        challenge = @challenges[req.path]\n\n        # handle incoming request\n        challenge = @challenges[req.path]\n        return req.respond(nil, ':status' => 400) unless challenge\n\n        req.respond(challenge.file_content, 'content-type' => challenge.content_type)\n      end\n    end\n\n    class CertificateStore\n      def set(name, private_key:, certificate:, expired_stamp:)\n        raise NotImplementedError\n      end\n\n      def get(name)\n        raise NotImplementedError\n      end\n    end\n\n    class InMemoryCertificateStore\n      def initialize\n        @store = {}\n      end\n\n      def set(name, private_key:, certificate:, expired_stamp:)\n        @store[name] = {\n          private_key:    private_key,\n          certificate:    certificate,\n          expired_stamp:  expired_stamp\n        }\n      end\n\n      def get(name)\n        entry = @store[name]\n        return nil unless entry\n        if Time.now >= entry[:expired_stamp]\n          @store.delete(name)\n          return nil\n        end\n\n        entry\n      end\n    end\n\n    class SQLiteCertificateStore\n      attr_reader :db\n\n      def initialize(path)\n        require 'extralite'\n\n        @db = Extralite::Database.new(path)\n        @db.query(\"\n          create table if not exists certificates (\n            name primary key not null,\n            private_key not null,\n            certificate not null,\n            expired_stamp not null\n          );\"\n        )\n      end\n\n      def set(name, private_key:, certificate:, expired_stamp:)\n        @db.query(\"\n          insert into certificates values (?, ?, ?, ?)\n        \", name, private_key.to_s, certificate, expired_stamp.to_i)\n      rescue Extralite::Error => e\n        p error_in_set: e\n        raise e\n      end\n\n      def get(name)\n        remove_expired_certificates\n\n        entry = @db.query_single_row(\"\n          select name, private_key, certificate, expired_stamp\n            from certificates\n           where name = ?\n        \", name)\n        return nil unless entry\n        entry[:expired_stamp] = Time.at(entry[:expired_stamp])\n        entry[:private_key] = OpenSSL::PKey::RSA.new(entry[:private_key])\n        entry\n      rescue Extralite::Error => e\n        p error_in_get: e\n        raise e\n      end\n\n      def remove_expired_certificates\n        @db.query(\"\n          delete from certificates\n          where expired_stamp < ?\n        \", Time.now.to_i)\n      rescue Extralite::Error => e\n        p error_in_remove_expired_certificates: e\n        raise e\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/cli.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'tipi'\nrequire 'fileutils'\nrequire 'tipi/supervisor'\nrequire 'optparse'\n\nmodule Tipi\n  DEFAULT_OPTS = {\n    app_type: :web,\n    mode: :polyphony,\n    workers: 1,\n    threads: 1,\n    listen: ['http', 'localhost', 1234],\n    path: '.',\n  }\n\n  def self.opts_from_argv(argv)\n    opts = DEFAULT_OPTS.dup\n    parser = OptionParser.new do |o|\n      o.banner = \"Usage: tipi [options] path\"\n      o.on('-h', '--help', 'Show this help') { puts o; exit }\n      o.on('-wNUM', '--workers NUM', 'Number of worker processes (default: 1)') do |v|\n        opts[:workers] = v\n      end\n      o.on('-tNUM', '--threads NUM', 'Number of worker threads (default: 1)') do |v|\n        opts[:threads] = v\n        opts[:mode] = :stock\n      end\n      o.on('-c', '--compatibility', 'Use compatibility mode') do\n        opts[:mode] = :stock\n      end\n      o.on('-lSPEC', '--listen SPEC', 'Setup HTTP listener') do |v|\n        opts[:listen] = parse_listen_spec('http', v)\n      end\n      o.on('-sSPEC', '--secure SPEC', 'Setup HTTPS listener (for localhost)') do |v|\n        opts[:listen] = parse_listen_spec('https', v)\n      end\n      o.on('-fSPEC', '--full-service SPEC', 'Setup HTTP/HTTPS listeners (with automatic certificates)') do |v|\n        opts[:listen] = parse_listen_spec('full', v)\n      end\n      o.on('-v', '--verbose', 'Verbose output') do\n        opts[:verbose] = true\n      end\n    end.parse!(argv)\n    opts[:path] = argv.shift unless argv.empty?\n    verify_path(opts[:path])\n    opts\n  end\n\n  def self.parse_listen_spec(type, spec)\n    [type, *spec.split(':').map { |s| str_to_native_type(s) }]\n  end\n\n  def self.str_to_native_type(str)\n    case str\n    when /^\\d+$/\n      str.to_i\n    else\n      str\n    end\n  end\n\n  def self.verify_path(path)\n    return if File.file?(path) || File.directory?(path)\n\n    puts \"Invalid path specified #{path}\"\n    exit!\n  end\n\n  module CLI\n    BANNER =\n      \"\\n\" +\n      \"         ooo\\n\" +\n      \"       oo\\n\" +\n      \"     o\\n\" +\n      \"   \\\\|/    Tipi - a better web server for a better world\\n\" +\n      \"   / \\\\       \\n\" +\n      \"  /   \\\\      https://github.com/digital-fabric/tipi\\n\" +\n      \"⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺\\n\"\n\n    def self.start(argv = ARGV.dup)\n      opts = Tipi.opts_from_argv(argv)\n      display_banner if STDOUT.tty? && !opts[:silent]\n\n      Tipi::Supervisor.run(opts)\n    end\n\n    def self.display_banner\n      puts BANNER\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/config_dsl.rb",
    "content": "# frozen_string_literal: true\n\nmodule Tipi\n  module Configuration\n    class Interpreter\n      # make_blank_slate\n\n      def initialize(assembler)\n        @assembler = assembler\n      end\n\n      def gzip_response\n        @assembler.emit 'req = Tipi::GZip.wrap(req)'\n      end\n\n      def log(out)\n        @assembler.wrap_current_frame 'logger.log_request(req) do |req|'\n      end\n\n      def error(&block)\n        assembler.emit_exception_handler &block\n      end\n\n      def match(pattern, &block)\n        @assembler.emit_conditional \"if req.path =~ #{pattern.inspect}\", &block\n      end\n    end\n\n    class Assembler\n      def self.from_source(code)\n        new.from_source code\n      end\n\n      def from_source(code)\n        @stack = [new_frame]\n        @app_procs = {}\n        @interpreter = Interpreter.new self\n        @interpreter.instance_eval code\n\n        loop do\n          frame = @stack.pop\n          return assemble_app_proc(frame).join(\"\\n\") if @stack.empty?\n\n          @stack.last[:body] << assemble_frame(frame)\n        end\n      end\n\n      def new_frame\n        {\n          prelude: [],\n          body: []\n        }\n      end\n\n      def add_frame(&block)\n        @stack.push new_frame\n        yield\n      ensure\n        frame = @stack.pop\n        emit assemble(frame)\n      end\n\n      def wrap_current_frame(head)\n        frame = @stack.pop\n        wrapper = new_frame\n        wrapper[:body] << head\n        @stack.push wrapper\n        @stack.push frame\n      end\n\n      def emit(code)\n        @stack.last[:body] << code\n      end\n\n      def emit_prelude(code)\n        @stack.last[:prelude] << code\n      end\n\n      def emit_exception_handler(&block)\n        proc_id = add_app_proc block\n        @stack.last[:rescue_proc_id] = proc_id\n      end\n\n      def emit_block(conditional, &block)\n        proc_id = add_app_proc block\n        @stack.last[:branched] = true\n        emit conditional\n        add_frame &block\n      end\n\n      def add_app_proc(proc)\n        id = :\"proc#{@app_procs.size}\"\n        @app_procs[id] = proc\n        id\n      end\n\n      def assemble_frame(frame)\n        indent = 0\n        lines = []\n        emit_code lines, frame[:prelude], indent\n        if frame[:rescue_proc_id]\n          emit_code lines, 'begin', indent\n          indent += 1\n        end\n        emit_code lines, frame[:body], indent\n        if frame[:rescue_proc_id]\n          emit_code lines, 'rescue => e', indent\n          emit_code lines, \"  app_procs[#{frame[:rescue_proc_id].inspect}].call(req, e)\", indent\n          emit_code lines, 'end', indent\n          indent -= 1\n        end\n        lines\n      end\n\n      def assemble_app_proc(frame)\n        indent = 0\n        lines = []\n        emit_code lines, frame[:prelude], indent\n        emit_code lines, 'proc do |req|', indent\n        emit_code lines, frame[:body], indent + 1\n        emit_code lines, 'end', indent\n\n        lines\n      end\n\n      def emit_code(lines, code, indent)\n        if code.is_a? Array\n          code.each { |l| emit_code lines, l, indent + 1 }\n        else\n          lines << (indent_line code, indent)\n        end\n      end\n\n      @@indents = Hash.new { |h, k| h[k] =  '  ' * k }\n\n      def indent_line(code, indent)\n        indent == 0 ? code : \"#{ @@indents[indent] }#{code}\"\n      end\n    end\n  end\nend\n\n\ndef assemble(code)\n  Tipi::Configuration::Assembler.from_source(code)\nend\n\ncode = assemble <<~RUBY\ngzip_response\nlog STDOUT\nRUBY\n\nputs code\n"
  },
  {
    "path": "lib/tipi/configuration.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative './handler'\n\nmodule Tipi\n  module Configuration\n    class << self\n      def supervise_config\n        current_runner = nil\n        while (config = receive)\n          old_runner, current_runner = current_runner, spin { run(config) }\n          old_runner&.stop\n        end\n      end\n\n      def run(config)\n        start_listeners(config)\n        config[:forked] ? forked_supervise(config) : simple_supervise(config)\n      end\n\n      def start_listeners(config)\n        puts \"Listening on port 1234\"\n        @server = Polyphony::Net.tcp_listen('0.0.0.0', 1234, { reuse_addr: true, dont_linger: true })\n      end\n\n      def simple_supervise(config)\n        virtual_hosts = setup_virtual_hosts(config)\n        start_acceptors(config, virtual_hosts)\n        suspend\n        # supervise(restart: true)\n      end\n\n      def forked_supervise(config)\n        config[:forked].times do\n          spin { Polyphony.watch_process { simple_supervise(config) } }\n        end\n        suspend\n      end\n\n      def setup_virtual_hosts(config)\n        {\n          '*': Tipi::DefaultHandler.new(config)\n        }\n      end\n\n      def start_acceptors(config, virtual_hosts)\n        spin do\n          puts \"pid: #{Process.pid}\"\n          while (connection = @server.accept)\n            spin { virtual_hosts[:'*'].call(connection) }\n          end\n        end\n      end\n    end\n  end\nend"
  },
  {
    "path": "lib/tipi/controller/bare_polyphony.rb",
    "content": ""
  },
  {
    "path": "lib/tipi/controller/bare_stock.rb",
    "content": "# frozen_string_literal: true\n\nmodule Tipi\n  module Apps\n    module Bare\n      def start(opts)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/controller/extensions.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'tipi'\n\nmodule Kernel\n  def run(app = nil, &block)\n    Tipi.app = app || block\n  end\nend\n\nmodule Tipi\n  class << self\n    attr_writer :app\n\n    def app\n      return @app if @app\n\n      raise 'No app define. The app to run should be set using `Tipi.app = ...`'\n    end\n\n    def run_sites(site_map)\n      sites = site_map.each_with_object({}) { |(k, v), h| h[k] = v.to_proc }\n      valid_hosts = sites.keys\n\n      @app = ->(req) {\n        handler = sites[req.host]\n        if handler\n          handler.call(req)\n        else\n          req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)\n        end\n      }\n\n      @app.define_singleton_method(:valid_hosts) { valid_hosts }\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/controller/stock_http1_adapter.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'tipi/http1_adapter'\n\nmodule Tipi\n  class StockHTTP1Adapter < HTTP1Adapter\n    def initialize(conn, opts)\n      super(conn, opts)\n\n    end\n\n    def each(&block)\n    end\n  end\nend"
  },
  {
    "path": "lib/tipi/controller/web_polyphony.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'tipi'\nrequire 'localhost/authority'\nrequire_relative './extensions'\n\nmodule Tipi\n  class Controller\n    def initialize(opts)\n      @opts = opts\n      @path = File.expand_path(@opts['path'])\n      @service = prepare_service\n    end\n\n    WORKER_COUNT_RANGE = (1..32).freeze\n\n    def run\n      worker_count = (@opts['workers'] || 1).to_i.clamp(WORKER_COUNT_RANGE)\n      return run_worker if worker_count == 1\n\n      supervise_workers(worker_count)\n    end\n\n  private\n\n    def supervise_workers(worker_count)\n      supervisor = spin(:web_worker_supervisor) do\n        worker_count.times do\n          spin(:web_worker) do\n            pid = Polyphony.fork { run_worker }\n            puts \"Forked worker pid: #{pid}\"\n            Polyphony.backend_waitpid(pid)\n            puts \"Done worker pid: #{pid}\"\n          end\n        end\n        supervise(restart: :always)\n      rescue Polyphony::Terminate\n        # TODO: find out how Terminate can leak like that (it's supposed to be\n        # caught in Fiber#run)\n      end\n      trap('SIGTERM') { supervisor.terminate(graceful: true) }\n      trap('SIGINT') do\n        trap('SIGINT') { exit! }\n        supervisor.terminate(graceful: true)\n      end\n\n      supervisor.await\n    rescue Polyphony::Terminate\n      # TODO: find out how Terminate can leak etc.\n    end\n\n    def run_worker\n      server = start_server(@service)\n      trap('SIGTERM') { server&.terminate(graceful: true) }\n      trap('SIGINT') do\n        trap('SIGINT') { exit! }\n        server&.terminate(graceful: true)\n      end\n      raise 'Server not started' unless server\n      server.await\n    rescue Polyphony::Terminate\n      # TODO: find out why this exception leaks from the server fiber\n      # ignore\n    end\n\n    def prepare_service\n      if File.file?(@path)\n        File.extname(@path) == '.ru' ? rack_service : tipi_service\n      elsif File.directory?(@path)\n        static_service\n      else\n        raise \"Invalid path specified #{@path}\"\n      end\n    end\n\n    def start_app\n      if File.extname(@path) == '.ru'\n        start_rack_app\n      else\n        require(@path)\n      end\n    end\n\n    def rack_service\n      puts \"Loading Rack app from #{@path}\"\n      app = Tipi::RackAdapter.load(@path)\n      web_service(app)\n    end\n\n    def tipi_service\n      puts \"Loading Tipi app from #{@path}\"\n      require(@path)\n      app = Tipi.app\n      web_service(app)\n      # proc { spin { Object.run } }\n    end\n\n    def static_service\n      puts \"Serving static files from #{@path}\"\n      app = proc do |req|\n        full_path = find_path(@path, req.path)\n        if full_path\n          req.serve_file(full_path)\n        else\n          req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)\n        end\n      end\n      web_service(app)\n    end\n\n    def web_service(app)\n      app = add_connection_headers(app)\n\n      prepare_listener(@opts['listen'], app)\n    end\n\n    def prepare_listener(spec, app)\n      case spec.shift\n      when 'http'\n        case spec.size\n        when 2\n          host, port = spec\n          port ||= 80\n        when 1\n          host = '0.0.0.0'\n          port = spec.first || 80\n        else\n          raise \"Invalid listener spec\"\n        end\n        prepare_http_listener(port, app)\n      when 'https'\n        case spec.size\n        when 2\n          host, port = spec\n          port ||= 80\n        when 1\n          host = 'localhost'\n          port = spec.first || 80\n        else\n          raise \"Invalid listener spec\"\n        end\n        port ||= 443\n        prepare_https_listener(host, port, app)\n      when 'full'\n        host, http_port, https_port = spec\n        http_port ||= 80\n        https_port ||= 443\n        prepare_full_service_listeners(host, http_port, https_port, app)\n      end\n    end\n\n    def prepare_http_listener(port, app)\n      puts \"Listening for HTTP on localhost:#{port}\"\n\n      proc do\n        spin_accept_loop('HTTP', port) do |socket|\n          Tipi.client_loop(socket, @opts, &app)\n        end\n      end\n    end\n\n    LOCALHOST_REGEXP = /^(.+\\.)?localhost$/.freeze\n\n    def prepare_https_listener(host, port, app)\n      localhost = host =~ LOCALHOST_REGEXP\n      return prepare_localhost_https_listener(port, app) if localhost\n\n      raise \"No certificate found for #{host}\"\n      # TODO: implement loading certificate\n    end\n\n    def prepare_localhost_https_listener(port, app)\n      puts \"Listening for HTTPS on localhost:#{port}\"\n\n      authority = Localhost::Authority.fetch\n      ctx = authority.server_context\n      ctx.ciphers = 'ECDH+aRSA'\n      Polyphony::Net.setup_alpn(ctx, Tipi::ALPN_PROTOCOLS)\n\n      proc do\n        https_listener = spin_accept_loop('HTTPS', port) do |socket|\n          start_https_connection_fiber(socket, ctx, nil, app)\n        rescue Exception => e\n          puts \"Exception in https_listener block: #{e.inspect}\\n#{e.backtrace.inspect}\"\n        end\n      end\n    end\n\n    def prepare_full_service_listeners(host, http_port, https_port, app)\n      puts \"Listening for HTTP on localhost:#{http_port}\"\n      puts \"Listening for HTTPS on localhost:#{https_port}\"\n\n      redirect_app = http_redirect_app(https_port)\n      ctx = OpenSSL::SSL::SSLContext.new\n      ctx.ciphers = 'ECDH+aRSA'\n      Polyphony::Net.setup_alpn(ctx, Tipi::ALPN_PROTOCOLS)\n      certificate_store = create_certificate_store\n\n      proc do\n        challenge_handler = Tipi::ACME::HTTPChallengeHandler.new\n        certificate_manager = Tipi::ACME::CertificateManager.new(\n          master_ctx: ctx,\n          store: certificate_store,\n          challenge_handler: challenge_handler,\n          valid_hosts: app.respond_to?(:valid_hosts) ? app.valid_hosts : nil\n        )\n        http_app = certificate_manager.challenge_routing_app(redirect_app)\n\n        http_listener = spin_accept_loop('HTTP', http_port) do |socket|\n          Tipi.client_loop(socket, @opts, &http_app)\n        end\n\n        ssl_accept_thread_pool = Polyphony::ThreadPool.new(4)\n\n        https_listener = spin_accept_loop('HTTPS', https_port) do |socket|\n          start_https_connection_fiber(socket, ctx, ssl_accept_thread_pool, app)\n        rescue Exception => e\n          puts \"Exception in https_listener block: #{e.inspect}\\n#{e.backtrace.inspect}\"\n        end\n      end\n    end\n\n    def http_redirect_app(https_port)\n      case https_port\n      when 443, 10443\n        ->(req) { req.redirect(\"https://#{req.host}#{req.path}\") }\n      else\n        ->(req) { req.redirect(\"https://#{req.host}:#{https_port}#{req.path}\") }\n      end\n    end\n\n    INVALID_PATH_REGEXP = /\\/?(\\.\\.|\\.)\\//\n\n    def find_path(base, path)\n      return nil if path =~ INVALID_PATH_REGEXP\n\n      full_path = File.join(base, path)\n      return full_path if File.file?(full_path)\n      return find_path(full_path, 'index') if File.directory?(full_path)\n\n      qualified = \"#{full_path}.html\"\n      return qualified if File.file?(qualified)\n\n      nil\n    end\n\n    SOCKET_OPTS = {\n      reuse_addr:   true,\n      reuse_port:   true,\n      dont_linger:  true,\n    }.freeze\n\n    def spin_accept_loop(name, port, &block)\n      spin(:accept_loop) do\n        server = Polyphony::Net.tcp_listen('0.0.0.0', port, SOCKET_OPTS)\n        loop do\n          socket = server.accept\n          spin_connection_handler(name, socket, block)\n        rescue Polyphony::BaseException => e\n          raise\n        rescue Exception => e\n          puts \"#{name} listener uncaught exception: #{e.inspect}\"\n        end\n      ensure\n        finalize_listener(server) if server\n      end\n    end\n\n    def spin_connection_handler(name, socket, block)\n      spin(:connection_handler) do\n        block.(socket)\n      rescue Polyphony::BaseException\n        raise\n      rescue Exception => e\n        puts \"Uncaught error in #{name} handler: #{e.inspect}\"\n        p e.backtrace\n      end\n    end\n\n    def finalize_listener(server)\n      fiber  = Fiber.current\n      server.close\n      gracefully_terminate_conections(fiber) if fiber.graceful_shutdown?\n    rescue Polyphony::BaseException\n      raise\n    rescue Exception => e\n      trace \"Exception in finalize_listener: #{e.inspect}\"\n    end\n\n    def gracefully_terminate_conections(fiber)\n      supervisor = spin(:connection_termination_supervisor) { supervise }.detach\n      fiber.attach_all_children_to(supervisor)\n\n      # terminating the supervisor will\n      supervisor.terminate(graceful: true)\n    end\n\n    def add_connection_headers(app)\n      app\n      # proc do |req|\n      #   conn = req.adapter.conn\n      #   # req.headers[':peer'] = conn.peeraddr(false)[2]\n      #   req.headers[':scheme'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'\n      #   app.(req)\n      # end\n    end\n\n    def ssl_accept(client)\n      client.accept\n      true\n    rescue Polyphony::BaseException\n      raise\n    rescue Exception => e\n      p e\n      e\n    end\n\n    def start_https_connection_fiber(socket, ctx, thread_pool, app)\n      client = OpenSSL::SSL::SSLSocket.new(socket, ctx)\n      client.sync_close = true\n\n      result = thread_pool ?\n        thread_pool.process { ssl_accept(client) } : ssl_accept(client)\n\n      if result.is_a?(Exception)\n        puts \"Exception in SSL handshake: #{result.inspect}\"\n        return\n      end\n\n      Tipi.client_loop(client, @opts, &app)\n    rescue => e\n      puts \"Uncaught error in HTTPS connection fiber: #{e.inspect} bt: #{e.backtrace.inspect}\"\n    ensure\n      (client ? client.close : socket.close) rescue nil\n    end\n\n    CERTIFICATE_STORE_DEFAULT_DIR = File.expand_path('~/.tipi').freeze\n    CERTIFICATE_STORE_DEFAULT_DB_PATH = File.join(\n      CERTIFICATE_STORE_DEFAULT_DIR, 'certificates.db').freeze\n\n    def create_certificate_store\n      FileUtils.mkdir(CERTIFICATE_STORE_DEFAULT_DIR) rescue nil\n      Tipi::ACME::SQLiteCertificateStore.new(CERTIFICATE_STORE_DEFAULT_DB_PATH)\n    end\n\n    def start_server(service)\n      spin(:web_server) do\n        service.call\n        supervise(restart: :always)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/controller/web_stock.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'ever'\nrequire 'localhost/authority'\nrequire 'http/parser'\nrequire 'qeweney'\nrequire 'tipi/rack_adapter'\nrequire_relative './extensions'\n\nmodule Tipi\n  class Listener\n    def initialize(server, &handler)\n      @server = server\n      @handler = handler\n    end\n\n    def accept\n      socket, _addrinfo = @server.accept\n      @handler.call(socket)\n    end\n  end\n\n  class Connection\n    def io_ready\n      raise NotImplementedError\n    end\n  end\n\n  class HTTP1Connection < Connection\n    attr_reader :io\n\n    def initialize(io, evloop, &app)\n      @io = io\n      @evloop = evloop\n      @parser = Http::Parser.new(self)\n      @app = app\n      setup_read_request\n    end\n\n    def setup_read_request\n      @request_complete = nil\n      @request = nil\n      @response_buffer = nil\n    end\n\n    def on_headers_complete(headers)\n      headers = normalize_headers(headers)\n      headers[':path'] = @parser.request_url\n      headers[':method'] = @parser.http_method.downcase\n      scheme = (proto = headers['x-forwarded-proto']) ?\n                proto.downcase : scheme_from_connection\n      headers[':scheme'] = scheme\n      @request = Qeweney::Request.new(headers, self)\n    end\n\n    def normalize_headers(headers)\n      headers.each_with_object({}) do |(k, v), h|\n        k = k.downcase\n        hk = h[k]\n        if hk\n          hk = h[k] = [hk] unless hk.is_a?(Array)\n          v.is_a?(Array) ? hk.concat(v) : hk << v\n        else\n          h[k] = v\n        end\n      end\n    end\n\n    def scheme_from_connection\n      @io.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'\n    end\n\n    def on_body(chunk)\n      @request.buffer_body_chunk(chunk)\n    end\n\n    def on_message_complete\n      @request_complete = true\n    end\n\n    def io_ready\n      if !@request_complete\n        handle_read_request\n      else\n        handle_write_response\n      end\n    end\n\n    def handle_read_request\n      result = @io.read_nonblock(16384, exception: false)\n      case result\n      when :wait_readable\n        watch_io(false)\n      when :wait_writable\n        watch_io(true)\n      when nil\n        close_io\n      else\n        @parser << result\n        if @request_complete\n          handle_request\n          # @response = handle_request(@request_headers, @request_body)\n          # handle_write_response\n        else\n          watch_io(false)\n        end\n      end\n    rescue HTTP::Parser::Error, SystemCallError, IOError\n      close_io\n    end\n\n    def watch_io(rw)\n      @evloop.watch_io(self, @io, rw, true)\n      # @evloop.emit([:watch_io, self, @io, rw, true])\n    end\n\n    def close_io\n      @evloop.emit([:close_io, self, @io])\n    end\n\n    def handle_request\n      @app.call(@request)\n      # req = Qeweney::Request.new(headers, self)\n      # response_body = \"Hello, world!\"\n      # \"HTTP/1.1 200 OK\\nContent-Length: #{response_body.bytesize}\\n\\n#{response_body}\"\n    end\n\n    # response API\n\n    CRLF = \"\\r\\n\"\n    CRLF_ZERO_CRLF_CRLF = \"\\r\\n0\\r\\n\\r\\n\"\n\n    # Sends response including headers and body. Waits for the request to complete\n    # if not yet completed. The body is sent using chunked transfer encoding.\n    # @param request [Qeweney::Request] HTTP request\n    # @param body [String] response body\n    # @param headers\n    def respond(request, body, headers)\n      formatted_headers = format_headers(headers, body, false)\n      request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))\n      if body\n        handle_write(formatted_headers + body)\n      else\n        handle_write(formatted_headers)\n      end\n    end\n\n    # Sends response headers. If empty_response is truthy, the response status\n    # code will default to 204, otherwise to 200.\n    # @param request [Qeweney::Request] HTTP request\n    # @param headers [Hash] response headers\n    # @param empty_response [boolean] whether a response body will be sent\n    # @param chunked [boolean] whether to use chunked transfer encoding\n    # @return [void]\n    def send_headers(request, headers, empty_response: false, chunked: true)\n      formatted_headers = format_headers(headers, !empty_response, http1_1?(request) && chunked)\n      request.tx_incr(formatted_headers.bytesize)\n      handle_write(formatted_headers)\n    end\n\n    def http1_1?(request)\n      request.headers[':protocol'] == 'http/1.1'\n    end\n\n    # Sends a response body chunk. If no headers were sent, default headers are\n    # sent using #send_headers. if the done option is true(thy), an empty chunk\n    # will be sent to signal response completion to the client.\n    # @param request [Qeweney::Request] HTTP request\n    # @param chunk [String] response body chunk\n    # @param done [boolean] whether the response is completed\n    # @return [void]\n    def send_chunk(request, chunk, done: false)\n      data = +''\n      data << \"#{chunk.bytesize.to_s(16)}\\r\\n#{chunk}\\r\\n\" if chunk\n      data << \"0\\r\\n\\r\\n\" if done\n      return if data.empty?\n\n      request.tx_incr(data.bytesize)\n      handle_write(data)\n    end\n\n    # Finishes the response to the current request. If no headers were sent,\n    # default headers are sent using #send_headers.\n    # @return [void]\n    def finish(request)\n      request.tx_incr(5)\n      handle_write(\"0\\r\\n\\r\\n\")\n    end\n\n    INTERNAL_HEADER_REGEXP = /^:/.freeze\n\n    # Formats response headers into an array. If empty_response is true(thy),\n    # the response status code will default to 204, otherwise to 200.\n    # @param headers [Hash] response headers\n    # @param body [boolean] whether a response body will be sent\n    # @param chunked [boolean] whether to use chunked transfer encoding\n    # @return [String] formatted response headers\n    def format_headers(headers, body, chunked)\n      status = headers[':status']\n      status ||= (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)\n      lines = format_status_line(body, status, chunked)\n      headers.each do |k, v|\n        next if k =~ INTERNAL_HEADER_REGEXP\n\n        collect_header_lines(lines, k, v)\n      end\n      lines << CRLF\n      lines\n    end\n\n    def format_status_line(body, status, chunked)\n      if !body\n        empty_status_line(status)\n      else\n        with_body_status_line(status, body, chunked)\n      end\n    end\n\n    def empty_status_line(status)\n      if status == 204\n        +\"HTTP/1.1 #{status}\\r\\n\"\n      else\n        +\"HTTP/1.1 #{status}\\r\\nContent-Length: 0\\r\\n\"\n      end\n    end\n\n    def with_body_status_line(status, body, chunked)\n      if chunked\n        +\"HTTP/1.1 #{status}\\r\\nTransfer-Encoding: chunked\\r\\n\"\n      else\n        +\"HTTP/1.1 #{status}\\r\\nContent-Length: #{body.is_a?(String) ? body.bytesize : body.to_i}\\r\\n\"\n      end\n    end\n\n    def collect_header_lines(lines, key, value)\n      if value.is_a?(Array)\n        value.inject(lines) { |_, item| lines << \"#{key}: #{item}\\r\\n\" }\n      else\n        lines << \"#{key}: #{value}\\r\\n\"\n      end\n    end\n\n    def handle_write(data = nil)\n      if data\n        if @response_buffer\n          @response_buffer << data\n        else\n          @response_buffer = +data\n        end\n      end\n\n      result = @io.write_nonblock(@response_buffer, exception: false)\n      case result\n      when :wait_readable\n        watch_io(false)\n      when :wait_writable\n        watch_io(true)\n      when nil\n        close_io\n      else\n        setup_read_request\n        watch_io(false)\n      end\n    end\n  end\n\n  class Controller\n    def initialize(opts)\n      @opts = opts\n      @path = File.expand_path(@opts['path'])\n      @service = prepare_service\n    end\n\n    WORKER_COUNT_RANGE = (1..32).freeze\n\n    def run\n      worker_count = (@opts['workers'] || 1).to_i.clamp(WORKER_COUNT_RANGE)\n      return run_worker if worker_count == 1\n\n      supervise_workers(worker_count)\n    end\n\n  private\n\n    def supervise_workers(worker_count)\n      supervisor = spin do\n        worker_count.times do\n          pid = fork { run_worker }\n          puts \"Forked worker pid: #{pid}\"\n          Process.wait(pid)\n          puts \"Done worker pid: #{pid}\"\n        end\n        # supervise(restart: :always)\n      rescue Polyphony::Terminate\n        # TODO: find out how Terminate can leak like that (it's supposed to be\n        # caught in Fiber#run)\n      end\n      # trap('SIGTERM') { supervisor.terminate(graceful: true) }\n      # trap('SIGINT') do\n      #   trap('SIGINT') { exit! }\n      #   supervisor.terminate(graceful: true)\n      # end\n\n      # supervisor.await\n    end\n\n    def run_worker\n      @evloop = Ever::Loop.new\n      start_server(@service)\n      trap('SIGTERM') { @evloop.stop }\n      trap('SIGINT') do\n        trap('SIGINT') { exit! }\n        @evloop.stop\n      end\n      run_evloop\n    end\n\n    def run_evloop\n      @evloop.each do |event|\n        case event\n        when Listener\n          event.accept\n        when Connection\n          event.io_ready\n        when Array\n          cmd, key, io, rw, oneshot = event\n          case cmd\n          when :watch_io\n            @evloop.watch_io(key, io, rw, oneshot)\n          when :close_io\n            io.close\n          end\n        end\n      end\n    end\n\n    def prepare_service\n      if File.file?(@path)\n        File.extname(@path) == '.ru' ? rack_service : tipi_service\n      elsif File.directory?(@path)\n        static_service\n      else\n        raise \"Invalid path specified #{@path}\"\n      end\n    end\n\n    def start_app\n      if File.extname(@path) == '.ru'\n        start_rack_app\n      else\n        require(@path)\n      end\n    end\n\n    def rack_service\n      puts \"Loading Rack app from #{@path}\"\n      app = Tipi::RackAdapter.load(@path)\n      web_service(app)\n    end\n\n    def tipi_service\n      puts \"Loading Tipi app from #{@path}\"\n      require(@path)\n      app = Tipi.app\n      if !app\n        raise \"No app define. The app to run should be set using `Tipi.app = ...`\"\n      end\n      web_service(app)\n    end\n\n    def static_service\n      puts \"Serving static files from #{@path}\"\n      app = proc do |req|\n        p req: req\n        full_path = find_path(@path, req.path)\n        if full_path\n          req.serve_file(full_path)\n        else\n          req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)\n        end\n      end\n      web_service(app)\n    end\n\n    def web_service(app)\n      app = add_connection_headers(app)\n\n      prepare_listener(@opts['listen'], app)\n    end\n\n    def prepare_listener(spec, app)\n      case spec.shift\n      when 'http'\n        case spec.size\n        when 2\n          host, port = spec\n          port ||= 80\n        when 1\n          host = '0.0.0.0'\n          port = spec.first || 80\n        else\n          raise \"Invalid listener spec\"\n        end\n        prepare_http_listener(port, app)\n      when 'https'\n        case spec.size\n        when 2\n          host, port = spec\n          port ||= 80\n        when 1\n          host = 'localhost'\n          port = spec.first || 80\n        else\n          raise \"Invalid listener spec\"\n        end\n        port ||= 443\n        prepare_https_listener(host, port, app)\n      when 'full'\n        host, http_port, https_port = spec\n        http_port ||= 80\n        https_port ||= 443\n        prepare_full_service_listeners(host, http_port, https_port, app)\n      end\n    end\n\n    def prepare_http_listener(port, app)\n      puts \"Listening for HTTP on localhost:#{port}\"\n\n      proc do\n        start_listener('HTTP', port) do |socket|\n          start_client(socket, &app)\n        end\n      end\n    end\n\n    def start_client(socket, &app)\n      conn = HTTP1Connection.new(socket, @evloop, &app)\n      conn.watch_io(false)\n    end\n\n    LOCALHOST_REGEXP = /^(.+\\.)?localhost$/.freeze\n\n    def prepare_https_listener(host, port, app)\n      localhost = host =~ LOCALHOST_REGEXP\n      return prepare_localhost_https_listener(port, app) if localhost\n\n      raise \"No certificate found for #{host}\"\n      # TODO: implement loading certificate\n    end\n\n    def prepare_localhost_https_listener(port, app)\n      puts \"Listening for HTTPS on localhost:#{port}\"\n\n      authority = Localhost::Authority.fetch\n      ctx = authority.server_context\n      ctx.ciphers = 'ECDH+aRSA'\n      Polyphony::Net.setup_alpn(ctx, Tipi::ALPN_PROTOCOLS)\n\n      proc do\n        https_listener = spin_accept_loop('HTTPS', port) do |socket|\n          start_https_connection_fiber(socket, ctx, nil, app)\n        rescue Exception => e\n          puts \"Exception in https_listener block: #{e.inspect}\\n#{e.backtrace.inspect}\"\n        end\n      end\n    end\n\n    def prepare_full_service_listeners(host, http_port, https_port, app)\n      puts \"Listening for HTTP on localhost:#{http_port}\"\n      puts \"Listening for HTTPS on localhost:#{https_port}\"\n\n      redirect_host = (https_port == 443) ? host : \"#{host}:#{https_port}\"\n      redirect_app = ->(r) { r.redirect(\"https://#{redirect_host}#{r.path}\") }\n      ctx = OpenSSL::SSL::SSLContext.new\n      ctx.ciphers = 'ECDH+aRSA'\n      Polyphony::Net.setup_alpn(ctx, Tipi::ALPN_PROTOCOLS)\n      certificate_store = create_certificate_store\n\n      proc do\n        challenge_handler = Tipi::ACME::HTTPChallengeHandler.new\n        certificate_manager = Tipi::ACME::CertificateManager.new(\n          master_ctx: ctx,\n          store: certificate_store,\n          challenge_handler: challenge_handler\n        )\n        http_app = certificate_manager.challenge_routing_app(redirect_app)\n\n        http_listener = spin_accept_loop('HTTP', http_port) do |socket|\n          Tipi.client_loop(socket, @opts, &http_app)\n        end\n\n        ssl_accept_thread_pool = Polyphony::ThreadPool.new(4)\n\n        https_listener = spin_accept_loop('HTTPS', https_port) do |socket|\n          start_https_connection_fiber(socket, ctx, ssl_accept_thread_pool, app)\n        rescue Exception => e\n          puts \"Exception in https_listener block: #{e.inspect}\\n#{e.backtrace.inspect}\"\n        end\n      end\n\n    end\n\n    INVALID_PATH_REGEXP = /\\/?(\\.\\.|\\.)\\//\n\n    def find_path(base, path)\n      return nil if path =~ INVALID_PATH_REGEXP\n\n      full_path = File.join(base, path)\n      return full_path if File.file?(full_path)\n      return find_path(full_path, 'index') if File.directory?(full_path)\n\n      qualified = \"#{full_path}.html\"\n      return qualified if File.file?(qualified)\n\n      nil\n    end\n\n    SOCKET_OPTS = {\n      reuse_addr:   true,\n      reuse_port:   true,\n      dont_linger:  true,\n    }.freeze\n\n    def start_listener(name, port, &block)\n      host = '0.0.0.0'\n      socket = ::Socket.new(:INET, :STREAM).tap do |s|\n        s.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)\n        s.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)\n        s.setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, [0, 0].pack('ii'))\n        addr = ::Socket.sockaddr_in(port, host)\n        s.bind(addr)\n        s.listen(Socket::SOMAXCONN)\n      end\n      listener = Listener.new(socket, &block)\n      @evloop.watch_io(listener, socket, false, false)\n    end\n\n    def spin_accept_loop(name, port, &block)\n      spin do\n        server = Polyphony::Net.tcp_listen('0.0.0.0', port, SOCKET_OPTS)\n        loop do\n          socket = server.accept\n          spin_connection_handler(name, socket, block)\n        rescue Polyphony::BaseException => e\n          raise\n        rescue Exception => e\n          puts \"#{name} listener uncaught exception: #{e.inspect}\"\n        end\n      ensure\n        finalize_listener(server) if server\n      end\n    end\n\n    def spin_connection_handler(name, socket, block)\n      spin do\n        block.(socket)\n      rescue Polyphony::BaseException\n        raise\n      rescue Exception => e\n        puts \"Uncaught error in #{name} handler: #{e.inspect}\"\n        p e.backtrace\n      end\n    end\n\n    def finalize_listener(server)\n      fiber  = Fiber.current\n      gracefully_terminate_conections(fiber) if fiber.graceful_shutdown?\n      server.close\n    rescue Polyphony::BaseException\n      raise\n    rescue Exception => e\n      trace \"Exception in finalize_listener: #{e.inspect}\"\n    end\n\n    def gracefully_terminate_conections(fiber)\n      supervisor = spin { supervise }.detach\n      fiber.attach_all_children_to(supervisor)\n\n      # terminating the supervisor will\n      supervisor.terminate(graceful: true)\n    end\n\n    def add_connection_headers(app)\n      app\n      # proc do |req|\n      #   conn = req.adapter.conn\n      #   # req.headers[':peer'] = conn.peeraddr(false)[2]\n      #   req.headers[':scheme'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'\n      #   app.(req)\n      # end\n    end\n\n    def ssl_accept(client)\n      client.accept\n      true\n    rescue Polyphony::BaseException\n      raise\n    rescue Exception => e\n      p e\n      e\n    end\n\n    def start_https_connection_fiber(socket, ctx, thread_pool, app)\n      client = OpenSSL::SSL::SSLSocket.new(socket, ctx)\n      client.sync_close = true\n\n      result = thread_pool ?\n        thread_pool.process { ssl_accept(client) } : ssl_accept(client)\n\n      if result.is_a?(Exception)\n        puts \"Exception in SSL handshake: #{result.inspect}\"\n        return\n      end\n\n      Tipi.client_loop(client, @opts, &app)\n    rescue => e\n      puts \"Uncaught error in HTTPS connection fiber: #{e.inspect} bt: #{e.backtrace.inspect}\"\n    ensure\n      (client ? client.close : socket.close) rescue nil\n    end\n\n    CERTIFICATE_STORE_DEFAULT_DIR = File.expand_path('~/.tipi').freeze\n    CERTIFICATE_STORE_DEFAULT_DB_PATH = File.join(\n      CERTIFICATE_STORE_DEFAULT_DIR, 'certificates.db').freeze\n\n    def create_certificate_store\n      FileUtils.mkdir(CERTIFICATE_STORE_DEFAULT_DIR) rescue nil\n      Tipi::ACME::SQLiteCertificateStore.new(CERTIFICATE_STORE_DEFAULT_DB_PATH)\n    end\n\n    def start_server(service)\n      service.call\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/controller.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'json'\n\n# get opts from STDIN\nopts = JSON.parse(ARGV[0]) rescue nil\nmod_path = \"./controller/#{opts['app_type']}_#{opts['mode']}\"\nrequire_relative mod_path\n\ncontroller = Tipi::Controller.new(opts)\ncontroller.run\n"
  },
  {
    "path": "lib/tipi/digital_fabric/agent.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative './protocol'\nrequire_relative './request_adapter'\n\nrequire 'msgpack'\nrequire 'tipi/websocket'\nrequire 'tipi/request'\n\nmodule DigitalFabric\n  class Agent\n    def initialize(server_url, route, token)\n      @server_url = server_url\n      @route = route\n      @token = token\n      @requests = {}\n      @long_running_requests = {}\n      @name = '<unknown>'\n    end\n\n    class TimeoutError < RuntimeError\n    end\n\n    class GracefulShutdown < RuntimeError\n    end\n\n    @@id = 0\n\n    def run\n      @fiber = Fiber.current\n      @keep_alive_timer = spin_loop(\"#{@fiber.tag}-keep_alive\", interval: 5) { keep_alive }\n      while true\n        connect_and_process_incoming_requests\n        return if @shutdown\n        sleep 5\n      end\n    ensure\n      @keep_alive_timer.stop\n    end\n\n    def connect_and_process_incoming_requests\n      # log 'Connecting...'\n      @socket = connect_to_server\n      @last_recv = @last_send = Time.now\n\n      df_upgrade\n      @connected = true\n      @msgpack_reader = MessagePack::Unpacker.new\n\n      process_incoming_requests\n    rescue IOError, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE, TimeoutError\n      log 'Disconnected' if @connected\n      @connected = nil\n    end\n\n    def connect_to_server\n      if @server_url =~ /^([^\\:]+)\\:(\\d+)$/\n        host = Regexp.last_match(1)\n        port = Regexp.last_match(2)\n        Polyphony::Net.tcp_connect(host, port)\n      else\n        UNIXSocket.new(@server_url)\n      end\n    end\n\n    UPGRADE_REQUEST = <<~HTTP\n    GET / HTTP/1.1\n    Host: localhost\n    Connection: upgrade\n    Upgrade: df\n    DF-Token: %s\n    DF-Mount: %s\n\n    HTTP\n\n    def df_upgrade\n      @socket << format(UPGRADE_REQUEST, @token, mount_point)\n      while (line = @socket.gets)\n        break if line.chomp.empty?\n      end\n      # log 'Connection upgraded'\n    end\n\n    def mount_point\n      if @route[:host]\n        \"host=#{@route[:host]}\"\n      elsif @route[:path]\n        \"path=#{@route[:path]}\"\n      else\n        nil\n      end\n    end\n\n    def log(msg)\n      puts \"#{Time.now} (#{@name}) #{msg}\"\n    end\n\n    def process_incoming_requests\n      @socket.feed_loop(@msgpack_reader, :feed_each) do |msg|\n        recv_df_message(msg)\n        return if @shutdown && @requests.empty?\n      end\n    rescue IOError, SystemCallError, TimeoutError\n      # ignore\n    end\n\n    def keep_alive\n      return unless @connected\n\n      now = Time.now\n      if now - @last_send >= Protocol::SEND_TIMEOUT\n        send_df_message(Protocol.ping)\n      end\n      # if now - @last_recv >= Protocol::RECV_TIMEOUT\n      #   raise TimeoutError\n      # end\n    rescue IOError, SystemCallError => e\n      # transmit exception to fiber running the agent\n      @fiber.raise(e)\n    end\n\n    def recv_df_message(msg)\n      @last_recv = Time.now\n      case msg[Protocol::Attribute::KIND]\n      when Protocol::SHUTDOWN\n        recv_shutdown\n      when Protocol::HTTP_REQUEST\n        recv_http_request(msg)\n      when Protocol::HTTP_REQUEST_BODY\n        recv_http_request_body(msg)\n      when Protocol::WS_REQUEST\n        recv_ws_request(msg)\n      when Protocol::CONN_DATA, Protocol::CONN_CLOSE,\n           Protocol::WS_DATA, Protocol::WS_CLOSE\n        fiber = @requests[msg[Protocol::Attribute::ID]]\n        fiber << msg if fiber\n      end\n    end\n\n    def send_df_message(msg)\n      # we mark long-running requests by applying simple heuristics to sent DF\n      # messages. This is so we can correctly stop long-running requests\n      # upon graceful shutdown\n      if is_long_running_request_response?(msg)\n        id = msg[Protocol::Attribute::ID]\n        @long_running_requests[id] = @requests[id]\n      end\n      @last_send = Time.now\n      @socket << msg.to_msgpack\n    end\n\n    def is_long_running_request_response?(msg)\n      case msg[Protocol::Attribute::KIND]\n      when Protocol::HTTP_UPGRADE\n        true\n      when Protocol::HTTP_RESPONSE\n        !msg[Protocol::Attribute::HttpResponse::COMPLETE]\n      end\n    end\n\n    def recv_shutdown\n      # puts \"Received shutdown message (#{@requests.size} pending requests)\"\n      # puts \"  (Long running requests: #{@long_running_requests.size})\"\n      @shutdown = true\n      @long_running_requests.values.each { |f| f.terminate(graceful: true) }\n    end\n\n    def recv_http_request(msg)\n      req = prepare_http_request(msg)\n      id = msg[Protocol::Attribute::ID]\n      @requests[id] = spin(\"#{Fiber.current.tag}.#{id}\") do\n        http_request(req)\n      rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE\n        # ignore\n      rescue Polyphony::Terminate => e\n        req.respond(nil, { ':status' => Qeweney::Status::SERVICE_UNAVAILABLE }) if Fiber.current.graceful_shutdown?\n        raise e\n      ensure\n        @requests.delete(id)\n        @long_running_requests.delete(id)\n        @fiber.terminate if @shutdown && @requests.empty?\n      end\n    end\n\n    def prepare_http_request(msg)\n      headers = msg[Protocol::Attribute::HttpRequest::HEADERS]\n      body_chunk = msg[Protocol::Attribute::HttpRequest::BODY_CHUNK]\n      complete = msg[Protocol::Attribute::HttpRequest::COMPLETE]\n      req = Qeweney::Request.new(headers, RequestAdapter.new(self, msg))\n      req.buffer_body_chunk(body_chunk) if body_chunk\n      req\n    end\n\n    def recv_http_request_body(msg)\n      fiber = @requests[msg[Protocol::Attribute::ID]]\n      return unless fiber\n\n      fiber << msg[Protocol::Attribute::HttpRequestBody::BODY]\n    end\n\n    def get_http_request_body(id, limit)\n      send_df_message(Protocol.http_get_request_body(id, limit))\n      receive\n    end\n\n    def recv_ws_request(msg)\n      req = Qeweney::Request.new(msg[Protocol::Attribute::WS::HEADERS], RequestAdapter.new(self, msg))\n      id = msg[Protocol::Attribute::ID]\n      @requests[id] = @long_running_requests[id] = spin(\"#{Fiber.current.tag}.#{id}-ws\") do\n        ws_request(req)\n      rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE\n        # ignore\n      ensure\n        @requests.delete(id)\n        @long_running_requests.delete(id)\n        @fiber.terminate if @shutdown && @requests.empty?\n      end\n    end\n\n    # default handler for HTTP request\n    def http_request(req)\n      req.respond(nil, { ':status': Qeweney::Status::SERVICE_UNAVAILABLE })\n    end\n\n    # default handler for WS request\n    def ws_request(req)\n      req.respond(nil, { ':status': Qeweney::Status::SERVICE_UNAVAILABLE })\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/digital_fabric/agent_proxy.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative './protocol'\nrequire 'msgpack'\nrequire 'tipi/websocket'\n\nmodule DigitalFabric\n  class AgentProxy\n    def initialize(service, req)\n      @service = service\n      @req = req\n      @conn = req.adapter.conn\n      @msgpack_reader = MessagePack::Unpacker.new\n      @requests = {}\n      @current_request_count = 0\n      @last_request_id = 0\n      @last_recv = @last_send = Time.now\n      run\n    end\n\n    def current_request_count\n      @current_request_count\n    end\n\n    class TimeoutError < RuntimeError\n    end\n\n    class GracefulShutdown < RuntimeError\n    end\n\n    def run\n      @fiber = Fiber.current\n      @service.mount(route, self)\n      @mounted = true\n      # keep_alive_timer = spin_loop(\"#{@fiber.tag}-keep_alive\", interval: 5) { keep_alive }\n      process_incoming_messages(false)\n    rescue GracefulShutdown\n      puts \"Proxy got graceful shutdown, left: #{@requests.size} requests\" if @requests.size > 0\n      move_on_after(15) { process_incoming_messages(true) }\n    ensure\n      # keep_alive_timer&.stop\n      unmount\n    end\n\n    def process_incoming_messages(shutdown = false)\n      return if shutdown && @requests.empty?\n\n      @conn.feed_loop(@msgpack_reader, :feed_each) do |msg|\n        recv_df_message(msg)\n        return if shutdown && @requests.empty?\n      end\n    rescue TimeoutError, IOError, SystemCallError\n      # ignore and just return in order to terminate the proxy\n    end\n\n    def unmount\n      return unless @mounted\n\n      @service.unmount(self)\n      @mounted = nil\n    end\n\n    def send_shutdown\n      send_df_message(Protocol.shutdown)\n      @fiber.raise GracefulShutdown.new\n    end\n\n    def keep_alive\n      now = Time.now\n      if now - @last_send >= Protocol::SEND_TIMEOUT\n        send_df_message(Protocol.ping)\n      end\n      # if now - @last_recv >= Protocol::RECV_TIMEOUT\n      #   raise TimeoutError\n      # end\n    rescue TimeoutError, IOError\n    end\n\n    def route\n      case @req.headers['df-mount']\n      when /^\\s*host\\s*=\\s*([^\\s]+)/\n        { host: Regexp.last_match(1) }\n      when /^\\s*path\\s*=\\s*([^\\s]+)/\n        { path: Regexp.last_match(1) }\n      when /catch_all/\n        { catch_all: true }\n      else\n        nil\n      end\n    end\n\n    def recv_df_message(message)\n      @last_recv = Time.now\n      # puts \"<<< #{message.inspect}\"\n\n      case message[Protocol::Attribute::KIND]\n      when Protocol::PING\n        return\n      when Protocol::UNMOUNT\n        return unmount\n      when Protocol::STATS_REQUEST\n        return handle_stats_request(message[Protocol::Attribute::ID])\n      end\n\n      handler = @requests[message[Protocol::Attribute::ID]]\n      if !handler\n        # puts \"Unknown request id in #{message}\"\n        return\n      end\n\n      handler << message\n    end\n\n    def send_df_message(message)\n      # puts \">>> #{message.inspect}\" unless message[Protocol::Attribute::KIND] == Protocol::PING\n\n      @last_send = Time.now\n      @conn << message.to_msgpack\n    end\n\n    # HTTP / WebSocket\n\n    def register_request_fiber\n      id = (@last_request_id += 1)\n      @requests[id] = Fiber.current\n      id\n    end\n\n    def unregister_request_fiber(id)\n      @requests.delete(id)\n    end\n\n    def with_request\n      @current_request_count += 1\n      id = (@last_request_id += 1)\n      @requests[id] = Fiber.current\n      yield id\n    ensure\n      @current_request_count -= 1\n      @requests.delete(id)\n    end\n\n    def http_request(req)\n      t0 = Time.now\n      t1 = nil\n      with_request do |id|\n        msg = Protocol.http_request(id, req.headers, req.next_chunk(true), req.complete?)\n        send_df_message(msg)\n        while (message = receive)\n          kind = message[Protocol::Attribute::KIND]\n          unless t1\n            t1 = Time.now\n            if kind == Protocol::HTTP_RESPONSE\n              headers = message[Protocol::Attribute::HttpResponse::HEADERS]\n              status = (headers && headers[':status']) || 200\n              if status < Qeweney::Status::BAD_REQUEST\n                @service.record_latency_measurement(t1 - t0, req)\n              end\n            end\n          end\n          attributes = message[Protocol::Attribute::HttpRequest::HEADERS..-1]\n          return if http_request_message(id, req, kind, attributes)\n        end\n      end\n    rescue => e\n      p \"Internal server error: #{e.inspect}\"\n      puts e.backtrace.join(\"\\n\")\n      http_request_send_error_response(e)\n    end\n\n    def http_request_send_error_response(error)\n      response = format(\"Error: %s\\n%s\", error.inspect, error.backtrace.join(\"\\n\"))\n      req.respond(response, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)\n    rescue IOError, SystemCallError\n      # ignore\n    end\n\n    # @return [Boolean] true if response is complete\n    def http_request_message(id, req, kind, message)\n      case kind\n      when Protocol::HTTP_UPGRADE\n        http_custom_upgrade(id, req, *message)\n        true\n      when Protocol::HTTP_GET_REQUEST_BODY\n        http_get_request_body(id, req, *message)\n        false\n      when Protocol::HTTP_RESPONSE\n        http_response(id, req, *message)\n      else\n        # invalid message\n        true\n      end\n    end\n\n    def send_transfer_count(key, rx, tx)\n      send_df_message(Protocol.transfer_count(key, rx, tx))\n    end\n\n    def handle_stats_request(id)\n      stats = @service.get_stats\n      send_df_message(Protocol.stats_response(id, stats))\n    end\n\n    HTTP_RESPONSE_UPGRADE_HEADERS = { ':status' => Qeweney::Status::SWITCHING_PROTOCOLS }\n\n    def http_custom_upgrade(id, req, headers)\n      # send upgrade response\n      upgrade_headers = headers ?\n       headers.merge(HTTP_RESPONSE_UPGRADE_HEADERS) :\n        HTTP_RESPONSE_UPGRADE_HEADERS\n      req.send_headers(upgrade_headers, true)\n\n      conn = req.adapter.conn\n      reader = spin(\"#{Fiber.current.tag}.#{id}\") do\n        conn.recv_loop do |data|\n          send_df_message(Protocol.conn_data(id, data))\n        end\n      end\n      while (message = receive)\n        return if http_custom_upgrade_message(conn, message)\n      end\n    ensure\n      reader.stop\n    end\n\n    def http_custom_upgrade_message(conn, message)\n      case message[Protocol::Attribute::KIND]\n      when Protocol::CONN_DATA\n        conn << message[:Protocol::Attribute::ConnData::DATA]\n        false\n      when Protocol::CONN_CLOSE\n        true\n      else\n        # invalid message\n        true\n      end\n    end\n\n    def http_response(id, req, body, headers, complete, transfer_count_key)\n      if !req.headers_sent? && complete\n        req.respond(body, headers|| {})\n        if transfer_count_key\n          rx, tx = req.transfer_counts\n          send_transfer_count(transfer_count_key, rx, tx)\n        end\n        true\n      else\n        req.send_headers(headers) if headers && !req.headers_sent?\n        req.send_chunk(body, done: complete) if body or complete\n\n        if complete && transfer_count_key\n          rx, tx = req.transfer_counts\n          send_transfer_count(transfer_count_key, rx, tx)\n        end\n        complete\n      end\n    rescue IOError, SystemCallError\n      # ignore error\n    end\n\n    def http_get_request_body(id, req, limit)\n      case limit\n      when nil\n        body = req.read\n      else\n        limit = limit.to_i\n        body = nil\n        req.each_chunk do |chunk|\n          (body ||= +'') << chunk\n          break if body.bytesize >= limit\n        end\n      end\n      send_df_message(Protocol.http_request_body(id, body, req.complete?))\n    end\n\n    def http_upgrade(req, protocol)\n      if protocol == 'websocket'\n        handle_websocket_upgrade(req)\n      else\n        # other protocol upgrades should be handled by the agent, so we just run\n        # the request normally. The agent is expected to upgrade the connection\n        # using a http_upgrade message. From that moment on, two-way\n        # communication is handled using conn_data and conn_close messages.\n        http_request(req)\n      end\n    end\n\n    def handle_websocket_upgrade(req)\n      with_request do |id|\n        send_df_message(Protocol.ws_request(id, req.headers))\n        response = receive\n        case response[0]\n        when Protocol::WS_RESPONSE\n          headers = response[2] || {}\n          status = headers[':status'] || Qeweney::Status::SWITCHING_PROTOCOLS\n          if status != Qeweney::Status::SWITCHING_PROTOCOLS\n            req.respond(nil, headers)\n            return\n          end\n          ws = Tipi::Websocket.new(req.adapter.conn, req.headers)\n          run_websocket_connection(id, ws)\n        else\n          req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)\n        end\n      end\n    rescue IOError, SystemCallError\n      # ignore\n    end\n\n    def run_websocket_connection(id, websocket)\n      reader = spin(\"#{Fiber.current}.#{id}-ws\") do\n        websocket.recv_loop do |data|\n          send_df_message(Protocol.ws_data(id, data))\n        end\n      end\n      while (message = receive)\n        case message[Protocol::Attribute::KIND]\n        when Protocol::WS_DATA\n          websocket << message[Protocol::Attribute::WS::DATA]\n        when Protocol::WS_CLOSE\n          return\n        else\n          raise \"Unexpected websocket message #{message.inspect}\"\n        end\n      end\n    ensure\n      reader.stop\n      websocket.close\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/digital_fabric/executive/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n  <title>Digital Fabric Executive</title>\n  <style>\n    .hidden { display: none }\n  </style>\n</head>\n<body>\n  <h1>Digital Fabric Executive</h1>\n  <script>\n    function updateStats(update) {\n      for (let k in update.service) {\n        let v = update.service[k];\n        let e = document.querySelector('#' + k);\n        if (e) e.innerText = v;\n      }\n      for (let k in update.machine) {\n        let v = update.machine[k];\n        let e = document.querySelector('#' + k);\n        if (e) e.innerText = v;\n      }\n    }\n\n    function connect() {\n      console.log(\"connecting...\");\n      window.eventSource = new EventSource(\"/stream/stats\");\n\n      window.eventSource.onopen = function(e) {\n        console.log(\"connected\");\n        document.querySelector('#status').innerText = 'connected';\n        document.querySelector('#stats').className = '';\n        return false;\n      }\n\n      window.eventSource.onmessage = function(e) {\n        console.log(\"message\", e.data);\n        updateStats(JSON.parse(e.data));\n      }\n\n      window.eventSource.onerror = function(e) {\n        console.log(\"error\", e);\n        document.querySelector('#status').innerText = 'disconnected';\n        document.querySelector('#stats').className = 'hidden';\n        window.eventSource.close();\n        window.eventSource = null;\n        setTimeout(connect, 5000);\n      }\n    };\n\n    window.onload = connect;\n  </script>\n  <h2 id=\"status\"></h2>\n  <div id=\"stats\" class=\"hidden\">\n    <h2>Service</h2>\n    <p>Request rate: <span id=\"http_request_rate\"></span></p>\n    <p>Error rate: <span id=\"error_rate\"></span></p>\n    <p>Average Latency: <span id=\"average_latency\"></span>s</p>\n    <p>Connected agents: <span id=\"agent_count\"></span></p>\n    <p>Connected clients: <span id=\"connection_count\"></span></p>\n    <p>Concurrent requests: <span id=\"concurrent_requests\"></span></p>\n\n    <h2>Machine</h2>\n    <p>CPU utilization: <span id=\"cpu_utilization\"></span>%</p>\n    <p>Free memory: <span id=\"mem_free\"></span>MB</p>\n    <p>Load average: <span id=\"load_avg\"></span></p>\n  </div>\n</body>\n</html>"
  },
  {
    "path": "lib/tipi/digital_fabric/executive.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'tipi/digital_fabric'\nrequire 'json'\n\nmodule DigitalFabric\n  # agent for managing DF service\n  class Executive\n    INDEX_HTML = IO.read(File.join(__dir__, 'executive/index.html'))\n\n    attr_reader :last_service_stats\n\n    def initialize(service, route = { path: '/executive' })\n      @service = service\n      route[:executive] = true\n      @service.mount(route, self)\n      @current_request_count = 0\n      # @updater = spin_loop(:executive_updater, interval: 10) { update_service_stats }\n      update_service_stats\n    end\n\n    def current_request_count\n      @current_request_count\n    end\n\n    def http_request(req)\n      @current_request_count += 1\n      case req.path\n      when '/'\n        req.respond(INDEX_HTML, 'Content-Type' => 'text/html')\n      when '/stats'\n        message = last_service_stats\n        req.respond(message.to_json, { 'Content-Type' => 'text.json' })\n      when '/stream/stats'\n        stream_stats(req)\n      when '/upload'\n        req.respond(\"body: #{req.read.inspect}\")\n      else\n        req.respond('Invalid path', { ':status' => Qeweney::Status::NOT_FOUND })\n      end\n    rescue => e\n      puts \"Error: #{e.inspect}\"\n    ensure\n      @current_request_count -= 1\n    end\n\n    def stream_stats(req)\n      req.send_headers({ 'Content-Type' => 'text/event-stream' })\n\n      every(10) do\n        message = last_service_stats\n        req.send_chunk(format_sse_event(message.to_json))\n      end\n    rescue IOError, SystemCallError\n      # ignore\n    ensure\n      req.send_chunk(\"retry: 0\\n\\n\", true) rescue nil\n    end\n\n    def format_sse_event(data)\n      \"data: #{data}\\n\\n\"\n    end\n\n    def update_service_stats\n      @last_service_stats = {\n        service: @service.stats,\n        machine: machine_stats\n      }\n    end\n\n    TOP_CPU_REGEXP = /%Cpu(.+)/.freeze\n    TOP_CPU_IDLE_REGEXP = /([\\d\\.]+) id/.freeze\n    TOP_MEM_REGEXP = /MiB Mem(.+)/.freeze\n    TOP_MEM_FREE_REGEXP = /([\\d\\.]+) free/.freeze\n    LOADAVG_REGEXP = /^([\\d\\.]+)/.freeze\n\n    def machine_stats\n      top = `top -bn1 | head -n4`\n      unless top =~ TOP_CPU_REGEXP && Regexp.last_match(1) =~ TOP_CPU_IDLE_REGEXP\n        p top =~ TOP_CPU_REGEXP\n        p Regexp.last_match(1)\n        p Regexp.last_match(1) =~ TOP_CPU_IDLE_REGEXP\n        raise 'Invalid output from top (cpu)'\n      end\n      cpu_utilization = 100 - Regexp.last_match(1).to_i\n\n      unless top =~ TOP_MEM_REGEXP && Regexp.last_match(1) =~ TOP_MEM_FREE_REGEXP\n        raise 'Invalid output from top (mem)'\n      end\n\n      mem_free = Regexp.last_match(1).to_f\n\n      stats = `cat /proc/loadavg`\n      raise 'Invalid output from /proc/loadavg' unless stats =~ LOADAVG_REGEXP\n      load_avg = Regexp.last_match(1).to_f\n\n      {\n        mem_free: mem_free,\n        cpu_utilization: cpu_utilization,\n        load_avg: load_avg\n      }\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/digital_fabric/protocol.rb",
    "content": "# frozen_string_literal: true\n\nmodule DigitalFabric\n  module Protocol\n    PING = 'ping'\n    SHUTDOWN = 'shutdown'\n    UNMOUNT = 'unmount'\n\n    HTTP_REQUEST = 'http_request'\n    HTTP_RESPONSE = 'http_response'\n    HTTP_UPGRADE = 'http_upgrade'\n    HTTP_GET_REQUEST_BODY = 'http_get_request_body'\n    HTTP_REQUEST_BODY = 'http_request_body'\n\n    CONN_DATA = 'conn_data'\n    CONN_CLOSE = 'conn_close'\n\n    WS_REQUEST = 'ws_request'\n    WS_RESPONSE = 'ws_response'\n    WS_DATA = 'ws_data'\n    WS_CLOSE = 'ws_close'\n\n    TRANSFER_COUNT = 'transfer_count'\n\n    STATS_REQUEST = 'stats_request'\n    STATS_RESPONSE = 'stats_response'\n\n    SEND_TIMEOUT = 15\n    RECV_TIMEOUT = SEND_TIMEOUT + 5\n\n    module Attribute\n      KIND = 0\n      ID = 1\n\n      module HttpRequest\n        HEADERS = 2\n        BODY_CHUNK = 3\n        COMPLETE = 4\n      end\n\n      module HttpResponse\n        BODY = 2\n        HEADERS = 3\n        COMPLETE = 4\n        TRANSFER_COUNT_KEY = 5\n      end\n\n      module HttpUpgrade\n        HEADERS = 2\n      end\n\n      module HttpGetRequestBody\n        LIMIT = 2\n      end\n\n      module HttpRequestBody\n        BODY = 2\n        COMPLETE = 3\n      end\n\n      module ConnectionData\n        DATA = 2\n      end\n\n      module WS\n        HEADERS = 2\n        DATA = 2\n      end\n\n      module TransferCount\n        KEY = 1\n        RX = 2\n        TX = 3\n      end\n\n      module Stats\n        STATS = 2\n      end\n    end\n\n    class << self\n      def ping\n        [ PING ]\n      end\n\n      def shutdown\n        [ SHUTDOWN ]\n      end\n\n      def unmount\n        [ UNMOUNT ]\n      end\n\n      DF_UPGRADE_RESPONSE = <<~HTTP.gsub(\"\\n\", \"\\r\\n\")\n        HTTP/1.1 101 Switching Protocols\n        Upgrade: df\n        Connection: Upgrade\n\n      HTTP\n\n      def df_upgrade_response\n        DF_UPGRADE_RESPONSE\n      end\n\n      def http_request(id, headers, buffered_chunk, complete)\n        [ HTTP_REQUEST, id, headers, buffered_chunk, complete ]\n      end\n\n      def http_response(id, body, headers, complete, transfer_count_key = nil)\n        [ HTTP_RESPONSE, id, body, headers, complete, transfer_count_key ]\n      end\n\n      def http_upgrade(id, headers)\n        [ HTTP_UPGRADE, id, headers ]\n      end\n\n      def http_get_request_body(id, limit = nil)\n        [ HTTP_GET_REQUEST_BODY, id, limit ]\n      end\n\n      def http_request_body(id, body, complete)\n        [ HTTP_REQUEST_BODY, id, body, complete ]\n      end\n\n      def connection_data(id, data)\n        [ CONN_DATA, id, data ]\n      end\n\n      def connection_close(id)\n        [ CONN_CLOSE, id ]\n      end\n\n      def ws_request(id, headers)\n        [ WS_REQUEST, id, headers ]\n      end\n\n      def ws_response(id, headers)\n        [ WS_RESPONSE, id, headers ]\n      end\n\n      def ws_data(id, data)\n        [ WS_DATA, id, data ]\n      end\n\n      def ws_close(id)\n        [ WS_CLOSE, id ]\n      end\n\n      def transfer_count(key, rx, tx)\n        [ TRANSFER_COUNT, key, rx, tx ]\n      end\n\n      def stats_request(id)\n        [ STATS_REQUEST, id ]\n      end\n\n      def stats_response(id, stats)\n        [ STATS_RESPONSE, id, stats ]\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/digital_fabric/request_adapter.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative './protocol'\n\nmodule DigitalFabric\n  class RequestAdapter\n    def initialize(agent, msg)\n      @agent = agent\n      @id = msg[Protocol::Attribute::ID]\n    end\n\n    def protocol\n      'df'\n    end\n\n    def get_body_chunk(request)\n      @agent.get_http_request_body(@id, 1)\n    end\n\n    def respond(request, body, headers)\n      @agent.send_df_message(\n        Protocol.http_response(@id, body, headers, true)\n      )\n    end\n\n    def send_headers(request, headers, opts = {})\n      @agent.send_df_message(\n        Protocol.http_response(@id, nil, headers, false)\n      )\n  end\n\n    def send_chunk(request, body, done: )\n      @agent.send_df_message(\n        Protocol.http_response(@id, body, nil, done)\n      )\n    end\n\n    def finish(request)\n      @agent.send_df_message(\n        Protocol.http_response(@id, nil, nil, true)\n      )\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/digital_fabric/service.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative './protocol'\nrequire_relative './agent_proxy'\nrequire 'securerandom'\n\nmodule DigitalFabric\n  class Service\n    attr_reader :token\n    attr_reader :timer\n\n    def initialize(token: )\n      @token = token\n      @agents = {}\n      @routes = {}\n      @counters = {\n        connections: 0,\n        http_requests: 0,\n        errors: 0\n      }\n      @connection_count = 0\n      @current_request_count = 0\n      @http_latency_accumulator = 0\n      @http_latency_counter = 0\n      @http_latency_max = 0\n      @last_counters = @counters.merge(stamp: Time.now.to_f - 1)\n      @fiber = Fiber.current\n      # @timer = Polyphony::Timer.new('service_timer', resolution: 5)\n    end\n\n    def calculate_stats\n      now = Time.now.to_f\n      elapsed = now - @last_counters[:stamp]\n      connections = @counters[:connections] - @last_counters[:connections]\n      http_requests = @counters[:http_requests] - @last_counters[:http_requests]\n      errors = @counters[:errors] - @last_counters[:errors]\n      @last_counters = @counters.merge(stamp: now)\n\n      average_latency = @http_latency_counter == 0 ? 0 :\n                        @http_latency_accumulator / @http_latency_counter\n      @http_latency_accumulator = 0\n      @http_latency_counter = 0\n      max_latency = @http_latency_max\n      @http_latency_max = 0\n\n      cpu, rss = pid_cpu_and_rss(Process.pid)\n\n      backend_stats = Thread.backend.stats\n      op_rate = backend_stats[:op_count] / elapsed\n      switch_rate = backend_stats[:switch_count] / elapsed\n      poll_rate = backend_stats[:poll_count] / elapsed\n\n      object_space_stats = ObjectSpace.count_objects\n\n      {\n        service: {\n          agent_count: @agents.size,\n          connection_count: @connection_count,\n          connection_rate: connections / elapsed,\n          error_rate: errors / elapsed,\n          http_request_rate: http_requests / elapsed,\n          latency_avg: average_latency,\n          latency_max: max_latency,\n          pending_requests: @current_request_count,\n          },\n        backend: {\n          op_rate: op_rate,\n          pending_ops: backend_stats[:pending_ops],\n          poll_rate: poll_rate,\n          runqueue_size: backend_stats[:runqueue_size],\n          runqueue_high_watermark: backend_stats[:runqueue_max_length],\n          switch_rate: switch_rate,\n\n        },\n        process: {\n          cpu_usage: cpu,\n          rss: rss.to_f / 1024,\n          objects_total: object_space_stats[:TOTAL],\n          objects_free:  object_space_stats[:FREE]\n        }\n      }\n    end\n\n    def pid_cpu_and_rss(pid)\n      s = `ps -p #{pid} -o %cpu,rss`\n      cpu, rss = s.lines[1].chomp.strip.split(' ')\n      [cpu.to_f, rss.to_i]\n    rescue Polyphony::BaseException\n      raise\n    rescue Exception\n      [nil, nil]\n    end\n\n    def get_stats\n      calculate_stats\n    end\n\n    def incr_connection_count\n      @connection_count += 1\n    end\n\n    def decr_connection_count\n      @connection_count -= 1\n    end\n\n    attr_reader :stats\n\n    def total_request_count\n      count = 0\n      @agents.keys.each do |agent|\n        if agent.respond_to?(:current_request_count)\n          count += agent.current_request_count\n        end\n      end\n      count\n    end\n\n    def record_latency_measurement(latency, req)\n      @http_latency_accumulator += latency\n      @http_latency_counter += 1\n      @http_latency_max = latency if latency > @http_latency_max\n      return if latency < 1.0\n\n      puts format('slow request (%.1f): %p', latency, req.headers)\n    end\n\n    def http_request(req, allow_df_upgrade = false)\n      @current_request_count += 1\n      @counters[:http_requests] += 1\n      @counters[:connections] += 1 if req.headers[':first']\n\n      return upgrade_request(req, allow_df_upgrade) if req.upgrade_protocol\n\n      inject_request_headers(req)\n      agent = find_agent(req)\n      unless agent\n        @counters[:errors] += 1\n        return req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)\n      end\n\n      agent.http_request(req)\n    rescue IOError, SystemCallError, HTTP2::Error::StreamClosed\n      @counters[:errors] += 1\n    rescue => e\n      @counters[:errors] += 1\n      puts '*' * 40\n      p req\n      p e\n      puts e.backtrace.join(\"\\n\")\n      req.respond(e.inspect, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)\n    ensure\n      @current_request_count -= 1\n      req.adapter.conn.close if @shutdown\n    end\n\n    def inject_request_headers(req)\n      req.headers['x-request-id'] = SecureRandom.uuid\n      conn = req.adapter.conn\n      req.headers['x-forwarded-for'] = conn.peeraddr(false)[2]\n      req.headers['x-forwarded-proto'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'\n    end\n\n    def upgrade_request(req, allow_df_upgrade)\n      case (protocol = req.upgrade_protocol)\n      when 'df'\n        if allow_df_upgrade\n          df_upgrade(req)\n        else\n          req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)\n        end\n      else\n        agent = find_agent(req)\n        unless agent\n          @counters[:errors] += 1\n          return req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)\n        end\n\n        agent.http_upgrade(req, protocol)\n      end\n    end\n\n    def df_upgrade(req)\n      # we don't want to count connected agents\n      @current_request_count -= 1\n      if req.headers['df-token'] != @token\n        return req.respond(nil, ':status' => Qeweney::Status::FORBIDDEN)\n      end\n\n      req.adapter.conn << Protocol.df_upgrade_response\n      AgentProxy.new(self, req)\n    ensure\n      @current_request_count += 1\n    end\n\n    def mount(route, agent)\n      if route[:path]\n        route[:path_regexp] = path_regexp(route[:path])\n      end\n      @executive = agent if route[:executive]\n      @agents[agent] = route\n      @routing_changed = true\n    end\n\n    def unmount(agent)\n      route = @agents[agent]\n      return unless route\n\n      @executive = nil if route[:executive]\n      @agents.delete(agent)\n      @routing_changed = true\n    end\n\n    INVALID_HOST = 'INVALID_HOST'\n\n    def find_agent(req)\n      compile_agent_routes if @routing_changed\n\n      host = req.headers[':authority'] || req.headers['host'] || INVALID_HOST\n      path = req.headers[':path']\n\n      route = @route_keys.find do |route|\n        (host == route[:host]) || (path =~ route[:path_regexp])\n      end\n      return @routes[route] if route\n\n      nil\n    end\n\n    def compile_agent_routes\n      @routing_changed = false\n\n      @routes.clear\n      @agents.keys.reverse.each do |agent|\n        route = @agents[agent]\n        @routes[route] ||= agent\n      end\n      @route_keys = @routes.keys\n    end\n\n    def path_regexp(path)\n      /^#{path}/\n    end\n\n    def graceful_shutdown\n      @shutdown = true\n      @agents.keys.each do |agent|\n        if agent.respond_to?(:send_shutdown)\n          agent.send_shutdown\n        else\n          @agents.delete(agent)\n        end\n      end\n      move_on_after(60) do\n        while !@agents.empty?\n          sleep 0.25\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/digital_fabric.rb",
    "content": "module DigitalFabric\nend\n\n::DF = DigitalFabric\n\nrequire_relative 'digital_fabric/service'\nrequire_relative 'digital_fabric/agent_proxy'\n"
  },
  {
    "path": "lib/tipi/handler.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative './rack_adapter'\nrequire_relative './http1_adapter'\nrequire_relative './http2_adapter'\n\nmodule Tipi\n  class DefaultHandler\n    def initialize(config)\n      @config = config\n\n      app_path = ARGV.first || './config.ru'\n      @app = Tipi::RackAdapter.load(app_path)\n    end\n\n    def call(socket)\n      socket.no_delay if socket.respond_to?(:no_delay)\n      adapter = protocol_adapter(socket, {})\n      adapter.each(&@app)\n    ensure\n      socket.close\n    end\n\n    ALPN_PROTOCOLS = %w[h2 http/1.1].freeze\n    H2_PROTOCOL = 'h2'\n\n    def protocol_adapter(socket, opts)\n      use_http2 = socket.respond_to?(:alpn_protocol) &&\n                  socket.alpn_protocol == H2_PROTOCOL\n\n      klass = use_http2 ? HTTP2Adapter : HTTP1Adapter\n      klass.new(socket, opts)\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/http1_adapter.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'h1p'\nrequire 'qeweney/request'\n\nrequire_relative './http2_adapter'\n\nmodule Tipi\n  # HTTP1 protocol implementation\n  class HTTP1Adapter\n    attr_reader :conn\n\n    # Initializes a protocol adapter instance\n    def initialize(conn, opts)\n      @conn = conn\n      @opts = opts\n      @first = true\n      @parser = H1P::Parser.new(@conn, :server)\n    end\n\n    def each(&block)\n      while true\n        headers = @parser.parse_headers\n        break unless headers\n\n        # handle_request returns true if connection is not persistent or was\n        # upgraded\n        break if handle_request(headers, &block)\n      end\n    rescue SystemCallError, IOError, H1P::Error\n      # connection or parser error, ignore\n    ensure\n      finalize_client_loop\n    end\n\n    def handle_request(headers, &block)\n      scheme = (proto = headers['x-forwarded-proto']) ?\n                proto.downcase : scheme_from_connection\n      headers[':scheme'] = scheme\n      @protocol = headers[':protocol']\n      if @first\n        headers[':first'] = true\n        @first = nil\n      end\n\n      return true if upgrade_connection(headers, &block)\n\n      request = Qeweney::Request.new(headers, self)\n      if !@parser.complete?\n        request.buffer_body_chunk(@parser.read_body_chunk(true))\n      end\n      block.call(request)\n      return !persistent_connection?(headers)\n    end\n\n    def persistent_connection?(headers)\n      if headers[':protocol'] == 'http/1.1'\n        return headers['connection'] != 'close'\n      else\n        connection = headers['connection']\n        return connection && connection != 'close'\n      end\n    end\n\n    def finalize_client_loop\n      @parser = nil\n      @splicing_pipe = nil\n      @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil\n      @conn.close\n    end\n\n    # Reads a body chunk for the current request. Transfers control to the parse\n    # loop, and resumes once the parse_loop has fired the on_body callback\n    def get_body_chunk(request, buffered_only = false)\n      @parser.read_body_chunk(buffered_only)\n    end\n\n    def get_body(request)\n      @parser.read_body\n    end\n\n    def complete?(request)\n      @parser.complete?\n    end\n\n    def protocol\n      @protocol\n    end\n\n    # Upgrades the connection to a different protocol, if the 'Upgrade' header is\n    # given. By default the only supported upgrade protocol is HTTP2. Additional\n    # protocols, notably WebSocket, can be specified by passing a hash to the\n    # :upgrade option when starting a server:\n    #\n    #     def ws_handler(conn)\n    #       conn << 'hi'\n    #       msg = conn.recv\n    #       conn << \"You said #{msg}\"\n    #       conn << 'bye'\n    #       conn.close\n    #     end\n    #\n    #     opts = {\n    #       upgrade: {\n    #         websocket: Tipi::Websocket.handler(&method(:ws_handler))\n    #       }\n    #     }\n    #     Tipi.serve('0.0.0.0', 1234, opts) { |req| ... }\n    #\n    # @param headers [Hash] request headers\n    # @return [boolean] truthy if the connection has been upgraded\n    def upgrade_connection(headers, &block)\n      upgrade_protocol = headers['upgrade']\n      return nil unless upgrade_protocol\n\n      upgrade_protocol = upgrade_protocol.downcase.to_sym\n      upgrade_handler = @opts[:upgrade] && @opts[:upgrade][upgrade_protocol]\n      return upgrade_with_handler(upgrade_handler, headers) if upgrade_handler\n      return upgrade_to_http2(headers, &block) if upgrade_protocol == :h2c\n\n      nil\n    end\n\n    def upgrade_with_handler(handler, headers)\n      @parser = nil\n      handler.(self, headers)\n      true\n    end\n\n    def upgrade_to_http2(headers, &block)\n      headers = http2_upgraded_headers(headers)\n      body = @parser.read_body\n      HTTP2Adapter.upgrade_each(@conn, @opts, headers, body, &block)\n      true\n    end\n\n    # Returns headers for HTTP2 upgrade\n    # @param headers [Hash] request headers\n    # @return [Hash] headers for HTTP2 upgrade\n    def http2_upgraded_headers(headers)\n      headers.merge(\n        ':scheme'    => 'http',\n        ':authority' => headers['host']\n      )\n    end\n\n    def websocket_connection(request)\n      Tipi::Websocket.new(@conn, request.headers)\n    end\n\n    def scheme_from_connection\n      @conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'\n    end\n\n    # response API\n\n    CRLF = \"\\r\\n\"\n\n    # Sends response including headers and body. Waits for the request to complete\n    # if not yet completed. The body is sent using chunked transfer encoding.\n    # @param request [Qeweney::Request] HTTP request\n    # @param body [String] response body\n    # @param headers\n    def respond(request, body, headers)\n      written = H1P.send_response(@conn, headers, body)\n      request.tx_incr(written)\n    end\n\n    CHUNK_LENGTH_PROC = ->(len) { \"#{len.to_s(16)}\\r\\n\" }\n\n    def respond_from_io(request, io, headers, chunk_size = 2**14)\n      formatted_headers = format_headers(headers, true, true)\n      request.tx_incr(formatted_headers.bytesize)\n\n      # assume chunked encoding\n      Thread.current.backend.splice_chunks(\n        io,\n        @conn,\n        formatted_headers,\n        \"0\\r\\n\\r\\n\",\n        CHUNK_LENGTH_PROC,\n        \"\\r\\n\",\n        chunk_size\n      )\n    end\n\n    # Sends response headers. If empty_response is truthy, the response status\n    # code will default to 204, otherwise to 200.\n    # @param request [Qeweney::Request] HTTP request\n    # @param headers [Hash] response headers\n    # @param empty_response [boolean] whether a response body will be sent\n    # @param chunked [boolean] whether to use chunked transfer encoding\n    # @return [void]\n    def send_headers(request, headers, empty_response: false, chunked: true)\n      formatted_headers = format_headers(headers, !empty_response, http1_1?(request) && chunked)\n      request.tx_incr(formatted_headers.bytesize)\n      @conn.write(formatted_headers)\n    end\n\n    def http1_1?(request)\n      request.headers[':protocol'] == 'http/1.1'\n    end\n\n    # Sends a response body chunk. If no headers were sent, default headers are\n    # sent using #send_headers. if the done option is true(thy), an empty chunk\n    # will be sent to signal response completion to the client.\n    # @param request [Qeweney::Request] HTTP request\n    # @param chunk [String] response body chunk\n    # @param done [boolean] whether the response is completed\n    # @return [void]\n    def send_chunk(request, chunk, done: false)\n      if done\n        data = chunk ?\n          \"#{chunk.bytesize.to_s(16)}\\r\\n#{chunk}\\r\\n0\\r\\n\\r\\n\" :\n          \"0\\r\\n\\r\\n\"\n      elsif chunk\n        data = \"#{chunk.bytesize.to_s(16)}\\r\\n#{chunk}\\r\\n\"\n      else\n        return\n      end\n\n      request.tx_incr(data.bytesize)\n      @conn.write(data)\n    end\n\n    def send_chunk_from_io(request, io, r, w, chunk_size)\n      len = w.splice(io, chunk_size)\n      if len > 0\n        Thread.current.backend.chain(\n          [:write, @conn, \"#{len.to_s(16)}\\r\\n\"],\n          [:splice, r, @conn, len],\n          [:write, @conn, \"\\r\\n\"]\n        )\n      else\n        @conn.write(\"0\\r\\n\\r\\n\")\n      end\n      len\n    end\n\n    # Finishes the response to the current request. If no headers were sent,\n    # default headers are sent using #send_headers.\n    # @return [void]\n    def finish(request)\n      request.tx_incr(5)\n      @conn << \"0\\r\\n\\r\\n\"\n    end\n\n    def close\n      @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil\n      @conn.close\n    end\n\n    private\n\n    INTERNAL_HEADER_REGEXP = /^:/.freeze\n\n    # Formats response headers into an array. If empty_response is true(thy),\n    # the response status code will default to 204, otherwise to 200.\n    # @param headers [Hash] response headers\n    # @param body [boolean] whether a response body will be sent\n    # @param chunked [boolean] whether to use chunked transfer encoding\n    # @return [String] formatted response headers\n    def format_headers(headers, body, chunked)\n      status = headers[':status']\n      status ||= (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)\n      lines = format_status_line(body, status, chunked)\n      headers.each do |k, v|\n        next if k =~ INTERNAL_HEADER_REGEXP\n\n        collect_header_lines(lines, k, v)\n      end\n      lines << CRLF\n      lines\n    end\n\n    def format_status_line(body, status, chunked)\n      if !body\n        empty_status_line(status)\n      else\n        with_body_status_line(status, body, chunked)\n      end\n    end\n\n    def empty_status_line(status)\n      if status == 204\n        +\"HTTP/1.1 #{status}\\r\\n\"\n      else\n        +\"HTTP/1.1 #{status}\\r\\nContent-Length: 0\\r\\n\"\n      end\n    end\n\n    def with_body_status_line(status, body, chunked)\n      if chunked\n        +\"HTTP/1.1 #{status}\\r\\nTransfer-Encoding: chunked\\r\\n\"\n      else\n        +\"HTTP/1.1 #{status}\\r\\nContent-Length: #{body.is_a?(String) ? body.bytesize : body.to_i}\\r\\n\"\n      end\n    end\n\n    def collect_header_lines(lines, key, value)\n      if value.is_a?(Array)\n        value.inject(lines) { |_, item| lines << \"#{key}: #{item}\\r\\n\" }\n      else\n        lines << \"#{key}: #{value}\\r\\n\"\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/http2_adapter.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'http/2'\nrequire_relative './http2_stream'\n\n# patch to fix bug in HTTP2::Stream\nclass HTTP2::Stream\n  def end_stream?(frame)\n    case frame[:type]\n    when :data, :headers, :continuation\n      frame[:flags]&.include?(:end_stream)\n    else false\n    end\n  end\nend\n\nmodule Tipi\n  # HTTP2 server adapter\n  class HTTP2Adapter\n    def self.upgrade_each(socket, opts, headers, body, &block)\n      adapter = new(socket, opts, headers, body)\n      adapter.each(&block)\n    end\n\n    def initialize(conn, opts, upgrade_headers = nil, upgrade_body = nil)\n      @conn = conn\n      @opts = opts\n      @upgrade_headers = upgrade_headers\n      @upgrade_body = upgrade_body\n      @first = true\n      @rx = (upgrade_headers && upgrade_headers[':rx']) || 0\n      @tx = (upgrade_headers && upgrade_headers[':tx']) || 0\n\n      @interface = ::HTTP2::Server.new\n      @connection_fiber = Fiber.current\n      @interface.on(:frame, &method(:send_frame))\n      @streams = {}\n    end\n\n    def send_frame(data)\n      if @transfer_count_request\n        @transfer_count_request.tx_incr(data.bytesize)\n      end\n      @conn << data\n    rescue Polyphony::BaseException\n      raise\n    rescue Exception => e\n      @connection_fiber.transfer e\n    end\n\n    UPGRADE_MESSAGE = <<~HTTP.gsub(\"\\n\", \"\\r\\n\")\n    HTTP/1.1 101 Switching Protocols\n    Connection: Upgrade\n    Upgrade: h2c\n\n    HTTP\n\n    def upgrade\n      @conn << UPGRADE_MESSAGE\n      @tx += UPGRADE_MESSAGE.bytesize\n      settings = @upgrade_headers['http2-settings']\n      @interface.upgrade(settings, @upgrade_headers, @upgrade_body || '')\n    ensure\n      @upgrade_headers = nil\n    end\n\n    # Iterates over incoming requests\n    def each(&block)\n      @interface.on(:stream) { |stream| start_stream(stream, &block) }\n      upgrade if @upgrade_headers\n\n      @conn.recv_loop do |data|\n        @rx += data.bytesize\n        @interface << data\n      end\n    rescue SystemCallError, IOError, HTTP2::Error::Error\n      # ignore\n    ensure\n      finalize_client_loop\n    end\n\n    def get_rx_count\n      count = @rx\n      @rx = 0\n      count\n    end\n\n    def get_tx_count\n      count = @tx\n      @tx = 0\n      count\n    end\n\n    def start_stream(stream, &block)\n      stream = HTTP2StreamHandler.new(self, stream, @conn, @first, &block)\n      @first = nil if @first\n      @streams[stream] = true\n    end\n\n    def finalize_client_loop\n      @interface = nil\n      @streams.each_key(&:stop)\n      @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil\n      @conn.close\n    end\n\n    def close\n      @conn.shutdown if @conn.respond_to?(:shutdown) rescue nil\n      @conn.close\n    end\n\n    def set_request_for_transfer_count(request)\n      @transfer_count_request = request\n    end\n\n    def unset_request_for_transfer_count(request)\n      return unless @transfer_count_request == request\n\n      @transfer_count_request = nil\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/http2_stream.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'http/2'\nrequire 'qeweney/request'\n\nmodule Tipi\n  # Manages an HTTP 2 stream\n  class HTTP2StreamHandler\n    attr_reader :conn\n\n    def initialize(adapter, stream, conn, first, &block)\n      @adapter = adapter\n      @stream = stream\n      @conn = conn\n      @first = first\n      @connection_fiber = Fiber.current\n      @stream_fiber = spin { run(&block) }\n\n      # Stream callbacks occur on the connection fiber (see HTTP2Adapter#each).\n      # The request handler is run on a separate fiber for each stream, allowing\n      # concurrent handling of incoming requests on the same HTTP/2 connection.\n      #\n      # The different stream adapter APIs suspend the stream fiber, waiting for\n      # stream callbacks to be called. The callbacks, in turn, transfer control to\n      # the stream fiber, effectively causing the return of the adapter API calls.\n      #\n      # Note: the request handler is run once headers are received. Reading the\n      # request body, if present, is at the discretion of the request handler.\n      # This mirrors the behaviour of the HTTP/1 adapter.\n      stream.on(:headers, &method(:on_headers))\n      stream.on(:data, &method(:on_data))\n      stream.on(:half_close, &method(:on_half_close))\n    end\n\n    def run(&block)\n      request = receive\n      error = nil\n      block.(request)\n      @connection_fiber.schedule\n    rescue Polyphony::BaseException\n      raise\n    rescue Exception => e\n      error = e\n    ensure\n      @connection_fiber.schedule error\n    end\n\n    def on_headers(headers)\n      @request = Qeweney::Request.new(headers.to_h, self)\n      @request.rx_incr(@adapter.get_rx_count)\n      @request.tx_incr(@adapter.get_tx_count)\n      if @first\n        @request.headers[':first'] = true\n        @first = false\n      end\n      @stream_fiber << @request\n    end\n\n    def on_data(data)\n      data = data.to_s # chunks might be wrapped in a HTTP2::Buffer\n\n      (@buffered_chunks ||= []) << data\n      @get_body_chunk_fiber&.schedule\n    end\n\n    def on_half_close\n      @get_body_chunk_fiber&.schedule\n      @complete = true\n    end\n\n    def protocol\n      'h2'\n    end\n\n    def with_transfer_count(request)\n      @adapter.set_request_for_transfer_count(request)\n      yield\n    ensure\n      @adapter.unset_request_for_transfer_count(request)\n    end\n\n    def get_body_chunk(request, buffered_only = false)\n      @buffered_chunks ||= []\n      return @buffered_chunks.shift unless @buffered_chunks.empty?\n      return nil if @complete\n\n      begin\n        @get_body_chunk_fiber = Fiber.current\n        suspend\n      ensure\n        @get_body_chunk_fiber = nil\n      end\n      @buffered_chunks.shift\n    end\n\n    def get_body(request)\n      @buffered_chunks ||= []\n      return @buffered_chunks.join if @complete\n\n      while !@complete\n        begin\n          @get_body_chunk_fiber = Fiber.current\n          suspend\n        ensure\n          @get_body_chunk_fiber = nil\n        end\n      end\n      @buffered_chunks.join\n    end\n\n    def complete?(request)\n      @complete\n    end\n\n    # response API\n    def respond(request, body, headers)\n      headers = normalize_status_header(headers)\n      with_transfer_count(request) do\n        @stream.headers(transform_headers(headers))\n        @headers_sent = true\n        @stream.data(body || '')\n      end\n    rescue HTTP2::Error::StreamClosed\n      # ignore\n    end\n\n    def respond_from_io(request, io, headers, chunk_size = 2**16)\n      headers = normalize_status_header(headers)\n      with_transfer_count(request) do\n        @stream.headers(transform_headers(headers))\n        @headers_sent = true\n        while (chunk = io.read(chunk_size))\n          @stream.data(chunk)\n        end\n      end\n    rescue HTTP2::Error::StreamClosed\n      # ignore\n    end\n\n    def transform_headers(headers)\n      headers.each_with_object([]) do |(k, v), a|\n        if v.is_a?(Array)\n          v.each { |vv| a << [k, vv.to_s] }\n        else\n          a << [k, v.to_s]\n        end\n      end\n    end\n\n    def send_headers(request, headers, empty_response: false)\n      return if @headers_sent\n\n      status = empty_response ? Qeweney::Status::NO_CONTENT : Qeweney::Status::OK\n      headers = normalize_status_header(headers, status)\n      with_transfer_count(request) do\n        @stream.headers(transform_headers(headers), end_stream: false)\n      end\n      @headers_sent = true\n    rescue HTTP2::Error::StreamClosed\n      # ignore\n    end\n\n    def send_chunk(request, chunk, done: false)\n      send_headers({}, false) unless @headers_sent\n\n      if chunk\n        with_transfer_count(request) do\n          @stream.data(chunk, end_stream: done)\n        end\n      elsif done\n        @stream.close\n      end\n    rescue HTTP2::Error::StreamClosed\n      # ignore\n    end\n\n    def finish(request)\n      if @headers_sent\n        @stream.close\n      else\n        headers[':status'] ||= Qeweney::Status::NO_CONTENT\n        with_transfer_count(request) do\n          @stream.headers(transform_headers(headers), end_stream: true)\n        end\n      end\n    rescue HTTP2::Error::StreamClosed\n      # ignore\n    end\n\n    def stop\n      return if @complete\n\n      @stream.close\n      @stream_fiber.schedule(Polyphony::MoveOn.new)\n    end\n\n    private\n\n    def normalize_status_header(headers, default_status = Qeweney::Status::OK)\n      if !headers[':status']\n        headers.merge(':status' => default_status.to_s)\n      elsif !headers[':status'].is_a?(String)\n        headers.merge(headers[':status'].to_s)\n      else\n        headers\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/rack_adapter.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'rack'\n\nmodule Tipi\n  module RackAdapter\n    class << self\n      def run(app)\n        ->(req) { respond(req, app.(env(req))) }\n      end\n\n      def load(path)\n        src = IO.read(path)\n        instance_eval(src, path, 1)\n      end\n\n      def env(request)\n        Qeweney.rack_env_from_request(request)\n      end\n\n      def respond(request, (status_code, headers, body))\n        headers[':status'] = status_code.to_s\n\n        content =\n          if body.respond_to?(:to_path)\n            File.open(body.to_path, 'rb') { |f| f.read }\n          else\n            body.first\n          end\n\n        request.respond(content, headers)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/response_extensions.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'qeweney/request'\n\nmodule Tipi\n  module ResponseExtensions\n    SPLICE_CHUNKS_SIZE_THRESHOLD = 2**20\n\n    def serve_io(io, opts)\n      if !opts[:stat] || opts[:stat].size >= SPLICE_CHUNKS_SIZE_THRESHOLD\n        @adapter.respond_from_io(self, io, opts[:headers], opts[:chunk_size] || 2**14)\n      else\n        respond(io.read, opts[:headers] || {})\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/supervisor.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'polyphony'\nrequire 'json'\n\nmodule Tipi\n  module Supervisor\n    class << self\n      def run(opts)\n        puts \"Start supervisor pid: #{Process.pid}\"\n        @opts = opts\n        @controller_watcher = start_controller_watcher\n        supervise_loop\n      end\n\n      def start_controller_watcher\n        spin do\n          cmd = controller_cmd\n          puts \"Starting controller...\"\n          pid = Kernel.spawn(*cmd)\n          @controller_pid = pid\n          puts \"Controller pid: #{pid}\"\n          _pid, status = Polyphony.backend_waitpid(pid)\n          puts \"Controller has terminated with status: #{status.inspect}\"\n          terminated = true\n        ensure\n          if pid && !terminated\n            puts \"Terminate controller #{pid.inspect}\"\n            Polyphony::Process.kill_process(pid)\n          end\n          Fiber.current.parent << pid\n        end\n      end\n\n      def controller_cmd\n        [\n          'ruby',\n          File.join(__dir__, 'controller.rb'),\n          @opts.to_json\n        ]\n      end\n\n      def supervise_loop\n        this_fiber = Fiber.current\n        trap('SIGUSR2') { this_fiber << :replace_controller }\n        loop do\n          case (msg = receive)\n          when :replace_controller\n            replace_controller\n          when Integer\n            pid = msg\n            if pid == @controller_pid\n              puts 'Detected dead controller. Restarting...'\n              exit!\n              @controller_watcher.restart\n            end\n          else\n            raise \"Invalid message received: #{msg.inspect}\"\n          end\n        end\n      end\n\n      def replace_controller\n        puts \"Replacing controller\"\n        old_watcher = @controller_watcher\n        @controller_watcher = start_controller_watcher\n\n        # TODO: we'll want to get some kind of signal from the new controller once it's ready\n        sleep 1\n\n        old_watcher.terminate(graceful: true)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi/version.rb",
    "content": "# frozen_string_literal: true\n\nmodule Tipi\n  VERSION = '0.56'\nend\n"
  },
  {
    "path": "lib/tipi/websocket.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'digest/sha1'\nrequire 'websocket'\n\nmodule Tipi\n  # Websocket connection\n  class Websocket\n    def self.handler(&block)\n      proc do |adapter, headers|\n        req = Qeweney::Request.new(headers, adapter)\n        websocket = req.upgrade_to_websocket\n        block.(websocket)\n      end\n    end\n\n    def initialize(conn, headers)\n      @conn = conn\n      @headers = headers\n      @version = headers['sec-websocket-version'].to_i\n      @reader = ::WebSocket::Frame::Incoming::Server.new(version: @version)\n    end\n\n    def recv\n      if (msg = @reader.next)\n        return msg.to_s\n      end\n\n      @conn.recv_loop do |data|\n        @reader << data\n        if (msg = @reader.next)\n          return msg.to_s\n        end\n      end\n\n      nil\n    end\n\n    def recv_loop\n      if (msg = @reader.next)\n        yield msg.to_s\n      end\n\n      @conn.recv_loop do |data|\n        @reader << data\n        while (msg = @reader.next)\n          yield msg.to_s\n        end\n      end\n    end\n\n    OutgoingFrame = ::WebSocket::Frame::Outgoing::Server\n\n    def send(data)\n      frame = OutgoingFrame.new(\n        version: @version, data: data, type: :text\n      )\n      @conn << frame.to_s\n    end\n    alias_method :<<, :send\n\n    def close\n      @conn.close\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tipi.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'polyphony'\n\nrequire_relative './tipi/http1_adapter'\nrequire_relative './tipi/http2_adapter'\nrequire_relative './tipi/configuration'\nrequire_relative './tipi/response_extensions'\nrequire_relative './tipi/acme'\n\nrequire 'qeweney/request'\n\nclass Qeweney::Request\n  include Tipi::ResponseExtensions\nend\n\nmodule Tipi\n  ALPN_PROTOCOLS = %w[h2 http/1.1].freeze\n  H2_PROTOCOL = 'h2'\n\n  class << self\n    def serve(host, port, opts = {}, &handler)\n      opts[:alpn_protocols] = ALPN_PROTOCOLS\n      server = Polyphony::Net.tcp_listen(host, port, opts)\n      accept_loop(server, opts, &handler)\n    ensure\n      server&.close\n    end\n\n    def listen(host, port, opts = {})\n      opts[:alpn_protocols] = ALPN_PROTOCOLS\n      Polyphony::Net.tcp_listen(host, port, opts).tap do |socket|\n        socket.define_singleton_method(:each) do |&block|\n          ::Tipi.accept_loop(socket, opts, &block)\n        end\n      end\n    end\n\n    def accept_loop(server, opts, &handler)\n      server.accept_loop do |client|\n        spin { client_loop(client, opts, &handler) }\n      rescue OpenSSL::SSL::SSLError\n        # disregard\n      end\n    end\n\n    def client_loop(client, opts, &handler)\n      client.no_delay if client.respond_to?(:no_delay)\n      adapter = protocol_adapter(client, opts)\n      adapter.each(&handler)\n    rescue SystemCallError\n      # disregard\n    ensure\n      client.close rescue nil\n    end\n\n    def protocol_adapter(socket, opts)\n      use_http2 = socket.respond_to?(:alpn_protocol) &&\n                  socket.alpn_protocol == H2_PROTOCOL\n      klass = use_http2 ? HTTP2Adapter : HTTP1Adapter\n      klass.new(socket, opts)\n    end\n\n    def route(&block)\n      proc { |req| req.route(&block) }\n    end\n  end\nend\n"
  },
  {
    "path": "test/coverage.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'coverage'\nrequire 'simplecov'\n\nclass << SimpleCov::LinesClassifier\n  alias_method :orig_whitespace_line?, :whitespace_line?\n  def whitespace_line?(line)\n    line.strip =~ /^(begin|end|ensure|else|\\})|(\\s*rescue\\s.+)$/ || orig_whitespace_line?(line)\n  end\nend\n\nmodule Coverage\n  EXCLUDE = %w{coverage eg helper run\n  }.map { |n| File.expand_path(\"test/#{n}.rb\") }\n\n  LIB_FILES = Dir[\"#{File.join(FileUtils.pwd, 'lib')}/polyphony/**/*.rb\"]\n\n  class << self\n    def relevant_lines_for_filename(filename)\n      @classifier ||= SimpleCov::LinesClassifier.new\n      @classifier.classify(IO.read(filename).lines)\n    end\n\n    def start\n      @result = {}\n      trace = TracePoint.new(:line) do |tp|\n        next if tp.path =~ /\\(/\n\n        absolute = File.expand_path(tp.path)\n        next unless LIB_FILES.include?(absolute)# =~ /^#{LIB_DIR}/\n\n        @result[absolute] ||= relevant_lines_for_filename(absolute)\n        @result[absolute][tp.lineno - 1] = 1\n      end\n      trace.enable\n    end\n\n    def result\n      @result\n    end\n  end\nend\n\nSimpleCov.start"
  },
  {
    "path": "test/eg.rb",
    "content": "# frozen_string_literal: true\n\nmodule Kernel\n  RE_CONST  = /^[A-Z]/.freeze\n  RE_ATTR   = /^@(.+)$/.freeze\n\n  def eg(hash)\n    Module.new.tap do |m|\n      s = m.singleton_class\n      hash.each do |k, v|\n        case k\n        when RE_CONST\n          m.const_set(k, v)\n        when RE_ATTR\n          m.instance_variable_set(k, v)\n        else\n          block = if v.respond_to?(:to_proc)\n                    proc { |*args, &block| instance_exec { v.(*args, &block) } }\n                  else\n                    proc { v }\n                  end\n          s.define_method(k, &block)\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/helper.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\n\nrequire 'fileutils'\nrequire_relative './eg'\n\nrequire_relative './coverage' if ENV['COVERAGE']\n\nrequire 'minitest/autorun'\n\nrequire 'polyphony'\n\n::Exception.__disable_sanitized_backtrace__ = true\n\nclass Minitest::Test\n  def setup\n    # trace \"* setup #{self.name}\"\n    Fiber.current.setup_main_fiber\n    Fiber.current.instance_variable_set(:@auto_watcher, nil)\n    Thread.current.backend.finalize\n    Thread.current.backend = Polyphony::Backend.new\n    sleep 0\n  end\n\n  def teardown\n    # trace \"* teardown #{self.name}\"\n    Fiber.current.shutdown_all_children\n    if Fiber.current.children.size > 0\n      puts \"Children left after #{self.name}: #{Fiber.current.children.inspect}\"\n      exit!\n    end\n    Fiber.current.instance_variable_set(:@auto_watcher, nil)\n  rescue => e\n    puts e\n    puts e.backtrace.join(\"\\n\")\n    exit!\n  end\nend\n\nmodule Kernel\n  def capture_exception\n    yield\n  rescue Exception => e\n    e\n  end\n\n  def trace(*args)\n    STDOUT.orig_write(format_trace(args))\n  end\n\n  def format_trace(args)\n    if args.first.is_a?(String)\n      if args.size > 1\n        format(\"%s: %p\\n\", args.shift, args)\n      else\n        format(\"%s\\n\", args.first)\n      end\n    else\n      format(\"%p\\n\", args.size == 1 ? args.first : args)\n    end\n  end\nend\n\nclass IO\n  # Creates two mockup sockets for simulating server-client communication\n  def self.server_client_mockup\n    server_in, client_out = IO.pipe\n    client_in, server_out = IO.pipe\n\n    server_connection = mockup_connection(server_in, server_out, client_out)\n    client_connection = mockup_connection(client_in, client_out, server_out)\n\n    [server_connection, client_connection]\n  end\n\n  def self.mockup_connection(input, output, output2)\n    eg(\n      __read_method__:  -> { :readpartial },\n      read:             ->(*args) { input.read(*args) },\n      read_loop:        ->(*args, &block) { input.read_loop(*args, &block) },\n      recv_loop:        ->(*args, &block) { input.read_loop(*args, &block) },\n      readpartial:      ->(*args) { input.readpartial(*args) },\n      recv:             ->(*args) { input.readpartial(*args) },\n      '<<':             ->(*args) { output.write(*args) },\n      write:            ->(*args) { output.write(*args) },\n      close:            -> { output.close },\n      eof?:             -> { output2.closed? }\n    )\n  end\nend\n\nmodule Minitest::Assertions\n  def assert_in_range exp_range, act\n    msg = message(msg) { \"Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}\" }\n    assert exp_range.include?(act), msg\n  end\nend\n"
  },
  {
    "path": "test/run.rb",
    "content": "# frozen_string_literal: true\n\nDir.glob(\"#{__dir__}/test_*.rb\").each do |path|\n  require(path)\nend\n"
  },
  {
    "path": "test/test_http_server.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative 'helper'\nrequire 'tipi'\n\nclass String\n  def crlf_lines\n    gsub \"\\n\", \"\\r\\n\"\n  end\nend\n\nclass HTTP1ServerTest < Minitest::Test\n  def teardown\n    @server&.interrupt if @server&.alive?\n    sleep 0.01\n    super\n  end\n\n  def spin_server(opts = {}, &handler)\n    server_connection, client_connection = IO.server_client_mockup\n    coproc = spin do\n      Tipi.client_loop(server_connection, opts, &handler)\n    end\n    [coproc, client_connection, server_connection]\n  end\n\n  def test_that_server_uses_content_length_in_http_1_0\n    @server, connection = spin_server do |req|\n      req.respond('Hello, world!', {})\n    end\n\n    # using HTTP 1.0, server should close connection after responding\n    connection << \"GET / HTTP/1.0\\r\\n\\r\\n\"\n\n    response = connection.readpartial(8192)\n    expected = <<~HTTP.chomp.crlf_lines.chomp\n      HTTP/1.1 200 OK\n      Content-Length: 13\n\n      Hello, world!\n    HTTP\n    assert_equal(expected, response)\n  end\n\n  def test_that_server_uses_chunked_encoding_in_http_1_1\n    @server, connection = spin_server do |req|\n      req.respond('Hello, world!')\n    end\n\n    # using HTTP 1.0, server should close connection after responding\n    connection << \"GET / HTTP/1.1\\r\\n\\r\\n\"\n\n    response = connection.readpartial(8192)\n    expected = <<~HTTP.crlf_lines.chomp\n      HTTP/1.1 200 OK\n      Content-Length: 13\n\n      Hello, world!\n    HTTP\n    assert_equal(expected, response)\n  end\n\n  def test_that_server_maintains_connection_when_using_keep_alives\n    @server, connection = spin_server do |req|\n      req.respond('Hi', {})\n    end\n\n    connection << \"GET / HTTP/1.0\\r\\nConnection: keep-alive\\r\\n\\r\\n\"\n    response = connection.readpartial(8192)\n    sleep 0.01\n    assert !connection.eof?\n    assert_equal(\"HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\n\\r\\nHi\", response)\n\n    connection << \"GET / HTTP/1.1\\r\\n\\r\\n\"\n    response = connection.readpartial(8192)\n    sleep 0.01\n    assert !connection.eof?\n    expected = <<~HTTP.crlf_lines.chomp\n      HTTP/1.1 200 OK\n      Content-Length: 2\n\n      Hi\n    HTTP\n    assert_equal(expected, response)\n\n    connection << \"GET / HTTP/1.0\\r\\n\\r\\n\"\n    response = connection.readpartial(8192)\n    sleep 0.01\n    assert connection.eof?\n    assert_equal(\"HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\n\\r\\nHi\", response)\n  end\n\n  def test_pipelining_client\n    @server, connection = spin_server do |req|\n      if req.headers['foo'] == 'bar'\n        req.respond('Hello, foobar!', {})\n      else\n        req.respond('Hello, world!', {})\n      end\n    end\n\n    connection << \"GET / HTTP/1.1\\r\\n\\r\\nGET / HTTP/1.1\\r\\nFoo: bar\\r\\n\\r\\n\"\n    sleep 0.01\n    response = connection.readpartial(8192)\n\n    expected = <<~HTTP.crlf_lines.chomp\n      HTTP/1.1 200 OK\n      Content-Length: 13\n\n      Hello, world!HTTP/1.1 200 OK\n      Content-Length: 14\n\n      Hello, foobar!\n    HTTP\n    assert_equal(expected, response)\n  end\n\n  def test_body_chunks\n    chunks = []\n    request = nil\n    @server, connection = spin_server do |req|\n      request = req\n      req.send_headers\n      req.each_chunk do |c|\n        chunks << c\n        req << c.upcase\n      end\n      req.finish\n    end\n\n    connection << <<~HTTP.crlf_lines\n      POST / HTTP/1.1\n      Transfer-Encoding: chunked\n\n      6\n      foobar\n    HTTP\n    sleep 0.01\n    assert request\n    assert_equal %w[foobar], chunks\n    assert !request.complete?\n\n    connection << \"6\\r\\nbazbud\\r\\n\"\n    sleep 0.01\n    assert_equal %w[foobar bazbud], chunks\n    assert !request.complete?\n\n    connection << \"0\\r\\n\\r\\n\"\n    sleep 0.01\n    assert_equal %w[foobar bazbud], chunks\n    assert request.complete?\n\n    sleep 0.01\n\n    response = connection.readpartial(8192)\n\n    expected = <<~HTTP.crlf_lines\n      HTTP/1.1 200\n      Transfer-Encoding: chunked\n\n      6\n      FOOBAR\n      6\n      BAZBUD\n      0\n\n    HTTP\n    assert_equal(expected, response)\n  end\n\n  def test_upgrade\n    done = nil\n\n    opts = {\n      upgrade: {\n        echo: lambda do |adapter, _headers|\n          conn = adapter.conn\n          conn << <<~HTTP.crlf_lines\n            HTTP/1.1 101 Switching Protocols\n            Upgrade: echo\n            Connection: Upgrade\n\n          HTTP\n\n          conn.read_loop { |data| conn << data }\n          done = true\n        end\n      }\n    }\n\n    @server, connection = spin_server(opts) do |req|\n      req.respond('Hi')\n    end\n\n    connection << \"GET / HTTP/1.1\\r\\n\\r\\n\"\n    response = connection.readpartial(8192)\n    sleep 0.01\n    assert !connection.eof?\n    expected = <<~HTTP.crlf_lines.chomp\n      HTTP/1.1 200 OK\n      Content-Length: 2\n\n      Hi\n    HTTP\n    assert_equal(expected, response)\n\n    connection << <<~HTTP.crlf_lines\n      GET / HTTP/1.1\n      Upgrade: echo\n      Connection: upgrade\n\n    HTTP\n\n    snooze\n    response = connection.readpartial(8192)\n    snooze\n    assert !connection.eof?\n    expected = <<~HTTP.crlf_lines\n      HTTP/1.1 101 Switching Protocols\n      Upgrade: echo\n      Connection: Upgrade\n\n    HTTP\n    assert_equal(expected, response)\n\n    assert !done\n\n    connection << 'foo'\n    assert_equal 'foo', connection.readpartial(8192)\n\n    connection << 'bar'\n    assert_equal 'bar', connection.readpartial(8192)\n\n    connection.close\n    assert !done\n\n    sleep 0.01\n    assert done\n  end\n\n  def test_big_download\n    chunk_size = 1000\n    chunk_count = 1000\n    chunk = '*' * chunk_size\n    @server, connection = spin_server do |req|\n      req.send_headers\n      chunk_count.times do |i|\n        req << chunk\n        snooze\n      end\n      req.finish\n      req.adapter.close\n    end\n\n    response = +''\n    count = 0\n\n    connection << \"GET / HTTP/1.1\\r\\n\\r\\n\"\n\n    while (data = connection.read(chunk_size))\n      response << data\n      count += 1\n      snooze\n    end\n\n    chunks = \"#{chunk_size.to_s(16)}\\n#{'*' * chunk_size}\\n\" * chunk_count\n    expected = <<~HTTP.crlf_lines\n      HTTP/1.1 200\n      Transfer-Encoding: chunked\n\n      #{chunks}0\n\n    HTTP\n\n    assert_equal expected, response\n    assert count >= chunk_count\n  end\nend\n"
  },
  {
    "path": "test/test_request.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative 'helper'\nrequire 'tipi'\n\nclass String\n  def http_lines\n    gsub \"\\n\", \"\\r\\n\"\n  end\nend\n\nclass RequestHeadersTest < Minitest::Test\n  def teardown\n    @server&.interrupt if @server&.alive?\n    snooze\n    super\n  end\n\n  def spin_server(opts = {}, &handler)\n    server_connection, client_connection = IO.server_client_mockup\n    coproc = spin do\n      Tipi.client_loop(server_connection, opts, &handler)\n    end\n    [coproc, client_connection, server_connection]\n  end\n\n  def test_request_headers\n    req = nil\n    @server, connection = spin_server do |r|\n      req = r\n      req.respond('Hello, world!')\n    end\n\n    connection << \"GET /titi HTTP/1.1\\r\\nHost: blah.com\\r\\nFoo: bar\\r\\nhi: 1\\r\\nHi: 2\\r\\nhi: 3\\r\\n\\r\\n\"\n\n    sleep 0.01\n\n    assert_kind_of Qeweney::Request, req\n    assert_equal 'blah.com', req.headers['host']\n    assert_equal 'bar', req.headers['foo']\n    assert_equal ['1', '2', '3'], req.headers['hi']\n    assert_equal 'GET', req.headers[':method']\n    assert_equal '/titi', req.headers[':path']\n  end\n\n  def test_request_host\n    req = nil\n    @server, connection = spin_server do |r|\n      req = r\n      req.respond('Hello, world!')\n    end\n\n    connection << \"GET /titi HTTP/1.1\\nHost: blah.com\\nFoo: bar\\nhi: 1\\nHi: 2\\nhi: 3\\n\\n\"\n    sleep 0.01\n    assert_equal 'blah.com', req.host\n  end\n\n  def test_request_connection\n    req = nil\n    @server, connection = spin_server do |r|\n      req = r\n      req.respond('Hello, world!')\n    end\n\n    connection << \"GET /titi HTTP/1.1\\nConnection: keep-alive\\nFoo: bar\\nhi: 1\\nHi: 2\\nhi: 3\\n\\n\"\n    sleep 0.01\n    assert_equal 'keep-alive', req.connection\n  end\n\n  def test_request_upgrade_protocol\n    req = nil\n    @server, connection = spin_server do |r|\n      req = r\n      req.respond('Hello, world!')\n    end\n\n    connection << \"GET /titi HTTP/1.1\\nConnection: upgrade\\nUpgrade: foobar\\n\\n\"\n    sleep 0.01\n    assert_equal 'foobar', req.upgrade_protocol\n  end\nend\n"
  },
  {
    "path": "tipi.gemspec",
    "content": "require_relative './lib/tipi/version'\n\nGem::Specification.new do |s|\n  s.name        = 'tipi'\n  s.version     = Tipi::VERSION\n  s.licenses    = ['MIT']\n  s.summary     = 'Tipi - the All-in-one Web Server for Ruby Apps'\n  s.author      = 'Sharon Rosner'\n  s.email       = 'sharon@noteflakes.com'\n  s.files       = `git ls-files`.split\n  s.homepage    = 'http://github.com/digital-fabric/tipi'\n  s.metadata    = {\n    \"source_code_uri\" => \"https://github.com/digital-fabric/tipi\",\n    \"documentation_uri\" => \"https://www.rubydoc.info/gems/tipi\",\n    \"homepage_uri\" => \"https://github.com/digital-fabric/tipi\",\n    \"changelog_uri\" => \"https://github.com/digital-fabric/tipi/blob/master/CHANGELOG.md\"\n  }\n  s.rdoc_options = [\"--title\", \"tipi\", \"--main\", \"README.md\"]\n  s.extra_rdoc_files = [\"README.md\"]\n  s.require_paths = [\"lib\"]\n  s.required_ruby_version = '>= 3.2'\n\n  s.executables   = ['tipi']\n\n  s.add_runtime_dependency      'base64',             '~>0.3'\n  s.add_runtime_dependency      'mutex_m',            '~>0.3'\n\n  s.add_runtime_dependency      'polyphony',          '~>1.4'\n  s.add_runtime_dependency      'ever',               '~>0.2'\n  s.add_runtime_dependency      'qeweney',            '~>0.24'\n  s.add_runtime_dependency      'extralite',          '~>2.13'\n  s.add_runtime_dependency      'h1p',                '~>1.1'\n\n  s.add_runtime_dependency      'http-2',             '~>1.1'\n  s.add_runtime_dependency      'rack',               '>=2.0.8', '<3.3.0'\n  s.add_runtime_dependency      'websocket',          '~>1.2.11'\n  s.add_runtime_dependency      'acme-client',        '~>2.0.26'\n  s.add_runtime_dependency      'localhost',          '~>1.6.0'\n\n  # for digital fabric\n  s.add_runtime_dependency      'msgpack',            '~>1.8.0'\n\n  s.add_development_dependency  'logger',             '~>1.7'\n  s.add_development_dependency  'ostruct',            '~>0.3'\n\n  s.add_development_dependency  'rake',               '~>13.3.0'\n  s.add_development_dependency  'minitest',           '~>5.26.0'\n  s.add_development_dependency  'simplecov',          '~>0.22.0'\n  s.add_development_dependency  'memory_profiler',    '~>1.1.0'\n\n  s.add_development_dependency  'cuba',               '~>4.0.3'\nend\n"
  }
]