@cdxoo/npm-lockdown-proxy 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/package.json +1 -1
- package/proxy.js +63 -11
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npx @cdxoo/npm-lockdown-proxy
|
|
|
16
16
|
|
|
17
17
|
npm install -g @cdxoo/npm-lockdown-proxy
|
|
18
18
|
npm-lockdown-proxy
|
|
19
|
-
npm-lockdown-proxy-
|
|
19
|
+
npm-lockdown-proxy-whitelist-from-lockfile some-package-lock.json [--merge]
|
|
20
20
|
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -28,6 +28,12 @@ npm install <pkg> --registry http://localhost:4873
|
|
|
28
28
|
echo "registry=http://localhost:4873" >> my-project/.npmrc # or ~/.npmrc
|
|
29
29
|
# or
|
|
30
30
|
npm config set registry http://localhost:4873
|
|
31
|
+
|
|
32
|
+
# if you previously installed a version of the same package that is not whitelisted
|
|
33
|
+
# you may hit the local npm cache which will make it fail in this case install with
|
|
34
|
+
npm install --cache /dev/null ...
|
|
35
|
+
# or clear the local cache with
|
|
36
|
+
npm cache clean --force
|
|
31
37
|
```
|
|
32
38
|
|
|
33
39
|
## Server Env Vars
|
package/package.json
CHANGED
package/proxy.js
CHANGED
|
@@ -40,17 +40,19 @@ process.on('SIGHUP', () => {
|
|
|
40
40
|
console.log('whitelist reloaded');
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
// Parse the request pathname into { pkg, version }.
|
|
44
|
-
// pkg
|
|
45
|
-
// version
|
|
43
|
+
// Parse the request pathname into { pkg, version, isMetadata }.
|
|
44
|
+
// pkg - package name (scoped or plain), null for npm-internal paths
|
|
45
|
+
// version - string if this is a tarball request, otherwise null
|
|
46
|
+
// isMetadata - true if this is a bare package metadata request
|
|
46
47
|
function parseRequest(pathname) {
|
|
47
|
-
|
|
48
|
+
pathname = decodeURIComponent(pathname);
|
|
49
|
+
if (pathname.startsWith('/-/')) return { pkg: null, version: null, isMetadata: false };
|
|
48
50
|
|
|
49
51
|
const parts = pathname.slice(1).split('/'); // drop leading /
|
|
50
52
|
let pkg, rest;
|
|
51
53
|
|
|
52
54
|
if (parts[0].startsWith('@')) {
|
|
53
|
-
if (parts.length < 2) return { pkg: null, version: null };
|
|
55
|
+
if (parts.length < 2) return { pkg: null, version: null, isMetadata: false };
|
|
54
56
|
pkg = `${parts[0]}/${parts[1]}`;
|
|
55
57
|
rest = parts.slice(2);
|
|
56
58
|
} else {
|
|
@@ -59,7 +61,6 @@ function parseRequest(pathname) {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
// Tarball path: /pkg/-/pkg-1.2.3.tgz or /@scope/pkg/-/pkg-1.2.3.tgz
|
|
62
|
-
// rest would be ['-', 'pkg-1.2.3.tgz'] at this point
|
|
63
64
|
let version = null;
|
|
64
65
|
if (rest[0] === '-' && rest[1]?.endsWith('.tgz')) {
|
|
65
66
|
const filename = rest[1];
|
|
@@ -70,7 +71,9 @@ function parseRequest(pathname) {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
const isMetadata = version === null && rest.length === 0;
|
|
75
|
+
|
|
76
|
+
return { pkg, version, isMetadata };
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
function deny(res, msg) {
|
|
@@ -79,8 +82,38 @@ function deny(res, msg) {
|
|
|
79
82
|
res.end(body);
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
// Filter a package manifest to only include whitelisted versions.
|
|
86
|
+
// Removes non-allowed entries from versions, time, and dist-tags.
|
|
87
|
+
function filterManifest(body, allowed) {
|
|
88
|
+
const data = JSON.parse(body);
|
|
89
|
+
|
|
90
|
+
for (const v of Object.keys(data.versions || {})) {
|
|
91
|
+
if (!allowed.has(v)) delete data.versions[v];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const k of Object.keys(data.time || {})) {
|
|
95
|
+
if (k !== 'created' && k !== 'modified' && !allowed.has(k)) {
|
|
96
|
+
delete data.time[k];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const [tag, v] of Object.entries(data['dist-tags'] || {})) {
|
|
101
|
+
if (!allowed.has(v)) delete data['dist-tags'][tag];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// If latest was removed, point it at the highest remaining allowed version.
|
|
105
|
+
if (data['dist-tags'] && !data['dist-tags'].latest) {
|
|
106
|
+
const remaining = Object.keys(data.versions);
|
|
107
|
+
if (remaining.length > 0) {
|
|
108
|
+
data['dist-tags'].latest = remaining[remaining.length - 1];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return Buffer.from(JSON.stringify(data));
|
|
113
|
+
}
|
|
114
|
+
|
|
82
115
|
const server = http.createServer((req, res) => {
|
|
83
|
-
const { pkg, version } = parseRequest(req.url.split('?')[0]);
|
|
116
|
+
const { pkg, version, isMetadata } = parseRequest(req.url.split('?')[0]);
|
|
84
117
|
|
|
85
118
|
if (pkg !== null) {
|
|
86
119
|
if (!whitelist.has(pkg)) {
|
|
@@ -98,17 +131,36 @@ const server = http.createServer((req, res) => {
|
|
|
98
131
|
console.log(`ALLOW ${req.method} ${req.url}`);
|
|
99
132
|
|
|
100
133
|
const url = new URL(req.url, UPSTREAM);
|
|
134
|
+
|
|
135
|
+
// When we need to filter the manifest, request uncompressed so we can parse it.
|
|
136
|
+
const needsFilter = isMetadata && pkg !== null && whitelist.get(pkg) !== '*';
|
|
137
|
+
const headers = { ...req.headers, host: url.hostname };
|
|
138
|
+
if (needsFilter) headers['accept-encoding'] = 'identity';
|
|
139
|
+
|
|
101
140
|
const options = {
|
|
102
141
|
hostname: url.hostname,
|
|
103
142
|
port: url.port || 443,
|
|
104
143
|
path: url.pathname + url.search,
|
|
105
144
|
method: req.method,
|
|
106
|
-
headers
|
|
145
|
+
headers,
|
|
107
146
|
};
|
|
108
147
|
|
|
109
148
|
const proxy = https.request(options, (upstream) => {
|
|
110
|
-
|
|
111
|
-
|
|
149
|
+
if (!needsFilter) {
|
|
150
|
+
res.writeHead(upstream.statusCode, upstream.headers);
|
|
151
|
+
upstream.pipe(res);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const chunks = [];
|
|
156
|
+
upstream.on('data', chunk => chunks.push(chunk));
|
|
157
|
+
upstream.on('end', () => {
|
|
158
|
+
const filtered = filterManifest(Buffer.concat(chunks), whitelist.get(pkg));
|
|
159
|
+
const responseHeaders = { ...upstream.headers, 'content-length': filtered.length };
|
|
160
|
+
delete responseHeaders['content-encoding'];
|
|
161
|
+
res.writeHead(upstream.statusCode, responseHeaders);
|
|
162
|
+
res.end(filtered);
|
|
163
|
+
});
|
|
112
164
|
});
|
|
113
165
|
|
|
114
166
|
proxy.on('error', (err) => {
|