Skip to content

Instantly share code, notes, and snippets.

@tenderlove
Last active October 25, 2023 21:10
  • Star 12 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save tenderlove/79e7e0de4b2097e43356 to your computer and use it in GitHub Desktop.
require 'webrick'
require 'webrick/https'
require 'ds9'
class HTTP2Server < WEBrick::HTTPServer
SETTINGS = [ [DS9::Settings::MAX_CONCURRENT_STREAMS, 100] ]
class HTTP2Response < WEBrick::HTTPResponse
def initialize config, ctx, stream_id
@ctx = ctx
@stream_id = stream_id
super(config)
end
def send_header socket
@header.delete 'connection'
headers = [[':status', @status.to_s]] + @header.map { |key, value|
[key.downcase, value.to_s]
} + @cookies.map { |cookie| ['set-cookie', cookie.to_s] }
@ctx.submit_response @stream_id, headers
end
end
class HTTP2Request < WEBrick::HTTPRequest
def parse socket, headers
@socket = socket
begin
@peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : []
@addr = socket.respond_to?(:addr) ? socket.addr : []
rescue Errno::ENOTCONN
raise HTTPStatus::EOFError
end
@request_time = Time.now
@request_method = headers[':method']
@unparsed_uri = headers[':path']
@http_version = WEBrick::HTTPVersion.new '2.0'
@request_line = "#{headers[':method']} #{headers[':path']} HTTP/2.0"
@request_uri = URI.parse "#{headers[':scheme']}://#{headers[':authority']}#{headers[':path']}"
@header = headers.each_with_object(Hash.new([].freeze)) do |(k,v), h|
h[k] = [v]
end
@header['cookie'].each{|cookie|
@cookies += WEBrick::Cookie::parse(cookie)
}
@accept = WEBrick::HTTPUtils.parse_qvalues(self['accept'])
@accept_charset = WEBrick::HTTPUtils.parse_qvalues(self['accept-charset'])
@accept_encoding = WEBrick::HTTPUtils.parse_qvalues(self['accept-encoding'])
@accept_language = WEBrick::HTTPUtils.parse_qvalues(self['accept-language'])
return if @request_method == "CONNECT"
return if @unparsed_uri == "*"
begin
setup_forwarded_info
@path = WEBrick::HTTPUtils::unescape(@request_uri.path)
@path = WEBrick::HTTPUtils::normalize_path(@path)
@host = @request_uri.host
@port = @request_uri.port
@query_string = @request_uri.query
@script_name = ""
@path_info = @path.dup
rescue
raise WEBrick::HTTPStatus::BadRequest, "bad URI `#{@unparsed_uri}'."
end
@keep_alive = true
end
end
class MySession < DS9::Server
def initialize sock, server
super()
@sock = sock
@config = server.config
@server = server
@write_streams = {}
@read_streams = {}
end
def on_data_source_read stream_id, length
@write_streams[stream_id].read(length)
end
def on_stream_close id, error_code
@write_streams.delete id
@read_streams.delete id
end
def on_begin_headers frame
@read_streams[frame.stream_id] = {}
end
def on_header name, value, frame, flags
@read_streams[frame.stream_id][name] = value
end
def on_frame_recv frame
return unless frame.headers?
rd, wr = IO.pipe
@write_streams[frame.stream_id] = rd
res = HTTP2Response.new(@config, self, frame.stream_id)
req = HTTP2Request.new(@config)
req.parse @sock, @read_streams[frame.stream_id]
res.request_method = req.request_method
res.request_uri = req.request_uri
res.request_http_version = req.http_version
res.keep_alive = false
begin
@server.service req, res
rescue WEBrick::HTTPStatus::EOFError, WEBrick::HTTPStatus::RequestTimeout => ex
res.set_error(ex)
rescue WEBrick::HTTPStatus::Error => ex
@server.logger.error(ex.message)
res.set_error(ex)
rescue WEBrick::HTTPStatus::Status => ex
res.status = ex.code
rescue StandardError => ex
@server.logger.error(ex)
res.set_error(ex, true)
ensure
res.send_response(wr)
wr.close
@server.access_log(@config, req, res)
end
true
end
def send_event string
@sock.write string
end
def recv_event length
return '' unless want_read? || want_write?
case data = @sock.read_nonblock(length, nil, exception: false)
when :wait_readable
DS9::ERR_WOULDBLOCK
when nil
DS9::ERR_EOF
else
data
end
end
def run
while want_read? || want_write?
if want_read?
@sock.to_io.wait_readable
begin
return if @sock.eof?
rescue OpenSSL::SSL::SSLError
return
end
receive
end
if want_write?
@sock.to_io.wait_writable
send
end
end
end
end
def run socket
return super unless socket.npn_protocol == 'h2'
session = MySession.new socket, self
session.submit_settings SETTINGS
session.run
end
def setup_ssl_context config
ctx = super
ctx.ssl_version = "SSLv23_server"
ctx.npn_protocols = [DS9::PROTO_VERSION_ID]
ctx.tmp_ecdh_callback = ->(ssl, export, len) { PKEY }
ctx
end
end
PKEY = OpenSSL::PKey::EC.new "prime256v1"
CERT, KEY = WEBrick::Utils.create_self_signed_cert(2048,
[['CN', 'localhost']],
'such secure!')
server = HTTP2Server.new SSLEnable: true,
SSLCertificate: CERT,
SSLPrivateKey: KEY,
Port: 8080
server.mount_proc('/hi-mom') do |req, res|
res.body = 'HI MOM!!!'
end
server.start
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment