564 lines
14 KiB
C++
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;
|
|
}
|