arlo-cameras
Version:
A node.js library for Arlo Cameras and base station
521 lines (443 loc) • 12.6 kB
JavaScript
/**
* Import libraries
*/
const DebugModule = require('debug');
const Imap = require('imap');
const Mailparser = require('mailparser');
// eslint-disable-next-line import/no-unresolved
const setTimeout = require('timers/promises');
const { simpleParser } = Mailparser;
const debug = new DebugModule('Arlo:mfa');
const URL_MFA_BASE_MFA = 'https://ocapi-app.arlo.com/api';
const URL_MFA_BASE_MOBILE = `${URL_MFA_BASE_MFA}/v2`;
const AUTH_URLS_MOBILE = {
VALID_TOKEN: `${URL_MFA_BASE_MOBILE}/ocAccessTokenValidate_PHP_MFA`,
GET_AUTH_TOKEN: `${URL_MFA_BASE_MOBILE}/ocAuth_PHP_MFA`,
GET_FACTORS: `${URL_MFA_BASE_MOBILE}/ocGetFactors_PHP_MFA`,
REQUEST_MFA_CODE: `${URL_MFA_BASE_MOBILE}/ocStart2FAauth_PHP_MFA`,
};
const AUTH_URLS_MFA = {
GET_AUTH_TOKEN: `${URL_MFA_BASE_MFA}/auth`,
GET_FACTORS: `${URL_MFA_BASE_MFA}/getFactors?data=`,
REQUEST_MFA_CODE: `${URL_MFA_BASE_MFA}/startAuth`,
SUBMIT_MFACODE: `${URL_MFA_BASE_MFA}/finishAuth`,
VERIFY_AUTH: `${URL_MFA_BASE_MFA}/validateAccessToken?data=`,
START_NEW_SESSION: `https://myapi.arlo.com/hmsweb/users/session/v2`,
};
const auth = {};
// Validate authorization token
async function _validateToken() {
try {
const url = this.config.mfa
? AUTH_URLS_MFA.VALID_TOKEN
: AUTH_URLS_MOBILE.VALID_TOKEN;
const response = await this._get(url);
if (response instanceof Error || typeof response === 'undefined') {
return false;
}
if (response.meta.code !== 200) {
debug(response.meta.message);
return false;
}
if (!response.data.tokenValidated) {
debug('Token not validated');
return false;
}
return true;
} catch (err) {
debug(err.message);
return err;
}
}
// Get authorization token
async function _getAuthToken() {
try {
debug('Get auth token');
const url = this.config.mfa
? AUTH_URLS_MFA.GET_AUTH_TOKEN
: AUTH_URLS_MOBILE.GET_AUTH_TOKEN;
const postBody = {
email: this.config.arloUser,
password: this.config.arloPassword,
language: 'en',
EnvSource: 'prod',
};
const response = await this._post(url, postBody);
if (response instanceof Error || typeof response === 'undefined') {
return false;
}
if (response.meta.code !== 200) {
debug(response.meta.message);
return false;
}
if (!response.data.mfa) {
debug('Account is not MFA enabled');
return false;
}
const { token } = response.data;
const buff = Buffer.from(token);
const tokenBase64 = buff.toString('base64');
this.headers.authorization = tokenBase64;
auth.authenticated = response.data.authenticated;
auth.userID = response.data.userId;
this.headers.accessToken = response.data.token;
return true;
} catch (err) {
debug(err.message);
return false;
}
}
// Get mfa factors
async function _getFactors() {
try {
debug('Get factors');
const url = this.config.mfa
? AUTH_URLS_MFA.GET_FACTORS + auth.authenticated
: AUTH_URLS_MOBILE.GET_FACTORS;
const response = await this._get(url);
if (response instanceof Error || typeof response === 'undefined') {
debug(response);
return false;
}
if (response.meta.code !== 200) {
debug(response.meta.message);
return false;
}
const mfaType = this.config.mfa ? 'EMAIL' : 'PUSH';
debug(`Filter factors to get ${mfaType}`);
const factor = response.data.items.filter(
(item) => item.factorType === mfaType
);
if (factor.length < 1) {
debug('No mfa found');
return false;
}
debug(`Found ${factor[0].factorType} factor`);
auth.factorID = factor[0].factorId;
auth.applicationID = factor[0].applicationId;
return true;
} catch (err) {
debug(err.message);
return err;
}
}
// Request MFA code
async function _requestMFACode() {
try {
debug('Request MFA code');
const url = this.config.mfa
? AUTH_URLS_MFA.REQUEST_MFA_CODE
: `${AUTH_URLS_MOBILE.REQUEST_MFA_CODE}?applicationId=${auth.applicationID}`;
const postBody = {
factorId: auth.factorID,
factorType: '',
userId: auth.userID,
};
if (!this.config.mfa) postBody.mobilePayload = this.mobilePayload;
const response = await this._post(url, postBody);
if (response instanceof Error || typeof response === 'undefined') {
debug(response);
return false;
}
if (!Object.keys(response).length) {
debug('Reqiest MFA Code returned empty');
return false;
}
if (response.meta.code !== 200) {
debug(response.meta.message);
return false;
}
if (this.config.mfa) {
auth.factorAuthCode = response.data.factorAuthCode;
} else {
this.token = response.data.accessToken.token;
this.tokenExpires = response.data.accessToken.expiredIn;
this.headers.accessToken = this.token;
}
return true;
} catch (err) {
debug(err.message);
return err;
}
}
// Delete Arlo MFA email
function _deleteEmail(mailServer, uids) {
return new Promise((resolve, reject) => {
debug('Delete arlo MFA email');
uids.forEach((uid) => {
mailServer.addFlags(uid, 'Deleted', (err) => {
if (err) reject(err);
});
});
mailServer.expunge((err) => {
if (err) {
reject(err);
return;
}
resolve(true);
});
});
}
function _getMFACode(message) {
return new Promise((resolve, reject) => {
try {
debug('Find MFA code in html');
const searchArray = message.match(/\d{6}/);
if (searchArray.length === 0) throw Error('No MFA code found in email');
[auth.MFACode] = searchArray;
debug(`Found MFA code: ${auth.MFACode}`);
resolve(true);
} catch (err) {
debug(err);
reject(err);
}
});
}
function _fetchEmail(mailServer, uid) {
return new Promise((resolve, reject) => {
const fetchOptions = {
bodies: ['TEXT'],
markSeen: true,
struct: true,
};
const fetch = mailServer.fetch(uid, fetchOptions);
function fetchOnMessage(message) {
message.on('body', async (stream) => {
debug('Convert email to html');
try {
const email = await simpleParser(stream);
resolve(email.textAsHtml);
} catch (err) {
reject(err);
}
});
}
function fetchOnError(err) {
debug(err.message);
reject(err);
}
function removeListeners() {
fetch.removeListener('message', fetchOnMessage);
fetch.removeListener('error', fetchOnError);
}
fetch.on('message', fetchOnMessage);
fetch.once('error', fetchOnError);
fetch.once('end', removeListeners);
});
}
function _searchInbox(mailServer) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
const searchCriteria = [
['SUBJECT', 'Your one-time authentication code from Arlo'],
];
debug('Search inbox for MFA email');
// eslint-disable-next-line no-await-in-loop
mailServer.search(searchCriteria, (err, uids) => {
if (err) {
reject(err);
return;
}
// Reject if no email found
if (!uids || !uids.length) {
debug('No email with Arlo MFA found');
resolve([]);
return;
}
// Found email, mark as read
mailServer.setFlags(uids, ['\\Seen'], (setErr) => {
if (setErr) debug(setErr);
else debug('Marked message as read');
});
resolve(uids);
});
});
}
function _searchForEmail(mailServer, retry = 1) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
let uids;
for (let i = 1; i <= retry; i += 1) {
try {
// eslint-disable-next-line no-await-in-loop
uids = await _searchInbox(mailServer);
if (uids.length === 0) {
debug('Waiting before retry search');
// eslint-disable-next-line no-await-in-loop
await setTimeout(5000);
} else {
break;
}
} catch (err) {
debug(err);
reject(err);
}
}
if (uids.length === 0) {
reject(new Error('Timed out searching for Arlo MFA email'));
return;
}
resolve(uids);
});
}
async function _openInbox(mailServer) {
return new Promise((resolve) => {
mailServer.openBox('INBOX', false, (err) => {
if (err) {
debug(err.message);
resolve(false);
}
resolve(true);
});
});
}
async function _getMFACodeFromEmail() {
const emailServerConfig = {
// IMAP connection config
user: this.config.emailUser,
password: this.config.emailPassword,
host: this.config.emailServer,
port: 993,
tls: true,
tlsOptions: {
rejectUnauthorized: false,
},
};
debug('Connect to imap server');
return new Promise((resolve) => {
try {
const mailServer = new Imap(emailServerConfig);
mailServer.once('error', (err) => {
debug(err);
resolve(false);
});
mailServer.once('end', () => {
debug('Connection to imap server ended');
resolve(false);
});
mailServer.once('ready', async () => {
debug('Connected to imap server');
debug('Open inbox');
const proceed = await _openInbox(mailServer);
if (!proceed) {
debug('Unable to open inbox');
resolve(false);
return;
}
try {
await _requestMFACode.call(this);
} catch (err) {
debug(err);
resolve(false);
return;
}
let uids;
let email;
let sucess = false;
try {
// Find MFA email
uids = await _searchForEmail(mailServer, 4);
// Get MFA email as html
email = await _fetchEmail(mailServer, uids);
// Extract code from MFA email
await _getMFACode(email);
// Delete MFA email
await _deleteEmail(mailServer, uids);
sucess = true;
} catch (err) {
debug(err.message);
sucess = false;
} finally {
// mailServer.end();
resolve(sucess);
}
});
mailServer.connect();
} catch (err) {
debug(err.message);
resolve(false);
}
});
}
// Submit MFA token
async function _submitMFACode() {
debug('Submit MFA code');
try {
const url = AUTH_URLS_MFA.SUBMIT_MFACODE;
const postBody = {
factorAuthCode: auth.factorAuthCode,
isBrowserTrusted: true,
otp: auth.MFACode,
};
const response = await this._post(url, postBody);
if (response instanceof Error || typeof response === 'undefined') {
debug(response);
return false;
}
if (response.meta.code !== 200) {
debug(response.meta.message);
return false;
}
auth.token = response.data.token;
const buff = Buffer.from(auth.token);
const tokenBase64 = buff.toString('base64');
this.headers.authorization = tokenBase64;
auth.tokenExpires = response.data.expiresIn;
return true;
} catch (err) {
debug('Error in response object');
debug(err);
return false;
}
}
// Verifiy authorization token
async function _verifyAuthToken() {
debug('Verifiy authorization token');
try {
const url = AUTH_URLS_MFA.VERIFY_AUTH + auth.authenticated;
const response = await this._get(url);
if (response instanceof Error || typeof response === 'undefined') {
debug(response);
return false;
}
this.userProfile = response.data;
return true;
} catch (err) {
debug(err.message);
return false;
}
}
// New session
async function _newSession() {
debug('Start new session');
try {
// Set headers
this.headers.accept = 'application/json';
this.headers.authorization = auth.token;
// this.headers.Host = API_DOMAIN;
const url = AUTH_URLS_MFA.START_NEW_SESSION;
const response = await this._get(url);
if (response instanceof Error || typeof response === 'undefined') {
debug(response);
return false;
}
this.token = response.data.token;
this.userId = response.data.userId;
this.serialNumber = response.data.serialNumber;
this.sessionExpires = auth.tokenExpires;
return true;
} catch (err) {
debug(err.message);
return false;
}
}
module.exports = {
_validateToken,
_getAuthToken,
_getFactors,
_getMFACodeFromEmail,
_submitMFACode,
_requestMFACode,
_verifyAuthToken,
_newSession,
};