diff --git a/src/GZCTF/ClientApp/src/components/ChallengeDetailModal.tsx b/src/GZCTF/ClientApp/src/components/ChallengeDetailModal.tsx index c8d30f89d..22c21f691 100644 --- a/src/GZCTF/ClientApp/src/components/ChallengeDetailModal.tsx +++ b/src/GZCTF/ClientApp/src/components/ChallengeDetailModal.tsx @@ -7,7 +7,6 @@ import { Box, Button, Card, - Code, Divider, Group, LoadingOverlay, @@ -18,19 +17,18 @@ import { Text, TextInput, Title, - Tooltip, } from '@mantine/core' -import { useClipboard, useDisclosure, useInputState } from '@mantine/hooks' +import { useDisclosure, useInputState } from '@mantine/hooks' import { notifications, showNotification, updateNotification } from '@mantine/notifications' import { mdiCheck, mdiClose, mdiDownload, mdiLightbulbOnOutline, mdiLoading } from '@mdi/js' import { Icon } from '@mdi/react' import MarkdownRender, { InlineMarkdownRender } from '@Components/MarkdownRender' import { showErrorNotification } from '@Utils/ApiErrorHandler' import { ChallengeTagItemProps } from '@Utils/Shared' -import { useTooltipStyles } from '@Utils/ThemeOverride' import { OnceSWRConfig } from '@Utils/useConfig' import { useTypographyStyles } from '@Utils/useTypographyStyles' import api, { AnswerResult, ChallengeType } from '@Api' +import InstanceEntry from './InstanceEntry' interface ChallengeDetailModalProps extends ModalProps { gameId: number @@ -130,15 +128,10 @@ const ChallengeDetailModal: FC = (props) => { setPlaceholder(FlagPlaceholders[Math.floor(Math.random() * FlagPlaceholders.length)]) }, [challengeId]) - const instanceCloseTime = dayjs(challenge?.context?.closeTime ?? 0) - const instanceLeft = instanceCloseTime.diff(dayjs(), 'minute') - const isDynamic = challenge?.type === ChallengeType.StaticContainer || challenge?.type === ChallengeType.DynamicContainer const { classes, theme } = useTypographyStyles() - const { classes: tooltipClasses } = useTooltipStyles() - const clipBoard = useClipboard() const [disabled, setDisabled] = useState(false) const [onSubmitting, setOnSubmitting] = useState(false) @@ -404,41 +397,12 @@ const ChallengeDetailModal: FC = (props) => { )} {isDynamic && challenge?.context?.instanceEntry && ( - - - - 实例访问入口: - - { - clipBoard.copy(challenge.context?.instanceEntry ?? '') - showNotification({ - color: 'teal', - message: '实例入口已复制到剪贴板', - icon: , - }) - }} - > - {challenge?.context?.instanceEntry} - - - - - - - - - - + )} diff --git a/src/GZCTF/ClientApp/src/components/InstanceEntry.tsx b/src/GZCTF/ClientApp/src/components/InstanceEntry.tsx new file mode 100644 index 000000000..84f94e96c --- /dev/null +++ b/src/GZCTF/ClientApp/src/components/InstanceEntry.tsx @@ -0,0 +1,85 @@ +import dayjs from 'dayjs' +import { FC } from 'react' +import { Stack, Text, Button, Group, Code, Tooltip, Anchor } from '@mantine/core' +import { useClipboard } from '@mantine/hooks' +import { showNotification } from '@mantine/notifications' +import { mdiCheck } from '@mdi/js' +import { Icon } from '@mdi/react' +import { getProxyUrl } from '@Utils/Shared' +import { useTooltipStyles } from '@Utils/ThemeOverride' +import { ClientFlagContext } from '@Api' +import { Countdown } from './ChallengeDetailModal' + +interface InstanceEntryProps { + context: ClientFlagContext + disabled: boolean + onProlong: () => void + onDestroy: () => void +} + +export const InstanceEntry: FC = (props) => { + const { context, onProlong, disabled, onDestroy } = props + const clipBoard = useClipboard() + const instanceCloseTime = dayjs(context.closeTime ?? 0) + const instanceLeft = instanceCloseTime.diff(dayjs(), 'minute') + const { classes: tooltipClasses, theme } = useTooltipStyles() + + const instanceEntry = context.instanceEntry ?? '' + const isPlatfromProxy = instanceEntry.length === 36 && !instanceEntry.includes(':') + const copyEntry = isPlatfromProxy ? getProxyUrl(instanceEntry) : instanceEntry + + return ( + + + + 实例入口: + + { + clipBoard.copy(copyEntry) + showNotification({ + color: 'teal', + title: isPlatfromProxy ? '实例入口已复制到剪贴板' : undefined, + message: isPlatfromProxy ? '请使用客户端进行访问' : '实例入口已复制到剪贴板', + icon: , + }) + }} + > + {instanceEntry} + + + + + + {isPlatfromProxy && ( + + + 获取客户端: + + WebSocketReflectorX + + + + )} + + + + + + ) +} + +export default InstanceEntry diff --git a/src/GZCTF/ClientApp/src/components/admin/ChallengePreviewModal.tsx b/src/GZCTF/ClientApp/src/components/admin/ChallengePreviewModal.tsx index 3b2e346e9..6f04266fc 100644 --- a/src/GZCTF/ClientApp/src/components/admin/ChallengePreviewModal.tsx +++ b/src/GZCTF/ClientApp/src/components/admin/ChallengePreviewModal.tsx @@ -5,7 +5,6 @@ import { ActionIcon, Box, Button, - Code, Divider, Group, LoadingOverlay, @@ -16,16 +15,15 @@ import { Text, TextInput, Title, - Tooltip, } from '@mantine/core' import { useDisclosure, useInputState } from '@mantine/hooks' import { showNotification } from '@mantine/notifications' import { mdiCheck, mdiDownload, mdiLightbulbOnOutline } from '@mdi/js' import { Icon } from '@mdi/react' -import { Countdown, FlagPlaceholders } from '@Components/ChallengeDetailModal' +import { FlagPlaceholders } from '@Components/ChallengeDetailModal' +import InstanceEntry from '@Components/InstanceEntry' import MarkdownRender, { InlineMarkdownRender } from '@Components/MarkdownRender' import { ChallengeTagItemProps } from '@Utils/Shared' -import { useTooltipStyles } from '@Utils/ThemeOverride' import { useTypographyStyles } from '@Utils/useTypographyStyles' import { ChallengeType, ChallengeUpdateModel, FileType } from '@Api' @@ -42,9 +40,8 @@ const ChallengePreviewModal: FC = (props) => { const [placeholder, setPlaceholder] = useState('') const [flag, setFlag] = useInputState('') - const [withContainer, setWithContainer] = useState(false) const [startTime, setStartTime] = useState(dayjs()) - const { classes: tooltipClasses } = useTooltipStyles() + const [withContainer, setWithContainer] = useState(false) const onSubmit = (event: React.FormEvent) => { event.preventDefault() @@ -183,33 +180,15 @@ const ChallengePreviewModal: FC = (props) => { )} {isDynamic && withContainer && ( - - - - 实例访问入口: - - - localhost:2333 - - - - - - - - - - + {}} + onDestroy={() => setWithContainer(false)} + /> )} diff --git a/src/GZCTF/ClientApp/src/pages/admin/Instances.tsx b/src/GZCTF/ClientApp/src/pages/admin/Instances.tsx index f0d1d7140..e19607016 100644 --- a/src/GZCTF/ClientApp/src/pages/admin/Instances.tsx +++ b/src/GZCTF/ClientApp/src/pages/admin/Instances.tsx @@ -27,7 +27,7 @@ import { Icon } from '@mdi/react' import { ActionIconWithConfirm } from '@Components/ActionIconWithConfirm' import AdminPage from '@Components/admin/AdminPage' import { showErrorNotification } from '@Utils/ApiErrorHandler' -import { ChallengeTagLabelMap } from '@Utils/Shared' +import { ChallengeTagLabelMap, getProxyUrl } from '@Utils/Shared' import { useTableStyles, useTooltipStyles } from '@Utils/ThemeOverride' import api, { ChallengeModel, ChallengeTag, TeamModel } from '@Api' @@ -205,8 +205,8 @@ const Instances: FC = () => { 队伍 题目 - 容器 Id 生命周期 + 容器 Id 访问入口 @@ -233,11 +233,6 @@ const Instances: FC = () => { - - - {inst.containerId?.substring(0, 20)} - - @@ -249,6 +244,39 @@ const Instances: FC = () => { + + + + { + clipBoard.copy( + inst.containerGuid && getProxyUrl(inst.containerGuid) + ) + showNotification({ + color: 'teal', + title: '代理 URL 已复制到剪贴板', + message: '请使用客户端进行访问', + icon: , + }) + }} + > + {inst.containerGuid} + + + + { clipBoard.copy(`${inst.ip ?? ''}:${inst.port ?? ''}`) showNotification({ color: 'teal', - message: '实例入口已复制到剪贴板', + message: '访问入口已复制到剪贴板', icon: , }) }} @@ -286,7 +314,7 @@ const Instances: FC = () => { onDelete(inst.containerGuid)} /> diff --git a/src/GZCTF/ClientApp/src/utils/Shared.tsx b/src/GZCTF/ClientApp/src/utils/Shared.tsx index f4f854c98..00e9086db 100644 --- a/src/GZCTF/ClientApp/src/utils/Shared.tsx +++ b/src/GZCTF/ClientApp/src/utils/Shared.tsx @@ -309,3 +309,8 @@ export const TaskStatusColorMap = new Map([ [TaskStatus.Duplicate, 'lime'], [null, 'gray'], ]) + +export const getProxyUrl = (guid: string) => { + const protocol = window.location.protocol.replace('http', 'ws') + return `${protocol}//${window.location.host}/api/proxy/${guid}` +}