Skip to content
2 changes: 2 additions & 0 deletions app/protos/grpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6842,6 +6842,7 @@ message HTTPFlow {
string HiddenIndex = 49;
string FromPlugin = 50;
string Host = 52;
string PathSuffix = 53;
}

message FuzzableParam {
Expand Down Expand Up @@ -6872,6 +6873,7 @@ message HTTPFlowsFieldGroupRequest {
message HTTPFlowsFieldGroupResponse {
repeated TagsCode Tags = 1;
repeated TagsCode StatusCode = 2;
repeated TagsCode Suffixes = 3;
}

message HTTPFlowsShareRequest {
Expand Down
1 change: 1 addition & 0 deletions app/renderer/src/main/public/locales/en/history.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"bodyLength": "Body Length",
"params": "Parameters",
"contentType": "Content Type",
"pathSuffix": "Path Suffix",
"durationMs": "Duration (ms)",
"updatedAt": "Request Time",
"requestSizeVerbose": "Request Size",
Expand Down
1 change: 1 addition & 0 deletions app/renderer/src/main/public/locales/zh/history.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"bodyLength": "响应长度",
"params": "参数",
"contentType": "响应类型",
"pathSuffix": "扩展名",
"durationMs": "延迟(ms)",
"updatedAt": "请求时间",
"requestSizeVerbose": "请求大小",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const getHTTPFlowExportFields = (t: (key: string) => string) => {
import { TFunction } from '@/i18n/useI18nNamespaces'

export const getHTTPFlowExportFields = (t: TFunction) => {
return [
{
title: t('YakitTable.order'),
Expand Down Expand Up @@ -70,6 +72,11 @@ export const getHTTPFlowExportFields = (t: (key: string) => string) => {
key: 'content_type',
dataKey: 'ContentType',
},
{
title: t('HTTPFlowTable.pathSuffix'),
key: 'path_suffix',
dataKey: 'PathSuffix',
},
{
title: t('HTTPFlowTable.durationMs'),
key: 'duration',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { FiltersItemProps } from '../TableVirtualResize/TableVirtualResizeType'

type TagsCodeLike = {
Value?: string
Total?: number
}

const PATH_SUFFIX_MODIFIER_SEPARATOR = /[!@]/
const VALID_PATH_SUFFIX_REGEXP = /^[a-zA-Z0-9]+$/

export const normalizeHTTPFlowPathSuffix = (value?: string) => {
if (!value) return ''

let normalized = value.trim()
if (!normalized) return ''

if (normalized.startsWith('.')) {
normalized = normalized.slice(1)
}
normalized = normalized.split(PATH_SUFFIX_MODIFIER_SEPARATOR)[0] || ''

if (!VALID_PATH_SUFFIX_REGEXP.test(normalized)) {
return ''
}
return normalized
}

export const getHTTPFlowPathSuffixValue = (path: string, pathSuffix?: string) => {
const normalizedPathSuffix = normalizeHTTPFlowPathSuffix(pathSuffix)
if (normalizedPathSuffix) {
return normalizedPathSuffix
}

const cleanPath = path.split('?')[0].replace(/\/+$/, '')
const match = cleanPath.match(/\.([a-zA-Z0-9]+)(?:[!@][^/]*)?$/)
return match?.[1] || ''
}

export const formatHTTPFlowPathSuffix = (path: string, pathSuffix?: string) => {
return getHTTPFlowPathSuffixValue(path, pathSuffix) || '-'
}

export const buildHTTPFlowSuffixOptions = (suffixes: TagsCodeLike[]): FiltersItemProps[] => {
const uniqueSuffixes = new Set<string>()

return suffixes.reduce<FiltersItemProps[]>((acc, item) => {
const normalized = normalizeHTTPFlowPathSuffix(item.Value)
if (!normalized || uniqueSuffixes.has(normalized)) {
return acc
}

uniqueSuffixes.add(normalized)
acc.push({
label: normalized,
value: normalized,
})
return acc
}, [])
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { HTTPFlowDetail, HTTPFlowDetailProp } from '../HTTPFlowDetail'
import { info, yakitNotify, yakitFailed } from '../../utils/notification'
import style from './HTTPFlowTable.module.scss'
import { formatTimestamp } from '../../utils/timeUtil'
import { buildHTTPFlowSuffixOptions, formatHTTPFlowPathSuffix } from './HTTPFlowPathSuffix'
import {
useControllableValue,
useCreation,
Expand Down Expand Up @@ -184,6 +185,7 @@ export interface HTTPFlow {
HostPort?: string
IPAddress?: string
HtmlTitle?: string
PathSuffix?: string

GetParams: FuzzableParams[]
PostParams: FuzzableParams[]
Expand Down Expand Up @@ -548,6 +550,7 @@ export interface HTTPFlowsToOnlineBatchResponse {
export interface HTTPFlowsFieldGroupResponse {
Tags: TagsCode[]
StatusCode: TagsCode[]
Suffixes: TagsCode[]
}

export interface TagsCode {
Expand Down Expand Up @@ -604,18 +607,6 @@ export const getClassNameData = (resData: HTTPFlow[]) => {
return newData
}

export const filterData = (filterArr: HTTPFlow[], key: keyof HTTPFlow) => {
const uniqueData: HTTPFlow[] = []
const idSet = new Set<HTTPFlow[keyof HTTPFlow]>()
filterArr.forEach((item) => {
if (!idSet.has(item[key])) {
idSet.add(item[key])
uniqueData.push(item)
}
})
return uniqueData
}

/**
* @description 根据单位转为对应的值
* @returns {number}
Expand Down Expand Up @@ -747,6 +738,8 @@ export const HTTPFlowTable = React.memo<HTTPFlowTableProp>((props) => {
const isOneceLoading = useRef<boolean>(true)

const [total, setTotal] = useState<number>(0)
const [suffixList, setSuffixList] = useState<FiltersItemProps[]>([])
const comSuffixList = useCampare(suffixList)
const [loading, setLoading] = useState(false)
const [selected, setSelected, getSelected] = useGetSetState<HTTPFlow>()

Expand Down Expand Up @@ -774,6 +767,7 @@ export const HTTPFlowTable = React.memo<HTTPFlowTableProp>((props) => {
const [afterBodyLength, setAfterBodyLength, getAfterBodyLength] = useGetSetState<number>()
const [beforeBodyLength, setBeforeBodyLength, getBeforeBodyLength] = useGetSetState<number>()
const [isReset, setIsReset] = useState<boolean>(false)
const [watchRefresh, setWatchRefresh] = useState<boolean>(false)

const [checkBodyLength, setCheckBodyLength] = useState<boolean>(false) // 查询BodyLength大于0

Expand Down Expand Up @@ -1082,6 +1076,20 @@ export const HTTPFlowTable = React.memo<HTTPFlowTableProp>((props) => {
isOneceLoading.current = false
})
})
useDebounceEffect(
() => {
if (!inViewport) return
ipcRenderer
.invoke('HTTPFlowsFieldGroup', { RefreshRequest: true, IsAll: true })
.then((rsp: HTTPFlowsFieldGroupResponse) => {
setSuffixList(buildHTTPFlowSuffixOptions(rsp.Suffixes || []))
})
.catch(() => {})
},
[inViewport, refresh, watchRefresh],
{ wait: 500 },
)

const onTableChange = useDebounceFn(
(page: number, limit: number, sort: SortProps, filter: any) => {
if (sort.order === 'none') {
Expand Down Expand Up @@ -1596,6 +1604,7 @@ export const HTTPFlowTable = React.memo<HTTPFlowTableProp>((props) => {
}
}
} catch (error) {}
setWatchRefresh((prev) => !prev)
setIsLoop(true)
})
useEffect(() => {
Expand Down Expand Up @@ -2160,10 +2169,24 @@ export const HTTPFlowTable = React.memo<HTTPFlowTableProp>((props) => {
filterSearchInputProps: {
size: 'small',
},
filterIcon: <OutlineSearchIcon className={style['filter-icon']} />,
filters: contentType,
},
},
{
title: t('HTTPFlowTable.pathSuffix'),
dataKey: 'PathSuffix',
width: 100,
filterProps: {
filterKey: 'IncludeSuffix',
filtersType: 'select',
filterMultiple: true,
filterSearchInputProps: { size: 'small' },
filters: suffixList,
},
render: (_, rowData) => {
return <div>{formatHTTPFlowPathSuffix(rowData.Path || '', rowData.PathSuffix)}</div>
},
},
{
title: t('HTTPFlowTable.durationMs'),
dataKey: 'DurationMs',
Expand Down Expand Up @@ -2322,6 +2345,7 @@ export const HTTPFlowTable = React.memo<HTTPFlowTableProp>((props) => {
excludeColumnsKey,
idFixed,
i18n.language,
comSuffixList,
])
// #endregion

Expand Down Expand Up @@ -2582,6 +2606,9 @@ export const HTTPFlowTable = React.memo<HTTPFlowTableProp>((props) => {
if (j === 'UpdatedAt') {
return formatTimestamp(v[j])
}
if (j === 'PathSuffix') {
return formatHTTPFlowPathSuffix(v['Path'], v['PathSuffix'])
}
return v[j]
}),
)
Expand Down Expand Up @@ -3721,6 +3748,7 @@ export const HTTPFlowTable = React.memo<HTTPFlowTableProp>((props) => {
const resetAllFun = useMemoizedFn(() => {
sortRef.current = defSort
setIsReset(!isReset)
setWatchRefresh((prev) => !prev)
setColor([])
setOnlyFavorite(false)
setCheckBodyLength(false)
Expand Down Expand Up @@ -4082,6 +4110,7 @@ export const HTTPFlowTable = React.memo<HTTPFlowTableProp>((props) => {

const onMitmNoResetRefresh = useMemoizedFn((version: string) => {
if (version !== mitmVersion) return
setWatchRefresh((prev) => !prev)
updateData()
})

Expand Down Expand Up @@ -4507,6 +4536,7 @@ export const HTTPFlowTable = React.memo<HTTPFlowTableProp>((props) => {
onClick: ({ key }) => {
switch (key) {
case 'noResetRefresh':
setWatchRefresh((prev) => !prev)
updateData()
break
case 'resetRefresh':
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest'
import {
buildHTTPFlowSuffixOptions,
getHTTPFlowPathSuffixValue,
normalizeHTTPFlowPathSuffix,
} from '../HTTPFlowPathSuffix'

describe('HTTPFlowPathSuffix', () => {
it('normalizes a valid suffix and strips the leading dot', () => {
expect(normalizeHTTPFlowPathSuffix('.js')).toBe('js')
expect(normalizeHTTPFlowPathSuffix('json')).toBe('json')
})

it('rejects malformed suffix values from backend aggregation', () => {
expect(normalizeHTTPFlowPathSuffix('.com&app=2021&size=w240')).toBe('')
expect(normalizeHTTPFlowPathSuffix('.png!cc_216x216')).toBe('png')
})

it('prefers a valid PathSuffix field and falls back to parsing Path', () => {
expect(getHTTPFlowPathSuffixValue('/static/app.js?version=1', '.js')).toBe('js')
expect(getHTTPFlowPathSuffixValue('/item/assets/428.png!cc_216x216', '')).toBe('png')
})

it('does not treat embedded urls in path payloads as file suffixes', () => {
expect(
getHTTPFlowPathSuffixValue(
'/search/src=https://imgsrc.baidu.com/forum&app=2021&size=w240&n=0&g=0n&fmt=auto',
'.com&app=2021&size=w240&n=0&g=0n&fmt=auto',
),
).toBe('')
})

it('filters invalid suffix options from field group response', () => {
expect(
buildHTTPFlowSuffixOptions([
{ Value: '.js', Total: 2 },
{ Value: '.com&app=2021&size=w240', Total: 1 },
{ Value: '.png!cc_216x216', Total: 1 },
]),
).toEqual([
{ label: 'js', value: 'js' },
{ label: 'png', value: 'png' },
])
})
})
Loading
Loading