1 | #! perl # optional depends=tcp |
1 | #! perl # optional depends=tcp |
2 | |
2 | |
3 | # http server |
3 | # http server - this tried to speak enough of http 1.1 (and 1.0) |
|
|
4 | # to work with browsers. it does not even attempt to be a complete |
|
|
5 | # implementation, although it should be mostly correct for that it does. |
4 | |
6 | |
5 | sub send { |
7 | sub send { |
6 | my $self = $_[0]; |
8 | my $self = $_[0]; |
7 | |
9 | |
|
|
10 | if (length $self->{wbuf}) { |
8 | $self->{wbuf} .= $_[1]; |
11 | $self->{wbuf} .= $_[1]; |
|
|
12 | } else { |
|
|
13 | $self->{wbuf} .= $_[1]; |
9 | |
14 | |
10 | $self->{ww} ||= AE::io $self->{fh}, 1, sub { |
|
|
11 | my $len = syswrite $self->{fh}, $self->{wbuf}; |
15 | my $len = syswrite $self->{fh}, $self->{wbuf}; |
12 | substr $self->{wbuf}, 0, $len, ""; |
16 | substr $self->{wbuf}, 0, $len, ""; |
13 | |
17 | |
|
|
18 | $self->{ww} = AE::io $self->{fh}, 1, sub { |
|
|
19 | my $len = syswrite $self->{fh}, $self->{wbuf}; |
|
|
20 | substr $self->{wbuf}, 0, $len, ""; |
|
|
21 | |
|
|
22 | delete $self->{ww} unless $len; # in case of errors, stop |
14 | delete $self->{ww} unless length $self->{wbuf}; |
23 | delete $self->{ww} unless length $self->{wbuf}; |
|
|
24 | } if length $self->{wbuf}; |
15 | }; |
25 | } |
16 | } |
26 | } |
17 | |
27 | |
18 | sub fatal { |
28 | sub fatal { |
19 | my ($self) = @_; |
29 | my ($self) = @_; |
20 | |
30 | |
21 | $self->send ("HTTP/1.1 500 internal error\015\012"); |
31 | $self->send ("HTTP/1.1 500 internal error\015\012"); |
22 | delete $self->{rw}; |
|
|
23 | } |
32 | } |
24 | |
33 | |
25 | sub respond { |
34 | sub respond { |
26 | $_[0]->send ("HTTP/1.1 $_[1]\015\012" |
35 | $_[0]->send ("HTTP/1.1 $_[1]\015\012" |
27 | . "content-length: " . (0 + length $_[2]) . "\015\012" |
36 | . "content-length: " . (0 + length $_[2]) . "\015\012" |
28 | . "access-control-allow-origin: *\015\012" |
37 | . "access-control-allow-origin: *\015\012" |
29 | . "$_[3]\015\012$_[2]"); |
38 | . $_[0]{ohdr} |
|
|
39 | . "$_[3]\015\012" . ($_[0]{give_head} ? "" : $_[2])); |
30 | } |
40 | } |
31 | |
41 | |
32 | my $cache_headers = "cache-control: max-age=8640000\015\012" |
42 | my $cache_headers = "cache-control: max-age=8640000\015\012" |
33 | . "etag: \"0\"\015\012"; |
43 | . "etag: \"0\"\015\012"; |
34 | |
44 | |
35 | sub content_type { |
45 | sub content_type { |
36 | return "content-type: image/png\015\012" if $_[0] =~ /^\x89PNG/; |
46 | return "content-type: image/png\015\012" if $_[0] =~ /^\x89PNG/; |
37 | return "content-type: image/jpeg\015\012" if $_[0] =~ /^......JFIF/s; |
47 | return "content-type: image/jpeg\015\012" if $_[0] =~ /^......JFIF/s; |
38 | return "content-type: audio/wav\015\012" if $_[0] =~ /^RIFF/; |
48 | return "content-type: audio/wav\015\012" if $_[0] =~ /^RIFF/; |
39 | return "content-type: audio/ogg\015\012" if $_[0] =~ /^OggS/; |
49 | return "content-type: audio/ogg\015\012" if $_[0] =~ /^OggS/; |
|
|
50 | return "content-type: text/html\015\012" if $_[0] =~ /^</; |
40 | |
51 | |
41 | "content-type: text/plain\015\012" |
52 | "content-type: text/plain\015\012" |
42 | } |
53 | } |
43 | |
54 | |
44 | sub handle_req { |
55 | sub handle_con { |
45 | my ($self) = @_; |
56 | my ($self) = @_; |
46 | |
57 | |
|
|
58 | my ($method, $uri, $http, $req, $close, $len); |
|
|
59 | |
|
|
60 | while () { |
47 | while ($self->{rbuf} =~ s/^( (?: [^\015]+ | . )+? )\015\012\015\012//xs) { |
61 | while ($self->{rbuf} =~ s/^( (?: [^\015]+ | . )+? \015\012)\015\012//xs) { |
48 | my $req = $1; |
62 | $req = $1; |
49 | |
63 | |
50 | # we ignore headers atm. |
64 | # we ignore headers atm. |
51 | |
65 | |
52 | $req =~ m%^GET (\S+) HTTP/[0-9.]+\015\012%i |
66 | $req =~ m%^(\S+) (\S+) HTTP/([0-9.]+)\015\012%ig |
53 | or return $self->fatal; |
67 | or return $self->respond ("400 bad request"); |
54 | |
68 | |
|
|
69 | $method = uc $1; |
55 | my $uri = $1; |
70 | $uri = $2; |
|
|
71 | $http = $3; |
56 | |
72 | |
57 | $uri =~ s%^http://[^/]*%%i; # just in case |
73 | $uri =~ s%^http://[^/]*%%i; # just in case |
|
|
74 | $uri =~ s/%([0-9a-fA-F][0-9a-fA-F])/chr hex $1/ge; # %-decode |
58 | |
75 | |
|
|
76 | # my %hdr; |
|
|
77 | # |
|
|
78 | # $hdr{lc $1} .= ",$2" |
|
|
79 | # while $req =~ /\G |
|
|
80 | # ([^:\000-\037]*): |
|
|
81 | # [\011\040]* |
|
|
82 | # ((?: [^\012]+ | \012[\011\040] )*) |
|
|
83 | # \012 |
|
|
84 | # /gxc; |
|
|
85 | # |
|
|
86 | # $req =~ /\G$/ |
|
|
87 | # or return $self->respond ("400 bad request"); |
|
|
88 | # |
|
|
89 | # # remove the "," prefix we added to all headers above |
|
|
90 | # substr $_, 0, 1, "" |
|
|
91 | # for values %hdr; |
|
|
92 | |
|
|
93 | if ($http == 1.0) { |
|
|
94 | if ($req =~ /^connection\s*:\s*keep-alive/mi) { |
|
|
95 | $self->{ohdr} = "connection: keep-alive\015\012"; |
|
|
96 | } else { |
|
|
97 | $self->{ohdr} = "connection: close\015\012"; |
|
|
98 | $close = 1; |
|
|
99 | } |
|
|
100 | } |
|
|
101 | |
|
|
102 | $self->{give_head} = $method eq "HEAD" |
|
|
103 | and $method = "GET"; |
|
|
104 | |
59 | cf::debug "HTTP GET: $self->{id} $uri"; |
105 | cf::debug "HTTP $method: $self->{id} $uri"; |
60 | |
106 | |
61 | if ($uri =~ m%^/([0-9a-f]+)(M?)$%) { # faces |
107 | if ($method ne "GET") { |
62 | my $want_meta = $2; |
108 | $self->respond ("405 no $method"); |
63 | my $idx = $cf::FACEHASH{pack "H*", $1}; |
|
|
64 | |
|
|
65 | $idx |
|
|
66 | or do { $self->respond ("404 illegal face name"), next }; |
|
|
67 | |
|
|
68 | if ($req =~ /if-none-match/i) { # dirtiest hack evar |
|
|
69 | $self->respond ("304 not modified", "", $cache_headers); |
|
|
70 | next; |
109 | next; |
71 | } |
110 | } |
72 | |
111 | |
|
|
112 | # not needed for GET, but who knows |
|
|
113 | $self->send ("HTTP/1.1 100 go on\015\012") |
|
|
114 | if $req =~ /^expect:.*\b100-continue\b/i; |
|
|
115 | |
|
|
116 | if ($uri =~ m%^/([0-9a-f]+)(M?)$%) { # faces |
|
|
117 | my $want_meta = $2; |
|
|
118 | my $idx = $cf::face::HASH{pack "H*", $1}; |
|
|
119 | |
|
|
120 | $idx |
|
|
121 | or do { $self->respond ("404 illegal face name"), next }; |
|
|
122 | |
|
|
123 | if ($req =~ /if-none-match/i) { # dirtiest hack evar |
|
|
124 | $self->respond ("304 not modified", "", $cache_headers); |
|
|
125 | next; |
|
|
126 | } |
|
|
127 | |
73 | my $type = cf::face::get_type $idx, 1; |
128 | my $type = cf::face::get_type $idx; |
74 | my $data = cf::face::get_data $idx, 1; |
129 | my $data = cf::face::get_data $idx; |
75 | |
130 | |
76 | (my $meta, $data) = unpack "(w/a*)*", $data |
131 | (my $meta, $data) = unpack "(w/a*)*", $data |
77 | if $type & 1; |
132 | if $type & 1; |
78 | |
133 | |
79 | if ($want_meta) { |
134 | if ($want_meta) { |
80 | if ($type & 1) { |
135 | if ($type & 1) { |
81 | $self->respond ("200 OK", $meta, "content-type: text/plain\015\012" . $cache_headers); |
136 | $self->respond ("200 OK", $meta, "content-type: text/plain\015\012" . $cache_headers); |
|
|
137 | } else { |
|
|
138 | $self->respond ("404 type $type has no metadata"); |
|
|
139 | } |
82 | } else { |
140 | } else { |
83 | $self->respond ("404 type $type has no metadata"); |
141 | $self->respond ("200 OK", $data, (content_type $data) . $cache_headers); |
|
|
142 | } |
|
|
143 | |
|
|
144 | } elsif (my $idx = (cf::face::find "res/http$uri") || (cf::face::find "res/http${uri}index.html")) { |
|
|
145 | # TODO: use etag (shudder) |
|
|
146 | my $data = cf::face::get_data $idx; |
|
|
147 | $self->respond ("200 OK", $data, (content_type $data) . $cache_headers); |
|
|
148 | |
|
|
149 | } elsif (cf::face::find "res/http$uri/index.html") { |
|
|
150 | $self->respond ("302 dirslash", "", "location: $uri/\015\012"); |
|
|
151 | |
|
|
152 | } elsif ($uri eq "/debug") { # for debugging |
|
|
153 | my @body = "<html><body>"; |
|
|
154 | |
|
|
155 | for my $type (6, 5, 4, 3, 2, 1, 0) { |
|
|
156 | push @body, "<h1>$type</h1><table><tr><th>#</th><th>csum</th><th>size</th><th>name</th><th>meta</th></tr>"; |
|
|
157 | |
|
|
158 | for (1 .. cf::face::faces_size - 1) { |
|
|
159 | cf::cede_to_tick; |
|
|
160 | |
|
|
161 | next if $type != cf::face::get_type $_; |
|
|
162 | my $name = cf::face::get_name $_; |
|
|
163 | my $id = unpack "H*", cf::face::get_csum $_, 0; |
|
|
164 | push @body, "<tr><td>$_</td><td>$id</td><td>$cf::face::SIZE[0][$_]</td><td><a href='$id'>$name</a></td>"; |
|
|
165 | push @body, "<td><a href='${id}M'>meta</a>" if $type & 1; |
|
|
166 | push @body, "</tr>"; |
84 | } |
167 | } |
|
|
168 | |
|
|
169 | push @body, "</table>"; |
|
|
170 | } |
|
|
171 | |
|
|
172 | push @body, "</body></html>"; |
|
|
173 | |
|
|
174 | my $body = join "", @body; |
|
|
175 | utf8::encode $body; |
|
|
176 | $self->respond ("200 OK", $body, "content-type: text/html\015\012"); |
|
|
177 | |
85 | } else { |
178 | } else { |
86 | $self->respond ("200 OK", $data, (content_type $data) . $cache_headers); |
179 | $self->respond ("404 not found"); |
87 | } |
180 | } |
88 | |
181 | |
89 | } elsif ($uri eq "/debug") { # for debugging |
182 | cf::cede_to_tick; |
90 | my @body = "<html><body>"; |
|
|
91 | |
|
|
92 | for my $type (6, 5, 4, 3, 2, 1, 0) { |
|
|
93 | push @body, "<h1>$type</h1>"; |
|
|
94 | |
|
|
95 | for (1 .. cf::face::faces_size - 1) { |
|
|
96 | next if $type != cf::face::get_type $_; |
|
|
97 | my $name = cf::face::get_name $_; |
|
|
98 | my $id = unpack "H*", cf::face::get_chksum $_, 1; |
|
|
99 | push @body, "$_ <a href='$id'>$name ($id)</a>"; |
|
|
100 | push @body, " <a href='${id}M'>(meta)</a>" if $type & 1; |
|
|
101 | push @body, "<br>"; |
|
|
102 | } |
|
|
103 | } |
|
|
104 | |
|
|
105 | push @body, "</body></html>"; |
|
|
106 | |
|
|
107 | $self->respond ("200 OK", (join "", @body), "content-type: text/html\015\012"); |
|
|
108 | } elsif ($uri eq "/ws" && defined &ext::ws::server) { |
|
|
109 | &ext::ws::server ($self->{id}, $self->{fh}, "$req\015\012\015\012$self->{rbuf}"); |
|
|
110 | |
|
|
111 | %$self = (); |
|
|
112 | |
|
|
113 | } elsif ($uri eq "/") { |
|
|
114 | $self->respond ("302 hack", "", "location: http://cvs.schmorp.de/deliantra/html5client/client.html\015\012"); |
|
|
115 | |
|
|
116 | } else { |
|
|
117 | $self->respond ("404 not found"); |
|
|
118 | } |
183 | } |
|
|
184 | |
|
|
185 | return if $close; # http 1.0 only currently |
|
|
186 | return if length $self->{rbuf} > 8192; # headers too long |
|
|
187 | |
|
|
188 | Coro::AnyEvent::readable $self->{fh}, 6; |
|
|
189 | |
|
|
190 | $len = sysread $self->{fh}, $self->{rbuf}, 4096, length $self->{rbuf}; |
|
|
191 | |
|
|
192 | return if $len <= 0; |
119 | } |
193 | }; |
120 | } |
194 | } |
121 | |
195 | |
122 | our $DETECTOR = ext::tcp::register http => 64, sub { |
196 | our $DETECTOR = ext::tcp::register http => 64, sub { |
123 | # regex avoids conflict with websockets, which use /ws |
197 | # regex avoids conflict with websockets, which use /ws |
124 | m{^(?i:GET|HEAD|OPTIONS) \ (?! (?i:http://[^/]+)? /ws \ ) }x |
198 | m{^(?i:GET|HEAD|OPTIONS) \ (?! (?i:http://[^/]+)? /ws \ ) }x |
… | |
… | |
128 | fh => $_[1], |
202 | fh => $_[1], |
129 | rbuf => $_[2], |
203 | rbuf => $_[2], |
130 | wbuf => "", |
204 | wbuf => "", |
131 | }; |
205 | }; |
132 | |
206 | |
133 | $self->{rw} = AE::io $self->{fh}, 0, sub { |
207 | $self->{async} = Coro::async_pool { |
134 | my $len = sysread $self->{fh}, $self->{rbuf}, 4096, length $self->{rbuf}; |
208 | $Coro::current->nice (4); |
|
|
209 | $Coro::current->{desc} = "http $self->{id}"; |
135 | |
210 | |
136 | if ($len == 0) { |
|
|
137 | delete $self->{rw}; |
|
|
138 | } else { |
|
|
139 | $self->handle_req; |
211 | $self->handle_con; |
140 | |
|
|
141 | delete $self->{rw} if length $self->{rbuf} > 8192; # headers too long |
|
|
142 | } |
|
|
143 | }; |
212 | }; |
144 | |
|
|
145 | $self->handle_req; # in the unlikely case of the buffer already forming a valid request |
|
|
146 | }; |
213 | }; |
147 | |
214 | |
148 | cf::register_exticmd http_faceurl => sub { |
215 | cf::register_exticmd http_faceurl => sub { |
149 | my ($ns) = @_; |
216 | my ($ns) = @_; |
150 | |
217 | |