@@ -7,15 +7,14 @@ const npa = require('npm-package-arg')
7
7
const rpj = require ( 'read-package-json-fast' )
8
8
const pickManifest = require ( 'npm-pick-manifest' )
9
9
const ssri = require ( 'ssri' )
10
+ const crypto = require ( 'crypto' )
10
11
11
12
// Corgis are cute. 🐕🐶
12
13
const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
13
14
const fullDoc = 'application/json'
14
15
15
16
const fetch = require ( 'npm-registry-fetch' )
16
17
17
- // TODO: memoize reg requests, so we don't even have to check cache
18
-
19
18
const _headers = Symbol ( '_headers' )
20
19
class RegistryFetcher extends Fetcher {
21
20
constructor ( spec , opts ) {
@@ -39,28 +38,30 @@ class RegistryFetcher extends Fetcher {
39
38
this . packumentUrl = removeTrailingSlashes ( this . registry ) + '/' +
40
39
this . spec . escapedName
41
40
41
+ const parsed = new URL ( this . registry )
42
+ const regKey = `//${ parsed . host } ${ parsed . pathname } `
43
+ // unlike the nerf-darted auth keys, this one does *not* allow a mismatch
44
+ // of trailing slashes. It must match exactly.
45
+ if ( this . opts [ `${ regKey } :_keys` ] ) {
46
+ this . registryKeys = this . opts [ `${ regKey } :_keys` ]
47
+ }
48
+
42
49
// XXX pacote <=9 has some logic to ignore opts.resolved if
43
50
// the resolved URL doesn't go to the same registry.
44
51
// Consider reproducing that here, to throw away this.resolved
45
52
// in that case.
46
53
}
47
54
48
- resolve ( ) {
49
- if ( this . resolved ) {
50
- return Promise . resolve ( this . resolved )
51
- }
52
-
53
- // fetching the manifest sets resolved and (usually) integrity
54
- return this . manifest ( ) . then ( ( ) => {
55
- if ( this . resolved ) {
56
- return this . resolved
57
- }
58
-
55
+ async resolve ( ) {
56
+ // fetching the manifest sets resolved and (if present) integrity
57
+ await this . manifest ( )
58
+ if ( ! this . resolved ) {
59
59
throw Object . assign (
60
60
new Error ( 'Invalid package manifest: no `dist.tarball` field' ) ,
61
61
{ package : this . spec . toString ( ) }
62
62
)
63
- } )
63
+ }
64
+ return this . resolved
64
65
}
65
66
66
67
[ _headers ] ( ) {
@@ -87,91 +88,127 @@ class RegistryFetcher extends Fetcher {
87
88
// npm-registry-fetch the packument
88
89
// set the appropriate header for corgis if fullMetadata isn't set
89
90
// return the res.json() promise
90
- const p = fetch ( this . packumentUrl , {
91
- ...this . opts ,
92
- headers : this [ _headers ] ( ) ,
93
- spec : this . spec ,
94
- // never check integrity for packuments themselves
95
- integrity : null ,
96
- } ) . then ( res => res . json ( ) . then ( packument => {
91
+ try {
92
+ const res = await fetch ( this . packumentUrl , {
93
+ ...this . opts ,
94
+ headers : this [ _headers ] ( ) ,
95
+ spec : this . spec ,
96
+ // never check integrity for packuments themselves
97
+ integrity : null ,
98
+ } )
99
+ const packument = await res . json ( )
97
100
packument . _cached = res . headers . has ( 'x-local-cache' )
98
101
packument . _contentLength = + res . headers . get ( 'content-length' )
99
102
if ( this . packumentCache ) {
100
103
this . packumentCache . set ( this . packumentUrl , packument )
101
104
}
102
105
return packument
103
- } ) ) . catch ( er => {
106
+ } catch ( err ) {
104
107
if ( this . packumentCache ) {
105
108
this . packumentCache . delete ( this . packumentUrl )
106
109
}
107
- if ( er . code === 'E404' && ! this . fullMetadata ) {
108
- // possible that corgis are not supported by this registry
109
- this . fullMetadata = true
110
- return this . packument ( )
110
+ if ( err . code !== 'E404' || this . fullMetadata ) {
111
+ throw err
111
112
}
112
- throw er
113
- } )
114
- if ( this . packumentCache ) {
115
- this . packumentCache . set ( this . packumentUrl , p )
113
+ // possible that corgis are not supported by this registry
114
+ this . fullMetadata = true
115
+ return this . packument ( )
116
116
}
117
- return p
118
117
}
119
118
120
- manifest ( ) {
119
+ async manifest ( ) {
121
120
if ( this . package ) {
122
- return Promise . resolve ( this . package )
121
+ return this . package
123
122
}
124
123
125
- return this . packument ( )
126
- . then ( packument => pickManifest ( packument , this . spec . fetchSpec , {
127
- ...this . opts ,
128
- defaultTag : this . defaultTag ,
129
- before : this . before ,
130
- } ) /* XXX add ETARGET and E403 revalidation of cached packuments here */ )
131
- . then ( mani => {
132
- // add _resolved and _integrity from dist object
133
- const { dist } = mani
134
- if ( dist ) {
135
- this . resolved = mani . _resolved = dist . tarball
136
- mani . _from = this . from
137
- const distIntegrity = dist . integrity ? ssri . parse ( dist . integrity )
138
- : dist . shasum ? ssri . fromHex ( dist . shasum , 'sha1' , { ...this . opts } )
139
- : null
140
- if ( distIntegrity ) {
141
- if ( ! this . integrity ) {
142
- this . integrity = distIntegrity
143
- } else if ( ! this . integrity . match ( distIntegrity ) ) {
144
- // only bork if they have algos in common.
145
- // otherwise we end up breaking if we have saved a sha512
146
- // previously for the tarball, but the manifest only
147
- // provides a sha1, which is possible for older publishes.
148
- // Otherwise, this is almost certainly a case of holding it
149
- // wrong, and will result in weird or insecure behavior
150
- // later on when building package tree.
151
- for ( const algo of Object . keys ( this . integrity ) ) {
152
- if ( distIntegrity [ algo ] ) {
153
- throw Object . assign ( new Error (
154
- `Integrity checksum failed when using ${ algo } : ` +
155
- `wanted ${ this . integrity } but got ${ distIntegrity } .`
156
- ) , { code : 'EINTEGRITY' } )
157
- }
158
- }
159
- // made it this far, the integrity is worthwhile. accept it.
160
- // the setter here will take care of merging it into what we
161
- // already had.
162
- this . integrity = distIntegrity
124
+ const packument = await this . packument ( )
125
+ const mani = await pickManifest ( packument , this . spec . fetchSpec , {
126
+ ...this . opts ,
127
+ defaultTag : this . defaultTag ,
128
+ before : this . before ,
129
+ } )
130
+ /* XXX add ETARGET and E403 revalidation of cached packuments here */
131
+
132
+ // add _resolved and _integrity from dist object
133
+ const { dist } = mani
134
+ if ( dist ) {
135
+ this . resolved = mani . _resolved = dist . tarball
136
+ mani . _from = this . from
137
+ const distIntegrity = dist . integrity ? ssri . parse ( dist . integrity )
138
+ : dist . shasum ? ssri . fromHex ( dist . shasum , 'sha1' , { ...this . opts } )
139
+ : null
140
+ if ( distIntegrity ) {
141
+ if ( this . integrity && ! this . integrity . match ( distIntegrity ) ) {
142
+ // only bork if they have algos in common.
143
+ // otherwise we end up breaking if we have saved a sha512
144
+ // previously for the tarball, but the manifest only
145
+ // provides a sha1, which is possible for older publishes.
146
+ // Otherwise, this is almost certainly a case of holding it
147
+ // wrong, and will result in weird or insecure behavior
148
+ // later on when building package tree.
149
+ for ( const algo of Object . keys ( this . integrity ) ) {
150
+ if ( distIntegrity [ algo ] ) {
151
+ throw Object . assign ( new Error (
152
+ `Integrity checksum failed when using ${ algo } : ` +
153
+ `wanted ${ this . integrity } but got ${ distIntegrity } .`
154
+ ) , { code : 'EINTEGRITY' } )
163
155
}
164
156
}
165
157
}
166
- if ( this . integrity ) {
167
- mani . _integrity = String ( this . integrity )
168
- if ( dist . signatures ) {
158
+ // made it this far, the integrity is worthwhile. accept it.
159
+ // the setter here will take care of merging it into what we already
160
+ // had.
161
+ this . integrity = distIntegrity
162
+ }
163
+ }
164
+ if ( this . integrity ) {
165
+ mani . _integrity = String ( this . integrity )
166
+ if ( dist . signatures ) {
167
+ if ( this . opts . verifySignatures ) {
168
+ if ( this . registryKeys ) {
169
+ // validate and throw on error, then set _signatures
170
+ const message = `${ mani . _id } :${ mani . _integrity } `
171
+ for ( const signature of dist . signatures ) {
172
+ const publicKey = this . registryKeys . filter ( key => ( key . keyid === signature . keyid ) ) [ 0 ]
173
+ if ( ! publicKey ) {
174
+ throw Object . assign ( new Error (
175
+ `${ mani . _id } has a signature with keyid: ${ signature . keyid } ` +
176
+ 'but no corresponding public key can be found.'
177
+ ) , { code : 'EMISSINGSIGNATUREKEY' } )
178
+ }
179
+ const validPublicKey =
180
+ ! publicKey . expires || ( Date . parse ( publicKey . expires ) > Date . now ( ) )
181
+ if ( ! validPublicKey ) {
182
+ throw Object . assign ( new Error (
183
+ `${ mani . _id } has a signature with keyid: ${ signature . keyid } ` +
184
+ `but the corresponding public key has expired ${ publicKey . expires } `
185
+ ) , { code : 'EEXPIREDSIGNATUREKEY' } )
186
+ }
187
+ const verifier = crypto . createVerify ( 'SHA256' )
188
+ verifier . write ( message )
189
+ verifier . end ( )
190
+ const valid = verifier . verify (
191
+ publicKey . pemkey ,
192
+ signature . sig ,
193
+ 'base64'
194
+ )
195
+ if ( ! valid ) {
196
+ throw Object . assign ( new Error (
197
+ 'Integrity checksum signature failed: ' +
198
+ `key ${ publicKey . keyid } signature ${ signature . sig } `
199
+ ) , { code : 'EINTEGRITYSIGNATURE' } )
200
+ }
201
+ }
169
202
mani . _signatures = dist . signatures
170
203
}
204
+ // if no keys, don't set _signatures
205
+ } else {
206
+ mani . _signatures = dist . signatures
171
207
}
172
- this . package = rpj . normalize ( mani )
173
- return this . package
174
- } )
208
+ }
209
+ }
210
+ this . package = rpj . normalize ( mani )
211
+ return this . package
175
212
}
176
213
177
214
[ _tarballFromResolved ] ( ) {
0 commit comments