Newer Older
447 lines | 12.962kb
add files
Yuki Kimoto authored on 2014-03-26
1
package Mojo::UserAgent::Transactor;
2
use Mojo::Base -base;
3

            
4
use File::Basename 'basename';
5
use Mojo::Asset::File;
6
use Mojo::Asset::Memory;
7
use Mojo::Content::MultiPart;
8
use Mojo::Content::Single;
9
use Mojo::JSON;
10
use Mojo::Parameters;
11
use Mojo::Transaction::HTTP;
12
use Mojo::Transaction::WebSocket;
13
use Mojo::URL;
14
use Mojo::Util 'encode';
15

            
16
has generators => sub { {form => \&_form, json => \&_json} };
17
has name => 'Mojolicious (Perl)';
18

            
19
sub add_generator {
20
  my ($self, $name, $cb) = @_;
21
  $self->generators->{$name} = $cb;
22
  return $self;
23
}
24

            
25
sub endpoint {
26
  my ($self, $tx) = @_;
27

            
28
  # Basic endpoint
29
  my $req   = $tx->req;
30
  my $url   = $req->url;
31
  my $proto = $url->protocol || 'http';
32
  my $host  = $url->ihost;
33
  my $port  = $url->port || ($proto eq 'https' ? 443 : 80);
34

            
35
  # Proxy for normal HTTP requests
36
  return $self->_proxy($tx, $proto, $host, $port)
37
    if $proto eq 'http' && lc($req->headers->upgrade // '') ne 'websocket';
38

            
39
  return $proto, $host, $port;
40
}
41

            
42
sub peer {
43
  my ($self, $tx) = @_;
44
  return $self->_proxy($tx, $self->endpoint($tx));
45
}
46

            
47
sub proxy_connect {
48
  my ($self, $old) = @_;
49

            
50
  # Already a CONNECT request
51
  my $req = $old->req;
52
  return undef if uc $req->method eq 'CONNECT';
53

            
54
  # No proxy
55
  return undef unless my $proxy = $req->proxy;
56

            
57
  # WebSocket and/or HTTPS
58
  my $url = $req->url;
59
  my $upgrade = lc($req->headers->upgrade // '');
60
  return undef unless $upgrade eq 'websocket' || $url->protocol eq 'https';
61

            
62
  # CONNECT request
63
  my $new = $self->tx(CONNECT => $url->clone->userinfo(undef));
64
  $new->req->proxy($proxy);
65

            
66
  return $new;
67
}
68

            
69
sub redirect {
70
  my ($self, $old) = @_;
71

            
72
  # Commonly used codes
73
  my $res = $old->res;
74
  my $code = $res->code // '';
75
  return undef unless grep { $_ eq $code } 301, 302, 303, 307, 308;
76

            
77
  # Fix broken location without authority and/or scheme
78
  return unless my $location = $res->headers->location;
79
  $location = Mojo::URL->new($location);
80
  $location = $location->base($old->req->url)->to_abs unless $location->is_abs;
81

            
82
  # Clone request if necessary
83
  my $new    = Mojo::Transaction::HTTP->new;
84
  my $req    = $old->req;
85
  my $method = uc $req->method;
86
  if ($code eq 301 || $code eq 307 || $code eq 308) {
87
    return undef unless my $req = $req->clone;
88
    $new->req($req);
89
    $req->headers->remove('Host')->remove('Cookie')->remove('Referer');
90
  }
91
  elsif ($method ne 'HEAD') { $method = 'GET' }
92
  $new->req->method($method)->url($location);
93
  return $new->previous($old);
94
}
95

            
96
sub tx {
97
  my $self = shift;
98

            
99
  # Method and URL
100
  my $tx  = Mojo::Transaction::HTTP->new;
101
  my $req = $tx->req->method(shift);
102
  my $url = shift;
103
  $url = "http://$url" unless $url =~ m!^/|://!;
104
  ref $url ? $req->url($url) : $req->url->parse($url);
105

            
106
  # Headers (we identify ourselves and accept gzip compression)
107
  my $headers = $req->headers;
108
  $headers->from_hash(shift) if ref $_[0] eq 'HASH';
109
  $headers->user_agent($self->name) unless $headers->user_agent;
110
  $headers->accept_encoding('gzip') unless $headers->accept_encoding;
111

            
112
  # Generator
113
  if (@_ > 1) {
114
    return $tx unless my $generator = $self->generators->{shift()};
115
    $self->$generator($tx, @_);
116
  }
117

            
118
  # Body
119
  elsif (@_) { $req->body(shift) }
120

            
121
  return $tx;
122
}
123

            
124
sub upgrade {
125
  my ($self, $tx) = @_;
126
  my $code = $tx->res->code // '';
127
  return undef unless $tx->req->headers->upgrade && $code eq '101';
128
  my $ws = Mojo::Transaction::WebSocket->new(handshake => $tx, masked => 1);
129
  return $ws->client_challenge ? $ws : undef;
130
}
131

            
132
sub websocket {
133
  my $self = shift;
134

            
135
  # New WebSocket transaction
136
  my $sub = ref $_[-1] eq 'ARRAY' ? pop : [];
137
  my $tx = $self->tx(GET => @_);
138
  my $req = $tx->req;
139
  $req->headers->sec_websocket_protocol(join ', ', @$sub) if @$sub;
140
  my $url   = $req->url;
141
  my $proto = $url->protocol;
142
  $url->scheme($proto eq 'wss' ? 'https' : 'http') if $proto;
143

            
144
  # Handshake
145
  Mojo::Transaction::WebSocket->new(handshake => $tx)->client_handshake;
146

            
147
  return $tx;
148
}
149

            
150
sub _form {
151
  my ($self, $tx, $form, %options) = @_;
152

            
153
  # Check for uploads and force multipart if necessary
154
  my $multipart;
155
  for my $value (map { ref $_ eq 'ARRAY' ? @$_ : $_ } values %$form) {
156
    ++$multipart and last if ref $value eq 'HASH';
157
  }
158
  my $req     = $tx->req;
159
  my $headers = $req->headers;
160
  $headers->content_type('multipart/form-data') if $multipart;
161

            
162
  # Multipart
163
  if (($headers->content_type // '') eq 'multipart/form-data') {
164
    my $parts = $self->_multipart($options{charset}, $form);
165
    $req->content(
166
      Mojo::Content::MultiPart->new(headers => $headers, parts => $parts));
167
    return $tx;
168
  }
169

            
170
  # Query parameters or urlencoded
171
  my $p = Mojo::Parameters->new(map { $_ => $form->{$_} } sort keys %$form);
172
  $p->charset($options{charset}) if defined $options{charset};
173
  my $method = uc $req->method;
174
  if ($method eq 'GET' || $method eq 'HEAD') { $req->url->query->merge($p) }
175
  else {
176
    $req->body($p->to_string);
177
    $headers->content_type('application/x-www-form-urlencoded');
178
  }
179
  return $tx;
180
}
181

            
182
sub _json {
183
  my ($self, $tx, $data) = @_;
184
  $tx->req->body(Mojo::JSON->new->encode($data));
185
  my $headers = $tx->req->headers;
186
  $headers->content_type('application/json') unless $headers->content_type;
187
  return $tx;
188
}
189

            
190
sub _multipart {
191
  my ($self, $charset, $form) = @_;
192

            
193
  my @parts;
194
  for my $name (sort keys %$form) {
195
    my $values = $form->{$name};
196
    for my $value (ref $values eq 'ARRAY' ? @$values : ($values)) {
197
      push @parts, my $part = Mojo::Content::Single->new;
198

            
199
      # Upload
200
      my $filename;
201
      my $headers = $part->headers;
202
      if (ref $value eq 'HASH') {
203

            
204
        # File
205
        if (my $file = delete $value->{file}) {
206
          $file = Mojo::Asset::File->new(path => $file) unless ref $file;
207
          $part->asset($file);
208
          $value->{filename} ||= basename $file->path
209
            if $file->isa('Mojo::Asset::File');
210
        }
211

            
212
        # Memory
213
        elsif (defined(my $content = delete $value->{content})) {
214
          $part->asset(Mojo::Asset::Memory->new->add_chunk($content));
215
        }
216

            
217
        # Filename and headers
218
        $filename = delete $value->{filename} || $name;
219
        $filename = encode $charset, $filename if $charset;
220
        $headers->from_hash($value);
221
      }
222

            
223
      # Field
224
      else {
225
        $value = encode $charset, $value if $charset;
226
        $part->asset(Mojo::Asset::Memory->new->add_chunk($value));
227
      }
228

            
229
      # Content-Disposition
230
      $name = encode $charset, $name if $charset;
231
      my $disposition = qq{form-data; name="$name"};
232
      $disposition .= qq{; filename="$filename"} if $filename;
233
      $headers->content_disposition($disposition);
234
    }
235
  }
236

            
237
  return \@parts;
238
}
239

            
240
sub _proxy {
241
  my ($self, $tx, $proto, $host, $port) = @_;
242

            
243
  # Update with proxy information
244
  if (my $proxy = $tx->req->proxy) {
245
    $proto = $proxy->protocol;
246
    $host  = $proxy->ihost;
247
    $port  = $proxy->port || ($proto eq 'https' ? 443 : 80);
248
  }
249

            
250
  return $proto, $host, $port;
251
}
252

            
253
1;
254

            
255
=encoding utf8
256

            
257
=head1 NAME
258

            
259
Mojo::UserAgent::Transactor - User agent transactor
260

            
261
=head1 SYNOPSIS
262

            
263
  use Mojo::UserAgent::Transactor;
264

            
265
  # Simple GET request
266
  my $t = Mojo::UserAgent::Transactor->new;
267
  say $t->tx(GET => 'http://example.com')->req->to_string;
268

            
269
  # PATCH request with "Do Not Track" header and content
270
  say $t->tx(PATCH => 'example.com' => {DNT => 1} => 'Hi!')->req->to_string;
271

            
272
  # POST request with form-data
273
  say $t->tx(POST => 'example.com' => form => {a => 'b'})->req->to_string;
274

            
275
  # PUT request with JSON data
276
  say $t->tx(PUT => 'example.com' => json => {a => 'b'})->req->to_string;
277

            
278
=head1 DESCRIPTION
279

            
280
L<Mojo::UserAgent::Transactor> is the transaction building and manipulation
281
framework used by L<Mojo::UserAgent>.
282

            
283
=head1 ATTRIBUTES
284

            
285
L<Mojo::UserAgent::Transactor> implements the following attributes.
286

            
287
=head2 generators
288

            
289
  my $generators = $t->generators;
290
  $t             = $t->generators({foo => sub {...}});
291

            
292
Registered content generators, by default only C<form> and C<json> are already
293
defined.
294

            
295
=head2 name
296

            
297
  my $name = $t->name;
298
  $t       = $t->name('Mojolicious');
299

            
300
Value for C<User-Agent> request header of generated transactions, defaults to
301
C<Mojolicious (Perl)>.
302

            
303
=head1 METHODS
304

            
305
L<Mojo::UserAgent::Transactor> inherits all methods from L<Mojo::Base> and
306
implements the following new ones.
307

            
308
=head2 add_generator
309

            
310
  $t = $t->add_generator(foo => sub {...});
311

            
312
Register a new content generator.
313

            
314
=head2 endpoint
315

            
316
  my ($proto, $host, $port) = $t->endpoint(Mojo::Transaction::HTTP->new);
317

            
318
Actual endpoint for transaction.
319

            
320
=head2 peer
321

            
322
  my ($proto, $host, $port) = $t->peer(Mojo::Transaction::HTTP->new);
323

            
324
Actual peer for transaction.
325

            
326
=head2 proxy_connect
327

            
328
  my $tx = $t->proxy_connect(Mojo::Transaction::HTTP->new);
329

            
330
Build L<Mojo::Transaction::HTTP> proxy connect request for transaction if
331
possible.
332

            
333
=head2 redirect
334

            
335
  my $tx = $t->redirect(Mojo::Transaction::HTTP->new);
336

            
337
Build L<Mojo::Transaction::HTTP> followup request for C<301>, C<302>, C<303>,
338
C<307> or C<308> redirect response if possible.
339

            
340
=head2 tx
341

            
342
  my $tx = $t->tx(GET  => 'example.com');
343
  my $tx = $t->tx(POST => 'http://example.com');
344
  my $tx = $t->tx(GET  => 'http://example.com' => {DNT => 1});
345
  my $tx = $t->tx(PUT  => 'http://example.com' => 'Hi!');
346
  my $tx = $t->tx(PUT  => 'http://example.com' => form => {a => 'b'});
347
  my $tx = $t->tx(PUT  => 'http://example.com' => json => {a => 'b'});
348
  my $tx = $t->tx(POST => 'http://example.com' => {DNT => 1} => 'Hi!');
349
  my $tx = $t->tx(
350
    PUT  => 'http://example.com' => {DNT => 1} => form => {a => 'b'});
351
  my $tx = $t->tx(
352
    PUT  => 'http://example.com' => {DNT => 1} => json => {a => 'b'});
353

            
354
Versatile general purpose L<Mojo::Transaction::HTTP> transaction builder for
355
requests, with support for content generators.
356

            
357
  # Generate and inspect custom GET request with DNT header and content
358
  say $t->tx(GET => 'example.com' => {DNT => 1} => 'Bye!')->req->to_string;
359

            
360
  # Use a custom socket for processing this transaction
361
  my $tx = $t->tx(GET => 'http://example.com');
362
  $tx->connection($sock);
363

            
364
  # Stream response content to STDOUT
365
  my $tx = $t->tx(GET => 'http://example.com');
366
  $tx->res->content->unsubscribe('read')->on(read => sub { say $_[1] });
367

            
368
  # PUT request with content streamed from file
369
  my $tx = $t->tx(PUT => 'http://example.com');
370
  $tx->req->content->asset(Mojo::Asset::File->new(path => '/foo.txt'));
371

            
372
  # GET request with query parameters
373
  my $tx = $t->tx(GET => 'http://example.com' => form => {a => 'b'});
374

            
375
  # POST request with "application/json" content
376
  my $tx = $t->tx(
377
    POST => 'http://example.com' => json => {a => 'b', c => [1, 2, 3]});
378

            
379
  # POST request with "application/x-www-form-urlencoded" content
380
  my $tx = $t->tx(
381
    POST => 'http://example.com' => form => {a => 'b', c => 'd'});
382

            
383
  # PUT request with UTF-8 encoded form values
384
  my $tx = $t->tx(
385
    PUT => 'http://example.com' => form => {a => 'b'} => charset => 'UTF-8');
386

            
387
  # POST request with form values sharing the same name
388
  my $tx = $t->tx(POST => 'http://example.com' => form => {a => [qw(b c d)]});
389

            
390
  # POST request with "multipart/form-data" content
391
  my $tx = $t->tx(
392
    POST => 'http://example.com' => form => {mytext => {content => 'lala'}});
393

            
394
  # POST request with upload streamed from file
395
  my $tx = $t->tx(
396
    POST => 'http://example.com' => form => {mytext => {file => '/foo.txt'}});
397

            
398
  # POST request with upload streamed from asset
399
  my $asset = Mojo::Asset::Memory->new->add_chunk('lalala');
400
  my $tx    = $t->tx(
401
    POST => 'http://example.com' => form => {mytext => {file => $asset}});
402

            
403
  # POST request with multiple files sharing the same name
404
  my $tx = $t->tx(POST => 'http://example.com' =>
405
    form => {mytext => [{content => 'first'}, {content => 'second'}]});
406

            
407
  # POST request with form values and customized upload (filename and header)
408
  my $tx = $t->tx(POST => 'http://example.com' => form => {
409
    a      => 'b',
410
    c      => 'd',
411
    mytext => {
412
      content        => 'lalala',
413
      filename       => 'foo.txt',
414
      'Content-Type' => 'text/plain'
415
    }
416
  });
417

            
418
The C<form> content generator will automatically use query parameters for
419
GET/HEAD requests and the "application/x-www-form-urlencoded" content type for
420
everything else. Both get upgraded automatically to using the
421
"multipart/form-data" content type when necessary or when the header has been
422
set manually.
423

            
424
  # Force "multipart/form-data"
425
  my $headers = {'Content-Type' => 'multipart/form-data'};
426
  my $tx = $t->tx(POST => 'example.com' => $headers => form => {a => 'b'});
427

            
428
=head2 upgrade
429

            
430
  my $tx = $t->upgrade(Mojo::Transaction::HTTP->new);
431

            
432
Build L<Mojo::Transaction::WebSocket> followup transaction for WebSocket
433
handshake if possible.
434

            
435
=head2 websocket
436

            
437
  my $tx = $t->websocket('ws://example.com');
438
  my $tx = $t->websocket('ws://example.com' => {DNT => 1} => ['v1.proto']);
439

            
440
Versatile L<Mojo::Transaction::HTTP> transaction builder for WebSocket
441
handshake requests.
442

            
443
=head1 SEE ALSO
444

            
445
L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>.
446

            
447
=cut