Skip to content

Commit 2111ffe

Browse files
committed
refactor(nginx): use acme.sh better, move nginx conf to templates
closes #363, closes #365, closes #348, closes #332, closes #183 - move nginx configuration to templates - rely on acme.sh for cronjobs, remove ssl-renew setup and command - cleanup & simplify nginx setup - add subdirectory support
1 parent be95343 commit 2111ffe

File tree

9 files changed

+137
-257
lines changed

9 files changed

+137
-257
lines changed

extensions/nginx/.gitignore

-1
This file was deleted.

extensions/nginx/commands/ssl-renew.js

-22
This file was deleted.

extensions/nginx/index.js

+89-154
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
'use strict';
22

33
const fs = require('fs-extra');
4+
const os = require('os');
45
const dns = require('dns');
56
const url = require('url');
67
const path = require('path');
78
const execa = require('execa');
8-
const template = require('lodash/template');
9-
109
const Promise = require('bluebird');
11-
const NginxConfFile = require('nginx-conf').NginxConfFile;
10+
const template = require('lodash/template');
1211

1312
const cli = require('../../lib');
1413

@@ -21,7 +20,6 @@ class NginxExtension extends cli.Extension {
2120

2221
cmd.addStage('nginx', this.setupNginx.bind(this), null, 'Nginx');
2322
cmd.addStage('ssl', this.setupSSL.bind(this), 'nginx', 'SSL');
24-
cmd.addStage('ssl-renew', this.setupRenew.bind(this), 'ssl', 'automatic SSL renewal');
2523
}
2624

2725
setupNginx(argv, ctx, task) {
@@ -37,57 +35,39 @@ class NginxExtension extends cli.Extension {
3735
return task.skip();
3836
}
3937

40-
if (parsedUrl.pathname !== '/') {
41-
this.ui.log('The Nginx service does not support subdirectory configurations yet. Skipping Nginx setup.', 'yellow');
42-
return task.skip();
43-
}
38+
let confFile = `${parsedUrl.hostname}.conf`;
4439

45-
if (fs.existsSync(`/etc/nginx/sites-available/${parsedUrl.hostname}.conf`)) {
40+
if (fs.existsSync(`/etc/nginx/sites-available/${confFile}`)) {
4641
this.ui.log('Nginx configuration already found for this url. Skipping Nginx setup.', 'yellow');
4742
return task.skip();
4843
}
4944

50-
return Promise.fromNode((cb) => NginxConfFile.createFromSource('', cb)).then((conf) => {
51-
conf.nginx._add('server');
52-
53-
let http = conf.nginx.server;
45+
let conf = template(fs.readFileSync(path.join(__dirname, 'templates', 'nginx.conf'), 'utf8'));
5446

55-
http._add('listen', '80');
56-
http._add('listen', '[::]:80');
57-
http._add('server_name', parsedUrl.hostname);
58-
59-
let rootPath = path.resolve(ctx.instance.dir, 'system', 'nginx-root');
60-
fs.ensureDirSync(rootPath);
61-
http._add('root', rootPath);
62-
63-
http._add('location', '/');
64-
this._addProxyBlock(http.location, ctx.instance.config.get('server.port'));
65-
66-
http._add('client_max_body_size', '50m');
67-
68-
let confFile = `${parsedUrl.hostname}.conf`;
69-
70-
return ctx.instance.template(
71-
conf.toString(),
72-
'nginx config',
73-
confFile,
74-
'/etc/nginx/sites-available'
75-
).then((generated) => {
76-
if (!generated) {
77-
this.ui.log('Nginx config not generated', 'yellow');
78-
return;
79-
}
80-
81-
ctx.instance.cliConfig.set('extension.nginx', true).save();
47+
let rootPath = path.resolve(ctx.instance.dir, 'system', 'nginx-root');
8248

83-
return this.ui.sudo(`ln -sf /etc/nginx/sites-available/${confFile} /etc/nginx/sites-enabled/${confFile}`)
84-
.then(() => this.restartNginx());
85-
});
49+
let generatedConfig = conf({
50+
url: parsedUrl.hostname,
51+
webroot: rootPath,
52+
location: parsedUrl.pathname !== '/' ? `^~ ${parsedUrl.pathname}` : '/',
53+
port: ctx.instance.config.get('server.port')
8654
});
55+
56+
return ctx.instance.template(
57+
generatedConfig,
58+
'nginx config',
59+
confFile,
60+
'/etc/nginx/sites-available'
61+
).then(() => {
62+
return this.ui.sudo(`ln -sf /etc/nginx/sites-available/${confFile} /etc/nginx/sites-enabled/${confFile}`);
63+
}).then(() => this.restartNginx());
8764
}
8865

8966
setupSSL(argv, ctx, task) {
90-
if (ctx.instance.cliConfig.get('extension.ssl', false)) {
67+
let parsedUrl = url.parse(ctx.instance.config.get('url'));
68+
let confFile = `${parsedUrl.hostname}-ssl.conf`;
69+
70+
if (fs.existsSync(`/etc/nginx/sites-available/${confFile}`)) {
9171
this.ui.log('SSL has already been set up, skipping', 'yellow');
9272
return task.skip();
9373
}
@@ -97,12 +77,7 @@ class NginxExtension extends cli.Extension {
9777
return task.skip();
9878
}
9979

100-
let parsedUrl = url.parse(ctx.instance.config.get('url'));
101-
102-
let confFile = `${parsedUrl.hostname}.conf`;
103-
let nginxConfPath = path.join(ctx.instance.dir, 'system', 'files', confFile);
104-
105-
if (!fs.existsSync(nginxConfPath)) {
80+
if (!fs.existsSync(`/etc/nginx/sites-available/${parsedUrl.hostname}.conf`)) {
10681
if (ctx.single) {
10782
this.ui.log('Nginx config file does not exist, skipping SSL setup', 'yellow');
10883
}
@@ -111,7 +86,7 @@ class NginxExtension extends cli.Extension {
11186
}
11287

11388
let rootPath = path.resolve(ctx.instance.dir, 'system', 'nginx-root');
114-
const letsencrypt = require('./letsencrypt');
89+
let dhparamFile = path.join(ctx.instance.dir, 'system', 'files', 'dhparam.pem');
11590

11691
return this.ui.listr([{
11792
title: 'Checking DNS resolution',
@@ -135,9 +110,9 @@ class NginxExtension extends cli.Extension {
135110
});
136111
}
137112
}, {
138-
title: 'Preparing Nginx for Let\'s Encrypt SSL certificate creation',
113+
title: 'Getting additional configuration',
139114
skip: (ctx) => ctx.dnsfail,
140-
task: (ctx) => {
115+
task: () => {
141116
let promise;
142117

143118
if (argv.sslemail) {
@@ -151,91 +126,68 @@ class NginxExtension extends cli.Extension {
151126
}).then(answer => { argv.sslemail = answer.email; });
152127
}
153128

154-
return promise.then(() => {
155-
return Promise.fromCallback((cb) => NginxConfFile.create(nginxConfPath, cb)).then((conf) => {
156-
ctx.ssl = {};
157-
158-
ctx.ssl.conf = conf;
159-
ctx.ssl.http = conf.nginx.server;
160-
161-
let location = ctx.ssl.http.location;
162-
163-
// Don't add well-known block if it already exists
164-
if (!Array.isArray(location) || location.length === 1) {
165-
ctx.ssl.http._add('location', '~ /.well-known');
166-
ctx.ssl.http.location[1]._add('allow', 'all');
167-
}
168-
});
169-
});
129+
return promise;
170130
}
171131
}, {
172-
title: 'Restarting Nginx',
173-
skip: (ctx) => ctx.dnsfail,
174-
task: () => this.restartNginx()
175-
}, {
176-
title: 'Getting SSL Certificate',
132+
title: 'Getting SSL Certificate from Let\'s Encrypt',
177133
skip: (ctx) => ctx.dnsfail,
178134
task: () => {
179-
return letsencrypt(ctx.instance, argv.sslemail, argv.sslstaging).catch((error) => {
180-
if (!(error instanceof cli.errors.ProcessError)) {
181-
return Promise.reject(error);
182-
}
135+
return execa.shell('curl https://get.acme.sh | sh').then(() => {
136+
let acmeScriptPath = path.join(os.homedir(), '.acme.sh', 'acme.sh');
183137

184-
// Ensure ~/.well-known location gets cleaned up
185-
ctx.ssl.http._remove('location', 1);
186-
return Promise.reject(error);
138+
let cmd = `${acmeScriptPath} --issue --domain ${parsedUrl.hostname} --webroot ${rootPath} ` +
139+
`--accountemail ${argv.sslemail}${argv.sslstaging ? ' --staging' : ''}`;
140+
141+
return execa.shell(cmd);
142+
}).catch((error) => {
143+
// Certs have been generated before, skip
144+
if (!error.stdout.match(/Skip/)) {
145+
return Promise.reject(new cli.errors.ProcessError(error));
146+
}
187147
});
188148
}
189149
}, {
190150
title: 'Generating Encryption Key (may take a few minutes)',
191151
skip: (ctx) => ctx.dnsfail,
192-
task: (ctx) => {
193-
ctx.ssl.dhparamOutFile = path.join(ctx.instance.dir, 'system', 'files', 'dhparam.pem');
194-
return execa.shell(`openssl dhparam -out ${ctx.ssl.dhparamOutFile} 2048`)
152+
task: () => {
153+
return execa.shell(`openssl dhparam -out ${dhparamFile} 2048`)
195154
.catch((error) => Promise.reject(new cli.errors.ProcessError(error)));
196155
}
197156
}, {
198-
title: 'Writing SSL parameters',
157+
title: 'Generating SSL security headers',
199158
skip: (ctx) => ctx.dnsfail,
200159
task: (ctx) => {
201-
let sslParamsTemplate = template(fs.readFileSync(path.join(__dirname, 'ssl-params.conf.template'), 'utf8'));
202-
return ctx.instance.template(sslParamsTemplate({
203-
dhparam: ctx.ssl.dhparamOutFile
204-
}), 'ssl parameters', 'ssl-params.conf');
160+
let sslParamsConf = template(fs.readFileSync(path.join(__dirname, 'templates', 'ssl-params.conf'), 'utf8'));
161+
return ctx.instance.template(
162+
sslParamsConf({ dhparam: dhparamFile }),
163+
'ssl security parameters',
164+
'ssl-params.conf'
165+
);
205166
}
206167
}, {
207-
title: 'Updating Nginx with SSL config',
168+
title: 'Generating SSL configuration',
208169
skip: (ctx) => ctx.dnsfail,
209170
task: (ctx) => {
210-
// add ssl server block
211-
ctx.ssl.conf.nginx._add('server');
212-
let https = ctx.ssl.conf.nginx.server[1];
213-
214-
// add listen directives
215-
https._add('listen', '443 ssl http2');
216-
https._add('listen', '[::]:443 ssl http2');
217-
https._add('server_name', parsedUrl.hostname);
218-
219-
let letsencryptPath = path.join(ctx.instance.dir, 'system', 'letsencrypt');
220-
221-
// add ssl cert directives
222-
https._add('ssl_certificate', path.join(letsencryptPath, 'fullchain.pem'));
223-
https._add('ssl_certificate_key', path.join(letsencryptPath, 'privkey.pem'));
224-
// add ssl-params snippet
225-
https._add('include', path.join(ctx.instance.dir, 'system', 'files', 'ssl-params.conf'));
226-
// add root directive
227-
https._add('root', rootPath);
228-
229-
https._add('location', '/');
230-
this._addProxyBlock(https.location, ctx.instance.config.get('server.port'));
231-
232-
https._add('client_max_body_size', '50m');
233-
234-
https._add('location', '~ /.well-known');
235-
https.location[1]._add('allow', 'all');
171+
let acmeFolder = path.join(os.homedir(), '.acme.sh', parsedUrl.hostname);
172+
let sslConf = template(fs.readFileSync(path.join(__dirname, 'templates', 'nginx-ssl.conf'), 'utf8'));
173+
let generatedSslConfig = sslConf({
174+
url: parsedUrl.hostname,
175+
webroot: rootPath,
176+
fullchain: path.join(acmeFolder, 'fullchain.cer'),
177+
privkey: path.join(acmeFolder, `${parsedUrl.hostname}.key`),
178+
sslparams: path.join(ctx.instance.dir, 'system', 'files', 'ssl-params.conf'),
179+
location: parsedUrl.pathname !== '/' ? `^~ ${parsedUrl.pathname}` : '/',
180+
port: ctx.instance.config.get('server.port')
181+
});
236182

237-
ctx.instance.cliConfig.set('extension.ssl', true)
238-
.set('extension.sslemail', argv.sslemail).save();
183+
return ctx.instance.template(
184+
generatedSslConfig,
185+
'ssl config',
186+
confFile,
187+
'/etc/nginx/sites-available'
188+
).then(
189+
() => this.ui.sudo(`ln -sf /etc/nginx/sites-available/${confFile} /etc/nginx/sites-enabled/${confFile}`)
190+
);
239191
}
240192
}, {
241193
title: 'Restarting Nginx',
@@ -244,55 +196,38 @@ class NginxExtension extends cli.Extension {
244196
}], false);
245197
}
246198

247-
setupRenew(argv, ctx) {
248-
return this._cron((cron) => {
249-
// Ensure any crontab with the same instance name is removed
250-
cron.remove({comment: ctx.instance.name});
251-
let cmd = `cd ${ctx.instance.dir} && ${process.argv.slice(0, 2).join(' ')} ssl-renew`;
252-
cron.create(cmd, '@monthly', ctx.instance.name);
253-
});
254-
}
255-
256-
_addProxyBlock(location, port) {
257-
location._add('proxy_set_header', 'X-Forwarded-For $proxy_add_x_forwarded_for');
258-
location._add('proxy_set_header', 'X-Forwarded-Proto $scheme');
259-
location._add('proxy_set_header', 'X-Real-IP $remote_addr');
260-
location._add('proxy_set_header', 'Host $http_host');
261-
location._add('proxy_pass', `http://127.0.0.1:${port}`);
262-
}
263-
264199
uninstall(instance) {
265-
if (!instance.cliConfig.get('extension.nginx', false)) {
266-
return;
267-
}
268-
269200
let parsedUrl = url.parse(instance.config.get('url'));
270201
let confFile = `${parsedUrl.hostname}.conf`;
202+
let sslConfFile = `${parsedUrl.hostname}-ssl.conf`;
271203

272-
let promises = [
273-
this._cron(cron => cron.remove({comment: instance.name}))
274-
];
204+
let promises = [];
275205

276206
if (fs.existsSync(`/etc/nginx/sites-available/${confFile}`)) {
207+
// Nginx config exists, remove it
277208
promises.push(
278-
this.ui.sudo(`rm /etc/nginx/sites-available/${confFile}`).then(() => {
279-
return this.ui.sudo(`rm /etc/nginx/sites-enabled/${confFile}`);
280-
}).catch(
209+
Promise.all([
210+
this.ui.sudo(`rm -f /etc/nginx/sites-available/${confFile}`),
211+
this.ui.sudo(`rm -f /etc/nginx/sites-enabled/${confFile}`)
212+
]).catch(
281213
() => Promise.reject(new cli.errors.SystemError('Nginx config file link could not be removed, you will need to do this manually.'))
282214
)
283215
);
284216
}
285217

286-
return Promise.all(promises).then(() => this.restartNginx());
287-
}
288-
289-
_cron(fn) {
290-
const crontab = require('crontab');
218+
if (fs.existsSync(`/etc/nginx/sites-available/${sslConfFile}`)) {
219+
// SSL config exists, remove it
220+
promises.push(
221+
Promise.all([
222+
this.ui.sudo(`rm -f /etc/nginx/sites-available/${sslConfFile}`),
223+
this.ui.sudo(`rm -f /etc/nginx/sites-enabled/${sslConfFile}`)
224+
]).catch(
225+
() => Promise.reject(new cli.errors.SystemError('SSL config file link could not be removed, you will need to do this manually.'))
226+
)
227+
);
228+
}
291229

292-
return Promise.fromCallback(cb => crontab.load(cb)).then((cron) => {
293-
fn(cron);
294-
return Promise.fromCallback(cb => cron.save(cb));
295-
});
230+
return Promise.all(promises).then(() => this.restartNginx());
296231
}
297232

298233
restartNginx() {

0 commit comments

Comments
 (0)