git-userhub 3.0.1 → 3.0.2

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/bin/git-user CHANGED
Binary file
package/bin/git-user.js CHANGED
@@ -5,6 +5,7 @@ const https = require('https');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const os = require('os');
8
+ const crypto = require('crypto');
8
9
  const tar = require('tar');
9
10
  const pkg = require('../package.json');
10
11
 
@@ -34,25 +35,71 @@ function getPlatform() {
34
35
  };
35
36
  }
36
37
 
37
- // Download file from URL
38
- function download(url, dest) {
38
+ // Download file to memory (for checksums.txt)
39
+ function fetchText(url, redirectCount = 0) {
39
40
  return new Promise((resolve, reject) => {
41
+ if (redirectCount > 3) return reject(new Error('Too many redirects'));
42
+
43
+ const parsedUrl = new URL(url);
44
+ if (parsedUrl.protocol !== 'https:') {
45
+ return reject(new Error('Only HTTPS is allowed'));
46
+ }
47
+
48
+ https.get(url, { headers: { 'User-Agent': 'git-user-cli' } }, (res) => {
49
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
50
+ return fetchText(res.headers.location, redirectCount + 1).then(resolve).catch(reject);
51
+ }
52
+ if (res.statusCode !== 200) {
53
+ return reject(new Error(`Failed to fetch: ${res.statusCode} ${url}`));
54
+ }
55
+ let data = '';
56
+ res.on('data', chunk => data += chunk);
57
+ res.on('end', () => resolve(data));
58
+ }).on('error', reject);
59
+ });
60
+ }
61
+
62
+ // Download file from URL, and verify hash
63
+ function downloadAndVerify(url, dest, expectedHash, redirectCount = 0) {
64
+ return new Promise((resolve, reject) => {
65
+ if (redirectCount > 3) return reject(new Error('Too many redirects'));
66
+
67
+ const parsedUrl = new URL(url);
68
+ if (parsedUrl.protocol !== 'https:') {
69
+ return reject(new Error('Only HTTPS is allowed'));
70
+ }
71
+
40
72
  const file = fs.createWriteStream(dest);
73
+ const hash = crypto.createHash('sha256');
41
74
 
42
- https.get(url, (response) => {
43
- if (response.statusCode === 302 || response.statusCode === 301) {
44
- return download(response.headers.location, dest).then(resolve).catch(reject);
75
+ https.get(url, { headers: { 'User-Agent': 'git-user-cli' } }, (response) => {
76
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
77
+ file.close();
78
+ fs.unlink(dest, () => {});
79
+ return downloadAndVerify(response.headers.location, dest, expectedHash, redirectCount + 1)
80
+ .then(resolve)
81
+ .catch(reject);
45
82
  }
46
83
  if (response.statusCode !== 200) {
47
- reject(new Error(`Failed to download: ${response.statusCode}`));
48
- return;
84
+ file.close();
85
+ fs.unlink(dest, () => {});
86
+ return reject(new Error(`Failed to download: ${response.statusCode} ${url}`));
49
87
  }
88
+
89
+ response.on('data', chunk => hash.update(chunk));
50
90
  response.pipe(file);
91
+
51
92
  file.on('finish', () => {
52
93
  file.close();
94
+ const actualHash = hash.digest('hex');
95
+ if (actualHash !== expectedHash) {
96
+ fs.unlink(dest, () => {});
97
+ return reject(new Error(`Checksum mismatch! Expected ${expectedHash}, got ${actualHash}`));
98
+ }
53
99
  resolve();
54
100
  });
55
101
  }).on('error', (err) => {
102
+ file.close();
56
103
  fs.unlink(dest, () => {});
57
104
  reject(err);
58
105
  });
@@ -71,6 +118,9 @@ function getRelease() {
71
118
  };
72
119
 
73
120
  https.get(options, (res) => {
121
+ if (res.statusCode !== 200) {
122
+ return reject(new Error(`GitHub API returned ${res.statusCode} for release v${pkg.version}`));
123
+ }
74
124
  let data = '';
75
125
  res.on('data', (chunk) => data += chunk);
76
126
  res.on('end', () => {
@@ -116,13 +166,39 @@ async function installAndRun() {
116
166
  console.error(` Looking for a ${osName} binary matching architecture: ${arch}`);
117
167
  process.exit(1);
118
168
  }
169
+
170
+ console.log('🔐 Fetching checksums...');
171
+ // Fetch checksums.txt from the release assets or release tag
172
+ // GitHub Releases typically attach checksums.txt if goreleaser is used
173
+ const checksumAsset = release.assets?.find(a => a.name === 'checksums.txt');
174
+ let checksumsUrl = '';
175
+ if (checksumAsset) {
176
+ checksumsUrl = checksumAsset.browser_download_url;
177
+ } else {
178
+ // Fallback pattern if asset list doesn't have it, try direct URL
179
+ checksumsUrl = `https://github.com/${REPO}/releases/download/v${pkg.version}/checksums.txt`;
180
+ }
119
181
 
120
- console.log(`⬇️ Downloading ${asset.name}...`);
182
+ const checksumsText = await fetchText(checksumsUrl);
183
+
184
+ // Parse checksums.txt to find the hash for our asset
185
+ const expectedHashLine = checksumsText.split('\n').find(line => line.includes(asset.name));
186
+ if (!expectedHashLine) {
187
+ throw new Error(`Checksum for ${asset.name} not found in checksums.txt`);
188
+ }
189
+ const expectedHash = expectedHashLine.trim().split(/\s+/)[0];
190
+
191
+ console.log(`⬇️ Downloading ${asset.name} (verifying SHA256 checksum)...`);
121
192
  const archivePath = path.join(BIN_DIR, asset.name);
122
- await download(asset.browser_download_url, archivePath);
193
+ await downloadAndVerify(asset.browser_download_url, archivePath, expectedHash);
194
+
195
+ console.log('📂 Extracting securely...');
196
+ await tar.extract({
197
+ file: archivePath,
198
+ cwd: BIN_DIR,
199
+ filter: (p) => p === `git-user${ext}` || p === `./git-user${ext}`
200
+ });
123
201
 
124
- console.log('📂 Extracting...');
125
- await tar.extract({ file: archivePath, cwd: BIN_DIR });
126
202
  fs.unlinkSync(archivePath);
127
203
 
128
204
  if (fs.existsSync(binaryPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-userhub",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "Switch Git accounts in one command. No config editing. No SSH key chaos.",
5
5
  "bin": {
6
6
  "git-user": "bin/git-user.js"
package/test_tar.js ADDED
@@ -0,0 +1,44 @@
1
+ const fs = require('fs');
2
+ const crypto = require('crypto');
3
+ const tar = require('tar');
4
+
5
+ const assetName = "git-user_darwin_arm64.tar.gz";
6
+
7
+ async function main() {
8
+ console.log("Creating dummy tar file using node-tar...");
9
+ // create dummy git-user
10
+ fs.writeFileSync('git-user', 'dummy binary');
11
+ fs.writeFileSync('malicious.sh', 'echo bad');
12
+
13
+ await tar.create({
14
+ gzip: true,
15
+ file: assetName,
16
+ }, ['git-user', 'malicious.sh']);
17
+
18
+ console.log("Tar created.");
19
+
20
+ const file = fs.readFileSync(assetName);
21
+ const expectedHash = crypto.createHash('sha256').update(file).digest('hex');
22
+
23
+ fs.unlinkSync('git-user');
24
+ fs.unlinkSync('malicious.sh');
25
+
26
+ console.log("Expected hash:", expectedHash);
27
+
28
+ // Now extract securely
29
+ fs.mkdirSync('bin', { recursive: true });
30
+
31
+ await tar.extract({
32
+ file: assetName,
33
+ cwd: 'bin',
34
+ filter: (p, entry) => {
35
+ console.log('Filtering path:', p, entry.path);
36
+ return p === 'git-user' || p === 'git-user.exe' || p === './git-user' || p === './git-user.exe';
37
+ }
38
+ });
39
+
40
+ const binContents = fs.readdirSync('bin');
41
+ console.log("Bin contents:", binContents);
42
+ }
43
+
44
+ main().catch(console.error);