| // 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/browser/ash/system_web_apps/apps/terminal_source.h" |
| |
| #include <optional> |
| #include <string_view> |
| |
| #include "ash/constants/ash_features.h" |
| #include "base/containers/fixed_flat_map.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/string_util.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "chrome/browser/ash/crostini/crostini_pref_names.h" |
| #include "chrome/browser/ash/file_manager/path_util.h" |
| #include "chrome/browser/ash/file_system_provider/provided_file_system_info.h" |
| #include "chrome/browser/ash/file_system_provider/provider_interface.h" |
| #include "chrome/browser/ash/file_system_provider/service.h" |
| #include "chrome/browser/ash/guest_os/guest_os_terminal.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/common/url_constants.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chromeos/ash/components/channel/channel_info.h" |
| #include "components/content_settings/core/common/content_settings_types.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/tabs/public/tab_interface.h" |
| #include "components/version_info/channel.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/web_contents.h" |
| #include "net/base/mime_util.h" |
| #include "services/network/public/mojom/content_security_policy.mojom.h" |
| #include "third_party/zlib/google/compression_utils.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/webui/webui_allowlist.h" |
| |
| namespace { |
| constexpr base::FilePath::CharType kTerminalRoot[] = |
| FILE_PATH_LITERAL("/usr/share/chromeos-assets/crosh_builtin"); |
| constexpr char kDefaultMime[] = "text/html"; |
| |
| class TerminalFileSystemProvider |
| : public ash::file_system_provider::ExtensionProvider { |
| public: |
| TerminalFileSystemProvider() |
| : ash::file_system_provider::ExtensionProvider( |
| ProfileManager::GetPrimaryUserProfile(), |
| ash::file_system_provider::ProviderId::CreateFromExtensionId( |
| guest_os::kTerminalSystemAppId), |
| ash::file_system_provider::Capabilities{ |
| .configurable = true, |
| .watchable = false, |
| .multiple_mounts = true, |
| .source = extensions::FileSystemProviderSource::SOURCE_NETWORK}, |
| l10n_util::GetStringUTF8(IDS_CROSTINI_TERMINAL_APP_NAME), |
| /*icon_set=*/std::nullopt) {} |
| bool RequestMount( |
| Profile* profile, |
| ash::file_system_provider::RequestMountCallback callback) override { |
| guest_os::LaunchTerminalHome(profile, display::kInvalidDisplayId, |
| /*restore_id=*/0); |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), base::File::FILE_OK)); |
| return true; |
| } |
| }; |
| |
| // Attempts to read |path| as plain file. If read fails, attempts to read |
| // |path|.gz and decompress. Returns true if either file is read ok. |
| bool ReadUncompressedOrGzip(base::FilePath path, std::string* content) { |
| bool result = base::ReadFileToString(path, content); |
| if (!result) { |
| result = |
| base::ReadFileToString(base::FilePath(path.value() + ".gz"), content); |
| result = compression::GzipUncompress(*content, content); |
| } |
| return result; |
| } |
| |
| void ReadFile(const base::FilePath downloads, |
| const std::string& relative_path, |
| content::URLDataSource::GotDataCallback callback) { |
| base::FilePath path; |
| std::string content; |
| bool result = false; |
| |
| // If chrome://flags#terminal-dev set on dev channel, check Downloads. |
| if (ash::GetChannel() <= version_info::Channel::DEV && |
| base::FeatureList::IsEnabled(ash::features::kTerminalDev)) { |
| path = downloads.Append("crosh_builtin").Append(relative_path); |
| result = ReadUncompressedOrGzip(path, &content); |
| } |
| if (!result) { |
| path = base::FilePath(kTerminalRoot).Append(relative_path); |
| result = ReadUncompressedOrGzip(path, &content); |
| } |
| |
| // Terminal gets files from /usr/share/chromeos-assets/crosh-builtin. |
| // In chromium tests, these files don't exist, so we serve dummy values. |
| if (!result) { |
| static constexpr auto kTestFiles = |
| base::MakeFixedFlatMap<std::string_view, std::string_view>({ |
| {"html/crosh.html", ""}, |
| {"html/terminal.html", "<script src='/js/terminal.js'></script>"}, |
| {"js/terminal.js", |
| "chrome.terminalPrivate.openVmshellProcess([], () => {})"}, |
| }); |
| auto it = kTestFiles.find(relative_path); |
| if (it != kTestFiles.end()) { |
| content = it->second; |
| result = true; |
| } |
| } |
| |
| DCHECK(result) << path; |
| std::move(callback).Run( |
| base::MakeRefCounted<base::RefCountedString>(std::move(content))); |
| } |
| } // namespace |
| |
| // static |
| std::unique_ptr<TerminalSource> TerminalSource::ForCrosh(Profile* profile) { |
| return base::WrapUnique( |
| new TerminalSource(profile, chrome::kChromeUIUntrustedCroshURL, false)); |
| } |
| |
| // static |
| std::unique_ptr<TerminalSource> TerminalSource::ForTerminal(Profile* profile) { |
| ash::file_system_provider::Service::Get(profile)->RegisterProvider( |
| std::make_unique<TerminalFileSystemProvider>()); |
| return base::WrapUnique(new TerminalSource( |
| profile, chrome::kChromeUIUntrustedTerminalURL, |
| profile->GetPrefs() |
| ->FindPreference(crostini::prefs::kTerminalSshAllowedByPolicy) |
| ->GetValue() |
| ->GetBool())); |
| } |
| |
| TerminalSource::TerminalSource(Profile* profile, |
| std::string source, |
| bool ssh_allowed) |
| : profile_(profile), |
| source_(source), |
| ssh_allowed_(ssh_allowed), |
| downloads_(file_manager::util::GetDownloadsFolderForProfile(profile)) { |
| auto* webui_allowlist = WebUIAllowlist::GetOrCreate(profile); |
| const url::Origin terminal_origin = url::Origin::Create(GURL(source)); |
| CHECK(!terminal_origin.opaque()); |
| webui_allowlist->RegisterAutoGrantedPermissions( |
| terminal_origin, |
| {ContentSettingsType::CLIPBOARD_READ_WRITE, ContentSettingsType::COOKIES, |
| ContentSettingsType::IMAGES, ContentSettingsType::JAVASCRIPT, |
| ContentSettingsType::NOTIFICATIONS, ContentSettingsType::POPUPS, |
| ContentSettingsType::SOUND}); |
| webui_allowlist->RegisterAutoGrantedThirdPartyCookies( |
| terminal_origin, {ContentSettingsPattern::Wildcard()}); |
| } |
| |
| TerminalSource::~TerminalSource() = default; |
| |
| std::string TerminalSource::GetSource() { |
| return source_; |
| } |
| |
| void TerminalSource::StartDataRequest( |
| const GURL& url, |
| const content::WebContents::Getter& wc_getter, |
| content::URLDataSource::GotDataCallback callback) { |
| // skip first '/' in path. |
| std::string path = url.path().substr(1); |
| if (path.empty()) { |
| path = "html/terminal.html"; |
| } |
| |
| // Refresh the $i8n{themeColor} replacement for css files. |
| if (base::EndsWith(path, ".css", base::CompareCase::INSENSITIVE_ASCII)) { |
| GURL contents_url; |
| std::optional<SkColor> opener_background_color; |
| content::WebContents* contents = wc_getter.Run(); |
| if (contents) { |
| contents_url = contents->GetVisibleURL(); |
| TabStripModel* tab_strip; |
| int tab_index; |
| extensions::ExtensionTabUtil::GetTabStripModel(contents, &tab_strip, |
| &tab_index); |
| tabs::TabInterface* opener_tab = tab_strip->GetOpenerOfTabAt(tab_index); |
| if (opener_tab) { |
| CHECK(opener_tab->GetContents()); |
| opener_background_color = |
| opener_tab->GetContents()->GetBackgroundColor(); |
| } |
| } |
| replacements_["themeColor"] = |
| base::EscapeForHTML(guest_os::GetTerminalSettingBackgroundColor( |
| profile_, contents_url, opener_background_color)); |
| } |
| |
| base::ThreadPool::PostTask( |
| FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING}, |
| base::BindOnce(&ReadFile, downloads_, path, std::move(callback))); |
| } |
| |
| std::string TerminalSource::GetMimeType(const GURL& url) { |
| std::string mime_type; |
| if (!net::GetWellKnownMimeTypeFromFile(base::FilePath(url.path_piece()), |
| &mime_type)) { |
| return kDefaultMime; |
| } |
| |
| return mime_type; |
| } |
| |
| bool TerminalSource::ShouldServeMimeTypeAsContentTypeHeader() { |
| // TerminalSource pages include js modules which require an explicit MimeType. |
| return true; |
| } |
| |
| const ui::TemplateReplacements* TerminalSource::GetReplacements() { |
| return &replacements_; |
| } |
| |
| std::string TerminalSource::GetContentSecurityPolicy( |
| network::mojom::CSPDirectiveName directive) { |
| // CSP required for SSH. |
| if (ssh_allowed_) { |
| switch (directive) { |
| case network::mojom::CSPDirectiveName::ConnectSrc: |
| return "connect-src *;"; |
| case network::mojom::CSPDirectiveName::FrameAncestors: |
| return "frame-ancestors 'self';"; |
| case network::mojom::CSPDirectiveName::FrameSrc: |
| return "frame-src 'self';"; |
| case network::mojom::CSPDirectiveName::ObjectSrc: |
| return "object-src 'self';"; |
| case network::mojom::CSPDirectiveName::ScriptSrc: |
| return "script-src 'self' 'wasm-unsafe-eval';"; |
| case network::mojom::CSPDirectiveName::WorkerSrc: |
| return "worker-src 'self';"; |
| default: |
| break; |
| } |
| } |
| |
| switch (directive) { |
| case network::mojom::CSPDirectiveName::ImgSrc: |
| return "img-src * data: blob:;"; |
| case network::mojom::CSPDirectiveName::MediaSrc: |
| return "media-src data:;"; |
| case network::mojom::CSPDirectiveName::StyleSrc: |
| return "style-src * 'unsafe-inline'; font-src *;"; |
| case network::mojom::CSPDirectiveName::RequireTrustedTypesFor: |
| [[fallthrough]]; |
| case network::mojom::CSPDirectiveName::TrustedTypes: |
| // TODO(crbug.com/40137141): Trusted Type remaining WebUI |
| // This removes require-trusted-types-for and trusted-types directives |
| // from the CSP header. |
| return std::string(); |
| default: |
| return content::URLDataSource::GetContentSecurityPolicy(directive); |
| } |
| } |
| |
| // Improve security, and it is required for wasm SharedArrayBuffer. |
| std::string TerminalSource::GetCrossOriginOpenerPolicy() { |
| return "same-origin"; |
| } |
| |
| // Required for wasm SharedArrayBuffer. |
| std::string TerminalSource::GetCrossOriginEmbedderPolicy() { |
| return "require-corp"; |
| } |