@@ -2,6 +2,7 @@ import * as os from 'os';
22import * as path from 'path' ;
33import * as jsonc from 'jsonc-parser' ;
44
5+ import { runCommandNoPty , plainExec } from '../spec-common/commonUtils' ;
56import { request , requestResolveHeaders } from '../spec-utils/httpRequest' ;
67import { LogLevel } from '../spec-utils/log' ;
78import { isLocalFile , readLocalFile } from '../spec-utils/pfs' ;
@@ -16,6 +17,15 @@ interface DockerConfigFile {
1617 identitytoken ?: string ; // Used by Azure Container Registry
1718 } ;
1819 } ;
20+ credHelpers : {
21+ [ registry : string ] : string ;
22+ } ;
23+ credsStore : string ;
24+ }
25+
26+ interface CredentialHelperResult {
27+ Username : string ;
28+ Secret : string ;
1929}
2030
2131// WWW-Authenticate Regex
@@ -137,8 +147,6 @@ async function getCredential(params: CommonParams, ociRef: OCIRef | OCICollectio
137147 const { output, env } = params ;
138148 const { registry } = ociRef ;
139149
140- // TODO: Ask docker credential helper for credentials.
141-
142150 if ( ! ! env [ 'GITHUB_TOKEN' ] && registry === 'ghcr.io' ) {
143151 output . write ( '[httpOci] Using environment GITHUB_TOKEN for auth' , LogLevel . Trace ) ;
144152 const userToken = `USERNAME:${ env [ 'GITHUB_TOKEN' ] } ` ;
@@ -161,15 +169,31 @@ async function getCredential(params: CommonParams, ociRef: OCIRef | OCICollectio
161169 } ;
162170 }
163171 } else {
172+ let configContainsAuth = false ;
164173 try {
165174 const homeDir = os . homedir ( ) ;
166175 if ( homeDir ) {
167176 const dockerConfigPath = path . join ( homeDir , '.docker' , 'config.json' ) ;
168177 if ( await isLocalFile ( dockerConfigPath ) ) {
169178 const dockerConfig : DockerConfigFile = jsonc . parse ( ( await readLocalFile ( dockerConfigPath ) ) . toString ( ) ) ;
170179
180+ configContainsAuth = Object . keys ( dockerConfig . credHelpers ) . length > 0 || ! ! dockerConfig . credsStore || Object . keys ( dockerConfig . auths ) . length > 0 ;
181+ if ( dockerConfig . credHelpers && dockerConfig . credHelpers [ registry ] ) {
182+ const credHelper = dockerConfig . credHelpers [ registry ] ;
183+ output . write ( `[httpOci] Found credential helper '${ credHelper } ' in '${ dockerConfigPath } ' registry '${ registry } '` , LogLevel . Trace ) ;
184+ const auth = await getCredentialFromHelper ( params , registry , credHelper ) ;
185+ if ( auth ) {
186+ return auth ;
187+ }
188+ } else if ( dockerConfig . credsStore ) {
189+ output . write ( `[httpOci] Invoking credsStore credential helper '${ dockerConfig . credsStore } '` , LogLevel . Trace ) ;
190+ const auth = await getCredentialFromHelper ( params , registry , dockerConfig . credsStore ) ;
191+ if ( auth ) {
192+ return auth ;
193+ }
194+ }
171195 if ( dockerConfig . auths && dockerConfig . auths [ registry ] ) {
172- output . write ( `[httpOci] Found entry in config.json for registry '${ registry } '` , LogLevel . Trace ) ;
196+ output . write ( `[httpOci] Found auths entry in ' ${ dockerConfigPath } ' for registry '${ registry } '` , LogLevel . Trace ) ;
173197 const auth = dockerConfig . auths [ registry ] . auth ;
174198 const identityToken = dockerConfig . auths [ registry ] . identitytoken ; // Refresh token, seen when running: 'az acr login -n <registry>'
175199
@@ -191,13 +215,91 @@ async function getCredential(params: CommonParams, ociRef: OCIRef | OCICollectio
191215 } catch ( err ) {
192216 output . write ( `[httpOci] Failed to read docker config.json: ${ err } ` , LogLevel . Trace ) ;
193217 }
218+
219+ if ( ! configContainsAuth ) {
220+ let defaultCredHelper = '' ;
221+ // Try platform-specific default credential helper
222+ if ( process . platform === 'linux' ) {
223+ if ( await existsInPath ( 'pass' ) ) {
224+ defaultCredHelper = 'pass' ;
225+ } else {
226+ defaultCredHelper = 'secret' ;
227+ }
228+ } else if ( process . platform === 'win32' ) {
229+ defaultCredHelper = 'wincred' ;
230+ } else if ( process . platform === 'darwin' ) {
231+ defaultCredHelper = 'osxkeychain' ;
232+ }
233+ if ( defaultCredHelper !== '' ) {
234+ output . write ( `[httpOci] Invoking platform default credential helper '${ defaultCredHelper } '` , LogLevel . Trace ) ;
235+ const auth = await getCredentialFromHelper ( params , registry , defaultCredHelper ) ;
236+ if ( auth ) {
237+ return auth ;
238+ }
239+ }
240+ }
194241 }
195242
196243 // Represents anonymous access.
197244 output . write ( `[httpOci] No authentication credentials found for registry '${ registry } '. Accessing anonymously.` , LogLevel . Trace ) ;
198245 return ;
199246}
200247
248+ async function existsInPath ( filename : string ) : Promise < boolean > {
249+ if ( ! process . env . PATH ) {
250+ return false ;
251+ }
252+ const paths = process . env . PATH . split ( ':' ) ;
253+ for ( const path of paths ) {
254+ const fullPath = `${ path } /${ filename } ` ;
255+ if ( await isLocalFile ( fullPath ) ) {
256+ return true ;
257+ }
258+ }
259+ return false ;
260+ }
261+
262+ async function getCredentialFromHelper ( params : CommonParams , registry : string , credHelperName : string ) : Promise < { base64EncodedCredential : string | undefined ; refreshToken : string | undefined } | undefined > {
263+ const { output } = params ;
264+
265+ let helperOutput : Buffer ;
266+ try {
267+ const { stdout } = await runCommandNoPty ( {
268+ exec : plainExec ( undefined ) ,
269+ cmd : 'docker-credential-' + credHelperName ,
270+ args : [ 'get' ] ,
271+ stdin : Buffer . from ( registry , 'utf-8' ) ,
272+ output,
273+ } ) ;
274+ helperOutput = stdout ;
275+ } catch ( err ) {
276+ output . write ( `[httpOci] Failed to execute credential helper ${ credHelperName } ` , LogLevel . Error ) ;
277+ return undefined ;
278+ }
279+ if ( helperOutput . length === 0 ) {
280+ return undefined ;
281+ }
282+
283+ let errors : jsonc . ParseError [ ] = [ ] ;
284+ const creds :CredentialHelperResult = jsonc . parse ( helperOutput . toString ( ) , errors ) ;
285+ if ( errors . length !== 0 ) {
286+ output . write ( `[httpOci] Credential helper ${ credHelperName } returned non-JSON response "${ helperOutput . toString ( ) } " for registry ${ registry } ` , LogLevel . Warning ) ;
287+ return undefined ;
288+ }
289+
290+ if ( creds . Username === '<token>' ) {
291+ return {
292+ refreshToken : creds . Secret ,
293+ base64EncodedCredential : undefined ,
294+ } ;
295+ }
296+ const userToken = `${ creds . Username } :${ creds . Secret } ` ;
297+ return {
298+ base64EncodedCredential : Buffer . from ( userToken ) . toString ( 'base64' ) ,
299+ refreshToken : undefined ,
300+ } ;
301+ }
302+
201303// https://docs.docker.com/registry/spec/auth/token/#requesting-a-token
202304async function fetchRegistryBearerToken ( params : CommonParams , ociRef : OCIRef | OCICollectionRef , wwwAuthenticateData : { realm : string ; service : string ; scope : string } ) : Promise < string | undefined > {
203305 const { output } = params ;
0 commit comments