Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F7112654
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
19 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/android/code/index.mjs b/android/code/index.mjs
index 7ae0ad8..11531b5 100644
--- a/android/code/index.mjs
+++ b/android/code/index.mjs
@@ -1,51 +1,60 @@
import { WebSocketServer } from "ws";
import child_process from "child_process";
import fs from "fs";
+import { send_notification } from "./notifications.mjs"
async function spawnPromise(program, args) {
return new Promise((resolve, reject) => {
+ let output = "";
const process = child_process.spawn(program, args);
- process.on("close", (_) => {
- resolve();
+ process.stdout.on('data', (data) => {
+ output += data;
+ });
+ process.stderr.on('data', (data) => {
+ output += data;
+ });
+ process.on("close", (code) => {
+ resolve({output, code});
});
});
}
const wss = new WebSocketServer({ port: 3000 });
//maybe check output of child processes and send errors in some way
wss.on("connection", (ws) => {
ws.on("message", async (dataBuf) => {
let data = dataBuf.toString();
if (data === "screenshot") {
await spawnPromise("bash", ["/conf/screenshot.sh"]);
ws.send(fs.readFileSync("/screenshot.png"));
} else if (data.includes("touch")) {
const dataSplit = data.split(" ");
await spawnPromise("bash", [
"/conf/touch.sh",
dataSplit[1],
dataSplit[2],
]);
} else if (data === "back") {
await spawnPromise("bash", ["/conf/back.sh"]);
} else if (data === "home") {
await spawnPromise("bash", ["/conf/home.sh"]);
} else if (data === "install") {
- await spawnPromise("bash", ["/conf/install.sh"]);
+ const res = await spawnPromise("bash", ["/conf/install.sh"]);
+ send_notification(res.code === 0, "Installing the application", res.output);
} else if (data.includes("drag")) {
const dataSplit = data.split(" ");
await spawnPromise("bash", [
"/conf/drag.sh",
dataSplit[1],
dataSplit[2],
dataSplit[3],
dataSplit[4],
]);
}
});
ws.on("close", (_) => {
ws.close();
});
});
diff --git a/android/code/notifications.mjs b/android/code/notifications.mjs
new file mode 100644
index 0000000..200d5b2
--- /dev/null
+++ b/android/code/notifications.mjs
@@ -0,0 +1,36 @@
+import { WebSocket } from "ws";
+import { WebSocketServer } from "ws";
+
+const notification_proxy = new WebSocketServer({ port: 3001 });
+let notification_subs = [];
+
+notification_proxy.on('connection', (ws) => {
+ notification_subs.push(ws);
+});
+
+export function send_notification(is_ok, context, message)
+{
+ let updated_subs = [];
+
+ if (notification_subs.length === 0) {
+ console.log("WARNING: Got a notification, but nobody is subscribed");
+ }
+ for (const sub of notification_subs) {
+ if (sub.readyState == WebSocket.CONNECTING) {
+ console.log("WARNING: Unable to forward a notification to client that is still connecting");
+ updated_subs.push(sub);
+ } else {
+ try {
+ sub.send(JSON.stringify({
+ is_ok,
+ context,
+ message
+ }));
+ updated_subs.push(sub);
+ } catch {
+ sub.close();
+ console.log("WARNING: Fail to send a notification, closing the connection");
+ }
+ }
+ }
+}
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 661ca4b..1505ce7 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,57 +1,58 @@
services:
android:
build:
context: ./android/
args:
PROXY_PORT: "45459"
SERVER_PORT: "45456"
LOOPBACK_PORT: "10000"
MONITORING_API_PORT: "10001"
container_name: httptoolkit_server
container_name: android
sysctls:
- net.ipv6.conf.all.disable_ipv6=1
cap_add:
- NET_ADMIN
devices:
- /dev/kvm
networks:
- rent_gen_android
ports:
- 45456:45459 # This cannot change
- 45457:45459 # This cannot change
- 10001:10001 # api port
- 3000:3000 # android server port
+ - 3001:3001 # Notifications server
volumes:
- $PWD/shared_buffer:/shared_buffer
- $PWD/android/conf:/conf
- $PWD/certificates:/certificates
- $PWD/android/code:/code
httptoolkit_ui:
build:
context: ./httptoolkit_ui/
args:
# The ip / hostname using which,
# the browser can reach this docker session
DOCKER_HOST: "127.0.0.1"
container_name: httptoolkit_ui
networks:
- rent_gen_android
ports:
- 9080:9080
http_server:
build: ./http_server/
container_name: http_server
networks:
- rent_gen_android
volumes:
- $PWD/http_server/code:/code
- $PWD/shared_buffer:/shared_buffer
- $PWD/log:/log
ports:
- 8080:8080
networks:
rent_gen_android:
driver: bridge
diff --git a/http_server/code/index.html b/http_server/code/index.html
index 0780381..83ab631 100644
--- a/http_server/code/index.html
+++ b/http_server/code/index.html
@@ -1,320 +1,327 @@
<!DOCTYPE html>
<html lang="en">
<head style="height: 100vh">
<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-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;
}
.tab-section {
display: flex; flex-direction: column; flex-grow: 1;
}
<!-- TODO: A notification system -->
#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;
+ }
</style>
</head>
<body>
- <div id="resp"></div>
+ <div id="notifications"></div>
+ <div id="resp" style="display: none;"></div>
<main>
<section class="screen-section" >
<img
id="screen"
alt="android screen"
src=""
draggable="false"
class="screen"
style="flex-grow: 0"
/>
<div class="screen-buttons" style="flex-grow: 0">
<button class="screen-buttons-home">home</button>
<button class="screen-buttons-back">back</button>
</div>
<form id="upload_form" hx-post="/upload_apk" enctype="multipart/form-data" hx-target="#resp">
<label id="upload_input" for="app">Select file
<input type="file" id="app" name="app" accept=".apk" required multiple/>
</label>
<button type="submit">Install the app</button>
</form>
</section>
<div class="tab-section" >
<div class="tab">
<button class="tablinks active" onclick="open_tab(event, 'httptoolkit-tab')">HttpToolkit UI</button>
<button class="tablinks" onclick="open_tab(event, 'logs-tab')">Logs</button>
</div>
<div class="tabcontent" id="logs-tab">
<p id="clicks-log" class="log-section" ></p>
<p id="traffic-log" class="log-section"></p>
</div>
<div class="tabcontent active" id="httptoolkit-tab">
<iframe id="httptoolkit-frame" style="flex-grow: 1;" src="http://localhost:9080/" title="httptoolkit"></iframe>
</div>
</div>
</main>
<script>
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");
}
</script>
<script>
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);
fetch(path, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
...(body ? { body } : {}),
});
};
homeButton.addEventListener("click", () =>
registerClick({ path: "home", logText: "await homeButton();" })
);
backButton.addEventListener("click", () =>
registerClick({ path: "back", logText: "await backButton();" })
);
async function displayImage() {
try {
const response = await fetch("screen");
const blob = await response.blob();
screen.src = URL.createObjectURL(blob);
} catch (error) {
console.error("Error fetching image: ", error);
}
}
let isDragging = false;
let startDraggingPosX = 0;
let endDraggingPosX = 0;
let startDraggingPosY = 0;
let endDraggingPosY = 0;
const screenSize = [320, 640];
const handleDraggStart = (e) => {
e.preventDefault();
isDragging = true;
startDraggingPosX = e.offsetX;
startDraggingPosY = e.offsetY;
};
screen.addEventListener("mousedown", handleDraggStart);
document.addEventListener("mouseup", (e) => {
endDraggingPosX = e.offsetX;
endDraggingPosY = e.offsetY;
if (
isDragging &&
(Math.abs(endDraggingPosY - startDraggingPosY) > 10 ||
Math.abs(endDraggingPosX - startDraggingPosX) > 10)
) {
registerClick({
path: "drag",
logText: `await drag({x:${startDraggingPosX},y:${startDraggingPosY}},{x:${e.offsetX},y:${e.offsetY}});`,
body: `startX=${startDraggingPosX}&startY=${startDraggingPosY}&endX=${e.offsetX}&endY=${e.offsetY}`,
});
} else if (e.target === screen) {
const phoneX = event.offsetX;
const phoneY = event.offsetY;
if (phoneX <= screenSize[0] && phoneY <= screenSize[1])
registerClick({
path: "touch",
logText: `await click(${phoneX}, ${phoneY});`,
body: `x=${phoneX}&y=${phoneY}`,
});
}
isDragging = false;
});
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>
diff --git a/http_server/code/index.mjs b/http_server/code/index.mjs
index 32d82bf..1e5dc42 100644
--- a/http_server/code/index.mjs
+++ b/http_server/code/index.mjs
@@ -1,111 +1,113 @@
import express from "express";
import { readFile } from "node:fs/promises";
import {
guardedScreenshot,
android_websocket,
waitFullBoot,
} from "./screenshot.mjs";
import { execSync } from 'node:child_process';
import fileUpload from 'express-fileupload';
const device_size_x = 320;
const device_size_y = 640;
const app = express();
app.use(express.urlencoded({ extended: false }));
+app.use(express.static('/code/dist'))
+
console.log("Waiting for full boot...");
await waitFullBoot();
console.log("Boot detected! activating endpoints");
//GET
app.get("/favicon.ico", function (req, res) {
res.sendFile("/code/favicon.ico");
});
-app.get("/trafficLog.js", function (req, res) {
- res.sendFile("/code/dist/trafficLog.js");
-});
-
app.get("/htmx.js", function (req, res) {
res.sendFile("/code/node_modules/htmx.org/dist/htmx.min.js");
});
app.get("/trafficLog", async function (req, res) {
res.sendFile("/log/trafficLog");
});
app.get("/screen", async function (req, res) {
await guardedScreenshot();
res.sendFile("/code/screenshot.png");
});
app.get("/", async function (req, res) {
let fileData = (await readFile("/code/index.html")).toString();
fileData = fileData.replace(
"___screenshotDelayMs___",
process.env.screenshotDelayMs
);
res.setHeader("Content-Type", "text/html");
res.setHeader("Content-Disposition", "inline");
res.send(fileData);
});
//POST
app.post("/back", function (req, res) {
android_websocket.send(`back`);
res.sendStatus(200);
});
// default options
app.use(fileUpload());
app.post('/upload_apk', async function (req, res) {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).send('No files were uploaded.');
}
execSync("rm -rf /shared_buffer/*");
- console.log(req.files);
- for (const [idx, file] of req.files.app.entries()) {
- let uploadPath = '/shared_buffer/app' + idx + '.apk';
- await file.mv(uploadPath);
+ if (Array.isArray(req.files.app)) {
+ for (const [idx, file] of req.files.app.entries()) {
+ let uploadPath = '/shared_buffer/app' + idx + '.apk';
+ await file.mv(uploadPath);
+ }
+ } else {
+ let uploadPath = '/shared_buffer/app' + 0 + '.apk';
+ await req.files.app.mv(uploadPath);
}
android_websocket.send(`install`);
res.send('Files uploaded!');
})
app.post("/home", function (req, res) {
android_websocket.send(`home`);
res.sendStatus(200);
});
app.post("/touch", function (req, res) {
const x = parseInt(req.body.x);
const y = parseInt(req.body.y);
if (isNaN(x) || isNaN(y) || x > device_size_x || y > device_size_y) {
res.send(
`the query params must be x <= ${device_size_x}, y <= ${device_size_y}\n`
);
} else {
android_websocket.send(`touch ${x} ${y}`);
res.sendStatus(200);
}
});
app.post("/drag", function (req, res) {
const body = req.body;
const startX = Number(body.startX);
const startY = Number(body.startY);
const endX = Number(body.endX);
const endY = Number(body.endY);
android_websocket.send(`drag ${startX} ${startY} ${endX} ${endY}`);
res.sendStatus(200);
});
app.listen(8080, () => console.log("Listening in port 8080"));
diff --git a/http_server/code/package.json b/http_server/code/package.json
index ad975f7..4af3af4 100644
--- a/http_server/code/package.json
+++ b/http_server/code/package.json
@@ -1,15 +1,15 @@
{
"scripts": {
- "build": "esbuild --sourcemap --bundle src/trafficLog.jsx --outfile=dist/trafficLog.js --jsx-factory=h --jsx-fragment=Fragment"
+ "build": "esbuild --sourcemap --bundle src/trafficLog.jsx src/notifications.jsx --outdir=dist/ --jsx-factory=h --jsx-fragment=Fragment"
},
"dependencies": {
"express": "^4.18.2",
"express-fileupload": "^1.5.1",
"htmx.org": "^1.9.12",
"preact": "^10.18.1",
"ws": "^8.18.0"
},
"devDependencies": {
"esbuild": "^0.19.5"
}
}
diff --git a/http_server/code/src/notifications.jsx b/http_server/code/src/notifications.jsx
new file mode 100644
index 0000000..9ba6110
--- /dev/null
+++ b/http_server/code/src/notifications.jsx
@@ -0,0 +1,69 @@
+import { render, Component } from "preact";
+import { useState } from "preact/hooks";
+
+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
+ this.connection = new WebSocket("ws://127.0.0.1:3001");
+
+ this.connection.onmessage = (msg) => {
+ let new_id = rand_num();
+ this.setState({
+ notifications: [
+ { id: new_id, notification: JSON.parse(msg.data) },
+ ...this.state.notifications,
+ ],
+ });
+ // a 10 sec timeout
+ setTimeout(() => {
+ this.remove_notification(new_id)
+ }, 10000)
+ };
+ }
+
+ render() {
+ console.log("Render", this.state.notifications);
+ 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"));
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Jul 4, 00:14 (1 d, 7 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
799745
Default Alt Text
(19 KB)
Attached To
Mode
R134 rentgen-android
Attached
Detach File
Event Timeline
Log In to Comment