Skip to content

Commit

Permalink
admin/internet: generate certificate with multiple domains
Browse files Browse the repository at this point in the history
  • Loading branch information
rejetto committed Dec 6, 2023
1 parent 0e7c476 commit dcdecf5
Show file tree
Hide file tree
Showing 4 changed files with 18 additions and 16 deletions.
8 changes: 4 additions & 4 deletions admin/src/InternetPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export default function InternetPage() {
cert.element || with_(cert.data, c => h(Box, {},
h(CardMembership, { fontSize: 'small', sx: { mr: 1, verticalAlign: 'middle' } }), "Current certificate",
h('ul', {},
h('li', {}, "Domain: ", c.subject?.CN || '-'),
h('li', {}, "Domain: ", c.altNames?.join(' + ') ||'-'),
h('li', {}, "Issuer: ", c.issuer?.O || h('i', {}, 'self-signed')),
h('li', {}, "Validity: ", ['validFrom', 'validTo'].map(k => formatTimestamp(c[k])).join(' – ')),
)
Expand All @@ -122,23 +122,23 @@ export default function InternetPage() {
},
fields: [
md("Generate certificate using [Let's Encrypt](https://letsencrypt.org)"),
{ k: 'acme_domain', label: "Domain for certificate", sm: 6, required: true, helperText: "example: your.domain.com" },
{ k: 'acme_domain', label: "Domain for certificate", sm: 6, required: true, helperText: md("Example: your.domain.com\nMultiple domains separated by commas") },
{ k: 'acme_email', label: "E-mail for certificate", sm: 6 },
{ k: 'acme_renew', label: "Automatic renew one month before expiration", comp: BoolField, disabled: !values.acme_domain },
],
save: {
children: "Request",
startIcon: h(Send),
async onClick() {
const domain = values.acme_domain
const [domain, ...altNames] = values.acme_domain.split(',')
const fresh = domain === cert.data.subject?.CN && Number(new Date(cert.data.validTo)) - Date.now() >= 30 * DAY
if (fresh && !await confirmDialog("Your certificate is still good", { confirmText: "Make a new one anyway" }))
return
if (!await confirmDialog("HFS must temporarily serve HTTP on public port 80, and your router must be configured or this operation will fail")) return
const res = await apiCall('check_domain', { domain }).catch(e =>
confirmDialog(String(e), { confirmText: "Continue anyway" }) )
if (res === false) return
await apiCall('make_cert', { domain, email: values.acme_email }, { timeout: 20_000 })
await apiCall('make_cert', { domain, altNames, email: values.acme_email }, { timeout: 20_000 })
.then(async () => {
await alertDialog("Certificate created", 'success')
if (!listening)
Expand Down
8 changes: 4 additions & 4 deletions src/acme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const acmeMiddleware: Middleware = (ctx, next) => { // koa format
return next()
}

async function generateSSLCert(domain: string, email?: string) {
async function generateSSLCert(domain: string, email?: string, altNames?: string[]) {
// will answer challenge through our koa app (if on port 80) or must we spawn a dedicated server?
const nat = await getNatInfo()
const { http } = await getServerStatus()
Expand Down Expand Up @@ -58,7 +58,7 @@ async function generateSSLCert(domain: string, email?: string) {
directoryUrl: acme.directory.letsencrypt.production
})
acme.setLogger(console.debug)
const [key, csr] = await acme.crypto.createCsr({ commonName: domain })
const [key, csr] = await acme.crypto.createCsr({ commonName: domain, altNames })
const cert = await acmeClient.auto({
csr,
email,
Expand All @@ -81,9 +81,9 @@ async function generateSSLCert(domain: string, email?: string) {
}
}

export const makeCert = debounceAsync(async (domain: string, email?: string) => {
export const makeCert = debounceAsync(async (domain: string, email?: string, altNames?: string[]) => {
if (!domain) return new ApiError(HTTP_BAD_REQUEST, 'bad params')
const res = await generateSSLCert(domain, email)
const res = await generateSSLCert(domain, email, altNames)
const CERT_FILE = 'acme.cert'
const KEY_FILE = 'acme.key'
await fs.writeFile(CERT_FILE, res.cert)
Expand Down
11 changes: 6 additions & 5 deletions src/api.net.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt

import { ApiError, ApiHandlers } from './apiMiddleware'
import { HTTP_FAILED_DEPENDENCY, HTTP_SERVER_ERROR, HTTP_SERVICE_UNAVAILABLE, HTTP_PRECONDITION_FAILED } from './const'
import { HTTP_FAILED_DEPENDENCY, HTTP_SERVER_ERROR, HTTP_SERVICE_UNAVAILABLE, HTTP_PRECONDITION_FAILED, HTTP_NOT_FOUND
} from './const'
import _ from 'lodash'
import { getCertObject } from './listen'
import { getProjectInfo } from './github'
import { apiAssertTypes, objSameKeys, onlyTruthy, promiseBestEffort } from './misc'
import { apiAssertTypes, onlyTruthy, promiseBestEffort } from './misc'
import { lookup, Resolver } from 'dns/promises'
import { isIPv6 } from 'net'
import { getNatInfo, upnpClient } from './nat'
Expand Down Expand Up @@ -70,13 +71,13 @@ const apis: ApiHandlers = {
return results.length ? results : new ApiError(HTTP_SERVICE_UNAVAILABLE)
},

async make_cert({domain, email}) {
await makeCert(domain, email)
async make_cert({domain, email, altNames}) {
await makeCert(domain, email, altNames)
return {}
},

get_cert() {
return objSameKeys(_.pick(getCertObject(), ['subject', 'issuer', 'validFrom', 'validTo']), v => v)
return getCertObject() || new ApiError(HTTP_NOT_FOUND)
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/listen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ export function openAdmin() {

export function getCertObject() {
if (!httpsOptions.cert) return
const o = new X509Certificate(httpsOptions.cert)
const some = _.pick(o, ['subject', 'issuer', 'validFrom', 'validTo'])
return objSameKeys(some, v => v?.includes('=') ? Object.fromEntries(v.split('\n').map(x => x.split('='))) : v)
const all = new X509Certificate(httpsOptions.cert)
const some = _.pick(all, ['subject', 'issuer', 'validFrom', 'validTo'])
const ret = objSameKeys(some, v => v?.includes('=') ? Object.fromEntries(v.split('\n').map(x => x.split('='))) : v)
return Object.assign(ret, { altNames: all.subjectAltName?.replace(/DNS:/g, '').split(/, */) })
}

const considerHttps = debounceAsync(async () => {
Expand Down

0 comments on commit dcdecf5

Please sign in to comment.