1
0
Fork 0
apt/apt-private/private-json-hooks.cc
Daniel Baumann 6810ba718b
Adding upstream version 3.0.2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-20 21:10:43 +02:00

564 lines
14 KiB
C++

/*
* private-json-hooks.cc - 2nd generation, JSON-RPC, hooks for APT
*
* Copyright (c) 2018 Canonical Ltd
*
* SPDX-License-Identifier: GPL-2.0+
*/
#include <config.h>
#include <apt-pkg/debsystem.h>
#include <apt-pkg/fileutl.h>
#include <apt-pkg/macros.h>
#include <apt-pkg/strutl.h>
#include <apt-private/private-json-hooks.h>
#include <apt-private/private-output.h>
#include <iomanip>
#include <ostream>
#include <sstream>
#include <stack>
#include <unordered_map>
#include <csignal>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
/**
* @brief Simple JSON writer
*
* This performs no error checking, so be careful.
*/
class APT_HIDDEN JsonWriter
{
std::ostream &os;
std::locale old_locale;
enum write_state
{
empty,
in_array_first_element,
in_array,
in_object_first_key,
in_object_key,
in_object_val
} state = empty;
std::stack<write_state> old_states;
void maybeComma()
{
switch (state)
{
case empty:
break;
case in_object_val:
state = in_object_key;
break;
case in_object_key:
state = in_object_val;
os << ',';
break;
case in_array:
os << ',';
break;
case in_array_first_element:
state = in_array;
break;
case in_object_first_key:
state = in_object_val;
break;
default:
abort();
}
}
void pushState(write_state state)
{
old_states.push(this->state);
this->state = state;
}
void popState()
{
this->state = old_states.top();
old_states.pop();
}
public:
explicit JsonWriter(std::ostream &os) : os(os), old_locale{os.imbue(std::locale::classic())} {}
~JsonWriter() { os.imbue(old_locale); }
JsonWriter &beginArray()
{
maybeComma();
pushState(in_array_first_element);
os << '[';
return *this;
}
JsonWriter &endArray()
{
popState();
os << ']';
return *this;
}
JsonWriter &beginObject()
{
maybeComma();
pushState(in_object_first_key);
os << '{';
return *this;
}
JsonWriter &endObject()
{
popState();
os << '}';
return *this;
}
std::ostream &encodeString(std::ostream &out, std::string const &str)
{
out << '"';
for (std::string::const_iterator c = str.begin(); c != str.end(); c++)
{
if (*c <= 0x1F || *c == '"' || *c == '\\')
ioprintf(out, "\\u%04X", *c);
else
out << *c;
}
out << '"';
return out;
}
JsonWriter &name(std::string const &name)
{
maybeComma();
encodeString(os, name) << ':';
return *this;
}
JsonWriter &value(std::string const &value)
{
maybeComma();
encodeString(os, value);
return *this;
}
JsonWriter &value(const char *value)
{
maybeComma();
if (value == nullptr)
os << "null";
else
encodeString(os, value);
return *this;
}
JsonWriter &value(int value)
{
maybeComma();
os << value;
return *this;
}
JsonWriter &value(long value)
{
maybeComma();
os << value;
return *this;
}
JsonWriter &value(long long value)
{
maybeComma();
os << value;
return *this;
}
JsonWriter &value(unsigned long long value)
{
maybeComma();
os << value;
return *this;
}
JsonWriter &value(unsigned long value)
{
maybeComma();
os << value;
return *this;
}
JsonWriter &value(unsigned int value)
{
maybeComma();
os << value;
return *this;
}
JsonWriter &value(bool value)
{
maybeComma();
os << (value ? "true" : "false");
return *this;
}
JsonWriter &value(double value)
{
maybeComma();
os << value;
return *this;
}
};
/**
* @brief Write a VerFileIterator to a JsonWriter
*/
static void verFiletoJson(JsonWriter &writer, CacheFile &, pkgCache::VerFileIterator const &vf)
{
auto pf = vf.File(); // Packages file
auto rf = pf.ReleaseFile(); // release file
writer.beginObject();
if (not rf.end()) {
if (rf->Archive != 0)
writer.name("archive").value(rf.Archive());
if (rf->Codename != 0)
writer.name("codename").value(rf.Codename());
if (rf->Version != 0)
writer.name("version").value(rf.Version());
if (rf->Origin != 0)
writer.name("origin").value(rf.Origin());
if (rf->Label != 0)
writer.name("label").value(rf.Label());
if (rf->Site != 0)
writer.name("site").value(rf.Site());
}
writer.endObject();
}
/**
* @brief Write a VerIterator to a JsonWriter
*/
static void verIterToJson(JsonWriter &writer, CacheFile &Cache, pkgCache::VerIterator const &Ver)
{
writer.beginObject();
writer.name("id").value(Ver->ID);
writer.name("version").value(Ver.VerStr());
writer.name("architecture").value(Ver.Arch());
writer.name("pin").value(Cache->GetPolicy().GetPriority(Ver));
writer.name("origins");
writer.beginArray();
for (auto vf = Ver.FileList(); !vf.end(); vf++)
if ((vf.File()->Flags & pkgCache::Flag::NotSource) == 0)
verFiletoJson(writer, Cache, vf);
writer.endArray();
writer.endObject();
}
/**
* @brief Copy of debSystem::DpkgChrootDirectory()
* @todo Remove
*/
static void DpkgChrootDirectory()
{
std::string const chrootDir = _config->FindDir("DPkg::Chroot-Directory");
if (chrootDir == "/")
return;
std::cerr << "Chrooting into " << chrootDir << std::endl;
if (chroot(chrootDir.c_str()) != 0)
_exit(100);
if (chdir("/") != 0)
_exit(100);
}
/**
* @brief Send a notification to the hook's stream
*/
static void NotifyHook(std::ostream &os, std::string const &method, const char **FileList, CacheFile &Cache, std::set<std::string> const &UnknownPackages, int hookVersion)
{
SortedPackageUniverse Universe(Cache);
JsonWriter jsonWriter{os};
jsonWriter.beginObject();
jsonWriter.name("jsonrpc").value("2.0");
jsonWriter.name("method").value(method);
/* Build params */
jsonWriter.name("params").beginObject();
if (FileList != nullptr)
{
jsonWriter.name("command").value(FileList[0]);
jsonWriter.name("search-terms").beginArray();
for (int i = 1; FileList[i] != NULL; i++)
jsonWriter.value(FileList[i]);
jsonWriter.endArray();
}
jsonWriter.name("unknown-packages").beginArray();
for (auto const &PkgName : UnknownPackages)
jsonWriter.value(PkgName);
jsonWriter.endArray();
jsonWriter.name("packages").beginArray();
for (auto const &Pkg : Universe)
{
switch (Cache[Pkg].Mode)
{
case pkgDepCache::ModeInstall:
case pkgDepCache::ModeDelete:
break;
default:
continue;
}
jsonWriter.beginObject();
jsonWriter.name("id").value(Pkg->ID);
jsonWriter.name("name").value(Pkg.Name());
jsonWriter.name("architecture").value(Pkg.Arch());
switch (Cache[Pkg].Mode)
{
case pkgDepCache::ModeInstall:
if (Pkg->CurrentVer != 0 && Cache[Pkg].Upgrade() && hookVersion >= 0x020)
jsonWriter.name("mode").value("upgrade");
else if (Pkg->CurrentVer != 0 && Cache[Pkg].Downgrade() && hookVersion >= 0x020)
jsonWriter.name("mode").value("downgrade");
else if (Pkg->CurrentVer != 0 && Cache[Pkg].ReInstall() && hookVersion >= 0x020)
jsonWriter.name("mode").value("reinstall");
else
jsonWriter.name("mode").value("install");
break;
case pkgDepCache::ModeDelete:
jsonWriter.name("mode").value(Cache[Pkg].Purge() ? "purge" : "deinstall");
break;
default:
continue;
}
jsonWriter.name("automatic").value(bool(Cache[Pkg].Flags & pkgCache::Flag::Auto));
jsonWriter.name("versions").beginObject();
if (Cache[Pkg].CandidateVer != nullptr)
verIterToJson(jsonWriter.name("candidate"), Cache, Cache[Pkg].CandidateVerIter(Cache));
if (Cache[Pkg].InstallVer != nullptr)
verIterToJson(jsonWriter.name("install"), Cache, Cache[Pkg].InstVerIter(Cache));
if (Pkg->CurrentVer != 0)
verIterToJson(jsonWriter.name("current"), Cache, Pkg.CurrentVer());
jsonWriter.endObject();
jsonWriter.endObject();
}
jsonWriter.endArray(); // packages
jsonWriter.endObject(); // params
jsonWriter.endObject(); // main
}
static JsonWriter &BuildOptionsArray(JsonWriter &writer)
{
const Configuration::Item *Top = _config->Tree(nullptr);
if (Top == 0)
return writer.beginArray().endArray();
writer.beginArray();
do
{
if (not Top->Value.empty())
writer.beginObject().name("name").value(Top->FullTag()).name("value").value(Top->Value).endObject();
if (Top->Child != 0)
{
Top = Top->Child;
continue;
}
while (Top != 0 && Top->Next == 0)
Top = Top->Parent;
if (Top != 0)
Top = Top->Next;
} while (Top != 0);
writer.endArray();
return writer;
}
/// @brief Build the hello handshake message for 0.1 protocol
static std::string BuildHelloMessage()
{
std::stringstream Hello;
auto writer = JsonWriter(Hello).beginObject().name("jsonrpc").value("2.0").name("method").value("org.debian.apt.hooks.hello").name("id").value(0).name("params").beginObject().name("versions").beginArray().value("0.1").value("0.2").endArray().name("options");
BuildOptionsArray(writer);
writer.endObject().endObject();
return Hello.str();
}
/// @brief Build the bye notification for 0.1 protocol
static std::string BuildByeMessage()
{
std::stringstream Bye;
JsonWriter(Bye).beginObject().name("jsonrpc").value("2.0").name("method").value("org.debian.apt.hooks.bye").name("params").beginObject().endObject().endObject();
return Bye.str();
}
/// @brief Run the Json hook processes in the given option.
bool RunJsonHook(std::string const &option, std::string const &method, const char **FileList, CacheFile &Cache, std::set<std::string> const &UnknownPackages)
{
std::unordered_map<int, std::string> notifications;
std::string HelloData = BuildHelloMessage();
std::string ByeData = BuildByeMessage();
int hookVersion;
bool result = true;
Configuration::Item const *Opts = _config->Tree(option.c_str());
if (Opts == 0 || Opts->Child == 0)
return true;
Opts = Opts->Child;
// Flush output before calling hooks
std::clog.flush();
std::cerr.flush();
std::cout.flush();
c2out.flush();
c1out.flush();
sighandler_t old_sigpipe = signal(SIGPIPE, SIG_IGN);
sighandler_t old_sigint = signal(SIGINT, SIG_IGN);
sighandler_t old_sigquit = signal(SIGQUIT, SIG_IGN);
unsigned int Count = 1;
for (; Opts != 0; Opts = Opts->Next, Count++)
{
if (Opts->Value.empty() == true)
continue;
if (_config->FindB("Debug::RunScripts", false) == true)
std::clog << "Running external script with list of all .deb file: '"
<< Opts->Value << "'" << std::endl;
// Create the pipes
std::set<int> KeepFDs;
MergeKeepFdsFromConfiguration(KeepFDs);
int Pipes[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, Pipes) != 0)
{
result = _error->Errno("pipe", "Failed to create IPC pipe to subprocess");
break;
}
int InfoFD = 3;
if (InfoFD != Pipes[0])
SetCloseExec(Pipes[0], true);
else
KeepFDs.insert(Pipes[0]);
SetCloseExec(Pipes[1], true);
// Purified Fork for running the script
pid_t Process = ExecFork(KeepFDs);
if (Process == 0)
{
// Setup the FDs
dup2(Pipes[0], InfoFD);
SetCloseExec(STDOUT_FILENO, false);
SetCloseExec(STDIN_FILENO, false);
SetCloseExec(STDERR_FILENO, false);
std::string hookfd;
strprintf(hookfd, "%d", InfoFD);
setenv("APT_HOOK_SOCKET", hookfd.c_str(), 1);
DpkgChrootDirectory();
const char *Args[4];
Args[0] = "/bin/sh";
Args[1] = "-c";
Args[2] = Opts->Value.c_str();
Args[3] = 0;
execv(Args[0], (char **)Args);
_exit(100);
}
close(Pipes[0]);
FILE *F = fdopen(Pipes[1], "w+");
if (F == 0)
{
result = _error->Errno("fdopen", "Failed to open new FD");
break;
}
fwrite(HelloData.data(), HelloData.size(), 1, F);
fwrite("\n\n", 2, 1, F);
fflush(F);
char *line = nullptr;
size_t linesize = 0;
ssize_t size = getline(&line, &linesize, F);
if (size < 0)
{
if (errno != ECONNRESET && errno != EPIPE)
_error->Error("Could not read response to hello message from hook %s: %s", Opts->Value.c_str(), strerror(errno));
goto out;
}
else if (strstr(line, "error") != nullptr)
{
_error->Error("Hook %s reported an error during hello: %s", Opts->Value.c_str(), line);
goto out;
}
if (strstr(line, "\"0.1\""))
{
hookVersion = 0x010;
}
else if (strstr(line, "\"0.2\""))
{
hookVersion = 0x020;
}
else
{
_error->Error("Unknown hook version in handshake from hook %s: %s", Opts->Value.c_str(), line);
goto out;
}
size = getline(&line, &linesize, F);
if (size < 0)
{
_error->Error("Could not read message separator line after handshake from %s: %s", Opts->Value.c_str(), feof(F) ? "end of file" : strerror(errno));
goto out;
}
else if (size == 0 || line[0] != '\n')
{
_error->Error("Expected empty line after handshake from %s, received %s", Opts->Value.c_str(), line);
goto out;
}
{
std::string &data = notifications[hookVersion];
if (data.empty())
{
std::stringstream ss;
NotifyHook(ss, method, FileList, Cache, UnknownPackages, hookVersion);
;
data = ss.str();
}
fwrite(data.data(), data.size(), 1, F);
fwrite("\n\n", 2, 1, F);
}
fwrite(ByeData.data(), ByeData.size(), 1, F);
fwrite("\n\n", 2, 1, F);
out:
fclose(F);
// Clean up the sub process
if (ExecWait(Process, Opts->Value.c_str()) == false)
{
result = _error->Error("Failure running hook %s", Opts->Value.c_str());
break;
}
}
signal(SIGINT, old_sigint);
signal(SIGPIPE, old_sigpipe);
signal(SIGQUIT, old_sigquit);
return result;
}