Skip to content

Instantly share code, notes, and snippets.

@peaBerberian
Last active April 22, 2022 08:28
Show Gist options
  • Save peaBerberian/5471f397b6dd3682bc5980d11cfc4421 to your computer and use it in GitHub Desktop.
Save peaBerberian/5471f397b6dd3682bc5980d11cfc4421 to your computer and use it in GitHub Desktop.
Log client/server for local debugging on embedded devices
// Note: if not on localhost, don't forget to add something like:
// `<meta http-equiv="Content-Security-Policy" content="connect-src 'self' ws: wss:">`
// to the HTML page to allow websocket connections to external websites
// SERVER_URL of the server (relative to the client).
// Localhost by default
const SERVER_URL = "ws://127.0.0.1";
// Port on which the server is listening.
// If going through ngrok, you can set it to `null`
// as a default
const SERVER_PORT = 8080;
// If the server has token usage enabled, the `SERVER_TOKEN` constant should be
// set to that value.
const SERVER_TOKEN = "";
// To set to true if you also want to log when xhr are received / sent
const SHOULD_LOG_REQUESTS = true;
const wsUrl = SERVER_PORT != null
? `${SERVER_URL}:${SERVER_PORT}/${SERVER_TOKEN}`
: `${SERVER_URL}/${SERVER_TOKEN}`;
const socket = new WebSocket(wsUrl);
const logQueue = [];
let isInitialized = false;
let sendLog = (log) => {
logQueue.push(log);
};
socket.addEventListener("open", function () {
sendLog = (log) => socket.send(log);
isInitialized = true;
for (const log of logQueue) {
sendLog(log);
}
logQueue.length = 0;
});
function formatAndSendLog(namespace, log) {
const time = performance.now().toFixed(2);
const logText = `${time} [${namespace}] ${log}`;
sendLog(logText);
}
function processArg(arg) {
if (typeof arg === "object") {
try {
const stringified = JSON.stringify(arg);
if (stringified.length > 300) {
return stringified.substring(0, 296) + " ...";
}
return stringified;
} catch (_) {}
}
return arg;
}
[ "log", "error", "info", "warn", "debug"].forEach(meth => {
console[meth] = function (...args) {
const argStr = args.map(processArg).join(" ");
formatAndSendLog(meth, argStr);
};
});
if (SHOULD_LOG_REQUESTS) {
const originalXhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function () {
const method = arguments[0];
const url = arguments[1];
if (typeof method !== "string" || typeof url !== "string") {
return originalXhrOpen.apply(this, arguments);
}
this.addEventListener("load", function () {
formatAndSendLog("Network", `Loaded ${method} XHR from: ${url} ` +
`(status: ${this.status})`);
});
this.addEventListener("error", function () {
formatAndSendLog("Network", `Errored ${method} XHR from: ${url}`);
});
this.abort = function() {
formatAndSendLog("Network", `Aborted ${method} XHR from: ${url}`);
return XMLHttpRequest.prototype.abort.apply(this, arguments);
}
this.send = function () {
formatAndSendLog("Network", `Sending ${method} XHR to: ${url}`);
return XMLHttpRequest.prototype.send.apply(this, arguments);
};
return originalXhrOpen.apply(this, arguments);
}
const originalFetch = window.fetch;
window.fetch = function() {
let url;
let method;
if (arguments[0] == null) {
url = undefined;
} else if (typeof arguments[0] === "string") {
url = arguments[0];
} else if (arguments[0] instanceof URL) {
url = arguments[0].href;
} else if (typeof arguments[0].url === "string") {
url = arguments[0].url;
} else {
try {
url = arguments[0].toString();
} catch (_) {}
}
if (arguments[0] == null) {
method = "GET";
} else if (typeof arguments[0].method === "string") {
method = arguments[0].method;
} else if (arguments[1] != null && typeof arguments[1].method === "string") {
method = arguments[0].method;
} else {
method = "GET";
}
formatAndSendLog("Network", `Sending ${method} fetch to: ${url}`);
const realFetch = originalFetch.apply(this, arguments);
return realFetch.then(
(res) => {
formatAndSendLog("Network", `Loaded ${method} fetch from: ${url} ` +
`(status: ${res.status})`);
return res;
},
(err) => {
formatAndSendLog("Network", `Errored/Aborted ${method} fetch from: ${url}`);
throw err;
});
};
}
const WebSocket = require('ws');
const fs = require("fs");
/** Port on which your server will be listening */
const PORT = 8080;
/**
* Security mechanism to ensure only authorized client are sending requests.
* If set to `true`, a token will be displayed once you will start the server
* that should be set as the path by the client when doing the WebSocket request
* for exemple, for a token "foo", the URL used by the client should be:
* ws://SERVER_IP:PORT/foo
*
* If set to `false`, this security mechanism will be disabled.
*/
const useToken = true;
const ANSI_COLOR_RESET = "\x1b[0m";
let token;
if (useToken) {
token = generateToken();
console.log("Generated token:", token);
}
const wss = new WebSocket.Server({ port: PORT });
wss.on('connection', (ws, req) => {
if (useToken && req.url.substring(1) !== token) {
console.warn("Received request with invalid token:", req.url.substring(1));
ws.close();
return;
}
ws.on('message', message => {
let color = "";
let formattedMsg = message;
const indexOfNamespaceStart = message.indexOf("[");
if (indexOfNamespaceStart >= 0) {
const indexOfNamespaceEnd = message.indexOf("]");
if (indexOfNamespaceEnd > 0) {
const namespace = message.substring(indexOfNamespaceStart + 1, indexOfNamespaceEnd);
switch (namespace) {
case "error":
color = "\x1b[31m"; // red
break;
case "warn":
color = "\x1b[33m"; // yellow
break;
case "info":
color = "\x1b[34m"; // blue
break;
case "Network":
color = "\x1b[35m"; // magenta
break;
}
formattedMsg = message.replace(/\n/g, "\n" + " ".repeat(indexOfNamespaceEnd + 2));
}
}
console.log(color + formattedMsg + ANSI_COLOR_RESET);
fs.appendFile("logs.txt", formattedMsg + "\n", function() {
// on finished. Do nothing for now.
});
});
console.log("received connection");
});
console.log(`Listening at ws://127.0.0.1:${PORT}`);
function generateToken() {
return generateHalfToken() + generateHalfToken();
function generateHalfToken() {
return Math.random().toString(36).substring(2); // remove `0.`
}
}
@peaBerberian
Copy link
Author

peaBerberian commented Jan 18, 2022

Those scripts allows to send logs from a JavaScript application to a server, by using WebSocket connections for lighter real-time log reports.

The point of that script is mainly to facilitate debugging on some embedded devices where using more complex debugging tool (like Chrome Inspector) can be too heavy.

Example of usage:

  1. Ensure you have nodejs installed on your PC.

  2. run node websocket_server.js.

  3. (optional) Optionally, you may want to run some tool like ngrok, to enable for example WebSocket Secure Connection (same principle than for HTTPS), especially if the tested platform is on a different device than your PC

  4. A token should have been outputed by the websocket_server.js script, copy that token and paste it as the value of the SERVER_TOKEN constant in websocket_client.js.
    This token ensure that only authorized clients are sending logs to the server.

    If a redirecting tool such as ngrok is used, you may also want to update in that script:

    • the SERVER_URL constant: to set to the URL returned by the tool.
      Note that wss and ws should be used as protocol instead of respectively https and http
    • the SERVER_PORT constant: to the port given by the tool. If no specific port was given, this should be set to null, so that the defaut ports are used (which are the same ports than what HTTP and HTTPS use).

    Moreover requests (trough XMLHttpRequest and through fetch) are logged by default. Depending on what you want to do, you might want to disable requests logs by setting SHOULD_LOG_REQUESTS to false.

  5. To ensure that the browser is not blocking WebSocket connections, you might want to allow the corresponding "Content Security Policy".

    A simple way of doing that is by adding the following element in the <head> part of your HTML page:
    <meta http-equiv="Content-Security-Policy" content="connect-src 'self' ws: wss:">
    Note that this allows all WebSocket connections opened by your page, which is something you may want to avoid in some cases on security grounds (it offers a level of XSS protection).

  6. You can then add the script of websocket_client.js to your page. This has to be done before other script are loaded.

    One simple way of doing that is to copy-paste its content inside a new <script> element of your HTML page, which should be declared before any other script tags.

  7. You're done!
    Most logs should now be outputed at two places:

    • by the websocket_server.js script which should still be running, in real time.
    • progressively on a logs.txt file, in the same directory than websocket_server.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment