3 # CLI client for the FEX service for retrieving files
7 # Author: Ulli Horlacher <framstag@rus.uni-stuttgart.de>
9 # Perl Artistic Licence
12 use strict qw'vars subs';
22 use Time::HiRes 'time';
23 use constant k => 2**10;
24 use constant M => 2**20;
26 eval 'use Net::INET6Glue::INET_is_INET6';
31 our ($fexhome,$idf,$tmpdir,$windoof,$useragent);
33 our $bs = 2**16; # blocksize for tcp-reading and writing file
34 our $version = 20160919;
35 our $CTYPE = 'ISO-8859-1';
36 our $fexsend = $ENV{FEXSEND} || 'fexsend';
37 our $DEBUG = $ENV{DEBUG};
40 my %SSL = (SSL_version => 'TLSv1');
43 # inquire default character set
44 # cannot use "use I18N::Langinfo" because of no windows support!
47 require I18N::Langinfo;
48 I18N::Langinfo->import(qw'langinfo CODESET');
49 $CTYPE = langinfo(CODESET());
52 if ($Config{osname} =~ /^mswin/i) {
53 $windoof = $Config{osname};
54 $ENV{HOME} = $ENV{USERPROFILE};
55 $fexhome = $ENV{FEXHOME} || $ENV{HOME}.'/fex';
56 $tmpdir = $ENV{FEXTMP} || $ENV{TMP} || "$fexhome/tmp";
58 $useragent = sprintf("fexget-$version (%s %s)",
59 $Config{osname},$Config{archname});
60 $SSL{SSL_verify_mode} = 0;
61 chdir $ENV{USERPROFILE}.'\Desktop';
62 # open XX,'>XXXXXX';close XX;
63 } elsif ($Config{osname} =~ /^darwin/i or $ENV{MACOS}) {
64 $0 =~ s:(.*)/:: and $ENV{PATH} .= ":$1";
65 $fexhome = $ENV{FEXHOME} || $ENV{HOME}.'/.fex';
66 $tmpdir = $ENV{FEXTMP} || $ENV{TMPDIR} || "$fexhome/tmp";
68 $_ = `sw_vers -productVersion 2>/dev/null`||'';
70 $useragent = "fexget-$version (MacOS $_)";
72 $0 =~ s:(.*)/:: and $ENV{PATH} .= ":$1";
73 $fexhome = $ENV{FEXHOME} || $ENV{HOME}.'/.fex';
74 $tmpdir = $ENV{FEXTMP} || "$fexhome/tmp";
76 $_ = `(lsb_release -d||uname -a)2>/dev/null`||'';
79 $useragent = "fexget-$version ($_)";
82 if (-f ($_ = '/etc/fex/config.pl')) {
83 eval { require } or warn $@;
87 usage: $0 [-v] [-m limit] [-s filename] [-o] [-k] [-X] [-P proxy:port] F*EX-URL(s)
88 or: $0 [-v] -d F*EX-URL(s)
89 or: $0 [-v] -f F*EX-URL(s) e-mail-address
94 options: -v verbose mode
96 -s save to filename (-s- means: write to STDOUT/pipe)
97 -o overwrite existing file
98 -k keep on server after download
99 -X do not extract archive files or autoview file
100 -d delete without download
101 -f forward a file to another recipient
102 -a get all files (implies -X)
103 -l list files on server
104 -i tag alternate server/account, see: $fexsend -h
105 -P use Proxy for connection to the F*EX server
106 -H show hints and examples
107 -V show version and ask for upgrade
108 argument: F*EX-URL may be file number (see: $0 -l)
112 When you download a file with extension .jpg .gif .png or .tif an image viewer
113 will be started. This can be xv or xdg-open.
114 In $HOME/.fex/config.pl you can set your prefered autoview applications:
117 '\.(gif|jpg|png|tiff?)' => 'my_prefered_image_viewer',
118 '\.(avi|mp4|mov)' => 'vlc -f',
122 For HTTPS you can set the environment variables:
123 SSLVERIFY=1 # activate server identity verification
124 SSLVERSION=TLSv1 # this is the default
125 SSLCAPATH=/etc/ssl/certs # path to trusted (root) certificates
126 SSLCAFILE=/etc/ssl/cert.pem # file with trusted (root) certificates
127 SSLCIPHERLIST=HIGH:!3DES # see http://www.openssl.org/docs/apps/ciphers.html
129 You can set these environment variables also in $HOME/.fex/config.pl, as well as
130 the $opt_* variables, e.g.:
132 $ENV{SSLVERSION} = 'TLSv1';
137 if ($windoof and not @ARGV and not $ENV{PROMPT}) {
138 # restart with cmd.exe to have mouse cut+paste
139 my $cmd = "cmd /k \"$0\"";
145 my $atype = '\.(tgz|tar|zip|7z)$';
147 my $proxy_prefix = '';
150 our ($opt_h,$opt_v,$opt_l,$opt_d,$opt_m,$opt_z,$opt_K,$opt_o,$opt_a);
151 our ($opt_s,$opt_k,$opt_i,$opt_V,$opt_X,$opt_f,$opt_P,$opt_L,$opt_H);
152 $opt_m = $opt_h = $opt_v = $opt_l = $opt_d = $opt_K = $opt_o = $opt_a = 0;
153 $opt_V = $opt_X = $opt_f = $opt_L = $opt_H = 0;
155 $opt_s = $opt_k = $opt_i = $opt_P = '';
156 $_ = "$fexhome/config.pl"; require if -f;
157 getopts('hvVHlLdkzoaXVf+m:s:i:K:P:') or die $usage;
158 $opt_k = '?KEEP' if $opt_k;
160 if ($opt_m =~ /(\d+)/) {
167 print "Version: $version\n";
169 print "Upgrade fexget? ";
172 my $new = `wget -nv -O- http://fex.belwue.de/download/fexget`;
173 my $newversion = $1 if $new =~ /version = (\d+)/;
174 if ($new !~ /upgrade fexget/ or not $newversion) {
175 die "$0: bad update\n";
177 if ($newversion <= $version) {
178 die "$0: no newer version\n";
181 system qw'rsync -a',$_0,$_0.'_old';
183 open $_0,'>',$_0 or die "$0: cannot write $_0. - $!\n";
190 exit if "@ARGV" eq '.';
193 die $usage if $opt_h;
201 my $ffl = "$tmpdir/fexget"; # F*EX files list (cache)
210 "
\e[A \\\\/\\\\/ \n",
211 "
\e[A //\\\\//\\\\\n"
216 my $cmd = "$fexsend -Z";
217 $cmd .= " -i $opt_i" if $opt_i;
218 warn "$cmd\n" if $opt_v;
220 die "$0: cannot run $cmd : $!\n";
229 my $cmd = "$fexsend -L";
230 $cmd .= " -i $opt_i" if $opt_i;
231 warn "$cmd\n" if $opt_v;
233 die "$0: cannot run $cmd : $!\n";
237 if ($opt_P =~ /^([\w.-]+:\d+)(:(\d+))?/) {
239 $chunksize = $3 || 0;
241 die "$0: proxy must be: SERVER:PORT\n";
250 if (open $ffl,$ffl) {
252 push @ARGV,$1 if /^\s+(\d+)/;
261 print "download-URL: ";
262 chomp($url = <STDIN>);
263 if ($url =~ /^http/) {
274 my ($file,%files,$download,$server,$port,$fop,$https);
277 unless ($ENV{FEXID} or -f $ENV{HOME}.'/.fex/id') {
278 die "$0: no local FEXID\n";
281 if ($opt_f =~ /^\d+$|^https?:/) {
282 die "$0: $opt_f is not an e-mail address\n";
286 URL: foreach my $url (@ARGV) {
288 # do not overrun server
291 if ($url !~ /^http/) {
293 open $ffl,$ffl or die "$0: no $ffl, use first: $0 -l\n";
296 if (/^from (.+) :$/) {
298 } elsif (/^\s*(\d+)\)\s+\d+ MB.* (http\S+)/) {
299 push @{$files{all}},$2;
300 push @{$files{$from}},$2;
306 if ($url =~ /^(\d+)$/) {
307 $url = ${files{all}}[$1-1] or die "$0: unknown file number\n";
311 if ($url =~ m{^http(s?)://([\w\.\-]+)(:(\d+))?(/.*fop/\S+)}) {
314 $port = $4 || ($1?443:80);
317 die "$0: unknown F*EX URL $url\n";
321 if ($port == 80) { $proxy_prefix = "http://$server" }
322 elsif ($port == 443) { $proxy_prefix = "" }
323 else { $proxy_prefix = "http://$server:$port" }
326 serverconnect($server,$port);
337 ($file) = grep { $_ = $1 if /^X-File:\s+(.+)/ } @r;
338 $file = $url unless $file;
340 printf "%s deleted\n",locale(decode_utf8(urldecode($file)));
343 die "$0: server response: $_";
354 print "$file kept\n";
357 die "$0: server response: $_";
362 $download = download($server,$port,$fop);
363 exit if $opt_s eq '-';
364 unlink $download unless -s $download;
365 exit 2 unless -f $download;
372 if (not $opt_X and $download =~ /\.gpg$/) {
374 print "decrypt \"$download\"? ";
377 print "keeping \"$download\"\n";
381 if (system('gpg',$download) == 0) {
383 $download =~ s/\.gpg$//;
389 foreach my $a (keys %autoview) {
390 if ($download =~ /$a$/i and $autoview{$a}) {
391 printf "run \"%s %s\" [Yn] ? ",$autoview{$a},basename($download);
393 system sprintf("%s %s",$autoview{$a},quote($download)) if /^y|^$/i;
398 if ($ENV{DISPLAY} and $download =~ /\.(gif|jpg|png|tiff?)$/i) {
399 # see also mimeopen and xdg-mime
400 # http://unix.stackexchange.com/questions/144047/how-does-xdg-open-do-its-work
401 if (my $xv = $xv || pathsearch('xv') || pathsearch('xdg-open')) {
402 printf "run \"%s %s\" [Yn] ? ",basename($xv),basename($download);
404 system $xv,$download if /^y|^$/i;
409 if ($download =~ /$atype/) {
410 if ($download =~ /\.(tgz|tar.gz)$/) { extract('tar tvzf','tar xvzf') }
411 elsif ($download =~ /\.tar$/) { extract('tar tvf','tar xvf') }
412 elsif ($download =~ /\.zip$/i) { extract('unzip -l','unzip') }
413 elsif ($download =~ /\.7z$/i) { extract('7z l','7z x') }
414 else { die "$0: unknown archive \"$download\"\n" }
418 die "$0: keeping \"$download\"\n";
434 if (-t and not $windoof) {
435 print "Files in archive:\n";
436 system(split(' ',$l),$download);
441 $xd = inquire("extract to directory (Ctrl-C to keep archive): ",$d);
442 last if $xd =~ s:^(\./*)*!?$::;
444 print "keeping $download\n";
449 print "directory $xd does already exist, add \"!\" to overwrite\n";
453 print "cannot mkdir $xd - $!\n";
458 print "cannot chdir $xd - $!\n";
464 print "extracting to $xd :\n" if $xd;
465 system(split(' ',$x),$download);
466 print "extracted to $xd\n" if $xd;
475 if ($url =~ m{^http(s?)://([\w\.\-]+)(:(\d+))?(/fop/.+)}) {
477 $port = $4 || ($1?443:80);
480 die "$0: unknown F*EX URL $url\n";
483 sendheader("$server:$port","GET $del HTTP/1.1","User-Agent: $useragent");
486 last if /^\n/; # ignore HTML output
487 warn "<-- $_" if $opt_v;
490 die "$0: no response from fex server $server\n" unless @r;
498 my ($uri,$dkey,$list,$cmd,$n,$copy);
501 if ($url =~ m{^http(s?)://([\w\.\-]+)(:(\d+))?(/fop/.+)}) {
503 $port = $4 || ($1?443:80);
506 die "$0: unknown F*EX URL $url\n";
511 "GET $uri?COPY HTTP/1.1",
512 "User-Agent: $useragent",
516 die "$0: no reply from fex server $server\n" unless $_;
517 warn "<-- $_" if $opt_v;
519 if (/^HTTP.*already exists/) {
520 if ($uri =~ m:/fop/(\w+)/:) {
523 } elsif (/^HTTP.*200/) {
532 last if /^\n/; # ignore HTML output
533 $dkey = $1 if /^Location:.*\/(\w+)\/.+/;
534 warn "<-- $_" if $opt_v;
537 print "fexsend -l\n" if $opt_v;
538 system 'fexsend -l >/dev/null 2>&1';
539 $list = $ENV{HOME}.'/.fex/tmp/fexlist';
540 open $list,$list or die "$0: cannot open $list - $!\n";
542 if (/^\s+(\d+)\) (\w+)/ and $2 eq $dkey) {
544 $cmd = "fexsend -b $n $opt_f";
545 print "$cmd\n" if $opt_v;
553 $cmd = "fexsend -d $n >/dev/null 2>&1";
554 print "$cmd\n" if $opt_v;
557 warn "$0: forwarding failed\n";
568 if ($url =~ m{^http(s?)://([\w\.\-]+)(:(\d+))?(/fop/.+)}) {
570 $port = $4 || ($1?443:80);
571 $keep = "$5?KEEP=$opt_K";
573 die "$0: unknown F*EX URL $url\n";
576 push @hh,"GET $keep HTTP/1.1",
577 "Host: $server:$port",
578 "User-Agent: $useragent",
582 warn $_,"\n" if $opt_v;
590 die "$0: no response from fex server $server\n" unless @r;
591 grep { warn "\t$_" } @r if $opt_v;
597 my ($server,$port,$fop,$nocheck) = @_;
598 my ($file,$download,$ssl,$pipe,$filesize,$checkstorage,$dkey);
600 my ($t0,$t1,$t2,$tt,$tm,$ts,$kBs,$b,$bt,$tb,$B,$buf);
610 $pipe = $download = $opt_s;
611 } elsif (-p $opt_s or -c $opt_s) {
613 $nocheck = 'pipe or character device';
615 $download = $file.'.tmp';
616 $seek = -s $download || 0;
619 # ask server for real file name
622 "HEAD $proxy_prefix$fop HTTP/1.1",
623 "User-Agent: $useragent"
625 my $reply = $_ = <$SH>;
626 unless (defined $_ and /\w/) {
627 die "$0: no response from server\n";
629 warn "<-- $_" if $opt_v;
630 unless (/^HTTP\/[\d.]+ 200/) {
632 die "$0: server response: $_";
636 warn "<-- $_" if $opt_v;
638 if (/^Content-Disposition: attachment; filename="(.+)"/i) {
639 $file = locale(decode_utf8($1));
647 $download = $file.'.tmp';
648 $seek = -s $download || 0;
651 $fop =~ m:/fop/(\w+)/: and $dkey=$1 or $dkey='';
653 push @hh,"GET $proxy_prefix$fop$opt_k HTTP/1.1",
654 "User-Agent: $useragent",
655 "Cookie: dkey=$dkey",
657 push @hh,"Range: bytes=$seek-" if $seek;
659 # HTTPS needs a new connection for actually downloading the file
660 serverconnect($server,$port) if $opt_P and $port == 443;
661 sendheader("$server:$port",@hh);
663 die "$0: no response from fex server $server\n" unless $_;
666 if (/^HTTP\/[\d.]+ 2/) {
667 warn "<-- $_" if $opt_v;
670 warn "<-- $_" if $opt_v;
672 if (/^Content-length:\s*(\d+)/i) {
674 } elsif (/^X-Size: (\d+)/i) {
679 s/HTTP\/[\d.]+ \d+ //;
680 die "$0: bad server reply: $_";
686 if ($opt_s and $opt_s eq $download) {
687 open X,'>',$download or die "$0: cannot write to \"$download\" - $!\n";
688 $checkstorage = $filesize unless $nocheck;
690 if (-e $file and not $opt_o) {
691 die "$0: destination file \"$file\" does already exist\n";
694 open X,'>>',$download or die "$0: cannot write to \"$download\" - $!\n";
696 open X,'>',$download or die "$0: cannot write to \"$download\" - $!\n";
697 $checkstorage = $filesize unless $nocheck;
700 if ($checkstorage and not $nocheck) {
701 my $t0 = my $t1 = my $t2 = time;
704 my $storagetest = $file.'.test';
705 my $error = "$0: cannot write \"$storagetest\"";
706 open $storagetest,'>',$storagetest or die "$error - $!\n";
707 print STDERR "checking storage...\r";
708 while (-s $storagetest < $checkstorage) {
709 syswrite $storagetest,$buf or do {
711 die "\n$error - $!\n";
716 print STDERR "checking storage... ".$n." MB\r";
720 close $storagetest or do {
722 die "\n$error - $!\n";
724 print STDERR "checking storage... ".$n." MB ok!\n";
727 # retry after timeout
728 serverconnect($server,$port);
729 return(download($server,$port,$fop,'nocheck'))
734 $t0 = $t1 = $t2 = int(time);
736 printf STDERR "resuming at byte %s\n",$seek if $seek;
737 print $rcamel[0] if ${'opt_+'};
738 while ($B < $length and $b = read $SH,$buf,$bs) {
744 if (${'opt_+'} and int($t2*10)>$tc) {
745 print $rcamel[$tc%2+1];
748 if (int($t2) > $t1) {
749 $kBs = int($bt/k/($t2-$t1));
750 $kBs = int($tb/k/($t2-$t0)) if $kBs < 10;
753 # smaller block size is better on slow links
754 $bs = 4096 if $bs>4096 and $tb/($t2-$t0)<65536;
756 printf STDERR "%s: %d kB (%d%%) %d kB/s \r",
759 int(($tb+$seek)/($length+$seek)*100),
762 printf STDERR "%s: %d MB (%d%%) %d kB/s \r",
765 int(($tb+$seek)/($length+$seek)*100),
770 if ($t2 == $t0 and $B > $opt_m*k) {
771 print "\nsleeping...\r" if $opt_v;
774 while ($t2 > $t0 and $tb/k/($t2-$t0) > $opt_m) {
775 print "\nsleeping...\r" if $opt_v;
785 print $rcamel[2] if ${'opt_+'};
790 $kBs = int($tb/k/($tt||1));
792 printf STDERR "$file: %d MB, last %d MB in %d s (%d kB/s) \n",
793 int(($tb+$seek)/M),int($tb/M),$tt,$kBs;
795 printf STDERR "$file: %d MB in %d s (%d kB/s) \n",
799 if ($tb != $length) {
804 die "$0: $server annouced $length bytes, but only $tb bytes has been read\n";
808 unless ($pipe or -p $download or -c $download) {
809 my @s = stat $file if -e $file;
810 rename $download,$file
811 or die "$0: cannot rename \"$download\" to \"$file\" - $!\n";
812 chmod $s[2],$file if @s;
815 return sprintf("%s/%s",getcwd(),$file);
820 my $cmd = "$fexsend -L";
821 $cmd .= " -i $opt_i" if $opt_i;
826 open $cmd,"$cmd|" or die "$0: cannot run $cmd : $!\n";
827 open $ffl,'>',$ffl or die "$0: cannot open $ffl : $!\n";
830 if (/\d MB .*http/) {
832 printf {$ffl} "%4d) %s",$n,$_;
834 printf "%4d) %s",$n,$_;
847 if ($CTYPE =~ /UTF-?8/i) {
849 } elsif (grep { $CTYPE =~ /^$_$/i } Encode->encodings()) {
850 return encode($CTYPE,$string);
852 return encode('ISO-8859-1',$string);
863 foreach my $dir (split(':',$ENV{PATH})) {
864 return "$dir/$prg" if -x "$dir/$prg";
878 if (defined $default) {
880 chomp($tty = `tty 2>/dev/null`);
881 eval { local $^W; require "sys/ioctl.ph"; };
884 if (defined(&TIOCSTI) and $tty and open($tty,'>',$tty)) {
886 # push default answer into keyboard buffer
887 foreach my $a (split("",$default)) { ioctl($tty,&TIOCSTI,$a) }
888 chomp($_ = <STDIN>||'');
890 $prompt =~ s/([\?:=]\s*)/ [$default]$1/ or $prompt .= " [$default] ";
892 chomp($_ = <STDIN>||'');
893 $_ = $default unless length;
897 chomp($_ = <STDIN>||'');
905 ### common functions ###
909 my @d = localtime((stat shift)[9]);
910 return sprintf('%d%02d%02d',$d[5]+1900,$d[4]+1,$d[3]);
916 s/\%([a-f\d]{2})/chr(hex($1))/ige;
922 # set SSL/TLS options
923 $SSL{SSL_verify_mode} = $ENV{SSLVERIFY} if defined($ENV{SSLVERIFY});
933 $SSL{$opt} = $ENV{$env} if defined($ENV{$env});
936 if ($SSL{SSL_verify_mode}) {
938 unless ($SSL{SSL_ca_path} or $SSL{SSL_ca_file}) {
939 die "$0: \$SSLVERIFYMODE, but not valid \$SSLCAPATH or \$SSLCAFILE\n";
941 } elsif (defined($SSL{SSL_verify_mode})) {
942 # user has set SSLVERIFY=0 !
945 $SSL{SSL_verify_mode} = 1 if $SSL{SSL_ca_path} or $SSL{SSL_ca_file};
951 return if $SSL{SSL_ca_file} or $SSL{SSL_ca_path};
952 foreach (qw(/etc/ssl/certs/ca-certificates.crt)) {
954 $SSL{SSL_ca_file} = $_;
958 foreach (qw(/etc/ssl/certs /etc/pki/tls/certs)) {
960 $SSL{SSL_ca_path} = $_;
968 my ($server,$port) = @_;
969 my $connect = "CONNECT $server:$port HTTP/1.1";
973 tcpconnect(split(':',$proxy));
975 printf "--> %s\n",$connect if $opt_v;
976 nvtsend($connect,"");
979 printf "<-- $_"if $opt_v;
980 unless (/^HTTP.1.. 200/) {
981 die "$0: proxy error : $_";
984 $SH = IO::Socket::SSL->start_SSL($SH,%SSL);
987 tcpconnect($server,$port);
989 # if ($https and $opt_v) {
990 # printf "%s\n",$SH->get_cipher();
995 # set up tcp/ip connection
997 my ($server,$port) = @_;
1005 # eval "use IO::Socket::SSL qw(debug3)";
1007 $SH = IO::Socket::SSL->new(
1008 PeerAddr => $server,
1014 $SH = IO::Socket::INET->new(
1015 PeerAddr => $server,
1025 die "$0: cannot connect $server:$port - $@\n";
1028 print "TCPCONNECT to $server:$port\n" if $opt_v;
1033 eval "use IO::Socket::SSL";
1034 die "$0: cannot load IO::Socket::SSL\n" if $@;
1035 eval '$SSL{SSL_verify_mode} = 0 if Net::SSLeay::SSLeay() <= 9470143';
1037 foreach my $v (keys %SSL) {
1038 printf "%s => %s\n",$v,$SSL{$v};
1049 push @head,"Host: $sp";
1051 foreach $head (@head) {
1052 print "--> $head\n" if $opt_v;
1053 print {$SH} $head,"\r\n";
1055 print "-->\n" if $opt_v;
1061 local $SIG{PIPE} = sub { $sigpipe = "@_" };
1065 die "$0: internal error: no active network handle\n" unless $SH;
1066 die "$0: remote host has closed the link\n" unless $SH->connected;
1068 foreach my $line (@_) {
1069 print {$SH} $line,"\r\n";
1082 s/([^\w\@\/%^,.=+_:+-])/\\$1/g;
1088 print "## DEBUG: @_\n" if $DEBUG;
1092 # from MIME::Base64::Perl
1099 $res = join '',map(pack('u',$_)=~ /^.(\S*)/, ($_[0]=~/(.{1,45})/gs));
1100 $res =~ tr|` -_|AA-Za-z0-9+/|;
1101 $padding = (3-length($_[0])%3)%3;
1102 $res =~ s/.{$padding}$/'=' x $padding/e if $padding;