Skip to content

Commit

Permalink
✨ Nginx Service
Browse files Browse the repository at this point in the history
closes TryGhost#64
- deps: nginx-conf@1.3.0
- deps: greenlock-cli@2.2.5
  • Loading branch information
acburdine committed Mar 2, 2017
1 parent b0ac8cd commit eecdc24
Show file tree
Hide file tree
Showing 7 changed files with 400 additions and 4 deletions.
6 changes: 6 additions & 0 deletions lib/commands/service.js
@@ -1,5 +1,11 @@
'use strict';
const Config = require('../utils/config');
const checkValidInstall = require('../utils/check-valid-install');

module.exports.execute = function execute(command, args) {
checkValidInstall('service');

this.service.setConfig(Config.load(this.environment));

return this.service.callCommand(command, args);
};
4 changes: 4 additions & 0 deletions lib/services/base.js
Expand Up @@ -18,6 +18,10 @@ class BaseService {
}

command(name, fn) {
if (typeof fn !== 'function') {
fn = this[fn];
}

this.serviceManager.registerCommand(name, fn, this.name);
}

Expand Down
5 changes: 3 additions & 2 deletions lib/services/index.js
Expand Up @@ -82,7 +82,7 @@ class ServiceManager {
let command = this.commands[name];
let service = this.services[command[0]];

return Promise.resolve(service[command[1]].apply(service, args));
return Promise.resolve(command[1].apply(service, args));
}

setConfig(config, force) {
Expand Down Expand Up @@ -169,7 +169,8 @@ ServiceManager.knownServices = [
// TODO: we only know about the nginx & built in process manager services
// for now, in the future make this load globally installed services
require('./process/systemd'),
localService
localService,
require('./nginx')
];

module.exports = ServiceManager;
15 changes: 15 additions & 0 deletions lib/services/nginx/files/ssl-params.conf
@@ -0,0 +1,15 @@
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

ssl_dhparam /etc/ssl/certs/dhparam.pem;
184 changes: 184 additions & 0 deletions lib/services/nginx/index.js
@@ -0,0 +1,184 @@
'use strict';
const fs = require('fs-extra');
const url = require('url');
const path = require('path');
const execa = require('execa');
const validator = require('validator');
const Promise = require('bluebird');
const NginxConfFile = require('nginx-conf').NginxConfFile;

const BaseService = require('../base');

const LIVE_URL = 'https://acme-v01.api.letsencrypt.org/directory';
const STAGING_URL = 'https://acme-staging.api.letsencrypt.org/directory';

class NginxService extends BaseService {
init() {
this.on('setup', 'setup');
this.command('nginx-conf', 'setupConf');
this.command('nginx-ssl', 'setupSSL');
// TODO implement
// this.command('ssl-renew', 'renewSSL');
}

get parsedUrl() {
return url.parse(this.config.get('url'));
}

setup(context) {
// This is the result from the `ghost doctor setup` command - it will be false
// if nginx does not exist on the system
if (!context.nginx) {
return;
}

if (this.parsedUrl.port) {
this.ui.log('Your url contains a port. Skipping automatic nginx setup.', 'yellow');
return;
}

if (this.parsedUrl.pathname !== '/') {
this.ui.log('The Nginx service does not support subdirectory configurations yet. Skipping automatic nginx setup.', 'yellow');
return;
}

if (fs.existsSync(`/etc/nginx/sites-available/${this.parsedUrl.hostname}`)) {
this.ui.log('Nginx configuration already found for this url. Skipping automatic nginx configuration.', 'yellow');
return;
}

let prompts = [{
type: 'confirm',
name: 'ssl',
message: 'Do you want to set up your blog with SSL (using letsencrypt)?',
default: true
}, {
type: 'input',
name: 'email',
message: 'Enter your email (used for ssl registration)',
when: ans => ans.ssl,
validate: email => validator.isEmail(email) || 'Invalid email'
}];

if (this.config.environment === 'development') {
prompts.splice(1, 0, {
type: 'confirm',
name: 'staging',
message: 'You are running in development mode. Would you like to use letsencrypt\'s' +
' staging servers instead of the production servers?',
default: true,
when: ans => ans.ssl
});
}

let answers;
let ghostExec = process.argv.slice(0,2).join(' ');

return this.ui.prompt(prompts).then((_answers) => {
answers = _answers;

return this.ui.noSpin(execa.shell(`sudo ${ghostExec} service nginx-conf${!answers.ssl ? ' no-ssl' : ''}`, {stdio: 'inherit'}));
}).then(() => {
if (answers.ssl) {
return this.ui.noSpin(execa.shell(`sudo ${ghostExec} service nginx-ssl ${answers.email}${answers.staging ? ' staging' : ''}`, {
stdio: 'inherit'
}));
}
});
}

setupConf(ssl) {
let isSSL = (!ssl || ssl !== 'no-ssl');
let confFile = `${this.parsedUrl.hostname}.conf`;
let confFilePath = `/etc/nginx/sites-available/${confFile}`;

fs.ensureFileSync(confFilePath);
fs.ensureSymlinkSync(confFilePath, `/etc/nginx/sites-enabled/${confFile}`);

return Promise.fromNode((cb) => NginxConfFile.create(confFilePath, cb)).then((conf) => {
conf.nginx._add('server');

let http = conf.nginx.server;

http._add('listen', '80');
http._add('listen', '[::]:80');
http._add('server_name', this.parsedUrl.hostname);

let rootPath = path.resolve(process.cwd(), 'root');
fs.ensureDirSync(rootPath);
http._add('root', rootPath);

http._add('location', '/');
this._addProxyBlock(http.location);

if (isSSL) {
http._add('location', '~ /.well-known');
http.location[1]._add('allow', 'all');
}
}).then(() => execa.shell('service nginx restart', {stdio: 'inherit'}));
}

setupSSL(email, staging) {
let rootPath = path.resolve(process.cwd(), 'root');

let command = `${process.execPath} ${path.resolve(__dirname, '../../../node_modules/.bin/letsencrypt')} certonly` +
` --agree-tos --email ${email} --webroot --webroot-path ${rootPath}` +
` --config-dir /etc/letsencrypt --domains ${this.parsedUrl.hostname} --server ${staging ? STAGING_URL : LIVE_URL}`;

return this.ui.run(execa.shell(command, {stdio: 'ignore'}), 'Getting SSL certificate').then(() => {
if (fs.existsSync('/etc/ssl/certs/dhparam.pem')) {
// Diffie-Hellman cert already exists, skip
return;
}

return this.ui.run(execa.shell('cd /etc/ssl/certs && openssl dhparam -out dhparam.pem 2048'), 'Generating encryption key (hold tight, this may take a while)');
}).then(() => {
// The SSL config for Ghost uses an `ssl-params` snippet conf taken from https://cipherli.st
fs.ensureDirSync('/etc/nginx/snippets');
fs.copySync(path.resolve(__dirname, 'files/ssl-params.conf'), '/etc/nginx/snippets/ssl-params.conf', {overwrite: false});

return Promise.fromNode((cb) => NginxConfFile.create(`/etc/nginx/sites-available/${this.parsedUrl.hostname}.conf`, cb));
}).then((conf) => {
let http = conf.nginx.server;
// remove proxy && well-known location from port 80 server block
http._remove('location');
// remove root path
http._remove('root');
// add 'location /' block with 301 redirect to ssl
http._add('return', '301 https://$server_name$request_uri');

// add ssl server block
conf.nginx._add('server');

let https = conf.nginx.server[1];
// add listen directives
https._add('listen', '443 ssl http2');
https._add('listen', '[::]:443 ssl http2');
// add ssl cert directives
https._add('ssl_certificate', `/etc/letsencrypt/live/${this.parsedUrl.hostname}/fullchain.pem`);
https._add('ssl_certificate_key', `/etc/letsencrypt/live/${this.parsedUrl.hostname}/privkey.pem`);
// add ssl-params snippet
https._add('include', 'snippets/ssl-params.conf');
// add root directive
https._add('root', rootPath);

https._add('location', '/');
this._addProxyBlock(https.location);
https._add('location', '~ /.well-known');
https.location[1]._add('allow', 'all');
}).then(() => execa.shell('service nginx restart', {stdio: 'inherit'}));
}

_addProxyBlock(location) {
location._add('proxy_set_header', 'X-Forwarded-For $proxy_add_x_forwarded_for');
location._add('proxy_set_header', 'X-Forwarded-Proto $scheme');
location._add('proxy_set_header', 'X-Real-IP $remote_addr');
location._add('proxy_set_header', 'Host $http_host');
location._add('proxy_pass', `http://127.0.0.1:${this.config.get('server.port')}`);
}
};

module.exports = {
name: 'nginx',
class: NginxService
};
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -46,10 +46,12 @@
"execa": "0.6.0",
"fkill": "4.1.0",
"fs-extra": "2.0.0",
"greenlock-cli": "2.2.5",
"inquirer": "3.0.4",
"knex-migrator": "2.0.7",
"listr": "0.11.0",
"lodash": "4.17.4",
"nginx-conf": "1.3.0",
"ora": "1.1.0",
"portfinder": "1.0.13",
"rxjs": "5.2.0",
Expand Down

0 comments on commit eecdc24

Please sign in to comment.