Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: localhost SSL support #341

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ $ hexo server
Option | Description | Default
--- | --- | ---
`-i`, `--ip` | Override the default server IP. | `::` when IPv6 is available, else `0.0.0.0` (note: in most systems, `::` also binds to `0.0.0.0`)
`-p`, `--port` | Override the default port. | 4000
`-s`, `--static` | Only serve static files. | false
`-l`, `--log [format]` | Enable logger. Override log format. | false
`-o`, `--open` | Immediately open the server url in your default web browser. | false
`-p`, `--port` | Override the default port. | `4000`
`-s`, `--static` | Only serve static files. | `false`
`-l`, `--log [format]` | Enable logger. Override log format. | `false`
`-o`, `--open` | Immediately open the server url in your default web browser. | `false`
`-c`, `--cert` | Certificate path | `<lib>/certificates/localhost.crt`
`-ck`, `--key` | Certificate key path | `<lib>/certificates/localhost.key`
`h`, `--ssl` | Enable SSL localhost. If `--cert` and `--key` is present, ssl will enabled automatically. If `--cert` and `--key` is not present, but `--ssl` is preset, default certificate will be applied. | `false`

## Options

Expand Down
23 changes: 23 additions & 0 deletions certificates/localhost.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDwTCCAqkCFEXMrQ4TmdoCjzJOwz9otI85kQ6tMA0GCSqGSIb3DQEBCwUAMIGc
MQswCQYDVQQGEwJJRDESMBAGA1UECAwJRWFzdCBKYXZhMREwDwYDVQQHDAhTdXJh
YmF5YTEMMAoGA1UECgwDV01JMRIwEAYDVQQLDAlEZXZlbG9wZXIxHTAbBgNVBAMM
FGRldi53ZWJtYW5hamVtZW4uY29tMSUwIwYJKoZIhvcNAQkBFhZkaW1hc2xhbmph
a2FAZ21haWwuY29tMB4XDTI0MDcyNjA5MDExN1oXDTI1MDcyNjA5MDExN1owgZwx
CzAJBgNVBAYTAklEMRIwEAYDVQQIDAlFYXN0IEphdmExETAPBgNVBAcMCFN1cmFi
YXlhMQwwCgYDVQQKDANXTUkxEjAQBgNVBAsMCURldmVsb3BlcjEdMBsGA1UEAwwU
ZGV2LndlYm1hbmFqZW1lbi5jb20xJTAjBgkqhkiG9w0BCQEWFmRpbWFzbGFuamFr
YUBnbWFpbC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqL+4K
xiNBbMn1ssJSsHhB4TfhgcQ4tR9ov6SoVRvnv+50qHvY9mS/ksRCuWIKAC2e76nv
iV0zUY7c2ycBsTo7BTCm8KPL5TfLkvykg6m/BFPkR5wBDMA8jDpiZ1L+rE8ttXF7
LxyYToSEjezOV92ivT+2MA3LwUoUkTWmLdinuQSfQN2gDzNCGFjqF1GDEwjiGiz9
xVV7BmhnPMfhlt1jkE53nzSaYzjAUTYhSvw8hAWPNa+o90E1jJBC1T6c0AQKJ+I/
rleZPAosMQdZvbokccwQXAF0uvxQ/gYu5TpB3gKp6ntjXmsqtqE837Egok4NF6Lx
b2sgHi9+7UtLbqS/AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEpoBcHPiC/IZP/2
jrf+qOWY51graP0lpi5NJBqcokbhmD1u3Ah7/oNgUqn2y4VCjQaM2lhNttjnKoWr
UYdVbzkZAFTtNDLbgNQgMRhN9J9J4+UqtrQ6sMqAENYO0ZWsx+Nth7hE1l4nsa0E
hCPtnnAgACCtS1KHiuDddkQ/gagt52Uf5QkdIGpKO6VEmv7SHkomiub9WUgT4kRK
HzBGup2zNegZ4HznCLhQtr2EqDaeBekYNRSfgF6G756QRFbnGnC1r362yi2pycFa
28I3tqDBN0Yr116urzYIFAXX1b/fkIEo8Om6jl7S+QRkDQBh/dvTZNADplJsvzvx
XRPbQp0=
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions certificates/localhost.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqL+4KxiNBbMn1
ssJSsHhB4TfhgcQ4tR9ov6SoVRvnv+50qHvY9mS/ksRCuWIKAC2e76nviV0zUY7c
2ycBsTo7BTCm8KPL5TfLkvykg6m/BFPkR5wBDMA8jDpiZ1L+rE8ttXF7LxyYToSE
jezOV92ivT+2MA3LwUoUkTWmLdinuQSfQN2gDzNCGFjqF1GDEwjiGiz9xVV7Bmhn
PMfhlt1jkE53nzSaYzjAUTYhSvw8hAWPNa+o90E1jJBC1T6c0AQKJ+I/rleZPAos
MQdZvbokccwQXAF0uvxQ/gYu5TpB3gKp6ntjXmsqtqE837Egok4NF6Lxb2sgHi9+
7UtLbqS/AgMBAAECggEAGHaYTJMrqTFmnHtMJI+6UZn6qt844f/jGm8Fz66gOsFj
mQBJASh11fXWYVL9jTt2U1TXBBgmPgS4uPWFl2Au4yH9WtnXaa0yxoAD8e/9G/iW
yIcknSAEmA9+Kvv4OaRyIztkKxVcEmai9Nzjz8tgkA6NFHa23PWVXPx8jj0j2gPp
oPCEE2QL2fy0Mi6R6LIn5d85zsSn8TrZNdmvkMUxPcaSYyg2oxZ6J8/9Mn4oE9e5
VmIt02TnN8q14KgixkcUQeI9JtgHwIZBP+jZIb1zx4C/8PXVs7CkZYifY518h/GM
4/w6AKJt3BZObLUwHkvucSTSVF2p2NdJZNN6SwX9QQKBgQDmgw8l1JYe2VtGfN+F
OdE8PuM8u3TO+r9POMK28gAC62VRoxPjfeNsVXTX2piliAnRSg+6nakn1vf8l9MP
DZaYj8jDMXzGU/vzvnzjfwyvjENmLCc4vkbu57gIOrD601DL/CqBYeBPwQGYxvSC
GE3UUXIOSOQ+VG58YDXrNfBgwQKBgQC9AU0DvL2tBZOHxTrWzyU4+BP8oych6q+u
n7QPNiQ+UvGMno8LngLXnoCnMz5T4PtAl0iZb51S+YTyXLQ8noqQEeJ4OivKS5hw
X2FBf+J/u4jrMQyIpcI34XCTtSl4EOQ7UxrxygtZIFugsX8PXFphKjo2wDaMBBV9
cl0f6SLlfwKBgQCHHcVwSFciiAevnpyqjARwivBJ9ht3A5XGCyBfeiS1kWOXYb7T
t0PqiYDu0cxuIvqWOhJUMfwoRSKhZiEqDq36iTWF7OkVm77w1fSAqUU3VUFgj3sC
EM6lVSATesuoitsuZoZHxqZkOV8FPYGvDC36yS7Q3rsjKfyFXSPd1oUrQQKBgE/C
4cZm+0CuLsFIOXl3d4TgJEckbxpAGR2/ZdRZi9gFVsx6CXHkn9xwlmh5Fp99PWrX
rRqbYLAofrNs7d77JQyBj6ofGmXHmzApADkNB//Rm4ltbJWqJhlA+SpMdJCnyDlE
7AUHt9xH7IMXBMDtv3JryJ9cZGiYPJ1xCt2xnDlvAoGAQobmLEpUALINBBAtxRTz
OFl6J611TcWzrnhnr8tXmsBifirXg6usCn1HTLMP+TIfuQhpVLt7r7SbS5Ok/az4
phsIfpfi1momli4uBwqr6ZD6z2bNeoxOvBa1XFCAzMwZ+y5Ylb0ReHkgNHibLF8A
69lB9UMeoSpe0O2SoqfQiPg=
-----END PRIVATE KEY-----
30 changes: 30 additions & 0 deletions certificates/openssl.cnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext

[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = ID
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = East Java
localityName = Locality Name (eg, city)
localityName_default = Surabaya
organizationName = Organization Name (eg, company)
organizationName_default = WMI
organizationalUnitName = Organizational Unit Name (eg, section)
organizationalUnitName_default = Developer
commonName = Common Name (e.g. server FQDN or YOUR name)
commonName_default = dev.webmanajemen.com
commonName_max = 64
emailAddress = Email Address
emailAddress_default = [email protected]

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = dev.webmanajemen.com
DNS.2 = localhost
DNS.3 = 192.168.1.75
DNS.3 = 127.0.0.1
11 changes: 11 additions & 0 deletions certificates/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## How to generate self-certificate

```bash
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout localhost.key -out localhost.crt
```

## With custom config

```bash
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout localhost.key -out localhost.crt -config openssl.cnf
```
30 changes: 0 additions & 30 deletions index.js

This file was deleted.

33 changes: 33 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* global hexo */

'use strict';

hexo.config.server = Object.assign({
port: 4000,
log: false,
// `undefined` uses Node's default (try `::` with fallback to `0.0.0.0`)
ip: undefined,
compress: false,
header: true
}, hexo.config.server);

hexo.extend.console.register('server', 'Start the server.', {
desc: 'Start the server and watch for file changes.',
options: [
{name: '-i, --ip', desc: 'Override the default server IP. Bind to all IP address by default.'},
{name: '-p, --port', desc: 'Override the default port.'},
{name: '-s, --static', desc: 'Only serve static files.'},
{name: '-l, --log [format]', desc: 'Enable logger. Override log format.'},
{name: '-o, --open', desc: 'Immediately open the server url in your default web browser.'},
{name: '-c, --cert [path]', desc: 'SSL certificate path.'},
{name: '-ck, --key [path]', desc: 'SSL private certificate path.'},
{name: '-h, --ssl', desc: 'Enable SSL localhost. If --cert and --key is present, ssl will enabled automatically. If --cert and --key is not present, but --ssl is preset, default certificate will be applied.'}
]
}, require('./server'));

hexo.extend.filter.register('server_middleware', require('./middlewares/header'));
hexo.extend.filter.register('server_middleware', require('./middlewares/gzip'));
hexo.extend.filter.register('server_middleware', require('./middlewares/logger'));
hexo.extend.filter.register('server_middleware', require('./middlewares/route'));
hexo.extend.filter.register('server_middleware', require('./middlewares/static'));
hexo.extend.filter.register('server_middleware', require('./middlewares/redirect'));
80 changes: 80 additions & 0 deletions lib/server-http.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use strict';

const connect = require('connect');
const http = require('http');
const { underline } = require('picocolors');
const Promise = require('bluebird');
const open = require('open');
const net = require('net');

module.exports = function(args) {
const app = connect();
const { config } = this;
const ip = args.i || args.ip || config.server.ip || undefined;
const port = parseInt(args.p || args.port || config.server.port || process.env.port, 10) || 4000;
const { root } = config;

return checkPort(ip, port).then(() => this.extend.filter.exec('server_middleware', app, {context: this})).then(() => {
if (args.s || args.static) {
return this.load();
}

return this.watch();
}).then(() => startServer(http.createServer(app), port, ip)).then(server => {
const addr = server.address();
const addrString = formatAddress(ip || addr.address, addr.port, root);

this.log.info('Hexo is running at %s . Press Ctrl+C to stop.', underline(addrString));
this.emit('server');

if (args.o || args.open) {
open(addrString);
}

return server;
}).catch(err => {
switch (err.code) {
case 'EADDRINUSE':
this.log.fatal(`Port ${port} has been used. Try other port instead.`);
break;

case 'EACCES':
this.log.fatal(`Permission denied. You can't use port ${port}.`);
break;
}

this.unwatch();
throw err;
});
};

function startServer(server, port, ip) {
return new Promise((resolve, reject) => {
server.listen(port, ip, resolve);
server.on('error', reject);
}).then(() => server);
}

function checkPort(ip, port) {
if (port > 65535 || port < 1) {
return Promise.reject(new RangeError(`Port number ${port} is invalid. Try a number between 1 and 65535.`));
}

const server = net.createServer();

return new Promise((resolve, reject) => {
server.once('error', reject);
server.once('listening', resolve);
server.listen(port, ip);
}).then(() => { server.close(); });
}

function formatAddress(ip, port, root) {
let hostname = ip;
if (ip === '0.0.0.0' || ip === '::') {
hostname = 'localhost';
}

const path = root.startsWith('/') ? root : `/${root}`;
return new URL(`http://${hostname}:${port}${path}`).toString();
}
88 changes: 88 additions & 0 deletions lib/server-ssl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use strict';

const connect = require('connect');
const https = require('https');
const fs = require('fs');
const { underline } = require('picocolors');
const Promise = require('bluebird');
const open = require('open');
const net = require('net');

module.exports = function(args) {
const app = connect();
const { config } = this;
const ip = args.i || args.ip || config.server.ip || undefined;
const port = parseInt(args.p || args.port || config.server.port || process.env.port, 10) || 4000;
const { root } = config;

// SSL certificates path
const sslOptions = {
key: fs.readFileSync(args.key),
cert: fs.readFileSync(args.cert)
};

return checkPort(ip, port).then(() => this.extend.filter.exec('server_middleware', app, { context: this })).then(() => {
if (args.s || args.static) {
return this.load();
}

return this.watch();
}).then(() => startServer(https.createServer(sslOptions, app), port, ip)).then(server => { // Create HTTPS server
const addr = server.address();
const addrString = formatAddress(ip || addr.address, addr.port, root, true); // Use https

this.log.info('Hexo is running at %s . Press Ctrl+C to stop.', underline(addrString));
this.emit('server');

if (args.o || args.open) {
open(addrString);
}

return server;
}).catch(err => {
switch (err.code) {
case 'EADDRINUSE':
this.log.fatal(`Port ${port} has been used. Try another port instead.`);
break;

case 'EACCES':
this.log.fatal(`Permission denied. You can't use port ${port}.`);
break;
}

this.unwatch();
throw err;
});
};

function startServer(server, port, ip) {
return new Promise((resolve, reject) => {
server.listen(port, ip, resolve);
server.on('error', reject);
}).then(() => server);
}

function checkPort(ip, port) {
if (port > 65535 || port < 1) {
return Promise.reject(new RangeError(`Port number ${port} is invalid. Try a number between 1 and 65535.`));
}

const server = net.createServer();

return new Promise((resolve, reject) => {
server.once('error', reject);
server.once('listening', resolve);
server.listen(port, ip);
}).then(() => { server.close(); });
}

function formatAddress(ip, port, root, useHttps = false) {
let hostname = ip;
if (ip === '0.0.0.0' || ip === '::') {
hostname = 'localhost';
}

const protocol = useHttps ? 'https' : 'http'; // Change protocol based on HTTPS or HTTP
const path = root.startsWith('/') ? root : `/${root}`;
return new URL(`${protocol}://${hostname}:${port}${path}`).toString();
}
Loading