Skip to content

Commit eb373f0

Browse files
noahw3bluwy
andauthored
feat: add optional ttlDays parameter (#98)
Co-authored-by: bluwy <bjornlu.dev@gmail.com>
1 parent 4b9d639 commit eb373f0

File tree

6 files changed

+120
-8
lines changed

6 files changed

+120
-8
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export default {
1717
name: 'test',
1818
/** custom trust domains */
1919
domains: ['*.custom.com'],
20+
/** optional, days before certificate expires */
21+
ttlDays: 30,
2022
/** custom certification directory */
2123
certDir: '/Users/.../.devServer/cert',
2224
}),

src/certificate-expiration.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { X509Certificate } from 'node:crypto'
2+
3+
const MONTHS = [
4+
'Jan',
5+
'Feb',
6+
'Mar',
7+
'Apr',
8+
'May',
9+
'Jun',
10+
'Jul',
11+
'Aug',
12+
'Sep',
13+
'Oct',
14+
'Nov',
15+
'Dec',
16+
]
17+
18+
export function isCertificateExpired(content: string): boolean {
19+
const cert = new X509Certificate(content)
20+
const expirationDate = getCertificateExpirationDate(cert)
21+
return new Date() > expirationDate
22+
}
23+
24+
function getCertificateExpirationDate(cert: X509Certificate): Date {
25+
// validToDate is not available until node 22
26+
if (cert.validToDate) {
27+
return cert.validToDate
28+
}
29+
30+
return parseNonStandardDateString(cert.validTo)
31+
}
32+
33+
// validTo is a nonstandard format: %s %2d %02d:%02d:%02d %d%s GMT
34+
// https://github.com/nodejs/node/issues/52931
35+
export function parseNonStandardDateString(str: string): Date {
36+
const [month, day, time, year] = str.split(' ').filter((part) => !!part)
37+
// convert string month to number
38+
const monthIndex = MONTHS.indexOf(month) + 1
39+
return new Date(
40+
`${year}-${monthIndex.toString().padStart(2, '0')}-${day.padStart(2, '0')}T${time}Z`,
41+
)
42+
}

src/certificate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ function toPositiveHex(hexString: string) {
5555
export function createCertificate(
5656
name: string = 'example.org',
5757
domains?: string[],
58+
ttlDays = 30,
5859
): string {
59-
const days = 30
6060
const keySize = 2048
6161

6262
const appendDomains = domains
@@ -146,7 +146,7 @@ export function createCertificate(
146146

147147
cert.validity.notBefore = new Date()
148148
cert.validity.notAfter = new Date()
149-
cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + days)
149+
cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + ttlDays)
150150

151151
cert.setSubject(attrs)
152152
cert.setIssuer(attrs)

src/index.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import path from 'node:path'
22
import { promises as fsp } from 'node:fs'
33
import type { Plugin } from 'vite'
4+
import { isCertificateExpired } from './certificate-expiration'
45

56
const defaultCacheDir = 'node_modules/.vite'
67

78
interface Options {
89
certDir: string
910
domains: string[]
1011
name: string
12+
ttlDays: number
1113
}
1214

1315
function viteBasicSslPlugin(options?: Partial<Options>): Plugin {
@@ -18,6 +20,7 @@ function viteBasicSslPlugin(options?: Partial<Options>): Plugin {
1820
options?.certDir ?? (config.cacheDir ?? defaultCacheDir) + '/basic-ssl',
1921
options?.name,
2022
options?.domains,
23+
options?.ttlDays,
2124
)
2225
const https = () => ({ cert: certificate, key: certificate })
2326
if (config.server.https === undefined || !!config.server.https) {
@@ -34,16 +37,15 @@ export async function getCertificate(
3437
cacheDir: string,
3538
name?: string,
3639
domains?: string[],
40+
ttlDays?: number,
3741
) {
3842
const cachePath = path.join(cacheDir, '_cert.pem')
3943

4044
try {
41-
const [stat, content] = await Promise.all([
42-
fsp.stat(cachePath),
43-
fsp.readFile(cachePath, 'utf8'),
44-
])
45+
const content = await fsp.readFile(cachePath, 'utf8')
46+
const isExpired = isCertificateExpired(content)
4547

46-
if (Date.now() - stat.ctime.valueOf() > 30 * 24 * 60 * 60 * 1000) {
48+
if (isExpired) {
4749
throw new Error('cache is outdated.')
4850
}
4951

@@ -52,6 +54,7 @@ export async function getCertificate(
5254
const content = (await import('./certificate')).createCertificate(
5355
name,
5456
domains,
57+
ttlDays,
5558
)
5659
fsp
5760
.mkdir(cacheDir, { recursive: true })

test/test.spec.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { test, expect } from 'vitest'
1+
import { beforeEach, describe, test, expect, vi, Mock } from 'vitest'
2+
import { X509Certificate } from 'node:crypto'
23
import { createCertificate } from '../src/certificate'
4+
import {
5+
isCertificateExpired,
6+
parseNonStandardDateString,
7+
} from '../src/certificate-expiration'
38

49
test('create certificate', () => {
510
const content = createCertificate()
@@ -10,3 +15,62 @@ test('create certificate', () => {
1015
/-----BEGIN CERTIFICATE-----(\n|\r|.)*-----END CERTIFICATE-----/,
1116
)
1217
})
18+
19+
describe('isCertificateExpired', () => {
20+
let validToDateMock: Mock
21+
let validToMock: Mock
22+
23+
beforeEach(() => {
24+
validToDateMock = vi.spyOn(X509Certificate.prototype, 'validToDate', 'get')
25+
validToMock = vi.spyOn(X509Certificate.prototype, 'validTo', 'get')
26+
})
27+
28+
describe('with validToDate', () => {
29+
test('returns false', () => {
30+
validToDateMock.mockReturnValue(new Date(Date.now() + 10000))
31+
32+
const content = createCertificate()
33+
const isExpired = isCertificateExpired(content)
34+
expect(isExpired).toBe(false)
35+
})
36+
37+
test('returns true', () => {
38+
validToDateMock.mockReturnValue(new Date(Date.now() - 10000))
39+
40+
const content = createCertificate()
41+
const isExpired = isCertificateExpired(content)
42+
expect(isExpired).toBe(true)
43+
})
44+
})
45+
46+
describe('with validTo', () => {
47+
test('returns false', () => {
48+
validToDateMock.mockReturnValue(undefined)
49+
validToMock.mockReturnValue('Sep 3 21:40:37 2296 GMT')
50+
51+
const content = createCertificate()
52+
const isExpired = isCertificateExpired(content)
53+
expect(isExpired).toBe(false)
54+
})
55+
56+
test('returns true', () => {
57+
validToDateMock.mockReturnValue(undefined)
58+
validToMock.mockReturnValue('Jan 22 08:20:44 2022 GMT')
59+
60+
const content = createCertificate()
61+
const isExpired = isCertificateExpired(content)
62+
expect(isExpired).toBe(true)
63+
})
64+
})
65+
})
66+
67+
test('parseNonStandardDateString', () => {
68+
const content = createCertificate()
69+
const cert = new X509Certificate(content)
70+
const date = parseNonStandardDateString(cert.validTo)
71+
expect(date).toBeInstanceOf(Date)
72+
expect(date.getTime()).toBeGreaterThan(0)
73+
if (cert.validToDate) {
74+
expect(date.getTime()).toBe(cert.validToDate.getTime())
75+
}
76+
})

test/vitest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ import { defineConfig } from 'vite'
44
export default defineConfig({
55
test: {
66
testTimeout: 100000,
7+
restoreMocks: true,
78
},
89
})

0 commit comments

Comments
 (0)