'use strict';
'require ui';
'require view';
'require dom';
'require poll';
'require uci';
'require rpc';
'require fs';
'require form';
'require network';
'require tools.widgets as widgets';
return view.extend({
NextUpdateStrings : {
'Verify' : _("Verify"),
'Run once' : _("Run once"),
'Disabled' : _("Disabled"),
'Stopped' : _("Stopped")
},
time_res : {
seconds : 1,
minutes : 60,
hours : 3600,
},
callGetLogServices: rpc.declare({
object: 'luci.ddns',
method: 'get_services_log',
params: [ 'service_name' ],
expect: { },
}),
callInitAction: rpc.declare({
object: 'luci',
method: 'setInitAction',
params: [ 'name', 'action' ],
expect: { result: false }
}),
callDDnsGetStatus: rpc.declare({
object: 'luci.ddns',
method: 'get_ddns_state',
expect: { }
}),
callDDnsGetEnv: rpc.declare({
object: 'luci.ddns',
method: 'get_env',
expect: { }
}),
callDDnsGetServicesStatus: rpc.declare({
object: 'luci.ddns',
method: 'get_services_status',
expect: { }
}),
services: {},
/*
* Services list is generated by 3 different sources:
* 1. /usr/share/ddns/default contains the service installed by package-manager
* 2. /usr/share/ddns/custom contains any service installed by the
* user or the ddns script (for example when services are
* downloaded)
* 3. /usr/share/ddns/list contains all the services that can be
* downloaded by using the ddns script ('service on demand' feature)
*
* (Special services that requires a dedicated package ARE NOT
* supported by the 'service on demand' feature)
*/
callGenServiceList: function(m, ev) {
return Promise.all([
L.resolveDefault(fs.list('/usr/share/ddns/default'), []),
L.resolveDefault(fs.list('/usr/share/ddns/custom'), []),
L.resolveDefault(fs.read('/usr/share/ddns/list'), null)
]).then(L.bind(function (data) {
var default_service = data[0],
custom_service = data[1],
list_service = data[2] && data[2].split("\n") || [],
_this = this;
this.services = {};
default_service.forEach(function (service) {
_this.services[service.name.replace('.json','')] = true
});
custom_service.forEach(function (service) {
_this.services[service.name.replace('.json','')] = true
});
this.services = Object.fromEntries(Object.entries(this.services).sort());
list_service.forEach(function (service) {
if (!_this.services[service])
_this.services[service] = false;
});
}, this))
},
/*
* Figure out what the wan interface on the device is.
* Determine if the physical device exist, or if we should use an alias.
*/
callGetWanInterface: function(m, ev) {
return network.getDevice('wan').then(dev => dev.getName())
.catch(err => network.getNetwork('wan').then(net => '@' + net.getName()))
.catch(err => null);
},
/*
* Check whether or not the service is supported.
* If the script doesn't find any JSON, assume a 'service on demand' install.
* If a JSON is found, check if the IP type is supported.
* Invalidate the service_name if it is not supported.
*/
handleCheckService : function(s, service_name, ipv6, ev, section_id) {
var value = service_name.formvalue(section_id);
s.service_supported = null;
service_name.triggerValidation(section_id);
return this.handleGetServiceData(value)
.then(L.bind(function (service_data) {
if (value != '-' && service_data) {
service_data = JSON.parse(service_data);
if (ipv6.formvalue(section_id) == "1" && !service_data.ipv6) {
s.service_supported = false;
return;
}
}
s.service_supported = true;
}, service_name))
.then(L.bind(service_name.triggerValidation, service_name, section_id))
},
handleGetServiceData: function(service) {
return Promise.all([
L.resolveDefault(fs.read('/usr/share/ddns/custom/'+service+'.json'), null),
L.resolveDefault(fs.read('/usr/share/ddns/default/'+service+'.json'), null)
]).then(function(data) {
return data[0] || data[1] || null;
})
},
handleInstallService: function(m, service_name, section_id, section, _this, ev) {
var service = service_name.formvalue(section_id)
return fs.exec('/usr/bin/ddns', ['service', 'install', service])
.then(L.bind(_this.callGenServiceList, _this))
.then(L.bind(m.render, m))
.then(L.bind(this.renderMoreOptionsModal, this, section))
.catch(function(e) { ui.addNotification(null, E('p', e.message)) });
},
handleRefreshServicesList: function(m, ev) {
return fs.exec('/usr/bin/ddns', ['service', 'update'])
.then(L.bind(this.load, this))
.then(L.bind(this.render, this))
.catch(function(e) { ui.addNotification(null, E('p', e.message)) });
},
handleReloadDDnsRule: function(m, section_id, ev) {
return fs.exec('/usr/lib/ddns/dynamic_dns_lucihelper.sh',
[ '-S', section_id, '--', 'start' ])
.then(L.bind(m.load, m))
.then(L.bind(m.render, m))
.catch(function(e) { ui.addNotification(null, E('p', e.message)) });
},
HandleStopDDnsRule: function(m, section_id, ev) {
return fs.exec('/usr/lib/ddns/dynamic_dns_lucihelper.sh',
[ '-S', section_id, '--', 'start' ])
.then(L.bind(m.render, m))
.catch(function(e) { ui.addNotification(null, E('p', e.message)) });
},
handleToggleDDns: function(m, ev) {
return this.callInitAction('ddns', 'enable')
.then(L.bind(function (action) { return this.callInitAction('ddns', action ? 'disable' : 'enable')}, this))
.then(L.bind(function (action) { return this.callInitAction('ddns', action ? 'stop' : 'start')}, this))
.then(L.bind(m.render, m))
.catch(function(e) { ui.addNotification(null, E('p', e.message)) });
},
handleRestartDDns: function(m, ev) {
return this.callInitAction('ddns', 'restart')
.then(L.bind(m.render, m));
},
poll_status: function(map, data) {
var status = data[1] || [], service = data[0] || [], rows = map.querySelectorAll('.cbi-section-table-row[data-sid]'),
ddns_enabled = map.querySelector('[data-name="_enabled"]').querySelector('.cbi-value-field'),
ddns_toggle = map.querySelector('[data-name="_toggle"]').querySelector('button'),
services_list = map.querySelector('[data-name="_services_list"]').querySelector('.cbi-value-field');
ddns_toggle.innerHTML = status['_enabled'] ? _('Stop DDNS') : _('Start DDNS')
services_list.innerHTML = status['_services_list'];
dom.content(ddns_enabled, function() {
return E([], [
E('div', {}, status['_enabled'] ? _('DDNS Autostart enabled') : [
_('DDNS Autostart disabled'),
E('div', { 'class' : 'cbi-value-description' },
_("Currently DDNS updates are not started at boot or on interface events.") + "
" +
_("This is the default if you run DDNS scripts by yourself (i.e. via cron with force_interval set to '0')"))
]),]);
});
for (var i = 0; i < rows.length; i++) {
const section_id = rows[i].getAttribute('data-sid');
const cfg_detail_ip = rows[i].querySelector('[data-name="_cfg_detail_ip"]');
const cfg_update = rows[i].querySelector('[data-name="_cfg_update"]');
const cfg_status = rows[i].querySelector('[data-name="_cfg_status"]');
const reload = rows[i].querySelector('.cbi-section-actions .reload');
const stop = rows[i].querySelector('.cbi-section-actions .stop');
const cfg_enabled = uci.get('ddns', section_id, 'enabled');
reload.disabled = (status['_enabled'] == 0 || cfg_enabled == 0);
stop.disabled = (!service[section_id].pid);
const host = uci.get('ddns', section_id, 'lookup_host') || _('Configuration Error');
const ip = service[section_id]?.ip || _('No Data');
const last_update = service[section_id]?.last_update || _('Never');
const next_update = this.NextUpdateStrings[service[section_id]?.next_update] || service[section_id]?.next_update || _('Unknown');
const next_check = service[section_id]?.next_check || _('Unknown');
const service_status = service[section_id]?.pid ? '' + _('Running') + ' : ' + service[section_id]?.pid : '' + _('Not Running') + '';
cfg_detail_ip.innerHTML = host + '
' + ip;
cfg_update.innerHTML = last_update + '
' + next_check + '
' + next_update ;
cfg_status.innerHTML = service_status;
}
return;
},
load: function() {
return Promise.all([
this.callDDnsGetServicesStatus(),
this.callDDnsGetStatus(),
this.callDDnsGetEnv(),
this.callGenServiceList(),
uci.load('ddns'),
this.callGetWanInterface()
]);
},
render: function(data) {
var resolved = data[0] || [];
var status = data[1] || [];
var env = data[2] || [];
var logdir = uci.get('ddns', 'global', 'ddns_logdir') || "/var/log/ddns";
var wan_interface = data[5];
var _this = this;
let m, s, o;
m = new form.Map('ddns', _('Dynamic DNS'));
s = m.section(form.NamedSection, 'global', 'ddns',);
s.tab('info', _('Information'));
s.tab('global', _('Global Settings'));
o = s.taboption('info', form.DummyValue, '_version', _('Dynamic DNS Version'));
o.cfgvalue = function() {
return status[this.option];
};
o = s.taboption('info', form.DummyValue, '_enabled', _('State'));
o.cfgvalue = function() {
var res = status[this.option];
if (!res) {
this.description = _("Currently DDNS updates are not started at boot or on interface events.") + "
" +
_("This is the default if you run DDNS scripts by yourself (i.e. via cron with force_interval set to '0')")
}
return res ? _('DDNS Autostart enabled') : _('DDNS Autostart disabled')
};
o = s.taboption('info', form.Button, '_toggle');
o.title = ' ';
o.inputtitle = _((status['_enabled'] ? 'stop' : 'start').toUpperCase() + ' DDns');
o.inputstyle = 'apply';
o.onclick = L.bind(this.handleToggleDDns, this, m);
o = s.taboption('info', form.Button, '_restart');
o.title = ' ';
o.inputtitle = _('Restart DDns');
o.inputstyle = 'apply';
o.onclick = L.bind(this.handleRestartDDns, this, m);
o = s.taboption('info', form.DummyValue, '_services_list', _('Services list last update'));
o.cfgvalue = function() {
return status[this.option];
};
o = s.taboption('info', form.Button, '_refresh_services');
o.title = ' ';
o.inputtitle = _('Update DDns Services List');
o.inputstyle = 'apply';
o.onclick = L.bind(this.handleRefreshServicesList, this, m);
// DDns hints
if (!env['has_ipv6']) {
o = s.taboption('info', form.DummyValue, '_no_ipv6');
o.rawhtml = true;
o.title = '' + _("IPv6 not supported") + '';
o.cfgvalue = function() { return _("IPv6 is not supported by this system") + "
" +
_("Please follow the instructions on OpenWrt's homepage to enable IPv6 support") + "
" +
_("or update your system to the latest OpenWrt Release")};
}
if (!env['has_ssl']) {
o = s.taboption('info', form.DummyValue, '_no_https');
o.titleref = L.url("admin", "system", "package-manager")
o.rawhtml = true;
o.title = '' + _("HTTPS not supported") + '';
o.cfgvalue = function() { return _("Neither GNU Wget with SSL nor cURL is installed to support secure updates via HTTPS protocol.") +
"
- " +
_("You should install 'wget' or 'curl' or 'uclient-fetch' with 'libustream-*ssl' package.") +
"
- " +
_("In some versions cURL/libcurl in OpenWrt is compiled without proxy support.")};
}
if (!env['has_bindnet']) {
o = s.taboption('info', form.DummyValue, '_no_bind_network');
o.titleref = L.url("admin", "system", "package-manager")
o.rawhtml = true;
o.title = '' + _("Binding to a specific network not supported") + '';
o.cfgvalue = function() { return _("Neither GNU Wget with SSL nor cURL is installed to select a network to use for communication.") +
"
- " +
_("This is only a problem with multiple WAN interfaces and your DDNS provider is unreachable via one of them.") +
"
- " +
_("You should install 'wget' or 'curl' package.") +
"
- " +
_("GNU Wget will use the IP of given network, cURL will use the physical interface.") +
"
- " +
_("In some versions cURL/libcurl in OpenWrt is compiled without proxy support.")};
}
if (!env['has_proxy']) {
o = s.taboption('info', form.DummyValue, '_no_proxy');
o.titleref = L.url("admin", "system", "package-manager")
o.rawhtml = true;
o.title = '' + _("cURL without Proxy Support") + '';
o.cfgvalue = function() { return _("cURL is installed, but libcurl was compiled without proxy support.") +
"
- " +
_("You should install 'wget' or 'uclient-fetch' package or replace libcurl.") +
"
- " +
_("In some versions cURL/libcurl in OpenWrt is compiled without proxy support.")};
}
if (!env['has_bindhost']) {
o = s.taboption('info', form.DummyValue, '_no_dnstcp');
o.titleref = L.url("admin", "system", "package-manager")
o.rawhtml = true;
o.title = '' + _("DNS requests via TCP not supported") + '';
o.cfgvalue = function() { return _("BusyBox's nslookup and hostip do not support TCP " +
"instead of the default UDP when sending requests to the DNS server!") +
"
- " +
_("Install 'bind-host' or 'knot-host' or 'drill' package if you know you need TCP for DNS requests.")};
}
if (!env['has_dnsserver']) {
o = s.taboption('info', form.DummyValue, '_no_dnsserver');
o.titleref = L.url("admin", "system", "package-manager")
o.rawhtml = true;
o.title = '' + _("Using specific DNS Server not supported") + '';
o.cfgvalue = function() { return _("BusyBox's nslookup in the current compiled version " +
"does not handle given DNS Servers correctly!") +
"
- " +
_("You should install 'bind-host' or 'knot-host' or 'drill' or 'hostip' package, " +
"if you need to specify a DNS server to detect your registered IP.")};
}
if (env['has_ssl'] && !env['has_cacerts']) {
o = s.taboption('info', form.DummyValue, '_no_certs');
o.titleref = L.url("admin", "system", "package-manager")
o.rawhtml = true;
o.title = '' + _("No certificates found") + '';
o.cfgvalue = function() { return _("If using secure communication you should verify server certificates!") +
"
- " +
_("Install 'ca-certificates' package or needed certificates " +
"by hand into /etc/ssl/certs default directory")};
}
// Advanced Configuration Section
o = s.taboption('global', form.Flag, 'upd_privateip', _("Allow non-public IPs"));
o.description = _("Non-public and by default blocked IPs") + ':'
+ '
IPv4: '
+ '0/8, 10/8, 100.64/10, 127/8, 169.254/16, 172.16/12, 192.168/16'
+ '
IPv6: '
+ '::/32, f000::/4';
o.default = "0";
o.optional = true;
o = s.taboption('global', form.Value, 'ddns_dateformat', _('Date format'));
o.description = ''
+ _("For supported codes look here")
+ '
' +
_('Current setting: ') + '' + status['_curr_dateformat'] + '';
o.default = "%F %R"
o.optional = true;
o.rmempty = true;
o = s.taboption('global', form.Value, 'ddns_rundir', _('Status directory'));
o.description = _('Contains PID and other status information for each running section.');
o.default = "/var/run/ddns";
o.optional = true;
o.rmempty = true;
o = s.taboption('global', form.Value, 'ddns_logdir', _('Log directory'));
o.description = _('Contains Log files for each running section.');
o.default = "/var/log/ddns";
o.optional = true;
o.rmempty = true;
o.validate = function(section_id, formvalue) {
if (formvalue.indexOf('../') !== -1)
return _('"../" not allowed in path for Security Reason.')
return true;
}
o = s.taboption('global', form.Value, 'ddns_loglines', _('Log length'));
o.description = _('Number of last lines stored in log files');
o.datatype = 'min(1)';
o.default = '250';
if (env['has_wget'] && env['has_curl']) {
o = s.taboption('global', form.Flag, 'use_curl', _('Use cURL'));
o.description = _('If Wget and cURL package are installed, Wget is used for communication by default.');
o.default = "0";
o.optional = true;
o.rmempty = true;
}
o = s.taboption('global', form.Value, 'cacert', _('CA cert bundle file'));
o.description = _('CA certificate bundle file that will be used to download services data. Set IGNORE to skip certificate validation.');
o.placeholder = 'IGNORE';
o.write = function(section_id, value) {
uci.set('ddns', section_id, 'cacert', value == 'ignore' ? value.toUpperCase() : value);
};
o = s.taboption('global', form.Value, 'services_url', _('Services URL Download'));
o.description = _('Source URL for services file. Defaults to the master openwrt ddns package repo.');
o.placeholder = 'https://raw.githubusercontent.com/openwrt/packages/master/net/ddns-scripts/files';
// DDns services
s = m.section(form.GridSection, 'service', _('Services'));
s.anonymous = true;
s.addremove = true;
s.addbtntitle = _('Add new services...');
s.sortable = true;
s.handleCreateDDnsRule = function(m, name, service_name, ipv6, ev) {
var section_id = name.isValid('_new_') ? name.formvalue('_new_') : null,
service_value = service_name.isValid('_new_') ? service_name.formvalue('_new_') : null,
ipv6_value = ipv6.isValid('_new_') ? ipv6.formvalue('_new_') : null;
if (!section_id || !service_value || !ipv6_value)
return;
return m.save(function() {
uci.add('ddns', 'service', section_id);
if (service_value != '-') {
uci.set('ddns', section_id, 'service_name', service_value);
}
uci.set('ddns', section_id, 'use_ipv6', ipv6_value);
ui.hideModal();
}).then(L.bind(m.children[1].renderMoreOptionsModal, m.children[1], section_id));
};
s.handleAdd = function(ev) {
var m2 = new form.Map('ddns'),
s2 = m2.section(form.NamedSection, '_new_'),
name, ipv6, service_name;
s2.render = function() {
return Promise.all([
{},
this.renderUCISection('_new_')
]).then(this.renderContents.bind(this));
};
name = s2.option(form.Value, 'name', _('Name'));
name.rmempty = false;
name.datatype = 'uciname';
name.placeholder = _('New DDns Service…');
name.validate = function(section_id, value) {
if (uci.get('ddns', value) != null)
return _('The service name is already used');
return true;
};
ipv6 = s2.option( form.ListValue, 'use_ipv6',
_("IP address version"),
_("Which record type to update at the DDNS provider (A/AAAA)"));
ipv6.default = '0';
ipv6.value("0", _("IPv4-Address"))
if (env["has_ipv6"]) {
ipv6.value("1", _("IPv6-Address"))
}
service_name = s2.option(form.ListValue, 'service_name',
String.format('%s', _("DDNS Service provider")));
service_name.value('-',"📝 " + _("custom") );
Object.keys(_this.services).sort().forEach(name => service_name.value(name));
service_name.validate = function(section_id, value) {
if (value == '-') return true;
if (!value) return _("Select a service");
if (!s2.service_supported) return _("Service doesn't support this IP type");
return true;
};
ipv6.onchange = L.bind(_this.handleCheckService, _this, s2, service_name, ipv6);
service_name.onchange = L.bind(_this.handleCheckService, _this, s2, service_name, ipv6);
m2.render().then(L.bind(function(nodes) {
ui.showModal(_('Add new services...'), [
nodes,
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Cancel')), ' ',
E('button', {
'class': 'cbi-button cbi-button-positive important',
'click': ui.createHandlerFn(this, 'handleCreateDDnsRule', m, name, service_name, ipv6)
}, _('Create service'))
])
], 'cbi-modal');
nodes.querySelector('[id="%s"] input[type="text"]'.format(name.cbid('_new_'))).focus();
}, this));
};
s.renderRowActions = function(section_id) {
var tdEl = this.super('renderRowActions', [ section_id, _('Edit') ]),
cfg_enabled = uci.get('ddns', section_id, 'enabled'),
reload_opt = {
'class': 'cbi-button cbi-button-neutral reload',
'click': ui.createHandlerFn(_this, 'handleReloadDDnsRule', m, section_id),
'title': _('Reload this service'),
},
stop_opt = {
'class': 'cbi-button cbi-button-neutral stop',
'click': ui.createHandlerFn(_this, 'HandleStopDDnsRule', m, section_id),
'title': _('Stop this service'),
};
if (status['_enabled'] == 0 || cfg_enabled == 0)
reload_opt['disabled'] = 'disabled';
if (!resolved[section_id] || !resolved[section_id].pid ||
(resolved[section_id].pid && cfg_enabled == '1'))
stop_opt['disabled'] = 'disabled';
dom.content(tdEl.lastChild, [
E('button', stop_opt, _('Stop')),
E('button', reload_opt, _('Reload')),
tdEl.lastChild.childNodes[0],
tdEl.lastChild.childNodes[1],
tdEl.lastChild.childNodes[2]
]);
return tdEl;
};
s.modaltitle = function(section_id) {
return _('DDns Service') + ' » ' + section_id;
};
s.addModalOptions = function(s, section_id) {
var service = uci.get('ddns', section_id, 'service_name') || '-',
ipv6 = uci.get('ddns', section_id, 'use_ipv6'), service_name, use_ipv6;
return _this.handleGetServiceData(service).then(L.bind(function (service_data) {
s.service_available = true;
s.service_supported = true;
s.url = null;
if (service != '-') {
if (!service_data)
s.service_available = false;
else {
service_data = JSON.parse(service_data);
if (ipv6 == "1" && !service_data.ipv6)
s.service_supported = false;
else if (ipv6 == "1") {
s.url = service_data.ipv6.url;
} else {
s.url = service_data.ipv4.url;
}
}
}
s.tab('basic', _('Basic Settings'));
s.tab('advanced', _('Advanced Settings'));
s.tab('timer', _('Timer Settings'));
s.tab('logview', _('Log File Viewer'));
o = s.taboption('basic', form.Flag, 'enabled',
_('Enabled'),
_("If this service section is disabled it will not be started.")
+ "
" +
_("Neither from LuCI interface nor from console."));
o.modalonly = true;
o.rmempty = false;
o.default = '1';
o = s.taboption('basic', form.Value, 'lookup_host',
_("Lookup Hostname"),
_("Hostname/FQDN to validate, whether an IP update is necessary"));
o.rmempty = false;
o.placeholder = "myhost.example.com";
o.datatype = 'and(minlength(3),hostname("strict"))';
o.modalonly = true;
use_ipv6 = s.taboption('basic', form.ListValue, 'use_ipv6',
_("IP address version"),
_("Which record type to update at the DDNS provider (A/AAAA)"));
use_ipv6.default = '0';
use_ipv6.modalonly = true;
use_ipv6.rmempty = false;
use_ipv6.value("0", _("IPv4-Address"))
if (env["has_ipv6"]) {
use_ipv6.value("1", _("IPv6-Address"))
}
service_name = s.taboption('basic', form.ListValue, 'service_name',
String.format('%s', _("DDNS Service provider")));
service_name.modalonly = true;
service_name.value('-',"📝 " + _("custom") );
Object.keys(_this.services).sort().forEach(name => service_name.value(name));
service_name.cfgvalue = function(section_id) {
return uci.get('ddns', section_id, 'service_name') || '-';
};
service_name.write = function(section_id, service) {
if (service != '-') {
uci.unset('ddns', section_id, 'update_url');
uci.unset('ddns', section_id, 'update_script');
return uci.set('ddns', section_id, 'service_name', service);
}
return uci.unset('ddns', section_id, 'service_name');
};
service_name.validate = function(section_id, value) {
if (value == '-') return true;
if (!value) return _("Select a service");
if (!s.service_available) return _('Service not installed');
if (!s.service_supported) return _("Service doesn't support this IP type");
return true;
};
service_name.onchange = L.bind(_this.handleCheckService, _this, s, service_name, use_ipv6);
use_ipv6.onchange = L.bind(_this.handleCheckService, _this, s, service_name, use_ipv6);
if (!s.service_available) {
o = s.taboption('basic', form.Button, '_download_service');
o.modalonly = true;
o.title = _('Service not installed');
o.inputtitle = _('Install Service');
o.inputstyle = 'apply';
o.onclick = L.bind(_this.handleInstallService,
this, m, service_name, section_id, s.section, _this)
}
if (!s.service_supported) {
o = s.taboption('basic', form.DummyValue, '_not_supported', ' ');
o.cfgvalue = function () {
return _("Service doesn't support this IP type")
};
}
if (Boolean(s.url)) {
o = s.taboption('basic', form.DummyValue, '_url', _("Update URL"));
o.rawhtml = true;
o.default = '