You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

426 lines
12 KiB

#!/usr/bin/perl
use MIME::Base64;
sub usage {
print "h2c.pl [options] < file \n",
" -a Allow curl's default headers\n",
" -d Output man page HTML links after command line\n",
" -h Show short help\n",
" -H Output HTTP generated URLs instead\n",
" -i Ignore HTTP version\n",
" --libcurl Output libcurl code instead\n",
" -n Output notes after command line\n",
" -s Use short command line options\n",
" -v Add a verbose option to the command line\n";
exit;
}
sub manpage {
my ($p, $n, $desc) = @_;
if(!$n) {
$n = $p;
}
return sprintf("%s;%s;$desc", $p, $n);
}
my $usesamehttpversion = 1;
my $disableheadersnotseen = 1;
my $shellcompatible = 1; # may not been windows command prompt compat
my $uselongoptions = 1; # instead of short
my $uselibcurl = 0; # --libcurl
my $usehttp = 0;
while($ARGV[0]) {
if(($ARGV[0] eq "-h") || ($ARGV[0] eq "--help")) {
usage();
}
elsif($ARGV[0] eq "-a") {
$disableheadersnotseen = 0;
shift @ARGV;
}
elsif($ARGV[0] eq "-d") {
$usedocs = 1;
shift @ARGV;
}
elsif($ARGV[0] eq "-H") {
$usehttp = 1;
shift @ARGV;
}
elsif($ARGV[0] eq "-i") {
$usesamehttpversion = 0;
shift @ARGV;
}
elsif($ARGV[0] eq "--libcurl") {
$uselibcurl = 1;
shift @ARGV;
}
elsif($ARGV[0] eq "-n") {
$usenotes = 1;
shift @ARGV;
}
elsif($ARGV[0] eq "-s") {
$uselongoptions = 0;
shift @ARGV;
}
elsif($ARGV[0] eq "-v") {
$useverbose = 1;
shift @ARGV;
}
else {
usage();
}
}
my $state; # 0 is request-line, 1-headers, 2-body
my $line = 1;
while(<STDIN>) {
my $l = $_;
# discard CRs completely
$l =~ s/\r//g;
if(!$state) {
chomp $l;
if($l =~ /([^ ]*) +(.*) +(HTTP\/.*)/) {
$method = $1;
$path = $2;
$http = $3;
}
else {
$error="bad request-line";
last;
}
$state++;
}
elsif(1 == $state) {
chomp $l;
if($l =~ /([^:]*): *(.*)/) {
$header{lc($1)}=$2;
$exactcase{lc($1)}=$1; # to allow us to use it as read
}
elsif(length($l)<2) {
# body time
$state++;
}
else {
$error="illegal HTTP header on line $line";
last;
}
}
elsif(2 == $state) {
push @body, $l;
}
$line++;
}
if(!$header{'host'}) {
$error = "No Host: header makes it impossible to tell URL\n";
}
error:
if($error) {
print "Error: $error\n";
exit;
}
if($uselongoptions) {
$opt_data = "--data";
$opt_request = "--request";
$opt_head = "--head";
$opt_header = "--header";
$opt_user_agent = "--user-agent";
$opt_cookie = "--cookie";
$opt_verbose = "--verbose";
$opt_form = "--form";
$opt_user = "--user";
}
else {
$opt_data = "-d";
$opt_request = "-X";
$opt_head = "-I";
$opt_header = "-H";
$opt_user_agent = "-A";
$opt_cookie = "-b";
$opt_verbose = "-v";
$opt_form = "-F";
$opt_user = "-u";
}
my $httpver="";
my $disabledheaders="";
my $addedheaders="";
if($header{"content-type"} =~ /^multipart\/form-data;/) {
# multipart formpost, this is special
my $type = $header{"content-type"};
my $boundary = $type;
$boundary =~ s/.*boundary=(.*)/$1/;
my $inbound = $body[0];
chomp $inbound;
# a body MUST start with dash-dash-boundary
if("--$boundary" ne $inbound) {
$error = "unexpected multipart format";
goto error;
}
my $bline=1;
my %fheader;
my $fstate = 0;
my @fbody;
while($body[$bline]) {
my $l = $body[$bline];
if(0 == $fstate) {
# headers
chomp $l;
if($l =~ /([^:]*): *(.*)/) {
$fheader{lc($1)}=$2;
}
elsif(length($l)<2) {
# body time
$fstate++;
}
}
elsif($fstate) {
if($l =~ /^--$boundary/) {
# end of this part
my $cd = $fheader{'content-disposition'};
if(!$cd) {
$error = "multi-part without Content-Disposition: header!";
goto error;
}
# Content-Disposition: form-data; name="name"
# Content-Disposition: form-data; name="file"; filename="README.md"
if($cd =~ /^form-data; name=([^;]*)[;]? *(.*)/i) {
my ($n, $f)=($1, $2);
# name is with or without quotes
$n =~ s/\"//g;
if($f =~ /^filename=(.*)/) {
# filename is with or without quotes
$f = $1;
$f =~ s/\"//g;
}
if(!$multipart) {
push @docs, manpage("-F", $opt_form, "send a multipart formpost");
}
if(!$f) {
my $fbody = join("", @fbody);
$fbody =~ s/[ \n\r]+\z//g;
$fbody =~ s/([\\\$\"\'\`])/\\$1/g;
$multipart .= "$opt_form $n=\"$fbody\" ";
@fbody="";
}
else {
# file name was present
$multipart .= "$opt_form $n=\@$f ";
}
}
$fstate = 0;
%fheader = 0;
$bline++;
next;
}
push @fbody, $l;
}
$bline++;
}
if($body[$bline-1] !~ /^--$boundary--/) {
print STDERR "bad last line?";
}
$header{"content-type"} = ""; # blank it
$do_multipart = 1;
}
elsif(length(join("", @body))) {
# TODO: escape the body
my $esc = join("", @body);
chomp $esc; # trim the final newline
if($shellcompatible) {
$esc =~ s/\n/ /g; # turn newlines into space!
$esc =~ s/\"/\\"/g; # escape double quotes
if(!$unixescaped) {
push @notes, "uses quotes suitable for *nix command lines";
$unixescaped++;
}
}
$usebody= sprintf("--data-binary \"%s\" ", $esc);
push @docs, manpage("--data-binary", "", "send this string as a body with POST");
}
if(uc($method) eq "HEAD") {
$usemethod = "$opt_head ";
push @docs, manpage("-I", $opt_head, "send a HEAD request");
}
elsif(uc($method) eq "POST") {
if(!$usebody && !$do_multipart) {
$usebody= sprintf("$opt_data \"\" ");
push @docs, manpage("-d", $opt_data, "send this string as a body with POST");
}
}
elsif(uc($method) eq "PUT") {
if(!$usebody) {
$usebody= sprintf("$opt_data \"\" ");
push @docs, manpage("-d", $opt_data, "send this string as a body with POST");
}
$usebody .= "$opt_request PUT ";
push @docs, manpage("-X", $opt_request, "replace the request method with this string");
}
elsif(uc($method) eq "OPTIONS") {
$usemethod .= "$opt_request OPTIONS ";
if($path !~ /^\//) {
# very special case
$requesttarget="--request-target \"$path\" ";
push @docs, manpage("--request-target", "",
"specify request target to use instead of using the URL's");
$path = "";
}
}
elsif(uc($method) ne "GET") {
$error = "unsupported HTTP method $method";
goto error;
}
if($usebody) {
# body is set, handle the content-type
if(!$header{"content-type"}) {
$disabledheaders .= "$opt_header Content-Type: ";
}
elsif(lc($header{"content-type"}) ne
"application/x-www-form-urlencoded") {
# custom
$ignore_contenttype = 1;
$addedheaders .= sprintf("$opt_header \"Content-Type: %s\" ",
$header{"content-type"});
}
elsif((lc($header{"content-type"}) eq
"application/x-www-form-urlencoded") && !$do_multipart &&
($method eq "POST")) {
# default for normal POST
$ignore_contenttype = 1;
}
}
if($usesamehttpversion) {
if(uc($http) eq "HTTP/1.1") {
$httpver = "--http1.1 ";
push @docs, manpage("--http1.1", "", "use HTTP protocol version 1.1");
}
elsif(uc($http) eq "HTTP/2") {
$httpver = "--http2 ";
push @docs, manpage("--http2", "", "use HTTP protocol version 2");
}
else {
$error = "unsupported HTTP version $http";
goto error;
}
}
if($disableheadersnotseen) {
if(!$header{'accept'}) {
$disabledheaders .= "$opt_header Accept: ";
}
if(!$header{'user-agent'}) {
$disabledheaders .= "$opt_header User-Agent: ";
}
}
if($do_multipart) {
if(!$header{lc("expect")}) {
# no expect header, disable it for us too since curl -F defaults to
# Expect: 100-continue
$disabledheaders .= "$opt_header Expect: ";
}
}
# go through the headers alphabetically just to make the order fixed
foreach my $h (sort keys %header) {
if(lc($h) eq "host") {
# We use Host: for the URL creation
}
elsif((lc($h) eq "authorization") &&
($header{'authorization'} =~ /^Basic (.*)/)) {
my $decoded = decode_base64($1);
$addedheaders .= sprintf("%s \"%s\" ", $opt_user, $decoded);
push @docs, manpage("-u", $opt_user, "use this user and password for Basic auth");
}
elsif(lc($h) eq "expect") {
# let curl do expect on its own
}
elsif(lc($h) eq "content-type" &&
($do_multipart || $ignore_contenttype)) {
# skip this for multipart
}
elsif(($h eq "accept-encoding") &&
($header{$h} =~ /gzip/)) {
push @docs, manpage("--compressed", "", "request a compressed response");
$addedheaders .= "--compressed ";
}
elsif((lc($h) eq "accept") &&
($header{"accept"} eq "*/*")) {
# ignore if set to */* as that's a curl default
}
elsif(lc($h) eq "content-length") {
# we don't set custom size, just usebody
}
else {
$exact = $exactcase{$h};
my $opt = sprintf("$opt_header \"%s: ", $exact);
if(lc($h) eq "user-agent") {
$opt = "$opt_user_agent \"";
push @docs, manpage("-A", $opt_user_agent, "use this custom User-Agent request header");
}
elsif(lc($h) eq "cookie") {
$opt = "$opt_cookie \"";
push @docs, manpage("-b", $opt_cookie, "pass on this custom Cookie: request header");
}
$addedheaders .= sprintf("%s%s\" ", $opt, $header{$h});
}
}
if($path =~ /[ &?]/) {
$url = sprintf "\"%s://%s%s\"", $usehttp ? "http" : "https", $header{'host'}, $path;
}
else {
$url = sprintf "%s://%s%s", $usehttp ? "http" : "https", $header{'host'}, $path;
}
if($disabledheaders || $addedheaders) {
push @docs, manpage("-H", $opt_header, "add, replace or remove HTTP headers from the request");
}
if($useverbose) {
$useverbose = "$opt_verbose ";
push @docs, manpage("-v", $opt_verbose, "show verbose output");
}
# This adds the -x option to prevent a curl request to actually go out to any
# remote server
my $lib="--libcurl - -x localhost:0 " if($uselibcurl);
my $curlcmd = sprintf "curl ${useverbose}${usemethod}${httpver}${disabledheaders}${addedheaders}${usebody}${multipart}${requesttarget}${lib}${url}";
if($uselibcurl) {
# this actually runs curl which will fail to connect so ignore errors
open(C, "$curlcmd 2>/dev/null|");
while(<C>) {
# skip CURLOPT_PROXY since that's only used to avoid network
if($_ !~ /CURLOPT_PROXY, /) {
print $_;
}
}
close(C);
}
else {
print "$curlcmd\n";
}
if($usenotes) {
print "---\n";
foreach my $n (@notes) {
print "$n\n";
}
}
if($usedocs) {
print "---\n";
foreach my $d (@docs) {
print "$d\n";
}
}