Merge pull request #269 from actions/socket-timeout

Adds socket timeout and validate file size
This commit is contained in:
David Hadka 2020-04-29 12:21:27 -05:00 committed by GitHub
commit 54626c4a4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 88 additions and 1 deletions

28
dist/restore/index.js vendored
View File

@ -2285,7 +2285,24 @@ function downloadCache(archiveLocation, archivePath) {
const stream = fs.createWriteStream(archivePath); const stream = fs.createWriteStream(archivePath);
const httpClient = new http_client_1.HttpClient("actions/cache"); const httpClient = new http_client_1.HttpClient("actions/cache");
const downloadResponse = yield httpClient.get(archiveLocation); const downloadResponse = yield httpClient.get(archiveLocation);
// Abort download if no traffic received over the socket.
downloadResponse.message.socket.setTimeout(constants_1.SocketTimeout, () => {
downloadResponse.message.destroy();
core.debug(`Aborting download, socket timed out after ${constants_1.SocketTimeout} ms`);
});
yield pipeResponseToStream(downloadResponse, stream); yield pipeResponseToStream(downloadResponse, stream);
// Validate download size.
const contentLengthHeader = downloadResponse.message.headers["content-length"];
if (contentLengthHeader) {
const expectedLength = parseInt(contentLengthHeader);
const actualLength = utils.getArchiveFileSize(archivePath);
if (actualLength != expectedLength) {
throw new Error(`Incomplete download. Expected file size: ${expectedLength}, actual file size: ${actualLength}`);
}
}
else {
core.debug("Unable to validate download, no Content-Length header");
}
}); });
} }
exports.downloadCache = downloadCache; exports.downloadCache = downloadCache;
@ -3583,6 +3600,12 @@ class HttpClientResponse {
this.message.on('data', (chunk) => { this.message.on('data', (chunk) => {
output = Buffer.concat([output, chunk]); output = Buffer.concat([output, chunk]);
}); });
this.message.on('aborted', () => {
reject("Request was aborted or closed prematurely");
});
this.message.on('timeout', (socket) => {
reject("Request timed out");
});
this.message.on('end', () => { this.message.on('end', () => {
resolve(output.toString()); resolve(output.toString());
}); });
@ -3704,6 +3727,7 @@ class HttpClient {
let response; let response;
while (numTries < maxTries) { while (numTries < maxTries) {
response = await this.requestRaw(info, data); response = await this.requestRaw(info, data);
// Check if it's an authentication challenge // Check if it's an authentication challenge
if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) { if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) {
let authenticationHandler; let authenticationHandler;
@ -4468,6 +4492,10 @@ var Events;
Events["PullRequest"] = "pull_request"; Events["PullRequest"] = "pull_request";
})(Events = exports.Events || (exports.Events = {})); })(Events = exports.Events || (exports.Events = {}));
exports.CacheFilename = "cache.tgz"; exports.CacheFilename = "cache.tgz";
// Socket timeout in milliseconds during download. If no traffic is received
// over the socket during this period, the socket is destroyed and the download
// is aborted.
exports.SocketTimeout = 5000;
/***/ }), /***/ }),

28
dist/save/index.js vendored
View File

@ -2285,7 +2285,24 @@ function downloadCache(archiveLocation, archivePath) {
const stream = fs.createWriteStream(archivePath); const stream = fs.createWriteStream(archivePath);
const httpClient = new http_client_1.HttpClient("actions/cache"); const httpClient = new http_client_1.HttpClient("actions/cache");
const downloadResponse = yield httpClient.get(archiveLocation); const downloadResponse = yield httpClient.get(archiveLocation);
// Abort download if no traffic received over the socket.
downloadResponse.message.socket.setTimeout(constants_1.SocketTimeout, () => {
downloadResponse.message.destroy();
core.debug(`Aborting download, socket timed out after ${constants_1.SocketTimeout} ms`);
});
yield pipeResponseToStream(downloadResponse, stream); yield pipeResponseToStream(downloadResponse, stream);
// Validate download size.
const contentLengthHeader = downloadResponse.message.headers["content-length"];
if (contentLengthHeader) {
const expectedLength = parseInt(contentLengthHeader);
const actualLength = utils.getArchiveFileSize(archivePath);
if (actualLength != expectedLength) {
throw new Error(`Incomplete download. Expected file size: ${expectedLength}, actual file size: ${actualLength}`);
}
}
else {
core.debug("Unable to validate download, no Content-Length header");
}
}); });
} }
exports.downloadCache = downloadCache; exports.downloadCache = downloadCache;
@ -3583,6 +3600,12 @@ class HttpClientResponse {
this.message.on('data', (chunk) => { this.message.on('data', (chunk) => {
output = Buffer.concat([output, chunk]); output = Buffer.concat([output, chunk]);
}); });
this.message.on('aborted', () => {
reject("Request was aborted or closed prematurely");
});
this.message.on('timeout', (socket) => {
reject("Request timed out");
});
this.message.on('end', () => { this.message.on('end', () => {
resolve(output.toString()); resolve(output.toString());
}); });
@ -3704,6 +3727,7 @@ class HttpClient {
let response; let response;
while (numTries < maxTries) { while (numTries < maxTries) {
response = await this.requestRaw(info, data); response = await this.requestRaw(info, data);
// Check if it's an authentication challenge // Check if it's an authentication challenge
if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) { if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) {
let authenticationHandler; let authenticationHandler;
@ -4554,6 +4578,10 @@ var Events;
Events["PullRequest"] = "pull_request"; Events["PullRequest"] = "pull_request";
})(Events = exports.Events || (exports.Events = {})); })(Events = exports.Events || (exports.Events = {}));
exports.CacheFilename = "cache.tgz"; exports.CacheFilename = "cache.tgz";
// Socket timeout in milliseconds during download. If no traffic is received
// over the socket during this period, the socket is destroyed and the download
// is aborted.
exports.SocketTimeout = 5000;
/***/ }), /***/ }),

View File

@ -9,7 +9,7 @@ import {
import * as crypto from "crypto"; import * as crypto from "crypto";
import * as fs from "fs"; import * as fs from "fs";
import { Inputs } from "./constants"; import { Inputs, SocketTimeout } from "./constants";
import { import {
ArtifactCacheEntry, ArtifactCacheEntry,
CommitCacheRequest, CommitCacheRequest,
@ -144,7 +144,33 @@ export async function downloadCache(
const stream = fs.createWriteStream(archivePath); const stream = fs.createWriteStream(archivePath);
const httpClient = new HttpClient("actions/cache"); const httpClient = new HttpClient("actions/cache");
const downloadResponse = await httpClient.get(archiveLocation); const downloadResponse = await httpClient.get(archiveLocation);
// Abort download if no traffic received over the socket.
downloadResponse.message.socket.setTimeout(SocketTimeout, () => {
downloadResponse.message.destroy();
core.debug(
`Aborting download, socket timed out after ${SocketTimeout} ms`
);
});
await pipeResponseToStream(downloadResponse, stream); await pipeResponseToStream(downloadResponse, stream);
// Validate download size.
const contentLengthHeader =
downloadResponse.message.headers["content-length"];
if (contentLengthHeader) {
const expectedLength = parseInt(contentLengthHeader);
const actualLength = utils.getArchiveFileSize(archivePath);
if (actualLength != expectedLength) {
throw new Error(
`Incomplete download. Expected file size: ${expectedLength}, actual file size: ${actualLength}`
);
}
} else {
core.debug("Unable to validate download, no Content-Length header");
}
} }
// Reserve Cache // Reserve Cache

View File

@ -20,3 +20,8 @@ export enum Events {
} }
export const CacheFilename = "cache.tgz"; export const CacheFilename = "cache.tgz";
// Socket timeout in milliseconds during download. If no traffic is received
// over the socket during this period, the socket is destroyed and the download
// is aborted.
export const SocketTimeout = 5000;