From 7ceb8f1537f5050d07770dec443d98e2fdc5fb82 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 6 May 2022 11:41:39 +0200 Subject: [PATCH] fix: Better DNS check to prevent errors --- src/lib/common.ts | 98 +++++++++++++++ src/lib/locales/en.json | 4 +- src/lib/queues/index.ts | 2 +- src/routes/applications/[id]/check.json.ts | 52 ++++---- src/routes/applications/[id]/index.svelte | 55 +++++++-- src/routes/settings/check.json.ts | 136 ++++----------------- src/routes/settings/index.svelte | 30 ++++- 7 files changed, 229 insertions(+), 148 deletions(-) diff --git a/src/lib/common.ts b/src/lib/common.ts index acbe6c88c..78da1338f 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -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 { + 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.

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 { + 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 }) + }; + } + } +} diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index 313fc44af..f6c4ff1e6 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -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}}.

Please check your DNS settings.", + "dns_not_set_error": "DNS not set correctly or propogated for {{domain}}.

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", diff --git a/src/lib/queues/index.ts b/src/lib/queues/index.ts index cc340b883..b35a058d7 100644 --- a/src/lib/queues/index.ts +++ b/src/lib/queues/index.ts @@ -117,7 +117,7 @@ const cron = async (): Promise => { 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'); diff --git a/src/routes/applications/[id]/check.json.ts b/src/routes/applications/[id]/check.json.ts index 8c4b12af1..0960014fc 100644 --- a/src/routes/applications/[id]/check.json.ts +++ b/src/routes/applications/[id]/check.json.ts @@ -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 }; diff --git a/src/routes/applications/[id]/index.svelte b/src/routes/applications/[id]/index.svelte index 71cfa3dc3..02a158fc5 100644 --- a/src/routes/applications/[id]/index.svelte +++ b/src/routes/applications/[id]/index.svelte @@ -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); + } + }
@@ -387,16 +400,34 @@ {/if}
- +
+ + {#if forceSave} +
+ + {#if dualCerts} + + {/if} +
+ {/if} +
{ + 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 }; diff --git a/src/routes/settings/index.svelte b/src/routes/settings/index.svelte index 1a4f25801..7d3f7aad3 100644 --- a/src/routes/settings/index.svelte +++ b/src/routes/settings/index.svelte @@ -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); + } + }
@@ -176,6 +187,23 @@ pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$" placeholder="{$t('forms.eg')}: https://coolify.io" /> + + {#if forceSave} +
+ + {#if dualCerts} + + {/if} +
+ {/if}