chromium/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_server_mixin.cc

// Copyright 2024 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/browser/web_applications/isolated_web_apps/isolated_web_app_update_server_mixin.h"

#include "base/containers/contains.h"
#include "base/containers/map_util.h"
#include "base/containers/to_value_list.h"
#include "base/json/json_writer.h"
#include "base/strings/string_split.h"
#include "base/types/expected_macros.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_policy_constants.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "net/http/http_status_code.h"

namespace web_app {

namespace {
constexpr std::string_view kUpdateManifestFileName = "update_manifest.json";

std::unique_ptr<net::test_server::HttpResponse> HttpNotFound() {
  auto response = std::make_unique<net::test_server::BasicHttpResponse>();
  response->set_code(net::HTTP_NOT_FOUND);
  return response;
}

}  // namespace

IsolatedWebAppUpdateServerMixin::IsolatedWebAppUpdateServerMixin(
    InProcessBrowserTestMixinHost* mixin_host)
    : InProcessBrowserTestMixin(mixin_host) {}

IsolatedWebAppUpdateServerMixin::~IsolatedWebAppUpdateServerMixin() = default;

void IsolatedWebAppUpdateServerMixin::SetUpOnMainThread() {
  iwa_server_.RegisterRequestHandler(base::BindRepeating(
      &IsolatedWebAppUpdateServerMixin::HandleRequest, base::Unretained(this)));
  EXPECT_TRUE(iwa_server_.Start());
}

GURL IsolatedWebAppUpdateServerMixin::GetUpdateManifestUrl(
    const web_package::SignedWebBundleId& web_bundle_id) const {
  return iwa_server_.GetURL(
      base::StrCat({"/", web_bundle_id.id(), "/", kUpdateManifestFileName}));
}

base::Value::Dict
IsolatedWebAppUpdateServerMixin::CreateForceInstallPolicyEntry(
    const web_package::SignedWebBundleId& web_bundle_id) const {
  return base::Value::Dict()
      .Set(kPolicyWebBundleIdKey, web_bundle_id.id())
      .Set(kPolicyUpdateManifestUrlKey,
           GetUpdateManifestUrl(web_bundle_id).spec());
}

void IsolatedWebAppUpdateServerMixin::AddBundle(
    std::unique_ptr<BundledIsolatedWebApp> bundle) {
  auto* bundle_ptr = bundle.get();
  bundle_versions_per_id_[bundle_ptr->web_bundle_id()][bundle_ptr->version()] =
      std::move(bundle);
}

void IsolatedWebAppUpdateServerMixin::RemoveBundle(
    const web_package::SignedWebBundleId& web_bundle_id,
    const base::Version& version) {
  CHECK(base::Contains(bundle_versions_per_id_, web_bundle_id));
  auto& bundle_versions = bundle_versions_per_id_[web_bundle_id];
  CHECK(base::Contains(bundle_versions, version));
  bundle_versions.erase(version);
  if (bundle_versions.empty()) {
    bundle_versions_per_id_.erase(web_bundle_id);
  }
}

std::unique_ptr<net::test_server::HttpResponse>
IsolatedWebAppUpdateServerMixin::HandleRequest(
    const net::test_server::HttpRequest& request) {
  // Parses /<web_bundle_id>/<file_name> into { <web_bundle_id>, <file_name> }.
  auto pieces =
      base::SplitString(request.GetURL().path(), "/", base::TRIM_WHITESPACE,
                        base::SPLIT_WANT_NONEMPTY);
  if (pieces.size() != 2) {
    return HttpNotFound();
  }

  ASSIGN_OR_RETURN(auto web_bundle_id,
                   web_package::SignedWebBundleId::Create(pieces[0]),
                   [](auto) { return HttpNotFound(); });
  const auto* bundle_versions =
      base::FindOrNull(bundle_versions_per_id_, web_bundle_id);
  if (!bundle_versions) {
    return HttpNotFound();
  }

  const auto& path = pieces[1];
  if (path == kUpdateManifestFileName) {
    auto response = std::make_unique<net::test_server::BasicHttpResponse>();
    response->set_code(net::HTTP_OK);
    response->set_content_type("application/json");
    auto update_manifest = base::Value::Dict().Set(
        "versions",
        base::ToValueList(*bundle_versions, [&](const auto& bundle_info) {
          const auto& version = bundle_info.first;
          auto relative_bundle_path = base::StrCat(
              {"/", web_bundle_id.id(), "/", version.GetString(), ".swbn"});
          return base::Value::Dict()
              .Set("version", version.GetString())
              .Set("src", iwa_server_.GetURL(relative_bundle_path).spec());
        }));
    response->set_content(*base::WriteJson(update_manifest));
    return response;
  } else if (path.ends_with(".swbn")) {
    base::Version version(path.substr(0, path.size() - 5));
    if (version.IsValid()) {
      if (auto* bundle = base::FindPtrOrNull(*bundle_versions, version)) {
        auto response = std::make_unique<net::test_server::BasicHttpResponse>();
        response->set_code(net::HTTP_OK);
        response->set_content_type("application/octet-stream");
        response->set_content(bundle->GetBundleData());
        return response;
      }
    }
  }

  return HttpNotFound();
}

}  // namespace web_app