ghostwire 1.0.0 → 2.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/bin/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import chalk from "chalk";
3
3
  import startServer from "../src/handler/server.js";
4
4
  import startClient from "../src/handler/client.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghostwire",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Terminal Chat with File Sharing",
5
5
  "main": "bin/index.js",
6
6
  "bin": {
@@ -1,10 +1,10 @@
1
1
  import net from 'net';
2
2
  import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import path from 'path';
3
5
  import showBanner from '../lib/banner.js';
4
6
 
5
7
  export default function startClient(address) {
6
- // Input might be "0.tcp.ngrok.io:12345" OR "localhost:3000"
7
- // We strip the "tcp://" prefix if the user copied it directly from Ngrok
8
8
  const cleanAddress = address.replace('tcp://', '');
9
9
  const parts = cleanAddress.split(':');
10
10
 
@@ -61,13 +61,25 @@ export default function startClient(address) {
61
61
  }
62
62
 
63
63
  if (input) {
64
+ // --- FILE SHARING (SIDECAR) ---
65
+ if (input.startsWith('/share ')) {
66
+ const filePath = input.split('/share ')[1]?.trim();
67
+ handleFileUpload(filePath, HOST, PORT);
68
+ }
69
+ // --- FILE DOWNLOADING (SIDECAR) ---
70
+ else if (input.startsWith('/download ')) {
71
+ const fileName = input.split('/download ')[1]?.trim();
72
+ handleFileDownload(fileName, HOST, PORT);
73
+ }
74
+ // --- NORMAL CHAT ---
75
+ else {
76
+ const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toLowerCase();
77
+ console.log(`${chalk.blue('┌──(')}${chalk.green.bold('YOU@ghostwire')}${chalk.blue(')-[')}${chalk.white(time)}${chalk.blue(']')}`);
78
+ console.log(`${chalk.blue('└─#')} ${chalk.white(input)}`);
64
79
 
65
- const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toLowerCase();
66
- console.log(`${chalk.blue('┌──(')}${chalk.green.bold('YOU@ghostwire')}${chalk.blue(')-[')}${chalk.white(time)}${chalk.blue(']')}`);
67
- console.log(`${chalk.blue('└─#')} ${chalk.white(input)}`);
68
-
69
- // Send to server
70
- client.write(input);
80
+ // Send to server
81
+ client.write(input);
82
+ }
71
83
  }
72
84
 
73
85
  // Draw new prompt
@@ -82,4 +94,80 @@ export default function startClient(address) {
82
94
  client.on('error', (err) => {
83
95
  console.log(chalk.red(`\n❌ Error: ${err.message}`));
84
96
  });
85
- };
97
+ };
98
+
99
+ // --- HELPER FUNCTIONS ---
100
+
101
+ function handleFileUpload(filePath, host, port) {
102
+ if (!filePath) {
103
+ console.log(chalk.red('Usage: /share <path_to_file>'));
104
+ return;
105
+ }
106
+
107
+ // Check if file exists
108
+ // Resolve path relative to current working directory of the user
109
+ const absolutePath = path.resolve(process.cwd(), filePath);
110
+
111
+ if (!fs.existsSync(absolutePath)) {
112
+ console.log(chalk.red(`\n ❌ File not found: ${absolutePath}`));
113
+ return;
114
+ }
115
+
116
+ const filename = path.basename(absolutePath);
117
+ console.log(chalk.cyan(`\n 🚀 Starting upload: ${filename} (Sidecar Socket)...`));
118
+
119
+ const sidecar = new net.Socket();
120
+ sidecar.connect(port, host, () => {
121
+ // Handshake
122
+ sidecar.write(`SIDECAR_UPLOAD|${filename}\n`);
123
+
124
+ const fileStream = fs.createReadStream(absolutePath);
125
+ fileStream.pipe(sidecar);
126
+ });
127
+
128
+ sidecar.on('close', () => {
129
+ // console.log(chalk.green(`\n ✅ Upload finished: ${filename}`));
130
+ // We rely on server system message or just silent finish to not clutter too much
131
+ });
132
+
133
+ sidecar.on('error', (err) => {
134
+ console.log(chalk.red(`\n ❌ Upload failed: ${err.message}`));
135
+ });
136
+ }
137
+
138
+ function handleFileDownload(filename, host, port) {
139
+ if (!filename) {
140
+ console.log(chalk.red('Usage: /download <filename>'));
141
+ return;
142
+ }
143
+
144
+ // Ensure downloads folder
145
+ const dlDir = path.join(process.cwd(), 'downloads');
146
+ if (!fs.existsSync(dlDir)) {
147
+ fs.mkdirSync(dlDir, { recursive: true });
148
+ }
149
+
150
+ const destPath = path.join(dlDir, filename);
151
+ console.log(chalk.cyan(`\n 📥 requesting download: ${filename}...`));
152
+
153
+ const sidecar = new net.Socket();
154
+ sidecar.connect(port, host, () => {
155
+ sidecar.write(`SIDECAR_DOWNLOAD|${filename}\n`);
156
+ });
157
+
158
+ const fileStream = fs.createWriteStream(destPath);
159
+ sidecar.pipe(fileStream);
160
+
161
+ fileStream.on('finish', () => {
162
+ console.log(chalk.green(`\n ✅ File saved to: ${destPath}`));
163
+ sidecar.end();
164
+ });
165
+
166
+ sidecar.on('error', (err) => {
167
+ console.log(chalk.red(`\n ❌ Sidecar Error: ${err.message}`));
168
+ });
169
+
170
+ fileStream.on('error', (err) => {
171
+ console.log(chalk.red(`\n ❌ File Write Error: ${err.message}`));
172
+ });
173
+ }
@@ -1,67 +1,173 @@
1
1
  import net from 'net';
2
2
  import SendMessage from '../lib/sendMessage.js';
3
- import express from 'express';
4
3
  import chalk from 'chalk';
5
4
  import fs from 'fs';
6
5
  import path from 'path';
7
6
  import state from '../lib/state.js';
8
7
  import showBanner from '../lib/banner.js';
9
8
 
9
+ // Ensure server storage exists
10
+ const SERVER_STORAGE = path.join(process.cwd(), 'server_storage');
11
+ if (!fs.existsSync(SERVER_STORAGE)) {
12
+ fs.mkdirSync(SERVER_STORAGE, { recursive: true });
13
+ }
14
+
10
15
  export default function startServer() {
11
- const CHAT_PORT = 8000;
12
- const HTTP_PORT = 8001;
16
+ const PORT = 3000;
17
+
13
18
  const server = net.createServer((socket) => {
14
- socket.name = `User-${state.sockets.length + 1}`; // Temporary name
15
- socket.isRegistered = false;
16
- state.sockets.push(socket);
19
+ // We listen for the first packet to determine connection type
20
+ socket.once('data', (initialData) => {
21
+ // Find newline delimiter
22
+ const newlineIndex = initialData.indexOf(10); // 0x0A is \n
17
23
 
18
- // Welcome the user and ask for name
19
- const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toLowerCase();
20
- socket.write(`${chalk.gray(`[${time}]`)} ${chalk.yellow.bold('⚡ SYSTEM:')} ${chalk.white(`Welcome to Ghostwire!`)}\n`);
24
+ let headerStr = '';
25
+ let remainingData = null;
26
+
27
+ if (newlineIndex !== -1) {
28
+ headerStr = initialData.subarray(0, newlineIndex).toString().trim();
29
+ remainingData = initialData.subarray(newlineIndex + 1);
30
+ } else {
31
+ headerStr = initialData.toString().trim();
32
+ }
33
+
34
+ // --- FILE UPLOAD SIDECAR ---
35
+ // Protocol: SIDECAR_UPLOAD|<filename>\n
36
+ if (headerStr.startsWith('SIDECAR_UPLOAD|')) {
37
+ const parts = headerStr.split('|');
38
+ const filename = parts[1];
39
+ if (!filename) {
40
+ socket.end();
41
+ return;
42
+ }
21
43
 
22
- socket.on('data', (data) => {
23
- const msg = data.toString().trim();
24
- if (!msg) return;
44
+ const safeFilename = path.basename(filename);
45
+ console.log(chalk.gray(`[FILE] Incoming upload: ${safeFilename}`));
25
46
 
26
- if (!socket.isRegistered) {
27
- socket.name = msg;
28
- socket.isRegistered = true;
47
+ const writeStream = fs.createWriteStream(path.join(SERVER_STORAGE, safeFilename));
48
+
49
+ // Write the remaining validation data if any exists in the first chunk
50
+ if (remainingData && remainingData.length > 0) {
51
+ writeStream.write(remainingData);
52
+ }
29
53
 
30
- const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toLowerCase();
31
- socket.write(`${chalk.gray(`[${time}]`)} ${chalk.yellow.bold('⚡ SYSTEM:')} ${chalk.white('Name set to')} ${chalk.cyan.bold(socket.name)}\n`);
54
+ socket.pipe(writeStream);
32
55
 
33
- SendMessage('SYSTEM', `${socket.name} joined the chat`);
56
+ writeStream.on('finish', () => {
57
+ console.log(chalk.green(`[FILE] Upload complete: ${safeFilename}`));
58
+ // Broadcast availability
59
+ SendMessage('SYSTEM', `New file shared: ${chalk.bold(safeFilename)} (Type /download ${safeFilename})`);
60
+ socket.end();
61
+ });
62
+
63
+ writeStream.on('error', (err) => {
64
+ console.error(chalk.red(`[FILE] Write error: ${err.message}`));
65
+ socket.end();
66
+ });
34
67
  return;
35
68
  }
36
69
 
37
- if (msg.startsWith('/nick ')) {
38
- const name = msg.split(' ')[1];
39
- if (name) {
40
- socket.name = name;
41
- const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toLowerCase();
42
- socket.write(`${chalk.gray(`[${time}]`)} ${chalk.yellow.bold('⚡ SYSTEM:')} ${chalk.white('Name changed to')} ${chalk.cyan.bold(name)}\n`);
70
+ // --- FILE DOWNLOAD SIDECAR ---
71
+ // Protocol: SIDECAR_DOWNLOAD|<filename>\n
72
+ if (headerStr.startsWith('SIDECAR_DOWNLOAD|')) {
73
+ const parts = headerStr.split('|');
74
+ const filename = parts[1];
75
+ if (!filename) {
76
+ socket.end();
77
+ return;
43
78
  }
44
- } else {
45
- SendMessage(socket.name, msg, socket);
79
+
80
+ const safeFilename = path.basename(filename);
81
+ const filePath = path.join(SERVER_STORAGE, safeFilename);
82
+
83
+ console.log(chalk.gray(`[FILE] download request: ${safeFilename}`));
84
+
85
+ if (fs.existsSync(filePath)) {
86
+ const readStream = fs.createReadStream(filePath);
87
+ readStream.pipe(socket);
88
+ readStream.on('error', (err) => {
89
+ console.log(chalk.red('[FILE] Read error'));
90
+ });
91
+ // Socket ends when stream ends
92
+ } else {
93
+ console.log(chalk.red(`[FILE] Not found: ${safeFilename}`));
94
+ socket.write('ERROR_NOT_FOUND');
95
+ socket.end();
96
+ }
97
+ return;
46
98
  }
47
- });
48
- // Handle Disconnects
49
- socket.on('end', () => {
50
- state.sockets = state.sockets.filter(s => s !== socket);
51
- SendMessage('SYSTEM', `${socket.name} disconnected.`);
99
+
100
+ // --- STANDARD CHAT CLIENT ---
101
+ // Treat first packet as username/registration or just init
102
+ handleChatConnection(socket, initialData);
52
103
  });
53
104
 
54
- socket.on('error', () => {
55
- state.sockets = state.sockets.filter(s => s !== socket);
56
- SendMessage('SYSTEM', `${socket.name} disconnected.`);
105
+ socket.on('error', (err) => {
106
+ // Silent handle - cleanliness
57
107
  });
58
108
  });
59
- server.listen(CHAT_PORT, () => {
109
+
110
+ server.listen(PORT, () => {
60
111
  showBanner('GHOSTWIRE', 'HOST ACTIVE');
61
112
  console.log(chalk.gray('--------------------------------------------------'));
62
- console.log(chalk.green(` ✅ TCP Chat running on Port ${CHAT_PORT}`));
113
+ console.log(chalk.green(` ✅ TCP Server (Chat + Files) running on Port ${PORT}`));
63
114
  console.log(chalk.gray('--------------------------------------------------'));
64
115
  console.log(chalk.yellow('\n ⚠ NEXT STEP: Start Ngrok ⚠'));
65
- console.log(` To start ngrok, open new terminal and run: ${chalk.cyan.bold('ngrok tcp 8000')}`);
116
+ console.log(` To start ngrok, open new terminal and run: ${chalk.cyan.bold(`ngrok tcp ${PORT}`)}`);
117
+ });
118
+ };
119
+
120
+ function handleChatConnection(socket, initialData) {
121
+ socket.name = `User-${state.sockets.length + 1}`;
122
+ socket.isRegistered = false;
123
+ state.sockets.push(socket);
124
+
125
+ const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toLowerCase();
126
+ socket.write(`${chalk.gray(`[${time}]`)} ${chalk.yellow.bold('⚡ SYSTEM:')} ${chalk.white(`Welcome to Ghostwire!`)}\n`);
127
+
128
+ // Process the initial data (likely the username request or empty)
129
+ processChatData(socket, initialData);
130
+
131
+ // Continue listening
132
+ socket.on('data', (data) => {
133
+ processChatData(socket, data);
66
134
  });
67
- };
135
+
136
+ socket.on('end', () => {
137
+ state.sockets = state.sockets.filter(s => s !== socket);
138
+ if (socket.isRegistered) {
139
+ SendMessage('SYSTEM', `${socket.name} disconnected.`);
140
+ }
141
+ });
142
+
143
+ socket.on('error', () => {
144
+ state.sockets = state.sockets.filter(s => s !== socket);
145
+ });
146
+ }
147
+
148
+ function processChatData(socket, data) {
149
+ const msg = data.toString().trim();
150
+ if (!msg) return;
151
+
152
+ if (!socket.isRegistered) {
153
+ socket.name = msg;
154
+ socket.isRegistered = true;
155
+
156
+ const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toLowerCase();
157
+ socket.write(`${chalk.gray(`[${time}]`)} ${chalk.yellow.bold('⚡ SYSTEM:')} ${chalk.white('Name set to')} ${chalk.cyan.bold(socket.name)}\n`);
158
+
159
+ SendMessage('SYSTEM', `${socket.name} joined the chat`);
160
+ return;
161
+ }
162
+
163
+ if (msg.startsWith('/nick ')) {
164
+ const name = msg.split(' ')[1];
165
+ if (name) {
166
+ socket.name = name;
167
+ const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toLowerCase();
168
+ socket.write(`${chalk.gray(`[${time}]`)} ${chalk.yellow.bold('⚡ SYSTEM:')} ${chalk.white('Name changed to')} ${chalk.cyan.bold(name)}\n`);
169
+ }
170
+ } else {
171
+ SendMessage(socket.name, msg, socket);
172
+ }
173
+ }