chromium/extensions/browser/api/feedback_private/log_source_access_manager.cc

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

#include "extensions/browser/api/feedback_private/log_source_access_manager.h"

#include <algorithm>
#include <utility>

#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/strings/string_split.h"
#include "base/task/thread_pool.h"
#include "base/time/default_tick_clock.h"
#include "extensions/browser/api/api_resource_manager.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/browser/api/feedback_private/feedback_private_delegate.h"
#include "extensions/browser/api/feedback_private/log_source_resource.h"
#include "extensions/common/extension_id.h"

namespace extensions {

namespace {

using LogSource = api::feedback_private::LogSource;
using ReadLogSourceParams = api::feedback_private::ReadLogSourceParams;
using ReadLogSourceResult = api::feedback_private::ReadLogSourceResult;
using SystemLogsResponse = system_logs::SystemLogsResponse;

// Default value of |g_max_num_burst_accesses|.
constexpr int kDefaultMaxNumBurstAccesses = 10;

// The minimum time between consecutive reads of a log source by a particular
// extension.
constexpr base::TimeDelta kDefaultRateLimitingTimeout =
    base::Milliseconds(1000);

// The maximum number of accesses on a single log source that can be allowed
// before the next recharge increment. See access_rate_limiter.h for more info.
int g_max_num_burst_accesses = kDefaultMaxNumBurstAccesses;

// If this is null, then |kDefaultRateLimitingTimeoutMs| is used as the timeout.
const base::TimeDelta* g_rate_limiting_timeout = nullptr;

base::TimeDelta GetMinTimeBetweenReads() {
  return g_rate_limiting_timeout ? *g_rate_limiting_timeout
                                 : kDefaultRateLimitingTimeout;
}

// SystemLogsResponse is a map of strings -> strings. The map value has the
// actual log contents, a string containing all lines, separated by newlines.
// This function extracts the individual lines and converts them into a vector
// of strings, each string containing a single line.
void GetLogLinesFromSystemLogsResponse(const SystemLogsResponse& response,
                                       std::vector<std::string>* log_lines) {
  for (const std::pair<const std::string, std::string>& pair : response) {
    std::vector<std::string> new_lines = base::SplitString(
        pair.second, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
    log_lines->reserve(log_lines->size() + new_lines.size());
    log_lines->insert(log_lines->end(), new_lines.begin(), new_lines.end());
  }
}

// Redacts the strings in |result|.
void RedactResults(
    scoped_refptr<redaction::RedactionToolContainer> redactor_container,
    ReadLogSourceResult* result) {
  redaction::RedactionTool* redactor = redactor_container->Get();
  for (std::string& line : result->log_lines)
    line = redactor->Redact(line);
}

}  // namespace

LogSourceAccessManager::LogSourceAccessManager(content::BrowserContext* context)
    : context_(context),
      tick_clock_(base::DefaultTickClock::GetInstance()),
      task_runner_for_redactor_(base::ThreadPool::CreateSequencedTaskRunner(
          // User visible as the feedback_api is used by the Chrome (OS)
          // feedback extension while the user may be looking at a spinner.
          {base::TaskPriority::USER_VISIBLE,
           base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})),
      redactor_container_(
          base::MakeRefCounted<redaction::RedactionToolContainer>(
              task_runner_for_redactor_,
              /* first_party_extension_ids= */ nullptr)) {}

LogSourceAccessManager::~LogSourceAccessManager() = default;

// static
void LogSourceAccessManager::SetMaxNumBurstAccessesForTesting(
    int num_accesses) {
  g_max_num_burst_accesses = num_accesses;
}

// static
void LogSourceAccessManager::SetRateLimitingTimeoutForTesting(
    const base::TimeDelta* timeout) {
  g_rate_limiting_timeout = timeout;
}

bool LogSourceAccessManager::FetchFromSource(const ReadLogSourceParams& params,
                                             const ExtensionId& extension_id,
                                             ReadLogSourceCallback callback) {
  int requested_resource_id =
      params.reader_id ? *params.reader_id : kInvalidResourceId;
  ApiResourceManager<LogSourceResource>* resource_manager =
      ApiResourceManager<LogSourceResource>::Get(context_);

  const ResourceId resource_id =
      requested_resource_id != kInvalidResourceId
          ? requested_resource_id
          : CreateResource(params.source, extension_id);
  if (resource_id == kInvalidResourceId)
    return false;

  LogSourceResource* resource =
      resource_manager->Get(extension_id, resource_id);
  if (!resource)
    return false;

  // Enforce the rules: rate-limit access to the source from the current reader
  // handle. If not enough time has elapsed since the last access, do not read
  // from the source, but instead return an empty response. From the caller's
  // perspective, there is no new data. There is no need for the caller to keep
  // track of the time since last access.
  if (!UpdateSourceAccessTime(resource_id)) {
    std::move(callback).Run(
        std::make_unique<api::feedback_private::ReadLogSourceResult>());
    return true;
  }

  // If the API call requested a non-incremental access, clean up the
  // SingleLogSource by removing its API resource. Even if the existing source
  // were originally created as incremental, passing in incremental=false on a
  // later access indicates that the source should be closed afterwards.
  const bool delete_resource_when_done = !params.incremental;

  resource->GetLogSource()->Fetch(
      base::BindOnce(&LogSourceAccessManager::OnFetchComplete,
                     weak_factory_.GetWeakPtr(), extension_id, resource_id,
                     delete_resource_when_done, std::move(callback)));
  return true;
}

void LogSourceAccessManager::OnFetchComplete(
    const ExtensionId& extension_id,
    ResourceId resource_id,
    bool delete_resource,
    ReadLogSourceCallback callback,
    std::unique_ptr<SystemLogsResponse> response) {
  auto result = std::make_unique<ReadLogSourceResult>();

  // Always return invalid resource ID if there is a cleanup.
  result->reader_id = delete_resource ? kInvalidResourceId : resource_id;

  GetLogLinesFromSystemLogsResponse(*response, &result->log_lines);

  // Retrieve result pointer before the PostTaskAndReply to fix issues with
  // an undefined execution order of arguments in a function call
  // (std::move(result) being executed before result.get()).
  ReadLogSourceResult* result_ptr = result.get();
  task_runner_for_redactor_->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(RedactResults, redactor_container_,
                     base::Unretained(result_ptr)),
      base::BindOnce(std::move(callback), std::move(result)));

  if (delete_resource) {
    // This should also remove the entry from |sources_|.
    ApiResourceManager<LogSourceResource>::Get(context_)->Remove(extension_id,
                                                                 resource_id);
  }
}

void LogSourceAccessManager::RemoveHandle(ResourceId id) {
  auto iter = open_handles_.find(id);
  if (iter == open_handles_.end())
    return;
  const SourceAndExtension& source_and_extension = *iter->second;
  const LogSource source = source_and_extension.source;
  if (num_readers_per_source_.find(source) != num_readers_per_source_.end())
    num_readers_per_source_[source]--;

  open_handles_.erase(id);
}

LogSourceAccessManager::SourceAndExtension::SourceAndExtension(
    LogSource source,
    const ExtensionId& extension_id)
    : source(source), extension_id(extension_id) {}

LogSourceAccessManager::ResourceId LogSourceAccessManager::CreateResource(
    LogSource source,
    const ExtensionId& extension_id) {
  // Enforce the rules: Do not create too many SingleLogSource objects to read
  // from a source, even if they are from different extensions.
  if (GetNumActiveResourcesForSource(source) >= kMaxReadersPerSource)
    return kInvalidResourceId;

  auto new_resource = std::make_unique<LogSourceResource>(
      extension_id, ExtensionsAPIClient::Get()
                        ->GetFeedbackPrivateDelegate()
                        ->CreateSingleLogSource(source));

  auto* resource_manager = ApiResourceManager<LogSourceResource>::Get(context_);
  // Create an ID for the resource using the API Resource Manager, but don't
  // release ownership of it until a valid ID has been secured.
  ResourceId resource_id = resource_manager->Add(new_resource.get());
  if (resource_id == kInvalidResourceId)
    return kInvalidResourceId;

  if (base::Contains(open_handles_, resource_id)) {
    return kInvalidResourceId;
  }

  // Now that |resource_id| has been determined to be valid, release ownership
  // of the LogSourceResource, which is now owned by the API resource manager.
  new_resource.release();

  // The resource ID isn't known until |new_resource| is added to the API
  // Resource Manager, but it needs to be passed into the resource afterward, so
  // that the resource can unregister itself from LogSourceAccessManager. It is
  // passed in as part of a callback.
  resource_manager->Get(extension_id, resource_id)
      ->set_unregister_callback(
          base::BindOnce(&LogSourceAccessManager::RemoveHandle,
                         weak_factory_.GetWeakPtr(), resource_id));

  open_handles_.emplace(
      resource_id, std::make_unique<SourceAndExtension>(source, extension_id));
  num_readers_per_source_[source]++;

  return resource_id;
}

bool LogSourceAccessManager::UpdateSourceAccessTime(ResourceId id) {
  auto iter = open_handles_.find(id);
  if (iter == open_handles_.end())
    return false;

  const SourceAndExtension& key = *iter->second;
  if (rate_limiters_.find(key) == rate_limiters_.end()) {
    rate_limiters_.emplace(key, std::make_unique<AccessRateLimiter>(
                                    g_max_num_burst_accesses,
                                    GetMinTimeBetweenReads(), tick_clock_));
  }
  return rate_limiters_[key]->AttemptAccess();
}

size_t LogSourceAccessManager::GetNumActiveResourcesForSource(
    LogSource source) const {
  auto iter = num_readers_per_source_.find(source);
  if (iter == num_readers_per_source_.end())
    return 0;
  return iter->second;
}

}  // namespace extensions