| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // 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 <utility> |
| #include <vector> |
| |
| #include "base/containers/cxx20_erase.h" |
| #include "base/containers/span.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_piece.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" |
| |
| namespace cups_proxy { |
| namespace { |
| |
| using ValueType = ipp_parser::mojom::ValueType; |
| |
| // 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->type) { |
| case ValueType::DATE: |
| return 1; |
| |
| case ValueType::BOOLEAN: |
| DCHECK(attr_value->is_bools()); |
| return attr_value->get_bools().size(); |
| case ValueType::INTEGER: |
| DCHECK(attr_value->is_ints()); |
| return attr_value->get_ints().size(); |
| case ValueType::STRING: |
| DCHECK(attr_value->is_strings()); |
| return attr_value->get_strings().size(); |
| case ValueType::OCTET: |
| DCHECK(attr_value->is_octets()); |
| return attr_value->get_octets().size(); |
| case ValueType::RESOLUTION: |
| DCHECK(attr_value->is_resolutions()); |
| return attr_value->get_resolutions().size(); |
| |
| default: |
| break; |
| } |
| |
| DVLOG(1) << "Unknown CupsIppParser ValueType " << attr->type; |
| 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. |
| absl::optional<HttpRequestLine> IppValidator::ValidateHttpRequestLine( |
| base::StringPiece method, |
| base::StringPiece endpoint, |
| base::StringPiece http_version) { |
| if (method != "POST") { |
| return absl::nullopt; |
| } |
| if (http_version != "HTTP/1.1") { |
| return absl::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 absl::nullopt; |
| } |
| |
| auto printer = delegate_->GetPrinter(*printer_id); |
| if (!printer.has_value()) { |
| return absl::nullopt; |
| } |
| |
| return HttpRequestLine{std::string(method), std::string(endpoint), |
| std::string(http_version)}; |
| } |
| |
| absl::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 absl::nullopt; |
| } |
| } |
| |
| std::vector<ipp_converter::HttpHeader> ret(headers.begin(), headers.end()); |
| |
| // Update the ContentLength. |
| base::EraseIf(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->type, |
| 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->type) { |
| case ValueType::BOOLEAN: { |
| DCHECK(attribute->value->is_bools()); |
| 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::DATE: { |
| DCHECK(attribute->value->is_date()); |
| 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::INTEGER: { |
| DCHECK(attribute->value->is_ints()); |
| 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::STRING: { |
| DCHECK(attribute->value->is_strings()); |
| |
| // 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::OCTET: { |
| DCHECK(attribute->value->is_octets()); |
| 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::RESOLUTION: { |
| DCHECK(attribute->value->is_resolutions()); |
| 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() << "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; |
| |
| absl::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 absl::nullopt; |
| } |
| |
| // Validate ipp data. |
| // TODO(crbug/894607): Validate ippData (pdf). |
| if (!ValidateIppData(to_validate->data)) { |
| return absl::nullopt; |
| } |
| |
| // Build request line. |
| auto request_line = ValidateHttpRequestLine( |
| to_validate->method, to_validate->endpoint, to_validate->http_version); |
| if (!request_line.has_value()) { |
| return absl::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 absl::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 absl::nullopt; |
| } |
| |
| ret.buffer = std::move(*request_buffer); |
| return ret; |
| } |
| |
| } // namespace cups_proxy |