unplugin-https-reverse-proxy 4.0.2 → 4.0.3
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/dist/{chunk-JA3D5AWS.js → chunk-MD5CVZP7.js} +166 -16
- package/dist/esbuild.cjs +166 -16
- package/dist/esbuild.js +1 -1
- package/dist/index.cjs +166 -16
- package/dist/index.js +1 -1
- package/dist/nuxt.cjs +166 -16
- package/dist/nuxt.js +1 -1
- package/dist/rollup.cjs +166 -16
- package/dist/rollup.js +1 -1
- package/dist/rspack.cjs +166 -16
- package/dist/rspack.js +1 -1
- package/dist/vite.cjs +166 -16
- package/dist/vite.js +1 -1
- package/dist/webpack.cjs +166 -16
- package/dist/webpack.js +1 -1
- package/package.json +1 -1
- package/src/caddy/caddyfile.ts +1 -1
- package/src/caddy/index.ts +86 -16
- package/src/caddy/log.ts +16 -0
- package/src/caddy/tls-health.ts +142 -0
package/src/caddy/log.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface CaddyLogEntry {
|
|
2
|
+
error?: unknown
|
|
3
|
+
logger?: unknown
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function isCaddyReloadCancellation(line: string): boolean {
|
|
7
|
+
try {
|
|
8
|
+
const entry = JSON.parse(line) as CaddyLogEntry
|
|
9
|
+
return entry.logger === 'tls'
|
|
10
|
+
&& typeof entry.error === 'string'
|
|
11
|
+
&& entry.error.endsWith('context canceled')
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { ConnectionOptions, PeerCertificate, TLSSocket } from 'node:tls'
|
|
2
|
+
import { checkServerIdentity, connect } from 'node:tls'
|
|
3
|
+
|
|
4
|
+
export type TlsHealthReason =
|
|
5
|
+
| 'certificate_expired'
|
|
6
|
+
| 'certificate_invalid'
|
|
7
|
+
| 'certificate_missing'
|
|
8
|
+
| 'connection_failed'
|
|
9
|
+
| 'hostname_mismatch'
|
|
10
|
+
|
|
11
|
+
export type TlsHealthResult =
|
|
12
|
+
| {
|
|
13
|
+
healthy: true
|
|
14
|
+
expiresAt: Date
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
healthy: false
|
|
18
|
+
reason: TlsHealthReason
|
|
19
|
+
expiresAt?: Date
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type HostnameVerifier = (hostname: string, certificate: PeerCertificate) => Error | undefined
|
|
23
|
+
|
|
24
|
+
type TlsConnector = (options: ConnectionOptions) => TLSSocket
|
|
25
|
+
type TlsProbe = (hostname: string, options: ProbeLocalTlsOptions) => Promise<TlsHealthResult>
|
|
26
|
+
type Sleep = (ms: number) => Promise<void>
|
|
27
|
+
|
|
28
|
+
export function assessTlsCertificate(
|
|
29
|
+
certificate: PeerCertificate,
|
|
30
|
+
hostname: string,
|
|
31
|
+
now = Date.now(),
|
|
32
|
+
verifyHostname: HostnameVerifier = checkServerIdentity,
|
|
33
|
+
): TlsHealthResult {
|
|
34
|
+
if (!certificate.valid_to)
|
|
35
|
+
return { healthy: false, reason: 'certificate_missing' }
|
|
36
|
+
|
|
37
|
+
const expiresAt = new Date(certificate.valid_to)
|
|
38
|
+
if (Number.isNaN(expiresAt.getTime()))
|
|
39
|
+
return { healthy: false, reason: 'certificate_invalid' }
|
|
40
|
+
|
|
41
|
+
if (expiresAt.getTime() <= now)
|
|
42
|
+
return { healthy: false, reason: 'certificate_expired', expiresAt }
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
if (verifyHostname(hostname, certificate))
|
|
46
|
+
return { healthy: false, reason: 'hostname_mismatch' }
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return { healthy: false, reason: 'certificate_invalid' }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { healthy: true, expiresAt }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ProbeLocalTlsOptions {
|
|
56
|
+
connect?: TlsConnector
|
|
57
|
+
port?: number
|
|
58
|
+
timeoutMs?: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function probeLocalTls(hostname: string, options: ProbeLocalTlsOptions = {}): Promise<TlsHealthResult> {
|
|
62
|
+
const {
|
|
63
|
+
connect: openTlsConnection = connect,
|
|
64
|
+
port = 443,
|
|
65
|
+
timeoutMs = 1000,
|
|
66
|
+
} = options
|
|
67
|
+
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
let settled = false
|
|
70
|
+
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
71
|
+
const socket = openTlsConnection({
|
|
72
|
+
host: '127.0.0.1',
|
|
73
|
+
port,
|
|
74
|
+
servername: hostname,
|
|
75
|
+
rejectUnauthorized: false,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const finish = (result: TlsHealthResult): void => {
|
|
79
|
+
if (settled)
|
|
80
|
+
return
|
|
81
|
+
settled = true
|
|
82
|
+
if (timeout)
|
|
83
|
+
clearTimeout(timeout)
|
|
84
|
+
socket.destroy()
|
|
85
|
+
resolve(result)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
timeout = setTimeout(() => {
|
|
89
|
+
finish({ healthy: false, reason: 'connection_failed' })
|
|
90
|
+
}, timeoutMs)
|
|
91
|
+
|
|
92
|
+
socket.once('secureConnect', () => {
|
|
93
|
+
finish(assessTlsCertificate(socket.getPeerCertificate(), hostname))
|
|
94
|
+
})
|
|
95
|
+
socket.once('error', () => {
|
|
96
|
+
finish({ healthy: false, reason: 'connection_failed' })
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface WaitForLocalTlsOptions extends ProbeLocalTlsOptions {
|
|
102
|
+
probe?: TlsProbe
|
|
103
|
+
retryIntervalMs?: number
|
|
104
|
+
retryWindowMs?: number
|
|
105
|
+
sleep?: Sleep
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function sleep(ms: number): Promise<void> {
|
|
109
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function waitForLocalTls(hostname: string, options: WaitForLocalTlsOptions = {}): Promise<TlsHealthResult> {
|
|
113
|
+
const {
|
|
114
|
+
probe: checkTls = probeLocalTls,
|
|
115
|
+
retryIntervalMs = 250,
|
|
116
|
+
retryWindowMs = 30_000,
|
|
117
|
+
sleep: wait = sleep,
|
|
118
|
+
...probeOptions
|
|
119
|
+
} = options
|
|
120
|
+
const deadline = Date.now() + retryWindowMs
|
|
121
|
+
let result: TlsHealthResult = { healthy: false, reason: 'connection_failed' }
|
|
122
|
+
|
|
123
|
+
do {
|
|
124
|
+
result = await checkTls(hostname, probeOptions)
|
|
125
|
+
if (result.healthy)
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
const remaining = deadline - Date.now()
|
|
129
|
+
if (remaining > 0)
|
|
130
|
+
await wait(Math.min(retryIntervalMs, remaining))
|
|
131
|
+
} while (Date.now() < deadline)
|
|
132
|
+
|
|
133
|
+
return result
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function formatTlsHealthFailure(hostname: string, result: Exclude<TlsHealthResult, { healthy: true }>): string {
|
|
137
|
+
const reason = result.reason === 'certificate_expired' && result.expiresAt
|
|
138
|
+
? `the certificate expired at ${result.expiresAt.toISOString()}`
|
|
139
|
+
: result.reason.replaceAll('_', ' ')
|
|
140
|
+
|
|
141
|
+
return `[unplugin-https-reverse-proxy] Caddy cannot serve valid HTTPS for ${hostname}: ${reason}. Restart Caddy to retry its automatic certificate renewal. Do not remove Caddy PKI data automatically because that replaces the trusted local root certificate.`
|
|
142
|
+
}
|