Skip to content

Instantly share code, notes, and snippets.

@tenderlove
Last active August 14, 2024 06:10
Show Gist options
  • Save tenderlove/d68d9a3c4f7941192c9b to your computer and use it in GitHub Desktop.
Save tenderlove/d68d9a3c4f7941192c9b to your computer and use it in GitHub Desktop.
Demo HTTP/2 server with Puma
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]
# This needs https://bugs.ruby-lang.org/issues/11356
@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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment