More performance improvements. Added undocumented "headers_only" and "sync" options...
[catagits/HTTP-Request-AsCGI.git] / lib / HTTP / Request / AsCGI.pm
1 package HTTP::Request::AsCGI;
2
3 use strict;
4 use warnings;
5 use bytes;
6 use base 'Class::Accessor::Fast';
7
8 use Carp            qw[croak];
9 use HTTP::Response  qw[];
10 use IO::Handle      qw[];
11 use IO::File        qw[SEEK_SET];
12 use Symbol          qw[];
13
14 __PACKAGE__->mk_accessors(qw[environment request is_restored is_setuped is_prepared should_dup should_restore should_rewind stdin stdout stderr]);
15
16 our $VERSION = 0.6_01;
17
18 sub new {
19     my $class  = ref $_[0] ? ref shift : shift;
20     my $params = {};
21
22     if ( @_ % 2 == 0 ) {
23         $params = { @_ };
24     }
25     else {
26         $params = { request => shift, environment => { @_ } };
27     }
28
29     return bless( {}, $class )->initialize($params);
30 }
31
32 sub initialize {
33     my ( $self, $params ) = @_;
34
35     if ( exists $params->{request} ) {
36         $self->request( $params->{request} );
37     }
38     else {
39         croak("Mandatory parameter 'request' is missing.");
40     }
41
42     if ( exists $params->{environment} ) {
43         $self->environment( $params->{environment} );
44     }
45     else {
46         $self->environment( {} );
47     }
48
49     if ( exists $params->{stdin} ) {
50         $self->stdin( $params->{stdin} );
51     }
52     else {
53         $self->stdin( IO::File->new_tmpfile );
54     }
55
56     if ( exists $params->{stdout} ) {
57         $self->stdout( $params->{stdout} );
58     }
59     else {
60         $self->stdout( IO::File->new_tmpfile );
61     }
62
63     if ( exists $params->{stderr} ) {
64         $self->stderr( $params->{stderr} );
65     }
66
67     if ( exists $params->{dup} ) {
68         $self->should_dup( $params->{dup} ? 1 : 0 );
69     }
70     else {
71         $self->should_dup(1);
72     }
73
74     if ( exists $params->{restore} ) {
75         $self->should_restore( $params->{restore} ? 1 : 0 );
76     }
77     else {
78         $self->should_restore(1);
79     }
80
81     if ( exists $params->{rewind} ) {
82         $self->should_rewind( $params->{rewind} ? 1 : 0 );
83     }
84     else {
85         $self->should_rewind(1);
86     }
87
88     $self->prepare;
89
90     return $self;
91 }
92
93 *enviroment = \&environment;
94
95 sub has_stdin  { return defined $_[0]->stdin  }
96 sub has_stdout { return defined $_[0]->stdout }
97 sub has_stderr { return defined $_[0]->stderr }
98
99 sub prepare {
100     my $self = shift;
101
102     my $environment = $self->environment;
103     my $request     = $self->request;
104
105     my $host = $request->header('Host');
106     my $uri  = $request->uri->clone;
107
108     $uri->scheme('http')    unless $uri->scheme;
109     $uri->host('localhost') unless $uri->host;
110     $uri->port(80)          unless $uri->port;
111     $uri->host_port($host)  unless !$host || ( $host eq $uri->host_port );
112
113     $uri = $uri->canonical;
114
115     my %cgi = (
116         GATEWAY_INTERFACE => 'CGI/1.1',
117         HTTP_HOST         => $uri->host_port,
118         HTTPS             => ( $uri->scheme eq 'https' ) ? 'ON' : 'OFF',  # not in RFC 3875
119         PATH_INFO         => $uri->path,
120         QUERY_STRING      => $uri->query || '',
121         SCRIPT_NAME       => '/',
122         SERVER_NAME       => $uri->host,
123         SERVER_PORT       => $uri->port,
124         SERVER_PROTOCOL   => $request->protocol || 'HTTP/1.1',
125         SERVER_SOFTWARE   => "HTTP-Request-AsCGI/$VERSION",
126         REMOTE_ADDR       => '127.0.0.1',
127         REMOTE_HOST       => 'localhost',
128         REMOTE_PORT       => int( rand(64000) + 1000 ),                   # not in RFC 3875
129         REQUEST_URI       => $uri->path_query,                            # not in RFC 3875
130         REQUEST_METHOD    => $request->method
131     );
132
133     foreach my $key ( keys %cgi ) {
134
135         unless ( exists $environment->{ $key } ) {
136             $environment->{ $key } = $cgi{ $key };
137         }
138     }
139
140     foreach my $field ( $self->request->headers->header_field_names ) {
141
142         my $key = uc("HTTP_$field");
143         $key =~ tr/-/_/;
144         $key =~ s/^HTTP_// if $field =~ /^Content-(Length|Type)$/;
145
146         unless ( exists $environment->{ $key } ) {
147             $environment->{ $key } = $self->request->headers->header($field);
148         }
149     }
150
151     unless ( $environment->{SCRIPT_NAME} eq '/' && $environment->{PATH_INFO} ) {
152         $environment->{PATH_INFO} =~ s/^\Q$environment->{SCRIPT_NAME}\E/\//;
153         $environment->{PATH_INFO} =~ s/^\/+/\//;
154     }
155
156     $self->is_prepared(1);
157 }
158
159 sub setup {
160     my $self = shift;
161
162     $self->setup_stdin;
163     $self->setup_stdout;
164     $self->setup_stderr;
165     $self->setup_environment;
166
167     if ( $INC{'CGI.pm'} ) {
168         CGI::initialize_globals();
169     }
170
171     $self->is_setuped(1);
172
173     return $self;
174 }
175
176 sub setup_environment {
177     my $self = shift;
178
179     no warnings 'uninitialized';
180
181     if ( $self->should_restore ) {
182         $self->{restore}->{environment} = { %ENV };
183     }
184
185     %ENV = %{ $self->environment };
186 }
187
188 sub setup_stdin {
189     my $self = shift;
190
191     if ( $self->has_stdin ) {
192
193         binmode( $self->stdin );
194
195         if ( $self->request->content_length ) {
196
197             syswrite( $self->stdin, $self->request->content )
198               or croak("Couldn't write request content to stdin handle: '$!'");
199
200             sysseek( $self->stdin, 0, SEEK_SET )
201               or croak("Couldn't seek stdin handle: '$!'");
202         }
203
204         if ( $self->should_dup ) {
205
206             if ( $self->should_restore ) {
207
208                 open( my $stdin, '<&STDIN' )
209                   or croak("Couldn't dup STDIN: '$!'");
210
211                 $self->{restore}->{stdin} = $stdin;
212             }
213
214             STDIN->fdopen( $self->stdin, '<' )
215               or croak("Couldn't redirect STDIN: '$!'");
216         }
217         else {
218
219             my $stdin = Symbol::qualify_to_ref('STDIN');
220
221             if ( $self->should_restore ) {
222
223                 $self->{restore}->{stdin}     = *$stdin;
224                 $self->{restore}->{stdin_ref} = \*$stdin;
225             }
226
227             *{ $stdin } = $self->stdin;
228         }
229
230         binmode( STDIN );
231     }
232 }
233
234 sub setup_stdout {
235     my $self = shift;
236
237     if ( $self->has_stdout ) {
238
239         if ( $self->should_dup ) {
240
241             if ( $self->should_restore ) {
242
243                 open( my $stdout, '>&STDOUT' )
244                   or croak("Couldn't dup STDOUT: '$!'");
245
246                 $self->{restore}->{stdout} = $stdout;
247             }
248
249             STDOUT->fdopen( $self->stdout, '>' )
250               or croak("Couldn't redirect STDOUT: '$!'");
251         }
252         else {
253
254             my $stdout = Symbol::qualify_to_ref('STDOUT');
255
256             if ( $self->should_restore ) {
257
258                 $self->{restore}->{stdout}     = *$stdout;
259                 $self->{restore}->{stdout_ref} = \*$stdout;
260             }
261
262             *{ $stdout } = $self->stdout;
263         }
264
265         binmode( $self->stdout );
266         binmode( STDOUT);
267     }
268 }
269
270 sub setup_stderr {
271     my $self = shift;
272
273     if ( $self->has_stderr ) {
274
275         if ( $self->should_dup ) {
276
277             if ( $self->should_restore ) {
278
279                 open( my $stderr, '>&STDERR' )
280                   or croak("Couldn't dup STDERR: '$!'");
281
282                 $self->{restore}->{stderr} = $stderr;
283             }
284
285             STDERR->fdopen( $self->stderr, '>' )
286               or croak("Couldn't redirect STDERR: '$!'");
287         }
288         else {
289
290             my $stderr = Symbol::qualify_to_ref('STDERR');
291
292             if ( $self->should_restore ) {
293
294                 $self->{restore}->{stderr}     = *$stderr;
295                 $self->{restore}->{stderr_ref} = \*$stderr;
296             }
297
298             *{ $stderr } = $self->stderr;
299         }
300
301         binmode( $self->stderr );
302         binmode( STDERR );
303     }
304 }
305
306 sub response {
307     my $self   = shift;
308     my %params = ( headers_only => 0, sync => 0, @_ );
309
310     return undef unless $self->stdout;
311
312     seek( $self->stdout, 0, SEEK_SET )
313       or croak("Couldn't seek stdout handle: '$!'");
314
315     my $headers;
316     while ( my $line = $self->stdout->getline ) {
317         $headers .= $line;
318         last if $headers =~ /\x0d?\x0a\x0d?\x0a$/;
319     }
320
321     unless ( defined $headers ) {
322         $headers = "HTTP/1.1 500 Internal Server Error\x0d\x0a";
323     }
324
325     unless ( $headers =~ /^HTTP/ ) {
326         $headers = "HTTP/1.1 200 OK\x0d\x0a" . $headers;
327     }
328
329     my $response = HTTP::Response->parse($headers);
330     $response->date( time() ) unless $response->date;
331
332     my $message = $response->message;
333     my $status  = $response->header('Status');
334
335     if ( $message && $message =~ /^(.+)\x0d$/ ) {
336         $response->message($1);
337     }
338
339     if ( $status && $status =~ /^(\d\d\d)\s?(.+)?$/ ) {
340
341         my $code    = $1;
342         my $message = $2 || HTTP::Status::status_message($code);
343
344         $response->code($code);
345         $response->message($message);
346     }
347
348     my $length = ( stat( $self->stdout ) )[7] - tell( $self->stdout );
349
350     if ( $response->code == 500 && !$length ) {
351
352         $response->content( $response->error_as_HTML );
353         $response->content_type('text/html');
354
355         return $response;
356     }
357
358     if ( $params{headers_only} ) {
359
360         if ( $params{sync} ) {
361
362             my $position = tell( $self->stdout )
363               or croak("Couldn't get file position from stdout handle: '$!'");
364
365             sysseek( $self->stdout, $position, SEEK_SET )
366               or croak("Couldn't seek stdout handle: '$!'");
367         }
368
369         return $response;
370     }
371
372     my $content        = undef;
373     my $content_length = 0;
374
375     while () {
376
377         my $r = $self->stdout->read( $content, 4096, $content_length );
378
379         if ( defined $r ) {
380
381             $content_length += $r;
382
383             last unless $r;
384         }
385         else {
386             croak("Couldn't read from stdin handle: '$!'");
387         }
388     }
389
390     if ( $content_length ) {
391
392         $response->content_ref(\$content);
393
394         if ( !$response->content_length ) {
395             $response->content_length($content_length);
396         }
397     }
398
399     return $response;
400 }
401
402 sub restore {
403     my $self = shift;
404
405     if ( $self->should_restore ) {
406
407         $self->restore_environment;
408         $self->restore_stdin;
409         $self->restore_stdout;
410         $self->restore_stderr;
411
412         $self->{restore} = {};
413
414         $self->is_restored(1);
415     }
416
417     return $self;
418 }
419
420 sub restore_environment {
421     my $self = shift;
422
423     no warnings 'uninitialized';
424
425     %ENV = %{ $self->{restore}->{environment} };
426 }
427
428 sub restore_stdin {
429     my $self = shift;
430
431     if ( $self->has_stdin ) {
432
433         my $stdin = $self->{restore}->{stdin};
434
435         if ( $self->should_dup ) {
436
437             STDIN->fdopen( $stdin, '<' )
438               or croak("Couldn't restore STDIN: '$!'");
439         }
440         else {
441
442             my $stdin_ref = $self->{restore}->{stdin_ref};
443
444             *{ $stdin_ref } = $stdin;
445         }
446
447         if ( $self->should_rewind ) {
448
449             seek( $self->stdin, 0, SEEK_SET )
450               or croak("Couldn't seek stdin handle: '$!'");
451         }
452     }
453 }
454
455 sub restore_stdout {
456     my $self = shift;
457
458     if ( $self->has_stdout ) {
459
460         my $stdout = $self->{restore}->{stdout};
461
462         if ( $self->should_dup ) {
463
464             STDOUT->flush
465               or croak("Couldn't flush STDOUT: '$!'");
466
467             STDOUT->fdopen( $stdout, '>' )
468               or croak("Couldn't restore STDOUT: '$!'");
469         }
470         else {
471
472             my $stdout_ref = $self->{restore}->{stdout_ref};
473
474             *{ $stdout_ref } = $stdout;
475         }
476
477         if ( $self->should_rewind ) {
478
479             seek( $self->stdout, 0, SEEK_SET )
480               or croak("Couldn't seek stdout handle: '$!'");
481         }
482     }
483 }
484
485 sub restore_stderr {
486     my $self = shift;
487
488     if ( $self->has_stderr ) {
489
490         my $stderr = $self->{restore}->{stderr};
491
492         if ( $self->should_dup ) {
493
494             STDERR->flush
495               or croak("Couldn't flush STDERR: '$!'");
496
497             STDERR->fdopen( $stderr, '>' )
498               or croak("Couldn't restore STDERR: '$!'");
499         }
500         else {
501
502             my $stderr_ref = $self->{restore}->{stderr_ref};
503
504             *{ $stderr_ref } = $stderr;
505         }
506
507         if ( $self->should_rewind ) {
508
509             seek( $self->stderr, 0, SEEK_SET )
510               or croak("Couldn't seek stderr handle: '$!'");
511         }
512     }
513 }
514
515 sub DESTROY {
516     my $self = shift;
517
518     if ( $self->should_restore ) {
519
520         if ( $self->is_setuped && !$self->is_restored ) {
521             $self->restore;
522         }
523     }
524 }
525
526 1;
527
528 __END__
529
530 =head1 NAME
531
532 HTTP::Request::AsCGI - Setup a CGI environment from a HTTP::Request
533
534 =head1 SYNOPSIS
535
536     use CGI;
537     use HTTP::Request;
538     use HTTP::Request::AsCGI;
539
540     my $request = HTTP::Request->new( GET => 'http://www.host.com/' );
541     my $stdout;
542
543     {
544         my $c = HTTP::Request::AsCGI->new($request)->setup;
545         my $q = CGI->new;
546
547         print $q->header,
548               $q->start_html('Hello World'),
549               $q->h1('Hello World'),
550               $q->end_html;
551
552         $stdout = $c->stdout;
553
554         # environment and descriptors will automatically be restored
555         # when $c is destructed.
556     }
557
558     while ( my $line = $stdout->getline ) {
559         print $line;
560     }
561
562 =head1 DESCRIPTION
563
564 Provides a convinient way of setting up an CGI environment from a HTTP::Request.
565
566 =head1 METHODS
567
568 =over 4
569
570 =item new ( $request [, key => value ] )
571
572 Contructor, first argument must be a instance of HTTP::Request
573 followed by optional pairs of environment key and value.
574
575 =item environment
576
577 Returns a hashref containing the environment that will be used in setup.
578 Changing the hashref after setup has been called will have no effect.
579
580 =item setup
581
582 Setups the environment and descriptors.
583
584 =item restore
585
586 Restores the environment and descriptors. Can only be called after setup.
587
588 =item request
589
590 Returns the request given to constructor.
591
592 =item response
593
594 Returns a HTTP::Response. Can only be called after restore.
595
596 =item stdin
597
598 Accessor for handle that will be used for STDIN, must be a real seekable
599 handle with an file descriptor. Defaults to a tempoary IO::File instance.
600
601 =item stdout
602
603 Accessor for handle that will be used for STDOUT, must be a real seekable
604 handle with an file descriptor. Defaults to a tempoary IO::File instance.
605
606 =item stderr
607
608 Accessor for handle that will be used for STDERR, must be a real seekable
609 handle with an file descriptor.
610
611 =back
612
613 =head1 SEE ALSO
614
615 =over 4
616
617 =item examples directory in this distribution.
618
619 =item L<WWW::Mechanize::CGI>
620
621 =item L<Test::WWW::Mechanize::CGI>
622
623 =back
624
625 =head1 THANKS TO
626
627 Thomas L. Shinnick for his valuable win32 testing.
628
629 =head1 AUTHOR
630
631 Christian Hansen, C<ch@ngmedia.com>
632
633 =head1 LICENSE
634
635 This library is free software. You can redistribute it and/or modify
636 it under the same terms as perl itself.
637
638 =cut