Files
makelore/electron/robot-hotspot/windows.ts

204 lines
10 KiB
TypeScript

import type { RobotHotspotAdapter, RobotHotspotAdapterCandidate } from './adapter';
import { RobotHotspotAdapterError, abortableDelay, throwIfAborted } from './adapter';
const ERROR_SUCCESS = 0;
const ERROR_ACCESS_DENIED = 5;
const WLAN_CONNECTION_MODE_DISCOVERY_UNSECURE = 3;
const DOT11_BSS_TYPE_INFRASTRUCTURE = 1;
const WLAN_INTERFACE_STATE_CONNECTED = 1;
const WLAN_INTF_OPCODE_CURRENT_CONNECTION = 7;
interface WindowsNativeKey {
interfaceGuid: Record<string, unknown>;
ssid: { uSSIDLength: number; ucSSID: Uint8Array | number[] };
}
function nativeFailure(result: number, operation: 'scan' | 'connect'): never {
if (result === ERROR_ACCESS_DENIED) throw new RobotHotspotAdapterError('permission_denied');
throw new RobotHotspotAdapterError(operation === 'scan' ? 'scan_failed' : 'connect_failed');
}
function bytesToSsid(value: { uSSIDLength: number; ucSSID: Uint8Array | number[] }): string {
return Buffer.from(value.ucSSID).subarray(0, value.uSSIDLength).toString('utf8');
}
function guidKey(value: Record<string, unknown>): string {
return JSON.stringify(value);
}
export async function createWindowsRobotHotspotAdapter(): Promise<RobotHotspotAdapter> {
if (process.platform !== 'win32') throw new RobotHotspotAdapterError('scan_failed');
const koffiModule = await import('koffi');
const koffi = (koffiModule as typeof koffiModule & { default: typeof koffiModule }).default;
const wlan = koffi.load('wlanapi.dll');
const GUID = koffi.struct({
Data1: 'uint32_t', Data2: 'uint16_t', Data3: 'uint16_t', Data4: koffi.array('uint8_t', 8),
});
const DOT11_SSID = koffi.struct({ uSSIDLength: 'uint32_t', ucSSID: koffi.array('uint8_t', 32) });
const WLAN_INTERFACE_INFO = koffi.struct({
InterfaceGuid: GUID,
strInterfaceDescription: koffi.array('char16_t', 256, 'String'),
isState: 'int32_t',
});
const WLAN_AVAILABLE_NETWORK = koffi.struct({
strProfileName: koffi.array('char16_t', 256, 'String'),
dot11Ssid: DOT11_SSID,
dot11BssType: 'int32_t',
uNumberOfBssids: 'uint32_t',
bNetworkConnectable: 'int32_t',
wlanNotConnectableReason: 'uint32_t',
uNumberOfPhyTypes: 'uint32_t',
dot11PhyTypes: koffi.array('int32_t', 8),
bMorePhyTypes: 'int32_t',
wlanSignalQuality: 'uint32_t',
bSecurityEnabled: 'int32_t',
dot11DefaultAuthAlgorithm: 'uint32_t',
dot11DefaultCipherAlgorithm: 'uint32_t',
dwFlags: 'uint32_t',
dwReserved: 'uint32_t',
});
const WLAN_ASSOCIATION_ATTRIBUTES = koffi.struct({
dot11Ssid: DOT11_SSID,
dot11BssType: 'int32_t',
dot11Bssid: koffi.array('uint8_t', 6),
dot11PhyType: 'int32_t',
uDot11PhyIndex: 'uint32_t',
wlanSignalQuality: 'uint32_t',
ulRxRate: 'uint32_t',
ulTxRate: 'uint32_t',
});
const WLAN_SECURITY_ATTRIBUTES = koffi.struct({
bSecurityEnabled: 'int32_t', bOneXEnabled: 'int32_t',
dot11AuthAlgorithm: 'uint32_t', dot11CipherAlgorithm: 'uint32_t',
});
const WLAN_CONNECTION_ATTRIBUTES = koffi.struct({
isState: 'int32_t', wlanConnectionMode: 'int32_t',
strProfileName: koffi.array('char16_t', 256, 'String'),
wlanAssociationAttributes: WLAN_ASSOCIATION_ATTRIBUTES,
wlanSecurityAttributes: WLAN_SECURITY_ATTRIBUTES,
});
const WLAN_CONNECTION_PARAMETERS = koffi.struct({
wlanConnectionMode: 'int32_t', strProfile: 'str16',
pDot11Ssid: koffi.pointer(DOT11_SSID), pDesiredBssidList: 'void *',
dot11BssType: 'int32_t', dwFlags: 'uint32_t',
});
const WlanOpenHandle = wlan.func('WlanOpenHandle', 'uint32_t', ['uint32_t', 'void *', koffi.out(koffi.pointer('uint32_t')), koffi.out(koffi.pointer('void *'))]);
const WlanCloseHandle = wlan.func('WlanCloseHandle', 'uint32_t', ['void *', 'void *']);
const WlanEnumInterfaces = wlan.func('WlanEnumInterfaces', 'uint32_t', ['void *', 'void *', koffi.out(koffi.pointer('void *'))]);
const WlanScan = wlan.func('WlanScan', 'uint32_t', ['void *', koffi.pointer(GUID), 'void *', 'void *', 'void *']);
const WlanGetAvailableNetworkList = wlan.func('WlanGetAvailableNetworkList', 'uint32_t', ['void *', koffi.pointer(GUID), 'uint32_t', 'void *', koffi.out(koffi.pointer('void *'))]);
const WlanConnect = wlan.func('WlanConnect', 'uint32_t', ['void *', koffi.pointer(GUID), koffi.pointer(WLAN_CONNECTION_PARAMETERS), 'void *']);
const WlanQueryInterface = wlan.func('WlanQueryInterface', 'uint32_t', ['void *', koffi.pointer(GUID), 'int32_t', 'void *', koffi.out(koffi.pointer('uint32_t')), koffi.out(koffi.pointer('void *')), koffi.out(koffi.pointer('int32_t'))]);
const WlanFreeMemory = wlan.func('WlanFreeMemory', 'void', ['void *']);
let connectingInterfaceGuid: Record<string, unknown> | null = null;
async function withHandle<T>(operation: 'scan' | 'connect', body: (handle: unknown) => Promise<T>): Promise<T> {
const negotiated = [0];
const handle = [null] as unknown[];
const result = WlanOpenHandle(2, null, negotiated, handle) as number;
if (result !== ERROR_SUCCESS || !handle[0]) nativeFailure(result, operation);
try { return await body(handle[0]); }
finally { WlanCloseHandle(handle[0], null); }
}
function interfaces(handle: unknown, operation: 'scan' | 'connect'): Array<Record<string, unknown>> {
const output = [null] as unknown[];
const result = WlanEnumInterfaces(handle, null, output) as number;
if (result !== ERROR_SUCCESS || !output[0]) nativeFailure(result, operation);
try {
const count = koffi.decode(output[0], 'uint32_t') as number;
const offset = 8;
return Array.from({ length: Math.min(count, 64) }, (_, index) => koffi.decode(output[0], offset + index * koffi.sizeof(WLAN_INTERFACE_INFO), WLAN_INTERFACE_INFO) as Record<string, unknown>);
} finally { WlanFreeMemory(output[0]); }
}
function currentOnInterface(handle: unknown, interfaceGuid: Record<string, unknown>, operation: 'scan' | 'connect'): string | null {
const size = [0];
const data = [null] as unknown[];
const valueType = [0];
const result = WlanQueryInterface(handle, interfaceGuid, WLAN_INTF_OPCODE_CURRENT_CONNECTION, null, size, data, valueType) as number;
if (result !== ERROR_SUCCESS || !data[0]) {
if (result === ERROR_ACCESS_DENIED) nativeFailure(result, operation);
return null;
}
try {
const connection = koffi.decode(data[0], WLAN_CONNECTION_ATTRIBUTES) as { isState: number; wlanAssociationAttributes: { dot11Ssid: WindowsNativeKey['ssid'] } };
return connection.isState === WLAN_INTERFACE_STATE_CONNECTED ? bytesToSsid(connection.wlanAssociationAttributes.dot11Ssid) : null;
} finally { WlanFreeMemory(data[0]); }
}
return {
async scan(signal) {
return withHandle('scan', async (handle) => {
const foundInterfaces = interfaces(handle, 'scan');
for (const item of foundInterfaces) {
throwIfAborted(signal);
const result = WlanScan(handle, item.InterfaceGuid, null, null, null) as number;
if (result !== ERROR_SUCCESS) nativeFailure(result, 'scan');
}
// WlanScan is asynchronous and usually needs about four seconds before the
// available-network cache represents the requested scan.
await abortableDelay(4_000, signal);
const candidates: RobotHotspotAdapterCandidate[] = [];
for (const item of foundInterfaces) {
throwIfAborted(signal);
const output = [null] as unknown[];
const result = WlanGetAvailableNetworkList(handle, item.InterfaceGuid, 0, null, output) as number;
if (result !== ERROR_SUCCESS || !output[0]) nativeFailure(result, 'scan');
try {
const count = koffi.decode(output[0], 'uint32_t') as number;
const connectedSsid = currentOnInterface(handle, item.InterfaceGuid as Record<string, unknown>, 'scan');
for (let index = 0; index < Math.min(count, 512); index += 1) {
const network = koffi.decode(output[0], 8 + index * koffi.sizeof(WLAN_AVAILABLE_NETWORK), WLAN_AVAILABLE_NETWORK) as {
dot11Ssid: WindowsNativeKey['ssid']; dot11BssType: number; wlanSignalQuality: number; bSecurityEnabled: number; bNetworkConnectable: number;
};
if (network.dot11BssType !== DOT11_BSS_TYPE_INFRASTRUCTURE || network.bSecurityEnabled || !network.bNetworkConnectable) continue;
const ssid = bytesToSsid(network.dot11Ssid);
candidates.push({
ssid, signalPercent: network.wlanSignalQuality, connected: connectedSsid === ssid, open: true,
nativeKey: { interfaceGuid: item.InterfaceGuid, ssid: network.dot11Ssid } satisfies WindowsNativeKey,
});
}
} finally { WlanFreeMemory(output[0]); }
}
return candidates;
});
},
async connect(candidate, signal) {
throwIfAborted(signal);
const key = candidate.nativeKey as WindowsNativeKey;
await withHandle('connect', async (handle) => {
const match = interfaces(handle, 'connect').find((item) => guidKey(item.InterfaceGuid as Record<string, unknown>) === guidKey(key.interfaceGuid));
if (!match) throw new RobotHotspotAdapterError('connect_failed');
const parameters = {
wlanConnectionMode: WLAN_CONNECTION_MODE_DISCOVERY_UNSECURE,
strProfile: null,
pDot11Ssid: key.ssid,
pDesiredBssidList: null,
dot11BssType: DOT11_BSS_TYPE_INFRASTRUCTURE,
dwFlags: 0,
};
const result = WlanConnect(handle, match.InterfaceGuid, parameters, null) as number;
if (result !== ERROR_SUCCESS) nativeFailure(result, 'connect');
connectingInterfaceGuid = match.InterfaceGuid as Record<string, unknown>;
});
},
async currentSsid(signal) {
throwIfAborted(signal);
return withHandle('connect', async (handle) => {
const foundInterfaces = interfaces(handle, 'connect');
const orderedInterfaces = connectingInterfaceGuid
? [...foundInterfaces].sort((left, right) => Number(guidKey(right.InterfaceGuid as Record<string, unknown>) === guidKey(connectingInterfaceGuid as Record<string, unknown>)) - Number(guidKey(left.InterfaceGuid as Record<string, unknown>) === guidKey(connectingInterfaceGuid as Record<string, unknown>)))
: foundInterfaces;
for (const item of orderedInterfaces) {
const ssid = currentOnInterface(handle, item.InterfaceGuid as Record<string, unknown>, 'connect');
if (ssid) return ssid;
}
return null;
});
},
clear() { connectingInterfaceGuid = null; },
};
}