From 8b2a57849f89113fb4b7174d124a9b582746e34a Mon Sep 17 00:00:00 2001 From: Dave Hadka Date: Wed, 22 Apr 2020 18:23:41 -0400 Subject: [PATCH 1/3] Adds socket timeout and validate file size --- src/cacheHttpClient.ts | 28 +++++++++++++++++++++++++++- src/constants.ts | 2 ++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/cacheHttpClient.ts b/src/cacheHttpClient.ts index c83e307..fc8f577 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. + var 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..320b15c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -20,3 +20,5 @@ export enum Events { } export const CacheFilename = "cache.tgz"; + +export const SocketTimeout = 5000; From 9bb13c71ec248693293697588c12d06a3159857e Mon Sep 17 00:00:00 2001 From: Dave Hadka Date: Wed, 22 Apr 2020 18:35:16 -0400 Subject: [PATCH 2/3] Fix lint issue, build .js files --- dist/restore/index.js | 33 +++++++++++++++++++++++++++++---- dist/save/index.js | 33 +++++++++++++++++++++++++++++---- src/cacheHttpClient.ts | 2 +- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/dist/restore/index.js b/dist/restore/index.js index ac49982..393f1c2 100644 --- a/dist/restore/index.js +++ b/dist/restore/index.js @@ -2182,12 +2182,12 @@ var __importStar = (this && this.__importStar) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(__webpack_require__(470)); -const fs = __importStar(__webpack_require__(747)); -const crypto = __importStar(__webpack_require__(417)); const http_client_1 = __webpack_require__(539); const auth_1 = __webpack_require__(226); -const utils = __importStar(__webpack_require__(443)); +const crypto = __importStar(__webpack_require__(417)); +const fs = __importStar(__webpack_require__(747)); const constants_1 = __webpack_require__(694); +const utils = __importStar(__webpack_require__(443)); const versionSalt = "1.0"; function isSuccessStatusCode(statusCode) { if (!statusCode) { @@ -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; @@ -3185,8 +3202,8 @@ var __importStar = (this && this.__importStar) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(__webpack_require__(470)); -const io = __importStar(__webpack_require__(1)); const glob = __importStar(__webpack_require__(281)); +const io = __importStar(__webpack_require__(1)); const fs = __importStar(__webpack_require__(747)); const path = __importStar(__webpack_require__(622)); const util = __importStar(__webpack_require__(669)); @@ -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,7 @@ var Events; Events["PullRequest"] = "pull_request"; })(Events = exports.Events || (exports.Events = {})); exports.CacheFilename = "cache.tgz"; +exports.SocketTimeout = 5000; /***/ }), diff --git a/dist/save/index.js b/dist/save/index.js index ca454ed..53ab6e0 100644 --- a/dist/save/index.js +++ b/dist/save/index.js @@ -2182,12 +2182,12 @@ var __importStar = (this && this.__importStar) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(__webpack_require__(470)); -const fs = __importStar(__webpack_require__(747)); -const crypto = __importStar(__webpack_require__(417)); const http_client_1 = __webpack_require__(539); const auth_1 = __webpack_require__(226); -const utils = __importStar(__webpack_require__(443)); +const crypto = __importStar(__webpack_require__(417)); +const fs = __importStar(__webpack_require__(747)); const constants_1 = __webpack_require__(694); +const utils = __importStar(__webpack_require__(443)); const versionSalt = "1.0"; function isSuccessStatusCode(statusCode) { if (!statusCode) { @@ -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; @@ -3185,8 +3202,8 @@ var __importStar = (this && this.__importStar) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(__webpack_require__(470)); -const io = __importStar(__webpack_require__(1)); const glob = __importStar(__webpack_require__(281)); +const io = __importStar(__webpack_require__(1)); const fs = __importStar(__webpack_require__(747)); const path = __importStar(__webpack_require__(622)); const util = __importStar(__webpack_require__(669)); @@ -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,7 @@ var Events; Events["PullRequest"] = "pull_request"; })(Events = exports.Events || (exports.Events = {})); exports.CacheFilename = "cache.tgz"; +exports.SocketTimeout = 5000; /***/ }), diff --git a/src/cacheHttpClient.ts b/src/cacheHttpClient.ts index fc8f577..e023abb 100644 --- a/src/cacheHttpClient.ts +++ b/src/cacheHttpClient.ts @@ -156,7 +156,7 @@ export async function downloadCache( await pipeResponseToStream(downloadResponse, stream); // Validate download size. - var contentLengthHeader = + const contentLengthHeader = downloadResponse.message.headers["content-length"]; if (contentLengthHeader) { From 48b62c1c529d0cb84897a268c7c9a992f441b406 Mon Sep 17 00:00:00 2001 From: Dave Hadka Date: Tue, 28 Apr 2020 21:31:41 -0400 Subject: [PATCH 3/3] Add comment for SocketTimeout --- dist/restore/index.js | 3 +++ dist/save/index.js | 3 +++ src/constants.ts | 3 +++ 3 files changed, 9 insertions(+) diff --git a/dist/restore/index.js b/dist/restore/index.js index 393f1c2..197c44c 100644 --- a/dist/restore/index.js +++ b/dist/restore/index.js @@ -4492,6 +4492,9 @@ 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 53ab6e0..16c38b6 100644 --- a/dist/save/index.js +++ b/dist/save/index.js @@ -4578,6 +4578,9 @@ 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/constants.ts b/src/constants.ts index 320b15c..a6ee7b0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -21,4 +21,7 @@ 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;