File Coverage

blib/lib/App/rs.pm
Criterion Covered Total %
statement 65 332 19.5
branch 14 204 6.8
condition 2 38 5.2
subroutine 10 31 32.2
pod 0 20 0.0
total 91 625 14.5


line stmt bran cond sub pod time code
1             =license
2              
3             Copyright © 2018 Yang Bo
4              
5             This file is part of RSLinux.
6              
7             RSLinux is free software: you can redistribute it and/or modify
8             it under the terms of the GNU General Public License as published by
9             the Free Software Foundation, either version 3 of the License, or
10             (at your option) any later version.
11              
12             RSLinux is distributed in the hope that it will be useful,
13             but WITHOUT ANY WARRANTY; without even the implied warranty of
14             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15             GNU General Public License for more details.
16              
17             You should have received a copy of the GNU General Public License
18             along with RSLinux. If not, see .
19              
20             =cut
21              
22             package App::rs;
23              
24             our $VERSION = 'v2.1.2';
25              
26 1     1   314 use strict;
  1         2  
  1         24  
27 1     1   3 use warnings qw/all FATAL uninitialized/;
  1         1  
  1         38  
28 1     1   3 use feature qw/state say/;
  1         2  
  1         238  
29              
30             require XSLoader;
31             XSLoader::load();
32              
33             sub _require ($) {
34 5     5   9 my $r = shift =~ s|::|/|gr . '.pm';
35 5 100       927 require $r if not $INC{$r};
36             }
37             sub flatten (;$) {
38 13 50   13 0 19 my $v = @_ ? shift : $_;
39 13 100       29 ref $v eq 'ARRAY' ? @$v : $v;
40             }
41 0         0 BEGIN {
42 1     1   5 no strict 'refs';
  1         1  
  1         381  
43 1     1   6 my @H = ($^H, ${^WARNING_BITS}, %^H);
44             sub import {
45 2     2   15 my $ns = caller . '::';
46 2         2 shift;
47 2         7 while (@_) {
48 4         5 my $q = shift;
49 4 100       10 if ($q eq 'iautoload') {
    100          
    50          
50 2         2 my (@pkg, %map);
51 2         2 for (@{+shift}) {
  2         4  
52 5         24 my ($p, @f) = flatten;
53 5         6 push @pkg, $p;
54 5         8 for (@f) {
55 8         10 my ($from, $to) = flatten;
56 8         37 $from =~ s/^([$@%&*])//;
57 8   33     28 $to ||= $from;
58 8 100       13 if (my $s = $1) {
59 5         7 state $sigil = {'$' => 'SCALAR',
60             '@' => 'ARRAY',
61             '%' => 'HASH',
62             '&' => 'CODE',
63             '*' => 'GLOB'};
64 5         14 _require $p;
65 5         5547 *{$ns . $to} = *{"${p}::$from"}{$sigil->{$s}};
  5         19  
  5         24  
66             } else {
67 3         13 $map{$to} = {from => $from,
68             module => $p};
69             }
70             }
71             }
72 2         903 *{$ns . 'AUTOLOAD'} = sub {
73             # "fully qualified name of the original subroutine".
74 0     0   0 my $q = our $AUTOLOAD;
75             # to avoid possibly overwrite @_ by successful regular expression match.
76 0         0 my ($to) = do { $q =~ /.*::(.*)/ };
  0         0  
77 0         0 my $u = $map{$to};
78 0   0     0 my $from = $u->{from} || $to;
79 0   0     0 for my $p ($u->{module} || @pkg) {
80             # calculate the actual file to be loaded thus avoid eval and
81             # checking $@ mannually.
82 0         0 _require $p;
83 0 0       0 if (my $r = *{"${p}::$from"}{CODE}) {
  0         0  
84 1     1   5 no warnings 'prototype';
  1         1  
  1         435  
85 0         0 *$q = $r;
86             # TODO: understand why using goto will lost context.
87             #goto &$r;
88 0         0 return &$r;
89             }
90             }
91 0         0 confess("unable to autoload $q.");
92 2         10 };
93             } elsif ($q eq 'oautoload') {
94 1         2 for my $p (@{+shift}) {
  1         1  
95 3         8 my $r = $p =~ s|::|/|gr . '.pm';
96             # ignore already loaded module.
97 3         5 my $f = "${p}::AUTOLOAD";
98 3 50 50     18 next if $INC{$r} or *$f{CODE};
99             *$f = sub {
100 1     1   2 my ($f) = do { our $AUTOLOAD =~ /.*::(.*)/ };
  1         4  
101 1         1 my $symtab = *{"${p}::"}{HASH};
  1         2  
102 1         3 delete $symtab->{AUTOLOAD};
103 1         673 require $r;
104 1         5311 &{$symtab->{$f}};
  1         3135  
105 3         10 };
106             }
107             } elsif ($q eq 'sane') {
108 1         8 ($^H, ${^WARNING_BITS}, %^H) = @H;
109             } else {
110 0           confess("unknown request $q");
111             }
112             }
113             };
114 1         2 my @a = qw/Cpanel::JSON::XS JSON::XS JSON::PP/;
115             App::rs->import(iautoload => ['Carp',
116             [qw'Compress::Zlib memGunzip'],
117             [qw/File::Path make_path/],
118             [qw'Socket getaddrinfo',
119 1         2 map { "&$_" } qw'AF_UNIX SOCK_STREAM MSG_NOSIGNAL']],
  3         11  
120             oautoload => [@a]);
121 1         2 my $o;
122 1         6 for (@a) {
123 1 50       2 last if eval {
124 1         7 $o = $_->new->pretty->canonical;
125             };
126             }
127 0     0 0   sub jw { $o->encode(shift) }
128 0     0 0   sub jr { $o->decode(shift) }
129             }
130             sub xsh {
131 0     0 0   my $f = shift;
132 0 0         if (not ref $f) {
133 0           my $h = {};
134 0 0         $h->{"capture-stdout"} = 1 if $f & 1;
135 0 0         $h->{"feed-stdin"} = 1 if $f & 2;
136 0           $f = $h;
137             }
138 0           my ($h, $i, $pr, @st) = ({pid => []}, 0);
139 0 0         if ($f->{"feed-stdin"}) {
140 0           my ($fi, $pid) = shift;
141 0           pipe $pr, my $pw;
142 0 0         if (not $pid = fork) {
143 0           close $pr;
144 0           print $pw $fi;
145 0           exit;
146             } else {
147 0           push @{$h->{pid}}, $pid;
  0            
148             }
149             }
150 0           while ($i <= @_) {
151 0           my $l = $i == @_;
152 0 0         my $a = $_[$i] if not $l;
153 0 0 0       if ($l or $a eq "|") {
154 0 0 0       pipe my $r, my $w if not $l or $f->{"capture-stdout"};
155             # there's no need to fork when executing the last command and we're required
156             # to substitute current process.
157 0 0 0       my $pid = fork unless $l and $f->{substitute};
158 0 0         if (not $pid) {
159             # always true except possibly the first.
160 0 0         open STDIN, "<&", $pr if $pr;
161             # always true except possibly the last.
162 0 0         open STDOUT, ">&", $w if $w;
163 0           while (ref $st[-1]) {
164 0           my ($h, $f) = pop @st;
165 0 0         if (ref \$h->{from} eq "SCALAR") { open $f, $h->{mode}, $h->{from} or die $! }
  0 0          
166 0           else { $f = $h->{from} }
167 0           open $h->{to}, $h->{mode} . "&", $f;
168             }
169 0           exec @st;
170             } else {
171 0           $pr = $r;
172 0           push @{$h->{pid}}, $pid;
  0            
173 0           @st = ();
174             }
175             } else {
176 0           push @st, $a;
177             }
178 0           $i++;
179             }
180 0 0         if ($f->{asynchronous}) {
181 0 0         $h->{stdout} = $pr if $f->{"capture-stdout"};
182 0 0         if ($f->{compact}) { $h }
  0 0          
183 0           elsif ($f->{"capture-stdout"}) { $pr }
184 0 0         else { wantarray ? @{$h->{pid}} : $h->{pid}[-1] }
  0            
185             } else {
186 0 0         if ($f->{"capture-stdout"}) {
187 0 0         local $/ if not wantarray;
188 0           $h->{stdout} = [<$pr>];
189             }
190 0           $h->{status} = [];
191 0 0         push @{$h->{status}}, waitpid($_, 0) == -1 ? undef : $? for @{$h->{pid}};
  0            
  0            
192             # they're meaningless now as they don't exist anymore.
193 0           delete $h->{pid};
194 0 0         if ($f->{compact}) { $h }
  0 0          
195 0 0         elsif ($f->{"capture-stdout"}) { wantarray ? @{$h->{stdout}} : $h->{stdout}[0] }
  0            
196 0 0         else { wantarray ? @{$h->{status}} : not $h->{status}[-1] }
  0            
197             }
198             }
199             sub arg_parse {
200 0     0 0   my $h = {};
201 0           while (@ARGV) {
202 0           my $a = shift @ARGV;
203 0 0         if ($a !~ /^-/) { unshift @ARGV, $a; last }
  0 0          
  0 0          
    0          
204 0           elsif ($a =~ /^--?$/) { last }
205 0           elsif ($a =~ /^--(.*?)=(.*)$/) { hash_madd_key($h, $1, $2) }
206 0           elsif ($a =~ /^--?(.*)$/) { $h->{$1} = 1 }
207             }
208 0           $h;
209             }
210             sub hash_madd_key {
211 0     0 0   my ($h, $k, $v) = @_;
212 0 0         if (exists $h->{$k}) {
213 0 0         $h->{$k} = [$h->{$k}] if ref $h->{$k} ne 'ARRAY';
214 0           push @{$h->{$k}}, $v;
  0            
215             } else {
216 0           $h->{$k} = $v;
217             }
218             }
219             sub linker {
220 0     0 0   my $s = shift;
221             $s->{i386} ?
222             "$s->{prefix}/lib/ld-linux.so.2" : $s->{arm} ?
223 0 0         "$s->{prefix}/lib/ld-linux-armhf.so.3" :
    0          
224             "$s->{prefix}/lib/ld-linux-x86-64.so.2";
225             }
226             sub add {
227 0     0 0   my $h = shift;
228 0           while (@_) {
229 0           my ($k, $v) = splice @_, 0, 2;
230 0           $h->{$k} = $v;
231             }
232             }
233             sub slice {
234 0     0 0   my $h = shift;
235 0           map { $_ => $h->{$_} } @_;
  0            
236             }
237             sub wf {
238 0     0 0   local $_ = shift;
239 0 0         if (-e) { unlink or die "$!: unable to remove $_ for writing.\n" }
  0 0          
    0          
240 0 0         elsif (m|(.*/)|) { make_path($1) unless -d }
241 0 0         open my $fh, '>', $_ or die "open $_ for writing: $!";
242 0 0         if (@_) { syswrite $fh, shift }
  0            
243 0           else { $fh }
244             }
245             sub purl {
246 0     0 0   my $o = shift;
247             my $x = {major => 1,
248             minor => 1,
249             type => 'request',
250             method => $o->{method},
251 0           hf => [qw/Host User-Agent Accept-Encoding Connection/],
252             hv => {connection => 'keep-alive',
253             'user-agent' => 'App-rs',
254             'accept-encoding' => 'gzip'}};
255 0 0         if ($o->{method} eq 'POST') {
256 0           push @{$x->{hf}}, qw/Content-Length Content-Type/;
  0            
257             add($x->{hv},
258 0           'content-length' => undef,
259             'content-type' => 'application/x-www-form-urlencoded');
260 0           $x->{c} = $o->{'post-data'};
261             }
262 0           my $url = $o->{url};
263 0           @$x{qw/protocol request-uri/} = ('http', '/');
264 0 0         ($x->{protocol}, $url) = ($1, $2) if $url =~ m|(.*)://(.*)|;
265 0 0         if ($url =~ m|(.*?)(/.*)|) {
266 0           ($x->{hv}{host}, $x->{'request-uri'}) = ($1, $2);
267             } else {
268 0           $x->{hv}{host} = $url;
269             }
270 0           my $r = http_req($x);
271 0           my $c = $r->{c};
272 0 0         $c = memGunzip($c) if eval { $r->{hv}{'content-encoding'} eq 'gzip' };
  0            
273 0 0         if ($o->{json}) { jr($c) }
  0 0          
    0          
    0          
274 0           elsif ($o->{plain}) { $c }
275 0           elsif ($o->{html}) { html_parse($c) }
276             elsif ($o->{save}) {
277 0 0         die $r->{b} unless $r->{'status-code'} == 200;
278 0           wf($o->{save}, $c);
279             }
280             }
281             sub http_req {
282             # socket pool.
283 0     0 0   state $pool = {};
284 0           my ($x, $f) = @_;
285             # host key to identify socket.
286 0           my $hk = $x->{protocol} . '://' . $x->{hv}{host};
287 0 0         if (not $pool->{$hk}) {
288 0           say "creating new pool socket $hk.";
289 0 0         if ($x->{protocol} eq 'https') { $pool->{$hk} = connect_tls($x->{hv}{host}, 443) }
  0            
290 0           else { $pool->{$hk} = connect_tcp($x->{hv}{host}, 80) }
291             }
292 0           send $pool->{$hk}, http_unparse($x), MSG_NOSIGNAL;
293 0           my $h = http_parse_new();
294             # avoid undefined warning when checking length of $h->{c}.
295 0           $h->{c} = '';
296 0           while (1) {
297 0           my $b;
298 0           eval {
299 0     0     local $SIG{ALRM} = sub { die };
  0            
300 0           alarm 12;
301 0           recv $pool->{$hk}, $b, 1048576, 0;
302 0           alarm 0;
303             };
304 0 0 0       if ($@ or not $b) {
305 0 0         if ($@) { say 'timeout.' }
  0            
306 0           else { say 'remote-close.' }
307 0           my $_h = http_parse_new();
308 0 0 0       if ($f->{range} and length($h->{c})) {
309 0           $_h->{c} = $h->{c};
310 0 0         push @{$x->{hf}}, 'Range' if not exists $x->{hv}{range};
  0            
311 0           $x->{hv}{range} = 'bytes=' . length($h->{c}) . '-';
312             }
313 0           $h = $_h;
314 0 0         if ($x->{protocol} eq 'https') { $pool->{$hk} = connect_tls($x->{hv}{host}, 443) }
  0            
315 0           else { $pool->{$hk} = connect_tcp($x->{hv}{host}, 80) }
316 0           send $pool->{$hk}, http_unparse($x), MSG_NOSIGNAL;
317             } else {
318 0 0         return $h if http_parse($h, $b);
319             }
320             }
321             }
322             sub connect_tcp {
323 0     0 0   my ($err, $a) = getaddrinfo(@_);
324 0 0         die "getaddrinfo: $err" if $err;
325 0 0         socket my $fh, $a->{family}, SOCK_STREAM, 0 or die $!;
326 0 0         connect $fh, $a->{addr} or die $!;
327 0           $fh;
328             }
329             sub connect_tls {
330 0     0 0   my ($host, $port) = @_;
331 0           my ($p, $q);
332 0           socketpair $p, $q, AF_UNIX, SOCK_STREAM, 0;
333 0           xsh({asynchronous => 1}, qw/socat -/, "OPENSSL:$host:$port",
334             {to => *STDIN,
335             from => $q,
336             mode => '<'}, {to => *STDOUT,
337             from => $q,
338             mode => '>'});
339 0           $p;
340             }
341             sub http_parse_new {
342 0     0 0   {st => 'reading-header',
343             # remaining length.
344             rl => 'line',
345             # header value.
346             hv => {},
347             # header field.
348             hf => [],
349             # first line.
350             fl => 1};
351             }
352             sub http_parse {
353 0     0 0   my ($h, $b) = @_;
354 0           $h->{b} .= $b;
355 0           my $i = 0;
356 0           while ($i < length($b)) {
357 0 0         if ($h->{rl} eq "line") {
358 0           pos($b) = $i;
359 0 0         if ($b =~ /\n/g) {
360 0           $h->{l} .= substr($b, $i, pos($b) - $i), $i = pos($b);
361 0           $h->{l} =~ s/\r?\n$//;
362 0 0         if ($h->{st} eq "reading-header") {
    0          
    0          
    0          
363 0 0         if ($h->{fl}) {
364 0 0         if ($h->{l}) {
365 0 0         if ($h->{l} =~ m|^HTTP\s*/\s*(\d)\s*\.\s*(\d)\s+(\d{3})\s+(.*)$|) {
    0          
366 0           @$h{qw/type major minor status-code reason-phrase/} = ("reply", $1, $2, $3, $4);
367             } elsif ($h->{l} =~ m|^(.*?)\s+(.*?)\s+HTTP\s*/\s*(\d)\s*\.\s*(\d)$|) {
368 0           @$h{qw/type method request-uri major minor/} = ("request", $1, $2, $3, $4);
369             } else {
370             }
371 0           $h->{fl} = 0;
372             }
373             # empty line before request/reply ignored.
374             } else {
375 0 0         if (not $h->{l}) {
    0          
376 0 0 0       if ($h->{type} eq "reply" and $h->{"status-code"} =~ /^(1\d{2}|204|304)$/) {
    0 0        
    0          
    0          
377 0           return $i;
378             } elsif (exists $h->{hv}{"transfer-encoding"} and $h->{hv}{"transfer-encoding"} !~ /^identity$/i) {
379 0           $h->{st} = "reading-chunk-size";
380             } elsif (exists $h->{hv}{"content-length"}) {
381 0           $h->{rl} = $h->{hv}{"content-length"}, $h->{st} = "reading-content";
382             # content-length could be 0.
383 0 0         return $i if not $h->{rl};
384             } elsif ($h->{type} eq "reply") {
385 0           $h->{rl} = "eof";
386             } else {
387 0           return $i;
388             }
389             } elsif ($h->{l} =~ /^\s/) {
390 0           my $k = lc $h->{hf}[$#{$h->{hf}}];
  0            
391 0 0         if (ref $h->{hv}{$k} eq "ARRAY") {
392 0           my $r = $h->{hv}{$k};
393 0           $r->[$#$r] .= $h->{l};
394             } else {
395 0           $h->{hv}{$k} .= $h->{l};
396             }
397             } else {
398 0           my ($f, $v) = $h->{l} =~ /^(.*?)\s*:\s*(.*?)\s*$/;
399 0           my $k = lc($f);
400 0 0         if (exists $h->{hv}{$k}) {
401 0 0         if (ref $h->{hv}{$k} eq "ARRAY") {
402 0           push @{$h->{hv}{$k}}, $v;
  0            
403             } else {
404 0           $h->{hv}{$k} = [$h->{hv}{$k}, $v];
405             }
406             } else {
407 0           $h->{hv}{$k} = $v;
408             }
409 0           push @{$h->{hf}}, $f;
  0            
410             }
411             }
412             } elsif ($h->{st} eq "reading-chunk-size") {
413 0           $h->{l} =~ /^([A-Fa-f0-9]+)/;
414 0 0         if ($1 !~ /^0+$/) { $h->{rl} = hex $1, $h->{st} = "reading-chunk-data" }
  0            
415 0           else { $h->{st} = "reading-trailer" }
416             } elsif ($h->{st} eq "reading-crlf") {
417 0           $h->{st} = "reading-chunk-size";
418             } elsif ($h->{st} eq "reading-trailer") {
419             # trailer ignored.
420 0 0         return $i unless $h->{l};
421             }
422 0           $h->{l} = "";
423             } else {
424 0           $h->{l} .= substr($b, $i), $i = length($b);
425             }
426             } else {
427 0 0 0       if ($h->{rl} ne "eof" and $h->{rl} <= length($b) - $i) {
428 0           $h->{c} .= substr($b, $i, $h->{rl}), $i += $h->{rl};
429 0 0         if ($h->{st} eq "reading-chunk-data") { $h->{rl} = "line", $h->{st} = "reading-crlf" }
  0            
430 0           else { return $i }
431             } else {
432 0           $h->{c} .= substr($b, $i), $h->{rl} -= length($b) - $i, $i = length($b);
433             }
434             }
435             }
436 0           undef;
437             }
438             sub http_unparse {
439 0     0 0   my $h = shift;
440 0           my $b;
441 0           my $v = "HTTP/$h->{major}.$h->{minor}";
442 0 0         if ($h->{type} eq "request") { $b = join " ", $h->{method}, $h->{"request-uri"}, $v }
  0            
443 0           else { $b = join " ", $v, $h->{"status-code"}, $h->{"reason-phrase"} }
444 0           $b .= "\r\n";
445 0 0         $h->{hv}{"content-length"} = length($h->{c}) if exists $h->{hv}{"content-length"};
446 0           my $i = {};
447 0           for (@{$h->{hf}}) {
  0            
448 0           $b .= "$_: ";
449 0           my $k = lc $_;
450 0 0         if (ref $h->{hv}{$k} eq "ARRAY") { $b .= $h->{hv}{$k}[$i->{$k}++] }
  0            
451 0           else { $b .= $h->{hv}{$k} }
452 0           $b .= "\r\n";
453             }
454 0           $b .= "\r\n";
455 0 0         if (exists $h->{c}) {
456 0 0 0       if (exists $h->{hv}{"transfer-encoding"} and $h->{hv}{"transfer-encoding"} !~ /^identity$/i) {
457 0           $b .= sprintf("%x\r\n", length($h->{c})) . $h->{c} . "\r\n0\r\n\r\n";
458             } else {
459 0           $b .= $h->{c};
460             }
461             }
462 0           $b;
463             }
464             sub vcmp ($$) {
465 0     0 0   my ($a, $b) = @_;
466 0           version->parse($a) <=> version->parse($b);
467             }
468             sub vsat {
469 0     0 0   my ($pkg, $ver) = @_;
470 0 0         return vcmp($^V, $ver) >= 0 if $pkg eq 'perl';
471 0 0         if (my $pid = fork) {
472 0 0         die unless $pid == waitpid $pid, 0;
473 0           not $?;
474             } else {
475 0           exit not eval {
476 0           require $pkg =~ s|::|/|gr . '.pm';
477 0           $pkg->VERSION($ver);
478             };
479             }
480             }
481             sub rf {
482 0     0 0   local (@ARGV, $/) = @_;
483 0           <>;
484             }
485             1;