fix: Better DNS check to prevent errors

This commit is contained in:
Andras Bacsai 2022-05-06 11:41:39 +02:00
parent b0eae8cfe9
commit 7ceb8f1537
7 changed files with 229 additions and 148 deletions

View File

@ -4,6 +4,8 @@ import { dev } from '$app/env';
import * as Sentry from '@sentry/node';
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
import type { Config } from 'unique-names-generator';
import { promises as dns } from 'dns';
import { isIP } from 'is-ip';
import * as db from '$lib/database';
import { buildLogQueue } from './queues';
@ -14,6 +16,7 @@ import Cookie from 'cookie';
import os from 'os';
import type { RequestEvent } from '@sveltejs/kit/types/internal';
import type { Job } from 'bullmq';
import { t } from './translations';
try {
if (!dev) {
@ -179,3 +182,98 @@ export function getDomain(domain: string): string {
export function getOsArch() {
return os.arch();
}
export async function isDNSValid(event: any, domain: string): Promise<any> {
let resolves = [];
try {
if (isIP(event.url.hostname)) {
resolves = [event.url.hostname];
} else {
resolves = await dns.resolve4(event.url.hostname);
}
} catch (error) {
throw {
message:
"Could not resolve domain or it's not pointing to the server IP address. <br><br>Please check your domain name and try again."
};
}
try {
let ipDomainFound = false;
dns.setServers(['1.1.1.1', '8.8.8.8']);
const dnsResolve = await dns.resolve4(domain);
if (dnsResolve.length > 0) {
for (const ip of dnsResolve) {
if (resolves.includes(ip)) {
ipDomainFound = true;
}
}
}
if (!ipDomainFound) throw false;
} catch (error) {
throw {
message: t.get('application.domain_not_valid')
};
}
}
export async function checkDomainsIsValidInDNS({ event, fqdn, dualCerts }): Promise<any> {
const domain = getDomain(fqdn);
const domainDualCert = domain.includes('www.') ? domain.replace('www.', '') : `www.${domain}`;
dns.setServers(['1.1.1.1', '8.8.8.8']);
let resolves = [];
try {
if (isIP(event.url.hostname)) {
resolves = [event.url.hostname];
} else {
resolves = await dns.resolve4(event.url.hostname);
}
} catch (error) {
throw {
message: t.get('application.dns_not_set_error', { domain: domain })
};
}
if (dualCerts) {
try {
const ipDomain = await dns.resolve4(domain);
const ipDomainDualCert = await dns.resolve4(domainDualCert);
let ipDomainFound = false;
let ipDomainDualCertFound = false;
for (const ip of ipDomain) {
if (resolves.includes(ip)) {
ipDomainFound = true;
}
}
for (const ip of ipDomainDualCert) {
if (resolves.includes(ip)) {
ipDomainDualCertFound = true;
}
}
if (ipDomainFound && ipDomainDualCertFound) return { status: 200 };
throw false;
} catch (error) {
throw {
message: t.get('application.dns_not_set_error', { domain })
};
}
} else {
try {
const ipDomain = await dns.resolve4(domain);
let ipDomainFound = false;
for (const ip of ipDomain) {
if (resolves.includes(ip)) {
ipDomainFound = true;
}
}
if (ipDomainFound) return { status: 200 };
throw false;
} catch (error) {
throw {
message: t.get('application.dns_not_set_error', { domain })
};
}
}
}

View File

@ -178,9 +178,11 @@
"delete_application": "Delete application",
"permission_denied_delete_application": "You do not have permission to delete this application",
"domain_already_in_use": "Domain {{domain}} is already used.",
"dns_not_set_error": "DNS not set or propogated for {{domain}}.<br><br>Please check your DNS settings.",
"dns_not_set_error": "DNS not set correctly or propogated for {{domain}}.<br><br>Please check your DNS settings.",
"domain_required": "Domain is required.",
"settings_saved": "Settings saved.",
"dns_not_set_partial_error": "DNS not set",
"domain_not_valid": "Domain is not valid.",
"git_source": "Git Source",
"git_repository": "Git Repository",
"build_pack": "Build Pack",

View File

@ -117,7 +117,7 @@ const cron = async (): Promise<void> => {
await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
if (!dev) await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });
await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } });
if (!dev) await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } });
};
cron().catch((error) => {
console.log('cron failed to start');

View File

@ -1,5 +1,5 @@
import { dev } from '$app/env';
import { getDomain, getUserDetails } from '$lib/common';
import { checkDomainsIsValidInDNS, getDomain, getUserDetails, isDNSValid } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
@ -7,16 +7,38 @@ import { promises as dns } from 'dns';
import getPort from 'get-port';
import { t } from '$lib/translations';
export const get: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const domain = event.url.searchParams.get('domain');
if (!domain) {
return {
status: 500,
body: {
message: t.get('application.domain_required')
}
};
}
try {
await isDNSValid(event, domain);
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let { exposePort, fqdn, forceSave } = await event.request.json();
let { exposePort, fqdn, forceSave, dualCerts } = await event.request.json();
fqdn = fqdn.toLowerCase();
try {
const domain = getDomain(fqdn);
const { isDNSCheckEnabled } = await db.prisma.setting.findFirst({});
const found = await db.isDomainConfigured({ id, fqdn });
if (found) {
throw {
@ -25,26 +47,6 @@ export const post: RequestHandler = async (event) => {
})
};
}
if (!dev && !forceSave) {
let ip = [];
let localIp = [];
dns.setServers(['1.1.1.1', '8.8.8.8']);
try {
localIp = await dns.resolve4(event.url.hostname);
} catch (error) {}
try {
ip = await dns.resolve4(domain);
} catch (error) {}
if (localIp?.length > 0) {
if (ip?.length === 0 || !ip.includes(localIp[0])) {
throw {
message: t.get('application.dns_not_set_error', { domain: domain })
};
}
}
}
if (exposePort) {
exposePort = Number(exposePort);
@ -59,6 +61,10 @@ export const post: RequestHandler = async (event) => {
}
}
if (isDNSCheckEnabled && !forceSave) {
return await checkDomainsIsValidInDNS({ event, fqdn, dualCerts });
}
return {
status: 200
};

View File

@ -45,9 +45,9 @@
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import type Prisma from '@prisma/client';
import { notNodeDeployments, staticDeployments } from '$lib/components/common';
import { getDomain, notNodeDeployments, staticDeployments } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast';
import { post } from '$lib/api';
import { get, post } from '$lib/api';
import cuid from 'cuid';
import { browser } from '$app/env';
import { disabledButton } from '$lib/store';
@ -63,6 +63,8 @@
let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy;
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
let wsgis = [
{
value: 'None',
@ -127,13 +129,16 @@
async function handleSubmit() {
loading = true;
try {
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
await post(`/applications/${id}/check.json`, {
fqdn: application.fqdn,
forceSave,
dualCerts,
exposePort: application.exposePort
});
await post(`/applications/${id}.json`, { ...application });
$disabledButton = false;
forceSave = false;
return toast.push('Configurations saved.');
} catch ({ error }) {
if (error?.startsWith($t('application.dns_not_set_partial_error'))) {
@ -155,6 +160,14 @@
application.baseBuildImage = event.detail.value;
await handleSubmit();
}
async function isDNSValid(domain) {
try {
await get(`/applications/${id}/check.json?domain=${domain}`);
toast.push('Domain is valid in DNS.');
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
@ -387,16 +400,34 @@
{/if}
<Explainer text={$t('application.https_explainer')} />
</div>
<input
readonly={!$session.isAdmin || isRunning}
disabled={!$session.isAdmin || isRunning}
bind:this={domainEl}
name="fqdn"
id="fqdn"
bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
/>
<div>
<input
readonly={!$session.isAdmin || isRunning}
disabled={!$session.isAdmin || isRunning}
bind:this={domainEl}
name="fqdn"
id="fqdn"
bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
/>
{#if forceSave}
<div class="pt-4">
<button
class="bg-coollabs hover:bg-coollabs-100"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain))}
>Check {nonWWWDomain} DNS Record</button
>
{#if dualCerts}
<button
class="bg-coollabs hover:bg-coollabs-100"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`))}
>Check www.{nonWWWDomain} DNS Record</button
>
{/if}
</div>
{/if}
</div>
</div>
<div class="grid grid-cols-2 items-center pb-8">
<Setting

View File

@ -1,11 +1,31 @@
import { dev } from '$app/env';
import { asyncExecShell, getDomain, getEngine, getUserDetails } from '$lib/common';
import { checkDomainsIsValidInDNS, getDomain, getUserDetails, isDNSValid } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { t } from '$lib/translations';
import { promises as dns } from 'dns';
import type { RequestHandler } from '@sveltejs/kit';
import { isIP } from 'is-ip';
export const get: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const domain = event.url.searchParams.get('domain');
if (!domain) {
return {
status: 500,
body: {
message: t.get('application.domain_required')
}
};
}
try {
await isDNSValid(event, domain);
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
@ -14,11 +34,9 @@ export const post: RequestHandler = async (event) => {
let { fqdn, forceSave, dualCerts, isDNSCheckEnabled } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
try {
const domain = getDomain(fqdn);
const domainDualCert = domain.includes('www.') ? domain.replace('www.', '') : `www.${domain}`;
const found = await db.isDomainConfigured({ id, fqdn });
console.log(found);
if (found) {
throw {
message: t.get('application.domain_already_in_use', {
@ -26,111 +44,9 @@ export const post: RequestHandler = async (event) => {
})
};
}
if (isDNSCheckEnabled) {
if (!forceSave) {
dns.setServers(['1.1.1.1', '8.8.8.8']);
if (dualCerts) {
try {
const ipDomain = await dns.resolve4(domain);
const ipDomainDualCert = await dns.resolve4(domainDualCert);
console.log({ ipDomain, ipDomainDualCert });
// TODO: Check if ipDomain and ipDomainDualCert are the same, but not like this
if (
ipDomain.length === ipDomainDualCert.length &&
ipDomain.every((v) => ipDomainDualCert.indexOf(v) >= 0)
) {
let resolves = [];
if (isIP(event.url.hostname)) {
resolves = [event.url.hostname];
} else {
resolves = await dns.resolve4(event.url.hostname);
}
console.log({ resolves });
for (const ip of ipDomain) {
if (resolves.includes(ip)) {
return {
status: 200
};
}
}
for (const ip of ipDomainDualCert) {
if (resolves.includes(ip)) {
return {
status: 200
};
}
}
throw false;
} else {
throw false;
}
} catch (error) {
console.log(error);
throw {
message: t.get('application.dns_not_set_error', { domain })
};
}
} else {
let resolves = [];
try {
const ipDomain = await dns.resolve4(domain);
console.log({ ipDomain });
if (isIP(event.url.hostname)) {
resolves = [event.url.hostname];
} else {
resolves = await dns.resolve4(event.url.hostname);
}
console.log({ resolves });
for (const ip of ipDomain) {
if (resolves.includes(ip)) {
return {
status: 200
};
}
}
throw false;
} catch (error) {
console.log(error);
throw {
message: t.get('application.dns_not_set_error', { domain })
};
}
}
// let localReverseDomains = [];
// let newIps = [];
// let newIpsWWW = [];
// let localIps = [];
// try {
// localReverseDomains = await dns.reverse(event.url.hostname)
// } catch (error) { }
// try {
// localIps = await dns.resolve4(event.url.hostname);
// } catch (error) { }
// try {
// newIps = await dns.resolve4(domain);
// if (dualCerts) {
// newIpsWWW = await dns.resolve4(`${isWWW ? nonWWW : domain}`)
// }
// console.log(newIps)
// } catch (error) { }
// console.log({ localIps, newIps, localReverseDomains, dualCerts, isWWW, nonWWW })
// if (localReverseDomains?.length > 0) {
// if ((newIps?.length === 0 || !newIps.includes(event.url.hostname)) || (dualCerts && newIpsWWW?.length === 0 && !newIpsWWW.includes(`${isWWW ? nonWWW : domain}`))) {
// throw {
// message: t.get('application.dns_not_set_error', { domain })
// };
// }
// }
// if (localIps?.length > 0) {
// if (newIps?.length === 0 || !localIps.includes(newIps[0])) {
// throw {
// message: t.get('application.dns_not_set_error', { domain })
// };
// }
// }
}
if (isDNSCheckEnabled && !forceSave) {
return await checkDomainsIsValidInDNS({ event, fqdn, dualCerts });
}
return {
status: 200
};

View File

@ -31,7 +31,7 @@
import Setting from '$lib/components/Setting.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
import { del, post } from '$lib/api';
import { del, get, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { browser } from '$app/env';
import { getDomain } from '$lib/components/common';
@ -49,6 +49,7 @@
let forceSave = false;
let fqdn = settings.fqdn;
let nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
let isFqdnSet = !!settings.fqdn;
let loading = {
save: false,
@ -96,6 +97,7 @@
async function handleSubmit() {
try {
loading.save = true;
nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
if (fqdn !== settings.fqdn) {
await post(`/settings/check.json`, { fqdn, forceSave, dualCerts, isDNSCheckEnabled });
await post(`/settings.json`, { fqdn });
@ -106,6 +108,7 @@
settings.minPort = minPort;
settings.maxPort = maxPort;
}
forceSave = false;
} catch ({ error }) {
if (error?.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
@ -123,6 +126,14 @@
return errorNotification(error);
}
}
async function isDNSValid(domain) {
try {
await get(`/settings/check.json?domain=${domain}`);
toast.push('Domain is valid in DNS.');
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
@ -176,6 +187,23 @@
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="{$t('forms.eg')}: https://coolify.io"
/>
{#if forceSave}
<div class="pt-4">
<button
class="bg-coollabs hover:bg-coollabs-100"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain))}
>Check {nonWWWDomain} DNS Record</button
>
{#if dualCerts}
<button
class="bg-coollabs hover:bg-coollabs-100"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`))}
>Check www.{nonWWWDomain} DNS Record</button
>
{/if}
</div>
{/if}
</div>
</div>
<div class="grid grid-cols-2 items-start py-6">