mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
fix(frontend): keep node-reported values one URL path segment
InboundInfoModal put a client's email into /panel/api/clients/ips and clearIps unencoded, and the host mutations did the same with a host group id. Both values reach the master from a node's sync without the panel's own character rules, so "../" and "?" in them chose the route and query of the admin's authenticated POST; the IP-log request fires as soon as the window opens. Encode them as every other client route in the SPA already does.
This commit is contained in:
@@ -20,14 +20,19 @@ export function useHostMutations() {
|
|||||||
|
|
||||||
const updateMut = useMutation({
|
const updateMut = useMutation({
|
||||||
mutationFn: ({ groupId, payload }: { groupId: string; payload: BulkAddHostValues }) =>
|
mutationFn: ({ groupId, payload }: { groupId: string; payload: BulkAddHostValues }) =>
|
||||||
HttpUtil.post(`/panel/api/hosts/update/${groupId}`, payload, JSON_HEADERS),
|
HttpUtil.post(
|
||||||
|
`/panel/api/hosts/update/${encodeURIComponent(groupId)}`,
|
||||||
|
payload,
|
||||||
|
JSON_HEADERS,
|
||||||
|
),
|
||||||
onSuccess: (msg) => {
|
onSuccess: (msg) => {
|
||||||
if (msg?.success) invalidate();
|
if (msg?.success) invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const removeMut = useMutation({
|
const removeMut = useMutation({
|
||||||
mutationFn: (groupId: string) => HttpUtil.post(`/panel/api/hosts/del/${groupId}`),
|
mutationFn: (groupId: string) =>
|
||||||
|
HttpUtil.post(`/panel/api/hosts/del/${encodeURIComponent(groupId)}`),
|
||||||
onSuccess: (msg) => {
|
onSuccess: (msg) => {
|
||||||
if (msg?.success) invalidate();
|
if (msg?.success) invalidate();
|
||||||
},
|
},
|
||||||
@@ -35,7 +40,7 @@ export function useHostMutations() {
|
|||||||
|
|
||||||
const setEnableMut = useMutation({
|
const setEnableMut = useMutation({
|
||||||
mutationFn: ({ groupId, enable }: { groupId: string; enable: boolean }) =>
|
mutationFn: ({ groupId, enable }: { groupId: string; enable: boolean }) =>
|
||||||
HttpUtil.post(`/panel/api/hosts/setEnable/${groupId}`, { enable }),
|
HttpUtil.post(`/panel/api/hosts/setEnable/${encodeURIComponent(groupId)}`, { enable }),
|
||||||
onSuccess: (msg) => {
|
onSuccess: (msg) => {
|
||||||
if (msg?.success) invalidate();
|
if (msg?.success) invalidate();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ export default function InboundInfoModal({
|
|||||||
if (!clientStats?.email) return;
|
if (!clientStats?.email) return;
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.post(`/panel/api/clients/ips/${clientStats.email}`);
|
const msg = await HttpUtil.post(
|
||||||
|
`/panel/api/clients/ips/${encodeURIComponent(clientStats.email)}`,
|
||||||
|
);
|
||||||
if (!msg?.success) {
|
if (!msg?.success) {
|
||||||
setClientIpsText((msg?.obj as string) || 'No IP record');
|
setClientIpsText((msg?.obj as string) || 'No IP record');
|
||||||
setClientIpsArray([]);
|
setClientIpsArray([]);
|
||||||
@@ -101,7 +103,9 @@ export default function InboundInfoModal({
|
|||||||
|
|
||||||
const clearClientIps = useCallback(async () => {
|
const clearClientIps = useCallback(async () => {
|
||||||
if (!clientStats?.email) return;
|
if (!clientStats?.email) return;
|
||||||
const msg = await HttpUtil.post(`/panel/api/clients/clearIps/${clientStats.email}`);
|
const msg = await HttpUtil.post(
|
||||||
|
`/panel/api/clients/clearIps/${encodeURIComponent(clientStats.email)}`,
|
||||||
|
);
|
||||||
if (msg?.success) {
|
if (msg?.success) {
|
||||||
setClientIpsArray([]);
|
setClientIpsArray([]);
|
||||||
setClientIpsText(t('tgbot.noIpRecord'));
|
setClientIpsText(t('tgbot.noIpRecord'));
|
||||||
@@ -223,7 +227,8 @@ export default function InboundInfoModal({
|
|||||||
setClientIpsText('');
|
setClientIpsText('');
|
||||||
|
|
||||||
if (ipLimitEnable && (clientSet?.limitIp ?? 0) > 0 && stats?.email) {
|
if (ipLimitEnable && (clientSet?.limitIp ?? 0) > 0 && stats?.email) {
|
||||||
void HttpUtil.post(`/panel/api/clients/ips/${stats.email}`).then((msg) => {
|
const email = encodeURIComponent(stats.email);
|
||||||
|
void HttpUtil.post(`/panel/api/clients/ips/${email}`).then((msg) => {
|
||||||
if (!msg?.success) {
|
if (!msg?.success) {
|
||||||
setClientIpsText((msg?.obj as string) || 'No IP record');
|
setClientIpsText((msg?.obj as string) || 'No IP record');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { act, render, renderHook } from '@testing-library/react';
|
||||||
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { useHostMutations } from '@/api/queries/useHostMutations';
|
||||||
|
import { BulkAddHostSchema } from '@/schemas/api/host';
|
||||||
|
import { DBInbound } from '@/models/dbinbound';
|
||||||
|
import InboundInfoModal from '@/pages/inbounds/info/InboundInfoModal';
|
||||||
|
import { makeTestQueryClient } from '@/test/test-utils';
|
||||||
|
import { HttpUtil } from '@/utils';
|
||||||
|
|
||||||
|
// Client emails and host group ids adopted from a node skip the panel's own
|
||||||
|
// character rules: unencoded, "../" and "?" re-target the admin's POST.
|
||||||
|
const hostile = '../../setting/update?tgBotEnable=true';
|
||||||
|
const encoded = encodeURIComponent(hostile);
|
||||||
|
|
||||||
|
describe('node-reported values stay one URL path segment', () => {
|
||||||
|
const post = vi.mocked(HttpUtil.post);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
post.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encodes the client email in the inbound info IP-log request', () => {
|
||||||
|
const dbInbound = new DBInbound({
|
||||||
|
id: 2,
|
||||||
|
protocol: 'http',
|
||||||
|
port: 44301,
|
||||||
|
settings: JSON.stringify({ clients: [{ email: hostile, enable: true, limitIp: 1 }] }),
|
||||||
|
streamSettings: JSON.stringify({ network: 'tcp', security: 'none' }),
|
||||||
|
sniffing: '',
|
||||||
|
clientStats: [{ email: hostile, enable: true, up: 1, down: 1, total: 0, expiryTime: 0 }],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<InboundInfoModal open onClose={() => {}} dbInbound={dbInbound} ipLimitEnable />);
|
||||||
|
|
||||||
|
expect(post).toHaveBeenCalledWith(`/panel/api/clients/ips/${encoded}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encodes the host group id in the host mutations', async () => {
|
||||||
|
const queryClient = makeTestQueryClient();
|
||||||
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
);
|
||||||
|
const { result } = renderHook(() => useHostMutations(), { wrapper });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.update(
|
||||||
|
hostile,
|
||||||
|
BulkAddHostSchema.parse({ inboundIds: [1], remark: 'cdn' }),
|
||||||
|
);
|
||||||
|
await result.current.setEnable(hostile, false);
|
||||||
|
await result.current.remove(hostile);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(post.mock.calls.map(([url]) => url)).toEqual([
|
||||||
|
`/panel/api/hosts/update/${encoded}`,
|
||||||
|
`/panel/api/hosts/setEnable/${encoded}`,
|
||||||
|
`/panel/api/hosts/del/${encoded}`,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user