diff --git a/examples/file-transfer/Makefile b/examples/file-transfer/Makefile new file mode 100644 index 00000000..02f37d8f --- /dev/null +++ b/examples/file-transfer/Makefile @@ -0,0 +1,35 @@ +SPROG ?= server # Program we are building +CPROG ?= client # Program we are building +DELETE = rm -rf # Command to remove files +SOUT ?= -o $(SPROG) # Compiler argument for output file +COUT ?= -o $(CPROG) # Compiler argument for output file +SSOURCES = server.c mongoose.c # Source code files +CSOURCES = client.c mongoose.c # Source code files +CFLAGS = -W -Wall -Wextra -g -I. # Build options + +# Mongoose build options. See https://mongoose.ws/documentation/#build-options +#CFLAGS_MONGOOSE += -DMG_ENABLE_LINES + +ifeq ($(OS),Windows_NT) # Windows settings. Assume MinGW compiler. To use VC: make CC=cl CFLAGS=/MD OUT=/Feprog.exe + SPROG ?= server.exe # Use .exe suffix for the binary + CPROG ?= client.exe # Use .exe suffix for the binary + CC = gcc # Use MinGW gcc compiler + CFLAGS += -lws2_32 # Link against Winsock library + DELETE = cmd /C del /Q /F /S # Command prompt command to delete files + SOUT ?= -o $(SPROG) # Build output + COUT ?= -o $(CPROG) # Build output +endif + +all: example # Default target. Build all and run server + $(RUN) ./$(SPROG) $(SARGS) + +example: $(SPROG) $(CPROG) + +$(SPROG): $(SSOURCES) # Build program from sources + $(CC) $(SSOURCES) $(CFLAGS) $(CFLAGS_MONGOOSE) $(CFLAGS_EXTRA) $(SOUT) + +$(CPROG): $(CSOURCES) # Build program from sources + $(CC) $(CSOURCES) $(CFLAGS) $(CFLAGS_MONGOOSE) $(CFLAGS_EXTRA) $(COUT) + +clean: # Cleanup. Delete built program and all build artifacts + $(DELETE) $(SPROG) $(CPROG) *.o *.obj *.exe *.dSYM diff --git a/examples/file-transfer/README.md b/examples/file-transfer/README.md new file mode 100644 index 00000000..8ea1cc44 --- /dev/null +++ b/examples/file-transfer/README.md @@ -0,0 +1,47 @@ +# File Transfer + +This example contains minimal HTTP client and server. + +The client uploads a file to the server in a single POST, shaping traffic to send small data chunks. + +The server manually processes requests in order to be able to write as soon as data arrives, to avoid buffering a whole (possibly huge) file not fitting in RAM. + +Uploads are authenticated using Basic Auth. Both client and server have a default user/pass and can be configured using the command line. Only authenticated users can upload a file. + +The server can also accept regular uploads from any HTTP client, for example curl: + +```sh + curl -su user:pass http://localhost:8090/upload/foo.txt --data-binary @Makefile +``` + +- Follow the [Build Tools](../tools/) tutorial to setup your development environment. +- Start a terminal in this project directory; and build the example: + + ```sh + cd mongoose/examples/file-transfer + make clean all + ``` + +- Manually start the server, either in background (to reuse the same terminal window) or in foreground; in which case you'll need another terminal to run the client. The server will listen at all interfaces in port 8090 + + ```sh + ./server + 6332b7 2 server.c:157:main Mongoose version : v7.12 + 6332b7 2 server.c:158:main Listening on : http://0.0.0.0:8090 + 6332b7 2 server.c:159:main Web root : [/home/mongoose/examples/file-transfer/web_root] + 6332b7 2 server.c:160:main Uploading to : [/home/mongoose/examples/file-transfer/upload] + ``` + +- Manually run the client to send a file, default is to send it as "foo.txt" to the server in localhost at port 8090 + + ```sh + ./client -f Makefile + ok + ``` + +Default operation is to assume hardcoded username and password. Call both server and client with no arguments to see usage instructions + +See detailed tutorials at + https://mongoose.ws/tutorials/file-uploads/ + https://mongoose.ws/tutorials/http-server/ + https://mongoose.ws/tutorials/http-client/ diff --git a/examples/file-transfer/client.c b/examples/file-transfer/client.c new file mode 100644 index 00000000..80f38b26 --- /dev/null +++ b/examples/file-transfer/client.c @@ -0,0 +1,117 @@ +// Copyright (c) 2021 Cesanta Software Limited +// All rights reserved +// +// Example HTTP client. Connect to `s_url`, send request, wait for a response, +// print the response and exit. +// You can change `s_url` from the command line by executing: ./example YOUR_URL +// +// To enable SSL/TLS, , see https://mongoose.ws/tutorials/tls/#how-to-build + +#include "mongoose.h" + +static int s_debug_level = MG_LL_INFO; +static const char *s_user = "user"; +static const char *s_pass = "pass"; +static const char *s_fname = NULL; +static struct mg_fd *fd; // file descriptor +static size_t fsize; +static const char *s_url = "http://localhost:8090/upload/foo.txt"; +static const uint64_t s_timeout_ms = 1500; // Connect timeout in milliseconds + +// Print HTTP response and signal that we're done +static void fn(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_OPEN) { + // Connection created. Store connect expiration time in c->data + *(uint64_t *) c->data = mg_millis() + s_timeout_ms; + } else if (ev == MG_EV_POLL) { + if (mg_millis() > *(uint64_t *) c->data && + (c->is_connecting || c->is_resolving)) { + mg_error(c, "Connect timeout"); + } + } else if (ev == MG_EV_CONNECT) { + // Connected to server. Extract host name from URL + struct mg_str host = mg_url_host(s_url); + // Send request + MG_DEBUG(("Connected, send request")); + mg_printf(c, + "POST %s HTTP/1.0\r\n" + "Host: %.*s\r\n" + "Content-Type: octet-stream\r\n" + "Content-Length: %d\r\n", + mg_url_uri(s_url), (int) host.len, host.ptr, fsize); + mg_http_bauth(c, s_user, s_pass); // Add Basic auth header + mg_printf(c, "%s", "\r\n"); // End HTTP headers + } else if (ev == MG_EV_WRITE && c->send.len < MG_IO_SIZE) { + uint8_t *buf = alloca(MG_IO_SIZE); + size_t len = MG_IO_SIZE - c->send.len; + len = fsize < len ? fsize : len; + fd->fs->rd(fd->fd, buf, len); + mg_send(c, buf, len); + fsize -= len; + MG_DEBUG(("sent %u bytes", len)); + } else if (ev == MG_EV_HTTP_MSG) { + MG_DEBUG(("MSG")); + // Response is received. Print it + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + printf("%.*s", (int) hm->body.len, hm->body.ptr); + c->is_draining = 1; // Tell mongoose to close this connection + mg_fs_close(fd); + *(bool *) c->fn_data = true; // Tell event loop to stop + } else if (ev == MG_EV_ERROR) { + MG_DEBUG(("ERROR")); + mg_fs_close(fd); + *(bool *) c->fn_data = true; // Error, tell event loop to stop + } +} + +static void usage(const char *prog) { + fprintf(stderr, + "File Transfer client based on Mongoose v.%s\n" + "Usage: %s -f NAME OPTIONS\n" + " -u NAME - user name, default: '%s'\n" + " -p PWD - password, default: '%s'\n" + " -U URL - Full server URL, including destination file name; " + "default: '%s'\n" + " -f NAME - File to send\n" + " -v LEVEL - debug level, from 0 to 4, default: %d\n", + MG_VERSION, prog, s_user, s_pass, s_url, s_debug_level); + exit(EXIT_FAILURE); +} + +int main(int argc, char *argv[]) { + struct mg_mgr mgr; // Event manager + bool done = false; // Event handler flips it to true + time_t mtime; + int i; + + // Parse command-line flags + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-f") == 0) { + s_fname = argv[++i]; + } else if (strcmp(argv[i], "-u") == 0) { + s_user = argv[++i]; + } else if (strcmp(argv[i], "-p") == 0) { + s_pass = argv[++i]; + } else if (strcmp(argv[i], "-U") == 0) { + s_url = argv[++i]; + } else if (strcmp(argv[i], "-v") == 0) { + s_debug_level = atoi(argv[++i]); + } else { + usage(argv[0]); + } + } + if (s_fname == NULL) usage(argv[0]); + mg_fs_posix.st(s_fname, &fsize, &mtime); + if (fsize == 0 || + (fd = mg_fs_open(&mg_fs_posix, s_fname, MG_FS_READ)) == NULL) { + MG_ERROR(("open failed: %d", errno)); + exit(EXIT_FAILURE); + } + + mg_log_set(s_debug_level); + mg_mgr_init(&mgr); // Initialise event manager + mg_http_connect(&mgr, s_url, fn, &done); // Create client connection + while (!done) mg_mgr_poll(&mgr, 50); // Event manager loops until 'done' + mg_mgr_free(&mgr); // Free resources + return 0; +} diff --git a/examples/file-transfer/mongoose.c b/examples/file-transfer/mongoose.c new file mode 120000 index 00000000..8ef6e62d --- /dev/null +++ b/examples/file-transfer/mongoose.c @@ -0,0 +1 @@ +../../mongoose.c \ No newline at end of file diff --git a/examples/file-transfer/mongoose.h b/examples/file-transfer/mongoose.h new file mode 120000 index 00000000..488ef358 --- /dev/null +++ b/examples/file-transfer/mongoose.h @@ -0,0 +1 @@ +../../mongoose.h \ No newline at end of file diff --git a/examples/file-transfer/server.c b/examples/file-transfer/server.c new file mode 100644 index 00000000..6287901c --- /dev/null +++ b/examples/file-transfer/server.c @@ -0,0 +1,176 @@ +// Copyright (c) 2024 Cesanta Software Limited +// All rights reserved + +#include +#include "mongoose.h" + +static int s_debug_level = MG_LL_INFO; +static int s_max_size = 10000; +static const char *s_root_dir = "web_root"; +static const char *s_upld_dir = "upload"; +static const char *s_listening_address = "http://0.0.0.0:8090"; +static const char *s_user = "user"; +static const char *s_pass = "pass"; + +// Handle interrupts, like Ctrl-C +static int s_signo; +static void signal_handler(int signo) { + s_signo = signo; +} + +static bool authuser(struct mg_http_message *hm) { + char user[256], pass[256]; + mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass)); + if (strcmp(user, s_user) == 0 && strcmp(pass, s_pass) == 0) return true; + return false; +} + +// Streaming upload example. Demonstrates how to use MG_EV_READ events +// to get large payload in smaller chunks. To test, use curl utility: +static void cb(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_READ) { + // Parse the incoming data ourselves. If we can parse the request, + // store two size_t variables in c->data: expected len and recv len. + size_t *data = (size_t *) c->data; + struct mg_fd *fd = (struct mg_fd *) c->fn_data; // get file descriptor + if (data[0]) { // Already parsed, receiving body + data[1] += c->recv.len; + MG_DEBUG(("Got chunk len %lu, %lu total", c->recv.len, data[1])); + fd->fs->wr(fd->fd, c->recv.buf, c->recv.len); + c->recv.len = 0; // And cleanup the receive buffer. Streaming! + if (data[1] >= data[0]) { + mg_fs_close(fd); + mg_http_reply(c, 200, "", "ok\n"); + } + } else if(c->is_resp == 0) { + struct mg_http_message hm; + int n = mg_http_parse((char *) c->recv.buf, c->recv.len, &hm); + if (n < 0) mg_error(c, "Bad response"); + if (n > 0) { + if (mg_http_match_uri(&hm, "/upload/#")) { + if (!authuser(&hm)) { + mg_http_reply(c, 403, "", "Denied\n"); + c->is_draining = 1; // Tell mongoose to close this connection + } else if (hm.body.len > (size_t) s_max_size) { + mg_http_reply(c, 400, "", "Too long\n"); + c->is_draining = 1; // Tell mongoose to close this connection + } else if (hm.uri.len == 8) { // 8: /upload/ + mg_http_reply(c, 400, "", "Name required\n"); + c->is_draining = 1; // Tell mongoose to close this connection + } else if (strlen(s_upld_dir) + (hm.uri.len - 8) + 2 > + MG_PATH_MAX) { // 2: MG_DIRSEP + NUL + mg_http_reply(c, 400, "", "Path is too long\n"); + c->is_draining = 1; // Tell mongoose to close this connection + } else { + char fpath[MG_PATH_MAX]; + snprintf(fpath, MG_PATH_MAX, "%s%c", s_upld_dir, MG_DIRSEP); + strncat(fpath, hm.uri.ptr + 8, hm.uri.len - 8); + if (!mg_path_is_sane(fpath)) { + mg_http_reply(c, 400, "", "Invalid path\n"); + c->is_draining = 1; // Tell mongoose to close this connection + } else { + MG_DEBUG(("Got request, chunk len %lu", c->recv.len - n)); + if ((fd = mg_fs_open(&mg_fs_posix, fpath, MG_FS_WRITE)) == NULL) { + mg_http_reply(c, 400, "", "open failed: %d", errno); + c->is_draining = 1; // Tell mongoose to close this connection + } else { + c->fn_data = fd; + c->recv.len -= n; // remove headers + data[0] = hm.body.len; + data[1] = c->recv.len; + if (c->recv.len) + fd->fs->wr(fd->fd, c->recv.buf + n, c->recv.len); + c->recv.len = 0; // consume data + if (data[1] >= data[0]) { + mg_fs_close(fd); + mg_http_reply(c, 200, "", "ok\n"); + } + } + } + } + c->is_resp = 1; // ignore the rest of the body + } else { + struct mg_http_serve_opts opts = {0}; + opts.root_dir = s_root_dir; + mg_http_serve_dir(c, &hm, &opts); + } + } + } + } + (void) ev_data; +} + +static void usage(const char *prog) { + fprintf(stderr, + "File Transfer server based on Mongoose v.%s\n" + "Usage: %s OPTIONS\n" + " -u NAME - user name, default: '%s'\n" + " -p PWD - password, default: '%s'\n" + " -d DIR - directory to serve, default: '%s'\n" + " -D DIR - directory to store uploads, default: '%s'\n" + " -s SIZE - maximum allowed file size, default: '%d'\n" + " -l ADDR - listening address, default: '%s'\n" + " -v LEVEL - debug level, from 0 to 4, default: %d\n", + MG_VERSION, prog, s_user, s_pass, s_root_dir, s_upld_dir, s_max_size, + s_listening_address, s_debug_level); + exit(EXIT_FAILURE); +} + +int main(int argc, char *argv[]) { + char spath[MG_PATH_MAX] = "."; + char upath[MG_PATH_MAX] = "."; + struct mg_mgr mgr; + int i; + + // Parse command-line flags + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-d") == 0) { + s_root_dir = argv[++i]; + } else if (strcmp(argv[i], "-D") == 0) { + s_upld_dir = argv[++i]; + } else if (strcmp(argv[i], "-u") == 0) { + s_user = argv[++i]; + } else if (strcmp(argv[i], "-p") == 0) { + s_pass = argv[++i]; + } else if (strcmp(argv[i], "-l") == 0) { + s_listening_address = argv[++i]; + } else if (strcmp(argv[i], "-v") == 0) { + s_debug_level = atoi(argv[++i]); + } else if (strcmp(argv[i], "-s") == 0) { + s_max_size = atoi(argv[++i]); + } else { + usage(argv[0]); + } + } + + // Root directory must not contain double dots. Make it absolute + // Do the conversion only if the root dir spec does not contain overrides + if (strchr(s_root_dir, ',') == NULL) { + realpath(s_root_dir, spath); + s_root_dir = spath; + } + if (strchr(s_upld_dir, ',') == NULL) { + realpath(s_upld_dir, upath); + s_upld_dir = upath; + } + + // Initialise stuff + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + mg_log_set(s_debug_level); + mg_mgr_init(&mgr); + if (mg_http_listen(&mgr, s_listening_address, cb, NULL) == NULL) { + MG_ERROR(("Cannot listen on %s.", s_listening_address)); + exit(EXIT_FAILURE); + } + + // Start infinite event loop + MG_INFO(("Mongoose version : v%s", MG_VERSION)); + MG_INFO(("Listening on : %s", s_listening_address)); + MG_INFO(("Web root : [%s]", s_root_dir)); + MG_INFO(("Uploading to : [%s]", s_upld_dir)); + while (s_signo == 0) mg_mgr_poll(&mgr, 1000); + mg_mgr_free(&mgr); + MG_INFO(("Exiting on signal %d", s_signo)); + return 0; +} diff --git a/examples/file-transfer/upload/README.md b/examples/file-transfer/upload/README.md new file mode 100644 index 00000000..e69de29b diff --git a/examples/file-transfer/web_root/index.html b/examples/file-transfer/web_root/index.html new file mode 100644 index 00000000..1e4b6916 --- /dev/null +++ b/examples/file-transfer/web_root/index.html @@ -0,0 +1,9 @@ + + + + File Transfer + + +

😃

+ +