mirror of
https://github.com/cheveguerra/whatsapp-web.js.git
synced 2026-04-17 19:26:20 +00:00
feat: RemoteAuth Strategy (#1450)
* index.js * [authReady] - new BaseAuthStrategy function * [RemoteAuth] - new Class RemoteAuth added * Eslint Fixes * Eslint Fixes * Added types for RemoteAuth (mostly assumed types from PR message and src/authStrategies/RemoteAuth.js) * [dependency updates] - added unzipper & archiver * [Types] - Fixing typescript declarations * Renaming Base Class Hook * auth hook rename on client * [Error Handling] - Delegate responsability to end users * [Refactor] - deletemetadata code refactor * [Refactor] - backupSyncIntervalMs renamed * [Refactor] - Minor improvement on deleteMetadata * [Refactor] - backupSyncIntervalMs rename on index.d.ts * [Update] - Fix for Ubuntu crahsing on extractSession * [Update] - Delegate responsability to stores of making sure the previous session is deleted strictly only after the new one is saved * [Update] - Improve file paths handling & naming (reduce assumptions between RemoteAuth and stores) * [Update] - Adding new event <REMOTE_SESSION_SAVED> on Constants.js * [Update] - Adding new authHooks <destroy> & <disconnect> * [Update] - Adding <destroy> & <disconnect> hooks on Client.js * [Update] - Adding new features to index.d.ts * [RemoteAuth] - New Features added to RemoteAuth Class * [dependency updates] - added fs-extra * [Cross Platform] - Windows is now compatible with RemoteAuth * [optionalDependencies] - moved archiver, fs-extra & unzipper to optional dependencies on package.json * [optionalDependencies] - adding validation for when optional dependencies are missing * [Update] - Node Deprecation warining for rmdir changed for rm instead Co-authored-by: h110m <nichtwitzig228@gmail.com>
This commit is contained in:
31
index.d.ts
vendored
31
index.d.ts
vendored
@@ -265,6 +265,9 @@ declare namespace WAWebJS {
|
|||||||
|
|
||||||
/** Emitted when the client has initialized and is ready to receive messages */
|
/** Emitted when the client has initialized and is ready to receive messages */
|
||||||
on(event: 'ready', listener: () => void): this
|
on(event: 'ready', listener: () => void): this
|
||||||
|
|
||||||
|
/** Emitted when the RemoteAuth session is saved successfully on the external Database */
|
||||||
|
on(event: 'remote_session_saved', listener: () => void): this
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Current connection information */
|
/** Current connection information */
|
||||||
@@ -354,6 +357,9 @@ declare namespace WAWebJS {
|
|||||||
failureEventPayload?: any
|
failureEventPayload?: any
|
||||||
}>;
|
}>;
|
||||||
getAuthEventPayload: () => Promise<any>;
|
getAuthEventPayload: () => Promise<any>;
|
||||||
|
afterAuthReady: () => Promise<void>;
|
||||||
|
disconnect: () => Promise<void>;
|
||||||
|
destroy: () => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,6 +381,30 @@ declare namespace WAWebJS {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote-based authentication
|
||||||
|
*/
|
||||||
|
export class RemoteAuth extends AuthStrategy {
|
||||||
|
public clientId?: string;
|
||||||
|
public dataPath?: string;
|
||||||
|
constructor(options?: {
|
||||||
|
store: Store,
|
||||||
|
clientId?: string,
|
||||||
|
dataPath?: string,
|
||||||
|
backupSyncIntervalMs: number
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote store interface
|
||||||
|
*/
|
||||||
|
export interface Store {
|
||||||
|
sessionExists: ({session: string}) => Promise<boolean> | boolean,
|
||||||
|
delete: ({session: string}) => Promise<any> | any,
|
||||||
|
save: ({session: string}) => Promise<any> | any,
|
||||||
|
extract: ({session: string, path: string}) => Promise<any> | any,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy session auth strategy
|
* Legacy session auth strategy
|
||||||
* Not compatible with multi-device accounts.
|
* Not compatible with multi-device accounts.
|
||||||
@@ -476,6 +506,7 @@ declare namespace WAWebJS {
|
|||||||
DISCONNECTED = 'disconnected',
|
DISCONNECTED = 'disconnected',
|
||||||
STATE_CHANGED = 'change_state',
|
STATE_CHANGED = 'change_state',
|
||||||
BATTERY_CHANGED = 'change_battery',
|
BATTERY_CHANGED = 'change_battery',
|
||||||
|
REMOTE_SESSION_SAVED = 'remote_session_saved'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Group notification types */
|
/** Group notification types */
|
||||||
|
|||||||
1
index.js
1
index.js
@@ -25,6 +25,7 @@ module.exports = {
|
|||||||
// Auth Strategies
|
// Auth Strategies
|
||||||
NoAuth: require('./src/authStrategies/NoAuth'),
|
NoAuth: require('./src/authStrategies/NoAuth'),
|
||||||
LocalAuth: require('./src/authStrategies/LocalAuth'),
|
LocalAuth: require('./src/authStrategies/LocalAuth'),
|
||||||
|
RemoteAuth: require('./src/authStrategies/RemoteAuth'),
|
||||||
LegacySessionAuth: require('./src/authStrategies/LegacySessionAuth'),
|
LegacySessionAuth: require('./src/authStrategies/LegacySessionAuth'),
|
||||||
|
|
||||||
...Constants
|
...Constants
|
||||||
|
|||||||
@@ -51,5 +51,10 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"archiver": "^5.3.1",
|
||||||
|
"fs-extra": "^10.1.0",
|
||||||
|
"unzipper": "^0.10.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ class Client extends EventEmitter {
|
|||||||
this.emit(Events.MEDIA_UPLOADED, message);
|
this.emit(Events.MEDIA_UPLOADED, message);
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.exposeFunction('onAppStateChangedEvent', (state) => {
|
await page.exposeFunction('onAppStateChangedEvent', async (state) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the connection state changes
|
* Emitted when the connection state changes
|
||||||
@@ -446,6 +446,7 @@ class Client extends EventEmitter {
|
|||||||
* @event Client#disconnected
|
* @event Client#disconnected
|
||||||
* @param {WAState|"NAVIGATION"} reason reason that caused the disconnect
|
* @param {WAState|"NAVIGATION"} reason reason that caused the disconnect
|
||||||
*/
|
*/
|
||||||
|
await this.authStrategy.disconnect();
|
||||||
this.emit(Events.DISCONNECTED, state);
|
this.emit(Events.DISCONNECTED, state);
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}
|
}
|
||||||
@@ -548,11 +549,13 @@ class Client extends EventEmitter {
|
|||||||
* @event Client#ready
|
* @event Client#ready
|
||||||
*/
|
*/
|
||||||
this.emit(Events.READY);
|
this.emit(Events.READY);
|
||||||
|
this.authStrategy.afterAuthReady();
|
||||||
|
|
||||||
// Disconnect when navigating away when in PAIRING state (detect logout)
|
// Disconnect when navigating away when in PAIRING state (detect logout)
|
||||||
this.pupPage.on('framenavigated', async () => {
|
this.pupPage.on('framenavigated', async () => {
|
||||||
const appState = await this.getState();
|
const appState = await this.getState();
|
||||||
if(!appState || appState === WAState.PAIRING) {
|
if(!appState || appState === WAState.PAIRING) {
|
||||||
|
await this.authStrategy.disconnect();
|
||||||
this.emit(Events.DISCONNECTED, 'NAVIGATION');
|
this.emit(Events.DISCONNECTED, 'NAVIGATION');
|
||||||
await this.destroy();
|
await this.destroy();
|
||||||
}
|
}
|
||||||
@@ -564,6 +567,7 @@ class Client extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
async destroy() {
|
async destroy() {
|
||||||
await this.pupBrowser.close();
|
await this.pupBrowser.close();
|
||||||
|
await this.authStrategy.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ class BaseAuthStrategy {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
async getAuthEventPayload() {}
|
async getAuthEventPayload() {}
|
||||||
|
async afterAuthReady() {}
|
||||||
|
async disconnect() {}
|
||||||
|
async destroy() {}
|
||||||
async logout() {}
|
async logout() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
201
src/authStrategies/RemoteAuth.js
Normal file
201
src/authStrategies/RemoteAuth.js
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* Require Optional Dependencies */
|
||||||
|
try {
|
||||||
|
var fs = require('fs-extra');
|
||||||
|
var unzipper = require('unzipper');
|
||||||
|
var archiver = require('archiver');
|
||||||
|
} catch {
|
||||||
|
throw new Error('Optional Dependencies [fs-extra, unzipper, archiver] are required to use RemoteAuth. Make sure to run npm install correctly and remove the --no-optional flag');
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const { Events } = require('./../util/Constants');
|
||||||
|
const BaseAuthStrategy = require('./BaseAuthStrategy');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote-based authentication
|
||||||
|
* @param {object} options - options
|
||||||
|
* @param {object} options.store - Remote database store instance
|
||||||
|
* @param {string} options.clientId - Client id to distinguish instances if you are using multiple, otherwise keep null if you are using only one instance
|
||||||
|
* @param {string} options.dataPath - Change the default path for saving session files, default is: "./.wwebjs_auth/"
|
||||||
|
* @param {number} options.backupSyncIntervalMs - Sets the time interval for periodic session backups. Accepts values starting from 60000ms {1 minute}
|
||||||
|
*/
|
||||||
|
class RemoteAuth extends BaseAuthStrategy {
|
||||||
|
constructor({ clientId, dataPath, store, backupSyncIntervalMs } = {}) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const idRegex = /^[-_\w]+$/i;
|
||||||
|
if (clientId && !idRegex.test(clientId)) {
|
||||||
|
throw new Error('Invalid clientId. Only alphanumeric characters, underscores and hyphens are allowed.');
|
||||||
|
}
|
||||||
|
if (!backupSyncIntervalMs || backupSyncIntervalMs < 60000) {
|
||||||
|
throw new Error('Invalid backupSyncIntervalMs. Accepts values starting from 60000ms {1 minute}.');
|
||||||
|
}
|
||||||
|
if(!store) throw new Error('Remote database store is required.');
|
||||||
|
|
||||||
|
this.store = store;
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.backupSyncIntervalMs = backupSyncIntervalMs;
|
||||||
|
this.dataPath = path.resolve(dataPath || './.wwebjs_auth/');
|
||||||
|
this.tempDir = `${this.dataPath}/wwebjs_temp_session`;
|
||||||
|
this.requiredDirs = ['Default', 'IndexedDB', 'Local Storage']; /* => Required Files & Dirs in WWebJS to restore session */
|
||||||
|
}
|
||||||
|
|
||||||
|
async beforeBrowserInitialized() {
|
||||||
|
const puppeteerOpts = this.client.options.puppeteer;
|
||||||
|
const sessionDirName = this.clientId ? `RemoteAuth-${this.clientId}` : 'RemoteAuth';
|
||||||
|
const dirPath = path.join(this.dataPath, sessionDirName);
|
||||||
|
|
||||||
|
if (puppeteerOpts.userDataDir && puppeteerOpts.userDataDir !== dirPath) {
|
||||||
|
throw new Error('RemoteAuth is not compatible with a user-supplied userDataDir.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.userDataDir = dirPath;
|
||||||
|
this.sessionName = sessionDirName;
|
||||||
|
|
||||||
|
await this.extractRemoteSession();
|
||||||
|
|
||||||
|
this.client.options.puppeteer = {
|
||||||
|
...puppeteerOpts,
|
||||||
|
userDataDir: dirPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
await this.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy() {
|
||||||
|
clearInterval(this.backupSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
await this.deleteRemoteSession();
|
||||||
|
|
||||||
|
let pathExists = await this.isValidPath(this.userDataDir);
|
||||||
|
if (pathExists) {
|
||||||
|
await fs.promises.rm(this.userDataDir, {
|
||||||
|
recursive: true,
|
||||||
|
force: true
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
clearInterval(this.backupSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterAuthReady() {
|
||||||
|
const sessionExists = await this.store.sessionExists({session: this.sessionName});
|
||||||
|
if(!sessionExists) {
|
||||||
|
await this.delay(60000); /* Initial delay sync required for session to be stable enough to recover */
|
||||||
|
await this.storeRemoteSession({emit: true});
|
||||||
|
}
|
||||||
|
var self = this;
|
||||||
|
this.backupSync = setInterval(async function () {
|
||||||
|
await self.storeRemoteSession();
|
||||||
|
}, this.backupSyncIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeRemoteSession(options) {
|
||||||
|
/* Compress & Store Session */
|
||||||
|
const pathExists = await this.isValidPath(this.userDataDir);
|
||||||
|
if (pathExists) {
|
||||||
|
await this.compressSession();
|
||||||
|
await this.store.save({session: this.sessionName});
|
||||||
|
await fs.promises.unlink(`${this.sessionName}.zip`);
|
||||||
|
await fs.promises.rm(`${this.tempDir}`, {
|
||||||
|
recursive: true,
|
||||||
|
force: true
|
||||||
|
}).catch(() => {});
|
||||||
|
if(options && options.emit) this.client.emit(Events.REMOTE_SESSION_SAVED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async extractRemoteSession() {
|
||||||
|
const pathExists = await this.isValidPath(this.userDataDir);
|
||||||
|
const compressedSessionPath = `${this.sessionName}.zip`;
|
||||||
|
const sessionExists = await this.store.sessionExists({session: this.sessionName});
|
||||||
|
if (pathExists) {
|
||||||
|
await fs.promises.rm(this.userDataDir, {
|
||||||
|
recursive: true,
|
||||||
|
force: true
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
if (sessionExists) {
|
||||||
|
await this.store.extract({session: this.sessionName, path: compressedSessionPath});
|
||||||
|
await this.unCompressSession(compressedSessionPath);
|
||||||
|
} else {
|
||||||
|
fs.mkdirSync(this.userDataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRemoteSession() {
|
||||||
|
const sessionExists = await this.store.sessionExists({session: this.sessionName});
|
||||||
|
if (sessionExists) await this.store.delete({session: this.sessionName});
|
||||||
|
}
|
||||||
|
|
||||||
|
async compressSession() {
|
||||||
|
const archive = archiver('zip');
|
||||||
|
const stream = fs.createWriteStream(`${this.sessionName}.zip`);
|
||||||
|
|
||||||
|
await fs.copy(this.userDataDir, this.tempDir).catch(() => {});
|
||||||
|
await this.deleteMetadata();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
archive
|
||||||
|
.directory(this.tempDir, false)
|
||||||
|
.on('error', err => reject(err))
|
||||||
|
.pipe(stream);
|
||||||
|
|
||||||
|
stream.on('close', () => resolve());
|
||||||
|
archive.finalize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async unCompressSession(compressedSessionPath) {
|
||||||
|
var stream = fs.createReadStream(compressedSessionPath);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
stream.pipe(unzipper.Extract({
|
||||||
|
path: this.userDataDir
|
||||||
|
}))
|
||||||
|
.on('error', err => reject(err))
|
||||||
|
.on('finish', () => resolve());
|
||||||
|
});
|
||||||
|
await fs.promises.unlink(compressedSessionPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMetadata() {
|
||||||
|
const sessionDirs = [this.tempDir, path.join(this.tempDir, 'Default')];
|
||||||
|
for (const dir of sessionDirs) {
|
||||||
|
const sessionFiles = await fs.promises.readdir(dir);
|
||||||
|
for (const element of sessionFiles) {
|
||||||
|
if (!this.requiredDirs.includes(element)) {
|
||||||
|
const dirElement = path.join(dir, element);
|
||||||
|
const stats = await fs.promises.lstat(dirElement);
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
await fs.promises.rm(dirElement, {
|
||||||
|
recursive: true,
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await fs.promises.unlink(dirElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isValidPath(path) {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(path);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delay(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RemoteAuth;
|
||||||
@@ -51,7 +51,8 @@ exports.Events = {
|
|||||||
DISCONNECTED: 'disconnected',
|
DISCONNECTED: 'disconnected',
|
||||||
STATE_CHANGED: 'change_state',
|
STATE_CHANGED: 'change_state',
|
||||||
BATTERY_CHANGED: 'change_battery',
|
BATTERY_CHANGED: 'change_battery',
|
||||||
INCOMING_CALL: 'incoming_call'
|
INCOMING_CALL: 'incoming_call',
|
||||||
|
REMOTE_SESSION_SAVED: 'remote_session_saved'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user