Initial Commit

This commit is contained in:
Riley Schneider
2025-12-03 16:38:10 +01:00
parent c5e26bf594
commit b732d8d4b5
17680 changed files with 5977495 additions and 2 deletions

View File

@@ -0,0 +1,446 @@
package Mojo::Message::Request;
use Mojo::Base 'Mojo::Message';
use Digest::SHA qw(sha1_base64);
use Mojo::Cookie::Request;
use Mojo::Util qw(b64_encode b64_decode sha1_sum);
use Mojo::URL;
has env => sub { {} };
has method => 'GET';
has [qw(proxy reverse_proxy)];
has request_id => sub {
state $seed = $$ . time . rand;
state $counter = int rand 0xffffff;
my $b64 = substr(sha1_base64($seed . ($counter = ($counter + 1) % 0xffffff)), 0, 8);
$b64 =~ tr!+/!-_!;
return $b64;
};
has url => sub { Mojo::URL->new };
has via_proxy => 1;
sub clone {
my $self = shift;
# Dynamic requests cannot be cloned
return undef unless my $content = $self->content->clone;
my $clone
= $self->new(content => $content, method => $self->method, url => $self->url->clone, version => $self->version);
$clone->{proxy} = $self->{proxy}->clone if $self->{proxy};
return $clone;
}
sub cookies {
my $self = shift;
# Parse cookies
my $headers = $self->headers;
return [map { @{Mojo::Cookie::Request->parse($_)} } $headers->cookie] unless @_;
# Add cookies
my @cookies = map { ref $_ eq 'HASH' ? Mojo::Cookie::Request->new($_) : $_ } $headers->cookie || (), @_;
$headers->cookie(join '; ', @cookies);
return $self;
}
sub every_param { shift->params->every_param(@_) }
sub extract_start_line {
my ($self, $bufref) = @_;
# Ignore any leading empty lines
return undef unless $$bufref =~ s/^\s*(.*?)\x0d?\x0a//;
# We have a (hopefully) full request-line
return !$self->error({message => 'Bad request start-line'}) unless $1 =~ /^(\S+)\s+(\S+)\s+HTTP\/(\d\.\d)$/;
my $url = $self->method($1)->version($3)->url;
my $target = $2;
return !!$url->host_port($target) if $1 eq 'CONNECT';
return !!$url->parse($target)->fragment(undef) if $target =~ /^[^:\/?#]+:/;
return !!$url->path_query($target);
}
sub fix_headers {
my $self = shift;
$self->{fix} ? return $self : $self->SUPER::fix_headers(@_);
# Host
my $url = $self->url;
my $headers = $self->headers;
$headers->host($url->host_port) unless $headers->host;
# Basic authentication
if ((my $info = $url->userinfo) && !$headers->authorization) {
$headers->authorization('Basic ' . b64_encode($info, ''));
}
# Basic proxy authentication
return $self unless (my $proxy = $self->proxy) && $self->via_proxy;
return $self unless my $info = $proxy->userinfo;
$headers->proxy_authorization('Basic ' . b64_encode($info, '')) unless $headers->proxy_authorization;
return $self;
}
sub get_start_line_chunk {
my ($self, $offset) = @_;
$self->_start_line->emit(progress => 'start_line', $offset);
return substr $self->{start_buffer}, $offset, 131072;
}
sub is_handshake { lc($_[0]->headers->upgrade // '') eq 'websocket' }
sub is_secure {
my $url = shift->url;
return ($url->protocol || $url->base->protocol) eq 'https';
}
sub is_xhr { (shift->headers->header('X-Requested-With') // '') =~ /XMLHttpRequest/i }
sub param { shift->params->param(@_) }
sub params { $_[0]->{params} ||= $_[0]->body_params->clone->append($_[0]->query_params) }
sub parse {
my ($self, $env, $chunk) = (shift, ref $_[0] ? (shift, '') : (undef, shift));
# Parse CGI environment
$self->env($env)->_parse_env($env) if $env;
# Parse normal message
if (($self->{state} // '') ne 'cgi') { $self->SUPER::parse($chunk) }
# Parse CGI content
else { $self->content($self->content->parse_body($chunk))->SUPER::parse('') }
# Check if we can fix things that require all headers
return $self unless $self->is_finished;
# Base URL
my $base = $self->url->base;
$base->scheme('http') unless $base->scheme;
my $headers = $self->headers;
if (!$base->host && (my $host = $headers->host)) { $base->host_port($host) }
# Basic authentication
if (my $basic = _basic($headers->authorization)) { $base->userinfo($basic) }
# Basic proxy authentication
my $basic = _basic($headers->proxy_authorization);
$self->proxy(Mojo::URL->new->userinfo($basic)) if $basic;
# "X-Forwarded-Proto"
$base->scheme('https') if $self->reverse_proxy && ($headers->header('X-Forwarded-Proto') // '') eq 'https';
return $self;
}
sub query_params { shift->url->query }
sub start_line_size { length shift->_start_line->{start_buffer} }
sub _basic { $_[0] && $_[0] =~ /Basic (.+)$/ ? b64_decode $1 : undef }
sub _parse_env {
my ($self, $env) = @_;
# Bypass normal message parser
$self->{state} = 'cgi';
# Extract headers
my $headers = $self->headers;
my $url = $self->url;
my $base = $url->base;
for my $name (keys %$env) {
my $value = $env->{$name};
next unless $name =~ s/^HTTP_//i;
$name =~ y/_/-/;
$headers->header($name => $value);
# Host/Port
$value =~ s/:(\d+)$// ? $base->host($value)->port($1) : $base->host($value) if $name eq 'HOST';
}
# Content-Type is a special case on some servers
$headers->content_type($env->{CONTENT_TYPE}) if $env->{CONTENT_TYPE};
# Content-Length is a special case on some servers
$headers->content_length($env->{CONTENT_LENGTH}) if $env->{CONTENT_LENGTH};
# Query
$url->query->parse($env->{QUERY_STRING}) if $env->{QUERY_STRING};
# Method
$self->method($env->{REQUEST_METHOD}) if $env->{REQUEST_METHOD};
# Scheme/Version
$base->scheme($1) and $self->version($2) if ($env->{SERVER_PROTOCOL} // '') =~ m!^([^/]+)/([^/]+)$!;
# HTTPS
$base->scheme('https') if uc($env->{HTTPS} // '') eq 'ON';
# Path
my $path = $url->path->parse($env->{PATH_INFO} ? $env->{PATH_INFO} : '');
# Base path
if (my $value = $env->{SCRIPT_NAME}) {
# Make sure there is a trailing slash (important for merging)
$base->path->parse($value =~ m!/$! ? $value : "$value/");
# Remove SCRIPT_NAME prefix if necessary
my $buffer = $path->to_string;
$value =~ s!^/|/$!!g;
$buffer =~ s!^/?\Q$value\E/?!!;
$buffer =~ s!^/!!;
$path->parse($buffer);
}
}
sub _start_line {
my $self = shift;
return $self if defined $self->{start_buffer};
# Path
my $url = $self->url;
my $path = $url->path_query;
$path = "/$path" unless $path =~ m!^/!;
# CONNECT
my $method = uc $self->method;
if ($method eq 'CONNECT') {
my $port = $url->port // ($url->protocol eq 'https' ? '443' : '80');
$path = $url->ihost . ":$port";
}
# Proxy
elsif ($self->proxy && $self->via_proxy && $url->protocol ne 'https') {
$path = $url->clone->userinfo(undef) unless $self->is_handshake;
}
$self->{start_buffer} = "$method $path HTTP/@{[$self->version]}\x0d\x0a";
return $self;
}
1;
=encoding utf8
=head1 NAME
Mojo::Message::Request - HTTP request
=head1 SYNOPSIS
use Mojo::Message::Request;
# Parse
my $req = Mojo::Message::Request->new;
$req->parse("GET /foo HTTP/1.0\x0d\x0a");
$req->parse("Content-Length: 12\x0d\x0a");
$req->parse("Content-Type: text/plain\x0d\x0a\x0d\x0a");
$req->parse('Hello World!');
say $req->method;
say $req->headers->content_type;
say $req->body;
# Build
my $req = Mojo::Message::Request->new;
$req->url->parse('http://127.0.0.1/foo/bar');
$req->method('GET');
say $req->to_string;
=head1 DESCRIPTION
L<Mojo::Message::Request> is a container for HTTP requests, based on L<RFC 7230|https://tools.ietf.org/html/rfc7230>,
L<RFC 7231|https://tools.ietf.org/html/rfc7231>, L<RFC 7235|https://tools.ietf.org/html/rfc7235> and L<RFC
2817|https://tools.ietf.org/html/rfc2817>.
=head1 EVENTS
L<Mojo::Message::Request> inherits all events from L<Mojo::Message>.
=head1 ATTRIBUTES
L<Mojo::Message::Request> inherits all attributes from L<Mojo::Message> and implements the following new ones.
=head2 env
my $env = $req->env;
$req = $req->env({PATH_INFO => '/'});
Direct access to the C<CGI> or C<PSGI> environment hash if available.
# Check CGI version
my $version = $req->env->{GATEWAY_INTERFACE};
# Check PSGI version
my $version = $req->env->{'psgi.version'};
=head2 method
my $method = $req->method;
$req = $req->method('POST');
HTTP request method, defaults to C<GET>.
=head2 proxy
my $url = $req->proxy;
$req = $req->proxy(Mojo::URL->new('http://127.0.0.1:3000'));
Proxy URL for request.
=head2 reverse_proxy
my $bool = $req->reverse_proxy;
$req = $req->reverse_proxy($bool);
Request has been performed through a reverse proxy.
=head2 request_id
my $id = $req->request_id;
$req = $req->request_id('aee7d5d8');
Request ID, defaults to a reasonably unique value.
=head2 url
my $url = $req->url;
$req = $req->url(Mojo::URL->new);
HTTP request URL, defaults to a L<Mojo::URL> object.
# Get request information
my $info = $req->url->to_abs->userinfo;
my $host = $req->url->to_abs->host;
my $path = $req->url->to_abs->path;
=head2 via_proxy
my $bool = $req->via_proxy;
$req = $req->via_proxy($bool);
Request can be performed through a proxy server.
=head1 METHODS
L<Mojo::Message::Request> inherits all methods from L<Mojo::Message> and implements the following new ones.
=head2 clone
my $clone = $req->clone;
Return a new L<Mojo::Message::Request> object cloned from this request if possible, otherwise return C<undef>.
=head2 cookies
my $cookies = $req->cookies;
$req = $req->cookies(Mojo::Cookie::Request->new);
$req = $req->cookies({name => 'foo', value => 'bar'});
Access request cookies, usually L<Mojo::Cookie::Request> objects.
# Names of all cookies
say $_->name for @{$req->cookies};
=head2 every_param
my $values = $req->every_param('foo');
Similar to L</"param">, but returns all values sharing the same name as an array reference.
# Get first value
say $req->every_param('foo')->[0];
=head2 extract_start_line
my $bool = $req->extract_start_line(\$str);
Extract request-line from string.
=head2 fix_headers
$req = $req->fix_headers;
Make sure request has all required headers.
=head2 get_start_line_chunk
my $bytes = $req->get_start_line_chunk($offset);
Get a chunk of request-line data starting from a specific position. Note that this method finalizes the request.
=head2 is_handshake
my $bool = $req->is_handshake;
Check C<Upgrade> header for C<websocket> value.
=head2 is_secure
my $bool = $req->is_secure;
Check if connection is secure.
=head2 is_xhr
my $bool = $req->is_xhr;
Check C<X-Requested-With> header for C<XMLHttpRequest> value.
=head2 param
my $value = $req->param('foo');
Access C<GET> and C<POST> parameters extracted from the query string and C<application/x-www-form-urlencoded> or
C<multipart/form-data> message body. If there are multiple values sharing the same name, and you want to access more
than just the last one, you can use L</"every_param">. Note that this method caches all data, so it should not be
called before the entire request body has been received. Parts of the request body need to be loaded into memory to
parse C<POST> parameters, so you have to make sure it is not excessively large. There's a 16MiB limit for requests by
default.
=head2 params
my $params = $req->params;
All C<GET> and C<POST> parameters extracted from the query string and C<application/x-www-form-urlencoded> or
C<multipart/form-data> message body, usually a L<Mojo::Parameters> object. Note that this method caches all data, so it
should not be called before the entire request body has been received. Parts of the request body need to be loaded into
memory to parse C<POST> parameters, so you have to make sure it is not excessively large. There's a 16MiB limit for
requests by default.
# Get parameter names and values
my $hash = $req->params->to_hash;
=head2 parse
$req = $req->parse('GET /foo/bar HTTP/1.1');
$req = $req->parse({PATH_INFO => '/'});
Parse HTTP request chunks or environment hash.
=head2 query_params
my $params = $req->query_params;
All C<GET> parameters, usually a L<Mojo::Parameters> object.
# Turn GET parameters to hash and extract value
say $req->query_params->to_hash->{foo};
=head2 start_line_size
my $size = $req->start_line_size;
Size of the request-line in bytes. Note that this method finalizes the request.
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
=cut

View File

@@ -0,0 +1,315 @@
package Mojo::Message::Response;
use Mojo::Base 'Mojo::Message';
use Mojo::Cookie::Response;
use Mojo::Date;
has [qw(code message)];
has max_message_size => sub { $ENV{MOJO_MAX_MESSAGE_SIZE} // 2147483648 };
# Unmarked codes are from RFC 7231
my %MESSAGES = (
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing', # RFC 2518 (WebDAV)
103 => 'Early Hints', # RFC 8297
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status', # RFC 2518 (WebDAV)
208 => 'Already Reported', # RFC 5842
226 => 'IM Used', # RFC 3229
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect', # RFC 7538
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Request Range Not Satisfiable',
417 => 'Expectation Failed',
418 => "I'm a teapot", # RFC 2324 :)
421 => 'Misdirected Request', # RFC 7540
422 => 'Unprocessable Entity', # RFC 2518 (WebDAV)
423 => 'Locked', # RFC 2518 (WebDAV)
424 => 'Failed Dependency', # RFC 2518 (WebDAV)
425 => 'Too Early', # RFC 8470
426 => 'Upgrade Required', # RFC 2817
428 => 'Precondition Required', # RFC 6585
429 => 'Too Many Requests', # RFC 6585
431 => 'Request Header Fields Too Large', # RFC 6585
451 => 'Unavailable For Legal Reasons', # RFC 7725
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates', # RFC 2295
507 => 'Insufficient Storage', # RFC 2518 (WebDAV)
508 => 'Loop Detected', # RFC 5842
509 => 'Bandwidth Limit Exceeded', # Unofficial
510 => 'Not Extended', # RFC 2774
511 => 'Network Authentication Required' # RFC 6585
);
sub cookies {
my $self = shift;
# Parse cookies
my $headers = $self->headers;
return [@{Mojo::Cookie::Response->parse($headers->set_cookie)}] unless @_;
# Add cookies
$headers->add('Set-Cookie' => "$_") for map { ref $_ eq 'HASH' ? Mojo::Cookie::Response->new($_) : $_ } @_;
return $self;
}
sub default_message { $MESSAGES{$_[1] || $_[0]->code // 404} || '' }
sub extract_start_line {
my ($self, $bufref) = @_;
# We have a full response line
return undef unless $$bufref =~ s/^(.*?)\x0d?\x0a//;
return !$self->error({message => 'Bad response start-line'}) unless $1 =~ m!^\s*HTTP/(\d\.\d)\s+(\d\d\d)\s*(.+)?$!;
my $content = $self->content;
$content->skip_body(1) if $self->code($2)->is_empty;
defined $content->$_ or $content->$_(1) for qw(auto_decompress auto_relax);
return !!$self->version($1)->message($3);
}
sub fix_headers {
my $self = shift;
$self->{fix} ? return $self : $self->SUPER::fix_headers(@_);
# Date
my $headers = $self->headers;
$headers->date(Mojo::Date->new->to_string) unless $headers->date;
# RFC 7230 3.3.2
$headers->remove('Content-Length') if $self->is_empty;
return $self;
}
sub get_start_line_chunk {
my ($self, $offset) = @_;
$self->_start_line->emit(progress => 'start_line', $offset);
return substr $self->{start_buffer}, $offset, 131072;
}
sub is_client_error { shift->_status_class(400) }
sub is_empty {
my $self = shift;
return undef unless my $code = $self->code;
return $self->is_info || $code == 204 || $code == 304;
}
sub is_error { shift->_status_class(400, 500) }
sub is_info { shift->_status_class(100) }
sub is_redirect { shift->_status_class(300) }
sub is_server_error { shift->_status_class(500) }
sub is_success { shift->_status_class(200) }
sub start_line_size { length shift->_start_line->{start_buffer} }
sub _start_line {
my $self = shift;
return $self if defined $self->{start_buffer};
my $code = $self->code || 404;
my $msg = $self->message || $self->default_message;
$self->{start_buffer} = "HTTP/@{[$self->version]} $code $msg\x0d\x0a";
return $self;
}
sub _status_class {
my ($self, @classes) = @_;
return undef unless my $code = $self->code;
return !!grep { $code >= $_ && $code < ($_ + 100) } @classes;
}
1;
=encoding utf8
=head1 NAME
Mojo::Message::Response - HTTP response
=head1 SYNOPSIS
use Mojo::Message::Response;
# Parse
my $res = Mojo::Message::Response->new;
$res->parse("HTTP/1.0 200 OK\x0d\x0a");
$res->parse("Content-Length: 12\x0d\x0a");
$res->parse("Content-Type: text/plain\x0d\x0a\x0d\x0a");
$res->parse('Hello World!');
say $res->code;
say $res->headers->content_type;
say $res->body;
# Build
my $res = Mojo::Message::Response->new;
$res->code(200);
$res->headers->content_type('text/plain');
$res->body('Hello World!');
say $res->to_string;
=head1 DESCRIPTION
L<Mojo::Message::Response> is a container for HTTP responses, based on L<RFC 7230|https://tools.ietf.org/html/rfc7230>
and L<RFC 7231|https://tools.ietf.org/html/rfc7231>.
=head1 EVENTS
L<Mojo::Message::Response> inherits all events from L<Mojo::Message>.
=head1 ATTRIBUTES
L<Mojo::Message::Response> inherits all attributes from L<Mojo::Message> and implements the following new ones.
=head2 code
my $code = $res->code;
$res = $res->code(200);
HTTP response status code.
=head2 max_message_size
my $size = $res->max_message_size;
$res = $res->max_message_size(1024);
Maximum message size in bytes, defaults to the value of the C<MOJO_MAX_MESSAGE_SIZE> environment variable or
C<2147483648> (2GiB). Setting the value to C<0> will allow messages of indefinite size.
=head2 message
my $msg = $res->message;
$res = $res->message('OK');
HTTP response status message.
=head1 METHODS
L<Mojo::Message::Response> inherits all methods from L<Mojo::Message> and implements the following new ones.
=head2 cookies
my $cookies = $res->cookies;
$res = $res->cookies(Mojo::Cookie::Response->new);
$res = $res->cookies({name => 'foo', value => 'bar'});
Access response cookies, usually L<Mojo::Cookie::Response> objects.
# Names of all cookies
say $_->name for @{$res->cookies};
=head2 default_message
my $msg = $res->default_message;
my $msg = $res->default_message(418);
Generate default response message for status code, defaults to using L</"code">.
=head2 extract_start_line
my $bool = $res->extract_start_line(\$str);
Extract status-line from string.
=head2 fix_headers
$res = $res->fix_headers;
Make sure response has all required headers.
=head2 get_start_line_chunk
my $bytes = $res->get_start_line_chunk($offset);
Get a chunk of status-line data starting from a specific position. Note that this method finalizes the response.
=head2 is_client_error
my $bool = $res->is_client_error;
Check if this response has a C<4xx> status L</"code">.
=head2 is_empty
my $bool = $res->is_empty;
Check if this response has a C<1xx>, C<204> or C<304> status L</"code">.
=head2 is_error
my $bool = $res->is_error;
Check if this response has a C<4xx> or C<5xx> status L</"code">.
=head2 is_info
my $bool = $res->is_info;
Check if this response has a C<1xx> status L</"code">.
=head2 is_redirect
my $bool = $res->is_redirect;
Check if this response has a C<3xx> status L</"code">.
=head2 is_server_error
my $bool = $res->is_server_error;
Check if this response has a C<5xx> status L</"code">.
=head2 is_success
my $bool = $res->is_success;
Check if this response has a C<2xx> status L</"code">.
=head2 start_line_size
my $size = $req->start_line_size;
Size of the status-line in bytes. Note that this method finalizes the response.
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
=cut