├── .gitignore ├── LICENSE ├── README.md ├── h2c ├── h2c.html └── html /.gitignore: -------------------------------------------------------------------------------- 1 | !Build/ 2 | .last_cover_stats 3 | /META.yml 4 | /META.json 5 | /MYMETA.* 6 | *.o 7 | *.pm.tdy 8 | *.bs 9 | 10 | # Devel::Cover 11 | cover_db/ 12 | 13 | # Devel::NYTProf 14 | nytprof.out 15 | 16 | # Dizt::Zilla 17 | /.build/ 18 | 19 | # Module::Build 20 | _build/ 21 | Build 22 | Build.bat 23 | 24 | # Module::Install 25 | inc/ 26 | 27 | # ExtUitls::MakeMaker 28 | /blib/ 29 | /_eumm/ 30 | /*.gz 31 | /Makefile 32 | /Makefile.old 33 | /MANIFEST.bak 34 | /pm_to_blib 35 | /*.zip 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Daniel Stenberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # h2c 2 | headers 2 curl. Provided a set of HTTP request headers, output the curl command line for generating that set. 3 | 4 | $ cat test 5 | HEAD / HTTP/1.1 6 | Host: curl.se 7 | User-Agent: moo 8 | Shoesize: 12 9 | 10 | $ ./h2c < test 11 | curl --head --http1.1 --header Accept: --user-agent "moo" --header "Shoesize: 12" https://curl.se/ 12 | 13 | or a more complicated one: 14 | 15 | $ cat test2 16 | PUT /this is me HTTP/2 17 | Host: curl.se 18 | User-Agent: moo on you all 19 | Shoesize: 12 20 | Cookie: a=12; b=23 21 | Content-Type: application/json 22 | Content-Length: 57 23 | 24 | {"I do not speak": "jason"} 25 | {"I do not write": "either"} 26 | 27 | $ ./h2c < test2 28 | curl --http2 --header Accept: --user-agent "moo on you all" --header "shoesize: 12" --cookie "a=12; b=23" --header "content-type: application/json" --data-binary "{\"I do not speak\": \"jason\"} {\"I do not write\": \"either\"}" --request PUT "https://curl.se/this is me" 29 | 30 | multipart! 31 | 32 | $ cat multipart 33 | POST /upload HTTP/1.1 34 | Host: example.com 35 | User-Agent: curl/7.55.0 36 | Accept: */* 37 | Content-Length: 1236 38 | Expect: 100-continue 39 | Content-Type: multipart/form-data; boundary=------------------------2494bcbbb6e66a98 40 | 41 | --------------------------2494bcbbb6e66a98 42 | Content-Disposition: form-data; name="name" 43 | 44 | moo 45 | --------------------------2494bcbbb6e66a98 46 | Content-Disposition: form-data; name="file"; filename="README.md" 47 | Content-Type: application/octet-stream 48 | 49 | contents 50 | 51 | --------------------------2494bcbbb6e66a98-- 52 | 53 | $ ./h2c < multipart 54 | curl --http1.1 --user-agent "curl/7.55.0" --form name=moo --form file=@README.md https://example.com/upload 55 | 56 | authentication 57 | 58 | $ cat basic 59 | GET /index.html HTTP/2 60 | Host: example.com 61 | Authorization: Basic aGVsbG86eW91Zm9vbA== 62 | Accept: */* 63 | 64 | $ ./h2c < basic 65 | curl --http2 --header User-Agent: --user "hello:youfool" https://example.com/index.html 66 | -------------------------------------------------------------------------------- /h2c: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use MIME::Base64; 4 | 5 | sub usage { 6 | print "h2c.pl [options] < file \n", 7 | " -a Allow curl's default headers\n", 8 | " -d Output man page HTML links after command line\n", 9 | " -h Show short help\n", 10 | " -H Output HTTP generated URLs instead\n", 11 | " -i Ignore HTTP version\n", 12 | " --libcurl Output libcurl code instead\n", 13 | " -n Output notes after command line\n", 14 | " -s Use short command line options\n", 15 | " -v Add a verbose option to the command line\n"; 16 | exit; 17 | } 18 | 19 | sub manpage { 20 | my ($p, $n, $desc) = @_; 21 | if(!$n) { 22 | $n = $p; 23 | } 24 | return sprintf("%s;%s;$desc", $p, $n); 25 | } 26 | 27 | my $usesamehttpversion = 1; 28 | my $disableheadersnotseen = 1; 29 | my $shellcompatible = 1; # may not been windows command prompt compat 30 | my $uselongoptions = 1; # instead of short 31 | my $uselibcurl = 0; # --libcurl 32 | my $usehttp = 0; 33 | 34 | while($ARGV[0]) { 35 | if(($ARGV[0] eq "-h") || ($ARGV[0] eq "--help")) { 36 | usage(); 37 | } 38 | elsif($ARGV[0] eq "-a") { 39 | $disableheadersnotseen = 0; 40 | shift @ARGV; 41 | } 42 | elsif($ARGV[0] eq "-d") { 43 | $usedocs = 1; 44 | shift @ARGV; 45 | } 46 | elsif($ARGV[0] eq "-H") { 47 | $usehttp = 1; 48 | shift @ARGV; 49 | } 50 | elsif($ARGV[0] eq "-i") { 51 | $usesamehttpversion = 0; 52 | shift @ARGV; 53 | } 54 | elsif($ARGV[0] eq "--libcurl") { 55 | $uselibcurl = 1; 56 | shift @ARGV; 57 | } 58 | elsif($ARGV[0] eq "-n") { 59 | $usenotes = 1; 60 | shift @ARGV; 61 | } 62 | elsif($ARGV[0] eq "-s") { 63 | $uselongoptions = 0; 64 | shift @ARGV; 65 | } 66 | elsif($ARGV[0] eq "-v") { 67 | $useverbose = 1; 68 | shift @ARGV; 69 | } 70 | else { 71 | usage(); 72 | } 73 | } 74 | 75 | 76 | my $state; # 0 is request-line, 1-headers, 2-body 77 | my $line = 1; 78 | while() { 79 | my $l = $_; 80 | # discard CRs completely 81 | $l =~ s/\r//g; 82 | if(!$state) { 83 | chomp $l; 84 | if($l =~ /([^ ]*) +(.*) +(HTTP\/.*)/) { 85 | $method = $1; 86 | $path = $2; 87 | $http = $3; 88 | # convenience thing: convert spaces to %20 89 | $path =~ s/ /%20/g; 90 | } 91 | else { 92 | $error="bad request-line"; 93 | last; 94 | } 95 | $state++; 96 | } 97 | elsif(1 == $state) { 98 | chomp $l; 99 | if($l =~ /([^:]*): *(.*)/) { 100 | $header{lc($1)}=$2; 101 | $exactcase{lc($1)}=$1; # to allow us to use it as read 102 | } 103 | elsif(length($l)<2) { 104 | # body time 105 | $state++; 106 | } 107 | else { 108 | $error="illegal HTTP header on line $line"; 109 | last; 110 | } 111 | } 112 | elsif(2 == $state) { 113 | push @body, $l; 114 | } 115 | $line++; 116 | } 117 | 118 | if(!$header{'host'}) { 119 | $error = "No Host: header makes it impossible to tell URL\n"; 120 | } 121 | 122 | error: 123 | if($error) { 124 | print "Error: $error\n"; 125 | exit; 126 | } 127 | 128 | if($uselongoptions) { 129 | $opt_data = "--data"; 130 | $opt_request = "--request"; 131 | $opt_head = "--head"; 132 | $opt_header = "--header"; 133 | $opt_user_agent = "--user-agent"; 134 | $opt_cookie = "--cookie"; 135 | $opt_verbose = "--verbose"; 136 | $opt_form = "--form"; 137 | $opt_user = "--user"; 138 | } 139 | else { 140 | $opt_data = "-d"; 141 | $opt_request = "-X"; 142 | $opt_head = "-I"; 143 | $opt_header = "-H"; 144 | $opt_user_agent = "-A"; 145 | $opt_cookie = "-b"; 146 | $opt_verbose = "-v"; 147 | $opt_form = "-F"; 148 | $opt_user = "-u"; 149 | } 150 | 151 | my $httpver=""; 152 | my $disabledheaders=""; 153 | my $addedheaders=""; 154 | 155 | if($header{"content-type"} =~ /^multipart\/form-data;/) { 156 | # multipart formpost, this is special 157 | my $type = $header{"content-type"}; 158 | my $boundary = $type; 159 | $boundary =~ s/.*boundary=(.*)/$1/; 160 | my $inbound = $body[0]; 161 | chomp $inbound; 162 | # a body MUST start with dash-dash-boundary 163 | if("--$boundary" ne $inbound) { 164 | $error = "unexpected multipart format"; 165 | goto error; 166 | } 167 | my $bline=1; 168 | 169 | my %fheader; 170 | my $fstate = 0; 171 | my @fbody; 172 | while($body[$bline]) { 173 | my $l = $body[$bline]; 174 | if(0 == $fstate) { 175 | # headers 176 | chomp $l; 177 | if($l =~ /([^:]*): *(.*)/) { 178 | $fheader{lc($1)}=$2; 179 | } 180 | elsif(length($l)<2) { 181 | # body time 182 | $fstate++; 183 | } 184 | } 185 | elsif($fstate) { 186 | if($l =~ /^--$boundary/) { 187 | # end of this part 188 | my $cd = $fheader{'content-disposition'}; 189 | if(!$cd) { 190 | $error = "multi-part without Content-Disposition: header!"; 191 | goto error; 192 | } 193 | # Content-Disposition: form-data; name="name" 194 | # Content-Disposition: form-data; name="file"; filename="README.md" 195 | if($cd =~ /^form-data; name=([^;]*)[;]? *(.*)/i) { 196 | my ($n, $f)=($1, $2); 197 | # name is with or without quotes 198 | $n =~ s/\"//g; 199 | if($f =~ /^filename=(.*)/) { 200 | # filename is with or without quotes 201 | $f = $1; 202 | $f =~ s/\"//g; 203 | } 204 | if(!$multipart) { 205 | push @docs, manpage("-F", $opt_form, "send a multipart formpost"); 206 | } 207 | if(!$f) { 208 | my $fbody = join("", @fbody); 209 | $fbody =~ s/[ \n\r]+\z//g; 210 | $fbody =~ s/([\\\$\"\'\`])/\\$1/g; 211 | $multipart .= "$opt_form $n=\"$fbody\" "; 212 | @fbody=""; 213 | } 214 | else { 215 | # file name was present 216 | $multipart .= "$opt_form $n=\@$f "; 217 | } 218 | } 219 | $fstate = 0; 220 | %fheader = 0; 221 | $bline++; 222 | next; 223 | } 224 | push @fbody, $l; 225 | } 226 | $bline++; 227 | } 228 | if($body[$bline-1] !~ /^--$boundary--/) { 229 | print STDERR "bad last line?"; 230 | } 231 | 232 | $header{"content-type"} = ""; # blank it 233 | $do_multipart = 1; 234 | 235 | } 236 | elsif(length(join("", @body))) { 237 | # TODO: escape the body 238 | my $esc = join("", @body); 239 | chomp $esc; # trim the final newline 240 | if($shellcompatible) { 241 | $esc =~ s/\n/ /g; # turn newlines into space! 242 | $esc =~ s/\"/\\"/g; # escape double quotes 243 | if(!$unixescaped) { 244 | push @notes, "uses quotes suitable for *nix command lines"; 245 | $unixescaped++; 246 | } 247 | } 248 | $usebody= sprintf("--data-binary \"%s\" ", $esc); 249 | push @docs, manpage("--data-binary", "", "send this string as a body with POST"); 250 | } 251 | if(uc($method) eq "HEAD") { 252 | $usemethod = "$opt_head "; 253 | push @docs, manpage("-I", $opt_head, "send a HEAD request"); 254 | } 255 | elsif(uc($method) eq "POST") { 256 | if(!$usebody && !$do_multipart) { 257 | $usebody= sprintf("$opt_data \"\" "); 258 | push @docs, manpage("-d", $opt_data, "send this string as a body with POST"); 259 | } 260 | } 261 | elsif(uc($method) eq "PUT") { 262 | if(!$usebody) { 263 | $usebody= sprintf("$opt_data \"\" "); 264 | push @docs, manpage("-d", $opt_data, "send this string as a body with POST"); 265 | } 266 | $usebody .= "$opt_request PUT "; 267 | push @docs, manpage("-X", $opt_request, "replace the request method with this string"); 268 | } 269 | elsif(uc($method) eq "OPTIONS") { 270 | $usemethod .= "$opt_request OPTIONS "; 271 | if($path !~ /^\//) { 272 | # very special case 273 | $requesttarget="--request-target \"$path\" "; 274 | push @docs, manpage("--request-target", "", 275 | "specify request target to use instead of using the URL's"); 276 | $path = ""; 277 | } 278 | } 279 | elsif(uc($method) ne "GET") { 280 | $error = "unsupported HTTP method $method"; 281 | goto error; 282 | } 283 | 284 | if($usebody) { 285 | # body is set, handle the content-type 286 | if(!$header{"content-type"}) { 287 | $disabledheaders .= "$opt_header Content-Type: "; 288 | } 289 | elsif(lc($header{"content-type"}) ne 290 | "application/x-www-form-urlencoded") { 291 | # custom 292 | $ignore_contenttype = 1; 293 | $addedheaders .= sprintf("$opt_header \"Content-Type: %s\" ", 294 | $header{"content-type"}); 295 | } 296 | elsif((lc($header{"content-type"}) eq 297 | "application/x-www-form-urlencoded") && !$do_multipart && 298 | (uc($method) eq "POST")) { 299 | # default for normal POST 300 | $ignore_contenttype = 1; 301 | } 302 | } 303 | 304 | if($usesamehttpversion) { 305 | if(uc($http) eq "HTTP/1.1") { 306 | $httpver = "--http1.1 "; 307 | push @docs, manpage("--http1.1", "", "use HTTP protocol version 1.1"); 308 | } 309 | elsif(uc($http) eq "HTTP/2") { 310 | $httpver = "--http2 "; 311 | push @docs, manpage("--http2", "", "use HTTP protocol version 2"); 312 | } 313 | else { 314 | $error = "unsupported HTTP version $http"; 315 | goto error; 316 | } 317 | } 318 | if($disableheadersnotseen) { 319 | if(!$header{'accept'}) { 320 | $disabledheaders .= "$opt_header Accept: "; 321 | } 322 | if(!$header{'user-agent'}) { 323 | $disabledheaders .= "$opt_header User-Agent: "; 324 | } 325 | } 326 | 327 | if($do_multipart) { 328 | if(!$header{lc("expect")}) { 329 | # no expect header, disable it for us too since curl -F defaults to 330 | # Expect: 100-continue 331 | $disabledheaders .= "$opt_header Expect: "; 332 | } 333 | } 334 | 335 | # go through the headers alphabetically just to make the order fixed 336 | foreach my $h (sort keys %header) { 337 | if(lc($h) eq "host") { 338 | # We use Host: for the URL creation 339 | } 340 | elsif((lc($h) eq "authorization") && 341 | ($header{'authorization'} =~ /^Basic (.*)/)) { 342 | my $decoded = decode_base64($1); 343 | $addedheaders .= sprintf("%s \"%s\" ", $opt_user, $decoded); 344 | push @docs, manpage("-u", $opt_user, "use this user and password for Basic auth"); 345 | } 346 | elsif(lc($h) eq "expect") { 347 | # let curl do expect on its own 348 | } 349 | elsif(lc($h) eq "content-type" && 350 | ($do_multipart || $ignore_contenttype)) { 351 | # skip this for multipart 352 | } 353 | elsif(($h eq "accept-encoding") && 354 | ($header{$h} =~ /gzip/)) { 355 | push @docs, manpage("--compressed", "", "request a compressed response"); 356 | $addedheaders .= "--compressed "; 357 | } 358 | elsif((lc($h) eq "accept") && 359 | ($header{"accept"} eq "*/*")) { 360 | # ignore if set to */* as that's a curl default 361 | } 362 | elsif(lc($h) eq "content-length") { 363 | # we don't set custom size, just usebody 364 | } 365 | else { 366 | $exact = $exactcase{$h}; 367 | my $opt = sprintf("$opt_header \"%s: ", $exact); 368 | if(lc($h) eq "user-agent") { 369 | $opt = "$opt_user_agent \""; 370 | push @docs, manpage("-A", $opt_user_agent, "use this custom User-Agent request header"); 371 | } 372 | elsif(lc($h) eq "cookie") { 373 | $opt = "$opt_cookie \""; 374 | push @docs, manpage("-b", $opt_cookie, "pass on this custom Cookie: request header"); 375 | } 376 | $addedheaders .= sprintf("%s%s\" ", $opt, $header{$h}); 377 | } 378 | } 379 | 380 | if($path =~ /[&?]/) { 381 | $url = sprintf "\"%s://%s%s\"", $usehttp ? "http" : "https", $header{'host'}, $path; 382 | } 383 | else { 384 | $url = sprintf "%s://%s%s", 385 | $usehttp ? "http" : "https", 386 | $header{'host'}, $path; 387 | } 388 | 389 | if($disabledheaders || $addedheaders) { 390 | push @docs, manpage("-H", $opt_header, "add, replace or remove HTTP headers from the request"); 391 | } 392 | 393 | if($useverbose) { 394 | $useverbose = "$opt_verbose "; 395 | push @docs, manpage("-v", $opt_verbose, "show verbose output"); 396 | } 397 | 398 | # This adds the -x option to prevent a curl request to actually go out to any 399 | # remote server 400 | my $lib="--libcurl - -x localhost:0 " if($uselibcurl); 401 | my $curlcmd = "curl ${useverbose}${usemethod}${httpver}${disabledheaders}${addedheaders}${usebody}${multipart}${requesttarget}${lib}${url}"; 402 | 403 | if($uselibcurl) { 404 | # this actually runs curl which will fail to connect so ignore errors 405 | open(C, "$curlcmd 2>/dev/null|"); 406 | while() { 407 | # skip CURLOPT_PROXY since that's only used to avoid network 408 | if($_ !~ /CURLOPT_PROXY, /) { 409 | print $_; 410 | } 411 | } 412 | close(C); 413 | } 414 | else { 415 | print "$curlcmd\n"; 416 | } 417 | 418 | if($usenotes) { 419 | print "---\n"; 420 | foreach my $n (@notes) { 421 | print "$n\n"; 422 | } 423 | } 424 | 425 | if($usedocs) { 426 | print "---\n"; 427 | foreach my $d (@docs) { 428 | print "$d\n"; 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /h2c.html: -------------------------------------------------------------------------------- 1 |

2 | Paste the HTTP request into the text field and get the correspending curl 3 | command line for generating such a request. 4 |

5 |

6 |
7 | 10 | 11 | use short command line options 12 |
13 | enable verbose output 14 |
15 | allow curl's default headers 16 |
17 | insist on same HTTP version 18 |
19 | 20 | 21 |
22 | 23 |

24 | Command: 25 |

26 | %curl% 27 |
28 | 29 |

Documentation links for used options

30 | 33 | 34 |

35 | The generated command line assumes a HTTPS site and generates such a 36 | URL. There is typically nothing in the request that tells if HTTPS or HTTP 37 | was used. 38 |

39 | If you submit an illegally formatted HTTP request, chances are the shown 40 | curl command line might not reproduce it perfectly. curl is primarily made 41 | to generate fine HTTP. 42 | 43 |

Use this with curl!

44 |

45 | Save the HTTP request you want to send in a local file, then send it 46 | here like this: 47 | 48 |

49 | curl --data-urlencode http@file https://curl.haxx.se/h2c/ 50 |
51 |

52 | ... and you'll get a curl command line in response. 53 | 54 |

Privacy

55 |

56 | All data you submit to this web service will be crunched and converted on the 57 | curl server. To keep your HTTP requests private, download h2c and run it 58 | yourself locally. 59 | 60 |


61 | h2c on github 62 | 63 | Fork me on GitHub 64 | -------------------------------------------------------------------------------- /html: -------------------------------------------------------------------------------- 1 |

2 | Paste the HTTP request into the text field and get the correspending curl 3 | command line for generating such a request. 4 |

5 |

6 |
7 | 10 | 11 | use short command line options 12 |
13 | enable verbose output 14 |
15 | allow curl's default headers 16 |
17 | insist on same HTTP version 18 |
19 | 20 | 21 |
22 | 23 |

24 | Command: 25 |

26 | %curl% 27 |
28 | 29 |

Documentation links for used options

30 | 33 | 34 |

35 | The generated command line assumes a HTTPS site and generates such a 36 | URL. There is typically nothing in the request that tells if HTTPS or HTTP 37 | was used. 38 |

39 | If you submit an illegally formatted HTTP request, chances are the shown 40 | curl command line might not reproduce it perfectly. curl is primarily made 41 | to generate fine HTTP. 42 | 43 |

Use this with curl!

44 |

45 | Save the HTTP request you want to send in a local file, then send it 46 | here like this: 47 | 48 |

49 | curl --data-urlencode http@file https://curl.se/h2c/ 50 |
51 |

52 | ... and you'll get a curl command line in response. 53 | 54 |

Privacy

55 |

56 | All data you submit to this web service will be crunched and converted on the 57 | curl server. To keep your HTTP requests private, download h2c and run it 58 | yourself locally. 59 | 60 |


61 | h2c on github 62 | 63 | --------------------------------------------------------------------------------