This commit is contained in:
Nihanth Subramanya 2018-02-08 00:46:39 +05:30
Родитель a0344e18b4
Коммит 5b78112be4
7 изменённых файлов: 305 добавлений и 208 удалений

Просмотреть файл

@ -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=""

32
app-constants.js Normal file
Просмотреть файл

@ -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;

81
email-utils.js Normal file
Просмотреть файл

@ -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;

65
routes/oauth.js Normal file
Просмотреть файл

@ -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;

96
routes/user.js Normal file
Просмотреть файл

@ -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
Просмотреть файл

@ -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);
});

7
subscribers.js Normal file
Просмотреть файл

@ -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;