File Coverage

blib/lib/Protocol/UWSGI.pm
Criterion Covered Total %
statement 54 77 70.1
branch 8 30 26.6
condition 8 15 53.3
subroutine 9 12 75.0
pod 5 6 83.3
total 84 140 60.0


line stmt bran cond sub pod time code
1             package Protocol::UWSGI;
2             # ABSTRACT: support for the UWSGI protocol
3 1     1   27856 use strict;
  1         3  
  1         35  
4 1     1   4 use warnings;
  1         3  
  1         28  
5              
6 1     1   885 use parent qw(Exporter);
  1         314  
  1         6  
7              
8             our $VERSION = '1.000';
9              
10             =head1 NAME
11              
12             Protocol::UWSGI - handle the UWSGI wire protocol
13              
14             =head1 VERSION
15              
16             Version 1.000
17              
18             =head1 SYNOPSIS
19              
20             use strict;
21             use warnings;
22             use Protocol::UWSGI qw(:all);
23             # Encode...
24             my $req = build_request(
25             uri => 'http://localhost',
26             method => 'GET',
27             remote => '1.2.3.4:1234',
28             );
29             # ... and decode again
30             warn "URI was " . uri_from_env(
31             extract_frame(\$req)
32             );
33              
34             =head1 DESCRIPTION
35              
36             Provides protocol-level support for UWSGI packet generation/decoding, as
37             defined by L.
38             Currently expects to deal with PSGI data (modifier 1 == 5), although this
39             may be extended later if there's any demand for the other packet types.
40              
41             This is unlikely to be useful in an application - it's intended to provide
42             support for dealing with the protocol in an existing framework: it deals
43             with the abstract protocol only, and has no network transport handling at
44             all. Try L for an implementation that actually does
45             something useful.
46              
47             Typically you'd create a UNIX socket and listen for requests, passing
48             any data to the L function and handling the resulting
49             data if that function returns something other than undef:
50              
51             # Detect read - first packet is usually the UWSGI header, everything
52             # after that would be the HTTP request body if there is one:
53             sub on_read {
54             my ($self, $buffref) = @_;
55             while(my $pkt = extract_frame($buffref)) {
56             $self->handle_uwsgi($pkt);
57             }
58             }
59              
60             # and probably an EOF handler to detect client hangup
61             # sub on_eof { ... }
62              
63             =head1 IMPLEMENTATION - Server
64              
65             A server implementation typically accepts requests from a reverse
66             proxy, such as nginx, and returns HTTP responses.
67              
68             Import the :server tag to get L, L
69             and in future maybe L functions:
70              
71             use Protocol::UWSGI qw(:server);
72              
73             =head1 IMPLEMENTATION - Client
74              
75             A client implementation typically accepts HTTP requests and converts
76             them to UWSGI for passing to a UWSGI-capable application.
77              
78             Import the :client tag to get L:
79              
80             use Protocol::UWSGI qw(:client);
81              
82             =cut
83              
84 1     1   1005 use Encode ();
  1         13151  
  1         24  
85 1     1   915 use URI;
  1         8053  
  1         39  
86              
87             use constant {
88 1         1166 PSGI_MODIFIER1 => 5,
89             PSGI_MODIFIER2 => 0,
90 1     1   8 };
  1         2  
91              
92             our @EXPORT_OK = qw(
93             extract_frame
94             uri_from_env
95              
96             build_request
97              
98             PSGI_MODIFIER1
99             PSGI_MODIFIER2
100             );
101             our %EXPORT_TAGS = (
102             'server' => [qw(extract_frame uri_from_env)],
103             'client' => [qw(build_request)],
104             'all' => \@EXPORT_OK
105             );
106              
107             =head1 FUNCTIONS
108              
109             If you're handling incoming UWSGI requests, you'll need to instantiate
110             via L then decode the request using L.
111              
112             If you're making UWSGI requests against an external UWSGI server,
113             that'll be L.
114              
115             Just want to decode captured traffic? L again.
116              
117             =cut
118              
119             =head2 extract_frame
120              
121             Attempts to extract a single UWSGI packet from the given buffer (which
122             should be passed as a scalar ref, e.g.
123              
124             my $buffref = \"...";
125             my $req = Protocol::UWSGI->extract_frame($buffref)
126             or die "could not find UWSGI frame";
127              
128             If we had enough data for a packet, that packet will be removed from
129             the buffer and returned. There may be additional packet data that
130             can be extracted, or non-UWSGI data such as HTTP request body.
131              
132             If this returns undef, there's not enough data to process - in this case,
133             the buffer is guaranteed not to be modified.
134              
135             This may be called as a class method or an instance method.
136             The instance state will remain unchanged after calling this method.
137              
138             Note that there is no constructor provided in this
139             class - if you want to call this as an instance method,
140             you'll need to bless manually or be applying this as
141             a role/mixin.
142              
143             =cut
144              
145             sub extract_frame {
146 4     4 1 7 my ($buffref) = @_;
147              
148 4         16 my ($modifier1, $length, $modifier2) = unpack 'C1v1C1', $$buffref;
149             # no, still too short
150 4 50 33     23 return undef unless $length && length $$buffref >= $length + 4;
151              
152             # then do the modifier-specific handling
153 4 50       9 die "Unsupported modifier1 $modifier1" unless $modifier1 == PSGI_MODIFIER1;
154              
155             # hack bits off the buffer
156 4         9 substr $$buffref, 0, 4, '';
157              
158 4         50 my %env = unpack '(v1/a*)*', substr $$buffref, 0, $length, '';
159 4         32 \%env
160             }
161              
162             # For cases where non-PSGI modifiers are wanted. Takes about 2.5x as long.
163             sub extract_frame_universal {
164 0     0 0 0 my $buffref = shift;
165             # too short
166 0 0       0 return undef unless length $$buffref >= 4;
167              
168 0         0 my ($modifier1, $length, $modifier2) = unpack 'C1v1C1', $$buffref;
169             # no, still too short
170 0 0       0 return undef unless length $$buffref >= $length + 4;
171              
172             # hack bits off the buffer
173 0         0 substr $$buffref, 0, 4, '';
174              
175             # then do the modifier-specific handling
176 0         0 return extract_modifier(
177             modifier1 => $modifier1,
178             modifier2 => $modifier2,
179             length => $length,
180             buffer => $buffref,
181             );
182             }
183              
184             =head2 bytes_required
185              
186             Returns the number of additional bytes we'll need in order to proceed.
187              
188             If zero, this means we should be able to extract a valid frame.
189              
190             =cut
191              
192             sub bytes_required {
193 0     0 1 0 my $buffref = shift;
194 0 0       0 return 4 - length($$buffref) unless length $$buffref >= 4;
195              
196 0         0 (undef, my $length) = unpack 'C1v1', $$buffref;
197 0 0       0 return ($length + 4) - length $$buffref unless length $$buffref >= $length + 4;
198              
199 0         0 return 0;
200             }
201              
202             =head2 build_request
203              
204             Builds an UWSGI request using the given modifier, defaulting
205             to modifier1 == 5 and modifier2 == 0, i.e. PSGI request.
206              
207             Takes the following named parameters:
208              
209             =over 4
210              
211             =item * modifier1 - the modifier1 value, defaults to 5 if not provided
212              
213             =item * modifier2 - the modifier2 value, defaults to 0 if not provided
214              
215             =item * method - the HTTP request method
216              
217             =item * uri - which L we're requesting, can be passed as a plain string
218             in which case we'll upgrade to a L object internally
219              
220             =item * headers - a hashref of HTTP headers, e.g. { 'Content-Type' => 'text/html' }
221              
222             =back
223              
224             Returns a scalar containing packet data or raises an exception on failure.
225              
226             =cut
227              
228             sub build_request {
229 5     5 1 4269 my %args = @_;
230              
231             # my $type = delete $args{type} or die 'no type provided';
232 5 50       19 my $uri = delete $args{uri} or die 'no URI provided';
233 5 50       29 $uri = URI->new($uri) unless ref $uri;
234              
235 5         8816 my %env;
236 5         20 $env{REQUEST_METHOD} = uc delete $args{method};
237 5         28 $env{UWSGI_SCHEME} = $uri->scheme;
238 5         381 $env{HTTP_HOST} = $uri->host;
239 5   50     147 $env{SERVER_PORT} = $uri->port // 80;
240 5         120 $env{PATH_INFO} = $uri->path;
241 5 50       61 $env{QUERY_STRING} = $uri->query if defined $uri->query;
242 5 50       68 @env{qw(REMOTE_ADDR REMOTE_PORT)} = split ':', delete $args{remote}, 2 if $args{remote};
243              
244 5   100     25 $args{headers} ||= {};
245 5         5 foreach my $k (keys %{$args{headers}}) {
  5         18  
246 3         6 (my $env_k = uc $k) =~ tr/-/_/;
247 3   50     14 $env{"HTTP_$env_k"} = $args{headers}{$k} // '';
248             }
249 5         13 delete $args{headers};
250              
251 5         13 my @modifier = delete @args{qw(modifier1 modifier2)};
252 5         9 my $data = '';
253 5         36 %env = (%args, %env);
254 5         31 foreach my $k (sort keys %env) {
255 38 50       589 die "Undef value found for $k" unless defined $env{$k};
256 38         49 $data .= pack 'v1/av1/a', map { Encode::encode('utf8', $_) } $k, $env{$k};
  76         726  
257             }
258              
259 5   50     172 return pack('C1v1C1',
      50        
260             $modifier[0] // PSGI_MODIFIER1,
261             length($data),
262             $modifier[1] // PSGI_MODIFIER2,
263             ) . $data;
264             }
265              
266             =head2 extract_modifier
267              
268             Used internally to extract and handle the modifier-specific data.
269              
270             =cut
271              
272             sub extract_modifier {
273 0     0 1 0 my %args = @_;
274              
275 0 0       0 die "Unsupported modifier1 $args{modifier1}" unless $args{modifier1} == PSGI_MODIFIER1;
276              
277 0 0       0 my $len = delete $args{length} or die "no length found";
278 0 0       0 my $buffer = delete $args{buffer} or die "no buffer found";
279 0         0 my %env;
280 0         0 while($len) {
281 0         0 my ($k, $v) = unpack 'v1/a*v1/a*', $$buffer;
282 0         0 $env{$k} = $v;
283 0         0 my $sublen = 4 + length($k) + length($v);
284 0         0 substr $$buffer, 0, $sublen, '';
285 0         0 $len -= $sublen;
286             }
287 0         0 return \%env;
288             }
289              
290             =head2 uri_from_env
291              
292             Returns a L object parsed from a request ("environment").
293              
294             =cut
295              
296             sub uri_from_env {
297 4     4 1 7 my ($env) = @_;
298 4         15 my $uri = $env->{UWSGI_SCHEME} . '://' . $env->{HTTP_HOST} . ':' . $env->{SERVER_PORT} . $env->{PATH_INFO};
299 4 50 50     21 $uri .= '?' . $env->{QUERY_STRING} if length($env->{QUERY_STRING} // '');
300 4         33 return URI->new($uri);
301             }
302              
303             1;
304              
305             __END__