Skip to content

Instantly share code, notes, and snippets.

@rmoreira
Forked from tenderlove/h2_puma.rb
Created February 27, 2017 22:14
Show Gist options
  • Save rmoreira/9a4c82fbcd5f071a19fc0f9b1689bbf9 to your computer and use it in GitHub Desktop.
Save rmoreira/9a4c82fbcd5f071a19fc0f9b1689bbf9 to your computer and use it in GitHub Desktop.

Revisions

  1. @tenderlove tenderlove revised this gist Jul 15, 2015. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions h2_puma.rb
    Original file line number Diff line number Diff line change
    @@ -103,6 +103,8 @@ class Context
    def initialize host, port
    @ctx = OpenSSL::SSL::SSLContext.new
    @ctx.npn_protocols = [DS9::PROTO_VERSION_ID]

    # This needs https://bugs.ruby-lang.org/issues/11356
    @ctx.tmp_ecdh_callback = ->(ssl, export, len) { PKEY }

    @ctx.cert = CERT
  2. @tenderlove tenderlove created this gist Jul 15, 2015.
    184 changes: 184 additions & 0 deletions h2_puma.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,184 @@
    require 'socket'
    require 'openssl'
    require 'puma/server'
    require 'ds9'

    class Server < DS9::Server
    def initialize socket, app
    @app = app
    @read_streams = {}
    @write_streams = {}
    @socket = socket
    super()
    end

    def send_event string
    @socket.write_nonblock string
    end

    def recv_event length
    case data = @socket.read_nonblock(length, nil, exception: false)
    when :wait_readable then DS9::ERR_WOULDBLOCK
    when nil then DS9::ERR_EOF
    else
    data
    end
    end

    def on_begin_headers frame
    @read_streams[frame.stream_id] = []
    end

    def on_data_source_read stream_id, length
    @write_streams[stream_id].body.read length
    end

    def on_stream_close id, error_code
    @read_streams.delete id
    @write_streams.delete id
    end

    def submit_push_promise stream_id, headers, block
    response = Response.new(self, super(stream_id, headers), [])
    @app.call Hash[headers], response
    @write_streams[response.stream_id] = response
    end

    def on_header name, value, frame, flags
    @read_streams[frame.stream_id] << [name, value]
    end

    class Response < Struct.new :stream, :stream_id, :body
    def push headers, &block
    stream.submit_push_promise stream_id, headers, block
    end

    def submit_response headers
    stream.submit_response stream_id, headers
    end

    def finish str
    self.body = StringIO.new str
    end
    end

    def on_frame_recv frame
    return unless frame.headers?
    req_headers = @read_streams[frame.stream_id]
    response = Response.new(self, frame.stream_id, [])
    @app.call Hash[req_headers], response
    @write_streams[frame.stream_id] = response
    end

    def run
    while want_read? || want_write?
    if want_read?
    rd, _, _ = IO.select([@socket])
    return if @socket.eof?

    receive
    end

    if want_write?
    _, wr, _ = IO.select(nil, [@socket])
    send
    end
    end
    end

    def self.connect_ssl sock, ctx
    ssl_sock = OpenSSL::SSL::SSLSocket.new sock, ctx
    ssl_sock.accept
    ssl_sock
    end
    end

    CERT = OpenSSL::X509::Certificate.new File.read ARGV[0]
    KEY = OpenSSL::PKey::RSA.new File.read ARGV[1]
    PKEY = OpenSSL::PKey::EC.new "prime256v1"

    class Context
    STR = "This server only supports HTTP2 requests\n"

    def initialize host, port
    @ctx = OpenSSL::SSL::SSLContext.new
    @ctx.npn_protocols = [DS9::PROTO_VERSION_ID]
    @ctx.tmp_ecdh_callback = ->(ssl, export, len) { PKEY }

    @ctx.cert = CERT
    @ctx.key = KEY
    @authority = ['localhost', port.to_s].join ':'
    end

    def call _, sock
    ssl_sock = Server.connect_ssl sock, @ctx

    if ssl_sock.npn_protocol == DS9::PROTO_VERSION_ID
    app = ->(headers, response) {
    puts headers[":path"]

    case headers[":path"]
    when "/favicon.ico"
    response.submit_response [[':status', '200'],
    ["server", 'test server'],
    ["date", 'Sat, 27 Jun 2015 17:29:21 GMT']]
    puts "PUSHING FAVICON.PNG"
    response.finish File.binread "favicon.ico"

    when "/test.png"
    response.submit_response [[':status', '200'],
    ["server", 'test server'],
    ["date", 'Sat, 27 Jun 2015 17:29:21 GMT']]
    puts "PUSHING TEST.PNG"
    response.finish File.binread "test.png"

    when "/"
    response.push [[":method", "GET"],
    [":path", "/favicon.ico"],
    [":scheme", "https"],
    [":authority", @authority]]

    response.push [[":method", "GET"],
    [":path", "/test.png"],
    [":scheme", "https"],
    [":authority", @authority]]

    response.submit_response [[':status', '200'],
    ["server", 'test server'],
    ["content-type", 'text/html'],
    ["date", 'Sat, 27 Jun 2015 17:29:21 GMT']]
    response.finish "<html><body><img src='/test.png' /></body></html>"
    else
    response.submit_response [[':status', '404'],
    ["server", 'test server'],
    ["content-type", 'text/plain'],
    ["date", 'Sat, 27 Jun 2015 17:29:21 GMT']]
    response.finish "Not Found"
    end
    }
    session = Server.new ssl_sock, app
    puts "OPENED"
    session.submit_settings [[DS9::Settings::MAX_CONCURRENT_STREAMS, 100]]
    session.run
    ssl_sock.close
    puts "CLOSED"
    else
    ssl_sock.write "HTTP/1.1 505 HTTP Version Not Supported\r\n"
    ssl_sock.write "Content-Type: text/plain\r\n"
    ssl_sock.write "Content-Length: #{STR.bytesize}\r\n"
    ssl_sock.write "Connection: close\r\n"
    ssl_sock.write "\r\n"
    ssl_sock.write STR
    ssl_sock.close
    end
    end
    end

    PORT = 8080
    HOST = "localhost"

    server = Puma::Server.new Context.new(HOST, PORT)
    server.add_tcp_listener HOST, PORT
    server.tcp_mode!
    server.run
    server.thread.join