@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.
Files changed (3) hide show
  1. package/README.md +7 -1
  2. package/package.json +1 -1
  3. 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-create-whitelist-form-lockfile some-package-lock.json [--merge]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cdxoo/npm-lockdown-proxy",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Minimal npm registry proxy with package/version whitelisting",
5
5
  "bin": {
6
6
  "npm-lockdown-proxy": "./proxy.js",
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 - package name (scoped or plain), null for npm-internal paths
45
- // version - string if this is a tarball request, otherwise null
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
- if (pathname.startsWith('/-/')) return { pkg: null, version: null };
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
- return { pkg, version };
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: { ...req.headers, host: url.hostname },
145
+ headers,
107
146
  };
108
147
 
109
148
  const proxy = https.request(options, (upstream) => {
110
- res.writeHead(upstream.statusCode, upstream.headers);
111
- upstream.pipe(res);
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) => {