blob: 329ccbc8de4c898a7413ed8431095394aba92fc7 [file] [log] [blame]
// Copyright 2016 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Tool to manipulate CUPS.
#include "debugd/src/cups_tool.h"
#include <errno.h>
#include <ftw.h>
#include <sys/types.h>
#include <unistd.h>
#include <algorithm>
#include <string>
#include <vector>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
#include <base/logging.h>
#include <base/strings/string_piece.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <brillo/flag_helper.h>
#include <brillo/userdb_utils.h>
#include "debugd/src/constants.h"
#include "debugd/src/process_with_output.h"
namespace debugd {
namespace {
const char kJobName[] = "cupsd";
const char kLpadminCommand[] = "/usr/sbin/lpadmin";
const char kLpadminSeccompPolicy[] = "/usr/share/policy/lpadmin-seccomp.policy";
const char kTestPPDCommand[] = "/usr/bin/cupstestppd";
const char kTestPPDSeccompPolicy[] =
"/usr/share/policy/cupstestppd-seccomp.policy";
const char kLpadminUser[] = "lpadmin";
const char kLpadminGroup[] = "lpadmin";
const char kLpGroup[] = "lp";
const char kDownloadsFolder[] = "Downloads";
// This pathname has to match the path generated in
// //chrome/browser/ui/webui/settings/chromeos/cups_printers_handler.cc
const char kPPDCacheFolder[] = "PPDCache";
bool IsHexDigits(base::StringPiece digits) {
for (char digit : digits) {
if (!base::IsHexDigit(digit))
return false;
}
return true;
}
bool MatchesUserString(base::StringPiece input) {
std::vector<base::StringPiece> pieces = base::SplitStringPiece(
input, "-", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
return pieces.size() == 2 && pieces[0] == "u" && IsHexDigits(pieces[1]);
}
bool IsInPPDCache(const std::vector<std::string>& path_components) {
// {/, home, chronos, u-HEX, PPDCache, <FileName>}
return path_components.size() == 6 && path_components[0] == "/" &&
path_components[1] == "home" && path_components[2] == "chronos" &&
MatchesUserString(path_components[3]) &&
path_components[4] == kPPDCacheFolder;
}
// Returns true if the path is a child of the primary user's Downloads folder.
bool MatchesPrimaryUserDownloads(
const std::vector<std::string>& path_components) {
std::vector<std::string> primary_user = {"/", "home", "chronos", "user",
kDownloadsFolder};
return path_components.size() >= primary_user.size() &&
std::equal(primary_user.begin(), primary_user.end(),
path_components.begin());
}
// Matches the path reported by Chrome for the Downloads folder.
bool MatchesChromeDownloadsPath(
const std::vector<std::string>& path_components) {
return path_components.size() > 5 && path_components[0] == "/" &&
path_components[1] == "home" && path_components[2] == "chronos" &&
MatchesUserString(path_components[3]) &&
path_components[4] == kDownloadsFolder;
}
// Returns true if the path is a child of the user's Downloads folder in a
// multi-user environment.
bool MatchesMultiUserDownloads(
const std::vector<std::string>& path_components) {
// Path consists of /home/user/<hex string>/Downloads.
return path_components.size() > 5 && path_components[0] == "/" &&
path_components[1] == "home" && path_components[2] == "user" &&
IsHexDigits(path_components[3]) &&
path_components[4] == kDownloadsFolder;
}
bool IsDownload(const std::vector<std::string>& path_components) {
return MatchesChromeDownloadsPath(path_components) ||
MatchesPrimaryUserDownloads(path_components) ||
MatchesMultiUserDownloads(path_components);
}
bool SetPerms(const base::FilePath& fpath, mode_t mode, uid_t uid, gid_t gid) {
const std::string& path = fpath.value();
if (!base::SetPosixFilePermissions(fpath, mode)) {
PLOG(ERROR) << "Could not modify file permissions";
return false;
}
if (chown(path.c_str(), uid, gid) != 0) {
PLOG(ERROR) << "Could not take ownership of file";
return false;
}
return true;
}
int StopCups(DBus::Error* error) {
int result;
result = ProcessWithOutput::RunProcess("initctl", {"stop", kJobName},
true, // requires root
nullptr, // stdin
nullptr, // stdout
nullptr, // stderr
error);
// Don't log errors, since job may not be started.
return result;
}
int RemovePath(const char* fpath, const struct stat* sb, int typeflag,
struct FTW* ftwbuf) {
int ret = remove(fpath);
if (ret)
PLOG(WARNING) << "could not remove file/path: " << fpath;
return ret;
}
int ClearDirectory(const char* path) {
if (access(path, F_OK) && errno == ENOENT)
// Directory doesn't exist. Skip quietly.
return 0;
return nftw(path, RemovePath, FTW_D, FTW_DEPTH | FTW_PHYS);
}
int ClearCupsState() {
int ret = 0;
ret |= ClearDirectory("/var/cache/cups");
ret |= ClearDirectory("/var/spool/cups");
return ret;
}
// Returns the exit code for the executed process. Supports RunAsUser except
// that access to the root mount namespace is enabled if |root_mount_ns| is
// true.
int RunAsUserWithMount(const std::string& user, const std::string& group,
const std::string& command,
const std::string& seccomp_policy,
const ProcessWithOutput::ArgList& arg_list,
bool root_mount_ns, DBus::Error* error) {
ProcessWithOutput process;
process.set_separate_stderr(true);
process.SandboxAs(user, group);
if (!seccomp_policy.empty())
process.SetSeccompFilterPolicyFile(seccomp_policy);
if (root_mount_ns)
process.AllowAccessRootMountNamespace();
if (!process.Init())
return ProcessWithOutput::kRunError;
process.AddArg(command);
for (const std::string& arg : arg_list) {
process.AddArg(arg);
}
int result = process.Run();
if (result != 0) {
std::string error_msg;
process.GetError(&error_msg);
PLOG(ERROR) << error_msg;
}
return result;
}
// Runs |command| as |user|:|group| with args |arg_list|. If |seccomp_policy|
// is non-empty, apply it to restrict syscalls. Returns the exit code
// for the executed process.
int RunAsUser(const std::string& user, const std::string& group,
const std::string& command,
const std::string& seccomp_policy,
const ProcessWithOutput::ArgList& arg_list, DBus::Error* error) {
return RunAsUserWithMount(user, group, command, seccomp_policy, arg_list,
false, error);
}
// Runs cupstestppd on |file_name| returns true if it is a valid ppd file.
bool TestPPD(const std::string& path, DBus::Error* error) {
// TODO(skau): Run cupstestppd in seccomp crbug.com/633383.
return RunAsUser(kLpadminUser, kLpadminGroup, kTestPPDCommand,
kTestPPDSeccompPolicy, {path}, error) == 0;
}
// Runs lpadmin with the provided |arg_list|.
int Lpadmin(const ProcessWithOutput::ArgList& arg_list, DBus::Error* error) {
// TODO(skau): Run lpadmin in seccomp crbug.com/637160.
// Run in lp group so we can read and write /run/cups/cups.sock.
return RunAsUser(kLpadminUser, kLpGroup, kLpadminCommand,
kLpadminSeccompPolicy, arg_list, error);
}
// Runs /bin/cp in a new minijail. This is done because /home might not be
// mounted in our current minijail. Returns true if successful.
bool RunCopy(const std::string& src, const std::string& dst,
DBus::Error* error) {
std::string err_msg;
int result =
RunAsUserWithMount("root", "root", "/bin/cp", "", // No seccomp policy.
{"-u", src, dst}, // update, source, destination
true, // access to root mount namespace required
error);
if (result != 0) {
LOG(ERROR) << "Could not copy file src(" << src << "), "
<< "dst(" << dst << ")";
return false;
}
return true;
}
// Copies the file at |src_path| to |temp_dir_path| and fixes the permissions.
// Returns true if the file was copied successfully.
bool CopyPPDFile(const base::FilePath& src_path,
const base::FilePath& temp_dir_path, DBus::Error* error) {
const base::FilePath destination = temp_dir_path.Append(src_path.BaseName());
if (!RunCopy(src_path.value(), destination.value(), error))
return false;
// Transfer file ownership.
uid_t uid;
gid_t gid = 0;
// Set ownership to lpadmin:lpadmin so cupstestppd and lpadmin can use the
// file appropriately. root:lpadmin doesn't work because lpadmin is run as
// lpadmin:lp.
if (!brillo::userdb::GetUserInfo(kLpadminUser, &uid, nullptr) ||
!brillo::userdb::GetGroupInfo(kLpadminGroup, &gid)) {
PLOG(ERROR) << "Could not retrieve owner information";
return false;
}
if (!SetPerms(temp_dir_path, 0750, uid, gid)) {
PLOG(FATAL) << "Could not change directory permissions";
return false;
}
if (!SetPerms(destination, 0640, uid, gid)) {
PLOG(FATAL) << "Could not change file permissions";
return false;
}
return true;
}
// Returns true if the path is one from which we allow copying. i.e. user's
// Downloads directories or the PPD cache.
bool ValidatePPDPath(const base::FilePath& path) {
if (!path.IsAbsolute()) {
LOG(ERROR) << "Path must be absolute " << path.value();
return false;
}
if (path.ReferencesParent()) {
LOG(ERROR) << "Parent references are not allowed " << path.value();
return false;
}
std::vector<std::string> path_components;
path.GetComponents(&path_components);
if (!IsInPPDCache(path_components) && !IsDownload(path_components)) {
LOG(ERROR) << "Illegal path " << path.value();
return false;
}
return true;
}
bool ConfigurePrinter(const std::string& name, const std::string& uri,
const std::string& ppd_file, DBus::Error* error) {
if (ppd_file.empty()) {
LOG(ERROR) << "A ppd path must be provided";
return false;
}
base::FilePath src_ppd(ppd_file);
if (!ValidatePPDPath(src_ppd))
return false;
// Scoped temp dir will cleanup the directory when we're done.
base::ScopedTempDir temp_dir;
if (!temp_dir.CreateUniqueTempDir()) {
PLOG(ERROR) << "Could not create temp directory";
return false;
}
const base::FilePath& temp_dir_path = temp_dir.path();
if (!CopyPPDFile(src_ppd, temp_dir_path, error))
return false;
base::FilePath copied_ppd = temp_dir_path.Append(src_ppd.BaseName());
int result = EXIT_FAILURE;
if (TestPPD(copied_ppd.value(), error))
result =
Lpadmin({"-v", uri, "-p", name, "-P", copied_ppd.value(), "-E"}, error);
return result == EXIT_SUCCESS;
}
bool AutoConfigurePrinter(const std::string& name, const std::string& uri,
DBus::Error* error) {
// Autoconfiguration requires ipp or ipps.
if (!base::StartsWith(uri, "ipp://", base::CompareCase::INSENSITIVE_ASCII) &&
!base::StartsWith(uri, "ipps://", base::CompareCase::INSENSITIVE_ASCII)) {
LOG(WARNING) << "IPP or IPPS required for IPP Everywhere: " << uri;
return false;
}
return Lpadmin({"-v", uri, "-p", name, "-m", "everywhere", "-E"}, error) == 0;
}
} // namespace
// Invokes lpadmin with arguments to configure a new printer. For IPP
// printers, it can attempt autoconf using '-m everywhere'.
bool CupsTool::AddPrinter(const std::string& name, const std::string& uri,
const std::string& ppd_file, bool ipp_everywhere,
DBus::Error* error) {
if (ipp_everywhere)
return AutoConfigurePrinter(name, uri, error);
return ConfigurePrinter(name, uri, ppd_file, error);
}
// Invokes lpadmin with -x to delete a printer.
bool CupsTool::RemovePrinter(const std::string& name, DBus::Error* error) {
return Lpadmin({"-x", name}, error) == 0;
}
// Stop cupsd and clear its state. Needs to launch helper with root
// permissions, so we can restart Upstart jobs, and clear privileged
// directories.
void CupsTool::ResetState(DBus::Error* error) {
// Ignore errors; CUPS may not even be started.
StopCups(error);
// There's technically a race -- cups can be restarted in the meantime -- but
// (a) we don't expect applications to be racing with this (e.g., this method
// may be used on logout or login) and
// (b) clearing CUPS's state while it's running should at most confuse CUPS
// (e.g., missing printers or jobs).
ClearCupsState();
}
} // namespace debugd