Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
80 changes: 73 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,30 @@ Server module for [Hexo].

## Installation

``` bash
```bash
$ npm install hexo-server --save
```

## Usage

``` bash
```bash
$ 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 | `/path/to/cert.crt`
`-ck`, `--key` | Certificate key path | `/path/to/key.key`
`--ssl` | Enable SSL localhost. If `--cert` and `--key` are present, SSL will be enabled automatically. If `--cert` and `--key` are not present, but `--ssl` is present, Hexo will automatically generate a self-signed certificate. | `false`

## Options

``` yaml
```yaml
server:
port: 4000
log: false
Expand All @@ -51,6 +54,69 @@ server:
- **header**: Add `X-Powered-By: Hexo` header
- **serveStatic**: Extra options passed to [serve-static](https://github.com/expressjs/serve-static#options)

## Generate self-certificate

You can build your own OpenSSL from the official source: [https://openssl-library.org/source/](https://openssl-library.org/source/).

### For Windows Users
You can download precompiled OpenSSL binaries for Windows from trusted sources like:
- [https://slproweb.com/products/Win32OpenSSL.html](https://slproweb.com/products/Win32OpenSSL.html)

Make sure to install the version matching your system architecture (32-bit or 64-bit).

Once installed, you can generate a self-signed certificate using the command line:

### Default config

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

### Custom config

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

### `openssl.cnf` contents

```conf
[ 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.4 = 127.0.0.1
```

#### description

- `alt_names` is your dev/localhost domain. (set on your `hosts` file)

## License

MIT
Expand Down
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'));
208 changes: 208 additions & 0 deletions lib/mkcert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
'use strict';

const path = require('path');
const fs = require('fs');
const { X509Certificate, createPrivateKey } = require('crypto');
const { execSync } = require('child_process');
const Log = console;
const MKCERT_VERSION = 'v1.4.4';
const cacheDirectory = path.join(process.cwd(), 'node_modules/.cache/hexo-server-certificates');
const keyPath = path.join(cacheDirectory, 'localhost-key.pem');
const certPath = path.join(cacheDirectory, 'localhost.pem');

/**
* Returns the platform-specific mkcert binary name based on OS and architecture.
*
* @returns {string} The name of the mkcert binary file.
* @throws {Error} If the platform is not supported.
*/
function getBinaryName() {
const platform = process.platform;
const arch = process.arch === 'x64' ? 'amd64' : process.arch;

if (platform === 'win32') {
return `mkcert-${MKCERT_VERSION}-windows-${arch}.exe`;
}
if (platform === 'darwin') {
return `mkcert-${MKCERT_VERSION}-darwin-${arch}`;
}
if (platform === 'linux') {
return `mkcert-${MKCERT_VERSION}-linux-${arch}`;
}

throw new Error(`Unsupported platform: ${platform}`);
}

/**
* Downloads the mkcert binary for the current platform and architecture.
*
* If the binary already exists in the cache directory, it returns the cached path.
* Otherwise, it downloads the binary, saves it to disk, sets executable permissions,
* and returns the binary path.
*
* @async
* @returns {Promise<string|undefined>} The path to the downloaded or cached mkcert binary, or undefined if an error occurs.
*/
async function downloadBinary() {
try {
const binaryName = getBinaryName();
const binaryPath = path.join(cacheDirectory, binaryName);
const downloadUrl = `https://github.com/FiloSottile/mkcert/releases/download/${MKCERT_VERSION}/${binaryName}`;

// Fetch remote file size first
const headResponse = await fetch(downloadUrl, { method: 'HEAD' });

if (!headResponse.ok) {
throw new Error(`Failed to fetch file header. Status: ${headResponse.status}`);
}

const remoteFileSize = parseInt(headResponse.headers.get('content-length'), 10);

if (fs.existsSync(binaryPath)) {
const localStats = await fs.promises.stat(binaryPath);
// Fix file corruption
if (localStats.size === remoteFileSize) {
Log.info('Local mkcert binary is up-to-date, skipping download.');
return binaryPath;
}
Log.info('Local mkcert binary size mismatch, re-downloading...');
} else {
await fs.promises.mkdir(cacheDirectory, { recursive: true });
}

Log.info('Downloading mkcert package...');

const response = await fetch(downloadUrl);

if (!response.ok || !response.body) {
throw new Error(`Download failed with status ${response.status}`);
}

Log.info('Download response was successful, writing to disk');

const binaryWriteStream = fs.createWriteStream(binaryPath);

await response.body.pipeTo(
new WritableStream({
write(chunk) {
return new Promise((resolve, reject) => {
binaryWriteStream.write(chunk, error => {
if (error) {
reject(error);
return;
}
resolve();
});
});
},
close() {
return new Promise((resolve, reject) => {
binaryWriteStream.close(error => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
})
);

await fs.promises.chmod(binaryPath, 0o755);

return binaryPath;
} catch (err) {
Log.error('Error downloading mkcert:', err);
throw err; // Important to rethrow if you want callers to know the download failed
}
}

/**
* @typedef {Object} SelfSignedCertificate
* @property {string} key - Path to the generated private key file.
* @property {string} cert - Path to the generated certificate file.
* @property {string} rootCA - Path to the root Certificate Authority (CA) certificate.
*/

/**
* Creates a self-signed SSL certificate using mkcert.
*
* @async
* @param {string|string[]} [host] - Optional additional host to include in the certificate.
* @returns {Promise<SelfSignedCertificate|undefined>} The paths to key, cert, and rootCA, or undefined on error.
*/
async function createSelfSignedCertificate(host) {
try {
const binaryPath = await downloadBinary();
if (!binaryPath) throw new Error('missing mkcert binary');

await fs.promises.mkdir(cacheDirectory, { recursive: true });

// Ensure host is always an array
let hostList = [];
if (Array.isArray(host)) {
hostList = host;
} else if (typeof host === 'string' && host.length > 0) {
hostList = [host];
}

if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
const cert = new X509Certificate(fs.readFileSync(certPath));
const key = fs.readFileSync(keyPath);

// Check the certificate for each host
for (const h of hostList) {
if (!cert.checkHost(h)) {
Log.warn(`Certificate is not valid for host: ${h}`);
} else {
Log.info(`Certificate is valid for host: ${h}`);
}
}

if (cert.checkPrivateKey(createPrivateKey(key))) {
Log.info('Using already generated self signed certificate');
const caLocation = execSync(`"${binaryPath}" -CAROOT`).toString().trim();
Log.info(`CA location at ${caLocation}`);

return {
key: keyPath,
cert: certPath,
rootCA: `${caLocation}/rootCA.pem`
};
}
}

// Download mkcert binary
downloadBinary();

Log.info('Attempting to generate self signed certificate. This may prompt for your password');

const defaultHosts = ['localhost', '127.0.0.1', '::1'];
const allHosts = [...defaultHosts, ...hostList.filter(h => !defaultHosts.includes(h))];

// Install certificate for all hosts
execSync(`"${binaryPath}" -install -key-file "${keyPath}" -cert-file "${certPath}" ${allHosts.join(' ')}`, {
stdio: 'ignore'
});

const caLocation = execSync(`"${binaryPath}" -CAROOT`).toString().trim();

if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) {
throw new Error('Certificate files not found');
}

Log.info(`CA Root certificate created in ${caLocation}`);
Log.info(`Certificates created in ${cacheDirectory}`);

return {
key: keyPath,
cert: certPath,
rootCA: `${caLocation}/rootCA.pem`
};
} catch (err) {
Log.error('Failed to generate self-signed certificate. Falling back to http.', err);
}
}

module.exports = { createSelfSignedCertificate, getBinaryName, downloadBinary, cacheDirectory, keyPath, certPath };
Loading