@vividengine/fastio-cli 0.2.85 → 0.2.87

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 (2) hide show
  1. package/install.js +158 -15
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -1,16 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
- const { execSync } = require("child_process");
4
+ const crypto = require("crypto");
5
5
  const fs = require("fs");
6
6
  const https = require("https");
7
- const http = require("http");
8
7
  const path = require("path");
9
8
  const os = require("os");
9
+ const { URL } = require("url");
10
10
 
11
11
  const REPO = "MediaFire/fastio_cli";
12
12
  const VERSION = require("./package.json").version;
13
13
 
14
+ const MAX_REDIRECTS = 10;
15
+ // Abort a request that goes this long without receiving data.
16
+ const REQUEST_TIMEOUT_MS = 30 * 1000;
17
+ // Upper bound on the whole install, across every download. Generous so slow
18
+ // links still finish; stalled connections are caught by REQUEST_TIMEOUT_MS.
19
+ const OVERALL_TIMEOUT_MS = 20 * 60 * 1000;
20
+
14
21
  const PLATFORM_MAP = {
15
22
  "darwin-arm64": "fastio-darwin-arm64",
16
23
  "darwin-x64": "fastio-darwin-x64",
@@ -46,24 +53,142 @@ function getBinaryPath() {
46
53
  return path.join(dir, isWindows ? "fastio.exe" : "fastio");
47
54
  }
48
55
 
49
- function download(url) {
56
+ /**
57
+ * Parse `sha256sum` output (`<64 hex> <name>`, or `<64 hex> *<name>` in
58
+ * binary mode) into a Map of file name to lowercase hex digest. Blank lines
59
+ * are ignored; any other line that does not match the format, or a name
60
+ * listed twice with different digests, is an error.
61
+ */
62
+ function parseChecksums(text) {
63
+ const sums = new Map();
64
+ const lines = String(text).split("\n");
65
+ for (let i = 0; i < lines.length; i++) {
66
+ const line = lines[i].replace(/\r$/, "");
67
+ if (line.trim() === "") {
68
+ continue;
69
+ }
70
+ const match = /^([0-9a-fA-F]{64}) [ *](.+)$/.exec(line);
71
+ if (!match) {
72
+ throw new Error(`Malformed SHA256SUMS line ${i + 1}`);
73
+ }
74
+ const hash = match[1].toLowerCase();
75
+ const name = match[2];
76
+ if (sums.has(name) && sums.get(name) !== hash) {
77
+ throw new Error(`Conflicting SHA256SUMS entries for ${name}`);
78
+ }
79
+ sums.set(name, hash);
80
+ }
81
+ return sums;
82
+ }
83
+
84
+ /** Return the expected digest for `name`, or throw if it is not listed. */
85
+ function expectedHashFor(sums, name) {
86
+ const hash = sums.get(name);
87
+ if (!hash) {
88
+ throw new Error(`No checksum for ${name} in SHA256SUMS`);
89
+ }
90
+ return hash;
91
+ }
92
+
93
+ function sha256Hex(data) {
94
+ return crypto.createHash("sha256").update(data).digest("hex");
95
+ }
96
+
97
+ /**
98
+ * Throw unless `data` hashes to the digest SHA256SUMS lists for `name`.
99
+ */
100
+ function verifyChecksum(data, checksumsText, name) {
101
+ const expected = expectedHashFor(parseChecksums(checksumsText), name);
102
+ const actual = sha256Hex(data);
103
+ if (actual !== expected) {
104
+ throw new Error(
105
+ `Checksum mismatch for ${name}: expected ${expected}, got ${actual}`
106
+ );
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Resolve a redirect `Location` against the URL that returned it. Only
112
+ * https targets are followed.
113
+ */
114
+ function resolveRedirect(fromUrl, location) {
115
+ const next = new URL(location, fromUrl);
116
+ if (next.protocol !== "https:") {
117
+ throw new Error(`Refusing to follow redirect to non-https URL: ${next.href}`);
118
+ }
119
+ return next.href;
120
+ }
121
+
122
+ function download(url, redirectsLeft = MAX_REDIRECTS) {
50
123
  return new Promise((resolve, reject) => {
51
- const client = url.startsWith("https") ? https : http;
52
- client
53
- .get(url, { headers: { "User-Agent": "fastio-cli-npm" } }, (res) => {
124
+ if (new URL(url).protocol !== "https:") {
125
+ reject(new Error(`Refusing to download from non-https URL: ${url}`));
126
+ return;
127
+ }
128
+ const req = https.get(
129
+ url,
130
+ { headers: { "User-Agent": "fastio-cli-npm" } },
131
+ (res) => {
54
132
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
55
- return download(res.headers.location).then(resolve, reject);
133
+ res.resume();
134
+ if (redirectsLeft <= 0) {
135
+ reject(new Error("Download failed: too many redirects"));
136
+ return;
137
+ }
138
+ let next;
139
+ try {
140
+ next = resolveRedirect(url, res.headers.location);
141
+ } catch (err) {
142
+ reject(err);
143
+ return;
144
+ }
145
+ download(next, redirectsLeft - 1).then(resolve, reject);
146
+ return;
56
147
  }
57
148
  if (res.statusCode !== 200) {
58
- return reject(new Error(`Download failed: HTTP ${res.statusCode}`));
149
+ res.resume();
150
+ reject(new Error(`Download failed: HTTP ${res.statusCode}`));
151
+ return;
59
152
  }
60
153
  const chunks = [];
61
154
  res.on("data", (chunk) => chunks.push(chunk));
62
155
  res.on("end", () => resolve(Buffer.concat(chunks)));
63
156
  res.on("error", reject);
64
- })
65
- .on("error", reject);
157
+ }
158
+ );
159
+ req.setTimeout(REQUEST_TIMEOUT_MS, () => {
160
+ req.destroy(new Error("Download failed: request timed out"));
161
+ });
162
+ req.on("error", reject);
163
+ });
164
+ }
165
+
166
+ function withTimeout(promise, ms, message) {
167
+ let timer;
168
+ const timeout = new Promise((_, reject) => {
169
+ timer = setTimeout(() => reject(new Error(message)), ms);
66
170
  });
171
+ return Promise.race([promise, timeout]).then(
172
+ (value) => {
173
+ clearTimeout(timer);
174
+ return value;
175
+ },
176
+ (err) => {
177
+ clearTimeout(timer);
178
+ throw err;
179
+ }
180
+ );
181
+ }
182
+
183
+ async function install(binaryName, binaryPath) {
184
+ const base = `https://github.com/${REPO}/releases/download/v${VERSION}`;
185
+ const checksums = (await download(`${base}/SHA256SUMS`)).toString("utf8");
186
+ // Fail on a missing or malformed entry before fetching the binary.
187
+ expectedHashFor(parseChecksums(checksums), binaryName);
188
+ const data = await download(`${base}/${binaryName}`);
189
+ verifyChecksum(data, checksums, binaryName);
190
+ fs.writeFileSync(binaryPath, data);
191
+ fs.chmodSync(binaryPath, 0o755);
67
192
  }
68
193
 
69
194
  async function main() {
@@ -78,12 +203,19 @@ async function main() {
78
203
  console.log(`Downloading fastio v${VERSION} for ${getPlatformKey()}...`);
79
204
 
80
205
  try {
81
- const data = await download(url);
82
- fs.writeFileSync(binaryPath, data);
83
- fs.chmodSync(binaryPath, 0o755);
206
+ await withTimeout(
207
+ install(binaryName, binaryPath),
208
+ OVERALL_TIMEOUT_MS,
209
+ "Download failed: timed out"
210
+ );
84
211
  console.log(`Installed fastio to ${binaryPath}`);
85
212
  } catch (err) {
86
- console.error(`Failed to download fastio: ${err.message}`);
213
+ try {
214
+ fs.unlinkSync(binaryPath);
215
+ } catch (_) {
216
+ // Nothing was written.
217
+ }
218
+ console.error(`Failed to install fastio: ${err.message}`);
87
219
  console.error(`URL: ${url}`);
88
220
  console.error("");
89
221
  console.error("You can download manually from:");
@@ -92,4 +224,15 @@ async function main() {
92
224
  }
93
225
  }
94
226
 
95
- main();
227
+ if (require.main === module) {
228
+ main();
229
+ }
230
+
231
+ module.exports = {
232
+ MAX_REDIRECTS,
233
+ parseChecksums,
234
+ expectedHashFor,
235
+ sha256Hex,
236
+ verifyChecksum,
237
+ resolveRedirect,
238
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vividengine/fastio-cli",
3
- "version": "0.2.85",
3
+ "version": "0.2.87",
4
4
  "description": "Command-line interface for the Fast.io cloud storage platform",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {