This commit is contained in:
Andrii Dokhniak 2025-08-05 11:27:44 +02:00
parent 5b9866a8bd
commit e23cac512e
20 changed files with 950 additions and 724 deletions

View File

@ -9,7 +9,7 @@ RUN node --version
RUN git clone https://github.com/httptoolkit/httptoolkit-server /httptoolkit-server
WORKDIR /httptoolkit-server
RUN git checkout 5c60a70b08d30126639484314f5b5619a388b026 \
RUN git checkout 490b1b6f5180ad634b60997778c5f96b2f62bf0b \
&& npm i && npm run build:src
# Set up proxy_cache_thing

View File

@ -73,12 +73,19 @@ io.on("connection", (socket) => {
socket.on("back", async () => {
if (gps_setting_in_progress) {
send_notification(socket, false, "Interactions not allowed when setting gps coordinates", "");
send_notification(socket, false, "Back", "");
return ;
}
await spawnPromise("bash", ["/conf/back.sh"]);
});
socket.on("recent", async () => {
if (gps_setting_in_progress) {
send_notification(socket, false, "Interactions not allowed when setting gps coordinates", "");
return ;
}
await spawnPromise("bash", ["/conf/recent.sh"]);
});
socket.on("home", async () => {
if (gps_setting_in_progress) {
send_notification(socket, false, "Interactions not allowed when setting gps coordinates", "");

View File

@ -1 +1 @@
adb shell su root cat /data/data/com.google.android.gms/shared_prefs/adid_settings.xml | xmllint --xpath 'string(//map/string[@name="adid_key"])' -
adb shell su root cat /data/data/com.google.android.gms/shared_prefs/adid_settings.xml | xmllint --xpath 'string(//map/string[@name="adid_key"])' - | tr -d $'\n'

1
android/conf/recent.sh Normal file
View File

@ -0,0 +1 @@
/opt/android-sdk-linux/platform-tools/adb shell input keyevent 187

View File

@ -3,7 +3,7 @@ FROM node:22.14.0
RUN git clone https://github.com/httptoolkit/httptoolkit-server
WORKDIR /httptoolkit-server
RUN git checkout 5c60a70b08d30126639484314f5b5619a388b026 \
RUN git checkout 490b1b6f5180ad634b60997778c5f96b2f62bf0b \
&& npm i && npm run build:src
CMD /httptoolkit-server/bin/run start -c /certificates

View File

@ -2,5 +2,5 @@
npm i
npm run build
node --watch index.mjs
#tail -f /dev/null
node index.mjs &
tail -f /dev/null

View File

@ -4,154 +4,8 @@
<meta charset="UTF-8" />
<title>Rentgen android</title>
<script src="/htmx.js"></script>
<style>
main {
display: flex;
}
.log-section {
height: auto;
width: 400px;
overflow: auto;
display: flex;
flex-direction: column;
margin-left: 20px;
}
.screen {
display: inline-block;
cursor: pointer;
}
.screen-buttons {
display: flex;
justify-content: space-around;
margin-top: 5px;
gap: 10px;
}
.screen-buttons button {
font-size: 1.1rem;
padding: 10px 20px;
width: 100%;
cursor: pointer;
background-color: transparent;
}
.screen-buttons button:hover {
background-color: aqua;
}
#clicks-log {
font-family:
Menlo,
Consolas,
Monaco,
Liberation Mono,
Lucida Console,
monospace;
}
.tab {
border: 1px solid #ccc;
background-color: #f1f1f1;
}
/* Style the buttons that are used to open the tab content */
.tab button {
background-color: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
}
/* Change background color of buttons on hover */
.tab button:hover {
background-color: #ddd;
}
/* Create an active/current tablink class */
.tab button.active {
background-color: #ccc;
}
.tabcontent.active {
display: flex;
flex-direction: column;
flex-grow: 1;
}
/* Style the tab content */
.tabcontent {
display: none;
padding: 6px 12px;
border: 1px solid #ccc;
border-top: none;
}
html,
body,
main {
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
}
main {
display: flex;
flex-direction: row;
align-items: stretch;
}
#logs-tab {
overflow: auto;
text-wrap: wrap;
}
.screen-section {
display: flex;
flex-direction: column;
}
.screen-section #screen,
.screen-section.screen_buttons {
flex-grow: 0;
}
#screen {
user-select: none;
}
.tab-section {
display: flex;
flex-direction: column;
flex-grow: 1;
}
#resp {
display: none;
}
#upload_form {
display: flex;
flex-direction: column;
}
#upload_form button,
#upload_form label {
border: 2px solid #ccc;
background-color: #f1f1f1;
cursor: pointer;
padding: 3px 10px;
transition: 0.3s;
}
#upload_form button:hover,
#upload_form label:hover {
background-color: #ddd;
}
#notifications {
width: 40%;
margin-left: 60%;
position: absolute;
}
#logs {
display: flex;
flex-direction: row;
}
</style>
<link rel="stylesheet" href="styles.css" />
<script src="/main.js" type="module"></script>
</head>
<body>
<div id="notifications"></div>
@ -168,6 +22,7 @@
tabindex="0"
/>
<div class="screen-buttons" style="flex-grow: 0">
<button class="screen-buttons-recent-apps">recent-apps</button>
<button class="screen-buttons-home">home</button>
<button class="screen-buttons-back">back</button>
</div>
@ -196,14 +51,17 @@
<div class="tab">
<button
class="tablinks active"
onclick="open_tab(event, 'httptoolkit-tab')"
onclick="main.open_tab(event, 'httptoolkit-tab')"
>
HttpToolkit UI
</button>
<button class="tablinks" onclick="open_tab(event, 'logs-tab')">
<button class="tablinks" onclick="main.open_tab(event, 'logs-tab')">
Logs
</button>
<button class="tablinks" onclick="open_tab(event, 'controls-tab')">
<button
class="tablinks"
onclick="main.open_tab(event, 'controls-tab')"
>
Device Controls
</button>
</div>
@ -211,7 +69,23 @@
<div class="tabcontent" id="logs-tab">
<div id="logs">
<p id="clicks-log" class="log-section"></p>
<p id="traffic-log" class="log-section"></p>
<div id="traffic-log" class="log-section">
<div>
<button onClick="main.download_har()">Download HAR</button>
<button onClick="main.inspect_har()">Inspect HAR</button>
<div>
<h2>stats:</h2>
<p id="traffic-log-req-res-pairs">
Request + responce pairs: 0
</p>
<p id="traffic-log-waiting">
Waiting for the responce: 0
</p>
</div>
<div id="traffic-log-lines">
</div>
</div>
</div>
</div>
</div>
@ -224,7 +98,7 @@
></iframe>
</div>
<div class="tabcontent" id="controls-tab">
<form id="set_coords" onsubmit="coords_handler(event)">
<form id="set_coords" onsubmit="main.coords_handler(event)">
<label>
Latitude:
<input type="text" name="lat" />
@ -235,7 +109,9 @@
</label>
<button type="submit">Submit coords</button>
</form>
<button id="reset_adid_btn" onclick="reset_adid_handler(event)">Reset ADID</button>
<button id="reset_adid_btn" onclick="main.reset_adid_handler(event)">
Reset ADID
</button>
<table>
<thead></thead>
<tbody>
@ -256,219 +132,5 @@
</div>
</div>
</main>
<script src="/socket.io.js"></script>
<script>
var socket = io();
function reset_adid_handler(e) {
socket.emit("reset_adid");
}
function coords_handler(e) {
e.preventDefault();
const form_data = new FormData(e.target);
console.log(form_data);
socket.emit("setcoord", {
lon: Number.parseFloat(form_data.get("lon")),
lat: Number.parseFloat(form_data.get("lat")),
});
}
function open_tab(evt, tab_name) {
let i, tabcontent, tablinks;
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
if (tabcontent[i].id != tab_name) {
tabcontent[i].classList.remove("active");
} else {
tabcontent[i].classList.add("active");
}
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].classList.remove("active");
}
// Show the current tab, and add an "active" class to the button that opened the tab
evt.currentTarget.classList.add("active");
}
var screen = document.getElementById("screen");
var clicksLog = document.getElementById("clicks-log");
const homeButton = document.querySelector(".screen-buttons-home");
const backButton = document.querySelector(".screen-buttons-back");
let lastTouch = new Date().getTime();
const calculateElapsedTime = (last) => {
const currentTouch = new Date().getTime();
const elapsedTime = currentTouch - lastTouch;
const elapsedSec = Math.round(elapsedTime / 1000);
lastTouch = currentTouch;
return elapsedSec;
};
const waitToLog = (clickInfoText) => {
const clickInfo = document.createElement("span");
const waitInfo = document.createElement("span");
waitInfo.textContent = `await wait(${calculateElapsedTime(
lastTouch
)});`;
clicksLog.appendChild(waitInfo);
clickInfo.textContent = clickInfoText;
clicksLog.appendChild(clickInfo);
};
const registerClick = ({ path, logText, body }) => {
const clicksLog = document.getElementById("clicks-log");
const span = document.createElement("span");
waitToLog(logText);
socket.emit(path, body ? body : {});
};
homeButton.addEventListener("click", () =>
registerClick({ path: "home", logText: "await homeButton();" })
);
backButton.addEventListener("click", () =>
registerClick({ path: "back", logText: "await backButton();" })
);
socket.on("screenshot_data", (data) => {
try {
const blob = new Blob([data]);
screen.src = URL.createObjectURL(blob);
} catch (error) {
console.error("Error fetching image: ", error);
}
});
socket.on("private_info", (data) => {
console.log("private_info");
adid_priv_info_table.textContent = data.adid;
lat_priv_info_table.textContent = data.latitude;
lon_priv_info_table.textContent = data.longitude;
});
socket.emit("private_info_req");
socket.onAny((ev, ...args) => {
console.log("ev: ", ev, args);
});
async function displayImage() {
socket.emit("screenshot");
}
let isDragging = false;
const screenSize = [320, 640];
function calcMousePos(event) {
let rect = screen.getBoundingClientRect();
let x = ((event.clientX - rect.left) / rect.width) * screenSize[0];
let y = ((event.clientY - rect.top) / rect.height) * screenSize[1];
x = Math.min(Math.max(x, 0), screenSize[0]);
y = Math.min(Math.max(y, 0), screenSize[1]);
return { x, y };
}
screen.addEventListener(
"mousemove",
(event) => {
if (!isDragging) return;
let pos = calcMousePos(window.event);
if (isDragging) {
registerClick({
path: "motionevent",
logText: `await motionevent({motionType: "MOVE", x:${pos.x},y:${pos.y}}});`,
body: {
motionType: "MOVE",
x: pos.x,
y: pos.y,
},
});
}
},
false
);
const handleDraggStart = (event) => {
isDragging = true;
let pos = calcMousePos(event);
registerClick({
path: "motionevent",
logText: `await motionevent({motionType: "DOWN", x:${pos.x},y:${pos.y}}});`,
body: {
motionType: "DOWN",
x: pos.x,
y: pos.y,
},
});
};
screen.addEventListener("mousedown", handleDraggStart);
document.addEventListener("mouseup", (e) => {
if (!isDragging) return;
isDragging = false;
let pos = calcMousePos(e);
registerClick({
path: "motionevent",
logText: `await motionevent({motionType: "MOVE", x:${pos.x},y:${pos.y}}});`,
body: {
motionType: "MOVE",
x: pos.x,
y: pos.y,
},
});
registerClick({
path: "motionevent",
logText: `await motionevent({motionType: "UP", x:${pos.x},y:${pos.y}}});`,
body: {
motionType: "UP",
x: pos.x,
y: pos.y,
},
});
});
window.addEventListener("keydown", (event) => {
let key = event.key;
if (key === "Space") key = " ";
else if (key !== "Enter" && key !== "Backspace" && key.length !== 1)
return;
console.log(event.key, key);
if (document.getElementById("screen").matches(":hover")) {
registerClick({
path: "key",
logText: `await key(${event.key});`,
body: { key },
});
}
});
async function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
async function screenshot_loop() {
var before;
while (true) {
before = performance.now();
await displayImage();
while (performance.now() - before < ___screenshotDelayMs___)
await sleep(50);
}
}
screenshot_loop();
</script>
<script src="/trafficLog.js"></script>
<script src="/notifications.js"></script>
</body>
</html>

View File

@ -4,11 +4,14 @@ import { execSync } from "node:child_process";
import { Server } from "socket.io";
import { io } from "socket.io-client";
import { build_html } from "./har-analyzer/build_html.js"
import { build_html } from "har-analyzer"
import fileUpload from "express-fileupload";
const app = express();
import multer from "multer"
const upload = multer();
async function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time));
@ -60,14 +63,15 @@ app.get("/", async function (req, res) {
//POST
app.use(express.text({limit: "100mb"}));
app.post("/inspect_har", function (req, res) {
let body = JSON.parse(req.body);
let har = JSON.stringify(body.har);
app.post("/inspect_har", upload.none(), function (req, res) {
let body = req.body;
let har = body.har;
let private_data;
if (body.private_data)
private_data = body.private_data;
private_data = JSON.parse(body.private_data);
res.setHeader("Content-Type", "text/html");
res.send(build_html(har, private_data, "/code/har-analyzer/"));
console.log(private_data);
res.send(build_html(har, private_data));
});
app.use(fileUpload());

View File

@ -8,7 +8,9 @@
"@types/har-format": "^1.2.16",
"express": "^4.18.2",
"express-fileupload": "^1.5.1",
"har-analyzer": "git+ssh://git@hub.sealcode.org/diffusion/171/har-parser.git#master",
"htmx.org": "^1.9.12",
"multer": "^2.0.2",
"preact": "^10.18.1",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
@ -370,6 +372,18 @@
"node": ">=12"
}
},
"node_modules/@smithy/util-hex-encoding": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz",
"integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@ -412,11 +426,37 @@
"node": ">= 0.6"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
@ -449,6 +489,12 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@ -480,6 +526,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -857,6 +918,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/har-analyzer": {
"name": "module-starter",
"version": "0.0.1",
"resolved": "git+ssh://git@hub.sealcode.org/diffusion/171/har-parser.git#167d6bce983a30ac9699474c26ec6b0222a7309e",
"license": "ISC",
"dependencies": {
"@smithy/util-hex-encoding": "^4.0.0",
"base64-js": "^1.5.1",
"pako": "^2.1.0",
"tabulator-tables": "^6.3.1"
}
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@ -986,11 +1059,50 @@
"node": ">= 0.6"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/multer": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
"integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"mkdirp": "^0.5.6",
"object-assign": "^4.1.1",
"type-is": "^1.6.18",
"xtend": "^4.0.2"
},
"engines": {
"node": ">= 10.16.0"
}
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -1027,6 +1139,12 @@
"node": ">= 0.8"
}
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -1097,6 +1215,20 @@
"node": ">= 0.8"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -1366,6 +1498,21 @@
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/tabulator-tables": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/tabulator-tables/-/tabulator-tables-6.3.1.tgz",
"integrity": "sha512-qFW7kfadtcaISQIibKAIy0f3eeIXUVi8242Vly1iJfMD79kfEGzfczNuPBN/80hDxHzQJXYbmJ8VipI40hQtfA==",
"license": "MIT"
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -1374,6 +1521,12 @@
"node": ">=0.6"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -1386,6 +1539,12 @@
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@ -1400,6 +1559,12 @@
"node": ">= 0.8"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -1443,6 +1608,15 @@
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
}
}
}

View File

@ -1,16 +1,18 @@
{
"scripts": {
"build": "esbuild --sourcemap --bundle src/trafficLog.tsx src/notifications.jsx --outdir=dist/ --jsx-factory=h --jsx-fragment=Fragment"
"build": "esbuild --format=esm --sourcemap --bundle src/main.ts --outdir=dist/ --jsx-factory=h --jsx-fragment=Fragment"
},
"dependencies": {
"@types/har-format": "^1.2.16",
"express": "^4.18.2",
"express-fileupload": "^1.5.1",
"htmx.org": "^1.9.12",
"multer": "^2.0.2",
"preact": "^10.18.1",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"ws": "^8.18.0"
"ws": "^8.18.0",
"har-analyzer": "git+ssh://git@hub.sealcode.org/diffusion/171/har-parser.git#master"
},
"devDependencies": {
"esbuild": "^0.19.5"

View File

@ -0,0 +1,232 @@
// make the export accessible from inline js
import * as main from "./main";
// for some reason doing the same with the window object doesn't work
(globalThis as any).main = main;
import {
backButton,
clicksLog,
homeButton,
socket,
screen,
adid_priv_info_table,
lat_priv_info_table,
lon_priv_info_table,
recentButton,
} from "./shared";
import { start_notifications } from "./notifications";
import { start_traffic_log } from "./traffic_log";
export { download_har, inspect_har } from "./traffic_log";
export function reset_adid_handler(_: Event) {
socket.emit("reset_adid");
}
export function coords_handler(e: FormDataEvent) {
e.preventDefault();
const form_data = new FormData(e.target as HTMLFormElement);
socket.emit("setcoord", {
lon: Number.parseFloat(form_data.get("lon") as string),
lat: Number.parseFloat(form_data.get("lat") as string),
});
}
export function open_tab(evt: Event, tab_name: string) {
let i, tabcontent, tablinks;
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
if (tabcontent[i].id != tab_name) {
tabcontent[i].classList.remove("active");
} else {
tabcontent[i].classList.add("active");
}
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].classList.remove("active");
}
// Show the current tab, and add an "active" class to the button that opened the tab
(evt.currentTarget as HTMLElement).classList.add("active");
}
let lastTouch = new Date().getTime();
export const calculateElapsedTime = () => {
const currentTouch = new Date().getTime();
const elapsedTime = currentTouch - lastTouch;
const elapsedSec = Math.round(elapsedTime / 1000);
lastTouch = currentTouch;
return elapsedSec;
};
export const waitToLog = (clickInfoText: string) => {
const clickInfo = document.createElement("span");
const waitInfo = document.createElement("span");
waitInfo.textContent = `await wait(${calculateElapsedTime()});`;
clicksLog.appendChild(waitInfo);
clickInfo.textContent = clickInfoText;
clicksLog.appendChild(clickInfo);
};
export const registerClick = ({
path,
logText,
body,
}: {
path: string;
logText: string;
body?: any;
}) => {
waitToLog(logText);
socket.emit(path, body ? body : {});
};
homeButton.addEventListener("click", () =>
registerClick({ path: "home", logText: "await homeButton();" })
);
backButton.addEventListener("click", () =>
registerClick({ path: "back", logText: "await backButton();" })
);
recentButton.addEventListener("click", () =>
registerClick({ path: "recent", logText: "await recentButton();" })
);
socket.on("screenshot_data", (data) => {
try {
const blob = new Blob([data]);
screen.src = URL.createObjectURL(blob);
} catch (error) {
console.error("Error fetching image: ", error);
}
});
socket.on("private_info", (data) => {
console.log("private_info");
adid_priv_info_table.textContent = data.adid;
lat_priv_info_table.textContent = data.latitude;
lon_priv_info_table.textContent = data.longitude;
});
socket.emit("private_info_req");
socket.onAny((ev, ...args) => {
console.log("ev: ", ev, args);
});
async function displayImage() {
socket.emit("screenshot");
}
let isDragging = false;
const screenSize = [320, 640];
export function calcMousePos(event: MouseEvent) {
let rect = screen.getBoundingClientRect();
let x = ((event.clientX - rect.left) / rect.width) * screenSize[0];
let y = ((event.clientY - rect.top) / rect.height) * screenSize[1];
x = Math.min(Math.max(x, 0), screenSize[0]);
y = Math.min(Math.max(y, 0), screenSize[1]);
return { x, y };
}
screen.addEventListener(
"mousemove",
(event) => {
if (!isDragging) return;
let pos = calcMousePos(event);
if (isDragging) {
registerClick({
path: "motionevent",
logText: `await motionevent({motionType: "MOVE", x:${pos.x},y:${pos.y}}});`,
body: {
motionType: "MOVE",
x: pos.x,
y: pos.y,
},
});
}
},
false
);
export const handleDraggStart = (event: MouseEvent) => {
isDragging = true;
let pos = calcMousePos(event);
registerClick({
path: "motionevent",
logText: `await motionevent({motionType: "DOWN", x:${pos.x},y:${pos.y}}});`,
body: {
motionType: "DOWN",
x: pos.x,
y: pos.y,
},
});
};
screen.addEventListener("mousedown", handleDraggStart);
document.addEventListener("mouseup", (e) => {
if (!isDragging) return;
isDragging = false;
let pos = calcMousePos(e);
registerClick({
path: "motionevent",
logText: `await motionevent({motionType: "MOVE", x:${pos.x},y:${pos.y}}});`,
body: {
motionType: "MOVE",
x: pos.x,
y: pos.y,
},
});
registerClick({
path: "motionevent",
logText: `await motionevent({motionType: "UP", x:${pos.x},y:${pos.y}}});`,
body: {
motionType: "UP",
x: pos.x,
y: pos.y,
},
});
});
window.addEventListener("keydown", (event) => {
let key = event.key;
if (key === "Space") key = " ";
else if (key !== "Enter" && key !== "Backspace" && key.length !== 1) return;
console.log(event.key, key);
if (screen.matches(":hover")) {
registerClick({
path: "key",
logText: `await key(${event.key});`,
body: { key },
});
}
});
export async function sleep(time: number) {
return new Promise((resolve) => setTimeout(resolve, time));
}
async function screenshot_loop() {
var before;
while (true) {
before = performance.now();
await displayImage();
// TODO: Make this dynamic again
while (performance.now() - before < 100) await sleep(50);
}
}
screenshot_loop();
start_notifications();
start_traffic_log();

View File

@ -1,67 +0,0 @@
import { render, Component } from "preact";
import { io } from "socket.io-client";
function rand_num() {
return Math.floor(Math.random() * Number.MAX_VALUE);
}
class Notifications extends Component {
constructor() {
super();
this.state = { notifications: [] };
}
remove_notification(id) {
const newNotifications = this.state.notifications.filter(
(notification) => notification.id !== id
);
this.setState({ notifications: newNotifications });
}
componentDidMount() {
// This should also be dynamic
io().on("notification", (data) => {
let new_id = rand_num();
this.setState({
notifications: [
{ id: new_id, notification: data },
...this.state.notifications,
],
});
// a 10 sec timeout
setTimeout(() => {
this.remove_notification(new_id);
}, 10000);
});
}
render() {
return this.state.notifications.map(({ id, notification }) => (
<div
onClick={() => this.remove_notification(id)}
style={`
background-color: ${notification.is_ok ? "#66ff99" : "#ff5c33"};
border-radius: 5px;
border-width: 2px;
border-style: solid;
border-color: ${notification.is_ok ? "#369648" : "#a23915"};
padding: 5px;
margin-top: 2px;
margin-bottom: 2px;
`}
>
<div>
<b>{notification.context}</b>
</div>
<div>
{notification.message.split("\n").map((line) => (
<p>{line}</p>
))}
</div>
</div>
));
}
}
render(<Notifications />, document.getElementById("notifications"));

View File

@ -0,0 +1,41 @@
import * as shared from "./shared";
const notifications = document.getElementById("notifications")!;
function create_notification(notification: {
is_ok: boolean;
message: string;
context: string;
}): HTMLElement {
const el = document.createElement("div");
el.addEventListener("click", (_) => {
el.remove();
});
el.classList.add("notification");
if (notification.is_ok) el.classList.add("notification_ok");
else el.classList.add("notification_err");
// el.innerText = notification.message;
el.innerHTML = `
<div><b>${notification.context}</b></div>
<div>
${notification.message
.split("\n")
.map((line) => `<p>${line}</p>`)
.join("")}
</div>`;
return el;
}
export function start_notifications() {
shared.socket.on("notification", (data) => {
let el = create_notification(data);
notifications.appendChild(el);
// a 10 sec timeout
setTimeout(() => {
el.remove();
}, 10000);
});
}

View File

@ -0,0 +1,15 @@
import { io } from "socket.io-client";
export const screen = document.getElementById("screen")! as HTMLImageElement;
export const clicksLog = document.getElementById("clicks-log")!;
export const recentButton = document.querySelector(".screen-buttons-recent-apps")!;
export const homeButton = document.querySelector(".screen-buttons-home")!;
export const backButton = document.querySelector(".screen-buttons-back")!;
export const adid_priv_info_table = document.getElementById("adid_priv_info_table")!;
export const lat_priv_info_table = document.getElementById("lat_priv_info_table")!;
export const lon_priv_info_table = document.getElementById("lon_priv_info_table")!;
export const socket = io();

View File

@ -1,253 +0,0 @@
import { Entry, Har, PostData, Request, Response } from "har-format";
import { render, Component } from "preact";
type MyState = {
finished_entries: Entry[];
unfinished_entries: Map<string, Entry>;
};
class TrafficLog extends Component {
connection: WebSocket | undefined;
state: MyState = { finished_entries: [], unfinished_entries: new Map() };
constructor() {
super();
}
componentDidMount() {
// This should also be dynamic
this.connection = new WebSocket("ws://localhost:10001");
this.connection.onmessage = (msg) => {
this.process_msg(msg.data);
this.setState({
finished_entries: this.state.finished_entries,
unfinished_entries: this.state.unfinished_entries,
});
};
this.connection.onclose = this.connection.onerror = () => {
window.location.reload();
};
}
render() {
const download_har = () => {
var tempLink = document.createElement("a");
var taBlob = new Blob([JSON.stringify(this.export_har())], {
type: "text/plain",
});
tempLink.setAttribute("href", URL.createObjectURL(taBlob));
tempLink.setAttribute("download", `rentgendroid-capture.har`);
tempLink.click();
URL.revokeObjectURL(tempLink.href);
};
const inspect_har = async () => {
const req_body = {
har: this.export_har(),
private_data: [
[
"adid",
document.getElementById("adid_priv_info_table")!
.textContent,
],
[
"latitude",
document.getElementById("lat_priv_info_table")!
.textContent,
],
[
"longitude",
document.getElementById("lon_priv_info_table")!
.textContent,
],
],
};
const resp = await fetch("/inspect_har", {
method: "POST",
body: JSON.stringify(req_body),
});
const resp_text = await resp.text();
const newWindow = window.open();
newWindow?.document.write(resp_text);
newWindow?.document.close();
};
const contentWithLineBreaks = this.state.finished_entries.map((req) => {
return (
<span>
{req.request.url}
<br />
</span>
);
});
return (
<div>
<button onClick={download_har}>Download HAR</button>
<button onClick={inspect_har}>Inspect HAR</button>
<div>
<h2>stats: </h2>
<p>
Request + responce pairs:{" "}
{this.state.finished_entries.length}
</p>
<p>
Waiting for the responce:{" "}
{this.state.unfinished_entries.size}
</p>
</div>
<div>{contentWithLineBreaks}</div>
</div>
);
}
process_msg(s: string) {
let obj = JSON.parse(s);
console.log(obj);
if (obj.type !== "data") return;
if (obj.payload && obj.payload.data && obj.payload.data.requestReceived)
this.process_req(obj.payload.data.requestReceived);
if (
obj.payload &&
obj.payload.data &&
obj.payload.data.responseCompleted
)
this.process_res(obj.payload.data.responseCompleted);
}
process_res(res: any) {
let entry = this.state.unfinished_entries.get(res.id)!;
let content_type = "application/text";
let headers = JSON.parse(res.rawHeaders).map(
(header: [string, string]) => {
if (header[0].toLowerCase() === "content-type")
content_type = header[1];
return { name: header[0], value: header[1], comment: "" };
}
);
//'{"startTime":1751745139334,
// "startTimestamp":347666.762487,
// "bodyReceivedTimestamp":347667.529477,
// "headersSentTimestamp":347906.038202,
// "responseSentTimestamp":347906.616067}'
let timing_events = JSON.parse(res.timingEvents);
let start_ts = timing_events.startTimestamp;
let got_headers_ts = timing_events.headersSentTimestamp;
let end_ts = timing_events.responseSentTimestamp;
let wait_time = got_headers_ts - start_ts;
let recieve_time = end_ts - got_headers_ts;
let response: Response = {
status: res.statusCode,
statusText: res.statusMessage,
httpVersion: entry.request.httpVersion,
cookies: [],
headers,
content: {
size: 0,
mimeType: content_type,
text: res.body,
encoding: "base64",
},
redirectURL: "",
headersSize: -1,
bodySize: -1,
};
entry.response = response;
entry.timings.wait = wait_time;
entry.timings.receive = recieve_time;
this.state.unfinished_entries.delete(res.id);
this.state.finished_entries.push(entry);
}
process_req(req: any) {
let content_type = "application/text";
let headers = JSON.parse(req.rawHeaders).map(
(header: [string, string]) => {
if (header[0].toLowerCase() === "Content-Type")
content_type = header[1];
return { name: header[0], value: header[1], comment: "" };
}
);
let timing_events = JSON.parse(req.timingEvents);
let start_time: number = timing_events.startTime!;
let start_datetime = new Date(start_time).toISOString();
let request: Request = {
method: req.method,
url: req.url,
httpVersion: req.httpVersion,
cookies: [],
headers,
queryString: [],
postData: req.body
? ({ text: req.body, mimeType: content_type } as PostData)
: undefined,
headersSize: -1,
bodySize: -1,
comment: "",
};
//'{"startTime":1751745139334,"startTimestamp":347666.762487,"bodyReceivedTimestamp":347667.529477,"headersSentTimestamp":347906.038202,"responseSentTimestamp":347906.616067}'
let entry: Entry = {
startedDateTime: start_datetime,
time: 0,
request: request,
response: {
status: 0,
statusText: "",
httpVersion: "",
cookies: [],
headers: [],
content: {
size: 0,
mimeType: "",
},
redirectURL: "",
headersSize: 0,
bodySize: 0,
},
cache: {},
timings: {
wait: 0,
receive: 0,
},
};
this.state.unfinished_entries.set(req.id, entry);
}
export_har(): Har {
let ret: Har = {
log: {
version: "1.2",
creator: {
name: "Rentgendroid",
version: "0.0.1",
},
entries: [
...this.state.finished_entries,
...this.state.unfinished_entries.values(),
],
},
};
return ret;
}
}
render(<TrafficLog />, document.getElementById("traffic-log")!);

View File

@ -0,0 +1,224 @@
import { Entry, Har, PostData, Request, Response } from "har-format";
import {
adid_priv_info_table,
lat_priv_info_table,
lon_priv_info_table,
} from "./shared";
// Request + responce pairs: 0
const req_res_pairs = document.getElementById("traffic-log-req-res-pairs")!;
// Waiting for the responce: 0
const waiting = document.getElementById("traffic-log-waiting")!;
const traffic_log_lines = document.getElementById("traffic-log-lines")!;
const inspect_har_form: HTMLFormElement = document.getElementById(
"inspect-har-form"
)! as HTMLFormElement;
const connection = new WebSocket("ws://localhost:10001");
const unfinished_entries: Map<string, Entry> = new Map();
const finished_entries: Entry[] = [];
export function start_traffic_log() {
update_stats();
connection.onmessage = (msg) => {
process_msg(msg.data);
update_stats();
};
connection.onclose = connection.onerror = () => {
window.location.reload();
};
}
export function download_har() {
var tempLink = document.createElement("a");
var taBlob = new Blob([JSON.stringify(export_har())], {
type: "text/plain",
});
tempLink.setAttribute("href", URL.createObjectURL(taBlob));
tempLink.setAttribute("download", `rentgendroid-capture.har`);
tempLink.click();
URL.revokeObjectURL(tempLink.href);
}
function launch_window_with_post(url: string, data: any) {
let form = document.createElement("form");
form.target = "_blank";
form.method = "POST";
form.action = url;
form.enctype = "multipart/form-data"
form.style.display = "none";
for (var key in data) {
var input = document.createElement("input");
input.type = "hidden";
input.name = key;
input.value = data[key];
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
}
export async function inspect_har() {
let data = {
har: JSON.stringify(export_har()),
private_data: JSON.stringify([
{desc: "adid", data: adid_priv_info_table.textContent},
{desc: "latitude", data: lat_priv_info_table.textContent },
{desc: "longitude", data: lon_priv_info_table.textContent },
]),
};
launch_window_with_post("/inspect_har", data);
}
function update_stats() {
req_res_pairs.innerText = `Request + responce pairs: ${finished_entries.length}`;
waiting.innerText = `Waiting for the responce: ${unfinished_entries.size}`;
}
function process_msg(s: string) {
let obj = JSON.parse(s);
console.log(obj);
if (obj.type !== "data") return;
if (obj.payload && obj.payload.data && obj.payload.data.requestReceived)
process_req(obj.payload.data.requestReceived);
if (obj.payload && obj.payload.data && obj.payload.data.responseCompleted)
process_res(obj.payload.data.responseCompleted);
}
function process_res(res: any) {
let entry = unfinished_entries.get(res.id)!;
let content_type = "application/text";
let headers = JSON.parse(res.rawHeaders).map((header: [string, string]) => {
if (header[0].toLowerCase() === "content-type")
content_type = header[1];
return { name: header[0], value: header[1], comment: "" };
});
//'{"startTime":1751745139334,
// "startTimestamp":347666.762487,
// "bodyReceivedTimestamp":347667.529477,
// "headersSentTimestamp":347906.038202,
// "responseSentTimestamp":347906.616067}'
let timing_events = JSON.parse(res.timingEvents);
let start_ts = timing_events.startTimestamp;
let got_headers_ts = timing_events.headersSentTimestamp;
let end_ts = timing_events.responseSentTimestamp;
let wait_time = got_headers_ts - start_ts;
let recieve_time = end_ts - got_headers_ts;
let response: Response = {
status: res.statusCode,
statusText: res.statusMessage,
httpVersion: entry.request.httpVersion,
cookies: [],
headers,
content: {
size: 0,
mimeType: content_type,
text: res.body,
encoding: "base64",
},
redirectURL: "",
headersSize: -1,
bodySize: -1,
};
entry.response = response;
entry.timings.wait = wait_time;
entry.timings.receive = recieve_time;
unfinished_entries.delete(res.id);
finished_entries.push(entry);
let el = document.createElement("span");
el.innerHTML = `
${entry.request.url}
<br />`;
traffic_log_lines.appendChild(el);
}
function process_req(req: any) {
let content_type = "application/text";
let headers = JSON.parse(req.rawHeaders).map((header: [string, string]) => {
if (header[0].toLowerCase() === "Content-Type")
content_type = header[1];
return { name: header[0], value: header[1], comment: "" };
});
let timing_events = JSON.parse(req.timingEvents);
let start_time: number = timing_events.startTime!;
let start_datetime = new Date(start_time).toISOString();
let request: Request = {
method: req.method,
url: req.url,
httpVersion: req.httpVersion,
cookies: [],
headers,
queryString: [],
postData: req.body
? ({ text: req.body, mimeType: content_type } as PostData)
: undefined,
headersSize: -1,
bodySize: -1,
comment: "",
};
//'{"startTime":1751745139334,"startTimestamp":347666.762487,"bodyReceivedTimestamp":347667.529477,"headersSentTimestamp":347906.038202,"responseSentTimestamp":347906.616067}'
let entry: Entry = {
startedDateTime: start_datetime,
time: 0,
request: request,
response: {
status: 0,
statusText: "",
httpVersion: "",
cookies: [],
headers: [],
content: {
size: 0,
mimeType: "",
},
redirectURL: "",
headersSize: 0,
bodySize: 0,
},
cache: {},
timings: {
wait: 0,
receive: 0,
},
};
unfinished_entries.set(req.id, entry);
}
function export_har(): Har {
let ret: Har = {
log: {
version: "1.2",
creator: {
name: "Rentgendroid",
version: "0.0.1",
},
entries: [...finished_entries, ...unfinished_entries.values()],
},
};
return ret;
}

167
http_server/code/styles.css Normal file
View File

@ -0,0 +1,167 @@
main {
display: flex;
}
.log-section {
height: auto;
width: 400px;
overflow: auto;
display: flex;
flex-direction: column;
margin-left: 20px;
}
.screen {
display: inline-block;
cursor: pointer;
}
.screen-buttons {
display: flex;
justify-content: space-around;
margin-top: 5px;
gap: 10px;
}
.screen-buttons button {
font-size: 1.1rem;
padding: 10px 20px;
width: 100%;
cursor: pointer;
background-color: transparent;
}
.screen-buttons button:hover {
background-color: aqua;
}
#clicks-log {
font-family:
Menlo,
Consolas,
Monaco,
Liberation Mono,
Lucida Console,
monospace;
}
.tab {
border: 1px solid #ccc;
background-color: #f1f1f1;
}
/* Style the buttons that are used to open the tab content */
.tab button {
background-color: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
}
/* Change background color of buttons on hover */
.tab button:hover {
background-color: #ddd;
}
/* Create an active/current tablink class */
.tab button.active {
background-color: #ccc;
}
.tabcontent.active {
display: flex;
flex-direction: column;
flex-grow: 1;
}
/* Style the tab content */
.tabcontent {
display: none;
padding: 6px 12px;
border: 1px solid #ccc;
border-top: none;
}
html,
body,
main {
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
}
main {
display: flex;
flex-direction: row;
align-items: stretch;
}
#logs-tab {
overflow: auto;
text-wrap: wrap;
}
.screen-section {
display: flex;
flex-direction: column;
}
.screen-section #screen,
.screen-section.screen_buttons {
flex-grow: 0;
}
#screen {
user-select: none;
}
.tab-section {
display: flex;
flex-direction: column;
flex-grow: 1;
}
#resp {
display: none;
}
#upload_form {
display: flex;
flex-direction: column;
}
#upload_form button,
#upload_form label {
border: 2px solid #ccc;
background-color: #f1f1f1;
cursor: pointer;
padding: 3px 10px;
transition: 0.3s;
}
#upload_form button:hover,
#upload_form label:hover {
background-color: #ddd;
}
#notifications {
width: 40%;
margin-left: 60%;
position: absolute;
}
.notification {
border-radius: 5px;
border-width: 2px;
border-style: solid;
padding: 5px;
margin-top: 2px;
margin-bottom: 2px;
}
.notification_ok {
background-color: #66ff99;
border-color: #369648;
}
.notification_err {
background-color: #ff5c33;
border-color: #a23915;
}
#logs {
display: flex;
flex-direction: row;
}

View File

@ -0,0 +1,15 @@
<script type="text/javascript">
function randomlinks(){
var myrandom=Math.round(Math.random()*3)
var links=new Array()
links[0]="https://www.DemiCardGame.com"
links[1]="https://www.games.xyphienllc.com"
links[2]="https://www.htcg.news"
links[3]="https://www.eTableCon.com"
window.open(links[myrandom], '_blank');
}
</script>
<form>
<input type="button" value="random link!" onClick="randomlinks()">
</form>

View File

@ -1,12 +1,15 @@
FROM node:20.18.1
# Set up node
RUN npm install -g n && n install 22.14.0 && n use 22.14.0 && hash -r
# If iproute2 is not installed,
# node can't figure out what address to bind 0.0.0.0 to
RUN apt-get update && apt-get install iproute2 python3 --yes \
&& git clone https://github.com/httptoolkit/httptoolkit-ui
WORKDIR httptoolkit-ui
RUN git checkout a0dbb8e1cd38b346fe411b03de0c5191ff06c669 \
RUN git checkout ac44f8e6e1f5f41be988a32eb89c98f57723d005 \
&& npm i && npm run server:setup
ARG DOCKER_HOST=localhost

View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">