#!/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() { 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() { # 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"; } }