ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MCP/lib/AnyEvent/MCP/Server/HTTP.pm
Revision: 1.16
Committed: Wed Oct 21 17:28:16 2026 UTC (2 weeks, 2 days ago) by root
Branch: MAIN
CVS Tags: rel-0_61, rel-0_6
Changes since 1.15: +23 -0 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 =head1 NAME
2    
3     AnyEvent::MCP::Server::HTTP - an optional MCP HTTP listener and socket handler
4    
5     =head1 DESCRIPTION
6    
7 root 1.3 A subclass of L<AnyEvent::MCP::Server> that handles HTTP POST requests.
8     It can open its own listener, or you can give it sockets accepted elsewhere.
9     If your application already parses HTTP, you can pass decoded requests to
10     L<AnyEvent::MCP::Server> instead of running another HTTP implementation.
11    
12     Requests sharing one connection are dispatched in sequence, preserving their
13     HTTP response order; requests on other connections are independent. This
14     implementation returns JSON and drops notifications. It does not support
15     SSE or subscriptions.
16    
17     Request bodies need C<Content-Length>: chunked transfer encoding is not
18     supported. The server does not assign protocol sessions. Legacy clients
19     use the 2025 request/response subset.
20 root 1.1
21 root 1.10 C<Mcp-Param-*> headers from C<x-mcp-header> tool schema annotations are not
22 root 1.13 currently validated against the body. The built-in protocol headers
23     C<Mcp-Protocol-Version>, C<Mcp-Method> and, where applicable, C<Mcp-Name>
24     must agree with modern request metadata and parameters; a mismatch is
25     rejected. Custom schema-derived headers do not receive this check.
26 root 1.10
27 root 1.16 =head1 FIRST USE
28    
29     For an application that already registers tools in
30     L<AnyEvent::MCP::Server::Toolset>, start an HTTP listener:
31    
32     use AnyEvent;
33     use AnyEvent::MCP::Server::HTTP;
34    
35     my $server = AnyEvent::MCP::Server::HTTP->new (
36     toolsets => [$tools], tokens => [$secret],
37     host => "127.0.0.1", port => 31740,
38     );
39     $server->listen;
40     print $server->url, "\n";
41     AE::cv->recv;
42    
43     C<$tools> is a Toolset and C<$secret> is a previously generated private
44     URL-safe token. Do not use a sample or hardcoded token in a real service.
45     This object handles sockets and HTTP; L<AnyEvent::MCP::Server> dispatches
46     requests, while the Toolset runs tool callbacks. For a standalone program
47     with automatic configuration and startup, prefer
48     L<AnyEvent::MCP::Server::Simple>.
49    
50 root 1.7 =head1 AUTHENTICATION
51    
52     Unlike L<AnyEvent::MCP::Server::Simple/mcp_start>, this low-level object
53     does not load global configuration or derive credentials. Supply C<tokens>
54     explicitly, for example:
55    
56     my $server = AnyEvent::MCP::Server::HTTP->new (
57     host => "127.0.0.1",
58     port => 31740,
59     path => "/mcp/",
60     tokens => ["a-long-random-secret", "another-long-random-secret"],
61     );
62     $server->listen;
63    
64     Each token authorizes the same requests. For either token, a client may
65     POST to C</TOKEN/mcp/> without an Authorization header, or POST to
66     C</mcp/> with C<Authorization: Bearer TOKEN>. URL credentials are path
67     components, B<not> HTTP cookies. C<url> advertises the first token;
68     C<url (0)> returns the clean endpoint.
69    
70     The accepted tokens are static, URL-safe strings. HTTP Basic and OAuth
71     access-token validation are not implemented here. There is no automatic
72     fallback to the master-secret-derived token: that belongs to C<mcp_start>
73     or L<AnyEvent::MCP::Config/http_settings>.
74    
75 root 1.8 Authentication defaults to C<tokens>, and construction B<fails> unless a
76     nonempty array of valid tokens is provided. To deliberately delegate
77     authentication to a trusted embedding layer (for example, a reverse proxy
78     that rejects unauthorized requests before they reach MCP), specify
79     C<auth =E<gt> "none"> instead, B<without> C<tokens>. This is an explicit
80     opt-out, not a permissive default. The embedding layer then owns the
81     entire authentication boundary.
82 root 1.7 The C<origins> setting filters browser origins; it is not authentication.
83     URLs can leak through logs and histories, and HTTP alone does not encrypt
84     tokens. Use TLS when credentials cross an untrusted network. A local address
85     is a sensible default, but not a credential.
86    
87 root 1.1 =head1 METHODS
88    
89 root 1.13 =over
90    
91     =item $server = AnyEvent::MCP::Server::HTTP->new (%config)
92 root 1.1
93     Construction does not start a listener. In addition to Server options,
94     the following configuration options are supported:
95    
96 root 1.3 =over
97 root 1.1
98     =item host => $host
99    
100     The listening address, defaulting to C<127.0.0.1>.
101    
102     =item port => $port
103    
104     The listening port, defaulting to zero for an automatically assigned port.
105    
106     =item path => $path
107    
108 root 1.6 The MCP endpoint path, defaulting to C</mcp/>. Bearer authentication uses
109     C<$path> directly; URL tokens prefix it with C</TOKEN>.
110 root 1.1
111     =item connection_close => $boolean
112    
113     When true, disables persistent connections, sends C<Connection: close> and
114     closes the socket after draining each response. Defaults to false.
115     This is useful when an existing listener routes sockets by their first
116     request and cannot route subsequent requests independently.
117    
118     =item max_size => $bytes
119    
120     Limits incoming headers and request bodies, defaulting to 1 MiB.
121    
122     =item origins => [$origin...]
123    
124 root 1.4 Allowed Origin header values. Defaults to C<["*"]>, permitting browser requests
125     from any origin. An empty array rejects requests with an Origin header, while
126     requests without one are accepted. Allowed origins receive CORS response headers,
127     and nonempty origin lists enable OPTIONS preflight for POST. Preflight does not
128 root 1.1 require authentication; the subsequent POST still does.
129    
130     =item handle => { %handle }
131    
132     AnyEvent::Handle constructor options, such as C<tls>, C<tls_ctx>, C<timeout>
133     or C<on_starttls>. The socket and the read/error/EOF handlers belong to this
134     class.
135    
136 root 1.6 =item tokens => [$token, ...]
137 root 1.1
138 root 1.6 Ordered tokens accepted either as C</TOKEN> before the configured
139     C<path> or in an C<Authorization: Bearer TOKEN> header. The first token
140 root 1.8 is used in advertised URLs. Token strings must be URL-safe and nonempty.
141    
142     =item auth => "tokens" | "none"
143    
144     Select authentication mode. C<tokens> is the default and requires at least
145     one token; C<none> explicitly disables HTTP authentication and cannot be
146     combined with tokens. Other values are rejected. Browser origins are
147     not a replacement for authentication.
148 root 1.1
149     =item name => $name
150    
151 root 1.6 Identifies the service. Use L<AnyEvent::MCP::Config/http_settings>
152     to resolve named listener configuration before constructing this transport.
153 root 1.1
154     =item url_host => $hostname
155    
156     Overrides the advertised hostname, for example when listening on a wildcard
157     address. Otherwise a wildcard uses the nodename from C<uname>.
158    
159     =item url_scheme => $scheme
160    
161     Overrides the advertised scheme; otherwise TLS handle options select
162     C<https>, and plain handles select C<http>.
163    
164     =back
165    
166 root 1.6 Every configured token authenticates in either the URL or the Authorization
167 root 1.8 header. Without tokens, the constructor requires C<auth =E<gt> "none">.
168 root 1.6
169 root 1.13 =item $url = $server->url ($with_token)
170 root 1.6
171     Returns the advertised URL with the actual listening port. C<$with_token>
172     defaults to true, including the first configured token. Pass false for
173     the clean URL to use with an Authorization bearer token.
174 root 1.1
175     The port must be known. Wildcard hostname resolution and reverse-proxy
176     routing remain the caller's responsibility.
177    
178 root 1.13 =item $server->listen ($host, $port, %handle)
179 root 1.1
180     Starts a listener, replacing any previous listener. Omitted arguments use
181     the configured host and port. The actual bound port is stored in
182     C<< $server->{port} >>, including when requesting an ephemeral port.
183    
184     C<%handle> overrides the configured C<handle> options for connections
185     accepted by this listener.
186    
187 root 1.13 =item $handle = $server->accept ($socket, %handle)
188 root 1.1
189     Starts handling an already accepted socket. The server takes ownership of
190     C<$socket>. No listener is needed.
191    
192     C<%handle> overrides the configured C<handle> options for this connection.
193    
194 root 1.13 =item $server->close
195 root 1.1
196     Stops listening, closes accepted sockets and cancels outstanding requests.
197     The same object can subsequently listen or accept again.
198    
199 root 1.13 =back
200    
201 root 1.1 =head1 EVENTS
202    
203 root 1.13 =over
204    
205 root 1.14 =item on_extension => $callback->($server, $handle, $hello)
206    
207     Accept raw extension connections on the HTTP listener. A first byte of C<[>
208     selects a bounded, newline-terminated JSON hello whose first two fields are
209     the protocol name and an accepted listener token. Authenticate before invoking
210     this callback, which validates the protocol-specific fields and takes ownership
211     of the handle, preserving buffered input. Throw an exception to reject it.
212     HTTP connections continue through the normal parser. This dispatch examines
213     decrypted bytes when TLS is configured, but the built-in reverse connector
214     currently supports only plain C<http://> endpoints.
215    
216 root 1.15 =item on_http_request => $callback->($server, $request, $reply)
217    
218     Called for every complete, syntactically valid HTTP request before MCP routing
219     or authentication. C<$request> is a hash containing the parsed HTTP fields
220     (including C<method>, C<uri>, and C<http_version>) and a C<body> field holding
221     the raw request body. Return false to use the normal MCP handling, or true to
222     handle the request yourself.
223    
224     C<$reply-E<gt>($status, $reason, \%headers, $body)> sends a response, either
225     immediately or asynchronously after the callback returns. C<%headers> is
226     optional; C<Content-Type> defaults to C<application/json>, and the server
227     generates C<Content-Length> and manages persistent connections. The body is
228     an octet string. Reply at most once, and return true when taking ownership
229     of the request, even if the response will be sent later. The next request on
230     the same connection is not read until that response is sent.
231    
232     The hook runs B<before> MCP authentication and CORS checks. Application
233     endpoints must perform their own authentication and browser request checks.
234     It does not replace or bypass the normal authentication of unhandled MCP
235     requests. Malformed or oversized requests are rejected before the hook.
236    
237 root 1.13 =item on_connection_error => $callback->($server, $handle, $message)
238 root 1.1
239     Called for socket errors before closing the connection.
240    
241 root 1.13 =item on_close => $callback->($server)
242 root 1.1
243     Called once after C<close> stops the listener and closes all connections.
244     Calling C<listen> or C<accept> again starts a new lifetime.
245    
246 root 1.13 =back
247    
248 root 1.1 =cut
249    
250     package AnyEvent::MCP::Server::HTTP;
251    
252     use common::sense;
253     use parent "AnyEvent::MCP::Server";
254     use AnyEvent::Handle ();
255     use AnyEvent::Socket ();
256     use JSON::XS ();
257     use Scalar::Util qw(weaken);
258     use POSIX ();
259    
260     sub new {
261     my ($class, %kv) = @_;
262 root 1.12
263 root 1.6 !grep (exists $kv{$_}, qw(cookie bearer users))
264     or die "Legacy HTTP authentication options are unsupported; use tokens\n";
265 root 1.1
266 root 1.8 $kv{auth} //= "tokens";
267     if ($kv{auth} eq "tokens") {
268 root 1.6 ref $kv{tokens} eq "ARRAY" && @{$kv{tokens}}
269     or die "HTTP server requires nonempty tokens\n";
270     /\A[A-Za-z0-9_.~-]+\z/ or die "Invalid HTTP server token\n"
271     for @{$kv{tokens}};
272 root 1.8 } elsif ($kv{auth} eq "none") {
273     !exists $kv{tokens}
274     or die "Unauthenticated HTTP server must not configure tokens\n";
275     } else {
276     die "Unsupported HTTP authentication type '$kv{auth}'\n";
277 root 1.1 }
278    
279 root 1.4 $class->SUPER::new (host => "127.0.0.1", port => 0, path => "/mcp/", origins => ["*"], %kv)
280 root 1.1 }
281    
282     sub listen {
283     my ($self, $host, $port, %handle) = @_;
284    
285     delete $self->{_closed};
286     $host = $self->{host} unless defined $host;
287     $port = $self->{port} unless defined $port;
288    
289     $self->{listen_host} = $host;
290     $self->{listen_handle} = { %{$self->{handle} || {}}, %handle };
291    
292     delete $self->{listener};
293     weaken $self;
294    
295     $self->{listener} = AnyEvent::Socket::tcp_server $host, $port, sub {
296     $self->accept ($_[0], %handle) if $self;
297     }, sub {
298     $self->{port} = $_[2] if $self;
299     ()
300     };
301     }
302    
303 root 1.6 sub _token_path {
304     my ($self, $token) = @_;
305 root 1.12
306 root 1.6 "/$token$self->{path}"
307 root 1.1 }
308    
309     sub url {
310 root 1.12 my ($self, $with_token) = @_;
311 root 1.1
312 root 1.12 $with_token = 1 unless defined $with_token;
313 root 1.1
314     my $host = $self->{url_host}
315     || (defined $self->{listen_host} ? $self->{listen_host} : $self->{host});
316     $host = (POSIX::uname)[1] if !length $host || $host eq "0.0.0.0" || $host eq "::";
317 root 1.9 $host =~ s/\A\[(.*)\]\z/$1/; # allow already bracketed IPv6 URL hosts
318 root 1.1
319     my $port = $self->{port} or die "HTTP port is not known\n";
320     my $scheme = $self->{url_scheme}
321     || (($self->{listen_handle} || $self->{handle} || {})->{tls} ? "https" : "http");
322 root 1.12 my $path = $with_token && $self->{tokens} && @{$self->{tokens}}
323 root 1.6 ? $self->_token_path ($self->{tokens}[0]) : $self->{path};
324 root 1.1
325 root 1.9 "$scheme://" . AnyEvent::Socket::format_hostport ($host, $port) . $path
326 root 1.1 }
327    
328     sub accept {
329     my ($self, $socket, %handle) = @_;
330    
331     delete $self->{_closed};
332     weaken $self;
333    
334     my $handle = AnyEvent::Handle->new (
335     rbuf_max => $self->{max_size} || 1024 * 1024,
336     %{$self->{handle}},
337     %handle,
338     fh => $socket,
339     on_read => undef,
340     on_error => sub {
341     $self->event (connection_error => $_[0], $_[2]) if $self;
342     $self->_close ($_[0]) if $self;
343     },
344     on_eof => sub { $self->_close ($_[0]) if $self },
345     );
346    
347     $handle->{mcp_peer} = {};
348     $self->{connections}{$handle} = $handle;
349 root 1.14 if ($self->{on_extension}) {
350     $handle->rtimeout (15);
351     $handle->push_read (chunk => 1, sub {
352     my ($handle, $first) = @_;
353     return unless $self;
354     $handle->{rbuf} = $first . $handle->{rbuf};
355    
356     if ($first eq "[") {
357     require AnyEvent::MCP::Extension;
358     AnyEvent::MCP::Extension::accept ($handle, $self->{tokens} || [], sub {
359     my ($handle, $hello) = @_;
360     return $handle->destroy unless $self;
361     delete $self->{connections}{$handle};
362     $self->{on_extension}->($self, $handle, $hello);
363     }, sub { $self->_close ($_[0]) if $self });
364     } else {
365     $handle->rtimeout (0);
366     $self->_read ($handle);
367     }
368     });
369     } else {
370     $self->_read ($handle);
371     }
372 root 1.1
373     $handle
374     }
375    
376 root 1.12 # parse a request line and HTTP fields, using the same comma-combining
377 root 1.10 # approach as AnyEvent::HTTP::_parse_hdr. The caller already bounds headers.
378     sub _parse_request_headers {
379     local $_ = $_[0] . "\r\n";
380     /\A([!#$%&'*+.^_\x60|~0-9A-Za-z-]+) [ ]+
381     ([^\x00-\x20]+) [ ]+ HTTP\/0*([0-9.]+)\r\n/gcix
382     or return;
383     my ($method, $uri, $version) = ($1, $2, $3);
384     return unless $version =~ /\A1\.[0-9]+\z/;
385     my %hdr;
386    
387     $hdr{lc $1} .= ",$2"
388     while /\G
389     ([!\#$%&'*+.^_\x60|~0-9A-Za-z-]+):
390     [\t ]*
391     ([^\x00-\x08\x0a-\x1f\x7f]*)
392     \r\n
393     /gcx;
394    
395     /\G\z/gc or return;
396    
397 root 1.12 # discard the prefix used for comma-combining repeated field values.
398 root 1.10 substr $_, 0, 1, "" for values %hdr;
399     s/[\t ]+\z// for values %hdr;
400    
401 root 1.12 # request-line fields override any colliding HTTP header names.
402 root 1.11 +{ %hdr, method => $method, uri => $uri, http_version => $version }
403 root 1.10 }
404    
405 root 1.1 sub _read {
406     my ($self, $handle) = @_;
407    
408     weaken $self;
409    
410     $handle->push_read (line => "\r\n\r\n", sub {
411     my ($handle, $header) = @_;
412 root 1.12
413 root 1.1 return unless $self;
414    
415 root 1.11 my $hdr = _parse_request_headers ($header);
416 root 1.1 $handle->{mcp_close} = $self->{connection_close}
417 root 1.11 || !$hdr || $hdr->{http_version} =~ m{\A1\.0+\z}
418     || $hdr->{connection} =~ /\bclose\b/i;
419 root 1.1
420 root 1.11 return $self->_respond ($handle, 400, "Bad Request", "") unless $hdr;
421 root 1.1
422 root 1.11 $hdr->{"content-length"} = 0 unless defined $hdr->{"content-length"};
423 root 1.1 return $self->_respond ($handle, 400, "Bad Request", "")
424 root 1.11 if exists $hdr->{"transfer-encoding"}
425     || $hdr->{"content-length"} !~ /\A[0-9]+\z/;
426 root 1.1
427     return $self->_respond ($handle, 413, "Content Too Large", "")
428 root 1.11 if $hdr->{"content-length"} > ($self->{max_size} || 1024 * 1024);
429 root 1.1
430 root 1.11 if ($hdr->{expect}) {
431 root 1.1 return $self->_respond ($handle, 417, "Expectation Failed", "")
432 root 1.11 unless lc $hdr->{expect} eq "100-continue";
433 root 1.1
434     $handle->push_write ("HTTP/1.1 100 Continue\r\n\r\n");
435     }
436    
437 root 1.11 $handle->push_read (chunk => $hdr->{"content-length"}, sub {
438 root 1.1 my ($handle, $body) = @_;
439    
440 root 1.11 $self->_request ($handle, $hdr, $body) if $self;
441 root 1.1 });
442     });
443     }
444    
445     sub _request {
446 root 1.11 my ($self, $handle, $hdr, $body) = @_;
447 root 1.1
448     delete $handle->{mcp_origin};
449    
450 root 1.15 if (my $hook = $self->{on_http_request}) {
451     weaken $self;
452     my $replied;
453     my $reply = sub {
454     return if $replied++ || !$self || $handle->{mcp_closed};
455     my ($status, $reason, $headers, $content) = @_;
456     $self->_respond ($handle, $status, $reason, $content, $headers);
457     };
458    
459     return if $hook->($self, { %$hdr, body => $body }, $reply);
460     }
461    
462 root 1.6 my $token_path = $self->{tokens}
463 root 1.11 && grep $hdr->{uri} eq $self->_token_path ($_), @{$self->{tokens}};
464 root 1.1
465     return $self->_respond ($handle, 404, "Not Found", "")
466 root 1.11 unless $hdr->{uri} eq $self->{path} || $token_path;
467 root 1.1
468 root 1.11 if (exists $hdr->{origin}) {
469 root 1.1 my $wildcard = grep $_ eq "*", @{$self->{origins}};
470    
471     return $self->_respond ($handle, 403, "Forbidden", "")
472 root 1.11 unless $wildcard || grep $_ eq $hdr->{origin}, @{$self->{origins}};
473 root 1.1
474 root 1.11 $handle->{mcp_origin} = $wildcard ? "*" : $hdr->{origin};
475 root 1.1 }
476    
477 root 1.11 if ($hdr->{method} eq "OPTIONS" && @{$self->{origins}}) {
478 root 1.1 return $self->_respond ($handle, 405, "Method Not Allowed", "")
479 root 1.11 if exists $hdr->{"access-control-request-method"}
480     && $hdr->{"access-control-request-method"} ne "POST";
481 root 1.1
482     return $self->_respond ($handle, 204, "No Content", "");
483     }
484    
485     return $self->_respond ($handle, 405, "Method Not Allowed", "")
486 root 1.11 unless $hdr->{method} eq "POST";
487 root 1.1
488 root 1.6 if ($self->{tokens}) {
489     my $authenticated = $token_path;
490 root 1.11 my $authorization = $hdr->{authorization};
491 root 1.1
492 root 1.6 if ($authorization =~ /\ABearer ([^\s]+)\z/i) {
493     my $token = $1;
494     $authenticated ||= grep $_ eq $token, @{$self->{tokens}};
495 root 1.1 }
496    
497     return $self->_respond ($handle, 401, "Unauthorized", "")
498     unless $authenticated;
499     }
500    
501     return $self->_respond ($handle, 415, "Unsupported Media Type", "")
502 root 1.11 unless $hdr->{"content-type"} =~ m{\Aapplication/json(?:\s*;|\z)}i;
503 root 1.1
504     my $message = eval { JSON::XS::decode_json $body };
505    
506     if ($@) {
507     return $self->_rpc_error (sub {
508     $self->_respond ($handle, 400, "Bad Request", JSON::XS::encode_json $_[0]);
509     }, undef, -32700, "Parse error");
510     }
511    
512     my $params = ref $message eq "HASH" && ref $message->{params} eq "HASH" ? $message->{params} : {};
513     my $meta = ref $params->{_meta} eq "HASH" ? $params->{_meta} : {};
514     my $modern = exists $meta->{"io.modelcontextprotocol/protocolVersion"}
515 root 1.11 || $hdr->{"mcp-protocol-version"} ge "2026";
516 root 1.1
517     if ($modern && ref $message eq "HASH" && exists $message->{id}) {
518 root 1.11 my %expected = (
519     "mcp-protocol-version" => $meta->{"io.modelcontextprotocol/protocolVersion"},
520     "mcp-method" => $message->{method},
521 root 1.1 );
522    
523 root 1.11 $expected{"mcp-name"} = exists $params->{name} ? $params->{name} : $params->{uri}
524 root 1.1 if $message->{method} =~ /\A(?:tools\/call|prompts\/get|resources\/read)\z/;
525    
526 root 1.11 for my $name (keys %expected) {
527     my $value = $hdr->{$name};
528 root 1.1
529 root 1.11 if ($name eq "mcp-name" && $value =~ /\A=\?base64\?([A-Za-z0-9+\/=]*)\?=\z/) {
530 root 1.2 $value = eval { AnyEvent::MCP::utf8dec (MIME::Base64::decode_base64 ($1)) };
531 root 1.1 }
532    
533 root 1.11 unless (defined $value && defined $expected{$name} && $value eq $expected{$name}) {
534 root 1.1 return $self->_rpc_error (sub {
535     $self->_respond ($handle, 400, "Bad Request", JSON::XS::encode_json $_[0]);
536     }, $message->{id}, -32020, "Header mismatch: $name");
537     }
538     }
539     }
540    
541     weaken $self;
542    
543     $self->rpc ($message, sub {
544     my ($response) = @_;
545 root 1.12
546 root 1.1 return unless $self && !$handle->{mcp_closed};
547    
548     my ($status, $reason) = defined $response ? (200, "OK") : (202, "Accepted");
549    
550     if ($modern && defined $response && $response->{error}) {
551     ($status, $reason) = $response->{error}{code} == -32601
552     ? (404, "Not Found") : (400, "Bad Request");
553     }
554    
555     $self->_respond ($handle, $status, $reason, defined $response ? JSON::XS::encode_json $response : "");
556     },
557 root 1.12 peer => $handle->{mcp_peer},
558 root 1.11 protocol_version => $hdr->{"mcp-protocol-version"} || "2025-03-26",
559 root 1.1 );
560     }
561    
562     sub _respond {
563 root 1.15 my ($self, $handle, $status, $reason, $body, $headers) = @_;
564 root 1.1
565     return if $handle->{mcp_closed};
566    
567 root 1.15 $body = "" unless defined $body;
568     my $content_type = "application/json";
569     my $extra = "";
570    
571     if ($headers) {
572     ref $headers eq "HASH" or die "HTTP response headers must be a hash\n";
573     while (my ($name, $value) = each %$headers) {
574     $name =~ /\A[!#\$%&'*+.^_`|~0-9A-Za-z-]+\z/
575     && defined $value && !ref $value && $value !~ /[\r\n\0]/
576     or die "Invalid HTTP response header\n";
577     $name !~ /\A(?:content-length|connection|transfer-encoding)\z/i
578     or die "HTTP response framing headers are managed by the server\n";
579     if (lc $name eq "content-type") {
580     $content_type = $value;
581     } else {
582     $extra .= "$name: $value\r\n";
583     }
584     }
585     }
586    
587 root 1.12 my $close = $handle->{mcp_close} || $status != 200 && $status != 202;
588    
589 root 1.1 $handle->push_write (
590     "HTTP/1.1 $status $reason\r\n"
591 root 1.15 . "Content-Type: $content_type\r\n"
592     . "Content-Length: " . (length $body) . "\r\n"
593     . $extra
594 root 1.1 . (defined $handle->{mcp_origin} ? "Access-Control-Allow-Origin: $handle->{mcp_origin}\r\n"
595     . ($handle->{mcp_origin} ne "*" ? "Vary: Origin\r\n" : "") : "")
596     . ($status == 204 ? "Access-Control-Allow-Methods: POST, OPTIONS\r\n"
597     . "Access-Control-Allow-Headers: content-type,authorization,mcp-protocol-version,mcp-method,mcp-name\r\n"
598     . "Access-Control-Max-Age: 86400\r\n" : "")
599     . ($status == 405 ? "Allow: POST" . (@{$self->{origins}} ? ", OPTIONS" : "") . "\r\n" : "")
600 root 1.6 . ($status == 401 && $self->{tokens} ? "WWW-Authenticate: Bearer\r\n" : "")
601 root 1.12 . ($close ? "Connection: close\r\n" : "")
602 root 1.1 . "\r\n$body"
603     );
604    
605 root 1.12 if ($close) {
606 root 1.1 weaken $self;
607     $handle->on_drain (sub { $self->_close ($_[0]) if $self });
608     } else {
609     $self->_read ($handle);
610     }
611     }
612    
613     sub _close {
614     my ($self, $handle) = @_;
615    
616     return if $handle->{mcp_closed};
617     $handle->{mcp_closed} = 1;
618    
619     my $requests = delete $handle->{mcp_peer}{requests} || {};
620     $_->cancel ("Connection closed") for grep defined, values %$requests;
621    
622     delete $self->{connections}{$handle};
623     $handle->destroy;
624     }
625    
626     sub close {
627     my ($self) = @_;
628    
629     return if $self->{_closed}++;
630     delete $self->{listener};
631     $self->_close ($_) for values %{$self->{connections} || {}};
632     $self->event (close =>);
633     }
634    
635     sub DESTROY {
636     $_[0]->close
637     }
638    
639     1