Skip to content

Commit 05e7651

Browse files
committed
Implement #8 and #10: certificates in AppData at firts run.
This prevent the script to generate a new certificate every time the module is updated. `certs.js` manages the certificates: creation and deletion, and provides them to index.js. An uninstall script is added, it deletes the folder at uninstall time. makes problems, generate the certificates at first run. If the index.js won't find the certificates, certs.js will generate them. Some edits in README accordingly to changes. Add also a link on README to CONTRIBUTING. Upgrade to v4.1.0 in package.json. Upgrade of dependencies.
1 parent ea32bac commit 05e7651

File tree

7 files changed

+1269
-2354
lines changed

7 files changed

+1269
-2354
lines changed

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@ It works with MacOS, Linux and Windows, on Chrome and Firefox, and requires you
1212
[![Known Vulnerabilities](https://snyk.io/test/npm/https-localhost/badge.svg)](https://snyk.io/test/npm/https-localhost)
1313
[![GitHub issues](https://img.shields.io/github/issues/daquinoaldo/https-localhost.svg)](https://github.com/daquinoaldo/https-localhost/issues)
1414

15+
1516
## Install and use standalone
1617
```
17-
npm i -g https-localhost
18+
npm i -g --only=prod https-localhost
1819
```
1920
```
2021
serve ~/myproj
2122
```
2223
- `sudo` may be necessary.
2324
- If a static path is not provided the current directory content will be served.
24-
- You can change the port setting the PORT environmental variable: `PORT=4433 serve ~/myproj`.
25+
- You can change the port setting the PORT environmental variable: `PORT=4433 serve ~/myproj`. Specifying port number will also prevent http to https redirect.
2526

2627

2728
## Use as module
@@ -40,6 +41,7 @@ app.listen(port)
4041
- To redirect the http traffic to https use `app.redirect()`.
4142
- You can serve static files with `app.serve(path)`.
4243

44+
4345
## Production
4446
This tool has a production version that activates **HTTP/2**, **compression** and **minify**.
4547
```
@@ -49,6 +51,7 @@ I decide to not activate it by default since it is usually an unwanted behaviour
4951

5052
**IMPORTANT**: the fact that there is a production enviornment doesn't mean that this tool is suitable for production. It's intended to be used only for local testing.
5153

54+
5255
## Why and how it works
5356
Serving static content on localhost in a trusted SSL connection is not so simple.
5457
It requires to manually generate and trust certificates, with complicate commands and many manual steps.
@@ -79,6 +82,10 @@ Checkout the updated list [here](https://github.com/FiloSottile/mkcert/blob/mast
7982
https-localhost requires Node.js 7.6 or higher.
8083
<sub>If you need compatibility with previously Node.js versions let me know, I'll try to rearrange the code.</sub>
8184

85+
### root required
86+
- **At first run** this tool generate a trusted certificate. The sudo password may be required. If you cannot provide the sudo password generate a `localhost.key` and `localhost.crt` and specify its path with `CERT_PATH=/diractory/containing/certificates/ serve ~/myproj`.
87+
- **At each run** the password may be required to run the server on port 443 and 80. To avoid the script ask for password specify a different port number: `PORT=4433 serve ~/myproj`.
88+
8289
### RangeError
8390
```
8491
RangeError: Invalid typed array length: -4095
@@ -89,6 +96,12 @@ It should be present only with `NODE_ENV=production`, hence the easiest fix is t
8996

9097
I've tried to reproduce this error without any success (checkout the [Travis build logs](https://travis-ci.org/daquinoaldo/https-localhost)). If you can help please open an issue and describe as better as you can how to reproduce it, I'll be happy to help you.
9198

99+
100+
## Contributing
101+
Each contribute is welcome!
102+
Please, checkout the [contributing guidelines](.github/CONTRIBUTING.md).
103+
104+
92105
## License
93106
Is released under [AGPL-3.0 - GNU Affero General Public License v3.0](LICENSE).
94107

cert/generate.js

Lines changed: 0 additions & 85 deletions
This file was deleted.

certs.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env node
2+
3+
const path = require("path")
4+
const fs = require("fs")
5+
const exec = require("child_process").exec
6+
const https = require("https")
7+
const getAppDataPath = require("appdata-path")
8+
9+
const MKCERT_VERSION = "v1.3.0"
10+
const CERT_PATH = getAppDataPath("https-localhost")
11+
12+
// get the executable name
13+
function getExe() {
14+
/* istanbul ignore next: tested on all platform on travis */
15+
switch (process.platform) {
16+
case "darwin":
17+
return "mkcert-" + MKCERT_VERSION + "-darwin-amd64"
18+
case "linux":
19+
return "mkcert-" + MKCERT_VERSION + "-linux-amd64"
20+
case "win32":
21+
return "mkcert-" + MKCERT_VERSION + "-windows-amd64.exe"
22+
default:
23+
console.warn("Cannot generate the localhost certificate on your " +
24+
"platform. Please, consider contacting the developer if you can help.")
25+
process.exit(0)
26+
}
27+
}
28+
29+
// download a binary file
30+
function download(url, path) {
31+
console.log("Downloading the mkcert executable...")
32+
const file = fs.createWriteStream(path)
33+
return new Promise(resolve => {
34+
function get(url, file) {
35+
https.get(url, (response) => {
36+
if (response.statusCode === 302) get(response.headers.location, file)
37+
else response.pipe(file).on("finish", resolve)
38+
})
39+
}
40+
get(url, file)
41+
})
42+
}
43+
44+
// execute the binary executable to generate the certificates
45+
function mkcert(appDataPath, exe) {
46+
const logPath = path.join(appDataPath, "mkcert.log")
47+
const errPath = path.join(appDataPath, "mkcert.err")
48+
// escape spaces in appDataPath (Mac OS)
49+
appDataPath = appDataPath.replace(" ", "\\ ")
50+
const exePath = path.join(appDataPath, exe)
51+
const crtPath = path.join(appDataPath, "localhost.crt")
52+
const keyPath = path.join(appDataPath, "localhost.key")
53+
const cmd = exePath + " -install -cert-file " + crtPath +
54+
" -key-file " + keyPath + " localhost"
55+
return new Promise((resolve, reject) => {
56+
console.log("Running mkcert to generate certificates...")
57+
exec(cmd, (err, stdout, stderr) => {
58+
// log
59+
const errFun = err => {
60+
/* istanbul ignore if: cannot be tested */
61+
if (err) console.error(err)
62+
}
63+
fs.writeFile(logPath, stdout, errFun)
64+
fs.writeFile(errPath, stderr, errFun)
65+
/* istanbul ignore if: cannot be tested */
66+
if (err) reject(err)
67+
resolve()
68+
})
69+
})
70+
}
71+
72+
async function generate(appDataPath = CERT_PATH) {
73+
console.info("Generating certificates...")
74+
// mkdir if not exists
75+
/* istanbul ignore else: not relevant */
76+
if (!fs.existsSync(appDataPath))
77+
fs.mkdirSync(appDataPath)
78+
// build the executable url and path
79+
const url = "https://github.com/FiloSottile/mkcert/releases/download/" +
80+
MKCERT_VERSION + "/"
81+
const exe = getExe()
82+
const exePath = path.join(appDataPath, exe)
83+
// download the executable
84+
await download(url + exe, exePath)
85+
// make binary executable
86+
fs.chmodSync(exePath, "0755")
87+
// execute the binary
88+
await mkcert(appDataPath, exe)
89+
console.log("Certificates generated, installed and trusted. Ready to go!")
90+
}
91+
92+
async function getCerts() {
93+
const certPath = process.env.CERT_PATH || CERT_PATH
94+
try {
95+
return {
96+
key: fs.readFileSync(path.join(certPath, "localhost.key")),
97+
cert: fs.readFileSync(path.join(certPath, "localhost.crt"))
98+
}
99+
} catch (e) {
100+
if (certPath !== CERT_PATH) {
101+
console.error("Cannot find localhost.key and localhost.crt in the" +
102+
" specified path: " + certPath)
103+
process.exit(1)
104+
} else {
105+
// Missing certificates (first run)
106+
// generate the certificate
107+
await generate(CERT_PATH)
108+
// recursive call
109+
return getCerts()
110+
}
111+
}
112+
}
113+
114+
// delete a folder and the file inside it
115+
function remove(appDataPath = CERT_PATH) {
116+
if (fs.existsSync(appDataPath)) {
117+
fs.readdirSync(appDataPath)
118+
.forEach(file => fs.unlinkSync(path.join(appDataPath, file)))
119+
fs.rmdirSync(appDataPath)
120+
}
121+
}
122+
123+
// run as script
124+
/* istanbul ignore if: cannot be tested */
125+
if (require.main === module)
126+
// if run with -u or --uninstall
127+
if (process.argv.length === 3 &&
128+
(process.argv[2] === "-u" || process.argv[2] === "--uninstall")) {
129+
remove()
130+
console.info("Certificates removed.")
131+
} else try { // install
132+
generate()
133+
} catch (err) { console.error("\nExec error: " + err) }
134+
135+
// export as module
136+
module.exports = {
137+
getCerts,
138+
generate,
139+
remove
140+
}

index.js

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,27 @@ const http = require("http")
77
const https = process.env.NODE_ENV === "production"
88
? require("spdy") : require("https")
99
const express = require("express")
10-
const compression = require("compression")
11-
const minify = require("express-minify")
10+
const getCerts = require(path.resolve(__dirname, "certs.js")).getCerts
1211

1312
/* CONFIGURE THE SERVER */
1413

1514
// SSL certificate
16-
let certOptions
17-
try {
18-
certOptions = {
19-
key: fs.readFileSync(path.resolve(__dirname, "cert/localhost.key")),
20-
cert: fs.readFileSync(path.resolve(__dirname, "cert/localhost.crt"))
21-
}
22-
} catch (e) /* istanbul ignore next: TODO, not so important */ {
23-
console.error("Cannot find the certificates. Try to reinstall the module.")
24-
process.exit(1)
25-
}
26-
2715
const createServer = () => {
2816
// create a server with express
2917
const app = express()
3018

3119
// override the default express listen method to use our server
32-
app.listen = function(port = process.env.PORT ||
20+
app.listen = async function(port = process.env.PORT ||
3321
/* istanbul ignore next: cannot be tested on Travis */ 443) {
34-
app.server = https.createServer(certOptions, app).listen(port)
22+
app.server = https.createServer(await getCerts(), app).listen(port)
3523
console.info("Server running on port " + port + ".")
3624
return app.server
3725
}
3826

3927
// use gzip compression minify
4028
if (process.env.NODE_ENV === "production") {
29+
const compression = require("compression")
30+
const minify = require("express-minify")
4131
app.use(compression({ threshold: 1 }))
4232
app.use(minify())
4333
app.set("json spaces", 0)
@@ -90,8 +80,8 @@ if (require.main === module) {
9080
// retrieve the static path from the process argv or use the cwd
9181
// 1st is node, 2nd is serve or index.js, 3rd (if exists) is the path
9282
app.serve(process.argv.length === 3 ? process.argv[2] : process.cwd())
93-
// redirect http to https
94-
app.redirect()
83+
// redirect http to https (only if https port is the default one)
84+
if (!process.env.PORT) app.redirect()
9585
}
9686

9787
// export as module

0 commit comments

Comments
 (0)