feat(validation): implement isPrivateOrLocalAddress utility and integrate into webhook components for improved URL validation

This commit is contained in:
zhsama 2025-11-04 18:32:19 +08:00
parent 6e76f2aff2
commit ce56286329
5 changed files with 81 additions and 62 deletions

View File

@ -18,6 +18,7 @@ import {
useVerifyTriggerSubscriptionBuilder,
} from '@/service/use-triggers'
import { parsePluginErrorMessage } from '@/utils/error-parser'
import { isPrivateOrLocalAddress } from '@/utils/urlValidation'
import { RiLoader2Line } from '@remixicon/react'
import { debounce } from 'lodash-es'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@ -66,43 +67,6 @@ const normalizeFormType = (type: FormTypeEnum | string): FormTypeEnum => {
}
}
// Check if URL is a private/local network address
const isPrivateOrLocalAddress = (url: string): boolean => {
try {
const urlObj = new URL(url)
const hostname = urlObj.hostname.toLowerCase()
// Check for localhost
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1')
return true
// Check for private IP ranges
const ipv4Regex = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
const ipv4Match = hostname.match(ipv4Regex)
if (ipv4Match) {
const [, a, b] = ipv4Match.map(Number)
// 10.0.0.0/8
if (a === 10)
return true
// 172.16.0.0/12
if (a === 172 && b >= 16 && b <= 31)
return true
// 192.168.0.0/16
if (a === 192 && b === 168)
return true
// 169.254.0.0/16 (link-local)
if (a === 169 && b === 254)
return true
}
// Check for .local domains
return hostname.endsWith('.local')
}
catch {
return false
}
}
const StatusStep = ({ isActive, text }: { isActive: boolean, text: string }) => {
return <div className={`system-2xs-semibold-uppercase flex items-center gap-1 ${isActive
? 'text-state-accent-solid'
@ -193,6 +157,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
if (form)
form.setFieldValue('callback_url', subscriptionBuilder.endpoint)
if (isPrivateOrLocalAddress(subscriptionBuilder.endpoint)) {
console.log('isPrivateOrLocalAddress', isPrivateOrLocalAddress(subscriptionBuilder.endpoint))
subscriptionFormRef.current?.setFields([{
name: 'callback_url',
warnings: [t('pluginTrigger.modal.form.callbackUrl.privateAddressWarning')],

View File

@ -18,6 +18,7 @@ import { SimpleSelect } from '@/app/components/base/select'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
import copy from 'copy-to-clipboard'
import { isPrivateOrLocalAddress } from '@/utils/urlValidation'
const i18nPrefix = 'workflow.nodes.triggerWebhook'
@ -103,33 +104,40 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
</div>
</div>
{inputs.webhook_debug_url && (
<Tooltip
popupContent={debugUrlCopied ? t(`${i18nPrefix}.debugUrlCopied`) : t(`${i18nPrefix}.debugUrlCopy`)}
popupClassName="system-xs-regular text-text-primary bg-components-tooltip-bg border border-components-panel-border shadow-lg backdrop-blur-sm rounded-md px-1.5 py-1"
position="top"
offset={{ mainAxis: -20 }}
needsDelay={true}
>
<div
className="flex cursor-pointer gap-1.5 rounded-lg px-1 py-1.5 transition-colors"
style={{ width: '368px', height: '38px' }}
onClick={() => {
copy(inputs.webhook_debug_url || '')
setDebugUrlCopied(true)
setTimeout(() => setDebugUrlCopied(false), 2000)
}}
<div className="space-y-2">
<Tooltip
popupContent={debugUrlCopied ? t(`${i18nPrefix}.debugUrlCopied`) : t(`${i18nPrefix}.debugUrlCopy`)}
popupClassName="system-xs-regular text-text-primary bg-components-tooltip-bg border border-components-panel-border shadow-lg backdrop-blur-sm rounded-md px-1.5 py-1"
position="top"
offset={{ mainAxis: -20 }}
needsDelay={true}
>
<div className="mt-0.5 w-0.5 bg-divider-regular" style={{ height: '28px' }}></div>
<div className="flex-1" style={{ width: '352px', height: '32px' }}>
<div className="text-xs leading-4 text-text-tertiary">
{t(`${i18nPrefix}.debugUrlTitle`)}
</div>
<div className="truncate text-xs leading-4 text-text-primary">
{inputs.webhook_debug_url}
<div
className="flex cursor-pointer gap-1.5 rounded-lg px-1 py-1.5 transition-colors"
style={{ width: '368px', height: '38px' }}
onClick={() => {
copy(inputs.webhook_debug_url || '')
setDebugUrlCopied(true)
setTimeout(() => setDebugUrlCopied(false), 2000)
}}
>
<div className="mt-0.5 w-0.5 bg-divider-regular" style={{ height: '28px' }}></div>
<div className="flex-1" style={{ width: '352px', height: '32px' }}>
<div className="text-xs leading-4 text-text-tertiary">
{t(`${i18nPrefix}.debugUrlTitle`)}
</div>
<div className="truncate text-xs leading-4 text-text-primary">
{inputs.webhook_debug_url}
</div>
</div>
</div>
</div>
</Tooltip>
</Tooltip>
{isPrivateOrLocalAddress(inputs.webhook_debug_url) && (
<div className="system-xs-regular mt-1 px-0 py-[2px] text-text-warning">
{t(`${i18nPrefix}.debugUrlPrivateAddressWarning`)}
</div>
)}
</div>
)}
</div>
</Field>

View File

@ -153,7 +153,7 @@ const translation = {
description: 'This URL will receive webhook events',
tooltip: 'Provide a publicly accessible endpoint that can receive callback requests from the trigger provider.',
placeholder: 'Generating...',
privateAddressWarning: 'This URL appears to be an internal address, which may cause webhook requests to fail.',
privateAddressWarning: 'This URL appears to be an internal address, which may cause webhook requests to fail. You may change TRIGGER_URL to a public address.',
},
},
errors: {

View File

@ -1144,6 +1144,7 @@ const translation = {
debugUrlTitle: 'For test runs, always use this URL',
debugUrlCopy: 'Click to copy',
debugUrlCopied: 'Copied!',
debugUrlPrivateAddressWarning: 'This URL appears to be an internal address, which may cause webhook requests to fail. You may change TRIGGER_URL to a public address.',
errorHandling: 'Error Handling',
errorStrategy: 'Error Handling',
responseConfiguration: 'Response',

View File

@ -21,3 +21,48 @@ export function validateRedirectUrl(url: string): void {
throw new Error(`Invalid URL: ${url}`)
}
}
/**
* Check if URL is a private/local network address or cloud debug URL
* @param url - The URL string to check
* @returns true if the URL is a private/local address or cloud debug URL
*/
export function isPrivateOrLocalAddress(url: string): boolean {
try {
const urlObj = new URL(url)
const hostname = urlObj.hostname.toLowerCase()
// Check for Dify cloud trigger debug URLs
if (hostname === 'cloud-trigger.dify.dev')
return true
// Check for localhost
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1')
return true
// Check for private IP ranges
const ipv4Regex = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
const ipv4Match = hostname.match(ipv4Regex)
if (ipv4Match) {
const [, a, b] = ipv4Match.map(Number)
// 10.0.0.0/8
if (a === 10)
return true
// 172.16.0.0/12
if (a === 172 && b >= 16 && b <= 31)
return true
// 192.168.0.0/16
if (a === 192 && b === 168)
return true
// 169.254.0.0/16 (link-local)
if (a === 169 && b === 254)
return true
}
// Check for .local domains
return hostname.endsWith('.local')
}
catch {
return false
}
}