diff --git a/dist/restore/index.js b/dist/restore/index.js index 52de098..1f1e16e 100644 --- a/dist/restore/index.js +++ b/dist/restore/index.js @@ -2285,7 +2285,24 @@ function downloadCache(archiveLocation, archivePath) { const stream = fs.createWriteStream(archivePath); const httpClient = new http_client_1.HttpClient("actions/cache"); 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); + // 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; @@ -3583,6 +3600,12 @@ class HttpClientResponse { this.message.on('data', (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', () => { resolve(output.toString()); }); @@ -3704,6 +3727,7 @@ class HttpClient { let response; while (numTries < maxTries) { response = await this.requestRaw(info, data); + // Check if it's an authentication challenge if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) { let authenticationHandler; @@ -4468,6 +4492,10 @@ var Events; Events["PullRequest"] = "pull_request"; })(Events = exports.Events || (exports.Events = {})); 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; /***/ }), diff --git a/dist/save/index.js b/dist/save/index.js index 3f784aa..4e968c5 100644 --- a/dist/save/index.js +++ b/dist/save/index.js @@ -2285,7 +2285,24 @@ function downloadCache(archiveLocation, archivePath) { const stream = fs.createWriteStream(archivePath); const httpClient = new http_client_1.HttpClient("actions/cache"); 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); + // 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; @@ -3583,6 +3600,12 @@ class HttpClientResponse { this.message.on('data', (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', () => { resolve(output.toString()); }); @@ -3704,6 +3727,7 @@ class HttpClient { let response; while (numTries < maxTries) { response = await this.requestRaw(info, data); + // Check if it's an authentication challenge if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) { let authenticationHandler; @@ -4554,6 +4578,10 @@ var Events; Events["PullRequest"] = "pull_request"; })(Events = exports.Events || (exports.Events = {})); 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; /***/ }), diff --git a/src/cacheHttpClient.ts b/src/cacheHttpClient.ts index c83e307..e023abb 100644 --- a/src/cacheHttpClient.ts +++ b/src/cacheHttpClient.ts @@ -9,7 +9,7 @@ import { import * as crypto from "crypto"; import * as fs from "fs"; -import { Inputs } from "./constants"; +import { Inputs, SocketTimeout } from "./constants"; import { ArtifactCacheEntry, CommitCacheRequest, @@ -144,7 +144,33 @@ export async function downloadCache( const stream = fs.createWriteStream(archivePath); const httpClient = new HttpClient("actions/cache"); 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); + + // 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 diff --git a/src/constants.ts b/src/constants.ts index 2b78f62..a6ee7b0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -20,3 +20,8 @@ export enum Events { } 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;