cert-sync-script.test.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. import assert from 'node:assert/strict';
  2. import {
  3. chmodSync,
  4. mkdirSync,
  5. mkdtempSync,
  6. readFileSync,
  7. readdirSync,
  8. rmSync,
  9. statSync,
  10. writeFileSync
  11. } from 'node:fs';
  12. import os from 'node:os';
  13. import path from 'node:path';
  14. import { fileURLToPath } from 'node:url';
  15. import { spawnSync } from 'node:child_process';
  16. import { test } from 'node:test';
  17. const scriptPath = fileURLToPath(new URL('../scripts/sync-tls-certificate.sh', import.meta.url));
  18. const scriptSource = readFileSync(scriptPath, 'utf8');
  19. const opensslLookup = process.platform === 'win32'
  20. ? { status: 1, stdout: '' }
  21. : spawnSync('sh', ['-c', 'command -v openssl'], { encoding: 'utf8' });
  22. const opensslPath = opensslLookup.status === 0 ? opensslLookup.stdout.trim() : '';
  23. const canRun = process.platform !== 'win32' && Boolean(opensslPath);
  24. test('uses protocol-specific STARTTLS probes for explicit TLS upgrade ports', () => {
  25. assert.match(scriptSource, /25\|587\|2525\) protocol_args=\(-starttls smtp\)/);
  26. assert.match(scriptSource, /110\).*protocol_args=\(-starttls pop3\)/);
  27. assert.match(scriptSource, /143\).*protocol_args=\(-starttls imap\)/);
  28. });
  29. test('falls back to docker port when compose cannot resolve the Dovecot mapping', { skip: !canRun }, (t) => {
  30. const fixture = createFixture(t);
  31. generateCertificate(fixture.sourceDir, 'example.test');
  32. const fallback = createComposePortFallbackPath(fixture.root, fixture.path);
  33. const result = runSync(fixture, {
  34. restart: '1',
  35. env: {
  36. PATH: fallback.path,
  37. MAILHUB_CERT_VERIFY_ENDPOINTS: '993',
  38. MAILHUB_TEST_DOCKER_LOG: fallback.logFile,
  39. MAILHUB_TEST_PRESENTED_CERT: fixture.sourceCert,
  40. MAILHUB_TEST_REAL_OPENSSL: opensslPath
  41. }
  42. });
  43. assert.equal(result.status, 0, result.stderr);
  44. const commands = readFileSync(fallback.logFile, 'utf8').trim().split('\n');
  45. assert.ok(commands.includes('compose port dovecot 31993'), commands.join('\n'));
  46. assert.ok(commands.includes('compose ps -q dovecot'), commands.join('\n'));
  47. assert.ok(commands.includes('port dovecot-fallback 31993/tcp'), commands.join('\n'));
  48. });
  49. test('atomically synchronizes a valid wildcard certificate and preserves safe permissions', { skip: !canRun }, (t) => {
  50. const fixture = createFixture(t);
  51. generateCertificate(fixture.sourceDir, 'example.test');
  52. const first = runSync(fixture);
  53. assert.equal(first.status, 0, first.stderr);
  54. assert.match(first.stdout, /MailHub TLS certificate synchronized for mail\.example\.test\./);
  55. assert.deepEqual(readFileSync(fixture.targetCert), readFileSync(fixture.sourceCert));
  56. assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
  57. assert.equal(fileMode(fixture.targetCert), 0o644);
  58. assert.equal(fileMode(fixture.targetKey), 0o640);
  59. assert.deepEqual(stagingFiles(fixture.certsDir), []);
  60. const previousCert = readFileSync(fixture.targetCert);
  61. const previousKey = readFileSync(fixture.targetKey);
  62. chmodSync(fixture.targetCert, 0o640);
  63. chmodSync(fixture.targetKey, 0o600);
  64. generateCertificate(fixture.sourceDir, 'example.test');
  65. const replacement = runSync(fixture);
  66. assert.equal(replacement.status, 0, replacement.stderr);
  67. assert.deepEqual(readFileSync(`${fixture.targetCert}.previous`), previousCert);
  68. assert.deepEqual(readFileSync(`${fixture.targetKey}.previous`), previousKey);
  69. assert.deepEqual(readFileSync(fixture.targetCert), readFileSync(fixture.sourceCert));
  70. assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
  71. assert.equal(fileMode(fixture.targetCert), 0o644);
  72. assert.equal(fileMode(fixture.targetKey), 0o640);
  73. assert.deepEqual(stagingFiles(fixture.certsDir), []);
  74. });
  75. test('preserves complete certificate chains by default', { skip: !canRun }, (t) => {
  76. const fixture = createFixture(t);
  77. const extraOne = path.join(fixture.root, 'extra-one');
  78. const extraTwo = path.join(fixture.root, 'extra-two');
  79. mkdirSync(extraOne, { recursive: true });
  80. mkdirSync(extraTwo, { recursive: true });
  81. generateCertificate(fixture.sourceDir, 'example.test');
  82. generateCertificate(extraOne, 'intermediate.example.test');
  83. generateCertificate(extraTwo, 'root.example.test');
  84. const leaf = readFileSync(fixture.sourceCert);
  85. const intermediate = readFileSync(path.join(extraOne, 'fullchain.pem'));
  86. const root = readFileSync(path.join(extraTwo, 'fullchain.pem'));
  87. writeFileSync(fixture.sourceCert, Buffer.concat([leaf, intermediate, root]));
  88. const result = runSync(fixture);
  89. assert.equal(result.status, 0, result.stderr);
  90. assert.deepEqual(readFileSync(fixture.targetCert), Buffer.concat([leaf, intermediate, root]));
  91. assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
  92. });
  93. test('can compact oversized certificate chains when explicitly enabled', { skip: !canRun }, (t) => {
  94. const fixture = createFixture(t);
  95. const extraOne = path.join(fixture.root, 'extra-one');
  96. const extraTwo = path.join(fixture.root, 'extra-two');
  97. mkdirSync(extraOne, { recursive: true });
  98. mkdirSync(extraTwo, { recursive: true });
  99. generateCertificate(fixture.sourceDir, 'example.test');
  100. generateCertificate(extraOne, 'intermediate.example.test');
  101. generateCertificate(extraTwo, 'root.example.test');
  102. const leaf = readFileSync(fixture.sourceCert);
  103. const intermediate = readFileSync(path.join(extraOne, 'fullchain.pem'));
  104. const root = readFileSync(path.join(extraTwo, 'fullchain.pem'));
  105. writeFileSync(fixture.sourceCert, Buffer.concat([leaf, intermediate, root]));
  106. const result = runSync(fixture, { env: { MAILHUB_CERT_MAX_CHAIN_CERTS: '2' } });
  107. assert.equal(result.status, 0, result.stderr);
  108. assert.deepEqual(readFileSync(fixture.targetCert), Buffer.concat([leaf, intermediate]));
  109. assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
  110. });
  111. test('is idempotent when the synchronized certificate is already current', { skip: !canRun }, (t) => {
  112. const fixture = createFixture(t);
  113. generateCertificate(fixture.sourceDir, 'example.test');
  114. const first = runSync(fixture);
  115. assert.equal(first.status, 0, first.stderr);
  116. const certBefore = statSnapshot(fixture.targetCert);
  117. const keyBefore = statSnapshot(fixture.targetKey);
  118. const second = runSync(fixture);
  119. assert.equal(second.status, 0, second.stderr);
  120. assert.match(second.stdout, /MailHub TLS certificate is already up to date\./);
  121. assert.deepEqual(statSnapshot(fixture.targetCert), certBefore);
  122. assert.deepEqual(statSnapshot(fixture.targetKey), keyBefore);
  123. assert.equal(readdirSync(fixture.certsDir).some((name) => name.endsWith('.previous')), false);
  124. assert.deepEqual(stagingFiles(fixture.certsDir), []);
  125. });
  126. test('rejects a certificate that does not cover MAIL_HOSTNAME without touching the targets', { skip: !canRun }, (t) => {
  127. const fixture = createFixture(t);
  128. generateCertificate(fixture.sourceDir, 'other.test');
  129. mkdirSync(fixture.certsDir, { recursive: true });
  130. writeFileSync(fixture.targetCert, 'existing certificate\n');
  131. writeFileSync(fixture.targetKey, 'existing private key\n');
  132. chmodSync(fixture.targetCert, 0o640);
  133. chmodSync(fixture.targetKey, 0o600);
  134. const certBefore = readFileSync(fixture.targetCert);
  135. const keyBefore = readFileSync(fixture.targetKey);
  136. const result = runSync(fixture);
  137. assert.notEqual(result.status, 0);
  138. assert.match(result.stderr, /source certificate does not cover mail\.example\.test\./);
  139. assert.deepEqual(readFileSync(fixture.targetCert), certBefore);
  140. assert.deepEqual(readFileSync(fixture.targetKey), keyBefore);
  141. assert.equal(fileMode(fixture.targetCert), 0o640);
  142. assert.equal(fileMode(fixture.targetKey), 0o600);
  143. assert.equal(readdirSync(fixture.certsDir).some((name) => name.endsWith('.previous')), false);
  144. assert.deepEqual(stagingFiles(fixture.certsDir), []);
  145. });
  146. test('rejects certificate and private key paths that resolve to the same target', { skip: !canRun }, (t) => {
  147. const fixture = createFixture(t);
  148. generateCertificate(fixture.sourceDir, 'example.test');
  149. mkdirSync(fixture.certsDir, { recursive: true });
  150. writeEnvironment(fixture, {
  151. SUBMISSION_TLS_KEY: '/certs/fullchain.pem'
  152. });
  153. const result = runSync(fixture);
  154. assert.notEqual(result.status, 0);
  155. assert.equal(readdirSync(fixture.certsDir).some((name) => name === 'fullchain.pem' || name === 'privkey.pem'), false);
  156. assert.deepEqual(stagingFiles(fixture.certsDir), []);
  157. });
  158. test('rejects a mismatched certificate and private key without touching the targets', { skip: !canRun }, (t) => {
  159. const fixture = createFixture(t);
  160. const unrelatedDir = path.join(fixture.root, 'unrelated');
  161. mkdirSync(unrelatedDir, { recursive: true });
  162. generateCertificate(fixture.sourceDir, 'example.test');
  163. generateCertificate(unrelatedDir, 'example.test');
  164. writeFileSync(fixture.sourceKey, readFileSync(path.join(unrelatedDir, 'privkey.pem')));
  165. mkdirSync(fixture.certsDir, { recursive: true });
  166. writeFileSync(fixture.targetCert, 'existing certificate\n');
  167. writeFileSync(fixture.targetKey, 'existing private key\n');
  168. const certBefore = readFileSync(fixture.targetCert);
  169. const keyBefore = readFileSync(fixture.targetKey);
  170. const result = runSync(fixture);
  171. assert.notEqual(result.status, 0);
  172. assert.match(result.stderr, /certificate and private key do not match/i);
  173. assert.deepEqual(readFileSync(fixture.targetCert), certBefore);
  174. assert.deepEqual(readFileSync(fixture.targetKey), keyBefore);
  175. assert.equal(readdirSync(fixture.certsDir).some((name) => name.endsWith('.previous')), false);
  176. assert.deepEqual(stagingFiles(fixture.certsDir), []);
  177. });
  178. test('tightens an overly permissive existing private key to mode 0640', { skip: !canRun }, (t) => {
  179. const fixture = createFixture(t);
  180. generateCertificate(fixture.sourceDir, 'example.test');
  181. const first = runSync(fixture);
  182. assert.equal(first.status, 0, first.stderr);
  183. chmodSync(fixture.targetKey, 0o666);
  184. generateCertificate(fixture.sourceDir, 'example.test');
  185. const replacement = runSync(fixture);
  186. assert.equal(replacement.status, 0, replacement.stderr);
  187. assert.equal(fileMode(fixture.targetKey), 0o640);
  188. assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
  189. });
  190. test('validates and installs a staged source snapshot when managed files change mid-run', { skip: !canRun }, (t) => {
  191. const fixture = createFixture(t);
  192. const replacementDir = path.join(fixture.root, 'replacement');
  193. mkdirSync(replacementDir, { recursive: true });
  194. generateCertificate(fixture.sourceDir, 'example.test');
  195. generateCertificate(replacementDir, 'example.test');
  196. const expectedCert = readFileSync(fixture.sourceCert);
  197. const expectedKey = readFileSync(fixture.sourceKey);
  198. const marker = path.join(fixture.root, 'source-mutated');
  199. const mutationPath = createOpenSslMutationPath(fixture.root, fixture.path);
  200. const result = runSync(fixture, {
  201. env: {
  202. PATH: mutationPath,
  203. MAILHUB_TEST_REAL_OPENSSL: opensslPath,
  204. MAILHUB_TEST_MUTATION_MARKER: marker,
  205. MAILHUB_TEST_SOURCE_CERT: fixture.sourceCert,
  206. MAILHUB_TEST_SOURCE_KEY: fixture.sourceKey,
  207. MAILHUB_TEST_REPLACEMENT_CERT: path.join(replacementDir, 'fullchain.pem'),
  208. MAILHUB_TEST_REPLACEMENT_KEY: path.join(replacementDir, 'privkey.pem')
  209. }
  210. });
  211. assert.equal(result.status, 0, result.stderr);
  212. assert.equal(readFileSync(fixture.targetCert).equals(expectedCert), true, 'installed certificate must use the staged snapshot');
  213. assert.equal(readFileSync(fixture.targetKey).equals(expectedKey), true, 'installed private key must use the staged snapshot');
  214. assert.notDeepEqual(readFileSync(fixture.sourceCert), expectedCert);
  215. assert.notDeepEqual(readFileSync(fixture.sourceKey), expectedKey);
  216. assert.deepEqual(stagingFiles(fixture.certsDir), []);
  217. });
  218. test('rolls back both certificate files when post-restart health validation fails', { skip: !canRun }, (t) => {
  219. const fixture = createFixture(t);
  220. generateCertificate(fixture.sourceDir, 'example.test');
  221. const first = runSync(fixture);
  222. assert.equal(first.status, 0, first.stderr);
  223. const certBefore = readFileSync(fixture.targetCert);
  224. const keyBefore = readFileSync(fixture.targetKey);
  225. const certModeBefore = fileMode(fixture.targetCert);
  226. const keyModeBefore = fileMode(fixture.targetKey);
  227. generateCertificate(fixture.sourceDir, 'example.test');
  228. const failurePath = createFailedHealthPath(fixture.root, fixture.path);
  229. const result = runSync(fixture, {
  230. restart: '1',
  231. env: { PATH: failurePath }
  232. });
  233. assert.notEqual(result.status, 0);
  234. assert.match(result.stderr, /did not become healthy/i);
  235. assert.equal(readFileSync(fixture.targetCert).equals(certBefore), true, 'certificate must roll back after failed health validation');
  236. assert.equal(readFileSync(fixture.targetKey).equals(keyBefore), true, 'private key must roll back after failed health validation');
  237. assert.equal(fileMode(fixture.targetCert), certModeBefore);
  238. assert.equal(fileMode(fixture.targetKey), keyModeBefore);
  239. assert.deepEqual(stagingFiles(fixture.certsDir), []);
  240. });
  241. function createFixture(t) {
  242. const root = mkdtempSync(path.join(os.tmpdir(), 'mailhub-cert-sync-'));
  243. const projectDir = path.join(root, 'project');
  244. const sourceDir = path.join(root, 'source');
  245. const certsDir = path.join(projectDir, 'certs');
  246. const envFile = path.join(projectDir, '.env');
  247. mkdirSync(projectDir, { recursive: true });
  248. mkdirSync(sourceDir, { recursive: true });
  249. writeFileSync(envFile, [
  250. 'MAIL_HOSTNAME=mail.example.test',
  251. 'SUBMISSION_TLS_CERT=/certs/fullchain.pem',
  252. 'SUBMISSION_TLS_KEY=/certs/privkey.pem',
  253. ''
  254. ].join('\n'));
  255. t.after(() => rmSync(root, { recursive: true, force: true }));
  256. return {
  257. root,
  258. projectDir,
  259. sourceDir,
  260. certsDir,
  261. envFile,
  262. sourceCert: path.join(sourceDir, 'fullchain.pem'),
  263. sourceKey: path.join(sourceDir, 'privkey.pem'),
  264. targetCert: path.join(certsDir, 'fullchain.pem'),
  265. targetKey: path.join(certsDir, 'privkey.pem'),
  266. path: createCompatibilityPath(root)
  267. };
  268. }
  269. function writeEnvironment(fixture, overrides = {}) {
  270. const values = {
  271. MAIL_HOSTNAME: 'mail.example.test',
  272. SUBMISSION_TLS_CERT: '/certs/fullchain.pem',
  273. SUBMISSION_TLS_KEY: '/certs/privkey.pem',
  274. ...overrides
  275. };
  276. writeFileSync(fixture.envFile, [
  277. `MAIL_HOSTNAME=${values.MAIL_HOSTNAME}`,
  278. `SUBMISSION_TLS_CERT=${values.SUBMISSION_TLS_CERT}`,
  279. `SUBMISSION_TLS_KEY=${values.SUBMISSION_TLS_KEY}`,
  280. ''
  281. ].join('\n'));
  282. }
  283. function generateCertificate(sourceDir, domain) {
  284. const result = spawnSync('openssl', [
  285. 'req',
  286. '-x509',
  287. '-newkey', 'rsa:2048',
  288. '-nodes',
  289. '-sha256',
  290. '-days', '30',
  291. '-subj', `/CN=*.${domain}`,
  292. '-addext', `subjectAltName=DNS:*.${domain},DNS:${domain}`,
  293. '-keyout', path.join(sourceDir, 'privkey.pem'),
  294. '-out', path.join(sourceDir, 'fullchain.pem')
  295. ], { encoding: 'utf8' });
  296. assert.equal(result.status, 0, result.stderr);
  297. }
  298. function runSync(fixture, { restart = '0', env = {} } = {}) {
  299. const rootOwnershipEnv = typeof process.getuid === 'function' && process.getuid() === 0
  300. ? { MAILHUB_CERT_READER_GID: String(process.getgid()) }
  301. : {};
  302. return spawnSync('bash', [scriptPath], {
  303. cwd: fixture.projectDir,
  304. encoding: 'utf8',
  305. env: {
  306. ...process.env,
  307. PATH: fixture.path,
  308. MAILHUB_CERT_PROJECT_DIR: fixture.projectDir,
  309. MAILHUB_CERT_ENV_FILE: fixture.envFile,
  310. MAILHUB_CERT_SOURCE_DIR: fixture.sourceDir,
  311. MAILHUB_CERT_RESTART: restart,
  312. ...rootOwnershipEnv,
  313. ...env
  314. }
  315. });
  316. }
  317. function statSnapshot(file) {
  318. const stat = statSync(file);
  319. return {
  320. ino: stat.ino,
  321. mode: stat.mode & 0o777,
  322. size: stat.size,
  323. mtimeMs: stat.mtimeMs
  324. };
  325. }
  326. function fileMode(file) {
  327. return statSync(file).mode & 0o777;
  328. }
  329. function stagingFiles(directory) {
  330. return readdirSync(directory).filter((name) => name.includes('.tmp.') || name.startsWith('.cert-sync.'));
  331. }
  332. function createCompatibilityPath(root) {
  333. if (process.platform !== 'darwin') return process.env.PATH || '';
  334. const binDir = path.join(root, 'bin');
  335. mkdirSync(binDir, { recursive: true });
  336. writeExecutable(path.join(binDir, 'cp'), stripDoubleDashWrapper('/bin/cp'));
  337. writeExecutable(path.join(binDir, 'mv'), stripDoubleDashWrapper('/bin/mv'));
  338. writeExecutable(path.join(binDir, 'rm'), stripDoubleDashWrapper('/bin/rm'));
  339. writeExecutable(path.join(binDir, 'chmod'), `#!/usr/bin/env bash
  340. set -euo pipefail
  341. if [[ "\${1:-}" == --reference=* ]]; then
  342. reference="\${1#*=}"
  343. shift
  344. mode="$(/usr/bin/stat -f '%Lp' "\${reference}")"
  345. exec /bin/chmod "\${mode}" "$@"
  346. fi
  347. exec /bin/chmod "$@"
  348. `);
  349. writeExecutable(path.join(binDir, 'chown'), `#!/usr/bin/env bash
  350. set -euo pipefail
  351. if [[ "\${1:-}" == --reference=* ]]; then
  352. reference="\${1#*=}"
  353. shift
  354. owner="$(/usr/bin/stat -f '%u:%g' "\${reference}")"
  355. exec /usr/sbin/chown "\${owner}" "$@"
  356. fi
  357. exec /usr/sbin/chown "$@"
  358. `);
  359. writeExecutable(path.join(binDir, 'sha256sum'), `#!/usr/bin/env bash
  360. exec /usr/bin/shasum -a 256 "$@"
  361. `);
  362. return `${binDir}${path.delimiter}${process.env.PATH || ''}`;
  363. }
  364. function createOpenSslMutationPath(root, basePath) {
  365. const binDir = path.join(root, 'mutating-openssl-bin');
  366. mkdirSync(binDir, { recursive: true });
  367. writeExecutable(path.join(binDir, 'openssl'), `#!/usr/bin/env bash
  368. set -euo pipefail
  369. if [[ ! -e "\${MAILHUB_TEST_MUTATION_MARKER}" ]]; then
  370. : > "\${MAILHUB_TEST_MUTATION_MARKER}"
  371. cp "\${MAILHUB_TEST_REPLACEMENT_CERT}" "\${MAILHUB_TEST_SOURCE_CERT}"
  372. cp "\${MAILHUB_TEST_REPLACEMENT_KEY}" "\${MAILHUB_TEST_SOURCE_KEY}"
  373. fi
  374. exec "\${MAILHUB_TEST_REAL_OPENSSL}" "$@"
  375. `);
  376. return `${binDir}${path.delimiter}${basePath}`;
  377. }
  378. function createFailedHealthPath(root, basePath) {
  379. const binDir = path.join(root, 'failed-health-bin');
  380. mkdirSync(binDir, { recursive: true });
  381. writeExecutable(path.join(binDir, 'docker'), `#!/usr/bin/env bash
  382. set -euo pipefail
  383. if [[ "$*" == *"tls.createSecureContext"* ]]; then
  384. exit 0
  385. fi
  386. if [[ "$*" == *"test -r"* ]]; then
  387. exit 0
  388. fi
  389. if [[ "\${1:-}" == "compose" && "\${2:-}" == "restart" ]]; then
  390. exit 0
  391. fi
  392. exit 1
  393. `);
  394. writeExecutable(path.join(binDir, 'sleep'), `#!/usr/bin/env bash
  395. exit 0
  396. `);
  397. return `${binDir}${path.delimiter}${basePath}`;
  398. }
  399. function createComposePortFallbackPath(root, basePath) {
  400. const binDir = path.join(root, 'compose-port-fallback-bin');
  401. const logFile = path.join(root, 'compose-port-fallback.log');
  402. mkdirSync(binDir, { recursive: true });
  403. writeFileSync(logFile, '');
  404. writeExecutable(path.join(binDir, 'docker'), `#!/usr/bin/env bash
  405. set -euo pipefail
  406. printf '%s\\n' "$*" >> "\${MAILHUB_TEST_DOCKER_LOG}"
  407. if [[ "\${1:-}" == "compose" && "\${2:-}" == "exec" ]]; then
  408. exit 0
  409. fi
  410. if [[ "\${1:-}" == "compose" && "\${2:-}" == "restart" ]]; then
  411. exit 0
  412. fi
  413. if [[ "\${1:-}" == "compose" && "\${2:-}" == "port" ]]; then
  414. printf '%s\\n' 'invalid IP:0' >&2
  415. exit 1
  416. fi
  417. if [[ "\${1:-}" == "compose" && "\${2:-}" == "ps" && "\${3:-}" == "-q" ]]; then
  418. printf '%s\\n' 'dovecot-fallback'
  419. exit 0
  420. fi
  421. if [[ "\${1:-}" == "port" && "\${2:-}" == "dovecot-fallback" && "\${3:-}" == "31993/tcp" ]]; then
  422. printf '%s\\n' '0.0.0.0:1993'
  423. exit 0
  424. fi
  425. exit 1
  426. `);
  427. writeExecutable(path.join(binDir, 'openssl'), `#!/usr/bin/env bash
  428. set -euo pipefail
  429. if [[ "\${1:-}" == "s_client" ]]; then
  430. connection=""
  431. previous=""
  432. for argument in "$@"; do
  433. if [[ "\${previous}" == "-connect" ]]; then
  434. connection="\${argument}"
  435. break
  436. fi
  437. previous="\${argument}"
  438. done
  439. [[ "\${connection}" == "127.0.0.1:1993" ]] || exit 71
  440. cat "\${MAILHUB_TEST_PRESENTED_CERT}"
  441. exit 0
  442. fi
  443. exec "\${MAILHUB_TEST_REAL_OPENSSL}" "$@"
  444. `);
  445. return {
  446. logFile,
  447. path: `${binDir}${path.delimiter}${basePath}`
  448. };
  449. }
  450. function stripDoubleDashWrapper(command) {
  451. return `#!/usr/bin/env bash
  452. set -euo pipefail
  453. args=()
  454. for argument in "$@"; do
  455. [[ "\${argument}" == "--" ]] || args+=("\${argument}")
  456. done
  457. exec ${command} "\${args[@]}"
  458. `;
  459. }
  460. function writeExecutable(file, content) {
  461. writeFileSync(file, content);
  462. chmodSync(file, 0o755);
  463. }