/* * private-json-hooks.cc - 2nd generation, JSON-RPC, hooks for APT * * Copyright (c) 2018 Canonical Ltd * * SPDX-License-Identifier: GPL-2.0+ */ #include #include #include #include #include #include #include #include #include /** * @brief Simple JSON writer * * This performs no error checking, or string escaping, 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 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(); } public: 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; } JsonWriter &name(std::string const &name) { maybeComma(); os << '"' << name << '"' << ':'; return *this; } JsonWriter &value(std::string const &value) { maybeComma(); os << '"' << value << '"'; return *this; } JsonWriter &value(const char *value) { maybeComma(); 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 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.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 const &UnknownPackages) { 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(); 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: 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 } /// @brief Build the hello handshake message for 0.1 protocol static std::string BuildHelloMessage() { std::stringstream Hello; 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").endArray().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 const &UnknownPackages) { std::stringstream ss; NotifyHook(ss, method, FileList, Cache, UnknownPackages); std::string TheData = ss.str(); std::string HelloData = BuildHelloMessage(); std::string ByeData = BuildByeMessage(); bool result = true; Configuration::Item const *Opts = _config->Tree(option.c_str()); if (Opts == 0 || Opts->Child == 0) return true; Opts = Opts->Child; 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 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); 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; } 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; } fwrite(TheData.data(), TheData.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; }