John 43120bc3c3
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>
2026-06-12 11:50:16 +01:00

198 lines
6.1 KiB
TypeScript

import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
import fs from 'fs';
import path from 'path';
import semver from 'semver';
import {JavaBase} from '../base-installer';
import {IAdoptAvailableVersions} from './models';
import {
JavaDownloadRelease,
JavaInstallerOptions,
JavaInstallerResults
} from '../base-models';
import {
extractJdkFile,
getNextPageUrlFromLinkHeader,
getDownloadArchiveExtension,
isVersionSatisfies,
renameWinArchive,
MAX_PAGINATION_PAGES,
validatePaginationUrl
} from '../../util';
export enum AdoptImplementation {
Hotspot = 'Hotspot',
OpenJ9 = 'OpenJ9'
}
export class AdoptDistribution extends JavaBase {
constructor(
installerOptions: JavaInstallerOptions,
private readonly jvmImpl: AdoptImplementation
) {
super(`Adopt-${jvmImpl}`, installerOptions);
}
protected async findPackageForDownload(
version: string
): Promise<JavaDownloadRelease> {
const availableVersionsRaw = await this.getAvailableVersions();
const availableVersionsWithBinaries = availableVersionsRaw
.filter(item => item.binaries.length > 0)
.map(item => {
return {
version: item.version_data.semver,
url: item.binaries[0].package.link
} as JavaDownloadRelease;
});
const satisfiedVersions = availableVersionsWithBinaries
.filter(item => isVersionSatisfies(version, item.version))
.sort((a, b) => {
return -semver.compareBuild(a.version, b.version);
});
const resolvedFullVersion =
satisfiedVersions.length > 0 ? satisfiedVersions[0] : null;
if (!resolvedFullVersion) {
const availableVersionStrings = availableVersionsWithBinaries.map(
item => item.version
);
throw this.createVersionNotFoundError(version, availableVersionStrings);
}
return resolvedFullVersion;
}
protected async downloadTool(
javaRelease: JavaDownloadRelease
): Promise<JavaInstallerResults> {
core.info(
`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`
);
let javaArchivePath = await tc.downloadTool(javaRelease.url);
core.info(`Extracting Java archive...`);
const extension = getDownloadArchiveExtension();
if (process.platform === 'win32') {
javaArchivePath = renameWinArchive(javaArchivePath);
}
const extractedJavaPath = await extractJdkFile(javaArchivePath, extension);
const archiveName = fs.readdirSync(extractedJavaPath)[0];
const archivePath = path.join(extractedJavaPath, archiveName);
const version = this.getToolcacheVersionName(javaRelease.version);
const javaPath = await tc.cacheDir(
archivePath,
this.toolcacheFolderName,
version,
this.architecture
);
return {version: javaRelease.version, path: javaPath};
}
protected get toolcacheFolderName(): string {
if (this.jvmImpl === AdoptImplementation.Hotspot) {
// exclude Hotspot postfix from distribution name because Hosted runners have pre-cached Adopt OpenJDK under "Java_Adopt_jdk"
// for more information see: https://github.com/actions/setup-java/pull/155#discussion_r610451063
return `Java_Adopt_${this.packageType}`;
}
return super.toolcacheFolderName;
}
private async getAvailableVersions(): Promise<IAdoptAvailableVersions[]> {
const platform = this.getPlatformOption();
const arch = this.distributionArchitecture();
const imageType = this.packageType;
const versionRange = encodeURI('[1.0,100.0]'); // retrieve all available versions
const releaseType = this.stable ? 'ga' : 'ea';
if (core.isDebug()) {
console.time('Retrieving available versions for Adopt took'); // eslint-disable-line no-console
}
const baseRequestArguments = [
`project=jdk`,
'vendor=adoptopenjdk',
`heap_size=normal`,
'sort_method=DEFAULT',
'sort_order=DESC',
`os=${platform}`,
`architecture=${arch}`,
`image_type=${imageType}`,
`release_type=${releaseType}`,
`jvm_impl=${this.jvmImpl.toLowerCase()}`
].join('&');
const requestArguments = `${baseRequestArguments}&page_size=20&page=0`;
let availableVersionsUrl: string | null =
`https://api.adoptopenjdk.net/v3/assets/version/${versionRange}?${requestArguments}`;
const availableVersions: IAdoptAvailableVersions[] = [];
let pageCount = 0;
if (core.isDebug()) {
core.debug(`Gathering available versions from '${availableVersionsUrl}'`);
}
while (availableVersionsUrl) {
pageCount++;
const response =
await this.http.getJson<IAdoptAvailableVersions[]>(
availableVersionsUrl
);
const paginationPage = response.result;
const nextUrl = getNextPageUrlFromLinkHeader(response.headers);
if (
nextUrl &&
!validatePaginationUrl(nextUrl, 'https://api.adoptopenjdk.net')
) {
core.warning(
`Ignoring pagination link with unexpected origin: ${nextUrl}`
);
availableVersionsUrl = null;
} else {
availableVersionsUrl = nextUrl;
}
if (paginationPage === null || paginationPage.length === 0) {
break;
}
availableVersions.push(...paginationPage);
if (pageCount >= MAX_PAGINATION_PAGES) {
core.warning(
`Reached pagination safeguard limit (${MAX_PAGINATION_PAGES} pages) while listing Adopt releases.`
);
break;
}
}
if (core.isDebug()) {
core.startGroup('Print information about available versions');
console.timeEnd('Retrieving available versions for Adopt took'); // eslint-disable-line no-console
core.debug(`Available versions: [${availableVersions.length}]`);
core.debug(
availableVersions.map(item => item.version_data.semver).join(', ')
);
core.endGroup();
}
return availableVersions;
}
private getPlatformOption(): string {
// Adopt has own platform names so need to map them
switch (process.platform) {
case 'darwin':
return 'mac';
case 'win32':
return 'windows';
default:
return process.platform;
}
}
}