Compare commits

..

2 commits

Author SHA1 Message Date
d791136abb feat: add git tracking of logs directory with logs/.gitkeep (#2)
This is more of a follow-up for #1. Probably a good idea to add the new `.env.ini` to the gitignore too 🤦. Good thing that file is not really important (right now).

Reviewed-on: https://codeberg.org/vlw/honeypot/pulls/2
2025-09-23 22:22:52 +02:00
vlw
0234ef984b refactor: release 2.0.0 (#1)
Baby steps that implements everything from the original [unfinished] version of this project from a bit over 2 years ago. We'll see what fun stuff we can add over time!

Reviewed-on: https://codeberg.org/vlw/honeypot/pulls/1
Co-authored-by: vlw <victor@vlw.se>
Co-committed-by: vlw <victor@vlw.se>
2025-09-23 20:09:41 +02:00
35 changed files with 539 additions and 396 deletions

View file

@ -1,2 +1 @@
; Save request details in a SQLite database at this location LOG_ENABLED = true
DB_POT=""

54
.gitignore vendored
View file

@ -1,51 +1,5 @@
# Bootstrapping # logs/*
################# !logs/.gitkeep
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.ini .env.ini
.env.backup vendor
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
public/robots.txt
# OS generated files #
######################
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db
.directory
# Tool specific files #
#######################
# vim
*~
*.swp
*.swo
# sublime text & textmate
*.sublime-*
*.stTheme.cache
*.tmlanguage.cache
*.tmPreferences.cache
# Eclipse
.settings/*
# JetBrains, aka PHPStorm, IntelliJ IDEA
.idea/*
# NetBeans
nbproject/*
# Visual Studio Code
.vscode
# Sass preprocessor
.sass-cache/

6
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "database/9f50ea1a5be726e610dc2fe134926869"] [submodule "vegvisir"]
path = database/9f50ea1a5be726e610dc2fe134926869 path = vegvisir
url = https://gist.github.com/9f50ea1a5be726e610dc2fe134926869.git url = https://codeberg.org/vegvisir/vegvisir

View file

@ -1,40 +1,29 @@
# Work in progress
I'm reviving this project. [The last commit was pushed over 2½ years ago](https://codeberg.org/vlw/honeypot/commit/2e4cc7e31ccc4190abfb327ecee0bdb553c3f565) at the time of writing! It was created (and not finished either) in the second generation of [my web framework](https://vegvisir.vlw.se). A lot has changes since then and I think it would be fun to revive this project.
# 🍯 Honeypot # 🍯 Honeypot
Can the IP, HTTP Headers and more from anyone trying to log in to this site. Everything entered into this page will be saved to an SQLite database. Yoink mouse clicks, mouse moves, keyboard pressed from the client, and the whole `$_SERVER` superglobal from the server on this website that is intentionally slow, and old-school looking to mimic the `D-Link DIR-880L` WiFi router's web interface. This is a typical WiFi router that you might find in a standard family home so maybe we can assume since the site isn't proxied, that someone hasn't changed the default credentials either? ;)
**Logs are saved as timestamped JSON-files in the `logs/` directory grouped under a subdirectory for each client using a [best-effort] client fingerprint.**
Logging only starts when the sneaky starts. Interact with the fake login screen input elements, and we're off.
![44302_](https://user-images.githubusercontent.com/35688133/204114986-123a5a9a-c164-49c4-a837-43d8c6f7ba45.png) ![44302_](https://user-images.githubusercontent.com/35688133/204114986-123a5a9a-c164-49c4-a837-43d8c6f7ba45.png)
> **Note** This project is not related to D-Link in any form or fashion. I just chose their mydlink portal as it features both IP camera and WiFi router controls from the same interface, which I intend to implement fun dummies of in the future. By the way, this project has nothing to do with D-Link specifically. The "mydlink" software apparently features both "WiFi-router" and IP-camera configuration from the same web interface - which adds additional
## Installation ## Installation
1. **Install Pragma**
This website is built on the [Pragma web framwork](https://github.com/VictorWesterlund/pragma), and is meant as a showcase of some of it's features.
[**Install the Pragma framwork**](https://github.com/VictorWesterlund/pragma) 1. **Clone this repo**
2. **Clone this repo**
Clone this repo (with submodules) anywhere Pragma can read its contents.
``` ```
$ git clone http://github.com/VictorWesterlund/honeypot --recurse-submodules $ git clone https://codeberg.org/vlw/honeypot
``` ```
3. **Set env var**
Make a copy of the `.env.example.ini` file 2. **Install the [Vegvisir](https://vegvisir.vlw.se) framework**
[**Run the Vegvisir install script**](https://codeberg.org/vegvisir/install#get-started)
``` 3. **Set write permission**
$ cp -p .env.example.ini .env.ini
```
and set the `DB_POT` variable to an absolute path to the SQLite database to create Make sure that the user your PHP configuration runs as has write access to the `logs/` directory in this repository.
```ini
DB_POT="/home/me/pot.sql"
````
## Data saved
The database will dump the contents of `$_POST` and `$_SERVER` into a JSON string along with the timestamp since Unix epoch the request was received.

View file

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

View file

@ -1,104 +0,0 @@
:root {
--padding: 20px;
--color-accent: #00b0d0;
}
* {
font-family: "Verdana", "Arial", sans-serif;
font-size: 12px;
}
body {
margin: 0;
background: url("/assets/media/Inner-page_cut_02.png") repeat-x right top;
}
a {
color: inherit;
text-decoration: none;
}
/* ---- */
header,
section {
width: 100%;
display: grid;
align-items: center;
justify-items: center;
}
header {
height: 100px;
}
.container {
width: 100%;
max-width: 1000px;
}
/* ---- */
header .container {
display: flex;
justify-content: space-between;
}
header nav {
margin-left: auto;
display: flex;
align-items: flex-end;
}
header nav p {
position: relative;
margin: 0;
padding: 5px 10px;
border-radius: 10px;
color: var(--color-accent);
}
/* --- */
#title h1 {
color: white;
font-size: 17px;
margin-left: var(--padding);
}
.content {
background-color: white;
box-sizing: border-box;
padding: var(--padding);
border-radius: 6px;
border: solid 1px #eee;
min-height: 450px;
box-shadow: 0 0 10px 5px #00000017;
border: solid 1px #e6e6e6;
}
.content * {
margin: 0;
}
@media (hover: hover) {
header nav p:hover {
background-color: var(--color-accent);
color: white;
}
header nav p:hover::after {
--size: 7px;
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: var(--size) solid transparent;
border-right: var(--size) solid transparent;
border-top: var(--size) solid var(--color-accent);
}
}

View file

@ -1,48 +1,5 @@
.content#login { vv-shell {
display: grid; display: grid;
grid-template-columns: 1fr 300px; justify-items: center;
} align-items: center;
.content#login aside {
background-color: #f7f7f7;
box-sizing: border-box;
padding: var(--padding);
display: flex;
flex-direction: column;
gap: var(--padding);
border-radius: 6px;
}
.content#login aside h2 {
color: var(--color-accent);
font-size: 17px;
}
.content#login form {
max-width: 400px;
display: flex;
flex-direction: column;
gap: var(--padding);
}
.content#login .error {
background-color: #ff000010;
color: red;
padding: 10px;
}
.content#login input[type="submit"] {
width: 100px;
padding: 7px;
background: linear-gradient(0deg, rgba(0,134,167,1) 0%, rgba(0,176,208,1) 100%);
border-radius: 3px;
border: none;
color: white;
cursor: pointer;
}
@media (hover: hover) {
.content#login input[type="submit"]:hover {
background: rgba(0,134,167,1);
}
} }

View file

@ -0,0 +1,26 @@
vv-shell {
display: grid;
align-items: baseline;
grid-template-columns: 1fr 300px;
}
form {
gap: 10px;
display: flex;
flex-direction: column;
button {
margin-top: 20px;
}
}
aside {
height: 100%;
padding: 20px;
border-radius: 6px;
background-color: var(--color-grey-light);
> * {
margin-bottom: 10px;
}
}

View file

@ -1,25 +0,0 @@
footer {
margin-top: var(--padding);
}
footer #footer_list {
--color: #888;
display: grid;
grid-template-columns: repeat(4, 1fr);
color: var(--color);
}
footer #footer_list > div {
display: flex;
flex-direction: column;
padding-left: var(--padding);
color: var(--color);
}
footer #footer_list > div p {
font-weight: bold;
}
footer #footer_list > div:not(:first-child) {
border-left: solid 1px var(--color);
}

151
assets/css/shell.css Normal file
View file

@ -0,0 +1,151 @@
:root {
--color-grey: #888888;
--color-dlink: #00B0D0;
--color-grey-dark: #424242;
--color-grey-light: #F7F7F7;
}
* {
color: inherit;
margin: 0;
box-sizing: border-box;
font-family: Arial, Helvetica, sans-serif;
}
html {
display: grid;
justify-items: center;
}
body {
width: 1000px;
display: grid;
justify-items: center;
background-image: url("/assets/media/Inner-page_cut_02.png");
background-size: 1200px;
background-repeat: no-repeat;
grid-template-rows: 70px 1fr 200px;
background-position: 50% -30px;
grid-template-columns: 1fr;
}
/* Components */
h1, h2, h3 {
color: var(--color-dlink);
}
p, label, a {
font-size: 13px;
}
button {
color: white;
height: 30px;
cursor: pointer;
border: solid 1px var(--color-grey-light);
min-width: 100px;
align-self: baseline;
background: linear-gradient(180deg,rgba(0, 176, 208, 1) 0%, rgba(0, 134, 167, 1) 100%);
justify-self: baseline;
border-radius: 4px;
&:hover {
border-color: var(--color-dlink);
}
&:active {
background: linear-gradient(180deg,rgba(0, 176, 208, 1) 0%, rgba(0, 134, 167, 1) 0%);
}
}
dialog {
margin: auto;
}
/* Sections */
vv-shell {
width: calc(100% - 30px);
margin: 40px 0;
padding: 20px;
position: relative;
min-height: 400px;
box-shadow: 0 0 9px 3px #00000026;
border-radius: 9px;
background-color: white;
&[vv-loading="true"] ::not(dialog) {
pointer-events: none;
}
&[vv-loading="true"]::after {
--size: 150px;
top: 50%;
left: 50%;
color: var(--color-dlink);
width: var(--size);
height: var(--size);
padding: 15px;
content: "";
position: absolute;
transform: translate(-50%, -50%);
font-weight: bolder;
background-size: contain;
background-image: url("/assets/media/spinner.gif");
}
}
header {
width: 100%;
display: flex;
align-items: end;
justify-content: space-between;
img {
height: 60px;
}
nav ul {
gap: 20px;
display: flex;
list-style: none;
a {
color: var(--color-dlink);
font-weight: bolder;
text-decoration: none;
}
}
}
footer {
width: 100%;
display: grid;
margin-top: 100px;
color: var(--color-grey);
grid-template-columns: repeat(4, 1fr);
section {
padding: 20px;
&:not(:first-child) {
border-left: solid 1px var(--color-grey);
}
p {
font-weight: bolder;
margin-bottom: 10px;
}
ul {
padding: unset;
list-style: none;
& a {
text-decoration: 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 +0,0 @@
globalThis.pragma.Interactions("document", {});

57
assets/js/pages/login.js Normal file
View file

@ -0,0 +1,57 @@
// Simulate a fake login page
{
const WHITELIST_USERNAMES = [
"user",
"root",
"admin",
"mydlink"
];
const WHITELIST_PASSWORDS = [
"root",
"admin",
"12345",
"mydlink",
"password",
"123456789"
];
const INPUT_NAME_USERNAME = "username";
const INPUT_NAME_PASSWORD = "password";
document.querySelector("form button").addEventListener("click", (event) => {
event.preventDefault();
VV.shell.setAttribute("vv-loading", true);
const form = new FormData(event.target.closest("form"));
// 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 }));
}

30
assets/js/shell.js Normal file
View file

@ -0,0 +1,30 @@
const LOGIN_PAGE = "/login";
const STORAGE_KEY_LOGGEDIN = "mydlink_dashboard_login";
// Set a generous global navigation delay to simulate crappy web software
VV.delay = 3500;
// Redirect the user to the login page if session storage key is not set
if (!sessionStorage.getItem(STORAGE_KEY_LOGGEDIN) && window.location.pathname !== LOGIN_PAGE) {
const getRandomString = (length = 16) => {
const CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let string = "";
for (let i = 0; i < length; i++) string += CHARSET[Math.floor(Math.random() * CHARSET.length)];
return string;
};
const url = new URL(window.location);
// Set some legit looking overcomplicated search parameters
url.searchParams.set("mydl_sid", getRandomString());
// This is our fake "user is logged in" Storage API key
url.searchParams.set("action", STORAGE_KEY_LOGGEDIN);
url.searchParams.set(`mydl_${getRandomString(3)}`, "dashboard");
url.searchParams.set(`mydl_asas_${getRandomString(4)}_${getRandomString(8)}`, "login_cgi");
url.pathname = LOGIN_PAGE;
new VV().navigate(url);
}

@ -1 +0,0 @@
Subproject commit ba34c5719fda3131a66ed9664ee182900c495bbd

View file

@ -1,33 +0,0 @@
<?php
require_once Path::root("database/9f50ea1a5be726e610dc2fe134926869/SQLite.php");
class PotDB extends SQLiteDriver {
public function __construct() {
// Check that we have a location to pot our catch
if (empty($_ENV["DB_POT"])) {
die("Where do you want it? Set DB_POT to a path on disk where the SQLite database will be created.");
}
// Initialize the SQLite interface
parent::__construct($_ENV["DB_POT"], Path::root("database/init/POT.sql"));
}
// Gottem
public function yoink(): bool {
// Stringiy all POST and SERVER fields into JSON
$data = json_encode([
"POST" => $_POST,
"SERVER" => $_SERVER
]);
// And save it!
$sql = "INSERT OR IGNORE INTO pot (id, data, version, created) VALUES (?, ?, ?, ?)";
return $this->return_bool($sql, [
crc32(uniqid($data, true)),
$data,
1,
time()
]);
}
}

View file

@ -1,6 +0,0 @@
CREATE TABLE pot (
id TEXT PRIMARY KEY NOT NULL,
data TEXT,
version INT NOT NULL,
created INT NOT NULL
);

0
logs/.gitkeep Normal file
View file

View file

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>mydlink</title>
<link rel="shortcut icon" href="/assets/media/favicon.ico"/>
<style><?= Page::css("pages/document") ?></style>
</head>
<body>
<header>
<div class="container">
<img src="/assets/media/logo.gif"/>
<nav>
<a href="/" data-trigger="document" data-action="nav"><p>Home</p></a>
<a href="/" data-trigger="document" data-action="nav"><p>Products</p></a>
<a href="/" data-trigger="document" data-action="nav"><p>Mobile App</p></a>
<a href="/" data-trigger="document" data-action="nav"><p>Help</p></a>
</nav>
</div>
</header>
<main>
<?= Page::include("index") ?>
</main>
<footer>
<?= Page::include("partials/footer") ?>
</footer>
<script><?= Page::include("pragma") ?></script>
<script>{<?= Page::js("pages/document") ?>}</script>
</body>
</html>

View file

@ -1,38 +0,0 @@
<?php
// I'll have that tyvm
if ($_SERVER["REQUEST_METHOD"] === "POST") {
require_once Path::root("database/Pot.php");
(new PotDB())->yoink();
}
?>
<style><?= Page::css("pages/index") ?></style>
<section>
<div id="title" class="container">
<h1>Sign In to mydlink</h1>
</div>
</section>
<section>
<div id="login" class="content container">
<form method="POST">
<?php if ($_SERVER["REQUEST_METHOD"] === "POST"): ?>
<p class="error">Invalid username or password. Please try again.</p>
<?php endif; ?>
<div>
<label>Username</label>
<input type="text" name="username">
</div>
<div>
<label>Password</label>
<input type="password" name="password">
</div>
<input type="submit" value="Log in">
</form>
<aside>
<h2>Not Registered yet?</h2>
<p>To get started with mydlink cloud services, you need to have a mydlink-enabled product. Learn more about supported products <a href="https://se.mydlink.com/content/productfamily">here</a>.</p>
<p>Please follow the steps in order to register your mydlink-enabled product and get access to both mydlink.com and our mobile apps. Learn more details <a href="https://se.mydlink.com/content/notreg">here</a>.</p>
</aside>
</div>
</section>

View file

@ -1,28 +0,0 @@
<style><?= Page::css("pages/partials/footer") ?></style>
<section>
<div id="footer_list" class="container">
<div>
<p>Official Information</p>
<a href="http://www.dlink.com/">Global D-Link</a>
<a href="https://se.mydlink.com/content/productfamily">About mydlink</a>
<a href="https://se.mydlink.com/termsOfUse">Terms of Use</a>
<a href="https://se.mydlink.com/privacyPolicy">Privacy Policy</a>
<a href="https://sso.dlink.com/privacy-pledge">Privacy Pledge</a>
<a href="">Cookie Preferences</a>
</div>
<div>
<p>Product</p>
<a href="">Cloud Cameras</a>
</div>
<div>
<p>Mobile App</p>
<a href="https://se.mydlink.com/apps">Download Apps</a>
</div>
<div>
<p>Help</p>
<a href="https://se.mydlink.com/faq">Download Apps</a>
<a href="https://se.mydlink.com/download">Download</a>
<a href="https://www.dlink.com/support">Support</a>
</div>
</div>
</section>

View file

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

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>

2
public/index.php Normal file
View file

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

11
public/log.php Normal file
View file

@ -0,0 +1,11 @@
<?php
use function Honeypot\Log\save_log;
require_once VV::root("src/Log.php");
if ($_SERVER["REQUEST_METHOD"] === "POST" && !empty($_POST)) {
save_log((object) $_POST);
}
?>

24
public/login.php Normal file
View file

@ -0,0 +1,24 @@
<style><?= VV::css("assets/css/pages/login") ?></style>
<form method="POST">
<label>
Username
<input name="username" type="text" required></input>
</label>
<label>
Password
<input name="password" type="password" required></input>
</label>
<button type="submit">Log in</button>
</form>
<aside>
<h3>Not Registered yet?</h3>
<p>To get started with mydlink cloud services, you need to have a mydlink-enabled product. Learn more about supported products 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>
<dialog>
<form method="dialog">
<p>Incorrect username or password</p>
<button>Try again</button>
</form>
</dialog>
<script type="module"><?= VV::js("assets/js/pages/login") ?></script>

62
public/shell.php Normal file
View file

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>mydlink</title>
<link rel="icon" href="/assets/media/favicon.ico">
<style><?= VV::css("assets/css/shell") ?></style>
</head>
<body>
<header>
<img src="/assets/media/logo.gif">
<p>DIR-880L</p>
<nav>
<ul>
<li><a href="">Home</a></li>
<li><a href="">Products</a></li>
<li><a href="">Mobile App</a></li>
<li><a href="">Help</a></li>
</ul>
</nav>
</header>
<vv-shell></vv-shell>
<footer>
<section>
<p>Official information</p>
<ul>
<li><a href="">Global D-Link</a></li>
<li><a href="">About mydlink</a></li>
<li><a href="">Terms of Use</a></li>
<li><a href="">Privacy Policy</a></li>
<li><a href="">Privacy Pledge</a></li>
<li><a href="">Cookie Preferences</a></li>
</ul>
</section>
<section>
<p>Product</p>
<ul>
<li><a href="">Cloud Cameras</a></li>
</ul>
</section>
<section>
<p>Mobile App</p>
<ul>
<li><a href="">Download Apps</a></li>
</ul>
</section>
<section>
<p>Help</p>
<ul>
<li><a href="">Download Apps</a></li>
<li><a href="">Download</a></li>
<li><a href="">Support</a></li>
</ul>
</section>
</footer>
<?= VV::init() ?>
<script><?= VV::js("assets/js/modules/Logger.js") ?></script>
<script><?= VV::js("assets/js/shell") ?></script>
</body>
</html>

34
src/Log.php Normal file
View file

@ -0,0 +1,34 @@
<?php
namespace Honeypot\Log;
use \VV;
// Save logs to this directory
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));
}

1
vegvisir Submodule

@ -0,0 +1 @@
Subproject commit 016b88068212243ce33894fbba9ffa91009146f0