Newer Older
440 lines | 10.901kb
add files
Yuki Kimoto authored on 2014-03-26
1
package Mojo::Message::Request;
2
use Mojo::Base 'Mojo::Message';
3

            
4
use Mojo::Cookie::Request;
5
use Mojo::Util qw(b64_encode b64_decode get_line);
6
use Mojo::URL;
7

            
8
has env => sub { {} };
9
has method => 'GET';
10
has url => sub { Mojo::URL->new };
11

            
12
my $START_LINE_RE = qr/
13
  ^
14
  ([a-zA-Z]+)                                            # Method
15
  \s+
16
  ([0-9a-zA-Z!#\$\%&'()*+,\-.\/:;=?\@[\\\]^_`\{|\}~]+)   # URL
17
  (?:\s+HTTP\/(\d\.\d))?                                 # Version
18
  $
19
/x;
20

            
21
sub clone {
22
  my $self = shift;
23

            
24
  # Dynamic requests cannot be cloned
25
  return undef unless my $content = $self->content->clone;
26
  my $clone = $self->new(
27
    content => $content,
28
    method  => $self->method,
29
    url     => $self->url->clone,
30
    version => $self->version
31
  );
32
  $clone->{proxy} = $self->{proxy}->clone if $self->{proxy};
33

            
34
  return $clone;
35
}
36

            
37
sub cookies {
38
  my $self = shift;
39

            
40
  # Parse cookies
41
  my $headers = $self->headers;
42
  return [map { @{Mojo::Cookie::Request->parse($_)} } $headers->cookie]
43
    unless @_;
44

            
45
  # Add cookies
46
  my @cookies = $headers->cookie || ();
47
  for my $cookie (@_) {
48
    $cookie = Mojo::Cookie::Request->new($cookie) if ref $cookie eq 'HASH';
49
    push @cookies, $cookie;
50
  }
51
  $headers->cookie(join('; ', @cookies));
52

            
53
  return $self;
54
}
55

            
56
sub extract_start_line {
57
  my ($self, $bufref) = @_;
58

            
59
  # Ignore any leading empty lines
60
  $$bufref =~ s/^\s+//;
61
  return undef unless defined(my $line = get_line $bufref);
62

            
63
  # We have a (hopefully) full request line
64
  $self->error('Bad request start line', 400) and return undef
65
    unless $line =~ $START_LINE_RE;
66
  my $url = $self->method($1)->version($3)->url;
67
  return !!($1 eq 'CONNECT' ? $url->authority($2) : $url->parse($2));
68
}
69

            
70
sub fix_headers {
71
  my $self = shift;
72
  $self->{fix} ? return $self : $self->SUPER::fix_headers(@_);
73

            
74
  # Basic authentication
75
  my $url     = $self->url;
76
  my $headers = $self->headers;
77
  my $auth    = $url->userinfo;
78
  $headers->authorization('Basic ' . b64_encode($auth, ''))
79
    if $auth && !$headers->authorization;
80

            
81
  # Basic proxy authentication
82
  if (my $proxy = $self->proxy) {
83
    my $proxy_auth = $proxy->userinfo;
84
    $headers->proxy_authorization('Basic ' . b64_encode($proxy_auth, ''))
85
      if $proxy_auth && !$headers->proxy_authorization;
86
  }
87

            
88
  # Host
89
  my $host = $url->ihost;
90
  my $port = $url->port;
91
  $headers->host($port ? "$host:$port" : $host) unless $headers->host;
92

            
93
  return $self;
94
}
95

            
96
sub get_start_line_chunk {
97
  my ($self, $offset) = @_;
98

            
99
  unless (defined $self->{start_buffer}) {
100

            
101
    # Path
102
    my $url   = $self->url;
103
    my $path  = $url->path->to_string;
104
    my $query = $url->query->to_string;
105
    $path .= "?$query" if $query;
106
    $path = "/$path" unless $path =~ m!^/!;
107

            
108
    # CONNECT
109
    my $method = uc $self->method;
110
    if ($method eq 'CONNECT') {
111
      my $port = $url->port || ($url->protocol eq 'https' ? '443' : '80');
112
      $path = $url->host . ":$port";
113
    }
114

            
115
    # Proxy
116
    elsif ($self->proxy) {
117
      my $clone = $url = $url->clone->userinfo(undef);
118
      my $upgrade = lc($self->headers->upgrade // '');
119
      $path = $clone
120
        unless $upgrade eq 'websocket' || $url->protocol eq 'https';
121
    }
122

            
123
    $self->{start_buffer} = "$method $path HTTP/@{[$self->version]}\x0d\x0a";
124
  }
125

            
126
  $self->emit(progress => 'start_line', $offset);
127
  return substr $self->{start_buffer}, $offset, 131072;
128
}
129

            
130
sub is_secure {
131
  my $url = shift->url;
132
  return ($url->protocol || $url->base->protocol) eq 'https';
133
}
134

            
135
sub is_xhr {
136
  (shift->headers->header('X-Requested-With') // '') =~ /XMLHttpRequest/i;
137
}
138

            
139
sub param { shift->params->param(@_) }
140

            
141
sub params {
142
  my $self = shift;
143
  return $self->{params}
144
    ||= $self->body_params->clone->merge($self->query_params);
145
}
146

            
147
sub parse {
148
  my $self = shift;
149

            
150
  # Parse CGI environment
151
  my $env = @_ > 1 ? {@_} : ref $_[0] eq 'HASH' ? $_[0] : undef;
152
  $self->env($env)->_parse_env($env) if $env;
153

            
154
  # Parse normal message
155
  my @args = $env ? () : @_;
156
  if (($self->{state} // '') ne 'cgi') { $self->SUPER::parse(@args) }
157

            
158
  # Parse CGI content
159
  else { $self->content($self->content->parse_body(@args))->SUPER::parse }
160

            
161
  # Check if we can fix things that require all headers
162
  return $self unless $self->is_finished;
163

            
164
  # Base URL
165
  my $base = $self->url->base;
166
  $base->scheme('http') unless $base->scheme;
167
  my $headers = $self->headers;
168
  if (!$base->host && (my $host = $headers->host)) { $base->authority($host) }
169

            
170
  # Basic authentication
171
  my $auth = _parse_basic_auth($headers->authorization);
172
  $base->userinfo($auth) if $auth;
173

            
174
  # Basic proxy authentication
175
  my $proxy_auth = _parse_basic_auth($headers->proxy_authorization);
176
  $self->proxy(Mojo::URL->new->userinfo($proxy_auth)) if $proxy_auth;
177

            
178
  # "X-Forwarded-HTTPS"
179
  $base->scheme('https')
180
    if $ENV{MOJO_REVERSE_PROXY} && $headers->header('X-Forwarded-HTTPS');
181

            
182
  return $self;
183
}
184

            
185
sub proxy {
186
  my $self = shift;
187
  return $self->{proxy} unless @_;
188
  $self->{proxy} = !$_[0] || ref $_[0] ? shift : Mojo::URL->new(shift);
189
  return $self;
190
}
191

            
192
sub query_params { shift->url->query }
193

            
194
sub _parse_basic_auth {
195
  return undef unless my $header = shift;
196
  return $header =~ /Basic (.+)$/ ? b64_decode($1) : undef;
197
}
198

            
199
sub _parse_env {
200
  my ($self, $env) = @_;
201

            
202
  # Extract headers
203
  my $headers = $self->headers;
204
  my $url     = $self->url;
205
  my $base    = $url->base;
206
  while (my ($name, $value) = each %$env) {
207
    next unless $name =~ s/^HTTP_//i;
208
    $name =~ s/_/-/g;
209
    $headers->header($name => $value);
210

            
211
    # Host/Port
212
    if ($name eq 'HOST') {
213
      my ($host, $port) = ($value, undef);
214
      ($host, $port) = ($1, $2) if $host =~ /^([^:]*):?(.*)$/;
215
      $base->host($host)->port($port);
216
    }
217
  }
218

            
219
  # Content-Type is a special case on some servers
220
  $headers->content_type($env->{CONTENT_TYPE}) if $env->{CONTENT_TYPE};
221

            
222
  # Content-Length is a special case on some servers
223
  $headers->content_length($env->{CONTENT_LENGTH}) if $env->{CONTENT_LENGTH};
224

            
225
  # Query
226
  $url->query->parse($env->{QUERY_STRING}) if $env->{QUERY_STRING};
227

            
228
  # Method
229
  $self->method($env->{REQUEST_METHOD}) if $env->{REQUEST_METHOD};
230

            
231
  # Scheme/Version
232
  if (($env->{SERVER_PROTOCOL} // '') =~ m!^([^/]+)/([^/]+)$!) {
233
    $base->scheme($1);
234
    $self->version($2);
235
  }
236

            
237
  # HTTPS
238
  $base->scheme('https') if $env->{HTTPS};
239

            
240
  # Path
241
  my $path = $url->path->parse($env->{PATH_INFO} ? $env->{PATH_INFO} : '');
242

            
243
  # Base path
244
  if (my $value = $env->{SCRIPT_NAME}) {
245

            
246
    # Make sure there is a trailing slash (important for merging)
247
    $base->path->parse($value =~ m!/$! ? $value : "$value/");
248

            
249
    # Remove SCRIPT_NAME prefix if necessary
250
    my $buffer = $path->to_string;
251
    $value =~ s!^/|/$!!g;
252
    $buffer =~ s!^/?\Q$value\E/?!!;
253
    $buffer =~ s!^/!!;
254
    $path->parse($buffer);
255
  }
256

            
257
  # Bypass normal message parser
258
  $self->{state} = 'cgi';
259
}
260

            
261
1;
262

            
263
=encoding utf8
264

            
265
=head1 NAME
266

            
267
Mojo::Message::Request - HTTP request
268

            
269
=head1 SYNOPSIS
270

            
271
  use Mojo::Message::Request;
272

            
273
  # Parse
274
  my $req = Mojo::Message::Request->new;
275
  $req->parse("GET /foo HTTP/1.0\x0d\x0a");
276
  $req->parse("Content-Length: 12\x0d\x0a");
277
  $req->parse("Content-Type: text/plain\x0d\x0a\x0d\x0a");
278
  $req->parse('Hello World!');
279
  say $req->method;
280
  say $req->headers->content_type;
281
  say $req->body;
282

            
283
  # Build
284
  my $req = Mojo::Message::Request->new;
285
  $req->url->parse('http://127.0.0.1/foo/bar');
286
  $req->method('GET');
287
  say $req->to_string;
288

            
289
=head1 DESCRIPTION
290

            
291
L<Mojo::Message::Request> is a container for HTTP requests as described in RFC
292
2616 and RFC 2817.
293

            
294
=head1 EVENTS
295

            
296
L<Mojo::Message::Request> inherits all events from L<Mojo::Message>.
297

            
298
=head1 ATTRIBUTES
299

            
300
L<Mojo::Message::Request> inherits all attributes from L<Mojo::Message> and
301
implements the following new ones.
302

            
303
=head2 env
304

            
305
  my $env = $req->env;
306
  $req    = $req->env({});
307

            
308
Direct access to the C<CGI> or C<PSGI> environment hash if available.
309

            
310
  # Check CGI version
311
  my $version = $req->env->{GATEWAY_INTERFACE};
312

            
313
  # Check PSGI version
314
  my $version = $req->env->{'psgi.version'};
315

            
316
=head2 method
317

            
318
  my $method = $req->method;
319
  $req       = $req->method('POST');
320

            
321
HTTP request method, defaults to C<GET>.
322

            
323
=head2 url
324

            
325
  my $url = $req->url;
326
  $req    = $req->url(Mojo::URL->new);
327

            
328
HTTP request URL, defaults to a L<Mojo::URL> object.
329

            
330
  # Get request information
331
  say $req->url->to_abs->userinfo;
332
  say $req->url->to_abs->host;
333
  say $req->url->to_abs->path;
334

            
335
=head1 METHODS
336

            
337
L<Mojo::Message::Request> inherits all methods from L<Mojo::Message> and
338
implements the following new ones.
339

            
340
=head2 clone
341

            
342
  my $clone = $req->clone;
343

            
344
Clone request if possible, otherwise return C<undef>.
345

            
346
=head2 cookies
347

            
348
  my $cookies = $req->cookies;
349
  $req        = $req->cookies(Mojo::Cookie::Request->new);
350
  $req        = $req->cookies({name => 'foo', value => 'bar'});
351

            
352
Access request cookies, usually L<Mojo::Cookie::Request> objects.
353

            
354
=head2 extract_start_line
355

            
356
  my $bool = $req->extract_start_line(\$str);
357

            
358
Extract request line from string.
359

            
360
=head2 fix_headers
361

            
362
  $req = $req->fix_headers;
363

            
364
Make sure request has all required headers.
365

            
366
=head2 get_start_line_chunk
367

            
368
  my $bytes = $req->get_start_line_chunk($offset);
369

            
370
Get a chunk of request line data starting from a specific position.
371

            
372
=head2 is_secure
373

            
374
  my $bool = $req->is_secure;
375

            
376
Check if connection is secure.
377

            
378
=head2 is_xhr
379

            
380
  my $bool = $req->is_xhr;
381

            
382
Check C<X-Requested-With> header for C<XMLHttpRequest> value.
383

            
384
=head2 param
385

            
386
  my @names = $req->param;
387
  my $foo   = $req->param('foo');
388
  my @foo   = $req->param('foo');
389

            
390
Access GET and POST parameters. Note that this method caches all data, so it
391
should not be called before the entire request body has been received. Parts
392
of the request body need to be loaded into memory to parse POST parameters, so
393
you have to make sure it is not excessively large.
394

            
395
=head2 params
396

            
397
  my $params = $req->params;
398

            
399
All GET and POST parameters, usually a L<Mojo::Parameters> object. Note that
400
this method caches all data, so it should not be called before the entire
401
request body has been received. Parts of the request body need to be loaded
402
into memory to parse POST parameters, so you have to make sure it is not
403
excessively large.
404

            
405
  # Get parameter value
406
  say $req->params->param('foo');
407

            
408
=head2 parse
409

            
410
  $req = $req->parse('GET /foo/bar HTTP/1.1');
411
  $req = $req->parse(REQUEST_METHOD => 'GET');
412
  $req = $req->parse({REQUEST_METHOD => 'GET'});
413

            
414
Parse HTTP request chunks or environment hash.
415

            
416
=head2 proxy
417

            
418
  my $proxy = $req->proxy;
419
  $req      = $req->proxy('http://foo:bar@127.0.0.1:3000');
420
  $req      = $req->proxy(Mojo::URL->new('http://127.0.0.1:3000'));
421

            
422
Proxy URL for request.
423

            
424
  # Disable proxy
425
  $req->proxy(0);
426

            
427
=head2 query_params
428

            
429
  my $params = $req->query_params;
430

            
431
All GET parameters, usually a L<Mojo::Parameters> object.
432

            
433
  # Turn GET parameters to hash and extract value
434
  say $req->query_params->to_hash->{foo};
435

            
436
=head1 SEE ALSO
437

            
438
L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>.
439

            
440
=cut