File size: 3,954 Bytes
5ec2e9b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import { retry, type RetryOptions } from "@std/async";
import type { Config } from "./config.ts";
import { generateRandomIPv6 } from "./ipv6Rotation.ts";
import { getCurrentProxy } from "./proxyManager.ts";

type FetchInputParameter = Parameters<typeof fetch>[0];
type FetchInitParameterWithClient =
    | RequestInit
    | RequestInit & { client: Deno.HttpClient };
type FetchReturn = ReturnType<typeof fetch>;

export const getFetchClient = (config: Config): {
    (
        input: FetchInputParameter,
        init?: FetchInitParameterWithClient,
    ): FetchReturn;
} => {
    return async (
        input: FetchInputParameter,
        init?: RequestInit,
    ) => {
        // Use auto-fetched proxy if enabled, otherwise use configured proxy
        const proxyAddress = config.networking.auto_proxy
            ? getCurrentProxy()
            : config.networking.proxy;
        const ipv6Block = config.networking.ipv6_block;

        // If proxy or IPv6 rotation is configured, create a custom HTTP client
        if (proxyAddress || ipv6Block) {
            const clientOptions: Deno.CreateHttpClientOptions = {};

            if (proxyAddress) {
                try {
                    const proxyUrl = new URL(proxyAddress);
                    // Extract credentials if present
                    if (proxyUrl.username && proxyUrl.password) {
                        clientOptions.proxy = {
                            url: `${proxyUrl.protocol}//${proxyUrl.host}`,
                            basicAuth: {
                                username: decodeURIComponent(proxyUrl.username),
                                password: decodeURIComponent(proxyUrl.password),
                            },
                        };
                    } else {
                        clientOptions.proxy = {
                            url: proxyAddress,
                        };
                    }
                } catch {
                    clientOptions.proxy = {
                        url: proxyAddress,
                    };
                }
            }

            if (ipv6Block) {
                clientOptions.localAddress = generateRandomIPv6(ipv6Block);
            }

            const client = Deno.createHttpClient(clientOptions);
            const fetchRes = await fetchShim(config, input, {
                client,
                headers: init?.headers,
                method: init?.method,
                body: init?.body,
            });
            client.close(); // Important: close client to avoid leaking resources
            return new Response(fetchRes.body, {
                status: fetchRes.status,
                headers: fetchRes.headers,
            });
        }

        return fetchShim(config, input, init);
    };
};

function fetchShim(
    config: Config,
    input: FetchInputParameter,
    init?: FetchInitParameterWithClient,
): FetchReturn {
    const fetchTimeout = config.networking.fetch?.timeout_ms;
    const fetchRetry = config.networking.fetch?.retry?.enabled;
    const fetchMaxAttempts = config.networking.fetch?.retry?.times;
    const fetchInitialDebounce = config.networking.fetch?.retry
        ?.initial_debounce;
    const fetchDebounceMultiplier = config.networking.fetch?.retry
        ?.debounce_multiplier;
    const retryOptions: RetryOptions = {
        maxAttempts: fetchMaxAttempts,
        minTimeout: fetchInitialDebounce,
        multiplier: fetchDebounceMultiplier,
        jitter: 0,
    };

    const callFetch = () =>
        fetch(input, {
            // only set the AbortSignal if the timeout is supplied in the config
            signal: fetchTimeout
                ? AbortSignal.timeout(Number(fetchTimeout))
                : null,
            ...(init || {}),
        });
    // if retry enabled, call retry with the fetch shim, otherwise pass the fetch shim back directly
    return fetchRetry ? retry(callFetch, retryOptions) : callFetch();
}