mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-27 06:42:52 +08:00
904 lines
35 KiB
JavaScript
904 lines
35 KiB
JavaScript
'use strict';
|
|
'require dom';
|
|
'require form';
|
|
'require fs';
|
|
'require poll';
|
|
'require rpc';
|
|
'require uci';
|
|
'require ui';
|
|
'require view';
|
|
|
|
const HELPER = '/usr/libexec/openlist2-helper';
|
|
|
|
const callServiceList = rpc.declare({
|
|
object: 'service',
|
|
method: 'list',
|
|
params: ['name'],
|
|
expect: { '': {} }
|
|
});
|
|
|
|
const getServiceStatus = () => {
|
|
return L.resolveDefault(callServiceList('openlist2'), {}).then(res => {
|
|
let isRunning = false;
|
|
try {
|
|
isRunning = res['openlist2']['instances']['openlist2']['running'];
|
|
} catch (e) { }
|
|
return isRunning;
|
|
});
|
|
};
|
|
|
|
const getWebInterfaceUrl = (protocol, webport, site_url) => {
|
|
const configuredUrl = (site_url || '').trim();
|
|
if (configuredUrl) {
|
|
try {
|
|
const url = new URL(configuredUrl);
|
|
if (url.protocol === 'http:' || url.protocol === 'https:')
|
|
return url.href;
|
|
} catch (e) { }
|
|
}
|
|
return webport ? protocol + '//' + window.location.hostname + ':' + webport + '/' : null;
|
|
};
|
|
|
|
const renderStatus = (isRunning, protocol, webport, site_url) => {
|
|
const status = E('em', {}, E('span', { 'style': `color:${isRunning ? 'green' : 'red'}` },
|
|
E('strong', {}, ['OpenList ', isRunning ? _('RUNNING') : _('NOT RUNNING')])));
|
|
|
|
if (isRunning && (webport || site_url)) {
|
|
const buttonUrl = getWebInterfaceUrl(protocol, webport, site_url);
|
|
if (!buttonUrl)
|
|
return status;
|
|
const button = E('input', {
|
|
'class': 'cbi-button-reload',
|
|
'type': 'button',
|
|
'style': 'margin-left: 50px',
|
|
'value': _('Open Web Interface')
|
|
});
|
|
button.addEventListener('click', () => window.open(buttonUrl, '_blank', 'noopener'));
|
|
|
|
return E('span', {}, [status, button]);
|
|
}
|
|
|
|
return status;
|
|
};
|
|
|
|
const isSafeLogPath = value => !/[\r\n]/.test(value || '') &&
|
|
/^\/var\/log\/openlist2(?:[._-][A-Za-z0-9._-]+)?$/.test(value || '');
|
|
|
|
const isEnabledPort = value => /^\d+$/.test(String(value || '').trim()) && Number(value) >= 1 && Number(value) <= 65535;
|
|
|
|
const getListenPortValue = (section_id, option, fallback) => {
|
|
const value = uci.get('openlist2', section_id, option);
|
|
if (value != null && String(value).trim() !== '')
|
|
return value;
|
|
const legacyPort = uci.get('openlist2', section_id, 'port');
|
|
const legacySsl = uci.get('openlist2', section_id, 'ssl');
|
|
if (legacyPort != null || legacySsl != null) {
|
|
const tls = ['1', 'true', 'on', 'yes', 'enabled'].includes(legacySsl);
|
|
return option === 'listen_https_port'
|
|
? (tls ? legacyPort || '5244' : '-1')
|
|
: (tls ? '-1' : legacyPort || '5244');
|
|
}
|
|
return fallback;
|
|
};
|
|
|
|
const validateListenPort = value => {
|
|
const port = (value || '').trim();
|
|
if (port === '-1')
|
|
return true;
|
|
|
|
if (/^\d+$/.test(port)) {
|
|
const number = Number(port);
|
|
if (number >= 1 && number <= 65535)
|
|
return true;
|
|
}
|
|
|
|
return _('Port must be -1 or a valid port number from 1 to 65535.');
|
|
};
|
|
|
|
const isHttpUrl = value => {
|
|
try {
|
|
const url = new URL(value);
|
|
return !!url.hostname && (url.protocol === 'http:' || url.protocol === 'https:');
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const validateSiteUrl = value => {
|
|
const url = (value || '').trim();
|
|
if (!url)
|
|
return true;
|
|
|
|
if (isHttpUrl(url) && !url.endsWith('/'))
|
|
return true;
|
|
|
|
return _('Site URL must start with http:// or https:// and must not end with /.');
|
|
};
|
|
|
|
const validateCorsOrigins = value => {
|
|
const origins = (value || '').split(',')
|
|
.map(origin => origin.trim())
|
|
.filter(Boolean);
|
|
|
|
for (const origin of origins) {
|
|
if (origin === '*' || isHttpUrl(origin))
|
|
continue;
|
|
|
|
return _('CORS origins must be * or include http:// or https://.');
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const getEffectiveWebEndpoint = config => {
|
|
const httpPort = getListenPortValue('@openlist2[0]', 'listen_http_port', '5244');
|
|
const httpsPort = getListenPortValue('@openlist2[0]', 'listen_https_port', '-1');
|
|
|
|
if (isEnabledPort(httpsPort))
|
|
return { protocol: 'https:', port: String(Number(httpsPort)) };
|
|
|
|
if (isEnabledPort(httpPort))
|
|
return { protocol: 'http:', port: String(Number(httpPort)) };
|
|
|
|
return { protocol: 'http:', port: '' };
|
|
};
|
|
|
|
|
|
const normalizePath = value => String(value || '').replace(/\/+/g, '/').replace(/\/$/, '') || '/';
|
|
const containsPath = (parent, child) => child === parent || child.startsWith(parent + '/');
|
|
const broadDirectories = new Set(['/', '/bin', '/boot', '/dev', '/etc', '/home', '/lib', '/lib64', '/media', '/mnt', '/opt', '/overlay', '/proc', '/root', '/rom', '/run', '/sbin', '/srv', '/sys', '/tmp', '/usr', '/var', '/www', '/tmp/log', '/tmp/run']);
|
|
const isBroadDirectory = path => broadDirectories.has(path) ||
|
|
/^\/(?:usr|bin|sbin|lib|lib64|dev|proc|sys|rom|overlay|www)\//.test(path) ||
|
|
/^\/(?:etc\/(?:config|dropbear|ssl)|tmp\/log)(?:\/|$)/.test(path);
|
|
|
|
const validateAbsolutePath = (value, required) => {
|
|
const path = String(value || '');
|
|
if (!path && !required)
|
|
return true;
|
|
if (!path.startsWith('/') || /[\x00-\x1f\x7f]/.test(path) || /\/\.{1,2}(?:\/|$)/.test(path))
|
|
return _('Use an absolute path without control characters or dot segments.');
|
|
return true;
|
|
};
|
|
|
|
const passiveRanges = value => (Array.isArray(value) ? value : [value || ''])
|
|
.flatMap(item => String(item).split(/[,\s]+/)).filter(Boolean);
|
|
|
|
const validatePassivePorts = value => {
|
|
for (const range of passiveRanges(value)) {
|
|
if (!/^\d+(?:-\d+)?$/.test(range))
|
|
return _('Use passive ports or ranges from 1024 to 65535.');
|
|
const [first, last = first] = range.split('-').map(Number);
|
|
if (first < 1024 || last > 65535 || first > last)
|
|
return _('Use passive ports or ranges from 1024 to 65535.');
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const backendUrl = route => {
|
|
const endpoint = getEffectiveWebEndpoint('openlist2');
|
|
const base = getWebInterfaceUrl(endpoint.protocol, endpoint.port,
|
|
uci.get('openlist2', '@openlist2[0]', 'site_url'));
|
|
if (!base)
|
|
return null;
|
|
const url = new URL(base);
|
|
url.pathname = url.pathname.replace(/\/$/, '') + '/' + route.replace(/^\//, '');
|
|
url.search = '';
|
|
url.hash = '';
|
|
return url.href;
|
|
};
|
|
|
|
const pollServiceStatus = () => getServiceStatus().then(running => {
|
|
const node = document.getElementById('service_status');
|
|
const endpoint = L.hasViewPermission() ? getEffectiveWebEndpoint('openlist2') : { protocol: 'http:', port: '' };
|
|
const site = L.hasViewPermission() ? uci.get('openlist2', '@openlist2[0]', 'site_url') : '';
|
|
if (node)
|
|
dom.content(node, renderStatus(running, endpoint.protocol, endpoint.port, site));
|
|
});
|
|
|
|
const renderServiceSection = () => {
|
|
poll.add(pollServiceStatus);
|
|
return E('div', { 'class': 'cbi-section', 'id': 'status_bar' },
|
|
E('p', { 'id': 'service_status' }, _('Collecting data...')));
|
|
};
|
|
|
|
const copyPassword = async value => {
|
|
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
try {
|
|
await navigator.clipboard.writeText(value);
|
|
return true;
|
|
} catch (e) { }
|
|
}
|
|
const field = E('textarea', { 'style': 'position:fixed;left:-10000px' }, value);
|
|
document.body.appendChild(field);
|
|
try {
|
|
field.select();
|
|
return document.execCommand('copy') === true;
|
|
} catch (e) {
|
|
return false;
|
|
} finally {
|
|
document.body.removeChild(field);
|
|
}
|
|
};
|
|
|
|
return view.extend({
|
|
load() {
|
|
// UCI contains credentials; readers may view status and bounded logs only.
|
|
return L.hasViewPermission() ? uci.load('openlist2') : Promise.resolve();
|
|
},
|
|
|
|
async handleResetPassword() {
|
|
if (!L.hasViewPermission())
|
|
return;
|
|
try {
|
|
const result = await fs.exec(HELPER, ['password-reset']);
|
|
const output = (result.stdout || '').replace(/\u001b\[[0-9;]*m/g, '');
|
|
const username = output.match(/^username:\s*(.+)$/m);
|
|
const password = output.match(/^password:\s*(\S+)\s*$/m);
|
|
if (result.code !== 0 || !username || !password)
|
|
throw new Error((result.stderr || _('The password reset did not return a username and password.')).trim());
|
|
|
|
const newPassword = password[1];
|
|
const message = E('p', { 'role': 'status' }, _('Copy the new password before closing this dialog.'));
|
|
const copy = async () => {
|
|
message.textContent = await copyPassword(newPassword)
|
|
? _('New password has been copied to clipboard.')
|
|
: _('Automatic copy failed. Select and copy the password manually.');
|
|
};
|
|
ui.showModal(_('Password reset'), [
|
|
E('p', {}, _('Username:') + ' ' + username[1].trim()),
|
|
E('label', {}, _('New Password:')),
|
|
E('input', { 'type': 'text', 'readonly': 'readonly', 'value': newPassword, 'style': 'width:100%' }),
|
|
message,
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', { 'class': 'btn cbi-button-action', 'click': ui.createHandlerFn(this, copy) }, _('Copy password')),
|
|
' ',
|
|
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
|
|
])
|
|
]);
|
|
await copy();
|
|
} catch (error) {
|
|
ui.addNotification(null, E('p', _('Unable to reset the password: %s').format(error.message)));
|
|
}
|
|
},
|
|
|
|
render() {
|
|
if (!L.hasViewPermission())
|
|
return E('div', {}, [
|
|
renderServiceSection(),
|
|
E('p', {}, _('Configuration contains credentials and is available only to users with write access.'))
|
|
]);
|
|
let m, s, o;
|
|
|
|
m = new form.Map('openlist2', _('OpenList'),
|
|
_('A file list program that supports multiple storage.') + '<br />' +
|
|
_('Initial login username is "%s" and password is "%s". Change it immediately after first login.').format('admin', 'admin'));
|
|
|
|
s = m.section(form.TypedSection);
|
|
s.anonymous = true;
|
|
s.addremove = false;
|
|
|
|
s.render = renderServiceSection;
|
|
|
|
s = m.section(form.NamedSection, '@openlist2[0]', 'openlist2');
|
|
const fields = {};
|
|
const addOption = (tab, type, name, ...args) => {
|
|
const option = s.taboption(tab, type, name, ...args);
|
|
fields[name] = option;
|
|
return option;
|
|
};
|
|
const fieldValue = (section_id, name, fallback = '') => {
|
|
const value = fields[name] && fields[name].formvalue(section_id);
|
|
return value == null ? uci.get('openlist2', section_id, name) ?? fields[name]?.default ?? fallback : value;
|
|
};
|
|
const addBackendLink = (tab, name, route, label, description) => {
|
|
const option = addOption(tab, form.DummyValue, name, label, description);
|
|
option.renderWidget = () => {
|
|
const url = backendUrl(route);
|
|
return url ? E('a', { 'href': url, 'target': '_blank', 'rel': 'noopener noreferrer' }, label)
|
|
: E('span', {}, _('Configure a web port or Site URL to open the management page.'));
|
|
};
|
|
return option;
|
|
};
|
|
|
|
s.tab('basic', _('Basic Settings'));
|
|
s.tab('global', _('Global Settings'));
|
|
s.tab('log', _('Logs'));
|
|
s.tab('database', _('Database'));
|
|
s.tab('search', _('Search'));
|
|
s.tab('scheme', _('Web Protocol'));
|
|
s.tab('tasks', _('Task threads'),
|
|
_('Worker counts below only initialize missing settings. After the first start, change worker counts and speed limits in OpenList Management > Settings > Traffic. Upload and decompression-upload tasks do not support persistence.'));
|
|
s.tab('cors', _('CORS Settings'));
|
|
s.tab('s3', _('Object Storage'));
|
|
s.tab('ftp', _('FTP'));
|
|
s.tab('sftp', _('SFTP'));
|
|
s.tab('mcp', _('MCP'));
|
|
|
|
// init
|
|
o = addOption('basic', form.Flag, 'enabled', _('Enabled'));
|
|
o.default = o.disabled;
|
|
o.rmempty = false;
|
|
|
|
o = addOption('basic', form.Flag, 'debug', _('Debug logging'),
|
|
_('Pass --debug to OpenList and send stderr to procd logs.'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('basic', form.Value, 'delayed_start', _('Delayed Start (seconds)'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '0';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('basic', form.Flag, 'allow_wan', _('Open firewall port'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('basic', form.Value, 'data_dir', _('Data directory'),
|
|
_('Changing this path does not move data. Stop OpenList, back up and move the complete data directory, then apply the new path.'));
|
|
o.default = '/etc/openlist2';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('basic', form.Value, 'temp_dir', _('Cache directory'),
|
|
_('OpenList clears this directory on startup. Use a dedicated empty subdirectory. Never select a disk mount root or a directory containing files you want to keep.'));
|
|
o.default = '/tmp/openlist2';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('basic', form.Button, '_newpassword', _('Reset Password'),
|
|
_('Generate a new random password using the saved and applied data directory. Start OpenList once before using this action.'));
|
|
o.inputtitle = _('Reset Password');
|
|
o.inputstyle = 'apply';
|
|
o.onclick = ui.createHandlerFn(this, 'handleResetPassword');
|
|
|
|
// global
|
|
o = addOption('global', form.Flag, 'force', _('Force read config'),
|
|
_('Setting this to true will force the program to read the configuration file, ignoring environment variables.'));
|
|
o.default = '1';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('global', form.Value, 'site_url', _('Site URL'),
|
|
_('When the web is reverse proxied to a subdirectory, this option must be filled out to ensure proper functioning of the web. Do not include \'/\' at the end of the URL'));
|
|
o.validate = function(section_id, value) {
|
|
return validateSiteUrl(value);
|
|
};
|
|
|
|
o = addOption('global', form.Value, 'cdn', _('CDN URL'));
|
|
o.default = '';
|
|
|
|
o = addOption('global', form.Value, 'jwt_secret', _('JWT Key'));
|
|
o.password = true;
|
|
o.default = '';
|
|
|
|
o = addOption('global', form.Value, 'token_expires_in', _('Login Validity Period (hours)'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '48';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('global', form.Value, 'proxy_address', _('Proxy address'),
|
|
_('HTTP, HTTPS, SOCKS4, SOCKS5 or SOCKS5HOSTNAME proxy used by OpenList outbound requests.'));
|
|
o.password = true;
|
|
o.default = '';
|
|
|
|
o = addOption('global', form.Value, 'auto_memory_limit', _('Auto memory limit (MB)'),
|
|
_('0 disables the automatic memory limit. The upstream default is 4 MB.'));
|
|
o.default = '4';
|
|
o.datatype = 'uinteger';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('global', form.Value, 'min_free_memory', _('Minimum free memory (MB)'),
|
|
_('Values below 16 let OpenList calculate a default; negative values disable memory cache.'));
|
|
o.default = '0';
|
|
o.datatype = 'integer';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('global', form.Value, 'max_block_limit', _('Maximum block size (MB)'),
|
|
_('Values below 4 let OpenList calculate a default.'));
|
|
o.default = '0';
|
|
o.datatype = 'uinteger';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('global', form.Value, 'max_connections', _('Max Connections'),
|
|
_('0 is unlimited, It is recommend to set a low number of concurrency (10-20) for poor performance device'));
|
|
o.default = '0';
|
|
o.datatype = 'uinteger';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('global', form.Value, 'max_concurrency', _('Max concurrency of local proxies'),
|
|
_('0 is unlimited, Limit the maximum concurrency of local agents. The default value is 64'));
|
|
o.default = '64';
|
|
o.datatype = 'uinteger';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('global', form.Flag, 'tls_insecure_skip_verify', _('Disable TLS Verify'),
|
|
_('Skip remote TLS certificate verification (not recommended).'));
|
|
o.default = '0';
|
|
o.rmempty = false;
|
|
|
|
// Logs
|
|
o = addOption('log', form.Flag, 'log', _('Enable Logs'));
|
|
o.default = '1';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('log', form.Value, 'log_path', _('Log path'));
|
|
o.default = '/var/log/openlist2.log';
|
|
o.rmempty = false;
|
|
o.depends('log', '1');
|
|
o.validate = function(section_id, value) {
|
|
if (isSafeLogPath(value))
|
|
return true;
|
|
|
|
return _('Log path must be /var/log/openlist2* without subdirectories.');
|
|
};
|
|
|
|
o = addOption('log', form.Value, 'log_max_size', _('Max Size (MB)'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '1';
|
|
o.rmempty = false;
|
|
o.depends('log', '1');
|
|
|
|
o = addOption('log', form.Value, 'log_max_backups', _('Max backups'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '3';
|
|
o.rmempty = false;
|
|
o.depends('log', '1');
|
|
|
|
o = addOption('log', form.Value, 'log_max_age', _('Max age'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '28';
|
|
o.rmempty = false;
|
|
o.depends('log', '1');
|
|
|
|
o = addOption('log', form.Flag, 'log_compress', _('Log Compress'));
|
|
o.default = '0';
|
|
o.rmempty = false;
|
|
o.depends('log', '1');
|
|
|
|
o = addOption('log', form.Flag, 'log_filter', _('Enable common log filters'),
|
|
_('Filter noisy access logs such as health checks, HEAD requests and WebDAV PROPFIND.'));
|
|
o.rmempty = false;
|
|
o.depends('log', '1');
|
|
|
|
o = addOption('log', form.Value, 'log_filter_cidr', _('Custom log filter CIDR'));
|
|
o.depends('log_filter', '1');
|
|
|
|
o = addOption('log', form.Value, 'log_filter_path', _('Custom log filter path'));
|
|
o.depends('log_filter', '1');
|
|
|
|
o = addOption('log', form.Value, 'log_filter_method', _('Custom log filter method'));
|
|
o.depends('log_filter', '1');
|
|
|
|
// database
|
|
o = addOption('database', form.ListValue, 'database_type', _('Database Type'));
|
|
o.default = 'sqlite3';
|
|
o.value('sqlite3', _('SQLite'));
|
|
o.value('mysql', _('MySQL'));
|
|
o.value('postgres', _('PostgreSQL'));
|
|
|
|
o = addOption('database', form.Value, 'mysql_host', _('Database Host'));
|
|
o.depends('database_type', 'mysql');
|
|
o.depends('database_type', 'postgres');
|
|
|
|
o = addOption('database', form.Value, 'mysql_port', _('Database Port'),
|
|
_('Use 0 to apply the usual default for the selected database: 3306 for MySQL, 5432 for PostgreSQL.'));
|
|
o.datatype = 'port';
|
|
o.default = '0';
|
|
o.depends('database_type', 'mysql');
|
|
o.depends('database_type', 'postgres');
|
|
|
|
o = addOption('database', form.Value, 'mysql_username', _('Database Username'));
|
|
o.depends('database_type', 'mysql');
|
|
o.depends('database_type', 'postgres');
|
|
|
|
o = addOption('database', form.Value, 'mysql_password', _('Database Password'));
|
|
o.password = true;
|
|
o.depends('database_type', 'mysql');
|
|
o.depends('database_type', 'postgres');
|
|
|
|
o = addOption('database', form.Value, 'mysql_database', _('Database Name'));
|
|
o.depends('database_type', 'mysql');
|
|
o.depends('database_type', 'postgres');
|
|
|
|
o = addOption('database', form.Value, 'mysql_table_prefix', _('Database Table Prefix'));
|
|
o.default = 'x_';
|
|
o.depends('database_type', 'mysql');
|
|
o.depends('database_type', 'postgres');
|
|
|
|
o = addOption('database', form.Value, 'mysql_ssl_mode', _('Database SSL Mode'));
|
|
o.depends('database_type', 'mysql');
|
|
o.depends('database_type', 'postgres');
|
|
|
|
o = addOption('database', form.Value, 'mysql_dsn', _('Database DSN'));
|
|
o.password = true;
|
|
o.depends('database_type', 'mysql');
|
|
o.depends('database_type', 'postgres');
|
|
|
|
o = addOption('search', form.Value, 'meilisearch_host', _('Meilisearch Host'));
|
|
o.default = 'http://localhost:7700';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('search', form.Value, 'meilisearch_api_key', _('Meilisearch API Key'));
|
|
o.password = true;
|
|
|
|
o = addOption('search', form.Value, 'meilisearch_index', _('Meilisearch Index'));
|
|
o.default = 'openlist';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('search', form.Value, 'bleve_dir', _('Bleve index directory'),
|
|
_('Empty uses the bleve subdirectory of the data directory. Keep the index separate from cache and frontend files.'));
|
|
o.default = '';
|
|
|
|
o = addOption('global', form.Value, 'dist_dir', _('External frontend directory'),
|
|
_('Optional directory containing index.html from a matching OpenList frontend. Empty uses the bundled frontend. Restart OpenList after changing it.'));
|
|
o.default = '';
|
|
|
|
// scheme
|
|
o = addOption('scheme', form.Value, 'listen_addr', _('Listen address'));
|
|
o.default = '0.0.0.0';
|
|
o.datatype = 'ipaddr';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('scheme', form.Value, 'listen_http_port', _('HTTP listen port'),
|
|
_('The upstream default is 5244. Set to -1 to disable HTTP.'));
|
|
o.datatype = 'integer';
|
|
o.default = '5244';
|
|
o.rmempty = false;
|
|
o.cfgvalue = section_id => getListenPortValue(section_id, 'listen_http_port', '5244');
|
|
o.validate = function(section_id, value) {
|
|
return validateListenPort(value);
|
|
};
|
|
|
|
o = addOption('scheme', form.Value, 'listen_https_port', _('HTTPS listen port'),
|
|
_('Set to -1 to disable HTTPS, or 1-65535 to enable it. Leave both certificate paths empty to use an automatically generated self-signed certificate.'));
|
|
o.datatype = 'integer';
|
|
o.default = '-1';
|
|
o.rmempty = false;
|
|
o.cfgvalue = section_id => getListenPortValue(section_id, 'listen_https_port', '-1');
|
|
o.validate = function(section_id, value) {
|
|
return validateListenPort(value);
|
|
};
|
|
|
|
o = addOption('scheme', form.Flag, 'force_https', _('Force HTTPS'),
|
|
_('Redirect HTTP requests to HTTPS. The HTTPS listen port enables HTTPS independently of this option.'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('scheme', form.Value, 'ssl_cert', _('SSL cert'),
|
|
_('Leave both paths empty to manage a self-signed certificate in the TLS subdirectory of the data directory. It is reused and renewed at startup when fewer than 30 days remain. Browsers do not trust self-signed certificates automatically.'));
|
|
|
|
o = addOption('scheme', form.Value, 'ssl_key', _('SSL key'),
|
|
_('For custom TLS, specify both certificate and private key paths. The certificate must be currently valid and match its private key. Custom files are never overwritten automatically.'));
|
|
|
|
o = addOption('scheme', form.Value, 'listen_unix_file', _('Unix socket file'));
|
|
|
|
o = addOption('scheme', form.Value, 'listen_unix_file_perm', _('Unix socket permission'));
|
|
|
|
o = addOption('scheme', form.Flag, 'listen_enable_h2c', _('Enable H2C'),
|
|
_('Enable cleartext HTTP/2 for reverse proxies that use grpc_pass.'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('scheme', form.Flag, 'listen_enable_h3', _('Enable HTTP/3/QUIC'),
|
|
_('Enable HTTP/3 over QUIC on the HTTPS listen port. HTTPS must be enabled. Certificate settings are shared with HTTPS.'));
|
|
o.rmempty = false;
|
|
|
|
// tasks
|
|
addBackendLink('tasks', '_traffic', '@manage/settings/traffic', _('Open traffic settings'),
|
|
_('Use the OpenList backend to change the effective worker counts and transfer speed limits.'));
|
|
o = addOption('tasks', form.Value, 'download_workers', _('Download Workers (initial)'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '5';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Value, 'download_max_retry', _('Download Max Retry'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '1';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Flag, 'download_task_persistant', _('Download Task Persistence'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Value, 'transfer_workers', _('Transfer Workers (initial)'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '5';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Value, 'transfer_max_retry', _('Transfer Max Retry'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '2';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Flag, 'transfer_task_persistant', _('Transfer Task Persistence'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Value, 'upload_workers', _('Upload Workers (initial)'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '5';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Value, 'upload_max_retry', _('Upload Max Retry'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '0';
|
|
o.rmempty = false;
|
|
|
|
|
|
o = addOption('tasks', form.Value, 'copy_workers', _('Copy Workers (initial)'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '5';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Value, 'copy_max_retry', _('Copy Max Retry'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '2';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Flag, 'copy_task_persistant', _('Copy Task Persistence'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Value, 'move_workers', _('Move Workers (initial)'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '5';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Value, 'move_max_retry', _('Move Max Retry'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '2';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Flag, 'move_task_persistant', _('Move Task Persistence'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Value, 'decompress_workers', _('Decompress Workers (initial)'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '5';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Value, 'decompress_max_retry', _('Decompress Max Retry'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '2';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Flag, 'decompress_task_persistant', _('Decompress Task Persistence'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Value, 'decompress_upload_workers', _('Decompress Upload Workers (initial)'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '5';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('tasks', form.Value, 'decompress_upload_max_retry', _('Decompress Upload Max Retry'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '2';
|
|
o.rmempty = false;
|
|
|
|
|
|
o = addOption('tasks', form.Flag, 'allow_retry_canceled', _('Allow retry canceled tasks'));
|
|
o.rmempty = false;
|
|
|
|
// cors
|
|
o = addOption('cors', form.Value, 'cors_allow_origins', _('Allow Origins'),
|
|
_('Comma-separated list. Use * to allow any value.'));
|
|
o.default = '*';
|
|
o.rmempty = false;
|
|
o.validate = function(section_id, value) {
|
|
return validateCorsOrigins(value);
|
|
};
|
|
|
|
o = addOption('cors', form.Value, 'cors_allow_methods', _('Allow Methods'),
|
|
_('Comma-separated list. Use * to allow any value.'));
|
|
o.default = '*';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('cors', form.Value, 'cors_allow_headers', _('Allow Headers'),
|
|
_('Comma-separated list. Use * to allow any value.'));
|
|
o.default = '*';
|
|
o.rmempty = false;
|
|
|
|
// s3
|
|
addBackendLink('s3', '_s3_settings', '@manage/settings/s3', _('Open S3 management'),
|
|
_('Generate the S3 access key and secret in OpenList settings, then configure bucket mappings. Restart OpenList after changing credentials.'));
|
|
o = addOption('s3', form.Flag, 's3', _('Enabled S3'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('s3', form.Value, 's3_port', _('Port'));
|
|
o.datatype = 'and(port,min(1))';
|
|
o.default = 5246;
|
|
o.rmempty = false;
|
|
|
|
o = addOption('s3', form.Flag, 's3_ssl', _('Enable SSL'),
|
|
_('Use the same custom or automatically generated certificate as HTTPS. Configure certificate paths on the Web Protocol tab.'));
|
|
o.rmempty = false;
|
|
|
|
// ftp
|
|
addBackendLink('ftp', '_ftp_settings', '@manage/settings/ftp', _('Open FTP settings'),
|
|
_('Configure the public host and passive port mapping in OpenList. Restart the service after changing FTP settings.'));
|
|
o = addOption('ftp', form.DynamicList, 'ftp_pasv_ports', _('FTP passive firewall ports'),
|
|
_('Ports or ranges to allow from WAN, for example 50000-50100. They must match the listening ports in OpenList passive port mapping. Empty means passive ports must be allowed manually.'));
|
|
o.placeholder = '50000-50100';
|
|
o.depends({ ftp: '1', allow_wan: '1' });
|
|
o = addOption('ftp', form.Flag, 'ftp', _('Enabled FTP'),
|
|
_('Enable FTP/SFTP access for the OpenList account in Management > Users. Also grant FTP/SFTP management permission to allow uploads and changes.'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('ftp', form.Value, 'ftp_port', _('FTP Port'));
|
|
o.datatype = 'and(port,min(1))';
|
|
o.default = 5221;
|
|
o.rmempty = false;
|
|
|
|
o = addOption('ftp', form.Value, 'find_pasv_port_attempts', _('Max retries on port conflict during passive transfer'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '50';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('ftp', form.Flag, 'active_transfer_port_non_20', _('Enable non-20 port for active transfer'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('ftp', form.Value, 'idle_timeout', _('Client idle timeout (seconds)'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '900';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('ftp', form.Value, 'connection_timeout', _('Connection timeout (seconds)'));
|
|
o.datatype = 'uinteger';
|
|
o.default = '30';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('ftp', form.Flag, 'disable_active_mode', _('Disable active transfer mode'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('ftp', form.Flag, 'default_transfer_binary', _('Enable binary transfer mode'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('ftp', form.Flag, 'enable_active_conn_ip_check', _('Client IP check in active transfer mode'));
|
|
o.default = '1';
|
|
o.rmempty = false;
|
|
|
|
o = addOption('ftp', form.Flag, 'enable_pasv_conn_ip_check', _('Client IP check in passive transfer mode'));
|
|
o.default = '1';
|
|
o.rmempty = false;
|
|
|
|
// sftp
|
|
o = addOption('sftp', form.Flag, 'sftp', _('Enabled SFTP'),
|
|
_('Enable FTP/SFTP access for the OpenList account in Management > Users. Also grant FTP/SFTP management permission to allow uploads and changes.'));
|
|
o.rmempty = false;
|
|
|
|
o = addOption('sftp', form.Value, 'sftp_port', _('SFTP Port'));
|
|
o.datatype = 'and(port,min(1))';
|
|
o.default = 5222;
|
|
o.rmempty = false;
|
|
|
|
// mcp
|
|
o = addOption('mcp', form.Flag, 'mcp', _('Enabled MCP'),
|
|
_('Enable MCP (Model Context Protocol) server.'));
|
|
o.rmempty = false;
|
|
|
|
|
|
o = addOption('mcp', form.DummyValue, '_mcp_endpoint', _('MCP endpoint'),
|
|
_('Use Streamable HTTP with Authorization: TOKEN, without a Bearer prefix. Use an OpenList administrator login token; retain MCP-Session-Id for subsequent requests.'));
|
|
o.depends('mcp', '1');
|
|
o.renderWidget = () => E('div', {}, [
|
|
E('code', {}, backendUrl('mcp') || _('Configure a web port or Site URL.')),
|
|
E('p', {}, E('a', { 'href': 'https://doc.oplist.org/guide/advanced/mcp', 'target': '_blank', 'rel': 'noopener noreferrer' }, _('MCP documentation')))
|
|
]);
|
|
|
|
const validateDirectories = (section_id, changed, value) => {
|
|
const required = changed === 'data_dir' || changed === 'temp_dir';
|
|
const valid = validateAbsolutePath(value, required);
|
|
if (valid !== true)
|
|
return valid;
|
|
const get = (name, fallback) => normalizePath(name === changed ? value || fallback : fieldValue(section_id, name, fallback) || fallback);
|
|
const data = get('data_dir', '/etc/openlist2');
|
|
const cache = get('temp_dir', '/tmp/openlist2');
|
|
const index = get('bleve_dir', data + '/bleve');
|
|
const distValue = changed === 'dist_dir' ? value : fieldValue(section_id, 'dist_dir');
|
|
const dist = distValue ? normalizePath(distValue) : '';
|
|
for (const path of [data, cache, index, dist].filter(Boolean))
|
|
if (isBroadDirectory(path))
|
|
return _('Use a dedicated subdirectory, not a system directory or disk mount root.');
|
|
if (/^\/(?:mnt|media)\/[^/]+$/.test(cache))
|
|
return _('Use a cache subdirectory below the disk mount.');
|
|
if (containsPath(cache, data) || containsPath(index, data) ||
|
|
containsPath(cache, index) || containsPath(index, cache))
|
|
return _('Cache and index directories must be separate and must not contain the data directory.');
|
|
if (dist && (containsPath(dist, data) || containsPath(dist, cache) ||
|
|
containsPath(cache, dist) || containsPath(dist, index) || containsPath(index, dist)))
|
|
return _('Frontend files must be separate from private data, cache and index files.');
|
|
if (tlsRequired(section_id) && !fieldValue(section_id, 'ssl_cert') && !fieldValue(section_id, 'ssl_key')) {
|
|
const tls = data + '/tls';
|
|
for (const directory of [cache, index, dist].filter(Boolean))
|
|
if (containsPath(tls, directory) || containsPath(directory, tls))
|
|
return _('Automatic TLS directory must be separate from cache, index and public frontend directories.');
|
|
}
|
|
return true;
|
|
};
|
|
for (const name of ['data_dir', 'temp_dir', 'bleve_dir', 'dist_dir'])
|
|
fields[name].validate = (section_id, value) => validateDirectories(section_id, name, value);
|
|
|
|
const tlsRequired = section_id => {
|
|
const httpsPort = fieldValue(section_id, 'listen_https_port', '-1');
|
|
const s3Enabled = fieldValue(section_id, 's3') === '1';
|
|
const s3Ssl = fieldValue(section_id, 's3_ssl') === '1';
|
|
return isEnabledPort(httpsPort) || (s3Enabled && s3Ssl);
|
|
};
|
|
const validateCertificatePaths = (section_id, changed, value) => {
|
|
if (!tlsRequired(section_id))
|
|
return true;
|
|
const cert = changed === 'ssl_cert' ? value : fieldValue(section_id, 'ssl_cert');
|
|
const key = changed === 'ssl_key' ? value : fieldValue(section_id, 'ssl_key');
|
|
if (!cert && !key)
|
|
return true;
|
|
if (!cert || !key)
|
|
return _('Leave both certificate paths empty for automatic TLS, or provide both custom certificate and key paths.');
|
|
for (const path of [cert, key]) {
|
|
const valid = validateAbsolutePath(path, true);
|
|
if (valid !== true)
|
|
return valid;
|
|
}
|
|
return true;
|
|
};
|
|
for (const name of ['ssl_cert', 'ssl_key'])
|
|
fields[name].validate = (section_id, value) => validateCertificatePaths(section_id, name, value);
|
|
// LuCI passes input.value (always "1") for checkbox validation.
|
|
// Read formvalue() to distinguish checked and unchecked flags.
|
|
for (const name of ['force_https', 'listen_enable_h3'])
|
|
fields[name].validate = section_id => fieldValue(section_id, name) !== '1' || isEnabledPort(fieldValue(section_id, 'listen_https_port', '-1'))
|
|
? true : _('Force HTTPS and HTTP/3 require an enabled HTTPS port.');
|
|
fields.s3_ssl.validate = section_id => fieldValue(section_id, 's3') !== '1' || fieldValue(section_id, 's3_ssl') !== '1'
|
|
? true : validateCertificatePaths(section_id);
|
|
fields.listen_unix_file.validate = (section_id, value) => validateAbsolutePath(value, false);
|
|
fields.listen_unix_file_perm.validate = (section_id, value) => !value || /^[0-7]{3,4}$/.test(value)
|
|
? true : _('Use 3 or 4 octal digits, for example 660 or 0660.');
|
|
fields.log_max_size.datatype = 'and(uinteger,min(1))';
|
|
|
|
const validatePorts = (section_id, changed, value) => {
|
|
const get = name => name === changed ? value : fieldValue(section_id, name,
|
|
name === 'listen_http_port' ? '5244' : name === 'listen_https_port' ? '-1' : '');
|
|
const names = ['listen_http_port', 'listen_https_port'];
|
|
if (fieldValue(section_id, 's3') === '1') names.push('s3_port');
|
|
if (fieldValue(section_id, 'ftp') === '1') names.push('ftp_port');
|
|
if (fieldValue(section_id, 'sftp') === '1') names.push('sftp_port');
|
|
const seen = new Set();
|
|
const usePassive = changed === 'ftp_pasv_ports' ||
|
|
(fieldValue(section_id, 'ftp') === '1' && fieldValue(section_id, 'allow_wan') === '1');
|
|
const passive = usePassive ? passiveRanges(changed === 'ftp_pasv_ports' ? value : fieldValue(section_id, 'ftp_pasv_ports')) : [];
|
|
const passiveValid = validatePassivePorts(passive);
|
|
if (passiveValid !== true)
|
|
return passiveValid;
|
|
for (const name of names) {
|
|
const candidate = String(get(name)).trim();
|
|
const valid = validateListenPort(candidate);
|
|
if (valid !== true)
|
|
return valid;
|
|
if (candidate === '-1')
|
|
continue;
|
|
const port = Number(candidate);
|
|
if (seen.has(port))
|
|
return _('Enabled services cannot share TCP port %s.').format(port);
|
|
seen.add(port);
|
|
if (fieldValue(section_id, 'ftp') === '1')
|
|
for (const range of passive) {
|
|
const [first, last = first] = range.split('-').map(Number);
|
|
if (port >= first && port <= last)
|
|
return _('FTP passive ports must not overlap service port %s.').format(port);
|
|
}
|
|
}
|
|
if (!isEnabledPort(get('listen_https_port')) &&
|
|
(fieldValue(section_id, 'force_https') === '1' || fieldValue(section_id, 'listen_enable_h3') === '1'))
|
|
return _('Force HTTPS and HTTP/3 require an enabled HTTPS port.');
|
|
return true;
|
|
};
|
|
for (const name of ['listen_http_port', 'listen_https_port', 's3_port', 'ftp_port', 'sftp_port', 'ftp_pasv_ports'])
|
|
fields[name].validate = (section_id, value) => validatePorts(section_id, name, value);
|
|
const protocolFields = ['listen_http_port', 'listen_https_port', 'force_https', 'listen_enable_h3',
|
|
'ssl_cert', 'ssl_key', 's3', 's3_ssl', 's3_port', 'ftp', 'ftp_port', 'sftp', 'sftp_port',
|
|
'ftp_pasv_ports', 'allow_wan', 'data_dir', 'temp_dir', 'bleve_dir', 'dist_dir'];
|
|
const revalidateProtocolFields = (event, section_id) => {
|
|
for (const name of protocolFields)
|
|
if (fields[name].isActive(section_id))
|
|
fields[name].triggerValidation(section_id);
|
|
ui.tabs.updateTabs(event, m.root);
|
|
};
|
|
for (const name of protocolFields)
|
|
fields[name].onchange = revalidateProtocolFields;
|
|
for (const name of ['ftp_pasv_ports', 'log_path', 'log_max_size', 'log_max_backups', 'log_max_age',
|
|
'log_compress', 'log_filter', 'log_filter_cidr', 'log_filter_path', 'log_filter_method'])
|
|
fields[name].retain = true;
|
|
|
|
return m.render();
|
|
}
|
|
});
|