diff --git a/docs/cmdline-opts/Makefile.inc b/docs/cmdline-opts/Makefile.inc index f8b5711271..afa3d7931f 100644 --- a/docs/cmdline-opts/Makefile.inc +++ b/docs/cmdline-opts/Makefile.inc @@ -5,7 +5,7 @@ # | (__| |_| | _ <| |___ # \___|\___/|_| \_\_____| # -# Copyright (C) 1998 - 2021, Daniel Stenberg, , et al. +# Copyright (C) 1998 - 2022, Daniel Stenberg, , et al. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms @@ -111,6 +111,7 @@ DPAGES = \ interface.d \ ipv4.d \ ipv6.d \ + json.d \ junk-session-cookies.d \ keepalive-time.d \ key-type.d \ diff --git a/docs/cmdline-opts/json.d b/docs/cmdline-opts/json.d new file mode 100644 index 0000000000..3bdb0a7769 --- /dev/null +++ b/docs/cmdline-opts/json.d @@ -0,0 +1,32 @@ +Long: json +Arg: +Help: HTTP POST JSON +Protocols: HTTP +See-also: data-binary data-raw +Mutexed: form head upload-file +Category: http post upload +Example: --json '{ "drink": "coffe" }' $URL +Example: --json '{ "drink":' --json ' "coffe" }' $URL +Example: --json @prepared $URL +Example: --json @- $URL < json.txt +Added: 7.82.0 +--- +Sends the specified JSON data in a POST request to the HTTP server. --json +works as a shortcut for passing on these three options: + + --data [arg] + --header "Content-Type: application/json" + --header "Accept: application/json" + +There is **no verification** that the passed in data is actual JSON or that +the syntax is correct. + +If you start the data with the letter @, the rest should be a file name to +read the data from, or a single dash (-) if you want curl to read the data +from stdin. Posting data from a file named \&'foobar' would thus be done with +--json @foobar and to instead read the data from stdin, use --json @-. + +If this option is used more than once on the same command line, the additional +data pieces will be concatenated to the previous before sending. + +The headers this option sets can be overriden with --header as usual. diff --git a/docs/options-in-versions b/docs/options-in-versions index e75df6e060..5d242b8ffc 100644 --- a/docs/options-in-versions +++ b/docs/options-in-versions @@ -99,6 +99,7 @@ --interface 7.3 --ipv4 (-4) 7.10.8 --ipv6 (-6) 7.10.8 +--json 7.82.0 --junk-session-cookies (-j) 7.9.7 --keepalive-time 7.18.0 --key 7.9.3 diff --git a/src/tool_cfgable.h b/src/tool_cfgable.h index 227b914e33..a06ef60281 100644 --- a/src/tool_cfgable.h +++ b/src/tool_cfgable.h @@ -7,7 +7,7 @@ * | (__| |_| | _ <| |___ * \___|\___/|_| \_\_____| * - * Copyright (C) 1998 - 2021, Daniel Stenberg, , et al. + * Copyright (C) 1998 - 2022, Daniel Stenberg, , et al. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms @@ -189,6 +189,7 @@ struct OperationConfig { bool proxydigest; bool proxybasic; bool proxyanyauth; + bool jsoned; /* added json content-type */ char *writeout; /* %-styled format string to output */ struct curl_slist *quote; struct curl_slist *postquote; diff --git a/src/tool_getparam.c b/src/tool_getparam.c index 14dca833cd..9bf1c594cf 100644 --- a/src/tool_getparam.c +++ b/src/tool_getparam.c @@ -230,6 +230,7 @@ static const struct LongShort aliases[]= { {"da", "data-ascii", ARG_STRING}, {"db", "data-binary", ARG_STRING}, {"de", "data-urlencode", ARG_STRING}, + {"df", "json", ARG_STRING}, {"D", "dump-header", ARG_FILENAME}, {"e", "referer", ARG_STRING}, {"E", "cert", ARG_FILENAME}, @@ -1386,7 +1387,7 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */ size_t size = 0; bool raw_mode = (subletter == 'r'); - if(subletter == 'e') { /* --data-urlencode*/ + if(subletter == 'e') { /* --data-urlencode */ /* [name]=[content], we encode the content part only * [name]@[file name] * @@ -1489,7 +1490,8 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */ "an empty POST.\n", nextarg); } - if(subletter == 'b') + if((subletter == 'b') || /* --data-binary */ + (subletter == 'f') /* --json */) /* forced binary */ err = file2memory(&postdata, &size, file); else { @@ -1516,6 +1518,8 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */ if(postdata) size = strlen(postdata); } + if(subletter == 'f') + config->jsoned = TRUE; #ifdef CURL_DOES_CONVERSIONS if(subletter != 'b') { @@ -1540,13 +1544,21 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */ return PARAM_NO_MEM; } memcpy(config->postfields, oldpost, (size_t)oldlen); - /* use byte value 0x26 for '&' to accommodate non-ASCII platforms */ - config->postfields[oldlen] = '\x26'; - memcpy(&config->postfields[oldlen + 1], postdata, size); - config->postfields[oldlen + 1 + size] = '\0'; + if(subletter != 'f') { + /* skip this treatment for --json */ + /* use byte value 0x26 for '&' to accommodate non-ASCII platforms */ + config->postfields[oldlen] = '\x26'; + memcpy(&config->postfields[oldlen + 1], postdata, size); + config->postfields[oldlen + 1 + size] = '\0'; + config->postfieldsize += size + 1; + } + else { + memcpy(&config->postfields[oldlen], postdata, size); + config->postfields[oldlen + size] = '\0'; + config->postfieldsize += size; + } Curl_safefree(oldpost); Curl_safefree(postdata); - config->postfieldsize += size + 1; } else { config->postfields = postdata; @@ -2367,6 +2379,7 @@ ParameterError parse_args(struct GlobalConfig *global, int argc, : NULL; result = getparameter(orig_opt, nextarg, &passarg, global, config); + curlx_unicodefree(nextarg); config = global->last; if(result == PARAM_NEXT_OPERATION) { diff --git a/src/tool_listhelp.c b/src/tool_listhelp.c index 448fc7cb30..3bca52c0e6 100644 --- a/src/tool_listhelp.c +++ b/src/tool_listhelp.c @@ -5,7 +5,7 @@ * | (__| |_| | _ <| |___ * \___|\___/|_| \_\_____| * - * Copyright (C) 1998 - 2021, Daniel Stenberg, , et al. + * Copyright (C) 1998 - 2022, Daniel Stenberg, , et al. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms @@ -298,6 +298,9 @@ const struct helptxt helptext[] = { {"-6, --ipv6", "Resolve names to IPv6 addresses", CURLHELP_CONNECTION | CURLHELP_DNS}, + {" --json ", + "HTTP POST JSON", + CURLHELP_HTTP | CURLHELP_POST | CURLHELP_UPLOAD}, {"-j, --junk-session-cookies", "Ignore session cookies read from file", CURLHELP_HTTP}, diff --git a/src/tool_paramhlp.c b/src/tool_paramhlp.c index 8ac6cf53e0..273805ef0d 100644 --- a/src/tool_paramhlp.c +++ b/src/tool_paramhlp.c @@ -5,7 +5,7 @@ * | (__| |_| | _ <| |___ * \___|\___/|_| \_\_____| * - * Copyright (C) 1998 - 2021, Daniel Stenberg, , et al. + * Copyright (C) 1998 - 2022, Daniel Stenberg, , et al. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms @@ -548,11 +548,45 @@ static char *my_useragent(void) return strdup(CURL_NAME "/" CURL_VERSION); } +#define isheadersep(x) ((((x)==':') || ((x)==';'))) + +/* + * inlist() returns true if the given 'checkfor' header is present in the + * header list. + */ +static bool inlist(const struct curl_slist *head, + const char *checkfor) +{ + size_t thislen = strlen(checkfor); + DEBUGASSERT(thislen); + DEBUGASSERT(checkfor[thislen-1] != ':'); + + for(; head; head = head->next) { + if(curl_strnequal(head->data, checkfor, thislen) && + isheadersep(head->data[thislen]) ) + return TRUE; + } + + return FALSE; +} + CURLcode get_args(struct OperationConfig *config, const size_t i) { CURLcode result = CURLE_OK; bool last = (config->next ? FALSE : TRUE); + if(config->jsoned) { + ParameterError err = PARAM_OK; + /* --json also implies json Content-Type: and Accept: headers - if + they are not set with -H */ + if(!inlist(config->headers, "Content-Type")) + err = add2list(&config->headers, "Content-Type: application/json"); + if(!err && !inlist(config->headers, "Accept")) + err = add2list(&config->headers, "Accept: application/json"); + if(err) + return CURLE_OUT_OF_MEMORY; + } + /* Check we have a password for the given host user */ if(config->userpwd && !config->oauth_bearer) { result = checkpasswd("host", i, last, &config->userpwd); diff --git a/tests/data/Makefile.inc b/tests/data/Makefile.inc index d0f2a71806..59d46bc40b 100644 --- a/tests/data/Makefile.inc +++ b/tests/data/Makefile.inc @@ -63,7 +63,8 @@ test352 test353 test354 test355 test356 test357 test358 test359 test360 \ test361 test362 test363 test364 test365 test366 test367 test368 test369 \ test370 test371 test372 test373 test374 \ \ -test380 test381 \ +test380 test381 test383 test384 test385 test386 \ +\ test392 test393 test394 test395 test396 test397 \ \ test400 test401 test402 test403 test404 test405 test406 test407 test408 \ diff --git a/tests/data/test383 b/tests/data/test383 new file mode 100644 index 0000000000..6ba8f5e2db --- /dev/null +++ b/tests/data/test383 @@ -0,0 +1,56 @@ + + + +HTTP +HTTP POST +--json + + +# +# Server-side + + +HTTP/1.1 200 OK +Date: Tue, 09 Nov 2010 14:49:00 GMT +Server: test-server/fake +Last-Modified: Tue, 13 Jun 2000 12:10:00 GMT +ETag: "21025-dc7-39462498" +Accept-Ranges: bytes +Content-Length: 6 +Connection: close +Content-Type: text/html +Funny-head: yesyes + +-foo- + + + +# +# Client-side + + +http + + +HTTP with --json + + +--json '{ "drink": "coffe" }' http://%HOSTIP:%HTTPPORT/%TESTNUMBER + + + +# +# Verify data after the test has been "shot" + + +POST /%TESTNUMBER HTTP/1.1 +Host: %HOSTIP:%HTTPPORT +User-Agent: curl/%VERSION +Content-Type: application/json +Accept: application/json +Content-Length: 20 + +{ "drink": "coffe" } + + + diff --git a/tests/data/test384 b/tests/data/test384 new file mode 100644 index 0000000000..9651f9f956 --- /dev/null +++ b/tests/data/test384 @@ -0,0 +1,59 @@ + + + +HTTP +HTTP POST +--json + + +# +# Server-side + + +HTTP/1.1 200 OK +Date: Tue, 09 Nov 2010 14:49:00 GMT +Server: test-server/fake +Last-Modified: Tue, 13 Jun 2000 12:10:00 GMT +ETag: "21025-dc7-39462498" +Accept-Ranges: bytes +Content-Length: 6 +Connection: close +Content-Type: text/html +Funny-head: yesyes + +-foo- + + + +# +# Client-side + + +http + + +HTTP with --json from stdin + + +{ "drink": "coffe" } + + +--json @- http://%HOSTIP:%HTTPPORT/%TESTNUMBER -H "Accept: foobar/*" + + + +# +# Verify data after the test has been "shot" + + +POST /%TESTNUMBER HTTP/1.1 +Host: %HOSTIP:%HTTPPORT +User-Agent: curl/%VERSION +Accept: foobar/* +Content-Type: application/json +Content-Length: 21 + +{ "drink": "coffe" } + + + diff --git a/tests/data/test385 b/tests/data/test385 new file mode 100644 index 0000000000..ee543fa64f --- /dev/null +++ b/tests/data/test385 @@ -0,0 +1,56 @@ + + + +HTTP +HTTP POST +--json + + +# +# Server-side + + +HTTP/1.1 200 OK +Date: Tue, 09 Nov 2010 14:49:00 GMT +Server: test-server/fake +Last-Modified: Tue, 13 Jun 2000 12:10:00 GMT +ETag: "21025-dc7-39462498" +Accept-Ranges: bytes +Content-Length: 6 +Connection: close +Content-Type: text/html +Funny-head: yesyes + +-foo- + + + +# +# Client-side + + +http + + +HTTP with --json x 2 + + +--json '{ "drink": "coffe",' --json ' "crunch": "cookie" }' http://%HOSTIP:%HTTPPORT/%TESTNUMBER -H "Content-Type: drinks/hot" + + + +# +# Verify data after the test has been "shot" + + +POST /%TESTNUMBER HTTP/1.1 +Host: %HOSTIP:%HTTPPORT +User-Agent: curl/%VERSION +Content-Type: drinks/hot +Accept: application/json +Content-Length: 40 + +{ "drink": "coffe", "crunch": "cookie" } + + + diff --git a/tests/data/test386 b/tests/data/test386 new file mode 100644 index 0000000000..016dc466de --- /dev/null +++ b/tests/data/test386 @@ -0,0 +1,74 @@ + + + +HTTP +HTTP POST +--json + + +# +# Server-side + + +HTTP/1.1 200 OK +Date: Tue, 09 Nov 2010 14:49:00 GMT +Server: test-server/fake +Last-Modified: Tue, 13 Jun 2000 12:10:00 GMT +ETag: "21025-dc7-39462498" +Accept-Ranges: bytes +Content-Length: 6 +Connection: close +Content-Type: text/html +Funny-head: yesyes + +-foo- + + +HTTP/1.1 200 OK +Date: Tue, 09 Nov 2010 14:49:00 GMT +Server: test-server/fake +Last-Modified: Tue, 13 Jun 2000 12:10:00 GMT +ETag: "21025-dc7-39462498" +Accept-Ranges: bytes +Content-Length: 6 +Connection: close +Content-Type: text/html +Funny-head: yesyes + +hello + + + +# +# Client-side + + +http + + +HTTP with --json + --next + + +--json '{ "drink": "coffe" }' http://%HOSTIP:%HTTPPORT/%TESTNUMBER --next http://%HOSTIP:%HTTPPORT/%TESTNUMBER0002 + + + +# +# Verify data after the test has been "shot" + + +POST /%TESTNUMBER HTTP/1.1 +Host: %HOSTIP:%HTTPPORT +User-Agent: curl/%VERSION +Content-Type: application/json +Accept: application/json +Content-Length: 20 + +{ "drink": "coffe" }GET /%TESTNUMBER0002 HTTP/1.1 +Host: %HOSTIP:%HTTPPORT +User-Agent: curl/%VERSION +Accept: */* + + + +