mirror of
https://github.com/caprover/caprover
synced 2025-10-30 10:07:01 +00:00
refactor: use shell-quote to parse cert command rule
This commit is contained in:
16
package-lock.json
generated
16
package-lock.json
generated
@@ -32,6 +32,7 @@
|
||||
"request": "^2.88.2",
|
||||
"require-from-string": "^2.0.2",
|
||||
"serve-favicon": "~2.5.0",
|
||||
"shell-quote": "^1.8.1",
|
||||
"simple-git": "^2.45.0",
|
||||
"ssh2": "^1.4.0",
|
||||
"tar": "^6.1.11",
|
||||
@@ -60,6 +61,7 @@
|
||||
"@types/request": "^2.48.7",
|
||||
"@types/require-from-string": "^1.2.1",
|
||||
"@types/serve-favicon": "^2.5.3",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/ssh2": "^0.5.47",
|
||||
"@types/tar": "^4.0.5",
|
||||
"@types/uuid": "^8.3.1",
|
||||
@@ -1524,6 +1526,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/shell-quote": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz",
|
||||
"integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ssh2": {
|
||||
"version": "0.5.52",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz",
|
||||
@@ -7580,6 +7588,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
|
||||
"integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"request": "^2.88.2",
|
||||
"require-from-string": "^2.0.2",
|
||||
"serve-favicon": "~2.5.0",
|
||||
"shell-quote": "^1.8.1",
|
||||
"simple-git": "^2.45.0",
|
||||
"ssh2": "^1.4.0",
|
||||
"tar": "^6.1.11",
|
||||
@@ -66,6 +67,7 @@
|
||||
"@types/request": "^2.48.7",
|
||||
"@types/require-from-string": "^1.2.1",
|
||||
"@types/serve-favicon": "^2.5.3",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/ssh2": "^0.5.47",
|
||||
"@types/tar": "^4.0.5",
|
||||
"@types/uuid": "^8.3.1",
|
||||
|
||||
@@ -4,6 +4,7 @@ import CaptainConstants, { type CertbotCertCommandRule } from '../../utils/Capta
|
||||
import Logger from '../../utils/Logger'
|
||||
import Utils from '../../utils/Utils'
|
||||
import fs = require('fs-extra')
|
||||
import ShellQuote = require('shell-quote')
|
||||
|
||||
const WEBROOT_PATH_IN_CERTBOT = '/captain-webroot'
|
||||
const WEBROOT_PATH_IN_CAPTAIN =
|
||||
@@ -41,7 +42,7 @@ function isCertCommandSuccess(output: string) {
|
||||
|
||||
class CertbotManager {
|
||||
private isOperationInProcess: boolean
|
||||
private certCommandGenerator = new CertCommandGenerator(CaptainConstants.configs.certbotCertCommand ?? [], ['certbot', 'certonly', '--webroot', '-w', '${webroot}', '-d', '${domain}'])
|
||||
private certCommandGenerator = new CertCommandGenerator(CaptainConstants.configs.certbotCertCommandRules ?? [], 'certbot certonly --webroot -w ${webroot} -d ${domainName}')
|
||||
|
||||
constructor(private dockerApi: DockerApi) {
|
||||
this.dockerApi = dockerApi
|
||||
@@ -85,10 +86,7 @@ class CertbotManager {
|
||||
return self.ensureDomainHasDirectory(domainName)
|
||||
})
|
||||
.then(function () {
|
||||
const cmd = self.certCommandGenerator.getCertbotCertCommand(domainName, {
|
||||
domain: domainName,
|
||||
webroot: WEBROOT_PATH_IN_CERTBOT + '/' + domainName
|
||||
})
|
||||
const cmd = self.certCommandGenerator.getCertbotCertCommand(domainName, WEBROOT_PATH_IN_CERTBOT + '/' + domainName);
|
||||
|
||||
if (shouldUseStaging) {
|
||||
cmd.push('--staging')
|
||||
@@ -422,10 +420,10 @@ class CertbotManager {
|
||||
export default CertbotManager
|
||||
|
||||
export class CertCommandGenerator {
|
||||
constructor(private rules: CertbotCertCommandRule[], private defaultCommand: string[]) {
|
||||
constructor(private rules: CertbotCertCommandRule[], private defaultCommand: string) {
|
||||
}
|
||||
|
||||
private getCertbotCertCommandTemplate(domainName: string): string[] {
|
||||
private getCertbotCertCommandTemplate(domainName: string): string {
|
||||
for (const rule of this.rules) {
|
||||
if (rule.domain === '*'
|
||||
|| domainName === rule.domain
|
||||
@@ -436,8 +434,18 @@ export class CertCommandGenerator {
|
||||
}
|
||||
return this.defaultCommand
|
||||
}
|
||||
getCertbotCertCommand(domainName: string, variables: Record<string, string> = {}): string[] {
|
||||
getCertbotCertCommand(domainName: string, webroot:string): string[] {
|
||||
const variables: Record<string, string> = {
|
||||
domainName,
|
||||
webroot,
|
||||
}
|
||||
const command = this.getCertbotCertCommandTemplate(domainName)
|
||||
return command.map(c => c.replace(/\$\{(\w+)}/g, (match, p1) => variables[p1] ?? match))
|
||||
const parsed = ShellQuote.parse(command, (key: string)=> {
|
||||
return variables[key] ?? `\${${key}}`
|
||||
})
|
||||
if (parsed.some(x => typeof x !== 'string')) {
|
||||
throw new Error(`Invalid command: ${command}`)
|
||||
}
|
||||
return parsed as string[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ const configs = {
|
||||
|
||||
certbotImageName: 'caprover/certbot-sleeping:v1.6.0',
|
||||
|
||||
certbotCertCommand: undefined as CertbotCertCommandRule[] | undefined,
|
||||
certbotCertCommandRules: undefined as CertbotCertCommandRule[] | undefined,
|
||||
}
|
||||
|
||||
export interface CertbotCertCommandRule {
|
||||
@@ -66,9 +66,9 @@ export interface CertbotCertCommandRule {
|
||||
*/
|
||||
domain: string;
|
||||
/**
|
||||
* The Certbot command to execute, in Docker exec form, uses `${domain}` as the placeholder for the actual domain name
|
||||
* The Certbot command to execute, will be parsed using `shell-quote`, available variables are `${domainName}` and `${subdomain}`
|
||||
*/
|
||||
command?: string[];
|
||||
command?: string;
|
||||
}
|
||||
|
||||
const data = {
|
||||
|
||||
@@ -1,55 +1,61 @@
|
||||
import { CertCommandGenerator } from '../src/user/system/CertbotManager'
|
||||
|
||||
const defaultCommand = [ 'certbot', 'certonly', '--domain', '${domain}' ]
|
||||
const defaultCommand = 'certbot certonly --domain ${domainName}'
|
||||
const exampleRule = {
|
||||
domain: 'example.com',
|
||||
command: [ 'certbot', 'certonly', '--manual', '--preferred-challenges=dns', '--domain', '${domain}' ],
|
||||
command: 'certbot certonly --manual --preferred-challenges=dns --domain ${domainName}',
|
||||
}
|
||||
const wildcardRule = {
|
||||
domain: '*',
|
||||
command: [ 'certbot', 'renew' ],
|
||||
command: 'certbot renew',
|
||||
}
|
||||
const nullCommandRule = {
|
||||
domain: 'fallback.com',
|
||||
}
|
||||
const fakeWebroot = '/path/to/webroot'
|
||||
|
||||
test('uses default command when no rules match', () => {
|
||||
const generator = new CertCommandGenerator([], defaultCommand)
|
||||
expect(generator.getCertbotCertCommand('nonmatching.com')).toEqual(defaultCommand)
|
||||
expect(generator.getCertbotCertCommand('nonmatching.com', fakeWebroot))
|
||||
.toEqual([ 'certbot', 'certonly', '--domain', 'nonmatching.com' ])
|
||||
})
|
||||
|
||||
test('uses specific rule when domain matches exactly', () => {
|
||||
const generator = new CertCommandGenerator([ exampleRule ], defaultCommand)
|
||||
expect(generator.getCertbotCertCommand('example.com')).toEqual(exampleRule.command)
|
||||
expect(generator.getCertbotCertCommand('example.com', fakeWebroot))
|
||||
.toEqual([ 'certbot', 'certonly', '--manual', '--preferred-challenges=dns', '--domain', 'example.com' ])
|
||||
})
|
||||
|
||||
test('uses wildcard rule for any domain', () => {
|
||||
const generator = new CertCommandGenerator([ wildcardRule ], defaultCommand)
|
||||
expect(generator.getCertbotCertCommand('anything.com')).toEqual(wildcardRule.command)
|
||||
expect(generator.getCertbotCertCommand('anything.com', fakeWebroot)).toEqual([ 'certbot', 'renew' ])
|
||||
})
|
||||
|
||||
test('matches subdomain to rule', () => {
|
||||
const generator = new CertCommandGenerator([ exampleRule ], defaultCommand)
|
||||
expect(generator.getCertbotCertCommand('sub.example.com')).toEqual(exampleRule.command)
|
||||
expect(generator.getCertbotCertCommand('sub.example.com', fakeWebroot))
|
||||
.toEqual([ 'certbot', 'certonly', '--manual', '--preferred-challenges=dns', '--domain', 'sub.example.com' ])
|
||||
})
|
||||
|
||||
test('replaces variables in command template', () => {
|
||||
const generator = new CertCommandGenerator([], defaultCommand)
|
||||
expect(generator.getCertbotCertCommand('example.com', { 'domain': 'example.com' }))
|
||||
expect(generator.getCertbotCertCommand('example.com', fakeWebroot))
|
||||
.toEqual([ 'certbot', 'certonly', '--domain', 'example.com' ])
|
||||
})
|
||||
|
||||
test('leaves unreplaced placeholders unchanged', () => {
|
||||
const generator = new CertCommandGenerator([], [ 'echo', '${missing}' ])
|
||||
expect(generator.getCertbotCertCommand('example.com')).toEqual([ 'echo', '${missing}' ])
|
||||
const generator = new CertCommandGenerator([], 'echo ${missing}')
|
||||
expect(generator.getCertbotCertCommand('example.com', fakeWebroot)).toEqual([ 'echo', '${missing}' ])
|
||||
})
|
||||
|
||||
test('first matching rule is used when multiple rules could apply', () => {
|
||||
const generator = new CertCommandGenerator([ exampleRule, wildcardRule ], defaultCommand)
|
||||
expect(generator.getCertbotCertCommand('example.com')).toEqual(exampleRule.command)
|
||||
expect(generator.getCertbotCertCommand('example.com', fakeWebroot))
|
||||
.toEqual([ 'certbot', 'certonly', '--manual', '--preferred-challenges=dns', '--domain', 'example.com' ])
|
||||
})
|
||||
|
||||
test('falls back to default command when rule command is null', () => {
|
||||
const generator = new CertCommandGenerator([ nullCommandRule ], defaultCommand)
|
||||
expect(generator.getCertbotCertCommand('nullcommand.com')).toEqual(defaultCommand)
|
||||
expect(generator.getCertbotCertCommand('nullcommand.com', fakeWebroot))
|
||||
.toEqual([ 'certbot', 'certonly', '--domain', 'nullcommand.com' ])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user