#include <absl/strings/match.h> #include <absl/strings/str_cat.h> #include <glog/logging.h> #include "libstore/derivations.hh" #include "libstore/remote-store.hh" #include "libstore/serve-protocol.hh" #include "libstore/ssh.hh" #include "libstore/store-api.hh" #include "libstore/worker-protocol.hh" #include "libutil/archive.hh" #include "libutil/pool.hh" namespace nix { constexpr std::string_view kUriScheme = "ssh://"; struct LegacySSHStore : public Store { const Setting<int> maxConnections{ this, 1, "max-connections", "maximum number of concurrent SSH connections"}; const Setting<Path> sshKey{this, "", "ssh-key", "path to an SSH private key"}; const Setting<bool> compress{this, false, "compress", "whether to compress the connection"}; const Setting<Path> remoteProgram{ this, "nix-store", "remote-program", "path to the nix-store executable on the remote system"}; const Setting<std::string> remoteStore{ this, "", "remote-store", "URI of the store on the remote system"}; // Hack for getting remote build log output. const Setting<int> logFD{ this, -1, "log-fd", "file descriptor to which SSH's stderr is connected"}; struct Connection { std::unique_ptr<SSHMaster::Connection> sshConn; FdSink to; FdSource from; int remoteVersion; bool good = true; }; std::string host; ref<Pool<Connection>> connections; SSHMaster master; LegacySSHStore(const std::string& host, const Params& params) : Store(params), host(host), connections(make_ref<Pool<Connection>>( std::max(1, (int)maxConnections), [this]() { return openConnection(); }, [](const ref<Connection>& r) { return r->good; })), master(host, sshKey, // Use SSH master only if using more than 1 connection. connections->capacity() > 1, compress, logFD) {} ref<Connection> openConnection() { auto conn = make_ref<Connection>(); conn->sshConn = master.startCommand( fmt("%s --serve --write", remoteProgram) + (remoteStore.get().empty() ? "" : " --store " + shellEscape(remoteStore.get()))); conn->to = FdSink(conn->sshConn->in.get()); conn->from = FdSource(conn->sshConn->out.get()); try { conn->to << SERVE_MAGIC_1 << SERVE_PROTOCOL_VERSION; conn->to.flush(); unsigned int magic = readInt(conn->from); if (magic != SERVE_MAGIC_2) { throw Error("protocol mismatch with 'nix-store --serve' on '%s'", host); } conn->remoteVersion = readInt(conn->from); if (GET_PROTOCOL_MAJOR(conn->remoteVersion) != 0x200) { throw Error("unsupported 'nix-store --serve' protocol version on '%s'", host); } } catch (EndOfFile& e) { throw Error("cannot connect to '%1%'", host); } return conn; }; std::string getUri() override { return absl::StrCat(kUriScheme, host); } void queryPathInfoUncached( const Path& path, Callback<std::shared_ptr<ValidPathInfo>> callback) noexcept override { try { auto conn(connections->get()); DLOG(INFO) << "querying remote host '" << host << "' for info on '" << path << "'"; conn->to << cmdQueryPathInfos << PathSet{path}; conn->to.flush(); auto info = std::make_shared<ValidPathInfo>(); conn->from >> info->path; if (info->path.empty()) { return callback(nullptr); } assert(path == info->path); PathSet references; conn->from >> info->deriver; info->references = readStorePaths<PathSet>(*this, conn->from); readLongLong(conn->from); // download size info->narSize = readLongLong(conn->from); if (GET_PROTOCOL_MINOR(conn->remoteVersion) >= 4) { auto s = readString(conn->from); if (s.empty()) { info->narHash = Hash(); } else { auto hash_ = Hash::deserialize(s); info->narHash = Hash::unwrap_throw(hash_); } conn->from >> info->ca; info->sigs = readStrings<StringSet>(conn->from); } auto s = readString(conn->from); assert(s.empty()); callback(std::move(info)); } catch (...) { callback.rethrow(); } } void addToStore(const ValidPathInfo& info, Source& source, RepairFlag repair, CheckSigsFlag checkSigs, std::shared_ptr<FSAccessor> accessor) override { DLOG(INFO) << "adding path '" << info.path << "' to remote host '" << host << "'"; auto conn(connections->get()); if (GET_PROTOCOL_MINOR(conn->remoteVersion) >= 5) { conn->to << cmdAddToStoreNar << info.path << info.deriver << info.narHash.to_string(Base16, false) << info.references << info.registrationTime << info.narSize << static_cast<uint64_t>(info.ultimate) << info.sigs << info.ca; try { copyNAR(source, conn->to); } catch (...) { conn->good = false; throw; } conn->to.flush(); } else { conn->to << cmdImportPaths << 1; try { copyNAR(source, conn->to); } catch (...) { conn->good = false; throw; } conn->to << exportMagic << info.path << info.references << info.deriver << 0 << 0; conn->to.flush(); } if (readInt(conn->from) != 1) { throw Error( "failed to add path '%s' to remote host '%s', info.path, host"); } } void narFromPath(const Path& path, Sink& sink) override { auto conn(connections->get()); conn->to << cmdDumpStorePath << path; conn->to.flush(); copyNAR(conn->from, sink); } Path queryPathFromHashPart(const std::string& hashPart) override { unsupported("queryPathFromHashPart"); } Path addToStore(const std::string& name, const Path& srcPath, bool recursive, HashType hashAlgo, PathFilter& filter, RepairFlag repair) override { unsupported("addToStore"); } Path addTextToStore(const std::string& name, const std::string& s, const PathSet& references, RepairFlag repair) override { unsupported("addTextToStore"); } BuildResult buildDerivation(const Path& drvPath, const BasicDerivation& drv, BuildMode buildMode) override { auto conn(connections->get()); conn->to << cmdBuildDerivation << drvPath << drv << settings.maxSilentTime << settings.buildTimeout; if (GET_PROTOCOL_MINOR(conn->remoteVersion) >= 2) { conn->to << settings.maxLogSize; } if (GET_PROTOCOL_MINOR(conn->remoteVersion) >= 3) { conn->to << settings.buildRepeat << static_cast<uint64_t>(settings.enforceDeterminism); } conn->to.flush(); BuildResult status; status.status = static_cast<BuildResult::Status>(readInt(conn->from)); conn->from >> status.errorMsg; if (GET_PROTOCOL_MINOR(conn->remoteVersion) >= 3) { conn->from >> status.timesBuilt >> status.isNonDeterministic >> status.startTime >> status.stopTime; } return status; } void ensurePath(const Path& path) override { unsupported("ensurePath"); } void computeFSClosure(const PathSet& paths, PathSet& out, bool flipDirection = false, bool includeOutputs = false, bool includeDerivers = false) override { if (flipDirection || includeDerivers) { Store::computeFSClosure(paths, out, flipDirection, includeOutputs, includeDerivers); return; } auto conn(connections->get()); conn->to << cmdQueryClosure << static_cast<uint64_t>(includeOutputs) << paths; conn->to.flush(); auto res = readStorePaths<PathSet>(*this, conn->from); out.insert(res.begin(), res.end()); } PathSet queryValidPaths(const PathSet& paths, SubstituteFlag maybeSubstitute = NoSubstitute) override { auto conn(connections->get()); conn->to << cmdQueryValidPaths << 0u // lock << maybeSubstitute << paths; conn->to.flush(); return readStorePaths<PathSet>(*this, conn->from); } void connect() override { auto conn(connections->get()); } unsigned int getProtocol() override { auto conn(connections->get()); return conn->remoteVersion; } }; static RegisterStoreImplementation regStore( [](const std::string& uri, const Store::Params& params) -> std::shared_ptr<Store> { if (!absl::StartsWith(uri, kUriScheme)) { return nullptr; } return std::make_shared<LegacySSHStore>( std::string(uri, kUriScheme.size()), params); }); } // namespace nix