diff --git a/packages/remix/src/utils/getIpAddress.ts b/packages/remix/src/utils/getIpAddress.ts new file mode 100644 index 000000000000..4fb5b9959484 --- /dev/null +++ b/packages/remix/src/utils/getIpAddress.ts @@ -0,0 +1,85 @@ +// Vendored / modified from @sergiodxa/remix-utils +// https://github.com/sergiodxa/remix-utils/blob/02af80e12829a53696bfa8f3c2363975cf59f55e/src/server/get-client-ip-address.ts + +import { isIP } from 'net'; + +/** + * Get the IP address of the client sending a request. + * + * It receives a Request headers object and use it to get the + * IP address from one of the following headers in order. + * + * - X-Client-IP + * - X-Forwarded-For + * - Fly-Client-IP + * - CF-Connecting-IP + * - Fastly-Client-Ip + * - True-Client-Ip + * - X-Real-IP + * - X-Cluster-Client-IP + * - X-Forwarded + * - Forwarded-For + * - Forwarded + * + * If the IP address is valid, it will be returned. Otherwise, null will be + * returned. + * + * If the header values contains more than one IP address, the first valid one + * will be returned. + */ +export function getClientIPAddress(headers: Headers): string | null { + // The headers to check, in priority order + const headerNames = [ + 'X-Client-IP', + 'X-Forwarded-For', + 'Fly-Client-IP', + 'CF-Connecting-IP', + 'Fastly-Client-Ip', + 'True-Client-Ip', + 'X-Real-IP', + 'X-Cluster-Client-IP', + 'X-Forwarded', + 'Forwarded-For', + 'Forwarded', + ]; + + // This will end up being Array because of the various possible values a header + // can take + const headerValues = headerNames.map((headerName: string) => { + const value = headers.get(headerName); + + if (headerName === 'Forwarded') { + return parseForwardedHeader(value); + } + + return value?.split(', '); + }); + + // Flatten the array and filter out any falsy entries + const flattenedHeaderValues = headerValues.reduce((acc: string[], val) => { + if (!val) { + return acc; + } + + return acc.concat(val); + }, []); + + // Find the first value which is a valid IP address, if any + const ipAddress = flattenedHeaderValues.find(ip => ip !== null && isIP(ip)); + + return ipAddress || null; +} + +function parseForwardedHeader(value: string | null): string | null { + if (!value) { + return null; + } + + for (const part of value.split(';')) { + if (part.startsWith('for=')) { + return part.slice(4); + } + } + + return null; +} diff --git a/packages/remix/src/utils/web-fetch.ts b/packages/remix/src/utils/web-fetch.ts index 599f914a94ef..1a3ef151f2e6 100644 --- a/packages/remix/src/utils/web-fetch.ts +++ b/packages/remix/src/utils/web-fetch.ts @@ -1,6 +1,7 @@ // Based on Remix's implementation of Fetch API // https://github.com/remix-run/web-std-io/tree/main/packages/fetch +import { getClientIPAddress } from './getIpAddress'; import { RemixRequest } from './types'; /* @@ -92,6 +93,15 @@ export const normalizeRemixRequest = (request: RemixRequest): Record