Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for Web Bluetooth pin pairing #35818

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
65 changes: 65 additions & 0 deletions docs/api/session.md
Expand Up @@ -769,6 +769,71 @@ app.whenReady().then(() => {
})
```

#### `ses.setBluetoothPairingHandler(handler)` _Windows_ _Linux_

* `handler` Function | null
* `details` Object
* `deviceId` string
* `pairingKind` string - The type of pairing prompt being requested.
One of the following values:
* `confirm`
This prompt is requesting confirmation that the Bluetooth device should
be paired.
* `confirmPin`
This prompt is requesting confirmation that the provided PIN matches the
pin displayed on the device.
* `providePin`
This prompt is requesting that a pin be provided for the device.
* `frame` [WebFrameMain](web-frame-main.md)
* `pin` string (optional) - The pin value to verify if `pairingKind` is `confirmPin`.
* `callback` Function
* `response` Object
* `confirmed` boolean - `false` should be passed in if the dialog is canceled.
If the `pairingKind` is `confirm` or `confirmPin`, this value should indicate
if the pairing is confirmed. If the `pairingKind` is `providePin` the value
should be `true` when a value is provided.
* `pin` string | null (optional) - When the `pairingKind` is `providePin`
this value should be the required pin for the Bluetooth device.

Sets a handler to respond to Bluetooth pairing requests. This handler
allows developers to handle devices that require additional validation
before pairing. When a handler is not defined, any pairing on Linux or Windows
that requires additional validation will be automatically cancelled.
macOS does not require a handler because macOS handles the pairing
automatically. To clear the handler, call `setBluetoothPairingHandler(null)`.

```javascript

const { app, BrowserWindow, ipcMain, session } = require('electron')

let bluetoothPinCallback = null

function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
}

// Listen for an IPC message from the renderer to get the response for the Bluetooth pairing.
ipcMain.on('bluetooth-pairing-response', (event, response) => {
bluetoothPinCallback(response)
})

mainWindow.webContents.session.setBluetoothPairingHandler((details, callback) => {
bluetoothPinCallback = callback
// Send a IPC message to the renderer to prompt the user to confirm the pairing.
// Note that this will require logic in the renderer to handle this message and
// display a prompt to the user.
mainWindow.webContents.send('bluetooth-pairing-request', details)
})

app.whenReady().then(() => {
createWindow()
})
```

#### `ses.clearHostResolverCache()`

Returns `Promise<void>` - Resolves when the operation is complete.
Expand Down
19 changes: 17 additions & 2 deletions docs/fiddles/features/web-bluetooth/main.js
@@ -1,10 +1,13 @@
const {app, BrowserWindow} = require('electron')
const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')

function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

mainWindow.webContents.on('select-bluetooth-device', (event, deviceList, callback) => {
Expand All @@ -14,6 +17,18 @@ function createWindow () {
}
})

// Listen for a message from the renderer to get the response for the Bluetooth pairing.
ipcMain.on('bluetooth-pairing-response', (event, response) => {
bluetoothPinCallback(response)
})

mainWindow.webContents.session.setBluetoothPairingHandler((details, callback) => {

bluetoothPinCallback = callback
// Send a message to the renderer to prompt the user to confirm the pairing.
mainWindow.webContents.send('bluetooth-pairing-request', details)
})

mainWindow.loadFile('index.html')
}

Expand Down
6 changes: 6 additions & 0 deletions docs/fiddles/features/web-bluetooth/preload.js
@@ -0,0 +1,6 @@
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
bluetoothPairingRequest: (callback) => ipcRenderer.on('bluetooth-pairing-request', callback),
bluetoothPairingResponse: (response) => ipcRenderer.send('bluetooth-pairing-respnse', response)
})
28 changes: 27 additions & 1 deletion docs/fiddles/features/web-bluetooth/renderer.js
Expand Up @@ -5,4 +5,30 @@ async function testIt() {
document.getElementById('device-name').innerHTML = device.name || `ID: ${device.id}`
}

document.getElementById('clickme').addEventListener('click',testIt)
document.getElementById('clickme').addEventListener('click',testIt)

window.electronAPI.bluetoothPairingRequest((event, details) => {
const response = {}

switch (details.pairingKind) {
case 'confirm': {
response.confirmed = confirm(`Do you want to connect to device ${details.deviceId}?`)
break
}
case 'confirmPin': {
response.confirmed = confirm(`Does the pin ${details.pin} match the pin displayed on device ${details.deviceId}?`)
break
}
case 'providePin': {
const pin = prompt(`Please provide a pin for ${details.deviceId}.`)
if (pin) {
response.pin = pin
response.confirmed = true
} else {
response.confirmed = false
}
}
}

window.electronAPI.bluetoothPairingResponse(response)
})
4 changes: 4 additions & 0 deletions docs/tutorial/devices.md
Expand Up @@ -16,6 +16,10 @@ with bluetooth devices. In order to use this API in Electron, developers will
need to handle the [`select-bluetooth-device` event on the webContents](../api/web-contents.md#event-select-bluetooth-device)
associated with the device request.

Additionally, [`ses.setBluetoothPairingHandler(handler)`](../api/session.md#sessetbluetoothpairinghandlerhandler-windows-linux)
can be used to handle pairing to bluetooth devices on Windows or Linux when
additional validation such as a pin is needed.

### Example

This example demonstrates an Electron application that automatically selects
Expand Down
1 change: 1 addition & 0 deletions filenames.auto.gni
Expand Up @@ -142,6 +142,7 @@ auto_filenames = {
"lib/common/define-properties.ts",
"lib/common/ipc-messages.ts",
"lib/common/web-view-methods.ts",
"lib/common/webpack-globals-provider.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.ts",
"lib/renderer/api/ipc-renderer.ts",
Expand Down
14 changes: 14 additions & 0 deletions shell/browser/api/electron_api_session.cc
Expand Up @@ -655,6 +655,18 @@ void Session::SetDevicePermissionHandler(v8::Local<v8::Value> val,
permission_manager->SetDevicePermissionHandler(handler);
}

void Session::SetBluetoothPairingHandler(v8::Local<v8::Value> val,
gin::Arguments* args) {
ElectronPermissionManager::BluetoothPairingHandler handler;
if (!(val->IsNull() || gin::ConvertFromV8(args->isolate(), val, &handler))) {
args->ThrowTypeError("Must pass null or function");
return;
}
auto* permission_manager = static_cast<ElectronPermissionManager*>(
browser_context()->GetPermissionControllerDelegate());
permission_manager->SetBluetoothPairingHandler(handler);
}

v8::Local<v8::Promise> Session::ClearHostResolverCache(gin::Arguments* args) {
v8::Isolate* isolate = args->isolate();
gin_helper::Promise<void> promise(isolate);
Expand Down Expand Up @@ -1204,6 +1216,8 @@ gin::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
&Session::SetPermissionCheckHandler)
.SetMethod("setDevicePermissionHandler",
&Session::SetDevicePermissionHandler)
.SetMethod("setBluetoothPairingHandler",
&Session::SetBluetoothPairingHandler)
.SetMethod("clearHostResolverCache", &Session::ClearHostResolverCache)
.SetMethod("clearAuthCache", &Session::ClearAuthCache)
.SetMethod("allowNTLMCredentialsForDomains",
Expand Down
2 changes: 2 additions & 0 deletions shell/browser/api/electron_api_session.h
Expand Up @@ -105,6 +105,8 @@ class Session : public gin::Wrappable<Session>,
gin::Arguments* args);
void SetDevicePermissionHandler(v8::Local<v8::Value> val,
gin::Arguments* args);
void SetBluetoothPairingHandler(v8::Local<v8::Value> val,
gin::Arguments* args);
v8::Local<v8::Promise> ClearHostResolverCache(gin::Arguments* args);
v8::Local<v8::Promise> ClearAuthCache();
void AllowNTLMCredentialsForDomains(const std::string& domains);
Expand Down
69 changes: 66 additions & 3 deletions shell/browser/bluetooth/electron_bluetooth_delegate.cc
Expand Up @@ -7,14 +7,18 @@
#include <memory>
#include <utility>

#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "device/bluetooth/bluetooth_device.h"
#include "device/bluetooth/public/cpp/bluetooth_uuid.h"
#include "shell/browser/api/electron_api_web_contents.h"
#include "shell/browser/electron_permission_manager.h"
#include "shell/browser/lib/bluetooth_chooser.h"
#include "shell/common/gin_converters/frame_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "third_party/blink/public/common/bluetooth/web_bluetooth_device_id.h"
#include "third_party/blink/public/mojom/bluetooth/web_bluetooth.mojom.h"

Expand All @@ -23,6 +27,28 @@ using content::RenderFrameHost;
using content::WebContents;
using device::BluetoothUUID;

namespace gin {

template <>
struct Converter<content::BluetoothDelegate::PairingKind> {
static v8::Local<v8::Value> ToV8(
v8::Isolate* isolate,
content::BluetoothDelegate::PairingKind pairing_kind) {
switch (pairing_kind) {
case content::BluetoothDelegate::PairingKind::kConfirmOnly:
return StringToV8(isolate, "confirm");
case content::BluetoothDelegate::PairingKind::kConfirmPinMatch:
return StringToV8(isolate, "confirmPin");
case content::BluetoothDelegate::PairingKind::kProvidePin:
return StringToV8(isolate, "providePin");
default:
return StringToV8(isolate, "unknown");
}
}
};

} // namespace gin

namespace electron {

ElectronBluetoothDelegate::ElectronBluetoothDelegate() = default;
Expand Down Expand Up @@ -136,9 +162,46 @@ void ElectronBluetoothDelegate::ShowDevicePairPrompt(
PairPromptCallback callback,
PairingKind pairing_kind,
const absl::optional<std::u16string>& pin) {
NOTIMPLEMENTED();
std::move(callback).Run(BluetoothDelegate::PairPromptResult(
BluetoothDelegate::PairPromptStatus::kCancelled));
auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
if (web_contents) {
auto* permission_manager = static_cast<ElectronPermissionManager*>(
web_contents->GetBrowserContext()->GetPermissionControllerDelegate());

v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope scope(isolate);
gin_helper::Dictionary details =
gin_helper::Dictionary::CreateEmpty(isolate);
details.Set("deviceId", device_identifier);
details.Set("pairingKind", pairing_kind);
details.SetGetter("frame", frame);
if (pin.has_value()) {
details.Set("pin", pin.value());
}

permission_manager->CheckBluetoothDevicePair(
details, base::AdaptCallbackForRepeating(base::BindOnce(
&ElectronBluetoothDelegate::OnDevicePairPromptResponse,
weak_factory_.GetWeakPtr(), std::move(callback))));
}
}

void ElectronBluetoothDelegate::OnDevicePairPromptResponse(
PairPromptCallback callback,
base::Value::Dict response) {
BluetoothDelegate::PairPromptResult result;
if (response.FindBool("confirmed").value_or(false)) {
result.result_code = BluetoothDelegate::PairPromptStatus::kSuccess;
} else {
result.result_code = BluetoothDelegate::PairPromptStatus::kCancelled;
}

const std::string* pin = response.FindString("pin");
if (pin) {
std::u16string trimmed_input = base::UTF8ToUTF16(*pin);
base::TrimWhitespace(trimmed_input, base::TRIM_ALL, &trimmed_input);
result.pin = base::UTF16ToUTF8(trimmed_input);
}
std::move(callback).Run(result);
}

} // namespace electron
7 changes: 7 additions & 0 deletions shell/browser/bluetooth/electron_bluetooth_delegate.h
Expand Up @@ -9,6 +9,7 @@
#include <string>
#include <vector>

#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/scoped_observation.h"
#include "content/public/browser/bluetooth_delegate.h"
Expand Down Expand Up @@ -91,6 +92,12 @@ class ElectronBluetoothDelegate : public content::BluetoothDelegate {
void AddFramePermissionObserver(FramePermissionObserver* observer) override;
void RemoveFramePermissionObserver(
FramePermissionObserver* observer) override;

private:
void OnDevicePairPromptResponse(PairPromptCallback callback,
base::Value::Dict response);

base::WeakPtrFactory<ElectronBluetoothDelegate> weak_factory_{this};
};

} // namespace electron
Expand Down
17 changes: 17 additions & 0 deletions shell/browser/electron_permission_manager.cc
Expand Up @@ -130,6 +130,11 @@ void ElectronPermissionManager::SetDevicePermissionHandler(
device_permission_handler_ = handler;
}

void ElectronPermissionManager::SetBluetoothPairingHandler(
const BluetoothPairingHandler& handler) {
bluetooth_pairing_handler_ = handler;
}

void ElectronPermissionManager::RequestPermission(
blink::PermissionType permission,
content::RenderFrameHost* render_frame_host,
Expand Down Expand Up @@ -276,6 +281,18 @@ ElectronPermissionManager::SubscribePermissionStatusChange(
void ElectronPermissionManager::UnsubscribePermissionStatusChange(
SubscriptionId id) {}

void ElectronPermissionManager::CheckBluetoothDevicePair(
gin_helper::Dictionary details,
PairCallback pair_callback) const {
if (bluetooth_pairing_handler_.is_null()) {
base::Value::Dict response;
response.Set("confirmed", false);
std::move(pair_callback).Run(std::move(response));
} else {
bluetooth_pairing_handler_.Run(details, std::move(pair_callback));
}
}

bool ElectronPermissionManager::CheckPermissionWithDetails(
blink::PermissionType permission,
content::RenderFrameHost* render_frame_host,
Expand Down