fix: dns checker

This commit is contained in:
Andras Bacsai 2022-07-25 10:16:25 +00:00
parent fc9bbac372
commit dd2a876a67
6 changed files with 125 additions and 18 deletions

View File

@ -317,12 +317,16 @@ export function getDomain(domain: string): string {
export async function isDomainConfigured({ export async function isDomainConfigured({
id, id,
fqdn, fqdn,
checkOwn = false checkOwn = false,
dockerId = undefined
}: { }: {
id: string; id: string;
fqdn: string; fqdn: string;
checkOwn?: boolean; checkOwn?: boolean;
dockerId: string;
}): Promise<boolean> { }): Promise<boolean> {
console.log({checkOwn, dockerId})
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace('www.', ''); const nakedDomain = domain.replace('www.', '');
const foundApp = await prisma.application.findFirst({ const foundApp = await prisma.application.findFirst({
@ -331,7 +335,10 @@ export async function isDomainConfigured({
{ fqdn: { endsWith: `//${nakedDomain}` } }, { fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } } { fqdn: { endsWith: `//www.${nakedDomain}` } }
], ],
id: { not: id } id: { not: id },
destinationDocker: {
id: dockerId
}
}, },
select: { fqdn: true } select: { fqdn: true }
}); });
@ -343,7 +350,10 @@ export async function isDomainConfigured({
{ minio: { apiFqdn: { endsWith: `//${nakedDomain}` } } }, { minio: { apiFqdn: { endsWith: `//${nakedDomain}` } } },
{ minio: { apiFqdn: { endsWith: `//www.${nakedDomain}` } } } { minio: { apiFqdn: { endsWith: `//www.${nakedDomain}` } } }
], ],
id: { not: checkOwn ? undefined : id } id: { not: checkOwn ? undefined : id },
destinationDocker: {
id: dockerId
}
}, },
select: { fqdn: true } select: { fqdn: true }
}); });
@ -416,16 +426,13 @@ export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): P
} }
} else { } else {
try { try {
console.log({domain})
const ipDomain = await dns.resolve4(domain); const ipDomain = await dns.resolve4(domain);
console.log({ipDomain})
let ipDomainFound = false; let ipDomainFound = false;
for (const ip of ipDomain) { for (const ip of ipDomain) {
if (resolves.includes(ip)) { if (resolves.includes(ip)) {
ipDomainFound = true; ipDomainFound = true;
} }
} }
console.log({ipDomainFound})
if (ipDomainFound) return { status: 200 }; if (ipDomainFound) return { status: 200 };
throw { status: 500, message: `DNS not set correctly or propogated.<br>Please check your DNS settings.` } throw { status: 500, message: `DNS not set correctly or propogated.<br>Please check your DNS settings.` }
} catch (error) { } catch (error) {

View File

@ -368,7 +368,7 @@ export async function checkDNS(request: FastifyRequest<CheckDNS>) {
const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
const { isDNSCheckEnabled } = await prisma.setting.findFirst({}); const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
const found = await isDomainConfigured({ id, fqdn }); const found = await isDomainConfigured({ id, fqdn, dockerId });
if (found) { if (found) {
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
} }

View File

@ -2,13 +2,13 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
import fs from 'fs/promises'; import fs from 'fs/promises';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { prisma, uniqueName, asyncExecShell, getServiceImage, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd, listSettings, getFreeExposedPort } from '../../../../lib/common'; import { prisma, uniqueName, asyncExecShell, getServiceImage, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd, listSettings, getFreeExposedPort, checkDomainsIsValidInDNS } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { checkContainer, dockerInstance, isContainerExited, removeContainer } from '../../../../lib/docker'; import { checkContainer, dockerInstance, isContainerExited, removeContainer } from '../../../../lib/docker';
import cuid from 'cuid'; import cuid from 'cuid';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { ActivateWordpressFtp, CheckService, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types';
// async function startServiceNew(request: FastifyRequest<OnlyId>) { // async function startServiceNew(request: FastifyRequest<OnlyId>) {
// try { // try {
@ -346,22 +346,35 @@ export async function saveServiceSettings(request: FastifyRequest<SaveServiceSet
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function checkServiceDomain(request: FastifyRequest<CheckServiceDomain>) {
try {
const { id } = request.params
const { domain } = request.query
const { fqdn, dualCerts } = await prisma.service.findUnique({ where: { id }})
return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts });
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function checkService(request: FastifyRequest<CheckService>) { export async function checkService(request: FastifyRequest<CheckService>) {
try { try {
const { id } = request.params; const { id } = request.params;
let { fqdn, exposePort, otherFqdns } = request.body; let { fqdn, exposePort, forceSave, otherFqdns, dualCerts } = request.body;
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase()); if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase());
if (exposePort) exposePort = Number(exposePort); if (exposePort) exposePort = Number(exposePort);
let found = await isDomainConfigured({ id, fqdn }); const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } })
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
let found = await isDomainConfigured({ id, fqdn, dockerId });
if (found) { if (found) {
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
} }
if (otherFqdns && otherFqdns.length > 0) { if (otherFqdns && otherFqdns.length > 0) {
for (const ofqdn of otherFqdns) { for (const ofqdn of otherFqdns) {
found = await isDomainConfigured({ id, fqdn: ofqdn, checkOwn: true }); found = await isDomainConfigured({ id, fqdn: ofqdn, dockerId });
if (found) { if (found) {
throw { status: 500, message: `Domain ${getDomain(ofqdn).replace('www.', '')} is already in use!` } throw { status: 500, message: `Domain ${getDomain(ofqdn).replace('www.', '')} is already in use!` }
} }
@ -371,7 +384,7 @@ export async function checkService(request: FastifyRequest<CheckService>) {
if (exposePort < 1024 || exposePort > 65535) { if (exposePort < 1024 || exposePort > 65535) {
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` } throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
} }
const { destinationDocker: { id: dockerId, remoteIpAddress }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } })
if (configuredPort !== exposePort) { if (configuredPort !== exposePort) {
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress); const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
if (availablePort.toString() !== exposePort.toString()) { if (availablePort.toString() !== exposePort.toString()) {
@ -379,6 +392,11 @@ export async function checkService(request: FastifyRequest<CheckService>) {
} }
} }
} }
if (isDNSCheckEnabled && !isDev && !forceSave) {
let hostname = request.hostname.split(':')[0];
if (remoteEngine) hostname = remoteIpAddress;
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
}
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })

View File

@ -3,6 +3,7 @@ import {
activatePlausibleUsers, activatePlausibleUsers,
activateWordpressFtp, activateWordpressFtp,
checkService, checkService,
checkServiceDomain,
deleteService, deleteService,
deleteServiceSecret, deleteServiceSecret,
deleteServiceStorage, deleteServiceStorage,
@ -29,7 +30,7 @@ import {
} from './handlers'; } from './handlers';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { ActivateWordpressFtp, CheckService, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => { fastify.addHook('onRequest', async (request) => {
@ -44,6 +45,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<OnlyId>('/:id/status', async (request) => await getServiceStatus(request)); fastify.get<OnlyId>('/:id/status', async (request) => await getServiceStatus(request));
fastify.get<CheckServiceDomain>('/:id/check', async (request) => await checkServiceDomain(request));
fastify.post<CheckService>('/:id/check', async (request) => await checkService(request)); fastify.post<CheckService>('/:id/check', async (request) => await checkService(request));
fastify.post<SaveServiceSettings>('/:id/settings', async (request, reply) => await saveServiceSettings(request, reply)); fastify.post<SaveServiceSettings>('/:id/settings', async (request, reply) => await saveServiceSettings(request, reply));

View File

@ -25,9 +25,16 @@ export interface SaveServiceSettings extends OnlyId {
dualCerts: boolean dualCerts: boolean
} }
} }
export interface CheckServiceDomain extends OnlyId {
Querystring: {
domain: string
}
}
export interface CheckService extends OnlyId { export interface CheckService extends OnlyId {
Body: { Body: {
fqdn: string, fqdn: string,
forceSave: boolean,
dualCerts: boolean,
exposePort: number, exposePort: number,
otherFqdns: Array<string> otherFqdns: Array<string>
} }

View File

@ -11,7 +11,7 @@
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification, getDomain } from '$lib/common';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { appSession, disabledButton, status, location, setLocation } from '$lib/store'; import { appSession, disabledButton, status, location, setLocation } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
@ -30,28 +30,63 @@
import Moodle from './_Moodle.svelte'; import Moodle from './_Moodle.svelte';
const { id } = $page.params; const { id } = $page.params;
$: isDisabled = $: isDisabled =
!$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading; !$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading;
let forceSave = false;
let loading = false; let loading = false;
let loadingVerification = false; let loadingVerification = false;
let dualCerts = service.dualCerts; let dualCerts = service.dualCerts;
let nonWWWDomain = service.fqdn && getDomain(service.fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
async function isDNSValid(domain: any, isWWW: any) {
try {
await get(`/services/${id}/check?domain=${domain}`);
toast.push('DNS configuration is valid.');
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true;
} catch (error) {
errorNotification(error);
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
return false;
}
}
async function handleSubmit() { async function handleSubmit() {
if (loading) return; if (loading) return;
loading = true; loading = true;
try { try {
await post(`/services/${id}/check`, { await post(`/services/${id}/check`, {
fqdn: service.fqdn, fqdn: service.fqdn,
forceSave,
dualCerts,
otherFqdns: service.minio?.apiFqdn ? [service.minio?.apiFqdn] : [], otherFqdns: service.minio?.apiFqdn ? [service.minio?.apiFqdn] : [],
exposePort: service.exposePort exposePort: service.exposePort
}); });
await post(`/services/${id}`, { ...service }); await post(`/services/${id}`, { ...service });
setLocation(service); setLocation(service);
$disabledButton = false; $disabledButton = false;
forceSave = false;
toast.push('Configuration saved.'); toast.push('Configuration saved.');
} catch (error) { } catch (error) {
//@ts-ignore
if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
if (dualCerts) {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
const isWWW = getDomain(service.fqdn).includes('www.');
if (isWWW) {
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
}
}
}
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; loading = false;
@ -111,8 +146,15 @@
<button <button
type="submit" type="submit"
class:bg-pink-600={!loading} class:bg-pink-600={!loading}
class:bg-orange-600={forceSave}
class:hover:bg-pink-500={!loading} class:hover:bg-pink-500={!loading}
disabled={loading}>{loading ? $t('forms.saving') : $t('forms.save')}</button class:hover:bg-orange-400={forceSave}
disabled={loading}
>{loading
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
> >
{/if} {/if}
{#if service.type === 'plausibleanalytics' && $status.service.isRunning} {#if service.type === 'plausibleanalytics' && $status.service.isRunning}
@ -235,7 +277,38 @@
/> />
</div> </div>
{/if} {/if}
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{/if}
</div>
{/if}
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
disabled={$status.service.isRunning} disabled={$status.service.isRunning}