diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx index dd5309559f..00c0160056 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx @@ -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
{ 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')], diff --git a/web/app/components/workflow/nodes/trigger-webhook/panel.tsx b/web/app/components/workflow/nodes/trigger-webhook/panel.tsx index 52b0066adf..1de18bd806 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/panel.tsx +++ b/web/app/components/workflow/nodes/trigger-webhook/panel.tsx @@ -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> = ({
{inputs.webhook_debug_url && ( - -
{ - copy(inputs.webhook_debug_url || '') - setDebugUrlCopied(true) - setTimeout(() => setDebugUrlCopied(false), 2000) - }} +
+ -
-
-
- {t(`${i18nPrefix}.debugUrlTitle`)} -
-
- {inputs.webhook_debug_url} +
{ + copy(inputs.webhook_debug_url || '') + setDebugUrlCopied(true) + setTimeout(() => setDebugUrlCopied(false), 2000) + }} + > +
+
+
+ {t(`${i18nPrefix}.debugUrlTitle`)} +
+
+ {inputs.webhook_debug_url} +
-
- + + {isPrivateOrLocalAddress(inputs.webhook_debug_url) && ( +
+ {t(`${i18nPrefix}.debugUrlPrivateAddressWarning`)} +
+ )} +
)}
diff --git a/web/i18n/en-US/plugin-trigger.ts b/web/i18n/en-US/plugin-trigger.ts index bf1787d2c7..aedd0c6225 100644 --- a/web/i18n/en-US/plugin-trigger.ts +++ b/web/i18n/en-US/plugin-trigger.ts @@ -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: { diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index d8de0eabaf..77e71b0973 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -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', diff --git a/web/utils/urlValidation.ts b/web/utils/urlValidation.ts index abc15a1365..80b72e4e0a 100644 --- a/web/utils/urlValidation.ts +++ b/web/utils/urlValidation.ts @@ -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 + } +}