From 8d4a23ccd260e7b15d1a3e1d6971641df78360f4 Mon Sep 17 00:00:00 2001 From: Austin Burdine Date: Sun, 19 Feb 2017 17:33:14 -0600 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Nginx=20Service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs #64 - add nginx service - add ssl provisioning via greenlock-cli --- lib/services/index.js | 3 +- lib/services/nginx/files/site.conf.template | 45 +++++++ .../nginx/files/ssl-cert.conf.template | 2 + lib/services/nginx/files/ssl-params.conf | 21 +++ lib/services/nginx/index.js | 125 ++++++++++++++++++ 5 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 lib/services/nginx/files/site.conf.template create mode 100644 lib/services/nginx/files/ssl-cert.conf.template create mode 100644 lib/services/nginx/files/ssl-params.conf create mode 100644 lib/services/nginx/index.js diff --git a/lib/services/index.js b/lib/services/index.js index e715bf23d..8d06390e7 100644 --- a/lib/services/index.js +++ b/lib/services/index.js @@ -131,7 +131,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; diff --git a/lib/services/nginx/files/site.conf.template b/lib/services/nginx/files/site.conf.template new file mode 100644 index 000000000..2c0aaf490 --- /dev/null +++ b/lib/services/nginx/files/site.conf.template @@ -0,0 +1,45 @@ +server { + listen 80; + listen [::]:80; + + server_name <%= url %>; + + <% if (ssl) { %> + location / { + return 301 https://$server_name$request_uri; + } + + location ~ /.well-known { + allow all; + } + <% } else { %> + root <%= root %>; + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_pass http://127.0.0.1:<%= port %>; + } + <% } %> +} + +<% if (ssl) { %> +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + # SSL Configuration snippets + include snippets/ssl-<%= url %>.conf; + include snippets/ssl-params.conf; + + root <%= root %>; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_pass http://127.0.0.1:<%= port %>; + } +} +<% } %> diff --git a/lib/services/nginx/files/ssl-cert.conf.template b/lib/services/nginx/files/ssl-cert.conf.template new file mode 100644 index 000000000..305b208d2 --- /dev/null +++ b/lib/services/nginx/files/ssl-cert.conf.template @@ -0,0 +1,2 @@ +ssl_certificate /etc/letsencrypt/live/<%= url %>/fullchain.pem; +ssl_certificate_key /etc/letsencrypt/live/<%= url %>/privkey.pem; diff --git a/lib/services/nginx/files/ssl-params.conf b/lib/services/nginx/files/ssl-params.conf new file mode 100644 index 000000000..9d01a5431 --- /dev/null +++ b/lib/services/nginx/files/ssl-params.conf @@ -0,0 +1,21 @@ +# from https://cipherli.st/ +# and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html + +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; +ssl_session_cache shared:SSL:10m; +ssl_session_tickets off; +ssl_stapling on; +ssl_stapling_verify on; +resolver 8.8.8.8 8.8.4.4 valid=300s; +resolver_timeout 5s; +# Disable preloading HSTS for now. You can use the commented out header line that includes +# the "preload" directive if you understand the implications. +#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; +add_header Strict-Transport-Security "max-age=63072000; includeSubdomains"; +add_header X-Frame-Options DENY; +add_header X-Content-Type-Options nosniff; + +ssl_dhparam /etc/ssl/certs/dhparam.pem; diff --git a/lib/services/nginx/index.js b/lib/services/nginx/index.js new file mode 100644 index 000000000..2a17e7103 --- /dev/null +++ b/lib/services/nginx/index.js @@ -0,0 +1,125 @@ +'use strict'; +const fs = require('fs-extra'); +const url = require('url'); +const path = require('path'); +const execa = require('execa'); +const template = require('lodash/template'); +const validator = require('validator'); + +const BaseService = require('../base'); + +class NginxService extends BaseService { + init() { + this.on('setup', 'setup'); + } + + get parsedUrl() { + return url.parse(this.config.get('url')); + } + + setup(context) { + 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; + } + + 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; + + return this.ui.prompt(prompts).then((_answers) => { + answers = _answers; + + fs.ensureDirSync(path.join(process.cwd(), 'root')); + + let conf = template(fs.readFileSync(path.join(__dirname, 'files', 'site.conf.template'), 'utf8')); + let confFile = `${this.parsedUrl.hostname}.conf`; + + fs.writeFileSync(path.join(process.cwd(), confFile), conf({ + ssl: answers.ssl, + root: path.join(process.cwd(), 'root'), + url: this.parsedUrl.hostname, + port: this.config.get('server.port') + })); + + return this.ui.noSpin(execa.shell(`sudo mv ${confFile} /etc/nginx/sites-available && ` + + `sudo ln -s /etc/nginx/sites-available/${confFile} /etc/nginx/sites-enabled && ` + + 'sudo service nginx restart', {stdio: 'inherit'})); + }).then(() => { + if (!answers.ssl) { + return; + } + + return this._ssl(answers); + }); + } + + _ssl(options) { + let letsencrypt = path.resolve(__dirname, '../../../node_modules/.bin/letsencrypt') + let command = `sudo ${letsencrypt} certonly --agree-tos --email ${options.email} --webroot ` + + `--webroot-path ${path.join(process.cwd(), 'root')} --config-dir /etc/letsencrypt ` + + `--domains ${this.parsedUrl.hostname}`; + + if (options.staging) { + // Use LetsEncrypt's staging server + command += ' --server https://acme-staging.api.letsencrypt.org/directory'; + } + + return this.ui.noSpin(execa.shell(command, {stdio: 'inherit'})).then(() => { + if (fs.existsSync('/etc/nginx/snippets/ssl-params.conf')) { + return; + } + + return this.ui.noSpin(execa.shell( + `sudo mv ${path.join(__dirname, 'files', 'ssl-params.conf')} /etc/nginx/snippets`, + {stdio: 'inherit'} + )); + }).then(() => { + let sslConf = template(fs.readFileSync(path.join(__dirname, 'files', 'ssl-cert.conf.template'), 'utf8')); + let sslConfFile = `ssl-${this.parsedUrl.hostname}.conf`; + + fs.writeFileSync(path.join(process.cwd(), sslConfFile), sslConf({ + url: this.parsedUrl.hostname + })); + + return this.ui.noSpin(execa.shell(`sudo mv ${sslConfFile} /etc/nginx/snippets && ` + + 'sudo service nginx restart')); + }); + } +} + +module.exports = { + name: 'nginx', + class: NginxService +};