ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Coro/eg/myhttpd
Revision: 1.3
Committed: Fri Sep 14 15:40:56 2001 UTC (22 years, 9 months ago) by root
Branch: MAIN
Changes since 1.2: +2 -2 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 #!/usr/bin/perl
2    
3 root 1.2 # this is a relatively small web-server, using coroutines
4     # for connections. play around with it but do not use
5     # it in production. ask myhttpd@plan9.de for a better
6     # version that's more usable and bugfixed.
7    
8 root 1.1 use Coro;
9     use Coro::Semaphore;
10     use Coro::Event;
11     use Coro::Socket;
12    
13     no utf8;
14     use bytes;
15    
16     # at least on my machine, this thingy serves files
17     # quite a bit faster than apache, ;)
18     # and quite a bit slower than thttpd :(
19    
20     $MAX_CONNECTS = 500; # maximum simult. connects
21     $REQ_TIMEOUT = 60; # request timeout
22     $RES_TIMEOUT = 180; # response timeout
23     $MAX_POOL = 20; # max. number of idle workers
24     $DOCROOT = "/usr/www/htdocs"; # document root
25     $INDEXPROG = "/usr/www/bin/dols"; # indexing program (nph-cgi script)
26     $SERVER_HOST = "0.0.0.0"; # host to bind on
27     $SERVER_PORT = 80; # port to listen on
28    
29     my $port = new Coro::Socket
30     LocalAddr => $SERVER_HOST,
31     LocalPort => $SERVER_PORT,
32     ReuseAddr => 1,
33     Listen => 1,
34     or die "unable to start server";
35    
36     $SIG{PIPE} = 'IGNORE';
37    
38     sub slog {
39     my $level = shift;
40     my $format = shift;
41     #printf "---: $format\n", @_;
42     }
43    
44     my $connections = new Coro::Semaphore $MAX_CONNECTS;
45    
46     my @fh;
47     my @pool;
48    
49     sub handler {
50     while () {
51     my $fh = pop @fh;
52     if ($fh) {
53     eval {
54     conn->new($fh)->handle;
55     };
56     close $fh;
57     slog 1, "$@" if $@ && !ref $@;
58     $connections->up;
59     } else {
60     last if @pool >= $MAX_POOL;
61     push @pool, $Coro::current;
62     schedule;
63     }
64     }
65     }
66    
67     async {
68     slog 1, "accepting connections";
69     while () {
70     $connections->down;
71     push @fh, $port->accept;
72     #slog 3, "accepted @$connections ".scalar(@pool);
73     if (@pool) {
74     (pop @pool)->ready;
75     } else {
76     async \&handler;
77     }
78    
79     }
80     };
81    
82     loop;
83     print "ende\n";#d#
84    
85     package conn;
86    
87     use Socket;
88     use HTTP::Date;
89    
90     sub new {
91     my $class = shift;
92     my $fh = shift;
93 root 1.3 my (undef, $iaddr) = unpack_sockaddr_in $fh->peername
94 root 1.1 or $self->err(500, "unable to get peername");
95     $self->{remote_address} = inet_ntoa $iaddr;
96     bless { fh => $fh }, $class;
97     }
98    
99     sub slog {
100     main::slog(@_);
101     }
102    
103     sub print_response {
104     my ($self, $code, $msg, $hdr, $content) = @_;
105     my $res = "HTTP/1.0 $code $msg\015\012";
106    
107     $hdr->{Date} = time2str time; # slow? nah.
108    
109     while (my ($h, $v) = each %$hdr) {
110     $res .= "$h: $v\015\012"
111     }
112     $res .= "\015\012$content" if defined $content;
113    
114     print {$self->{fh}} $res;
115     }
116    
117     sub err {
118     my $self = shift;
119     my ($code, $msg, $hdr, $content) = @_;
120    
121     unless (defined $content) {
122     $content = "$code $msg";
123     $hdr->{"Content-Type"} = "text/plain";
124     $hdr->{"Content-Length"} = length $content;
125     }
126    
127     $self->slog($msg) if $code;
128    
129     $self->print_response($code, $msg, $hdr, $content);
130    
131     die bless {}, err::;
132     }
133    
134     sub handle {
135     my $self = shift;
136     my $fh = $self->{fh};
137    
138     #while() {
139     $self->{h} = {};
140    
141     # read request and parse first line
142     $fh->timeout($::REQ_TIMEOUT);
143     my $req = $fh->readline("\015\012\015\012");
144     $fh->timeout($::RES_TIMEOUT);
145    
146     defined $req or
147     $self->err(408, "request timeout");
148    
149     $req =~ /^(?:\015\012)?
150     (GET|HEAD) \040+
151     ([^\040]+) \040+
152     HTTP\/([0-9]+\.[0-9]+)
153     \015\012/gx
154     or $self->err(403, "method not allowed", { Allow => "GET,HEAD" });
155    
156     $2 ne "1.0"
157     or $self->err(506, "http protocol version not supported");
158    
159     $self->{method} = $1;
160     $self->{uri} = $2;
161    
162     # parse headers
163     {
164     my (%hdr, $h, $v);
165    
166     $hdr{lc $1} .= ",$2"
167     while $req =~ /\G
168     ([^:\000-\040]+):
169     [\008\040]*
170     ((?: [^\015\012]+ | \015\012[\008\040] )*)
171     \015\012
172     /gxc;
173    
174     $req =~ /\G\015\012$/
175     or $self->err(400, "bad request");
176    
177     $self->{h}{$h} = substr $v, 1
178     while ($h, $v) = each %hdr;
179     }
180    
181     $self->{server_port} = $self->{h}{host} =~ s/:([0-9]+)$// ? $1 : 80;
182    
183     $self->map_uri;
184     $self->respond;
185     #}
186     }
187    
188     # uri => path mapping
189     sub map_uri {
190     my $self = shift;
191     my $host = $self->{h}{host} || "default";
192     my $uri = $self->{uri};
193    
194     # some massaging, also makes it more secure
195     $uri =~ s/%([0-9a-fA-F][0-9a-fA-F])/chr hex $1/ge;
196     $uri =~ s%//+%/%g;
197     $uri =~ s%/\.(?=/|$)%%g;
198     1 while $uri =~ s%/[^/]+/\.\.(?=/|$)%%;
199    
200     $uri =~ m%^/?\.\.(?=/|$)%
201     and $self->err(400, "bad request");
202    
203     $self->{name} = $uri;
204    
205     # now do the path mapping
206     $self->{path} = "$::DOCROOT/$host$uri";
207     }
208    
209     sub server_address {
210     my $self = shift;
211 root 1.3 my ($port, $iaddr) = unpack_sockaddr_in $self->{fh}->sockname
212 root 1.1 or $self->err(500, "unable to get socket name");
213     ((inet_ntoa $iaddr), $port);
214     }
215    
216     sub server_host {
217     my $self = shift;
218     if (exists $self->{h}{host}) {
219     return $self->{h}{host};
220     } else {
221     return (($self->server_address)[0]);
222     }
223     }
224    
225     sub server_hostport {
226     my $self = shift;
227     my ($host, $port);
228     if (exists $self->{h}{host}) {
229     ($host, $port) = ($self->{h}{host}, $self->{server_port});
230     } else {
231     ($host, $port) = $self->server_address;
232     }
233     $port = $port == 80 ? "" : ":$port";
234     $host.$port;
235     }
236    
237 root 1.2 # no, this doesn't do cgi, but it's close enough
238     # for the no-longer-used directory indexing script.
239 root 1.1 sub _cgi {
240     my $self = shift;
241     my $path = shift;
242     my $fh;
243    
244     # no two-way xxx supported
245     if (0 == fork) {
246     open STDOUT, ">&".fileno($self->{fh});
247     if (chdir $::DOCROOT) {
248     $ENV{SERVER_SOFTWARE} = "thttpd-myhttpd"; # we are thttpd-alike
249     $ENV{HTTP_HOST} = $self->server_host;
250     $ENV{HTTP_PORT} = $self->{server_host};
251     $ENV{SCRIPT_NAME} = $self->{name};
252     exec $::INDEXPROG;
253     }
254     Coro::State::_exit(0);
255     } else {
256     }
257     }
258    
259     sub respond {
260     my $self = shift;
261     my $path = $self->{path};
262    
263     stat $path
264     or $self->err(404, "not found");
265    
266     # idiotic netscape sends idiotic headers AGAIN
267     my $ims = $self->{h}{"if-modified-since"} =~ /^([^;]+)/
268     ? str2time $1 : 0;
269    
270     if (-d _ && -r _) {
271     # directory
272     if ($path !~ /\/$/) {
273     # create a redirect to get the trailing "/"
274     my $host = $self->server_hostport;
275     $self->err(301, "moved permanently", { Location => "http://$host$self->{uri}/" });
276     } else {
277     $ims < (stat _)[9]
278     or $self->err(304, "not modified");
279    
280     if ($self->{method} eq "GET") {
281     if (-r "$path/index.html") {
282     $self->{path} .= "/index.html";
283     $self->handle_file;
284     } else {
285     $self->handle_dir;
286     }
287     }
288     }
289     } elsif (-f _ && -r _) {
290     -x _ and $self->err(403, "forbidden");
291     $self->handle_file;
292     } else {
293     $self->err(404, "not found");
294     }
295     }
296    
297     sub handle_dir {
298     my $self = shift;
299     $self->_cgi($::INDEXPROG);
300     }
301    
302     sub handle_file {
303     my $self = shift;
304     my $length = -s _;
305     my $hdr = {
306     "Last-Modified" => time2str ((stat _)[9]),
307     };
308    
309     my @code = (200, "ok");
310     my ($l, $h);
311    
312     if ($self->{h}{range} =~ /^bytes=(.*)$/) {
313     for (split /,/, $1) {
314     if (/^-(\d+)$/) {
315     ($l, $h) = ($length - $1, $length - 1);
316     } elsif (/^(\d+)-(\d*)$/) {
317     ($l, $h) = ($1, ($2 ne "" || $2 >= $length) ? $2 : $length - 1);
318     } else {
319     ($l, $h) = (0, $length - 1);
320     goto ignore;
321     }
322     goto satisfiable if $l >= 0 && $l < $length && $h >= 0 && $h > $l;
323     }
324     $hdr->{"Content-Range"} = "bytes */$length";
325     $self->err(416, "not satisfiable", $hdr);
326    
327     satisfiable:
328     $hdr->{"Content-Range"} = "bytes $l-$h/$length";
329     @code = (206, "partial content");
330     $length = $h - $l + 1;
331    
332     ignore:
333     } else {
334     ($l, $h) = (0, $length - 1);
335     }
336    
337     if ($self->{path} =~ /\.html$/) {
338     $hdr->{"Content-Type"} = "text/html";
339     } else {
340     $hdr->{"Content-Type"} = "application/octet-stream";
341     }
342    
343     $hdr->{"Content-Length"} = $length;
344    
345     $self->print_response(@code, $hdr, "");
346    
347     if ($self->{method} eq "GET") {
348     my ($fh, $buf);
349     open $fh, "<", $self->{path}
350     or die "$self->{path}: late open failure ($!)";
351    
352     if ($l) {
353     sysseek $fh, $l, 0
354     or die "$self->{path}: cannot seek to $l ($!)";
355     }
356    
357     $h -= $l - 1;
358    
359     while ($h > 0) {
360     $h -= sysread $fh, $buf, $h > 4096 ? 4096 : $h;
361     print {$self->{fh}} $buf
362     or last;
363     }
364     }
365    
366     close $fh;
367     }