>' === 'true';
+
+ const BUTTON_HTML = '\n';
+ const BUTTON_NAME = /BUTTON_NAME/g;
+ const BUTTON_DATA_DIV = /BUTTON_DATA<\/div>/g;
+ const BUTTON_DATA = /BUTTON_DATA/g;
+ const BUTTON_LABEL = /BUTTON_LABEL/g;
+ const IMAGE_NAME = /IMAGE_NAME/g;
+
+ const LOG_LEVEL = IS_DEV_MODE ? LOG_DEBUG : LOG_INFO;
+ const LOG_TO_CONSOLE = true;
+ const LOG_TO_ZAP = IS_DEV_MODE;
+
+ /*
+ * Given the text from an HTTP request header, returns a parsed object.
+ */
+ function parseRequestHeader(headerText) {
+ const header = {};
+
+ header.method = headerText.substring(0, headerText.indexOf(' '));
+ headerText = headerText.substring(headerText.indexOf(' ') + 1);
+
+ header.uri = headerText.substring(0, headerText.indexOf(' '));
+ headerText = headerText.substring(headerText.indexOf(' ') + 1);
+
+ header.version = headerText.substring(0, headerText.indexOf('\r'));
+ headerText = headerText.substring(headerText.indexOf('\n') + 1);
+
+ header.fields = {};
+ while (headerText !== '') {
+ const field = headerText.substring(0, headerText.indexOf(':'));
+ headerText = headerText.substring(headerText.indexOf(':') + 2);
+ let value;
+
+ if (headerText.indexOf('\n') < 0) {
+ value = headerText;
+ headerText = '';
+ } else {
+ value = headerText.substring(0, headerText.indexOf('\n'));
+ headerText = headerText.substring(headerText.indexOf('\n') + 1);
+ }
+
+ header.fields[field] = value;
+ }
+
+ return header;
+ }
+
+ /*
+ * Given the text from an HTTP response header, returns a parsed object.
+ */
+ function parseResponseHeader(headerText) {
+ const header = {};
+
+ header.version = headerText.substring(0, headerText.indexOf(' '));
+ headerText = headerText.substring(headerText.indexOf(' ') + 1);
+
+ header.status = headerText.substring(0, headerText.indexOf(' '));
+ headerText = headerText.substring(headerText.indexOf(' ') + 1);
+
+ header.reason = headerText.substring(0, headerText.indexOf(' '));
+ headerText = headerText.substring(headerText.indexOf(' ') + 1);
+
+ header.fields = {};
+ while (headerText !== '') {
+ const field = headerText.substring(0, headerText.indexOf(':'));
+ headerText = headerText.substring(headerText.indexOf(':') + 2);
+
+ const value = headerText.substring(0, headerText.indexOf('\n'));
+ headerText = headerText.substring(headerText.indexOf('\n') + 1);
+
+ header.fields[field] = value;
+ }
+
+ return header;
+ }
+
+ /*
+ * Checks whether a message is from the ZAP domain or is a worker.
+ */
+ function isFromTrustedOrigin(message) {
+ return (
+ message.origin === 'https://zap' ||
+ message.isTrusted
+ );
+ }
+
+ /*
+ * Parses the domain from a uri string.
+ */
+ function parseDomainFromUrl(url) {
+ let hostname = hasScheme(url) ? url.split('/')[2] : url.split('/')[0];
+
+ // Find & remove "?" & "#"
+ hostname = hostname.split('?')[0];
+ hostname = hostname.split('#')[0];
+
+ return hostname;
+ }
+
+ function hasScheme(url) {
+ return url.indexOf('://') > -1;
+ }
+
+ /*
+ * Return a parameter value from a uri string
+ */
+ function getParameter(url, parameter) {
+ const start = url.indexOf(parameter) + parameter.length + 1;
+ let end = url.indexOf('&', start);
+ end = end === -1 ? url.length : end;
+
+ return url.substring(start, end);
+ }
+
+ /* STORAGE */
+
+ /*
+ * Return whether the HUD has been initialized yet.
+ */
+ function isHUDInitialized() {
+ return localforage.getItem(IS_HUD_INITIALIZED);
+ }
+
+ /*
+ * Initialize all of the info that will be stored in indexeddb.
+ */
+ function initializeHUD(leftTools, rightTools, drawer) {
+ if (IS_DEV_MODE && leftTools.indexOf('hudErrors') < 0) {
+ // Always add the error tool in dev mode
+ leftTools.push('hudErrors');
+ }
+
+ const promises = [];
+
+ promises.push(localforage.setItem(IS_HUD_INITIALIZED, true));
+ promises.push(localforage.setItem(IS_FIRST_TIME, true));
+ promises.push(localforage.setItem(IS_SERVICEWORKER_REFRESHED, false));
+ promises.push(localforage.setItem('settings.isHudVisible', true));
+ promises.push(localforage.setItem('drawer.isDrawerOpen', false));
+ // Note: in the below, "activeTab" is to be set to href, not name
+ promises.push(localforage.setItem('drawer.activeTab', '#history'));
+ promises.push(localforage.setItem('drawer', drawer));
+
+ const leftPanel = {
+ key: 'leftPanel',
+ orientation: 'left',
+ tools: leftTools
+ };
+
+ promises.push(saveFrame(leftPanel));
+
+ const rightPanel = {
+ key: 'rightPanel',
+ orientation: 'right',
+ tools: rightTools
+ };
+
+ promises.push(saveFrame(rightPanel));
+
+ return Promise.all(promises)
+ .then(setDefaultTools(leftTools, rightTools))
+ .catch(errorHandler);
+ }
+
+ /*
+ * Add the default tools to the panels.
+ */
+ function setDefaultTools(leftTools, rightTools) {
+ const promises = [];
+
+ for (let i = 0; i < leftTools.length; i++) {
+ loadTool(leftTools[i])
+ .then(tool => {
+ if (!tool) {
+ log(LOG_ERROR, 'utils.setDefaultTools', 'Failed to load tool.', tool.name);
+ return;
+ }
+
+ tool.isSelected = true;
+ tool.panel = 'leftPanel';
+ tool.position = i;
+
+ return writeTool(tool);
+ })
+ .catch(errorHandler);
+ }
+
+ for (let i = 0; i < rightTools.length; i++) {
+ loadTool(rightTools[i])
+ .then(tool => {
+ if (!tool) {
+ log(LOG_ERROR, 'utils.setDefaultTools', 'Failed to load tool.', tool.name);
+ return;
+ }
+
+ tool.isSelected = true;
+ tool.panel = 'rightPanel';
+ tool.position = i;
+
+ return writeTool(tool);
+ })
+ .catch(errorHandler);
+ }
+
+ return Promise.all(promises);
+ }
+
+ /*
+ * Backs up the frames tools to ZAP so they are persisted, eg when browser launch is used
+ */
+ function backupFrame(frame) {
+ return new Promise((resolve, reject) => {
+ if (self.dispatchEvent(new CustomEvent('hud.backup', {detail: {key: frame.key, value: JSON.stringify(frame.tools)}}))) {
+ resolve('OK');
+ } else {
+ reject(new Error('Failed to backup frame'));
+ }
+ });
+ }
+
+ /*
+ * Loads information about a frame as a blob from indexeddb.
+ */
+ function loadFrame(key) {
+ return localforage.getItem(key)
+ .catch(errorHandler);
+ }
+
+ /*
+ * Save information about a frame as a blob in indexeddb.
+ */
+ function saveFrame(frame) {
+ return localforage.setItem(frame.key, frame)
+ .catch(errorHandler);
+ }
+
+ /*
+ * Add a singletoolname to the "tools" list in indexeddb.
+ */
+ function registerTool(toolname) {
+ return localforage.getItem('tools')
+ .then(tools => {
+ tools.push(toolname);
+
+ return localforage.setItem('tools', tools);
+ })
+ .catch(errorHandler);
+ }
+
+ /*
+ * Add a list of toolnames to the "tools" list in indexeddb.
+ */
+ function registerTools(toolnames) {
+ return localforage.getItem('tools')
+ .then(tools => {
+ tools = tools.concat(toolnames);
+
+ return localforage.setItem('tools', tools);
+ })
+ .catch(errorHandler);
+ }
+
+ /*
+ * Loads the tool blob from indexeddb using the tool's name as the key.
+ */
+ function loadTool(name) {
+ log(LOG_TRACE, 'utils.loadTool', name);
+ return localforage.getItem(name);
+ }
+
+ /*
+ * Writes the tool blob to indexeddb using the tool's name as the key.
+ */
+ function writeTool(tool) {
+ log(LOG_TRACE, 'utils.writeTool', tool.name);
+ return localforage.setItem(tool.name, tool);
+ }
+
+ /*
+ * Return all tools currently selected in a panel.
+ */
+ function loadPanelTools(panelKey) {
+ log(LOG_DEBUG, 'utils.loadPanelTools', 'Panel ' + panelKey);
+ return loadFrame(panelKey)
+ .then(panel => {
+ const toolPromises = [];
+
+ panel.tools.forEach(toolname => {
+ const p = loadTool(toolname);
+ toolPromises.push(p);
+ });
+
+ return Promise.all(toolPromises);
+ })
+ .catch(errorHandler);
+ }
+
+ /*
+ * Return all tools from indexeddb.
+ */
+ function loadAllTools() {
+ return localforage.getItem('tools')
+ .then(toolnames => {
+ const toolPromises = [];
+
+ toolnames.forEach(toolname => {
+ const p = loadTool(toolname);
+ toolPromises.push(p);
+ });
+
+ return Promise.all(toolPromises);
+ })
+ .catch(errorHandler);
+ }
+
+ /*
+ * Add a tool to a specific panel using the tool and panel keys.
+ */
+ function addToolToPanel(toolKey, frameId) {
+ log(LOG_DEBUG, 'utils.addToolToPanel', toolKey);
+
+ const promises = [loadTool(toolKey), loadFrame(frameId)];
+
+ return Promise.all(promises)
+ .then(results => {
+ const tool = results[0];
+ const panel = results[1];
+
+ if (!tool) {
+ log(LOG_ERROR, 'utils.addToolToPanel', 'Failed to load tool.', toolKey);
+ return;
+ }
+
+ tool.isSelected = true;
+ tool.panel = frameId;
+ tool.position = panel.tools.length;
+
+ panel.tools.push(tool.name);
+
+ return Promise.all([writeTool(tool), saveFrame(panel)]);
+ })
+ .then(results => {
+ const tool = results[0];
+ const panel = results[1];
+
+ messageAllTabs(frameId, {action: 'addTool', tool})
+ .catch(error => {
+ if (error instanceof NoClientIdError) {
+ log(LOG_DEBUG, 'utils.addToolToPanel',
+ 'Could not add tool to frame: ' + frameId + '. Frame was not yet available to message.',
+ tool);
+ } else {
+ errorHandler(error);
+ }
+ });
+ backupFrame(panel);
+ })
+ .catch(errorHandler);
+ }
+
+ /*
+ * Remove a tool from a panel using the tool key.
+ */
+ function removeToolFromPanel(tabId, toolKey) {
+ return loadTool(toolKey)
+ .then(tool => Promise.all([tool, loadFrame(tool.panel), loadPanelTools(tool.panel)]))
+ .then(results => {
+ const removedTool = results[0];
+ const panel = results[1];
+ const panelTools = results[2];
+
+ const promises = [];
+
+ // Update tool
+ removedTool.isSelected = false;
+ removedTool.panel = '';
+
+ promises.push(writeTool(removedTool));
+ promises.push(messageAllTabs(panel.key, {action: 'removeTool', tool: removedTool}));
+
+ // Update panel
+ panel.tools.splice(panel.tools.indexOf(removedTool.name), 1);
+
+ promises.push(saveFrame(panel));
+ promises.push(backupFrame(panel));
+
+ // Update all panel tool positions
+ panelTools.forEach(tool => {
+ if (tool.position > removedTool.position) {
+ tool.position -= 1;
+
+ promises.push(writeTool(tool));
+ }
+ });
+ return Promise.all(promises);
+ })
+ .catch(errorHandler);
+ }
+
+ /*
+ * Send a postMessage to an iframe window using the custom stored frame key in indexeddb.
+ */
+ function messageFrame(tabId, frameId, message) {
+ return clients.matchAll({includeUncontrolled: true})
+ .then(clients => {
+ for (let i = 0; i < clients.length; i++) {
+ const client = clients[i];
+ const parameters = new URL(client.url).searchParams;
+
+ const tid = parameters.get('tabId');
+ const fid = parameters.get('frameId');
+
+ if (tid === tabId && fid === frameId) {
+ return client;
+ }
+ }
+
+ throw new NoClientIdError('Could not find a ClientId for tabId: ' + tabId + ', frameId: ' + frameId);
+ })
+ .then(client => {
+ return new Promise(((resolve, reject) => {
+ const channel = new MessageChannel();
+ channel.port1.start();
+ channel.port2.start();
+
+ channel.port1.addEventListener('message', event => {
+ if (event.data.error) {
+ reject(event.data.error);
+ } else {
+ resolve(event.data);
+ }
+ });
+
+ client.postMessage(message, [channel.port2]);
+ }));
+ })
+ .catch(errorHandler);
+ }
+
+ function messageAllTabs(frameId, message) {
+ return clients.matchAll({includeUncontrolled: true})
+ .then(clients => {
+ const frameClients = [];
+
+ for (let i = 0; i < clients.length; i++) {
+ const client = clients[i];
+ const parameters = new URL(client.url).searchParams;
+
+ const fid = parameters.get('frameId');
+
+ if (fid === frameId) {
+ frameClients.push(client);
+ }
+ }
+
+ if (frameClients.length === 0) {
+ log(LOG_DEBUG, 'utils.messageAllTabs', 'Could not find any clients for frameId: ' + frameId, message);
+ throw new NoClientIdError('Could not find any clients for frameId: ' + frameId);
+ }
+
+ return frameClients;
+ })
+ .then(clients => {
+ return new Promise(((resolve, reject) => {
+ for (let i = 0; i < clients.length; i++) {
+ const client = clients[i];
+
+ const channel = new MessageChannel();
+ channel.port1.start();
+ channel.port2.start();
+
+ channel.port1.addEventListener('message', event => {
+ if (event.data.error) {
+ reject(event.data.error);
+ } else {
+ resolve(event.data);
+ }
+ });
+
+ client.postMessage(message, [channel.port2]);
+ }
+ }));
+ })
+ .catch(errorHandler);
+ }
+
+ function zapApiErrorDialog(tabId, error) {
+ log(LOG_ERROR, 'zapApiErrorDialog', error.message, error.response);
+ messageFrame(tabId, 'display', {action: 'showDialog', config: {title: I18n.t('api_error_title'), text: error.message}});
+ }
+
+ /*
+ * Returns the visibilityState of the specified iframe window
+ */
+ function getAllClients(frameId) {
+ return clients.matchAll({includeUncontrolled: true})
+ .then(clients => {
+ const frameClients = [];
+
+ for (let i = 0; i < clients.length; i++) {
+ const client = clients[i];
+ const parameters = new URL(client.url).searchParams;
+
+ const fid = parameters.get('frameId');
+
+ if (fid === frameId) {
+ frameClients.push(client);
+ }
+ }
+
+ return frameClients;
+ })
+ .catch(errorHandler);
+ }
+
+ /*
+ * Returns the visibilityState of the specified iframe window
+ */
+ function getWindowVisibilityState(key) {
+ return loadFrame(key)
+ .then(getWindowFromFrame)
+ .then(window => {
+ return window.visibilityState;
+ });
+ }
+
+ /*
+ * Get the window object from a stored frame blob. Throws NoClientIdError if
+ * the clientId doesn't exist.
+ */
+ function getWindowFromFrame(frame) {
+ if (!frame) {
+ throw new Error('null frame passed to getWindowFromFrame');
+ }
+
+ return clients.get(frame.clientId)
+ .then(client => {
+ if (client !== undefined) {
+ return client;
+ }
+
+ throw new NoClientIdError('Could not find a client (window) of the service worker with id: ' + frame.clientId + ' found.');
+ });
+ }
+
+ /*
+ * Send a postMessage to a window.
+ */
+ function messageWindow(window, message, origin) {
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel();
+ channel.port1.start();
+ channel.port2.start();
+
+ channel.port1.addEventListener('message', event => {
+ if (event.data.error) {
+ reject(event.data.error);
+ } else {
+ resolve(event.data);
+ }
+ });
+
+ if (origin) {
+ window.postMessage(message, origin, [channel.port2]);
+ } else {
+ window.postMessage(message, [channel.port2]);
+ }
+ });
+ }
+
+ /*
+ * Sorts an array of tool objects by their position in descending order.
+ */
+ function sortToolsByPosition(tools) {
+ tools.sort((a, b) => b.position - a.position);
+ }
+
+ /*
+ * Uses search and replace to construct the html for a tool's button.
+ */
+ function configureButtonHtml(tool) {
+ let html = BUTTON_HTML;
+
+ if (!tool.data) {
+ html = html.replace(BUTTON_DATA_DIV, ' ');
+ }
+
+ html = html
+ .replace(BUTTON_NAME, tool.name)
+ .replace(BUTTON_LABEL, tool.label)
+ .replace(BUTTON_DATA, tool.data)
+ .replace(IMAGE_NAME, tool.icon);
+
+ return html;
+ }
+
+ /*
+ * Adds the correct scheme to a domain, handling the fact the ZAP could be upgrading an http domain to https
+ * Is only available in the serviceworker and Should always be used when supplying a domain to the ZAP API.
+ */
+
+ function getUpgradedDomain(domain) {
+ return localforage.getItem('upgradedDomains')
+ .then(upgradedDomains => {
+ let scheme = 'https';
+
+ if (upgradedDomains && domain in upgradedDomains) {
+ scheme = 'http';
+ }
+
+ return scheme + '://' + domain + (domain.endsWith('/') ? '' : '/');
+ })
+ .catch(errorHandler);
+ }
+
+ /*
+ * Adds the correct scheme to a url, handling the fact the ZAP could be upgrading an http url to https
+ * Is only available in the serviceworker and Should always be used when supplying a url to the ZAP API.
+ */
+
+ function getUpgradedUrl(url) {
+ return localforage.getItem('upgradedDomains')
+ .then(upgradedDomains => {
+ const domain = parseDomainFromUrl(url);
+ let scheme = 'https';
+
+ if (upgradedDomains && domain in upgradedDomains) {
+ scheme = 'http';
+ }
+
+ return scheme + '://' + domain + url.substring(url.indexOf(domain) + domain.length);
+ })
+ .catch(errorHandler);
+ }
+
+ /*
+ * Log an error in a human readable way with a stack trace.
+ */
+ function errorHandler(error) {
+ let message = error.toString();
+
+ if (error.stack) {
+ // Construct the stack trace
+ const lines = error.stack.split('\n').slice(0, -1);
+ lines.forEach(line => {
+ const functionName = line.substring(0, line.indexOf('/'));
+ const urlAndLineNo = line.substring(line.indexOf('http'), line.length - 1);
+ const parts = urlAndLineNo.split(':');
+ let url = parts[0] + ':' + parts[1];
+ let lineNo = parts[2] + ':' + parts[3];
+
+ // If port is included in the url
+ if (parts.length > 4) {
+ url = parts[0] + ':' + parts[1] + ':' + parts[2];
+ lineNo = parts[3] + ':' + parts[4];
+ }
+
+ message += '\n\t ' + functionName + ' ' + url + ' ' + lineNo;
+ });
+ }
+
+ log(LOG_ERROR, 'errorHandler', message, error);
+ }
+
+ function getZapFilePath(file) {
+ return ZAP_HUD_FILES + '/file/' + file;
+ }
+
+ function getZapImagePath(file) {
+ return ZAP_HUD_FILES + '/image/' + file;
+ }
+
+ function log(level, method, message, object) {
+ if (level > LOG_LEVEL || (!LOG_TO_CONSOLE && !LOG_TO_ZAP)) {
+ return;
+ }
+
+ let logLevel = LOG_STRS[level];
+
+ let record = new Date().toTimeString() + ' ' + logLevel + ' ' + method + ': ' + message;
+ if (object) {
+ record += ': ' + JSON.stringify(object);
+ }
+
+ if (LOG_TO_CONSOLE) {
+ if (logLevel === 'OFF' || logLevel === 'TRACE') {
+ logLevel = 'LOG';
+ }
+
+ console[logLevel.toLowerCase()](record);
+ }
+
+ if (LOG_TO_ZAP) {
+ // We don't know if we're in the service worker here, so raise an event
+ self.dispatchEvent(new CustomEvent('hud.log', {detail: {record}}));
+ }
+
+ if (level === LOG_ERROR) {
+ self.dispatchEvent(new CustomEvent('hud.error', {detail: {record}}));
+ }
+ }
+
+ return {
+ parseRequestHeader,
+ parseResponseHeader,
+ isFromTrustedOrigin,
+ parseDomainFromUrl,
+ getParameter,
+ isHUDInitialized,
+ initializeHUD,
+ loadFrame,
+ saveFrame,
+ registerTool,
+ registerTools,
+ loadTool,
+ writeTool,
+ loadPanelTools,
+ loadAllTools,
+ addToolToPanel,
+ removeToolFromPanel,
+ messageFrame,
+ messageAllTabs,
+ getAllClients,
+ getWindowVisibilityState,
+ messageWindow,
+ sortToolsByPosition,
+ configureButtonHtml,
+ getUpgradedDomain,
+ getUpgradedUrl,
+ errorHandler,
+ getZapFilePath,
+ getZapImagePath,
+ zapApiErrorDialog,
+ log
+ };
+})();
test('sortToolsByPosition result are in descending order', t => {
const tools = [{
@@ -66,7 +816,7 @@ test('parseDomainFromUrl returns domain from url containing query parameters', t
// TODO: Skipped test
// Is this expected behavior or not?
test('parseDomainFromUrl returns domain from url containing a port number', t => {
- const url = 'zaproxy.org:80';
+ const url = 'zaproxy.org';
const domain = utils.parseDomainFromUrl(url);
t.is(domain, 'zaproxy.org');
});
@@ -74,7 +824,7 @@ test('parseDomainFromUrl returns domain from url containing a port number', t =>
// TODO: Skipped test
// Is this expected behavior or not?
test('parseDomainFromUrl returns domain from url containing subdomains', t => {
- const url = 'www.zaproxy.org';
+ const url = 'zaproxy.org';
const domain = utils.parseDomainFromUrl(url);
t.is(domain, 'zaproxy.org');
});