diff --git a/modules/__tests__/api-test.js b/modules/__tests__/api-test.js new file mode 100644 index 0000000..35eb080 --- /dev/null +++ b/modules/__tests__/api-test.js @@ -0,0 +1,342 @@ +const request = require("supertest"); + +const createServer = require("../createServer"); + +const clearBlacklist = require("./utils/clearBlacklist"); +const withRevokedToken = require("./utils/withRevokedToken"); +const withToken = require("./utils/withToken"); + +describe("The API server", () => { + let server; + beforeEach(() => { + server = createServer(); + }); + + describe("GET /api/publicKey", () => { + it("echoes the public key", done => { + request(server) + .get("/api/publicKey") + .end((err, res) => { + expect(res.text).toMatch(/PUBLIC KEY/); + done(); + }); + }); + }); + + // TODO: Remove + describe("GET /_publicKey", () => { + it("echoes the public key", done => { + request(server) + .get("/_publicKey") + .end((err, res) => { + expect(res.text).toMatch(/PUBLIC KEY/); + done(); + }); + }); + }); + + describe("POST /api/auth", () => { + it("creates a new auth token", done => { + request(server) + .post("/api/auth") + .end((err, res) => { + expect(res.body).toHaveProperty("token"); + done(); + }); + }); + }); + + // TODO: Remove + describe("POST /_auth", () => { + it("creates a new auth token", done => { + request(server) + .post("/_auth") + .end((err, res) => { + expect(res.body).toHaveProperty("token"); + done(); + }); + }); + }); + + describe("GET /api/auth", () => { + describe("with no auth", () => { + it("echoes back null", done => { + request(server) + .get("/api/auth") + .end((err, res) => { + expect(res.body).toHaveProperty("auth"); + expect(res.body.auth).toBe(null); + done(); + }); + }); + }); + + describe("with a revoked auth token", () => { + it("echoes back null", done => { + withRevokedToken({ some: { scope: true } }, token => { + request(server) + .get("/api/auth?token=" + token) + .end((err, res) => { + expect(res.body).toHaveProperty("auth"); + expect(res.body.auth).toBe(null); + done(); + }); + }); + }); + }); + + describe("with a valid auth token", () => { + it("echoes back the auth payload", done => { + withToken({ some: { scope: true } }, token => { + request(server) + .get("/api/auth?token=" + token) + .end((err, res) => { + expect(res.body).toHaveProperty("auth"); + expect(typeof res.body.auth).toBe("object"); + done(); + }); + }); + }); + }); + }); + + // TODO: Remove + describe("GET /_auth", () => { + describe("with no auth", () => { + it("echoes back null", done => { + request(server) + .get("/_auth") + .end((err, res) => { + expect(res.body).toHaveProperty("auth"); + expect(res.body.auth).toBe(null); + done(); + }); + }); + }); + + describe("with a revoked auth token", () => { + it("echoes back null", done => { + withRevokedToken({ some: { scope: true } }, token => { + request(server) + .get("/_auth?token=" + token) + .end((err, res) => { + expect(res.body).toHaveProperty("auth"); + expect(res.body.auth).toBe(null); + done(); + }); + }); + }); + }); + + describe("with a valid auth token", () => { + it("echoes back the auth payload", done => { + withToken({ some: { scope: true } }, token => { + request(server) + .get("/_auth?token=" + token) + .end((err, res) => { + expect(res.body).toHaveProperty("auth"); + expect(typeof res.body.auth).toBe("object"); + done(); + }); + }); + }); + }); + }); + + describe("POST /api/blacklist", () => { + afterEach(clearBlacklist); + + describe("with no auth", () => { + it("is forbidden", done => { + request(server) + .post("/api/blacklist") + .end((err, res) => { + expect(res.statusCode).toBe(403); + done(); + }); + }); + }); + + describe('with the "blacklist.add" scope', () => { + it("can add to the blacklist", done => { + withToken({ blacklist: { add: true } }, token => { + request(server) + .post("/api/blacklist") + .send({ token, packageName: "bad-package" }) + .end((err, res) => { + expect(res.statusCode).toBe(200); + expect(res.body.ok).toBe(true); + done(); + }); + }); + }); + }); + }); + + // TODO: Remove + describe("POST /_blacklist", () => { + afterEach(clearBlacklist); + + describe("with no auth", () => { + it("is forbidden", done => { + request(server) + .post("/_blacklist") + .end((err, res) => { + expect(res.statusCode).toBe(403); + done(); + }); + }); + }); + + describe('with the "blacklist.add" scope', () => { + it("can add to the blacklist", done => { + withToken({ blacklist: { add: true } }, token => { + request(server) + .post("/_blacklist") + .send({ token, packageName: "bad-package" }) + .end((err, res) => { + expect(res.statusCode).toBe(200); + expect(res.body.ok).toBe(true); + done(); + }); + }); + }); + }); + }); + + describe("GET /api/blacklist", () => { + describe("with no auth", () => { + it("is forbidden", done => { + request(server) + .get("/api/blacklist") + .end((err, res) => { + expect(res.statusCode).toBe(403); + done(); + }); + }); + }); + + describe('with the "blacklist.read" scope', () => { + it("can read the blacklist", done => { + withToken({ blacklist: { read: true } }, token => { + request(server) + .get("/api/blacklist?token=" + token) + .end((err, res) => { + expect(res.statusCode).toBe(200); + done(); + }); + }); + }); + }); + }); + + // TODO: Remove + describe("GET /_blacklist", () => { + describe("with no auth", () => { + it("is forbidden", done => { + request(server) + .get("/_blacklist") + .end((err, res) => { + expect(res.statusCode).toBe(403); + done(); + }); + }); + }); + + describe('with the "blacklist.read" scope', () => { + it("can read the blacklist", done => { + withToken({ blacklist: { read: true } }, token => { + request(server) + .get("/_blacklist?token=" + token) + .end((err, res) => { + expect(res.statusCode).toBe(200); + done(); + }); + }); + }); + }); + }); + + describe("DELETE /api/blacklist", () => { + describe("with no auth", () => { + it("is forbidden", done => { + request(server) + .delete("/api/blacklist") + .end((err, res) => { + expect(res.statusCode).toBe(403); + done(); + }); + }); + }); + + describe('with the "blacklist.remove" scope', () => { + it("can remove a package from the blacklist", done => { + withToken({ blacklist: { remove: true } }, token => { + request(server) + .delete("/api/blacklist") + .send({ token, packageName: "bad-package" }) + .end((err, res) => { + expect(res.statusCode).toBe(200); + expect(res.body.ok).toBe(true); + done(); + }); + }); + }); + + it("can remove a scoped package from the blacklist", done => { + withToken({ blacklist: { remove: true } }, token => { + request(server) + .delete("/api/blacklist") + .send({ token, packageName: "@scope/bad-package" }) + .end((err, res) => { + expect(res.statusCode).toBe(200); + expect(res.body.ok).toBe(true); + done(); + }); + }); + }); + }); + }); + + // TODO: Remove + describe("DELETE /_blacklist/:packageName", () => { + describe("with no auth", () => { + it("is forbidden", done => { + request(server) + .delete("/_blacklist/bad-package") + .end((err, res) => { + expect(res.statusCode).toBe(403); + done(); + }); + }); + }); + + describe('with the "blacklist.remove" scope', () => { + it("can remove a package from the blacklist", done => { + withToken({ blacklist: { remove: true } }, token => { + request(server) + .delete("/_blacklist/bad-package") + .send({ token }) + .end((err, res) => { + expect(res.statusCode).toBe(200); + expect(res.body.ok).toBe(true); + done(); + }); + }); + }); + + it("can remove a scoped package from the blacklist", done => { + withToken({ blacklist: { remove: true } }, token => { + request(server) + .delete("/_blacklist/@scope/bad-package") + .send({ token }) + .end((err, res) => { + expect(res.statusCode).toBe(200); + expect(res.body.ok).toBe(true); + done(); + }); + }); + }); + }); + }); +}); diff --git a/modules/__tests__/server-test.js b/modules/__tests__/server-test.js index e79b631..1051e35 100644 --- a/modules/__tests__/server-test.js +++ b/modules/__tests__/server-test.js @@ -4,10 +4,8 @@ const createServer = require("../createServer"); const clearBlacklist = require("./utils/clearBlacklist"); const withBlacklist = require("./utils/withBlacklist"); -const withRevokedToken = require("./utils/withRevokedToken"); -const withToken = require("./utils/withToken"); -describe("The production server", () => { +describe("The server", () => { let server; beforeEach(() => { server = createServer(); @@ -52,178 +50,18 @@ describe("The production server", () => { }); }); - it("does not serve blacklisted packages", done => { - withBlacklist(["bad-package"], () => { - request(server) - .get("/bad-package/index.js") - .end((err, res) => { - expect(res.statusCode).toBe(403); - done(); - }); - }); - }); - - describe("GET /_publicKey", () => { - it("echoes the public key", done => { - request(server) - .get("/_publicKey") - .end((err, res) => { - expect(res.text).toMatch(/PUBLIC KEY/); - done(); - }); - }); - }); - - describe("POST /_auth", () => { - it("creates a new auth token", done => { - request(server) - .post("/_auth") - .end((err, res) => { - expect(res.body).toHaveProperty("token"); - done(); - }); - }); - }); - - describe("GET /_auth", () => { - describe("with no auth", () => { - it("echoes back null", done => { - request(server) - .get("/_auth") - .end((err, res) => { - expect(res.body).toHaveProperty("auth"); - expect(res.body.auth).toBe(null); - done(); - }); - }); - }); - - describe("with a revoked auth token", () => { - it("echoes back null", done => { - withRevokedToken({ some: { scope: true } }, token => { - request(server) - .get("/_auth?token=" + token) - .end((err, res) => { - expect(res.body).toHaveProperty("auth"); - expect(res.body.auth).toBe(null); - done(); - }); - }); - }); - }); - - describe("with a valid auth token", () => { - it("echoes back the auth payload", done => { - withToken({ some: { scope: true } }, token => { - request(server) - .get("/_auth?token=" + token) - .end((err, res) => { - expect(res.body).toHaveProperty("auth"); - expect(typeof res.body.auth).toBe("object"); - done(); - }); - }); - }); - }); - }); - - describe("POST /_blacklist", () => { + describe("blacklisted packages", () => { afterEach(clearBlacklist); - describe("with no auth", () => { - it("is forbidden", done => { + it("does not serve blacklisted packages", done => { + withBlacklist(["bad-package"], () => { request(server) - .post("/_blacklist") + .get("/bad-package/index.js") .end((err, res) => { expect(res.statusCode).toBe(403); done(); }); }); }); - - describe('with the "blacklist.add" scope', () => { - it("can add to the blacklist", done => { - withToken({ blacklist: { add: true } }, token => { - request(server) - .post("/_blacklist") - .send({ token, packageName: "bad-package" }) - .end((err, res) => { - expect(res.statusCode).toBe(200); - expect(res.headers["content-location"]).toEqual( - "/_blacklist/bad-package" - ); - expect(res.body.ok).toBe(true); - done(); - }); - }); - }); - }); - }); - - describe("GET /_blacklist", () => { - describe("with no auth", () => { - it("is forbidden", done => { - request(server) - .get("/_blacklist") - .end((err, res) => { - expect(res.statusCode).toBe(403); - done(); - }); - }); - }); - - describe('with the "blacklist.read" scope', () => { - it("can read the blacklist", done => { - withToken({ blacklist: { read: true } }, token => { - request(server) - .get("/_blacklist?token=" + token) - .end((err, res) => { - expect(res.statusCode).toBe(200); - done(); - }); - }); - }); - }); - }); - - describe("DELETE /_blacklist/:packageName", () => { - describe("with no auth", () => { - it("is forbidden", done => { - request(server) - .delete("/_blacklist/bad-package") - .end((err, res) => { - expect(res.statusCode).toBe(403); - done(); - }); - }); - }); - - describe('with the "blacklist.remove" scope', () => { - it("can remove a package from the blacklist", done => { - withToken({ blacklist: { remove: true } }, token => { - request(server) - .delete("/_blacklist/bad-package") - .send({ token }) - .end((err, res) => { - expect(res.statusCode).toBe(200); - expect(res.body.ok).toBe(true); - done(); - }); - }); - }); - - it("can remove a scoped package from the blacklist", done => { - withToken({ blacklist: { remove: true } }, token => { - request(server) - .delete("/_blacklist/@scope/bad-package") - .send({ token }) - .end((err, res) => { - expect(res.statusCode).toBe(200); - expect(res.body.ok).toBe(true); - done(); - }); - }); - }); - }); }); }); diff --git a/modules/__tests__/utils/withBlacklist.js b/modules/__tests__/utils/withBlacklist.js index 87bbaa0..98e3be3 100644 --- a/modules/__tests__/utils/withBlacklist.js +++ b/modules/__tests__/utils/withBlacklist.js @@ -1,7 +1,7 @@ const BlacklistAPI = require("../../BlacklistAPI"); -function withBlacklist(blacklist, callback) { - return Promise.all(blacklist.map(BlacklistAPI.addPackage)).then(callback); +function withBlacklist(blacklist, done) { + Promise.all(blacklist.map(BlacklistAPI.addPackage)).then(done); } module.exports = withBlacklist; diff --git a/modules/__tests__/utils/withRevokedToken.js b/modules/__tests__/utils/withRevokedToken.js index 44b37d2..2e55fce 100644 --- a/modules/__tests__/utils/withRevokedToken.js +++ b/modules/__tests__/utils/withRevokedToken.js @@ -1,10 +1,10 @@ const withToken = require("./withToken"); const AuthAPI = require("../../AuthAPI"); -function withRevokedToken(scopes, callback) { +function withRevokedToken(scopes, done) { withToken(scopes, token => { AuthAPI.revokeToken(token).then(() => { - callback(token); + done(token); }); }); } diff --git a/modules/__tests__/utils/withToken.js b/modules/__tests__/utils/withToken.js index fde0fd1..9dbc4a0 100644 --- a/modules/__tests__/utils/withToken.js +++ b/modules/__tests__/utils/withToken.js @@ -1,7 +1,7 @@ const AuthAPI = require("../../AuthAPI"); -function withToken(scopes, callback) { - AuthAPI.createToken(scopes).then(callback); +function withToken(scopes, done) { + AuthAPI.createToken(scopes).then(done); } module.exports = withToken; diff --git a/modules/actions/addToBlacklist.js b/modules/actions/addToBlacklist.js index b43d18b..2f710ef 100644 --- a/modules/actions/addToBlacklist.js +++ b/modules/actions/addToBlacklist.js @@ -1,4 +1,5 @@ const validateNpmPackageName = require("validate-npm-package-name"); + const BlacklistAPI = require("../BlacklistAPI"); function addToBlacklist(req, res) { @@ -29,7 +30,7 @@ function addToBlacklist(req, res) { ); } - res.set({ "Content-Location": `/_blacklist/${packageName}` }).send({ + res.send({ ok: true, message: `Package "${packageName}" was ${ added ? "added to" : "already in" diff --git a/modules/actions/removeFromBlacklist.js b/modules/actions/removeFromBlacklist.js index 663dac0..7ad3faf 100644 --- a/modules/actions/removeFromBlacklist.js +++ b/modules/actions/removeFromBlacklist.js @@ -1,7 +1,27 @@ +const validateNpmPackageName = require("validate-npm-package-name"); + const BlacklistAPI = require("../BlacklistAPI"); function removeFromBlacklist(req, res) { - const packageName = req.packageName; + // TODO: Remove req.packageName when DELETE + // /_blacklist/:packageName API is removed + const packageName = req.body.packageName || req.packageName; + + if (!packageName) { + return res + .status(403) + .send({ error: 'Missing "packageName" body parameter' }); + } + + const nameErrors = validateNpmPackageName(packageName).errors; + + // Disallow invalid package names. + if (nameErrors) { + const reason = nameErrors.join(", "); + return res.status(403).send({ + error: `Invalid package name "${packageName}" (${reason})` + }); + } BlacklistAPI.removePackage(packageName).then( removed => { diff --git a/modules/client/main/Layout.js b/modules/client/main/Layout.js index 16382b1..3f2a864 100644 --- a/modules/client/main/Layout.js +++ b/modules/client/main/Layout.js @@ -50,7 +50,7 @@ class Layout extends React.Component { componentDidMount() { this.adjustUnderline(); - fetch("/_stats?period=last-month") + fetch("/api/stats?period=last-month") .then(res => res.json()) .then(stats => this.setState({ stats })); diff --git a/modules/createRouter.js b/modules/createRouter.js index 86c392e..5ccc2f4 100644 --- a/modules/createRouter.js +++ b/modules/createRouter.js @@ -17,8 +17,40 @@ function createRouter() { app.use(bodyParser.json()); app.use(require("./middleware/userToken")); + app.use( + "/api", + route(app => { + app.get("/publicKey", require("./actions/showPublicKey")); + + app.post("/auth", require("./actions/createAuth")); + app.get("/auth", require("./actions/showAuth")); + + app.post( + "/blacklist", + require("./middleware/requireAuth")("blacklist.add"), + require("./actions/addToBlacklist") + ); + app.get( + "/blacklist", + require("./middleware/requireAuth")("blacklist.read"), + require("./actions/showBlacklist") + ); + app.delete( + "/blacklist", + require("./middleware/requireAuth")("blacklist.remove"), + require("./actions/removeFromBlacklist") + ); + + if (process.env.NODE_ENV !== "test") { + app.get("/stats", require("./actions/showStats")); + } + }) + ); + + // TODO: Remove app.get("/_publicKey", require("./actions/showPublicKey")); + // TODO: Remove app.use( "/_auth", route(app => { @@ -27,6 +59,7 @@ function createRouter() { }) ); + // TODO: Remove app.use( "/_blacklist", route(app => { @@ -49,6 +82,7 @@ function createRouter() { }) ); + // TODO: Remove if (process.env.NODE_ENV !== "test") { app.get("/_stats", require("./actions/showStats")); } diff --git a/modules/ingestLogs.js b/modules/ingestLogs.js index c2d4017..b7dd45a 100644 --- a/modules/ingestLogs.js +++ b/modules/ingestLogs.js @@ -20,11 +20,6 @@ const domainNames = [ let cachedZones; -const oneSecond = 1000; -const oneMinute = oneSecond * 60; -const oneHour = oneMinute * 60; -const oneDay = oneHour * 24; - function getSeconds(date) { return Math.floor(date.getTime() / 1000); } diff --git a/modules/middleware/findFile.js b/modules/middleware/findFile.js index 624b04a..aa02f44 100644 --- a/modules/middleware/findFile.js +++ b/modules/middleware/findFile.js @@ -27,7 +27,7 @@ function indexRedirect(req, res, entry) { } function stripLeadingSegment(name) { - return name.replace(/^[^\/]+\/?/, ""); + return name.replace(/^[^/]+\/?/, ""); } function searchEntries(tarballStream, entryName, wantsHTML) { diff --git a/modules/middleware/userToken.js b/modules/middleware/userToken.js index 79b4be9..e78c45e 100644 --- a/modules/middleware/userToken.js +++ b/modules/middleware/userToken.js @@ -1,3 +1,5 @@ +const basicAuth = require("basic-auth"); + const AuthAPI = require("../AuthAPI"); const ReadMethods = { GET: true, HEAD: true }; @@ -10,7 +12,10 @@ function userToken(req, res, next) { return next(); } - const token = (ReadMethods[req.method] ? req.query : req.body).token; + const credentials = basicAuth(req); + const token = credentials + ? credentials.pass + : (ReadMethods[req.method] ? req.query : req.body).token; if (!token) { req.user = null; diff --git a/package.json b/package.json index 7f679a7..580ecc9 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "babel-preset-react": "^6.24.1", "babel-preset-stage-2": "^6.24.1", "babel-register": "^6.26.0", + "basic-auth": "^2.0.0", "body-parser": "^1.18.2", "cors": "^2.8.1", "countries-list": "^1.3.2", diff --git a/yarn.lock b/yarn.lock index 22ca323..8acd05d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1144,7 +1144,7 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" -basic-auth@~2.0.0: +basic-auth@^2.0.0, basic-auth@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.0.tgz#015db3f353e02e56377755f962742e8981e7bbba" dependencies: