@cdxoo/npm-lockdown-proxy 0.0.3 → 0.0.5
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 +6 -0
- package/package.json +1 -1
- package/proxy.js +68 -11
package/README.md
CHANGED
|
@@ -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,41 @@ 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
|
+
const needsFilter = isMetadata && pkg !== null && whitelist.get(pkg) !== '*';
|
|
136
|
+
const headers = { ...req.headers, host: url.hostname };
|
|
137
|
+
if (needsFilter) headers['accept-encoding'] = 'identity';
|
|
138
|
+
|
|
101
139
|
const options = {
|
|
102
140
|
hostname: url.hostname,
|
|
103
141
|
port: url.port || 443,
|
|
104
142
|
path: url.pathname + url.search,
|
|
105
143
|
method: req.method,
|
|
106
|
-
headers
|
|
144
|
+
headers,
|
|
107
145
|
};
|
|
108
146
|
|
|
109
147
|
const proxy = https.request(options, (upstream) => {
|
|
110
|
-
|
|
111
|
-
|
|
148
|
+
if (!needsFilter) {
|
|
149
|
+
res.writeHead(upstream.statusCode, upstream.headers);
|
|
150
|
+
upstream.pipe(res);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const chunks = [];
|
|
155
|
+
upstream.on('data', chunk => chunks.push(chunk));
|
|
156
|
+
upstream.on('end', () => {
|
|
157
|
+
const raw = Buffer.concat(chunks);
|
|
158
|
+
if (raw.length === 0) {
|
|
159
|
+
res.writeHead(upstream.statusCode, upstream.headers);
|
|
160
|
+
res.end();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const filtered = filterManifest(raw, whitelist.get(pkg));
|
|
164
|
+
const responseHeaders = { ...upstream.headers, 'content-length': filtered.length };
|
|
165
|
+
delete responseHeaders['transfer-encoding'];
|
|
166
|
+
res.writeHead(upstream.statusCode, responseHeaders);
|
|
167
|
+
res.end(filtered);
|
|
168
|
+
});
|
|
112
169
|
});
|
|
113
170
|
|
|
114
171
|
proxy.on('error', (err) => {
|