Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f77296bb2c | ||
|
|
c0f7220040 | ||
|
|
d435b8b4e3 | ||
|
|
c478ace8bd | ||
|
|
5cfec4c371 | ||
|
|
40cdf0c94d | ||
|
|
9908031b68 | ||
|
|
c5b48b4386 | ||
|
|
11fd3cafb5 | ||
|
|
18dda10b54 | ||
|
|
1a73ddea23 | ||
|
|
f15b7dd75c | ||
|
|
51ed5b78f2 | ||
|
|
fb2ec52464 | ||
|
|
d158ba0464 | ||
|
|
b6b1eb2353 | ||
|
|
fd8eed048a | ||
|
|
25ee2170f6 | ||
|
|
c6641d23cd | ||
|
|
f5f6b69d5d |
20
CHANGES
20
CHANGES
@@ -2899,3 +2899,23 @@
|
||||
* firewall: add masquerading rules for containers to reach each other via public IP
|
||||
* docker: fix parsing of optional namespace in image refs
|
||||
|
||||
[8.2.4]
|
||||
* restore: fix crash with invalid backup id
|
||||
* setup: add inwx to dns setup
|
||||
* backups: add preserve attributes checkbox
|
||||
* mail: add ipv6 rdns check
|
||||
* mail: disable OCR in tika. this is too slow
|
||||
* mail: rebuild index script
|
||||
* backups: add preserve attributes checkbox
|
||||
* username: only ending with .app is reserved
|
||||
* cloudron-support: add helper function to free up disk space when full
|
||||
* cloudflare: list API does not return `zone_id` anymore
|
||||
|
||||
[8.3.0]
|
||||
* new base image: cloudron/base:5.0.0@sha256:04fd70dbd8ad6149c19de39e35718e024417c3e01dc9c6637eaf4a41ec4e596c
|
||||
* Database upgrades are automatically performed. This might take some time depending on the amount of data.
|
||||
* Postgres v16
|
||||
* Mongodb v7
|
||||
* PHP v8.3
|
||||
* Node.js v22 LTS
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
{ name: 'GoDaddy', value: 'godaddy' },
|
||||
{ name: 'Google Cloud DNS', value: 'gcdns' },
|
||||
{ name: 'Hetzner', value: 'hetzner' },
|
||||
{ name: 'INWX', value: 'inwx' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Name.com', value: 'namecom' },
|
||||
{ name: 'Namecheap', value: 'namecheap' },
|
||||
@@ -98,6 +99,8 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
bunnyAccessKey: '',
|
||||
dnsimpleAccessToken: '',
|
||||
hetznerToken: '',
|
||||
inwxUsername: '',
|
||||
inwxPassword: '',
|
||||
vultrToken: '',
|
||||
deSecToken: '',
|
||||
nameComUsername: '',
|
||||
@@ -199,6 +202,9 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
|
||||
config.accessToken = $scope.dnsCredentials.dnsimpleAccessToken;
|
||||
} else if (provider === 'hetzner') {
|
||||
config.token = $scope.dnsCredentials.hetznerToken;
|
||||
} else if (provider === 'inwx') {
|
||||
config.username = $scope.dnsCredentials.inwxUsername;
|
||||
config.password = $scope.dnsCredentials.inwxPassword;
|
||||
} else if (provider === 'vultr') {
|
||||
config.token = $scope.dnsCredentials.vultrToken;
|
||||
} else if (provider === 'desec') {
|
||||
|
||||
@@ -629,7 +629,8 @@
|
||||
"cifsSealSupport": "Use seal encryption. Requires at least SMB v3",
|
||||
"chown": "Remote file system supports chown",
|
||||
"encryptedFilenames": "Encrypted filenames",
|
||||
"encryptFilenames": "Encrypt Filenames"
|
||||
"encryptFilenames": "Encrypt Filenames",
|
||||
"preserveAttributesLabel": "Preserve file attributes"
|
||||
},
|
||||
"check": {
|
||||
"noop": "Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://docs.cloudron.io/backups/#storage-providers for more information.",
|
||||
|
||||
@@ -1502,7 +1502,8 @@
|
||||
"certificateRenewalFailed": "Vernieuwen certificaten mislukt",
|
||||
"appOutOfMemory": "App had te weinig geheugen",
|
||||
"appUp": "App is offline",
|
||||
"appDown": "App werkt niet"
|
||||
"appDown": "App werkt niet",
|
||||
"rebootRequired": "Server herstart noodzakelijk"
|
||||
},
|
||||
"settingsDialog": {
|
||||
"description": "Beheer hier je persoonlijke notificatie -instellingen. Cloudron zal een e-mail versturen voor de geselecteerde gebeurtenissen naar je primaire e-mailadres."
|
||||
|
||||
@@ -256,6 +256,13 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CIFS/mountpoint -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'mountpoint' || configureBackup.provider === 'cifs'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="configureBackup.preserveAttributes">{{ 'backups.configureBackupStorage.preserveAttributesLabel' | tr }}</input>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- mountpoint -->
|
||||
<div class="checkbox" ng-show="configureBackup.provider === 'mountpoint'">
|
||||
<label>
|
||||
|
||||
@@ -695,6 +695,9 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
downloadConcurrency: '',
|
||||
syncConcurrency: '', // sort of similar to upload
|
||||
|
||||
noHardlinks: false,
|
||||
preserveAttributes: true,
|
||||
|
||||
blockDevices: [],
|
||||
disk: null,
|
||||
mountOptions: {
|
||||
@@ -764,6 +767,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
$scope.configureBackup.format = $scope.backupConfig.format;
|
||||
$scope.configureBackup.acceptSelfSignedCerts = !!$scope.backupConfig.acceptSelfSignedCerts;
|
||||
$scope.configureBackup.useHardlinks = !$scope.backupConfig.noHardlinks;
|
||||
$scope.configureBackup.preserveAttributes = !!$scope.backupConfig.preserveAttributes;
|
||||
$scope.configureBackup.chown = $scope.backupConfig.chown;
|
||||
|
||||
const limits = $scope.backupConfig.limits || {};
|
||||
@@ -916,21 +920,25 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
|
||||
backupConfig.mountOptions.username = $scope.configureBackup.mountOptions.username;
|
||||
backupConfig.mountOptions.password = $scope.configureBackup.mountOptions.password;
|
||||
backupConfig.mountOptions.seal = $scope.configureBackup.mountOptions.seal;
|
||||
backupConfig.preserveAttributes = $scope.configureBackup.preserveAttributes;
|
||||
} else if (backupConfig.provider === 'sshfs') {
|
||||
backupConfig.mountOptions.user = $scope.configureBackup.mountOptions.user;
|
||||
backupConfig.mountOptions.port = $scope.configureBackup.mountOptions.port;
|
||||
backupConfig.mountOptions.privateKey = $scope.configureBackup.mountOptions.privateKey;
|
||||
backupConfig.preserveAttributes = true;
|
||||
}
|
||||
} else if (backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs' || backupConfig.provider === 'disk') {
|
||||
backupConfig.mountOptions.diskPath = $scope.configureBackup.mountOptions.diskPath;
|
||||
backupConfig.preserveAttributes = true;
|
||||
} else if (backupConfig.provider === 'mountpoint') {
|
||||
backupConfig.mountPoint = $scope.configureBackup.mountPoint;
|
||||
backupConfig.chown = $scope.configureBackup.chown;
|
||||
backupConfig.preserveAttributes = true;
|
||||
backupConfig.preserveAttributes = $scope.configureBackup.preserveAttributes;
|
||||
}
|
||||
} else if (backupConfig.provider === 'filesystem') {
|
||||
backupConfig.backupFolder = $scope.configureBackup.backupFolder;
|
||||
backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks;
|
||||
backupConfig.preserveAttributes = true;
|
||||
}
|
||||
|
||||
if (backupConfig.format === 'rsync') {
|
||||
|
||||
@@ -671,7 +671,7 @@
|
||||
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p ng-show="record.name === 'MX' && domain.provider === 'namecheap'">{{ 'email.dnsStatus.namecheapInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/domains/#namecheap-dns" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<p ng-show="record.name === 'PTR'">{{ 'email.dnsStatus.ptrInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#ptr-record" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<p ng-show="record.name === 'PTR4' || record.name === 'PTR6'">{{ 'email.dnsStatus.ptrInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#ptr-record" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<p ng-show="expectedDnsRecords[record.value].name">{{ 'email.dnsStatus.hostname' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].name }}</tt></b></p>
|
||||
<p ng-hide="expectedDnsRecords[record.value].name">{{ 'email.dnsStatus.domain' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].domain }}</tt></b></p>
|
||||
<p>{{ 'email.dnsStatus.type' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].type }}</tt></b></p>
|
||||
|
||||
@@ -45,11 +45,12 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$scope.storageQuotaTicks = [ 500*1000*1000, 5*1000*1000*1000, 15*1000*1000*1000, 50*1000*1000*1000, 100*1000*1000*1000 ];
|
||||
|
||||
$scope.expectedDnsRecords = {
|
||||
mx: { },
|
||||
dkim: { },
|
||||
spf: { },
|
||||
dmarc: { },
|
||||
ptr: { }
|
||||
mx: {},
|
||||
dkim: {},
|
||||
spf: {},
|
||||
dmarc: {},
|
||||
ptr4: {},
|
||||
ptr6: {}
|
||||
};
|
||||
|
||||
$scope.expectedDnsRecordsTypes = [
|
||||
@@ -57,7 +58,9 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
{ name: 'DKIM', value: 'dkim' },
|
||||
{ name: 'SPF', value: 'spf' },
|
||||
{ name: 'DMARC', value: 'dmarc' },
|
||||
{ name: 'PTR', value: 'ptr' }
|
||||
{ name: 'PTR4', value: 'ptr4' },
|
||||
{ name: 'PTR6', value: 'ptr6' }
|
||||
|
||||
];
|
||||
|
||||
$scope.openSubscriptionSetup = function () {
|
||||
|
||||
@@ -370,7 +370,7 @@ function print_ipv6_disable_howto() {
|
||||
echo -e "\tsysctl -w net.ipv6.conf.${iface}.disable_ipv6=1"
|
||||
done
|
||||
|
||||
echo "For above configuration to persist reboots, you have to add below to /etc/sysctl.conf"
|
||||
echo "For the above configuration to persist across reboots, you have to add below to /etc/sysctl.conf"
|
||||
for iface in $(ls /sys/class/net | grep -vE '^(lo|veth|docker|virbr|br|vmnet|tun|tap|wl|we)'); do
|
||||
echo -e "\tnet.ipv6.conf.${iface}.disable_ipv6=1"
|
||||
done
|
||||
@@ -580,15 +580,39 @@ function troubleshoot() {
|
||||
check_unbound # this is less fatal after 8.0
|
||||
}
|
||||
|
||||
function cleanup_disk_space() {
|
||||
read -p "Truncate log files to reclaim space ? (y/N) " -n 1 -r choice
|
||||
echo -e "\n"
|
||||
if [[ $choice =~ ^[Yy]$ ]]; then
|
||||
truncate -s0 /home/yellowtent/platformdata/logs/*/*.log
|
||||
rm -f /home/yellowtent/platformdata/logs/*.log.* # delete the log.1, log.2 etc
|
||||
fi
|
||||
|
||||
read -p "Prune docker system resources to reclaim space ? (y/N) " -n 1 -r choice
|
||||
echo -e "\n"
|
||||
if [[ $choice =~ ^[Yy]$ ]]; then
|
||||
docker images prune -fa || true
|
||||
fi
|
||||
|
||||
read -p "Prune docker volumes to reclaim space ? (y/N) " -n 1 -r choice
|
||||
echo -e "\n"
|
||||
if [[ $choice =~ ^[Yy]$ ]]; then
|
||||
for container in $(docker ps --format "{{.ID}}"); do
|
||||
docker exec "$container" find /tmp -type f -mtime +1 -delete || true
|
||||
docker exec "$container" find /run -type f -mtime +1 -delete || true
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
function check_disk_space() {
|
||||
# check if at least 10mb root partition space is available
|
||||
if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
|
||||
echo "No more space left on /"
|
||||
echo "This is likely the root case of the issue. Free up some space and also check other partitions below:"
|
||||
echo ""
|
||||
df -h
|
||||
echo ""
|
||||
echo "To recover from a full disk, follow the guide at https://docs.cloudron.io/troubleshooting/#recovery-after-disk-full"
|
||||
echo "No more space left on / (see df -h output)"
|
||||
cleanup_disk_space
|
||||
fi
|
||||
|
||||
if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
|
||||
echo "Still no space despite cleaning up. If you have backups (/var/backups) on this disk, delete old backups to free some space"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -2265,6 +2265,7 @@ async function restore(app, backupId, auditSource) {
|
||||
|
||||
// for empty or null backupId, use existing manifest to mimic a reinstall
|
||||
const backupInfo = backupId ? await backups.get(backupId) : { manifest: app.manifest };
|
||||
if (!backupInfo) throw new BoxError(BoxError.BAD_FIELD, 'No such backup');
|
||||
const manifest = backupInfo.manifest;
|
||||
|
||||
if (!manifest) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest');
|
||||
|
||||
@@ -8,7 +8,7 @@ class AuditSource {
|
||||
}
|
||||
|
||||
static fromRequest(req) {
|
||||
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || null;
|
||||
return new AuditSource(req.user?.username, req.user?.id, ip);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class AuditSource {
|
||||
}
|
||||
|
||||
static fromOidcRequest(req) {
|
||||
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || null;
|
||||
return new AuditSource('oidc', req.body?.username, ip);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ const assert = require('assert'),
|
||||
ipaddr = require('ipaddr.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -38,20 +37,20 @@ function injectPrivateFields(newConfig, currentConfig) {
|
||||
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'));
|
||||
if (result.statusCode === 404) return new BoxError(BoxError.NOT_FOUND, `[${result.statusCode}] ${JSON.stringify(result.body)}`);
|
||||
if (result.statusCode === 422) return new BoxError(BoxError.BAD_FIELD, result.body.message);
|
||||
if (result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) {
|
||||
let message = 'Unknown error';
|
||||
if (typeof result.body.error === 'string') {
|
||||
message = `message: ${result.body.error} statusCode: ${result.statusCode}`;
|
||||
message = `[${result.statusCode}] ${result.body.error}`;
|
||||
} else if (Array.isArray(result.body.errors) && result.body.errors.length > 0) {
|
||||
const error = result.body.errors[0];
|
||||
message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
|
||||
message = `[${result.statusCode}] ${error.message} code:${error.code}`;
|
||||
}
|
||||
return new BoxError(BoxError.ACCESS_DENIED, message);
|
||||
}
|
||||
|
||||
return new BoxError(BoxError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body));
|
||||
return new BoxError(BoxError.EXTERNAL_ERROR, `${result.statusCode} ${JSON.stringify(result.body)}`);
|
||||
}
|
||||
|
||||
function createRequest(method, url, domainConfig) {
|
||||
@@ -207,15 +206,13 @@ async function del(domainObject, location, type, values) {
|
||||
const result = await getDnsRecords(domainConfig, zone.id, fqdn, type);
|
||||
if (result.length === 0) return;
|
||||
|
||||
const zoneId = result[0].zone_id;
|
||||
|
||||
const tmp = result.filter(function (record) { return values.some(function (value) { return value === record.content; }); });
|
||||
debug('del: %j', tmp);
|
||||
|
||||
if (tmp.length === 0) return;
|
||||
|
||||
for (const r of tmp) {
|
||||
const [error, response] = await safe(createRequest('DELETE', `${CLOUDFLARE_ENDPOINT}/zones/${zoneId}/dns_records/${r.id}`, domainConfig));
|
||||
const [error, response] = await safe(createRequest('DELETE', `${CLOUDFLARE_ENDPOINT}/zones/${zone.id}/dns_records/${r.id}`, domainConfig));
|
||||
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
||||
if (response.statusCode !== 200 || response.body.success !== true) throw translateRequestError(response);
|
||||
}
|
||||
|
||||
@@ -19,13 +19,12 @@ const assert = require('assert'),
|
||||
safe = require('safetydance'),
|
||||
timers = require('timers/promises'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
const DESEC_ENDPOINT = 'https://desec.io/api/v1';
|
||||
|
||||
function formatError(response) {
|
||||
return util.format('deSEC DNS error [%s] %j', response.statusCode, response.body);
|
||||
return `deSEC DNS error [${response.statusCode}] ${JSON.stringify(response.body)}`;
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
|
||||
@@ -18,13 +18,12 @@ const assert = require('assert'),
|
||||
dns = require('../dns.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
const GANDI_API = 'https://dns.api.gandi.net/api/v5';
|
||||
|
||||
function formatError(response) {
|
||||
return util.format(`Gandi DNS error [${response.statusCode}] ${response.body.message}`);
|
||||
return `Gandi DNS error [${response.statusCode}] ${response.body.message}`;
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
|
||||
@@ -18,14 +18,13 @@ const assert = require('assert'),
|
||||
dns = require('../dns.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
// const GODADDY_API_OTE = 'https://api.ote-godaddy.com/v1/domains';
|
||||
const GODADDY_API = 'https://api.godaddy.com/v1/domains';
|
||||
|
||||
function formatError(response) {
|
||||
return util.format(`GoDaddy DNS error [${response.statusCode}] ${response.body.message}`);
|
||||
return `GoDaddy DNS error [${response.statusCode}] ${response.body.message}`;
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
|
||||
@@ -18,13 +18,12 @@ const assert = require('assert'),
|
||||
dns = require('../dns.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
const LINODE_ENDPOINT = 'https://api.linode.com/v4';
|
||||
|
||||
function formatError(response) {
|
||||
return util.format('Linode DNS error [%s] %j', response.statusCode, response.body);
|
||||
return `Linode DNS error [${response.statusCode}] ${JSON.stringify(response.body)}`;
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
@@ -126,7 +125,8 @@ async function upsert(domainObject, location, type, values) {
|
||||
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
|
||||
|
||||
const { zoneId, records } = await getZoneRecords(domainConfig, zoneName, name, type);
|
||||
let i = 0, recordIds = []; // used to track available records to update instead of create
|
||||
let i = 0; // used to track available records to update instead of create
|
||||
const recordIds = [];
|
||||
|
||||
for (const value of values) {
|
||||
const data = {
|
||||
|
||||
@@ -144,14 +144,14 @@ async function upsert(domainObject, subdomain, type, values) {
|
||||
const result = await getZone(domainConfig, zoneName);
|
||||
|
||||
// Array to keep track of records that need to be inserted
|
||||
let toInsert = [];
|
||||
const toInsert = [];
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
let curValue = values[i];
|
||||
const curValue = values[i];
|
||||
let wasUpdate = false;
|
||||
|
||||
for (let j = 0; j < result.length; j++) {
|
||||
let curHost = result[j];
|
||||
const curHost = result[j];
|
||||
|
||||
if (curHost.Type === type && curHost.Name === subdomain) {
|
||||
// Updating an already existing host
|
||||
@@ -167,7 +167,7 @@ async function upsert(domainObject, subdomain, type, values) {
|
||||
|
||||
// We don't have this host at all yet, let's push to toInsert array
|
||||
if (!wasUpdate) {
|
||||
let newRecord = {
|
||||
const newRecord = {
|
||||
RecordType: type,
|
||||
HostName: subdomain,
|
||||
Address: curValue
|
||||
@@ -226,7 +226,7 @@ async function del(domainObject, subdomain, type, values) {
|
||||
const originalLength = result.length;
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
let curValue = values[i];
|
||||
const curValue = values[i];
|
||||
|
||||
result = result.filter(curHost => curHost.Type !== type || curHost.Name !== subdomain || curHost.Address !== curValue);
|
||||
}
|
||||
|
||||
@@ -18,14 +18,13 @@ const assert = require('assert'),
|
||||
dns = require('../dns.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
const API_ENDPOINT = 'https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON';
|
||||
|
||||
function formatError(response) {
|
||||
if (response.body) return util.format('Netcup DNS error [%s] %s', response.body.statuscode, response.body.longmessage);
|
||||
else return util.format('Netcup DNS error [%s] %s', response.statusCode, response.text);
|
||||
if (response.body) return `Netcup DNS error [${response.body.statuscode}] ${response.body.longmessage}`;
|
||||
else return `Netcup DNS error [${response.statusCode}] ${response.text}`;
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
|
||||
@@ -18,13 +18,12 @@ const assert = require('assert'),
|
||||
dns = require('../dns.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
const VULTR_ENDPOINT = 'https://api.vultr.com/v2';
|
||||
|
||||
function formatError(response) {
|
||||
return util.format('Vultr DNS error [%s] %j', response.statusCode, response.body);
|
||||
return `Vultr DNS error [${response.statusCode}] ${JSON.stringify(response.body)}`;
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
|
||||
@@ -29,7 +29,7 @@ async function authorizeApp(req, res, next) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const [error, app] = await safe(apps.getByIpAddress(req.connection.remoteAddress));
|
||||
const [error, app] = await safe(apps.getByIpAddress(req.socket.remoteAddress));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (!app) return next(new HttpError(401, 'Unauthorized'));
|
||||
if (!app.manifest.addons?.docker) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
@@ -11,14 +11,14 @@ exports = module.exports = {
|
||||
// a major version bump in the db containers will trigger the restore logic that uses the db dumps
|
||||
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256 . note this has registry in it because manifest id is registry specific!
|
||||
'images': {
|
||||
// 'base': 'registry.docker.com/cloudron/base:4.2.0@sha256:46da2fffb36353ef714f97ae8e962bd2c212ca091108d768ba473078319a47f4',
|
||||
'graphite': 'registry.docker.com/cloudron/graphite:3.4.3@sha256:75df420ece34b31a7ce8d45b932246b7f524c123e1854f5e8f115a9e94e33f20',
|
||||
'mail': 'registry.docker.com/cloudron/mail:3.14.6@sha256:49ddb10b7355aa01e7b40c07ae9c583d171df9566491a40bb88ae91879f8f7eb',
|
||||
'mongodb': 'registry.docker.com/cloudron/mongodb:6.0.0@sha256:1108319805acfb66115aa96a8fdbf2cded28d46da0e04d171a87ec734b453d1e',
|
||||
'mysql': 'registry.docker.com/cloudron/mysql:3.4.3@sha256:8934c5ddcd69f24740d9a38f0de2937e47240238f3b8f5c482862eeccc5a21d2',
|
||||
'postgresql': 'registry.docker.com/cloudron/postgresql:5.3.1@sha256:eaea598aec086c90c0bb7bb8227bcde51b368bcca83d0082a4919bbb6f2d039f',
|
||||
'redis': 'registry.docker.com/cloudron/redis:3.5.4@sha256:7c97adb4ee1606d5a0d38aa5ed107a9c27efa13a251f1c1585979292c23de4ec',
|
||||
'sftp': 'registry.docker.com/cloudron/sftp:3.8.9@sha256:f2a126839df99ca420a3ad8177594f58b113f6292e98719f2cf2e0ddc3597696',
|
||||
'turn': 'registry.docker.com/cloudron/turn:1.7.2@sha256:9ed8da613c1edc5cb8700657cf6e49f0f285b446222a8f459f80919945352f6d',
|
||||
// 'base': 'registry.docker.com/cloudron/base:5.0.0@sha256:04fd70dbd8ad6149c19de39e35718e024417c3e01dc9c6637eaf4a41ec4e596c',
|
||||
'graphite': 'registry.docker.com/cloudron/graphite:3.5.0@sha256:ee7c9dc49a6507cb3e3cee25495b2044908feb91dac5df87a9633dea38fdeb8a',
|
||||
'mail': 'registry.docker.com/cloudron/mail:3.15.0@sha256:c93b5a83fc4e775bda4e05010bd19e5a658936e7a09cf7e51281e3696fde4536',
|
||||
'mongodb': 'registry.docker.com/cloudron/mongodb:6.1.0@sha256:e0eae0335546310d9b4fd60b225adbfa07f60204e766275a99c083726f7453fe',
|
||||
'mysql': 'registry.docker.com/cloudron/mysql:3.5.0@sha256:969ea5b2f91861940ca6309c7676c52e479d2a864ba3aabd08a4266799707280',
|
||||
'postgresql': 'registry.docker.com/cloudron/postgresql:6.0.0@sha256:197ca3747502b8f924134b55362ddd88c9924093de40730b6078858f45c2b020',
|
||||
'redis': 'registry.docker.com/cloudron/redis:3.6.0@sha256:cd240086189f4b1467b7ae3496d0fbd14becd5c4280fdcac1e565f82f2f7da62',
|
||||
'sftp': 'registry.docker.com/cloudron/sftp:3.9.1@sha256:aa67eb58a957cbaf09fdcb332aa56575821a4246059448cf39aeca8faba1fb4b',
|
||||
'turn': 'registry.docker.com/cloudron/turn:1.8.0@sha256:cdbe83c3c83b8f25de3a5814b121eb941b457dca7127d2e6ff446c7a0cfa1570',
|
||||
}
|
||||
};
|
||||
|
||||
65
src/mail.js
65
src/mail.js
@@ -397,8 +397,61 @@ async function checkDmarc(domain) {
|
||||
return dmarc;
|
||||
}
|
||||
|
||||
// TODO: check ip6.arpa for IPv6 PTR
|
||||
async function checkPtr(mailFqdn) {
|
||||
async function checkPtr6(mailFqdn) {
|
||||
assert.strictEqual(typeof mailFqdn, 'string');
|
||||
|
||||
const ptr = {
|
||||
domain: null,
|
||||
name: null,
|
||||
type: 'PTR',
|
||||
value: null,
|
||||
expected: mailFqdn, // any trailing '.' is added by client software (https://lists.gt.net/spf/devel/7918)
|
||||
status: false,
|
||||
errorMessage: ''
|
||||
};
|
||||
|
||||
const [error, ip] = await safe(network.getIPv6());
|
||||
if (error) {
|
||||
ptr.errorMessage = error.message;
|
||||
return ptr;
|
||||
}
|
||||
if (ip === null) {
|
||||
ptr.status = true;
|
||||
ptr.expected = 'Check skipped, server has no IPv6';
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function expandIPv6(ipv6) {
|
||||
const parts = ipv6.split('::');
|
||||
const left = parts[0].split(':');
|
||||
const right = parts[1] ? parts[1].split(':') : [];
|
||||
const fill = new Array(8 - left.length - right.length).fill('0');
|
||||
const full = [...left, ...fill, ...right];
|
||||
return full.map(part => part.padStart(4, '0')).join('');
|
||||
}
|
||||
|
||||
const expanded = expandIPv6(ip);
|
||||
const reversed = expanded.split('').reverse().join('');
|
||||
const reversedWithDots = reversed.split('').join('.');
|
||||
|
||||
ptr.domain = `${reversedWithDots}.ip6.arpa`;
|
||||
ptr.name = ip;
|
||||
|
||||
const [error2, ptrRecords] = await safe(dig.resolve(ptr.domain, 'PTR', DNS_OPTIONS));
|
||||
if (error2) {
|
||||
ptr.errorMessage = error2.message;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
if (ptrRecords.length !== 0) {
|
||||
ptr.value = ptrRecords.join(' ');
|
||||
ptr.status = ptrRecords.some(function (v) { return v === ptr.expected; });
|
||||
}
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
async function checkPtr4(mailFqdn) {
|
||||
assert.strictEqual(typeof mailFqdn, 'string');
|
||||
|
||||
const ptr = {
|
||||
@@ -416,6 +469,11 @@ async function checkPtr(mailFqdn) {
|
||||
ptr.errorMessage = error.message;
|
||||
return ptr;
|
||||
}
|
||||
if (ip === null) {
|
||||
ptr.status = true;
|
||||
ptr.expected = 'Check skipped, server has no IPv4';
|
||||
return ptr;
|
||||
}
|
||||
|
||||
ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
|
||||
ptr.name = ip;
|
||||
@@ -550,7 +608,8 @@ async function getStatus(domain) {
|
||||
checks.push(
|
||||
{ what: 'dns.spf', promise: checkSpf(domain, fqdn) },
|
||||
{ what: 'dns.dkim', promise: checkDkim(mailDomain) },
|
||||
{ what: 'dns.ptr', promise: checkPtr(fqdn) },
|
||||
{ what: 'dns.ptr4', promise: checkPtr4(fqdn) },
|
||||
{ what: 'dns.ptr6', promise: checkPtr6(fqdn) },
|
||||
{ what: 'relay', promise: checkOutboundPort25() },
|
||||
{ what: 'rbl', promise: checkRblStatus(domain) },
|
||||
);
|
||||
|
||||
@@ -545,7 +545,7 @@ function interactionLogin(provider) {
|
||||
return next(new HttpError(400, detailsError));
|
||||
}
|
||||
|
||||
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || null;
|
||||
const userAgent = req.headers['user-agent'] || '';
|
||||
const clientId = details.params.client_id;
|
||||
|
||||
@@ -615,7 +615,7 @@ function interactionConfirm(provider) {
|
||||
return async function (req, res, next) {
|
||||
async function raiseLoginEvent(user, clientId) {
|
||||
try {
|
||||
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || null;
|
||||
const userAgent = req.headers['user-agent'] || '';
|
||||
const auditSource = AuditSource.fromOidcRequest(req);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ async function login(req, res, next) {
|
||||
if ('type' in req.body && typeof req.body.type !== 'string') return next(new HttpError(400, 'type must be a string'));
|
||||
|
||||
const type = req.body.type || tokens.ID_WEBADMIN;
|
||||
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || null;
|
||||
const userAgent = req.headers['user-agent'] || '';
|
||||
|
||||
const tokenTypeError = tokens.validateTokenType(type);
|
||||
|
||||
@@ -109,7 +109,7 @@ async function activate(req, res, next) {
|
||||
const { username, password, email } = req.body;
|
||||
const displayName = req.body.displayName || '';
|
||||
|
||||
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
||||
|
||||
const [error, info] = await safe(provision.activate(username, password, email, displayName, ip, AuditSource.fromRequest(req)));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
@@ -138,11 +138,11 @@ describe('Mail API', function () {
|
||||
expect(response.body.dns.mx.expected).to.eql(`10 ${mailFqdn}.`);
|
||||
expect(response.body.dns.mx.status).to.eql(false);
|
||||
|
||||
expect(response.body.dns.ptr).to.be.an('object');
|
||||
expect(response.body.dns.ptr.type).to.eql('PTR');
|
||||
expect(response.body.dns.ptr4).to.be.an('object');
|
||||
expect(response.body.dns.ptr4.type).to.eql('PTR');
|
||||
// expect(response.body.ptr.value).to.eql(null); this will be anything random
|
||||
expect(response.body.dns.ptr.expected).to.eql(mailFqdn);
|
||||
expect(response.body.dns.ptr.status).to.eql(false);
|
||||
expect(response.body.dns.ptr4.expected).to.eql(mailFqdn);
|
||||
expect(response.body.dns.ptr4.status).to.eql(false);
|
||||
});
|
||||
|
||||
it('succeeds with "undefined" spf, dkim, dmarc, mx, ptr records', async function () {
|
||||
@@ -178,9 +178,9 @@ describe('Mail API', function () {
|
||||
expect(response.body.dns.mx.expected).to.eql(`10 ${mailFqdn}.`);
|
||||
expect(response.body.dns.mx.value).to.eql(null);
|
||||
|
||||
expect(response.body.dns.ptr).to.be.an('object');
|
||||
expect(response.body.dns.ptr.expected).to.eql(mailFqdn);
|
||||
expect(response.body.dns.ptr.status).to.eql(false);
|
||||
expect(response.body.dns.ptr4).to.be.an('object');
|
||||
expect(response.body.dns.ptr4.expected).to.eql(mailFqdn);
|
||||
expect(response.body.dns.ptr4.status).to.eql(false);
|
||||
// expect(response.body.ptr.value).to.eql(null); this will be anything random
|
||||
});
|
||||
|
||||
@@ -217,9 +217,9 @@ describe('Mail API', function () {
|
||||
expect(response.body.dns.mx.expected).to.eql(`10 ${mailFqdn}.`);
|
||||
expect(response.body.dns.mx.value).to.eql(`20 ${mailFqdn}. 10 some.other.server.`);
|
||||
|
||||
expect(response.body.dns.ptr).to.be.an('object');
|
||||
expect(response.body.dns.ptr.expected).to.eql(mailFqdn);
|
||||
expect(response.body.dns.ptr.status).to.eql(false);
|
||||
expect(response.body.dns.ptr4).to.be.an('object');
|
||||
expect(response.body.dns.ptr4.expected).to.eql(mailFqdn);
|
||||
expect(response.body.dns.ptr4.status).to.eql(false);
|
||||
// expect(response.body.ptr.value).to.eql(null); this will be anything random
|
||||
|
||||
expect(response.body.relay).to.be.an('object');
|
||||
|
||||
@@ -219,6 +219,7 @@ async function testConfig(apiConfig) {
|
||||
|
||||
if ('noHardlinks' in apiConfig && typeof apiConfig.noHardlinks !== 'boolean') throw new BoxError(BoxError.BAD_FIELD, 'noHardlinks must be boolean');
|
||||
if ('chown' in apiConfig && typeof apiConfig.chown !== 'boolean') throw new BoxError(BoxError.BAD_FIELD, 'chown must be boolean');
|
||||
if ('preserveAttributes' in apiConfig && typeof apiConfig.preserveAttributes !== 'boolean') throw new BoxError(BoxError.BAD_FIELD, 'preserveAttributes must be boolean');
|
||||
|
||||
let rootPath; // for managed mounts, this uses 'mountPath', which could be some temporary mount location
|
||||
if (apiConfig.provider === PROVIDER_FILESYSTEM) {
|
||||
|
||||
@@ -142,7 +142,7 @@ function validateUsername(username) {
|
||||
if (/[^a-zA-Z0-9.-]/.test(username)) return new BoxError(BoxError.BAD_FIELD, 'Username can only contain alphanumerals, dot and -');
|
||||
|
||||
// app emails are sent using the .app suffix
|
||||
if (username.indexOf('.app') !== -1) return new BoxError(BoxError.BAD_FIELD, 'Username pattern is reserved for apps');
|
||||
if (username.endsWith('.app')) return new BoxError(BoxError.BAD_FIELD, 'Username pattern is reserved for apps');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user