Passed
Push — main ( 7025c8...8c0a07 )
by Bjarn
03:07 queued 01:44
created

SecureController.createSslCertificate   A

Complexity

Conditions 1

Size

Total Lines 17
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 14
dl 0
loc 17
rs 9.7
c 0
b 0
f 0
1
import execa from 'execa'
2
import {existsSync, readFileSync, unlinkSync, writeFileSync} from 'fs'
3
import {Config} from '../models/config'
4
import Nginx from '../services/nginx'
5
import nginx from '../templates/nginx'
6
import opensslConfig from '../templates/openssl'
7
import {ensureDirectoryExists} from '../utils/filesystem'
8
import {getConfig, jaleSitesPath, jaleSslPath} from '../utils/jale'
9
10
class SecureController {
11
12
    config: Config
13
    project: string
14
    hostname: string
15
16
    keyPath: string
17
    csrPath: string
18
    crtPath: string
19
    configPath: string
20
21
    constructor() {
22
        this.config = getConfig()
23
        this.project = process.cwd().substring(process.cwd().lastIndexOf('/') + 1)
24
        this.hostname = `${this.project}.${this.config.domain}`
25
26
        this.keyPath = `${jaleSslPath}/${this.hostname}.key`
27
        this.csrPath = `${jaleSslPath}/${this.hostname}.csr`
28
        this.crtPath = `${jaleSslPath}/${this.hostname}.crt`
29
        this.configPath = `${jaleSslPath}/${this.hostname}.conf`
30
    }
31
32
    executeSecure = async (): Promise<void> => {
33
        console.log(`Securing ${this.hostname}...`)
34
        await ensureDirectoryExists(jaleSslPath)
35
36
        await this.unsecure()
37
38
        await this.createSslCertificate()
39
        this.secureNginxConfig()
40
41
        await (new Nginx()).restart()
42
43
        console.log(`${this.hostname} has been secured and is now reachable via https://${this.hostname}`)
44
    }
45
46
    executeUnsecure = async (): Promise<void> => {
47
        if (await this.unsecure()) {
48
            console.log(`${this.hostname} has been unsecured and is no longer reachable over https`)
49
            await (new Nginx()).restart()
50
        } else {
51
            console.log(`The site ${this.hostname} is not secured.`)
52
            return
53
        }
54
    }
55
56
    /**
57
     * Unsecure the current hostname.
58
     */
59
    private unsecure = async (): Promise<boolean> => {
60
        if (existsSync(this.crtPath)) {
61
            unlinkSync(this.csrPath)
62
            unlinkSync(this.keyPath)
63
            unlinkSync(this.crtPath)
64
            unlinkSync(this.configPath)
65
66
            await execa(
67
                'sudo',
68
                ['security', 'find-certificate', '-c', this.hostname, '-a', '-Z', '|', 'sudo', 'awk', '\'/SHA-1/{system("sudo security delete-certificate -Z "$NF)}\''],
69
                {shell: true, stdio: 'inherit'}
70
            )
71
72
            this.unsecureNginxConfig()
73
74
            return true
75
        } else {
76
            return false
77
        }
78
    }
79
80
    /**
81
     * Generate a certificate to secure a site.
82
     *
83
     * This will first generate an OpenSSL config which will be used for the CSR. Then we will create a private key and
84
     * generate a CSR. We will then request the certificate and trust it in our keychain.
85
     */
86
    private createSslCertificate = async (): Promise<void> => {
87
        // Write OpenSSL config for hostname
88
        await writeFileSync(this.configPath, opensslConfig(this.hostname))
89
90
        // Generate private key
91
        await execa('openssl', ['genrsa', '-out', this.keyPath, '2048'])
92
93
        // Generate certificate request with private key
94
        const subject = `/C=/ST=/O=/localityName=/commonName=*.${this.hostname}/organizationalUnitName=/emailAddress=/`
95
        await execa('openssl', ['req', '-new', '-key', this.keyPath, '-out', this.csrPath, '-subj',
96
            subject, '-config', this.configPath, '-passin', 'pass:'])
97
98
        await execa('openssl', ['x509', '-req', '-days', '365', '-in', this.csrPath, '-signkey',
99
            this.keyPath, '-out', this.crtPath, '-extensions', 'v3_req', '-extfile', this.configPath])
100
101
        // TODO: Make this cross-platform compatible.
102
        await execa('sudo', ['security', 'add-trusted-cert', '-d', '-r', 'trustRoot', '-k', '/Library/Keychains/System.keychain', this.crtPath])
103
    }
104
105
    /**
106
     * Make sure the Nginx config works with SSL.
107
     */
108
    private secureNginxConfig = () => {
109
        let nginxConfig = readFileSync(`${jaleSitesPath}/${this.hostname}.conf`, 'utf-8')
110
        if (nginxConfig.includes('listen 443 ssl http2')) {
111
            // TODO: Implement a nicer check. This is just a rushed thing to prevent duplicate ssl entries. Maybe it's
112
            // fine, but I ain't so sure about that.
113
            return
114
        }
115
116
        nginxConfig = nginxConfig.replace('listen [::]:80;', `listen [::]:80;
117
    listen 443 ssl http2;
118
    listen [::]:443 ssl http2;
119
    
120
    ssl_certificate ${this.crtPath};
121
    ssl_certificate_key ${this.keyPath};\n`)
122
123
        writeFileSync(`${jaleSitesPath}/${this.hostname}.conf`, nginxConfig)
124
    }
125
126
    /**
127
     * Clean up the Nginx config by removing references to the key en cert and stop listening on port 443.
128
     */
129
    private unsecureNginxConfig = () => {
130
        let nginxConfig = readFileSync(`${jaleSitesPath}/${this.hostname}.conf`, 'utf-8')
131
132
        nginxConfig = nginxConfig.replace(`listen [::]:80;
133
    listen 443 ssl http2;
134
    listen [::]:443 ssl http2;
135
    
136
    ssl_certificate ${this.crtPath};
137
    ssl_certificate_key ${this.keyPath};\n`, 'listen [::]:80;')
138
139
        writeFileSync(`${jaleSitesPath}/${this.hostname}.conf`, nginxConfig)
140
    }
141
}
142
143
export default SecureController