diff --git a/bin/cli.js b/bin/cli.js new file mode 100644 index 0000000..98040a5 --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,90 @@ +import { parseArgs } from 'util'; +import configs from '../src/js/configs.js'; +import minver from '../src/js/helpers/minver.js'; +import { xmlEntities } from '../src/js/utils.js'; +import { get_state } from '../src/js/state.js'; + +const servers = Object.keys(configs).filter(k => k !== 'openssl'); + +function show_help() { + process.stdout.write( + 'Usage: tlsref --server --config [options]\n' + + '\n' + + 'Options:\n' + + ' -s, --server Server software (required)\n' + + ' -c, --config Configuration level: modern, intermediate, old (required)\n' + + ' -v, --version Server software version (default: latest)\n' + + ' -o, --openssl OpenSSL version (default: latest)\n' + + ' --hsts Enable HSTS (when supported)\n' + + ' --ocsp Enable OCSP stapling (when supported)\n' + + ' -g, --guideline Guideline version (default: ' + guideln_latest + ')\n' + + ' -h, --help Show this help message\n' + + '\n' + + 'Available servers: ' + servers.join(', ') + '\n' + ); +} + +async function main() { + const { values } = parseArgs({ + args: process.argv.slice(2), + options: { + server: { type: 'string', short: 's' }, + config: { type: 'string', short: 'c' }, + version: { type: 'string', short: 'v' }, + openssl: { type: 'string', short: 'o' }, + guideline: { type: 'string', short: 'g' }, + hsts: { type: 'boolean' }, + ocsp: { type: 'boolean' }, + help: { type: 'boolean', short: 'h' }, + }, + }); + + if (values.help) { + show_help(); + return 0; + } + + if (!values.server) { + console.error('Error: --server is required'); + show_help(); + return 1; + } + + if (!servers.includes(values.server)) { + throw new Error(`unknown server '${values.server}'. Available servers: ${servers.join(', ')}`); + } + + if (!values.config) { + throw new Error('--config is required (modern, intermediate, old)'); + } + + const { form, output } = await get_state({ + server: values.server, + config: values.config, + serverVersion: values.version, + opensslVersion: values.openssl, + guideln: values.guideline, + hsts: values.hsts, + ocsp: values.ocsp, + }); + if (!form || !output) { + return 1; + } + + if (output.protocols.length === 0) { + process.stdout.write(`# unfortunately, ${form.version_tags} is not supported with these software versions.\n`); + return 1; + } + + const template = require('../src/js/helpers/' + values.server + '.js').default; + process.stdout.write(template(form, output)); + + return 0; +} + +main().catch(err => { + console.error('Error: %s', err.message); + return 1; +}).then(exitCode => { + process.exit(exitCode); +}); diff --git a/bin/tlsref.js b/bin/tlsref.js new file mode 100755 index 0000000..ac715fc --- /dev/null +++ b/bin/tlsref.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node +'use strict'; + +const babel = require('@babel/core'); +const Module = require('module'); +const fs = require('fs'); + +// Hook into Node's module loader to transform ES module syntax in source files +const original = Module._extensions['.js']; +Module._extensions['.js'] = function (mod, filename) { + if (filename.includes('node_modules')) { + return original(mod, filename); + } + const code = fs.readFileSync(filename, 'utf8'); + const result = babel.transformSync(code, { + filename, + presets: [['@babel/preset-env', { targets: { node: 'current' }, modules: 'commonjs' }]], + plugins: ['@babel/plugin-transform-object-rest-spread'], + }); + mod._compile(result.code, filename); +}; + +require('./cli.js'); diff --git a/src/js/state.js b/src/js/state.js index 39e1229..7421661 100644 --- a/src/js/state.js +++ b/src/js/state.js @@ -6,34 +6,37 @@ import { xmlEntities } from './utils.js'; const guideln_latest = '6.0'; // update when guideline changes const guidelines = {}; -export default async function () { - - async function fetch_guideline(guideln) { - // check for numerical version string, e.g. digit.digit - if (isNaN(guideln) || isNaN(parseFloat(guideln))) { - return guideln_latest; // invalid numerical version string +async function fetch_guideline(guideln) { + // check for numerical version string, e.g. digit.digit + if (isNaN(guideln) || isNaN(parseFloat(guideln))) { + return guideln_latest; // invalid numerical version string + } + const url = `https://data.tlsref.org/guidelines/${guideln}.json`; + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`error retrieving ${url}: ${response.status}`); } - const url = "https://data.tlsref.org/guidelines/"+guideln+".json"; - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`error retrieving ${url}: ${response.status}`); - } - guidelines[guideln] = await response.json(); - return guideln; - } catch (error) { - console.error(error.message); - return guideln_latest; - } + guidelines[guideln] = await response.json(); + return guideln; + } catch (error) { + console.error("Error: %s", error.message); + return guideln_latest; + } +} + +export async function get_state({server, config, serverVersion, opensslVersion, guideln, hsts, ocsp, url = new URL('https://configurator.tlsref.org/'), gitrev = ''}) { + if (!serverVersion) { + serverVersion = configs[server].latestVersion; + } + if (!opensslVersion) { + opensslVersion = configs.openssl.latestVersion; + } + if (!guideln) { + guideln = guideln_latest; } - const form = document.getElementById('form-generator').elements; - const config = form['config'].value; - const server = form['server'].value; - let guideln = form['guideline'].value !== '' - ? form['guideline'].value - : guideln_latest; let sstls = guidelines[guideln]; if (!sstls) { guideln = await fetch_guideline(guideln); @@ -56,37 +59,44 @@ export default async function () { // note: sstls.version for '5.0' is rendered as number 5, not string '5.0' sstls = guidelines[guideln]; } - const ssc = sstls.configurations[form['config'].value]; // server side tls config for that level + + const available_configs = Object.keys(sstls.configurations); + if (!available_configs.includes(config)) { + console.error("Error: config '%s' is not available in guideline %s (use: %s)", config, guideln, available_configs.join(', ')); + return { form: undefined, output: undefined }; + } + const ssc = sstls.configurations[config]; // server side tls config for that level const supportsOcspStapling = configs[server].supportsOcspStapling - && minver(configs[server].supportsOcspStapling, form['version'].value); - - const url = new URL(document.location); + && minver(configs[server].supportsOcspStapling, serverVersion); + + const usesOpenssl = configs[server].usesOpenssl !== false; + const supportsHsts = configs[server].supportsHsts !== false && hsts; // generate the fragment - let fragment = `server=${server}&version=${form['version'].value}&config=${config}`; - fragment += configs[server].usesOpenssl !== false ? `&openssl=${form['openssl'].value}` : ''; - fragment += configs[server].supportsHsts !== false && form['hsts'].checked ? '&hsts' : ''; - fragment += supportsOcspStapling && form['ocsp'].checked ? '&ocsp' : ''; + let fragment = `server=${server}&version=${serverVersion}&config=${config}`; + fragment += usesOpenssl ? `&openssl=${opensslVersion}` : ''; + fragment += supportsHsts ? '&hsts' : ''; + fragment += supportsOcspStapling && ocsp ? '&ocsp' : ''; fragment += `&guideline=${guideln}`; // generate the version tags - let version_tags = `${configs[server].name} ${form['version'].value}`; + let version_tags = `${configs[server].name} ${serverVersion}`; if (configs[server].eolBefore - && !minver(configs[server].eolBefore, form['version'].value)) { + && !minver(configs[server].eolBefore, serverVersion)) { version_tags += ' (UNSUPPORTED; end-of-life)'; } if (configs[server].usesOpenssl !== false) { - version_tags += `, OpenSSL ${form['openssl'].value}`; - if (!minver(configs['openssl'].eolBefore, form['openssl'].value)) { + version_tags += `, OpenSSL ${opensslVersion}`; + if (!minver(configs['openssl'].eolBefore, opensslVersion)) { version_tags += ' (UNSUPPORTED; end-of-life)'; } - else if (!minver("3.5.0", form['openssl'].value) + else if (!minver("3.5.0", opensslVersion) && minver("5.8", guideln)) { version_tags += ' (OLD: missing PQC hybrid MLKEMs)'; } } - version_tags += `, ${form['config'].value} config`; + version_tags += `, ${config} config`; // html-escape version_tags (even though version_tags is also used // outside HTML contexts, HTML is not expected in version strings) @@ -95,9 +105,8 @@ export default async function () { // generate the header const date = new Date().toISOString().substr(0, 10); let header = `generated ${date}, TLSRef Guideline v${guideln}, ${version_tags}`; - header += configs[server].supportsHsts !== false && form['hsts'].checked ? ', HSTS' : ''; - header += supportsOcspStapling && form['ocsp'].checked ? ', OCSP' : ''; - const gitrev = document.getElementById('gitrev').value; + header += supportsHsts ? ', HSTS' : ''; + header += supportsOcspStapling && ocsp ? ', OCSP' : ''; header += gitrev ? `, gitrev=${gitrev}` : ''; const link = `${url.origin}${url.pathname}#${fragment}`; @@ -105,12 +114,12 @@ export default async function () { // we need to remove TLS 1.3 from the supported protocols if the software is too old let protocols = ssc.tls_versions; if (!configs[server].tls13 - || !minver(configs[server].tls13, form['version'].value) - || !minver(configs['openssl'].tls13, form['openssl'].value)) { + || !minver(configs[server].tls13, serverVersion) + || !minver(configs['openssl'].tls13, opensslVersion)) { protocols = protocols.filter(ciphers => ciphers !== 'TLSv1.3'); } let tlsCurves = ssc.tls_curves; - if (!minver('3.5.0', form['openssl'].value)) { + if (!minver('3.5.0', opensslVersion)) { // future: may need to filter 'X25519MLKEM768','SecP256r1MLKEM768','SecP384r1MLKEM1024' tlsCurves = tlsCurves.filter(groups => groups !== 'X25519MLKEM768'); } @@ -122,24 +131,22 @@ export default async function () { : cipherFormat === 'go' ? configs['go'].supportedCiphers : null; if (supportedCiphers) { ciphers = ciphers.filter(suite => supportedCiphers.indexOf(suite) !== -1); - } else { - ciphers = ciphers; } if (ciphers.length && ciphers[0] === '@SECLEVEL=0') ciphers.shift(); - if (configs[server].usesOpenssl !== false && minver('3.0.0', form['openssl'].value)) { + if (configs[server].usesOpenssl !== false && minver('3.0.0', opensslVersion)) { // set SECLEVEL=0 via cipher string to support TLSv1-1.1 "old" with OpenSSL 3.x if (protocols.includes('TLSv1.1')) ciphers.unshift('@SECLEVEL=0'); } - const state = { + return { form: { - config: form['config'].value, - hsts: form['hsts'].checked && configs[server].supportsHsts !== false, - ocsp: form['ocsp'].checked && supportsOcspStapling, - opensslVersion: form['openssl'].value, + config, + hsts: supportsHsts, + ocsp: ocsp && supportsOcspStapling, + opensslVersion, server, serverName: configs[server].name, - serverVersion: form['version'].value, + serverVersion, version_tags, }, output: { @@ -152,24 +159,39 @@ export default async function () { hasVersions: configs[server].hasVersions !== false, header, hstsMaxAge: ssc.hsts_min_age, - //hstsRedirectCode: form['config'].value === 'old' ? 301 : 308, + //hstsRedirectCode: config === 'old' ? 301 : 308, hstsRedirectCode: 308, latestVersion: configs[server].latestVersion, link, oldestClients: ssc.oldest_clients, origin: url.origin, - protocols: protocols, + protocols, serverPreferredOrder: ssc.server_preferred_order, showSupports: configs[server].showSupports !== false, supportsHsts: configs[server].supportsHsts !== false, - supportsOcspStapling: supportsOcspStapling, - tlsCurves: tlsCurves, + supportsOcspStapling, + tlsCurves, // XXX: If DHE ciphers removed from guidelines, then usesDhe, dhCommand, // dhParamSize, and helpers/*.js code which uses them can be removed - usesDhe: ciphers.join(":").includes(":DHE") || ciphers.join(":").includes("_DHE_"), - usesOpenssl: configs[server].usesOpenssl !== false, + usesDhe: ciphers.join(":").includes(":DHE") || ciphers.join(":").includes("_DHE_"), + usesOpenssl, }, - }; + } +} + +export default async function () { + + const form = document.getElementById('form-generator').elements; - return state; + return get_state({ + server: form['server'].value, + config: form['config'].value, + serverVersion: form['version'].value, + opensslVersion: form['openssl'].value, + guideln: form['guideline'].value, + hsts: !!form['hsts'].checked, + ocsp: !!form['ocsp'].checked, + url: new URL(document.location), + gitrev: document.getElementById('gitrev').value, + }) };