electerm 3.1.26 → 3.3.0

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.
@@ -0,0 +1,21 @@
1
+ Copyright (c) Electron contributors
2
+ Copyright (c) 2013-2020 GitHub Inc.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/npm/install.js CHANGED
@@ -5,14 +5,50 @@
5
5
  const os = require('os')
6
6
  const { resolve } = require('path')
7
7
  const { exec, rm, mv } = require('shelljs')
8
- const rp = require('phin').promisified
9
- const download = require('download')
8
+ const { execFile } = require('child_process')
9
+ const { phin, download } = require('./utils')
10
+
10
11
  const plat = os.platform()
11
12
  const arch = os.arch()
12
13
  const { homepage } = require('../package.json')
14
+
13
15
  const releaseInfoUrl = `${homepage}/data/electerm-github-release.json?_=${+new Date()}`
14
16
  const versionUrl = `${homepage}/version.html?_=${+new Date()}`
15
17
 
18
+ // ---------------------------------------------------------------------------
19
+ // Security helpers
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Validate that a version string is a plain semver (e.g. "1.2.3" or "v1.2.3").
24
+ */
25
+ function sanitizeVersion (ver) {
26
+ const clean = String(ver).trim().replace(/^v/, '')
27
+ if (!/^\d+\.\d+\.\d+$/.test(clean)) {
28
+ throw new Error(
29
+ `Refusing to continue: remote version string failed validation: "${ver}"`
30
+ )
31
+ }
32
+ return clean
33
+ }
34
+
35
+ /**
36
+ * Validate that a release asset filename contains only safe characters.
37
+ */
38
+ function sanitizeFilename (name) {
39
+ const clean = String(name).trim()
40
+ if (!/^[\w.-]+$/.test(clean)) {
41
+ throw new Error(
42
+ `Refusing to continue: remote filename failed validation: "${name}"`
43
+ )
44
+ }
45
+ return clean
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Core helpers
50
+ // ---------------------------------------------------------------------------
51
+
16
52
  function down (url, extract = true) {
17
53
  const local = resolve(__dirname, '../')
18
54
  console.log('downloading ' + url)
@@ -22,7 +58,7 @@ function down (url, extract = true) {
22
58
  }
23
59
 
24
60
  function getVer () {
25
- return rp({
61
+ return phin({
26
62
  url: versionUrl,
27
63
  timeout: 15000
28
64
  })
@@ -30,7 +66,7 @@ function getVer () {
30
66
  }
31
67
 
32
68
  function getReleaseInfo (filter) {
33
- return rp({
69
+ return phin({
34
70
  url: releaseInfoUrl,
35
71
  timeout: 15000
36
72
  })
@@ -52,6 +88,10 @@ function showFinalMessage () {
52
88
  console.log('========================================\n')
53
89
  }
54
90
 
91
+ // ---------------------------------------------------------------------------
92
+ // Platform detection helpers
93
+ // ---------------------------------------------------------------------------
94
+
55
95
  // Check if running on Windows 7 or earlier
56
96
  function isWindows7OrEarlier (platform, release) {
57
97
  if (platform !== 'win32') return false
@@ -120,20 +160,30 @@ function getDownloadPattern (platform, architecture, options = {}) {
120
160
  return { pattern: new RegExp(`linux-x64${suffix}\\.tar\\.gz$`), type: `linux-x64${suffix}` }
121
161
  }
122
162
  }
163
+
123
164
  return { pattern: null, type: 'unsupported' }
124
165
  }
125
166
 
167
+ // ---------------------------------------------------------------------------
168
+ // Platform installers
126
169
  async function runLinux (folderName, filePattern) {
127
- const ver = await getVer()
128
- const target = resolve(__dirname, `../electerm-${ver.replace('v', '')}-${folderName}`)
170
+ const rawVer = await getVer()
171
+ const ver = sanitizeVersion(rawVer) // throws if tampered
172
+
173
+ const target = resolve(__dirname, `../electerm-${ver}-${folderName}`)
129
174
  const targetNew = resolve(__dirname, '../electerm')
130
- exec(`rm -rf ${target} ${targetNew}`)
175
+
176
+ // Use shelljs array API — no shell string interpolation
177
+ rm('-rf', [target, targetNew])
178
+
131
179
  const releaseInfo = await getReleaseInfo(r => r.name.includes(filePattern))
132
180
  if (!releaseInfo) {
133
181
  throw new Error(`No release found for pattern: ${filePattern}`)
134
182
  }
183
+
135
184
  await down(releaseInfo.browser_download_url)
136
- exec(`mv ${target} ${targetNew}`)
185
+
186
+ mv(target, targetNew)
137
187
  showFinalMessage()
138
188
  exec('electerm')
139
189
  }
@@ -144,37 +194,48 @@ async function runMac (archName) {
144
194
  if (!releaseInfo) {
145
195
  throw new Error(`No release found for Mac ${archName}`)
146
196
  }
197
+
198
+ const safeName = sanitizeFilename(releaseInfo.name) // throws if tampered
147
199
  await down(releaseInfo.browser_download_url, false)
148
- const target = resolve(__dirname, '../', releaseInfo.name)
200
+
201
+ const target = resolve(__dirname, '../', safeName)
149
202
  showFinalMessage()
150
- exec(`open ${target}`)
203
+
204
+ // execFile does not spawn a shell — no injection possible
205
+ execFile('open', [target])
151
206
  }
152
207
 
153
- // macOS 10.x specific version
154
208
  async function runMac10 () {
155
209
  const releaseInfo = await getReleaseInfo(r => /mac10-x64\.dmg$/.test(r.name))
156
210
  if (!releaseInfo) {
157
211
  throw new Error('No release found for macOS 10.x')
158
212
  }
213
+
214
+ const safeName = sanitizeFilename(releaseInfo.name) // throws if tampered
159
215
  await down(releaseInfo.browser_download_url, false)
160
- const target = resolve(__dirname, '../', releaseInfo.name)
216
+
217
+ const target = resolve(__dirname, '../', safeName)
161
218
  showFinalMessage()
162
- exec(`open ${target}`)
219
+
220
+ // execFile does not spawn a shell — no injection possible
221
+ execFile('open', [target])
163
222
  }
164
223
 
165
224
  async function runWin (archName) {
166
- const ver = await getVer()
167
- const target = resolve(__dirname, `../electerm-${ver.replace('v', '')}-win-${archName}`)
225
+ const rawVer = await getVer()
226
+ const ver = sanitizeVersion(rawVer) // consistent hardening
227
+
228
+ const target = resolve(__dirname, `../electerm-${ver}-win-${archName}`)
168
229
  const targetNew = resolve(__dirname, '../electerm')
169
- rm('-rf', [
170
- target,
171
- targetNew
172
- ])
230
+
231
+ rm('-rf', [target, targetNew])
232
+
173
233
  const pattern = new RegExp(`electerm-\\d+\\.\\d+\\.\\d+-win-${archName}\\.tar\\.gz$`)
174
234
  const releaseInfo = await getReleaseInfo(r => pattern.test(r.name))
175
235
  if (!releaseInfo) {
176
236
  throw new Error(`No release found for Windows ${archName}`)
177
237
  }
238
+
178
239
  await down(releaseInfo.browser_download_url)
179
240
  await mv(target, targetNew)
180
241
  showFinalMessage()
@@ -183,23 +244,29 @@ async function runWin (archName) {
183
244
 
184
245
  // Windows 7 specific version
185
246
  async function runWin7 () {
186
- const ver = await getVer()
187
- const target = resolve(__dirname, `../electerm-${ver.replace('v', '')}-win7`)
247
+ const rawVer = await getVer()
248
+ const ver = sanitizeVersion(rawVer) // consistent hardening
249
+
250
+ const target = resolve(__dirname, `../electerm-${ver}-win7`)
188
251
  const targetNew = resolve(__dirname, '../electerm')
189
- rm('-rf', [
190
- target,
191
- targetNew
192
- ])
252
+
253
+ rm('-rf', [target, targetNew])
254
+
193
255
  const releaseInfo = await getReleaseInfo(r => /electerm-\d+\.\d+\.\d+-win7\.tar\.gz$/.test(r.name))
194
256
  if (!releaseInfo) {
195
257
  throw new Error('No release found for Windows 7')
196
258
  }
259
+
197
260
  await down(releaseInfo.browser_download_url)
198
261
  await mv(target, targetNew)
199
262
  showFinalMessage()
200
263
  require('child_process').execFile(`${targetNew}\\electerm.exe`)
201
264
  }
202
265
 
266
+ // ---------------------------------------------------------------------------
267
+ // Main
268
+ // ---------------------------------------------------------------------------
269
+
203
270
  async function main () {
204
271
  console.log(`Detected platform: ${plat}, architecture: ${arch}`)
205
272
 
@@ -216,34 +283,28 @@ async function main () {
216
283
 
217
284
  try {
218
285
  if (plat === 'win32') {
219
- // Windows: x64, arm64, win7
220
286
  if (win7) {
221
287
  await runWin7()
222
288
  } else if (arch === 'arm64') {
223
289
  await runWin('arm64')
224
290
  } else {
225
- // Default to x64 for all other Windows architectures
226
291
  await runWin('x64')
227
292
  }
228
293
  } else if (plat === 'darwin') {
229
- // macOS: x64, arm64, mac10
230
294
  if (mac10) {
231
295
  await runMac10()
232
296
  } else if (arch === 'arm64') {
233
297
  await runMac('arm64')
234
298
  } else {
235
- // Default to x64 for Intel Macs
236
299
  await runMac('x64')
237
300
  }
238
301
  } else if (plat === 'linux') {
239
- // Linux: x64, arm64, armv7l (with legacy variants)
240
302
  const suffix = linuxLegacy ? '-legacy' : ''
241
303
  if (arch === 'arm64') {
242
304
  await runLinux(`linux-arm64${suffix}`, `linux-arm64${suffix}.tar.gz`)
243
305
  } else if (arch === 'arm') {
244
306
  await runLinux(`linux-armv7l${suffix}`, `linux-armv7l${suffix}.tar.gz`)
245
307
  } else {
246
- // Default to x64 for all other Linux architectures
247
308
  await runLinux(`linux-x64${suffix}`, `linux-x64${suffix}.tar.gz`)
248
309
  }
249
310
  } else {
@@ -261,12 +322,18 @@ async function main () {
261
322
  }
262
323
  }
263
324
 
325
+ // ---------------------------------------------------------------------------
326
+ // Exports
327
+ // ---------------------------------------------------------------------------
328
+
264
329
  // Export functions for testing
265
330
  module.exports = {
266
331
  isWindows7OrEarlier,
267
332
  isMacOS10,
268
333
  isLinuxLegacy,
269
- getDownloadPattern
334
+ getDownloadPattern,
335
+ sanitizeVersion,
336
+ sanitizeFilename
270
337
  }
271
338
 
272
339
  // Run main only if this file is executed directly
package/npm/utils.js ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Utility functions for npm installer
3
+ * Replaces download and phin packages with native Node.js http/https and tar
4
+ * Supports Node.js 16+
5
+ */
6
+
7
+ const https = require('https')
8
+ const http = require('http')
9
+ const fs = require('fs')
10
+ const path = require('path')
11
+ const tar = require('tar')
12
+
13
+ /**
14
+ * Make an HTTP GET request
15
+ * @param {string} url - URL to fetch
16
+ * @param {number} timeout - Request timeout in milliseconds (default: 15000)
17
+ * @returns {Promise<string>} Response body as string
18
+ */
19
+ function httpGet (url, timeout = 15000) {
20
+ return new Promise((resolve, reject) => {
21
+ const client = url.startsWith('https') ? https : http
22
+
23
+ const req = client.get(url, { timeout }, (res) => {
24
+ // Handle redirects
25
+ if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307) {
26
+ if (res.headers.location) {
27
+ resolve(httpGet(res.headers.location, timeout))
28
+ return
29
+ }
30
+ }
31
+
32
+ if (res.statusCode !== 200) {
33
+ reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage || 'Unknown error'}`))
34
+ return
35
+ }
36
+
37
+ const chunks = []
38
+ res.on('data', (chunk) => chunks.push(chunk))
39
+ res.on('end', () => {
40
+ const buffer = Buffer.concat(chunks)
41
+ resolve(buffer.toString())
42
+ })
43
+ res.on('error', reject)
44
+ })
45
+
46
+ req.on('error', reject)
47
+ req.on('timeout', () => {
48
+ req.destroy()
49
+ reject(new Error(`Request timeout after ${timeout}ms`))
50
+ })
51
+ })
52
+ }
53
+
54
+ /**
55
+ * Download a file from URL to local path
56
+ * @param {string} url - URL to download from
57
+ * @param {string} dest - Destination directory path
58
+ * @returns {Promise<string>} Path to downloaded file
59
+ */
60
+ function downloadFile (url, dest) {
61
+ return new Promise((resolve, reject) => {
62
+ const client = url.startsWith('https') ? https : http
63
+
64
+ const req = client.get(url, { timeout: 300000 }, (res) => {
65
+ // Handle redirects
66
+ if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307) {
67
+ if (res.headers.location) {
68
+ resolve(downloadFile(res.headers.location, dest))
69
+ return
70
+ }
71
+ }
72
+
73
+ if (res.statusCode !== 200) {
74
+ reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage || 'Unknown error'}`))
75
+ return
76
+ }
77
+
78
+ // Extract filename from URL or Content-Disposition header
79
+ let filename = 'download'
80
+ const contentDisposition = res.headers['content-disposition']
81
+ if (contentDisposition) {
82
+ const match = contentDisposition.match(/filename[^;=\n]*=(['"]?)([^'";\n]*)\1/)
83
+ if (match) {
84
+ filename = match[2]
85
+ }
86
+ } else {
87
+ const urlParts = url.split('/')
88
+ filename = urlParts[urlParts.length - 1].split('?')[0] || 'download'
89
+ }
90
+
91
+ const filepath = path.join(dest, filename)
92
+ const fileStream = fs.createWriteStream(filepath)
93
+
94
+ res.pipe(fileStream)
95
+ fileStream.on('finish', () => {
96
+ fileStream.close()
97
+ resolve(filepath)
98
+ })
99
+ fileStream.on('error', (err) => {
100
+ fs.unlink(filepath, () => {}) // Clean up partial download
101
+ reject(err)
102
+ })
103
+ })
104
+
105
+ req.on('error', reject)
106
+ req.on('timeout', () => {
107
+ req.destroy()
108
+ reject(new Error('Download timeout after 300s'))
109
+ })
110
+ })
111
+ }
112
+
113
+ /**
114
+ * Extract a tar.gz file to destination directory
115
+ * @param {string} filepath - Path to tar.gz file
116
+ * @param {string} dest - Destination directory
117
+ * @returns {Promise<void>}
118
+ */
119
+ function extractTarGz (filepath, dest) {
120
+ return tar.extract({
121
+ file: filepath,
122
+ cwd: dest,
123
+ strip: 1 // Strip top-level directory
124
+ })
125
+ }
126
+
127
+ /**
128
+ * Download and optionally extract a file
129
+ * Replaces the download package functionality
130
+ * @param {string} url - URL to download from
131
+ * @param {string} dest - Destination directory
132
+ * @param {boolean} extract - Whether to extract the file (default: true)
133
+ * @returns {Promise<void>}
134
+ */
135
+ async function download (url, dest, { extract: doExtract = true } = {}) {
136
+ console.log('downloading ' + url)
137
+
138
+ const filepath = await downloadFile(url, dest)
139
+
140
+ if (doExtract && (filepath.endsWith('.tar.gz') || filepath.endsWith('.tgz'))) {
141
+ await extractTarGz(filepath, dest)
142
+ // Clean up the downloaded archive
143
+ try {
144
+ fs.unlinkSync(filepath)
145
+ } catch (err) {
146
+ console.warn('Warning: Failed to clean up downloaded archive:', err.message)
147
+ }
148
+ }
149
+
150
+ console.log('done!')
151
+ }
152
+
153
+ /**
154
+ * Phin replacement - simple promisified HTTP client
155
+ * @param {object} options - Request options
156
+ * @param {string} options.url - URL to fetch
157
+ * @param {number} options.timeout - Request timeout (default: 15000)
158
+ * @returns {Promise<{body: string, statusCode: number, headers: object}>}
159
+ */
160
+ async function phin (options) {
161
+ const { url, timeout = 15000 } = options
162
+ const body = await httpGet(url, timeout)
163
+
164
+ return {
165
+ body: Buffer.from(body),
166
+ statusCode: 200,
167
+ headers: {}
168
+ }
169
+ }
170
+
171
+ // Export promisified version
172
+ phin.promisified = phin
173
+
174
+ module.exports = {
175
+ httpGet,
176
+ downloadFile,
177
+ extractTarGz,
178
+ download,
179
+ phin
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electerm",
3
- "version": "3.1.26",
3
+ "version": "3.3.0",
4
4
  "description": "Terminal/ssh/telnet/serialport/sftp client(linux, mac, win)",
5
5
  "main": "app.js",
6
6
  "bin": "npm/electerm",
@@ -28,8 +28,7 @@
28
28
  "preferGlobal": true,
29
29
  "dependencies": {
30
30
  "shelljs": "*",
31
- "phin": "*",
32
- "download": "*"
31
+ "tar": "*"
33
32
  },
34
33
  "files": [
35
34
  "npm",