Skip to content

Commit

Permalink
Add example
Browse files Browse the repository at this point in the history
  • Loading branch information
scaprile committed Jan 24, 2024
1 parent 922d148 commit 76193fe
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 0 deletions.
33 changes: 33 additions & 0 deletions examples/file-transfer/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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: $(SPROG) $(CPROG) # Default target. Build and run program
$(RUN) ./$(SPROG) $(SARGS)

$(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
47 changes: 47 additions & 0 deletions examples/file-transfer/README.md
Original file line number Diff line number Diff line change
@@ -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.

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/
115 changes: 115 additions & 0 deletions examples/file-transfer/client.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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://0.0.0.0: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) {
// 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_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;
}
1 change: 1 addition & 0 deletions examples/file-transfer/mongoose.c
1 change: 1 addition & 0 deletions examples/file-transfer/mongoose.h
165 changes: 165 additions & 0 deletions examples/file-transfer/server.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright (c) 2024 Cesanta Software Limited
// All rights reserved

#include <signal.h>
#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; // store 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 {
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");
} else if (hm.body.len > (size_t) s_max_size) {
mg_http_reply(c, 400, "", "Too long\n");
} else {
char fpath[MG_PATH_MAX];
char *fname = strndup(hm.uri.ptr + 8, hm.uri.len - 8); // 8: /upload/
if (fname[0] == '\0') {
mg_http_reply(c, 400, "", "Name required\n");
} else if (!mg_path_is_sane(fname)) {
mg_http_reply(c, 400, "", "Invalid path\n");
} else {
snprintf(fpath, MG_PATH_MAX, "%s%c%s", s_upld_dir, MG_DIRSEP, fname);
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);
} 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);
if (data[1] >= data[0]) {
mg_fs_close(fd);
mg_http_reply(c, 200, "", "ok\n");
}
}
}
free(fname);
}
} 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;
}
Empty file.
9 changes: 9 additions & 0 deletions examples/file-transfer/web_root/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>File Transfer</title>
</head>
<body>
<p style="font-size:100px">&#128515;</p>
</body>
</html>

0 comments on commit 76193fe

Please sign in to comment.