mirror of
https://github.com/langgenius/dify.git
synced 2026-04-26 02:06:35 +08:00
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,
|
useVerifyTriggerSubscriptionBuilder,
|
||||||
} from '@/service/use-triggers'
|
} from '@/service/use-triggers'
|
||||||
import { parsePluginErrorMessage } from '@/utils/error-parser'
|
import { parsePluginErrorMessage } from '@/utils/error-parser'
|
||||||
|
import { isPrivateOrLocalAddress } from '@/utils/urlValidation'
|
||||||
import { RiLoader2Line } from '@remixicon/react'
|
import { RiLoader2Line } from '@remixicon/react'
|
||||||
import { debounce } from 'lodash-es'
|
import { debounce } from 'lodash-es'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
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 }) => {
|
const StatusStep = ({ isActive, text }: { isActive: boolean, text: string }) => {
|
||||||
return <div className={`system-2xs-semibold-uppercase flex items-center gap-1 ${isActive
|
return <div className={`system-2xs-semibold-uppercase flex items-center gap-1 ${isActive
|
||||||
? 'text-state-accent-solid'
|
? 'text-state-accent-solid'
|
||||||
@ -193,6 +157,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||||||
if (form)
|
if (form)
|
||||||
form.setFieldValue('callback_url', subscriptionBuilder.endpoint)
|
form.setFieldValue('callback_url', subscriptionBuilder.endpoint)
|
||||||
if (isPrivateOrLocalAddress(subscriptionBuilder.endpoint)) {
|
if (isPrivateOrLocalAddress(subscriptionBuilder.endpoint)) {
|
||||||
|
console.log('isPrivateOrLocalAddress', isPrivateOrLocalAddress(subscriptionBuilder.endpoint))
|
||||||
subscriptionFormRef.current?.setFields([{
|
subscriptionFormRef.current?.setFields([{
|
||||||
name: 'callback_url',
|
name: 'callback_url',
|
||||||
warnings: [t('pluginTrigger.modal.form.callbackUrl.privateAddressWarning')],
|
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 Toast from '@/app/components/base/toast'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
|
import { isPrivateOrLocalAddress } from '@/utils/urlValidation'
|
||||||
|
|
||||||
const i18nPrefix = 'workflow.nodes.triggerWebhook'
|
const i18nPrefix = 'workflow.nodes.triggerWebhook'
|
||||||
|
|
||||||
@ -103,33 +104,40 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{inputs.webhook_debug_url && (
|
{inputs.webhook_debug_url && (
|
||||||
<Tooltip
|
<div className="space-y-2">
|
||||||
popupContent={debugUrlCopied ? t(`${i18nPrefix}.debugUrlCopied`) : t(`${i18nPrefix}.debugUrlCopy`)}
|
<Tooltip
|
||||||
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"
|
popupContent={debugUrlCopied ? t(`${i18nPrefix}.debugUrlCopied`) : t(`${i18nPrefix}.debugUrlCopy`)}
|
||||||
position="top"
|
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"
|
||||||
offset={{ mainAxis: -20 }}
|
position="top"
|
||||||
needsDelay={true}
|
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="mt-0.5 w-0.5 bg-divider-regular" style={{ height: '28px' }}></div>
|
<div
|
||||||
<div className="flex-1" style={{ width: '352px', height: '32px' }}>
|
className="flex cursor-pointer gap-1.5 rounded-lg px-1 py-1.5 transition-colors"
|
||||||
<div className="text-xs leading-4 text-text-tertiary">
|
style={{ width: '368px', height: '38px' }}
|
||||||
{t(`${i18nPrefix}.debugUrlTitle`)}
|
onClick={() => {
|
||||||
</div>
|
copy(inputs.webhook_debug_url || '')
|
||||||
<div className="truncate text-xs leading-4 text-text-primary">
|
setDebugUrlCopied(true)
|
||||||
{inputs.webhook_debug_url}
|
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>
|
</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>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@ -153,7 +153,7 @@ const translation = {
|
|||||||
description: 'This URL will receive webhook events',
|
description: 'This URL will receive webhook events',
|
||||||
tooltip: 'Provide a publicly accessible endpoint that can receive callback requests from the trigger provider.',
|
tooltip: 'Provide a publicly accessible endpoint that can receive callback requests from the trigger provider.',
|
||||||
placeholder: 'Generating...',
|
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: {
|
errors: {
|
||||||
|
|||||||
@ -1144,6 +1144,7 @@ const translation = {
|
|||||||
debugUrlTitle: 'For test runs, always use this URL',
|
debugUrlTitle: 'For test runs, always use this URL',
|
||||||
debugUrlCopy: 'Click to copy',
|
debugUrlCopy: 'Click to copy',
|
||||||
debugUrlCopied: 'Copied!',
|
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',
|
errorHandling: 'Error Handling',
|
||||||
errorStrategy: 'Error Handling',
|
errorStrategy: 'Error Handling',
|
||||||
responseConfiguration: 'Response',
|
responseConfiguration: 'Response',
|
||||||
|
|||||||
@ -21,3 +21,48 @@ export function validateRedirectUrl(url: string): void {
|
|||||||
throw new Error(`Invalid URL: ${url}`)
|
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
Block a user