@taskcluster/client 103.0.0 → 104.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/package.json +1 -1
- package/src/apis.js +2 -2
- package/src/download.js +66 -35
package/README.md
CHANGED
|
@@ -442,6 +442,28 @@ let contentType = await taskcluster.downloadArtifact({
|
|
|
442
442
|
});
|
|
443
443
|
```
|
|
444
444
|
|
|
445
|
+
#### Downloading Untrusted Artifacts
|
|
446
|
+
|
|
447
|
+
A `reference` artifact is fetched from a URL supplied by the task that published it, so
|
|
448
|
+
downloading one from a place with more network reach than the task itself -- a service in your
|
|
449
|
+
deployment, for example -- is a server-side request forgery.
|
|
450
|
+
|
|
451
|
+
`downloadManagedArtifact` takes the same options as `downloadArtifact`, but refuses a `reference`
|
|
452
|
+
artifact before reading its URL, throwing an error with `code` of
|
|
453
|
+
`'ArtifactStorageTypeRejected'`:
|
|
454
|
+
|
|
455
|
+
```javascript
|
|
456
|
+
let contentType = await taskcluster.downloadManagedArtifact({ taskId, name, queue, streamFactory });
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Use it whenever the task publishing the artifact is not fully trusted. `s3` and `object`
|
|
460
|
+
artifacts are unaffected, as the queue derives their URLs itself, and `link` artifacts are
|
|
461
|
+
resolved by the queue, so a link ending at a `reference` is refused as well.
|
|
462
|
+
|
|
463
|
+
Both functions throw an error with `code` of `'ArtifactError'` for an `error` artifact, and
|
|
464
|
+
report HTTP failures on `err.statusCode` regardless of whether the artifact record or its stored
|
|
465
|
+
content was missing.
|
|
466
|
+
|
|
445
467
|
### Inspecting Credentials
|
|
446
468
|
|
|
447
469
|
Your users may find the options for Taskcluster credentials overwhelming. You
|
package/package.json
CHANGED
package/src/apis.js
CHANGED
|
@@ -2408,7 +2408,7 @@ export default {
|
|
|
2408
2408
|
"taskGroupId"
|
|
2409
2409
|
],
|
|
2410
2410
|
"category": "Task Groups",
|
|
2411
|
-
"description": "Seal task group to prevent creation of new tasks.\n\nTask group can be sealed once and is irreversible. Calling it multiple times\nwill return same result and will not update it again.",
|
|
2411
|
+
"description": "Seal task group to prevent creation of new tasks.\n\nTask group can be sealed once and is irreversible. Calling it multiple times\nwill return same result and will not update it again.\n\nSealing makes `cancelTaskGroup` meaningful by stopping task creators\nfrom adding more tasks to a group being cancelled. It is not a\nsecurity feature: the check is not atomic with task creation, so a\n`createTask` racing this call may still succeed.",
|
|
2412
2412
|
"method": "post",
|
|
2413
2413
|
"name": "sealTaskGroup",
|
|
2414
2414
|
"output": "v1/task-group-response.json#",
|
|
@@ -4189,7 +4189,7 @@ export default {
|
|
|
4189
4189
|
"taskId"
|
|
4190
4190
|
],
|
|
4191
4191
|
"category": "Profiler",
|
|
4192
|
-
"description": "Generate a Firefox Profiler–compatible profile from a task's log output.\nParses `public/logs/live.log` (or `live_backing.log`) for timing data.",
|
|
4192
|
+
"description": "Generate a Firefox Profiler–compatible profile from a task's log output for resolved tasks.\nParses `public/logs/live.log` (or `live_backing.log`) for timing data.",
|
|
4193
4193
|
"method": "get",
|
|
4194
4194
|
"name": "taskProfile",
|
|
4195
4195
|
"query": [
|
package/src/download.js
CHANGED
|
@@ -8,15 +8,26 @@ import { clients } from './client.js';
|
|
|
8
8
|
const makeRetryCfg = ({ retries, delayFactor, randomizationFactor, maxDelay }) => ({
|
|
9
9
|
retries: retries === undefined ? 5 : retries,
|
|
10
10
|
delayFactor: delayFactor === undefined ? 100 : delayFactor,
|
|
11
|
-
randomizationFactor: randomizationFactor === undefined ?
|
|
11
|
+
randomizationFactor: randomizationFactor === undefined ? 0.25 : randomizationFactor,
|
|
12
12
|
maxDelay: maxDelay === undefined ? 30 * 1000 : maxDelay,
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
// got reports an HTTP status on `err.response.statusCode`, while Taskcluster client errors carry
|
|
16
|
+
// it on `err.statusCode`; normalize onto the latter so callers need only check one place.
|
|
17
|
+
const withStatusCode = err => {
|
|
18
|
+
if (err.statusCode === undefined && err.response?.statusCode !== undefined) {
|
|
19
|
+
err.statusCode = err.response.statusCode;
|
|
20
|
+
}
|
|
21
|
+
return err;
|
|
22
|
+
};
|
|
23
|
+
|
|
15
24
|
const s3 = async ({ url, streamFactory, retryCfg }) => {
|
|
16
25
|
return await retry(retryCfg, async retriableError => {
|
|
17
26
|
let contentType = 'application/binary';
|
|
18
27
|
try {
|
|
19
28
|
const src = got.stream(url, { retry: { limit: 0 } });
|
|
29
|
+
// aborted request emits again after pipeline() already rejected, crashes if unhandled
|
|
30
|
+
src.on('error', () => {});
|
|
20
31
|
src.on('response', res => {
|
|
21
32
|
contentType = res.headers['content-type'] || contentType;
|
|
22
33
|
});
|
|
@@ -49,6 +60,8 @@ const getUrl = async ({ object, name, resp, streamFactory, retryCfg }) => {
|
|
|
49
60
|
try {
|
|
50
61
|
responseUsed = true;
|
|
51
62
|
const src = got.stream(resp.url, { retry: { limit: 0 } });
|
|
63
|
+
// aborted request emits again after pipeline() already rejected, crashes if unhandled
|
|
64
|
+
src.on('error', () => {});
|
|
52
65
|
src.on('response', res => {
|
|
53
66
|
contentType = res.headers['content-type'] || contentType;
|
|
54
67
|
});
|
|
@@ -122,46 +135,64 @@ export const download = async ({
|
|
|
122
135
|
}
|
|
123
136
|
};
|
|
124
137
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
name,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
retries,
|
|
132
|
-
delayFactor,
|
|
133
|
-
randomizationFactor,
|
|
134
|
-
maxDelay,
|
|
135
|
-
}) => {
|
|
138
|
+
// when managedOnly is set, a `reference` artifact is rejected before its URL is read. The queue
|
|
139
|
+
// resolves `link` artifacts server-side, so the storageType seen here is always the final one.
|
|
140
|
+
const downloadArtifactImpl = async (
|
|
141
|
+
{ taskId, runId, name, queue, streamFactory, retries, delayFactor, randomizationFactor, maxDelay },
|
|
142
|
+
managedOnly
|
|
143
|
+
) => {
|
|
136
144
|
const retryCfg = makeRetryCfg({ retries, delayFactor, randomizationFactor, maxDelay });
|
|
137
145
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
try {
|
|
147
|
+
const artifact = await (runId === undefined
|
|
148
|
+
? queue.latestArtifact(taskId, name)
|
|
149
|
+
: queue.artifact(taskId, runId, name));
|
|
150
|
+
|
|
151
|
+
switch (artifact.storageType) {
|
|
152
|
+
case 'reference': {
|
|
153
|
+
if (managedOnly) {
|
|
154
|
+
const err = new Error(
|
|
155
|
+
'Refusing to download a `reference` artifact: its URL is supplied by the task. Use ' +
|
|
156
|
+
'`downloadArtifact` if fetching a task-supplied URL is intended.'
|
|
157
|
+
);
|
|
158
|
+
err.code = 'ArtifactStorageTypeRejected';
|
|
159
|
+
err.storageType = 'reference';
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
return await s3({ url: artifact.url, streamFactory, retryCfg });
|
|
163
|
+
}
|
|
164
|
+
case 's3': {
|
|
165
|
+
return await s3({ url: artifact.url, streamFactory, retryCfg });
|
|
166
|
+
}
|
|
141
167
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
168
|
+
case 'object': {
|
|
169
|
+
const object = new clients.Object({
|
|
170
|
+
rootUrl: queue._options._trueRootUrl,
|
|
171
|
+
credentials: artifact.credentials,
|
|
172
|
+
});
|
|
173
|
+
return await download({ name: artifact.name, object, streamFactory, ...retryCfg });
|
|
174
|
+
}
|
|
147
175
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
176
|
+
case 'error': {
|
|
177
|
+
const err = new Error(artifact.message);
|
|
178
|
+
err.code = 'ArtifactError';
|
|
179
|
+
err.reason = artifact.reason;
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
155
182
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
err.reason = artifact.reason;
|
|
159
|
-
throw err;
|
|
183
|
+
default:
|
|
184
|
+
throw new Error(`Unsupported artifact storageType '${artifact.storageType}'`);
|
|
160
185
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
throw new Error(`Unsupported artifact storageType '${artifact.storageType}'`);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
throw withStatusCode(err);
|
|
164
188
|
}
|
|
165
189
|
};
|
|
166
190
|
|
|
167
|
-
export
|
|
191
|
+
export const downloadArtifact = async options => await downloadArtifactImpl(options, false);
|
|
192
|
+
|
|
193
|
+
// as downloadArtifact, but refuses `reference` artifacts, whose content is fetched from a URL of
|
|
194
|
+
// the publishing task's choosing. Callers reading artifacts of untrusted tasks should use this,
|
|
195
|
+
// so that a task cannot direct them at an arbitrary URL reachable from their network position.
|
|
196
|
+
export const downloadManagedArtifact = async options => await downloadArtifactImpl(options, true);
|
|
197
|
+
|
|
198
|
+
export default { download, downloadArtifact, downloadManagedArtifact };
|