wip: 2025-09-23T19:29:57+0200 (1758648597)

This commit is contained in:
Victor Westerlund 2025-09-23 19:29:57 +02:00
parent 1c2153552c
commit 3edfd6c164
Signed by: vlw
GPG key ID: D0AD730E1057DFC6
17 changed files with 240 additions and 97 deletions

1
.env.example.ini Normal file
View file

@ -0,0 +1 @@
LOG_ENABLED = true

1
.env.ini Normal file
View file

@ -0,0 +1 @@
LOG_ENABLED = false

View file

@ -0,0 +1,5 @@
vv-shell {
display: grid;
justify-items: center;
align-items: center;
}

View file

@ -0,0 +1,5 @@
vv-shell {
display: grid;
justify-items: center;
align-items: center;
}

View file

@ -59,6 +59,10 @@ button {
} }
} }
dialog {
margin: auto;
}
/* Sections */ /* Sections */
vv-shell { vv-shell {
@ -71,7 +75,7 @@ vv-shell {
border-radius: 9px; border-radius: 9px;
background-color: white; background-color: white;
&[vv-loading="true"] { &[vv-loading="true"] ::not(dialog) {
pointer-events: none; pointer-events: none;
} }

View file

@ -0,0 +1,99 @@
const MOUSE_MOVE_TIMEOUT_MS = 100;
globalThis.Logger = class Logger {
#abort;
#mouseMoveTimeout;
static get url() {
const url = new URL(window.location);
url.pathname = "/log";
return url;
}
/**
* Return a best-effort unique string that identifies this client
* @returns {String}
*/
static async #fingerprint() {
// Create a hash from various identifying browser data
const buffer = await window.crypto.subtle.digest("SHA-1", new TextEncoder().encode(JSON.stringify([
navigator.userAgent,
navigator.buildId,
navigator.languages
])));
return new DataView(buffer).getBigUint64().toString();
}
/**
* Extract desired MouseEvent data into an object literal
* @param {MouseEvent} event
* @returns {Object}
*/
static #mouseEvent(event) {
return {
e: event.type,
w: window.innerWidth,
h: window.innerHeight,
x: event.x,
y: event.y
}
}
/**
* Extract desired KeyboardEvent data into an object literal
* @param {KeyboardEvent} event
* @returns {Object}
*/
static #keyEvent(event) {
return {
e: event.type,
c: event.key,
s: event.shiftKey
}
}
constructor() {}
/**
* Start logging user activities
*/
start() {
this.#abort = new AbortController();
document.addEventListener("keyup", (event) => this.log(Logger.#keyEvent(event)), { signal: this.#abort.signal });
document.addEventListener("keydown", (event) => this.log(Logger.#keyEvent(event)), { signal: this.#abort.signal });
document.addEventListener("click", (event) => this.log(Logger.#mouseEvent(event)), { signal: this.#abort.signal });
document.addEventListener("mousemove", (event) => {
// Throttle mousemove events
clearTimeout(this.#mouseMoveTimeout);
//this.#mouseMoveTimeout = setTimeout(() => this.#log(mouseEvent(event)), MOUSE_MOVE_TIMEOUT_MS);
}, { signal: this.#abort.signal });
}
/**
* Stop logging user activitiers
*/
stop() {
this.#abort.abort();
}
/**
* Log user data
* @param {Object} data
* @returns {Response}
*/
async log(data) {
return await fetch(Logger.url, {
body: JSON.stringify({
client: data,
fingerprint: (await Logger.#fingerprint())
}),
method: "POST",
headers: Object.assign({
"Content-Type": "application/json"
}, VV.header)
});
};
}

View file

@ -0,0 +1,5 @@
// Clear all content and display the loading spinner for now. I want to add more stuff here later!
setTimeout(() => {
VV.shell.innerHTML = "";
VV.shell.setAttribute("vv-loading", true);
}, VV.delay);

View file

@ -1,23 +1,57 @@
const WHITELIST_USERNAMES = [ // Simulate a fake login page
"user", {
"root", const WHITELIST_USERNAMES = [
"admin", "user",
"mydlink" "root",
]; "admin",
const WHITELIST_PASSWORDS = [ "mydlink"
"root", ];
"admin", const WHITELIST_PASSWORDS = [
"12345", "root",
"mydlink", "admin",
"password", "12345",
"123456789" "mydlink",
] "password",
"123456789"
];
const INPUT_NAME_USERNAME = "username";
const INPUT_NAME_PASSWORD = "password";
document.querySelector("form button").addEventListener("click", (event) => { document.querySelector("form button").addEventListener("click", (event) => {
event.preventDefault(); event.preventDefault();
VV.shell.setAttribute("vv-loading", true); VV.shell.setAttribute("vv-loading", true);
const form = new FormData(event.target.closest("form")); const form = new FormData(event.target.closest("form"));
console.log("Hello"); // Invalid fake username or password derp
}); if (
!WHITELIST_USERNAMES.includes(form.get(INPUT_NAME_USERNAME))
|| !WHITELIST_PASSWORDS.includes(form.get(INPUT_NAME_PASSWORD))
) {
// Show "incorrect credentials" dialog after global Vegvisir delay
setTimeout(() => {
VV.shell.setAttribute("vv-loading", false);
document.querySelector("dialog").showModal();
}, VV.delay);
return;
}
new VV().navigate("/dashboard");
});
}
// Only start logging if the user does something with the input fields
{
const abortInitialInputChange = new AbortController();
const startLogging = () =>{
abortInitialInputChange.abort();
new globalThis.Logger().start();
};
document.querySelector("button").addEventListener("click", () => startLogging(), { signal: abortInitialInputChange.signal });
document.querySelectorAll("input").forEach(element => element.addEventListener("click", () => startLogging(), { signal: abortInitialInputChange.signal }));
document.querySelectorAll("input").forEach(element => element.addEventListener("keydown", () => startLogging(), { signal: abortInitialInputChange.signal }));
document.querySelectorAll("input").forEach(element => element.addEventListener("change", () => startLogging(), { signal: abortInitialInputChange.signal }));
}

View file

@ -1,73 +1,30 @@
// Set a global delay to simulate crappy web software const LOGIN_PAGE = "/login";
VV.delay = 200; const STORAGE_KEY_LOGGEDIN = "mydlink_dashboard_login";
// Log user activities // Set a generous global navigation delay to simulate crappy web software
{ VV.delay = 3500;
const MOUSE_MOVE_TIMEOUT_MS = 100;
const logUrl = new URL(window.location); // Redirect the user to the login page if session storage key is not set
logUrl.pathname = "/log"; if (!sessionStorage.getItem(STORAGE_KEY_LOGGEDIN) && window.location.pathname !== LOGIN_PAGE) {
const getRandomString = (length = 16) => {
const CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let string = "";
let mouseMoveTimeout; for (let i = 0; i < length; i++) string += CHARSET[Math.floor(Math.random() * CHARSET.length)];
// Return a fingerprint for this browser return string;
const fingerprint = async () => {
const buffer = await window.crypto.subtle.digest("SHA-1", new TextEncoder().encode(JSON.stringify([
navigator.userAgent,
navigator.buildId,
navigator.languages
])));
let fingerprint;
for (let i = 0; i < buffer.byteLength; i++) {
fingerprint += buffer[i];
}
return fingerprint;
}; };
// Log data const url = new URL(window.location);
const log = async (data) => {
console.log(JSON.stringify({
data: data,
fingerprint: await fingerprint()
}));
return await fetch(logUrl, { // Set some legit looking overcomplicated search parameters
body: JSON.stringify({ url.searchParams.set("mydl_sid", getRandomString());
data: data, // This is our fake "user is logged in" Storage API key
fingerprint: await fingerprint() url.searchParams.set("action", STORAGE_KEY_LOGGEDIN);
}), url.searchParams.set(`mydl_${getRandomString(3)}`, "dashboard");
method: "POST", url.searchParams.set(`mydl_asas_${getRandomString(4)}_${getRandomString(8)}`, "login_cgi");
headers: VV.header
});
};
const mouseEvent = (event) => { url.pathname = LOGIN_PAGE;
return {
e: event.type,
w: window.innerWidth,
h: window.innerHeight,
x: event.x,
y: event.y
}
}
const keyEvent = (event) => { new VV().navigate(url);
return {
e: event.type,
c: event.key,
s: event.shiftKey
}
}
document.addEventListener("keyup", (event) => log(keyEvent(event)));
document.addEventListener("keydown", (event) => log(keyEvent(event)));
document.addEventListener("click", (event) => log(mouseEvent(event)));
document.addEventListener("mousemove", (event) => {
// Throttle mousemove events
clearTimeout(mouseMoveTimeout);
//mouseMoveTimeout = setTimeout(() => log(mouseEvent(event)), MOUSE_MOVE_TIMEOUT_MS);
});
} }

View file

3
public/dashboard.php Normal file
View file

@ -0,0 +1,3 @@
<style><?= VV::css("assets/css/pages/dashboard") ?></style>
<img src="/assets/media/loading.gif">
<script><?= VV::js("assets/js/pages/dashboard") ?></script>

1
public/error.php Normal file
View file

@ -0,0 +1 @@
<h1>404 Not Found</h1>

View file

@ -1 +1,2 @@
<?= VV::include("public/login") ?> <style><?= VV::css("assets/css/pages/index") ?></style>
<img src="/assets/media/loading.gif">

View file

@ -5,7 +5,7 @@
require_once VV::root("src/Log.php"); require_once VV::root("src/Log.php");
if ($_SERVER["REQUEST_METHOD"] === "POST" && !empty($_POST)) { if ($_SERVER["REQUEST_METHOD"] === "POST" && !empty($_POST)) {
save_log($_POST); save_log((object) $_POST);
} }
?> ?>

View file

@ -2,11 +2,11 @@
<form method="POST"> <form method="POST">
<label> <label>
Username Username
<input type="text" required></input> <input name="username" type="text" required></input>
</label> </label>
<label> <label>
Password Password
<input type="password" required></input> <input name="password" type="password" required></input>
</label> </label>
<button type="submit">Log in</button> <button type="submit">Log in</button>
</form> </form>
@ -16,9 +16,9 @@
<p>Please follow these steps in order to register your mdlink-enabled product and get access to both mydlink.com and our mobile apps. Learn more details here.</p> <p>Please follow these steps in order to register your mdlink-enabled product and get access to both mydlink.com and our mobile apps. Learn more details here.</p>
</aside> </aside>
<dialog> <dialog>
<p>Incorrect username or password</p>
<form method="dialog"> <form method="dialog">
<button>OK</button> <p>Incorrect username or password</p>
<button>Try again</button>
</form> </form>
</dialog> </dialog>
<script><?= VV::js("assets/js/pages/login") ?></script> <script type="module"><?= VV::js("assets/js/pages/login") ?></script>

View file

@ -2,14 +2,15 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mydlink</title> <title>mydlink</title>
<link rel="icon" href="/assets/media/favicon.ico"> <link rel="icon" href="/assets/media/favicon.ico">
<style><?= VV::css("assets/css/shell") ?></style> <style><?= VV::css("assets/css/shell") ?></style>
</head> </head>
<body> <body>
<header> <header>
<img src="/assets/media/logo.gif"> <img src="/assets/media/logo.gif">
<p>DIR-880L</p>
<nav> <nav>
<ul> <ul>
<li><a href="">Home</a></li> <li><a href="">Home</a></li>
@ -55,6 +56,7 @@
</footer> </footer>
<?= VV::init() ?> <?= VV::init() ?>
<script><?= VV::js("assets/js/modules/Logger.js") ?></script>
<script><?= VV::js("assets/js/shell") ?></script> <script><?= VV::js("assets/js/shell") ?></script>
</body> </body>
</html> </html>

View file

@ -4,6 +4,31 @@
use \VV; use \VV;
function save_log(array $data): bool { // Save logs to this directory
$log_dir = VV::root("logs"); const LOG_DIR = "logs/";
// Use this as the default fingerprint if we don't get one from the client
const DEFAULT_FINGERPRINT = "undefined";
function save_log(object $data): bool {
// Logging is explicitly disabled. No environment variable is treated as enabled
if (!($_ENV["LOG_ENABLED"] ?? true)) {
return false;
}
$data->fingerprint ??= DEFAULT_FINGERPRINT;
$log_dir = VV::root(LOG_DIR . $data->fingerprint);
// Create log directory for this fingerprint if it's the first time we've seen it
if (!is_dir($log_dir)) {
if (!mkdir($log_dir)) {
// Failed to create directory
return false;
}
}
// Include server data
$data->server = (object) $_SERVER;
// Write log file
return file_put_contents($log_dir . "/" . microtime() . ".json", json_encode($data));
} }