Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eeddc233dd | |||
| f48690ee11 | |||
| 3b0bdd9807 | |||
| 6dc5c4f13b | |||
| 9bb5096f1c | |||
| af42008fd3 | |||
| d6875d4949 | |||
| 4396bd3ea7 | |||
| db03053e05 | |||
| 193dff8c30 | |||
| 59582d081a | |||
| ef684d32a2 | |||
| fc2a326332 | |||
| e66a804012 | |||
| 5afa7345a5 | |||
| c100be4131 | |||
| d326d05ad6 | |||
| eb0662b245 | |||
| b92641d1b8 | |||
| 7912d521ca | |||
| 71dac64c4c | |||
| aab6f222b3 | |||
| 1cb1be321c |
@@ -1,5 +1,6 @@
|
||||
node_modules/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
webadmin/dist/
|
||||
installer/src/certs/server.key
|
||||
|
||||
|
||||
@@ -2435,3 +2435,17 @@
|
||||
* eventlog: log event for mailbox alias update
|
||||
* backups: fix incorrect mountpoint check with managed mounts
|
||||
|
||||
[7.1.3]
|
||||
* Fix security issue where an admin can impersonate an owner
|
||||
* block list: can upload up to 2MB
|
||||
* dns: fix issue where link local address was picked up for ipv6
|
||||
* setup: ufw may not be installed
|
||||
* mysql: fix default collation of databases
|
||||
|
||||
[7.1.4]
|
||||
* wildcard dns: fix handling of ENODATA
|
||||
* cloudflare: fix error handling
|
||||
* openvpn: ipv6 support
|
||||
* dyndns: fix issue where eventlog was getting filled with empty entries
|
||||
* mandatory 2fa: Fix typo in 2FA check
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ systemctl stop systemd-resolved || true
|
||||
systemctl disable systemd-resolved || true
|
||||
|
||||
# on vultr, ufw is enabled by default. we have our own firewall
|
||||
ufw disable
|
||||
ufw disable || true
|
||||
|
||||
# we need unbound to work as this is required for installer.sh to do any DNS requests
|
||||
echo -e "server:\n\tinterface: 127.0.0.1\n\tdo-ip6: no" > /etc/unbound/unbound.conf.d/cloudron-network.conf
|
||||
|
||||
Generated
+1963
-57
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -37,7 +37,7 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"json": "^11.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"ldapjs": "^2.3.1",
|
||||
"ldapjs": "^2.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"moment": "^2.29.1",
|
||||
@@ -77,6 +77,7 @@
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^13.2.1",
|
||||
"node-sass": "^7.0.1",
|
||||
"nyc": "^15.1.0",
|
||||
"recursive-readdir": "^2.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -79,10 +79,15 @@ echo "=> Run database migrations"
|
||||
cd "${source_dir}"
|
||||
BOX_ENV=test DATABASE_URL=mysql://root:password@${MYSQL_IP}/box node_modules/.bin/db-migrate up
|
||||
|
||||
echo "=> Run tests with mocha"
|
||||
TESTS=${DEFAULT_TESTS}
|
||||
if [[ $# -gt 0 ]]; then
|
||||
TESTS="$*"
|
||||
fi
|
||||
|
||||
BOX_ENV=test ./node_modules/mocha/bin/_mocha --bail --no-timeouts --exit -R spec ${TESTS}
|
||||
if [[ -z ${COVERAGE+x} ]]; then
|
||||
echo "=> Run tests with mocha"
|
||||
BOX_ENV=test ./node_modules/.bin/mocha --bail --no-timeouts --exit -R spec ${TESTS}
|
||||
else
|
||||
echo "=> Run tests with mocha and coverage"
|
||||
BOX_ENV=test ./node_modules/.bin/nyc --reporter=html ./node_modules/.bin/mocha --no-timeouts --exit -R spec ${TESTS}
|
||||
fi
|
||||
|
||||
+1
-1
@@ -324,7 +324,7 @@ Acme2.prototype.createKeyAndCsr = async function (hostname, keyFilePath, csrFile
|
||||
if (!csrDer) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
|
||||
if (!safe.fs.writeFileSync(csrFilePath, csrDer)) throw new BoxError(BoxError.FS_ERROR, safe.error); // bookkeeping. inspect with openssl req -text -noout -in hostname.csr -inform der
|
||||
|
||||
await safe(fs.promises.rmdir(tmpdir, { recursive: true }));
|
||||
await safe(fs.promises.rm(tmpdir, { recursive: true, force: true }));
|
||||
|
||||
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFilePath);
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ const apps = require('./apps.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
superagent = require('superagent'),
|
||||
support = require('./support.js'),
|
||||
util = require('util');
|
||||
@@ -379,6 +380,7 @@ async function createTicket(info, auditSource) {
|
||||
|
||||
if (info.enableSshSupport) {
|
||||
await safe(support.enableRemoteSupport(true, auditSource));
|
||||
info.ipv4 = await sysinfo.getServerIPv4();
|
||||
}
|
||||
|
||||
info.app = info.appId ? await apps.get(info.appId) : null;
|
||||
|
||||
+1
-1
@@ -1042,7 +1042,7 @@ async function fullBackup(options, progressCallback) {
|
||||
percent += step;
|
||||
|
||||
if (!app.enableBackup) {
|
||||
debug(`fullBackup: skipped backup ${app.fqdn} (${i+1}/${allApps.length})`);
|
||||
debug(`fullBackup: skipped backup ${app.fqdn} (${i+1}/${allApps.length}) since automatic backup disabled`);
|
||||
continue; // nothing to backup
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ async function startJobs() {
|
||||
|
||||
const randomTick = Math.floor(60*Math.random());
|
||||
gJobs.systemChecks = new CronJob({
|
||||
cronTime: '00 30 2 * * *', // once a day. if you change this interval, change the notification messages with correct duration
|
||||
cronTime: `${randomTick} ${randomTick} 2 * * *`, // once a day. if you change this interval, change the notification messages with correct duration
|
||||
onTick: async () => await safe(cloudron.runSystemChecks(), { debug }),
|
||||
start: true
|
||||
});
|
||||
|
||||
+2
-3
@@ -34,8 +34,7 @@ async function resolve(hostname, rrtype, options) {
|
||||
if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT';
|
||||
if (error) throw error;
|
||||
|
||||
// result is an empty array if there was no error but there is no record. when you query a random
|
||||
// domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different
|
||||
// type (CNAME) it is not an error and empty array. for TXT records, result is 2d array of strings
|
||||
// when you query a random record, it errors with ENOTFOUND. But, if you query a record which has a different type
|
||||
// we sometimes get empty array and sometimes ENODATA. for TXT records, result is 2d array of strings
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
async function translateRequestError(result) {
|
||||
function translateRequestError(result) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
|
||||
if (result.statusCode === 404) return new BoxError(BoxError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist'));
|
||||
|
||||
@@ -255,6 +255,9 @@ async function verifyDomainConfig(domainObject) {
|
||||
await upsert(newDomainObject, location, 'A', [ ip ]);
|
||||
debug('verifyDomainConfig: Test A record added');
|
||||
|
||||
await get(newDomainObject, location, 'A');
|
||||
debug('verifyDomainConfig: Can list record sets');
|
||||
|
||||
await del(newDomainObject, location, 'A', [ ip ]);
|
||||
debug('verifyDomainConfig: Test A record removed again');
|
||||
|
||||
|
||||
+6
-4
@@ -80,8 +80,9 @@ async function verifyDomainConfig(domainObject) {
|
||||
const fqdn = dns.fqdn(location, domainObject);
|
||||
|
||||
const [ipv4Error, ipv4Result] = await safe(dig.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 }));
|
||||
if (ipv4Error && ipv4Error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv4 of ${fqdn}. Please check if you have set up *.${domainObject.domain} to point to this server's IP`);
|
||||
if (ipv4Error || !ipv4Result) throw new BoxError(BoxError.BAD_FIELD, ipv4Error ? ipv4Error.message : `Unable to resolve IPv4 of ${fqdn}`);
|
||||
if (ipv4Error && (ipv4Error.code === 'ENOTFOUND' || ipv4Error.code === 'ENODATA')) throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv4 of ${fqdn}. Please check if you have set up *.${domainObject.domain} to point to this server's IP`);
|
||||
if (ipv4Error) throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv4 of ${fqdn}: ${ipv4Error.message}`);
|
||||
if (!ipv4Result) throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv4 of ${fqdn}`);
|
||||
|
||||
const ipv4 = await sysinfo.getServerIPv4();
|
||||
if (ipv4Result.length !== 1 || ipv4 !== ipv4Result[0]) throw new BoxError(BoxError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(ipv4Result)} instead of IPv4 ${ipv4}`);
|
||||
@@ -89,8 +90,9 @@ async function verifyDomainConfig(domainObject) {
|
||||
const ipv6 = await sysinfo.getServerIPv6(); // both should be RFC 5952 format
|
||||
if (ipv6) {
|
||||
const [ipv6Error, ipv6Result] = await safe(dig.resolve(fqdn, 'AAAA', { server: '127.0.0.1', timeout: 5000 }));
|
||||
if (ipv6Error && ipv6Error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv6 of ${fqdn}`);
|
||||
if (ipv6Error || !ipv6Result) throw new BoxError(BoxError.BAD_FIELD, ipv6Error ? ipv6Error.message : `Unable to resolve IPv6 of ${fqdn}`);
|
||||
if (ipv6Error && (ipv6Error.code === 'ENOTFOUND' || ipv6Error.code === 'ENODATA')) throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv6 of ${fqdn}`);
|
||||
if (ipv6Error) throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv6 of ${fqdn}: ${ipv6Error.message}`);
|
||||
if (!ipv6Result) throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv6 of ${fqdn}`);
|
||||
|
||||
if (ipv6Result.length !== 1 || ipv6 !== ipv6Result[0]) throw new BoxError(BoxError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(ipv6Result)} instead of IPv6 ${ipv6}`);
|
||||
}
|
||||
|
||||
+10
-4
@@ -247,12 +247,12 @@ function getAddresses() {
|
||||
|
||||
const addresses = [];
|
||||
for (const phy of physicalDevices) {
|
||||
const inet = safe.JSON.parse(safe.child_process.execSync(`ip -f inet -j addr show ${phy.name}`, { encoding: 'utf8' }));
|
||||
const inet = safe.JSON.parse(safe.child_process.execSync(`ip -f inet -j addr show dev ${phy.name} scope global`, { encoding: 'utf8' }));
|
||||
for (const r of inet) {
|
||||
const address = safe.query(r, 'addr_info[0].local');
|
||||
if (address) addresses.push(address);
|
||||
}
|
||||
const inet6 = safe.JSON.parse(safe.child_process.execSync(`ip -f inet6 -j addr show ${phy.name}`, { encoding: 'utf8' }));
|
||||
const inet6 = safe.JSON.parse(safe.child_process.execSync(`ip -f inet6 -j addr show dev ${phy.name} scope global`, { encoding: 'utf8' }));
|
||||
for (const r of inet6) {
|
||||
const address = safe.query(r, 'addr_info[0].local');
|
||||
if (address) addresses.push(address);
|
||||
@@ -355,7 +355,8 @@ async function createSubcontainer(app, name, cmd, options) {
|
||||
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
|
||||
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
|
||||
CapAdd: [],
|
||||
CapDrop: []
|
||||
CapDrop: [],
|
||||
Sysctls: {}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -388,7 +389,12 @@ async function createSubcontainer(app, name, cmd, options) {
|
||||
const capabilities = manifest.capabilities || [];
|
||||
|
||||
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
|
||||
if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
|
||||
if (capabilities.includes('net_admin')) {
|
||||
containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
|
||||
// ipv6 for new interfaces is disabled in the container. this prevents the openvpn tun device having ipv6
|
||||
// See https://github.com/moby/moby/issues/20569 and https://github.com/moby/moby/issues/33099
|
||||
containerOptions.HostConfig.Sysctls['net.ipv6.conf.all.disable_ipv6'] = '0';
|
||||
}
|
||||
if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping
|
||||
if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker
|
||||
|
||||
|
||||
+2
-2
@@ -27,7 +27,7 @@ async function sync(auditSource) {
|
||||
info.ipv4 = info.ip;
|
||||
delete info.ip;
|
||||
}
|
||||
const ipv4Changed = info.ip !== ipv4;
|
||||
const ipv4Changed = info.ipv4 !== ipv4;
|
||||
const ipv6Changed = ipv6 && info.ipv6 !== ipv6; // both should be RFC 5952 format
|
||||
|
||||
if (!ipv4Changed && !ipv6Changed) {
|
||||
@@ -35,7 +35,7 @@ async function sync(auditSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
debug(`refreshDNS: updating IP from ${info.ip} to ipv4: ${ipv4} (changed: ${ipv4Changed}) ipv6: ${ipv6} (changed: ${ipv6Changed})`);
|
||||
debug(`refreshDNS: updating IP from ${info.ipv4} to ipv4: ${ipv4} (changed: ${ipv4Changed}) ipv6: ${ipv6} (changed: ${ipv6Changed})`);
|
||||
if (ipv4Changed) await dns.upsertDnsRecords(constants.DASHBOARD_LOCATION, settings.dashboardDomain(), 'A', [ ipv4 ]);
|
||||
if (ipv6Changed) await dns.upsertDnsRecords(constants.DASHBOARD_LOCATION, settings.dashboardDomain(), 'AAAA', [ ipv6 ]);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ exports = module.exports = {
|
||||
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
|
||||
'images': {
|
||||
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.4.0@sha256:45817f1631992391d585f171498d257487d872480fd5646723a2b956cc4ef15d' },
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:3.2.0@sha256:c70d0a1ff7ce8a27dcf7d6f8d1718ddba82ef254079fee5d20fdf074f28cd009' },
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:3.2.1@sha256:75cef64ba4917ba9ec68bc0c9d9ba3a9eeae00a70173cd6d81cc6118038737d9' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:4.3.0@sha256:9f0cfe83310a6f67885c21804c01e73c8d256606217f44dcefb3e05a5402b2c9' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:4.2.0@sha256:c8ebdbe2663b26fcd58b1e6b97906b62565adbe4a06256ba0f86114f78b37e6b' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:3.2.1@sha256:30a5550c41c3be3a01dbf457497ba2f3c05f3121c595a17ef1aacbef931b6114' },
|
||||
|
||||
@@ -687,6 +687,14 @@ async function start() {
|
||||
res.end();
|
||||
});
|
||||
|
||||
// directus looks for the "DN" of the bind user
|
||||
gServer.search('ou=apps,dc=cloudron', function(req, res, next) {
|
||||
const obj = {
|
||||
dn: req.dn.toString(),
|
||||
};
|
||||
finalSend([obj], req, res, next);
|
||||
});
|
||||
|
||||
// just log that an attempt was made to unknown route, this helps a lot during app packaging
|
||||
gServer.use(function(req, res, next) {
|
||||
debug('not handled: dn %s, scope %s, filter %s (from %s)', req.dn ? req.dn.toString() : '-', req.scope, req.filter ? req.filter.toString() : '-', req.connection.ldap.id);
|
||||
|
||||
+2
-2
@@ -197,7 +197,7 @@ async function checkOutboundPort25() {
|
||||
relay.status = false;
|
||||
relay.value = `Connect to ${constants.PORT25_CHECK_SERVER} failed: ${error.message}. Check if port 25 (outbound) is blocked`;
|
||||
client.destroy();
|
||||
relay.errorMessage = `Connect to ${constants.PORT25_CHECK_SERVER} failed.`;
|
||||
relay.errorMessage = `Connect to ${constants.PORT25_CHECK_SERVER} failed: ${error.message}`;
|
||||
resolve(relay);
|
||||
});
|
||||
});
|
||||
@@ -516,7 +516,7 @@ async function checkRblStatus(domain) {
|
||||
blacklistedServers.push(result);
|
||||
}
|
||||
|
||||
debug(`checkRblStatus: ${domain} (ip: ${ip}) servers: ${JSON.stringify(blacklistedServers)})`);
|
||||
debug(`checkRblStatus: ${domain} (ip: ${ip}) blacklistedServers: ${JSON.stringify(blacklistedServers)})`);
|
||||
|
||||
return { status: blacklistedServers.length === 0, ip, servers: blacklistedServers };
|
||||
}
|
||||
|
||||
+4
-1
@@ -88,6 +88,9 @@ server {
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# some apps have underscores in headers. this is apparently disabled by default because of some legacy CGI compat
|
||||
underscores_in_headers on;
|
||||
|
||||
<% if (endpoint !== 'ip' && endpoint !== 'setup') { -%>
|
||||
# dhparams is generated only after dns setup
|
||||
ssl_dhparam /home/yellowtent/platformdata/dhparams.pem;
|
||||
@@ -215,7 +218,7 @@ server {
|
||||
<% if ( endpoint === 'dashboard' || endpoint === 'setup' ) { %>
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
client_max_body_size 1m;
|
||||
client_max_body_size 2m;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/cloudron/login$ {
|
||||
|
||||
@@ -180,6 +180,7 @@ async function setGhost(req, res, next) {
|
||||
|
||||
if (typeof req.body.password !== 'string' || !req.body.password) return next(new HttpError(400, 'password must be non-empty string'));
|
||||
if ('expiresAt' in req.body && typeof req.body.password !== 'number') return next(new HttpError(400, 'expiresAt must be a number'));
|
||||
if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`));
|
||||
|
||||
const [error] = await safe(users.setGhost(req.resource, req.body.password, req.body.expiresAt || 0));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ function initializeExpressSync() {
|
||||
|
||||
const wsServer = new ws.Server({ noServer: true }); // in noServer mode, we have to handle 'upgrade' and call handleUpgrade
|
||||
|
||||
const QUERY_LIMIT = '1mb', // max size for json and urlencoded queries (see also client_max_body_size in nginx)
|
||||
const QUERY_LIMIT = '2mb', // max size for json and urlencoded queries (see also client_max_body_size in nginx)
|
||||
FIELD_LIMIT = 2 * 1024 * 1024; // max fields that can appear in multipart
|
||||
|
||||
const REQUEST_TIMEOUT = 20000; // timeout for all requests (see also setTimeout on the httpServer)
|
||||
|
||||
+2
-3
@@ -18,6 +18,7 @@ function api(provider) {
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
|
||||
switch (provider) {
|
||||
case 'noop': return require('./sysinfo/noop.js');
|
||||
case 'fixed': return require('./sysinfo/fixed.js');
|
||||
case 'network-interface': return require('./sysinfo/network-interface.js');
|
||||
default: return require('./sysinfo/generic.js');
|
||||
@@ -33,10 +34,8 @@ async function getServerIPv4() {
|
||||
// returns RFC 5952 formatted address (https://datatracker.ietf.org/doc/html/rfc5952)
|
||||
async function getServerIPv6() {
|
||||
const config = await settings.getIPv6Config();
|
||||
|
||||
if (config.provider === 'noop') return null;
|
||||
|
||||
const result = await api(config.provider).getServerIPv6(config);
|
||||
if (!result) return null;
|
||||
return ipaddr.parse(result).toRFC5952String();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getServerIPv4,
|
||||
getServerIPv6,
|
||||
testIPv4Config,
|
||||
testIPv6Config
|
||||
};
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
async function getServerIPv4(config) {
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getServerIPv6(config) {
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function testIPv4Config(config) {
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function testIPv6Config(config) {
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user