|
|
@ -0,0 +1,426 @@ |
|
|
|
#!/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"; |
|
|
|
} |
|
|
|
} |