add files
|
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 |