hid_chooser_controller.cc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. // Copyright (c) 2021 Microsoft, Inc.
  2. // Use of this source code is governed by the MIT license that can be
  3. // found in the LICENSE file.
  4. #include "shell/browser/hid/hid_chooser_controller.h"
  5. #include <algorithm>
  6. #include <utility>
  7. #include "base/command_line.h"
  8. #include "base/containers/contains.h"
  9. #include "base/functional/bind.h"
  10. #include "content/public/browser/web_contents.h"
  11. #include "gin/data_object_builder.h"
  12. #include "services/device/public/cpp/hid/hid_blocklist.h"
  13. #include "services/device/public/cpp/hid/hid_switches.h"
  14. #include "shell/browser/api/electron_api_session.h"
  15. #include "shell/browser/hid/electron_hid_delegate.h"
  16. #include "shell/browser/hid/hid_chooser_context.h"
  17. #include "shell/browser/hid/hid_chooser_context_factory.h"
  18. #include "shell/browser/javascript_environment.h"
  19. #include "shell/common/gin_converters/callback_converter.h"
  20. #include "shell/common/gin_converters/content_converter.h"
  21. #include "shell/common/gin_converters/hid_device_info_converter.h"
  22. #include "shell/common/gin_converters/value_converter.h"
  23. #include "shell/common/node_util.h"
  24. #include "third_party/abseil-cpp/absl/strings/str_format.h"
  25. #include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
  26. #include "third_party/blink/public/mojom/hid/hid.mojom.h"
  27. #include "ui/base/l10n/l10n_util.h"
  28. namespace {
  29. bool FilterMatch(const blink::mojom::HidDeviceFilterPtr& filter,
  30. const device::mojom::HidDeviceInfo& device) {
  31. if (filter->device_ids) {
  32. if (filter->device_ids->is_vendor()) {
  33. if (filter->device_ids->get_vendor() != device.vendor_id)
  34. return false;
  35. } else if (filter->device_ids->is_vendor_and_product()) {
  36. const auto& vendor_and_product =
  37. filter->device_ids->get_vendor_and_product();
  38. if (vendor_and_product->vendor != device.vendor_id)
  39. return false;
  40. if (vendor_and_product->product != device.product_id)
  41. return false;
  42. }
  43. }
  44. if (filter->usage) {
  45. if (filter->usage->is_page()) {
  46. const uint16_t usage_page = filter->usage->get_page();
  47. auto find_it = std::ranges::find_if(
  48. device.collections,
  49. [=](const device::mojom::HidCollectionInfoPtr& c) {
  50. return usage_page == c->usage->usage_page;
  51. });
  52. if (find_it == device.collections.end())
  53. return false;
  54. } else if (filter->usage->is_usage_and_page()) {
  55. const auto& usage_and_page = filter->usage->get_usage_and_page();
  56. auto find_it = std::find_if(
  57. device.collections.begin(), device.collections.end(),
  58. [&usage_and_page](const device::mojom::HidCollectionInfoPtr& c) {
  59. return usage_and_page->usage_page == c->usage->usage_page &&
  60. usage_and_page->usage == c->usage->usage;
  61. });
  62. if (find_it == device.collections.end())
  63. return false;
  64. }
  65. }
  66. return true;
  67. }
  68. } // namespace
  69. namespace electron {
  70. HidChooserController::HidChooserController(
  71. content::RenderFrameHost* render_frame_host,
  72. std::vector<blink::mojom::HidDeviceFilterPtr> filters,
  73. std::vector<blink::mojom::HidDeviceFilterPtr> exclusion_filters,
  74. content::HidChooser::Callback callback,
  75. content::WebContents* web_contents,
  76. base::WeakPtr<ElectronHidDelegate> hid_delegate)
  77. : WebContentsObserver(web_contents),
  78. filters_(std::move(filters)),
  79. exclusion_filters_(std::move(exclusion_filters)),
  80. callback_(std::move(callback)),
  81. initiator_document_(render_frame_host->GetWeakDocumentPtr()),
  82. origin_(content::WebContents::FromRenderFrameHost(render_frame_host)
  83. ->GetPrimaryMainFrame()
  84. ->GetLastCommittedOrigin()),
  85. hid_delegate_(hid_delegate),
  86. render_frame_host_id_(render_frame_host->GetGlobalId()) {
  87. // The use above of GetMainFrame is safe as content::HidService instances are
  88. // not created for fenced frames.
  89. DCHECK(!render_frame_host->IsNestedWithinFencedFrame());
  90. chooser_context_ = HidChooserContextFactory::GetForBrowserContext(
  91. web_contents->GetBrowserContext())
  92. ->AsWeakPtr();
  93. DCHECK(chooser_context_);
  94. chooser_context_->GetHidManager()->GetDevices(base::BindOnce(
  95. &HidChooserController::OnGotDevices, weak_factory_.GetWeakPtr()));
  96. }
  97. HidChooserController::~HidChooserController() {
  98. if (callback_)
  99. std::move(callback_).Run(std::vector<device::mojom::HidDeviceInfoPtr>());
  100. }
  101. // static
  102. const std::string& HidChooserController::PhysicalDeviceIdFromDeviceInfo(
  103. const device::mojom::HidDeviceInfo& device) {
  104. // A single physical device may expose multiple HID interfaces, each
  105. // represented by a HidDeviceInfo object. When a device exposes multiple
  106. // HID interfaces, the HidDeviceInfo objects will share a common
  107. // |physical_device_id|. Group these devices so that a single chooser item
  108. // is shown for each physical device. If a device's physical device ID is
  109. // empty, use its GUID instead.
  110. return device.physical_device_id.empty() ? device.guid
  111. : device.physical_device_id;
  112. }
  113. api::Session* HidChooserController::GetSession() {
  114. if (!web_contents()) {
  115. return nullptr;
  116. }
  117. return api::Session::FromBrowserContext(web_contents()->GetBrowserContext());
  118. }
  119. void HidChooserController::OnDeviceAdded(
  120. const device::mojom::HidDeviceInfo& device) {
  121. if (!DisplayDevice(device))
  122. return;
  123. if (AddDeviceInfo(device)) {
  124. api::Session* session = GetSession();
  125. if (session) {
  126. auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_);
  127. v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
  128. v8::HandleScope scope(isolate);
  129. v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
  130. .Set("device", device.Clone())
  131. .Set("frame", rfh)
  132. .Build();
  133. session->Emit("hid-device-added", details);
  134. }
  135. }
  136. }
  137. void HidChooserController::OnDeviceRemoved(
  138. const device::mojom::HidDeviceInfo& device) {
  139. if (!base::Contains(items_, PhysicalDeviceIdFromDeviceInfo(device)))
  140. return;
  141. api::Session* session = GetSession();
  142. if (session) {
  143. auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_);
  144. v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
  145. v8::HandleScope scope(isolate);
  146. v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
  147. .Set("device", device.Clone())
  148. .Set("frame", rfh)
  149. .Build();
  150. session->Emit("hid-device-removed", details);
  151. }
  152. RemoveDeviceInfo(device);
  153. }
  154. void HidChooserController::OnDeviceChanged(
  155. const device::mojom::HidDeviceInfo& device) {
  156. bool has_chooser_item =
  157. base::Contains(items_, PhysicalDeviceIdFromDeviceInfo(device));
  158. if (!DisplayDevice(device)) {
  159. if (has_chooser_item)
  160. OnDeviceRemoved(device);
  161. return;
  162. }
  163. if (!has_chooser_item) {
  164. OnDeviceAdded(device);
  165. return;
  166. }
  167. // Update the item to replace the old device info with |device|.
  168. UpdateDeviceInfo(device);
  169. }
  170. void HidChooserController::OnDeviceChosen(gin::Arguments* args) {
  171. std::string device_id;
  172. if (!args->GetNext(&device_id) || device_id.empty()) {
  173. RunCallback({});
  174. } else {
  175. auto find_it = device_map_.find(device_id);
  176. if (find_it != device_map_.end()) {
  177. auto& device_infos = find_it->second;
  178. std::vector<device::mojom::HidDeviceInfoPtr> devices;
  179. devices.reserve(device_infos.size());
  180. for (auto& device : device_infos) {
  181. chooser_context_->GrantDevicePermission(origin_, *device);
  182. devices.push_back(device->Clone());
  183. }
  184. RunCallback(std::move(devices));
  185. } else {
  186. util::EmitWarning(
  187. base::StrCat({"The device id ", device_id, " was not found."}),
  188. "UnknownHIDDeviceId");
  189. RunCallback({});
  190. }
  191. }
  192. }
  193. void HidChooserController::OnHidManagerConnectionError() {
  194. observation_.Reset();
  195. }
  196. void HidChooserController::OnHidChooserContextShutdown() {
  197. observation_.Reset();
  198. }
  199. void HidChooserController::OnGotDevices(
  200. std::vector<device::mojom::HidDeviceInfoPtr> devices) {
  201. std::vector<device::mojom::HidDeviceInfoPtr> devicesToDisplay;
  202. devicesToDisplay.reserve(devices.size());
  203. for (auto& device : devices) {
  204. if (DisplayDevice(*device)) {
  205. if (AddDeviceInfo(*device))
  206. devicesToDisplay.push_back(device->Clone());
  207. }
  208. }
  209. // Listen to HidChooserContext for OnDeviceAdded/Removed events after the
  210. // enumeration.
  211. if (chooser_context_)
  212. observation_.Observe(chooser_context_.get());
  213. bool prevent_default = false;
  214. api::Session* session = GetSession();
  215. if (session) {
  216. auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_);
  217. v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
  218. v8::HandleScope scope(isolate);
  219. v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
  220. .Set("deviceList", devicesToDisplay)
  221. .Set("frame", rfh)
  222. .Build();
  223. prevent_default =
  224. session->Emit("select-hid-device", details,
  225. base::BindRepeating(&HidChooserController::OnDeviceChosen,
  226. weak_factory_.GetWeakPtr()));
  227. }
  228. if (!prevent_default) {
  229. RunCallback({});
  230. }
  231. }
  232. bool HidChooserController::DisplayDevice(
  233. const device::mojom::HidDeviceInfo& device) const {
  234. // Check if `device` has a top-level collection with a FIDO usage. FIDO
  235. // devices may be displayed if the origin is privileged or the blocklist is
  236. // disabled.
  237. const bool has_fido_collection =
  238. base::Contains(device.collections, device::mojom::kPageFido,
  239. [](const auto& c) { return c->usage->usage_page; });
  240. if (has_fido_collection) {
  241. if (base::CommandLine::ForCurrentProcess()->HasSwitch(
  242. switches::kDisableHidBlocklist) ||
  243. (chooser_context_ &&
  244. chooser_context_->IsFidoAllowedForOrigin(origin_))) {
  245. return FilterMatchesAny(device) && !IsExcluded(device);
  246. }
  247. AddMessageToConsole(
  248. blink::mojom::ConsoleMessageLevel::kInfo,
  249. absl::StrFormat(
  250. "Chooser dialog is not displaying a FIDO HID device: vendorId=%d, "
  251. "productId=%d, name='%s', serial='%s'",
  252. device.vendor_id, device.product_id, device.product_name.c_str(),
  253. device.serial_number.c_str()));
  254. return false;
  255. }
  256. if (device.is_excluded_by_blocklist) {
  257. AddMessageToConsole(
  258. blink::mojom::ConsoleMessageLevel::kInfo,
  259. absl::StrFormat("Chooser dialog is not displaying a device excluded by "
  260. "the HID blocklist: vendorId=%d, "
  261. "productId=%d, name='%s', serial='%s'",
  262. device.vendor_id, device.product_id,
  263. device.product_name.c_str(),
  264. device.serial_number.c_str()));
  265. return false;
  266. }
  267. return FilterMatchesAny(device) && !IsExcluded(device);
  268. }
  269. bool HidChooserController::FilterMatchesAny(
  270. const device::mojom::HidDeviceInfo& device) const {
  271. if (filters_.empty())
  272. return true;
  273. for (const auto& filter : filters_) {
  274. if (FilterMatch(filter, device))
  275. return true;
  276. }
  277. return false;
  278. }
  279. bool HidChooserController::IsExcluded(
  280. const device::mojom::HidDeviceInfo& device) const {
  281. for (const auto& exclusion_filter : exclusion_filters_) {
  282. if (FilterMatch(exclusion_filter, device))
  283. return true;
  284. }
  285. return false;
  286. }
  287. void HidChooserController::AddMessageToConsole(
  288. blink::mojom::ConsoleMessageLevel level,
  289. const std::string& message) const {
  290. if (content::RenderFrameHost* rfh =
  291. initiator_document_.AsRenderFrameHostIfValid()) {
  292. rfh->AddMessageToConsole(level, message);
  293. }
  294. }
  295. bool HidChooserController::AddDeviceInfo(
  296. const device::mojom::HidDeviceInfo& device) {
  297. const auto& id = PhysicalDeviceIdFromDeviceInfo(device);
  298. auto [iter, is_new_physical_device] = device_map_.try_emplace(id);
  299. iter->second.emplace_back(device.Clone());
  300. // append new devices to the chooser list
  301. if (is_new_physical_device)
  302. items_.emplace_back(id);
  303. return is_new_physical_device;
  304. }
  305. bool HidChooserController::RemoveDeviceInfo(
  306. const device::mojom::HidDeviceInfo& device) {
  307. const auto& id = PhysicalDeviceIdFromDeviceInfo(device);
  308. auto find_it = device_map_.find(id);
  309. DCHECK(find_it != device_map_.end());
  310. auto& device_infos = find_it->second;
  311. std::erase_if(device_infos,
  312. [&device](const device::mojom::HidDeviceInfoPtr& d) {
  313. return d->guid == device.guid;
  314. });
  315. if (!device_infos.empty())
  316. return false;
  317. // A device was disconnected. Remove it from the chooser list.
  318. device_map_.erase(find_it);
  319. std::erase(items_, id);
  320. return true;
  321. }
  322. void HidChooserController::UpdateDeviceInfo(
  323. const device::mojom::HidDeviceInfo& device) {
  324. const auto& id = PhysicalDeviceIdFromDeviceInfo(device);
  325. auto physical_device_it = device_map_.find(id);
  326. DCHECK(physical_device_it != device_map_.end());
  327. auto& device_infos = physical_device_it->second;
  328. auto device_it = std::ranges::find(device_infos, device.guid,
  329. &device::mojom::HidDeviceInfo::guid);
  330. DCHECK(device_it != device_infos.end());
  331. *device_it = device.Clone();
  332. }
  333. void HidChooserController::RunCallback(
  334. std::vector<device::mojom::HidDeviceInfoPtr> devices) {
  335. if (callback_) {
  336. std::move(callback_).Run(std::move(devices));
  337. }
  338. }
  339. void HidChooserController::RenderFrameDeleted(
  340. content::RenderFrameHost* render_frame_host) {
  341. if (hid_delegate_) {
  342. hid_delegate_->DeleteControllerForFrame(render_frame_host);
  343. }
  344. }
  345. } // namespace electron