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 29     29   149 use strict;
  29         61  
  29         907  
3 29     29   95 use warnings;
  29         36  
  29         41177  
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.18';
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 37     37 1 370 my ($class, %args) = @_;
29            
30 37   50     158 my $status = $args{status} // 200;
31 37   66     197 my $status_text = $args{status_text} // _status_text($status);
32 37   100     191 my $headers = $args{headers} // {};
33 37   50     181 my $body = $args{body} // '';
34 37   50     96 my $keep_alive = $args{keep_alive} // 1;
35 37   50     100 my $security_headers = $args{security_headers} // '';
36            
37 37 100 33     418 my $ct = $headers->{'Content-Type'}
38             // (($body =~ /^\s*[\[{]/) ? 'application/json' : 'text/plain');
39            
40 37 100       225 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 37 50       148 $response .= $security_headers if $security_headers;
47            
48             # Add custom headers
49 37         153 for my $h (keys %$headers) {
50 2 50 33     15 next if $h eq 'Content-Type' || $h eq 'Content-Length' || $h eq 'Connection';
      33        
51 2         5 $response .= "$h: $headers->{$h}\r\n";
52             }
53            
54 37         93 $response .= "\r\n" . $body;
55            
56 37         148 return $response;
57             }
58              
59             # Build 404 response at compile time
60             sub build_404_response {
61 14     14 0 129 my ($class, %args) = @_;
62 14   50     61 my $security_headers = $args{security_headers} // '';
63            
64 14         91 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 14     14 1 43 my ($class, $builder, $analysis) = @_;
77            
78 14   50     27 my %methods_used = %{$analysis->{methods_used} // {}};
  14         267  
79            
80             # Method lengths: GET=3, PUT=3, POST=4, HEAD=4, PATCH=5, DELETE=6, OPTIONS=7
81 14         177 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 14         25 my %by_length;
88 14         76 for my $method (keys %methods_used) {
89 18   33     48 my $len = $method_lens{$method} // length($method);
90 18         26 push @{$by_length{$len}}, $method;
  18         217  
91             }
92            
93             # If single method, generate super-optimized check
94 14 100       44 if ($analysis->{single_method}) {
95 12         24 my $method = $analysis->{single_method};
96 12         21 my $len = $method_lens{$method};
97 12         29 my $first_char = substr($method, 0, 1);
98 12         21 my $path_offset = $len + 1;
99            
100 12         325 $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 12         67 return $builder;
112             }
113            
114             # Multiple methods - generate only the length checks we need
115 2         38 $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 2         5 my $first = 1;
122 2         15 for my $len (sort { $a <=> $b } keys %by_length) {
  4         15  
123 5         4 my @methods_at_len = @{$by_length{$len}};
  5         11  
124 5         12 my $comment = join(', ', @methods_at_len);
125 5         5 my $path_offset = $len + 1;
126            
127 5 100       10 if ($first) {
128 2         10 $builder->if("recv_buf[$len] == ' '");
129 2         2 $first = 0;
130             } else {
131 3         8 $builder->elsif("recv_buf[$len] == ' '");
132             }
133 5         25 $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 2         75 ->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 2         10 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 14     14 1 38 my ($class, $builder) = @_;
155            
156 14         614 $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 14         27 return $builder;
171             }
172              
173             # Generate C code for finding request body (after \r\n\r\n)
174             sub gen_body_parser {
175 3     3 1 16 my ($class, $builder, %opts) = @_;
176            
177 3 100       8 if ($opts{has_body_access}) {
178 1         11 $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 2         13 $builder->comment('OPTIMIZED: No body parsing needed')
188             ->line('const char* body = "";')
189             ->line('int body_len = 0;');
190             }
191            
192 3         7 return $builder;
193             }
194              
195             # Generate C code for keep-alive detection
196             sub gen_keepalive_check {
197 14     14 1 77 my ($class, $builder) = @_;
198            
199 14         224 $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 14         22 return $builder;
212             }
213              
214             # Status code to text mapping (complete list)
215             sub _status_text {
216 23     23   48 my ($code) = @_;
217 23         720 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 23   50     147 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 7578 my ($class, $builder) = @_;
262            
263 2         65 $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 5093 my ($class, $builder) = @_;
297            
298 2         42 $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         4 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 4507 my ($class, $builder) = @_;
324            
325 2         17 $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 11841 my ($class, $data) = @_;
337            
338 9 100 100     47 return '' unless defined $data && length($data);
339            
340 7         10 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 7216 return "0\r\n\r\n";
347             }
348              
349             1;
350              
351             __END__