unplugin-https-reverse-proxy 1.0.0-beta.2 → 1.0.0-beta.20

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.
@@ -1,15 +1,16 @@
1
1
  import { platform } from 'node:os'
2
2
  import process from 'node:process'
3
- import { chmodSync, createWriteStream, existsSync, unlinkSync } from 'node:fs'
3
+ import { chmodSync, createWriteStream, existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
4
4
  import { spawn } from 'node:child_process'
5
+ import { readFile, unlink, writeFile } from 'node:fs/promises'
5
6
  import { got } from 'got-cjs'
6
7
  import { HttpProxyAgent } from 'http-proxy-agent'
7
8
  import { HttpsProxyAgent } from 'https-proxy-agent'
8
- import kill from 'kill-port'
9
- import { consola } from '../utils'
9
+ import * as lockfile from 'proper-lockfile'
10
+ import { chmodRecursive, consola, once } from '../utils'
10
11
  import { addHost, removeHost } from '../host'
11
- import { caddyPath, supportList } from './constants'
12
- import { logProgress, logProgressOver, tryPort } from './utils'
12
+ import { TEMP_DIR, caddyFilePath, caddyLockFilePath, caddyPath, supportList } from './constants'
13
+ import { kill, logProgress, logProgressOver, tryPort } from './utils'
13
14
 
14
15
  export async function download() {
15
16
  if (await testCaddy())
@@ -18,6 +19,11 @@ export async function download() {
18
19
  return new Promise<string>((resolve, reject) => {
19
20
  // https://caddyserver.com/api/download?os=darwin&arch=amd64
20
21
 
22
+ if (!existsSync(TEMP_DIR)) {
23
+ mkdirSync(TEMP_DIR, { recursive: true })
24
+ chmodSync(TEMP_DIR, 0o777)
25
+ }
26
+
21
27
  const file = createWriteStream(caddyPath)
22
28
 
23
29
  let _platform = platform().toString()
@@ -42,6 +48,11 @@ export async function download() {
42
48
  const httpAgent = httpProxy ? new HttpProxyAgent(httpProxy) : undefined
43
49
  const httpsAgent = httpsProxy ? new HttpsProxyAgent(httpsProxy) : undefined
44
50
 
51
+ const chmodCaddyOnce = once(() => {
52
+ // chmod +x
53
+ chmodSync(caddyPath, 0o777)
54
+ })
55
+
45
56
  got.stream(dowmloadLink, {
46
57
  agent: {
47
58
  http: httpAgent,
@@ -49,12 +60,13 @@ export async function download() {
49
60
  },
50
61
  }).on('downloadProgress', (progress) => {
51
62
  logProgress(progress.percent)
63
+ chmodCaddyOnce()
52
64
  }).pipe(file).on('finish', () => {
53
65
  logProgressOver()
54
66
  if (process.platform === 'win32')
55
67
  return resolve(caddyPath)
56
68
  // chmod +x
57
- chmodSync(caddyPath, 0o755)
69
+ chmodSync(caddyPath, 0o777)
58
70
  resolve(caddyPath)
59
71
  }).on('error', (err) => {
60
72
  // consola.error(err)
@@ -67,7 +79,7 @@ function testCaddy() {
67
79
  return new Promise((resolve, reject) => {
68
80
  if (!existsSync(caddyPath))
69
81
  return resolve(false)
70
- chmodSync(caddyPath, 0o755)
82
+ chmodSync(caddyPath, 0o777)
71
83
  const child = process.platform === 'win32' ? spawn(caddyPath, []) : spawn('sudo', ['-E', caddyPath])
72
84
  child.on('close', () => {
73
85
  return resolve(false)
@@ -92,10 +104,57 @@ export class CaddyInstant {
92
104
  private inited = false
93
105
  private stoped = false
94
106
 
107
+ /** has locked */
108
+ private locked = false
109
+ private caddyfile: string | undefined
110
+
111
+ private _caddyChild: ReturnType<typeof spawn> | undefined
112
+
113
+ private _source: string | undefined
114
+ private _target: string | undefined
115
+
95
116
  constructor() {
96
- testCaddy().then(() => {
117
+ testCaddy().then(async () => {
118
+ await this.getCaddyfile()
119
+
97
120
  this.inited = true
98
121
  })
122
+
123
+ this.locked = this.checkLock()
124
+ }
125
+
126
+ checkLock() {
127
+ if (!existsSync(caddyLockFilePath))
128
+ writeFileSync(caddyLockFilePath, '')
129
+ return lockfile.checkSync(caddyLockFilePath)
130
+ }
131
+
132
+ async getCaddyfile() {
133
+ if (!existsSync(caddyFilePath))
134
+ await this.initCaddyfile()
135
+
136
+ this.caddyfile = await readFile(caddyFilePath, 'utf-8')
137
+ return this.caddyfile
138
+ }
139
+
140
+ async initCaddyfile() {
141
+ const contet = `
142
+ {
143
+ debug
144
+ auto_https disable_redirects
145
+ }
146
+
147
+ `
148
+ await writeFile(caddyFilePath, contet)
149
+ this.caddyfile = contet
150
+ }
151
+
152
+ async writeCaddyfile() {
153
+ return await writeFile(caddyFilePath, this.caddyfile || '')
154
+ }
155
+
156
+ async deleteCaddyfile() {
157
+ return await unlink(caddyFilePath)
99
158
  }
100
159
 
101
160
  async init() {
@@ -117,95 +176,190 @@ export class CaddyInstant {
117
176
  https = false,
118
177
  } = options || {}
119
178
 
179
+ this._source = source
180
+ this._target = target
181
+
120
182
  if (!this.inited)
121
183
  await this.init()
122
184
 
123
- if (https && await tryPort(443))
124
- await kill(443, 'tcp')
125
- if ((process.platform === 'win32' || !https) && await tryPort(80))
126
- await kill(80, 'tcp')
127
-
128
185
  if (!await addHost(source.split(':')[0], target.split(':')[0]))
129
186
  throw new Error('update host failed')
130
187
  consola.success('update host success\n')
131
188
 
189
+ await this.getCaddyfile()
190
+
191
+ if (!(this.caddyfile || '').includes(`${target.split(':')[0]}${https ? '' : ':80'}`)) {
192
+ this.caddyfile = `${this.caddyfile}
193
+ ${target.split(':')[0]}${https ? '' : ':80'} {
194
+ ${https ? 'tls internal' : ''}
195
+ reverse_proxy http://${source} {
196
+ health_uri /
197
+ health_interval 2s
198
+ health_timeout 5s
199
+ fail_duration 2s
200
+ unhealthy_status 502 503 504 404
201
+ lb_try_duration 3s
202
+ lb_try_interval 300ms
203
+
204
+ @error status 502 503 504 404
205
+ handle_response @error {
206
+ respond * 503 {
207
+ body "Service Unavailable"
208
+ close
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ `
215
+
216
+ await this.writeCaddyfile()
217
+ }
218
+ if (!existsSync(caddyLockFilePath))
219
+ writeFileSync(caddyLockFilePath, '')
220
+
221
+ if (!this.locked) {
222
+ if (await tryPort(443))
223
+ await kill(443)
224
+ if ((process.platform === 'win32' || !https) && await tryPort(80))
225
+ await kill(80)
226
+ }
227
+
132
228
  return new Promise<void>((resolve, reject) => {
133
- // caddy reverse-proxy --from target --to source --internal-certs
134
- const child = process.platform !== 'win32'
135
- ? spawn('sudo', ['-E', caddyPath, 'reverse-proxy', '--from', `${target.split(':')[0]}${https ? '' : ':80'}`, '--to', `${source}`, '--internal-certs', '--insecure', '--disable-redirects'])
136
- : spawn(caddyPath, ['reverse-proxy', '--from', `${target.split(':')[0]}`, '--to', `${source}`, '--internal-certs'])
229
+ const setupCleanup = () => {
230
+ process.on('SIGINT', async () => {
231
+ if (this.stoped)
232
+ return
137
233
 
138
- child.on('error', (err) => {
139
- return reject(err)
140
- })
234
+ process.stdin.resume()
141
235
 
142
- process.on('SIGINT', async () => {
143
- if (!restore || this.stoped)
144
- return
236
+ try {
237
+ await this.baseCleanup(restore)
238
+ }
239
+ finally {
240
+ this.stoped = true
241
+ process.nextTick(() => {
242
+ process.exit()
243
+ })
244
+ }
245
+ })
145
246
 
146
- try {
147
- if (await removeHost(source.split(':')[0], target.split(':')[0]))
148
- consola.success('restore host success\n')
247
+ const originalExit = process.exit.bind(process)
149
248
 
150
- else
151
- consola.fail('restore host failed\n')
152
- }
153
- catch (e) {
154
- consola.error(e)
155
- consola.fail('restore host failed\n')
156
- }
157
- finally {
158
- this.stoped = true
159
- process.nextTick(() => {
160
- process.exit()
161
- })
162
- }
163
- })
249
+ // @ts-expect-error override
250
+ process.exit = async (code?: number) => {
251
+ const port = Number(source.split(':')[1])
252
+ if (!Number.isNaN(port) && await tryPort(port))
253
+ await kill(port)
164
254
 
165
- const originalExit = process.exit.bind(process)
255
+ // fix `Error: EPERM: operation not permitted`
256
+ const pwd = process.cwd()
257
+ const viteCacheDir = `${pwd}/node_modules/.vite`
258
+ if (existsSync(viteCacheDir))
259
+ await chmodRecursive(viteCacheDir, 0o777).catch(() => { })
260
+ const nuxtCacheDir = `${pwd}/.nuxt`
261
+ if (existsSync(nuxtCacheDir))
262
+ await chmodRecursive(nuxtCacheDir, 0o777).catch(() => { })
263
+ const webpackCacheDir = `${pwd}/node_modules/.cache`
264
+ if (existsSync(webpackCacheDir))
265
+ await chmodRecursive(webpackCacheDir, 0o777).catch(() => { })
166
266
 
167
- // @ts-expect-error override
168
- process.exit = async (code?: number) => {
169
- if (!restore || this.stoped)
170
- return originalExit(code)
267
+ if (this.stoped)
268
+ return originalExit(code)
171
269
 
172
- try {
173
- if (await removeHost(source.split(':')[0], target.split(':')[0]))
174
- consola.success('restore host success\n')
270
+ process.stdin.resume()
175
271
 
176
- else
177
- consola.fail('restore host failed\n')
178
- }
179
- catch (e) {
180
- consola.error(e)
181
- consola.fail('restore host failed\n')
182
- }
183
- finally {
184
- this.stoped = true
185
- process.nextTick(() => {
186
- originalExit(code)
187
- })
272
+ try {
273
+ await this.baseCleanup()
274
+ }
275
+
276
+ finally {
277
+ this.stoped = true
278
+ process.nextTick(() => {
279
+ originalExit(code)
280
+ })
281
+ }
188
282
  }
189
283
  }
190
284
 
191
- child.stderr.on('data', (data) => {
192
- const lines = (data.toString() as string).split('\n').map(line => line.trim())
193
- for (const line of lines) {
194
- // caddy log
195
- // eslint-disable-next-line no-console
196
- showCaddyLog && line && console.info(line)
197
- if (line.includes('Error:') || (line && JSON.parse(line).level === 'error')) {
198
- consola.error(line)
199
- // child.kill()
200
- return reject(line)
285
+ if (!this.locked) {
286
+ this._caddyChild = process.platform !== 'win32'
287
+ ? spawn('sudo', ['-E', caddyPath, 'run', '--config', caddyFilePath, '--watch'])
288
+ : spawn(caddyPath, ['run', '--config', caddyFilePath, '--watch'])
289
+
290
+ lockfile.lockSync(caddyLockFilePath)
291
+
292
+ this._caddyChild!.on('error', (err) => {
293
+ return reject(err)
294
+ })
295
+
296
+ this._caddyChild!.stderr?.on('data', async (data) => {
297
+ const lines = (data.toString() as string).split('\n').map(line => line.trim())
298
+ for (const line of lines) {
299
+ // caddy log
300
+ // eslint-disable-next-line no-console
301
+ showCaddyLog && line && console.info(line)
302
+ if (line.includes('Error:') || (line && JSON.parse(line).level === 'error')) {
303
+ if (line.includes('parsing caddyfile tokens')) {
304
+ consola.error('Caddyfile syntax error, please contact the author:\n', `${this.caddyfile}\n\n${line}\n`)
305
+ await this.baseCleanup()
306
+ process.exit()
307
+ }
308
+ else {
309
+ consola.error(`${line}\n`)
310
+ // child.kill()
311
+ return reject(line)
312
+ }
313
+ }
314
+ if (line.includes('config file changed'))
315
+ consola.success('Detect added a new reverse proxy config, restarted.\n')
316
+
317
+ if (line.includes('caddy proxying') || line.includes('serving initial'))
318
+ return resolve()
201
319
  }
202
- }
203
- })
320
+ })
321
+
322
+ this._caddyChild!.stdout?.on('data', (_data) => {
323
+ consola.info(_data.toString())
324
+ resolve()
325
+ })
326
+
327
+ setupCleanup()
328
+ }
329
+ else {
330
+ consola.info('Caddy is running in another process.')
331
+
332
+ setupCleanup()
204
333
 
205
- child.stdout.on('data', (_data) => {
206
- consola.info(_data.toString())
207
- resolve()
208
- })
334
+ return resolve()
335
+ }
209
336
  })
210
337
  }
338
+
339
+ async baseCleanup(restore: boolean = true) {
340
+ try {
341
+ if (restore && this._source && this._target) {
342
+ if (await removeHost(this._source.split(':')[0], this._target.split(':')[0]))
343
+ consola.success('Restore host success.\n')
344
+
345
+ else
346
+ consola.fail('Restore host failed.\n')
347
+ }
348
+ }
349
+ catch (e) {
350
+ consola.error(e)
351
+ consola.fail('Restore host failed.\n')
352
+ }
353
+ if (!this.locked && !this.stoped) {
354
+ this._caddyChild?.kill()
355
+ try {
356
+ await lockfile.unlock(caddyLockFilePath).catch(() => { })
357
+ await unlink(caddyFilePath).catch(() => { })
358
+ await unlink(caddyLockFilePath).catch(() => { })
359
+ }
360
+ catch (e) {
361
+ consola.error(e)
362
+ }
363
+ }
364
+ }
211
365
  }
@@ -1,4 +1,5 @@
1
1
  import { createServer } from 'node:net'
2
+ import { exec } from 'node:child_process'
2
3
  import ora from 'ora'
3
4
  import { consola } from '../utils'
4
5
 
@@ -28,6 +29,23 @@ export function tryPort(port: number) {
28
29
  })
29
30
  }
30
31
 
32
+ /**
33
+ * npx free-port :port
34
+ */
35
+ export async function kill(port: number) {
36
+ return new Promise<void>((resolve, reject) => {
37
+ exec(`npx @maxbo/free-port ${port} -s`, (err, stdout, stderr) => {
38
+ if (err) {
39
+ reject(err)
40
+ }
41
+ else {
42
+ consola.log(stdout)
43
+ resolve()
44
+ }
45
+ })
46
+ })
47
+ }
48
+
31
49
  function getNumber(number: number) {
32
50
  if (number <= 0)
33
51
  return '0%'
package/src/host/index.ts CHANGED
@@ -1,17 +1,13 @@
1
1
  import process from 'node:process'
2
2
  import { exec } from 'node:child_process'
3
3
  import { chmodSync } from 'node:fs'
4
+ import { promisify } from 'node:util'
4
5
 
5
- // @ts-expect-error no type
6
- import _Hosts from 'hosts-so-easy'
6
+ import hostile from 'hostile'
7
7
  import { consola } from '../utils'
8
8
 
9
- const Hosts = _Hosts.default || _Hosts
10
-
11
- const hostInstance = new Hosts({
12
- debounceTime: 50,
13
- hostsFile: getPathOfSystemHostsPath(),
14
- })
9
+ const _setHost = promisify(hostile.set)
10
+ const _removeHost = promisify(hostile.remove)
15
11
 
16
12
  function getPathOfSystemHostsPath() {
17
13
  return process.platform === 'win32'
@@ -38,20 +34,18 @@ async function syncHost() {
38
34
  export async function addHost(ip: string, host: string) {
39
35
  if (process.platform !== 'win32')
40
36
  chmodSync(getPathOfSystemHostsPath(), 0o777)
41
- hostInstance.add('127.0.0.1', 'localhost')
42
- hostInstance.add(ip, host)
37
+ await _setHost('127.0.0.1', 'localhost')
38
+ await _setHost(ip, host)
43
39
  if (ip === 'localhost')
44
- hostInstance.add('127.0.0.1', host)
45
- await hostInstance.updateFinish()
40
+ await _setHost('127.0.0.1', host)
46
41
  return await syncHost()
47
42
  }
48
43
 
49
44
  export async function removeHost(ip: string, host: string) {
50
45
  if (process.platform !== 'win32')
51
46
  chmodSync(getPathOfSystemHostsPath(), 0o777)
52
- hostInstance.remove(ip, host)
47
+ await _removeHost(ip, host)
53
48
  if (ip === 'localhost')
54
- hostInstance.remove('127.0.0.1', host)
55
- await hostInstance.updateFinish()
49
+ await _removeHost('127.0.0.1', host)
56
50
  return await syncHost()
57
51
  }