/*************************************************************************** * _ _ ____ _ * Project ___| | | | _ \| | * / __| | | | |_) | | * | (__| |_| | _ <| |___ * \___|\___/|_| \_\_____| * * Copyright (C) 1998 - 2017, Daniel Stenberg, <daniel@haxx.se>, et al. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms * are also available at https://curl.haxx.se/docs/copyright.html. * * You may opt to use, copy, modify, merge, publish, distribute and/or sell * copies of the Software, and permit persons to whom the Software is * furnished to do so, under the terms of the COPYING file. * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY * KIND, either express or implied. * ***************************************************************************/ #include "tool_setup.h" #include "mime.h" #include "strcase.h" #define ENABLE_CURLX_PRINTF /* use our own printf() functions */ #include "curlx.h" #include "tool_cfgable.h" #include "tool_convert.h" #include "tool_msgs.h" #include "tool_binmode.h" #include "tool_getparam.h" #include "tool_paramhlp.h" #include "tool_formparse.h" #include "memdebug.h" /* keep this as LAST include */ /* Stdin parameters. */ typedef struct { char *data; /* Memory data. */ curl_off_t origin; /* File read origin offset. */ curl_off_t size; /* Data size. */ curl_off_t curpos; /* Current read position. */ } standard_input; /* * helper function to get a word from form param * after call get_parm_word, str either point to string end * or point to any of end chars. */ static char *get_param_word(char **str, char **end_pos, char endchar) { char *ptr = *str; char *word_begin = NULL; char *ptr2; char *escape = NULL; /* the first non-space char is here */ word_begin = ptr; if(*ptr == '"') { ++ptr; while(*ptr) { if(*ptr == '\\') { if(ptr[1] == '\\' || ptr[1] == '"') { /* remember the first escape position */ if(!escape) escape = ptr; /* skip escape of back-slash or double-quote */ ptr += 2; continue; } } if(*ptr == '"') { *end_pos = ptr; if(escape) { /* has escape, we restore the unescaped string here */ ptr = ptr2 = escape; do { if(*ptr == '\\' && (ptr[1] == '\\' || ptr[1] == '"')) ++ptr; *ptr2++ = *ptr++; } while(ptr < *end_pos); *end_pos = ptr2; } while(*ptr && *ptr != ';' && *ptr != endchar) ++ptr; *str = ptr; return word_begin + 1; } ++ptr; } /* end quote is missing, treat it as non-quoted. */ ptr = word_begin; } while(*ptr && *ptr != ';' && *ptr != endchar) ++ptr; *str = *end_pos = ptr; return word_begin; } /* Append slist item and return -1 if failed. */ static int slist_append(struct curl_slist **plist, const char *data) { struct curl_slist *s = curl_slist_append(*plist, data); if(!s) return -1; *plist = s; return 0; } /* Read headers from a file and append to list. */ static int read_field_headers(struct OperationConfig *config, const char *filename, FILE *fp, struct curl_slist **pheaders) { size_t hdrlen = 0; size_t pos = 0; int c; bool incomment = FALSE; int lineno = 1; char hdrbuf[999]; /* Max. header length + 1. */ for(;;) { c = getc(fp); if(c == EOF || (!pos && !ISSPACE(c))) { /* Strip and flush the current header. */ while(hdrlen && ISSPACE(hdrbuf[hdrlen - 1])) hdrlen--; if(hdrlen) { hdrbuf[hdrlen] = '\0'; if(slist_append(pheaders, hdrbuf)) { fprintf(config->global->errors, "Out of memory for field headers!\n"); return -1; } hdrlen = 0; } } switch(c) { case EOF: if(ferror(fp)) { fprintf(config->global->errors, "Header file %s read error: %s\n", filename, strerror(errno)); return -1; } return 0; /* Done. */ case '\r': continue; /* Ignore. */ case '\n': pos = 0; incomment = FALSE; lineno++; continue; case '#': if(!pos) incomment = TRUE; break; } pos++; if(!incomment) { if(hdrlen == sizeof hdrbuf - 1) { warnf(config->global, "File %s line %d: header too long (truncated)\n", filename, lineno); c = ' '; } if(hdrlen <= sizeof hdrbuf - 1) hdrbuf[hdrlen++] = (char) c; } } /* NOTREACHED */ } static int get_param_part(struct OperationConfig *config, char endchar, char **str, char **pdata, char **ptype, char **pfilename, char **pencoder, struct curl_slist **pheaders) { char *p = *str; char *type = NULL; char *filename = NULL; char *encoder = NULL; char *endpos; char *tp; char sep; char type_major[128] = ""; char type_minor[128] = ""; char *endct = NULL; struct curl_slist *headers = NULL; if(ptype) *ptype = NULL; if(pfilename) *pfilename = NULL; if(pheaders) *pheaders = NULL; if(pencoder) *pencoder = NULL; while(ISSPACE(*p)) p++; tp = p; *pdata = get_param_word(&p, &endpos, endchar); /* If not quoted, strip trailing spaces. */ if(*pdata == tp) while(endpos > *pdata && ISSPACE(endpos[-1])) endpos--; sep = *p; *endpos = '\0'; while(sep == ';') { while(ISSPACE(*++p)) ; if(!endct && checkprefix("type=", p)) { for(p += 5; ISSPACE(*p); p++) ; /* set type pointer */ type = p; /* verify that this is a fine type specifier */ if(2 != sscanf(type, "%127[^/ ]/%127[^;, \n]", type_major, type_minor)) { warnf(config->global, "Illegally formatted content-type field!\n"); curl_slist_free_all(headers); return -1; /* illegal content-type syntax! */ } /* now point beyond the content-type specifier */ p = type + strlen(type_major) + strlen(type_minor) + 1; for(endct = p; *p && *p != ';' && *p != endchar; p++) if(!ISSPACE(*p)) endct = p + 1; sep = *p; } else if(checkprefix("filename=", p)) { if(endct) { *endct = '\0'; endct = NULL; } for(p += 9; ISSPACE(*p); p++) ; tp = p; filename = get_param_word(&p, &endpos, endchar); /* If not quoted, strip trailing spaces. */ if(filename == tp) while(endpos > filename && ISSPACE(endpos[-1])) endpos--; sep = *p; *endpos = '\0'; } else if(checkprefix("headers=", p)) { if(endct) { *endct = '\0'; endct = NULL; } p += 8; if(*p == '@' || *p == '<') { char *hdrfile; FILE *fp; /* Read headers from a file. */ do { p++; } while(ISSPACE(*p)); tp = p; hdrfile = get_param_word(&p, &endpos, endchar); /* If not quoted, strip trailing spaces. */ if(hdrfile == tp) while(endpos > hdrfile && ISSPACE(endpos[-1])) endpos--; sep = *p; *endpos = '\0'; /* TODO: maybe special fopen for VMS? */ fp = fopen(hdrfile, FOPEN_READTEXT); if(!fp) warnf(config->global, "Cannot read from %s: %s\n", hdrfile, strerror(errno)); else { int i = read_field_headers(config, hdrfile, fp, &headers); fclose(fp); if(i) { curl_slist_free_all(headers); return -1; } } } else { char *hdr; while(ISSPACE(*p)) p++; tp = p; hdr = get_param_word(&p, &endpos, endchar); /* If not quoted, strip trailing spaces. */ if(hdr == tp) while(endpos > hdr && ISSPACE(endpos[-1])) endpos--; sep = *p; *endpos = '\0'; if(slist_append(&headers, hdr)) { fprintf(config->global->errors, "Out of memory for field header!\n"); curl_slist_free_all(headers); return -1; } } } else if(checkprefix("encoder=", p)) { if(endct) { *endct = '\0'; endct = NULL; } for(p += 8; ISSPACE(*p); p++) ; tp = p; encoder = get_param_word(&p, &endpos, endchar); /* If not quoted, strip trailing spaces. */ if(encoder == tp) while(endpos > encoder && ISSPACE(endpos[-1])) endpos--; sep = *p; *endpos = '\0'; } else if(endct) { /* This is part of content type. */ for(endct = p; *p && *p != ';' && *p != endchar; p++) if(!ISSPACE(*p)) endct = p + 1; sep = *p; } else { /* unknown prefix, skip to next block */ char *unknown = get_param_word(&p, &endpos, endchar); sep = *p; *endpos = '\0'; if(*unknown) warnf(config->global, "skip unknown form field: %s\n", unknown); } } /* Terminate content type. */ if(endct) *endct = '\0'; if(ptype) *ptype = type; else if(type) warnf(config->global, "Field content type not allowed here: %s\n", type); if(pfilename) *pfilename = filename; else if(filename) warnf(config->global, "Field file name not allowed here: %s\n", filename); if(pencoder) *pencoder = encoder; else if(encoder) warnf(config->global, "Field encoder not allowed here: %s\n", encoder); if(pheaders) *pheaders = headers; else if(headers) { warnf(config->global, "Field headers not allowed here: %s\n", headers->data); curl_slist_free_all(headers); } *str = p; return sep & 0xFF; } /* Mime part callbacks for stdin. */ static size_t stdin_read(char *buffer, size_t size, size_t nitems, void *arg) { standard_input *sip = (standard_input *) arg; curl_off_t bytesleft; (void) size; /* Always 1: ignored. */ if(sip->curpos >= sip->size) return 0; /* At eof. */ bytesleft = sip->size - sip->curpos; if((curl_off_t) nitems > bytesleft) nitems = (size_t) bytesleft; if(sip->data) { /* Return data from memory. */ memcpy(buffer, sip->data + (size_t) sip->curpos, nitems); } else { /* Read from stdin. */ nitems = fread(buffer, 1, nitems, stdin); } sip->curpos += nitems; return nitems; } static int stdin_seek(void *instream, curl_off_t offset, int whence) { standard_input *sip = (standard_input *) instream; switch(whence) { case SEEK_CUR: offset += sip->curpos; break; case SEEK_END: offset += sip->size; break; } if(offset < 0) return CURL_SEEKFUNC_CANTSEEK; if(!sip->data) { if(fseek(stdin, (long) (offset + sip->origin), SEEK_SET)) return CURL_SEEKFUNC_CANTSEEK; } sip->curpos = offset; return CURL_SEEKFUNC_OK; } static void stdin_free(void *ptr) { standard_input *sip = (standard_input *) ptr; Curl_safefree(sip->data); free(sip); } /* Set a part's data from a file, taking care about the pseudo filename "-" as * a shortcut to read stdin: if so, use a callback to read OUR stdin (to * workaround Windows DLL file handle caveat). * If stdin is a regular file opened in binary mode, save current offset as * origin for rewind and do not buffer data. Else read to EOF and keep in * memory. In all cases, compute the stdin data size. */ static CURLcode file_or_stdin(curl_mimepart *part, const char *file) { standard_input *sip = NULL; int fd = -1; CURLcode result = CURLE_OK; struct_stat sbuf; if(strcmp(file, "-")) return curl_mime_filedata(part, file); sip = (standard_input *) malloc(sizeof *sip); if(!sip) return CURLE_OUT_OF_MEMORY; memset((char *) sip, 0, sizeof *sip); set_binmode(stdin); /* If stdin is a regular file, do not buffer data but read it when needed. */ fd = fileno(stdin); sip->origin = ftell(stdin); if(fd >= 0 && sip->origin >= 0 && !fstat(fd, &sbuf) && #ifdef __VMS sbuf.st_fab_rfm != FAB$C_VAR && sbuf.st_fab_rfm != FAB$C_VFC && #endif S_ISREG(sbuf.st_mode)) { sip->size = sbuf.st_size - sip->origin; if(sip->size < 0) sip->size = 0; } else { /* Not suitable for direct use, buffer stdin data. */ size_t stdinsize = 0; sip->origin = 0; if(file2memory(&sip->data, &stdinsize, stdin) != PARAM_OK) result = CURLE_OUT_OF_MEMORY; else { if(!stdinsize) sip->data = NULL; /* Has been freed if no data. */ sip->size = stdinsize; if(ferror(stdin)) result = CURLE_READ_ERROR; } } /* Set remote file name. */ if(!result) result = curl_mime_filename(part, file); /* Set part's data from callback. */ if(!result) result = curl_mime_data_cb(part, sip->size, stdin_read, stdin_seek, stdin_free, sip); if(result) stdin_free(sip); return result; } /*************************************************************************** * * formparse() * * Reads a 'name=value' parameter and builds the appropriate linked list. * * Specify files to upload with 'name=@filename', or 'name=@"filename"' * in case the filename contain ',' or ';'. Supports specified * given Content-Type of the files. Such as ';type=<content-type>'. * * If literal_value is set, any initial '@' or '<' in the value string * loses its special meaning, as does any embedded ';type='. * * You may specify more than one file for a single name (field). Specify * multiple files by writing it like: * * 'name=@filename,filename2,filename3' * * or use double-quotes quote the filename: * * 'name=@"filename","filename2","filename3"' * * If you want content-types specified for each too, write them like: * * 'name=@filename;type=image/gif,filename2,filename3' * * If you want custom headers added for a single part, write them in a separate * file and do like this: * * 'name=foo;headers=@headerfile' or why not * 'name=@filemame;headers=@headerfile' * * To upload a file, but to fake the file name that will be included in the * formpost, do like this: * * 'name=@filename;filename=/dev/null' or quote the faked filename like: * 'name=@filename;filename="play, play, and play.txt"' * * If filename/path contains ',' or ';', it must be quoted by double-quotes, * else curl will fail to figure out the correct filename. if the filename * tobe quoted contains '"' or '\', '"' and '\' must be escaped by backslash. * * This function uses curl_formadd to fulfill it's job. Is heavily based on * the old curl_formparse code. * ***************************************************************************/ int formparse(struct OperationConfig *config, const char *input, curl_mime **mimepost, curl_mime **mimecurrent, bool literal_value) { /* input MUST be a string in the format 'name=contents' and we'll build a linked list with the info */ char *name = NULL; char *contents = NULL; char *contp; char *data; char *type = NULL; char *filename = NULL; char *encoder = NULL; struct curl_slist *headers = NULL; curl_mimepart *part = NULL; CURLcode res; int sep = '\0'; /* Allocate the main mime structure if needed. */ if(!*mimepost) { *mimepost = curl_mime_init(config->easy); if(!*mimepost) { warnf(config->global, "curl_mime_init failed!\n"); return 1; } *mimecurrent = *mimepost; } /* Make a copy we can overwrite. */ contents = strdup(input); if(!contents) { fprintf(config->global->errors, "out of memory\n"); return 2; } /* Scan for the end of the name. */ contp = strchr(contents, '='); if(contp) { if(contp > contents) name = contents; *contp++ = '\0'; if(*contp == '(' && !literal_value) { curl_mime *subparts; /* Starting a multipart. */ sep = get_param_part(config, '\0', &contp, &data, &type, NULL, NULL, &headers); if(sep < 0) { Curl_safefree(contents); return 3; } subparts = curl_mime_init(config->easy); if(!subparts) { warnf(config->global, "curl_mime_init failed!\n"); curl_slist_free_all(headers); Curl_safefree(contents); return 4; } part = curl_mime_addpart(*mimecurrent); if(!part) { warnf(config->global, "curl_mime_addpart failed!\n"); curl_mime_free(subparts); curl_slist_free_all(headers); Curl_safefree(contents); return 5; } if(curl_mime_subparts(part, subparts)) { warnf(config->global, "curl_mime_subparts failed!\n"); curl_mime_free(subparts); curl_slist_free_all(headers); Curl_safefree(contents); return 6; } *mimecurrent = subparts; if(curl_mime_headers(part, headers, 1)) { warnf(config->global, "curl_mime_headers failed!\n"); curl_slist_free_all(headers); Curl_safefree(contents); return 7; } if(curl_mime_type(part, type)) { warnf(config->global, "curl_mime_type failed!\n"); Curl_safefree(contents); return 8; } } else if(!name && !strcmp(contp, ")") && !literal_value) { /* Ending a mutipart. */ if(*mimecurrent == *mimepost) { warnf(config->global, "no multipart to terminate!\n"); Curl_safefree(contents); return 9; } *mimecurrent = (*mimecurrent)->parent->parent; } else if('@' == contp[0] && !literal_value) { /* we use the @-letter to indicate file name(s) */ curl_mime *subparts = NULL; do { /* since this was a file, it may have a content-type specifier at the end too, or a filename. Or both. */ ++contp; sep = get_param_part(config, ',', &contp, &data, &type, &filename, &encoder, &headers); if(sep < 0) { if(subparts != *mimecurrent) curl_mime_free(subparts); Curl_safefree(contents); return 10; } /* now contp point to comma or string end. If more files to come, make sure we have multiparts. */ if(!subparts) { if(sep != ',') /* If there is a single file. */ subparts = *mimecurrent; else { subparts = curl_mime_init(config->easy); if(!subparts) { warnf(config->global, "curl_mime_init failed!\n"); curl_slist_free_all(headers); Curl_safefree(contents); return 11; } } } /* Allocate a part for that file. */ part = curl_mime_addpart(subparts); if(!part) { warnf(config->global, "curl_mime_addpart failed!\n"); if(subparts != *mimecurrent) curl_mime_free(subparts); curl_slist_free_all(headers); Curl_safefree(contents); return 12; } /* Set part headers. */ if(curl_mime_headers(part, headers, 1)) { warnf(config->global, "curl_mime_headers failed!\n"); if(subparts != *mimecurrent) curl_mime_free(subparts); curl_slist_free_all(headers); Curl_safefree(contents); return 13; } /* Setup file in part. */ res = file_or_stdin(part, data); if(res) { warnf(config->global, "setting file %s failed!\n", data); if(res != CURLE_READ_ERROR) { if(subparts != *mimecurrent) curl_mime_free(subparts); Curl_safefree(contents); return 14; } } if(filename && curl_mime_filename(part, filename)) { warnf(config->global, "curl_mime_filename failed!\n"); if(subparts != *mimecurrent) curl_mime_free(subparts); Curl_safefree(contents); return 15; } if(curl_mime_type(part, type)) { warnf(config->global, "curl_mime_type failed!\n"); if(subparts != *mimecurrent) curl_mime_free(subparts); Curl_safefree(contents); return 16; } if(curl_mime_encoder(part, encoder)) { warnf(config->global, "curl_mime_encoder failed!\n"); if(subparts != *mimecurrent) curl_mime_free(subparts); Curl_safefree(contents); return 17; } /* *contp could be '\0', so we just check with the delimiter */ } while(sep); /* loop if there's another file name */ /* now we add the multiple files section */ if(subparts != *mimecurrent) { part = curl_mime_addpart(*mimecurrent); if(!part) { warnf(config->global, "curl_mime_addpart failed!\n"); curl_mime_free(subparts); Curl_safefree(contents); return 18; } if(curl_mime_subparts(part, subparts)) { warnf(config->global, "curl_mime_subparts failed!\n"); curl_mime_free(subparts); Curl_safefree(contents); return 19; } } } else { /* Allocate a mime part. */ part = curl_mime_addpart(*mimecurrent); if(!part) { warnf(config->global, "curl_mime_addpart failed!\n"); Curl_safefree(contents); return 20; } if(*contp == '<' && !literal_value) { ++contp; sep = get_param_part(config, '\0', &contp, &data, &type, NULL, &encoder, &headers); if(sep < 0) { Curl_safefree(contents); return 21; } /* Set part headers. */ if(curl_mime_headers(part, headers, 1)) { warnf(config->global, "curl_mime_headers failed!\n"); curl_slist_free_all(headers); Curl_safefree(contents); return 22; } /* Setup file in part. */ res = file_or_stdin(part, data); if(res) { warnf(config->global, "setting file %s failed!\n", data); if(res != CURLE_READ_ERROR) { Curl_safefree(contents); return 23; } } } else { if(literal_value) data = contp; else { sep = get_param_part(config, '\0', &contp, &data, &type, &filename, &encoder, &headers); if(sep < 0) { Curl_safefree(contents); return 24; } } /* Set part headers. */ if(curl_mime_headers(part, headers, 1)) { warnf(config->global, "curl_mime_headers failed!\n"); curl_slist_free_all(headers); Curl_safefree(contents); return 25; } #ifdef CURL_DOES_CONVERSIONS if(convert_to_network(data, strlen(data))) { warnf(config->global, "curl_formadd failed!\n"); Curl_safefree(contents); return 26; } #endif if(curl_mime_data(part, data, CURL_ZERO_TERMINATED)) { warnf(config->global, "curl_mime_data failed!\n"); Curl_safefree(contents); return 27; } } if(curl_mime_filename(part, filename)) { warnf(config->global, "curl_mime_filename failed!\n"); Curl_safefree(contents); return 28; } if(curl_mime_type(part, type)) { warnf(config->global, "curl_mime_type failed!\n"); Curl_safefree(contents); return 29; } if(curl_mime_encoder(part, encoder)) { warnf(config->global, "curl_mime_encoder failed!\n"); Curl_safefree(contents); return 30; } if(sep) { *contp = (char) sep; warnf(config->global, "garbage at end of field specification: %s\n", contp); } } /* Set part name. */ if(name && curl_mime_name(part, name)) { warnf(config->global, "curl_mime_name failed!\n"); Curl_safefree(contents); return 31; } } else { warnf(config->global, "Illegally formatted input field!\n"); Curl_safefree(contents); return 32; } Curl_safefree(contents); return 0; }