Skip to content

Commit

Permalink
Submit shot image to Watchdog on save. (mozilla-services#4377)
Browse files Browse the repository at this point in the history
  • Loading branch information
chenba committed Jun 6, 2018
1 parent 5dfdc7c commit 856910f
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ FXA_CLIENT_SECRET=14c2a3c7577fd3b28ee7141ad726dcd4160f24b2619e2f908960844430bdee
FXA_OAUTH_URI=https://oauth-stable.dev.lcip.org/v1
FXA_PROFILE_URI=https://stable.dev.lcip.org/profile/v1

# Watchdog config
ENABLE_WATCHDOG=false
WATCHDOG_ID=devuser
WATCHDOG_KEY=devkey
WATCHDOG_SUBMISSION_URL=https://watchdog-proxy.dev.mozaws.net/accept

SHOW_STACK_TRACES=true
LOG_LINT=true
LOG_LEVEL=debug
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"fluent-intl-polyfill": "0.1.0",
"fluent-langneg": "0.1.0",
"fluent-react": "0.4.1",
"form-data": "2.3.2",
"hawk": "7.0.7",
"jpm": "1.3.1",
"keygrip": "1.0.2",
"mobile-detect": "1.4.1",
Expand Down
51 changes: 51 additions & 0 deletions server/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,57 @@ const conf = convict({
default: false,
env: "ENABLE_COVERAGE",
arg: "enable-coverage"
},
watchdog: {
enable: {
doc: "If true, submit shots to Watchdog.",
format: Boolean,
default: true,
env: "ENABLE_WATCHDOG",
arg: "enable-watchdog"
},
id: {
doc: "Screenshots' user id at Watchdog",
format: String,
default: "",
env: "WATCHDOG_ID",
arg: "watchdog-id"
},
key: {
doc: "Screenshots' key from Watchdog.",
format: String,
default: "",
env: "WATCHDOG_KEY",
arg: "watchdog-key"
},
algorithm: {
doc: "The hash algorithm used in authenticating requests.",
format: String,
default: "sha256",
env: "WATCHDOG_AUTH_HASH_ALGORITHM",
arg: "watchdog-auth-hash-algorithm"
},
submissionUrl: {
doc: "The URL where Watchdog is accepting submissions.",
format: String,
default: "",
env: "WATCHDOG_SUBMISSION_URL",
arg: "watchdog-SUBMISSION_url"
},
positiveEmail: {
doc: "An optional, semicolon delimited, list of email addresses to receive notifications on positive matches.",
format: String,
default: "",
env: "WATCHDOG_POSITIVE_EMAIL",
arg: "watchdog-positive-email"
},
throttleDenominator: {
doc: "Temporary(?) config to throttle the rate of submission to Watchdog. A positive integer. One out of this many shots will be submitted to Watchdog. One or less will result in all shots being submitted.",
format: "int",
default: 1,
env: "WATCHDOG_THROTTLE_DENOMINATOR",
arg: "watchdog-throttle-denominator"
}
}
});

Expand Down
7 changes: 5 additions & 2 deletions server/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const path = require("path");
const { readFileSync, existsSync } = require("fs");
const Cookies = require("cookies");
const { URL } = require("url");
const { Watchdog } = require("./watchdog");

let istanbulMiddleware = null;
if (config.enableCoverage && process.env.NODE_ENV === "dev") {
Expand Down Expand Up @@ -720,6 +721,7 @@ app.put("/data/:id/:domain",
simpleResponse(res, "No shot updated", 403);
return;
}
Watchdog.submit(shot);
commands = commands || [];
simpleResponse(res, JSON.stringify({updates: commands.filter((x) => !!x)}), 200);
}).catch((err) => {
Expand Down Expand Up @@ -846,8 +848,9 @@ app.post("/api/save-edit", function(req, res) {
shot.getClip(name).image.dimensions.x = width;
shot.getClip(name).image.dimensions.y = height;
shot.thumbnail = thumbnail;
return shot.update();
}).then((updated) => {
return shot.update().then(updated => ({updated, shot}));
}).then(({updated, shot}) => {
Watchdog.submit(shot);
simpleResponse(res, "Updated", 200);
}).catch((err) => {
errorResponse(res, "Error updating image", err);
Expand Down
141 changes: 141 additions & 0 deletions server/src/watchdog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
const config = require("./config").getProperties();
const mozlog = require("./logging").mozlog("server");
const db = require("./db");
const { Shot } = require("./servershot");
const util = require("util");
const { URL } = require("url");
const fetch = require("node-fetch");
const Hawk = require("hawk");
const FormData = require("form-data");
const genUuid = require("nodify-uuid");

const isEnabled = config.watchdog.enable;
const isProperlyConfigured = validateConfiguration();

function validateConfiguration() {
if (!isEnabled) {
// Do no warn about config values if Watchdog is not enabled.
return false;
}

let isConfigValid = true;

if (!config.watchdog.id || !config.watchdog.key || !config.watchdog.algorithm) {
mozlog.warn("watchdog-invalid-config", {msg: "Watchdog credentials configuration values are missing."});
isConfigValid = false;
}

if (!config.watchdog.submissionUrl) {
mozlog.warn("watchdog-invalid-config", {msg: "Watchdog submission URL configuration value is missing."});
isConfigValid = false;
} else {
try {
new URL(config.watchdog.submissionUrl);
} catch (e) {
mozlog.warn("watchdog-invalid-config", {msg: "Watchdog submission URL is not a valid URL.", err: e});
isConfigValid = false;
}
}

return isConfigValid;
}

let positiveEmailList;
if (config.watchdog.positiveEmail) {
positiveEmailList = config.watchdog.positiveEmail.split(";");
}

let denominator = config.watchdog.throttleDenominator;
if (denominator <= 1) {
denominator = 1;
}
let odometer = 0;

function shouldSubmitShot() {
if (!isEnabled || !isProperlyConfigured) {
return false;
}
if (denominator === 1) {
return true;
}

const shouldSubmit = odometer === 0;
odometer = (odometer + 1) % denominator;

return shouldSubmit;
}

function getSubmissionId() {
return db.select(
"SELECT nextval(pg_get_serial_sequence('watchdog_submissions', 'id')) AS new_id",
).then(rows => {
if (rows.length) {
return rows[0].new_id;
}
throw new Error("Unable to retrieve a new Watchdog submission id.");
});
}

const credentials = {
id: config.watchdog.id,
key: config.watchdog.key,
algorithm: config.watchdog.algorithm
};

function submit(shot) {
if (!shouldSubmitShot()) {
return;
}

const imageUrl = shot.getClip(shot.clipNames()[0]).image.url;
const imageId = imageUrl.split("/").pop();
const form = new FormData();

Shot.getRawBytesForClip(
imageId
).then((obj) => {
if (obj === null) {
throw new Error(`Unable to fetch ${imageId} for Watchdog submission.`);
}
form.append("image", obj.data, {
filename: imageId,
contentType: obj.contentType
});

return getSubmissionId();
}).then(submissionId => {
const uuid = util.promisify(genUuid.generate);
return uuid(genUuid.V_RANDOM).then(nonce => ({submissionId, nonce}));
}).then(({submissionId, nonce}) => {
const callbackUrl = `${shot.backend}/watchdog/${submissionId}?nonce=${nonce}`;
form.append("positive_uri", callbackUrl);
form.append("negative_uri", callbackUrl);

if (positiveEmailList) {
for (const addr of positiveEmailList) {
form.append("positive_email[]", addr);
}
}

const req = {method: "POST", body: form};
const authHeader = Hawk.client.header(config.watchdog.submissionUrl, req.method, {credentials});
req.headers = Object.assign(
{Accept: "application/json", Authorization: authHeader.header},
form.getHeaders());
return fetch(config.watchdog.submissionUrl, req)
.then(res => res.json())
.then(respJson => ({submissionId, nonce, request_id: respJson.id}));
}).then(({submissionId, nonce, request_id}) => {
return db.insert(
`INSERT INTO watchdog_submissions (id, shot_id, request_id, nonce)
VALUES ($1, $2, $3, $4)`,
[submissionId, shot.id, request_id, nonce.toString()]
);
}).catch(e => {
mozlog.error("watchdog-failed-submission", {err: e});
});
}

exports.Watchdog = {
submit
};

0 comments on commit 856910f

Please # to comment.