Skip to content

Commit eb68bb2

Browse files
committed
Add file-transfer example
1 parent 922d148 commit eb68bb2

File tree

8 files changed

+386
-0
lines changed

8 files changed

+386
-0
lines changed

examples/file-transfer/Makefile

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
SPROG ?= server # Program we are building
2+
CPROG ?= client # Program we are building
3+
DELETE = rm -rf # Command to remove files
4+
SOUT ?= -o $(SPROG) # Compiler argument for output file
5+
COUT ?= -o $(CPROG) # Compiler argument for output file
6+
SSOURCES = server.c mongoose.c # Source code files
7+
CSOURCES = client.c mongoose.c # Source code files
8+
CFLAGS = -W -Wall -Wextra -g -I. # Build options
9+
10+
# Mongoose build options. See https://mongoose.ws/documentation/#build-options
11+
#CFLAGS_MONGOOSE += -DMG_ENABLE_LINES
12+
13+
ifeq ($(OS),Windows_NT) # Windows settings. Assume MinGW compiler. To use VC: make CC=cl CFLAGS=/MD OUT=/Feprog.exe
14+
SPROG ?= server.exe # Use .exe suffix for the binary
15+
CPROG ?= client.exe # Use .exe suffix for the binary
16+
CC = gcc # Use MinGW gcc compiler
17+
CFLAGS += -lws2_32 # Link against Winsock library
18+
DELETE = cmd /C del /Q /F /S # Command prompt command to delete files
19+
SOUT ?= -o $(SPROG) # Build output
20+
COUT ?= -o $(CPROG) # Build output
21+
endif
22+
23+
all: example # Default target. Build all and run server
24+
$(RUN) ./$(SPROG) $(SARGS)
25+
26+
example: $(SPROG) $(CPROG)
27+
28+
$(SPROG): $(SSOURCES) # Build program from sources
29+
$(CC) $(SSOURCES) $(CFLAGS) $(CFLAGS_MONGOOSE) $(CFLAGS_EXTRA) $(SOUT)
30+
31+
$(CPROG): $(CSOURCES) # Build program from sources
32+
$(CC) $(CSOURCES) $(CFLAGS) $(CFLAGS_MONGOOSE) $(CFLAGS_EXTRA) $(COUT)
33+
34+
clean: # Cleanup. Delete built program and all build artifacts
35+
$(DELETE) $(SPROG) $(CPROG) *.o *.obj *.exe *.dSYM

examples/file-transfer/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# File Transfer
2+
3+
This example contains minimal HTTP client and server.
4+
5+
The client uploads a file to the server in a single POST, shaping traffic to send small data chunks.
6+
7+
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.
8+
9+
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.
10+
11+
The server can also accept regular uploads from any HTTP client, for example curl:
12+
13+
```sh
14+
curl -su user:pass http://localhost:8090/upload/foo.txt --data-binary @Makefile
15+
```
16+
17+
- Follow the [Build Tools](../tools/) tutorial to setup your development environment.
18+
- Start a terminal in this project directory; and build the example:
19+
20+
```sh
21+
cd mongoose/examples/file-transfer
22+
make clean all
23+
```
24+
25+
- 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
26+
27+
```sh
28+
./server
29+
6332b7 2 server.c:157:main Mongoose version : v7.12
30+
6332b7 2 server.c:158:main Listening on : http://0.0.0.0:8090
31+
6332b7 2 server.c:159:main Web root : [/home/mongoose/examples/file-transfer/web_root]
32+
6332b7 2 server.c:160:main Uploading to : [/home/mongoose/examples/file-transfer/upload]
33+
```
34+
35+
- Manually run the client to send a file, default is to send it as "foo.txt" to the server in localhost at port 8090
36+
37+
```sh
38+
./client -f Makefile
39+
ok
40+
```
41+
42+
Default operation is to assume hardcoded username and password. Call both server and client with no arguments to see usage instructions
43+
44+
See detailed tutorials at
45+
https://mongoose.ws/tutorials/file-uploads/
46+
https://mongoose.ws/tutorials/http-server/
47+
https://mongoose.ws/tutorials/http-client/

examples/file-transfer/client.c

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (c) 2021 Cesanta Software Limited
2+
// All rights reserved
3+
//
4+
// Example HTTP client. Connect to `s_url`, send request, wait for a response,
5+
// print the response and exit.
6+
// You can change `s_url` from the command line by executing: ./example YOUR_URL
7+
//
8+
// To enable SSL/TLS, , see https://mongoose.ws/tutorials/tls/#how-to-build
9+
10+
#include "mongoose.h"
11+
12+
static int s_debug_level = MG_LL_INFO;
13+
static const char *s_user = "user";
14+
static const char *s_pass = "pass";
15+
static const char *s_fname = NULL;
16+
static struct mg_fd *fd; // file descriptor
17+
static size_t fsize;
18+
static const char *s_url = "http://localhost:8090/upload/foo.txt";
19+
static const uint64_t s_timeout_ms = 1500; // Connect timeout in milliseconds
20+
21+
// Print HTTP response and signal that we're done
22+
static void fn(struct mg_connection *c, int ev, void *ev_data) {
23+
if (ev == MG_EV_OPEN) {
24+
// Connection created. Store connect expiration time in c->data
25+
*(uint64_t *) c->data = mg_millis() + s_timeout_ms;
26+
} else if (ev == MG_EV_POLL) {
27+
if (mg_millis() > *(uint64_t *) c->data &&
28+
(c->is_connecting || c->is_resolving)) {
29+
mg_error(c, "Connect timeout");
30+
}
31+
} else if (ev == MG_EV_CONNECT) {
32+
// Connected to server. Extract host name from URL
33+
struct mg_str host = mg_url_host(s_url);
34+
// Send request
35+
MG_DEBUG(("Connected, send request"));
36+
mg_printf(c,
37+
"POST %s HTTP/1.0\r\n"
38+
"Host: %.*s\r\n"
39+
"Content-Type: octet-stream\r\n"
40+
"Content-Length: %d\r\n",
41+
mg_url_uri(s_url), (int) host.len, host.ptr, fsize);
42+
mg_http_bauth(c, s_user, s_pass); // Add Basic auth header
43+
mg_printf(c, "%s", "\r\n"); // End HTTP headers
44+
} else if (ev == MG_EV_WRITE && c->send.len < MG_IO_SIZE) {
45+
uint8_t *buf = alloca(MG_IO_SIZE);
46+
size_t len = MG_IO_SIZE - c->send.len;
47+
len = fsize < len ? fsize : len;
48+
fd->fs->rd(fd->fd, buf, len);
49+
mg_send(c, buf, len);
50+
fsize -= len;
51+
MG_DEBUG(("sent %u bytes", len));
52+
} else if (ev == MG_EV_HTTP_MSG) {
53+
MG_DEBUG(("MSG"));
54+
// Response is received. Print it
55+
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
56+
printf("%.*s", (int) hm->body.len, hm->body.ptr);
57+
c->is_draining = 1; // Tell mongoose to close this connection
58+
mg_fs_close(fd);
59+
*(bool *) c->fn_data = true; // Tell event loop to stop
60+
} else if (ev == MG_EV_ERROR) {
61+
MG_DEBUG(("ERROR"));
62+
mg_fs_close(fd);
63+
*(bool *) c->fn_data = true; // Error, tell event loop to stop
64+
}
65+
}
66+
67+
static void usage(const char *prog) {
68+
fprintf(stderr,
69+
"File Transfer client based on Mongoose v.%s\n"
70+
"Usage: %s -f NAME OPTIONS\n"
71+
" -u NAME - user name, default: '%s'\n"
72+
" -p PWD - password, default: '%s'\n"
73+
" -U URL - Full server URL, including destination file name; "
74+
"default: '%s'\n"
75+
" -f NAME - File to send\n"
76+
" -v LEVEL - debug level, from 0 to 4, default: %d\n",
77+
MG_VERSION, prog, s_user, s_pass, s_url, s_debug_level);
78+
exit(EXIT_FAILURE);
79+
}
80+
81+
int main(int argc, char *argv[]) {
82+
struct mg_mgr mgr; // Event manager
83+
bool done = false; // Event handler flips it to true
84+
time_t mtime;
85+
int i;
86+
87+
// Parse command-line flags
88+
for (i = 1; i < argc; i++) {
89+
if (strcmp(argv[i], "-f") == 0) {
90+
s_fname = argv[++i];
91+
} else if (strcmp(argv[i], "-u") == 0) {
92+
s_user = argv[++i];
93+
} else if (strcmp(argv[i], "-p") == 0) {
94+
s_pass = argv[++i];
95+
} else if (strcmp(argv[i], "-U") == 0) {
96+
s_url = argv[++i];
97+
} else if (strcmp(argv[i], "-v") == 0) {
98+
s_debug_level = atoi(argv[++i]);
99+
} else {
100+
usage(argv[0]);
101+
}
102+
}
103+
if (s_fname == NULL) usage(argv[0]);
104+
mg_fs_posix.st(s_fname, &fsize, &mtime);
105+
if (fsize == 0 ||
106+
(fd = mg_fs_open(&mg_fs_posix, s_fname, MG_FS_READ)) == NULL) {
107+
MG_ERROR(("open failed: %d", errno));
108+
exit(EXIT_FAILURE);
109+
}
110+
111+
mg_log_set(s_debug_level);
112+
mg_mgr_init(&mgr); // Initialise event manager
113+
mg_http_connect(&mgr, s_url, fn, &done); // Create client connection
114+
while (!done) mg_mgr_poll(&mgr, 50); // Event manager loops until 'done'
115+
mg_mgr_free(&mgr); // Free resources
116+
return 0;
117+
}

examples/file-transfer/mongoose.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../mongoose.c

examples/file-transfer/mongoose.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../mongoose.h

examples/file-transfer/server.c

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright (c) 2024 Cesanta Software Limited
2+
// All rights reserved
3+
4+
#include <signal.h>
5+
#include "mongoose.h"
6+
7+
static int s_debug_level = MG_LL_INFO;
8+
static int s_max_size = 10000;
9+
static const char *s_root_dir = "web_root";
10+
static const char *s_upld_dir = "upload";
11+
static const char *s_listening_address = "http://0.0.0.0:8090";
12+
static const char *s_user = "user";
13+
static const char *s_pass = "pass";
14+
15+
// Handle interrupts, like Ctrl-C
16+
static int s_signo;
17+
static void signal_handler(int signo) {
18+
s_signo = signo;
19+
}
20+
21+
static bool authuser(struct mg_http_message *hm) {
22+
char user[256], pass[256];
23+
mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass));
24+
if (strcmp(user, s_user) == 0 && strcmp(pass, s_pass) == 0) return true;
25+
return false;
26+
}
27+
28+
// Streaming upload example. Demonstrates how to use MG_EV_READ events
29+
// to get large payload in smaller chunks. To test, use curl utility:
30+
static void cb(struct mg_connection *c, int ev, void *ev_data) {
31+
if (ev == MG_EV_READ) {
32+
// Parse the incoming data ourselves. If we can parse the request,
33+
// store two size_t variables in c->data: expected len and recv len.
34+
size_t *data = (size_t *) c->data;
35+
struct mg_fd *fd = (struct mg_fd *) c->fn_data; // get file descriptor
36+
if (data[0]) { // Already parsed, receiving body
37+
data[1] += c->recv.len;
38+
MG_DEBUG(("Got chunk len %lu, %lu total", c->recv.len, data[1]));
39+
fd->fs->wr(fd->fd, c->recv.buf, c->recv.len);
40+
c->recv.len = 0; // And cleanup the receive buffer. Streaming!
41+
if (data[1] >= data[0]) {
42+
mg_fs_close(fd);
43+
mg_http_reply(c, 200, "", "ok\n");
44+
}
45+
} else if(c->is_resp == 0) {
46+
struct mg_http_message hm;
47+
int n = mg_http_parse((char *) c->recv.buf, c->recv.len, &hm);
48+
if (n < 0) mg_error(c, "Bad response");
49+
if (n > 0) {
50+
if (mg_http_match_uri(&hm, "/upload/#")) {
51+
if (!authuser(&hm)) {
52+
mg_http_reply(c, 403, "", "Denied\n");
53+
c->is_draining = 1; // Tell mongoose to close this connection
54+
} else if (hm.body.len > (size_t) s_max_size) {
55+
mg_http_reply(c, 400, "", "Too long\n");
56+
c->is_draining = 1; // Tell mongoose to close this connection
57+
} else if (hm.uri.len == 8) { // 8: /upload/
58+
mg_http_reply(c, 400, "", "Name required\n");
59+
c->is_draining = 1; // Tell mongoose to close this connection
60+
} else if (strlen(s_upld_dir) + (hm.uri.len - 8) + 2 >
61+
MG_PATH_MAX) { // 2: MG_DIRSEP + NUL
62+
mg_http_reply(c, 400, "", "Path is too long\n");
63+
c->is_draining = 1; // Tell mongoose to close this connection
64+
} else {
65+
char fpath[MG_PATH_MAX];
66+
snprintf(fpath, MG_PATH_MAX, "%s%c", s_upld_dir, MG_DIRSEP);
67+
strncat(fpath, hm.uri.ptr + 8, hm.uri.len - 8);
68+
if (!mg_path_is_sane(fpath)) {
69+
mg_http_reply(c, 400, "", "Invalid path\n");
70+
c->is_draining = 1; // Tell mongoose to close this connection
71+
} else {
72+
MG_DEBUG(("Got request, chunk len %lu", c->recv.len - n));
73+
if ((fd = mg_fs_open(&mg_fs_posix, fpath, MG_FS_WRITE)) == NULL) {
74+
mg_http_reply(c, 400, "", "open failed: %d", errno);
75+
c->is_draining = 1; // Tell mongoose to close this connection
76+
} else {
77+
c->fn_data = fd;
78+
c->recv.len -= n; // remove headers
79+
data[0] = hm.body.len;
80+
data[1] = c->recv.len;
81+
if (c->recv.len)
82+
fd->fs->wr(fd->fd, c->recv.buf + n, c->recv.len);
83+
c->recv.len = 0; // consume data
84+
if (data[1] >= data[0]) {
85+
mg_fs_close(fd);
86+
mg_http_reply(c, 200, "", "ok\n");
87+
}
88+
}
89+
}
90+
}
91+
c->is_resp = 1; // ignore the rest of the body
92+
} else {
93+
struct mg_http_serve_opts opts = {0};
94+
opts.root_dir = s_root_dir;
95+
mg_http_serve_dir(c, &hm, &opts);
96+
}
97+
}
98+
}
99+
}
100+
(void) ev_data;
101+
}
102+
103+
static void usage(const char *prog) {
104+
fprintf(stderr,
105+
"File Transfer server based on Mongoose v.%s\n"
106+
"Usage: %s OPTIONS\n"
107+
" -u NAME - user name, default: '%s'\n"
108+
" -p PWD - password, default: '%s'\n"
109+
" -d DIR - directory to serve, default: '%s'\n"
110+
" -D DIR - directory to store uploads, default: '%s'\n"
111+
" -s SIZE - maximum allowed file size, default: '%d'\n"
112+
" -l ADDR - listening address, default: '%s'\n"
113+
" -v LEVEL - debug level, from 0 to 4, default: %d\n",
114+
MG_VERSION, prog, s_user, s_pass, s_root_dir, s_upld_dir, s_max_size,
115+
s_listening_address, s_debug_level);
116+
exit(EXIT_FAILURE);
117+
}
118+
119+
int main(int argc, char *argv[]) {
120+
char spath[MG_PATH_MAX] = ".";
121+
char upath[MG_PATH_MAX] = ".";
122+
struct mg_mgr mgr;
123+
int i;
124+
125+
// Parse command-line flags
126+
for (i = 1; i < argc; i++) {
127+
if (strcmp(argv[i], "-d") == 0) {
128+
s_root_dir = argv[++i];
129+
} else if (strcmp(argv[i], "-D") == 0) {
130+
s_upld_dir = argv[++i];
131+
} else if (strcmp(argv[i], "-u") == 0) {
132+
s_user = argv[++i];
133+
} else if (strcmp(argv[i], "-p") == 0) {
134+
s_pass = argv[++i];
135+
} else if (strcmp(argv[i], "-l") == 0) {
136+
s_listening_address = argv[++i];
137+
} else if (strcmp(argv[i], "-v") == 0) {
138+
s_debug_level = atoi(argv[++i]);
139+
} else if (strcmp(argv[i], "-s") == 0) {
140+
s_max_size = atoi(argv[++i]);
141+
} else {
142+
usage(argv[0]);
143+
}
144+
}
145+
146+
// Root directory must not contain double dots. Make it absolute
147+
// Do the conversion only if the root dir spec does not contain overrides
148+
if (strchr(s_root_dir, ',') == NULL) {
149+
realpath(s_root_dir, spath);
150+
s_root_dir = spath;
151+
}
152+
if (strchr(s_upld_dir, ',') == NULL) {
153+
realpath(s_upld_dir, upath);
154+
s_upld_dir = upath;
155+
}
156+
157+
// Initialise stuff
158+
signal(SIGINT, signal_handler);
159+
signal(SIGTERM, signal_handler);
160+
mg_log_set(s_debug_level);
161+
mg_mgr_init(&mgr);
162+
if (mg_http_listen(&mgr, s_listening_address, cb, NULL) == NULL) {
163+
MG_ERROR(("Cannot listen on %s.", s_listening_address));
164+
exit(EXIT_FAILURE);
165+
}
166+
167+
// Start infinite event loop
168+
MG_INFO(("Mongoose version : v%s", MG_VERSION));
169+
MG_INFO(("Listening on : %s", s_listening_address));
170+
MG_INFO(("Web root : [%s]", s_root_dir));
171+
MG_INFO(("Uploading to : [%s]", s_upld_dir));
172+
while (s_signo == 0) mg_mgr_poll(&mgr, 1000);
173+
mg_mgr_free(&mgr);
174+
MG_INFO(("Exiting on signal %d", s_signo));
175+
return 0;
176+
}

examples/file-transfer/upload/README.md

Whitespace-only changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>File Transfer</title>
5+
</head>
6+
<body>
7+
<p style="font-size:100px">&#128515;</p>
8+
</body>
9+
</html>

0 commit comments

Comments
 (0)