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
|
---|