/* * CGI form variable and array functions for CUPS. * * Copyright © 2007-2019 by Apple Inc. * Copyright © 1997-2005 by Easy Software Products. * * Licensed under Apache License v2.0. See the file "LICENSE" for more * information. */ /* * Include necessary headers... */ /*#define DEBUG*/ #include "cgi-private.h" #include /* * Session ID name */ #define CUPS_SID "org.cups.sid" /* * Data structure to hold all the CGI form variables and arrays... */ typedef struct /**** Form variable structure ****/ { char *name; /* Name of variable */ int nvalues, /* Number of values */ avalues; /* Number of values allocated */ char **values; /* Value(s) of variable */ } _cgi_var_t; /* * Local globals... */ static int num_cookies = 0;/* Number of cookies */ static cups_option_t *cookies = NULL;/* Cookies */ static int form_count = 0, /* Form variable count */ form_alloc = 0; /* Number of variables allocated */ static _cgi_var_t *form_vars = NULL; /* Form variables */ static cgi_file_t *form_file = NULL; /* Uploaded file */ /* * Local functions... */ static void cgi_add_variable(const char *name, int element, const char *value); static int cgi_compare_variables(const _cgi_var_t *v1, const _cgi_var_t *v2); static _cgi_var_t *cgi_find_variable(const char *name); static void cgi_initialize_cookies(void); static int cgi_initialize_get(void); static int cgi_initialize_multipart(const char *boundary); static int cgi_initialize_post(void); static int cgi_initialize_string(const char *data); static const char *cgi_passwd(const char *prompt); static const char *cgi_set_sid(void); static void cgi_sort_variables(void); static void cgi_unlink_file(void); /* * 'cgiCheckVariables()' - Check for the presence of "required" variables. * * Names may be separated by spaces and/or commas. */ int /* O - 1 if all variables present, 0 otherwise */ cgiCheckVariables(const char *names) /* I - Variables to look for */ { char name[255], /* Current variable name */ *s; /* Pointer in string */ const char *val; /* Value of variable */ int element; /* Array element number */ if (names == NULL) return (1); while (*names != '\0') { while (*names == ' ' || *names == ',') names ++; for (s = name; *names != '\0' && *names != ' ' && *names != ','; s ++, names ++) *s = *names; *s = 0; if (name[0] == '\0') break; if ((s = strrchr(name, '-')) != NULL) { *s = '\0'; element = atoi(s + 1) - 1; val = cgiGetArray(name, element); } else val = cgiGetVariable(name); if (val == NULL) return (0); if (*val == '\0') { free((void *)val); return (0); /* Can't be blank, either! */ } free((void *)val); } return (1); } /* * 'cgiClearVariables()' - Clear all form variables. */ void cgiClearVariables(void) { int i, j; /* Looping vars */ _cgi_var_t *v; /* Current variable */ fputs("DEBUG: cgiClearVariables called.\n", stderr); for (v = form_vars, i = form_count; i > 0; v ++, i --) { free(v->name); for (j = 0; j < v->nvalues; j ++) if (v->values[j]) free(v->values[j]); } form_count = 0; cgi_unlink_file(); } /* * 'cgiGetArray()' - Get an element from a form array. */ char * /* O - Element value or NULL */ cgiGetArray(const char *name, /* I - Name of array variable */ int element) /* I - Element number (0 to N) */ { _cgi_var_t *var; /* Pointer to variable */ if ((var = cgi_find_variable(name)) == NULL) return (NULL); if (element < 0 || element >= var->nvalues) return (NULL); if (var->values[element] == NULL) return (NULL); return (strdup(var->values[element])); } /* * 'cgiGetCookie()' - Get a cookie value. */ const char * /* O - Value or NULL */ cgiGetCookie(const char *name) /* I - Name of cookie */ { return (cupsGetOption(name, num_cookies, cookies)); } /* * 'cgiGetFile()' - Get the file (if any) that was submitted in the form. */ const cgi_file_t * /* O - Attached file or NULL */ cgiGetFile(void) { return (form_file); } /* * 'cgiGetSize()' - Get the size of a form array value. */ int /* O - Number of elements */ cgiGetSize(const char *name) /* I - Name of variable */ { _cgi_var_t *var; /* Pointer to variable */ if ((var = cgi_find_variable(name)) == NULL) return (0); return (var->nvalues); } /* * 'cgiGetVariable()' - Get a CGI variable from the database. * * Returns NULL if the variable doesn't exist. If the variable is an * array of values, returns the last element. */ char * /* O - Value of variable */ cgiGetVariable(const char *name) /* I - Name of variable */ { const _cgi_var_t *var; /* Returned variable */ var = cgi_find_variable(name); return ((var == NULL) ? NULL : strdup(var->values[var->nvalues - 1])); } /* * 'cgiInitialize()' - Initialize the CGI variable "database". */ int /* O - Non-zero if there was form data */ cgiInitialize(void) { const char *method, /* Form posting method */ *content_type, /* Content-Type of post data */ *cups_sid_cookie, /* SID cookie */ *cups_sid_form; /* SID form variable */ /* * Setup a password callback for authentication... */ cupsSetPasswordCB(cgi_passwd); /* * Set the locale so that times, etc. are formatted properly... */ setlocale(LC_ALL, ""); #ifdef DEBUG /* * Disable output buffering to find bugs... */ setbuf(stdout, NULL); #endif /* DEBUG */ /* * Get cookies... */ cgi_initialize_cookies(); if ((cups_sid_cookie = cgiGetCookie(CUPS_SID)) == NULL) { fputs("DEBUG: " CUPS_SID " cookie not found, initializing!\n", stderr); cups_sid_cookie = cgi_set_sid(); } fprintf(stderr, "DEBUG: " CUPS_SID " cookie is \"%s\"\n", cups_sid_cookie); /* * Get the request method (GET or POST)... */ method = getenv("REQUEST_METHOD"); content_type = getenv("CONTENT_TYPE"); if (!method) return (0); /* * Grab form data from the corresponding location... */ if (!_cups_strcasecmp(method, "GET")) return (cgi_initialize_get()); else if (!_cups_strcasecmp(method, "POST") && content_type) { const char *boundary = strstr(content_type, "boundary="); if (boundary) boundary += 9; if (content_type && !strncmp(content_type, "multipart/form-data; ", 21)) { if (!cgi_initialize_multipart(boundary)) return (0); } else if (!cgi_initialize_post()) return (0); if ((cups_sid_form = cgiGetVariable(CUPS_SID)) == NULL || strcmp(cups_sid_cookie, cups_sid_form)) { if (cups_sid_form) fprintf(stderr, "DEBUG: " CUPS_SID " form variable is \"%s\"\n", cups_sid_form); else fputs("DEBUG: " CUPS_SID " form variable is not present.\n", stderr); free((void *)cups_sid_form); cgiClearVariables(); return (0); } else { free((void *)cups_sid_form); return (1); } } else return (0); } /* * 'cgiIsPOST()' - Determine whether this page was POSTed. */ int /* O - 1 if POST, 0 if GET */ cgiIsPOST(void) { const char *method; /* REQUEST_METHOD environment variable */ if ((method = getenv("REQUEST_METHOD")) == NULL) return (0); else return (!strcmp(method, "POST")); } /* * 'cgiSetArray()' - Set array element N to the specified string. * * If the variable array is smaller than (element + 1), the intervening * elements are set to NULL. */ void cgiSetArray(const char *name, /* I - Name of variable */ int element, /* I - Element number (0 to N) */ const char *value) /* I - Value of variable */ { int i; /* Looping var */ _cgi_var_t *var; /* Returned variable */ if (name == NULL || value == NULL || element < 0 || element > 100000) return; fprintf(stderr, "DEBUG: cgiSetArray: %s[%d]=\"%s\"\n", name, element, value); if ((var = cgi_find_variable(name)) == NULL) { cgi_add_variable(name, element, value); cgi_sort_variables(); } else { if (element >= var->avalues) { char **temp; /* Temporary pointer */ temp = (char **)realloc((void *)(var->values), sizeof(char *) * (size_t)(element + 16)); if (!temp) return; var->avalues = element + 16; var->values = temp; } if (element >= var->nvalues) { for (i = var->nvalues; i < element; i ++) var->values[i] = NULL; var->nvalues = element + 1; } else if (var->values[element]) free((char *)var->values[element]); var->values[element] = strdup(value); } } /* * 'cgiSetCookie()' - Set a cookie value. */ void cgiSetCookie(const char *name, /* I - Name */ const char *value, /* I - Value */ const char *path, /* I - Path (typically "/") */ const char *domain, /* I - Domain name */ time_t expires, /* I - Expiration date (0 for session) */ int secure) /* I - Require SSL */ { num_cookies = cupsAddOption(name, value, num_cookies, &cookies); printf("Set-Cookie: %s=%s;", name, value); if (path) printf(" path=%s;", path); if (domain) printf(" domain=%s;", domain); if (expires) { char date[256]; /* Date string */ printf(" expires=%s;", httpGetDateString2(expires, date, sizeof(date))); } if (secure) puts(" httponly; secure;"); else puts(" httponly;"); } /* * 'cgiSetSize()' - Set the array size. */ void cgiSetSize(const char *name, /* I - Name of variable */ int size) /* I - Number of elements (0 to N) */ { int i; /* Looping var */ _cgi_var_t *var; /* Returned variable */ if (name == NULL || size < 0 || size > 100000) return; if ((var = cgi_find_variable(name)) == NULL) return; if (size >= var->avalues) { char **temp; /* Temporary pointer */ temp = (char **)realloc((void *)(var->values), sizeof(char *) * (size_t)(size + 16)); if (!temp) return; var->avalues = size + 16; var->values = temp; } if (size > var->nvalues) { for (i = var->nvalues; i < size; i ++) var->values[i] = NULL; } else if (size < var->nvalues) { for (i = size; i < var->nvalues; i ++) if (var->values[i]) free((void *)(var->values[i])); } var->nvalues = size; } /* * 'cgiSetVariable()' - Set a CGI variable in the database. * * If the variable is an array, this truncates the array to a single element. */ void cgiSetVariable(const char *name, /* I - Name of variable */ const char *value) /* I - Value of variable */ { int i; /* Looping var */ _cgi_var_t *var; /* Returned variable */ if (name == NULL || value == NULL) return; fprintf(stderr, "cgiSetVariable: %s=\"%s\"\n", name, value); if ((var = cgi_find_variable(name)) == NULL) { cgi_add_variable(name, 0, value); cgi_sort_variables(); } else { for (i = 0; i < var->nvalues; i ++) if (var->values[i]) free((char *)var->values[i]); var->values[0] = strdup(value); var->nvalues = 1; } } /* * 'cgi_add_variable()' - Add a form variable. */ static void cgi_add_variable(const char *name, /* I - Variable name */ int element, /* I - Array element number */ const char *value) /* I - Variable value */ { _cgi_var_t *var; /* New variable */ if (name == NULL || value == NULL || element < 0 || element > 100000) return; if (form_count >= form_alloc) { _cgi_var_t *temp_vars; /* Temporary form pointer */ if (form_alloc == 0) temp_vars = malloc(sizeof(_cgi_var_t) * 16); else temp_vars = realloc(form_vars, (size_t)(form_alloc + 16) * sizeof(_cgi_var_t)); if (!temp_vars) return; form_vars = temp_vars; form_alloc += 16; } var = form_vars + form_count; if ((var->values = calloc((size_t)element + 1, sizeof(char *))) == NULL) return; var->name = strdup(name); var->nvalues = element + 1; var->avalues = element + 1; var->values[element] = strdup(value); form_count ++; } /* * 'cgi_compare_variables()' - Compare two variables. */ static int /* O - Result of comparison */ cgi_compare_variables( const _cgi_var_t *v1, /* I - First variable */ const _cgi_var_t *v2) /* I - Second variable */ { return (_cups_strcasecmp(v1->name, v2->name)); } /* * 'cgi_find_variable()' - Find a variable. */ static _cgi_var_t * /* O - Variable pointer or NULL */ cgi_find_variable(const char *name) /* I - Name of variable */ { _cgi_var_t key; /* Search key */ if (form_count < 1 || name == NULL) return (NULL); key.name = (char *)name; return ((_cgi_var_t *)bsearch(&key, form_vars, (size_t)form_count, sizeof(_cgi_var_t), (int (*)(const void *, const void *))cgi_compare_variables)); } /* * 'cgi_initialize_cookies()' - Initialize cookies. */ static void cgi_initialize_cookies(void) { const char *cookie; /* HTTP_COOKIE environment variable */ char name[128], /* Name string */ value[512], /* Value string */ *ptr; /* Pointer into name/value */ if ((cookie = getenv("HTTP_COOKIE")) == NULL) return; while (*cookie) { int skip = 0; /* Skip this cookie? */ /* * Skip leading whitespace... */ while (isspace(*cookie & 255)) cookie ++; if (!*cookie) break; /* * Copy the name... */ for (ptr = name; *cookie && *cookie != '=';) if (ptr < (name + sizeof(name) - 1)) { *ptr++ = *cookie++; } else { skip = 1; cookie ++; } if (*cookie != '=') break; *ptr = '\0'; cookie ++; /* * Then the value... */ if (*cookie == '\"') { for (cookie ++, ptr = value; *cookie && *cookie != '\"';) if (ptr < (value + sizeof(value) - 1)) { *ptr++ = *cookie++; } else { skip = 1; cookie ++; } if (*cookie == '\"') cookie ++; else skip = 1; } else { for (ptr = value; *cookie && *cookie != ';';) if (ptr < (value + sizeof(value) - 1)) { *ptr++ = *cookie++; } else { skip = 1; cookie ++; } } if (*cookie == ';') cookie ++; else if (*cookie) skip = 1; *ptr = '\0'; /* * Then add the cookie to an array as long as the name doesn't start with * "$"... */ if (name[0] != '$' && !skip) num_cookies = cupsAddOption(name, value, num_cookies, &cookies); } } /* * 'cgi_initialize_get()' - Initialize form variables using the GET method. */ static int /* O - 1 if form data read */ cgi_initialize_get(void) { char *data; /* Pointer to form data string */ /* * Check to see if there is anything for us to read... */ data = getenv("QUERY_STRING"); if (data == NULL || strlen(data) == 0) return (0); /* * Parse it out and return... */ return (cgi_initialize_string(data)); } /* * 'cgi_initialize_multipart()' - Initialize variables and file using the POST * method. * * TODO: Update to support files > 2GB. */ static int /* O - 1 if form data was read */ cgi_initialize_multipart( const char *boundary) /* I - Boundary string */ { char line[10240], /* MIME header line */ name[1024], /* Form variable name */ filename[1024], /* Form filename */ mimetype[1024], /* MIME media type */ bstring[256], /* Boundary string to look for */ *ptr, /* Pointer into name/filename */ *end; /* End of buffer */ int ch, /* Character from file */ fd; /* Temporary file descriptor */ size_t blen; /* Length of boundary string */ /* * Read multipart form data until we run out... */ name[0] = '\0'; filename[0] = '\0'; mimetype[0] = '\0'; snprintf(bstring, sizeof(bstring), "\r\n--%s", boundary); blen = strlen(bstring); while (fgets(line, sizeof(line), stdin)) { if (!strcmp(line, "\r\n")) { /* * End of headers, grab value... */ if (filename[0]) { /* * Read an embedded file... */ if (form_file) { /* * Remove previous file... */ cgi_unlink_file(); } /* * Allocate memory for the new file... */ if ((form_file = calloc(1, sizeof(cgi_file_t))) == NULL) return (0); form_file->name = strdup(name); form_file->filename = strdup(filename); form_file->mimetype = strdup(mimetype); fd = cupsTempFd(form_file->tempfile, sizeof(form_file->tempfile)); if (fd < 0) return (0); atexit(cgi_unlink_file); /* * Copy file data to the temp file... */ ptr = line; while ((ch = getchar()) != EOF) { *ptr++ = (char)ch; if ((size_t)(ptr - line) >= blen && !memcmp(ptr - blen, bstring, blen)) { ptr -= blen; break; } if ((ptr - line - (int)blen) >= 8192) { /* * Write out the first 8k of the buffer... */ write(fd, line, 8192); memmove(line, line + 8192, (size_t)(ptr - line - 8192)); ptr -= 8192; } } /* * Write the rest of the data and close the temp file... */ if (ptr > line) write(fd, line, (size_t)(ptr - line)); close(fd); } else { /* * Just get a form variable; the current code only handles * form values up to 10k in size... */ ptr = line; end = line + sizeof(line) - 1; while ((ch = getchar()) != EOF) { if (ptr < end) *ptr++ = (char)ch; if ((size_t)(ptr - line) >= blen && !memcmp(ptr - blen, bstring, blen)) { ptr -= blen; break; } } *ptr = '\0'; /* * Set the form variable... */ if ((ptr = strrchr(name, '-')) != NULL && isdigit(ptr[1] & 255)) { /* * Set a specific index in the array... */ *ptr++ = '\0'; if (line[0]) cgiSetArray(name, atoi(ptr) - 1, line); } else if ((ptr = cgiGetVariable(name)) != NULL) { /* * Add another element in the array... */ free(ptr); cgiSetArray(name, cgiGetSize(name), line); } else { /* * Just set the line... */ cgiSetVariable(name, line); } } /* * Read the rest of the current line... */ fgets(line, sizeof(line), stdin); /* * Clear the state vars... */ name[0] = '\0'; filename[0] = '\0'; mimetype[0] = '\0'; } else if (!_cups_strncasecmp(line, "Content-Disposition:", 20)) { if ((ptr = strstr(line + 20, " name=\"")) != NULL) { strlcpy(name, ptr + 7, sizeof(name)); if ((ptr = strchr(name, '\"')) != NULL) *ptr = '\0'; } if ((ptr = strstr(line + 20, " filename=\"")) != NULL) { strlcpy(filename, ptr + 11, sizeof(filename)); if ((ptr = strchr(filename, '\"')) != NULL) *ptr = '\0'; } } else if (!_cups_strncasecmp(line, "Content-Type:", 13)) { for (ptr = line + 13; isspace(*ptr & 255); ptr ++); strlcpy(mimetype, ptr, sizeof(mimetype)); for (ptr = mimetype + strlen(mimetype) - 1; ptr > mimetype && isspace(*ptr & 255); *ptr-- = '\0'); } } /* * Return 1 for "form data found"... */ return (1); } /* * 'cgi_initialize_post()' - Initialize variables using the POST method. */ static int /* O - 1 if form data was read */ cgi_initialize_post(void) { char *content_length, /* Length of input data (string) */ *data; /* Pointer to form data string */ size_t length, /* Length of input data */ tbytes; /* Total number of bytes read */ ssize_t nbytes; /* Number of bytes read this read() */ int status; /* Return status */ /* * Check to see if there is anything for us to read... */ content_length = getenv("CONTENT_LENGTH"); if (content_length == NULL || atoi(content_length) <= 0) return (0); /* * Get the length of the input stream and allocate a buffer for it... */ length = (size_t)strtol(content_length, NULL, 10); data = malloc(length + 1); // lgtm [cpp/uncontrolled-allocation-size] if (data == NULL) return (0); /* * Read the data into the buffer... */ for (tbytes = 0; tbytes < length; tbytes += (size_t)nbytes) if ((nbytes = read(0, data + tbytes, (size_t)(length - tbytes))) < 0) { if (errno != EAGAIN) { free(data); return (0); } else nbytes = 0; } else if (nbytes == 0) { /* * CUPS STR #3176: OpenBSD: Early end-of-file on POST data causes 100% CPU * * This should never happen, but does on OpenBSD. If we see early end-of- * file, treat this as an error and process no data. */ free(data); return (0); } data[length] = '\0'; /* * Parse it out... */ status = cgi_initialize_string(data); /* * Free the data and return... */ free(data); return (status); } /* * 'cgi_initialize_string()' - Initialize form variables from a string. */ static int /* O - 1 if form data was processed */ cgi_initialize_string(const char *data) /* I - Form data string */ { int done; /* True if we're done reading a form variable */ char *s, /* Pointer to current form string */ ch, /* Temporary character */ name[255], /* Name of form variable */ value[65536], /* Variable value */ *temp; /* Temporary pointer */ /* * Check input... */ if (data == NULL) return (0); /* * Loop until we've read all the form data... */ while (*data != '\0') { /* * Get the variable name... */ for (s = name; *data != '\0'; data ++) if (*data == '=') break; else if (*data >= ' ' && s < (name + sizeof(name) - 1)) *s++ = *data; *s = '\0'; if (*data == '=') data ++; else return (0); /* * Read the variable value... */ for (s = value, done = 0; !done && *data != '\0'; data ++) switch (*data) { case '&' : /* End of data... */ done = 1; break; case '+' : /* Escaped space character */ if (s < (value + sizeof(value) - 1)) *s++ = ' '; break; case '%' : /* Escaped control character */ /* * Read the hex code... */ if (!isxdigit(data[1] & 255) || !isxdigit(data[2] & 255)) return (0); if (s < (value + sizeof(value) - 1)) { data ++; ch = *data - '0'; if (ch > 9) ch -= 7; *s = (char)(ch << 4); data ++; ch = *data - '0'; if (ch > 9) ch -= 7; *s++ |= ch; } else data += 2; break; default : /* Other characters come straight through */ if (*data >= ' ' && s < (value + sizeof(value) - 1)) *s++ = *data; break; } *s = '\0'; /* nul terminate the string */ /* * Remove trailing whitespace... */ if (s > value) s --; while (s >= value && isspace(*s & 255)) *s-- = '\0'; /* * Add the string to the variable "database"... */ if ((s = strrchr(name, '-')) != NULL && isdigit(s[1] & 255)) { *s++ = '\0'; if (value[0]) cgiSetArray(name, atoi(s) - 1, value); } else if ((temp = cgiGetVariable(name)) != NULL) { free(temp); cgiSetArray(name, cgiGetSize(name), value); } else cgiSetVariable(name, value); } return (1); } /* * 'cgi_passwd()' - Catch authentication requests and notify the server. * * This function sends a Status header and exits, forcing authentication * for this request. */ static const char * /* O - NULL (no return) */ cgi_passwd(const char *prompt) /* I - Prompt (not used) */ { (void)prompt; fprintf(stderr, "DEBUG: cgi_passwd(prompt=\"%s\") called!\n", prompt ? prompt : "(null)"); /* * Send a 401 (unauthorized) status to the server, so it can notify * the client that authentication is required. */ puts("Status: 401\n"); exit(0); /* * This code is never executed, but is present to satisfy the compiler. */ return (NULL); } /* * 'cgi_set_sid()' - Set the CUPS session ID. */ static const char * /* O - New session ID */ cgi_set_sid(void) { char buffer[512], /* SID data */ sid[33]; /* SID string */ unsigned char sum[16]; /* MD5 sum */ const char *remote_addr, /* REMOTE_ADDR */ *server_name, /* SERVER_NAME */ *server_port; /* SERVER_PORT */ struct timeval curtime; /* Current time */ if ((remote_addr = getenv("REMOTE_ADDR")) == NULL) remote_addr = "REMOTE_ADDR"; if ((server_name = getenv("SERVER_NAME")) == NULL) server_name = "SERVER_NAME"; if ((server_port = getenv("SERVER_PORT")) == NULL) server_port = "SERVER_PORT"; gettimeofday(&curtime, NULL); CUPS_SRAND(curtime.tv_sec + curtime.tv_usec); snprintf(buffer, sizeof(buffer), "%s:%s:%s:%02X%02X%02X%02X%02X%02X%02X%02X", remote_addr, server_name, server_port, (unsigned)CUPS_RAND() & 255, (unsigned)CUPS_RAND() & 255, (unsigned)CUPS_RAND() & 255, (unsigned)CUPS_RAND() & 255, (unsigned)CUPS_RAND() & 255, (unsigned)CUPS_RAND() & 255, (unsigned)CUPS_RAND() & 255, (unsigned)CUPS_RAND() & 255); cupsHashData("md5", (unsigned char *)buffer, strlen(buffer), sum, sizeof(sum)); cgiSetCookie(CUPS_SID, cupsHashString(sum, sizeof(sum), sid, sizeof(sid)), "/", NULL, 0, 0); return (cupsGetOption(CUPS_SID, num_cookies, cookies)); } /* * 'cgi_sort_variables()' - Sort all form variables for faster lookup. */ static void cgi_sort_variables(void) { if (form_count < 2) return; qsort(form_vars, (size_t)form_count, sizeof(_cgi_var_t), (int (*)(const void *, const void *))cgi_compare_variables); } /* * 'cgi_unlink_file()' - Remove the uploaded form. */ static void cgi_unlink_file(void) { if (form_file) { /* * Remove the temporary file... */ unlink(form_file->tempfile); /* * Free memory used... */ free(form_file->name); free(form_file->filename); free(form_file->mimetype); free(form_file); form_file = NULL; } }