localssl-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/bin/localssl.js +81 -0
- package/package.json +37 -0
- package/src/bootstrap.js +175 -0
- package/src/certgen.js +66 -0
- package/src/ci.js +51 -0
- package/src/frameworks/cra.js +42 -0
- package/src/frameworks/detect.js +23 -0
- package/src/frameworks/express.js +17 -0
- package/src/frameworks/index.js +43 -0
- package/src/frameworks/nextjs.js +35 -0
- package/src/frameworks/vite.js +30 -0
- package/src/frameworks/webpack.js +30 -0
- package/src/gitignore.js +30 -0
- package/src/index.js +16 -0
- package/src/init.js +10 -0
- package/src/mobile.js +53 -0
- package/src/remove.js +34 -0
- package/src/renew.js +11 -0
- package/src/status.js +53 -0
- package/src/team.js +110 -0
- package/src/trust/chromium.js +106 -0
- package/src/trust/firefox.js +69 -0
- package/src/trust/linux.js +30 -0
- package/src/trust/macos.js +25 -0
- package/src/trust/windows.js +21 -0
- package/src/trust.js +9 -0
- package/src/use.js +40 -0
- package/src/utils.js +52 -0
package/src/gitignore.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { PROJECT_DIR } = require('./utils');
|
|
4
|
+
|
|
5
|
+
async function ensureGitignore() {
|
|
6
|
+
const gitignorePath = path.join(PROJECT_DIR, '.gitignore');
|
|
7
|
+
const required = ['.localssl/*.pem', '.localssl/*.key', '.localssl/key.pem'];
|
|
8
|
+
|
|
9
|
+
let current = '';
|
|
10
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
11
|
+
current = await fs.readFile(gitignorePath, 'utf8');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let updated = current;
|
|
15
|
+
let changed = false;
|
|
16
|
+
for (const item of required) {
|
|
17
|
+
if (!updated.includes(item)) {
|
|
18
|
+
updated += `${updated.endsWith('\n') || !updated ? '' : '\n'}${item}\n`;
|
|
19
|
+
changed = true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (changed) {
|
|
24
|
+
await fs.writeFile(gitignorePath, updated);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return changed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { ensureGitignore };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function getHttpsOptions(baseDir = process.cwd()) {
|
|
5
|
+
const certPath = path.join(baseDir, '.localssl', 'cert.pem');
|
|
6
|
+
const keyPath = path.join(baseDir, '.localssl', 'key.pem');
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
cert: fs.readFileSync(certPath),
|
|
10
|
+
key: fs.readFileSync(keyPath)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
getHttpsOptions
|
|
16
|
+
};
|
package/src/init.js
ADDED
package/src/mobile.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const detectPort = require('detect-port');
|
|
5
|
+
const qrcode = require('qrcode-terminal');
|
|
6
|
+
const { LOCALSSL_CA_PUBLIC, info } = require('./utils');
|
|
7
|
+
|
|
8
|
+
function getLocalIpAddress() {
|
|
9
|
+
const interfaces = os.networkInterfaces();
|
|
10
|
+
for (const values of Object.values(interfaces)) {
|
|
11
|
+
for (const item of values || []) {
|
|
12
|
+
if (item.family === 'IPv4' && !item.internal) {
|
|
13
|
+
return item.address;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return '127.0.0.1';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function runQrServer() {
|
|
21
|
+
if (!(await fs.pathExists(LOCALSSL_CA_PUBLIC))) {
|
|
22
|
+
throw new Error('No machine CA found. Run: localssl init');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const crt = await fs.readFile(LOCALSSL_CA_PUBLIC);
|
|
26
|
+
const host = getLocalIpAddress();
|
|
27
|
+
const port = await detectPort(9999);
|
|
28
|
+
|
|
29
|
+
const server = http.createServer((req, res) => {
|
|
30
|
+
if (req.url === '/ca.crt') {
|
|
31
|
+
res.writeHead(200, {
|
|
32
|
+
'Content-Type': 'application/x-x509-ca-cert',
|
|
33
|
+
'Content-Disposition': 'attachment; filename="localssl-ca.crt"'
|
|
34
|
+
});
|
|
35
|
+
res.end(crt);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
res.writeHead(404);
|
|
40
|
+
res.end('Not found');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await new Promise((resolve) => server.listen(port, resolve));
|
|
44
|
+
|
|
45
|
+
const url = `http://${host}:${port}/ca.crt`;
|
|
46
|
+
info(`Scan this QR from your phone:`);
|
|
47
|
+
qrcode.generate(url, { small: true });
|
|
48
|
+
info(`iOS: Settings > General > VPN & Device Management > Install > Certificate Trust Settings`);
|
|
49
|
+
info(`Android: Settings > Security > Install from storage`);
|
|
50
|
+
info(`Serving CA at ${url} (Ctrl+C to stop)`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { runQrServer };
|
package/src/remove.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { LOCALSSL_HOME, LOCALSSL_CA_PUBLIC, PROJECT_LOCALSSL_DIR, PROJECT_DIR } = require('./utils');
|
|
4
|
+
const { untrustCertificate: untrustMac } = require('./trust/macos');
|
|
5
|
+
const { untrustCertificate: untrustWindows } = require('./trust/windows');
|
|
6
|
+
const { untrustCertificate: untrustLinux } = require('./trust/linux');
|
|
7
|
+
const { untrustInFirefox } = require('./trust/firefox');
|
|
8
|
+
const { untrustInChromium } = require('./trust/chromium');
|
|
9
|
+
|
|
10
|
+
async function removeTrust() {
|
|
11
|
+
if (process.platform === 'darwin') await untrustMac(LOCALSSL_CA_PUBLIC);
|
|
12
|
+
else if (process.platform === 'win32') await untrustWindows(LOCALSSL_CA_PUBLIC);
|
|
13
|
+
else await untrustLinux();
|
|
14
|
+
|
|
15
|
+
await untrustInFirefox();
|
|
16
|
+
await untrustInChromium();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function cleanupFrameworkArtifacts() {
|
|
20
|
+
const helper = path.join(PROJECT_DIR, 'localssl.js');
|
|
21
|
+
if (await fs.pathExists(helper)) {
|
|
22
|
+
await fs.remove(helper);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function removeAll() {
|
|
27
|
+
await removeTrust().catch(() => {});
|
|
28
|
+
await fs.remove(LOCALSSL_HOME).catch(() => {});
|
|
29
|
+
await fs.remove(PROJECT_LOCALSSL_DIR).catch(() => {});
|
|
30
|
+
await cleanupFrameworkArtifacts();
|
|
31
|
+
console.log(' localssl removed ✓');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { removeAll };
|
package/src/renew.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const { ensureMkcert } = require('./bootstrap');
|
|
2
|
+
const { detectHostsFromProject, generateProjectCert } = require('./certgen');
|
|
3
|
+
|
|
4
|
+
async function renew() {
|
|
5
|
+
const mkcertPath = await ensureMkcert();
|
|
6
|
+
const hosts = await detectHostsFromProject();
|
|
7
|
+
await generateProjectCert({ mkcertPath, hosts, force: true });
|
|
8
|
+
console.log(' Project certificate renewed ✓');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = { renew };
|
package/src/status.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const { X509Certificate } = require('crypto');
|
|
3
|
+
const { LOCALSSL_CA_PUBLIC, PROJECT_CERT, PROJECT_CONFIG, PROJECT_LOCALSSL_DIR } = require('./utils');
|
|
4
|
+
const { detectFramework } = require('./frameworks/detect');
|
|
5
|
+
|
|
6
|
+
function certSummary(label, certPem) {
|
|
7
|
+
const cert = new X509Certificate(certPem);
|
|
8
|
+
const expiry = new Date(cert.validTo);
|
|
9
|
+
const days = Math.ceil((expiry.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
10
|
+
return { label, expiry: expiry.toISOString().slice(0, 10), days };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function status() {
|
|
14
|
+
const rows = [];
|
|
15
|
+
|
|
16
|
+
if (await fs.pathExists(LOCALSSL_CA_PUBLIC)) {
|
|
17
|
+
const caText = await fs.readFile(LOCALSSL_CA_PUBLIC, 'utf8');
|
|
18
|
+
rows.push(certSummary('Machine CA', caText));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (await fs.pathExists(PROJECT_CERT)) {
|
|
22
|
+
const certText = await fs.readFile(PROJECT_CERT, 'utf8');
|
|
23
|
+
rows.push(certSummary('Project cert', certText));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const row of rows) {
|
|
27
|
+
console.log(` ${row.label.padEnd(14)} ${row.expiry} (${row.days} days)`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const framework = await detectFramework();
|
|
31
|
+
console.log(` Framework: ${framework}`);
|
|
32
|
+
|
|
33
|
+
if (await fs.pathExists(PROJECT_CONFIG)) {
|
|
34
|
+
const cfg = await fs.readJson(PROJECT_CONFIG);
|
|
35
|
+
console.log(` Hosts: ${(cfg.hosts || []).join(', ') || 'localhost'}`);
|
|
36
|
+
console.log(` Team members: ${(cfg.team || []).length}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!(await fs.pathExists(PROJECT_LOCALSSL_DIR))) {
|
|
40
|
+
console.log(' localssl: not configured');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const warning = rows.find((r) => r.days <= 30);
|
|
45
|
+
if (warning) {
|
|
46
|
+
console.log(' Warning: cert expires in under 30 days. Run localssl renew');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(' All good ✓');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { status };
|
package/src/team.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const { PROJECT_CONFIG, LOCALSSL_CA_PUBLIC, PROJECT_DIR } = require('./utils');
|
|
5
|
+
const { trustCertificate: trustMac } = require('./trust/macos');
|
|
6
|
+
const { trustCertificate: trustWindows } = require('./trust/windows');
|
|
7
|
+
const { trustCertificate: trustLinux } = require('./trust/linux');
|
|
8
|
+
const { trustInFirefox } = require('./trust/firefox');
|
|
9
|
+
const { trustInChromium } = require('./trust/chromium');
|
|
10
|
+
|
|
11
|
+
function defaultConfig(hosts = []) {
|
|
12
|
+
return {
|
|
13
|
+
version: 1,
|
|
14
|
+
hosts,
|
|
15
|
+
team: []
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sanitizePem(pem) {
|
|
20
|
+
return pem.replace(/\r\n/g, '\n').trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assertPublicCert(pem) {
|
|
24
|
+
if (!pem.includes('BEGIN CERTIFICATE') || pem.includes('PRIVATE KEY')) {
|
|
25
|
+
throw new Error('localssl.json may only contain public certificates.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function loadTeamConfig(hosts = []) {
|
|
30
|
+
if (!(await fs.pathExists(PROJECT_CONFIG))) {
|
|
31
|
+
const cfg = defaultConfig(hosts);
|
|
32
|
+
await fs.writeJson(PROJECT_CONFIG, cfg, { spaces: 2 });
|
|
33
|
+
return cfg;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const cfg = await fs.readJson(PROJECT_CONFIG);
|
|
37
|
+
cfg.version = cfg.version || 1;
|
|
38
|
+
cfg.hosts = Array.isArray(cfg.hosts) ? cfg.hosts : hosts;
|
|
39
|
+
cfg.team = Array.isArray(cfg.team) ? cfg.team : [];
|
|
40
|
+
return cfg;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function saveTeamConfig(config) {
|
|
44
|
+
for (const entry of config.team || []) {
|
|
45
|
+
assertPublicCert(entry.caPubCert || '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await fs.writeJson(PROJECT_CONFIG, config, { spaces: 2 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function syncCurrentMachine(hosts) {
|
|
52
|
+
const config = await loadTeamConfig(hosts);
|
|
53
|
+
const caPubCert = sanitizePem(await fs.readFile(LOCALSSL_CA_PUBLIC, 'utf8'));
|
|
54
|
+
assertPublicCert(caPubCert);
|
|
55
|
+
|
|
56
|
+
const machine = os.hostname();
|
|
57
|
+
const existing = config.team.find((m) => m.machine === machine);
|
|
58
|
+
const payload = {
|
|
59
|
+
machine,
|
|
60
|
+
os: process.platform,
|
|
61
|
+
caPubCert,
|
|
62
|
+
addedAt: new Date().toISOString().slice(0, 10)
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (existing) {
|
|
66
|
+
Object.assign(existing, payload);
|
|
67
|
+
} else {
|
|
68
|
+
config.team.push(payload);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
config.hosts = [...new Set([...(config.hosts || []), ...hosts])];
|
|
72
|
+
await saveTeamConfig(config);
|
|
73
|
+
|
|
74
|
+
return config;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function trustTeamCertificates(config) {
|
|
78
|
+
let trusted = 0;
|
|
79
|
+
|
|
80
|
+
for (const member of config.team || []) {
|
|
81
|
+
if (!member.caPubCert || member.machine === os.hostname()) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
assertPublicCert(member.caPubCert);
|
|
86
|
+
const tempPath = path.join(PROJECT_DIR, '.localssl', `${member.machine}.crt`);
|
|
87
|
+
await fs.ensureDir(path.dirname(tempPath));
|
|
88
|
+
await fs.writeFile(tempPath, `${member.caPubCert}\n`);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
if (process.platform === 'darwin') await trustMac(tempPath);
|
|
92
|
+
else if (process.platform === 'win32') await trustWindows(tempPath);
|
|
93
|
+
else await trustLinux(tempPath);
|
|
94
|
+
|
|
95
|
+
await trustInFirefox(tempPath);
|
|
96
|
+
await trustInChromium(tempPath);
|
|
97
|
+
trusted += 1;
|
|
98
|
+
} catch {
|
|
99
|
+
// best-effort trust for teammate certs
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return trusted;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
syncCurrentMachine,
|
|
108
|
+
trustTeamCertificates,
|
|
109
|
+
loadTeamConfig
|
|
110
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const { execFile } = require('child_process');
|
|
5
|
+
|
|
6
|
+
function hasCertUtil() {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
execFile('certutil', ['-H'], (error) => resolve(!error));
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function getProfileRoots() {
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
|
|
15
|
+
if (process.platform === 'win32') {
|
|
16
|
+
const local = process.env.LOCALAPPDATA || '';
|
|
17
|
+
return [
|
|
18
|
+
path.join(local, 'Google', 'Chrome', 'User Data'),
|
|
19
|
+
path.join(local, 'Microsoft', 'Edge', 'User Data')
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (process.platform === 'darwin') {
|
|
24
|
+
return [
|
|
25
|
+
path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'),
|
|
26
|
+
path.join(home, 'Library', 'Application Support', 'Microsoft Edge')
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return [
|
|
31
|
+
path.join(home, '.config', 'google-chrome'),
|
|
32
|
+
path.join(home, '.config', 'chromium'),
|
|
33
|
+
path.join(home, '.config', 'microsoft-edge')
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function collectNssDatabases() {
|
|
38
|
+
const dbs = new Set();
|
|
39
|
+
const home = os.homedir();
|
|
40
|
+
|
|
41
|
+
const commonDb = path.join(home, '.pki', 'nssdb');
|
|
42
|
+
if (await fs.pathExists(path.join(commonDb, 'cert9.db'))) {
|
|
43
|
+
dbs.add(commonDb);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const roots = await getProfileRoots();
|
|
47
|
+
for (const root of roots) {
|
|
48
|
+
if (!(await fs.pathExists(root))) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const entries = await fs.readdir(root).catch(() => []);
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (entry !== 'Default' && !entry.startsWith('Profile ')) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const profile = path.join(root, entry);
|
|
59
|
+
if (await fs.pathExists(path.join(profile, 'cert9.db'))) {
|
|
60
|
+
dbs.add(profile);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return [...dbs];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function runCertUtil(args) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
execFile('certutil', args, () => resolve());
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function trustInChromium(certPath) {
|
|
75
|
+
const available = await hasCertUtil();
|
|
76
|
+
if (!available) {
|
|
77
|
+
return { trusted: false, reason: 'certutil not found' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const databases = await collectNssDatabases();
|
|
81
|
+
if (!databases.length) {
|
|
82
|
+
return { trusted: false, reason: 'no Chrome/Edge NSS DB found' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let applied = 0;
|
|
86
|
+
for (const db of databases) {
|
|
87
|
+
await runCertUtil(['-A', '-n', 'localssl', '-t', 'C,,', '-i', certPath, '-d', `sql:${db}`]);
|
|
88
|
+
applied += 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { trusted: true, reason: `${applied} NSS DB(s)` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function untrustInChromium() {
|
|
95
|
+
const available = await hasCertUtil();
|
|
96
|
+
if (!available) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const databases = await collectNssDatabases();
|
|
101
|
+
for (const db of databases) {
|
|
102
|
+
await runCertUtil(['-D', '-n', 'localssl', '-d', `sql:${db}`]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { trustInChromium, untrustInChromium };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const { execFile } = require('child_process');
|
|
5
|
+
|
|
6
|
+
function getFirefoxProfilesDir() {
|
|
7
|
+
const home = os.homedir();
|
|
8
|
+
if (process.platform === 'win32') {
|
|
9
|
+
return path.join(process.env.APPDATA || '', 'Mozilla', 'Firefox', 'Profiles');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (process.platform === 'darwin') {
|
|
13
|
+
return path.join(home, 'Library', 'Application Support', 'Firefox', 'Profiles');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return path.join(home, '.mozilla', 'firefox');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function hasCertUtil() {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
execFile('certutil', ['-H'], (error) => resolve(!error));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function listProfiles() {
|
|
26
|
+
const dir = getFirefoxProfilesDir();
|
|
27
|
+
if (!(await fs.pathExists(dir))) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const entries = await fs.readdir(dir);
|
|
32
|
+
return entries.map((entry) => path.join(dir, entry));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function trustInFirefox(certPath) {
|
|
36
|
+
const available = await hasCertUtil();
|
|
37
|
+
if (!available) {
|
|
38
|
+
return { trusted: false, reason: 'certutil not found' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const profiles = await listProfiles();
|
|
42
|
+
if (!profiles.length) {
|
|
43
|
+
return { trusted: false, reason: 'Firefox not found' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const profile of profiles) {
|
|
47
|
+
await new Promise((resolve) => {
|
|
48
|
+
execFile('certutil', ['-A', '-n', 'localssl', '-t', 'C,,', '-i', certPath, '-d', `sql:${profile}`], () => resolve());
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { trusted: true, reason: `${profiles.length} profile(s)` };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function untrustInFirefox() {
|
|
56
|
+
const available = await hasCertUtil();
|
|
57
|
+
if (!available) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const profiles = await listProfiles();
|
|
62
|
+
for (const profile of profiles) {
|
|
63
|
+
await new Promise((resolve) => {
|
|
64
|
+
execFile('certutil', ['-D', '-n', 'localssl', '-d', `sql:${profile}`], () => resolve());
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { trustInFirefox, untrustInFirefox };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const systemCertPath = '/usr/local/share/ca-certificates/localssl-ca.crt';
|
|
6
|
+
|
|
7
|
+
async function trustCertificate(certPath) {
|
|
8
|
+
await fs.copy(certPath, systemCertPath);
|
|
9
|
+
await new Promise((resolve, reject) => {
|
|
10
|
+
execFile('update-ca-certificates', [], (error) => {
|
|
11
|
+
if (error) {
|
|
12
|
+
reject(new Error('Linux trust update failed. Install ca-certificates tools and re-run with sudo.'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
resolve();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function untrustCertificate() {
|
|
21
|
+
if (await fs.pathExists(systemCertPath)) {
|
|
22
|
+
await fs.remove(systemCertPath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await new Promise((resolve) => {
|
|
26
|
+
execFile('update-ca-certificates', ['--fresh'], () => resolve());
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { trustCertificate, untrustCertificate, systemCertPath };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
|
+
|
|
3
|
+
function trustCertificate(certPath) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
execFile(
|
|
6
|
+
'security',
|
|
7
|
+
['add-trusted-cert', '-d', '-r', 'trustRoot', '-k', '/Library/Keychains/System.keychain', certPath],
|
|
8
|
+
(error) => {
|
|
9
|
+
if (error) {
|
|
10
|
+
reject(new Error('Admin privileges required to install CA on macOS. Re-run with sudo.'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
resolve();
|
|
14
|
+
}
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function untrustCertificate(certPath) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
execFile('security', ['remove-trusted-cert', '-d', certPath], () => resolve());
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { trustCertificate, untrustCertificate };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
|
+
|
|
3
|
+
function trustCertificate(certPath) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
execFile('certutil', ['-addstore', '-f', 'ROOT', certPath], { windowsHide: true }, (error) => {
|
|
6
|
+
if (error) {
|
|
7
|
+
reject(new Error('Admin privileges required to install CA on Windows. Re-run terminal as Administrator.'));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
resolve();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function untrustCertificate(certPath) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
execFile('certutil', ['-delstore', 'ROOT', certPath], { windowsHide: true }, () => resolve());
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { trustCertificate, untrustCertificate };
|
package/src/trust.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const { loadTeamConfig, trustTeamCertificates } = require('./team');
|
|
2
|
+
|
|
3
|
+
async function trustTeam() {
|
|
4
|
+
const config = await loadTeamConfig();
|
|
5
|
+
const trusted = await trustTeamCertificates(config);
|
|
6
|
+
console.log(` Trusted ${trusted} teammate CA certificate(s) ✓`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = { trustTeam };
|
package/src/use.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { initMachine } = require('./bootstrap');
|
|
2
|
+
const { detectHostsFromProject, generateProjectCert } = require('./certgen');
|
|
3
|
+
const { configureFramework } = require('./frameworks');
|
|
4
|
+
const { ensureGitignore } = require('./gitignore');
|
|
5
|
+
const { syncCurrentMachine, trustTeamCertificates } = require('./team');
|
|
6
|
+
const { step, info } = require('./utils');
|
|
7
|
+
|
|
8
|
+
async function useProject(hostsArg = []) {
|
|
9
|
+
info('Setting up local HTTPS for this project...');
|
|
10
|
+
|
|
11
|
+
const total = 4;
|
|
12
|
+
const { mkcertPath } = await initMachine({ quiet: true });
|
|
13
|
+
step(1, total, 'Installing local CA on your machine...', 'ok');
|
|
14
|
+
|
|
15
|
+
const detectedHosts = await detectHostsFromProject();
|
|
16
|
+
const hosts = hostsArg.length ? [...new Set([...detectedHosts, ...hostsArg])] : detectedHosts;
|
|
17
|
+
const certResult = await generateProjectCert({ mkcertPath, hosts });
|
|
18
|
+
step(
|
|
19
|
+
2,
|
|
20
|
+
total,
|
|
21
|
+
`Generating certificate for ${hosts[0]}...`,
|
|
22
|
+
certResult.generated ? 'ok' : 'skip',
|
|
23
|
+
certResult.generated ? '(valid 825 days)' : '(already configured)'
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const frameworkResult = await configureFramework(certResult.certPath, certResult.keyPath);
|
|
27
|
+
step(3, total, `Configuring ${frameworkResult.framework}...`, frameworkResult.updated ? 'ok' : 'skip', `(${frameworkResult.details})`);
|
|
28
|
+
|
|
29
|
+
const gitignoreChanged = await ensureGitignore();
|
|
30
|
+
step(4, total, 'Updating .gitignore...', gitignoreChanged ? 'ok' : 'skip');
|
|
31
|
+
|
|
32
|
+
const teamConfig = await syncCurrentMachine(hosts);
|
|
33
|
+
const trusted = await trustTeamCertificates(teamConfig);
|
|
34
|
+
|
|
35
|
+
info('Done. Start your dev server — HTTPS is ready.');
|
|
36
|
+
info('Run: npm run dev');
|
|
37
|
+
info(`Team CAs trusted: ${trusted}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { useProject };
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
const HOME_DIR = os.homedir();
|
|
6
|
+
const LOCALSSL_HOME = path.join(HOME_DIR, '.localssl');
|
|
7
|
+
const LOCALSSL_CA_PUBLIC = path.join(LOCALSSL_HOME, 'ca.crt');
|
|
8
|
+
const LOCALSSL_CA_KEY = path.join(LOCALSSL_HOME, 'ca.key');
|
|
9
|
+
const PROJECT_DIR = process.cwd();
|
|
10
|
+
const PROJECT_LOCALSSL_DIR = path.join(PROJECT_DIR, '.localssl');
|
|
11
|
+
const PROJECT_CERT = path.join(PROJECT_LOCALSSL_DIR, 'cert.pem');
|
|
12
|
+
const PROJECT_KEY = path.join(PROJECT_LOCALSSL_DIR, 'key.pem');
|
|
13
|
+
const PROJECT_CONFIG = path.join(PROJECT_DIR, 'localssl.json');
|
|
14
|
+
|
|
15
|
+
function step(index, total, message, status, details = '') {
|
|
16
|
+
const state = status === 'ok' ? chalk.green('✓') : status === 'skip' ? chalk.gray('↷') : chalk.red('✗');
|
|
17
|
+
const extra = details ? ` ${chalk.gray(details)}` : '';
|
|
18
|
+
console.log(` [${index}/${total}] ${message} ${state}${extra}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function info(message) {
|
|
22
|
+
console.log(` ${message}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function warn(message) {
|
|
26
|
+
console.log(chalk.yellow(` ${message}`));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function err(message) {
|
|
30
|
+
console.error(chalk.red(` ${message}`));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isCI() {
|
|
34
|
+
return process.env.CI === 'true' || process.env.CI === '1';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
HOME_DIR,
|
|
39
|
+
LOCALSSL_HOME,
|
|
40
|
+
LOCALSSL_CA_PUBLIC,
|
|
41
|
+
LOCALSSL_CA_KEY,
|
|
42
|
+
PROJECT_DIR,
|
|
43
|
+
PROJECT_LOCALSSL_DIR,
|
|
44
|
+
PROJECT_CERT,
|
|
45
|
+
PROJECT_KEY,
|
|
46
|
+
PROJECT_CONFIG,
|
|
47
|
+
step,
|
|
48
|
+
info,
|
|
49
|
+
warn,
|
|
50
|
+
err,
|
|
51
|
+
isCI
|
|
52
|
+
};
|