chromium/chrome/services/cups_proxy/ipp_validator.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/services/cups_proxy/ipp_validator.h"

#include <cups/cups.h>

#include <algorithm>
#include <map>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/containers/span.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "chrome/services/cups_proxy/ipp_attribute_validator.h"
#include "chrome/services/cups_proxy/public/cpp/cups_util.h"
#include "net/http/http_util.h"
#include "printing/backend/cups_ipp_helper.h"
#include "printing/backend/cups_weak_functions.h"

namespace cups_proxy {
namespace {

using ValueType = ipp_parser::mojom::IppAttributeValue::Tag;

// Initial version only supports english lcoales.
// TODO(crbug.com/945409): Extending to supporting arbitrary locales.
const char kLocaleEnglish[] = "en";

// Converting to vector<char> for libCUPS API:
// ippAddBooleans(..., int num_values, const char *values)
std::vector<char> ConvertBooleans(const std::vector<bool>& bools) {
  std::vector<char> ret;
  for (bool value : bools) {
    ret.push_back(value ? 1 : 0);
  }
  return ret;
}

// Converting to vector<const char*> for libCUPS API:
// ippAddStrings(..., int num_values, const char *const *values)
// Note: The values in the returned vector refer to |strings|; so |strings|
// must outlive them.
std::vector<const char*> ConvertStrings(
    const std::vector<std::string>& strings) {
  std::vector<const char*> ret;
  for (auto& value : strings) {
    ret.push_back(value.c_str());
  }
  return ret;
}

std::array<std::vector<int>, 2> ConvertResolutions(
    const std::vector<ipp_parser::mojom::ResolutionPtr>& resolutions) {
  std::array<std::vector<int>, 2> ret;
  for (auto& res : resolutions) {
    if (res->xres <= 0 || res->yres <= 0)
      continue;
    ret[0].push_back(res->xres);
    ret[1].push_back(res->yres);
  }
  return ret;
}

// Depending on |type|, returns the number of values associated with |attr|.
size_t GetAttributeValuesSize(const ipp_parser::mojom::IppAttributePtr& attr) {
  const auto& attr_value = attr->value;
  switch (attr->value->which()) {
    case ValueType::kDate:
      return 1;

    case ValueType::kBools:
      return attr_value->get_bools().size();
    case ValueType::kInts:
      return attr_value->get_ints().size();
    case ValueType::kStrings:
      return attr_value->get_strings().size();
    case ValueType::kOctets:
      return attr_value->get_octets().size();
    case ValueType::kResolutions:
      return attr_value->get_resolutions().size();
  }

  NOTREACHED_IN_MIGRATION();
  return 0;
}

// Returns true if |data| starts with the full |prefix|, false otherwise.
bool StartsWith(base::span<uint8_t const> data,
                base::span<uint8_t const> prefix) {
  if (data.size() < prefix.size())
    return false;
  return std::equal(data.begin(), data.begin() + prefix.size(), prefix.begin());
}

}  // namespace

// Verifies that |method|, |endpoint|, and |http_version| form a valid HTTP
// request-line. On success, returns a wrapper obj containing the verified
// request-line.
std::optional<HttpRequestLine> IppValidator::ValidateHttpRequestLine(
    std::string_view method,
    std::string_view endpoint,
    std::string_view http_version) {
  if (method != "POST") {
    return std::nullopt;
  }
  if (http_version != "HTTP/1.1") {
    return std::nullopt;
  }

  // Empty endpoint is allowed.
  if (endpoint == "/") {
    return HttpRequestLine{std::string(method), std::string(endpoint),
                           std::string(http_version)};
  }

  // Ensure endpoint is a known printer.
  auto printer_id = ParseEndpointForPrinterId(std::string(endpoint));
  if (!printer_id.has_value()) {
    return std::nullopt;
  }

  auto printer = delegate_->GetPrinter(*printer_id);
  if (!printer.has_value()) {
    return std::nullopt;
  }

  return HttpRequestLine{std::string(method), std::string(endpoint),
                         std::string(http_version)};
}

std::optional<std::vector<ipp_converter::HttpHeader>>
IppValidator::ValidateHttpHeaders(
    const size_t http_content_length,
    const base::flat_map<std::string, std::string>& headers) {
  // Sane, character-set checks.
  for (const auto& header : headers) {
    if (!net::HttpUtil::IsValidHeaderName(header.first) ||
        !net::HttpUtil::IsValidHeaderValue(header.second)) {
      return std::nullopt;
    }
  }

  std::vector<ipp_converter::HttpHeader> ret(headers.begin(), headers.end());

  // Update the ContentLength.
  std::erase_if(ret, [](const ipp_converter::HttpHeader& header) {
    return header.first == "Content-Length";
  });
  ret.push_back({"Content-Length", base::NumberToString(http_content_length)});

  return ret;
}

// Note: Since its possible to have valid IPP attributes that our
// ipp_attribute_validator.cc is unaware of, we drop unknown attributes, rather
// than fail the request.
ipp_t* IppValidator::ValidateIppMessage(
    ipp_parser::mojom::IppMessagePtr ipp_message) {
  printing::ScopedIppPtr ipp = printing::WrapIpp(ippNew());

  // Fill ids.
  if (!ippSetVersion(ipp.get(), ipp_message->major_version,
                     ipp_message->minor_version)) {
    return nullptr;
  }

  const ipp_op_t ipp_oper_id = static_cast<ipp_op_t>(ipp_message->operation_id);
  if (!ippSetOperation(ipp.get(), ipp_oper_id))
    return nullptr;

  if (!ippSetRequestId(ipp.get(), ipp_message->request_id)) {
    return nullptr;
  }

  // Fill attributes.
  for (size_t i = 0; i < ipp_message->attributes.size(); ++i) {
    ipp_parser::mojom::IppAttributePtr attribute =
        std::move(ipp_message->attributes[i]);

    size_t num_values = GetAttributeValuesSize(attribute);
    if (!num_values) {
      return nullptr;
    }

    auto ret = ValidateAttribute(ipp_oper_id, attribute->name,
                                 attribute->value->which(), num_values);
    if (ret == ValidateAttributeResult::kFatalError) {
      return nullptr;
    }
    if (ret == ValidateAttributeResult::kUnknownAttribute) {
      // We drop unknown attributes.
      DVLOG(1) << "CupsProxy validation: dropping unknown attribute "
               << attribute->name;
      continue;
    }

    switch (attribute->value->which()) {
      case ValueType::kBools: {
        std::vector<char> values =
            ConvertBooleans(attribute->value->get_bools());

        auto* attr = ippAddBooleans(
            ipp.get(), static_cast<ipp_tag_t>(attribute->group_tag),
            attribute->name.c_str(), values.size(), values.data());
        if (!attr) {
          return nullptr;
        }
        break;
      }
      case ValueType::kDate: {
        std::vector<uint8_t> date = attribute->value->get_date();

        // Per RFC2910, ipp_uchar_t is defined as an OCTET, so below
        // reinterpret_cast is safe.
        auto* attr =
            ippAddDate(ipp.get(), static_cast<ipp_tag_t>(attribute->group_tag),
                       attribute->name.c_str(),
                       reinterpret_cast<const ipp_uchar_t*>(date.data()));
        if (!attr) {
          return nullptr;
        }
        break;
      }
      case ValueType::kInts: {
        std::vector<int> values = attribute->value->get_ints();

        auto* attr = ippAddIntegers(
            ipp.get(), static_cast<ipp_tag_t>(attribute->group_tag),
            static_cast<ipp_tag_t>(attribute->value_tag),
            attribute->name.c_str(), values.size(), values.data());
        if (!attr) {
          return nullptr;
        }
        break;
      }
      case ValueType::kStrings: {
        // Note: cstrings_values references attribute->value's strings, i.e.
        // attribute->value MUST outlive cstrings_values.
        std::vector<const char*> cstrings_values =
            ConvertStrings(attribute->value->get_strings());

        auto* attr = ippAddStrings(
            ipp.get(), static_cast<ipp_tag_t>(attribute->group_tag),
            static_cast<ipp_tag_t>(attribute->value_tag),
            attribute->name.c_str(), cstrings_values.size(), kLocaleEnglish,
            cstrings_values.data());
        if (!attr) {
          return nullptr;
        }
        break;
      }
      case ValueType::kOctets: {
        size_t num = attribute->value->get_octets().size();
        if (num != 1) {
          LOG(ERROR) << "CUPS API only supports adding a single octet string "
                        "- cannot add "
                     << num << " octet strings.";
          return nullptr;
        }
        int size = attribute->value->get_octets()[0].size();
        if (size < 1) {
          LOG(ERROR) << "Invalid octet string size=" << size;
          return nullptr;
        }
        auto* attr = ippAddOctetString(
            ipp.get(), static_cast<ipp_tag_t>(attribute->group_tag),
            attribute->name.c_str(), attribute->value->get_octets()[0].data(),
            size);
        if (!attr) {
          return nullptr;
        }
        break;
      }
      case ValueType::kResolutions: {
        std::array<std::vector<int>, 2> res =
            ConvertResolutions(attribute->value->get_resolutions());
        auto* attr = ippAddResolutions(
            ipp.get(), static_cast<ipp_tag_t>(attribute->group_tag),
            attribute->name.c_str(), res[0].size(), IPP_RES_PER_INCH,
            res[0].data(), res[1].data());
        if (!attr) {
          return nullptr;
        }
        break;
      }
      default:
        NOTREACHED_IN_MIGRATION() << "Unknown IPP attribute type found.";
    }
  }

  // Validate Attributes.
  if (!ippValidateAttributes(ipp.get())) {
    return nullptr;
  }

  // Return built ipp object.
  return ipp.release();
}

// Requires |ipp_data| to be empty or look like a PDF or PostScript document.
bool IppValidator::ValidateIppData(const std::vector<uint8_t>& ipp_data) {
  // Empty IPP data portion.
  if (ipp_data.empty()) {
    return true;
  }

  // Check if |ipp_data| looks like a PDF or PostScript document.
  return StartsWith(ipp_data, pdf_magic_bytes) ||
         StartsWith(ipp_data, ps_magic_bytes);
}

IppValidator::IppValidator(CupsProxyServiceDelegate* const delegate)
    : delegate_(delegate) {}

IppValidator::~IppValidator() = default;

std::optional<IppRequest> IppValidator::ValidateIppRequest(
    ipp_parser::mojom::IppRequestPtr to_validate) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Build ipp message.
  // Note: Moving ipp here, to_validate->ipp no longer valid below.
  printing::ScopedIppPtr ipp =
      printing::WrapIpp(ValidateIppMessage(std::move(to_validate->ipp)));
  if (ipp == nullptr) {
    return std::nullopt;
  }

  // Validate ipp data.
  // TODO(crbug/894607): Validate ippData (pdf).
  if (!ValidateIppData(to_validate->data)) {
    return std::nullopt;
  }

  // Build request line.
  auto request_line = ValidateHttpRequestLine(
      to_validate->method, to_validate->endpoint, to_validate->http_version);
  if (!request_line.has_value()) {
    return std::nullopt;
  }

  // Build headers; must happen after ipp message/data since it requires the
  // ContentLength.
  const size_t http_content_length =
      ippLength(ipp.get()) + to_validate->data.size();
  auto headers = ValidateHttpHeaders(http_content_length, to_validate->headers);
  if (!headers.has_value()) {
    return std::nullopt;
  }

  // Marshall request
  IppRequest ret;
  ret.request_line = std::move(*request_line);
  ret.headers = std::move(*headers);
  ret.ipp = std::move(ipp);
  ret.ipp_data = std::move(to_validate->data);

  // Build parsed request buffer.
  auto request_buffer = ipp_converter::BuildIppRequest(
      ret.request_line.method, ret.request_line.endpoint,
      ret.request_line.http_version, ret.headers, ret.ipp.get(), ret.ipp_data);
  if (!request_buffer.has_value()) {
    return std::nullopt;
  }

  ret.buffer = std::move(*request_buffer);
  return ret;
}

}  // namespace cups_proxy