|
| 1 | +/*! |
| 2 | + * Copyright (c) Friendly Captcha GmbH 2023. |
| 3 | + * This Source Code Form is subject to the terms of the Mozilla Public |
| 4 | + * License, v. 2.0. If a copy of the MPL was not distributed with this |
| 5 | + * file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 6 | + */ |
| 7 | + |
| 8 | +import { shuffledCopy } from "../util/random.js"; |
| 9 | +import { originOf } from "../util/url.js"; |
| 10 | +import { stringHasPrefix } from "../util/string.js"; |
| 11 | + |
| 12 | +/** |
| 13 | + * Build a retry origin list with these rules: |
| 14 | + * 1) The first entry is the primary origin (origins[0]). |
| 15 | + * 2) Remaining entries are fallback origins (origins[1..]) in shuffled order. |
| 16 | + * |
| 17 | + * Example: |
| 18 | + * origins = ["https://0.example.com", "https://1.example.com", "https://2.example.com", "https://3.example.com"] |
| 19 | + * shuffled fallbacks (example) = ["https://2.example.com", "https://3.example.com", "https://1.example.com"] |
| 20 | + * output origins = [ |
| 21 | + * "https://0.example.com", |
| 22 | + * "https://2.example.com", |
| 23 | + * "https://3.example.com", |
| 24 | + * "https://1.example.com" |
| 25 | + * ] |
| 26 | + * |
| 27 | + * @private |
| 28 | + */ |
| 29 | +export function getRetryOrigins(origins: string[]): string[] { |
| 30 | + if (origins.length === 0) return []; |
| 31 | + return [origins[0]].concat(shuffledCopy(origins.slice(1))); |
| 32 | +} |
| 33 | + |
| 34 | +/** |
| 35 | + * Returns the index in retryOrigins to use for a given attempt number. |
| 36 | + * |
| 37 | + * - Attempts 0, 1, and 2 map to index 0 (primary). |
| 38 | + * - Following attempts map to fallback indices in order. |
| 39 | + * - If attempts exceed unique fallbacks, a random fallback index is used. |
| 40 | + * |
| 41 | + * @private |
| 42 | + */ |
| 43 | +export function getRetryOriginIndex( |
| 44 | + attemptNumber: number, |
| 45 | + retryOrigins: string[], |
| 46 | +): number { |
| 47 | + const retryOriginsLength = retryOrigins.length; |
| 48 | + if (retryOriginsLength === 0) return -1; |
| 49 | + // We intentionally map attempt 0 to primary as a bug-tolerant fallback. |
| 50 | + if (attemptNumber <= 2 || retryOriginsLength === 1) return 0; |
| 51 | + |
| 52 | + const fallbackCount = retryOriginsLength - 1; |
| 53 | + const fallbackAttempt = attemptNumber - 2; |
| 54 | + if (fallbackAttempt <= fallbackCount) { |
| 55 | + return fallbackAttempt; |
| 56 | + } |
| 57 | + |
| 58 | + return 1 + Math.floor(Math.random() * fallbackCount); |
| 59 | +} |
| 60 | + |
| 61 | +/** |
| 62 | + * Builds a retry URL by replacing the source origin with `nextOrigin` and |
| 63 | + * appending a `retry=<n>` query parameter. |
| 64 | + * |
| 65 | + * - Supports absolute and relative `src` values. |
| 66 | + * - Normalizes `nextOrigin` to its origin (ignoring trailing slashes/paths). |
| 67 | + * - Assumes `src` does not contain a URL fragment (`#...`). |
| 68 | + * |
| 69 | + * @private |
| 70 | + */ |
| 71 | +export function getRetrySrc(src: string, nextOrigin: string, retryCount: number): string { |
| 72 | + const srcOrigin = originOf(src); |
| 73 | + const normalizedNextOrigin = originOf(nextOrigin); |
| 74 | + |
| 75 | + let pathAndQuery = stringHasPrefix(src, srcOrigin) ? src.slice(srcOrigin.length) : src; |
| 76 | + if (pathAndQuery.length === 0) { |
| 77 | + pathAndQuery = "/"; |
| 78 | + } else if (!stringHasPrefix(pathAndQuery, "/")) { |
| 79 | + pathAndQuery = "/" + pathAndQuery; |
| 80 | + } |
| 81 | + |
| 82 | + const separator = pathAndQuery.indexOf("?") === -1 ? "?" : "&"; |
| 83 | + return normalizedNextOrigin + pathAndQuery + separator + "retry=" + retryCount; |
| 84 | +} |
0 commit comments