Passed
Push — develop ( f7a0d4...2b1143 )
by Bjarn
01:42
created

src/controllers/secureController.ts   A

Complexity

Total Complexity 11
Complexity/F 1.57

Size

Lines of Code 155
Function Count 7

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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