chromium/chrome/services/cups_proxy/ipp_validator_unittest.cc

// Copyright 2018 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 <set>
#include <string>
#include <utility>

#include "base/containers/contains.h"
#include "chrome/services/cups_proxy/fake_cups_proxy_service_delegate.h"
#include "chrome/services/cups_proxy/public/cpp/cups_util.h"
#include "chrome/services/cups_proxy/public/cpp/ipp_messages.h"
#include "printing/backend/cups_ipp_helper.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace cups_proxy {
namespace {

// Mojom using statements to remove chrome::mojom everywhere...
using ipp_parser::mojom::IppAttributePtr;
using ipp_parser::mojom::IppMessagePtr;
using ipp_parser::mojom::IppRequestPtr;

using Printer = chromeos::Printer;

std::string EncodeEndpointForPrinterId(std::string printer_id) {
  return "/printers/" + printer_id;
}

// Fake backend for CupsProxyServiceDelegate.
class FakeServiceDelegate : public FakeCupsProxyServiceDelegate {
 public:
  FakeServiceDelegate() = default;
  ~FakeServiceDelegate() override = default;

  void AddPrinter(const std::string& printer_id) {
    known_printers_.insert(printer_id);
  }

  std::optional<Printer> GetPrinter(const std::string& id) override {
    if (!base::Contains(known_printers_, id)) {
      return std::nullopt;
    }

    return Printer(id);
  }

 private:
  std::set<std::string> known_printers_;
};

class IppValidatorTest : public testing::Test {
 public:
  IppValidatorTest() {
    delegate_ = std::make_unique<FakeServiceDelegate>();
    ipp_validator_ = std::make_unique<IppValidator>(delegate_.get());
  }

  ~IppValidatorTest() override = default;

  std::optional<IppRequest> RunValidateIppRequest(
      const IppRequestPtr& request) {
    return ipp_validator_->ValidateIppRequest(request.Clone());
  }

 protected:
  // Fake delegate driving the IppValidator.
  std::unique_ptr<FakeServiceDelegate> delegate_;

  // The manager being tested. This must be declared, and thus initialized,
  // after the fakes.
  std::unique_ptr<IppValidator> ipp_validator_;
};

IppAttributePtr BuildAttributePtr(std::string name,
                                  ipp_tag_t group_tag,
                                  ipp_tag_t value_tag) {
  auto ret = ipp_parser::mojom::IppAttribute::New();
  ret->name = name;
  ret->group_tag = group_tag;
  ret->value_tag = value_tag;
  return ret;
}

// Returns a mojom representation of a standard IPP request.
IppRequestPtr GetBasicIppRequest() {
  IppRequestPtr ret = ipp_parser::mojom::IppRequest::New();

  // Request line.
  ret->method = "POST";
  ret->endpoint = "/";
  ret->http_version = "HTTP/1.1";

  // Map of Http headers.
  ret->headers = base::flat_map<ipp_converter::HttpHeader::first_type,
                                ipp_converter::HttpHeader::second_type>{
      {"Content-Length", "72"},
      {"Content-Type", "application/ipp"},
      {"Date", "Thu, 04 Oct 2018 20:25:59 GMT"},
      {"Host", "localhost:0"},
      {"User-Agent",
       "CUPS/2.3b1 (Linux 4.4.159-15303-g65f4b5a7b3d3; i686) IPP/2.0"}};

  // IppMessage.
  IppMessagePtr ipp_message = ipp_parser::mojom::IppMessage::New();
  ipp_message->major_version = 2;
  ipp_message->minor_version = 0;
  ipp_message->operation_id = IPP_OP_CUPS_GET_DEFAULT;
  ipp_message->request_id = 1;

  // Setup each attribute.
  IppAttributePtr attr_charset = BuildAttributePtr(
      "attributes-charset", IPP_TAG_OPERATION, IPP_TAG_CHARSET);
  attr_charset->value =
      ipp_parser::mojom::IppAttributeValue::NewStrings({"utf-8"});

  IppAttributePtr attr_natlang = BuildAttributePtr(
      "attributes-natural-language", IPP_TAG_OPERATION, IPP_TAG_LANGUAGE);
  attr_natlang->value =
      ipp_parser::mojom::IppAttributeValue::NewStrings({"en"});

  ipp_message->attributes.push_back(std::move(attr_charset));
  ipp_message->attributes.push_back(std::move(attr_natlang));
  ret->ipp = std::move(ipp_message);
  return ret;
}

// Ensure basic IPP request passes validation.
TEST_F(IppValidatorTest, SimpleSanityTest) {
  auto request = GetBasicIppRequest();
  auto validated_request = RunValidateIppRequest(request);
  EXPECT_TRUE(RunValidateIppRequest(request));
}

// Ensure printer endpoints are printers known to Chrome.
TEST_F(IppValidatorTest, EndpointIsKnownPrinter) {
  auto request = GetBasicIppRequest();
  std::string printer_id = "abc";

  request->endpoint = EncodeEndpointForPrinterId(printer_id);
  EXPECT_FALSE(RunValidateIppRequest(request));

  // Should succeed now that |delegate_| knows about the printer.
  delegate_->AddPrinter(printer_id);
  EXPECT_TRUE(RunValidateIppRequest(request));
}

TEST_F(IppValidatorTest, IncorrectHttpMethod) {
  auto request = GetBasicIppRequest();
  request->method = "GET";
  EXPECT_FALSE(RunValidateIppRequest(request));
}

TEST_F(IppValidatorTest, IncorrectHttpVersion) {
  auto request = GetBasicIppRequest();
  request->http_version = "HTTP/2.0";
  EXPECT_FALSE(RunValidateIppRequest(request));
}

TEST_F(IppValidatorTest, MissingHeaderName) {
  auto request = GetBasicIppRequest();

  // Adds new header with an empty name.
  request->headers[""] = "arbitrary valid header value";
  EXPECT_FALSE(RunValidateIppRequest(request));
}

TEST_F(IppValidatorTest, MissingHeaderValue) {
  auto request = GetBasicIppRequest();

  // Adds new header with an empty value.
  request->headers["arbitrary_valid_header_name"] = "";
  EXPECT_TRUE(RunValidateIppRequest(request));
}

// Test that we drop unknown attributes and succeed the request.
TEST_F(IppValidatorTest, UnknownAttribute) {
  auto request = GetBasicIppRequest();

  // Add fake attribute.
  std::string fake_attr_name = "fake-attribute-name";
  IppAttributePtr fake_attr =
      BuildAttributePtr(fake_attr_name, IPP_TAG_OPERATION, IPP_TAG_TEXT);
  fake_attr->value = ipp_parser::mojom::IppAttributeValue::NewStrings(
      {"fake_attribute_value"});
  request->ipp->attributes.push_back(std::move(fake_attr));

  auto result = RunValidateIppRequest(request);
  ASSERT_TRUE(result);

  // Ensure resulting validated IPP request doesn't contain fake_attr_name.
  ipp_t* ipp = result->ipp.get();
  EXPECT_FALSE(ippFindAttribute(ipp, fake_attr_name.c_str(), IPP_TAG_TEXT));
}

TEST_F(IppValidatorTest, IppDataVerification) {
  auto request = GetBasicIppRequest();

  // Should fail since the data too short to be a valid document.
  request->data = {0x0};
  EXPECT_FALSE(RunValidateIppRequest(request));

  // Valid PDF.
  request->data = {pdf_magic_bytes.begin(), pdf_magic_bytes.end()};
  EXPECT_TRUE(RunValidateIppRequest(request));

  // Valid PS.
  request->data = {ps_magic_bytes.begin(), ps_magic_bytes.end()};
  EXPECT_TRUE(RunValidateIppRequest(request));
}

// TODO(crbug.com/945409): Test IPP validation.

}  // namespace
}  // namespace cups_proxy