Passed
Push — main ( a5cb81...de7f7b )
by Bjarn
04:10 queued 02:01
created

SecureController.isSecure   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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