diff --git a/src/Projects/WebSocketCLI/cli-client.html b/src/Projects/WebSocketCLI/cli-client.html new file mode 100644 index 00000000..0c2fc7db --- /dev/null +++ b/src/Projects/WebSocketCLI/cli-client.html @@ -0,0 +1,156 @@ + + + + WebSocket CLI + + + +

WebSocket CLI Runner

+
+ + + Disconnected +
+ +
+ +
+ + + +
+ + + + diff --git a/src/Projects/WebSocketCLI/cli-client.py b/src/Projects/WebSocketCLI/cli-client.py new file mode 100644 index 00000000..dacc518a --- /dev/null +++ b/src/Projects/WebSocketCLI/cli-client.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +""" +WebSocket CLI Client +Usage: python cli-client.py [server-url] +Default: ws://localhost:8080/cli +""" + +import asyncio +import websockets +import json +import sys + +DEFAULT_URL = "ws://localhost:8080/cli" + +async def cli_client(url): + print(f"Connecting to {url}...") + + try: + async with websockets.connect(url) as ws: + print("Connected! Type commands (or 'exit' to quit)\n") + + # Task to receive messages + async def receiver(): + try: + async for message in ws: + data = json.loads(message) + msg_type = data.get('type', '') + content = data.get('data', '') + + if msg_type == 'output': + print(f" {content}") + elif msg_type == 'completed': + print(f"\n[Done]\n> ", end='', flush=True) + elif msg_type == 'error': + print(f"[ERROR] {content}") + elif msg_type == 'connected': + print(f"[Server] {content}\n> ", end='', flush=True) + else: + print(f"[{msg_type}] {content}") + except websockets.exceptions.ConnectionClosed: + print("\nConnection closed") + + # Start receiver task + recv_task = asyncio.create_task(receiver()) + + # Input loop + loop = asyncio.get_event_loop() + while True: + try: + cmd = await loop.run_in_executor(None, input, "") + + if cmd.lower() == 'exit': + print("Bye!") + break + + if cmd.strip(): + await ws.send(json.dumps({"command": cmd})) + + except EOFError: + break + + recv_task.cancel() + + except ConnectionRefusedError: + print(f"Cannot connect to {url}. Is the server running?") + except Exception as e: + print(f"Error: {e}") + +if __name__ == "__main__": + url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_URL + asyncio.run(cli_client(url)) diff --git a/src/Projects/WebSocketCLI/pom.xml b/src/Projects/WebSocketCLI/pom.xml new file mode 100644 index 00000000..0c6f566f --- /dev/null +++ b/src/Projects/WebSocketCLI/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + org.csystem + websocket-cli + 1.0.0 + WebSocket CLI Runner + WebSocket based remote CLI runner + + + 17 + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + org.springframework.boot + spring-boot-starter-web + + + + com.google.code.gson + gson + 2.10.1 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/Application.java b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/Application.java new file mode 100644 index 00000000..d2d6aba9 --- /dev/null +++ b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/Application.java @@ -0,0 +1,12 @@ +package org.csystem.app; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + System.out.println("WebSocket CLI Runner started on ws://localhost:8080/cli"); + } +} diff --git a/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/config/WebSocketConfig.java b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/config/WebSocketConfig.java new file mode 100644 index 00000000..9f2e17ae --- /dev/null +++ b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/config/WebSocketConfig.java @@ -0,0 +1,35 @@ +package org.csystem.app.config; + +import org.csystem.app.handler.CLIWebSocketHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; + +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + private final CLIWebSocketHandler cliWebSocketHandler; + + public WebSocketConfig(CLIWebSocketHandler cliWebSocketHandler) { + this.cliWebSocketHandler = cliWebSocketHandler; + } + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(cliWebSocketHandler, "/cli") + .setAllowedOrigins("*"); + } + + @Bean + public ServletServerContainerFactoryBean createWebSocketContainer() { + ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); + container.setMaxTextMessageBufferSize(65536); + container.setMaxBinaryMessageBufferSize(65536); + container.setMaxSessionIdleTimeout(600000L); + return container; + } +} diff --git a/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/handler/CLIWebSocketHandler.java b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/handler/CLIWebSocketHandler.java new file mode 100644 index 00000000..b336dc88 --- /dev/null +++ b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/handler/CLIWebSocketHandler.java @@ -0,0 +1,88 @@ +package org.csystem.app.handler; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import org.csystem.app.service.CommandExecutorService; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class CLIWebSocketHandler extends TextWebSocketHandler { + + private final CommandExecutorService commandExecutorService; + private final Gson gson = new Gson(); + private final Map sessions = new ConcurrentHashMap<>(); + + public CLIWebSocketHandler(CommandExecutorService commandExecutorService) { + this.commandExecutorService = commandExecutorService; + } + + @Override + public void afterConnectionEstablished(WebSocketSession session) { + sessions.put(session.getId(), session); + sendMessage(session, "connected", "WebSocket CLI Ready. Send commands as: {\"command\": \"your-command\"}"); + System.out.println("[+] Client connected: " + session.getId()); + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) { + try { + String payload = message.getPayload(); + JsonObject json = gson.fromJson(payload, JsonObject.class); + + if (json.has("command")) { + String command = json.get("command").getAsString(); + System.out.println("[>] Executing: " + command); + + sendMessage(session, "executing", "Running: " + command); + + commandExecutorService.executeCommand(command, line -> { + sendMessage(session, "output", line); + }).thenAccept(result -> { + sendMessage(session, "completed", result); + System.out.println("[<] Command completed"); + }); + + } else if (json.has("ping")) { + sendMessage(session, "pong", "alive"); + } else { + sendMessage(session, "error", "Unknown command. Use {\"command\": \"...\"}"); + } + + } catch (Exception e) { + sendMessage(session, "error", "Parse error: " + e.getMessage()); + } + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + sessions.remove(session.getId()); + System.out.println("[-] Client disconnected: " + session.getId()); + } + + @Override + public void handleTransportError(WebSocketSession session, Throwable exception) { + System.err.println("[!] Transport error: " + exception.getMessage()); + } + + private void sendMessage(WebSocketSession session, String type, String data) { + try { + if (session.isOpen()) { + JsonObject response = new JsonObject(); + response.addProperty("type", type); + response.addProperty("data", data); + response.addProperty("timestamp", System.currentTimeMillis()); + session.sendMessage(new TextMessage(gson.toJson(response))); + } + } catch (IOException e) { + System.err.println("[!] Failed to send message: " + e.getMessage()); + } + } +} diff --git a/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/service/CommandExecutorService.java b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/service/CommandExecutorService.java new file mode 100644 index 00000000..1b8d5245 --- /dev/null +++ b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/service/CommandExecutorService.java @@ -0,0 +1,61 @@ +package org.csystem.app.service; + +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +@Service +public class CommandExecutorService { + + private static final long TIMEOUT_SECONDS = 300; + + public CompletableFuture executeCommand(String command, Consumer outputCallback) { + return CompletableFuture.supplyAsync(() -> { + StringBuilder output = new StringBuilder(); + + try { + ProcessBuilder processBuilder = new ProcessBuilder(); + + if (System.getProperty("os.name").toLowerCase().contains("win")) { + processBuilder.command("cmd.exe", "/c", command); + } else { + processBuilder.command("sh", "-c", command); + } + + processBuilder.redirectErrorStream(true); + Process process = processBuilder.start(); + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + if (outputCallback != null) { + outputCallback.accept(line); + } + } + } + + boolean finished = process.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS); + + if (!finished) { + process.destroyForcibly(); + output.append("\n[TIMEOUT] Command exceeded ").append(TIMEOUT_SECONDS).append(" seconds"); + } + + int exitCode = process.exitValue(); + output.append("\n[EXIT CODE: ").append(exitCode).append("]"); + + } catch (Exception e) { + output.append("\n[ERROR] ").append(e.getMessage()); + } + + return output.toString(); + }); + } +} diff --git a/src/Projects/WebSocketCLI/src/main/resources/application.properties b/src/Projects/WebSocketCLI/src/main/resources/application.properties new file mode 100644 index 00000000..2c2a86a2 --- /dev/null +++ b/src/Projects/WebSocketCLI/src/main/resources/application.properties @@ -0,0 +1,6 @@ +server.port=8080 +spring.application.name=websocket-cli + +# WebSocket settings +spring.websocket.max-text-message-size=65536 +spring.websocket.max-binary-message-size=65536