Skip to content

Commit

Permalink
feat: provide check api
Browse files Browse the repository at this point in the history
  • Loading branch information
antongolub committed Oct 29, 2023
1 parent b3f115d commit 0189a73
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 26 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,19 @@ Default plugin to filter packages by their fields. May be used directly or via s
}
```

### Checks
To check the specified package version against the applied registry rules trigger its `_check` entrypoint.
For one package:
```bash
curl -X GET -k https://localhost:3000/registry/_check/eventsource/1.1.0
# {"[email protected]":"deny"}
```
To inspect a bulk on entries at once:
```bash
curl -X POST -k https://localhost:3000/registry/_check/bulk -d '["[email protected]"]'
# {"[email protected]":"deny"}
```

### Monitoring
#### /healthcheck
```json
Expand Down
22 changes: 22 additions & 0 deletions src/main/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
proxy,
timeout,
firewall,
advisory,
metrics,
} from './mwares/index.js'
import { loadConfig } from './config.js'
Expand Down Expand Up @@ -58,7 +59,10 @@ export const _createApp = (cfg, {
export const createRoutes = (config) =>
config.firewall.map(({base, entrypoint, registry, token, rules}) => {
const f = firewall({registry, rules, entrypoint, token})
const a = advisory({registry, rules, entrypoint, token})

return createRouter([
// tarball
[
'GET',
[
Expand All @@ -67,6 +71,7 @@ export const createRoutes = (config) =>
],
f
],
// packument
[
'*',
[
Expand All @@ -75,6 +80,23 @@ export const createRoutes = (config) =>
],
f
],
// advisory
[
'GET',
[
/^\/_check\/((?:(@[a-z0-9\-._]+)(?:%2[fF]|\/))?[a-z0-9\-._]+)\/(\d+\.\d+\.\d+(?:-[+\-.a-z0-9_]+)?)\/?$/,
['name', 'org', 'version']
],
a
],
[
'POST',
[
/^\/_check\/bulk\/?$/,
[]
],
a
],
proxy(registry),
errorBoundary,
], base)
Expand Down
26 changes: 19 additions & 7 deletions src/main/js/firewall/middleware.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {httpError, NOT_FOUND, ACCESS_DENIED, METHOD_NOT_ALLOWED, NOT_MODIFIED, OK, FOUND} from '../http/index.js'
import {getPolicy, getPackument, getAssets} from './engine/api.js'
import {getPolicy, getPackument, getAssets, assertPolicy} from './engine/api.js'
import {dropNullEntries, time, jsonBuffer} from '../util.js'
import {gzip} from '../zip.js'
import {hasHit, hasKey, isNoCache} from '../cache.js'
Expand Down Expand Up @@ -41,14 +41,26 @@ const warmupDepPackuments = (name, deps, boundContext, rules, warmup = getConfig
})
}

const getAuth = (token, auth) => token
? token?.startsWith('Bearer')
? token
:`Bearer ${token}`
: auth
export const advisory = ({registry, rules, token}) => async (req, res, next) => {
const {routeParams: {name, version}} = req
const data = version
? [`${name}@${version}`]
: await req.json()

const result = Object.fromEntries(await Promise.all(data.map(async entry => {
const atSepPos = entry.indexOf('@', 1)
const name = entry.slice(0, atSepPos)
const version = entry.slice(atSepPos + 1)

return [entry, await assertPolicy({name, version, registry, rules, token})]
})))

req.timed = true
res.json(result)
}

export const firewall = ({registry, rules, entrypoint, token}) => async (req, res, next) => {
const {routeParams: {name, version, org}, base, method} = req
const {routeParams: {name, version, org}, method} = req
req.timed = true

if (method !== 'GET' && method !== 'HEAD') {
Expand Down
4 changes: 2 additions & 2 deletions src/main/js/firewall/plugins/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const processQueue = async (queue, registry) => {
}

export const getAdvisoriesBatch = async (batch = [], registry) => {
const postData = JSON.stringify(batch.reduce((m, name) => {
const data = JSON.stringify(batch.reduce((m, name) => {
m[name] = ['0.0.0']
return m
}, {}))
Expand All @@ -95,7 +95,7 @@ export const getAdvisoriesBatch = async (batch = [], registry) => {
const {body} = await request({
method: 'POST',
url: `${registry}/-/npm/v1/security/advisories/bulk`,
postData,
data,
headers,
gzip: true
})
Expand Down
20 changes: 11 additions & 9 deletions src/main/js/http/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { logger } from '../logger.js'
import { pushMetric } from '../metric.js'

export const request = async (opts) => {
const {url, headers: _headers, method = 'GET', postData, pipe, gzip: _gzip, skipUnzip, followRedirects, timeout = 30_000, authorization = null} = opts
const {url, headers: _headers, method = 'GET', postData, data, body = postData || data, pipe, gzip: _gzip, skipUnzip, followRedirects, timeout = 30_000, authorization = null} = opts
const {
protocol,
isSecure = protocol === 'https:',
Expand All @@ -24,17 +24,20 @@ export const request = async (opts) => {
const lib = isSecure ? https : http
const agent = getAgent(isSecure)
const {promise, resolve, reject} = makeDeferred()
const data = postData && (_gzip ? await gzip(Buffer.from(postData), {level: zlib.constants.Z_BEST_COMPRESSION}) : Buffer.from(postData))
const _body = body && (_gzip ? await gzip(Buffer.from(body), {level: zlib.constants.Z_BEST_COMPRESSION}) : Buffer.from(body))
const contentEncoding = _gzip && method === 'POST' ? 'gzip' : undefined
const acceptEncoding = _gzip ? 'gzip' : '*'
const contentLength = !contentEncoding && body ? '' + Buffer.byteLength(_body) + '' : undefined
const headers = dropNullEntries({
connection: 'keep-alive',
...pipe?.req?.headers,
..._headers,
host,
authorization,
connection: 'keep-alive',
'content-encoding': _gzip && method === 'POST' ? 'gzip' : undefined,
'accept-encoding': _gzip ? 'gzip' : '*'
'Content-Encoding': contentEncoding,
'content-length': contentLength,
'Accept-Encoding': acceptEncoding
})

const params = {
protocol,
method,
Expand All @@ -45,7 +48,6 @@ export const request = async (opts) => {
agent,
headers
}

logger.debug('HTTP >', method, url)

const s = Date.now()
Expand Down Expand Up @@ -107,8 +109,8 @@ export const request = async (opts) => {
pipe.req.pipe(req, { end: true })//.pipe(pipe.res)

} else {
if (data) {
req.write(data)
if (_body) {
req.write(_body)
}
req.end()
}
Expand Down
4 changes: 3 additions & 1 deletion src/main/js/http/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const PERMANENT_REDIRECT = 301
export const FOUND = 302
export const NOT_MODIFIED = 304
export const TEMPORARY_REDIRECT = 307
export const BAD_REQUEST = 400
export const ACCESS_DENIED = 403
export const NOT_FOUND = 404
export const METHOD_NOT_ALLOWED = 405
Expand All @@ -21,7 +22,8 @@ export const statusMessages = {
[NOT_FOUND]: 'Not Found',
[REQUEST_TIMEOUT]: 'Request Timeout',
[INTERNAL_SERVER_ERROR]: 'Internal Server Error',
[METHOD_NOT_ALLOWED]: 'Method Not Allowed'
[METHOD_NOT_ALLOWED]: 'Method Not Allowed',
[BAD_REQUEST]: 'Bad request',
}

export const httpError = (code = INTERNAL_SERVER_ERROR, {
Expand Down
30 changes: 27 additions & 3 deletions src/main/js/http/server.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import http from 'node:http'
import https from 'node:https'
import { Buffer } from 'node:buffer'

import {makeDeferred} from '../util.js'
import {INTERNAL_SERVER_ERROR, statusMessages} from './error.js'
import {logger} from '../logger.js'
import { makeDeferred } from '../util.js'
import { INTERNAL_SERVER_ERROR, BAD_REQUEST, statusMessages, httpError } from './error.js'
import { logger } from '../logger.js'

const createSocketPool = () => {
const sockets = new Set()
Expand All @@ -30,13 +31,36 @@ const sendJson = function(data, code = 200) {
.end(buffer)
}

const getBody = async function() {
return new Promise((resolve) => {
const body = []
this
.on('data', chunk => {
body.push(chunk)
})
.on('end', () => {
resolve(Buffer.concat(body).toString())
})
})
}
const getJson = async function() {
try {
const body = await this.body()
return JSON.parse(body)
} catch {
throw httpError(BAD_REQUEST)
}
}

export const createServer = ({host, port, secure, router, keepAliveTimeout, headersTimeout, requestTimeout }) => {
const entrypoint = `${secure ? 'https' : 'http'}://${host}:${port}`
const lib = secure ? https : http
const options = {...secure}
const sockets = createSocketPool()
const server = lib.createServer(options, async (req, res) => {
try {
req.body = getBody
req.json = getJson
res.json = sendJson
await router(req, res)

Expand Down
2 changes: 1 addition & 1 deletion src/main/js/mwares/firewall.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { firewall } from '../firewall/index.js'
export { firewall, advisory } from '../firewall/index.js'
2 changes: 1 addition & 1 deletion src/main/js/mwares/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export { trace } from './trace.js'
export { proxy } from './proxy.js'
export { ctx } from './ctx.js'
export { timeout } from './timeout.js'
export { firewall } from './firewall.js'
export { firewall, advisory } from './firewall.js'
export { metrics } from './metrics.js'
14 changes: 12 additions & 2 deletions src/test/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ const cfg = {
"name": "colors",
"version": ">= v1.3.0"
},
{
"plugin": [["npm-registry-firewall/audit", {
"critical": "deny"
}]]
}
]
},
'/block-all': {
Expand Down Expand Up @@ -109,6 +114,11 @@ test('is runnable', async () => {
},
{ statusCode: 304 }
],
[
'provides check API',
{ url: 'http://localhost:3001/registry/_check/bulk', method: 'POST', body: '["[email protected]"]'},
{ statusCode: 200, body: '{"[email protected]":"deny"}' }
],
[
'works as proxy',
{ url: 'http://localhost:3001/npm-proxy/d', method: 'GET'},
Expand All @@ -119,13 +129,13 @@ test('is runnable', async () => {
{ url: 'http://localhost:3001/unknown/d', method: 'GET'},
{ statusCode: 200 }
],
].forEach(([name, {url, method, headers: _headers = {}}, expected]) => {
].forEach(([name, {url, method, body, headers: _headers = {}}, expected]) => {
test(name, async () => {
let result
const headers = typeof _headers === 'function' ? await _headers() : _headers

try {
const res = await request({url, method, headers})
const res = await request({url, method, body, headers, test: 'true'})
const hash = crypto
.createHash('sha512')
.update(res.buffer)
Expand Down

0 comments on commit 0189a73

Please sign in to comment.