-
Notifications
You must be signed in to change notification settings - Fork 1
/
ccc-render-results.js
246 lines (198 loc) · 10.9 KB
/
ccc-render-results.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
const debug = require('debug')('ccc-render-results');
debug('Entry: [%s]', __filename);
// Initialise console colours
const chalk = require('chalk');
function exportToCSV(outputTableRaw, settings) {
if (settings.options.exportToCSV) { // Check if `export to CSV` is enabled
// Import packages
const jsonexport = require('jsonexport'); // For converting JSON to CSV
const utils = require('./utils'); // Library of general helper functions
const fs = require('fs'); // Initialise File System object
const EOL = require('os').EOL; // Platform independent new line character and path separator
// Perform conversion of JSON output to CSV
jsonexport(outputTableRaw, (err, csv) => {
if (err) { // Check for an error
console.error(`${chalk.redBright('An error occurred converting the results into a CSV format: ')}%O`, err);
} else {
let filename = utils.generateUniqueFilename('csv');
// Write the results to file
try {
fs.writeFileSync(filename, csv);
// Notify the user where the file is saved
console.log(EOL); // New line
console.log(chalk.grey('Results written to [%s]'), filename);
console.log(EOL); // New line
// Open the file if configured to do so
if (settings.options.openAfterExport) {
debug('Opening [%s] ...', filename);
let open = require('open');
(async () => {
// Opens the image in the default image viewer and waits for the opened app to quit.
await open(filename);
})();
}
} catch (error) {
settings.options.exportToCSV = false; // Switch off further exporting to a file seeing as it hasn't worked
debug('An error occurred writing to results to disk. Switching off "exportToCSV".');
console.error(`${chalk.redBright('An error occurred writing to the file [%s]: ')}%O`, filename, error);
}
}
});
}
}
function renderHTTPResponses(responses, settings) {
return new Promise(function (resolve, reject) {
// Import packages
const { parse } = require('@tusbar/cache-control'); // Cache-control header parser
const matcher = require('multimatch'); // Initialise wildcard string parser
const columnify = require('columnify'); // Console output formatting into columns
if ((Array.isArray(responses) && responses.length)) { // Process responses[] array
debug('Parsing %s responses', responses.length);
// We'll collate the parsed results into an output array
let outputTable = [];
// We'll also collect the raw (unformatted for console output) which we'll use in exportToCSV;
let outputTableRaw = [];
// Iterate through responses[] array
for (let i = 0; i < responses.length; i++) {
// Each request/response will constitute a row in each of the output tables (formatted & raw)
let row = {};
let rowRaw = {};
// Indicate if a redirect was followed
if (settings.options.httpOptions.follow > 0) {
// Add the column and a placeholder for the redirects indicator
row.Redirects = ' ';
// Add the integer value to the raw results
rowRaw.Redirects = responses[i].redirectCount;
if (responses[i].redirectCount > 0) { // If the request resulted in one or more redirects, add the indicator character to the results
row.Redirects = global.CCC_OUTPUT.REDIRECT_INDICATOR;
}
}
// Populate basic request details
let timestamp = new Date();
// Pad the hours/mins/secs/mSecs with a leading '0' and then return (trim) the last 2 rightmost characters to ensure a leading zero for 1 digit numbers
let responseTimestamp = `${(`0${timestamp.getHours()}`).slice(-2)}:${(`0${timestamp.getMinutes()}`).slice(-2)}:${(`0${timestamp.getSeconds()}`).slice(-2)}:${(`0${timestamp.getMilliseconds()}`).slice(-2)}`;
row.Time = chalk.reset(responseTimestamp);
rowRaw.Time = responseTimestamp;
// Populate response status code, with colour indicator of success or failure
if (Number.isInteger(responses[i].statusCode)) {
if (responses[i].statusCode >= 400) {
// Failure response code, 4xx & 5xx
row.Status = chalk.red(responses[i].statusCode);
} else if (responses[i].statusCode >= 300) {
// Redirect response code (3xx)
row.Status = chalk.yellow(responses[i].statusCode);
} else {
// Success response code (1xx, 2xx)
row.Status = chalk.green(responses[i].statusCode);
}
} else if (responses[i].error) {
row.Status = chalk.bgRed.whiteBright(responses[i].statusCode);
}
// Write the status code to the raw output
rowRaw.Status = responses[i].statusCode;
// Record server hostname
row.Host = chalk.cyan(responses[i].request.host);
rowRaw.Host = responses[i].request.host;
// Record URL path
row.Path = chalk.cyan(responses[i].request.path);
rowRaw.Path = responses[i].request.path;
rowRaw.Protocol = responses[i].request.protocol;
rowRaw.URL = responses[i].request.url;
// Pull out select response headers
for (let attributeName in responses[i].response.headers) {
let attributeValue = responses[i].response.headers[attributeName];
// Check if the response header's name matches one in the header collection
if (matcher(attributeName, settings.headerCollection, { nocase: true }).length > 0) {
debug('Extracting header ==> %s : %s', attributeName, attributeValue);
// Add all response headers/values to raw collection here, for use in exportToCSV()
rowRaw[attributeName] = attributeValue;
// Parse header value for cache-control directives (for use inside the following switch blocks)
let clientCache = parse(attributeValue);
switch (attributeName.toLowerCase()) {
case 'cache-control': {
if ((clientCache.noStore === false) && (clientCache.maxAge > 0)) {
// Response IS cacheable. Colour it GREEN
row[attributeName] = chalk.green(attributeValue);
} else if ((clientCache.noStore === true) || (clientCache.maxAge === 0)) {
// Response is NOT cacheable. Colour it RED
row[attributeName] = chalk.red(attributeValue);
} else {
// Unknown cache state. Colour it YELLOW
row[attributeName] = chalk.yellow(attributeValue);
}
break;
}
case 'x-cache': {
// Examine x-cache value
if (attributeValue.toLowerCase().search('hit') !== -1) {
// Cache HIT. Colour it GREEN
row[attributeName] = chalk.green(attributeValue);
} else if (attributeValue.toLowerCase().search('miss') !== -1) {
// Cache MISS. Colour it RED
row[attributeName] = chalk.red(attributeValue);
} else {
// Unknown cache state. Colour it YELLOW
row[attributeName] = chalk.yellow(attributeValue);
}
break;
}
default:
// Add row with no formatting
row[attributeName] = chalk.reset(attributeValue);
}
} else {
debug('Ignoring header --> %s', attributeName);
}
}
outputTable.push(row); // Append completed row to the table
outputTableRaw.push(rowRaw);
}
// The Raw output is complete. Export it to CSV
if (settings.options.exportToCSV) {
exportToCSV(outputTableRaw, settings);
}
// Format output into columns for console output
let columns = columnify(outputTable, {
maxWidth: 35,
showHeaders: true,
preserveNewLines: true,
truncate: true,
config: {
'vary': { maxWidth: 20 },
'Host': { maxWidth: 30 },
'Path': { maxWidth: 60 },
'Redirects': { showHeaders: false }
}
});
// Return formatted output
resolve(columns);
} else {
reject(new Error('renderHTTPResponses() :: responses[] array either does not exist, is not an array, or is empty.'));
}
});
}
function renderHTTPResponseHeaders(responses) {
return new Promise(function (resolve, reject) {
if ((Array.isArray(responses) && responses.length)) { // Process responses[] array
// Import packages
const EOL = require('os').EOL; // Platform independent new line character and path separator
debug('Parsing %s responses for unique HTTP headers', responses.length);
let responseHeadersReceived = []; // Array to store names of received response headers
for (let i = 0; i < responses.length; i++) { // For each HTTP response
for (let attributeName in responses[i].response.headers) { // Loop through each response header
responseHeadersReceived.push(attributeName); // Save the header's name to an array
}
}
// Dedupe the list of collected response headers
debug('De-duplicating the collection of %i response headers', responseHeadersReceived.length);
let uniqueResponseHeaders = [...new Set(responseHeadersReceived.sort())];
debug('%i unique response headers', uniqueResponseHeaders.length);
// Render results to console
console.log('%i unique response headers (from %i collected):%s%O', uniqueResponseHeaders.length, responseHeadersReceived.length, EOL, uniqueResponseHeaders);
resolve(true);
} else {
reject(new Error('renderHTTPResponseHeaders() :: responses[]] array either does not exist, is not an array, or is empty.'));
}
});
}
module.exports = { renderHTTPResponses, renderHTTPResponseHeaders };