mirror of https://github.com/langgenius/dify.git
feat(validation): implement isPrivateOrLocalAddress utility and integrate into webhook components for improved URL validation
This commit is contained in:
parent
6e76f2aff2
commit
ce56286329
|
|
@ -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')],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue