File Coverage

blib/lib/Hypersonic/UA/HTTP2.pm
Criterion Covered Total %
statement 55 65 84.6
branch 3 6 50.0
condition 3 11 27.2
subroutine 14 16 87.5
pod 0 12 0.0
total 75 110 68.1


line stmt bran cond sub pod time code
1             package Hypersonic::UA::HTTP2;
2              
3 1     1   200175 use strict;
  1         1  
  1         28  
4 1     1   3 use warnings;
  1         1  
  1         38  
5 1     1   12 use 5.010;
  1         2  
6              
7             our $VERSION = '0.15';
8              
9             use constant {
10 1         1525 MAX_H2_SESSIONS => 100,
11             MAX_STREAMS_PER_SESSION => 100,
12 1     1   4 };
  1         2  
13              
14             my $NGHTTP2_AVAILABLE;
15              
16             sub check_nghttp2 {
17 1 50   1 0 4668 return $NGHTTP2_AVAILABLE if defined $NGHTTP2_AVAILABLE;
18              
19 1         5618 my $libs = `pkg-config --libs libnghttp2 2>/dev/null`;
20 1 50 33     29 if ($? == 0 && $libs) {
21 0         0 $NGHTTP2_AVAILABLE = 1;
22 0         0 return 1;
23             }
24              
25 1         12 for my $path (qw(/usr/include/nghttp2/nghttp2.h /usr/local/include/nghttp2/nghttp2.h)) {
26 2 50       49 if (-f $path) {
27 0         0 $NGHTTP2_AVAILABLE = 1;
28 0         0 return 1;
29             }
30             }
31              
32 1         7 $NGHTTP2_AVAILABLE = 0;
33 1         16 return 0;
34             }
35              
36             sub get_extra_cflags {
37 0   0 0 0 0 my $cflags = `pkg-config --cflags libnghttp2 2>/dev/null` || '';
38 0         0 chomp $cflags;
39 0         0 return $cflags;
40             }
41              
42             sub get_extra_ldflags {
43 0   0 0 0 0 my $ldflags = `pkg-config --libs libnghttp2 2>/dev/null` || '-lnghttp2';
44 0         0 chomp $ldflags;
45 0         0 return $ldflags;
46             }
47              
48             sub generate_c_code {
49 1     1 0 10396 my ($class, $builder, $opts) = @_;
50              
51 1   50     5 my $max_sessions = $opts->{max_h2_sessions} // MAX_H2_SESSIONS;
52 1   50     4 my $max_streams = $opts->{max_streams_per_session} // MAX_STREAMS_PER_SESSION;
53              
54 1         6 $class->gen_h2_registry($builder, $max_sessions, $max_streams);
55 1         3 $class->gen_h2_callbacks($builder);
56 1         4 $class->gen_xs_session_new($builder);
57 1         12 $class->gen_xs_submit_request($builder);
58 1         3 $class->gen_xs_receive($builder);
59 1         7 $class->gen_xs_session_close($builder);
60 1         3 $class->gen_xs_is_complete($builder);
61             }
62              
63             sub get_xs_functions {
64             return {
65 1     1 0 4263 'Hypersonic::UA::HTTP2::session_new' => { source => 'xs_h2_session_new', is_xs_native => 1 },
66             'Hypersonic::UA::HTTP2::submit_request' => { source => 'xs_h2_submit_request', is_xs_native => 1 },
67             'Hypersonic::UA::HTTP2::receive' => { source => 'xs_h2_receive', is_xs_native => 1 },
68             'Hypersonic::UA::HTTP2::session_close' => { source => 'xs_h2_session_close', is_xs_native => 1 },
69             'Hypersonic::UA::HTTP2::is_complete' => { source => 'xs_h2_is_complete', is_xs_native => 1 },
70             };
71             }
72              
73             sub gen_h2_registry {
74 1     1 0 2 my ($class, $builder, $max_sessions, $max_streams) = @_;
75              
76 1         13 $builder->line('#include ')
77             ->line('#include ')
78             ->line('#include ')
79             ->line('#include ')
80             ->line('#include ')
81             ->line('#include ')
82             ->blank;
83              
84 1         7 $builder->line("#define MAX_H2_SESSIONS $max_sessions")
85             ->line("#define MAX_STREAMS_PER_SESSION $max_streams")
86             ->blank;
87              
88 1         26 $builder->line('typedef struct {')
89             ->line(' int32_t stream_id;')
90             ->line(' int status;')
91             ->line(' char* body;')
92             ->line(' size_t body_len;')
93             ->line(' size_t body_cap;')
94             ->line(' HV* headers;')
95             ->line(' int complete;')
96             ->line('} H2Stream;')
97             ->blank;
98              
99 1         9 $builder->line('typedef struct {')
100             ->line(' int fd;')
101             ->line(' int tls;')
102             ->line(' nghttp2_session* session;')
103             ->line(" H2Stream streams[MAX_STREAMS_PER_SESSION];")
104             ->line(' int stream_count;')
105             ->line('} H2Session;')
106             ->blank;
107              
108 1         3 $builder->line("static H2Session h2_registry[MAX_H2_SESSIONS];")
109             ->blank;
110              
111             # Helper: find session by fd
112 1         10 $builder->line('static H2Session* h2_find_session(int fd) {')
113             ->line(' int i;')
114             ->line(' for (i = 0; i < MAX_H2_SESSIONS; i++) {')
115             ->line(' if (h2_registry[i].fd == fd) {')
116             ->line(' return &h2_registry[i];')
117             ->line(' }')
118             ->line(' }')
119             ->line(' return NULL;')
120             ->line('}')
121             ->blank;
122              
123             # Helper: get session by slot
124 1         7 $builder->line('static H2Session* h2_get_session(int slot) {')
125             ->line(' if (slot < 0 || slot >= MAX_H2_SESSIONS) return NULL;')
126             ->line(' if (h2_registry[slot].fd == 0) return NULL;')
127             ->line(' return &h2_registry[slot];')
128             ->line('}')
129             ->blank;
130              
131             # Helper: allocate session slot
132 1         18 $builder->line('static int h2_alloc_session(int fd) {')
133             ->line(' int i;')
134             ->line(' for (i = 0; i < MAX_H2_SESSIONS; i++) {')
135             ->line(' if (h2_registry[i].fd == 0) {')
136             ->line(' memset(&h2_registry[i], 0, sizeof(H2Session));')
137             ->line(' h2_registry[i].fd = fd;')
138             ->line(' return i;')
139             ->line(' }')
140             ->line(' }')
141             ->line(' return -1;')
142             ->line('}')
143             ->blank;
144              
145             # Helper: find stream in session
146 1         11 $builder->line('static H2Stream* h2_find_stream(H2Session* sess, int32_t stream_id) {')
147             ->line(' int i;')
148             ->line(' for (i = 0; i < sess->stream_count; i++) {')
149             ->line(' if (sess->streams[i].stream_id == stream_id) {')
150             ->line(' return &sess->streams[i];')
151             ->line(' }')
152             ->line(' }')
153             ->line(' return NULL;')
154             ->line('}')
155             ->blank;
156             }
157              
158             sub gen_h2_callbacks {
159 1     1 0 2 my ($class, $builder) = @_;
160              
161             # Send callback
162 1         19 $builder->comment('nghttp2 send callback')
163             ->line('static ssize_t h2_send_cb(nghttp2_session* session,')
164             ->line(' const uint8_t* data, size_t length,')
165             ->line(' int flags, void* user_data) {')
166             ->line(' H2Session* h2sess = (H2Session*)user_data;')
167             ->line(' ssize_t ret = send(h2sess->fd, data, length, 0);')
168             ->line(' if (ret < 0) {')
169             ->line(' if (errno == EAGAIN || errno == EWOULDBLOCK) {')
170             ->line(' return NGHTTP2_ERR_WOULDBLOCK;')
171             ->line(' }')
172             ->line(' return NGHTTP2_ERR_CALLBACK_FAILURE;')
173             ->line(' }')
174             ->line(' return ret;')
175             ->line('}')
176             ->blank;
177              
178             # Recv callback
179 1         24 $builder->comment('nghttp2 recv callback')
180             ->line('static ssize_t h2_recv_cb(nghttp2_session* session,')
181             ->line(' uint8_t* buf, size_t length,')
182             ->line(' int flags, void* user_data) {')
183             ->line(' H2Session* h2sess = (H2Session*)user_data;')
184             ->line(' ssize_t ret = recv(h2sess->fd, buf, length, 0);')
185             ->line(' if (ret < 0) {')
186             ->line(' if (errno == EAGAIN || errno == EWOULDBLOCK) {')
187             ->line(' return NGHTTP2_ERR_WOULDBLOCK;')
188             ->line(' }')
189             ->line(' return NGHTTP2_ERR_CALLBACK_FAILURE;')
190             ->line(' }')
191             ->line(' if (ret == 0) return NGHTTP2_ERR_EOF;')
192             ->line(' return ret;')
193             ->line('}')
194             ->blank;
195              
196             # Header callback
197 1         21 $builder->comment('nghttp2 header callback')
198             ->line('static int h2_on_header_cb(nghttp2_session* session,')
199             ->line(' const nghttp2_frame* frame,')
200             ->line(' const uint8_t* name, size_t namelen,')
201             ->line(' const uint8_t* value, size_t valuelen,')
202             ->line(' uint8_t flags, void* user_data) {')
203             ->line(' H2Session* h2sess = (H2Session*)user_data;')
204             ->line(' int32_t stream_id = frame->hd.stream_id;')
205             ->blank
206             ->line(' H2Stream* st = h2_find_stream(h2sess, stream_id);')
207             ->line(' if (!st) return 0;')
208             ->blank
209             ->line(' if (namelen == 7 && memcmp(name, ":status", 7) == 0) {')
210             ->line(' st->status = atoi((char*)value);')
211             ->line(' } else if (name[0] != \':\') {')
212             ->line(' if (!st->headers) st->headers = newHV();')
213             ->line(' hv_store(st->headers, (char*)name, namelen, newSVpvn((char*)value, valuelen), 0);')
214             ->line(' }')
215             ->line(' return 0;')
216             ->line('}')
217             ->blank;
218              
219             # Data chunk callback
220 1         17 $builder->comment('nghttp2 data chunk callback')
221             ->line('static int h2_on_data_chunk_cb(nghttp2_session* session, uint8_t flags,')
222             ->line(' int32_t stream_id,')
223             ->line(' const uint8_t* data, size_t len,')
224             ->line(' void* user_data) {')
225             ->line(' H2Session* h2sess = (H2Session*)user_data;')
226             ->line(' H2Stream* st = h2_find_stream(h2sess, stream_id);')
227             ->line(' if (!st) return 0;')
228             ->blank
229             ->line(' if (st->body_len + len > st->body_cap) {')
230             ->line(' size_t new_cap = (st->body_cap + len) * 2;')
231             ->line(' if (new_cap < 4096) new_cap = 4096;')
232             ->line(' st->body = realloc(st->body, new_cap);')
233             ->line(' st->body_cap = new_cap;')
234             ->line(' }')
235             ->blank
236             ->line(' memcpy(st->body + st->body_len, data, len);')
237             ->line(' st->body_len += len;')
238             ->line(' return 0;')
239             ->line('}')
240             ->blank;
241              
242             # Stream close callback
243 1         13 $builder->comment('nghttp2 stream close callback')
244             ->line('static int h2_on_stream_close_cb(nghttp2_session* session,')
245             ->line(' int32_t stream_id,')
246             ->line(' uint32_t error_code, void* user_data) {')
247             ->line(' H2Session* h2sess = (H2Session*)user_data;')
248             ->line(' H2Stream* st = h2_find_stream(h2sess, stream_id);')
249             ->line(' if (st) st->complete = 1;')
250             ->line(' return 0;')
251             ->line('}')
252             ->blank;
253             }
254              
255             sub gen_xs_session_new {
256 1     1 0 2 my ($class, $builder) = @_;
257              
258 1         66 $builder->comment('Create HTTP/2 client session')
259             ->xs_function('xs_h2_session_new')
260             ->xs_preamble
261             ->line('int fd;')
262             ->line('int slot;')
263             ->line('H2Session* h2sess;')
264             ->line('nghttp2_session_callbacks* callbacks;')
265             ->line('nghttp2_settings_entry iv[1];')
266             ->blank
267             ->line('if (items != 1) croak("Usage: session_new(fd)");')
268             ->line('fd = (int)SvIV(ST(0));')
269             ->blank
270             ->line('slot = h2_alloc_session(fd);')
271             ->line('if (slot < 0) {')
272             ->line(' ST(0) = sv_2mortal(newSViv(-1));')
273             ->line(' XSRETURN(1);')
274             ->line('}')
275             ->blank
276             ->line('h2sess = &h2_registry[slot];')
277             ->blank
278             ->line('nghttp2_session_callbacks_new(&callbacks);')
279             ->line('nghttp2_session_callbacks_set_send_callback(callbacks, h2_send_cb);')
280             ->line('nghttp2_session_callbacks_set_recv_callback(callbacks, h2_recv_cb);')
281             ->line('nghttp2_session_callbacks_set_on_header_callback(callbacks, h2_on_header_cb);')
282             ->line('nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, h2_on_data_chunk_cb);')
283             ->line('nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, h2_on_stream_close_cb);')
284             ->blank
285             ->line('nghttp2_session_client_new(&h2sess->session, callbacks, h2sess);')
286             ->line('nghttp2_session_callbacks_del(callbacks);')
287             ->blank
288             ->line('iv[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS;')
289             ->line('iv[0].value = 100;')
290             ->line('nghttp2_submit_settings(h2sess->session, NGHTTP2_FLAG_NONE, iv, 1);')
291             ->line('nghttp2_session_send(h2sess->session);')
292             ->blank
293             ->line('ST(0) = sv_2mortal(newSViv(slot));')
294             ->xs_return('1')
295             ->xs_end
296             ->blank;
297             }
298              
299             sub gen_xs_submit_request {
300 1     1 0 2 my ($class, $builder) = @_;
301              
302 1         58 $builder->comment('Submit HTTP/2 request')
303             ->xs_function('xs_h2_submit_request')
304             ->xs_preamble
305             ->line('int slot;')
306             ->line('STRLEN method_len, scheme_len, auth_len, path_len;')
307             ->line('const char* method;')
308             ->line('const char* scheme;')
309             ->line('const char* authority;')
310             ->line('const char* path;')
311             ->line('H2Session* h2sess;')
312             ->line('nghttp2_nv hdrs[4];')
313             ->line('int32_t stream_id;')
314             ->line('int idx;')
315             ->line('H2Stream* st;')
316             ->blank
317             ->line('if (items < 5) croak("Usage: submit_request(session_slot, method, scheme, authority, path)");')
318             ->blank
319             ->line('slot = (int)SvIV(ST(0));')
320             ->line('method = SvPV(ST(1), method_len);')
321             ->line('scheme = SvPV(ST(2), scheme_len);')
322             ->line('authority = SvPV(ST(3), auth_len);')
323             ->line('path = SvPV(ST(4), path_len);')
324             ->blank
325             ->line('h2sess = h2_get_session(slot);')
326             ->line('if (!h2sess) {')
327             ->line(' ST(0) = sv_2mortal(newSViv(-1));')
328             ->line(' XSRETURN(1);')
329             ->line('}')
330             ->blank
331             ->line('hdrs[0].name = (uint8_t*)":method";')
332             ->line('hdrs[0].value = (uint8_t*)method;')
333             ->line('hdrs[0].namelen = 7;')
334             ->line('hdrs[0].valuelen = method_len;')
335             ->line('hdrs[0].flags = NGHTTP2_NV_FLAG_NONE;')
336             ->line('hdrs[1].name = (uint8_t*)":scheme";')
337             ->line('hdrs[1].value = (uint8_t*)scheme;')
338             ->line('hdrs[1].namelen = 7;')
339             ->line('hdrs[1].valuelen = scheme_len;')
340             ->line('hdrs[1].flags = NGHTTP2_NV_FLAG_NONE;')
341             ->line('hdrs[2].name = (uint8_t*)":authority";')
342             ->line('hdrs[2].value = (uint8_t*)authority;')
343             ->line('hdrs[2].namelen = 10;')
344             ->line('hdrs[2].valuelen = auth_len;')
345             ->line('hdrs[2].flags = NGHTTP2_NV_FLAG_NONE;')
346             ->line('hdrs[3].name = (uint8_t*)":path";')
347             ->line('hdrs[3].value = (uint8_t*)path;')
348             ->line('hdrs[3].namelen = 5;')
349             ->line('hdrs[3].valuelen = path_len;')
350             ->line('hdrs[3].flags = NGHTTP2_NV_FLAG_NONE;')
351             ->blank
352             ->line('stream_id = nghttp2_submit_request(h2sess->session, NULL, hdrs, 4, NULL, NULL);')
353             ->line('if (stream_id < 0) {')
354             ->line(' ST(0) = sv_2mortal(newSViv(stream_id));')
355             ->line(' XSRETURN(1);')
356             ->line('}')
357             ->blank
358             ->line('idx = h2sess->stream_count++;')
359             ->line('st = &h2sess->streams[idx];')
360             ->line('memset(st, 0, sizeof(H2Stream));')
361             ->line('st->stream_id = stream_id;')
362             ->line('st->body = malloc(4096);')
363             ->line('st->body_cap = 4096;')
364             ->blank
365             ->line('nghttp2_session_send(h2sess->session);')
366             ->blank
367             ->line('ST(0) = sv_2mortal(newSViv(stream_id));')
368             ->xs_return('1')
369             ->xs_end
370             ->blank;
371             }
372              
373             sub gen_xs_receive {
374 1     1 0 3 my ($class, $builder) = @_;
375              
376 1         62 $builder->comment('Receive HTTP/2 response')
377             ->xs_function('xs_h2_receive')
378             ->xs_preamble
379             ->line('if (items != 2) croak("Usage: receive(session_slot, stream_id)");')
380             ->blank
381             ->line('int slot = (int)SvIV(ST(0));')
382             ->line('int32_t stream_id = (int32_t)SvIV(ST(1));')
383             ->blank
384             ->line('H2Session* h2sess = h2_get_session(slot);')
385             ->line('if (!h2sess) {')
386             ->line(' ST(0) = &PL_sv_undef;')
387             ->line(' XSRETURN(1);')
388             ->line('}')
389             ->blank
390             ->line('int max_loops = 1000;')
391             ->line('while (max_loops-- > 0) {')
392             ->line(' H2Stream* st = h2_find_stream(h2sess, stream_id);')
393             ->blank
394             ->line(' if (st && st->complete) {')
395             ->line(' AV* result = newAV();')
396             ->line(' av_push(result, newSViv(st->status));')
397             ->line(' av_push(result, st->body ? newSVpvn(st->body, st->body_len) : newSVpvn("", 0));')
398             ->line(' av_push(result, st->headers ? newRV_inc((SV*)st->headers) : newRV_noinc((SV*)newHV()));')
399             ->line(' ST(0) = sv_2mortal(newRV_noinc((SV*)result));')
400             ->line(' XSRETURN(1);')
401             ->line(' }')
402             ->blank
403             ->line(' int rv = nghttp2_session_recv(h2sess->session);')
404             ->line(' if (rv != 0 && rv != NGHTTP2_ERR_WOULDBLOCK) break;')
405             ->line('}')
406             ->blank
407             ->line('ST(0) = &PL_sv_undef;')
408             ->xs_return('1')
409             ->xs_end
410             ->blank;
411             }
412              
413             sub gen_xs_session_close {
414 1     1 0 4 my ($class, $builder) = @_;
415              
416 1         28 $builder->comment('Close HTTP/2 session')
417             ->xs_function('xs_h2_session_close')
418             ->xs_preamble
419             ->line('int i;')
420             ->line('if (items != 1) croak("Usage: session_close(session_slot)");')
421             ->blank
422             ->line('int slot = (int)SvIV(ST(0));')
423             ->line('H2Session* h2sess = h2_get_session(slot);')
424             ->line('if (!h2sess) {')
425             ->line(' ST(0) = sv_2mortal(newSViv(0));')
426             ->line(' XSRETURN(1);')
427             ->line('}')
428             ->blank
429             ->line('for (i = 0; i < h2sess->stream_count; i++) {')
430             ->line(' H2Stream* st = &h2sess->streams[i];')
431             ->line(' if (st->body) free(st->body);')
432             ->line(' if (st->headers) SvREFCNT_dec((SV*)st->headers);')
433             ->line('}')
434             ->blank
435             ->line('if (h2sess->session) {')
436             ->line(' nghttp2_session_del(h2sess->session);')
437             ->line('}')
438             ->blank
439             ->line('if (h2sess->fd > 0) {')
440             ->line(' close(h2sess->fd);')
441             ->line('}')
442             ->blank
443             ->line('memset(h2sess, 0, sizeof(H2Session));')
444             ->line('ST(0) = sv_2mortal(newSViv(1));')
445             ->xs_return('1')
446             ->xs_end
447             ->blank;
448             }
449              
450             sub gen_xs_is_complete {
451 1     1 0 1 my ($class, $builder) = @_;
452              
453 1         49 $builder->comment('Check if stream complete')
454             ->xs_function('xs_h2_is_complete')
455             ->xs_preamble
456             ->line('if (items != 2) croak("Usage: is_complete(session_slot, stream_id)");')
457             ->blank
458             ->line('int slot = (int)SvIV(ST(0));')
459             ->line('int32_t stream_id = (int32_t)SvIV(ST(1));')
460             ->blank
461             ->line('H2Session* h2sess = h2_get_session(slot);')
462             ->line('if (!h2sess) {')
463             ->line(' ST(0) = &PL_sv_no;')
464             ->line(' XSRETURN(1);')
465             ->line('}')
466             ->blank
467             ->line('H2Stream* st = h2_find_stream(h2sess, stream_id);')
468             ->line('ST(0) = (st && st->complete) ? &PL_sv_yes : &PL_sv_no;')
469             ->xs_return('1')
470             ->xs_end
471             ->blank;
472             }
473              
474             1;