Add auth and blacklist APIs
This commit is contained in:
118
server/AuthAPI.js
Normal file
118
server/AuthAPI.js
Normal file
@ -0,0 +1,118 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const crypto = require('crypto')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const invariant = require('invariant')
|
||||
const forge = require('node-forge')
|
||||
const db = require('./RedisClient')
|
||||
|
||||
let keys
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
keys = {
|
||||
public: fs.readFileSync(path.resolve(__dirname, '../public.key'), 'utf8'),
|
||||
private: process.env.PRIVATE_KEY
|
||||
}
|
||||
|
||||
invariant(keys.private, 'Missing $PRIVATE_KEY environment variable')
|
||||
} else {
|
||||
// Generate a random keypair for dev/testing.
|
||||
// See https://gist.github.com/sebadoom/2b70969e70db5da9a203bebd9cff099f
|
||||
const keypair = forge.rsa.generateKeyPair({ bits: 2048 })
|
||||
keys = {
|
||||
public: forge.pki.publicKeyToPem(keypair.publicKey, 72),
|
||||
private: forge.pki.privateKeyToPem(keypair.privateKey, 72)
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentSeconds() {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
function createTokenId() {
|
||||
return crypto.randomBytes(16).toString('hex')
|
||||
}
|
||||
|
||||
function createToken(scopes = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = {
|
||||
jti: createTokenId(),
|
||||
iss: 'https://unpkg.com',
|
||||
iat: getCurrentSeconds(),
|
||||
scopes
|
||||
}
|
||||
|
||||
jwt.sign(payload, keys.private, { algorithm: 'RS256' }, (error, token) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(token)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const RevokedTokensSet = 'revoked-tokens'
|
||||
|
||||
function verifyToken(token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = { algorithms: ['RS256'] }
|
||||
|
||||
jwt.verify(token, keys.public, options, (error, payload) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
if (payload.jti) {
|
||||
db.sismember(RevokedTokensSet, payload.jti, (error, value) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(value === 0 ? payload : null)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function revokeToken(token) {
|
||||
return verifyToken(token).then(payload => {
|
||||
if (payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.sadd(RevokedTokensSet, payload.jti, error => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function removeAllRevokedTokens() {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.del(RevokedTokensSet, error => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function getPublicKey() {
|
||||
return keys.public
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createToken,
|
||||
verifyToken,
|
||||
revokeToken,
|
||||
removeAllRevokedTokens,
|
||||
getPublicKey
|
||||
}
|
71
server/BlacklistAPI.js
Normal file
71
server/BlacklistAPI.js
Normal file
@ -0,0 +1,71 @@
|
||||
const db = require('./RedisClient')
|
||||
|
||||
const BlacklistSet = 'blacklisted-packages'
|
||||
|
||||
function addPackage(packageName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.sadd(BlacklistSet, packageName, (error, value) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(value === 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function removePackage(packageName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.srem(BlacklistSet, packageName, (error, value) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(value === 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function removeAllPackages() {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.del(BlacklistSet, error => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function getPackages() {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.smembers(BlacklistSet, (error, value) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(value)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function containsPackage(packageName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.sismember(BlacklistSet, packageName, (error, value) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(value === 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addPackage,
|
||||
removePackage,
|
||||
removeAllPackages,
|
||||
getPackages,
|
||||
containsPackage
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
{
|
||||
"blacklist": [
|
||||
"goodjsproject",
|
||||
"thisoneisevil",
|
||||
"03087dd164d4722425d74e095ff30bc2",
|
||||
"sf1b195d16f3f3c695888e7cde1b20978f",
|
||||
"sfd6e5f9f15adcc48d2fcb380e5aab44e5",
|
||||
"sfd1f03b91ff97ff303bb69254ec3a4fd3",
|
||||
"sf12fefe1d7b5bbe4d5661ff1bf6ea47bb",
|
||||
"sfabae91ef175b31df2d7e77ed948206f7",
|
||||
"sf149006f0b0c1c7e50e81181d9f5eba2d",
|
||||
"sfe6bd27516125ae460d5c2e63feb70c97",
|
||||
"sf09f01e9b87c212046c002a26f5117e87",
|
||||
"sf77de34c6c6f180be3a03226cee219442",
|
||||
"sf00e7081dec64c8557a40a79749e79d6c",
|
||||
"sfb2c291b5ce9ee8cbc0ed4f9e7ab7c3d1",
|
||||
"sf25d1f870f3355f4b02c34e65e451a8ef",
|
||||
"sf18d48571145efc20316195ae19cf7aeb",
|
||||
"sf694f2c2280ca2943d482059797ea1c97",
|
||||
"sf2f8a5346ecda7e03f803b398dd40b869",
|
||||
"sfaeebc97309de527e56215588f9c23dd3",
|
||||
"sf2976f560d7ec7f8b63d7adca6728aed2",
|
||||
"sf67de3bc862ca765e8cb9a72cf3453230",
|
||||
"sfe906b050b2f380096de1c090dafdbb29",
|
||||
"sfc98c62f745bac6d5f04c6e97e8294cec",
|
||||
"sf979c8da13b915e5eaa5d84373a7c4a9b",
|
||||
"sf6039ffb1c35d521773e4dcba7abf446c",
|
||||
"sfb2adb7312558f6dde15d99619ee7da27",
|
||||
"sfcba23f36564e58894e7aeebda67d3682",
|
||||
"sfe0786b97c862c7485d9cec1b912bc634",
|
||||
"sf024cf5fc99edfb929828b09084a9b6af",
|
||||
"sf7d6d35c4dc2d6be739993cf054b00d35",
|
||||
"sf78d0d2fbe897c94458a4822d624c969a",
|
||||
"sf2b1bbe2c61f6f90cda322a61338003af",
|
||||
"sff6a953f5f9960c2a1c907e186778d42f",
|
||||
"sf748f0c277591c02438e9c325a8cb4ff5",
|
||||
"sf606d995c90fd7c9fe17529a4697e4eb6",
|
||||
"sf695459fe0eb159619268fdf542d3bb25",
|
||||
"sf262b2b03ae6881a72365e05451b303ee",
|
||||
"sf8a8a260ab891ae90eae8155548683053",
|
||||
"sf9a307b3da7bfdcbb043c43857310d35b",
|
||||
"sf3e5834662f7c780cbb60ee5740622d12",
|
||||
"sff3a09cde265bde00a96ec01536a97029",
|
||||
"sf364acb7b43655b3496aa65c7d6bb561a",
|
||||
"sf89b5b735e7ba3bdfa52e7ae65026f9aa",
|
||||
"sfca1c0beaf0309c9c322adddaa1cbd40b",
|
||||
"sfef72db8254fe8714860d52da25b25fcc",
|
||||
"sff3114fad67cd8b012706478c4b9fed39",
|
||||
"sf1f60afa19f0a841aed6481bfcd91631c",
|
||||
"sf785d637bd20673dbf2213f77e3df1cba",
|
||||
"sf6142d76572f10a23113135dbee19dcf3",
|
||||
"sf37544977bf1874afa4d4e9e282f2bf4a",
|
||||
"sf0a49054aeba63d7f829eb3d02f0ad942",
|
||||
"sfcce0358fa72b83d85569c22e715a920e",
|
||||
"sf13890e52703bd39f71fb3815e555f0b7",
|
||||
"sfe00cc3e9fec6974a4c1131bdb0ce5ee6",
|
||||
"sf1343e7a08fa0ff25b6d215a3532bde13",
|
||||
"sff4f6781701d8edd0e7909201c356d7c9",
|
||||
"sf3d6dca96e72a14ecaed6ea97549fc088",
|
||||
"sfaec6e7e100bfadbc4cf244adf277da15",
|
||||
"sfa1b8f9052a194f3e9791769ce4cb352f",
|
||||
"sf8cfdc43795d38e3d6ba6a57aae334c29",
|
||||
"sf4606344a64d98d96120d5532b57b2a8b",
|
||||
"sf7bdb20e4d622f6569f3e8503138c859d",
|
||||
"sf0f4536523bf44a482a6bf466707c135e",
|
||||
"sfc4a5204967899f098e8c6486cc60af7d",
|
||||
"sf646dffa91307c680266d8e855b36f0be",
|
||||
"sfa68a6cbd527e17f7e55b574b6a5a53ae",
|
||||
"sff677d3897a4a7090aecfd5e13a6fc90b",
|
||||
"sfbd0ffe85dc79f626ccb492a78aeda94f",
|
||||
"sf37fe85de86dd3973db690594ae8b7bfc",
|
||||
"sf54e90e7565b48823679af97d05189af4",
|
||||
"sfb4ecfd026f1962076f3004cadc11931a",
|
||||
"sfdb26212339dc18d5dd794fa800d4e5a6",
|
||||
"sf4fdb84f60bad69846dd9fc9e2328a4f9",
|
||||
"sfcfc008ec1b2a6daf70571b7480ba6aa3",
|
||||
"sf1566a39461daff958cf2e4291ef13381",
|
||||
"sf8a2b7d68f1c7c7f34381dc1a198465b4",
|
||||
"sf3931b37c61d5b34186ca58f889d48047",
|
||||
"sf109ad06b86e6be8f0c3e94d5e4893f47",
|
||||
"sffefe6195a8b014a1cc7d9cf2449d1b50",
|
||||
"sf85bacaf85076693e911b948b2c02535a",
|
||||
"sf340a5f85afd785510da83f9cabf15726",
|
||||
"sffee5fae47344c13e9d7c6db0bb403b76",
|
||||
"sf4ec8bfe49e5a941b82bd07927f198b5d",
|
||||
"sf2b10045997d4f1f120a5393be267cd52",
|
||||
"sf14d2825be098ee2f80ead23cb181b8e4",
|
||||
"sfc1c052ab23baf866cc73b3c585c65503",
|
||||
"sfed9d0c920ecc6694c82ae859c1699758",
|
||||
"sf15c3851aa68992e9b80ec11211e401bc",
|
||||
"sf9c16721aff8f5ebb4fe7731a409eb622",
|
||||
"sfc65f86a6d8598a4171dec7f4c99fc856",
|
||||
"sf9b9ab3f53b6a705d772ca41a233be838",
|
||||
"sf0d200aa244146e0054f93c7f98c134c8",
|
||||
"sf3d9b13c2b94dea2a11a697d11f3312a8",
|
||||
"sfdf5195f21fffc06298b7c0b4f6bcb9ba",
|
||||
"sff25c5beafcd6f66bc2cc21e84f8aec85",
|
||||
"copyfish-npm-2-8-5",
|
||||
"54e90e7565b48823679af97d05189af4",
|
||||
"15c3851aa68992e9b80ec11211e401bc",
|
||||
"4fdb84f60bad69846dd9fc9e2328a4f9",
|
||||
"14d2825be098ee2f80ead23cb181b8e4",
|
||||
"024cf5fc99edfb929828b09084a9b6af",
|
||||
"8a2b7d68f1c7c7f34381dc1a198465b4",
|
||||
"7bdb20e4d622f6569f3e8503138c859d",
|
||||
"694f2c2280ca2943d482059797ea1c97",
|
||||
"b2c291b5ce9ee8cbc0ed4f9e7ab7c3d1",
|
||||
"9b9ab3f53b6a705d772ca41a233be838",
|
||||
"df5195f21fffc06298b7c0b4f6bcb9ba",
|
||||
"fefe6195a8b014a1cc7d9cf2449d1b50",
|
||||
"fee5fae47344c13e9d7c6db0bb403b76",
|
||||
"2976f560d7ec7f8b63d7adca6728aed2",
|
||||
"d6e5f9f15adcc48d2fcb380e5aab44e5",
|
||||
"c98c62f745bac6d5f04c6e97e8294cec",
|
||||
"abae91ef175b31df2d7e77ed948206f7",
|
||||
"09f01e9b87c212046c002a26f5117e87"
|
||||
]
|
||||
}
|
@ -1,14 +1,17 @@
|
||||
const cf = require('./CloudflareAPI')
|
||||
const db = require('./RedisClient')
|
||||
|
||||
const PackageBlacklist = require('./PackageBlacklist').blacklist
|
||||
const CloudflareAPI = require('./CloudflareAPI')
|
||||
const BlacklistAPI = require('./BlacklistAPI')
|
||||
|
||||
function prunePackages(packagesMap) {
|
||||
PackageBlacklist.forEach(packageName => {
|
||||
delete packagesMap[packageName]
|
||||
})
|
||||
|
||||
return packagesMap
|
||||
return Promise.all(
|
||||
Object.keys(packagesMap).map(packageName =>
|
||||
BlacklistAPI.isBlacklisted(packageName).then(blacklisted => {
|
||||
if (blacklisted) {
|
||||
delete packagesMap[packageName]
|
||||
}
|
||||
})
|
||||
)
|
||||
).then(() => packagesMap)
|
||||
}
|
||||
|
||||
function createDayKey(date) {
|
||||
@ -87,25 +90,25 @@ function sumMaps(maps) {
|
||||
}
|
||||
|
||||
function addDailyMetrics(result) {
|
||||
return Promise.all(result.timeseries.map(addDailyMetricsToTimeseries)).then(
|
||||
() => {
|
||||
result.totals.requests.package = sumMaps(
|
||||
result.timeseries.map(timeseries => {
|
||||
return timeseries.requests.package
|
||||
})
|
||||
)
|
||||
return Promise.all(
|
||||
result.timeseries.map(addDailyMetricsToTimeseries)
|
||||
).then(() => {
|
||||
result.totals.requests.package = sumMaps(
|
||||
result.timeseries.map(timeseries => {
|
||||
return timeseries.requests.package
|
||||
})
|
||||
)
|
||||
|
||||
result.totals.bandwidth.package = sumMaps(
|
||||
result.timeseries.map(timeseries => timeseries.bandwidth.package)
|
||||
)
|
||||
result.totals.bandwidth.package = sumMaps(
|
||||
result.timeseries.map(timeseries => timeseries.bandwidth.package)
|
||||
)
|
||||
|
||||
result.totals.requests.protocol = sumMaps(
|
||||
result.timeseries.map(timeseries => timeseries.requests.protocol)
|
||||
)
|
||||
result.totals.requests.protocol = sumMaps(
|
||||
result.timeseries.map(timeseries => timeseries.requests.protocol)
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
function extractPublicInfo(data) {
|
||||
@ -140,8 +143,12 @@ function extractPublicInfo(data) {
|
||||
const DomainNames = ['unpkg.com', 'npmcdn.com']
|
||||
|
||||
function fetchStats(since, until) {
|
||||
return cf.getZones(DomainNames).then(zones => {
|
||||
return cf.getZoneAnalyticsDashboard(zones, since, until).then(dashboard => {
|
||||
return CloudflareAPI.getZones(DomainNames).then(zones => {
|
||||
return CloudflareAPI.getZoneAnalyticsDashboard(
|
||||
zones,
|
||||
since,
|
||||
until
|
||||
).then(dashboard => {
|
||||
return {
|
||||
timeseries: dashboard.timeseries.map(extractPublicInfo),
|
||||
totals: extractPublicInfo(dashboard.totals)
|
||||
@ -154,14 +161,9 @@ const oneMinute = 1000 * 60
|
||||
const oneHour = oneMinute * 60
|
||||
const oneDay = oneHour * 24
|
||||
|
||||
function getStats(since, until, callback) {
|
||||
let promise = fetchStats(since, until)
|
||||
|
||||
if (until - since > oneDay) promise = promise.then(addDailyMetrics)
|
||||
|
||||
promise.then(value => {
|
||||
callback(null, value)
|
||||
}, callback)
|
||||
function getStats(since, until) {
|
||||
const promise = fetchStats(since, until)
|
||||
return until - since > oneDay ? promise.then(addDailyMetrics) : promise
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
39
server/__tests__/AuthAPI-test.js
Normal file
39
server/__tests__/AuthAPI-test.js
Normal file
@ -0,0 +1,39 @@
|
||||
const AuthAPI = require('../AuthAPI')
|
||||
|
||||
describe('Auth API', () => {
|
||||
beforeEach(done => {
|
||||
AuthAPI.removeAllRevokedTokens().then(() => done(), done)
|
||||
})
|
||||
|
||||
it('creates tokens with the right scopes', done => {
|
||||
const scopes = {
|
||||
blacklist: {
|
||||
add: true,
|
||||
remove: true
|
||||
}
|
||||
}
|
||||
|
||||
AuthAPI.createToken(scopes).then(token => {
|
||||
AuthAPI.verifyToken(token).then(payload => {
|
||||
expect(payload.jti).toEqual(expect.any(String))
|
||||
expect(payload.iss).toEqual(expect.any(String))
|
||||
expect(payload.iat).toEqual(expect.any(Number))
|
||||
expect(payload.scopes).toMatchObject(scopes)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('refuses to verify revoked tokens', done => {
|
||||
const scopes = {}
|
||||
|
||||
AuthAPI.createToken(scopes).then(token => {
|
||||
AuthAPI.revokeToken(token).then(() => {
|
||||
AuthAPI.verifyToken(token).then(payload => {
|
||||
expect(payload).toBe(null)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
24
server/__tests__/BlacklistAPI-test.js
Normal file
24
server/__tests__/BlacklistAPI-test.js
Normal file
@ -0,0 +1,24 @@
|
||||
const BlacklistAPI = require('../BlacklistAPI')
|
||||
|
||||
describe('Blacklist API', () => {
|
||||
beforeEach(done => {
|
||||
BlacklistAPI.removeAllPackages().then(() => done(), done)
|
||||
})
|
||||
|
||||
it('adds and removes packages to/from the blacklist', done => {
|
||||
const packageName = 'bad-package'
|
||||
|
||||
BlacklistAPI.addPackage(packageName).then(() => {
|
||||
BlacklistAPI.getPackages().then(packageNames => {
|
||||
expect(packageNames).toEqual([packageName])
|
||||
|
||||
BlacklistAPI.removePackage(packageName).then(() => {
|
||||
BlacklistAPI.getPackages().then(packageNames => {
|
||||
expect(packageNames).toEqual([])
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
201
server/__tests__/server-test.js
Normal file
201
server/__tests__/server-test.js
Normal file
@ -0,0 +1,201 @@
|
||||
const request = require('supertest')
|
||||
const createServer = require('../createServer')
|
||||
const withBlacklist = require('./utils/withBlacklist')
|
||||
const withRevokedToken = require('./utils/withRevokedToken')
|
||||
const withToken = require('./utils/withToken')
|
||||
|
||||
describe('The server', () => {
|
||||
let server
|
||||
beforeEach(() => {
|
||||
server = createServer()
|
||||
})
|
||||
|
||||
it('rejects invalid package names', done => {
|
||||
request(server)
|
||||
.get('/_invalid/index.js')
|
||||
.end((err, res) => {
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects invalid query params', done => {
|
||||
request(server)
|
||||
.get('/react?main=index&invalid')
|
||||
.end((err, res) => {
|
||||
expect(res.statusCode).toBe(302)
|
||||
expect(res.headers.location).toBe('/react?main=index')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects /_meta to ?meta', done => {
|
||||
request(server)
|
||||
.get('/_meta/react?main=index')
|
||||
.end((err, res) => {
|
||||
expect(res.statusCode).toBe(302)
|
||||
expect(res.headers.location).toBe('/react?main=index&meta')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
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('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('GET /_publicKey', () => {
|
||||
it('echoes the public key', done => {
|
||||
request(server)
|
||||
.get('/_publicKey')
|
||||
.end((err, res) => {
|
||||
expect(res.text).toMatch(/PUBLIC KEY/)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /_blacklist', () => {
|
||||
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.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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
7
server/__tests__/utils/withBlacklist.js
Normal file
7
server/__tests__/utils/withBlacklist.js
Normal file
@ -0,0 +1,7 @@
|
||||
const BlacklistAPI = require('../../BlacklistAPI')
|
||||
|
||||
function withBlacklist(blacklist, callback) {
|
||||
return Promise.all(blacklist.map(BlacklistAPI.addPackage)).then(callback)
|
||||
}
|
||||
|
||||
module.exports = withBlacklist
|
12
server/__tests__/utils/withRevokedToken.js
Normal file
12
server/__tests__/utils/withRevokedToken.js
Normal file
@ -0,0 +1,12 @@
|
||||
const withToken = require('./withToken')
|
||||
const AuthAPI = require('../../AuthAPI')
|
||||
|
||||
function withRevokedToken(scopes, callback) {
|
||||
withToken(scopes, token => {
|
||||
AuthAPI.revokeToken(token).then(() => {
|
||||
callback(token)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = withRevokedToken
|
7
server/__tests__/utils/withToken.js
Normal file
7
server/__tests__/utils/withToken.js
Normal file
@ -0,0 +1,7 @@
|
||||
const AuthAPI = require('../../AuthAPI')
|
||||
|
||||
function withToken(scopes, callback) {
|
||||
AuthAPI.createToken(scopes).then(callback)
|
||||
}
|
||||
|
||||
module.exports = withToken
|
48
server/actions/addToBlacklist.js
Normal file
48
server/actions/addToBlacklist.js
Normal file
@ -0,0 +1,48 @@
|
||||
const validateNpmPackageName = require('validate-npm-package-name')
|
||||
const BlacklistAPI = require('../BlacklistAPI')
|
||||
|
||||
function addToBlacklist(req, res) {
|
||||
const packageName = req.body.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.addPackage(packageName).then(
|
||||
added => {
|
||||
if (added) {
|
||||
const userId = req.user.jti
|
||||
console.log(
|
||||
`Package "${packageName}" was added to the blacklist by ${userId}`
|
||||
)
|
||||
}
|
||||
|
||||
res.set({ 'Content-Location': `/_blacklist/${packageName}` }).send({
|
||||
ok: true,
|
||||
message: `Package "${packageName}" was ${added
|
||||
? 'added to'
|
||||
: 'already in'} the blacklist`
|
||||
})
|
||||
},
|
||||
error => {
|
||||
console.error(error)
|
||||
res.status(500).send({
|
||||
error: `Unable to add "${packageName}" to the blacklist`
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = addToBlacklist
|
24
server/actions/createAuth.js
Normal file
24
server/actions/createAuth.js
Normal file
@ -0,0 +1,24 @@
|
||||
const AuthAPI = require('../AuthAPI')
|
||||
|
||||
const DefaultScopes = {
|
||||
blacklist: {
|
||||
read: true
|
||||
}
|
||||
}
|
||||
|
||||
function createAuth(req, res) {
|
||||
AuthAPI.createToken(DefaultScopes).then(
|
||||
token => {
|
||||
res.send({ token })
|
||||
},
|
||||
error => {
|
||||
console.error(error)
|
||||
|
||||
res.status(500).send({
|
||||
error: 'Unable to generate auth token'
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = createAuth
|
42
server/actions/removeFromBlacklist.js
Normal file
42
server/actions/removeFromBlacklist.js
Normal file
@ -0,0 +1,42 @@
|
||||
const validateNpmPackageName = require('validate-npm-package-name')
|
||||
const BlacklistAPI = require('../BlacklistAPI')
|
||||
|
||||
function removeFromBlacklist(req, res) {
|
||||
const packageName = req.params.packageName
|
||||
|
||||
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 => {
|
||||
if (removed) {
|
||||
const userId = req.user.jti
|
||||
console.log(
|
||||
`Package "${packageName}" was removed from the blacklist by ${userId}`
|
||||
)
|
||||
}
|
||||
|
||||
res.send({
|
||||
ok: true,
|
||||
message: `Package "${packageName}" was ${removed
|
||||
? 'removed from'
|
||||
: 'not in'} the blacklist`
|
||||
})
|
||||
},
|
||||
error => {
|
||||
console.error(error)
|
||||
res.status(500).send({
|
||||
error: `Unable to remove "${packageName}" from the blacklist`
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = removeFromBlacklist
|
5
server/actions/showAuth.js
Normal file
5
server/actions/showAuth.js
Normal file
@ -0,0 +1,5 @@
|
||||
function showAuth(req, res) {
|
||||
res.send({ auth: req.user })
|
||||
}
|
||||
|
||||
module.exports = showAuth
|
17
server/actions/showBlacklist.js
Normal file
17
server/actions/showBlacklist.js
Normal file
@ -0,0 +1,17 @@
|
||||
const BlacklistAPI = require('../BlacklistAPI')
|
||||
|
||||
function showBlacklist(req, res) {
|
||||
BlacklistAPI.getPackages().then(
|
||||
blacklist => {
|
||||
res.send({ blacklist })
|
||||
},
|
||||
error => {
|
||||
console.error(error)
|
||||
res.status(500).send({
|
||||
error: 'Unable to fetch blacklist'
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = showBlacklist
|
7
server/actions/showPublicKey.js
Normal file
7
server/actions/showPublicKey.js
Normal file
@ -0,0 +1,7 @@
|
||||
const AuthAPI = require('../AuthAPI')
|
||||
|
||||
function showPublicKey(req, res) {
|
||||
res.type('text').send(AuthAPI.getPublicKey())
|
||||
}
|
||||
|
||||
module.exports = showPublicKey
|
62
server/actions/showStats.js
Normal file
62
server/actions/showStats.js
Normal file
@ -0,0 +1,62 @@
|
||||
const subDays = require('date-fns/sub_days')
|
||||
const startOfDay = require('date-fns/start_of_day')
|
||||
const startOfSecond = require('date-fns/start_of_second')
|
||||
const StatsAPI = require('../StatsAPI')
|
||||
|
||||
function showStats(req, res) {
|
||||
let since, until
|
||||
switch (req.query.period) {
|
||||
case 'last-day':
|
||||
until = startOfDay(new Date())
|
||||
since = subDays(until, 1)
|
||||
break
|
||||
case 'last-week':
|
||||
until = startOfDay(new Date())
|
||||
since = subDays(until, 7)
|
||||
break
|
||||
case 'last-month':
|
||||
until = startOfDay(new Date())
|
||||
since = subDays(until, 30)
|
||||
break
|
||||
default:
|
||||
until = req.query.until
|
||||
? new Date(req.query.until)
|
||||
: startOfSecond(new Date())
|
||||
since = new Date(req.query.since)
|
||||
}
|
||||
|
||||
if (isNaN(since.getTime())) {
|
||||
return res.status(403).send({ error: '?since is not a valid date' })
|
||||
}
|
||||
|
||||
if (isNaN(until.getTime())) {
|
||||
return res.status(403).send({ error: '?until is not a valid date' })
|
||||
}
|
||||
|
||||
if (until <= since) {
|
||||
return res
|
||||
.status(403)
|
||||
.send({ error: '?until date must come after ?since date' })
|
||||
}
|
||||
|
||||
if (until >= new Date()) {
|
||||
return res.status(403).send({ error: '?until must be a date in the past' })
|
||||
}
|
||||
|
||||
StatsAPI.getStats(since, until).then(
|
||||
stats => {
|
||||
res
|
||||
.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'stats'
|
||||
})
|
||||
.send(stats)
|
||||
},
|
||||
error => {
|
||||
console.error(error)
|
||||
res.status(500).send({ error: 'Unable to fetch stats' })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = showStats
|
@ -1,23 +1,21 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
const cors = require('cors')
|
||||
const morgan = require('morgan')
|
||||
|
||||
const checkBlacklist = require('./middleware/checkBlacklist')
|
||||
const packageURL = require('./middleware/packageURL')
|
||||
const fetchFile = require('./middleware/fetchFile')
|
||||
const parseURL = require('./middleware/parseURL')
|
||||
const requireAuth = require('./middleware/requireAuth')
|
||||
const serveFile = require('./middleware/serveFile')
|
||||
const userToken = require('./middleware/userToken')
|
||||
|
||||
morgan.token('fwd', function(req) {
|
||||
return req.get('x-forwarded-for').replace(/\s/g, '')
|
||||
})
|
||||
|
||||
/**
|
||||
* A list of packages we refuse to serve.
|
||||
*/
|
||||
const PackageBlacklist = require('./PackageBlacklist').blacklist
|
||||
|
||||
function errorHandler(err, req, res, next) {
|
||||
console.error(err.stack)
|
||||
|
||||
@ -47,7 +45,6 @@ function createServer() {
|
||||
}
|
||||
|
||||
app.use(errorHandler)
|
||||
app.use(cors())
|
||||
|
||||
app.use(
|
||||
express.static('build', {
|
||||
@ -55,19 +52,36 @@ function createServer() {
|
||||
})
|
||||
)
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
app.use(cors())
|
||||
app.use(bodyParser.json())
|
||||
app.use(userToken)
|
||||
|
||||
const createStatsServer = require('./createStatsServer')
|
||||
app.use('/_stats', createStatsServer())
|
||||
app.get('/_publicKey', require('./actions/showPublicKey'))
|
||||
|
||||
app.post('/_auth', require('./actions/createAuth'))
|
||||
app.get('/_auth', require('./actions/showAuth'))
|
||||
|
||||
app.post(
|
||||
'/_blacklist',
|
||||
requireAuth('blacklist.add'),
|
||||
require('./actions/addToBlacklist')
|
||||
)
|
||||
app.get(
|
||||
'/_blacklist',
|
||||
requireAuth('blacklist.read'),
|
||||
require('./actions/showBlacklist')
|
||||
)
|
||||
app.delete(
|
||||
'/_blacklist/:packageName',
|
||||
requireAuth('blacklist.remove'),
|
||||
require('./actions/removeFromBlacklist')
|
||||
)
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
app.get('/_stats', require('./actions/showStats'))
|
||||
}
|
||||
|
||||
app.use(
|
||||
'/',
|
||||
packageURL,
|
||||
checkBlacklist(PackageBlacklist),
|
||||
fetchFile,
|
||||
serveFile
|
||||
)
|
||||
app.use('/', parseURL, checkBlacklist, fetchFile, serveFile)
|
||||
|
||||
return app
|
||||
}
|
||||
|
@ -1,38 +0,0 @@
|
||||
const request = require('supertest')
|
||||
const createServer = require('./createServer')
|
||||
|
||||
describe('The server app', function() {
|
||||
let app
|
||||
beforeEach(() => {
|
||||
app = createServer()
|
||||
})
|
||||
|
||||
it('rejects invalid package names', function(done) {
|
||||
request(app)
|
||||
.get('/_invalid/index.js')
|
||||
.then(res => {
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects invalid query params', function(done) {
|
||||
request(app)
|
||||
.get('/react?main=index&invalid')
|
||||
.then(res => {
|
||||
expect(res.statusCode).toBe(302)
|
||||
expect(res.headers.location).toBe('/react?main=index')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects /_meta to ?meta', function(done) {
|
||||
request(app)
|
||||
.get('/_meta/react?main=index')
|
||||
.then(res => {
|
||||
expect(res.statusCode).toBe(302)
|
||||
expect(res.headers.location).toBe('/react?main=index&meta')
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
@ -1,87 +0,0 @@
|
||||
const express = require('express')
|
||||
const subDays = require('date-fns/sub_days')
|
||||
const startOfDay = require('date-fns/start_of_day')
|
||||
const startOfSecond = require('date-fns/start_of_second')
|
||||
const StatsAPI = require('./StatsAPI')
|
||||
|
||||
function serveArbitraryStats(req, res) {
|
||||
const now = startOfSecond(new Date())
|
||||
const since = req.query.since ? new Date(req.query.since) : subDays(now, 30)
|
||||
const until = req.query.until ? new Date(req.query.until) : now
|
||||
|
||||
if (isNaN(since.getTime())) {
|
||||
return res.status(403).send({ error: '?since is not a valid date' })
|
||||
}
|
||||
|
||||
if (isNaN(until.getTime())) {
|
||||
return res.status(403).send({ error: '?until is not a valid date' })
|
||||
}
|
||||
|
||||
if (until <= since) {
|
||||
return res
|
||||
.status(403)
|
||||
.send({ error: '?until date must come after ?since date' })
|
||||
}
|
||||
|
||||
if (until > now) {
|
||||
return res.status(403).send({ error: '?until must be a date in the past' })
|
||||
}
|
||||
|
||||
StatsAPI.getStats(since, until, (error, stats) => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
res.status(500).send({ error: 'Unable to fetch stats' })
|
||||
} else {
|
||||
res
|
||||
.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'stats'
|
||||
})
|
||||
.send(stats)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function servePastDaysStats(days, req, res) {
|
||||
const until = startOfDay(new Date())
|
||||
const since = subDays(until, days)
|
||||
|
||||
StatsAPI.getStats(since, until, (error, stats) => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
res.status(500).send({ error: 'Unable to fetch stats' })
|
||||
} else {
|
||||
res
|
||||
.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'stats'
|
||||
})
|
||||
.send(stats)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function serveLastMonthStats(req, res) {
|
||||
servePastDaysStats(30, req, res)
|
||||
}
|
||||
|
||||
function serveLastWeekStats(req, res) {
|
||||
servePastDaysStats(7, req, res)
|
||||
}
|
||||
|
||||
function serveLastDayStats(req, res) {
|
||||
servePastDaysStats(1, req, res)
|
||||
}
|
||||
|
||||
function createStatsServer() {
|
||||
const app = express.Router()
|
||||
|
||||
app.get('/', serveArbitraryStats)
|
||||
app.get('/last-month', serveLastMonthStats)
|
||||
app.get('/last-week', serveLastWeekStats)
|
||||
app.get('/last-day', serveLastDayStats)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
module.exports = createStatsServer
|
@ -1,15 +1,26 @@
|
||||
function checkBlacklist(blacklist) {
|
||||
return function(req, res, next) {
|
||||
// Do not allow packages that have been blacklisted.
|
||||
if (blacklist.includes(req.packageName)) {
|
||||
res
|
||||
.status(403)
|
||||
.type('text')
|
||||
.send(`Package "${req.packageName}" is blacklisted`)
|
||||
} else {
|
||||
next()
|
||||
const BlacklistAPI = require('../BlacklistAPI')
|
||||
|
||||
function checkBlacklist(req, res, next) {
|
||||
BlacklistAPI.containsPackage(req.packageName).then(
|
||||
blacklisted => {
|
||||
// Disallow packages that have been blacklisted.
|
||||
if (blacklisted) {
|
||||
res
|
||||
.status(403)
|
||||
.type('text')
|
||||
.send(`Package "${req.packageName}" is blacklisted`)
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error(error)
|
||||
|
||||
res.status(500).send({
|
||||
error: 'Unable to fetch the blacklist'
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = checkBlacklist
|
||||
|
@ -1,4 +1,4 @@
|
||||
const validateNPMPackageName = require('validate-npm-package-name')
|
||||
const validateNpmPackageName = require('validate-npm-package-name')
|
||||
const parsePackageURL = require('../utils/parsePackageURL')
|
||||
const createSearch = require('./utils/createSearch')
|
||||
|
||||
@ -29,7 +29,7 @@ function sanitizeQuery(query) {
|
||||
/**
|
||||
* Parse and validate the URL.
|
||||
*/
|
||||
function packageURL(req, res, next) {
|
||||
function parseURL(req, res, next) {
|
||||
// Redirect /_meta/path to /path?meta.
|
||||
if (req.path.match(/^\/_meta\//)) {
|
||||
req.query.meta = ''
|
||||
@ -46,28 +46,30 @@ function packageURL(req, res, next) {
|
||||
// Redirect requests with unknown query params to their equivalents
|
||||
// with only known params so they can be served from the cache. This
|
||||
// prevents people using random query params designed to bust the cache.
|
||||
if (!queryIsKnown(req.query))
|
||||
if (!queryIsKnown(req.query)) {
|
||||
return res.redirect(302, req.path + createSearch(sanitizeQuery(req.query)))
|
||||
}
|
||||
|
||||
const url = parsePackageURL(req.url)
|
||||
|
||||
// Do not allow invalid URLs.
|
||||
if (url == null)
|
||||
// Disallow invalid URLs.
|
||||
if (url == null) {
|
||||
return res
|
||||
.status(403)
|
||||
.type('text')
|
||||
.send(`Invalid URL: ${req.url}`)
|
||||
}
|
||||
|
||||
const nameErrors = validateNPMPackageName(url.packageName).errors
|
||||
const nameErrors = validateNpmPackageName(url.packageName).errors
|
||||
|
||||
// Do not allow invalid package names.
|
||||
if (nameErrors)
|
||||
// Disallow invalid package names.
|
||||
if (nameErrors) {
|
||||
const reason = nameErrors.join(', ')
|
||||
return res
|
||||
.status(403)
|
||||
.type('text')
|
||||
.send(
|
||||
`Invalid package name: ${url.packageName} (${nameErrors.join(', ')})`
|
||||
)
|
||||
.send(`Invalid package name "${url.packageName}" (${reason})`)
|
||||
}
|
||||
|
||||
req.packageName = url.packageName
|
||||
req.packageVersion = url.packageVersion
|
||||
@ -80,4 +82,4 @@ function packageURL(req, res, next) {
|
||||
next()
|
||||
}
|
||||
|
||||
module.exports = packageURL
|
||||
module.exports = parseURL
|
40
server/middleware/requireAuth.js
Normal file
40
server/middleware/requireAuth.js
Normal file
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Adds the given scope to the array in req.auth if the user has sufficient
|
||||
* permissions. Otherwise rejects the request.
|
||||
*/
|
||||
function requireAuth(scope) {
|
||||
let checkScopes
|
||||
if (scope.includes('.')) {
|
||||
const parts = scope.split('.')
|
||||
checkScopes = scopes =>
|
||||
parts.reduce((memo, part) => memo && memo[part], scopes) != null
|
||||
} else {
|
||||
checkScopes = scopes => scopes[scope] != null
|
||||
}
|
||||
|
||||
return function(req, res, next) {
|
||||
if (req.auth && req.auth.includes(scope)) {
|
||||
return next() // Already auth'd
|
||||
}
|
||||
|
||||
const user = req.user
|
||||
|
||||
if (!user) {
|
||||
return res.status(403).send({ error: 'Missing auth token' })
|
||||
}
|
||||
|
||||
if (!user.scopes || !checkScopes(user.scopes)) {
|
||||
return res.status(403).send({ error: 'Insufficient scopes' })
|
||||
}
|
||||
|
||||
if (req.auth) {
|
||||
req.auth.push(scope)
|
||||
} else {
|
||||
req.auth = [scope]
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = requireAuth
|
41
server/middleware/userToken.js
Normal file
41
server/middleware/userToken.js
Normal file
@ -0,0 +1,41 @@
|
||||
const AuthAPI = require('../AuthAPI')
|
||||
|
||||
const ReadMethods = { GET: true, HEAD: true }
|
||||
|
||||
/**
|
||||
* Sets req.user from the payload in the auth token in the request.
|
||||
*/
|
||||
function userToken(req, res, next) {
|
||||
if (req.user) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const token = (ReadMethods[req.method] ? req.query : req.body).token
|
||||
|
||||
if (!token) {
|
||||
req.user = null
|
||||
return next()
|
||||
}
|
||||
|
||||
AuthAPI.verifyToken(token).then(
|
||||
payload => {
|
||||
req.user = payload
|
||||
next()
|
||||
},
|
||||
error => {
|
||||
if (error.name === 'JsonWebTokenError') {
|
||||
res.status(403).send({
|
||||
error: `Bad auth token: ${error.message}`
|
||||
})
|
||||
} else {
|
||||
console.error(error)
|
||||
|
||||
res.status(500).send({
|
||||
error: 'Unable to verify auth'
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = userToken
|
@ -1,4 +1,4 @@
|
||||
const getFileContentType = require('./getFileContentType')
|
||||
const getFileContentType = require('../getFileContentType')
|
||||
|
||||
it('gets a content type of text/plain for LICENSE|README|CHANGES|AUTHORS|Makefile', () => {
|
||||
expect(getFileContentType('AUTHORS')).toBe('text/plain')
|
@ -1,10 +1,10 @@
|
||||
const parsePackageURL = require('./parsePackageURL')
|
||||
const parsePackageURL = require('../parsePackageURL')
|
||||
|
||||
describe('parsePackageURL', () => {
|
||||
it('parses plain packages', () => {
|
||||
expect(parsePackageURL('/history@1.0.0/umd/history.min.js')).toEqual({
|
||||
pathname: '/history@1.0.0/umd/history.min.js',
|
||||
search: null,
|
||||
search: '',
|
||||
query: {},
|
||||
packageName: 'history',
|
||||
packageVersion: '1.0.0',
|
||||
@ -15,7 +15,7 @@ describe('parsePackageURL', () => {
|
||||
it('parses plain packages with a hyphen in the name', () => {
|
||||
expect(parsePackageURL('/query-string@5.0.0/index.js')).toEqual({
|
||||
pathname: '/query-string@5.0.0/index.js',
|
||||
search: null,
|
||||
search: '',
|
||||
query: {},
|
||||
packageName: 'query-string',
|
||||
packageVersion: '5.0.0',
|
||||
@ -26,7 +26,7 @@ describe('parsePackageURL', () => {
|
||||
it('parses plain packages with no version specified', () => {
|
||||
expect(parsePackageURL('/query-string/index.js')).toEqual({
|
||||
pathname: '/query-string/index.js',
|
||||
search: null,
|
||||
search: '',
|
||||
query: {},
|
||||
packageName: 'query-string',
|
||||
packageVersion: 'latest',
|
||||
@ -37,7 +37,7 @@ describe('parsePackageURL', () => {
|
||||
it('parses plain packages with version spec', () => {
|
||||
expect(parsePackageURL('/query-string@>=4.0.0/index.js')).toEqual({
|
||||
pathname: '/query-string@>=4.0.0/index.js',
|
||||
search: null,
|
||||
search: '',
|
||||
query: {},
|
||||
packageName: 'query-string',
|
||||
packageVersion: '>=4.0.0',
|
||||
@ -48,7 +48,7 @@ describe('parsePackageURL', () => {
|
||||
it('parses scoped packages', () => {
|
||||
expect(parsePackageURL('/@angular/router@4.3.3/src/index.d.ts')).toEqual({
|
||||
pathname: '/@angular/router@4.3.3/src/index.d.ts',
|
||||
search: null,
|
||||
search: '',
|
||||
query: {},
|
||||
packageName: '@angular/router',
|
||||
packageVersion: '4.3.3',
|
||||
@ -59,7 +59,7 @@ describe('parsePackageURL', () => {
|
||||
it('parses package names with a period in them', () => {
|
||||
expect(parsePackageURL('/index.js')).toEqual({
|
||||
pathname: '/index.js',
|
||||
search: null,
|
||||
search: '',
|
||||
query: {},
|
||||
packageName: 'index.js',
|
||||
packageVersion: 'latest',
|
Reference in New Issue
Block a user