├── .rspec ├── Gemfile ├── lib ├── panther │ ├── version.rb │ ├── rack │ │ ├── builder.rb │ │ ├── response.rb │ │ └── request.rb │ └── server.rb ├── panther.rb └── rack │ └── handler │ └── panther.rb ├── .travis.yml ├── example ├── config.ru ├── server ├── app.rb └── keys │ ├── mycert.pem │ ├── cert.crt │ ├── cert.pem │ ├── cert.key │ ├── mykey.pem │ └── key.pem ├── Rakefile ├── bin ├── setup └── console ├── .gitignore ├── script └── cert ├── README.md ├── exe └── panther └── panther.gemspec /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | gem 'byebug' 5 | -------------------------------------------------------------------------------- /lib/panther/version.rb: -------------------------------------------------------------------------------- 1 | module Panther 2 | VERSION = "0.1.0".freeze 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.2 4 | before_install: gem install bundler -v 1.10.5 5 | -------------------------------------------------------------------------------- /example/config.ru: -------------------------------------------------------------------------------- 1 | require 'panther' 2 | require './app' 3 | 4 | use Middleware::Runtime 5 | run MyApp.new 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new 5 | task default: :spec 6 | -------------------------------------------------------------------------------- /lib/panther.rb: -------------------------------------------------------------------------------- 1 | module Panther 2 | require 'panther/version' 3 | require 'panther/server' 4 | end 5 | 6 | require 'rack/handler/panther' 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | .byebug_history 11 | -------------------------------------------------------------------------------- /example/server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | main() { 6 | bundle exec ../exe/panther --key=./keys/cert.key --cert=./keys/cert.crt 7 | } 8 | 9 | main 10 | -------------------------------------------------------------------------------- /lib/rack/handler/panther.rb: -------------------------------------------------------------------------------- 1 | require 'panther' 2 | 3 | module Rack 4 | module Handler 5 | class Panther 6 | def self.run(app, options = {}) 7 | ::Panther::Server.new(app, options).run 8 | end 9 | end 10 | end 11 | end 12 | 13 | Rack::Handler.register('panther', 'Rack::Handler::Panther') 14 | -------------------------------------------------------------------------------- /script/cert: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | generate_certificate() { 6 | mkdir -p example/keys 7 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout example/keys/cert.key -out example/keys/cert.crt -subj "/C=IT/ST=Lazio/L=Rome/O=Hanami/OU=IT Department/CN=hanamirb.org" 8 | } 9 | 10 | main() { 11 | generate_certificate 12 | } 13 | 14 | main 15 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "panther" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Panther 2 | 3 | Experimental HTTP/2 server for Rack. 4 | 5 | **This is NOT production ready, just a code spike.** 6 | 7 | ## Usage 8 | 9 | ```shell 10 | % git clone https://github.com/jodosha/panther.git && cd panther 11 | % bundle && cd example 12 | % ./server 13 | ``` 14 | 15 | ```shell 16 | % curl -I -k --http2 https://localhost:7152 17 | ``` 18 | 19 | ## Contributing 20 | 21 | Bug reports and pull requests are welcome on GitHub at https://github.com/jodosha/panther. 22 | 23 | -------------------------------------------------------------------------------- /example/app.rb: -------------------------------------------------------------------------------- 1 | module Middleware 2 | class Runtime 3 | FORMAT_STRING = '%0.6f'.freeze 4 | 5 | def initialize(app) 6 | @app = app 7 | end 8 | 9 | def call(env) 10 | starting = Time.now.to_f 11 | status, header, body = @app.call(env) 12 | ending = Time.now.to_f 13 | 14 | header["X-Runtime"] = FORMAT_STRING % (ending - starting) 15 | 16 | [status, header, body] 17 | end 18 | end 19 | end 20 | 21 | class MyApp 22 | def call(env) 23 | [200, { "Content-Length" => "8", "Content-Type" => "text/plain" }, ["Hello H2"]] 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/panther/rack/builder.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module Panther 4 | module Rack 5 | class Builder 6 | def self.build(config) 7 | new(config).to_app 8 | end 9 | 10 | def initialize(config) 11 | @use = [] 12 | @config = Pathname.new(config).realpath 13 | eval(@config.read) # rubocop:disable Lint/Eval 14 | end 15 | 16 | def use(middleware, *args, &block) 17 | @use << [middleware, *args, block] 18 | end 19 | 20 | def run(app) # rubocop:disable Style/TrivialAccessors 21 | @run = app 22 | end 23 | 24 | def to_app 25 | @use.reverse.inject(@run) do |app, (m, args, block)| 26 | m.new(app, *args, &block) 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /exe/panther: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'optparse' 5 | require 'panther' 6 | require 'panther/rack/builder' 7 | 8 | options = { config: 'config.ru', port: 7152, host: 'localhost' } 9 | 10 | OptionParser.new do |opts| 11 | opts.banner = 'Usage: panther [options]' 12 | 13 | opts.on('-c', '--config [String]', 'config') do |v| 14 | options[:config] = v 15 | end 16 | 17 | opts.on('-h', '--host [String]', 'host') do |v| 18 | options[:host] = v 19 | end 20 | 21 | opts.on('-p', '--port [Integer]', 'listen port') do |v| 22 | options[:port] = v 23 | end 24 | 25 | opts.on('-k', '--key [String]', 'SSL key') do |v| 26 | options[:key] = v 27 | end 28 | 29 | opts.on('-c', '--cert [String]', 'SSL cert') do |v| 30 | options[:cert] = v 31 | end 32 | end.parse! 33 | 34 | app = Panther::Rack::Builder.build(options.fetch(:config)) 35 | Panther::Server.new(app, options).run 36 | -------------------------------------------------------------------------------- /panther.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'panther/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "panther" 8 | spec.version = Panther::VERSION 9 | spec.authors = ["Luca Guidi"] 10 | spec.email = ["me@lucaguidi.com"] 11 | 12 | spec.summary = "HTTP/2 for Rack" 13 | spec.description = "Experimental HTTP/2 server for Rack" 14 | spec.homepage = "http://jodosha.github.io/panther" 15 | 16 | spec.metadata['allowed_push_host'] = "https://rubygems.org" 17 | 18 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 19 | spec.bindir = "exe" 20 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 21 | spec.require_paths = ["lib"] 22 | 23 | spec.add_dependency "http-2", "~> 0.8" 24 | spec.add_dependency "rack", "~> 2.0" 25 | spec.add_development_dependency "bundler", "~> 1.10" 26 | spec.add_development_dependency "rake", "~> 11.3" 27 | spec.add_development_dependency "rspec", "~> 3.5" 28 | spec.add_development_dependency "aruba", "~> 0.14" 29 | end 30 | -------------------------------------------------------------------------------- /lib/panther/rack/response.rb: -------------------------------------------------------------------------------- 1 | module Panther 2 | module Rack 3 | class Response 4 | STATUS = ':status'.freeze 5 | 6 | CONTENT_LENGTH = 'content-length'.freeze 7 | CONTENT_TYPE = 'content-type'.freeze 8 | 9 | DEFAULT_CONTENT_TYPE = 'text/plain'.freeze 10 | 11 | CHUNK_MAX_LEN = 1024 12 | 13 | def initialize(status, headers, body) 14 | @status = status.to_s 15 | @headers = prepare_headers(headers, body.first) 16 | @body = StringIO.new(body.first) 17 | end 18 | 19 | def stream_headers 20 | # http-2 gem expects status to be first 21 | Hash[STATUS => status].merge(headers) 22 | end 23 | 24 | def buffer 25 | body.rewind 26 | while !body.closed? && !(body.eof? rescue true) # rubocop:disable Style/RescueModifier 27 | yield body.readpartial(CHUNK_MAX_LEN) 28 | end 29 | end 30 | 31 | private 32 | 33 | attr_reader :status, :headers, :body 34 | 35 | def prepare_headers(raw, body) 36 | result = raw.each_with_object({}) { |(k, v), ret| ret[k.downcase] = v } 37 | result[CONTENT_LENGTH] ||= body.bytesize.to_s 38 | result[CONTENT_TYPE] ||= DEFAULT_CONTENT_TYPE 39 | result 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /example/keys/mycert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID1zCCAr+gAwIBAgIJANjbVITTVqaAMA0GCSqGSIb3DQEBBQUAMFAxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMRgwFgYDVQQKEw9TUERZIFByb3h5 4 | IERlbW8xEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNDEwMTQwNDUwMTJaFw0xNTEw 5 | MTQwNDUwMTJaMFAxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMRgw 6 | FgYDVQQKEw9TUERZIFByb3h5IERlbW8xEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIw 7 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMxv7mqzNMVVFoBjqOSUy2cNM9c6 8 | 6gTgVLr9ssoLU0TC4biY1B/KoD7G9Ive6PwfdpipgGY+tuPHfEzBCCHD7exER1NL 9 | npWauo6Lwh3wOjuo5Er6klgBGFuHYx8jJ2jBwCFvTcG2zJRedU/Pby6Fa27X6acw 10 | faAtReG5YOHs8YRmg4ErWqfRucoM3zj8vvMWnushMhYQxo1EVLJ2EvvbHEkip4ap 11 | pro+2Ql0KY4XT3EoMTRHICbolK/uQYoe0musKnwCGPg2NL6e27uvi47G7GrIpcf3 12 | HN4HZMoOzJ8ti7IIEkF0fVTgQEVkluInfned69WCwxecMQZs5sdBuwE3Kh0CAwEA 13 | AaOBszCBsDAdBgNVHQ4EFgQU86bqiYciIYDN+KAPlnJL6tSbH6IwgYAGA1UdIwR5 14 | MHeAFPOm6omHIiGAzfigD5ZyS+rUmx+ioVSkUjBQMQswCQYDVQQGEwJBVTETMBEG 15 | A1UECBMKU29tZS1TdGF0ZTEYMBYGA1UEChMPU1BEWSBQcm94eSBEZW1vMRIwEAYD 16 | VQQDEwlsb2NhbGhvc3SCCQDY21SE01amgDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 17 | DQEBBQUAA4IBAQCkcr0DLPCbP5l9G0YI/XKVsUW9fXcTvge6Eko0R8qAkzTcsZQv 18 | DbKcIM3z52QguCuJ9k63X4p174FKq7+qmieqaifosGKV03pyyxWLMpRooUUVXEBM 19 | gZaRfp9VG2N4zrRaIklOSkAscnwybv2U3LZhKDlc7Yatsr1/TFkbCnzll514UnTz 20 | ewjrlzVitUSEkwEGvLhKQuVPM9/3MAm+ztFpx846/GZ2XJSAFQLtHudjMXnFLihA 21 | 7nGZvE4rudyT70YsKu0BP0KjVZXrxTh81C4kyJu9xo4YuiDCFtvwtjoty0ygbuQN 22 | a38i0bxFlYFmbWHooNCPUWVy59MOnW9zxaTV 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /example/keys/cert.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIELTCCAxWgAwIBAgIJAIQBrudm3NDTMA0GCSqGSIb3DQEBBQUAMGwxCzAJBgNV 3 | BAYTAklUMQ4wDAYDVQQIEwVMYXppbzENMAsGA1UEBxMEUm9tZTEPMA0GA1UEChMG 4 | SGFuYW1pMRYwFAYDVQQLEw1JVCBEZXBhcnRtZW50MRUwEwYDVQQDEwxoYW5hbWly 5 | Yi5vcmcwHhcNMTYxMTI5MDk0NzI2WhcNMTcxMTI5MDk0NzI2WjBsMQswCQYDVQQG 6 | EwJJVDEOMAwGA1UECBMFTGF6aW8xDTALBgNVBAcTBFJvbWUxDzANBgNVBAoTBkhh 7 | bmFtaTEWMBQGA1UECxMNSVQgRGVwYXJ0bWVudDEVMBMGA1UEAxMMaGFuYW1pcmIu 8 | b3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuo+sRtIVh+BDaPUF 9 | NHXaNaR94OFsqC8flK+mu8kxDcpEnWGXghZSfZR5PiMpufZFeNQnmD3w/w7tkRLI 10 | Xc0bo7CmuBh9n3tdjd/gVauDU83vpoFzA3oFMWXrPK/gH9whqErr/+e4UyAjQTxg 11 | slsY/HXnHXt98g9duvrlFlD7YeEcLiCMEYUgw4pGSRjA74XH2hPlq77y1hzexvRo 12 | 0vOhEaRLckMi6z3U5wMvSiUQzIzeC3WY4tFdwiFHDnGLPh4RNeyrF4h6VXNE1FhU 13 | 8CVYBUeVLlAzhjak2OBcFMlNTyPIaxO6dGbiYEgs/6lLFPtvXluuSEjND8GDNAXm 14 | 0OpbCQIDAQABo4HRMIHOMB0GA1UdDgQWBBS5xxbV+8wINJZz8DNjjGqepQ5Z8DCB 15 | ngYDVR0jBIGWMIGTgBS5xxbV+8wINJZz8DNjjGqepQ5Z8KFwpG4wbDELMAkGA1UE 16 | BhMCSVQxDjAMBgNVBAgTBUxhemlvMQ0wCwYDVQQHEwRSb21lMQ8wDQYDVQQKEwZI 17 | YW5hbWkxFjAUBgNVBAsTDUlUIERlcGFydG1lbnQxFTATBgNVBAMTDGhhbmFtaXJi 18 | Lm9yZ4IJAIQBrudm3NDTMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB 19 | ACTlDGk+tGfxbI1eYEAE+ctNQFhWZkjm56SNOBvc4ZqM95sGe6cJJTgxLCXqZa0H 20 | f4LvcUUsOqviYXIVrYGQfX5GbxI7klfDAtBaffbBA0eXU1xwGtekbPlP3VeKkO1m 21 | vFC7EfyQNW/U+LZxngF+H8UsJopkZ/lDavDotkzEk1Do52sUnMYu1pSjjNdkxuw1 22 | kBcuV03qKkXtdVIfJEY+uviqQM7hdsgJll3qm5g0gd2tMwymLAYkSTZMm/eSUn1e 23 | /CDWhlJ01L8rGj4HQSoDRIv65xiiIJNeTWqrJM6+2ykBQ3u/ETufY8RfHO1rE7TW 24 | OO5oQI2/ulplW8U2EzHbu6w= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /example/keys/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIERTCCAy2gAwIBAgIJAMJoTqeNpsqYMA0GCSqGSIb3DQEBBQUAMHQxCzAJBgNV 3 | BAYTAklUMQ4wDAYDVQQIEwVMYXppbzENMAsGA1UEBxMEUm9tZTEQMA4GA1UEChMH 4 | UGFudGhlcjETMBEGA1UEAxMKTHVjYSBHdWlkaTEfMB0GCSqGSIb3DQEJARYQbWVA 5 | bHVjYWd1aWRpLmNvbTAeFw0xNTA2MzAxNDU2MTRaFw0xNTA3MzAxNDU2MTRaMHQx 6 | CzAJBgNVBAYTAklUMQ4wDAYDVQQIEwVMYXppbzENMAsGA1UEBxMEUm9tZTEQMA4G 7 | A1UEChMHUGFudGhlcjETMBEGA1UEAxMKTHVjYSBHdWlkaTEfMB0GCSqGSIb3DQEJ 8 | ARYQbWVAbHVjYWd1aWRpLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 9 | ggEBAL3CT42LLOdwbND8oW+qB0bCItZtUw4Ks2OEABfqsJCo310boQEXvCkQ+Cr/ 10 | k4yOGFyNJ23OZq1fOe6kdj/3WWStEZd8v5pgGr4G9e8N3vev/VEf9EzN4l5qWaKS 11 | TrFE86fbrz6xolALPYRjy7TUsak3xfqgjExh8yzv5seALgE4m7LZ56EQl/DjSFAB 12 | GoWBGlzFDn8fjgiWAgGXqiy+nMWrLMshrxRaI0qS22Gktwn4s7fLD/jGgXGNB1Et 13 | RgzmlVDohrHklHZsmw75ZSiiRHMhMDsk5TgaPC0EH/9pLOXmehQF0hKtzJwTTsCf 14 | hyHmp6O8DIJyaUBBHSaN5y3xnDcCAwEAAaOB2TCB1jAdBgNVHQ4EFgQUhFCwg13k 15 | 0TtNv523Nzt58lQiYBMwgaYGA1UdIwSBnjCBm4AUhFCwg13k0TtNv523Nzt58lQi 16 | YBOheKR2MHQxCzAJBgNVBAYTAklUMQ4wDAYDVQQIEwVMYXppbzENMAsGA1UEBxME 17 | Um9tZTEQMA4GA1UEChMHUGFudGhlcjETMBEGA1UEAxMKTHVjYSBHdWlkaTEfMB0G 18 | CSqGSIb3DQEJARYQbWVAbHVjYWd1aWRpLmNvbYIJAMJoTqeNpsqYMAwGA1UdEwQF 19 | MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAkR8QbUzAONJn3mlq3H512rTBxF1Vs1 20 | iJuUygpO/WaUPOzuwf4i3SnkSvQcjCMjVjsiW3YFuJ6ISAPYAM+qGvfvvrAaQkTj 21 | ip2oTbwOcU3OFp5ClHF+tW1R9mudWL62dPfsFcBzUSuCcT5MZjmfC9vvFMFsJjAb 22 | kaR60pFzT4VCZd8zLwH1xh8dCIweg8B4yi3pJApH06PzcpnP0klGnUhVEMD6YcFC 23 | yfeebrs66FlX2RRkNmFm+u6Cv+3kmjV6QD5o1v5Ju6BR9VuQIRSAdcuBiVaqrOm/ 24 | EBhh7ZbI+xrrfnN9YCxFRUZUEli/V6srl+pvz06D0hphQNXqgIc2YQE= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /example/keys/cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAuo+sRtIVh+BDaPUFNHXaNaR94OFsqC8flK+mu8kxDcpEnWGX 3 | ghZSfZR5PiMpufZFeNQnmD3w/w7tkRLIXc0bo7CmuBh9n3tdjd/gVauDU83vpoFz 4 | A3oFMWXrPK/gH9whqErr/+e4UyAjQTxgslsY/HXnHXt98g9duvrlFlD7YeEcLiCM 5 | EYUgw4pGSRjA74XH2hPlq77y1hzexvRo0vOhEaRLckMi6z3U5wMvSiUQzIzeC3WY 6 | 4tFdwiFHDnGLPh4RNeyrF4h6VXNE1FhU8CVYBUeVLlAzhjak2OBcFMlNTyPIaxO6 7 | dGbiYEgs/6lLFPtvXluuSEjND8GDNAXm0OpbCQIDAQABAoIBACbgc70aYGQ9Rq7j 8 | iF8jRAKzsYugTj1J/KHGbM9rIK7H/L5zLNhuAGis/QsG1tYqzNVeGJYwKj1shfMa 9 | X+i0KlNVJ5jreZo3YgqDocMYh7h6DLu89BPAZL6jh37RA7p/6/+fBUkBp7ai1tXs 10 | WBYyx6UsiKnrJxa/PwAM8ppQ6+G8ZPLh6LUemj6nMUAxf8SBb6e1V7Yv5AWbqyd+ 11 | dEw+h3KmWerFDq+usBJ8qzkQRXX/g4HKAcnH/9wow0eIqzGQuqIJtkFELm3gRElB 12 | AsL5jEm+nhko5yoJZTmQRRzb8zawPOkM0nniXagx61t330hQx61grJPU4g4rnnJu 13 | FnHn4qUCgYEA7eGAtGTydb/kQL9M5RUL8rn2Gy26h2qye+ECif+V936hDUkbA8/3 14 | 0lmFz4Pr5wkyTHqxweC+5jeEZj0qAJ89hot7eQ3yXGz89wniIPdIHqTNNNkA3ZVK 15 | ZSU705O+aPvS9UaQLd5BJqcpv6knuaOUjWWuJ5Fk77NMN6rpTmQWrhsCgYEAyMV5 16 | 0Q59peksJMdln96TxcVHPznZaixOnFk3BkVND2TA/Mt4SQGkYHFlWTf1Xxsxc5Ff 17 | ux6bH95HVjFiyHSzz/0x2clbo2PPSH65/AmUMFYYV9PDb5ql5OIVXYAt2ZKXG2/C 18 | 3Buwgsi9yVS4lttUGbJ9dwi9oIhiPRUSAcDRHasCgYEA669U+WZa41cwKwZehUoJ 19 | AIBG2j7AZJLOK/aPsXJNf10y7BrWbTyL6RlRfnzSKaHu999IZzLpcObogvNuvhcH 20 | ulpQB2xOCEzjhU+Bf/AAwHu/5PBr7x6PCw+M6t+i9Bkstl8nUoq2Ojm9H2vVNBRi 21 | hoWLuyBOxT778NuhNE3uOqkCgYBDxGwmxXZhzv+odml1+eH1Km6vB668KLJsEa0B 22 | /9hP7tk5OtCiC9zY9M2ZvMqkzH/3m0Ut2tnPeu6nSEookUO0W6k88rtsvh7UQHo0 23 | eWM8oYLb+k2IWTHPvQQrKD3Rp6o7h5DFTM9ahbmRwHgB64xKlDXHPPsMuZw3M+p9 24 | DwFGuwKBgG+WRhuZ+HeokLTZeOH3F8vANNBziuVV0ZuCFT6+kB08Ke6DCT8ES10y 25 | 8K2S3MxzEPjpvnw2PaZrZVgEAW9QswMsY36aLfxPGWZaFK9HnbTFw9KHTGcdQdHk 26 | zFhhjivClwOB9IuDwZIxftQx2t/VsHQBRGdFfksbJFbC4FCiuJmr 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /example/keys/mykey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAzG/uarM0xVUWgGOo5JTLZw0z1zrqBOBUuv2yygtTRMLhuJjU 3 | H8qgPsb0i97o/B92mKmAZj6248d8TMEIIcPt7ERHU0uelZq6jovCHfA6O6jkSvqS 4 | WAEYW4djHyMnaMHAIW9NwbbMlF51T89vLoVrbtfppzB9oC1F4blg4ezxhGaDgSta 5 | p9G5ygzfOPy+8xae6yEyFhDGjURUsnYS+9scSSKnhqmmuj7ZCXQpjhdPcSgxNEcg 6 | JuiUr+5Bih7Sa6wqfAIY+DY0vp7bu6+Ljsbsasilx/cc3gdkyg7Mny2LsggSQXR9 7 | VOBARWSW4id+d53r1YLDF5wxBmzmx0G7ATcqHQIDAQABAoIBACybj85AZBdaxZom 8 | JMgbn3ZQ7yrbdAy0Vkim6sgjSHwMeewpjL+TGvwXtWx/qx64Tsxoz9d/f7Cb6odk 9 | 5z1W3ydajqWiLmw+Ys6PuD+IF2zFIWsq2ZvSQVpXZE17AjJddGrXOoQ2OtV09uv/ 10 | OydPfW2mNxl//ylgN4tVQ8qIRPq6b1GWWZvjTw4K3jPrlAifobYBBR+BSk446O7F 11 | iGvax5lNNCDMN2y+6hlnhlTHuvc0DXQA0XBhWTNYu8BNNrvC3I31RmxdY7Frm7IA 12 | RUGy/l2kLHCRCTF8Q0C4ydpE5ZFgpxkWK7p3QEv/gnVAwsOSN/nThdoorWWHTbNl 13 | pA5l1RECgYEA/ASaS9mqWWthUkOW51L6c7IIiRPAhrbPnx1mkAtUPeehHn1+G8Qu 14 | upUEXslWokhmQ3UAGhrId6FVYsfftNPMNck9mv4ntW7MoZLXZqTiFSqx4pQTjoYg 15 | PQ4c/jrQLsmislcKTiVx6kFYFcnI1ayXXEtaby0lri8XsAR5F90OpycCgYEAz6re 16 | DR5EZZKx61wyEfuPWE6aACWlEqlbTa8nTMddxnZUZXtzbwRFapGfs+uHCURF0dGj 17 | 37cl4q8JdGbbYePk9nlOC4RoSw8Zh3fB4yRSZocB4yB047ofpBmt4BigGtgZ5BLZ 18 | zqVREgBUI+tFPPHkMmBY4lCaUsCe11SEwyZFzxsCgYEA3nRNonBy/tVbJZtFw9Eq 19 | BB/9isolooQRxrjUBIgLh01Dmj9ZprbILKhHIEgGsd7IbfkD6wcDNx3w2e3mGJ7v 20 | 3fZR69M2R9+Sv3h3rEIU0mxKct8UWDUqldo0W3CcvP/9HgDYttw0rnuZfjoMjhf3 21 | z18wZ3xpi1RES3nXTeox+fcCgYBlPxkjrC4Ml4jHBxwiSFOK6keK6s+gWZF6Pnsa 22 | o9jEecyL7bRJ2/s8CeOjBKHBkte3hE4xNEn0SwKBDeTHxSRMRrgWRWfTsHjx4yFU 23 | bND/y7LP2XMj1Aq5JwvuxhLJA7Mbz1UBuvfbnu1m1b3cCNMI/JBZRpL25ZKLyVkx 24 | C+fdIQKBgA+tLeF10zqGGc4269b6nQWplc5E/qnIRK0cfnKb9BtffmA4FbjUpZKj 25 | +cGmbtbw7ySkAIKLp4HoJmzkXJageGTSEb/sQIodxMiJCGvvgJmPPnGzU8OiUGAl 26 | VmRjuAQ2eCcsUyvrJYgKW9UWskqSe6z5w/Uxo/sZdHlaGljNdKcn 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /example/keys/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,95BF3E430B0205A6 4 | 5 | 4funH6yIk/ZBZBbfADijtgnuRYnzQRJ9cBX1EVCI5GxtXlFQUT5ehYnrYuhelRdU 6 | jTTMkI/JKsWtOzJcG5/Lbo9pZQpBpknlL+pYuLIGiHIr6duu7uSO0d3nuJ4Yh5SP 7 | sddc2UgU6l7NjNWusUND6AiXNvRB8R7yjtJdvcSFOFbQsrlkSYqImhIBrE9JzHAl 8 | ZQaCwCs++Mi0GDdYr/U3vbR+/6D496CRr91L3WgHMWFGU6/02YHj0wWI6FdwYtBU 9 | Wsz5n1D3Zj8BYoSxf8e/gvBEv/gxwEL9kADFIGyBo0z8eg2XnMyceXLBRM7jFToE 10 | /t/BTXmA66Bl0p9HLBQ2M3d3RvOs1pw4IS+0qqtll//tfpYZsnQNlubtxvOLEJC6 11 | fkD+Z+EDHifBz4z99kmoMEJCvSgavg3efuY+oZ2wdIP9uuPhnN0wahdFqaoQYSGl 12 | C3KIj2cU53zuszygicXTmsM9i0MSv1xXmgHOdyXxLJ4kUMajfbcEeECPBK3sf3Mt 13 | bCwNQNjZ24BIbU9/GusLHOhupQuhoEd2bnWplI5i6hPTUAaQ3A2xmsgj00NpuBVt 14 | /nO0JOdSqnAE0P6JUiPi9SfX5k7/25rztf71/HkzGhnnAkFWppR8AOJFLsg4+zhv 15 | YOnZt1RiHePewHgbTq2T4uSWAGyUBYZ+6Ah9cQwfMgS3w93QU5Z1dqMm1xfvbPph 16 | /IH9/o5sOckd2uZt5O3+x1QWfE5khArJkEsd+pfWA9tPQxk9+vAA14GjX6bw3OlU 17 | /7N7eaICL0eLKVAG7Q+EwAwhj1GjAOWt2vuf5lMOkA1RFlONaz2uxVy5dVSeBLwY 18 | PwpZYJhAzHthhU+hjlIsFFGpQu1Oc9ZHuuObgb2obsxE/2xM5aEfjECGLyR6n8+6 19 | 1TCt/J/r5/6BcKHZz4i4Qss5z9Is6wlau05fviYpbWeE9GwmSUyYbNHbm1JW/bRO 20 | eXlLKOrRfHGA6Oia7e48zOlhgFsarZQUeBbPxktLd1h1LHpirWUngHMzUOVNp8A5 21 | KiY/9WzFcfbQ6N1UylqYpJrJl+q3SRJjwRHPMh1xxvVm74DD67DDc5SX6D/q6o71 22 | zzuomHWFUo4VkHGnxnwzuIeFX0jP7YIEmc20euvzAm/645GkHTUS+nOQFKFlAmiA 23 | CmLktfWJjgmg2dzGMaMW7EhnYkRa/TxPhimZgoDrJkXKKboLbrDG9z83nyOSIXIx 24 | 7zb8iZ5mUN34GkS2swF8q/0ogT9paIyylqEt0Q/5CvNhaazYhs66dPwtTJEc8H+W 25 | J0YSEnlu6H1qQEJCqtli4TJYjFJ2DxRUM1a3w3+z9+ouCQVqR62whMDUQmWOxvY7 26 | xU/eaXdjPdYMPixpCYm6VLvaIM7u/iqQbBzWkmD6/kF9q65ludMGfN1qBmiVZq8q 27 | M6x798FUYlZvMXuJ05MSuadnXGZiYtPuE7FUifGDFkENdPz7LWNL+TganpgcFZ7L 28 | iCvZubwrePBcZNMtaOWCEoVRfSZ8pLLKQ0YJsmfLq9qm+a6t9VRUQOd7H7v5I5BO 29 | a9UZbyGmOLV0V737W5yeDxB4Jzqne1YeLx64KkrleuXPUrMK1Mz0XA== 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /lib/panther/server.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'openssl' 3 | require 'http/2' 4 | require 'rack' 5 | require 'panther/rack/request' 6 | require 'panther/rack/response' 7 | 8 | module Panther 9 | class Logger 10 | def initialize(id) 11 | @id = id 12 | end 13 | 14 | def info(msg) 15 | puts "[Stream #{@id}]: #{msg}" 16 | end 17 | end 18 | 19 | class Server 20 | DRAFT = 'h2'.freeze 21 | DEFAULT_HOST = 'localhost'.freeze 22 | DEFAULT_PORT = 7152 23 | 24 | def initialize(app, options = {}) 25 | host = options.fetch(:host, DEFAULT_HOST) 26 | port = options.fetch(:port, DEFAULT_PORT) 27 | cert = options.fetch(:cert, nil) 28 | key = options.fetch(:key, nil) 29 | 30 | @server = TCPServer.new(host, port) 31 | 32 | if (ssl = cert && key) 33 | context = OpenSSL::SSL::SSLContext.new 34 | context.cert = OpenSSL::X509::Certificate.new(File.open(cert)) 35 | context.key = OpenSSL::PKey::RSA.new(File.open(key)) 36 | 37 | context.npn_protocols = [DRAFT] 38 | context.ssl_version = :SSLv23 39 | 40 | @server = OpenSSL::SSL::SSLServer.new(@server, context) 41 | @server.start_immediately = true 42 | end 43 | 44 | @app = app 45 | 46 | $stdout.puts "Panther: listening http#{ssl ? 's' : nil}://#{host}:#{port}" 47 | end 48 | 49 | def run 50 | app = @app 51 | 52 | loop do 53 | # process @server.accept 54 | sock = @server.accept 55 | puts 'New TCP connection!' 56 | 57 | conn = HTTP2::Server.new 58 | conn.on(:frame) do |bytes| 59 | # puts "Writing bytes: #{bytes.unpack("H*").first}" 60 | sock.write bytes 61 | end 62 | conn.on(:frame_sent) do |frame| 63 | puts "Sent frame: #{frame.inspect}" 64 | end 65 | conn.on(:frame_received) do |frame| 66 | puts "Received frame: #{frame.inspect}" 67 | end 68 | 69 | conn.on(:stream) do |stream| 70 | log = Logger.new(stream.id) 71 | req = nil 72 | 73 | stream.on(:active) { log.info 'cliend opened new stream' } 74 | stream.on(:close) { log.info 'stream closed' } 75 | 76 | stream.on(:headers) do |h| 77 | req = Panther::Rack::Request.new(Hash[*h.flatten], stream) 78 | log.info "request headers: #{h}" 79 | end 80 | 81 | stream.on(:data) do |d| 82 | log.info "payload chunk: <<#{d}>>" 83 | req.input << d 84 | end 85 | 86 | stream.on(:half_close) do 87 | log.info 'client closed its end of the stream' 88 | response = Panther::Rack::Response.new(*app.call(req.env)) 89 | stream.headers(response.stream_headers, end_stream: false) 90 | 91 | # split response into multiple DATA frames 92 | body = response.__send__(:body).string 93 | stream.data(body.slice!(0, 5), end_stream: false) 94 | stream.data(body) 95 | 96 | # response.buffer do |chunk| 97 | # stream.data(chunk, end_stream: false) 98 | # end 99 | # stream.data("") 100 | end 101 | end 102 | 103 | while !sock.closed? && !(sock.eof? rescue true) # rubocop:disable Style/RescueModifier 104 | data = sock.readpartial(1024) 105 | # puts "Received bytes: #{data.unpack("H*").first}" 106 | 107 | begin 108 | conn << data 109 | rescue => e 110 | puts "Exception: #{e}, #{e.message} - closing socket." 111 | sock.close 112 | end 113 | end 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/panther/rack/request.rb: -------------------------------------------------------------------------------- 1 | module Panther 2 | module Rack 3 | class Request 4 | REQUEST_METHOD = 'REQUEST_METHOD'.freeze 5 | SCRIPT_NAME = 'SCRIPT_NAME'.freeze 6 | PATH_INFO = 'PATH_INFO'.freeze 7 | QUERY_STRING = 'QUERY_STRING'.freeze 8 | SERVER_NAME = 'SERVER_NAME'.freeze 9 | SERVER_PORT = 'SERVER_PORT'.freeze 10 | HTTP_USER_AGENT = 'HTTP_USER_AGENT'.freeze 11 | HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze 12 | HTTP_ACCEPT_ENCODING = 'HTTP_ACCEPT_ENCODING'.freeze 13 | HTTP_ACCEPT_LANGUAGE = 'HTTP_ACCEPT_LANGUAGE'.freeze 14 | HTTP_CACHE_CONTROL = 'CACHE_CONTROL'.freeze 15 | RACK_VERSION = 'rack.version'.freeze 16 | RACK_URL_SCHEME = 'rack.url_scheme'.freeze 17 | RACK_INPUT = 'rack.input'.freeze 18 | RACK_ERRORS = 'rack.errors'.freeze 19 | RACK_MULTITHREAD = 'rack.multithread'.freeze 20 | RACK_MULTIPROCESS = 'rack.multiprocess'.freeze 21 | RACK_RUN_ONCE = 'rack.run_once'.freeze 22 | RACK_STREAM = 'rack.stream'.freeze 23 | 24 | AUTHORITY = ':authority'.freeze 25 | METHOD = ':method'.freeze 26 | PATH = ':path'.freeze 27 | SCHEME = ':scheme'.freeze 28 | ACCEPT = 'accept'.freeze 29 | ACCEPT_ENCODING = 'accept-encoding'.freeze 30 | ACCEPT_LANGUAGE = 'accept-language'.freeze 31 | CACHE_CONTROL = 'cache-control'.freeze 32 | PRAGMA = 'pragma'.freeze 33 | USER_AGENT = 'user-agent'.freeze 34 | 35 | INPUT_ENCODING = 'ASCII-8BIT'.freeze 36 | AUTHORITY_SEPARATOR = ':'.freeze 37 | QUERY_STRING_SEPARATOR = '?'.freeze 38 | 39 | attr_reader :input 40 | 41 | def initialize(raw, stream) 42 | @raw = raw 43 | @stream = stream 44 | @input = StringIO.new('') 45 | @input.set_encoding(INPUT_ENCODING) # Rack::Lint 46 | end 47 | 48 | def server_name 49 | server_and_port.first 50 | end 51 | 52 | def server_port 53 | server_and_port.last 54 | end 55 | 56 | def server_and_port 57 | @raw[AUTHORITY].split(AUTHORITY_SEPARATOR) 58 | end 59 | 60 | def accept 61 | @raw[ACCEPT] 62 | end 63 | 64 | def accept_encoding 65 | @raw[ACCEPT_ENCODING] 66 | end 67 | 68 | def accept_language 69 | @raw[ACCEPT_LANGUAGE] 70 | end 71 | 72 | def cache_control 73 | @raw[CACHE_CONTROL] 74 | end 75 | 76 | def pragma 77 | @raw[PRAGMA] 78 | end 79 | 80 | def user_agent 81 | @raw[USER_AGENT] 82 | end 83 | 84 | def env 85 | { 86 | REQUEST_METHOD => request_method, 87 | SCRIPT_NAME => '', 88 | PATH_INFO => location, 89 | QUERY_STRING => query_string, 90 | SERVER_NAME => server_name, 91 | SERVER_PORT => server_port, 92 | HTTP_USER_AGENT => user_agent, 93 | HTTP_ACCEPT => accept, 94 | HTTP_ACCEPT_ENCODING => accept_encoding, 95 | HTTP_ACCEPT_LANGUAGE => accept_language, 96 | HTTP_CACHE_CONTROL => cache_control, 97 | RACK_VERSION => ::Rack::VERSION, 98 | RACK_URL_SCHEME => scheme, 99 | RACK_INPUT => input, 100 | RACK_ERRORS => StringIO.new(''), 101 | RACK_MULTITHREAD => false, 102 | RACK_MULTIPROCESS => false, 103 | RACK_RUN_ONCE => false, 104 | RACK_STREAM => @stream, 105 | } 106 | end 107 | 108 | protected 109 | 110 | def request_method 111 | @raw[METHOD] 112 | end 113 | 114 | def location 115 | @raw[PATH] 116 | end 117 | 118 | def scheme 119 | @raw[SCHEME] 120 | end 121 | 122 | def query_string 123 | _, qs = *location.split(QUERY_STRING_SEPARATOR) 124 | qs || "" 125 | end 126 | end 127 | end 128 | end 129 | --------------------------------------------------------------------------------