Files
zftpd/tests/test_transfer.c
T
seregonwar b8f15aed5a feat(transfer): unified download manager with curl, NFS and torrent backends
One manager owns remote downloads: libcurl for HTTP/HTTPS/FTP (with ranged
parallel fetches), libnfs for nfs:// URLs and an optional libtorrent engine
for magnet links.  Jobs survive payload re-injections, support pause, cancel
and retry, and report per-job progress through the HTTP API.
2026-09-30 17:50:45 +02:00

181 lines
5.5 KiB
C

#include "transfer/transfer_manager.h"
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
static const char k_payload[] = "zftpd-transfer-integration-payload\n";
typedef struct {
int listen_fd;
int failed;
} server_ctx_t;
static int send_all(int fd, const void *buf, size_t len) {
const unsigned char *p = (const unsigned char *)buf;
while (len > 0U) {
ssize_t n = send(fd, p, len, 0);
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
if (n == 0) return -1;
p += (size_t)n;
len -= (size_t)n;
}
return 0;
}
static void *http_server(void *opaque) {
server_ctx_t *ctx = (server_ctx_t *)opaque;
/* The first request probes range support; this server ignores Range, so
* the downloader must fall back to its ordinary single-stream request. */
for (int request_idx = 0; request_idx < 2; request_idx++) {
int client = accept(ctx->listen_fd, NULL, NULL);
if (client < 0) {
ctx->failed = 1;
return NULL;
}
char request[4096];
size_t used = 0U;
while (used + 1U < sizeof(request)) {
ssize_t n = recv(client, request + used, sizeof(request) - used - 1U, 0);
if (n <= 0) break;
used += (size_t)n;
request[used] = '\0';
if (strstr(request, "\r\n\r\n") != NULL) break;
}
if (used == 0U || strstr(request, "GET /file.bin ") == NULL)
ctx->failed = 1;
char header[256];
int hn = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\nContent-Length: %zu\r\n"
"Content-Type: application/octet-stream\r\n"
"Connection: close\r\n\r\n",
strlen(k_payload));
if (hn <= 0 || (size_t)hn >= sizeof(header) ||
send_all(client, header, (size_t)hn) != 0 ||
send_all(client, k_payload, strlen(k_payload)) != 0)
ctx->failed = 1;
(void)close(client);
}
return NULL;
}
static int wait_for_transfer(int id, transfer_snapshot_t *out) {
for (int attempt = 0; attempt < 100; attempt++) {
transfer_snapshot_t snaps[TRANSFER_MAX_ACTIVE];
size_t count = transfer_snapshot_all(snaps, TRANSFER_MAX_ACTIVE);
for (size_t i = 0U; i < count; i++) {
if (snaps[i].id == id && snaps[i].done) {
*out = snaps[i];
return 0;
}
}
usleep(50000U);
}
return -1;
}
int main(void) {
#if !defined(ENABLE_LIBCURL) || !ENABLE_LIBCURL
puts("test_transfer: skipped (libcurl disabled)");
return 0;
#else
char reason[TRANSFER_ERROR_MAX];
if (!transfer_url_supported("https://example.invalid/file", reason,
sizeof(reason)) ||
!transfer_url_supported("ftp://example.invalid/file", reason,
sizeof(reason))) {
fprintf(stderr, "expected curl URL schemes to be supported: %s\n", reason);
return 1;
}
#if defined(ENABLE_LIBTORRENT) && ENABLE_LIBTORRENT
if (!transfer_url_supported(
"magnet:?xt=urn:btih:0123456789ABCDEF0123456789ABCDEF01234567&dn=test",
reason, sizeof(reason)) ||
transfer_url_supported("magnet:?xt=urn:btih:invalid", reason,
sizeof(reason))) {
fprintf(stderr, "magnet validation failed: %s\n", reason);
return 1;
}
#endif
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) return 1;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = 0;
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) != 0 ||
listen(listen_fd, 1) != 0) {
perror("listen");
return 1;
}
socklen_t addr_len = (socklen_t)sizeof(addr);
if (getsockname(listen_fd, (struct sockaddr *)&addr, &addr_len) != 0) return 1;
server_ctx_t server = {listen_fd, 0};
pthread_t thread;
if (pthread_create(&thread, NULL, http_server, &server) != 0) return 1;
char tmp_template[] = "/tmp/zftpd-transfer-XXXXXX";
char *tmp_dir = mkdtemp(tmp_template);
if (tmp_dir == NULL) return 1;
char url[128];
(void)snprintf(url, sizeof(url), "http://127.0.0.1:%u/file.bin",
(unsigned)ntohs(addr.sin_port));
int id = 0;
int queued = 0;
char name[TRANSFER_NAME_MAX];
char error[TRANSFER_ERROR_MAX];
if (transfer_start(url, tmp_dir, &id, &queued, name, sizeof(name), error,
sizeof(error)) != 0) {
fprintf(stderr, "transfer_start failed: %s\n", error);
return 1;
}
if (queued != 0) return 1;
transfer_snapshot_t snap;
if (wait_for_transfer(id, &snap) != 0) {
fprintf(stderr, "transfer timed out\n");
return 1;
}
(void)pthread_join(thread, NULL);
(void)close(listen_fd);
if (server.failed || snap.error || snap.downloaded != strlen(k_payload) ||
snap.total_size != strlen(k_payload)) {
fprintf(stderr, "bad transfer state: server=%d error=%d msg=%s bytes=%llu/%llu\n",
server.failed, snap.error, snap.error_msg,
(unsigned long long)snap.downloaded,
(unsigned long long)snap.total_size);
return 1;
}
char path[512];
(void)snprintf(path, sizeof(path), "%s/%s", tmp_dir, name);
FILE *fp = fopen(path, "rb");
if (fp == NULL) return 1;
char data[128];
size_t got = fread(data, 1U, sizeof(data) - 1U, fp);
(void)fclose(fp);
data[got] = '\0';
if (strcmp(data, k_payload) != 0) return 1;
(void)unlink(path);
(void)rmdir(tmp_dir);
puts("test_transfer: ok");
return 0;
#endif
}