Skip to content

Commit dad1a4e

Browse files
committed
Add 'nix serve' command
This is a built-in binary cache server, similar to nix-serve. It uses libmicrohttpd.
1 parent 3e9228a commit dad1a4e

3 files changed

Lines changed: 199 additions & 0 deletions

File tree

src/nix/meson.build

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ subdir('nix-meson-build-support/subprojects')
3434
subdir('nix-meson-build-support/export-all-symbols')
3535
subdir('nix-meson-build-support/windows-version')
3636

37+
libmicrohttpd = dependency('libmicrohttpd')
38+
deps_private += libmicrohttpd
39+
3740
configdata = configuration_data()
3841

3942
# The CLI has a more detailed version string than the libraries; see `nixVersion`
@@ -114,6 +117,7 @@ nix_sources = [ config_priv_h ] + files(
114117
'run.cc',
115118
'search.cc',
116119
'self-exe.cc',
120+
'serve.cc',
117121
'sigs.cc',
118122
'store-copy-log.cc',
119123
'store-delete.cc',

src/nix/package.nix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
nix-cmd,
1111
sentry-native,
1212

13+
libmicrohttpd,
14+
1315
# Configuration Options
1416

1517
version,
@@ -72,6 +74,7 @@ mkMesonExecutable (finalAttrs: {
7274
nix-expr
7375
nix-main
7476
nix-cmd
77+
libmicrohttpd
7578
]
7679
++ lib.optional (
7780
stdenv.cc.isClang

src/nix/serve.cc

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#include "nix/cmd/command.hh"
2+
#include "nix/util/file-system.hh"
3+
#include "nix/util/signals.hh"
4+
#include "nix/fetchers/git-utils.hh" // for Deleter
5+
#include "nix/store/nar-info.hh"
6+
7+
#include <future>
8+
#include <regex>
9+
10+
#include <arpa/inet.h>
11+
#include <microhttpd.h>
12+
13+
using namespace nix;
14+
15+
using Response = std::unique_ptr<MHD_Response, Deleter<MHD_destroy_response>>;
16+
17+
struct CmdServe : StoreCommand
18+
{
19+
uint16_t port = 8080;
20+
std::optional<int> priority;
21+
std::optional<std::filesystem::path> portFile;
22+
23+
CmdServe()
24+
{
25+
addFlag({
26+
.longName = "port",
27+
.shortName = 'p',
28+
.description = "Port to listen on (default: 8080). Use 0 to dynamically allocate a free port.",
29+
.labels = {"port"},
30+
.handler = {&port},
31+
});
32+
addFlag({
33+
.longName = "port-file",
34+
.description = "Write the bound port number to this file.",
35+
.labels = {"path"},
36+
.handler = {[this](std::string s) { portFile = s; }},
37+
});
38+
addFlag({
39+
.longName = "priority",
40+
.description = "Priority of this cache (overrides the store's default).",
41+
.labels = {"priority"},
42+
.handler = {[this](std::string s) { priority = std::stoi(s); }},
43+
});
44+
}
45+
46+
std::string description() override
47+
{
48+
return "serve a Nix store over the network";
49+
}
50+
51+
MHD_Result
52+
handleRequest(Store & store, MHD_Connection * connection, const std::string & url, std::string_view method)
53+
try {
54+
std::string clientAddr = "unknown";
55+
if (auto * info = MHD_get_connection_info(connection, MHD_CONNECTION_INFO_CLIENT_ADDRESS)) {
56+
char buf[INET6_ADDRSTRLEN];
57+
auto * addr = info->client_addr;
58+
const void * src = addr->sa_family == AF_INET6 ? (void *) &((sockaddr_in6 *) addr)->sin6_addr
59+
: (void *) &((sockaddr_in *) addr)->sin_addr;
60+
if (inet_ntop(addr->sa_family, src, buf, sizeof(buf)))
61+
clientAddr = buf;
62+
}
63+
64+
notice("request: client=%s, method=%s, url=%s", clientAddr, method, url);
65+
66+
Response response;
67+
68+
auto notFound = [&] {
69+
static constexpr std::string_view body = "404 not found\n";
70+
response.reset(MHD_create_response_from_buffer(body.size(), (void *) body.data(), MHD_RESPMEM_PERSISTENT));
71+
return MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response.get());
72+
};
73+
74+
static const std::regex narInfoUrlRegex{R"(^/([0-9a-z]+)\.narinfo$)"};
75+
static const std::regex narUrlRegex{R"(^/nar/([0-9a-z]+)-([0-9a-z]+)\.nar$)"};
76+
77+
if (method != MHD_HTTP_METHOD_GET && method != MHD_HTTP_METHOD_HEAD) {
78+
std::string_view body = "405 method not allowed\n";
79+
response.reset(MHD_create_response_from_buffer(body.size(), (void *) body.data(), MHD_RESPMEM_PERSISTENT));
80+
MHD_add_response_header(response.get(), "Allow", MHD_HTTP_METHOD_GET);
81+
return MHD_queue_response(connection, MHD_HTTP_METHOD_NOT_ALLOWED, response.get());
82+
}
83+
84+
if (url == "/nix-cache-info") {
85+
auto body = std::make_unique<std::string>(
86+
"StoreDir: " + store.storeDir + "\n"
87+
"WantMassQuery: " + (store.config.wantMassQuery ? "1" : "0") + "\n"
88+
"Priority: " + std::to_string(priority.value_or(store.config.priority)) + "\n");
89+
response.reset(MHD_create_response_from_buffer(body->size(), body->data(), MHD_RESPMEM_MUST_COPY));
90+
MHD_add_response_header(response.get(), "Content-Type", "text/x-nix-cache-info");
91+
92+
} else if (std::smatch m; std::regex_match(url, m, narInfoUrlRegex)) {
93+
auto hashPart = m[1].str();
94+
auto path = store.queryPathFromHashPart(hashPart);
95+
if (!path)
96+
return notFound();
97+
98+
auto info = store.queryPathInfo(*path);
99+
NarInfo ni(*info);
100+
ni.compression = "none";
101+
// FIXME: would be nicer to use just the NAR hash, but we can't look up NARs by NAR hash.
102+
ni.url = "nar/" + std::string(info->path.hashPart()) + "-"
103+
+ info->narHash.to_string(HashFormat::Nix32, false) + ".nar";
104+
ni.fileSize = info->narSize;
105+
auto body = ni.to_string(store);
106+
response.reset(MHD_create_response_from_buffer(body.size(), body.data(), MHD_RESPMEM_MUST_COPY));
107+
MHD_add_response_header(response.get(), "Content-Type", "text/x-nix-narinfo");
108+
109+
} else if (std::smatch m; std::regex_match(url, m, narUrlRegex)) {
110+
auto hashPart = m[1].str();
111+
auto expectedNarHash = m[2].str();
112+
auto path = store.queryPathFromHashPart(hashPart);
113+
if (!path)
114+
return notFound();
115+
116+
auto info = store.queryPathInfo(*path);
117+
if (info->narHash.to_string(HashFormat::Nix32, false) != expectedNarHash)
118+
return notFound();
119+
120+
StringSink sink;
121+
store.narFromPath(*path, sink);
122+
response.reset(MHD_create_response_from_buffer(sink.s.size(), sink.s.data(), MHD_RESPMEM_MUST_COPY));
123+
MHD_add_response_header(response.get(), "Content-Type", "application/x-nix-nar");
124+
125+
} else
126+
return notFound();
127+
128+
return MHD_queue_response(connection, MHD_HTTP_OK, response.get());
129+
130+
} catch (const Error & e) {
131+
auto body = fmt("500 Internal Server Error\n\nError: %s", e.message());
132+
Response response;
133+
response.reset(MHD_create_response_from_buffer(body.size(), body.data(), MHD_RESPMEM_MUST_COPY));
134+
MHD_add_response_header(response.get(), "Content-Type", "text/plain");
135+
return MHD_queue_response(connection, MHD_HTTP_INTERNAL_SERVER_ERROR, response.get());
136+
}
137+
138+
void run(ref<Store> store) override
139+
{
140+
struct Ctx
141+
{
142+
Store & store;
143+
CmdServe & cmd;
144+
};
145+
146+
Ctx ctx{*store, *this};
147+
148+
auto handler = [](void * cls,
149+
MHD_Connection * connection,
150+
const char * url,
151+
const char * method,
152+
const char * version,
153+
const char * upload_data,
154+
size_t * upload_data_size,
155+
void ** con_cls) -> MHD_Result {
156+
auto & ctx = *static_cast<Ctx *>(cls);
157+
auto & store = ctx.store;
158+
auto & cmd = ctx.cmd;
159+
return cmd.handleRequest(store, connection, std::string(url), method);
160+
};
161+
162+
auto * daemon = MHD_start_daemon(
163+
MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_DUAL_STACK,
164+
port,
165+
nullptr,
166+
nullptr,
167+
handler,
168+
&ctx,
169+
MHD_OPTION_END);
170+
171+
if (!daemon)
172+
throw Error("failed to start HTTP daemon on port %d", port);
173+
174+
auto * info = MHD_get_daemon_info(daemon, MHD_DAEMON_INFO_BIND_PORT);
175+
uint16_t boundPort = info ? info->port : port;
176+
notice("Listening on http://[::]:%d/", boundPort);
177+
178+
if (portFile)
179+
writeFile(*portFile, std::to_string(boundPort) + "\n");
180+
181+
/* Wait for Ctrl-C. */
182+
std::promise<void> interruptPromise;
183+
std::future<void> interruptFuture = interruptPromise.get_future();
184+
auto callback = createInterruptCallback([&]() { interruptPromise.set_value(); });
185+
interruptFuture.get();
186+
187+
notice("Shutting down...");
188+
MHD_stop_daemon(daemon);
189+
}
190+
};
191+
192+
static auto rCmdServe = registerCommand<CmdServe>("serve");

0 commit comments

Comments
 (0)