Skip to content

Commit

Permalink
create two new curl tool options to work with etags
Browse files Browse the repository at this point in the history
removing unnecessary custom files, putting header processing to header callback function

Adding new OutStruct for saving ETag to a file, and implementing function for saving

adding compare functionality in tool_operate, and waiting for response header callback in tool_cb_hdr

testing compare func

fixing minor issues with writing, and constructin If-None-Match etag header with different functions

adding tmp man pages, + fixing string formatting in etag-compare

fix typos in man option definition, import library for strtok_r

another fixes to satisfy builds

create two new curl tool options to work with etag

rewriting man pages, fixing issues mentioned in review

fix sscanf width restriction

lgtm complaint

init local variable, alloc memory for it

code pr quality fix

fixes after review, adding 2 tests for --etag-save and --etag-compare

type fix

add new test, fix build errors and leaks

type error
  • Loading branch information
Maros Priputen committed Nov 13, 2019
1 parent 674298d commit 6a0d7be
Show file tree
Hide file tree
Showing 15 changed files with 389 additions and 2 deletions.
2 changes: 2 additions & 0 deletions docs/cmdline-opts/Makefile.inc
Expand Up @@ -38,6 +38,8 @@ DPAGES = \
dump-header.d \
egd-file.d \
engine.d \
etag-save.d \
etag-compare.d \
expect100-timeout.d \
fail-early.d \
fail.d \
Expand Down
17 changes: 17 additions & 0 deletions docs/cmdline-opts/etag-compare.d
@@ -0,0 +1,17 @@
Long: etag-compare
Arg: <file>
Help: Pass an ETag from a file as a custom header
Protocols: HTTP
---
This option makes a conditional HTTP request for the specific
ETag read from the given file by sending a custom If-None-Match
header using the extracted ETag.

For correct results, make sure that specified file contains only a single
line with a desired ETag. An empty file is parsed as an empty ETag.

Use the option --etag-save to first save the ETag from a response, and
then use this option to compare using the saved ETag in a subsequent request.

\fCOMPARISON\fP: There are 2 types of comparison or ETags, Weak and Strong.
This option expects, and uses a strong comparison.
15 changes: 15 additions & 0 deletions docs/cmdline-opts/etag-save.d
@@ -0,0 +1,15 @@
Long: etag-save
Arg: <file>
Help: Parse ETag from a request and save it to a file
Protocols: HTTP
---
This option saves an HTTP ETag to the specified file. Etag is
usually part of headers returned by a request. When server sends an
ETag, it must be enveloped by a double quote. This option extracts the
ETag without the double quotes and saves it into the <file>.

A server can send a week ETag which is prefixed by "W/". This identifier
is not considered, and only relevant ETag between quotation marks is parsed.

It an ETag wasn't send by the server or it cannot be parsed, and empty
file is created.
54 changes: 54 additions & 0 deletions src/tool_cb_hdr.c
Expand Up @@ -59,6 +59,7 @@ size_t tool_header_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
struct HdrCbData *hdrcbdata = &per->hdrcbdata;
struct OutStruct *outs = &per->outs;
struct OutStruct *heads = &per->heads;
struct OutStruct *etag_save = &per->etag_save;
const char *str = ptr;
const size_t cb = size * nmemb;
const char *end = (char *)ptr + cb;
Expand Down Expand Up @@ -95,6 +96,59 @@ size_t tool_header_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
(void)fflush(heads->stream);
}

/*
* Write etag to file when --etag-save option is given.
* etag string that we want is enveloped in double quotes
*/
if(etag_save->config->etag_save_file && etag_save->stream) {
/* match only header that start with etag (case insensitive) */
if(curl_strnequal(str, "etag:", 5)) {
char *etag_h = NULL;
char *first = NULL;
char *last = NULL;
size_t etag_length = 0;

etag_h = ptr;
/* point to first occurence of double quote */
first = memchr(etag_h, '\"', cb);

/*
* if server side messed with the etag header and doesn't include
* double quotes around the etag, kindly exit with a warning
*/

if(!first) {
warnf(
etag_save->config->global,
"\nReceived header etag is missing double quote/s\n");
return 1;
}
else {
/* discard first double quote */
first++;
}

/* point to last occurence of double quote */
last = memchr(first, '\"', cb);

if(!last) {
warnf(
etag_save->config->global,
"\nReceived header etag is missing double quote/s\n");
return 1;
}

/* get length of desired etag */
etag_length = (size_t)last - (size_t)first;

fwrite(first, size, etag_length, etag_save->stream);
/* terminate with new line */
fputc('\n', etag_save->stream);
}

(void)fflush(etag_save->stream);
}

/*
* This callback sets the filename where output shall be written when
* curl options --remote-name (-O) and --remote-header-name (-J) have
Expand Down
1 change: 1 addition & 0 deletions src/tool_cb_hdr.h
Expand Up @@ -43,6 +43,7 @@ struct HdrCbData {
struct OperationConfig *config;
struct OutStruct *outs;
struct OutStruct *heads;
struct OutStruct *etag_save;
bool honor_cd_filename;
};

Expand Down
2 changes: 2 additions & 0 deletions src/tool_cfgable.c
Expand Up @@ -128,6 +128,8 @@ static void free_config_fields(struct OperationConfig *config)
Curl_safefree(config->pubkey);
Curl_safefree(config->hostpubmd5);
Curl_safefree(config->engine);
Curl_safefree(config->etag_save_file);
Curl_safefree(config->etag_compare_file);
Curl_safefree(config->request_target);
Curl_safefree(config->customrequest);
Curl_safefree(config->krblevel);
Expand Down
2 changes: 2 additions & 0 deletions src/tool_cfgable.h
Expand Up @@ -156,6 +156,8 @@ struct OperationConfig {
char *pubkey;
char *hostpubmd5;
char *engine;
char *etag_save_file;
char *etag_compare_file;
bool crlf;
char *customrequest;
char *krblevel;
Expand Down
10 changes: 10 additions & 0 deletions src/tool_getparam.c
Expand Up @@ -268,6 +268,8 @@ static const struct LongShort aliases[]= {
{"E9", "proxy-tlsv1", ARG_NONE},
{"EA", "socks5-basic", ARG_BOOL},
{"EB", "socks5-gssapi", ARG_BOOL},
{"EC", "etag-save", ARG_FILENAME},
{"ED", "etag-compare", ARG_FILENAME},
{"f", "fail", ARG_BOOL},
{"fa", "fail-early", ARG_BOOL},
{"fb", "styled-output", ARG_BOOL},
Expand Down Expand Up @@ -1696,6 +1698,14 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */
config->socks5_auth &= ~CURLAUTH_GSSAPI;
break;

case 'C':
GetStr(&config->etag_save_file, nextarg);
break;

case 'D':
GetStr(&config->etag_compare_file, nextarg);
break;

default: /* unknown flag */
return PARAM_OPTION_UNKNOWN;
}
Expand Down
4 changes: 4 additions & 0 deletions src/tool_help.c
Expand Up @@ -131,6 +131,10 @@ static const struct helptxt helptext[] = {
"EGD socket path for random data"},
{" --engine <name>",
"Crypto engine to use"},
{" --etag-save <file>",
"Get an ETag from response header and save it to a FILE"},
{" --etag-compare <file>",
"Get an ETag from a file and send a conditional request"},
{" --expect100-timeout <seconds>",
"How long to wait for 100-continue"},
{"-f, --fail",
Expand Down
101 changes: 101 additions & 0 deletions src/tool_operate.c
Expand Up @@ -644,6 +644,12 @@ static CURLcode post_per_transfer(struct GlobalConfig *global,
if(per->heads.alloc_filename)
Curl_safefree(per->heads.filename);

if(per->etag_save.fopened && per->etag_save.stream)
fclose(per->etag_save.stream);

if(per->etag_save.alloc_filename)
Curl_safefree(per->etag_save.filename);

curl_easy_cleanup(per->curl);
if(outs->alloc_filename)
free(outs->filename);
Expand Down Expand Up @@ -840,6 +846,7 @@ static CURLcode single_transfer(struct GlobalConfig *global,
struct OutStruct *outs;
struct InStruct *input;
struct OutStruct *heads;
struct OutStruct *etag_save;
struct HdrCbData *hdrcbdata = NULL;
CURL *curl = curl_easy_init();
result = add_per_transfer(&per);
Expand Down Expand Up @@ -888,6 +895,99 @@ static CURLcode single_transfer(struct GlobalConfig *global,
}
}

/* disallowing simultaneous use of --etag-save and --etag-compare */
if(config->etag_save_file && config->etag_compare_file) {
warnf(
config->global,
"Cannot use --etag-save and --etag-compare at the same time\n");

result = CURLE_UNKNOWN_OPTION;
break;
}

/* --etag-save */
etag_save = &per->etag_save;
etag_save->stream = stdout;
etag_save->config = config;
if(config->etag_save_file) {
/* open file for output: */
if(strcmp(config->etag_save_file, "-")) {
FILE *newfile = fopen(config->etag_save_file, "wb");
if(!newfile) {
warnf(
config->global,
"Failed to open %s\n", config->etag_save_file);

result = CURLE_WRITE_ERROR;
break;
}
else {
etag_save->filename = config->etag_save_file;
etag_save->s_isreg = TRUE;
etag_save->fopened = TRUE;
etag_save->stream = newfile;
}
}
else {
/* always use binary mode for protocol header output */
set_binmode(etag_save->stream);
}
}

/* --etag-compare */
if(config->etag_compare_file) {
char *etag_from_file = NULL;
char *header = NULL;
size_t file_size = 0;

/* open file for reading: */
FILE *file = fopen(config->etag_compare_file, FOPEN_READTEXT);
if(!file) {
warnf(
config->global,
"Failed to open %s\n", config->etag_compare_file);

result = CURLE_READ_ERROR;
break;
}

/* get file size */
fseek(file, 0, SEEK_END);
file_size = ftell(file);

/*
* check if file is empty, if it's not load etag
* else continue with empty etag
*/
if(file_size != 0) {
fseek(file, 0, SEEK_SET);
file2string(&etag_from_file, file);

header = aprintf("If-None-Match: \"%s\"", etag_from_file);
}
else {
header = aprintf("If-None-Match: \"\"");
}

if(!header) {
warnf(
config->global,
"Failed to allocate memory for custom etag header\n");

result = CURLE_OUT_OF_MEMORY;
break;
}

/* add Etag from file to list of custom headers */
add2list(&config->headers, header);

Curl_safefree(header);
Curl_safefree(etag_from_file);

if(file) {
fclose(file);
}
}

hdrcbdata = &per->hdrcbdata;

Expand Down Expand Up @@ -1768,6 +1868,7 @@ static CURLcode single_transfer(struct GlobalConfig *global,

hdrcbdata->outs = outs;
hdrcbdata->heads = heads;
hdrcbdata->etag_save = etag_save;
hdrcbdata->global = global;
hdrcbdata->config = config;

Expand Down
1 change: 1 addition & 0 deletions src/tool_operate.h
Expand Up @@ -47,6 +47,7 @@ struct per_transfer {
struct ProgressData progressbar;
struct OutStruct outs;
struct OutStruct heads;
struct OutStruct etag_save;
struct InStruct input;
struct HdrCbData hdrcbdata;
char errorbuffer[CURL_ERROR_SIZE];
Expand Down
3 changes: 1 addition & 2 deletions tests/data/Makefile.inc
Expand Up @@ -57,8 +57,7 @@ test298 test299 test300 test301 test302 test303 test304 test305 test306 \
test307 test308 test309 test310 test311 test312 test313 test314 test315 \
test316 test317 test318 test319 test320 test321 test322 test323 test324 \
test325 test326 test327 test328 test329 test330 test331 test332 test333 \
test334 test335 test336 test337 test338 \
test340 \
test334 test335 test336 test337 test338 test339 test340 test341 test342 \
\
test350 test351 test352 test353 test354 test355 test356 \
test393 test394 test395 \
Expand Down
63 changes: 63 additions & 0 deletions tests/data/test339
@@ -0,0 +1,63 @@
<testcase>
<info>
<keywords>
HTTP
HTTP GET
</keywords>
</info>
#
# Server-side
<reply>
<data nocheck="yes">
HTTP/1.1 200 funky chunky!
Server: fakeit/0.9 fakeitbad/1.0
Transfer-Encoding: chunked
Trailer: chunky-trailer
Connection: mooo
ETag: "asdf"

40
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
30
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
21;heresatest=moooo
cccccccccccccccccccccccccccccccc

0
chunky-trailer: header data

</data>
</reply>

#
# Client-side
<client>
<server>
http
</server>
<name>
Check if --etag-save saved correct etag to a file
</name>
<command>
http://%HOSTIP:%HTTPPORT/339 --etag-save log/etag339
</command>
</client>

#
# Verify data after the test has been "shot"
<verify>
<strip>
^User-Agent:.*
</strip>
<protocol>
GET /339 HTTP/1.1
Host: %HOSTIP:%HTTPPORT
Accept: */*

</protocol>
<file name="log/etag339">
asdf
</file>
</verify>

</testcase>

0 comments on commit 6a0d7be

Please sign in to comment.