chromium/chrome/services/cups_proxy/proxy_manager.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/proxy_manager.h"

#include <cups/ipp.h>

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

#include "base/containers/ring_buffer.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/time/time.h"
#include "chrome/services/cups_proxy/cups_proxy_service_delegate.h"
#include "chrome/services/cups_proxy/ipp_validator.h"
#include "chrome/services/cups_proxy/printer_installer.h"
#include "chrome/services/cups_proxy/public/cpp/cups_util.h"
#include "chrome/services/cups_proxy/public/cpp/ipp_messages.h"
#include "chrome/services/cups_proxy/public/cpp/type_conversions.h"
#include "chrome/services/cups_proxy/socket_manager.h"
#include "chrome/services/ipp_parser/public/cpp/browser/ipp_parser_launcher.h"
#include "chrome/services/ipp_parser/public/cpp/ipp_converter.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "printing/backend/cups_ipp_helper.h"

namespace cups_proxy {
namespace {

struct InFlightRequest {
  IppRequest request;
  ProxyManager::ProxyRequestCallback cb;
};

class ProxyManagerImpl : public ProxyManager {
 public:
  ProxyManagerImpl(mojo::PendingReceiver<mojom::CupsProxier> request,
                   std::unique_ptr<CupsProxyServiceDelegate> delegate,
                   std::unique_ptr<IppValidator> ipp_validator,
                   std::unique_ptr<PrinterInstaller> printer_installer,
                   std::unique_ptr<SocketManager> socket_manager)
      : delegate_(std::move(delegate)),
        ipp_validator_(std::move(ipp_validator)),
        printer_installer_(std::move(printer_installer)),
        socket_manager_(std::move(socket_manager)),
        receiver_(this, std::move(request)) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    receiver_.set_disconnect_handler(
        base::BindOnce([] { LOG(ERROR) << "CupsProxy mojo connection lost"; }));
  }

  ProxyManagerImpl(const ProxyManagerImpl&) = delete;
  ProxyManagerImpl& operator=(const ProxyManagerImpl&) = delete;

  ~ProxyManagerImpl() override;

  void ProxyRequest(const std::string& method,
                    const std::string& url,
                    const std::string& version,
                    const std::vector<ipp_converter::HttpHeader>& headers,
                    const std::vector<uint8_t>& body,
                    ProxyRequestCallback cb) override;

 private:
  // These methods interface with |ipp_parser_| to parse the syntax of the
  // inflight IPP request.
  void ParseIpp();
  void OnParseIpp(ipp_parser::mojom::IppRequestPtr parsed_request);

  // For CUPS-Get-Printers requests, we spoof the response.
  void SpoofGetPrinters();

  // The callback for interfacing with |printer_installer_|; used to
  // pre-install any printers referenced by the inflight request into CUPS.
  void OnInstallPrinter(InstallPrinterResult res);

  // These methods interface with |socket_manager_| to actually proxy the
  // inflight request to CUPS and propagate back its IPP response.
  void ProxyToCups();
  void OnProxyToCups(std::unique_ptr<std::vector<uint8_t>> response);

  // Proxy the IPP response back to the caller.
  void ProxyResponseToCaller(const std::vector<uint8_t>& response);

  // Fail in-flight request.
  void Fail(const std::string& error_message, int http_status_code);

  // Delegate providing necessary Profile dependencies.
  std::unique_ptr<CupsProxyServiceDelegate> delegate_;

  // Current in-flight request.
  std::unique_ptr<InFlightRequest> in_flight_;

  // Timestamp ring buffer.
  base::RingBuffer<base::TimeTicks, kRateLimit> timestamp_;

  // CupsIppParser Service handle.
  mojo::Remote<ipp_parser::mojom::IppParser> ipp_parser_;

  // Runs in current sequence.
  std::unique_ptr<IppValidator> ipp_validator_;
  std::unique_ptr<PrinterInstaller> printer_installer_;
  std::unique_ptr<SocketManager> socket_manager_;

  SEQUENCE_CHECKER(sequence_checker_);

  mojo::Receiver<mojom::CupsProxier> receiver_;
  base::WeakPtrFactory<ProxyManagerImpl> weak_factory_{this};
};

std::optional<std::vector<uint8_t>> RebuildIppRequest(
    const std::string& method,
    const std::string& url,
    const std::string& version,
    const std::vector<ipp_converter::HttpHeader>& headers,
    const std::vector<uint8_t>& body) {
  auto request_line_buffer =
      ipp_converter::BuildRequestLine(method, url, version);
  if (!request_line_buffer.has_value()) {
    return std::nullopt;
  }

  auto headers_buffer = ipp_converter::BuildHeaders(headers);
  if (!headers_buffer.has_value()) {
    return std::nullopt;
  }

  std::vector<uint8_t> ret;
  ret.insert(ret.end(), request_line_buffer->begin(),
             request_line_buffer->end());
  ret.insert(ret.end(), headers_buffer->begin(), headers_buffer->end());
  ret.insert(ret.end(), body.begin(), body.end());
  return ret;
}

ProxyManagerImpl::~ProxyManagerImpl() {
  if (in_flight_) {
    Fail("CupsProxy is shutting down", HTTP_STATUS_SERVICE_UNAVAILABLE);
  }
}

void ProxyManagerImpl::ProxyRequest(
    const std::string& method,
    const std::string& url,
    const std::string& version,
    const std::vector<ipp_converter::HttpHeader>& headers,
    const std::vector<uint8_t>& body,
    ProxyRequestCallback cb) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Limit the rate of requests to kRateLimit per second.
  // The way the algorithm works is if the number of times this method was
  // called between a second ago and now, including the current call, exceeds
  // kRateLimit, the request is blocked and the HTTP 429 Too Many Requests
  // response status code is returned.
  base::TimeTicks time = base::TimeTicks::Now();
  bool block_request = timestamp_.CurrentIndex() >= timestamp_.BufferSize() &&
                       time - timestamp_.ReadBuffer(0) < base::Seconds(1);
  timestamp_.SaveToBuffer(time);
  if (block_request) {
    LOG(WARNING) << "CupsPrintService: Rate limit (" << kRateLimit
                 << ") exceeded";
    std::move(cb).Run({}, {}, 429);  // HTTP_STATUS_TOO_MANY_REQUESTS
    return;
  }

  if (!delegate_->IsPrinterAccessAllowed()) {
    DVLOG(1) << "Printer access not allowed";
    std::move(cb).Run(/*headers=*/{}, /*ipp_message=*/{},
                      HTTP_STATUS_FORBIDDEN);
    return;
  }

  // If we already have an in-flight request, we fail this incoming one
  // directly.
  if (in_flight_) {
    DVLOG(1) << "CupsPrintService Error: Already have an in-flight request";
    std::move(cb).Run({}, {}, HTTP_STATUS_SERVICE_UNAVAILABLE);
    return;
  }

  in_flight_ = std::make_unique<InFlightRequest>();
  in_flight_->cb = std::move(cb);

  // TODO(crbug.com/945409): Rename in both proxy.mojom's: url -> endpoint.
  auto request_buffer = RebuildIppRequest(method, url, version, headers, body);
  if (!request_buffer) {
    return Fail("Failed to rebuild incoming IPP request",
                HTTP_STATUS_SERVER_ERROR);
  }

  // Save request.
  in_flight_->request.buffer = *request_buffer;
  ParseIpp();
  return;
}

void ProxyManagerImpl::ParseIpp() {
  // Launch CupsIppParser service, if needed.
  if (!ipp_parser_) {
    ipp_parser_.Bind(ipp_parser::LaunchIppParser());
    ipp_parser_.reset_on_disconnect();
  }

  // Run out-of-process IPP parsing.
  ipp_parser_->ParseIpp(in_flight_->request.buffer,
                        base::BindOnce(&ProxyManagerImpl::OnParseIpp,
                                       weak_factory_.GetWeakPtr()));
}

void ProxyManagerImpl::OnParseIpp(
    ipp_parser::mojom::IppRequestPtr parsed_request) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(in_flight_);

  if (!parsed_request) {
    return Fail("Failed to parse IPP request", HTTP_STATUS_BAD_REQUEST);
  }

  // Validate parsed request.
  auto valid_request =
      ipp_validator_->ValidateIppRequest(std::move(parsed_request));
  if (!valid_request) {
    return Fail("Failed to validate IPP request", HTTP_STATUS_BAD_REQUEST);
  }

  // Save newly validated request.
  in_flight_->request = std::move(*valid_request);

  auto opcode = ippGetOperation(in_flight_->request.ipp.get());
  if (opcode == IPP_OP_CUPS_NONE) {
    return Fail("Failed to parse IPP operation ID", HTTP_STATUS_BAD_REQUEST);
  }

  // Since Chrome is the source-of-truth for printers on ChromeOS, for
  // CUPS-Get-Printers requests, we spoof the response rather than proxying to
  // CUPS.
  if (opcode == IPP_OP_CUPS_GET_PRINTERS) {
    SpoofGetPrinters();
    return;
  }

  // If this request references a printer, pre-install it into CUPS.
  auto printer_uuid = GetPrinterId(in_flight_->request.ipp.get());
  if (printer_uuid.has_value()) {
    printer_installer_->InstallPrinter(
        *printer_uuid, base::BindOnce(&ProxyManagerImpl::OnInstallPrinter,
                                      weak_factory_.GetWeakPtr()));
    return;
  }

  // Nothing left to do, skip straight to proxying.
  ProxyToCups();
  return;
}

void ProxyManagerImpl::SpoofGetPrinters() {
  std::vector<chromeos::Printer> printers = FilterPrintersForPluginVm(
      delegate_->GetPrinters(chromeos::PrinterClass::kSaved),
      delegate_->GetPrinters(chromeos::PrinterClass::kEnterprise),
      delegate_->GetRecentlyUsedPrinters());
  std::optional<IppResponse> response =
      BuildGetDestsResponse(in_flight_->request, printers);
  if (!response.has_value()) {
    return Fail("Failed to spoof CUPS-Get-Printers response",
                HTTP_STATUS_SERVER_ERROR);
  }

  ProxyResponseToCaller(response->buffer);
  return;
}

void ProxyManagerImpl::OnInstallPrinter(InstallPrinterResult res) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(in_flight_);

  if (res != InstallPrinterResult::kSuccess) {
    return Fail("Failed to pre-install printer", HTTP_STATUS_SERVER_ERROR);
  }

  ProxyToCups();
  return;
}

void ProxyManagerImpl::ProxyToCups() {
  // Queue request with socket_manager_
  socket_manager_->ProxyToCups(std::move(in_flight_->request.buffer),
                               base::BindOnce(&ProxyManagerImpl::OnProxyToCups,
                                              weak_factory_.GetWeakPtr()));
}

void ProxyManagerImpl::OnProxyToCups(
    std::unique_ptr<std::vector<uint8_t>> response) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(in_flight_);

  if (!response) {
    return Fail("Failed to proxy request", HTTP_STATUS_SERVER_ERROR);
  }

  ProxyResponseToCaller(*response);
  return;
}

void ProxyManagerImpl::ProxyResponseToCaller(
    const std::vector<uint8_t>& response) {
  // Convert to string for parsing HTTP headers.
  std::string response_str = ipp_converter::ConvertToString(response);
  auto end_of_headers = net::HttpUtil::LocateEndOfHeaders(response_str.data(),
                                                          response_str.size());
  if (end_of_headers < 0) {
    return Fail("IPP response missing end of headers",
                HTTP_STATUS_SERVER_ERROR);
  }

  std::string_view headers_slice(response_str.data(), end_of_headers);
  scoped_refptr<net::HttpResponseHeaders> response_headers =
      net::HttpResponseHeaders::TryToCreate(headers_slice);
  if (!response_headers) {
    return Fail("Failed to parse HTTP response headers",
                HTTP_STATUS_SERVER_ERROR);
  }

  std::vector<ipp_converter::HttpHeader> parsed_headers;
  size_t iter = 0;
  std::string name, value;
  while (response_headers->EnumerateHeaderLines(&iter, &name, &value)) {
    parsed_headers.push_back({name, value});
  }

  // Slice off the ipp_message as is.
  std::vector<uint8_t> ipp_message(response.begin() + end_of_headers,
                                   response.end());

  // Send parsed response back to caller.
  std::move(in_flight_->cb)
      .Run(std::move(parsed_headers), std::move(ipp_message), HTTP_STATUS_OK);
  in_flight_.reset();
}

// TODO(crbug.com/945409): Fail with comprehensive HTTP Error response.
// Fails current request by running its callback with an empty response and
// clearing in_flight_.
void ProxyManagerImpl::Fail(const std::string& error_message,
                            int http_status_code) {
  DCHECK(in_flight_);

  DVLOG(1) << "CupsPrintService Error: " << error_message;

  std::move(in_flight_->cb).Run({}, {}, http_status_code);
  in_flight_.reset();
}

}  // namespace

// static
std::unique_ptr<ProxyManager> ProxyManager::Create(
    mojo::PendingReceiver<mojom::CupsProxier> request,
    std::unique_ptr<CupsProxyServiceDelegate> delegate) {
  auto* delegate_ptr = delegate.get();
  return std::make_unique<ProxyManagerImpl>(
      std::move(request), std::move(delegate),
      std::make_unique<IppValidator>(delegate_ptr),
      std::make_unique<PrinterInstaller>(delegate_ptr),
      SocketManager::Create(delegate_ptr));
}

std::unique_ptr<ProxyManager> ProxyManager::CreateForTesting(
    mojo::PendingReceiver<mojom::CupsProxier> request,
    std::unique_ptr<CupsProxyServiceDelegate> delegate,
    std::unique_ptr<IppValidator> ipp_validator,
    std::unique_ptr<PrinterInstaller> printer_installer,
    std::unique_ptr<SocketManager> socket_manager) {
  return std::make_unique<ProxyManagerImpl>(
      std::move(request), std::move(delegate), std::move(ipp_validator),
      std::move(printer_installer), std::move(socket_manager));
}

}  // namespace cups_proxy