mirror of
https://github.com/seregonwar/zftpd.git
synced 2026-10-21 19:22:28 -05:00
The HTTP stack had grown into a single file that owned sockets, parsing, uploads and every API domain. Split it into focused modules (listener, response stream, upload, json helpers) with the domain handlers next to the APIs they serve, and cover framing, streaming and uploads with unit tests.
148 lines
5.2 KiB
C
148 lines
5.2 KiB
C
#include "../src/http/http_api_internal.h"
|
|
#include "http_api.h"
|
|
#include "ftp_config.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#define CHECK(expr) do { \
|
|
if (!(expr)) { \
|
|
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #expr); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
static int test_query_parser(void) {
|
|
char out[128];
|
|
CHECK(http_api_parse_path_param("?path=%2Ffoo+bar&x=1", out,
|
|
sizeof(out)) == 0);
|
|
CHECK(strcmp(out, "/foo bar") == 0);
|
|
CHECK(http_api_parse_path_param("?path=", out, sizeof(out)) == 0);
|
|
CHECK(strcmp(out, "/") == 0);
|
|
CHECK(http_api_parse_query_param("?x=1&name=hello%20world", "name", out,
|
|
sizeof(out)) == 0);
|
|
CHECK(strcmp(out, "hello world") == 0);
|
|
return 0;
|
|
}
|
|
static int test_query_rejects_invalid_input(void) {
|
|
char out[16];
|
|
CHECK(http_api_parse_query_param("?name=%00evil", "name", out,
|
|
sizeof(out)) != 0);
|
|
CHECK(http_api_parse_query_param("?name=%ZZ", "name", out,
|
|
sizeof(out)) != 0);
|
|
CHECK(http_api_parse_query_param("?name=abcdef", "name", out, 4U) != 0);
|
|
CHECK(http_api_parse_query_param("?pathname=bad&path=good", "path", out,
|
|
sizeof(out)) == 0);
|
|
CHECK(strcmp(out, "good") == 0);
|
|
CHECK(http_api_query_has_param("?pathname=bad&path=good", "path") == 1);
|
|
CHECK(http_api_query_has_param("?pathname=bad&path=good", "pathname") == 1);
|
|
CHECK(http_api_query_has_param("?xid=CUSA12345", "id") == 0);
|
|
#if ENABLE_WEB_UPLOAD
|
|
CHECK(http_api_is_safe_filename("report..txt") == 1);
|
|
CHECK(http_api_is_safe_filename(".") == 0);
|
|
CHECK(http_api_is_safe_filename("..") == 0);
|
|
CHECK(http_api_is_safe_filename("a/b") == 0);
|
|
CHECK(http_api_is_safe_filename("a\\b") == 0);
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
static int test_json_helpers(void) {
|
|
char out[128];
|
|
size_t pos = 0U;
|
|
CHECK(http_json_escape_append(out, sizeof(out), &pos,
|
|
"a\"b\n\t") == 0);
|
|
out[pos] = '\0';
|
|
CHECK(strcmp(out, "a\\\"b\\n\\t") == 0);
|
|
return 0;
|
|
}
|
|
static int test_path_confinement(void) {
|
|
char root[] = "/tmp/zftpd-api-root-XXXXXX";
|
|
char outside[] = "/tmp/zftpd-api-out-XXXXXX";
|
|
CHECK(mkdtemp(root) != NULL);
|
|
CHECK(mkdtemp(outside) != NULL);
|
|
|
|
char child[512];
|
|
char link_path[512];
|
|
char safe[FTP_PATH_MAX];
|
|
snprintf(child, sizeof(child), "%s/child", root);
|
|
snprintf(link_path, sizeof(link_path), "%s/escape", root);
|
|
CHECK(mkdir(child, 0700) == 0);
|
|
CHECK(symlink(outside, link_path) == 0);
|
|
|
|
http_api_set_root(root);
|
|
CHECK(http_api_validate_path(child, safe, sizeof(safe)) == 1);
|
|
CHECK(strncmp(safe, http_api_get_root(), strlen(http_api_get_root())) == 0);
|
|
CHECK(http_api_validate_path(link_path, safe, sizeof(safe)) == 0);
|
|
|
|
char escaped_new[512];
|
|
snprintf(escaped_new, sizeof(escaped_new), "%s/escape/new/file.bin", root);
|
|
CHECK(http_api_validate_path(escaped_new, safe, sizeof(safe)) == 0);
|
|
|
|
char nested_new[512];
|
|
snprintf(nested_new, sizeof(nested_new), "%s/new/sub/file.bin", root);
|
|
CHECK(http_api_validate_path(nested_new, safe, sizeof(safe)) == 1);
|
|
CHECK(strncmp(safe, http_api_get_root(), strlen(http_api_get_root())) == 0);
|
|
|
|
char target[512], leaf_link[512], leaf_expected[512];
|
|
snprintf(target, sizeof(target), "%s/target", root);
|
|
snprintf(leaf_link, sizeof(leaf_link), "%s/leaf-link", root);
|
|
CHECK(mkdir(target, 0700) == 0);
|
|
CHECK(symlink(target, leaf_link) == 0);
|
|
CHECK(http_api_validate_path(leaf_link, safe, sizeof(safe)) == 1);
|
|
CHECK(strstr(safe, "/target") != NULL);
|
|
CHECK(http_api_validate_entry_path(leaf_link, safe, sizeof(safe)) == 1);
|
|
CHECK(realpath(root, leaf_expected) != NULL);
|
|
CHECK(strlen(leaf_expected) + sizeof("/leaf-link") < sizeof(leaf_expected));
|
|
strcat(leaf_expected, "/leaf-link");
|
|
CHECK(strcmp(safe, leaf_expected) == 0);
|
|
|
|
char escaped_entry[512];
|
|
snprintf(escaped_entry, sizeof(escaped_entry), "%s/escape/new-entry", root);
|
|
CHECK(http_api_validate_entry_path(escaped_entry, safe, sizeof(safe)) == 0);
|
|
|
|
char dot_name[512];
|
|
snprintf(dot_name, sizeof(dot_name), "%s/..cache", root);
|
|
CHECK(mkdir(dot_name, 0700) == 0);
|
|
CHECK(http_api_validate_path(dot_name, safe, sizeof(safe)) == 1);
|
|
|
|
char traversal[512];
|
|
snprintf(traversal, sizeof(traversal), "%s/../escape", root);
|
|
CHECK(http_api_validate_path(traversal, safe, sizeof(safe)) == 0);
|
|
http_api_set_root("/");
|
|
unlink(leaf_link);
|
|
rmdir(target);
|
|
unlink(link_path);
|
|
rmdir(dot_name);
|
|
rmdir(child);
|
|
rmdir(root);
|
|
rmdir(outside);
|
|
return 0;
|
|
}
|
|
static int test_json_responses_escape_messages(void) {
|
|
http_response_t *resp =
|
|
http_api_error_json(HTTP_STATUS_400_BAD_REQUEST, "bad \"quote\"\n");
|
|
CHECK(resp != NULL);
|
|
CHECK(strstr(resp->data, "bad \\\"quote\\\"\\n") != NULL);
|
|
http_response_destroy(resp);
|
|
|
|
resp = http_api_status_json_200(0, "bad \"message\"", 7);
|
|
CHECK(resp != NULL);
|
|
CHECK(strstr(resp->data, "bad \\\"message\\\"") != NULL);
|
|
http_response_destroy(resp);
|
|
return 0;
|
|
}
|
|
|
|
int main(void) {
|
|
CHECK(test_query_parser() == 0);
|
|
CHECK(test_query_rejects_invalid_input() == 0);
|
|
CHECK(test_json_helpers() == 0);
|
|
CHECK(test_path_confinement() == 0);
|
|
CHECK(test_json_responses_escape_messages() == 0);
|
|
puts("test_http_api_common: ok");
|
|
return 0;
|
|
}
|