Modularize code.
This commit is contained in:
Родитель
a0344e18b4
Коммит
5b78112be4
16
.env-dist
16
.env-dist
|
@ -1,12 +1,16 @@
|
|||
SERVER_URL=http://localhost:6060
|
||||
PORT=6060
|
||||
|
||||
COOKIE_SECRET=3895d33b5f9730f5eb2a2067fe0a690e298f55f5e382c032fd3656863412
|
||||
|
||||
DEBUG_DUMMY_SMTP=1
|
||||
SMTP_HOST=""
|
||||
SMTP_PORT=
|
||||
SMTP_USERNAME=""
|
||||
SMTP_PASSWORD=""
|
||||
|
||||
OAUTH_CLIENT_ID=cb1bef9d06bb9bc9
|
||||
OAUTH_CLIENT_SECRET=f5fb99de6e0af18ab17e013ac1d439903179a97a1c510fc10bc3bd50bbce089b
|
||||
OAUTH_AUTHORIZATION_URI="https://oauth-stable.dev.lcip.org/v1/authorization"
|
||||
OAUTH_PROFILE_URI="https://stable.dev.lcip.org/profile/v1/profile"
|
||||
OAUTH_TOKEN_URI="https://oauth-stable.dev.lcip.org/v1/token"
|
||||
SERVER_URL=http://localhost:6060
|
||||
COOKIE_SECRET=3895d33b5f9730f5eb2a2067fe0a690e298f55f5e382c032fd3656863412
|
||||
SMTP_HOST=""
|
||||
SMTP_PORT=
|
||||
SMTP_USERNAME=""
|
||||
SMTP_PASSWORD=""
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
"use strict";
|
||||
|
||||
require("dotenv").load();
|
||||
|
||||
const kEnvironmentVariables = [
|
||||
"SERVER_URL",
|
||||
"PORT",
|
||||
"COOKIE_SECRET",
|
||||
"SMTP_HOST",
|
||||
"SMTP_PORT",
|
||||
"SMTP_USERNAME",
|
||||
"SMTP_PASSWORD",
|
||||
"OAUTH_AUTHORIZATION_URI",
|
||||
"OAUTH_TOKEN_URI",
|
||||
"OAUTH_PROFILE_URI",
|
||||
"OAUTH_CLIENT_ID",
|
||||
"OAUTH_CLIENT_SECRET",
|
||||
];
|
||||
|
||||
const AppConstants = {
|
||||
init() {
|
||||
for (let v of kEnvironmentVariables) {
|
||||
if (process.env[v] === undefined) {
|
||||
throw new Error(`Required environment variable was not set: ${v}`);
|
||||
}
|
||||
this[v] = process.env[v];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AppConstants;
|
|
@ -0,0 +1,81 @@
|
|||
"use strict";
|
||||
|
||||
const AppConstants = require("./app-constants");
|
||||
|
||||
const nodemailer = require("nodemailer");
|
||||
|
||||
// This is set later when reading SMTP credentials from the environment.
|
||||
// This exists as a variable so we can use it in the from header of emails.
|
||||
let kSMTPUsername;
|
||||
|
||||
// The SMTP transport object. This is initialized to a nodemailer transport
|
||||
// object while reading SMTP credentials, or to a dummy function in debug mode.
|
||||
let gTransporter;
|
||||
|
||||
const EmailUtils = {
|
||||
init() {
|
||||
// Allow a debug mode that will send JSON back to the client instead of sending emails.
|
||||
if (process.env.DEBUG_DUMMY_SMTP) {
|
||||
console.log("Running in dummp SMTP mode, /user/breached will send a JSON response instead of sending emails.");
|
||||
gTransporter = {
|
||||
sendMail(options, callback) {
|
||||
callback(null, "dummy mode")
|
||||
},
|
||||
};
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
console.log("Attempting to get SMTP credentials from environment...");
|
||||
kSMTPUsername = AppConstants.SMTP_USERNAME;
|
||||
let password = AppConstants.SMTP_PASSWORD;
|
||||
let host = AppConstants.SMTP_HOST;
|
||||
let port = AppConstants.SMTP_PORT;
|
||||
if (!(kSMTPUsername && password && host && port)) {
|
||||
return Promise.reject("SMTP credentials could not be read from the environment");
|
||||
}
|
||||
|
||||
gTransporter = nodemailer.createTransport({
|
||||
host: host,
|
||||
port: port,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: kSMTPUsername,
|
||||
pass: password,
|
||||
},
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
gTransporter.verify(function(error, success) {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
gTransporter = null;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
sendEmail(aRecipient, aSubject, aBody) {
|
||||
if (!gTransporter) {
|
||||
return Promise.reject("SMTP transport not initialized");
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let mailOptions = {
|
||||
from: "\"Firefox Breach Alerts\" <" + kSMTPUsername + ">",
|
||||
to: aRecipient,
|
||||
subject: aSubject,
|
||||
text: aBody,
|
||||
};
|
||||
|
||||
gTransporter.sendMail(mailOptions, (error, info) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(info);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EmailUtils;
|
|
@ -0,0 +1,65 @@
|
|||
"use strict";
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
|
||||
const ClientOAuth2 = require("client-oauth2");
|
||||
const crypto = require("crypto");
|
||||
const express = require("express");
|
||||
const popsicle = require("popsicle");
|
||||
const router = express.Router();
|
||||
|
||||
const gEmails = require("../subscribers");
|
||||
|
||||
// This object exists instead of inlining the env vars to make it easy
|
||||
// to abstract fetching API endpoints from the OAuth server (instead
|
||||
// of specifying them in the environment) in the future.
|
||||
const FxAOAuthUtils = {
|
||||
get authorizationUri() { return AppConstants.OAUTH_AUTHORIZATION_URI },
|
||||
get tokenUri() { return AppConstants.OAUTH_TOKEN_URI },
|
||||
get profileUri() { return AppConstants.OAUTH_PROFILE_URI },
|
||||
};
|
||||
|
||||
var FxAOAuth = new ClientOAuth2({
|
||||
clientId: AppConstants.OAUTH_CLIENT_ID,
|
||||
clientSecret: AppConstants.OAUTH_CLIENT_SECRET,
|
||||
accessTokenUri: FxAOAuthUtils.tokenUri,
|
||||
authorizationUri: FxAOAuthUtils.authorizationUri,
|
||||
redirectUri: AppConstants.SERVER_URL + "/oauth/redirect",
|
||||
scopes: ["profile:email"],
|
||||
});
|
||||
|
||||
router.get("/init", function(req, res) {
|
||||
// Set a random state string in a cookie so that we can verify
|
||||
// the user when they're redirected back to us after auth.
|
||||
let state = crypto.randomBytes(40).toString("hex");
|
||||
let uri = FxAOAuth.code.getUri({state});
|
||||
req.session.state = state;
|
||||
res.redirect(uri);
|
||||
});
|
||||
|
||||
router.get("/redirect", function (req, res) {
|
||||
if (!req.session.state) {
|
||||
res.send("Who are you?");
|
||||
return;
|
||||
}
|
||||
FxAOAuth.code.getToken(req.originalUrl, { state: req.session.state })
|
||||
.then(function (user) {
|
||||
return popsicle.get({
|
||||
method: "get",
|
||||
url: FxAOAuthUtils.profileUri,
|
||||
body: "",
|
||||
headers: {
|
||||
Authorization: "Bearer " + user.accessToken,
|
||||
},
|
||||
}).then(function (data) {
|
||||
let email = JSON.parse(data.body).email;
|
||||
gEmails.add(email);
|
||||
res.send("Registered " + email + " for breach alerts. You may now close this window/tab.");
|
||||
});
|
||||
})
|
||||
.catch(function (err) {
|
||||
res.send(err);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,96 @@
|
|||
"use strict";
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
|
||||
const crypto = require("crypto");
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const EmailUtils = require("../email-utils");
|
||||
const gEmails = require("../subscribers");
|
||||
|
||||
// We send verification emails to addresses that want to subscribe.
|
||||
// These addresses are temporarily stored here, mapped to the unique
|
||||
// string token that is used for verification.
|
||||
var gUnverifiedEmails = new Map();
|
||||
|
||||
router.post("/add", function(req, res) {
|
||||
// TODO: use a hash of the email address instead of a random string.
|
||||
let state = crypto.randomBytes(40).toString("hex");
|
||||
req.session.state = state;
|
||||
let email = req.body.email;
|
||||
req.session.email = email;
|
||||
let url = AppConstants.SERVER_URL + "/user/verify?state=" + state;
|
||||
|
||||
EmailUtils.sendEmail(email, "Firefox Breach Alert",
|
||||
"Visit this link to subscribe: " + url)
|
||||
.then(info => {
|
||||
// TODO: set a timer to clear this after an arbitrary timeout period.
|
||||
gUnverifiedEmails.set(state, email);
|
||||
res.json({
|
||||
email,
|
||||
info: "sent verification link",
|
||||
// Send the would-be link back to the client in dummy mode.
|
||||
link: AppConstants.DEBUG_DUMMY_SMTP ? url : undefined
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
res.json({ email, info: "failed to send verification link", error });
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/verify", function(req, res) {
|
||||
let email = gUnverifiedEmails.get(req.query.state);
|
||||
if (email) {
|
||||
gEmails.add(email);
|
||||
gUnverifiedEmails.delete(req.query.state);
|
||||
res.json({ email, info: "Successfully added " + email});
|
||||
return;
|
||||
}
|
||||
res.json({ info: "Who are you?" });
|
||||
});
|
||||
|
||||
router.post("/remove", function(req, res) {
|
||||
gEmails.delete(req.body.email);
|
||||
res.json({ email: req.body.email, info: "removed user" });
|
||||
});
|
||||
|
||||
router.post("/reset", function(req, res) {
|
||||
gEmails.clear();
|
||||
res.json({ info: "user list cleared" });
|
||||
});
|
||||
|
||||
// This exists only right now for development purposes.
|
||||
router.post("/list", function(req, res) {
|
||||
res.json({ emails: Array.from(gEmails) });
|
||||
});
|
||||
|
||||
router.post("/breached", function(req, res) {
|
||||
let emails = req.body.emails;
|
||||
let response = [];
|
||||
|
||||
let emailQueue = Promise.resolve();
|
||||
// Send notification email to the intersection of the set of
|
||||
// emails in the request and the set of registered emails.
|
||||
for (let email of emails) {
|
||||
if (gEmails.has(email)) {
|
||||
emailQueue = emailQueue.then(() => {
|
||||
EmailUtils.sendEmail(email, "Firefox Breach Alert",
|
||||
"Your credentials were compromised in a breach.")
|
||||
.then(info => {
|
||||
response.push({ email, info });
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
response.push({ email, error });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
emailQueue.then(() => {
|
||||
res.json({ info: "breach alert sent", emails: response });
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
216
server.js
216
server.js
|
@ -1,45 +1,23 @@
|
|||
"use strict";
|
||||
|
||||
require("dotenv").load();
|
||||
const AppConstants = require("./app-constants").init();
|
||||
|
||||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const nodemailer = require("nodemailer");
|
||||
const ClientOAuth2 = require("client-oauth2");
|
||||
const popsicle = require("popsicle");
|
||||
const sessions = require("client-sessions");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const localServerURL = process.env.SERVER_URL || "http://localhost:6060";
|
||||
const EmailUtils = require("./email-utils");
|
||||
|
||||
// Returns a string of 80 hex chars. (1 byte = 2 hex chars)
|
||||
const getStateString = function() {
|
||||
return crypto.randomBytes(40).toString("hex");
|
||||
}
|
||||
const OAuthRoute = require("./routes/oauth");
|
||||
const UserRoute = require("./routes/user");
|
||||
|
||||
var app = express();
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.static("public"));
|
||||
|
||||
// Set of all registered emails.
|
||||
// TODO: Implement a real persistent storage solution.
|
||||
var gEmails = new Set();
|
||||
|
||||
// We send verification emails to addresses that want to subscribe.
|
||||
// These addresses are temporarily stored here, mapped to the unique
|
||||
// string token that is used for verification.
|
||||
var gUnverifiedEmails = new Map();
|
||||
|
||||
// This is set later when reading SMTP credentials from the environment.
|
||||
// This exists as a variable so we can use it in the from header of emails.
|
||||
var kSMTPUsername;
|
||||
|
||||
// The SMTP transport object. This is initialized to a nodemailer transport
|
||||
// object while reading SMTP credentials, or to a dummy function in debug mode.
|
||||
var gTransporter;
|
||||
|
||||
app.use(sessions({
|
||||
cookieName: "session",
|
||||
secret: process.env.COOKIE_SECRET,
|
||||
secret: AppConstants.COOKIE_SECRET,
|
||||
duration: 15 * 60 * 1000, // 15 minutes
|
||||
activeDuration: 5 * 60 * 1000, // 5 minutes
|
||||
}));
|
||||
|
@ -48,140 +26,8 @@ app.get("/", function(req, res) {
|
|||
res.send("blurts-server v0.01a");
|
||||
});
|
||||
|
||||
app.post("/user/add", function(req, res) {
|
||||
// TODO: use a hash of the email address instead of a random string.
|
||||
let state = getStateString();
|
||||
req.session.state = state;
|
||||
let email = req.body.email;
|
||||
req.session.email = email;
|
||||
let url = localServerURL + "/user/verify?state=" + state;
|
||||
let mailOptions = {
|
||||
from: "\"Firefox Breach Alerts\" <" + kSMTPUsername + ">", // sender address
|
||||
to: email, // list of receivers
|
||||
subject: "Firefox Breach Alert", // Subject line
|
||||
text: "Visit this link to subscribe: " + url, // plain text body
|
||||
};
|
||||
|
||||
gTransporter.sendMail(mailOptions, (error, info) => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
res.json({ email, info: "failed to send verification link", error });
|
||||
return;
|
||||
}
|
||||
// TODO: set a timer to clear this after an arbitrary timeout period.
|
||||
gUnverifiedEmails.set(state, email);
|
||||
res.json({
|
||||
email,
|
||||
info: "sent verification link",
|
||||
// Send the would-be link back to the client in dummy mode.
|
||||
link: process.env.DEBUG_DUMMY_SMTP ? url : undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/user/verify", function(req, res) {
|
||||
let email = gUnverifiedEmails.get(req.query.state);
|
||||
if (email) {
|
||||
gEmails.add(email);
|
||||
gUnverifiedEmails.delete(req.query.state);
|
||||
res.json({ email, info: "Successfully added " + email});
|
||||
return;
|
||||
}
|
||||
res.json({ info: "Who are you?" });
|
||||
});
|
||||
|
||||
app.post("/user/remove", function(req, res) {
|
||||
gEmails.delete(req.body.email);
|
||||
res.json({ email: req.body.email, info: "removed user" });
|
||||
});
|
||||
|
||||
app.post("/user/reset", function(req, res) {
|
||||
gEmails.clear();
|
||||
res.json({ info: "user list cleared" });
|
||||
});
|
||||
|
||||
// This exists only right now for development purposes.
|
||||
app.post("/user/list", function(req, res) {
|
||||
res.json({ emails: Array.from(gEmails) });
|
||||
});
|
||||
|
||||
app.post("/user/breached", function(req, res) {
|
||||
let emails = req.body.emails;
|
||||
let response = [];
|
||||
|
||||
// Send notification email to the intersection of the set of
|
||||
// emails in the request and the set of registered emails.
|
||||
for (let email of emails) {
|
||||
if (gEmails.has(email)) {
|
||||
let mailOptions = {
|
||||
from: "\"Firefox Breach Alerts\" <" + kSMTPUsername + ">", // sender address
|
||||
to: email, // list of receivers
|
||||
subject: "Firefox Breach Alert", // Subject line
|
||||
text: "Your credentials were compromised in a breach.", // plain text body
|
||||
};
|
||||
|
||||
gTransporter.sendMail(mailOptions, (error, info) => {
|
||||
response.push({ email, error, info });
|
||||
if (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
res.json({ info: "breach alert sent", emails: response });
|
||||
});
|
||||
|
||||
// This object exists instead of inlining the env vars to make it easy
|
||||
// to abstract fetching API endpoints from the OAuth server (instead
|
||||
// of specifying them in the environment) in the future.
|
||||
const FxAOAuthUtils = {
|
||||
get authorizationUri() { return process.env.OAUTH_AUTHORIZATION_URI },
|
||||
get tokenUri() { return process.env.OAUTH_TOKEN_URI },
|
||||
get profileUri() { return process.env.OAUTH_PROFILE_URI },
|
||||
};
|
||||
|
||||
var FxAOAuth = new ClientOAuth2({
|
||||
clientId: process.env.OAUTH_CLIENT_ID,
|
||||
clientSecret: process.env.OAUTH_CLIENT_SECRET,
|
||||
accessTokenUri: FxAOAuthUtils.tokenUri,
|
||||
authorizationUri: FxAOAuthUtils.authorizationUri,
|
||||
redirectUri: localServerURL + "/oauth/redirect",
|
||||
scopes: ["profile:email"],
|
||||
});
|
||||
|
||||
app.get("/oauth/init", function(req, res) {
|
||||
// Set a random state string in a cookie so that we can verify
|
||||
// the user when they're redirected back to us after auth.
|
||||
let state = getStateString();
|
||||
let uri = FxAOAuth.code.getUri({state});
|
||||
req.session.state = state;
|
||||
res.redirect(uri);
|
||||
});
|
||||
|
||||
app.get('/oauth/redirect', function (req, res) {
|
||||
if (!req.session.state) {
|
||||
res.send("Who are you?");
|
||||
return;
|
||||
}
|
||||
FxAOAuth.code.getToken(req.originalUrl, { state: req.session.state })
|
||||
.then(function (user) {
|
||||
popsicle.get({
|
||||
method: "get",
|
||||
url: FxAOAuthUtils.profileUri,
|
||||
body: "",
|
||||
headers: {
|
||||
Authorization: "Bearer " + user.accessToken,
|
||||
},
|
||||
}).then(function (data) {
|
||||
let email = JSON.parse(data.body).email;
|
||||
gEmails.add(email);
|
||||
res.send("Registered " + email + " for breach alerts. You may now close this window/tab.");
|
||||
});
|
||||
})
|
||||
.catch(function (err) {
|
||||
res.send(err);
|
||||
});
|
||||
});
|
||||
app.use("/oauth", OAuthRoute);
|
||||
app.use("/user", UserRoute);
|
||||
|
||||
|
||||
// TODO: remove this
|
||||
|
@ -189,44 +35,10 @@ app.get("/test", function(req, res) {
|
|||
res.send(req.body);
|
||||
})
|
||||
|
||||
var port = process.env.PORT || 6060;
|
||||
|
||||
// Allow a debug mode that will send JSON back to the client instead of sending emails.
|
||||
if (process.env.DEBUG_DUMMY_SMTP) {
|
||||
console.log("Running in dummp SMTP mode, /user/breached will send a JSON response instead of sending emails.");
|
||||
gTransporter = {
|
||||
sendMail(options, callback) {
|
||||
callback(null, "dummy mode")
|
||||
},
|
||||
};
|
||||
} else {
|
||||
console.log("Attempting to get SMTP config from environment...");
|
||||
kSMTPUsername = process.env.SMTP_USERNAME;
|
||||
let password = process.env.SMTP_PASSWORD;
|
||||
let host = process.env.SMTP_HOST;
|
||||
let port = process.env.SMTP_PORT;
|
||||
if (kSMTPUsername && password && host && port) {
|
||||
gTransporter = nodemailer.createTransport({
|
||||
host: host,
|
||||
port: port,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: kSMTPUsername,
|
||||
pass: password,
|
||||
},
|
||||
});
|
||||
gTransporter.verify(function(error, success) {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
gTransporter = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!gTransporter) {
|
||||
console.log("SMTP config unavailable. Email features will not work.")
|
||||
}
|
||||
app.listen(port, function() {
|
||||
console.log("Listening on " + port);
|
||||
EmailUtils.init().then(() => {
|
||||
app.listen(AppConstants.PORT, function() {
|
||||
console.log("Listening on " + AppConstants.PORT);
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
// Set of all registered emails.
|
||||
// TODO: Implement a real persistent storage solution.
|
||||
var gEmails = new Set();
|
||||
|
||||
module.exports = gEmails;
|
Загрузка…
Ссылка в новой задаче