Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 22 additions & 15 deletions browser/about_flags.cc
Original file line number Diff line number Diff line change
Expand Up @@ -335,21 +335,28 @@ const char* const kBraveSyncImplLink[1] = {"https://github.com/brave/go-sync"};
#endif // BUILDFLAG(ENABLE_BRAVE_NEWS)

#if BUILDFLAG(ENABLE_PLAYLIST)
#define PLAYLIST_FEATURE_ENTRIES \
EXPAND_FEATURE_ENTRIES( \
{ \
"playlist", \
"Playlist", \
"Enables Playlist", \
kOsMac | kOsWin | kOsLinux | kOsAndroid, \
FEATURE_VALUE_TYPE(playlist::features::kPlaylist), \
}, \
{ \
"playlist-fake-ua", \
"PlaylistFakeUA", \
"Use fake UA for playlist", \
kOsMac | kOsWin | kOsLinux | kOsAndroid, \
FEATURE_VALUE_TYPE(playlist::features::kPlaylistFakeUA), \
#define PLAYLIST_FEATURE_ENTRIES \
EXPAND_FEATURE_ENTRIES( \
{ \
"playlist", \
"Playlist", \
"Enables Playlist", \
kOsMac | kOsWin | kOsLinux | kOsAndroid, \
FEATURE_VALUE_TYPE(playlist::features::kPlaylist), \
}, \
{ \
"playlist-fake-ua", \
"PlaylistFakeUA", \
"Use fake UA for playlist", \
kOsMac | kOsWin | kOsLinux | kOsAndroid, \
FEATURE_VALUE_TYPE(playlist::features::kPlaylistFakeUA), \
}, \
{ \
"playlist-service-v2", \
"Playlist Service V2", \
"Enables the network-observation based media detection", \
kOsMac | kOsWin | kOsLinux | kOsAndroid, \
FEATURE_VALUE_TYPE(playlist::features::kPlaylistServiceV2), \
})
#else
#define PLAYLIST_FEATURE_ENTRIES
Expand Down
12 changes: 12 additions & 0 deletions browser/net/brave_proxying_url_loader_factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "brave/browser/net/url_context.h"
#include "brave/components/brave_shields/content/browser/adblock_stub_response.h"
#include "brave/components/brave_shields/core/common/features.h"
#include "brave/components/playlist/core/common/buildflags/buildflags.h" // IWYU pragma: keep
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
Expand All @@ -43,6 +44,10 @@
#include "third_party/blink/public/common/loader/throttling_url_loader.h"
#include "url/origin.h"

#if BUILDFLAG(ENABLE_PLAYLIST)
#include "brave/components/playlist/content/browser/playlist_network_observer.h"
#endif

namespace {

// Helper struct for crafting responses.
Expand Down Expand Up @@ -613,6 +618,13 @@ void BraveProxyingURLLoaderFactory<
return;
}

#if BUILDFLAG(ENABLE_PLAYLIST)
// Headers only - the response body is forwarded untouched below.
playlist::MaybeNotifyMediaResponse(
browser_context_, render_frame_token_, request_.url,
current_response_head_->mime_type, request_.destination);
#endif

proxied_client_receiver_.Resume();
target_client_->OnReceiveResponse(std::move(current_response_head_),
std::move(current_response_body_),
Expand Down
6 changes: 6 additions & 0 deletions browser/net/sources.gni
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import("//brave/components/brave_ads/buildflags/buildflags.gni")
import("//brave/components/brave_wallet/common/buildflags/buildflags.gni")
import("//brave/components/playlist/core/common/buildflags/buildflags.gni")
import("//brave/components/tor/buildflags/buildflags.gni")
import("//build/config/features.gni")
import("//extensions/buildflags/buildflags.gni")
Expand Down Expand Up @@ -78,6 +79,7 @@ brave_browser_net_deps = [
"//brave/components/constants",
"//brave/components/constants:brave_service_key_helper",
"//brave/components/geolocation",
"//brave/components/playlist/core/common/buildflags",
"//brave/components/query_filter/browser",
"//brave/components/safebrowsing",
"//brave/components/speech_to_text",
Expand Down Expand Up @@ -116,6 +118,10 @@ if (enable_brave_wallet) {
]
}

if (enable_playlist) {
brave_browser_net_deps += [ "//brave/components/playlist/content/browser" ]
}

if (enable_extensions) {
brave_browser_net_deps += [ "//extensions/common:common_constants" ]
}
Expand Down
147 changes: 138 additions & 9 deletions browser/playlist/playlist_data_source.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/heap_array.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/memory_mapped_file.h"
Expand All @@ -28,6 +30,7 @@
#include "base/task/thread_pool.h"
#include "brave/components/playlist/content/browser/mime_util.h"
#include "brave/components/playlist/content/browser/playlist_service.h"
#include "brave/components/playlist/core/common/features.h"
#include "components/favicon_base/favicon_url_parser.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/url_data_source.h"
Expand All @@ -44,6 +47,27 @@ namespace {

constexpr base::ByteSize kMediaChunkSize = base::MiBU(1); // 1MB

// `FinalExtension()` keeps the leading dot, but the mime table is keyed
// without it.
std::string GetMimeTypeForPath(const base::FilePath& path) {
base::FilePath::StringType extension = path.FinalExtension();
if (extension.starts_with(FILE_PATH_LITERAL("."))) {
extension.erase(0, 1);
}

if (auto mime_type = mime_util::GetMimeTypeForFileExtension(extension)) {
return *mime_type;
}

// Not in the table because it's only ever a piece of a stream, never a file
// Playlist would save on its own.
if (extension == FILE_PATH_LITERAL("m4s")) {
return "video/mp4";
}

return "application/octet-stream";
}

class RefCountedMemMap : public base::RefCountedMemory {
public:
explicit RefCountedMemMap(const base::FilePath& path) {
Expand Down Expand Up @@ -149,15 +173,51 @@ PlaylistDataSource::DataRequest::DataRequest(const GURL& url) {
const auto full_path = content::URLDataSource::URLToRequestPath(url);
const auto paths = base::SplitStringPiece(
full_path, "/", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (paths.size() != 2) {
LOG(ERROR) << "Invalid playlist data source URL, might be routed from "
"saved .m3u8 file: "
<< url.spec();
if (paths.size() < 2) {
LOG(ERROR) << "Invalid playlist data source URL: " << url.spec();
return;
}

id = paths.at(0);
const auto& type_string = paths.at(1);

// Locally saved HLS streams: <id>/hls/<file>, where <file> may itself
// contain slashes because it comes verbatim from the local manifest.
if (type_string == "hls" &&
base::FeatureList::IsEnabled(features::kPlaylistServiceV2)) {
if (paths.size() < 3) {
LOG(ERROR) << "HLS request is missing a file name: " << url.spec();
return;
}

base::FilePath file;
for (const auto& component : base::span(paths).subspan(2u)) {
// The manifest we generate only ever names plain relative files, so
// anything that could climb out of the item directory is a rejection,
// not something to normalize.
if (component == "." || component == ".." ||
component.find('\\') != std::string_view::npos) {
LOG(ERROR) << "Rejecting HLS path component: " << url.spec();
return;
}
file = file.Append(base::FilePath::FromASCII(component));
}

if (file.ReferencesParent()) {
LOG(ERROR) << "Rejecting HLS path referencing parent: " << url.spec();
return;
}

type = DataRequest::Type::kHls;
hls_file = std::move(file);
return;
}

if (paths.size() != 2) {
LOG(ERROR) << "Invalid playlist data source URL: " << url.spec();
return;
}

if (type_string == "thumbnail") {
type = DataRequest::Type::kThumbnail;
} else if (type_string == "media") {
Expand All @@ -166,9 +226,7 @@ PlaylistDataSource::DataRequest::DataRequest(const GURL& url) {
type = DataRequest::Type::kFavicon;
} else {
type = DataRequest::Type::kNone;
LOG(ERROR) << "Invalid playlist data source URL, might be routed from "
"saved .m3u8 file: "
<< url.spec();
LOG(ERROR) << "Invalid playlist data source URL: " << url.spec();
}
}

Expand Down Expand Up @@ -204,6 +262,10 @@ void PlaylistDataSource::StartDataRequest(
case DataRequest::Type::kFavicon:
GetFavicon(data_request, wc_getter, std::move(got_data_callback));
break;
case DataRequest::Type::kHls:
// Only manifests come through here; segments support range requests.
GetHlsManifest(data_request, std::move(got_data_callback));
break;
case DataRequest::Type::kMedia:
NOTREACHED() << "This request should call StartRangeDataRequest()";
}
Expand All @@ -215,7 +277,17 @@ void PlaylistDataSource::StartRangeDataRequest(
const net::HttpByteRange& range,
GotRangeDataCallback callback) {
DataRequest data_request(url);
if (data_request.type != DataRequest::Type::kMedia || !range.IsValid()) {
if (!range.IsValid()) {
std::move(callback).Run({});
return;
}

if (data_request.type == DataRequest::Type::kHls) {
GetHlsSegment(data_request, range, std::move(callback));
return;
}

if (data_request.type != DataRequest::Type::kMedia) {
std::move(callback).Run({});
return;
}
Expand Down Expand Up @@ -262,6 +334,54 @@ void PlaylistDataSource::GetMediaFile(
std::move(got_data_callback));
}

base::FilePath PlaylistDataSource::ResolveHlsPath(
const DataRequest& request) const {
if (request.hls_file.empty() || !service_->HasPlaylistItem(request.id)) {
return base::FilePath();
}

// Stream files live in their own subdirectory so they can't collide with
// the item's `media_file` / `thumbnail`.
const base::FilePath item_dir =
service_->GetPlaylistItemDirPath(request.id).AppendASCII("hls");
const base::FilePath path = item_dir.Append(request.hls_file);

// `DataRequest` already rejects "..", but the item directory is the security
// boundary here, so confirm it rather than trusting that.
if (!item_dir.IsParent(path)) {
return base::FilePath();
}

return path;
}

void PlaylistDataSource::GetHlsManifest(const DataRequest& request,
GotDataCallback got_data_callback) {
const base::FilePath path = ResolveHlsPath(request);
if (path.empty()) {
std::move(got_data_callback).Run(nullptr);
return;
}

base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, base::MayBlock(), base::BindOnce(&ReadMemoryMappedFile, path),
std::move(got_data_callback));
}

void PlaylistDataSource::GetHlsSegment(const DataRequest& request,
const net::HttpByteRange& range,
GotRangeDataCallback got_data_callback) {
const base::FilePath path = ResolveHlsPath(request);
if (path.empty()) {
std::move(got_data_callback).Run({});
return;
}

base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, base::MayBlock(), base::BindOnce(&ReadFileRange, path, range),
std::move(got_data_callback));
}

void PlaylistDataSource::GetFavicon(
const DataRequest& request,
const content::WebContents::Getter& wc_getter,
Expand Down Expand Up @@ -294,6 +414,8 @@ std::string PlaylistDataSource::GetMimeType(const GURL& url) {
// actual file extension in WebUIUrlLoader.
case DataRequest::Type::kFavicon:
return FaviconSource::GetMimeType(url);
case DataRequest::Type::kHls:
return GetMimeTypeForPath(data_request.hls_file);
case DataRequest::Type::kNone:
return {};
}
Expand All @@ -309,7 +431,14 @@ bool PlaylistDataSource::SupportsRangeRequests(const GURL& url) const {
return false;
}

return DataRequest(url).type == DataRequest::Type::kMedia;
DataRequest data_request(url);
if (data_request.type == DataRequest::Type::kHls) {
// Manifests are small and read whole; only segments are worth ranging.
return !data_request.hls_file.MatchesFinalExtension(
FILE_PATH_LITERAL(".m3u8"));
}

return data_request.type == DataRequest::Type::kMedia;
}

} // namespace playlist
19 changes: 19 additions & 0 deletions browser/playlist/playlist_data_source.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <string>

#include "base/files/file_path.h"
#include "chrome/browser/ui/webui/favicon_source.h"

class GURL;
Expand All @@ -20,6 +21,12 @@ class PlaylistService;
// A URL data source for
// chrome-untrusted://playlist-data/<playlist-id>/{thumbnail,media,favicon}/
// resources, for use in webui pages that want to get thumbnails or media data.
//
// Streams saved as HLS are served under
// chrome-untrusted://playlist-data/<playlist-id>/hls/<file>, where <file> is
// relative to the item's "hls" subdirectory. Chromium's built-in HLS demuxer
// selects itself based on the ".m3u8" suffix, then fetches the segments the
// local manifest names through this same route.
class PlaylistDataSource : public FaviconSource {
public:
PlaylistDataSource(Profile* profile, PlaylistService* service);
Expand Down Expand Up @@ -47,6 +54,7 @@ class PlaylistDataSource : public FaviconSource {
kThumbnail,
kMedia,
kFavicon,
kHls,
};

explicit DataRequest(const GURL& url);
Expand All @@ -56,6 +64,9 @@ class PlaylistDataSource : public FaviconSource {

std::string id;
Type type = Type::kNone;
// For `kHls` only: the requested file, relative to the item's "hls"
// subdirectory.
base::FilePath hls_file;
};

void GetThumbnail(const DataRequest& request,
Expand All @@ -68,6 +79,14 @@ class PlaylistDataSource : public FaviconSource {
const content::WebContents::Getter& wc_getter,
const net::HttpByteRange& range,
GotRangeDataCallback got_data_callback);
void GetHlsManifest(const DataRequest& request,
GotDataCallback got_data_callback);
void GetHlsSegment(const DataRequest& request,
const net::HttpByteRange& range,
GotRangeDataCallback got_data_callback);
// Returns the absolute path for a `kHls` request, or an empty path if the
// item is unknown or the request escapes the item's directory.
base::FilePath ResolveHlsPath(const DataRequest& request) const;

raw_ptr<PlaylistService, DanglingUntriaged> service_ = nullptr;
};
Expand Down
Loading
Loading