File size: 13,274 Bytes
4342d5f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';
import ipRegex from 'ip-regex';
import { canResolve, color, getConfigValue, stringToBool } from './util.js';

/**
 * @typedef {object} CommandLineArguments Parsed command line arguments
 * @property {string} dataRoot Data root directory
 * @property {number} port Port number
 * @property {boolean} listen If SillyTavern is listening on all network interfaces
 * @property {string} listenAddressIPv6 IPv6 address to listen to
 * @property {string} listenAddressIPv4 IPv4 address to listen to
 * @property {boolean|string} enableIPv4 If enable IPv4 protocol ("auto" is also allowed)
 * @property {boolean|string} enableIPv6 If enable IPv6 protocol ("auto" is also allowed)
 * @property {boolean} dnsPreferIPv6 If prefer IPv6 for DNS
 * @property {boolean} autorun If automatically launch SillyTavern in the browser
 * @property {string} autorunHostname Autorun hostname
 * @property {number} autorunPortOverride Autorun port override (-1 is use server port)
 * @property {boolean} enableCorsProxy If enable CORS proxy
 * @property {boolean} disableCsrf If disable CSRF protection
 * @property {boolean} ssl If enable SSL
 * @property {string} certPath Path to certificate
 * @property {string} keyPath Path to private key
 * @property {boolean} whitelistMode If enable whitelist mode
 * @property {boolean} avoidLocalhost If avoid using 'localhost' for autorun in auto mode
 * @property {boolean} basicAuthMode If enable basic authentication
 * @property {boolean} requestProxyEnabled If enable outgoing request proxy
 * @property {string} requestProxyUrl Request proxy URL
 * @property {string[]} requestProxyBypass Request proxy bypass list
 * @property {function(): URL} getIPv4ListenUrl Get IPv4 listen URL
 * @property {function(): URL} getIPv6ListenUrl Get IPv6 listen URL
 * @property {function(import('./server-startup.js').ServerStartupResult): Promise<string>} getAutorunHostname Get autorun hostname
 * @property {function(string): URL} getAutorunUrl Get autorun URL
 */

/**
 * Provides a command line arguments parser.
 */
export class CommandLineParser {
    constructor() {
        /** @type {CommandLineArguments} */
        this.default = Object.freeze({
            dataRoot: './data',
            port: 8000,
            listen: false,
            listenAddressIPv6: '[::]',
            listenAddressIPv4: '0.0.0.0',
            enableIPv4: true,
            enableIPv6: false,
            dnsPreferIPv6: false,
            autorun: false,
            autorunHostname: 'auto',
            autorunPortOverride: -1,
            enableCorsProxy: false,
            disableCsrf: false,
            ssl: false,
            certPath: 'certs/cert.pem',
            keyPath: 'certs/privkey.pem',
            whitelistMode: true,
            avoidLocalhost: false,
            basicAuthMode: false,
            requestProxyEnabled: false,
            requestProxyUrl: '',
            requestProxyBypass: [],
            getIPv4ListenUrl: function () {
                throw new Error('getIPv4ListenUrl is not implemented');
            },
            getIPv6ListenUrl: function () {
                throw new Error('getIPv6ListenUrl is not implemented');
            },
            getAutorunHostname: async function () {
                throw new Error('getAutorunHostname is not implemented');
            },
            getAutorunUrl: function () {
                throw new Error('getAutorunUrl is not implemented');
            },
        });

        this.booleanAutoOptions = [true, false, 'auto'];
    }

    /**
     * Parses command line arguments.
     * Arguments that are not provided will be filled with config values.
     * @param {string[]} args Process startup arguments.
     * @returns {CommandLineArguments} Parsed command line arguments.
     */
    parse(args) {
        const cliArguments = yargs(hideBin(args))
            .usage('Usage: <your-start-script> [options]\nOptions that are not provided will be filled with config values.')
            .option('enableIPv6', {
                type: 'string',
                default: null,
                describe: 'Enables IPv6 protocol',
            }).option('enableIPv4', {
                type: 'string',
                default: null,
                describe: 'Enables IPv4 protocol',
            }).option('port', {
                type: 'number',
                default: null,
                describe: 'Sets the server listening port',
            }).option('dnsPreferIPv6', {
                type: 'boolean',
                default: null,
                describe: 'Prefers IPv6 for DNS\nYou should probably have the enabled if you\'re on an IPv6 only network',
            }).option('autorun', {
                type: 'boolean',
                default: null,
                describe: 'Automatically launch SillyTavern in the browser',
            }).option('autorunHostname', {
                type: 'string',
                default: null,
                describe: 'Sets the autorun hostname, probably best left on \'auto\'.\nUse values like \'localhost\', \'st.example.com\'',
            }).option('autorunPortOverride', {
                type: 'string',
                default: null,
                describe: 'Overrides the port for autorun with open your browser with this port and ignore what port the server is running on. -1 is use server port',
            }).option('listen', {
                type: 'boolean',
                default: null,
                describe: 'Whether to listen on all network interfaces',
            }).option('listenAddressIPv6', {
                type: 'string',
                default: null,
                describe: 'Specific IPv6 address to listen to',
            }).option('listenAddressIPv4', {
                type: 'string',
                default: null,
                describe: 'Specific IPv4 address to listen to',
            }).option('corsProxy', {
                type: 'boolean',
                default: null,
                describe: 'Enables CORS proxy',
            }).option('disableCsrf', {
                type: 'boolean',
                default: null,
                describe: 'Disables CSRF protection - NOT RECOMMENDED',
            }).option('ssl', {
                type: 'boolean',
                default: null,
                describe: 'Enables SSL',
            }).option('certPath', {
                type: 'string',
                default: null,
                describe: 'Path to SSL certificate file',
            }).option('keyPath', {
                type: 'string',
                default: null,
                describe: 'Path to SSL private key file',
            }).option('whitelist', {
                type: 'boolean',
                default: null,
                describe: 'Enables whitelist mode',
            }).option('dataRoot', {
                type: 'string',
                default: null,
                describe: 'Root directory for data storage',
            }).option('avoidLocalhost', {
                type: 'boolean',
                default: null,
                describe: 'Avoids using \'localhost\' for autorun in auto mode.\nUse if you don\'t have \'localhost\' in your hosts file',
            }).option('basicAuthMode', {
                type: 'boolean',
                default: null,
                describe: 'Enables basic authentication',
            }).option('requestProxyEnabled', {
                type: 'boolean',
                default: null,
                describe: 'Enables a use of proxy for outgoing requests',
            }).option('requestProxyUrl', {
                type: 'string',
                default: null,
                describe: 'Request proxy URL (HTTP or SOCKS protocols)',
            }).option('requestProxyBypass', {
                type: 'array',
                describe: 'Request proxy bypass list (space separated list of hosts)',
            }).parseSync();

        /** @type {CommandLineArguments} */
        const result = {
            dataRoot: cliArguments.dataRoot ?? getConfigValue('dataRoot', this.default.dataRoot),
            port: cliArguments.port ?? getConfigValue('port', this.default.port, 'number'),
            listen: cliArguments.listen ?? getConfigValue('listen', this.default.listen, 'boolean'),
            listenAddressIPv6: cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', this.default.listenAddressIPv6),
            listenAddressIPv4: cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', this.default.listenAddressIPv4),
            enableIPv4: stringToBool(cliArguments.enableIPv4) ?? stringToBool(getConfigValue('protocol.ipv4', this.default.enableIPv4)) ?? this.default.enableIPv4,
            enableIPv6: stringToBool(cliArguments.enableIPv6) ?? stringToBool(getConfigValue('protocol.ipv6', this.default.enableIPv6)) ?? this.default.enableIPv6,
            dnsPreferIPv6: cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', this.default.dnsPreferIPv6, 'boolean'),
            autorun: cliArguments.autorun ?? getConfigValue('autorun', this.default.autorun, 'boolean'),
            autorunHostname: cliArguments.autorunHostname ?? getConfigValue('autorunHostname', this.default.autorunHostname),
            autorunPortOverride: cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', this.default.autorunPortOverride, 'number'),
            enableCorsProxy: cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', this.default.enableCorsProxy, 'boolean'),
            disableCsrf: cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', this.default.disableCsrf, 'boolean'),
            ssl: cliArguments.ssl ?? getConfigValue('ssl.enabled', this.default.ssl, 'boolean'),
            certPath: cliArguments.certPath ?? getConfigValue('ssl.certPath', this.default.certPath),
            keyPath: cliArguments.keyPath ?? getConfigValue('ssl.keyPath', this.default.keyPath),
            whitelistMode: cliArguments.whitelist ?? getConfigValue('whitelistMode', this.default.whitelistMode, 'boolean'),
            avoidLocalhost: cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', this.default.avoidLocalhost, 'boolean'),
            basicAuthMode: cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', this.default.basicAuthMode, 'boolean'),
            requestProxyEnabled: cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', this.default.requestProxyEnabled, 'boolean'),
            requestProxyUrl: cliArguments.requestProxyUrl ?? getConfigValue('requestProxy.url', this.default.requestProxyUrl),
            requestProxyBypass: cliArguments.requestProxyBypass ?? getConfigValue('requestProxy.bypass', this.default.requestProxyBypass),
            getIPv4ListenUrl: function () {
                const isValid = ipRegex.v4({ exact: true }).test(this.listenAddressIPv4);
                return new URL(
                    (this.ssl ? 'https://' : 'http://') +
                    (this.listen ? (isValid ? this.listenAddressIPv4 : '0.0.0.0') : '127.0.0.1') +
                    (':' + this.port),
                );
            },
            getIPv6ListenUrl: function () {
                const isValid = ipRegex.v6({ exact: true }).test(this.listenAddressIPv6);
                return new URL(
                    (this.ssl ? 'https://' : 'http://') +
                    (this.listen ? (isValid ? this.listenAddressIPv6 : '[::]') : '[::1]') +
                    (':' + this.port),
                );
            },
            getAutorunHostname: async function ({ useIPv6, useIPv4 }) {
                if (this.autorunHostname === 'auto') {
                    let localhostResolve = await canResolve('localhost', useIPv6, useIPv4);

                    if (useIPv6 && useIPv4) {
                        return (this.avoidLocalhost || !localhostResolve) ? '[::1]' : 'localhost';
                    }

                    if (useIPv6) {
                        return '[::1]';
                    }

                    if (useIPv4) {
                        return '127.0.0.1';
                    }
                }

                return this.autorunHostname;
            },
            getAutorunUrl: function (hostname) {
                const autorunPort = (this.autorunPortOverride >= 0) ? this.autorunPortOverride : this.port;
                return new URL(
                    (this.ssl ? 'https://' : 'http://') +
                    (hostname) +
                    (':') +
                    (autorunPort),
                );
            },
        };

        if (!this.booleanAutoOptions.includes(result.enableIPv6)) {
            console.warn(color.red('`protocol: ipv6` option invalid'), '\n use:', this.booleanAutoOptions, '\n setting to:', this.default.enableIPv6);
            result.enableIPv6 = this.default.enableIPv6;
        }

        if (!this.booleanAutoOptions.includes(result.enableIPv4)) {
            console.warn(color.red('`protocol: ipv4` option invalid'), '\n use:', this.booleanAutoOptions, '\n setting to:', this.default.enableIPv4);
            result.enableIPv4 = this.default.enableIPv4;
        }

        return result;
    }
}