1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
|
# frozen_string_literals: true
require_relative 'helper'
require 'http_parser'
options = { port: 8080 }
OptionParser.new do |opts|
opts.banner = 'Usage: server.rb [options]'
opts.on('-s', '--secure', 'HTTPS mode') do |v|
options[:secure] = v
end
opts.on('-p', '--port [Integer]', 'listen port') do |v|
options[:port] = v
end
end.parse!
puts "Starting server on port #{options[:port]}"
server = TCPServer.new(options[:port])
if options[:secure]
ctx = OpenSSL::SSL::SSLContext.new
ctx.cert = OpenSSL::X509::Certificate.new(File.open('keys/server.crt'))
ctx.key = OpenSSL::PKey::RSA.new(File.open('keys/server.key'))
ctx.npn_protocols = [DRAFT]
server = OpenSSL::SSL::SSLServer.new(server, ctx)
end
def request_header_hash
Hash.new do |hash, key|
k = key.to_s.downcase
k.tr! '_', '-'
_, value = hash.find { |header_key, _| header_key.downcase == k }
hash[key] = value if value
end
end
class UpgradeHandler
VALID_UPGRADE_METHODS = %w(GET OPTIONS).freeze
UPGRADE_RESPONSE = <<RESP.freeze
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
RESP
attr_reader :complete, :headers, :body, :parsing
def initialize(conn, sock)
@conn, @sock = conn, sock
@complete, @parsing = false, false
@headers = request_header_hash
@body = ''
@parser = ::HTTP::Parser.new(self)
end
def <<(data)
@parsing ||= true
@parser << data
return unless complete
@sock.write UPGRADE_RESPONSE
settings = headers['http2-settings']
request = {
':scheme' => 'http',
':method' => @parser.http_method,
':authority' => headers['Host'],
':path' => @parser.request_url,
}.merge(headers)
@conn.upgrade(settings, request, @body)
end
def complete!
@complete = true
end
def on_headers_complete(headers)
@headers.merge! headers
end
def on_body(chunk)
@body << chunk
end
def on_message_complete
fail unless VALID_UPGRADE_METHODS.include?(@parser.http_method)
@parsing = false
complete!
end
end
loop do
sock = server.accept
puts 'New TCP connection!'
conn = HTTP2::Server.new
conn.on(:frame) do |bytes|
# puts "Writing bytes: #{bytes.unpack("H*").first}"
sock.write bytes
end
conn.on(:frame_sent) do |frame|
puts "Sent frame: #{frame.inspect}"
end
conn.on(:frame_received) do |frame|
puts "Received frame: #{frame.inspect}"
end
conn.on(:stream) do |stream|
log = Logger.new(stream.id)
req = request_header_hash
buffer = ''
stream.on(:active) { log.info 'client opened new stream' }
stream.on(:close) do
log.info 'stream closed'
end
stream.on(:headers) do |h|
req.merge! Hash[*h.flatten]
log.info "request headers: #{h}"
end
stream.on(:data) do |d|
log.info "payload chunk: <<#{d}>>"
buffer << d
end
stream.on(:half_close) do
log.info 'client closed its end of the stream'
if req['Upgrade']
log.info "Processing h2c Upgrade request: #{req}"
if req[':method'] != 'OPTIONS' # Don't respond to OPTIONS...
response = 'Hello h2c world!'
stream.headers({
':status' => '200',
'content-length' => response.bytesize.to_s,
'content-type' => 'text/plain',
}, end_stream: false)
stream.data(response)
end
else
response = nil
if req[':method'] == 'POST'
log.info "Received POST request, payload: #{buffer}"
response = "Hello HTTP 2.0! POST payload: #{buffer}"
else
log.info 'Received GET request'
response = 'Hello HTTP 2.0! GET request'
end
stream.headers({
':status' => '200',
'content-length' => response.bytesize.to_s,
'content-type' => 'text/plain',
}, end_stream: false)
# split response into multiple DATA frames
stream.data(response.slice!(0, 5), end_stream: false)
stream.data(response)
end
end
end
uh = UpgradeHandler.new(conn, sock)
while !sock.closed? && !(sock.eof? rescue true) # rubocop:disable Style/RescueModifier
data = sock.readpartial(1024)
# puts "Received bytes: #{data.unpack("H*").first}"
begin
case
when !uh.parsing && !uh.complete
if data.start_with?(*UpgradeHandler::VALID_UPGRADE_METHODS)
uh << data
else
uh.complete!
conn << data
end
when uh.parsing && !uh.complete
uh << data
when uh.complete
conn << data
end
rescue StandardError => e
puts "Exception: #{e}, #{e.message} - closing socket."
puts e.backtrace.last(10).join("\n")
sock.close
end
end
end
# echo foo=bar | nghttp -d - -t 0 -vu http://127.0.0.1:8080/
# nghttp -vu http://127.0.0.1:8080/
|