stackedge 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +215 -0
- package/bin/stackedge.js +195 -0
- package/download.jfif +0 -0
- package/lib/commands/delete.js +44 -0
- package/lib/commands/restart.js +38 -0
- package/lib/commands/stop.js +13 -0
- package/lib/config.js +11 -0
- package/lib/process.js +88 -0
- package/lib/registry.js +58 -0
- package/lib/resurrect.js +30 -0
- package/lib/tor/bootstrap.js +54 -0
- package/lib/tor/control.js +37 -0
- package/lib/tor/tor.js +58 -0
- package/lib/tui/list.js +50 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
### Stackedge
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<img src="/download.jfif" alt="Stackedge Logo" width="200"/>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
## Decentralized App Hosting on Android using Termux + Tor
|
|
8
|
+
|
|
9
|
+
Stackedge is a Termux-friendly app hosting system that allows you to run any web app (Node.js, PHP, Go, etc.) on your Android device.
|
|
10
|
+
Inspired by the desire to build something decentralized, privacy-focused, and open-source, Stackedge integrates Tor onion services to make your apps accessible anywhere safely.
|
|
11
|
+
|
|
12
|
+
Your apps start immediately, Tor bootstraps in the background, and you can manage them via a simple CLI (start, stop, restart, list, resurrect).
|
|
13
|
+
|
|
14
|
+
**Features**
|
|
15
|
+
|
|
16
|
+
- Run any app (Node.js, PHP, Go, Python…) from the folder you are in.
|
|
17
|
+
|
|
18
|
+
- Tor integration for onion services.
|
|
19
|
+
|
|
20
|
+
- Apps start immediately; Tor bootstraps in the background.
|
|
21
|
+
|
|
22
|
+
- Resilient to Wi-Fi drops or Termux restarts.
|
|
23
|
+
|
|
24
|
+
## CLI commands similar to pm2:
|
|
25
|
+
|
|
26
|
+
- stackedge start <name> -- <command>
|
|
27
|
+
|
|
28
|
+
- stackedge stop <name>
|
|
29
|
+
|
|
30
|
+
- stackedge restart <name>
|
|
31
|
+
|
|
32
|
+
- stackedge list
|
|
33
|
+
|
|
34
|
+
- stackedge resurrect
|
|
35
|
+
|
|
36
|
+
- Fallback command shows status and help.
|
|
37
|
+
|
|
38
|
+
**Open-source and focused on privacy & decentralization.**
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### Table of Contents
|
|
42
|
+
|
|
43
|
+
**Requirements**
|
|
44
|
+
|
|
45
|
+
1.Installation
|
|
46
|
+
|
|
47
|
+
2.Setup
|
|
48
|
+
|
|
49
|
+
**Usage**
|
|
50
|
+
|
|
51
|
+
1.Termux Auto-Resurrect
|
|
52
|
+
|
|
53
|
+
**Project Philosophy**
|
|
54
|
+
|
|
55
|
+
## Support
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### Requirements
|
|
60
|
+
|
|
61
|
+
Android device
|
|
62
|
+
|
|
63
|
+
Termux
|
|
64
|
+
installed
|
|
65
|
+
|
|
66
|
+
**Installed Termux packages:**
|
|
67
|
+
```
|
|
68
|
+
pkg update && pkg upgrade -y
|
|
69
|
+
pkg install nodejs git tor -y
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Optional for PHP apps:**
|
|
73
|
+
```
|
|
74
|
+
pkg install php -y
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Optional for Go apps:**
|
|
78
|
+
```
|
|
79
|
+
pkg install golang -y
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
**Optional for Python apps:**
|
|
83
|
+
```
|
|
84
|
+
pkg install python -y
|
|
85
|
+
```
|
|
86
|
+
### Installation
|
|
87
|
+
|
|
88
|
+
**Clone the Stackedge repository:**
|
|
89
|
+
```
|
|
90
|
+
cd $HOME
|
|
91
|
+
git clone https://github.com/Frost-bit-star/stackedge.git
|
|
92
|
+
cd stackedge
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
**Install globally via npm:**
|
|
97
|
+
```
|
|
98
|
+
npm install -g stackedge
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Make sure the CLI is executable:**
|
|
102
|
+
```
|
|
103
|
+
chmod +x $HOME/.npm-global/bin/stackedge
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Verify installation:**
|
|
107
|
+
```
|
|
108
|
+
stackedge
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**You should see a status summary and available commands.**
|
|
112
|
+
|
|
113
|
+
### Setup
|
|
114
|
+
|
|
115
|
+
Stackedge uses the following directory structure in Termux:
|
|
116
|
+
```
|
|
117
|
+
$HOME/.stackedge/
|
|
118
|
+
├── apps.json # Stores all app info
|
|
119
|
+
├── tor/
|
|
120
|
+
│ ├── torrc # Tor config
|
|
121
|
+
│ └── services/ # Onion services storage
|
|
122
|
+
└── logs/ # App logs
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Stackedge automatically creates these directories on first run.**
|
|
126
|
+
|
|
127
|
+
## Usage
|
|
128
|
+
Start an app
|
|
129
|
+
|
|
130
|
+
Navigate to the app folder and run:
|
|
131
|
+
```
|
|
132
|
+
cd ~/my-react-app
|
|
133
|
+
stackedge start blog -- npm start
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- blog is the app name.
|
|
137
|
+
|
|
138
|
+
- Everything after -- is the command to start your app.
|
|
139
|
+
|
|
140
|
+
**Tor starts in the background; your app starts immediately.**
|
|
141
|
+
|
|
142
|
+
### The onion address will appear once Tor is fully bootstrapped.
|
|
143
|
+
|
|
144
|
+
## Stop an app
|
|
145
|
+
- stackedge stop blog
|
|
146
|
+
|
|
147
|
+
## Restart an app
|
|
148
|
+
- stackedge restart blog
|
|
149
|
+
|
|
150
|
+
## List all apps
|
|
151
|
+
- stackedge list
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
**Shows:**
|
|
156
|
+
|
|
157
|
+
- App name
|
|
158
|
+
|
|
159
|
+
- App state (running/stopped)
|
|
160
|
+
|
|
161
|
+
- Port
|
|
162
|
+
|
|
163
|
+
- Tor state (pending/online)
|
|
164
|
+
|
|
165
|
+
- Onion address
|
|
166
|
+
|
|
167
|
+
- Resurrect all apps
|
|
168
|
+
|
|
169
|
+
**Use this to restore apps after Termux restart or Wi-Fi loss:**
|
|
170
|
+
|
|
171
|
+
- stackedge resurrect
|
|
172
|
+
|
|
173
|
+
- Termux Auto-Resurrect
|
|
174
|
+
|
|
175
|
+
**To automatically restore apps when Termux opens:**
|
|
176
|
+
```bash
|
|
177
|
+
echo 'if command -v stackedge >/dev/null; then stackedge resurrect >/dev/null 2>&1 & fi' >> ~/.bashrc
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- Apps will start immediately.
|
|
181
|
+
|
|
182
|
+
- Tor will bootstrap in the background.
|
|
183
|
+
|
|
184
|
+
- No manual intervention needed.
|
|
185
|
+
|
|
186
|
+
- Project Philosophy
|
|
187
|
+
|
|
188
|
+
## Stackedge is:
|
|
189
|
+
|
|
190
|
+
### Open-source: Learn, modify, and contribute.
|
|
191
|
+
|
|
192
|
+
- Privacy-focused: Tor integration keeps your apps secure and accessible anonymously.
|
|
193
|
+
|
|
194
|
+
- Decentralized hosting on Android: Your device becomes your own server.
|
|
195
|
+
|
|
196
|
+
- Inspired by a love for Termux and building something great, this project is for developers, hackers, and privacy enthusiasts.
|
|
197
|
+
|
|
198
|
+
Check out my other projects and tutorials:
|
|
199
|
+
[here](https://www.youtube.com/@Mr_termux-r2l)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### ☕ Support This Project
|
|
205
|
+
|
|
206
|
+
If you value open-source and anonymity, support me so I can keep building decentralized hosting tools on Android:
|
|
207
|
+
|
|
208
|
+
<a href="https://buymeacoffee.com/morganmilsn" target="_blank"> <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" height="50" alt="Buy Me A Coffee"> </a>
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
License
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
MIT License – Open source for everyone.
|
package/bin/stackedge.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require("commander");
|
|
4
|
+
const { loadApps, saveApps } = require("../lib/registry");
|
|
5
|
+
const { startProcess } = require("../lib/process");
|
|
6
|
+
const { listApps } = require("../lib/tui/list");
|
|
7
|
+
const { resurrect } = require("../lib/resurrect");
|
|
8
|
+
const { stopApp } = require("../lib/commands/stop");
|
|
9
|
+
const { restartApp } = require("../lib/commands/restart");
|
|
10
|
+
|
|
11
|
+
const { spawn } = require("child_process");
|
|
12
|
+
const net = require("net");
|
|
13
|
+
const fs = require("fs-extra");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
|
|
16
|
+
/* =========================
|
|
17
|
+
TOR CONSTANTS
|
|
18
|
+
========================= */
|
|
19
|
+
|
|
20
|
+
const TOR_BIN = "/data/data/com.termux/files/usr/bin/tor";
|
|
21
|
+
const TOR_BASE = path.join(process.env.HOME, ".tor");
|
|
22
|
+
const TORRC = path.join(TOR_BASE, "torrc");
|
|
23
|
+
const TOR_HS_DIR = path.join(TOR_BASE, "hidden");
|
|
24
|
+
const CONTROL_PORT = 9051;
|
|
25
|
+
|
|
26
|
+
/* =========================
|
|
27
|
+
UTILS
|
|
28
|
+
========================= */
|
|
29
|
+
|
|
30
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
31
|
+
|
|
32
|
+
async function getFreePort(start = 3000, end = 9000) {
|
|
33
|
+
for (let p = start; p <= end; p++) {
|
|
34
|
+
try {
|
|
35
|
+
await new Promise((res, rej) => {
|
|
36
|
+
const s = net.createServer()
|
|
37
|
+
.once("error", rej)
|
|
38
|
+
.once("listening", () => s.close(res))
|
|
39
|
+
.listen(p, "127.0.0.1");
|
|
40
|
+
});
|
|
41
|
+
return p;
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
throw new Error("No free ports");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* =========================
|
|
48
|
+
TOR MANAGEMENT
|
|
49
|
+
========================= */
|
|
50
|
+
|
|
51
|
+
async function ensureTorFilesystem() {
|
|
52
|
+
await fs.ensureDir(TOR_BASE);
|
|
53
|
+
await fs.ensureDir(TOR_HS_DIR);
|
|
54
|
+
|
|
55
|
+
await fs.chmod(TOR_BASE, 0o700);
|
|
56
|
+
await fs.chmod(TOR_HS_DIR, 0o700);
|
|
57
|
+
|
|
58
|
+
if (!await fs.pathExists(TORRC)) {
|
|
59
|
+
await fs.writeFile(TORRC, `
|
|
60
|
+
DataDirectory ${TOR_BASE}
|
|
61
|
+
ControlPort ${CONTROL_PORT}
|
|
62
|
+
CookieAuthentication 1
|
|
63
|
+
AvoidDiskWrites 1
|
|
64
|
+
Log notice stdout
|
|
65
|
+
`.trim() + "\n");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function isTorRunning() {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const socket = net.createConnection(CONTROL_PORT, "127.0.0.1");
|
|
72
|
+
socket.once("connect", () => {
|
|
73
|
+
socket.end();
|
|
74
|
+
resolve(true);
|
|
75
|
+
});
|
|
76
|
+
socket.once("error", () => resolve(false));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function startTorOnce() {
|
|
81
|
+
await ensureTorFilesystem();
|
|
82
|
+
|
|
83
|
+
if (await isTorRunning()) return;
|
|
84
|
+
|
|
85
|
+
spawn(TOR_BIN, ["-f", TORRC], {
|
|
86
|
+
detached: true,
|
|
87
|
+
stdio: "ignore",
|
|
88
|
+
}).unref();
|
|
89
|
+
|
|
90
|
+
// wait for control port
|
|
91
|
+
for (let i = 0; i < 15; i++) {
|
|
92
|
+
if (await isTorRunning()) return;
|
|
93
|
+
await sleep(1000);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw new Error("Tor failed to start");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* =========================
|
|
100
|
+
TOR CONTROL PORT
|
|
101
|
+
========================= */
|
|
102
|
+
|
|
103
|
+
async function torControl(cmd) {
|
|
104
|
+
const cookie = await fs.readFile(
|
|
105
|
+
path.join(TOR_BASE, "control_auth_cookie")
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const socket = net.createConnection(CONTROL_PORT, "127.0.0.1");
|
|
110
|
+
|
|
111
|
+
socket.on("error", reject);
|
|
112
|
+
|
|
113
|
+
socket.on("data", (data) => {
|
|
114
|
+
if (data.toString().startsWith("250")) {
|
|
115
|
+
resolve(data.toString());
|
|
116
|
+
socket.end();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
socket.once("connect", () => {
|
|
121
|
+
socket.write(`AUTHENTICATE ${cookie.toString("hex")}\r\n`);
|
|
122
|
+
socket.write(cmd + "\r\n");
|
|
123
|
+
socket.write("QUIT\r\n");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function addHiddenService(name, port) {
|
|
129
|
+
const dir = path.join(TOR_HS_DIR, name);
|
|
130
|
+
await fs.ensureDir(dir);
|
|
131
|
+
await fs.chmod(dir, 0o700);
|
|
132
|
+
|
|
133
|
+
const res = await torControl(
|
|
134
|
+
`ADD_ONION NEW:ED25519-V3 Port=${port},127.0.0.1:${port}`
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return res.match(/ServiceID=(\S+)/)[1] + ".onion";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* =========================
|
|
141
|
+
START COMMAND
|
|
142
|
+
========================= */
|
|
143
|
+
|
|
144
|
+
program.command("start <name>")
|
|
145
|
+
.allowUnknownOption(true)
|
|
146
|
+
.action(async (name) => {
|
|
147
|
+
|
|
148
|
+
const idx = process.argv.indexOf("--");
|
|
149
|
+
if (idx === -1) {
|
|
150
|
+
console.log("Usage: stackedge start <name> -- <cmd>");
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const command = process.argv.slice(idx + 1).join(" ");
|
|
155
|
+
const apps = await loadApps();
|
|
156
|
+
|
|
157
|
+
const port = process.env.PORT || await getFreePort();
|
|
158
|
+
|
|
159
|
+
await startTorOnce();
|
|
160
|
+
|
|
161
|
+
const onion = await addHiddenService(name, port);
|
|
162
|
+
|
|
163
|
+
startProcess({
|
|
164
|
+
name,
|
|
165
|
+
command,
|
|
166
|
+
cwd: process.cwd(),
|
|
167
|
+
env: { ...process.env, PORT: String(port) }
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
apps.push({
|
|
171
|
+
name,
|
|
172
|
+
port,
|
|
173
|
+
onion,
|
|
174
|
+
command,
|
|
175
|
+
appState: "online",
|
|
176
|
+
torState: "online",
|
|
177
|
+
autorestart: true
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await saveApps(apps);
|
|
181
|
+
|
|
182
|
+
console.log(`✔ ${name} running`);
|
|
183
|
+
console.log(`🌐 ${onion}`);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/* =========================
|
|
187
|
+
OTHER COMMANDS
|
|
188
|
+
========================= */
|
|
189
|
+
|
|
190
|
+
program.command("stop <name>").action(stopApp);
|
|
191
|
+
program.command("restart <name>").action(restartApp);
|
|
192
|
+
program.command("list").action(listApps);
|
|
193
|
+
program.command("resurrect").action(resurrect);
|
|
194
|
+
|
|
195
|
+
program.parse(process.argv);
|
package/download.jfif
ADDED
|
Binary file
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const { loadApps, saveApps } = require("../registry");
|
|
2
|
+
const { stopProcess } = require("../process");
|
|
3
|
+
const fs = require("fs-extra");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Delete an app from the registry and stop it if running.
|
|
8
|
+
* @param {string} name - App name to delete
|
|
9
|
+
*/
|
|
10
|
+
async function deleteApp(name) {
|
|
11
|
+
const apps = await loadApps();
|
|
12
|
+
const index = apps.findIndex(a => a.name === name);
|
|
13
|
+
|
|
14
|
+
if (index === -1) {
|
|
15
|
+
console.log(`App '${name}' not found`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const app = apps[index];
|
|
20
|
+
|
|
21
|
+
// Stop the app if running
|
|
22
|
+
if (app.pid) {
|
|
23
|
+
stopProcess(app);
|
|
24
|
+
console.log(`Stopped '${name}'`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Remove app from registry
|
|
28
|
+
apps.splice(index, 1);
|
|
29
|
+
await saveApps(apps);
|
|
30
|
+
console.log(`Deleted '${name}' from registry`);
|
|
31
|
+
|
|
32
|
+
// Optional: delete app folder if you want
|
|
33
|
+
// await fs.remove(app.cwd);
|
|
34
|
+
// console.log(`Deleted folder: ${app.cwd}`);
|
|
35
|
+
|
|
36
|
+
// Optional: delete Tor hidden service if exists
|
|
37
|
+
// const hiddenServiceDir = path.join(process.env.HOME, ".tor", "hidden_service_" + name);
|
|
38
|
+
// if (await fs.pathExists(hiddenServiceDir)) {
|
|
39
|
+
// await fs.remove(hiddenServiceDir);
|
|
40
|
+
// console.log(`Deleted Tor hidden service: ${hiddenServiceDir}`);
|
|
41
|
+
// }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { deleteApp };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { loadApps, saveApps } = require("../registry");
|
|
2
|
+
const { startProcess, stopProcess } = require("../process");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Restart an app by stopping it and starting it again.
|
|
6
|
+
* @param {string} name - App name
|
|
7
|
+
*/
|
|
8
|
+
async function restartApp(name) {
|
|
9
|
+
const apps = await loadApps();
|
|
10
|
+
const app = apps.find(a => a.name === name);
|
|
11
|
+
|
|
12
|
+
if (!app) {
|
|
13
|
+
console.log(`App '${name}' not found`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Stop the app
|
|
18
|
+
stopProcess(app);
|
|
19
|
+
|
|
20
|
+
// Reset port and state
|
|
21
|
+
app.port = null;
|
|
22
|
+
app.appState = "starting";
|
|
23
|
+
app.torState = "pending";
|
|
24
|
+
|
|
25
|
+
// Start it again
|
|
26
|
+
startProcess(app);
|
|
27
|
+
|
|
28
|
+
// Save updated apps registry
|
|
29
|
+
const index = apps.findIndex(a => a.name === name);
|
|
30
|
+
if (index !== -1) {
|
|
31
|
+
apps[index] = app;
|
|
32
|
+
await saveApps(apps);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(`Restarted '${name}' successfully`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { restartApp };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const { loadApps, saveApps } = require("../registry");
|
|
2
|
+
const { stopProcess } = require("../process");
|
|
3
|
+
|
|
4
|
+
async function stopApp(name) {
|
|
5
|
+
const apps = await loadApps();
|
|
6
|
+
const app = apps.find(a => a.name === name);
|
|
7
|
+
if (!app) return console.log("App not found");
|
|
8
|
+
stopProcess(app);
|
|
9
|
+
await saveApps(apps);
|
|
10
|
+
console.log(`Stopped ${name}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { stopApp };
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const HOME = process.env.HOME;
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
HOME,
|
|
6
|
+
BASE_DIR: path.join(HOME, ".stackedge"),
|
|
7
|
+
APPS_FILE: path.join(HOME, ".stackedge", "apps.json"),
|
|
8
|
+
LOG_DIR: path.join(HOME, ".stackedge", "logs"),
|
|
9
|
+
TOR_DIR: path.join(HOME, ".stackedge", "tor"),
|
|
10
|
+
TORRC: path.join(HOME, ".stackedge", "tor", "torrc")
|
|
11
|
+
};
|
package/lib/process.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const { spawn } = require("child_process");
|
|
2
|
+
const { loadApps, saveApps } = require("./registry");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Start an application process
|
|
6
|
+
* @param {Object} app
|
|
7
|
+
*/
|
|
8
|
+
async function startProcess(app) {
|
|
9
|
+
if (!app || !app.command) {
|
|
10
|
+
throw new Error("App command is required to start a process.");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const child = spawn("sh", ["-c", app.command], {
|
|
14
|
+
cwd: app.cwd || process.cwd(),
|
|
15
|
+
env: { ...process.env, ...app.env },
|
|
16
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
17
|
+
detached: true
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
app.pid = child.pid;
|
|
21
|
+
app.appState = "running";
|
|
22
|
+
|
|
23
|
+
// Persist state immediately
|
|
24
|
+
const apps = await loadApps();
|
|
25
|
+
const idx = apps.findIndex(a => a.name === app.name);
|
|
26
|
+
if (idx !== -1) {
|
|
27
|
+
apps[idx] = app;
|
|
28
|
+
} else {
|
|
29
|
+
apps.push(app);
|
|
30
|
+
}
|
|
31
|
+
await saveApps(apps);
|
|
32
|
+
|
|
33
|
+
// Forward logs
|
|
34
|
+
child.stdout.on("data", data => {
|
|
35
|
+
process.stdout.write(`[${app.name}] ${data}`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
child.stderr.on("data", data => {
|
|
39
|
+
process.stderr.write(`[${app.name} ERROR] ${data}`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
child.on("exit", async (code, signal) => {
|
|
43
|
+
const apps = await loadApps();
|
|
44
|
+
const idx = apps.findIndex(a => a.name === app.name);
|
|
45
|
+
if (idx !== -1) {
|
|
46
|
+
apps[idx].appState = "stopped";
|
|
47
|
+
apps[idx].pid = null;
|
|
48
|
+
await saveApps(apps);
|
|
49
|
+
}
|
|
50
|
+
console.log(`${app.name} exited (${signal || code})`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
child.unref();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Stop a running application
|
|
58
|
+
* @param {Object} app
|
|
59
|
+
*/
|
|
60
|
+
async function stopProcess(app) {
|
|
61
|
+
if (!app || !app.pid) return;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
process.kill(app.pid, "SIGTERM");
|
|
65
|
+
|
|
66
|
+
// fallback kill after 3s
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
try {
|
|
69
|
+
process.kill(app.pid, "SIGKILL");
|
|
70
|
+
} catch {}
|
|
71
|
+
}, 3000);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error(`Failed to stop ${app.name}: ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const apps = await loadApps();
|
|
77
|
+
const idx = apps.findIndex(a => a.name === app.name);
|
|
78
|
+
if (idx !== -1) {
|
|
79
|
+
apps[idx].appState = "stopped";
|
|
80
|
+
apps[idx].pid = null;
|
|
81
|
+
await saveApps(apps);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
startProcess,
|
|
87
|
+
stopProcess
|
|
88
|
+
};
|
package/lib/registry.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// lib/registry.js
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { APPS_FILE } = require("./config");
|
|
5
|
+
|
|
6
|
+
// Default fields for each app
|
|
7
|
+
const DEFAULT_APP = {
|
|
8
|
+
name: "",
|
|
9
|
+
cwd: "",
|
|
10
|
+
command: "",
|
|
11
|
+
port: null,
|
|
12
|
+
pid: null,
|
|
13
|
+
appState: "starting",
|
|
14
|
+
torState: "pending",
|
|
15
|
+
onion: null,
|
|
16
|
+
autorestart: true
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load apps from registry
|
|
21
|
+
* @returns {Promise<Array>}
|
|
22
|
+
*/
|
|
23
|
+
async function loadApps() {
|
|
24
|
+
try {
|
|
25
|
+
if (!(await fs.pathExists(APPS_FILE))) return [];
|
|
26
|
+
const apps = await fs.readJson(APPS_FILE);
|
|
27
|
+
// Ensure all apps have default fields
|
|
28
|
+
return apps.map(a => ({ ...DEFAULT_APP, ...a }));
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error("Failed to load apps registry:", err);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Save or update apps in registry
|
|
37
|
+
* Merges by app name to avoid overwriting other apps
|
|
38
|
+
* @param {Array} appsToSave
|
|
39
|
+
*/
|
|
40
|
+
async function saveApps(appsToSave) {
|
|
41
|
+
try {
|
|
42
|
+
// Ensure directory exists
|
|
43
|
+
await fs.ensureDir(path.dirname(APPS_FILE));
|
|
44
|
+
|
|
45
|
+
// Load existing apps
|
|
46
|
+
const existingApps = await loadApps();
|
|
47
|
+
|
|
48
|
+
// Merge apps by name
|
|
49
|
+
const merged = [...existingApps.filter(e => !appsToSave.some(a => a.name === e.name)), ...appsToSave];
|
|
50
|
+
|
|
51
|
+
// Write JSON to file
|
|
52
|
+
await fs.writeJson(APPS_FILE, merged, { spaces: 2 });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error("Failed to save apps registry:", err);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { loadApps, saveApps };
|
package/lib/resurrect.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const { loadApps, saveApps } = require("./registry");
|
|
2
|
+
const { startProcess } = require("./process");
|
|
3
|
+
const { ensureTorConfig, startTor } = require("./tor/tor");
|
|
4
|
+
const { waitForBootstrap } = require("./tor/bootstrap");
|
|
5
|
+
const { createOnion } = require("./tor/control");
|
|
6
|
+
|
|
7
|
+
async function resurrect() {
|
|
8
|
+
const apps = await loadApps();
|
|
9
|
+
if (!apps.length) return;
|
|
10
|
+
|
|
11
|
+
for (const app of apps) {
|
|
12
|
+
startProcess(app);
|
|
13
|
+
app.appState = "running";
|
|
14
|
+
app.torState = "pending";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
await ensureTorConfig();
|
|
18
|
+
startTor();
|
|
19
|
+
await waitForBootstrap();
|
|
20
|
+
|
|
21
|
+
for (const app of apps) {
|
|
22
|
+
if (!app.onion) app.onion = await createOnion(app.port || 8080);
|
|
23
|
+
app.torState = "online";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await saveApps(apps);
|
|
27
|
+
console.log("All apps resurrected");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { resurrect };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const net = require("net");
|
|
4
|
+
|
|
5
|
+
async function waitForBootstrap(torDir, timeout = 30000) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const cookieFile = path.join(torDir, "control_auth_cookie");
|
|
8
|
+
|
|
9
|
+
let cookieHex;
|
|
10
|
+
try {
|
|
11
|
+
cookieHex = fs.readFileSync(cookieFile).toString("hex");
|
|
12
|
+
} catch (err) {
|
|
13
|
+
return reject(new Error("Cannot read Tor cookie: " + err.message));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const socket = net.connect(9051, "127.0.0.1");
|
|
17
|
+
const start = Date.now();
|
|
18
|
+
|
|
19
|
+
socket.on("connect", () => {
|
|
20
|
+
socket.write(`AUTHENTICATE ${cookieHex}\r\n`);
|
|
21
|
+
socket.write("SETEVENTS STATUS_GENERAL\r\n");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
socket.on("data", (data) => {
|
|
25
|
+
const lines = data.toString().split("\r\n");
|
|
26
|
+
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
// Example:
|
|
29
|
+
// 650 STATUS_GENERAL BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"
|
|
30
|
+
if (line.includes("BOOTSTRAP") && line.includes("PROGRESS=100")) {
|
|
31
|
+
socket.end();
|
|
32
|
+
return resolve();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (line.startsWith("515") || line.startsWith("550")) {
|
|
36
|
+
socket.end();
|
|
37
|
+
return reject(new Error("Tor control error: " + line));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
socket.on("error", reject);
|
|
43
|
+
|
|
44
|
+
const timer = setInterval(() => {
|
|
45
|
+
if (Date.now() - start > timeout) {
|
|
46
|
+
clearInterval(timer);
|
|
47
|
+
socket.end();
|
|
48
|
+
reject(new Error("Tor bootstrap timed out"));
|
|
49
|
+
}
|
|
50
|
+
}, 1000);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { waitForBootstrap };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const net = require("net");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
async function createOnion(port, torDir) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const socket = net.connect(9051, "127.0.0.1", () => {
|
|
8
|
+
// Read cookie for authentication
|
|
9
|
+
const cookieFile = path.join(torDir, "control_auth_cookie");
|
|
10
|
+
let cookieHex = "";
|
|
11
|
+
try {
|
|
12
|
+
cookieHex = fs.readFileSync(cookieFile).toString("hex");
|
|
13
|
+
} catch (err) {
|
|
14
|
+
return reject("Cannot read Tor cookie: " + err.message);
|
|
15
|
+
}
|
|
16
|
+
socket.write(`AUTHENTICATE ${cookieHex}\r\n`);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
socket.on("data", (data) => {
|
|
20
|
+
const lines = data.toString().split("\r\n");
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
if (line.startsWith("250-ServiceID=")) {
|
|
23
|
+
const id = line.split("=")[1].trim();
|
|
24
|
+
socket.end();
|
|
25
|
+
return resolve(`${id}.onion`);
|
|
26
|
+
} else if (line.startsWith("515") || line.startsWith("550")) {
|
|
27
|
+
socket.end();
|
|
28
|
+
return reject("Tor error: " + line);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
socket.on("error", (err) => reject(err));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { createOnion };
|
package/lib/tor/tor.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
|
+
|
|
5
|
+
const TOR_BIN = "/data/data/com.termux/files/usr/bin/tor"; // Termux Tor binary
|
|
6
|
+
const TOR_DIR = path.join(process.env.HOME, ".tor");
|
|
7
|
+
const TORRC = path.join(TOR_DIR, "torrc");
|
|
8
|
+
|
|
9
|
+
async function ensureTorConfig() {
|
|
10
|
+
await fs.ensureDir(TOR_DIR);
|
|
11
|
+
const torrc = `
|
|
12
|
+
DataDirectory ${TOR_DIR}
|
|
13
|
+
ControlPort 9051
|
|
14
|
+
CookieAuthentication 1
|
|
15
|
+
AvoidDiskWrites 1
|
|
16
|
+
`.trim();
|
|
17
|
+
await fs.writeFile(TORRC, torrc);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function startTor(appName, appPort) {
|
|
21
|
+
await ensureTorConfig();
|
|
22
|
+
|
|
23
|
+
// Hidden service for this app
|
|
24
|
+
const hiddenServiceDir = path.join(TOR_DIR, `hidden_service_${appName}`);
|
|
25
|
+
await fs.ensureDir(hiddenServiceDir);
|
|
26
|
+
|
|
27
|
+
// Append hidden service config to torrc
|
|
28
|
+
const hiddenServiceConfig = `
|
|
29
|
+
HiddenServiceDir ${hiddenServiceDir}
|
|
30
|
+
HiddenServicePort ${appPort} 127.0.0.1:${appPort}
|
|
31
|
+
`.trim();
|
|
32
|
+
await fs.appendFile(TORRC, "\n" + hiddenServiceConfig);
|
|
33
|
+
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const tor = spawn(TOR_BIN, ["-f", TORRC], {
|
|
36
|
+
cwd: TOR_DIR,
|
|
37
|
+
detached: true,
|
|
38
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
tor.stdout.on("data", (data) => {
|
|
42
|
+
const text = data.toString();
|
|
43
|
+
process.stdout.write(text);
|
|
44
|
+
|
|
45
|
+
if (text.includes("Bootstrapped 100%")) {
|
|
46
|
+
// Tor fully online
|
|
47
|
+
resolve(hiddenServiceDir);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
tor.stderr.on("data", (data) => process.stderr.write(data.toString()));
|
|
52
|
+
tor.on("error", (err) => reject(err));
|
|
53
|
+
|
|
54
|
+
tor.unref();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { ensureTorConfig, startTor, TOR_DIR, TORRC };
|
package/lib/tui/list.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// lib/tui/list.js
|
|
2
|
+
|
|
3
|
+
// For Chalk v5+, handle default export
|
|
4
|
+
const chalkModule = require("chalk");
|
|
5
|
+
const chalk = chalkModule.default ? chalkModule.default : chalkModule;
|
|
6
|
+
|
|
7
|
+
const { loadApps, saveApps } = require("../registry");
|
|
8
|
+
|
|
9
|
+
// Check if a PID is alive (works on Termux / Linux / Node)
|
|
10
|
+
function isProcessAlive(pid) {
|
|
11
|
+
try {
|
|
12
|
+
process.kill(pid, 0); // signal 0 just tests for existence
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function listApps() {
|
|
20
|
+
const apps = await loadApps();
|
|
21
|
+
|
|
22
|
+
console.log(chalk.bold("NAME APP STATE PORT TOR STATE ONION"));
|
|
23
|
+
|
|
24
|
+
for (const a of apps) {
|
|
25
|
+
// Real-time check if the app is actually running
|
|
26
|
+
const alive = a.pid && isProcessAlive(a.pid);
|
|
27
|
+
const appState = alive ? (a.appState || "running") : "stopped";
|
|
28
|
+
|
|
29
|
+
console.log(
|
|
30
|
+
`${a.name.padEnd(8)} ` +
|
|
31
|
+
`${(appState || "").padEnd(11)} ` +
|
|
32
|
+
`${String(a.port || "-").padEnd(6)} ` +
|
|
33
|
+
`${(a.torState || "").padEnd(10)} ` +
|
|
34
|
+
`${a.onion || "setting up"}`
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Update registry if state has changed
|
|
38
|
+
if (appState !== a.appState) {
|
|
39
|
+
a.appState = appState;
|
|
40
|
+
const allApps = await loadApps();
|
|
41
|
+
const index = allApps.findIndex(x => x.name === a.name);
|
|
42
|
+
if (index !== -1) {
|
|
43
|
+
allApps[index] = a;
|
|
44
|
+
await saveApps(allApps);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { listApps };
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stackedge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Termux-friendly decentralized app hosting with Tor",
|
|
5
|
+
"bin": {
|
|
6
|
+
"stackedge": "bin/stackedge.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"termux",
|
|
11
|
+
"tor",
|
|
12
|
+
"cli",
|
|
13
|
+
"hosting",
|
|
14
|
+
"node",
|
|
15
|
+
"decentralized",
|
|
16
|
+
"android"
|
|
17
|
+
],
|
|
18
|
+
"author": "Mr_termux-r2l <hr@stackverify.site>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/Frost-bit-star/stackedge.git"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://www.youtube.com/@Mr_termux-r2l",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"commander": "^11.0.0",
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"fs-extra": "^11.2.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"start": "node bin/stackedge.js"
|
|
35
|
+
}
|
|
36
|
+
}
|