Last active
April 22, 2022 08:28
-
-
Save peaBerberian/5471f397b6dd3682bc5980d11cfc4421 to your computer and use it in GitHub Desktop.
Log client/server for local debugging on embedded devices
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
}); | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.` | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:
Ensure you have nodejs installed on your PC.
run
node websocket_server.js
.(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 PCA token should have been outputed by the
websocket_server.js
script, copy that token and paste it as the value of theSERVER_TOKEN
constant inwebsocket_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:SERVER_URL
constant: to set to the URL returned by the tool.Note that
wss
andws
should be used as protocol instead of respectivelyhttps
andhttp
SERVER_PORT
constant: to the port given by the tool. If no specific port was given, this should be set tonull
, so that the defaut ports are used (which are the same ports than what HTTP and HTTPS use).Moreover requests (trough
XMLHttpRequest
and throughfetch
) are logged by default. Depending on what you want to do, you might want to disable requests logs by settingSHOULD_LOG_REQUESTS
tofalse
.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).
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.You're done!
Most logs should now be outputed at two places:
websocket_server.js
script which should still be running, in real time.logs.txt
file, in the same directory thanwebsocket_server.js