mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
feat(stalker): add validated redirect session hooks
This commit is contained in:
1 parent
8009233220
commit
9166d0f12c
2 files changed
+538
-24
No files matched your search
@@ -297,6 +297,313 @@ describe('requestWithValidatedRedirects', () => {
|
||||
await expect(resolvePinnedAddress(1)).resolves.toBe('142.250.191.110');
|
||||
});
|
||||
|
||||
it('runs narrow hooks in exact validated-hop order and collects redirects before preparing the next hop', async () => {
|
||||
const events: string[] = [];
|
||||
const resolveHostname = jest.fn(async (hostname: string) => {
|
||||
events.push(`validate:${hostname}`);
|
||||
return ['93.184.216.34'];
|
||||
});
|
||||
axiosMock
|
||||
.mockImplementationOnce(async (config) => {
|
||||
events.push(`axios:${config.url}:${config.headers['x-hop']}`);
|
||||
return {
|
||||
status: 302,
|
||||
headers: { location: '/next' },
|
||||
};
|
||||
})
|
||||
.mockImplementationOnce(async (config) => {
|
||||
events.push(`axios:${config.url}:${config.headers['x-hop']}`);
|
||||
return {
|
||||
status: 200,
|
||||
headers: {},
|
||||
data: 'ok',
|
||||
};
|
||||
});
|
||||
|
||||
await requestWithValidatedRedirects(
|
||||
'https://example.com/start',
|
||||
{
|
||||
method: 'GET',
|
||||
redirectHooks: {
|
||||
prepareHeaders: async (hop) => {
|
||||
events.push(`prepare:${hop.url}:${hop.method}`);
|
||||
return {
|
||||
...hop.headers,
|
||||
'x-hop': new URL(hop.url).pathname,
|
||||
};
|
||||
},
|
||||
collectResponse: async (response) => {
|
||||
events.push(
|
||||
`collect:${response.url}:${response.status}`
|
||||
);
|
||||
},
|
||||
beforeValidatedRedirect: async (redirect) => {
|
||||
events.push(
|
||||
`before:${redirect.fromUrl}->${redirect.toUrl}:${redirect.method}`
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{ resolveHostname }
|
||||
);
|
||||
|
||||
expect(events).toEqual([
|
||||
'validate:example.com',
|
||||
'prepare:https://example.com/start:GET',
|
||||
'axios:https://example.com/start:/start',
|
||||
'collect:https://example.com/start:302',
|
||||
'validate:example.com',
|
||||
'before:https://example.com/start->https://example.com/next:GET',
|
||||
'prepare:https://example.com/next:GET',
|
||||
'axios:https://example.com/next:/next',
|
||||
'collect:https://example.com/next:200',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not pass redirect hook internals to Axios', async () => {
|
||||
axiosMock.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
headers: {},
|
||||
data: 'ok',
|
||||
});
|
||||
const redirectHooks = {
|
||||
prepareHeaders: jest.fn(async (hop) => hop.headers),
|
||||
};
|
||||
|
||||
await requestWithValidatedRedirects(
|
||||
'https://example.com/start',
|
||||
{ method: 'GET', redirectHooks },
|
||||
{ resolveHostname: publicResolver }
|
||||
);
|
||||
|
||||
expect(axiosMock.mock.calls[0][0]).not.toHaveProperty(
|
||||
'redirectHooks'
|
||||
);
|
||||
});
|
||||
|
||||
it('prepares each hop from an isolated header snapshot', async () => {
|
||||
axiosMock
|
||||
.mockResolvedValueOnce({
|
||||
status: 302,
|
||||
headers: { location: '/next' },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
headers: {},
|
||||
data: 'ok',
|
||||
});
|
||||
const observedBaseHeaders: string[][] = [];
|
||||
|
||||
await requestWithValidatedRedirects(
|
||||
'https://example.com/start',
|
||||
{
|
||||
headers: { 'x-values': ['base'] },
|
||||
method: 'GET',
|
||||
redirectHooks: {
|
||||
prepareHeaders: (hop) => {
|
||||
const values = hop.headers['x-values'] as string[];
|
||||
observedBaseHeaders.push([...values]);
|
||||
values.push(new URL(hop.url).pathname);
|
||||
return hop.headers;
|
||||
},
|
||||
},
|
||||
},
|
||||
{ resolveHostname: publicResolver }
|
||||
);
|
||||
|
||||
expect(observedBaseHeaders).toEqual([['base'], ['base']]);
|
||||
expect(axiosMock.mock.calls[0][0].headers['x-values']).toEqual([
|
||||
'base',
|
||||
'/start',
|
||||
]);
|
||||
expect(axiosMock.mock.calls[1][0].headers['x-values']).toEqual([
|
||||
'base',
|
||||
'/next',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not let response collection rewrite the validated redirect target', async () => {
|
||||
axiosMock
|
||||
.mockResolvedValueOnce({
|
||||
status: 302,
|
||||
headers: { location: '/validated' },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
headers: {},
|
||||
data: 'ok',
|
||||
});
|
||||
|
||||
await requestWithValidatedRedirects(
|
||||
'https://example.com/start',
|
||||
{
|
||||
method: 'GET',
|
||||
redirectHooks: {
|
||||
collectResponse: (response) => {
|
||||
response.headers.location = '/tampered';
|
||||
},
|
||||
},
|
||||
},
|
||||
{ resolveHostname: publicResolver }
|
||||
);
|
||||
|
||||
expect(axiosMock.mock.calls[1][0].url).toBe(
|
||||
'https://example.com/validated'
|
||||
);
|
||||
});
|
||||
|
||||
it('collects a rejected HTTP response before propagating the Axios failure', async () => {
|
||||
const rejected = {
|
||||
response: {
|
||||
status: 401,
|
||||
headers: { 'set-cookie': ['session=synthetic; Path=/'] },
|
||||
},
|
||||
};
|
||||
const collectResponse = jest.fn();
|
||||
axiosMock.mockRejectedValueOnce(rejected);
|
||||
|
||||
await expect(
|
||||
requestWithValidatedRedirects(
|
||||
'https://example.com/start',
|
||||
{
|
||||
method: 'GET',
|
||||
redirectHooks: { collectResponse },
|
||||
},
|
||||
{ resolveHostname: publicResolver }
|
||||
)
|
||||
).rejects.toBe(rejected);
|
||||
|
||||
expect(collectResponse).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
'set-cookie': ['session=synthetic; Path=/'],
|
||||
},
|
||||
method: 'GET',
|
||||
status: 401,
|
||||
url: 'https://example.com/start',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a validated cross-origin redirect to pause before target preparation or contact', async () => {
|
||||
class RedirectPause extends Error {}
|
||||
|
||||
const pause = new RedirectPause('approval-required');
|
||||
const resolveHostname = jest.fn(async (hostname: string) =>
|
||||
hostname === 'example.com'
|
||||
? ['93.184.216.34']
|
||||
: ['142.250.191.110']
|
||||
);
|
||||
const prepareHeaders = jest.fn(async (hop) => hop.headers);
|
||||
const collectResponse = jest.fn(async () => undefined);
|
||||
const beforeValidatedRedirect = jest.fn(async () => {
|
||||
throw pause;
|
||||
});
|
||||
axiosMock.mockResolvedValueOnce({
|
||||
status: 302,
|
||||
headers: { location: 'https://target.example/landing' },
|
||||
});
|
||||
|
||||
await expect(
|
||||
requestWithValidatedRedirects(
|
||||
'https://example.com/start',
|
||||
{
|
||||
method: 'GET',
|
||||
redirectHooks: {
|
||||
beforeValidatedRedirect,
|
||||
collectResponse,
|
||||
prepareHeaders,
|
||||
},
|
||||
},
|
||||
{ resolveHostname }
|
||||
)
|
||||
).rejects.toBe(pause);
|
||||
|
||||
expect(resolveHostname).toHaveBeenNthCalledWith(1, 'example.com');
|
||||
expect(resolveHostname).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'target.example'
|
||||
);
|
||||
expect(collectResponse).toHaveBeenCalledTimes(1);
|
||||
expect(beforeValidatedRedirect).toHaveBeenCalledWith({
|
||||
crossOrigin: true,
|
||||
fromUrl: 'https://example.com/start',
|
||||
method: 'GET',
|
||||
status: 302,
|
||||
toUrl: 'https://target.example/landing',
|
||||
});
|
||||
expect(prepareHeaders).toHaveBeenCalledTimes(1);
|
||||
expect(axiosMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
location: '/same',
|
||||
name: 'same target',
|
||||
startUrl: 'https://example.com/same',
|
||||
},
|
||||
{
|
||||
location: '#two',
|
||||
name: 'fragment-only target',
|
||||
startUrl: 'https://example.com/same#one',
|
||||
},
|
||||
])(
|
||||
'rejects a repeated $name before another contact',
|
||||
async ({ location, startUrl }) => {
|
||||
const beforeValidatedRedirect = jest.fn();
|
||||
const prepareHeaders = jest.fn(async (hop) => hop.headers);
|
||||
axiosMock.mockResolvedValueOnce({
|
||||
status: 302,
|
||||
headers: { location },
|
||||
});
|
||||
|
||||
await expect(
|
||||
requestWithValidatedRedirects(
|
||||
startUrl,
|
||||
{
|
||||
method: 'GET',
|
||||
redirectHooks: {
|
||||
beforeValidatedRedirect,
|
||||
prepareHeaders,
|
||||
},
|
||||
},
|
||||
{ resolveHostname: publicResolver }
|
||||
)
|
||||
).rejects.toMatchObject({
|
||||
message: expect.stringMatching(/redirect loop/i),
|
||||
status: 502,
|
||||
});
|
||||
|
||||
expect(beforeValidatedRedirect).not.toHaveBeenCalled();
|
||||
expect(prepareHeaders).toHaveBeenCalledTimes(1);
|
||||
expect(axiosMock).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
);
|
||||
|
||||
it('allows a same-target POST redirect when its effective method rewrites to GET', async () => {
|
||||
axiosMock
|
||||
.mockResolvedValueOnce({
|
||||
status: 302,
|
||||
headers: { location: '/submit#result' },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
headers: {},
|
||||
data: 'ok',
|
||||
});
|
||||
|
||||
await requestWithValidatedRedirects(
|
||||
'https://example.com/submit',
|
||||
{ data: { value: 1 }, method: 'POST' },
|
||||
{ resolveHostname: publicResolver }
|
||||
);
|
||||
|
||||
expect(axiosMock).toHaveBeenCalledTimes(2);
|
||||
expect(axiosMock.mock.calls[1][0]).toMatchObject({
|
||||
data: undefined,
|
||||
method: 'GET',
|
||||
url: 'https://example.com/submit#result',
|
||||
});
|
||||
});
|
||||
|
||||
it('removes sensitive headers when a redirect changes origin', async () => {
|
||||
axiosMock
|
||||
.mockResolvedValueOnce({
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios, {
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
RawAxiosRequestHeaders,
|
||||
RawAxiosResponseHeaders,
|
||||
} from 'axios';
|
||||
import type { LookupAddress } from 'node:dns';
|
||||
import { Agent as HttpAgent } from 'node:http';
|
||||
@@ -10,6 +11,7 @@ import { isIP, LookupFunction } from 'node:net';
|
||||
import {
|
||||
RemoteUrlPolicy,
|
||||
UnsafeUrlError,
|
||||
ValidatedRemoteUrl,
|
||||
validateRemoteUrl,
|
||||
} from '../events/url-safety';
|
||||
|
||||
@@ -25,13 +27,73 @@ export interface ValidatedRequestAgentFactory {
|
||||
createHttpsAgent?(lookup?: LookupFunction, url?: URL): HttpsAgent;
|
||||
}
|
||||
|
||||
export interface ValidatedAxiosPrepareHeadersContext {
|
||||
readonly headers: RawAxiosRequestHeaders;
|
||||
readonly method: string;
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
export interface ValidatedAxiosResponseContext {
|
||||
readonly headers: RawAxiosResponseHeaders;
|
||||
readonly method: string;
|
||||
readonly status: number;
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
export interface ValidatedAxiosRedirectContext {
|
||||
readonly crossOrigin: boolean;
|
||||
readonly fromUrl: string;
|
||||
readonly method: string;
|
||||
readonly status: number;
|
||||
readonly toUrl: string;
|
||||
}
|
||||
|
||||
export interface ValidatedAxiosRedirectHooks {
|
||||
beforeValidatedRedirect?(
|
||||
redirect: ValidatedAxiosRedirectContext
|
||||
): Promise<void> | void;
|
||||
collectResponse?(
|
||||
response: ValidatedAxiosResponseContext
|
||||
): Promise<void> | void;
|
||||
prepareHeaders?(
|
||||
hop: ValidatedAxiosPrepareHeadersContext
|
||||
):
|
||||
| AxiosRequestConfig['headers']
|
||||
| Promise<AxiosRequestConfig['headers']>;
|
||||
}
|
||||
|
||||
export type ValidatedAxiosRequestConfig = Omit<
|
||||
AxiosRequestConfig,
|
||||
'httpAgent' | 'httpsAgent'
|
||||
> & {
|
||||
agentFactory?: ValidatedRequestAgentFactory;
|
||||
redirectHooks?: ValidatedAxiosRedirectHooks;
|
||||
};
|
||||
|
||||
function copyRequestHeaders(
|
||||
headers: AxiosRequestConfig['headers']
|
||||
): RawAxiosRequestHeaders {
|
||||
if (!headers) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const source =
|
||||
typeof (headers as { toJSON?: () => RawAxiosRequestHeaders })
|
||||
.toJSON === 'function'
|
||||
? (
|
||||
headers as {
|
||||
toJSON: () => RawAxiosRequestHeaders;
|
||||
}
|
||||
).toJSON()
|
||||
: headers;
|
||||
return Object.fromEntries(
|
||||
Object.entries(source).map(([name, value]) => [
|
||||
name,
|
||||
Array.isArray(value) ? [...value] : value,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function copyHeadersWithoutSensitiveValues(
|
||||
headers: AxiosRequestConfig['headers']
|
||||
): AxiosRequestConfig['headers'] {
|
||||
@@ -39,15 +101,7 @@ function copyHeadersWithoutSensitiveValues(
|
||||
return headers;
|
||||
}
|
||||
|
||||
const source =
|
||||
typeof (headers as { toJSON?: () => RawAxiosRequestHeaders }).toJSON ===
|
||||
'function'
|
||||
? (
|
||||
headers as {
|
||||
toJSON: () => RawAxiosRequestHeaders;
|
||||
}
|
||||
).toJSON()
|
||||
: headers;
|
||||
const source = copyRequestHeaders(headers);
|
||||
const sanitized: RawAxiosRequestHeaders = {};
|
||||
|
||||
for (const [name, value] of Object.entries(source)) {
|
||||
@@ -59,6 +113,19 @@ function copyHeadersWithoutSensitiveValues(
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
function copyResponseHeaders(
|
||||
headers: AxiosResponse['headers']
|
||||
): RawAxiosResponseHeaders {
|
||||
const source =
|
||||
typeof headers.toJSON === 'function' ? headers.toJSON() : headers;
|
||||
return Object.fromEntries(
|
||||
Object.entries(source).map(([name, value]) => [
|
||||
name,
|
||||
Array.isArray(value) ? [...value] : value,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function createPinnedLookup(addresses: readonly string[]): LookupFunction {
|
||||
const records: LookupAddress[] = addresses.map((address) => ({
|
||||
address,
|
||||
@@ -95,7 +162,11 @@ function pinRequestToValidatedAddresses(
|
||||
url: URL,
|
||||
addresses: readonly string[] | undefined
|
||||
): AxiosRequestConfig {
|
||||
const { agentFactory, ...axiosConfig } = config;
|
||||
const {
|
||||
agentFactory,
|
||||
redirectHooks: _redirectHooks,
|
||||
...axiosConfig
|
||||
} = config;
|
||||
if (!addresses) {
|
||||
if (url.protocol === 'https:' && agentFactory?.createHttpsAgent) {
|
||||
return {
|
||||
@@ -159,6 +230,87 @@ function getRedirectValidationPolicy(
|
||||
};
|
||||
}
|
||||
|
||||
function effectiveRequestMethod(method: string | undefined): string {
|
||||
return method?.toUpperCase() || 'GET';
|
||||
}
|
||||
|
||||
function canonicalVisitedTarget(url: URL, method: string): string {
|
||||
const canonicalUrl = new URL(url);
|
||||
canonicalUrl.hash = '';
|
||||
return `${method} ${canonicalUrl.toString()}`;
|
||||
}
|
||||
|
||||
function rememberValidatedTarget(
|
||||
visitedTargets: Set<string>,
|
||||
target: URL,
|
||||
method: string
|
||||
): void {
|
||||
const key = canonicalVisitedTarget(target, method);
|
||||
if (visitedTargets.has(key)) {
|
||||
throw new UnsafeUrlError('Redirect loop detected', 502);
|
||||
}
|
||||
visitedTargets.add(key);
|
||||
}
|
||||
|
||||
async function prepareHopConfig(
|
||||
config: ValidatedAxiosRequestConfig,
|
||||
validatedUrl: URL,
|
||||
method: string
|
||||
): Promise<ValidatedAxiosRequestConfig> {
|
||||
const prepareHeaders = config.redirectHooks?.prepareHeaders;
|
||||
if (!prepareHeaders) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const headers = await prepareHeaders({
|
||||
headers: copyRequestHeaders(config.headers),
|
||||
method,
|
||||
url: validatedUrl.toString(),
|
||||
});
|
||||
return {
|
||||
...config,
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
async function collectHopResponse(
|
||||
hooks: ValidatedAxiosRedirectHooks | undefined,
|
||||
response: AxiosResponse<unknown>,
|
||||
validatedUrl: URL,
|
||||
method: string
|
||||
): Promise<void> {
|
||||
await hooks?.collectResponse?.({
|
||||
headers: copyResponseHeaders(response.headers),
|
||||
method,
|
||||
status: response.status,
|
||||
url: validatedUrl.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
function rejectedAxiosResponse(
|
||||
error: unknown
|
||||
): AxiosResponse<unknown> | undefined {
|
||||
if (
|
||||
typeof error !== 'object' ||
|
||||
error === null ||
|
||||
!('response' in error)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const response = error.response;
|
||||
if (
|
||||
typeof response !== 'object' ||
|
||||
response === null ||
|
||||
typeof Reflect.get(response, 'status') !== 'number' ||
|
||||
typeof Reflect.get(response, 'headers') !== 'object' ||
|
||||
Reflect.get(response, 'headers') === null
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return response as AxiosResponse<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an Axios request while validating the initial URL and every redirect.
|
||||
* Redirects are followed manually so each target passes through the same
|
||||
@@ -177,17 +329,27 @@ export async function requestWithValidatedRedirects<T = unknown>(
|
||||
let requestConfig = { ...config };
|
||||
let initialOrigin: string | undefined;
|
||||
let initialAddresses: readonly string[] | undefined;
|
||||
let pendingValidatedTarget: ValidatedRemoteUrl | undefined;
|
||||
const visitedTargets = new Set<string>();
|
||||
|
||||
for (let redirectCount = 0; ; redirectCount += 1) {
|
||||
const validatedTarget = await validateRemoteUrl(
|
||||
currentUrl,
|
||||
getRedirectValidationPolicy(currentUrl, initialOrigin, policy)
|
||||
);
|
||||
const validatedTarget =
|
||||
pendingValidatedTarget ??
|
||||
(await validateRemoteUrl(
|
||||
currentUrl,
|
||||
getRedirectValidationPolicy(currentUrl, initialOrigin, policy)
|
||||
));
|
||||
pendingValidatedTarget = undefined;
|
||||
const validatedUrl = validatedTarget.url;
|
||||
const isInitialRequest = !initialOrigin;
|
||||
if (isInitialRequest) {
|
||||
initialOrigin = validatedUrl.origin;
|
||||
initialAddresses = validatedTarget.addresses;
|
||||
rememberValidatedTarget(
|
||||
visitedTargets,
|
||||
validatedUrl,
|
||||
effectiveRequestMethod(requestConfig.method)
|
||||
);
|
||||
}
|
||||
const addresses =
|
||||
!isInitialRequest &&
|
||||
@@ -196,18 +358,45 @@ export async function requestWithValidatedRedirects<T = unknown>(
|
||||
validatedUrl.origin === initialOrigin
|
||||
? initialAddresses
|
||||
: validatedTarget.addresses;
|
||||
const pinnedConfig = pinRequestToValidatedAddresses(
|
||||
const method = effectiveRequestMethod(requestConfig.method);
|
||||
const hopConfig = await prepareHopConfig(
|
||||
requestConfig,
|
||||
validatedUrl,
|
||||
method
|
||||
);
|
||||
const pinnedConfig = pinRequestToValidatedAddresses(
|
||||
hopConfig,
|
||||
validatedUrl,
|
||||
addresses
|
||||
);
|
||||
const response = await axios<T>({
|
||||
...pinnedConfig,
|
||||
maxRedirects: 0,
|
||||
url: validatedUrl.toString(),
|
||||
validateStatus: (status) =>
|
||||
REDIRECT_STATUSES.has(status) || originalValidateStatus(status),
|
||||
});
|
||||
let response: AxiosResponse<T>;
|
||||
try {
|
||||
response = await axios<T>({
|
||||
...pinnedConfig,
|
||||
maxRedirects: 0,
|
||||
url: validatedUrl.toString(),
|
||||
validateStatus: (status) =>
|
||||
REDIRECT_STATUSES.has(status) ||
|
||||
originalValidateStatus(status),
|
||||
});
|
||||
} catch (error) {
|
||||
const rejectedResponse = rejectedAxiosResponse(error);
|
||||
if (rejectedResponse) {
|
||||
await collectHopResponse(
|
||||
requestConfig.redirectHooks,
|
||||
rejectedResponse,
|
||||
validatedUrl,
|
||||
method
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await collectHopResponse(
|
||||
requestConfig.redirectHooks,
|
||||
response,
|
||||
validatedUrl,
|
||||
method
|
||||
);
|
||||
|
||||
if (!REDIRECT_STATUSES.has(response.status)) {
|
||||
return response;
|
||||
@@ -225,7 +414,6 @@ export async function requestWithValidatedRedirects<T = unknown>(
|
||||
}
|
||||
|
||||
const nextUrl = new URL(location, validatedUrl);
|
||||
const method = requestConfig.method?.toUpperCase();
|
||||
const shouldRewriteToGet =
|
||||
(response.status === 303 && method !== 'HEAD') ||
|
||||
((response.status === 301 || response.status === 302) &&
|
||||
@@ -254,6 +442,25 @@ export async function requestWithValidatedRedirects<T = unknown>(
|
||||
};
|
||||
}
|
||||
|
||||
currentUrl = nextUrl.toString();
|
||||
const nextMethod = effectiveRequestMethod(requestConfig.method);
|
||||
const nextTarget = await validateRemoteUrl(
|
||||
nextUrl.toString(),
|
||||
getRedirectValidationPolicy(
|
||||
nextUrl.toString(),
|
||||
initialOrigin,
|
||||
policy
|
||||
)
|
||||
);
|
||||
rememberValidatedTarget(visitedTargets, nextTarget.url, nextMethod);
|
||||
await requestConfig.redirectHooks?.beforeValidatedRedirect?.({
|
||||
crossOrigin: nextTarget.url.origin !== validatedUrl.origin,
|
||||
fromUrl: validatedUrl.toString(),
|
||||
method: nextMethod,
|
||||
status: response.status,
|
||||
toUrl: nextTarget.url.toString(),
|
||||
});
|
||||
|
||||
currentUrl = nextTarget.url.toString();
|
||||
pendingValidatedTarget = nextTarget;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user