Implement pagination with link headers for Adoptium based apis (#1014)

* Use Link headers for Adoptium pagination

* Fix nullable pagination URL types and rebuild dist

* Add 1000-page safeguard for JetBrains pagination

* Adjust plan for pagination safeguard scope

* Move pagination safeguard to non-JetBrains installers

* Add 1000-page safeguard to Adopt Temurin and Semeru pagination

* Fix Prettier formatting in adopt, semeru, and temurin installer files

* Fix CI audit failure by updating vulnerable transitive deps

* Address PR review: RFC-compliant Link parsing, SSRF validation, centralized constant

- Make getNextPageUrlFromLinkHeader RFC 8288 compliant by splitting
  link-values and checking for rel=next anywhere in the parameters,
  not just as the first parameter after the semicolon.
- Add validatePaginationUrl utility to reject pagination URLs that
  point to unexpected origins (SSRF mitigation).
- Centralize MAX_PAGINATION_PAGES in util.ts instead of duplicating
  across Adopt, Semeru, and Temurin installers.
- Add tests for rel not being the first parameter, and for URL
  origin validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address code review feedback on pagination implementation

- Tighten rel regex with word boundary to prevent false positives
  (e.g., rel="nextsomething" no longer matches).
- Use parsed.origin comparison in validatePaginationUrl to correctly
  handle explicit default ports (e.g., :443 for HTTPS).
- Fix pagination safeguard tests to use same-origin URLs so they
  actually exercise the 1000-page limit instead of being rejected
  by origin validation on the first request.
- Add test for rel="nextsomething" not matching.
- Add test for explicit default port acceptance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix prettier formatting in util.test.ts

* Rebuild dist/ to fix check-dist CI failure

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
John
2026-06-12 11:50:16 +01:00
committed by GitHub
parent ad9d6a6320
commit 43120bc3c3
11 changed files with 496 additions and 111 deletions

145
dist/setup/index.js vendored
View File

@@ -77896,24 +77896,34 @@ class AdoptDistribution extends base_installer_1.JavaBase {
`release_type=${releaseType}`,
`jvm_impl=${this.jvmImpl.toLowerCase()}`
].join('&');
// need to iterate through all pages to retrieve the list of all versions
// Adopt API doesn't provide way to retrieve the count of pages to iterate so infinity loop
let page_index = 0;
const requestArguments = `${baseRequestArguments}&page_size=20&page=0`;
let availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`;
const availableVersions = [];
while (true) {
const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`;
const availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`;
if (core.isDebug() && page_index === 0) {
// url is identical except page_index so print it once for debug
core.debug(`Gathering available versions from '${availableVersionsUrl}'`);
let pageCount = 0;
if (core.isDebug()) {
core.debug(`Gathering available versions from '${availableVersionsUrl}'`);
}
while (availableVersionsUrl) {
pageCount++;
const response = yield this.http.getJson(availableVersionsUrl);
const paginationPage = response.result;
const nextUrl = (0, util_1.getNextPageUrlFromLinkHeader)(response.headers);
if (nextUrl &&
!(0, util_1.validatePaginationUrl)(nextUrl, 'https://api.adoptopenjdk.net')) {
core.warning(`Ignoring pagination link with unexpected origin: ${nextUrl}`);
availableVersionsUrl = null;
}
else {
availableVersionsUrl = nextUrl;
}
const paginationPage = (yield this.http.getJson(availableVersionsUrl)).result;
if (paginationPage === null || paginationPage.length === 0) {
// break infinity loop because we have reached end of pagination
break;
}
availableVersions.push(...paginationPage);
page_index++;
if (pageCount >= util_1.MAX_PAGINATION_PAGES) {
core.warning(`Reached pagination safeguard limit (${util_1.MAX_PAGINATION_PAGES} pages) while listing Adopt releases.`);
break;
}
}
if (core.isDebug()) {
core.startGroup('Print information about available versions');
@@ -80071,24 +80081,34 @@ class SemeruDistribution extends base_installer_1.JavaBase {
`release_type=${releaseType}`,
`jvm_impl=openj9`
].join('&');
// need to iterate through all pages to retrieve the list of all versions
// Adoptium API doesn't provide way to retrieve the count of pages to iterate so infinity loop
let page_index = 0;
const requestArguments = `${baseRequestArguments}&page_size=20&page=0`;
let availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`;
const availableVersions = [];
while (true) {
const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`;
const availableVersionsUrl = `https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`;
if (core.isDebug() && page_index === 0) {
// url is identical except page_index so print it once for debug
core.debug(`Gathering available versions from '${availableVersionsUrl}'`);
let pageCount = 0;
if (core.isDebug()) {
core.debug(`Gathering available versions from '${availableVersionsUrl}'`);
}
while (availableVersionsUrl) {
pageCount++;
const response = yield this.http.getJson(availableVersionsUrl);
const paginationPage = response.result;
const nextUrl = (0, util_1.getNextPageUrlFromLinkHeader)(response.headers);
if (nextUrl &&
!(0, util_1.validatePaginationUrl)(nextUrl, 'https://api.adoptopenjdk.net')) {
core.warning(`Ignoring pagination link with unexpected origin: ${nextUrl}`);
availableVersionsUrl = null;
}
else {
availableVersionsUrl = nextUrl;
}
const paginationPage = (yield this.http.getJson(availableVersionsUrl)).result;
if (paginationPage === null || paginationPage.length === 0) {
// break infinity loop because we have reached end of pagination
break;
}
availableVersions.push(...paginationPage);
page_index++;
if (pageCount >= util_1.MAX_PAGINATION_PAGES) {
core.warning(`Reached pagination safeguard limit (${util_1.MAX_PAGINATION_PAGES} pages) while listing Semeru releases.`);
break;
}
}
if (core.isDebug()) {
core.startGroup('Print information about available versions');
@@ -80245,24 +80265,34 @@ class TemurinDistribution extends base_installer_1.JavaBase {
`release_type=${releaseType}`,
`jvm_impl=${this.jvmImpl.toLowerCase()}`
].join('&');
// need to iterate through all pages to retrieve the list of all versions
// Adoptium API doesn't provide way to retrieve the count of pages to iterate so infinity loop
let page_index = 0;
const requestArguments = `${baseRequestArguments}&page_size=20&page=0`;
let availableVersionsUrl = `https://api.adoptium.net/v3/assets/version/${versionRange}?${requestArguments}`;
const availableVersions = [];
while (true) {
const requestArguments = `${baseRequestArguments}&page_size=20&page=${page_index}`;
const availableVersionsUrl = `https://api.adoptium.net/v3/assets/version/${versionRange}?${requestArguments}`;
if (core.isDebug() && page_index === 0) {
// url is identical except page_index so print it once for debug
core.debug(`Gathering available versions from '${availableVersionsUrl}'`);
let pageCount = 0;
if (core.isDebug()) {
core.debug(`Gathering available versions from '${availableVersionsUrl}'`);
}
while (availableVersionsUrl) {
pageCount++;
const response = yield this.http.getJson(availableVersionsUrl);
const paginationPage = response.result;
const nextUrl = (0, util_1.getNextPageUrlFromLinkHeader)(response.headers);
if (nextUrl &&
!(0, util_1.validatePaginationUrl)(nextUrl, 'https://api.adoptium.net')) {
core.warning(`Ignoring pagination link with unexpected origin: ${nextUrl}`);
availableVersionsUrl = null;
}
else {
availableVersionsUrl = nextUrl;
}
const paginationPage = (yield this.http.getJson(availableVersionsUrl)).result;
if (paginationPage === null || paginationPage.length === 0) {
// break infinity loop because we have reached end of pagination
break;
}
availableVersions.push(...paginationPage);
page_index++;
if (pageCount >= util_1.MAX_PAGINATION_PAGES) {
core.warning(`Reached pagination safeguard limit (${util_1.MAX_PAGINATION_PAGES} pages) while listing Temurin releases.`);
break;
}
}
if (core.isDebug()) {
core.startGroup('Print information about available versions');
@@ -80893,7 +80923,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.renameWinArchive = exports.getGitHubHttpHeaders = exports.convertVersionToSemver = exports.getVersionFromFileContent = exports.isCacheFeatureAvailable = exports.isGhes = exports.isJobStatusSuccess = exports.getToolcachePath = exports.isVersionSatisfies = exports.getDownloadArchiveExtension = exports.extractJdkFile = exports.getVersionFromToolcachePath = exports.getBooleanInput = exports.getTempDir = void 0;
exports.renameWinArchive = exports.validatePaginationUrl = exports.getNextPageUrlFromLinkHeader = exports.MAX_PAGINATION_PAGES = exports.getGitHubHttpHeaders = exports.convertVersionToSemver = exports.getVersionFromFileContent = exports.isCacheFeatureAvailable = exports.isGhes = exports.isJobStatusSuccess = exports.getToolcachePath = exports.isVersionSatisfies = exports.getDownloadArchiveExtension = exports.extractJdkFile = exports.getVersionFromToolcachePath = exports.getBooleanInput = exports.getTempDir = void 0;
const os_1 = __importDefault(__nccwpck_require__(22037));
const path_1 = __importDefault(__nccwpck_require__(71017));
const fs = __importStar(__nccwpck_require__(57147));
@@ -81060,6 +81090,47 @@ function getGitHubHttpHeaders() {
return headers;
}
exports.getGitHubHttpHeaders = getGitHubHttpHeaders;
exports.MAX_PAGINATION_PAGES = 1000;
function getNextPageUrlFromLinkHeader(headers) {
var _a;
if (!headers) {
return null;
}
const linkHeader = (_a = headers.link) !== null && _a !== void 0 ? _a : headers.Link;
if (!linkHeader) {
return null;
}
const normalizedLinkHeader = Array.isArray(linkHeader)
? linkHeader.join(',')
: linkHeader;
// Split into individual link-values and find the one with rel="next"
// RFC 8288 allows rel to appear anywhere among the parameters
const linkValues = normalizedLinkHeader.split(/,(?=\s*<)/);
for (const linkValue of linkValues) {
const urlMatch = linkValue.match(/<([^>]+)>/);
if (!urlMatch)
continue;
const params = linkValue.slice(urlMatch[0].length);
// Use word boundary to match "next" as a standalone relation type
// RFC 8288 allows space-separated relation types like rel="next prev"
if (/;\s*rel="?[^"]*\bnext\b/i.test(params)) {
return urlMatch[1];
}
}
return null;
}
exports.getNextPageUrlFromLinkHeader = getNextPageUrlFromLinkHeader;
function validatePaginationUrl(url, allowedOrigin) {
try {
const parsed = new URL(url);
const allowed = new URL(allowedOrigin);
return parsed.origin === allowed.origin;
}
catch (_a) {
return false;
}
}
exports.validatePaginationUrl = validatePaginationUrl;
// Rename archive to add extension because after downloading
// archive does not contain extension type and it leads to some issues
// on Windows runners without PowerShell Core.