diff --git a/src/libfetchers/git-lfs-fetch.cc b/src/libfetchers/git-lfs-fetch.cc index 4585e68e58ee..9d2fb928d603 100644 --- a/src/libfetchers/git-lfs-fetch.cc +++ b/src/libfetchers/git-lfs-fetch.cc @@ -8,6 +8,7 @@ #include "nix/util/util.hh" #include "nix/util/hash.hh" #include "nix/store/ssh.hh" +#include "nix/util/deleter.hh" #include #include diff --git a/src/libfetchers/git-utils.cc b/src/libfetchers/git-utils.cc index 1911ebdd9dc5..4751130acca2 100644 --- a/src/libfetchers/git-utils.cc +++ b/src/libfetchers/git-utils.cc @@ -14,6 +14,7 @@ #include "nix/util/thread-pool.hh" #include "nix/util/pool.hh" #include "nix/util/executable-path.hh" +#include "nix/util/deleter.hh" #include #include diff --git a/src/libfetchers/include/nix/fetchers/git-utils.hh b/src/libfetchers/include/nix/fetchers/git-utils.hh index eada8745c3eb..e462bc9e7b54 100644 --- a/src/libfetchers/include/nix/fetchers/git-utils.hh +++ b/src/libfetchers/include/nix/fetchers/git-utils.hh @@ -136,17 +136,6 @@ struct GitRepo virtual Hash dereferenceSingletonDirectory(const Hash & oid) = 0; }; -// A helper to ensure that the `git_*_free` functions get called. -template -struct Deleter -{ - template - void operator()(T * p) const - { - del(p); - }; -}; - // A helper to ensure that we don't leak objects returned by libgit2. template struct Setter diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc index 0f225f22c7d3..6f1c6b96cc54 100644 --- a/src/libstore/nar-info.cc +++ b/src/libstore/nar-info.cc @@ -113,9 +113,12 @@ std::string NarInfo::to_string(const StoreDirConfig & store) const res += "URL: " + url + "\n"; assert(compression != ""); res += "Compression: " + compression + "\n"; - assert(fileHash && fileHash->algo == HashAlgorithm::SHA256); - res += "FileHash: " + fileHash->to_string(HashFormat::Nix32, true) + "\n"; - res += "FileSize: " + std::to_string(fileSize) + "\n"; + if (fileHash) { + assert(fileHash->algo == HashAlgorithm::SHA256); + res += "FileHash: " + fileHash->to_string(HashFormat::Nix32, true) + "\n"; + } + if (fileSize) + res += "FileSize: " + std::to_string(fileSize) + "\n"; assert(narHash.algo == HashAlgorithm::SHA256); res += "NarHash: " + narHash.to_string(HashFormat::Nix32, true) + "\n"; res += "NarSize: " + std::to_string(narSize) + "\n"; diff --git a/src/libutil/include/nix/util/deleter.hh b/src/libutil/include/nix/util/deleter.hh new file mode 100644 index 000000000000..7f349b10ee4b --- /dev/null +++ b/src/libutil/include/nix/util/deleter.hh @@ -0,0 +1,19 @@ +#pragma once + +namespace nix { + +/** + * A helper for `std::unique_ptr` that ensures that C APIs that require manual memory management get properly freed. The + * template parameter `del` is a function that takes a pointer and frees it. + */ +template +struct Deleter +{ + template + void operator()(T * p) const + { + del(p); + }; +}; + +} // namespace nix \ No newline at end of file diff --git a/src/libutil/include/nix/util/file-system.hh b/src/libutil/include/nix/util/file-system.hh index 067240812f9a..1a5cf10bf29e 100644 --- a/src/libutil/include/nix/util/file-system.hh +++ b/src/libutil/include/nix/util/file-system.hh @@ -12,6 +12,7 @@ #include "nix/util/types.hh" #include "nix/util/file-descriptor.hh" #include "nix/util/file-path.hh" +#include "nix/util/deleter.hh" #include #include @@ -374,15 +375,7 @@ public: } }; -struct DIRDeleter -{ - void operator()(DIR * dir) const - { - closedir(dir); - } -}; - -typedef std::unique_ptr AutoCloseDir; +typedef std::unique_ptr> AutoCloseDir; /** * Create a temporary directory. diff --git a/src/libutil/include/nix/util/meson.build b/src/libutil/include/nix/util/meson.build index 8682f9c4dc16..6061c57c2851 100644 --- a/src/libutil/include/nix/util/meson.build +++ b/src/libutil/include/nix/util/meson.build @@ -31,6 +31,7 @@ headers = [ config_pub_h ] + files( 'config-impl.hh', 'configuration.hh', 'current-process.hh', + 'deleter.hh', 'demangle.hh', 'english.hh', 'environment-variables.hh', diff --git a/src/nix/meson.build b/src/nix/meson.build index f5602e730013..b7ddcc8eec44 100644 --- a/src/nix/meson.build +++ b/src/nix/meson.build @@ -34,6 +34,9 @@ subdir('nix-meson-build-support/subprojects') subdir('nix-meson-build-support/export-all-symbols') subdir('nix-meson-build-support/windows-version') +libmicrohttpd = dependency('libmicrohttpd') +deps_private += libmicrohttpd + configdata = configuration_data() # The CLI has a more detailed version string than the libraries; see `nixVersion` @@ -114,6 +117,7 @@ nix_sources = [ config_priv_h ] + files( 'run.cc', 'search.cc', 'self-exe.cc', + 'serve.cc', 'sigs.cc', 'store-copy-log.cc', 'store-delete.cc', diff --git a/src/nix/package.nix b/src/nix/package.nix index 11962c466f45..b62feeec3ee9 100644 --- a/src/nix/package.nix +++ b/src/nix/package.nix @@ -10,6 +10,8 @@ nix-cmd, sentry-native, + libmicrohttpd, + # Configuration Options version, @@ -72,6 +74,7 @@ mkMesonExecutable (finalAttrs: { nix-expr nix-main nix-cmd + libmicrohttpd ] ++ lib.optional ( stdenv.cc.isClang diff --git a/src/nix/serve.cc b/src/nix/serve.cc new file mode 100644 index 000000000000..1702c3e9bdf6 --- /dev/null +++ b/src/nix/serve.cc @@ -0,0 +1,192 @@ +#include "nix/cmd/command.hh" +#include "nix/util/file-system.hh" +#include "nix/util/signals.hh" +#include "nix/util/deleter.hh" +#include "nix/store/nar-info.hh" + +#include +#include + +#include +#include + +using namespace nix; + +using Response = std::unique_ptr>; + +struct CmdServe : StoreCommand +{ + uint16_t port = 8080; + std::optional priority; + std::optional portFile; + + CmdServe() + { + addFlag({ + .longName = "port", + .shortName = 'p', + .description = "Port to listen on (default: 8080). Use 0 to dynamically allocate a free port.", + .labels = {"port"}, + .handler = {&port}, + }); + addFlag({ + .longName = "port-file", + .description = "Write the bound port number to this file.", + .labels = {"path"}, + .handler = {[this](std::string s) { portFile = s; }}, + }); + addFlag({ + .longName = "priority", + .description = "Priority of this cache (overrides the store's default).", + .labels = {"priority"}, + .handler = {[this](std::string s) { priority = std::stoi(s); }}, + }); + } + + std::string description() override + { + return "serve a Nix store over the network"; + } + + MHD_Result + handleRequest(Store & store, MHD_Connection * connection, const std::string & url, std::string_view method) + try { + std::string clientAddr = "unknown"; + if (auto * info = MHD_get_connection_info(connection, MHD_CONNECTION_INFO_CLIENT_ADDRESS)) { + char buf[INET6_ADDRSTRLEN]; + auto * addr = info->client_addr; + const void * src = addr->sa_family == AF_INET6 ? (void *) &((sockaddr_in6 *) addr)->sin6_addr + : (void *) &((sockaddr_in *) addr)->sin_addr; + if (inet_ntop(addr->sa_family, src, buf, sizeof(buf))) + clientAddr = buf; + } + + notice("request: client=%s, method=%s, url=%s", clientAddr, method, url); + + Response response; + + auto notFound = [&] { + static constexpr std::string_view body = "404 not found\n"; + response.reset(MHD_create_response_from_buffer(body.size(), (void *) body.data(), MHD_RESPMEM_PERSISTENT)); + return MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response.get()); + }; + + static const std::regex narInfoUrlRegex{R"(^/([0-9a-z]+)\.narinfo$)"}; + static const std::regex narUrlRegex{R"(^/nar/([0-9a-z]+)-([0-9a-z]+)\.nar$)"}; + + if (method != MHD_HTTP_METHOD_GET && method != MHD_HTTP_METHOD_HEAD) { + std::string_view body = "405 method not allowed\n"; + response.reset(MHD_create_response_from_buffer(body.size(), (void *) body.data(), MHD_RESPMEM_PERSISTENT)); + MHD_add_response_header(response.get(), "Allow", MHD_HTTP_METHOD_GET); + return MHD_queue_response(connection, MHD_HTTP_METHOD_NOT_ALLOWED, response.get()); + } + + if (url == "/nix-cache-info") { + auto body = std::make_unique( + "StoreDir: " + store.storeDir + "\n" + "WantMassQuery: " + (store.config.wantMassQuery ? "1" : "0") + "\n" + "Priority: " + std::to_string(priority.value_or(store.config.priority)) + "\n"); + response.reset(MHD_create_response_from_buffer(body->size(), body->data(), MHD_RESPMEM_MUST_COPY)); + MHD_add_response_header(response.get(), "Content-Type", "text/x-nix-cache-info"); + + } else if (std::smatch m; std::regex_match(url, m, narInfoUrlRegex)) { + auto hashPart = m[1].str(); + auto path = store.queryPathFromHashPart(hashPart); + if (!path) + return notFound(); + + auto info = store.queryPathInfo(*path); + NarInfo ni(*info); + ni.compression = "none"; + // FIXME: would be nicer to use just the NAR hash, but we can't look up NARs by NAR hash. + ni.url = "nar/" + std::string(info->path.hashPart()) + "-" + + info->narHash.to_string(HashFormat::Nix32, false) + ".nar"; + ni.fileSize = info->narSize; + auto body = ni.to_string(store); + response.reset(MHD_create_response_from_buffer(body.size(), body.data(), MHD_RESPMEM_MUST_COPY)); + MHD_add_response_header(response.get(), "Content-Type", "text/x-nix-narinfo"); + + } else if (std::smatch m; std::regex_match(url, m, narUrlRegex)) { + auto hashPart = m[1].str(); + auto expectedNarHash = m[2].str(); + auto path = store.queryPathFromHashPart(hashPart); + if (!path) + return notFound(); + + auto info = store.queryPathInfo(*path); + if (info->narHash.to_string(HashFormat::Nix32, false) != expectedNarHash) + return notFound(); + + StringSink sink; + store.narFromPath(*path, sink); + response.reset(MHD_create_response_from_buffer(sink.s.size(), sink.s.data(), MHD_RESPMEM_MUST_COPY)); + MHD_add_response_header(response.get(), "Content-Type", "application/x-nix-nar"); + + } else + return notFound(); + + return MHD_queue_response(connection, MHD_HTTP_OK, response.get()); + + } catch (const Error & e) { + auto body = fmt("500 Internal Server Error\n\nError: %s", e.message()); + Response response; + response.reset(MHD_create_response_from_buffer(body.size(), body.data(), MHD_RESPMEM_MUST_COPY)); + MHD_add_response_header(response.get(), "Content-Type", "text/plain"); + return MHD_queue_response(connection, MHD_HTTP_INTERNAL_SERVER_ERROR, response.get()); + } + + void run(ref store) override + { + struct Ctx + { + Store & store; + CmdServe & cmd; + }; + + Ctx ctx{*store, *this}; + + auto handler = [](void * cls, + MHD_Connection * connection, + const char * url, + const char * method, + const char * version, + const char * upload_data, + size_t * upload_data_size, + void ** con_cls) -> MHD_Result { + auto & ctx = *static_cast(cls); + auto & store = ctx.store; + auto & cmd = ctx.cmd; + return cmd.handleRequest(store, connection, std::string(url), method); + }; + + auto * daemon = MHD_start_daemon( + MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_DUAL_STACK, + port, + nullptr, + nullptr, + handler, + &ctx, + MHD_OPTION_END); + + if (!daemon) + throw Error("failed to start HTTP daemon on port %d", port); + + auto * info = MHD_get_daemon_info(daemon, MHD_DAEMON_INFO_BIND_PORT); + uint16_t boundPort = info ? info->port : port; + notice("Listening on http://[::]:%d/", boundPort); + + if (portFile) + writeFile(*portFile, std::to_string(boundPort) + "\n"); + + /* Wait for Ctrl-C. */ + std::promise interruptPromise; + std::future interruptFuture = interruptPromise.get_future(); + auto callback = createInterruptCallback([&]() { interruptPromise.set_value(); }); + interruptFuture.get(); + + notice("Shutting down..."); + MHD_stop_daemon(daemon); + } +}; + +static auto rCmdServe = registerCommand("serve");