176 строки
5.2 KiB
JavaScript
176 строки
5.2 KiB
JavaScript
"use strict";
|
|
|
|
const got = require("got");
|
|
|
|
const AppConstants = require("./app-constants");
|
|
const { FluentError } = require("./locale-utils");
|
|
const mozlog = require("./log");
|
|
const pkg = require("./package.json");
|
|
|
|
|
|
const HIBP_USER_AGENT = `${pkg.name}/${pkg.version}`;
|
|
const log = mozlog("hibp");
|
|
|
|
|
|
|
|
const HIBP = {
|
|
_addStandardOptions (options = {}) {
|
|
const hibpOptions = {
|
|
headers: {
|
|
"User-Agent": HIBP_USER_AGENT,
|
|
},
|
|
json: true,
|
|
};
|
|
return Object.assign(options, hibpOptions);
|
|
},
|
|
|
|
async _throttledGot (url, reqOptions, tryCount = 1) {
|
|
let response;
|
|
try {
|
|
response = await got(url, reqOptions);
|
|
return response;
|
|
} catch (err) {
|
|
log.error("_throttledGot", {err: err});
|
|
if (err.statusCode === 404) {
|
|
// 404 can mean "no results", return undefined response; sorry calling code
|
|
return response;
|
|
} else if (err.statusCode === 429) {
|
|
log.info("_throttledGot", {err: "got a 429, tryCount: " + tryCount});
|
|
if (tryCount >= AppConstants.HIBP_THROTTLE_MAX_TRIES) {
|
|
log.error("_throttledGot", {err: err});
|
|
throw new FluentError("error-hibp-throttled");
|
|
} else {
|
|
tryCount++;
|
|
await new Promise(resolve => setTimeout(resolve, AppConstants.HIBP_THROTTLE_DELAY * tryCount));
|
|
return await this._throttledGot(url, reqOptions, tryCount);
|
|
}
|
|
} else {
|
|
throw new FluentError("error-hibp-connect");
|
|
}
|
|
}
|
|
},
|
|
|
|
async req(path, options = {}) {
|
|
const url = `${AppConstants.HIBP_API_ROOT}${path}?code=${encodeURIComponent(AppConstants.HIBP_API_TOKEN)}`;
|
|
const reqOptions = this._addStandardOptions(options);
|
|
return await this._throttledGot(url, reqOptions);
|
|
},
|
|
|
|
async kAnonReq(path, options = {}) {
|
|
// Construct HIBP url and standard headers
|
|
const url = `${AppConstants.HIBP_KANON_API_ROOT}${path}?code=${encodeURIComponent(AppConstants.HIBP_KANON_API_TOKEN)}`;
|
|
const reqOptions = this._addStandardOptions(options);
|
|
return await this._throttledGot(url, reqOptions);
|
|
},
|
|
|
|
matchFluentID(dataCategory) {
|
|
return dataCategory.toLowerCase()
|
|
.replace(/[^-a-z0-9]/g, "-")
|
|
.replace(/-{2,}/g, "-")
|
|
.replace(/(^-|-$)/g, "");
|
|
},
|
|
|
|
formatDataClassesArray(dataCategories) {
|
|
const formattedArray = [];
|
|
dataCategories.forEach(category => {
|
|
formattedArray.push(this.matchFluentID(category));
|
|
});
|
|
return formattedArray;
|
|
},
|
|
|
|
async loadBreachesIntoApp(app) {
|
|
log.info("loadBreachesIntoApp");
|
|
try {
|
|
const breachesResponse = await this.req("/breaches");
|
|
const breaches = [];
|
|
|
|
for (const breach of breachesResponse.body) {
|
|
// const breach = breachesResponse.body[breachIndex];
|
|
// convert data class strings to Fluent IDs
|
|
breach.DataClasses = this.formatDataClassesArray(breach.DataClasses);
|
|
breaches.push(breach);
|
|
}
|
|
app.locals.breaches = breaches;
|
|
app.locals.breachesLoadedDateTime = Date.now();
|
|
app.locals.mostRecentBreachDateTime = this.getLatestBreachDateTime(breaches);
|
|
} catch (error) {
|
|
throw new FluentError("error-hibp-load-breaches");
|
|
}
|
|
log.info("done-loading-breaches");
|
|
},
|
|
|
|
async getUnsafeBreachesForEmail(sha1, allBreaches) {
|
|
const allFoundBreaches = await this.getBreachesForEmail(sha1, allBreaches, true);
|
|
return allFoundBreaches.filter(
|
|
breach => !breach.IsSpamList
|
|
);
|
|
},
|
|
|
|
async getBreachesForEmail(sha1, allBreaches, includeUnsafe = false) {
|
|
let foundBreaches = [];
|
|
const sha1Prefix = sha1.slice(0, 6).toUpperCase();
|
|
const path = `/breachedaccount/range/${sha1Prefix}`;
|
|
|
|
const response = await this.kAnonReq(path);
|
|
if (!response) {
|
|
return [];
|
|
}
|
|
// Parse response body, format:
|
|
// [
|
|
// {"hashSuffix":<suffix>,"websites":[<breach1Name>,...]},
|
|
// {"hashSuffix":<suffix>,"websites":[<breach1Name>,...]},
|
|
// ]
|
|
for (const breachedAccount of response.body) {
|
|
if (sha1.toUpperCase() === sha1Prefix + breachedAccount.hashSuffix) {
|
|
foundBreaches = allBreaches.filter(breach => breachedAccount.websites.includes(breach.Name));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (includeUnsafe) {
|
|
return foundBreaches;
|
|
}
|
|
return this.filterOutUnsafeBreaches(foundBreaches);
|
|
},
|
|
|
|
getBreachByName(allBreaches, breachName) {
|
|
return allBreaches.find(breach => breach.Name.toLowerCase() === breachName.toLowerCase());
|
|
},
|
|
|
|
|
|
filterOutUnsafeBreaches(breaches) {
|
|
return breaches.filter(
|
|
breach => breach.IsVerified &&
|
|
!breach.IsRetired &&
|
|
!breach.IsSensitive &&
|
|
!breach.IsSpamList
|
|
);
|
|
},
|
|
|
|
|
|
getLatestBreachDateTime(breaches) {
|
|
let latestBreachDateTime = new Date(0);
|
|
for (const breach of breaches) {
|
|
const breachAddedDate = new Date(breach.AddedDate);
|
|
if (breachAddedDate > latestBreachDateTime) {
|
|
latestBreachDateTime = breachAddedDate;
|
|
}
|
|
}
|
|
return latestBreachDateTime;
|
|
},
|
|
|
|
|
|
async subscribeHash(sha1) {
|
|
const sha1Prefix = sha1.slice(0, 6).toUpperCase();
|
|
const path = "/range/subscribe";
|
|
const options = {
|
|
method: "POST",
|
|
body: {hashPrefix: sha1Prefix},
|
|
};
|
|
|
|
return await this.kAnonReq(path, options);
|
|
},
|
|
};
|
|
|
|
module.exports = HIBP;
|