Skip to content

Commit 90efe29

Browse files
committed
Example implementation of an SMTP server
It is not complete, and in no way it's meant to be used for real It's just an example of how one can use the DSL for an interesting use case.
1 parent 43d90aa commit 90efe29

15 files changed

+945
-3
lines changed

.github/workflows/c-cpp.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
steps:
1515
- uses: actions/checkout@v3
1616
- name: build-depends
17-
run: sudo apt update && sudo apt -y install libgtest-dev build-essential cmake libgtest-dev libuv1-dev && sudo apt clean
17+
run: sudo apt update && sudo apt -y install libgtest-dev build-essential cmake libgtest-dev libuv1-dev libcli11-dev && sudo apt clean
1818
- name: configure
1919
run: mkdir build && cd build && cmake ../ -DCMAKE_BUILD_TYPE=Debug
2020
- name: make

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,4 @@ target_link_libraries( networkprotocoldsl_uv PUBLIC networkprotocoldsl ${LIBUV_L
251251

252252
enable_testing()
253253
add_subdirectory(tests)
254+
add_subdirectory(examples)

examples/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
add_subdirectory(smtpserver)

examples/smtpserver/CMakeLists.txt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Add a custom command to generate the embedded file.
2+
3+
set(SMTP_SOURCE_BASENAME smtp.networkprotocoldsl)
4+
set(SMTP_SOURCE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${SMTP_SOURCE_BASENAME})
5+
set(SMTP_STRING_LITERAL_FILE ${CMAKE_CURRENT_BINARY_DIR}/${SMTP_SOURCE_BASENAME}.literal)
6+
add_custom_command(
7+
OUTPUT ${SMTP_STRING_LITERAL_FILE}
8+
COMMAND ${CMAKE_COMMAND}
9+
-DINPUT_FILE="${SMTP_SOURCE_FILE}"
10+
-DOUTPUT_FILE="${SMTP_STRING_LITERAL_FILE}"
11+
-P "${CMAKE_CURRENT_SOURCE_DIR}/generate_string_literals.cmake"
12+
DEPENDS ${SMTP_SOURCE_FILE} generate_string_literals.cmake
13+
COMMENT "Generating ${SMTP_STRING_LITERAL_FILE} from ${SMTP_SOURCE_FILE}"
14+
)
15+
16+
# Instead of defining a separate target, mark main.cpp as dependent on the generated file.
17+
set_source_files_properties(
18+
interpreted_program.cpp PROPERTIES OBJECT_DEPENDS ${SMTP_STRING_LITERAL_FILE}
19+
)
20+
21+
# Build the smtpserver executable and make it depend on the generated file.
22+
find_package(CLI11 REQUIRED)
23+
24+
add_executable(smtpserver
25+
main.cpp
26+
interpreted_program.cpp
27+
interpreted_program.hpp
28+
server_core.cpp
29+
server_core.hpp
30+
server_processor.cpp
31+
server_processor.hpp
32+
)
33+
target_link_libraries(smtpserver PRIVATE networkprotocoldsl networkprotocoldsl_uv CLI11::CLI11)
34+
target_compile_definitions(smtpserver PRIVATE -DSMTP_NETWORKPROTOCOLDSL_LITERAL="${SMTP_STRING_LITERAL_FILE}")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
if(NOT DEFINED INPUT_FILE OR NOT DEFINED OUTPUT_FILE)
2+
message(FATAL_ERROR "Usage: cmake -DINPUT_FILE=<input> -DOUTPUT_FILE=<output> -P generate_string_literals.cmake")
3+
endif()
4+
5+
file(READ "${INPUT_FILE}" RAW_CONTENT)
6+
# Escape backslashes.
7+
string(REPLACE "\\" "\\\\" RAW_CONTENT_ESCAPED_BS "${RAW_CONTENT}")
8+
# Escape double quotes.
9+
string(REPLACE "\"" "\\\"" RAW_CONTENT_ESCAPED "${RAW_CONTENT_ESCAPED_BS}")
10+
# Replace newlines with newline escape and closing/opening quotes.
11+
string(REPLACE "\n" "\\n\"\n\"" TRANSFORMED_CONTENT "${RAW_CONTENT_ESCAPED}")
12+
set(TRANSFORMED_CONTENT "\"${TRANSFORMED_CONTENT}\"")
13+
file(WRITE "${OUTPUT_FILE}" "${TRANSFORMED_CONTENT}")
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#include "interpreted_program.hpp"
2+
#include <string>
3+
4+
namespace smtpserver {
5+
std::optional<networkprotocoldsl::InterpretedProgram>
6+
load_interpreted_program() {
7+
// Load the embedded static source code via the compile definition.
8+
std::string source_code = std::string(
9+
#ifndef SMTP_NETWORKPROTOCOLDSL_LITERAL
10+
#error "SMTP_NETWORKPROTOCOLDSL_LITERAL not defined"
11+
#endif
12+
#include SMTP_NETWORKPROTOCOLDSL_LITERAL
13+
);
14+
return networkprotocoldsl::InterpretedProgram::generate_server_from_source(
15+
source_code);
16+
}
17+
} // namespace smtpserver
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#ifndef SMTPSERVER_INTERPRETED_PROGRAM_HPP
2+
#define SMTPSERVER_INTERPRETED_PROGRAM_HPP
3+
4+
#include <networkprotocoldsl/interpretedprogram.hpp>
5+
#include <optional>
6+
7+
namespace smtpserver {
8+
// Loads the embedded static source and returns an interpreted program.
9+
std::optional<networkprotocoldsl::InterpretedProgram>
10+
load_interpreted_program();
11+
} // namespace smtpserver
12+
13+
#endif // SMTPSERVER_INTERPRETED_PROGRAM_HPP

examples/smtpserver/main.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#include "interpreted_program.hpp"
2+
#include "server_core.hpp"
3+
4+
#include <networkprotocoldsl_uv/asyncworkqueue.hpp>
5+
6+
#include <iostream>
7+
#include <span>
8+
#include <string>
9+
#include <thread>
10+
#include <uv.h>
11+
#include <vector>
12+
13+
int main(int argc, const char **argv) {
14+
std::cerr << "SMTP Server example started." << std::endl;
15+
16+
// Retrieve the interpreted program from the embedded static source.
17+
auto maybe_program = smtpserver::load_interpreted_program();
18+
if (!maybe_program.has_value()) {
19+
std::cerr << "Failed to load the interpreted program." << std::endl;
20+
return 1;
21+
}
22+
23+
// Set up libuv loop and async work queue.
24+
uv_loop_t *loop = uv_default_loop();
25+
networkprotocoldsl_uv::AsyncWorkQueue async_queue(loop);
26+
27+
// Start the libuv loop in a separate thread.
28+
std::thread io_thread([&]() { uv_run(loop, UV_RUN_DEFAULT); });
29+
30+
// Create a span from argv without creating a vector.
31+
std::span<const char *> args(argv, static_cast<size_t>(argc));
32+
smtpserver::main_server(args, *maybe_program, async_queue);
33+
34+
async_queue.shutdown().wait();
35+
io_thread.join();
36+
37+
return 0;
38+
}

examples/smtpserver/server_core.cpp

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#include "server_core.hpp"
2+
#include "server_processor.hpp"
3+
4+
#include <networkprotocoldsl_uv/libuvserverwrapper.hpp>
5+
6+
#include <CLI/CLI.hpp>
7+
8+
#include <condition_variable>
9+
#include <csignal>
10+
#include <future>
11+
#include <iostream>
12+
#include <mutex>
13+
#include <unordered_map>
14+
#include <unordered_set>
15+
16+
namespace {
17+
18+
std::mutex signal_mtx;
19+
std::condition_variable signal_cv;
20+
bool stop_signal_received = false;
21+
22+
void signal_handler(int signum) {
23+
std::lock_guard<std::mutex> lock(signal_mtx);
24+
stop_signal_received = true;
25+
std::cerr << "Received signal: " << signum << std::endl;
26+
signal_cv.notify_one();
27+
}
28+
29+
} // anonymous namespace
30+
31+
namespace smtpserver {
32+
33+
void parse_email_list(
34+
const std::string &str,
35+
std::unordered_map<std::string, std::unordered_set<std::string>> &map) {
36+
auto pos = str.find('@');
37+
if (pos == std::string::npos) {
38+
throw CLI::ValidationError("Invalid format for email");
39+
}
40+
map[str.substr(pos + 1)].insert(str.substr(0, pos));
41+
}
42+
43+
std::optional<int>
44+
main_server(const std::span<const char *> &args,
45+
const networkprotocoldsl::InterpretedProgram &program,
46+
networkprotocoldsl_uv::AsyncWorkQueue &async_queue) {
47+
// Setup CLI11 parser.
48+
ServerConfiguration config;
49+
50+
// add some sample values to the configuration
51+
config.valid_recipients["example.com"].insert("user1");
52+
config.valid_recipients["example.com"].insert("user2");
53+
config.valid_recipients["other.com"].insert("user3");
54+
config.blocked_senders["bad.com"].insert("user");
55+
config.blocked_client_domains.insert("bad.com");
56+
57+
CLI::App app{"Server configuration"};
58+
app.add_option("--bind-ip", config.bind_ip, "IP address to bind")
59+
->default_val("127.0.0.1");
60+
app.add_option("--bind-port", config.bind_port, "Port to bind")
61+
->default_val(8080);
62+
app.add_option("--server-name", config.server_name, "Server name")
63+
->default_val("testsrv");
64+
app.add_option("--maildir", config.maildir, "Mail directory")
65+
->default_val("./maildir/");
66+
app.add_option("--valid-recipients", config.valid_recipients,
67+
"Valid recipients");
68+
app.add_option("--blocked-senders", config.blocked_senders,
69+
"Blocked senders");
70+
app.add_option("--blocked-client-domains", config.blocked_client_domains,
71+
"Blocked client domains");
72+
73+
// Convert span to vector (skip program name).
74+
std::vector<std::string> args_cli(++args.begin(), args.end());
75+
try {
76+
app.parse(args_cli);
77+
} catch (const CLI::ParseError &e) {
78+
return app.exit(e);
79+
}
80+
81+
std::cerr << "Binding to IP: " << config.bind_ip
82+
<< ", Port: " << config.bind_port << std::endl;
83+
84+
// Get the server callbacks.
85+
auto server_callbacks = get_sever_callbacks(config);
86+
87+
// Create server wrapper and start the server.
88+
networkprotocoldsl_uv::LibuvServerWrapper server_wrapper(
89+
program, server_callbacks, async_queue);
90+
auto bind_future = server_wrapper.start(config.bind_ip, config.bind_port);
91+
bind_future.wait();
92+
auto bind_result = bind_future.get();
93+
if (std::holds_alternative<std::string>(bind_result)) {
94+
std::cerr << "Bind failed: " << std::get<std::string>(bind_result)
95+
<< std::endl;
96+
return std::nullopt;
97+
}
98+
std::cerr << "Server started on descriptor: " << std::get<int>(bind_result)
99+
<< std::endl;
100+
101+
// Setup signal handling for SIGINT.
102+
std::signal(SIGINT, signal_handler);
103+
std::cerr << "Server is running; press Ctrl+C to stop." << std::endl;
104+
105+
{
106+
std::unique_lock<std::mutex> lock(signal_mtx);
107+
signal_cv.wait(lock, [] { return stop_signal_received; });
108+
}
109+
110+
std::cerr << "Stopping server... " << std::flush;
111+
server_wrapper.stop();
112+
113+
std::cerr << "done" << std::endl;
114+
115+
return 0;
116+
}
117+
118+
} // namespace smtpserver

examples/smtpserver/server_core.hpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#ifndef SMTPSERVER_SERVER_CORE_HPP
2+
#define SMTPSERVER_SERVER_CORE_HPP
3+
4+
#include <networkprotocoldsl/interpretedprogram.hpp>
5+
#include <networkprotocoldsl_uv/asyncworkqueue.hpp>
6+
7+
#include <optional>
8+
#include <span>
9+
10+
#include <unordered_map>
11+
#include <unordered_set>
12+
13+
// ...existing code...
14+
namespace smtpserver {
15+
16+
struct ServerConfiguration {
17+
std::string server_name;
18+
std::unordered_map<std::string, std::unordered_set<std::string>>
19+
valid_recipients;
20+
std::unordered_map<std::string, std::unordered_set<std::string>>
21+
blocked_senders;
22+
std::unordered_set<std::string> blocked_client_domains;
23+
std::string maildir;
24+
std::string bind_ip;
25+
int bind_port;
26+
};
27+
28+
std::optional<int>
29+
main_server(const std::span<const char *> &args,
30+
const networkprotocoldsl::InterpretedProgram &program,
31+
networkprotocoldsl_uv::AsyncWorkQueue &async_queue);
32+
} // namespace smtpserver
33+
34+
#endif // SMTPSERVER_SERVER_CORE_HPP

0 commit comments

Comments
 (0)