Files
shields/services/nexus/nexus.service.js
chris48s 954147f7d9 URL validator tidyup; affects [discourse dynamic endpoint gerrit jira maven nexus osslifecycle python vpm website] securityheaders sonar swagger w3c (#10810)
* add a required url validator

* replace occurrences of optionalUrl.required() with url

* use standard validators in server.js
2025-01-18 19:16:41 +00:00

319 lines
9.4 KiB
JavaScript

import Joi from 'joi'
import { renderVersionBadge } from '../version.js'
import {
url,
optionalDottedVersionNClausesWithOptionalSuffix,
} from '../validators.js'
import {
BaseJsonService,
InvalidResponse,
NotFound,
pathParams,
queryParams,
} from '../index.js'
import { isSnapshotVersion } from './nexus-version.js'
const nexus2SearchApiSchema = Joi.object({
data: Joi.array()
.items(
Joi.object({
latestRelease: optionalDottedVersionNClausesWithOptionalSuffix,
latestSnapshot: optionalDottedVersionNClausesWithOptionalSuffix,
// `version` will almost always follow the same pattern as optionalDottedVersionNClausesWithOptionalSuffix.
// However, there are a couple exceptions where `version` may be a simple string (like `android-SNAPSHOT`)
// This schema is relaxed accordingly since for snapshot/release badges the schema has to validate
// the entire history of each published version for the artifact.
// Example artifact that includes such a historical version: https://oss.sonatype.org/service/local/lucene/search?g=com.google.guava&a=guava
version: Joi.string(),
}),
)
.required(),
}).required()
const nexus3SearchApiSchema = Joi.object({
items: Joi.array()
.items(
Joi.object({
// This schema is relaxed similarly to nexux2SearchApiSchema
version: Joi.string().required(),
}),
)
.required(),
}).required()
const nexus2ResolveApiSchema = Joi.object({
data: Joi.object({
baseVersion: optionalDottedVersionNClausesWithOptionalSuffix,
version: optionalDottedVersionNClausesWithOptionalSuffix,
}).required(),
}).required()
const queryParamSchema = Joi.object({
server: url,
queryOpt: Joi.string()
.regex(/(:[\w.]+=[^:]*)+/i)
.optional(),
nexusVersion: Joi.equal('2', '3'),
}).required()
const openApiQueryParams = queryParams(
{ name: 'server', example: 'https://oss.sonatype.org', required: true },
{
name: 'nexusVersion',
example: '2',
schema: { type: 'string', enum: ['2', '3'] },
description:
'Specifying `nexusVersion=3` when targeting Nexus 3 servers will speed up the badge rendering.',
},
{
name: 'queryOpt',
example: ':c=agent-apple-osx:p=tar.gz',
description: `
Note that you can use query options with any Nexus badge type (Releases, Snapshots, or Repository).
Query options should be provided as key=value pairs separated by a colon.
Possible values:
<ul>
<li><a href="https://nexus.pentaho.org/swagger-ui/#/search/search">All Nexus 3 badges</a></li>
<li><a href="https://repository.sonatype.org/nexus-restlet1x-plugin/default/docs/path__artifact_maven_resolve.html">Nexus 2 Releases and Snapshots badges</a></li>
<li><a href="https://repository.sonatype.org/nexus-indexer-lucene-plugin/default/docs/path__lucene_search.html">Nexus 2 Repository badges</a></li>
</ul>
`,
},
)
export default class Nexus extends BaseJsonService {
static category = 'version'
static route = {
base: 'nexus',
pattern: ':repo(r|s|[^/]+)/:groupId/:artifactId',
queryParamSchema,
}
static auth = {
userKey: 'nexus_user',
passKey: 'nexus_pass',
serviceKey: 'nexus',
}
static openApi = {
'/nexus/r/{groupId}/{artifactId}': {
get: {
summary: 'Sonatype Nexus (Releases)',
parameters: [
...pathParams(
{ name: 'groupId', example: 'com.google.guava' },
{ name: 'artifactId', example: 'guava' },
),
...openApiQueryParams,
],
},
},
'/nexus/s/{groupId}/{artifactId}': {
get: {
summary: 'Sonatype Nexus (Snapshots)',
parameters: [
...pathParams(
{ name: 'groupId', example: 'com.google.guava' },
{ name: 'artifactId', example: 'guava' },
),
...openApiQueryParams,
],
},
},
'/nexus/{repo}/{groupId}/{artifactId}': {
get: {
summary: 'Sonatype Nexus (Repository)',
parameters: [
...pathParams(
{ name: 'repo', example: 'snapshots' },
{ name: 'groupId', example: 'com.google.guava' },
{ name: 'artifactId', example: 'guava' },
),
...openApiQueryParams,
],
},
},
}
static defaultBadgeData = {
label: 'nexus',
}
addQueryParamsToQueryString({ searchParams, queryOpt }) {
// Users specify query options with 'key=value' pairs, using a
// colon delimiter between pairs ([:k1=v1[:k2=v2[...]]]).
// queryOpt will be a string containing those key/value pairs,
// For example: :c=agent-apple-osx:p=tar.gz
const keyValuePairs = queryOpt.split(':')
keyValuePairs.forEach(keyValuePair => {
const paramParts = keyValuePair.split('=')
const paramKey = paramParts[0]
const paramValue = paramParts[1]
searchParams[paramKey] = paramValue
})
}
async fetch({ server, repo, groupId, artifactId, queryOpt, nexusVersion }) {
if (nexusVersion === '3') {
return this.fetch3({ server, repo, groupId, artifactId, queryOpt })
}
// Most servers still use Nexus 2. Fall back to Nexus 3 if the hitting a
// Nexus 2 endpoint returns a Bad Request (=> InvalidResponse, for path /service/local/artifact/maven/resolve)
// or a Not Found (for path /service/local/artifact/maven/resolve).
try {
return await this.fetch2({ server, repo, groupId, artifactId, queryOpt })
} catch (e) {
if (e instanceof InvalidResponse || e instanceof NotFound) {
return this.fetch3({ server, repo, groupId, artifactId, queryOpt })
}
throw e
}
}
async fetch2({ server, repo, groupId, artifactId, queryOpt }) {
const searchParams = {
g: groupId,
a: artifactId,
}
let schema
let url = `${server}${server.slice(-1) === '/' ? '' : '/'}`
// API pattern:
// for /nexus/[rs]/... pattern, use the search api of the nexus server, and
// for /nexus/<repo-name>/... pattern, use the resolve api of the nexus server.
if (repo === 'r' || repo === 's') {
schema = nexus2SearchApiSchema
url += 'service/local/lucene/search'
} else {
schema = nexus2ResolveApiSchema
url += 'service/local/artifact/maven/resolve'
searchParams.r = repo
searchParams.v = 'LATEST'
}
if (queryOpt) {
this.addQueryParamsToQueryString({ searchParams, queryOpt })
}
const json = await this._requestJson(
this.authHelper.withBasicAuth({
schema,
url,
options: { searchParams },
httpErrors: {
404: 'artifact not found',
},
}),
)
return { actualNexusVersion: '2', json }
}
async fetch3({ server, repo, groupId, artifactId, queryOpt }) {
const searchParams = {
group: groupId,
name: artifactId,
sort: 'version',
}
switch (repo) {
case 's':
searchParams.prerelease = 'true'
break
case 'r':
searchParams.prerelease = 'false'
break
default:
searchParams.repository = repo
break
}
if (queryOpt) {
this.addQueryParamsToQueryString({ searchParams, queryOpt })
}
const url = `${server}${
server.slice(-1) === '/' ? '' : '/'
}service/rest/v1/search`
const json = await this._requestJson(
this.authHelper.withBasicAuth({
schema: nexus3SearchApiSchema,
url,
options: { searchParams },
httpErrors: {
404: 'artifact not found',
},
}),
)
return { actualNexusVersion: '3', json }
}
transform({ repo, json, actualNexusVersion }) {
return actualNexusVersion === '3'
? this.transform3({ repo, json })
: this.transform2({ repo, json })
}
transform2({ repo, json }) {
if (json.data.length === 0) {
throw new NotFound({ prettyMessage: 'artifact or version not found' })
}
if (repo === 'r') {
const version = json.data[0].latestRelease
if (!version) {
throw new InvalidResponse({ prettyMessage: 'invalid artifact version' })
}
return { version }
} else if (repo === 's') {
// only want to match 1.2.3-SNAPSHOT style versions, which may not always be in
// 'latestSnapshot' so check 'version' as well before continuing to next entry
for (const artifact of json.data) {
if (isSnapshotVersion(artifact.latestSnapshot)) {
return { version: artifact.latestSnapshot }
}
if (isSnapshotVersion(artifact.version)) {
return { version: artifact.version }
}
}
throw new InvalidResponse({ prettyMessage: 'no snapshot versions found' })
} else {
const version = json.data.baseVersion || json.data.version
if (!version) {
throw new InvalidResponse({ prettyMessage: 'invalid artifact version' })
}
return { version }
}
}
transform3({ repo, json }) {
if (json.items.length === 0) {
const versionType = repo === 's' ? 'snapshot ' : ''
throw new NotFound({
prettyMessage: `artifact or ${versionType}version not found`,
})
}
return { version: json.items[0].version }
}
async handle(
{ repo, groupId, artifactId },
{ server, queryOpt, nexusVersion },
) {
const { actualNexusVersion, json } = await this.fetch({
repo,
server,
groupId,
artifactId,
queryOpt,
nexusVersion,
})
const { version } = this.transform({ repo, json, actualNexusVersion })
return renderVersionBadge({ version })
}
}