[331] | 1 | # Required mrbgems: mruby-pack, mruby-regexp-pcre(or some regexp mrbgem)
|
---|
| 2 |
|
---|
| 3 | # TODO:
|
---|
| 4 | # - connect via proxy
|
---|
| 5 |
|
---|
| 6 | module HTTP2
|
---|
| 7 | FRAME_TYPE_DATA = 0
|
---|
| 8 | FRAME_TYPE_HEADERS = 1
|
---|
| 9 | FRAME_TYPE_SETTINGS = 4
|
---|
| 10 | FRAME_TYPE_GOAWAY = 7
|
---|
| 11 | FRAME_TYPE_BLOCK = 11
|
---|
| 12 |
|
---|
| 13 | class Client
|
---|
| 14 | FRAME_FLAG_SETTINGS_ACK = 0x1
|
---|
| 15 |
|
---|
| 16 | def initialize(host, port=443)
|
---|
| 17 | @tls = TLS.new host, {
|
---|
| 18 | :version => "TLSv1.2",
|
---|
| 19 | :port => port,
|
---|
| 20 | :alpn => "h2",
|
---|
| 21 | :certs => "nghttp2.crt", :identity => "nghttp2"
|
---|
| 22 | }
|
---|
| 23 | @recvbuf = ""
|
---|
| 24 | @my_next_stream_id = 1
|
---|
| 25 | @window = 0
|
---|
| 26 | @streams = {}
|
---|
| 27 |
|
---|
| 28 | @settings = {
|
---|
| 29 | :header_table_size => 4096,
|
---|
| 30 | :enable_push => 1,
|
---|
| 31 | :max_concurrent_streams => -1, # no limit
|
---|
| 32 | :enable_push => 1,
|
---|
| 33 | :initial_window_size => 65535,
|
---|
| 34 | :compress_data => 0
|
---|
| 35 | }
|
---|
| 36 |
|
---|
| 37 | self.connect
|
---|
| 38 | end
|
---|
| 39 |
|
---|
| 40 | def close
|
---|
| 41 | # send goaway
|
---|
| 42 | @tls.close
|
---|
| 43 | end
|
---|
| 44 |
|
---|
| 45 | def connect
|
---|
| 46 | self.send_magic
|
---|
| 47 | self.send_settings_frame
|
---|
| 48 | self.wait_for :settings_ack
|
---|
| 49 | end
|
---|
| 50 |
|
---|
| 51 | def get path, &block
|
---|
| 52 | stream = self.new_stream
|
---|
| 53 | self.send_headers_frame path
|
---|
| 54 | self.wait_for :data_end
|
---|
| 55 | $stdout.write stream.response_body
|
---|
| 56 | end
|
---|
| 57 |
|
---|
| 58 | def make_frame(type, flags, stream, payload)
|
---|
| 59 | len = [payload.size/65536, payload.size/256, payload.size].map {
|
---|
| 60 | |x| x % 256 }.pack("C3")
|
---|
| 61 | len + [type, flags, stream].pack("CCN") + payload
|
---|
| 62 | end
|
---|
| 63 |
|
---|
| 64 | def new_stream
|
---|
| 65 | id = @my_next_stream_id
|
---|
| 66 | @my_next_stream_id += 2
|
---|
| 67 | stream = Stream.new(self, id, @settings)
|
---|
| 68 | @streams[id] = stream
|
---|
| 69 | stream
|
---|
| 70 | end
|
---|
| 71 |
|
---|
| 72 | def send_frame f
|
---|
| 73 | puts "send_frame: #{f.inspect}" if $debug
|
---|
| 74 | @tls.write f.to_bytes
|
---|
| 75 | end
|
---|
| 76 |
|
---|
| 77 | def send_magic
|
---|
| 78 | @tls.write "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
---|
| 79 | end
|
---|
| 80 |
|
---|
| 81 | def send_settings_frame
|
---|
| 82 | payload = ""
|
---|
| 83 | frame = make_frame(4, 0, 0, "")
|
---|
| 84 | puts "send_settings_frame: #{frame.inspect}" if $debug
|
---|
| 85 | @tls.write frame
|
---|
| 86 | end
|
---|
| 87 |
|
---|
| 88 | def send_headers_frame path
|
---|
| 89 | def make_a_header(key, val)
|
---|
| 90 | "\x00" + [key.length].pack("C") + key + [val.length].pack("C") + val
|
---|
| 91 | end
|
---|
| 92 |
|
---|
| 93 | payload = "\x00\x00"
|
---|
| 94 | payload = ""
|
---|
| 95 | payload += make_a_header(":method", "GET")
|
---|
| 96 | payload += make_a_header(":scheme", "https")
|
---|
| 97 | payload += make_a_header(":authority", "1.2.3.4:80")
|
---|
| 98 | payload += make_a_header(":path", path)
|
---|
| 99 |
|
---|
| 100 | frame = make_frame(FRAME_TYPE_HEADERS, 5, 1, payload)
|
---|
| 101 | @tls.write frame
|
---|
| 102 | end
|
---|
| 103 |
|
---|
| 104 | def send_window_update(stream_id, inc)
|
---|
| 105 | self.send_frame WindowUpdateFrame.make_update(stream_id, inc)
|
---|
| 106 | self.send_frame WindowUpdateFrame.make_update(0, inc)
|
---|
| 107 | end
|
---|
| 108 |
|
---|
| 109 | def recv_frame
|
---|
| 110 | loop do
|
---|
| 111 | f = Frame.parse @recvbuf
|
---|
| 112 | if f
|
---|
| 113 | @recvbuf[0, f.bytelen] = ""
|
---|
| 114 | return f
|
---|
| 115 | end
|
---|
| 116 | bytes = @tls.read(1000)
|
---|
| 117 | @recvbuf += bytes
|
---|
| 118 | end
|
---|
| 119 | end
|
---|
| 120 |
|
---|
| 121 | def stream id
|
---|
| 122 | @streams[id]
|
---|
| 123 | end
|
---|
| 124 |
|
---|
| 125 | def wait_for cond
|
---|
| 126 | while frame = self.recv_frame
|
---|
| 127 | if frame.is_a? SettingsFrame
|
---|
| 128 | if frame.ack?
|
---|
| 129 | break if cond == :settings_ack
|
---|
| 130 | else
|
---|
| 131 | @settings = frame.settings
|
---|
| 132 | self.send_frame SettingsFrame.make_ack_frame
|
---|
| 133 | end
|
---|
| 134 | elsif frame.is_a? DataFrame
|
---|
| 135 | stream = @streams[frame.stream_id]
|
---|
| 136 | stream.recv_data_frame(frame)
|
---|
| 137 | break if cond == :data_end and frame.end_stream?
|
---|
| 138 | end
|
---|
| 139 | puts "wait_for receive: #{frame.inspect}" if $debug
|
---|
| 140 | end
|
---|
| 141 | end
|
---|
| 142 | end
|
---|
| 143 |
|
---|
| 144 | class Stream
|
---|
| 145 | def initialize(client, id, settings)
|
---|
| 146 | @client = client
|
---|
| 147 | @id = id
|
---|
| 148 | @window = settings[:initial_window_size]
|
---|
| 149 |
|
---|
| 150 | @response_body = ""
|
---|
| 151 | end
|
---|
| 152 |
|
---|
| 153 | attr_reader :response_body
|
---|
| 154 |
|
---|
| 155 | def close
|
---|
| 156 | end
|
---|
| 157 |
|
---|
| 158 | def recv_data_frame dframe
|
---|
| 159 | @response_body += dframe.payload
|
---|
| 160 | @client.send_window_update(@id, dframe.len) if dframe.len > 0
|
---|
| 161 | end
|
---|
| 162 | end
|
---|
| 163 |
|
---|
| 164 | class Frame
|
---|
| 165 | HEADERLEN = 9
|
---|
| 166 |
|
---|
| 167 | def initialize(len, type, flags, stream_id, payload)
|
---|
| 168 | @len = len
|
---|
| 169 | @type = type
|
---|
| 170 | @flags = flags
|
---|
| 171 | @stream_id = stream_id
|
---|
| 172 | @payload = payload
|
---|
| 173 | end
|
---|
| 174 |
|
---|
| 175 | attr_reader :len, :type, :flags, :stream_id, :payload
|
---|
| 176 |
|
---|
| 177 | def self.parse buf
|
---|
| 178 | return nil if buf.size < HEADERLEN
|
---|
| 179 | len0, len1, len2, type, flags, stream_id = buf.unpack("C3CCN")
|
---|
| 180 | len = len0*65536 + len1*256 + len2
|
---|
| 181 | return nil if buf.size < HEADERLEN + len
|
---|
| 182 | payload = buf[HEADERLEN, len]
|
---|
| 183 |
|
---|
| 184 | args = [ len, type, flags, stream_id, payload ]
|
---|
| 185 | case type
|
---|
| 186 | when HTTP2::FRAME_TYPE_DATA
|
---|
| 187 | f = DataFrame.parse(*args)
|
---|
| 188 | when HTTP2::FRAME_TYPE_HEADERS
|
---|
| 189 | f = HeadersFrame.parse(*args)
|
---|
| 190 | when HTTP2::FRAME_TYPE_SETTINGS
|
---|
| 191 | f = SettingsFrame.parse(*args)
|
---|
| 192 | when HTTP2::FRAME_TYPE_GOAWAY
|
---|
| 193 | f = GoawayFrame.parse(*args)
|
---|
| 194 | when HTTP2::FRAME_TYPE_BLOCK
|
---|
| 195 | f = BlockFrame.parse(*args)
|
---|
| 196 | else
|
---|
| 197 | raise "unsupported frame type: #{type}"
|
---|
| 198 | end
|
---|
| 199 | f
|
---|
| 200 | end
|
---|
| 201 |
|
---|
| 202 | def bytelen
|
---|
| 203 | HEADERLEN + @len
|
---|
| 204 | end
|
---|
| 205 |
|
---|
| 206 | def to_bytes
|
---|
| 207 | lens = [payload.size/65536, payload.size/256, payload.size].map {
|
---|
| 208 | |x| x % 256 }.pack("C3")
|
---|
| 209 | lens + [ @type, @flags, @stream_id ].pack("CCN") + @payload
|
---|
| 210 | end
|
---|
| 211 |
|
---|
| 212 | def inspect
|
---|
| 213 | format "<%s len=%d flags=0x%02x stream-id=%d%s>", self.class, @len, @flags, @stream_id, inspect_payload
|
---|
| 214 | end
|
---|
| 215 |
|
---|
| 216 | def inspect_payload
|
---|
| 217 | ""
|
---|
| 218 | end
|
---|
| 219 | end
|
---|
| 220 |
|
---|
| 221 | class DataFrame < Frame
|
---|
| 222 | def self.parse(len, type, flags, stream_id, payload)
|
---|
| 223 | f = self.new(len, type, flags, stream_id, payload)
|
---|
| 224 | end
|
---|
| 225 |
|
---|
| 226 | def end_stream?
|
---|
| 227 | (@flags & 1) > 0
|
---|
| 228 | end
|
---|
| 229 | end
|
---|
| 230 |
|
---|
| 231 | class HeadersFrame < Frame
|
---|
| 232 | def self.parse(len, type, flags, stream_id, payload)
|
---|
| 233 | f = self.new(len, type, flags, stream_id, payload)
|
---|
| 234 | end
|
---|
| 235 | end
|
---|
| 236 |
|
---|
| 237 | class SettingsFrame < Frame
|
---|
| 238 | def self.make_ack_frame
|
---|
| 239 | self.new(0, 4, 1, 0, "")
|
---|
| 240 | end
|
---|
| 241 |
|
---|
| 242 | def self.parse(len, type, flags, stream_id, payload)
|
---|
| 243 | if (flags & 1) == 0
|
---|
| 244 | s = payload.dup
|
---|
| 245 | h = {}
|
---|
| 246 | while s.length > 0
|
---|
| 247 | t, v = s.unpack("nN")
|
---|
| 248 | case t
|
---|
| 249 | when 1
|
---|
| 250 | h[:header_table_size] = v
|
---|
| 251 | when 2
|
---|
| 252 | h[:enable_push] = v
|
---|
| 253 | when 3
|
---|
| 254 | h[:max_concurrent_streams] = v
|
---|
| 255 | when 4
|
---|
| 256 | h[:initial_window_size] = v
|
---|
| 257 | when 5
|
---|
| 258 | h[:compress_data] = v
|
---|
| 259 | else
|
---|
| 260 | raise "unknown settings parameter: #{t} = #{v}"
|
---|
| 261 | end
|
---|
| 262 | s = s[6..-1]
|
---|
| 263 | end
|
---|
| 264 | f = self.new len, type, flags, stream_id, payload
|
---|
| 265 | f.settings = h
|
---|
| 266 | else
|
---|
| 267 | # SETTINGS ACK frame
|
---|
| 268 | f = self.new len, type, flags, stream_id, payload
|
---|
| 269 | end
|
---|
| 270 | f
|
---|
| 271 | end
|
---|
| 272 |
|
---|
| 273 | attr_accessor :settings
|
---|
| 274 |
|
---|
| 275 | def ack?
|
---|
| 276 | (flags & 1) == 1
|
---|
| 277 | end
|
---|
| 278 | end
|
---|
| 279 |
|
---|
| 280 | class WindowUpdateFrame < Frame
|
---|
| 281 | attr_accessor :inc
|
---|
| 282 |
|
---|
| 283 | def self.parse(len, type, flags, stream_id, payload)
|
---|
| 284 | f = self.new(len, type, flags, stream_id, payload)
|
---|
| 285 | end
|
---|
| 286 |
|
---|
| 287 | def self.make_update(stream_id, inc)
|
---|
| 288 | payload = [ inc ].pack("N")
|
---|
| 289 | f = self.new(payload.size, 8, 0, stream_id, payload)
|
---|
| 290 | f.inc = inc
|
---|
| 291 | f
|
---|
| 292 | end
|
---|
| 293 |
|
---|
| 294 | def inspect_payload
|
---|
| 295 | " inc=#{@inc}"
|
---|
| 296 | end
|
---|
| 297 | end
|
---|
| 298 |
|
---|
| 299 | class GoawayFrame < Frame
|
---|
| 300 | attr_accessor :laststream, :ecode, :debugdata
|
---|
| 301 |
|
---|
| 302 | def self.parse(len, type, flags, stream_id, payload)
|
---|
| 303 | f = self.new(len, type, flags, stream_id, payload)
|
---|
| 304 | f.laststream, f.ecode, f.debugdata = payload.unpack("NNa*")
|
---|
| 305 | f
|
---|
| 306 | end
|
---|
| 307 |
|
---|
| 308 | def inspect_payload
|
---|
| 309 | " last_stream_id=#{@laststream} error_code=#{@ecode} additional_debug_data=\"#{@debugdata}\""
|
---|
| 310 | end
|
---|
| 311 | end
|
---|
| 312 |
|
---|
| 313 | class BlockFrame < Frame
|
---|
| 314 | def self.parse(len, type, flags, stream_id, payload)
|
---|
| 315 | f = self.new(len, type, flags, stream_id, payload)
|
---|
| 316 | end
|
---|
| 317 | end
|
---|
| 318 | end
|
---|
| 319 |
|
---|
| 320 |
|
---|
| 321 | $debug = true
|
---|
| 322 | if ARGV.size != 1
|
---|
| 323 | puts "usage: mruby http2.rb <url>"
|
---|
| 324 | exit
|
---|
| 325 | end
|
---|
| 326 |
|
---|
| 327 | unless ARGV[0] =~ Regexp.new('https://([^:/]+)(:\d+)?(/.*)')
|
---|
| 328 | puts "unsupported url: #{ARGV[0]}"
|
---|
| 329 | exit 1
|
---|
| 330 | end
|
---|
| 331 | host = $1
|
---|
| 332 | port = ($2) ? $2[1..-1].to_i : 443
|
---|
| 333 | path = $3 || ""
|
---|
| 334 |
|
---|
| 335 | http2 = HTTP2::Client.new host, port
|
---|
| 336 | http2.get path
|
---|
| 337 | http2.close
|
---|