Skip to content

Commit 2b40dec

Browse files
committed
feat(proxy): add certbot integration
1 parent bc51016 commit 2b40dec

5 files changed

Lines changed: 386 additions & 4 deletions

File tree

CMakeLists.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ else()
192192
endif()
193193

194194
if (EXISTS "${_VIX_CRYPTO_DIR}/CMakeLists.txt" AND NOT TARGET vix::crypto)
195+
set(VIX_CRYPTO_BUILD_TESTS OFF CACHE BOOL "" FORCE)
196+
set(VIX_CRYPTO_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
195197
add_subdirectory("${_VIX_CRYPTO_DIR}" "${CMAKE_BINARY_DIR}/_vix_crypto")
196198
endif()
197199

@@ -353,6 +355,17 @@ else()
353355
message(STATUS "[cli] Game command disabled: vix::game target not found")
354356
endif()
355357

358+
# ----------------------------------------------------
359+
# Optional crypto command support
360+
# ----------------------------------------------------
361+
if (TARGET vix::crypto)
362+
message(STATUS "[cli] Crypto command enabled")
363+
target_link_libraries(vix_cli PRIVATE vix::crypto)
364+
target_compile_definitions(vix_cli PRIVATE VIX_CLI_HAS_CRYPTO=1)
365+
else()
366+
message(STATUS "[cli] Crypto command disabled: vix::crypto target not found")
367+
endif()
368+
356369
# ----------------------------------------------------
357370
# Optional db command support
358371
# ----------------------------------------------------
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
*
3+
* @file NginxCertbot.hpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2026, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*/
13+
#ifndef VIX_NGINX_CERTBOT_HPP
14+
#define VIX_NGINX_CERTBOT_HPP
15+
16+
#include <vix/cli/commands/proxy/ProxyTypes.hpp>
17+
18+
namespace vix::commands::proxy::nginx_certbot
19+
{
20+
/**
21+
* @brief Issue or renew a Let's Encrypt certificate using Certbot.
22+
*
23+
* This prepares an HTTP Nginx config first, runs Certbot with the Nginx
24+
* plugin, then reinstalls the final Vix TLS proxy config.
25+
*
26+
* @param cfg Loaded Nginx proxy configuration.
27+
* @return Process exit code.
28+
*/
29+
int run(const NginxProxyConfig &cfg);
30+
}
31+
32+
#endif

src/commands/ProxyCommand.cpp

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* Vix.cpp
1212
*/
1313
#include <vix/cli/commands/ProxyCommand.hpp>
14+
#include <vix/cli/commands/proxy/NginxCertbot.hpp>
1415
#include <vix/cli/commands/proxy/NginxChecker.hpp>
1516
#include <vix/cli/commands/proxy/NginxGenerator.hpp>
1617
#include <vix/cli/commands/proxy/NginxOutput.hpp>
@@ -32,11 +33,13 @@ namespace vix::commands
3233
<< "Commands:\n"
3334
<< " init Generate, install and enable an Nginx config\n"
3435
<< " check Validate the installed Nginx proxy config\n"
35-
<< " reload Validate and reload Nginx\n\n"
36+
<< " reload Validate and reload Nginx\n"
37+
<< " certbot Issue or renew a Let's Encrypt certificate\n\n"
3638
<< "Examples:\n"
3739
<< " vix proxy nginx init\n"
3840
<< " vix proxy nginx check\n"
39-
<< " vix proxy nginx reload\n";
41+
<< " vix proxy nginx reload\n"
42+
<< " vix proxy nginx certbot\n";
4043

4144
return 0;
4245
}
@@ -58,6 +61,9 @@ namespace vix::commands
5861
if (action == "reload")
5962
return proxy::nginx_checker::reload(cfg);
6063

64+
if (action == "certbot")
65+
return proxy::nginx_certbot::run(cfg);
66+
6167
proxy::nginx_output::error(
6268
std::cerr,
6369
"unknown nginx proxy command: " + action);
@@ -116,11 +122,13 @@ namespace vix::commands
116122
<< "Commands:\n"
117123
<< " nginx init Generate, install and enable an Nginx config\n"
118124
<< " nginx check Validate the installed Nginx proxy config\n"
119-
<< " nginx reload Validate and reload Nginx\n\n"
125+
<< " nginx reload Validate and reload Nginx\n"
126+
<< " nginx certbot Issue or renew a Let's Encrypt certificate\n\n"
120127
<< "Examples:\n"
121128
<< " vix proxy nginx init\n"
122129
<< " vix proxy nginx check\n"
123-
<< " vix proxy nginx reload\n";
130+
<< " vix proxy nginx reload\n"
131+
<< " vix proxy nginx certbot\n";
124132

125133
return 0;
126134
}
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/**
2+
*
3+
* @file NginxCertbot.cpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2026, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*/
13+
#include <vix/cli/commands/proxy/NginxCertbot.hpp>
14+
#include <vix/cli/commands/proxy/NginxGenerator.hpp>
15+
#include <vix/cli/commands/proxy/NginxOutput.hpp>
16+
17+
#include <cstdlib>
18+
#include <filesystem>
19+
#include <fstream>
20+
#include <iostream>
21+
#include <string>
22+
23+
namespace fs = std::filesystem;
24+
25+
namespace vix::commands::proxy::nginx_certbot
26+
{
27+
namespace
28+
{
29+
std::string shell_quote(const std::string &value)
30+
{
31+
std::string out = "'";
32+
33+
for (char c : value)
34+
{
35+
if (c == '\'')
36+
out += "'\\''";
37+
else
38+
out += c;
39+
}
40+
41+
out += "'";
42+
return out;
43+
}
44+
45+
bool run_cmd(const std::string &cmd)
46+
{
47+
return std::system(cmd.c_str()) == 0;
48+
}
49+
50+
fs::path default_certificate_path(const std::string &domain)
51+
{
52+
return fs::path("/etc/letsencrypt/live") / domain / "fullchain.pem";
53+
}
54+
55+
fs::path default_certificate_key_path(const std::string &domain)
56+
{
57+
return fs::path("/etc/letsencrypt/live") / domain / "privkey.pem";
58+
}
59+
60+
bool install_rendered_config(const NginxProxyConfig &cfg)
61+
{
62+
const std::string config = nginx_generator::render_config(cfg);
63+
const fs::path tmp = fs::temp_directory_path() / (cfg.appName + ".nginx");
64+
65+
{
66+
std::ofstream out(tmp);
67+
68+
if (!out)
69+
{
70+
nginx_output::error(
71+
std::cerr,
72+
"Failed to write temporary Nginx config: " + tmp.string());
73+
74+
return false;
75+
}
76+
77+
out << config;
78+
}
79+
80+
if (!run_cmd(
81+
"sudo cp " +
82+
shell_quote(tmp.string()) +
83+
" " +
84+
shell_quote(cfg.sitesAvailablePath.string())))
85+
{
86+
nginx_output::error(std::cerr, "Failed to install Nginx site config.");
87+
run_cmd("rm -f " + shell_quote(tmp.string()));
88+
return false;
89+
}
90+
91+
run_cmd("rm -f " + shell_quote(tmp.string()));
92+
93+
if (!run_cmd(
94+
"sudo ln -sfn " +
95+
shell_quote(cfg.sitesAvailablePath.string()) +
96+
" " +
97+
shell_quote(cfg.sitesEnabledPath.string())))
98+
{
99+
nginx_output::error(std::cerr, "Failed to enable Nginx site.");
100+
return false;
101+
}
102+
103+
return true;
104+
}
105+
106+
bool nginx_test()
107+
{
108+
if (!run_cmd("sudo nginx -t"))
109+
{
110+
nginx_output::error(std::cerr, "Nginx config is invalid.");
111+
nginx_output::fix(std::cerr, "sudo nginx -t");
112+
return false;
113+
}
114+
115+
nginx_output::ok(std::cout, "nginx config is valid");
116+
return true;
117+
}
118+
119+
bool reload_or_start_nginx()
120+
{
121+
if (run_cmd("systemctl is-active --quiet nginx"))
122+
{
123+
if (!run_cmd("sudo systemctl reload nginx"))
124+
{
125+
nginx_output::error(std::cerr, "Failed to reload Nginx.");
126+
nginx_output::fix(std::cerr, "sudo systemctl reload nginx");
127+
return false;
128+
}
129+
130+
nginx_output::ok(std::cout, "nginx reloaded");
131+
return true;
132+
}
133+
134+
if (!run_cmd("sudo systemctl start nginx"))
135+
{
136+
nginx_output::error(std::cerr, "Failed to start Nginx.");
137+
nginx_output::fix(std::cerr, "sudo systemctl start nginx");
138+
return false;
139+
}
140+
141+
nginx_output::ok(std::cout, "nginx started");
142+
return true;
143+
}
144+
145+
NginxProxyConfig make_http_bootstrap_config(const NginxProxyConfig &cfg)
146+
{
147+
NginxProxyConfig http_cfg = cfg;
148+
http_cfg.tlsEnabled = false;
149+
http_cfg.certificatePath.clear();
150+
http_cfg.certificateKeyPath.clear();
151+
return http_cfg;
152+
}
153+
154+
NginxProxyConfig make_final_tls_config(const NginxProxyConfig &cfg)
155+
{
156+
NginxProxyConfig tls_cfg = cfg;
157+
tls_cfg.tlsEnabled = true;
158+
159+
if (tls_cfg.certificatePath.empty())
160+
tls_cfg.certificatePath = default_certificate_path(tls_cfg.domain);
161+
162+
if (tls_cfg.certificateKeyPath.empty())
163+
tls_cfg.certificateKeyPath = default_certificate_key_path(tls_cfg.domain);
164+
165+
return tls_cfg;
166+
}
167+
}
168+
169+
int run(const NginxProxyConfig &cfg)
170+
{
171+
nginx_output::print_init_summary(std::cout, cfg);
172+
173+
if (cfg.domain.empty())
174+
{
175+
nginx_output::error(std::cerr, "Missing proxy domain.");
176+
nginx_output::fix(std::cerr, "add production.proxy.domain to vix.json");
177+
return 1;
178+
}
179+
180+
if (!run_cmd("command -v nginx >/dev/null 2>&1"))
181+
{
182+
nginx_output::error(std::cerr, "Nginx is not installed or not available in PATH.");
183+
nginx_output::fix(std::cerr, "install nginx");
184+
return 1;
185+
}
186+
187+
if (!run_cmd("command -v certbot >/dev/null 2>&1"))
188+
{
189+
nginx_output::error(std::cerr, "Certbot is not installed or not available in PATH.");
190+
nginx_output::fix(std::cerr, "sudo apt install certbot python3-certbot-nginx");
191+
return 1;
192+
}
193+
194+
nginx_output::ok(std::cout, "preparing temporary HTTP config for Certbot");
195+
196+
const NginxProxyConfig http_cfg = make_http_bootstrap_config(cfg);
197+
198+
if (!install_rendered_config(http_cfg))
199+
return 1;
200+
201+
if (!nginx_test())
202+
return 1;
203+
204+
if (!reload_or_start_nginx())
205+
return 1;
206+
207+
const std::string certbot_cmd =
208+
"sudo certbot --nginx -d " +
209+
shell_quote(cfg.domain);
210+
211+
nginx_output::ok(std::cout, "running certbot for " + cfg.domain);
212+
213+
if (!run_cmd(certbot_cmd))
214+
{
215+
nginx_output::error(std::cerr, "Certbot failed.");
216+
nginx_output::fix(std::cerr, certbot_cmd);
217+
return 1;
218+
}
219+
220+
nginx_output::ok(std::cout, "certificate issued or renewed");
221+
222+
const NginxProxyConfig tls_cfg = make_final_tls_config(cfg);
223+
224+
if (!fs::exists(tls_cfg.certificatePath))
225+
{
226+
nginx_output::error(
227+
std::cerr,
228+
"Let's Encrypt certificate was not found: " +
229+
tls_cfg.certificatePath.string());
230+
231+
nginx_output::fix(std::cerr, "check certbot output and domain DNS records");
232+
return 1;
233+
}
234+
235+
if (!fs::exists(tls_cfg.certificateKeyPath))
236+
{
237+
nginx_output::error(
238+
std::cerr,
239+
"Let's Encrypt private key was not found: " +
240+
tls_cfg.certificateKeyPath.string());
241+
242+
nginx_output::fix(std::cerr, "check certbot output and domain DNS records");
243+
return 1;
244+
}
245+
246+
nginx_output::ok(std::cout, "installing final Vix TLS proxy config");
247+
248+
if (!install_rendered_config(tls_cfg))
249+
return 1;
250+
251+
if (!nginx_test())
252+
return 1;
253+
254+
if (!reload_or_start_nginx())
255+
return 1;
256+
257+
nginx_output::ok(std::cout, "Let's Encrypt integration completed: " + cfg.domain);
258+
return 0;
259+
}
260+
}

0 commit comments

Comments
 (0)