import url from 'url' ; import { DateTime } from 'luxon' ; import { XmlDocument } from 'xmldoc' ; import { HOST_DISABLED } from '../../constants/error-messages' ; import { logger } from '../../logger' ; import { ExternalHostError } from '../../types/errors/external-host-error' ; import { Http , HttpResponse } from '../../util/http' ; import { regEx } from '../../util/regex' ; import { normalizeDate } from '../metadata' ; import type { ReleaseResult } from '../types' ; import { MAVEN_REPO , id } from './common' ; import type { HttpResourceCheckResult , MavenDependency , MavenXml , } from './types' ; const http = { } ; function httpByHostType ( hostType ) { if ( ! http [ hostType ] ) { http [ hostType ] = new Http ( hostType ) ; } return http [ hostType ] ; } const getHost = ( x ) : string => new url . URL ( x ) . host ; function isMavenCentral ( pkgUrl ) { const host = typeof pkgUrl === 'string' ? pkgUrl : pkgUrl . host ; return getHost ( MAVEN_REPO ) === host ; } function isTemporalError ( err ) { return ( err . code === 'ECONNRESET' || err . statusCode === 429 || ( err . statusCode >= 500 && err . statusCode < 600 ) ) ; } function isHostError ( err ) { return err . code === 'ETIMEDOUT' ; } function isNotFoundError ( err ) { return err . code === 'ENOTFOUND' || err . statusCode === 404 ; } function isPermissionsIssue ( err ) { return err . statusCode === 401 || err . statusCode === 403 ; } function isConnectionError ( err ) { return ( err . code === 'EAI_AGAIN' || err . code === 'ERR_TLS_CERT_ALTNAME_INVALID' || err . code === 'ECONNREFUSED' ) ; } function isUnsupportedHostError ( err ) { return err . name === 'UnsupportedProtocolError' ; } export async function downloadHttpProtocol ( pkgUrl , hostType = id ) { let raw ; try { const httpClient = httpByHostType ( hostType ) ; raw = await httpClient . get ( pkgUrl . toString ( ) ) ; return raw ; } catch ( err ) { const failedUrl = pkgUrl . toString ( ) ; if ( err . message === HOST_DISABLED ) { logger . trace ( { failedUrl } , 'Host disabled' ) ; } else if ( isNotFoundError ( err ) ) { logger . trace ( { failedUrl } , `Url not found` ) ; } else if ( isHostError ( err ) ) { logger . debug ( { failedUrl } , `Cannot connect to ${ hostType } host` ) ; } else if ( isPermissionsIssue ( err ) ) { logger . debug ( { failedUrl } , 'Dependency lookup unauthorized. Please add authentication with a hostRule' ) ; } else if ( isTemporalError ( err ) ) { logger . debug ( { failedUrl , err } , 'Temporary error' ) ; if ( isMavenCentral ( pkgUrl ) ) { throw new ExternalHostError ( err ) ; } } else if ( isConnectionError ( err ) ) { logger . debug ( { failedUrl } , 'Connection refused to maven registry' ) ; } else if ( isUnsupportedHostError ( err ) ) { logger . debug ( { failedUrl } , 'Unsupported host' ) ; } else { logger . info ( { failedUrl , err } , 'Unknown error' ) ; } return { } ; } } export async function checkHttpResource ( pkgUrl , hostType = id ) { try { const httpClient = httpByHostType ( hostType ) ; const res = await httpClient . head ( pkgUrl . toString ( ) ) ; const timestamp = res ?. headers ?. [ 'last-modified' ] ; if ( timestamp ) { const isoTimestamp = normalizeDate ( timestamp ) ; if ( isoTimestamp ) { const releaseDate = DateTime . fromISO ( isoTimestamp , { zone : 'UTC' , } ) . toJSDate ( ) ; return releaseDate ; } } return 'found' ; } catch ( err ) { if ( isNotFoundError ( err ) ) { return 'not-found' ; } const failedUrl = pkgUrl . toString ( ) ; logger . debug ( { failedUrl , statusCode : err . statusCode } , `Can't check HTTP resource existence` ) ; return 'error' ; } } function containsPlaceholder ( str ) { return regEx ( /\${.*?}/g ) . test ( str ) ; } export function getMavenUrl ( dependency , repoUrl , path ) { return new url . URL ( `${ dependency . dependencyUrl }/${ path }` , repoUrl ) ; } export async function downloadMavenXml ( pkgUrl ) { if ( ! pkgUrl ) { return { } ; } let rawContent ; let authorization ; let statusCode ; switch ( pkgUrl . protocol ) { case 'http:' : case 'https:' : ( { authorization , body : rawContent , statusCode , } = await downloadHttpProtocol ( pkgUrl ) ) ; break ; case 's3:' : logger . debug ( 'Skipping s3 dependency' ) ; return { } ; default : logger . debug ( { url : pkgUrl . toString ( ) } , `Unsupported Maven protocol` ) ; return { } ; } if ( ! rawContent ) { logger . debug ( { url : pkgUrl . toString ( ) , statusCode } , `Content is not found for Maven url` ) ; return { } ; } return { authorization , xml : new XmlDocument ( rawContent ) } ; } export function getDependencyParts ( lookupName ) { const [ group , name ] = lookupName . split ( ':' ) ; const dependencyUrl = `${ group . replace ( regEx ( /\./g ) , '/' ) }/${ name }` ; return { display : lookupName , group , name , dependencyUrl , } ; } export async function getDependencyInfo ( dependency , repoUrl , version , recursionLimit = 5 ) { const result = { } ; const path = `${ version }/${ dependency . name }-${ version }.pom` ; const pomUrl = getMavenUrl ( dependency , repoUrl , path ) ; const { xml : pomContent } = await downloadMavenXml ( pomUrl ) ; if ( ! pomContent ) { return result ; } const homepage = pomContent . valueWithPath ( 'url' ) ; if ( homepage && ! containsPlaceholder ( homepage ) ) { result . homepage = homepage ; } const sourceUrl = pomContent . valueWithPath ( 'scm.url' ) ; if ( sourceUrl && ! containsPlaceholder ( sourceUrl ) ) { result . sourceUrl = sourceUrl . replace ( regEx ( /^scm:/ ) , '' ) . replace ( regEx ( /^git:/ ) , '' ) . replace ( regEx ( /^git@github.com:/ ) , 'https://github.com/' ) . replace ( regEx ( /^git@github.com\// ) , 'https://github.com/' ) . replace ( regEx ( /\.git$/ ) , '' ) ; if ( result . sourceUrl . startsWith ( '//' ) ) { result . sourceUrl = `https:${ result . sourceUrl }` ; } } const parent = pomContent . childNamed ( 'parent' ) ; if ( recursionLimit > 0 && parent && ( ! result . sourceUrl || ! result . homepage ) ) { const [ parentGroupId , parentArtifactId , parentVersion ] = [ 'groupId' , 'artifactId' , 'version' , ] . map ( ( k ) => parent . valueWithPath ( k ) ?. replace ( /\s+/g , '' ) ) ; if ( parentGroupId && parentArtifactId && parentVersion ) { const parentDisplayId = `${ parentGroupId }:${ parentArtifactId }` ; const parentDependency = getDependencyParts ( parentDisplayId ) ; const parentInformation = await getDependencyInfo ( parentDependency , repoUrl , parentVersion , recursionLimit - 1 ) ; if ( ! result . sourceUrl && parentInformation . sourceUrl ) { result . sourceUrl = parentInformation . sourceUrl ; } if ( ! result . homepage && parentInformation . homepage ) { result . homepage = parentInformation . homepage ; } } } return result ; }