Improved tests
[catagits/HTTP-Request-AsCGI.git] / lib / HTTP / Request / AsCGI.pm
CommitLineData
b2e1304d 1package HTTP::Request::AsCGI;
2
3use strict;
4use warnings;
090cc060 5use bytes;
b2e1304d 6use base 'Class::Accessor::Fast';
7
8use Carp;
30efa07d 9use IO::Handle;
bd7813ac 10use IO::File;
b2e1304d 11
090cc060 12__PACKAGE__->mk_accessors(qw[ enviroment request stdin stdout stderr ]);
b2e1304d 13
ca38286c 14our $VERSION = 0.2;
b2e1304d 15
16sub new {
17 my $class = shift;
18 my $request = shift;
2d51e42f 19
20 unless ( @_ % 2 == 0 && eval { $request->isa('HTTP::Request') } ) {
21 croak(qq/usage: $class->new( \$request [, key => value] )/);
22 }
ca38286c 23
24 my $self = $class->SUPER::new( { restored => 0, setuped => 0 } );
25 $self->request($request);
26 $self->stdin( IO::File->new_tmpfile );
27 $self->stdout( IO::File->new_tmpfile );
b2e1304d 28
30efa07d 29 my $host = $request->header('Host');
30 my $uri = $request->uri->clone;
31 $uri->scheme('http') unless $uri->scheme;
32 $uri->host('localhost') unless $uri->host;
33 $uri->port(80) unless $uri->port;
34 $uri->host_port($host) unless !$host || ( $host eq $uri->host_port );
ca38286c 35
36 $uri = $uri->canonical;
30efa07d 37
ca38286c 38 my $enviroment = {
b2e1304d 39 GATEWAY_INTERFACE => 'CGI/1.1',
30efa07d 40 HTTP_HOST => $uri->host_port,
41 HTTPS => ( $uri->scheme eq 'https' ) ? 'ON' : 'OFF', # not in RFC 3875
42 PATH_INFO => $uri->path,
43 QUERY_STRING => $uri->query || '',
c1e07bf1 44 SCRIPT_NAME => '/',
30efa07d 45 SERVER_NAME => $uri->host,
46 SERVER_PORT => $uri->port,
b2e1304d 47 SERVER_PROTOCOL => $request->protocol || 'HTTP/1.1',
30efa07d 48 SERVER_SOFTWARE => "HTTP-Request-AsCGI/$VERSION",
b2e1304d 49 REMOTE_ADDR => '127.0.0.1',
50 REMOTE_HOST => 'localhost',
30efa07d 51 REMOTE_PORT => int( rand(64000) + 1000 ), # not in RFC 3875
ca38286c 52 REQUEST_URI => $uri->path_query, # not in RFC 3875
b2e1304d 53 REQUEST_METHOD => $request->method,
54 @_
55 };
56
57 foreach my $field ( $request->headers->header_field_names ) {
58
ca38286c 59 my $key = uc("HTTP_$field");
2aaf55bc 60 $key =~ tr/-/_/;
ca38286c 61 $key =~ s/^HTTP_// if $field =~ /^Content-(Length|Type)$/;
b2e1304d 62
ca38286c 63 unless ( exists $enviroment->{$key} ) {
64 $enviroment->{$key} = $request->headers->header($field);
b2e1304d 65 }
66 }
67
ca38286c 68 unless ( $enviroment->{SCRIPT_NAME} eq '/' && $enviroment->{PATH_INFO} ) {
69 $enviroment->{PATH_INFO} =~ s/^\Q$enviroment->{SCRIPT_NAME}\E/\//;
70 $enviroment->{PATH_INFO} =~ s/^\/+/\//;
71 }
72
73 $self->enviroment($enviroment);
74
75 return $self;
b2e1304d 76}
77
78sub setup {
79 my $self = shift;
80
090cc060 81 $self->{restore}->{enviroment} = {%ENV};
b2e1304d 82
17b370b0 83 binmode( $self->stdin );
b2e1304d 84
85 if ( $self->request->content_length ) {
86
30efa07d 87 syswrite( $self->stdin, $self->request->content )
780060e5 88 or croak("Can't write request content to stdin handle: $!");
b2e1304d 89
30efa07d 90 sysseek( $self->stdin, 0, SEEK_SET )
780060e5 91 or croak("Can't seek stdin handle: $!");
b2e1304d 92 }
93
ca38286c 94 open( $self->{restore}->{stdin}, '>&', STDIN->fileno )
95 or croak("Can't dup stdin: $!");
96
97 open( STDIN, '<&=', $self->stdin->fileno )
98 or croak("Can't open stdin: $!");
99
100 binmode( STDIN );
101
090cc060 102 if ( $self->stdout ) {
30efa07d 103
090cc060 104 open( $self->{restore}->{stdout}, '>&', STDOUT->fileno )
105 or croak("Can't dup stdout: $!");
106
107 open( STDOUT, '>&=', $self->stdout->fileno )
108 or croak("Can't open stdout: $!");
441eeb04 109
17b370b0 110 binmode( $self->stdout );
111 binmode( STDOUT);
090cc060 112 }
113
114 if ( $self->stderr ) {
30efa07d 115
090cc060 116 open( $self->{restore}->{stderr}, '>&', STDERR->fileno )
117 or croak("Can't dup stderr: $!");
118
119 open( STDERR, '>&=', $self->stderr->fileno )
120 or croak("Can't open stderr: $!");
441eeb04 121
17b370b0 122 binmode( $self->stderr );
123 binmode( STDERR );
090cc060 124 }
125
3cdea3c7 126 {
127 no warnings 'uninitialized';
128 %ENV = %{ $self->enviroment };
129 }
30efa07d 130
131 if ( $INC{'CGI.pm'} ) {
132 CGI::initialize_globals();
133 }
b2e1304d 134
6f5fb9a7 135 $self->{setuped}++;
b2e1304d 136
137 return $self;
138}
139
780060e5 140sub response {
141 my ( $self, $callback ) = @_;
142
143 return undef unless $self->{setuped};
144 return undef unless $self->{restored};
090cc060 145 return undef unless $self->{restore}->{stdout};
780060e5 146
147 require HTTP::Response;
148
30efa07d 149 seek( $self->stdout, 0, SEEK_SET )
150 or croak("Can't seek stdout handle: $!");
780060e5 151
decf17dc 152 my $message;
153 while ( my $line = $self->stdout->getline ) {
780060e5 154 $message .= $line;
decf17dc 155 last if $message =~ /\x0d?\x0a\x0d?\x0a$/;
780060e5 156 }
157
158 unless ( $message =~ /^HTTP/ ) {
decf17dc 159 $message = "HTTP/1.1 200 OK\x0d\x0a" . $message;
160 }
161
162 my $response = HTTP::Response->new;
163 my @headers = split( /\x0d?\x0a/, $message );
164 my $status = shift(@headers);
165
166 unless ( $status =~ s/^(HTTP\/\d\.\d) (\d{3}) (.*)$// ) {
167 croak( "Invalid Status-Line: '$status'" );
780060e5 168 }
169
decf17dc 170 $response->protocol($1);
171 $response->code($2);
172 $response->message($3);
173
174 my $token = qr/[^][\x00-\x1f\x7f()<>@,;:\\"\/?={} \t]+/;
175
176 foreach my $header (@headers) {
177
178 unless( $header =~ s/^($token):[\t ]*// ) {
179 croak( "Invalid header field name : '$header'" );
180 }
181
182 $response->push_header( $1 => $header );
183 }
780060e5 184
185 if ( my $code = $response->header('Status') ) {
186 $response->code($code);
decf17dc 187 $response->message( HTTP::Status::status_message($code) );
780060e5 188 }
189
780060e5 190 $response->headers->date( time() );
191
090cc060 192 if ($callback) {
780060e5 193 $response->content( sub {
194 if ( $self->stdout->read( my $buffer, 4096 ) ) {
195 return $buffer;
196 }
197 return undef;
090cc060 198 });
780060e5 199 }
200 else {
201 my $length = 0;
202 while ( $self->stdout->read( my $buffer, 4096 ) ) {
203 $length += length($buffer);
204 $response->add_content($buffer);
205 }
decf17dc 206
207 if ( $length && !$response->content_length ) {
208 $response->content_length($length);
209 }
780060e5 210 }
211
780060e5 212 return $response;
213}
214
b2e1304d 215sub restore {
216 my $self = shift;
217
218 %ENV = %{ $self->{restore}->{enviroment} };
219
220 open( STDIN, '>&', $self->{restore}->{stdin} )
221 or croak("Can't restore stdin: $!");
222
30efa07d 223 sysseek( $self->stdin, 0, SEEK_SET )
780060e5 224 or croak("Can't seek stdin: $!");
12852959 225
090cc060 226 if ( $self->{restore}->{stdout} ) {
30efa07d 227
228 STDOUT->flush
229 or croak("Can't flush stdout: $!");
230
090cc060 231 open( STDOUT, '>&', $self->{restore}->{stdout} )
232 or croak("Can't restore stdout: $!");
233
30efa07d 234 sysseek( $self->stdout, 0, SEEK_SET )
6f5fb9a7 235 or croak("Can't seek stdout: $!");
236 }
12852959 237
090cc060 238 if ( $self->{restore}->{stderr} ) {
30efa07d 239
240 STDERR->flush
241 or croak("Can't flush stderr: $!");
242
090cc060 243 open( STDERR, '>&', $self->{restore}->{stderr} )
244 or croak("Can't restore stderr: $!");
245
30efa07d 246 sysseek( $self->stderr, 0, SEEK_SET )
6f5fb9a7 247 or croak("Can't seek stderr: $!");
248 }
12852959 249
b2e1304d 250 $self->{restored}++;
090cc060 251
252 return $self;
b2e1304d 253}
254
255sub DESTROY {
256 my $self = shift;
6f5fb9a7 257 $self->restore if $self->{setuped} && !$self->{restored};
b2e1304d 258}
259
2601;
261
262__END__
263
264=head1 NAME
265
bd7813ac 266HTTP::Request::AsCGI - Setup a CGI enviroment from a HTTP::Request
b2e1304d 267
268=head1 SYNOPSIS
269
bd7813ac 270 use CGI;
271 use HTTP::Request;
272 use HTTP::Request::AsCGI;
273
274 my $request = HTTP::Request->new( GET => 'http://www.host.com/' );
275 my $stdout;
276
277 {
278 my $c = HTTP::Request::AsCGI->new($request)->setup;
279 my $q = CGI->new;
280
281 print $q->header,
282 $q->start_html('Hello World'),
283 $q->h1('Hello World'),
284 $q->end_html;
285
286 $stdout = $c->stdout;
287
2d51e42f 288 # enviroment and descriptors will automatically be restored
289 # when $c is destructed.
bd7813ac 290 }
291
bd7813ac 292 while ( my $line = $stdout->getline ) {
293 print $line;
294 }
295
b2e1304d 296=head1 DESCRIPTION
297
2d51e42f 298Provides a convinient way of setting up an CGI enviroment from a HTTP::Request.
299
b2e1304d 300=head1 METHODS
301
302=over 4
303
2d51e42f 304=item new ( $request [, key => value ] )
305
306Contructor, first argument must be a instance of HTTP::Request
ca38286c 307followed by optional pairs of environment key and value.
b2e1304d 308
bd7813ac 309=item enviroment
310
2d51e42f 311Returns a hashref containing the environment that will be used in setup.
312Changing the hashref after setup has been called will have no effect.
313
b2e1304d 314=item setup
315
2d51e42f 316Setups the environment and descriptors.
317
b2e1304d 318=item restore
319
2d51e42f 320Restores the enviroment and descriptors. Can only be called after setup.
321
b2e1304d 322=item request
323
2d51e42f 324Returns the request given to constructor.
325
780060e5 326=item response
327
2d51e42f 328Returns a HTTP::Response. Can only be called after restore.
329
b2e1304d 330=item stdin
331
2d51e42f 332Accessor for handle that will be used for STDIN, must be a real seekable
333handle with an file descriptor. Defaults to a tempoary IO::File instance.
334
b2e1304d 335=item stdout
336
2d51e42f 337Accessor for handle that will be used for STDOUT, must be a real seekable
338handle with an file descriptor. Defaults to a tempoary IO::File instance.
339
b2e1304d 340=item stderr
341
2d51e42f 342Accessor for handle that will be used for STDERR, must be a real seekable
343handle with an file descriptor.
b2e1304d 344
2d51e42f 345=back
b2e1304d 346
2d51e42f 347=head1 THANKS TO
17b370b0 348
349Thomas L. Shinnick for his valuable win32 testing.
350
b2e1304d 351=head1 AUTHOR
352
353Christian Hansen, C<ch@ngmedia.com>
354
355=head1 LICENSE
356
357This library is free software. You can redistribute it and/or modify
358it under the same terms as perl itself.
359
360=cut