File Coverage

blib/lib/Hypersonic/Protocol/HTTP1.pm
Criterion Covered Total %
statement 83 87 95.4
branch 14 16 87.5
condition 18 34 52.9
subroutine 14 17 82.3
pod 5 14 35.7
total 134 168 79.7


line stmt bran cond sub pod time code
1             package Hypersonic::Protocol::HTTP1;
2 35     35   197 use strict;
  35         55  
  35         1116  
3 35     35   128 use warnings;
  35         44  
  35         53821  
4              
5             # Hypersonic::Protocol::HTTP1 - JIT code generation for HTTP/1.1 protocol
6             #
7             # This module provides compile-time code generation methods for HTTP/1.1
8             # protocol handling. All methods return C code strings or use XS::JIT::Builder
9             # to generate code. There is NO runtime overhead - everything is JIT compiled.
10             #
11             # HTTP/1.1 specific features handled here:
12             # - Text-based request parsing (GET /path HTTP/1.1\r\n)
13             # - CRLF delimiters (\r\n\r\n header/body separator)
14             # - Connection: keep-alive/close header
15             # - Response format (HTTP/1.1 200 OK\r\nHeader: Value\r\n\r\nBody)
16              
17             our $VERSION = '0.15';
18              
19             # Protocol identifier - used for version negotiation
20 0     0 0 0 sub protocol_id { 'HTTP/1.1' }
21              
22             # HTTP version string for responses
23 0     0 0 0 sub version_string { 'HTTP/1.1' }
24              
25             # Generate a complete HTTP/1.1 response at compile time
26             # Returns the full response string with headers and body
27             sub build_response {
28 55     55 1 558 my ($class, %args) = @_;
29            
30 55   50     255 my $status = $args{status} // 200;
31 55   66     527 my $status_text = $args{status_text} // _status_text($status);
32 55   100     331 my $headers = $args{headers} // {};
33 55   50     173 my $body = $args{body} // '';
34 55   50     146 my $keep_alive = $args{keep_alive} // 1;
35 55   50     151 my $security_headers = $args{security_headers} // '';
36            
37 55 100 33     858 my $ct = $headers->{'Content-Type'}
38             // (($body =~ /^\s*[\[{]/) ? 'application/json' : 'text/plain');
39            
40 55 100       387 my $response = "HTTP/1.1 $status $status_text\r\n"
41             . "Content-Type: $ct\r\n"
42             . "Content-Length: " . length($body) . "\r\n"
43             . "Connection: " . ($keep_alive ? 'keep-alive' : 'close') . "\r\n";
44            
45             # Add security headers if provided
46 55 50       179 $response .= $security_headers if $security_headers;
47            
48             # Add custom headers
49 55         315 for my $h (keys %$headers) {
50 2 50 33     17 next if $h eq 'Content-Type' || $h eq 'Content-Length' || $h eq 'Connection';
      33        
51 2         7 $response .= "$h: $headers->{$h}\r\n";
52             }
53            
54 55         116 $response .= "\r\n" . $body;
55            
56 55         241 return $response;
57             }
58              
59             # Build 404 response at compile time
60             sub build_404_response {
61 20     20 0 216 my ($class, %args) = @_;
62 20   50     85 my $security_headers = $args{security_headers} // '';
63            
64 20         175 return $class->build_response(
65             status => 404,
66             status_text => 'Not Found',
67             body => 'Not Found',
68             keep_alive => 0, # Close on 404
69             security_headers => $security_headers,
70             );
71             }
72              
73             # Generate C code for parsing HTTP method from request buffer
74             # Uses XS::JIT::Builder API for clean code generation
75             sub gen_method_parser {
76 20     20 1 58 my ($class, $builder, $analysis) = @_;
77            
78 20   50     39 my %methods_used = %{$analysis->{methods_used} // {}};
  20         173  
79            
80             # Method lengths: GET=3, PUT=3, POST=4, HEAD=4, PATCH=5, DELETE=6, OPTIONS=7
81 20         247 my %method_lens = (
82             GET => 3, PUT => 3, POST => 4, HEAD => 4,
83             PATCH => 5, DELETE => 6, OPTIONS => 7
84             );
85            
86             # Group methods by length
87 20         149 my %by_length;
88 20         161 for my $method (keys %methods_used) {
89 27   33     99 my $len = $method_lens{$method} // length($method);
90 27         63 push @{$by_length{$len}}, $method;
  27         600  
91             }
92            
93             # If single method, generate super-optimized check
94 20 100       176 if ($analysis->{single_method}) {
95 15         33 my $method = $analysis->{single_method};
96 15         27 my $len = $method_lens{$method};
97 15         43 my $first_char = substr($method, 0, 1);
98 15         27 my $path_offset = $len + 1;
99            
100 15         541 $builder->comment("OPTIMIZED: Single method ($method) - verify first char only")
101             ->line('const char* method = recv_buf;')
102             ->line("int method_len = $len;")
103             ->line("const char* path = recv_buf + $path_offset;")
104             ->blank
105             ->comment("Quick validation: first char must be '$first_char'")
106             ->if("recv_buf[0] != '$first_char'")
107             ->line('HYPERSONIC_SEND(fd, RESP_404, RESP_404_LEN);')
108             ->line('continue;')
109             ->endif;
110            
111 15         114 return $builder;
112             }
113            
114             # Multiple methods - generate only the length checks we need
115 5         240 $builder->comment('HTTP/1.1: Parse method (space-delimited)')
116             ->line('const char* method = recv_buf;')
117             ->line('int method_len;')
118             ->line('const char* path;')
119             ->blank;
120            
121 5         36 my $first = 1;
122 5         69 for my $len (sort { $a <=> $b } keys %by_length) {
  7         49  
123 11         22 my @methods_at_len = @{$by_length{$len}};
  11         29  
124 11         45 my $comment = join(', ', @methods_at_len);
125 11         21 my $path_offset = $len + 1;
126            
127 11 100       28 if ($first) {
128 5         31 $builder->if("recv_buf[$len] == ' '");
129 5         8 $first = 0;
130             } else {
131 6         28 $builder->elsif("recv_buf[$len] == ' '");
132             }
133 11         180 $builder->line("method_len = $len; /* $comment */")
134             ->line("path = recv_buf + $path_offset;");
135             }
136            
137             # Add fallback for unknown methods
138             $builder->else
139 5         127 ->comment('Fallback: scan for space')
140             ->line('const char* sp = recv_buf;')
141             ->while('*sp && *sp != \' \'')
142             ->line('sp++;')
143             ->endwhile
144             ->line('method_len = sp - recv_buf;')
145             ->line('path = sp + 1;')
146             ->endif;
147            
148 5         37 return $builder;
149             }
150              
151             # Generate C code for parsing path from HTTP/1.1 request
152             # HTTP/1.1 format: METHOD /path?query HTTP/1.1\r\n
153             sub gen_path_parser {
154 20     20 1 77 my ($class, $builder) = @_;
155            
156 20         558 $builder->comment('HTTP/1.1: Find end of path (space before HTTP/1.1)')
157             ->line('const char* path_end = path;')
158             ->line('int full_path_len;')
159             ->line('const char* query_pos;')
160             ->line('int path_len;')
161             ->while('*path_end && *path_end != \' \'')
162             ->line('path_end++;')
163             ->endwhile
164             ->line('full_path_len = path_end - path;')
165             ->blank
166             ->comment('Strip query string for route dispatch')
167             ->line('query_pos = memchr(path, \'?\', full_path_len);')
168             ->line('path_len = query_pos ? (query_pos - path) : full_path_len;');
169            
170 20         47 return $builder;
171             }
172              
173             # Generate C code for finding request body (after \r\n\r\n)
174             sub gen_body_parser {
175 4     4 1 25 my ($class, $builder, %opts) = @_;
176            
177 4 100       15 if ($opts{has_body_access}) {
178 1         29 $builder->comment('HTTP/1.1: Find body after CRLF CRLF')
179             ->line('const char* body_start = strstr(recv_buf, "\\r\\n\\r\\n");')
180             ->line('const char* body = "";')
181             ->line('int body_len = 0;')
182             ->if('body_start')
183             ->line('body = body_start + 4;') # Skip \r\n\r\n
184             ->line('body_len = len - (body - recv_buf);')
185             ->endif;
186             } else {
187 3         23 $builder->comment('OPTIMIZED: No body parsing needed')
188             ->line('const char* body = "";')
189             ->line('int body_len = 0;');
190             }
191            
192 4         11 return $builder;
193             }
194              
195             # Generate C code for keep-alive detection
196             sub gen_keepalive_check {
197 20     20 1 59 my ($class, $builder) = @_;
198            
199 20         382 $builder->comment('HTTP/1.1: Check Connection header for keep-alive')
200             ->line('int keep_alive = 1;') # HTTP/1.1 default is keep-alive
201             ->if('len > 20')
202             ->comment('Search for "Connection:" header (case-insensitive C or c)')
203             ->line('const char* conn = strstr(recv_buf + 16, "onnection:");')
204             ->if('conn && (conn[-1] == \'C\' || conn[-1] == \'c\')')
205             ->if('strstr(conn, "close") || strstr(conn, "Close")')
206             ->line('keep_alive = 0;')
207             ->endif
208             ->endif
209             ->endif;
210            
211 20         34 return $builder;
212             }
213              
214             # Status code to text mapping (complete list)
215             sub _status_text {
216 35     35   82 my ($code) = @_;
217 35         2232 my %text = (
218             200 => 'OK',
219             201 => 'Created',
220             202 => 'Accepted',
221             204 => 'No Content',
222             301 => 'Moved Permanently',
223             302 => 'Found',
224             303 => 'See Other',
225             304 => 'Not Modified',
226             307 => 'Temporary Redirect',
227             308 => 'Permanent Redirect',
228             400 => 'Bad Request',
229             401 => 'Unauthorized',
230             403 => 'Forbidden',
231             404 => 'Not Found',
232             405 => 'Method Not Allowed',
233             408 => 'Request Timeout',
234             409 => 'Conflict',
235             410 => 'Gone',
236             413 => 'Payload Too Large',
237             415 => 'Unsupported Media Type',
238             422 => 'Unprocessable Entity',
239             429 => 'Too Many Requests',
240             500 => 'Internal Server Error',
241             501 => 'Not Implemented',
242             502 => 'Bad Gateway',
243             503 => 'Service Unavailable',
244             504 => 'Gateway Timeout',
245             );
246 35   50     267 return $text{$code} // 'Unknown';
247             }
248              
249             # Get status text (class method for external use)
250             sub status_text {
251 0     0 0 0 my ($class, $code) = @_;
252 0         0 return _status_text($code);
253             }
254              
255             # ============================================================
256             # Chunked Transfer Encoding (HTTP/1.1 streaming)
257             # ============================================================
258              
259             # Generate C code for chunked response headers
260             sub gen_chunked_start {
261 2     2 0 11413 my ($class, $builder) = @_;
262            
263 2         82 $builder->comment('Send HTTP/1.1 headers with chunked encoding')
264             ->line('static void send_chunked_headers(int fd, int status, const char* content_type) {')
265             ->line(' char headers[2048];')
266             ->line(' const char* status_str = "OK";')
267             ->line(' switch(status) {')
268             ->line(' case 200: status_str = "OK"; break;')
269             ->line(' case 201: status_str = "Created"; break;')
270             ->line(' case 202: status_str = "Accepted"; break;')
271             ->line(' case 204: status_str = "No Content"; break;')
272             ->line(' case 206: status_str = "Partial Content"; break;')
273             ->line(' case 400: status_str = "Bad Request"; break;')
274             ->line(' case 401: status_str = "Unauthorized"; break;')
275             ->line(' case 403: status_str = "Forbidden"; break;')
276             ->line(' case 404: status_str = "Not Found"; break;')
277             ->line(' case 500: status_str = "Internal Server Error"; break;')
278             ->line(' case 503: status_str = "Service Unavailable"; break;')
279             ->line(' }')
280             ->line(' int len = snprintf(headers, sizeof(headers),')
281             ->line(' "HTTP/1.1 %d %s\\r\\n"')
282             ->line(' "Content-Type: %s\\r\\n"')
283             ->line(' "Transfer-Encoding: chunked\\r\\n"')
284             ->line(' "Connection: keep-alive\\r\\n"')
285             ->line(' "\\r\\n",')
286             ->line(' status, status_str, content_type);')
287             ->line(' send(fd, headers, len, 0);')
288             ->line('}')
289             ->blank;
290            
291 2         4 return $builder;
292             }
293              
294             # Generate C code for sending a chunk (hex length + data + CRLF)
295             sub gen_chunked_write {
296 2     2 0 4940 my ($class, $builder) = @_;
297            
298 2         73 $builder->comment('Send a single chunk - HTTP/1.1 chunked transfer encoding')
299             ->line('static void send_chunk(int fd, const char* data, size_t len) {')
300             ->line(' if (len == 0) return;')
301             ->line(' ')
302             ->line(' char size_line[32];')
303             ->line(' int header_len = snprintf(size_line, sizeof(size_line), "%zx\\r\\n", len);')
304             ->line(' ')
305             ->line(' /* Use writev for efficiency (header + data + crlf in one syscall) */')
306             ->line(' struct iovec iov[3];')
307             ->line(' iov[0].iov_base = size_line;')
308             ->line(' iov[0].iov_len = header_len;')
309             ->line(' iov[1].iov_base = (void*)data;')
310             ->line(' iov[1].iov_len = len;')
311             ->line(' iov[2].iov_base = "\\r\\n";')
312             ->line(' iov[2].iov_len = 2;')
313             ->line(' ')
314             ->line(' writev(fd, iov, 3);')
315             ->line('}')
316             ->blank;
317            
318 2         5 return $builder;
319             }
320              
321             # Generate C code for final chunk (0\r\n\r\n)
322             sub gen_chunked_end {
323 2     2 0 4630 my ($class, $builder) = @_;
324            
325 2         22 $builder->comment('Send final zero-length chunk to end stream')
326             ->line('static void send_chunk_end(int fd) {')
327             ->line(' send(fd, "0\\r\\n\\r\\n", 5, 0);')
328             ->line('}')
329             ->blank;
330            
331 2         4 return $builder;
332             }
333              
334             # Build a pre-formatted chunk for compile-time use
335             sub build_chunk {
336 9     9 0 11586 my ($class, $data) = @_;
337            
338 9 100 100     57 return '' unless defined $data && length($data);
339            
340 7         12 my $len = length($data);
341 7         38 return sprintf("%x\r\n%s\r\n", $len, $data);
342             }
343              
344             # Build final chunk
345             sub build_final_chunk {
346 2     2 0 7239 return "0\r\n\r\n";
347             }
348              
349             1;
350              
351             __END__