Skip to content

Commit

Permalink
feat: add support for Web Bluetooth pin pairing (#35818)
Browse files Browse the repository at this point in the history
* feat: add bluetooth pairing handler

* Update docs/api/session.md

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* Update docs/api/session.md

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* docs: update based on review

* Apply suggestions from code review

Co-authored-by: Erick Zhao <erick@hotmail.ca>
Co-authored-by: Charles Kerr <charles@charleskerr.com>

* chore: update docs per review

* chore: cleanup callback per review

Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>
  • Loading branch information
4 people committed Sep 26, 2022
1 parent c979550 commit 7bf3913
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 6 deletions.
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

0 comments on commit 7bf3913

Please sign in to comment.