1
0
mirror of https://github.com/rkd77/elinks.git synced 2024-12-04 14:46:47 -05:00
elinks/src/protocol/http/post.c
2022-05-22 13:39:27 +02:00

325 lines
8.6 KiB
C

/** Parsing uri.post and uploading files in a POST request.
* @file */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h> /* 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;
}