/** Parsing uri.post and uploading files in a POST request. * @file */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #ifdef HAVE_UNISTD_H #include #endif #ifdef HAVE_FCNTL_H #include /* OS/2 needs this after sys/types.h */ #endif #include "elinks.h" #include "protocol/http/post.h" #include "protocol/uri.h" #include "util/conv.h" #include "util/error.h" /** Initialize *@a http_post so that done_http_post() can be safely * called. * * @relates http_post */ void init_http_post(struct http_post *http_post) { http_post->total_upload_length = 0; http_post->uploaded = 0; http_post->post_data = NULL; http_post->post_fd = -1; http_post->file_index = 0; http_post->file_count = 0; http_post->file_read = 0; http_post->files = NULL; } /** Free all resources owned by *@a http_post, but do not free the * structure itself. It is safe to call this multiple times. * * @relates http_post */ void done_http_post(struct http_post *http_post) { size_t i; http_post->total_upload_length = 0; http_post->uploaded = 0; http_post->post_data = NULL; if (http_post->post_fd != -1) { close(http_post->post_fd); http_post->post_fd = -1; } for (i = 0; i < http_post->file_count; i++) mem_free(http_post->files[i].name); http_post->file_index = 0; http_post->file_count = 0; http_post->file_read = 0; mem_free_set(&http_post->files, NULL); } /** Prepare to read POST data from a URI and possibly to upload files. * * @param http_post * Must have been initialized with init_http_post(). * @param[in] post_data * The body of the POST request as formatted by get_form_uri(). * However, unlike uri.post, @a post_data must not contain any * Content-Type. The caller must ensure that the @a post_data * pointer remains valid until done_http_post(). * @param[out] error * If the function fails, it writes the error state here so that * the caller can pass that on to abort_connection(). If the * function succeeds, the value of *@a error is undefined. * * This function does not parse the Content-Type from uri.post; the * caller must do that. This is because in local CGI, the child * process handles the Content-Type (saving it to an environment * variable before exec) but the parent process handles the body of * the request (feeding it to the child process via a pipe). * * @return nonzero on success, zero on error. * * @relates http_post */ int open_http_post(struct http_post *http_post, const char *post_data, struct connection_state *error) { off_t size = 0; size_t length = strlen(post_data); const char *end = post_data; done_http_post(http_post); http_post->post_data = end; while (1) { struct stat sb; const char *begin; int res; struct http_post_file *new_files; char *filename; begin = strchr(end, FILE_CHAR); if (!begin) break; end = strchr((begin + 1), FILE_CHAR); if (!end) break; filename = memacpy(begin + 1, end - begin - 1); /* adds '\0' */ if (!filename) { done_http_post(http_post); *error = connection_state(S_OUT_OF_MEM); return 0; } decode_uri(filename); res = stat(filename, &sb); if (res) { *error = connection_state_for_errno(errno); done_http_post(http_post); return 0; } /* This use of mem_realloc() in a loop consumes O(n^2) * time but how many files are you really going to * upload in one request? */ new_files = (struct http_post_file *)mem_realloc(http_post->files, (http_post->file_count + 1) * sizeof(*new_files)); if (new_files == NULL) { mem_free(filename); done_http_post(http_post); *error = connection_state(S_OUT_OF_MEM); return 0; } http_post->files = new_files; new_files[http_post->file_count].name = filename; new_files[http_post->file_count].size = sb.st_size; http_post->file_count++; size += sb.st_size; length -= (end - begin + 1); end++; } size += (length / 2); http_post->total_upload_length = size; return 1; } #ifndef O_BINARY #define O_BINARY 0 #endif /** @return -2 if no data was read but the caller should retry; * -1 if an error occurred and *@a error was set; 0 at end of data; * a positive number if that many bytes were read. * * @relates http_post */ static int read_http_post_inline(struct http_post *http_post, char buffer[], int max, struct connection_state *error) { const char *post = http_post->post_data; const char *end = strchr(post, FILE_CHAR); int total = 0; assert(http_post->post_fd < 0); if_assert_failed { *error = connection_state(S_INTERNAL); return -1; } if (!end) end = strchr(post, '\0'); while (post < end && total < max) { int h1, h2; h1 = unhx(post[0]); assertm(h1 >= 0 && h1 < 16, "h1 in the POST buffer is %d (%d/%c)", h1, post[0], post[0]); if_assert_failed h1 = 0; h2 = unhx(post[1]); assertm(h2 >= 0 && h2 < 16, "h2 in the POST buffer is %d (%d/%c)", h2, post[1], post[1]); if_assert_failed h2 = 0; buffer[total++] = (h1<<4) + h2; post += 2; } if (post != end || *end != FILE_CHAR) { http_post->post_data = post; return total; } http_post->file_read = 0; end = strchr((post + 1), FILE_CHAR); assert(end); http_post->post_fd = open(http_post->files[http_post->file_index].name, O_RDONLY|O_BINARY); /* Be careful not to change errno here. */ if (http_post->post_fd < 0) { http_post->post_data = post; if (total > 0) return total; /* retry the open on the next call */ else { *error = connection_state_for_errno(errno); return -1; } } http_post->post_data = end + 1; return total ? total : -2; } /** @return -2 if no data was read but the caller should retry; * -1 if an error occurred and *@a error was set; 0 at end of data; * a positive number if that many bytes were read. * * @relates http_post */ static int read_http_post_fd(struct http_post *http_post, char buffer[], int max, struct connection_state *error) { const struct http_post_file *const file = &http_post->files[http_post->file_index]; int ret; /* safe_read() would set errno = EBADF anyway, but check this * explicitly to make any such bugs easier to detect. */ assert(http_post->post_fd >= 0); if_assert_failed { *error = connection_state(S_INTERNAL); return -1; } ret = safe_read(http_post->post_fd, buffer, max); if (ret <= 0) { const int errno_from_read = errno; close(http_post->post_fd); http_post->post_fd = -1; http_post->file_index++; /* http_post->file_read is used below so don't clear it here. * It will be cleared when the next file is opened. */ if (ret == -1) { *error = connection_state_for_errno(errno_from_read); return -1; } else if (http_post->file_read != file->size) { /* ELinks already sent a Content-Length header * based on the size of this file, but the * file has since been shrunk. Abort the * connection because ELinks can no longer get * enough data to fill the Content-Length. * (Well, it could pad with zeroes, but that * would be just weird.) */ *error = connection_state(S_HTTP_UPLOAD_RESIZED); return -1; } else { /* The upload file ended but there may still * be more data in uri.post. If not, * read_http_post_inline() will return 0 to * indicate the final end of file. */ return -2; } } http_post->file_read += ret; if (http_post->file_read > file->size) { /* ELinks already sent a Content-Length header based * on the size of this file, but the file has since * been extended. Abort the connection because ELinks * can no longer fit the entire file in the original * Content-Length. */ *error = connection_state(S_HTTP_UPLOAD_RESIZED); return -1; } return ret; } /** Read data from connection.uri->post or from the files to which it * refers. * * @return >0 if read that many bytes; 0 if EOF; -1 on error and set * *@a error. * * @relates http_post */ int read_http_post(struct http_post *http_post, char buffer[], int max, struct connection_state *error) { int total = 0; while (total < max) { int chunk; if (http_post->post_fd < 0) chunk = read_http_post_inline(http_post, buffer + total, max - total, error); else chunk = read_http_post_fd(http_post, buffer + total, max - total, error); if (chunk > 0) { total += chunk; http_post->uploaded += chunk; } else if (chunk != -2) { assert(chunk == -1 || chunk == 0); /* If some data has already been successfully * read to buffer[], tell the caller about * that and forget about the error. The next * read_http_post() call will retry the * operation that failed. */ if (total != 0) return total; else return chunk; } } return total; }