root/trunk/lib/Perlbal/Plugin/Cgilike.pm

Revision 774, 9.6 kB (checked in by ask, 17 months ago)

Lots of typo corrections in documentation and comments from Nick Andrew

Line 
1#!/usr/bin/perl
2#
3# Copyright 2007 Martin Atkins <mart@degeneration.co.uk> and Six Apart Ltd.
4#
5
6=head1 NAME
7
8Perlbal::Plugin::Cgilike - Handle Perlbal requests with a Perl subroutine
9
10=head1 DESCRIPTION
11
12This module allows responses to be handled with a simple API that's similar in principle to
13CGI, mod_perl response handlers, etc.
14
15It does not, however, come anywhere close to conforming to the CGI "standard". It's actually
16more like mod_perl in usage, though there are several differences.
17Most notably, Perlbal is single-process and single-threaded, and handlers run inside the Perlbal
18process and must therefore return quickly and not do any blocking operations.
19
20As it currently stands, this is very bare-bones and has only really been used with basic GET
21requests. It lacks a nice API for handling the body of a POST or PUT request.
22
23It is not recommended to use this for extensive applications. Perlbal is first and foremost
24a load balancer, so if you're doing something at all complicated you're probably better off
25using something like Apache mod_perl and then putting Perlbal in front if it if necessary.
26However, this plugin may prove useful for simple handlers or perhaps embedding a simple
27HTTP service into another application that uses C<Danga::Socket>.
28
29=head1 SYNOPSIS
30
31This module provides a Perlbal plugin which can be loaded and used as follows.
32
33        LOAD cgilike
34        PERLREQUIRE = MyPackage
35       
36        CREATE SERVICE cgilike
37                SET role   = web_server
38                SET listen = 127.0.0.1:80
39                SET plugins = cgilike
40                PERLHANDLER = MyPackage::handler
41        ENABLE cgilike
42
43With this plugin loaded into a particular service, the plugin will then be called for
44all requests for that service.
45
46Set cgilike.handler to the name of a subroutine that will handle requests. This subroutine
47will receive an object which allows interaction with the Perlbal service.
48
49        package MyPackage
50        sub handler {
51            my ($r) = @_;
52                if ($r->uri eq '/') {
53                        print "<p>Hello, world</p>";
54                        return Perlbal::Plugin::Cgilike::HANDLED;
55                }
56                else {
57                        return 404;
58                }
59        }
60
61Return C<Perlbal::Plugin::Cgilike::HANDLED> to indicate that the request has been handled, or return some HTTP error code
62to produce a predefined error message.
63You may also return C<Perlbal::Plugin::Cgilike::DECLINED> if you do not wish to handle the request, in which case Perlbal
64will be allowed to handle the request in whatever way it would have done without Cgilike loaded.
65
66If your handler returns any non-success value, it B<MUST NOT> produce any output. If you
67produce output before returning such a value, the response to the client is likely to be
68utter nonsense.
69
70You may also return C<Perlbal::Plugin::Cgilike::POSTPONE_RESPONSE>, which is equivalent to
71returning zero except that the HTTP connection will be left open once you return. It is
72your responsibility to later call C<$r-E<gt>end_response()> when you have completed
73the response. This style is necessary when you need to perform some long operation
74before you can return a response; you'll need to use some appropriate method to set
75a callback to run when the operation completes and then do your response in the
76callback. Once you've called C<end_response>, you must not call any further methods on C<$r>;
77it's probably safest to just return immediately afterwards to avoid any mishaps.
78
79=head1 API DOCUMENTATION
80
81TODO: Write this
82
83=head1 TODO
84
85Currently there is no API for dealing with the body of a POST or PUT request. Ideally it'd be able
86to do automatic decoding of application/x-www-form-urlencoded data, too.
87
88The POSTPONE_RESPONSE functionality has not been tested extensively and is probably buggy.
89
90=head1 COPYRIGHT AND LICENSE
91
92Copyright 2007 Martin Atkins <mart@degeneration.co.uk> and Six Apart Ltd.
93
94This module is part of the Perlbal distribution, and as such can be distributed under the same licence terms as the rest of Perlbal.
95
96=cut
97
98package Perlbal::Plugin::Cgilike;
99
100use Perlbal;
101use strict;
102use Symbol;
103
104use constant DECLINED => -2;
105use constant HANDLED => 0;
106use constant POSTPONE_RESPONSE => -1;
107
108sub register {
109    my ($class, $svc) = @_;
110
111    $svc->register_hook('Cgilike', 'start_http_request', sub { Perlbal::Plugin::Cgilike::handle_request($svc, $_[0]); });
112
113}
114
115sub handle_request {
116    my Perlbal::Service $svc = shift;
117    my Perlbal::ClientProxy $pb = shift;
118    return 0 unless $pb->{req_headers};
119
120    # Create a new request object, and tie a filehandle to it
121    my $output_handle = Symbol::gensym();
122    my $req = tie(*{$output_handle}, 'Perlbal::Plugin::Cgilike::Request', $pb);
123
124    my $handler = $svc->{extra_config}->{_perlhandler};
125    if (! defined($handler)) {
126        return $pb->send_response(500, "No perlhandler is configured for this service");
127    }
128
129    # Our $output_handle is tied to the request object, which provides PRINT and PRINTF methods
130    # Set it as the default so that handlers can just use print and printf as normal.
131    my $oldfh = select($output_handle);
132
133    my $ret;
134    my $result = eval {
135        no strict;
136        $ret = &{$handler}($req);
137        1;
138    };
139
140    # Restore the old filehandle to avoid breaking anyone else
141    select($oldfh);
142
143    if ($result) {
144        if ($ret == 0 || $ret == POSTPONE_RESPONSE) {
145            if ($ret == 0) {
146                $req->end_response();
147                untie($req);
148            }
149            return 1;
150        }
151        elsif ($ret == DECLINED) {
152            return 0;
153        }
154        else {
155            return $pb->send_response($ret+0, $ret+0);
156        }
157    }
158    else {
159        return $pb->send_response(500, "Error in handler: ".$@);
160    }
161
162    return $pb->send_response(500, "I seem to have fallen into a place I shouldn't be.");
163
164}
165
166sub handle_perlrequire_command {
167    # This is defined with an equals because Perlbal lowercases all manage commands except
168    # after an equals, which means that having an equals here is the only way to actually
169    # get the correct case of the module name. Lame++.
170    my $mc = shift->parse(qr/^perlrequire\s*=\s*([\w:]+)$/, "usage: PERLREQUIRE=<module>");
171    my ($module) = $mc->args;
172
173    my $success = eval "require $module; 1;";
174
175    unless ($success) {
176        return $mc->err("Failed to load $module: $@")
177    }
178
179    return 1;
180}
181
182sub handle_perlhandler_command {
183    my $mc = shift->parse(qr/^perlhandler\s*=\s*([\w:]+)$/, "usage: PERLHANDLER=<package::subroutine>");
184    my ($subname) = $mc->args;
185
186    my $svcname;
187    unless ($svcname ||= $mc->{ctx}{last_created}) {
188        return $mc->err("No service name in context from CREATE SERVICE <name> or USE <service_name>");
189    }
190
191    my $svc = Perlbal->service($svcname);
192    return $mc->err("Non-existent service '$svcname'") unless $svc;
193
194    my $cfg = $svc->{extra_config}->{_perlhandler} = $subname;
195
196    return 1;
197}
198
199# called when we're no longer active on a service
200sub unregister {
201    my ($class, $svc) = @_;
202
203    $svc->unregister_hooks('Cgilike');
204    return 1;
205}
206
207# called when we are loaded
208sub load {
209    Perlbal::register_global_hook('manage_command.perlrequire', \&Perlbal::Plugin::Cgilike::handle_perlrequire_command);
210    Perlbal::register_global_hook('manage_command.perlhandler', \&Perlbal::Plugin::Cgilike::handle_perlhandler_command);
211
212    return 1;
213}
214
215# called for a global unload
216sub unload {
217    return 1;
218}
219
220package Perlbal::Plugin::Cgilike::Request;
221
222use URI;
223
224sub new {
225    my ($class, $pb) = @_;
226
227    return bless {
228        pb => $pb,
229        header_sent => 0,
230    }, $class;
231}
232
233# This class can also provide a tied handle which supports PRINT and PRINTF (but not much else)
234sub TIEHANDLE {
235    my ($class, $req_headers) = @_;
236    return $class->new($req_headers);
237}
238
239sub request_header {
240    return $_[0]->{pb}->{req_headers};
241}
242
243sub response_header {
244    my ($self, $k, $v) = @_;
245
246    if (defined($k)) {
247        my $hdrs = $self->response_header;
248        if (defined($v)) {
249            $hdrs->header($k => $v);
250            return $v;
251        }
252        else {
253            return $hdrs->header($k);
254        }
255    }
256    else {
257        if (defined($self->{response_headers})) {
258            return $self->{response_headers};
259        }
260        else {
261            return $self->{response_headers} = Perlbal::HTTPHeaders->new_response(200);
262        }
263    }
264}
265
266sub response_status_code {
267    my ($self, $value) = @_;
268
269    my $res = $self->response_header;
270    if (defined($value)) {
271        $res->code($value);
272    }
273
274    return $res->response_code;
275}
276
277sub uri {
278    my ($self) = @_;
279    return $self->{uri} ? $self->{uri} : $self->{uri} = URI->new($self->request_header->request_uri);
280}
281
282sub path {
283    my ($self) = @_;
284    return $self->uri->path;
285}
286
287sub path_segments {
288    my ($self) = @_;
289    my @segments = $self->uri->path_segments;
290    shift @segments; # Get rid of the empty segment at the start
291    return @segments;
292}
293
294sub query_string {
295    my ($self) = @_;
296    return $self->uri->query;
297}
298
299sub query_args {
300    my ($self) = @_;
301    return $self->uri->query_form;
302}
303
304sub method {
305    my ($self) = @_;
306    return $self->request_header->request_method;
307}
308
309sub send_response_header {
310    my ($self) = @_;
311    $self->response_header('Content-type' => 'text/html') unless $self->response_header('Content-type');
312    $self->{pb}->write($self->response_header->to_string_ref);
313    $self->{header_sent} = 1;
314}
315
316sub response_header_sent {
317    return $_[0]->{header_sent} ? 1 : 0;
318}
319
320sub PRINT {
321    my ($self, @stuff) = @_;
322    $self->print(@stuff);
323}
324
325sub PRINTF {
326    my ($self, $format, @stuff) = @_;
327    $self->print(sprintf($format, @stuff));
328}
329
330sub print {
331    my ($self, @stuff) = @_;
332    if (! $self->response_header_sent) {
333        $self->send_response_header();
334    }
335    $self->{pb}->write(join("", @stuff));
336}
337
338sub end_response {
339    my ($self) = @_;
340    $self->{pb}->write(sub { $self->{pb}->http_response_sent; });
341}
342
3431;
Note: See TracBrowser for help on using the browser.