mirror of
https://github.com/nicocha30/ligolo-iwa
synced 2026-06-06 16:24:27 +00:00
add CI, refactoring
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
name: Build IWA Bundle
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-iwa:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Materialize signing key
|
||||
env:
|
||||
IWA_SIGNING_KEY: ${{ secrets.IWA_SIGNING_KEY }}
|
||||
run: |
|
||||
if [ -z "$IWA_SIGNING_KEY" ]; then
|
||||
echo "::error::Missing required secret IWA_SIGNING_KEY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p keys
|
||||
printf '%s' "$IWA_SIGNING_KEY" > keys/ed25519.key
|
||||
chmod 600 keys/ed25519.key
|
||||
|
||||
- name: Build signed IWA bundle
|
||||
env:
|
||||
CI: "true"
|
||||
WEB_BUNDLE_SIGNING_PASSPHRASE: ${{ secrets.WEB_BUNDLE_SIGNING_PASSPHRASE }}
|
||||
run: make
|
||||
|
||||
- name: Upload bundle artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ligolo-iwa-bundle
|
||||
path: |
|
||||
out/ligolo-iwa.wbn
|
||||
out/ligolo-iwa.swbn
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
out/
|
||||
*.wbn
|
||||
*.swbn
|
||||
keys/*.key
|
||||
keys/*.pem
|
||||
keys/*.csr
|
||||
keys/*.cbor
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.vscode
|
||||
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
bun.lockb
|
||||
@@ -0,0 +1,100 @@
|
||||
# ==========================================
|
||||
# CONFIGURATION
|
||||
# ==========================================
|
||||
|
||||
# Directory containing compiled sources
|
||||
BUILD_DIR := dist
|
||||
|
||||
# Output directory for .wbn and .swbn files
|
||||
OUT_DIR := out
|
||||
|
||||
# Output filenames
|
||||
APP_NAME := ligolo-iwa
|
||||
UNSIGNED_BUNDLE := $(OUT_DIR)/$(APP_NAME).wbn
|
||||
SIGNED_BUNDLE := $(OUT_DIR)/$(APP_NAME).swbn
|
||||
|
||||
# Private key for signing (Ed25519)
|
||||
KEY_FILE := keys/ed25519.key
|
||||
|
||||
# Build command (e.g., npm run build, yarn build)
|
||||
BUILD_CMD := npm run build
|
||||
WBN_SIGN := npx wbn-sign
|
||||
WBN_DUMP_ID := npx wbn-dump-id
|
||||
|
||||
# ==========================================
|
||||
# TARGETS
|
||||
# ==========================================
|
||||
|
||||
.PHONY: all clean build pack sign key bundle-id help
|
||||
|
||||
# Default target: clean, build, ensure key, pack, and sign
|
||||
all: clean build key pack sign
|
||||
|
||||
# 1. Clean output directories
|
||||
clean:
|
||||
@echo "Cleaning..."
|
||||
@rm -rf $(OUT_DIR)
|
||||
@mkdir -p $(OUT_DIR)
|
||||
|
||||
# 2. Build the application
|
||||
build:
|
||||
@echo "Building source files..."
|
||||
@$(BUILD_CMD)
|
||||
@if [ ! -d "$(BUILD_DIR)" ]; then echo "Error: Directory $(BUILD_DIR) not found after build."; exit 1; fi
|
||||
|
||||
# 3. Generate private key if missing
|
||||
key:
|
||||
@if [ ! -f $(KEY_FILE) ]; then \
|
||||
if [ "$(CI)" = "true" ]; then \
|
||||
echo "Error: Signing key $(KEY_FILE) is missing in CI."; \
|
||||
echo "Provide it before running make (for example from a GitHub Actions secret)."; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo "Generating new Ed25519 private key..."; \
|
||||
openssl genpkey -algorithm Ed25519 -out $(KEY_FILE); \
|
||||
echo "Key generated: $(KEY_FILE)"; \
|
||||
echo "IMPORTANT: Keep this file safe!"; \
|
||||
else \
|
||||
echo "Using existing key: $(KEY_FILE)"; \
|
||||
fi
|
||||
|
||||
# 4. Create unsigned Web Bundle
|
||||
pack:
|
||||
@echo "Creating unsigned Web Bundle..."
|
||||
@rm -f out.wbn $(UNSIGNED_BUNDLE)
|
||||
@if command -v gen-bundle >/dev/null 2>&1; then \
|
||||
gen-bundle -dir $(BUILD_DIR) -o $(UNSIGNED_BUNDLE); \
|
||||
else \
|
||||
npx --yes wbn --dir $(BUILD_DIR); \
|
||||
mv out.wbn $(UNSIGNED_BUNDLE); \
|
||||
fi
|
||||
|
||||
# 5. Sign the bundle
|
||||
sign:
|
||||
@echo "Signing Web Bundle..."
|
||||
@if [ ! -f $(UNSIGNED_BUNDLE) ]; then echo "Error: Bundle $(UNSIGNED_BUNDLE) missing. Run 'make pack' first."; exit 1; fi
|
||||
@if [ ! -f $(KEY_FILE) ]; then echo "Error: Key $(KEY_FILE) missing."; exit 1; fi
|
||||
@$(WBN_SIGN) \
|
||||
-i $(UNSIGNED_BUNDLE) \
|
||||
-o $(SIGNED_BUNDLE) \
|
||||
-k $(KEY_FILE)
|
||||
@echo " "
|
||||
@echo "Success! Signed bundle generated:"
|
||||
@echo " $(SIGNED_BUNDLE)"
|
||||
@echo " "
|
||||
@$(MAKE) bundle-id
|
||||
|
||||
bundle-id:
|
||||
@echo "Web Bundle ID:"
|
||||
@$(WBN_DUMP_ID) -k $(KEY_FILE)
|
||||
@echo "IWA Origin:"
|
||||
@$(WBN_DUMP_ID) -k $(KEY_FILE) -s
|
||||
|
||||
# Help
|
||||
help:
|
||||
@echo "Usage:"
|
||||
@echo " make - Full build (clean -> build -> key -> pack -> sign)"
|
||||
@echo " make pack - Create unsigned bundle only"
|
||||
@echo " make sign - Sign existing bundle"
|
||||
@echo " make key - Generate private key if necessary"
|
||||
@echo " make bundle-id - Print the Web Bundle ID and isolated-app origin"
|
||||
@@ -1 +1,35 @@
|
||||
# Ligolo-IWA
|
||||
# Ligolo-IWA
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
[📑 Ligolo-IWA (Setup/Quickstart)](https://iwa.ligolo.ng/)
|
||||
[📑 Ligolo-ng Documentation (Setup/Quickstart)](https://docs.ligolo.ng/)
|
||||
|
||||
Build the signed Isolated Web App bundle locally with:
|
||||
|
||||
```sh
|
||||
make
|
||||
```
|
||||
|
||||
The signed artifact is written to `out/ligolo-iwa.swbn`.
|
||||
|
||||
## Install on Chrome
|
||||
|
||||
Chrome's current IWA testing flow uses Web App Internals:
|
||||
|
||||
1. Use Chrome 120 or later.
|
||||
2. Enable `chrome://flags/#enable-isolated-web-app-dev-mode`.
|
||||
3. Open `chrome://web-app-internals`.
|
||||
4. Choose `Install IWA from Signed Web Bundle`.
|
||||
5. Select `out/ligolo-iwa.swbn`.
|
||||
|
||||
## Install on Edge
|
||||
|
||||
Microsoft Edge is Chromium-based, and the developer installation flow is generally the same for testing:
|
||||
|
||||
1. Enable `edge://flags/#enable-isolated-web-app-dev-mode`.
|
||||
2. Open `edge://web-app-internals`.
|
||||
3. Choose `Install IWA from Signed Web Bundle`.
|
||||
4. Select `out/ligolo-iwa.swbn`.
|
||||
|
||||
Note: the Edge-specific internal URLs above are inferred from Chromium parity. If your Edge build does not expose them, use Chrome for installation and testing.
|
||||
+4
-4
@@ -4,14 +4,14 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/ligolo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tunnel using your browser</title>
|
||||
<meta key="title" content="Vite + HeroUI" property="og:title" />
|
||||
<title>Ligolo IWA</title>
|
||||
<meta key="title" content="Ligolo IWA" property="og:title" />
|
||||
<meta
|
||||
content="Make beautiful websites regardless of your design experience."
|
||||
content="Run the Ligolo-ng agent inside an Isolated Web App."
|
||||
property="og:description"
|
||||
/>
|
||||
<meta
|
||||
content="Make beautiful websites regardless of your design experience."
|
||||
content="Run the Ligolo-ng agent inside an Isolated Web App."
|
||||
name="description"
|
||||
/>
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"id": "/",
|
||||
"short_name": "Ligolo-IWA",
|
||||
"name": "Ligolo-ng",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/ligolo.png",
|
||||
"sizes": "48x48 72x72 96x96 128x128 256x256 512x512 1024x1024",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "500x500",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
@@ -19,4 +19,4 @@
|
||||
"direct-sockets": ["self"],
|
||||
"direct-sockets-private": ["self"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+267
-242
@@ -1,274 +1,298 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
Button,
|
||||
Spinner,
|
||||
CardHeader,
|
||||
Divider,
|
||||
Spinner,
|
||||
} from "@heroui/react";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { yamux } from "@chainsafe/libp2p-yamux";
|
||||
import { connect } from "it-ws/client";
|
||||
import { pipe } from "it-pipe";
|
||||
import { defaultLogger } from "@libp2p/logger";
|
||||
import { YamuxStream } from "@chainsafe/libp2p-yamux/stream";
|
||||
import * as toIterable from "stream-to-it";
|
||||
import { Handshake } from "it-handshake";
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { ThemeSwitcher } from "./components/theme-switcher";
|
||||
import { ConnectionForm } from "./components/connection-form";
|
||||
import { ConnectionStatistics } from "./components/connection-stats";
|
||||
import { ConnectionStatus } from "./components/connection-status";
|
||||
import { LogDisplay } from "./components/log-display";
|
||||
import { ConnectionForm } from "./components/connection-form";
|
||||
import { ThemeSwitcher } from "./components/theme-switcher";
|
||||
|
||||
import {
|
||||
ConnectRequestPacket,
|
||||
ConnectResponsePacket,
|
||||
InfoReplyPacket,
|
||||
LigoloEncoder,
|
||||
} from "@/ligolo/encdec.ts";
|
||||
import { LigoloDecoder, MESSAGE_TYPES, NetInterface } from "@/ligolo/encdec.ts";
|
||||
import { createTcpPipe, createUdpPipe } from "@/ligolo/socket.ts";
|
||||
import { ConnectionStatistics } from "@/components/connection-stats.tsx";
|
||||
type LogType = "info" | "error" | "success";
|
||||
|
||||
// Create a logger instance
|
||||
const component = {
|
||||
logger: defaultLogger(),
|
||||
};
|
||||
|
||||
async function tcpBridge(shake: Handshake, connection: TCPSocketOpenInfo) {
|
||||
const writer = connection.writable.getWriter();
|
||||
|
||||
for await (const chunk of shake.stream.source) {
|
||||
await writer.write(chunk.subarray());
|
||||
}
|
||||
interface ConnectionDetails {
|
||||
agentId: string;
|
||||
websocketUrl: string;
|
||||
}
|
||||
|
||||
async function udpBridge(shake: Handshake, connection: UDPSocketOpenInfo) {
|
||||
const writer = connection.writable.getWriter();
|
||||
interface ConnectionStats {
|
||||
activeConnections: number;
|
||||
bytesIn: number;
|
||||
bytesOut: number;
|
||||
totalConnections: number;
|
||||
}
|
||||
|
||||
for await (const chunk of shake.stream.source) {
|
||||
await writer.write({ data: chunk.subarray() });
|
||||
}
|
||||
interface LogEntry {
|
||||
id: number;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
type: LogType;
|
||||
}
|
||||
|
||||
const MAX_LOG_ENTRIES = 500;
|
||||
const STORAGE_KEYS = {
|
||||
agentId: "agentId",
|
||||
websocketUrl: "websocketUrl",
|
||||
} as const;
|
||||
const INITIAL_STATS: ConnectionStats = {
|
||||
activeConnections: 0,
|
||||
totalConnections: 0,
|
||||
bytesIn: 0,
|
||||
bytesOut: 0,
|
||||
};
|
||||
|
||||
function formatTimestamp(date = new Date()): string {
|
||||
return date.toLocaleTimeString("en-US", {
|
||||
hour: "2-digit",
|
||||
hour12: false,
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function createLogEntry(
|
||||
id: number,
|
||||
message: string,
|
||||
type: LogType = "info",
|
||||
): LogEntry {
|
||||
return {
|
||||
id,
|
||||
message,
|
||||
timestamp: formatTimestamp(),
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
function getInitialConnectionDetails(): ConnectionDetails {
|
||||
return {
|
||||
websocketUrl:
|
||||
window.localStorage.getItem(STORAGE_KEYS.websocketUrl) ??
|
||||
"ws://localhost:11601",
|
||||
agentId: window.localStorage.getItem(STORAGE_KEYS.agentId) ?? uuidv4(),
|
||||
};
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const nextLogIdRef = React.useRef(2);
|
||||
const pendingTrafficRef = React.useRef({ bytesIn: 0, bytesOut: 0 });
|
||||
const trafficFrameRef = React.useRef<number | null>(null);
|
||||
const sessionRef = React.useRef<{
|
||||
close(): Promise<void>;
|
||||
done: Promise<void>;
|
||||
} | null>(null);
|
||||
const disconnectRequestedRef = React.useRef(false);
|
||||
|
||||
const [isConnected, setIsConnected] = React.useState(false);
|
||||
const [isConnecting, setIsConnecting] = React.useState(false);
|
||||
const [logs, setLogs] = React.useState<
|
||||
Array<{
|
||||
timestamp: Date;
|
||||
message: string;
|
||||
type: "info" | "error" | "success";
|
||||
}>
|
||||
>([]);
|
||||
const [connectionDetails, setConnectionDetails] = React.useState({
|
||||
websocketUrl: window.localStorage.getItem("websocketUrl") ?? "ws://localhost:11601",
|
||||
agentId: window.localStorage.getItem("agentId") ?? uuidv4(),
|
||||
});
|
||||
const [stats, setStats] = React.useState({
|
||||
activeConnections: 0,
|
||||
totalConnections: 0,
|
||||
bytesIn: 0,
|
||||
bytesOut: 0,
|
||||
});
|
||||
const [logs, setLogs] = React.useState<LogEntry[]>(() => [
|
||||
createLogEntry(0, "Ligolo-ng agent initialized"),
|
||||
createLogEntry(1, "Ready to connect to Ligolo-ng Server"),
|
||||
]);
|
||||
const [connectionDetails, setConnectionDetails] =
|
||||
React.useState<ConnectionDetails>(getInitialConnectionDetails);
|
||||
const [stats, setStats] = React.useState(INITIAL_STATS);
|
||||
|
||||
const addLog = (
|
||||
message: string,
|
||||
type: "info" | "error" | "success" = "info",
|
||||
) => {
|
||||
setLogs((prevLogs) => [
|
||||
...prevLogs,
|
||||
{ timestamp: new Date(), message, type },
|
||||
]);
|
||||
const flushPendingTraffic = () => {
|
||||
trafficFrameRef.current = null;
|
||||
|
||||
if (
|
||||
pendingTrafficRef.current.bytesIn === 0 &&
|
||||
pendingTrafficRef.current.bytesOut === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextTraffic = pendingTrafficRef.current;
|
||||
|
||||
pendingTrafficRef.current = { bytesIn: 0, bytesOut: 0 };
|
||||
|
||||
setStats((previousStats) => ({
|
||||
...previousStats,
|
||||
bytesIn: previousStats.bytesIn + nextTraffic.bytesIn,
|
||||
bytesOut: previousStats.bytesOut + nextTraffic.bytesOut,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleConnect = () => {
|
||||
localStorage.setItem("websocketUrl", connectionDetails.websocketUrl);
|
||||
localStorage.setItem("agentId", connectionDetails.agentId);
|
||||
if (isConnected) {
|
||||
// Disconnect logic
|
||||
setIsConnecting(true);
|
||||
addLog("Disconnecting from agent...", "info");
|
||||
const queueTrafficUpdate = (
|
||||
direction: "bytesIn" | "bytesOut",
|
||||
bytes: number,
|
||||
) => {
|
||||
pendingTrafficRef.current[direction] += bytes;
|
||||
|
||||
setTimeout(() => {
|
||||
setIsConnected(false);
|
||||
setIsConnecting(false);
|
||||
addLog("Disconnected from agent", "info");
|
||||
}, 1500);
|
||||
} else {
|
||||
// Connect logic
|
||||
if (!connectionDetails.websocketUrl) {
|
||||
addLog("WebSocket URL is required", "error");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
startLigoloAgent(
|
||||
connectionDetails.websocketUrl,
|
||||
connectionDetails.agentId,
|
||||
);
|
||||
if (trafficFrameRef.current == null) {
|
||||
trafficFrameRef.current =
|
||||
window.requestAnimationFrame(flushPendingTraffic);
|
||||
}
|
||||
};
|
||||
|
||||
const addLog = (message: string, type: LogType = "info") => {
|
||||
const logEntry = createLogEntry(nextLogIdRef.current, message, type);
|
||||
|
||||
nextLogIdRef.current += 1;
|
||||
|
||||
React.startTransition(() => {
|
||||
setLogs((previousLogs) =>
|
||||
[...previousLogs, logEntry].slice(-MAX_LOG_ENTRIES),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const clearLogs = () => {
|
||||
setLogs([]);
|
||||
addLog("Logs cleared", "info");
|
||||
const clearedEntry = createLogEntry(nextLogIdRef.current, "Logs cleared");
|
||||
|
||||
nextLogIdRef.current += 1;
|
||||
setLogs([clearedEntry]);
|
||||
};
|
||||
|
||||
async function startLigoloAgent(WEBSOCKET_URL: string, agentId: string) {
|
||||
const handleDisconnect = async () => {
|
||||
if (sessionRef.current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
disconnectRequestedRef.current = true;
|
||||
setIsConnecting(true);
|
||||
addLog(`Connecting to WebSocket : ${WEBSOCKET_URL}`, "info");
|
||||
addLog("Disconnecting from agent...");
|
||||
await sessionRef.current.close();
|
||||
};
|
||||
|
||||
const handleConnect = async () => {
|
||||
window.localStorage.setItem(
|
||||
STORAGE_KEYS.websocketUrl,
|
||||
connectionDetails.websocketUrl,
|
||||
);
|
||||
window.localStorage.setItem(
|
||||
STORAGE_KEYS.agentId,
|
||||
connectionDetails.agentId,
|
||||
);
|
||||
|
||||
if (sessionRef.current != null) {
|
||||
await handleDisconnect();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connectionDetails.websocketUrl.trim()) {
|
||||
addLog("WebSocket URL is required", "error");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConnecting(true);
|
||||
setStats(INITIAL_STATS);
|
||||
pendingTrafficRef.current = { bytesIn: 0, bytesOut: 0 };
|
||||
|
||||
try {
|
||||
const webSocketStream = connect(WEBSOCKET_URL);
|
||||
const { createLigoloSession } = await import("./ligolo/session");
|
||||
let didReachConnectedState = false;
|
||||
|
||||
await webSocketStream.connected();
|
||||
|
||||
addLog("WebSocket connection success.", "success");
|
||||
|
||||
const muxer = yamux({
|
||||
maxInboundStreams: 10000000,
|
||||
maxOutboundStreams: 10000000,
|
||||
onIncomingStream: async (stream: YamuxStream) => {
|
||||
const decoder = new LigoloDecoder(stream.source);
|
||||
|
||||
await decoder.decode();
|
||||
console.log("Decode successful! Payload:", decoder);
|
||||
|
||||
const encoder = new LigoloEncoder(stream);
|
||||
|
||||
switch (decoder.payloadType) {
|
||||
case MESSAGE_TYPES.MessageInfoRequest:
|
||||
setIsConnected(true);
|
||||
setIsConnecting(false);
|
||||
addLog("Information Request packet received", "info");
|
||||
const netIfs = new NetInterface(
|
||||
0,
|
||||
1500,
|
||||
"Chrome IWA",
|
||||
"123456",
|
||||
0,
|
||||
[],
|
||||
);
|
||||
const infoPacket = new InfoReplyPacket(
|
||||
"Ligolo-ng/JS",
|
||||
[netIfs],
|
||||
agentId,
|
||||
);
|
||||
|
||||
await encoder.encode(infoPacket);
|
||||
break;
|
||||
case MESSAGE_TYPES.MessageConnectRequest:
|
||||
const connectInfo = decoder.payload as ConnectRequestPacket;
|
||||
let connection = null;
|
||||
|
||||
if (connectInfo.Transport == 0) {
|
||||
connection = await createTcpPipe(
|
||||
connectInfo.Address,
|
||||
connectInfo.Port,
|
||||
2000,
|
||||
);
|
||||
} else if (connectInfo.Transport == 1) {
|
||||
connection = await createUdpPipe(
|
||||
connectInfo.Address,
|
||||
connectInfo.Port,
|
||||
2000,
|
||||
);
|
||||
}
|
||||
|
||||
const connResult = new ConnectResponsePacket(true, false);
|
||||
const shake = await encoder.encode(connResult);
|
||||
|
||||
setStats((prevState) => ({
|
||||
...prevState,
|
||||
totalConnections: prevState.totalConnections + 1,
|
||||
activeConnections: prevState.activeConnections + 1,
|
||||
}));
|
||||
if (connectInfo.Transport == 0) {
|
||||
tcpBridge(shake, connection);
|
||||
await pipe(toIterable.source(connection), shake.stream);
|
||||
} else if (connectInfo.Transport == 1) {
|
||||
udpBridge(shake, connection);
|
||||
await pipe(async function* () {
|
||||
// A generator is async iterable
|
||||
const reader = connection?.readable.getReader();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
yield value["data"];
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}, shake.stream);
|
||||
}
|
||||
connection?.close();
|
||||
stream.close();
|
||||
setStats((prevState) => ({
|
||||
...prevState,
|
||||
activeConnections: prevState.activeConnections - 1,
|
||||
}));
|
||||
break;
|
||||
const session = await createLigoloSession({
|
||||
agentId: connectionDetails.agentId,
|
||||
websocketUrl: connectionDetails.websocketUrl,
|
||||
onActiveConnectionsChange: (delta) => {
|
||||
setStats((previousStats) => ({
|
||||
...previousStats,
|
||||
activeConnections: Math.max(
|
||||
previousStats.activeConnections + delta,
|
||||
0,
|
||||
),
|
||||
}));
|
||||
},
|
||||
onBytesIn: (bytes) => {
|
||||
queueTrafficUpdate("bytesIn", bytes);
|
||||
},
|
||||
onBytesOut: (bytes) => {
|
||||
queueTrafficUpdate("bytesOut", bytes);
|
||||
},
|
||||
onConnected: () => {
|
||||
if (didReachConnectedState) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
})(component);
|
||||
|
||||
const streamMuxer = muxer.createStreamMuxer();
|
||||
didReachConnectedState = true;
|
||||
setIsConnected(true);
|
||||
setIsConnecting(false);
|
||||
addLog("Ligolo session established", "success");
|
||||
},
|
||||
onLog: addLog,
|
||||
onTotalConnectionsChange: (delta) => {
|
||||
setStats((previousStats) => ({
|
||||
...previousStats,
|
||||
totalConnections: Math.max(
|
||||
previousStats.totalConnections + delta,
|
||||
0,
|
||||
),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
await pipe(
|
||||
webSocketStream,
|
||||
function transform(source) {
|
||||
return (async function* () {
|
||||
for await (const chunk of source) {
|
||||
setStats((prevState) => ({
|
||||
...prevState,
|
||||
bytesIn: prevState.bytesIn + chunk.byteLength,
|
||||
}));
|
||||
yield chunk.subarray();
|
||||
}
|
||||
})();
|
||||
},
|
||||
streamMuxer,
|
||||
function transform(source) {
|
||||
return (async function* () {
|
||||
for await (const chunk of source) {
|
||||
setStats((prevState) => ({
|
||||
...prevState,
|
||||
bytesOut: prevState.bytesOut + chunk.byteLength,
|
||||
}));
|
||||
yield chunk.subarray();
|
||||
}
|
||||
})();
|
||||
},
|
||||
webSocketStream,
|
||||
);
|
||||
sessionRef.current = session;
|
||||
|
||||
void session.done.finally(() => {
|
||||
if (trafficFrameRef.current != null) {
|
||||
window.cancelAnimationFrame(trafficFrameRef.current);
|
||||
}
|
||||
|
||||
flushPendingTraffic();
|
||||
sessionRef.current = null;
|
||||
setIsConnected(false);
|
||||
setIsConnecting(false);
|
||||
setStats((previousStats) => ({
|
||||
...previousStats,
|
||||
activeConnections: 0,
|
||||
}));
|
||||
|
||||
const disconnectRequested = disconnectRequestedRef.current;
|
||||
|
||||
disconnectRequestedRef.current = false;
|
||||
|
||||
if (disconnectRequested) {
|
||||
addLog("Disconnected from agent");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
addLog(
|
||||
didReachConnectedState
|
||||
? "Connection closed"
|
||||
: "Connection attempt ended before the session was ready",
|
||||
didReachConnectedState ? "info" : "error",
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
setIsConnected(false);
|
||||
setIsConnecting(false);
|
||||
addLog(`Error: ${error}`, "error");
|
||||
addLog(
|
||||
`Unable to start Ligolo session: ${error instanceof Error ? error.message : String(error)}`,
|
||||
"error",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
// Initial log
|
||||
addLog("Ligolo-ng agent initialized", "info");
|
||||
addLog("Ready to connect to Ligolo-ng Server", "info");
|
||||
return () => {
|
||||
if (trafficFrameRef.current != null) {
|
||||
window.cancelAnimationFrame(trafficFrameRef.current);
|
||||
}
|
||||
|
||||
void sessionRef.current?.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-4 md:p-8 flex flex-col">
|
||||
<div className="flex min-h-screen flex-col bg-background p-4 md:p-8">
|
||||
<header className="mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon className="text-primary text-2xl" icon="lucide:link" />
|
||||
<Icon className="text-2xl text-primary" icon="lucide:link" />
|
||||
<h1 className="text-2xl font-semibold text-foreground">
|
||||
Ligolo-ng Agent Connection
|
||||
</h1>
|
||||
@@ -280,8 +304,8 @@ export default function App() {
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 flex-grow">
|
||||
<div className="lg:col-span-1 flex flex-col gap-6">
|
||||
<div className="grid flex-grow grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div className="flex flex-col gap-6 lg:col-span-1">
|
||||
<Card className="h-auto overflow-visible">
|
||||
<CardHeader className="flex flex-col gap-1">
|
||||
<h2 className="text-lg font-medium">Connection Settings</h2>
|
||||
@@ -299,24 +323,25 @@ export default function App() {
|
||||
/>
|
||||
</CardBody>
|
||||
<Divider />
|
||||
<CardFooter className="flex justify-between items-center">
|
||||
<ConnectionStatus isConnected={isConnected} />
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
color={isConnected ? "danger" : "primary"}
|
||||
isDisabled={isConnecting}
|
||||
startContent={isConnecting ? <Spinner size="sm" /> : null}
|
||||
onPress={handleConnect}
|
||||
>
|
||||
{isConnecting
|
||||
? isConnected
|
||||
? "Disconnecting..."
|
||||
: "Connecting..."
|
||||
: isConnected
|
||||
? "Disconnect"
|
||||
: "Connect"}
|
||||
</Button>
|
||||
</div>
|
||||
<CardFooter className="flex items-center justify-between">
|
||||
<ConnectionStatus
|
||||
isConnected={isConnected}
|
||||
isConnecting={isConnecting}
|
||||
/>
|
||||
<Button
|
||||
color={isConnected ? "danger" : "primary"}
|
||||
isDisabled={isConnecting}
|
||||
startContent={isConnecting ? <Spinner size="sm" /> : null}
|
||||
onPress={handleConnect}
|
||||
>
|
||||
{isConnecting
|
||||
? isConnected
|
||||
? "Disconnecting..."
|
||||
: "Connecting..."
|
||||
: isConnected
|
||||
? "Disconnect"
|
||||
: "Connect"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -23,7 +23,10 @@ export const ConnectionForm: React.FC<ConnectionFormProps> = ({
|
||||
isConnecting,
|
||||
isConnected,
|
||||
}) => {
|
||||
const handleChange = (field: string, value: string) => {
|
||||
const handleChange = (
|
||||
field: keyof ConnectionFormProps["connectionDetails"],
|
||||
value: string,
|
||||
) => {
|
||||
setConnectionDetails((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
|
||||
@@ -2,6 +2,15 @@ import React from "react";
|
||||
import { Card, CardBody, CardHeader, Divider } from "@heroui/react";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return "0 B";
|
||||
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
const unitIndex = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
return `${parseFloat((bytes / 1024 ** unitIndex).toFixed(2))} ${sizes[unitIndex]}`;
|
||||
}
|
||||
|
||||
interface ConnectionStatisticsProps {
|
||||
isConnected: boolean;
|
||||
stats: {
|
||||
@@ -16,15 +25,6 @@ export const ConnectionStatistics: React.FC<ConnectionStatisticsProps> = ({
|
||||
isConnected,
|
||||
stats,
|
||||
}) => {
|
||||
const formatBytes = (bytes: number): string => {
|
||||
if (bytes === 0) return "0 B";
|
||||
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<CardHeader className="flex flex-col gap-1">
|
||||
@@ -36,7 +36,7 @@ export const ConnectionStatistics: React.FC<ConnectionStatisticsProps> = ({
|
||||
<Divider />
|
||||
<CardBody>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-1 p-3 rounded-medium bg-content2">
|
||||
<div className="flex flex-col gap-1 rounded-medium bg-content2 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="text-primary" icon="lucide:activity" />
|
||||
<span className="text-small text-foreground-500">
|
||||
@@ -48,7 +48,7 @@ export const ConnectionStatistics: React.FC<ConnectionStatisticsProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 p-3 rounded-medium bg-content2">
|
||||
<div className="flex flex-col gap-1 rounded-medium bg-content2 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="text-primary" icon="lucide:history" />
|
||||
<span className="text-small text-foreground-500">
|
||||
@@ -58,7 +58,7 @@ export const ConnectionStatistics: React.FC<ConnectionStatisticsProps> = ({
|
||||
<p className="text-2xl font-semibold">{stats.totalConnections}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 p-3 rounded-medium bg-content2">
|
||||
<div className="flex flex-col gap-1 rounded-medium bg-content2 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="text-success" icon="lucide:download" />
|
||||
<span className="text-small text-foreground-500">
|
||||
@@ -70,7 +70,7 @@ export const ConnectionStatistics: React.FC<ConnectionStatisticsProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 p-3 rounded-medium bg-content2">
|
||||
<div className="flex flex-col gap-1 rounded-medium bg-content2 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="text-warning" icon="lucide:upload" />
|
||||
<span className="text-small text-foreground-500">Data Sent</span>
|
||||
|
||||
@@ -2,18 +2,28 @@ import React from "react";
|
||||
|
||||
interface ConnectionStatusProps {
|
||||
isConnected: boolean;
|
||||
isConnecting: boolean;
|
||||
}
|
||||
|
||||
export const ConnectionStatus: React.FC<ConnectionStatusProps> = ({
|
||||
isConnected,
|
||||
isConnecting,
|
||||
}) => {
|
||||
const isPending = isConnecting && !isConnected;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full ${isConnected ? "bg-success animate-pulse" : "bg-danger"}`}
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
isConnected
|
||||
? "bg-success animate-pulse"
|
||||
: isPending
|
||||
? "bg-warning animate-pulse"
|
||||
: "bg-danger"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-small font-medium">
|
||||
{isConnected ? "Connected" : "Disconnected"}
|
||||
{isConnected ? "Connected" : isPending ? "Connecting" : "Disconnected"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
Button,
|
||||
CardHeader,
|
||||
Divider,
|
||||
} from "@heroui/react";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
interface LogDisplayProps {
|
||||
logs: Array<{
|
||||
timestamp: Date;
|
||||
id: number;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
type: "info" | "error" | "success";
|
||||
}>;
|
||||
clearLogs: () => void;
|
||||
@@ -22,21 +23,11 @@ export const LogDisplay: React.FC<LogDisplayProps> = ({ logs, clearLogs }) => {
|
||||
const logContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Auto-scroll to bottom when new logs are added
|
||||
if (logContainerRef.current) {
|
||||
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, [logs]);
|
||||
|
||||
const formatTimestamp = (date: Date) => {
|
||||
return date.toLocaleTimeString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
};
|
||||
|
||||
const getLogIcon = (type: "info" | "error" | "success") => {
|
||||
switch (type) {
|
||||
case "error":
|
||||
@@ -63,7 +54,7 @@ export const LogDisplay: React.FC<LogDisplayProps> = ({ logs, clearLogs }) => {
|
||||
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<CardHeader className="flex justify-between items-center">
|
||||
<CardHeader className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">Connection Logs</h2>
|
||||
<p className="text-small text-foreground-500">
|
||||
@@ -87,17 +78,17 @@ export const LogDisplay: React.FC<LogDisplayProps> = ({ logs, clearLogs }) => {
|
||||
className="h-[400px] overflow-y-auto p-4 font-mono text-small"
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-foreground-400">
|
||||
<Icon className="text-4xl mb-2" icon="lucide:file-text" />
|
||||
<div className="flex h-full flex-col items-center justify-center text-foreground-400">
|
||||
<Icon className="mb-2 text-4xl" icon="lucide:file-text" />
|
||||
<p>No logs available</p>
|
||||
</div>
|
||||
) : (
|
||||
logs.map((log, index) => (
|
||||
<div key={index} className="mb-2 flex items-start gap-2">
|
||||
<span className="text-foreground-400 shrink-0">
|
||||
[{formatTimestamp(log.timestamp)}]
|
||||
logs.map((log) => (
|
||||
<div key={log.id} className="mb-2 flex items-start gap-2">
|
||||
<span className="shrink-0 text-foreground-400">
|
||||
[{log.timestamp}]
|
||||
</span>
|
||||
<span className="shrink-0 mt-0.5">{getLogIcon(log.type)}</span>
|
||||
<span className="mt-0.5 shrink-0">{getLogIcon(log.type)}</span>
|
||||
<span className={getLogClass(log.type)}>{log.message}</span>
|
||||
</div>
|
||||
))
|
||||
@@ -119,19 +110,19 @@ export const LogDisplay: React.FC<LogDisplayProps> = ({ logs, clearLogs }) => {
|
||||
const logText = logs
|
||||
.map(
|
||||
(log) =>
|
||||
`[${formatTimestamp(log.timestamp)}] ${log.type.toUpperCase()}: ${log.message}`,
|
||||
`[${log.timestamp}] ${log.type.toUpperCase()}: ${log.message}`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const blob = new Blob([logText], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
const anchor = document.createElement("a");
|
||||
|
||||
a.href = url;
|
||||
a.download = `ligolo-logs-${new Date().toISOString().split("T")[0]}.txt`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
anchor.href = url;
|
||||
anchor.download = `ligolo-logs-${new Date().toISOString().split("T")[0]}.txt`;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
URL.revokeObjectURL(url);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -205,7 +205,7 @@ export type LigoloPacket =
|
||||
| ListenerSocketConnectionReady;
|
||||
|
||||
type MessageTypeValue = (typeof MESSAGE_TYPES)[keyof typeof MESSAGE_TYPES];
|
||||
type Constructor<T> = new (...args: any[]) => T;
|
||||
type Constructor<T> = new (...args: unknown[]) => T;
|
||||
|
||||
const PAYLOAD_CLASS_MAP = new Map<Constructor<LigoloPacket>, MessageTypeValue>([
|
||||
[InfoRequestPacket, MESSAGE_TYPES.MessageInfoRequest],
|
||||
@@ -243,7 +243,6 @@ const MESSAGE_TYPE_TO_CLASS_MAP = new Map<
|
||||
/** Encodes Ligolo-ng protocol messages to a stream. */
|
||||
export class LigoloEncoder {
|
||||
private readonly _stream: Handshake;
|
||||
// MODIFIED: Use a Packr instance for encoding.
|
||||
private readonly _packer = new Packr({
|
||||
mapsAsObjects: true,
|
||||
useRecords: false,
|
||||
@@ -307,9 +306,6 @@ export class LigoloDecoder {
|
||||
const unpackr = new Unpackr({ mapsAsObjects: true });
|
||||
|
||||
for await (const chunk of rawStream) {
|
||||
// unpackr.unpackMultiple returns an array of fully decoded objects from the chunk.
|
||||
// It handles messages that span across multiple chunks internally.
|
||||
// We use chunk.subarray() to get a Uint8Array, which unpackr expects.
|
||||
const messages = unpackr.unpackMultiple(chunk.subarray());
|
||||
|
||||
for (const message of messages) {
|
||||
@@ -320,7 +316,6 @@ export class LigoloDecoder {
|
||||
|
||||
/** Reads and decodes the next Ligolo-ng message from the stream. */
|
||||
public async decode(): Promise<void> {
|
||||
// 1. Decode the message type ID
|
||||
const typeResult = await this._decodedStream.next();
|
||||
|
||||
if (typeResult.done) {
|
||||
@@ -333,7 +328,6 @@ export class LigoloDecoder {
|
||||
this.payloadType = payloadType;
|
||||
this.messageType = PAYLOAD_TYPE_NAMES[payloadType];
|
||||
|
||||
// 2. Look up the corresponding class constructor
|
||||
const PacketClass = MESSAGE_TYPE_TO_CLASS_MAP.get(payloadType);
|
||||
|
||||
if (!PacketClass) {
|
||||
@@ -361,7 +355,7 @@ export class LigoloDecoder {
|
||||
|
||||
switch (PacketClass) {
|
||||
case InfoReplyPacket:
|
||||
const interfaces = rawPayload.Interfaces.map(
|
||||
const interfaces = (rawPayload.Interfaces ?? []).map(
|
||||
(iface: any) =>
|
||||
new NetInterface(
|
||||
iface.Index,
|
||||
@@ -416,7 +410,6 @@ export class LigoloDecoder {
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(rawPayload);
|
||||
throw new Error(
|
||||
`No constructor logic found for packet type: ${this.messageType}`,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
import type { YamuxStream } from "@chainsafe/libp2p-yamux/stream";
|
||||
import type { Handshake } from "it-handshake";
|
||||
|
||||
import { yamux } from "@chainsafe/libp2p-yamux";
|
||||
import { defaultLogger } from "@libp2p/logger";
|
||||
import { pipe } from "it-pipe";
|
||||
import * as toIterable from "stream-to-it";
|
||||
import { connect } from "it-ws/client";
|
||||
|
||||
import {
|
||||
ConnectRequestPacket,
|
||||
ConnectResponsePacket,
|
||||
InfoReplyPacket,
|
||||
LigoloDecoder,
|
||||
LigoloEncoder,
|
||||
MESSAGE_TYPES,
|
||||
NetInterface,
|
||||
TRANSPORT_TYPES,
|
||||
} from "./encdec";
|
||||
import { createTcpPipe, createUdpPipe } from "./socket";
|
||||
|
||||
const component = {
|
||||
logger: defaultLogger(),
|
||||
};
|
||||
|
||||
export type SessionLogType = "info" | "error" | "success";
|
||||
|
||||
export interface LigoloSessionCallbacks {
|
||||
onActiveConnectionsChange(delta: number): void;
|
||||
onBytesIn(bytes: number): void;
|
||||
onBytesOut(bytes: number): void;
|
||||
onConnected(): void;
|
||||
onLog(message: string, type?: SessionLogType): void;
|
||||
onTotalConnectionsChange(delta: number): void;
|
||||
}
|
||||
|
||||
export interface CreateLigoloSessionOptions extends LigoloSessionCallbacks {
|
||||
agentId: string;
|
||||
websocketUrl: string;
|
||||
}
|
||||
|
||||
export interface LigoloSession {
|
||||
close(): Promise<void>;
|
||||
done: Promise<void>;
|
||||
}
|
||||
|
||||
interface ActiveTunnel {
|
||||
close(): Promise<void>;
|
||||
release(): void;
|
||||
}
|
||||
|
||||
interface TunnelRegistry {
|
||||
closeAll(): Promise<void>;
|
||||
register(closeTunnel: () => Promise<void>): ActiveTunnel;
|
||||
}
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function createTunnelRegistry(
|
||||
callbacks: Pick<LigoloSessionCallbacks, "onActiveConnectionsChange">,
|
||||
): TunnelRegistry {
|
||||
let nextTunnelId = 0;
|
||||
const activeTunnels = new Map<number, () => Promise<void>>();
|
||||
|
||||
return {
|
||||
register(closeTunnel) {
|
||||
const tunnelId = nextTunnelId;
|
||||
|
||||
nextTunnelId += 1;
|
||||
activeTunnels.set(tunnelId, closeTunnel);
|
||||
callbacks.onActiveConnectionsChange(1);
|
||||
|
||||
let released = false;
|
||||
|
||||
return {
|
||||
close: async () => {
|
||||
await closeTunnel();
|
||||
},
|
||||
release: () => {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
|
||||
released = true;
|
||||
|
||||
if (activeTunnels.delete(tunnelId)) {
|
||||
callbacks.onActiveConnectionsChange(-1);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
closeAll: async () => {
|
||||
const tunnelsToClose = Array.from(activeTunnels.entries());
|
||||
|
||||
for (const [tunnelId] of tunnelsToClose) {
|
||||
if (activeTunnels.delete(tunnelId)) {
|
||||
callbacks.onActiveConnectionsChange(-1);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.allSettled(
|
||||
tunnelsToClose.map(([, closeTunnel]) => closeTunnel()),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function tcpBridge(
|
||||
tunnel: Handshake,
|
||||
connection: Awaited<ReturnType<typeof createTcpPipe>>,
|
||||
) {
|
||||
const writer = connection.writable.getWriter();
|
||||
|
||||
try {
|
||||
for await (const chunk of tunnel.stream.source) {
|
||||
await writer.write(chunk.subarray());
|
||||
}
|
||||
} finally {
|
||||
writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
async function udpBridge(
|
||||
tunnel: Handshake,
|
||||
connection: Awaited<ReturnType<typeof createUdpPipe>>,
|
||||
) {
|
||||
const writer = connection.writable.getWriter();
|
||||
|
||||
try {
|
||||
for await (const chunk of tunnel.stream.source) {
|
||||
await writer.write(chunk.subarray());
|
||||
}
|
||||
} finally {
|
||||
writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
async function* udpReadableSource(
|
||||
connection: Awaited<ReturnType<typeof createUdpPipe>>,
|
||||
) {
|
||||
const reader = connection.readable.getReader();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield new Uint8Array(value.data);
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
async function finalizeTunnel(
|
||||
activeTunnel: ActiveTunnel,
|
||||
tunnelFlows: Array<Promise<unknown>>,
|
||||
) {
|
||||
activeTunnel.release();
|
||||
await activeTunnel.close();
|
||||
await Promise.allSettled(tunnelFlows);
|
||||
}
|
||||
|
||||
async function handleTcpTunnel(
|
||||
stream: YamuxStream,
|
||||
packet: ConnectRequestPacket,
|
||||
callbacks: LigoloSessionCallbacks,
|
||||
tunnels: TunnelRegistry,
|
||||
) {
|
||||
const encoder = new LigoloEncoder(stream);
|
||||
const target = `${packet.Address}:${packet.Port}`;
|
||||
|
||||
try {
|
||||
const connection = await createTcpPipe(packet.Address, packet.Port, 2_000);
|
||||
const tunnel = await encoder.encode(new ConnectResponsePacket(true, false));
|
||||
const activeTunnel = tunnels.register(async () => {
|
||||
await Promise.allSettled([connection.close(), stream.close()]);
|
||||
});
|
||||
|
||||
callbacks.onTotalConnectionsChange(1);
|
||||
callbacks.onLog(`TCP tunnel opened to ${target}`, "success");
|
||||
|
||||
const tunnelFlows = [
|
||||
tcpBridge(tunnel, connection),
|
||||
pipe(toIterable.source(connection.readable), tunnel.stream),
|
||||
];
|
||||
|
||||
try {
|
||||
await Promise.race(tunnelFlows);
|
||||
} finally {
|
||||
await finalizeTunnel(activeTunnel, tunnelFlows);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
callbacks.onLog(
|
||||
`Unable to open tunnel to ${target}: ${formatError(error)}`,
|
||||
"error",
|
||||
);
|
||||
await encoder.encode(new ConnectResponsePacket(false, true));
|
||||
await stream.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUdpTunnel(
|
||||
stream: YamuxStream,
|
||||
packet: ConnectRequestPacket,
|
||||
callbacks: LigoloSessionCallbacks,
|
||||
tunnels: TunnelRegistry,
|
||||
) {
|
||||
const encoder = new LigoloEncoder(stream);
|
||||
const target = `${packet.Address}:${packet.Port}`;
|
||||
|
||||
try {
|
||||
const connection = await createUdpPipe(packet.Address, packet.Port, 2_000);
|
||||
const tunnel = await encoder.encode(new ConnectResponsePacket(true, false));
|
||||
const activeTunnel = tunnels.register(async () => {
|
||||
await Promise.allSettled([connection.close(), stream.close()]);
|
||||
});
|
||||
|
||||
callbacks.onTotalConnectionsChange(1);
|
||||
callbacks.onLog(`UDP tunnel opened to ${target}`, "success");
|
||||
|
||||
const tunnelFlows = [
|
||||
udpBridge(tunnel, connection),
|
||||
pipe(udpReadableSource(connection), tunnel.stream),
|
||||
];
|
||||
|
||||
try {
|
||||
await Promise.race(tunnelFlows);
|
||||
} finally {
|
||||
await finalizeTunnel(activeTunnel, tunnelFlows);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
callbacks.onLog(
|
||||
`Unable to open tunnel to ${target}: ${formatError(error)}`,
|
||||
"error",
|
||||
);
|
||||
await encoder.encode(new ConnectResponsePacket(false, true));
|
||||
await stream.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConnectRequest(
|
||||
stream: YamuxStream,
|
||||
packet: ConnectRequestPacket,
|
||||
callbacks: LigoloSessionCallbacks,
|
||||
tunnels: TunnelRegistry,
|
||||
) {
|
||||
const target = `${packet.Address}:${packet.Port}`;
|
||||
|
||||
if (packet.Transport === TRANSPORT_TYPES.TransportTCP) {
|
||||
await handleTcpTunnel(stream, packet, callbacks, tunnels);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet.Transport === TRANSPORT_TYPES.TransportUDP) {
|
||||
await handleUdpTunnel(stream, packet, callbacks, tunnels);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
callbacks.onLog(
|
||||
`Unsupported transport received for ${target}: ${packet.Transport}`,
|
||||
"error",
|
||||
);
|
||||
|
||||
const encoder = new LigoloEncoder(stream);
|
||||
|
||||
await encoder.encode(new ConnectResponsePacket(false, true));
|
||||
await stream.close();
|
||||
}
|
||||
|
||||
async function handleIncomingStream(
|
||||
stream: YamuxStream,
|
||||
agentId: string,
|
||||
callbacks: LigoloSessionCallbacks,
|
||||
tunnels: TunnelRegistry,
|
||||
) {
|
||||
const decoder = new LigoloDecoder(stream.source);
|
||||
|
||||
await decoder.decode();
|
||||
|
||||
switch (decoder.payloadType) {
|
||||
case MESSAGE_TYPES.MessageInfoRequest: {
|
||||
callbacks.onConnected();
|
||||
callbacks.onLog("Ligolo info request received", "info");
|
||||
|
||||
const encoder = new LigoloEncoder(stream);
|
||||
const netIfs = new NetInterface(0, 1500, "Chrome IWA", "123456", 0, []);
|
||||
const infoPacket = new InfoReplyPacket("Ligolo-ng/JS", [netIfs], agentId);
|
||||
|
||||
await encoder.encode(infoPacket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case MESSAGE_TYPES.MessageConnectRequest:
|
||||
await handleConnectRequest(
|
||||
stream,
|
||||
decoder.payload as ConnectRequestPacket,
|
||||
callbacks,
|
||||
tunnels,
|
||||
);
|
||||
|
||||
return;
|
||||
|
||||
default:
|
||||
callbacks.onLog(
|
||||
`Ignoring unsupported packet type: ${decoder.messageType ?? decoder.payloadType}`,
|
||||
"info",
|
||||
);
|
||||
await stream.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function createLigoloSession({
|
||||
agentId,
|
||||
websocketUrl,
|
||||
...callbacks
|
||||
}: CreateLigoloSessionOptions): Promise<LigoloSession> {
|
||||
callbacks.onLog(`Connecting to WebSocket: ${websocketUrl}`, "info");
|
||||
|
||||
const webSocketStream = connect(websocketUrl);
|
||||
|
||||
await webSocketStream.connected();
|
||||
callbacks.onLog("WebSocket connection established", "success");
|
||||
|
||||
let isShuttingDown = false;
|
||||
let closePromise: Promise<void> | null = null;
|
||||
let hasConnected = false;
|
||||
|
||||
const tunnels = createTunnelRegistry(callbacks);
|
||||
|
||||
const muxer = yamux({
|
||||
maxInboundStreams: 10_000_000,
|
||||
maxOutboundStreams: 10_000_000,
|
||||
onIncomingStream: async (stream: YamuxStream) => {
|
||||
try {
|
||||
await handleIncomingStream(
|
||||
stream,
|
||||
agentId,
|
||||
{
|
||||
...callbacks,
|
||||
onConnected: () => {
|
||||
if (hasConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasConnected = true;
|
||||
callbacks.onConnected();
|
||||
},
|
||||
},
|
||||
tunnels,
|
||||
);
|
||||
} catch (error) {
|
||||
callbacks.onLog(
|
||||
`Incoming stream failed: ${formatError(error)}`,
|
||||
"error",
|
||||
);
|
||||
await stream.close();
|
||||
}
|
||||
},
|
||||
})(component);
|
||||
|
||||
const streamMuxer = muxer.createStreamMuxer();
|
||||
|
||||
const shutdown = async () => {
|
||||
if (closePromise != null) {
|
||||
return closePromise;
|
||||
}
|
||||
|
||||
closePromise = Promise.allSettled([
|
||||
tunnels.closeAll(),
|
||||
streamMuxer.close(),
|
||||
webSocketStream.close(),
|
||||
]).then(() => undefined);
|
||||
|
||||
return closePromise;
|
||||
};
|
||||
|
||||
const done = (async () => {
|
||||
try {
|
||||
await pipe(
|
||||
webSocketStream,
|
||||
async function* trackInbound(source) {
|
||||
for await (const chunk of source) {
|
||||
callbacks.onBytesIn(chunk.byteLength);
|
||||
yield chunk.subarray();
|
||||
}
|
||||
},
|
||||
streamMuxer,
|
||||
async function* trackOutbound(source) {
|
||||
for await (const chunk of source) {
|
||||
callbacks.onBytesOut(chunk.byteLength);
|
||||
yield chunk.subarray();
|
||||
}
|
||||
},
|
||||
webSocketStream,
|
||||
);
|
||||
} catch (error) {
|
||||
if (!isShuttingDown) {
|
||||
callbacks.onLog(
|
||||
`Session closed with error: ${formatError(error)}`,
|
||||
"error",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await shutdown();
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
close: async () => {
|
||||
isShuttingDown = true;
|
||||
await shutdown();
|
||||
await done;
|
||||
},
|
||||
done,
|
||||
};
|
||||
}
|
||||
+78
-40
@@ -1,54 +1,92 @@
|
||||
export async function createTcpPipe(ip: string, port: number, timeoutMs: number) {
|
||||
export interface TcpSocketConnection {
|
||||
close(): Promise<void>;
|
||||
readable: ReadableStream<Uint8Array>;
|
||||
writable: WritableStream<Uint8Array>;
|
||||
}
|
||||
|
||||
export interface UdpSocketConnection {
|
||||
close(): Promise<void>;
|
||||
readable: ReadableStream<UDPMessage>;
|
||||
writable: UDPSocket["writable"];
|
||||
}
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return String(error);
|
||||
}
|
||||
|
||||
async function withTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number,
|
||||
message: string,
|
||||
): Promise<T> {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
try {
|
||||
const socket = new TCPSocket(ip, port, {});
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error(`Timeout de connexion dépassé (${timeoutMs} ms)`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
const { readable, writable } = await Promise.race([
|
||||
socket.opened,
|
||||
timeoutPromise
|
||||
return await Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(new Error(message));
|
||||
}, timeoutMs);
|
||||
}),
|
||||
]);
|
||||
|
||||
console.log(`TCP Connection established ${ip}:${port}`);
|
||||
|
||||
return {
|
||||
readable,
|
||||
writable,
|
||||
getReader: () => readable.getReader(),
|
||||
close: () => socket.close(),
|
||||
once: (_ : string) => {},
|
||||
destroy: () => {console.log("close")},
|
||||
removeListener: () => {}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Unable to connect to ${ip}:${port} (TCP)`, error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (timeoutId != null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createUdpPipe(ip: string, port: number, timeoutMs: number) {
|
||||
export async function createTcpPipe(
|
||||
ip: string,
|
||||
port: number,
|
||||
timeoutMs: number,
|
||||
): Promise<TcpSocketConnection> {
|
||||
try {
|
||||
const socket = new UDPSocket({remoteAddress: ip, remotePort: port});
|
||||
|
||||
const { readable, writable } = await socket.opened;
|
||||
|
||||
console.log(`UDP Connection established ${ip}:${port}`);
|
||||
const socket = new TCPSocket(ip, port, {});
|
||||
const { readable, writable } = await withTimeout(
|
||||
socket.opened,
|
||||
timeoutMs,
|
||||
`TCP connection timed out after ${timeoutMs} ms`,
|
||||
);
|
||||
|
||||
return {
|
||||
readable,
|
||||
writable,
|
||||
getReader: () => readable.getReader(),
|
||||
close: () => socket.close(),
|
||||
once: (_ : string) => {},
|
||||
destroy: () => {console.log("close")},
|
||||
removeListener: () => {}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Unable to connect to ${ip}:${port} (UDP)`, error);
|
||||
throw error;
|
||||
throw new Error(
|
||||
`Unable to connect to ${ip}:${port} over TCP: ${formatError(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createUdpPipe(
|
||||
ip: string,
|
||||
port: number,
|
||||
timeoutMs: number,
|
||||
): Promise<UdpSocketConnection> {
|
||||
try {
|
||||
const socket = new UDPSocket({ remoteAddress: ip, remotePort: port });
|
||||
const { readable, writable } = await withTimeout(
|
||||
socket.opened,
|
||||
timeoutMs,
|
||||
`UDP connection timed out after ${timeoutMs} ms`,
|
||||
);
|
||||
|
||||
return {
|
||||
readable,
|
||||
writable,
|
||||
close: () => socket.close(),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Unable to connect to ${ip}:${port} over UDP: ${formatError(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { HeroUIProvider, ToastProvider } from "@heroui/react";
|
||||
import { HeroUIProvider } from "@heroui/react";
|
||||
|
||||
import App from "./App.tsx";
|
||||
import "./styles/globals.css";
|
||||
@@ -8,7 +8,6 @@ import "./styles/globals.css";
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<HeroUIProvider>
|
||||
<ToastProvider />
|
||||
<App />
|
||||
</HeroUIProvider>
|
||||
</React.StrictMode>,
|
||||
|
||||
Vendored
+125
-118
@@ -16,181 +16,188 @@
|
||||
|
||||
/* https://wicg.github.io/direct-sockets/#dom-tcpsocketoptions */
|
||||
interface TCPSocketOptions {
|
||||
sendBufferSize?: number;
|
||||
receiveBufferSize? : number;
|
||||
sendBufferSize?: number;
|
||||
receiveBufferSize?: number;
|
||||
|
||||
noDelay?: boolean;
|
||||
keepAliveDelay?: number;
|
||||
noDelay?: boolean;
|
||||
keepAliveDelay?: number;
|
||||
}
|
||||
|
||||
/* https://wicg.github.io/direct-sockets/#dom-tcpsocketopeninfo */
|
||||
interface TCPSocketOpenInfo {
|
||||
readable: ReadableStream<Uint8Array>;
|
||||
writable: WritableStream<Uint8Array>;
|
||||
readable: ReadableStream<Uint8Array>;
|
||||
writable: WritableStream<Uint8Array>;
|
||||
|
||||
remoteAddress: string;
|
||||
remotePort: number;
|
||||
remoteAddress: string;
|
||||
remotePort: number;
|
||||
|
||||
localAddress: string;
|
||||
localPort: number;
|
||||
localAddress: string;
|
||||
localPort: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://wicg.github.io/direct-sockets/#dom-tcpsocket
|
||||
*/
|
||||
declare class TCPSocket {
|
||||
constructor(remoteAddress: string,
|
||||
remotePort: number,
|
||||
options?: TCPSocketOptions);
|
||||
constructor(
|
||||
remoteAddress: string,
|
||||
remotePort: number,
|
||||
options?: TCPSocketOptions,
|
||||
);
|
||||
|
||||
opened: Promise<TCPSocketOpenInfo>;
|
||||
closed: Promise<void>;
|
||||
opened: Promise<TCPSocketOpenInfo>;
|
||||
closed: Promise<void>;
|
||||
|
||||
close(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
/* https://wicg.github.io/direct-sockets/#dom-tcpserversocketoptions */
|
||||
interface TCPServerSocketOptions {
|
||||
localPort?: number;
|
||||
backlog?: number;
|
||||
localPort?: number;
|
||||
backlog?: number;
|
||||
|
||||
ipv6Only?: boolean;
|
||||
ipv6Only?: boolean;
|
||||
}
|
||||
|
||||
/* https://wicg.github.io/direct-sockets/#dom-tcpserversocketopeninfo */
|
||||
interface TCPServerSocketOpenInfo {
|
||||
readable: ReadableStream<TCPSocket>;
|
||||
readable: ReadableStream<TCPSocket>;
|
||||
|
||||
localAddress: string;
|
||||
localPort: number;
|
||||
localAddress: string;
|
||||
localPort: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://wicg.github.io/direct-sockets/#dom-tcpserversocket
|
||||
*/
|
||||
declare class TCPServerSocket {
|
||||
constructor(localAddress: string,
|
||||
options?: TCPServerSocketOptions);
|
||||
constructor(localAddress: string, options?: TCPServerSocketOptions);
|
||||
|
||||
opened: Promise<TCPServerSocketOpenInfo>;
|
||||
closed: Promise<void>;
|
||||
opened: Promise<TCPServerSocketOpenInfo>;
|
||||
closed: Promise<void>;
|
||||
|
||||
close(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @see https://wicg.github.io/direct-sockets/
|
||||
*/
|
||||
|
||||
interface UDPSocketOptions {
|
||||
/**
|
||||
* L'adresse IP distante à laquelle se connecter. Si elle est spécifiée, `remotePort` doit également l'être.
|
||||
*/
|
||||
remoteAddress?: string;
|
||||
/**
|
||||
* Le port distant auquel se connecter.
|
||||
*/
|
||||
remotePort?: number;
|
||||
/**
|
||||
* L'adresse IP locale à laquelle se lier.
|
||||
*/
|
||||
localAddress?: string;
|
||||
/**
|
||||
* Le port local auquel se lier.
|
||||
*/
|
||||
localPort?: number;
|
||||
/**
|
||||
* La taille du tampon de réception en octets.
|
||||
*/
|
||||
receiveBufferSize?: number;
|
||||
/**
|
||||
* La taille du tampon d'envoi en octets.
|
||||
*/
|
||||
sendBufferSize?: number;
|
||||
/**
|
||||
* L'adresse IP distante à laquelle se connecter. Si elle est spécifiée, `remotePort` doit également l'être.
|
||||
*/
|
||||
remoteAddress?: string;
|
||||
/**
|
||||
* Le port distant auquel se connecter.
|
||||
*/
|
||||
remotePort?: number;
|
||||
/**
|
||||
* L'adresse IP locale à laquelle se lier.
|
||||
*/
|
||||
localAddress?: string;
|
||||
/**
|
||||
* Le port local auquel se lier.
|
||||
*/
|
||||
localPort?: number;
|
||||
/**
|
||||
* La taille du tampon de réception en octets.
|
||||
*/
|
||||
receiveBufferSize?: number;
|
||||
/**
|
||||
* La taille du tampon d'envoi en octets.
|
||||
*/
|
||||
sendBufferSize?: number;
|
||||
}
|
||||
|
||||
interface UDPMessage {
|
||||
/**
|
||||
* Les données reçues.
|
||||
*/
|
||||
data: ArrayBuffer;
|
||||
/**
|
||||
* L'adresse IP de l'expéditeur.
|
||||
*/
|
||||
remoteAddress: string;
|
||||
/**
|
||||
* Le port de l'expéditeur.
|
||||
*/
|
||||
remotePort: number;
|
||||
/**
|
||||
* Les données reçues.
|
||||
*/
|
||||
data: ArrayBuffer;
|
||||
/**
|
||||
* L'adresse IP de l'expéditeur.
|
||||
*/
|
||||
remoteAddress: string;
|
||||
/**
|
||||
* Le port de l'expéditeur.
|
||||
*/
|
||||
remotePort: number;
|
||||
}
|
||||
|
||||
declare class UDPSocket {
|
||||
/**
|
||||
* Crée une nouvelle instance de UDPSocket.
|
||||
* @param options Les options pour configurer le socket.
|
||||
*/
|
||||
constructor(options?: UDPSocketOptions);
|
||||
/**
|
||||
* Crée une nouvelle instance de UDPSocket.
|
||||
* @param options Les options pour configurer le socket.
|
||||
*/
|
||||
constructor(options?: UDPSocketOptions);
|
||||
|
||||
/**
|
||||
* Une promesse qui se résout lorsque le socket est ouvert et prêt à être utilisé.
|
||||
*/
|
||||
readonly opened: Promise<UDPSocketOpenInfo>;
|
||||
/**
|
||||
* Une promesse qui se résout lorsque le socket est ouvert et prêt à être utilisé.
|
||||
*/
|
||||
readonly opened: Promise<UDPSocketOpenInfo>;
|
||||
|
||||
/**
|
||||
* Une promesse qui se résout lorsque le socket est fermé.
|
||||
*/
|
||||
readonly closed: Promise<void>;
|
||||
/**
|
||||
* Une promesse qui se résout lorsque le socket est fermé.
|
||||
*/
|
||||
readonly closed: Promise<void>;
|
||||
|
||||
/**
|
||||
* Un `ReadableStream` pour lire les paquets UDP entrants.
|
||||
* Chaque morceau du flux est un objet `UDPMessage`.
|
||||
*/
|
||||
readonly readable: ReadableStream<UDPMessage>;
|
||||
/**
|
||||
* Un `ReadableStream` pour lire les paquets UDP entrants.
|
||||
* Chaque morceau du flux est un objet `UDPMessage`.
|
||||
*/
|
||||
readonly readable: ReadableStream<UDPMessage>;
|
||||
|
||||
/**
|
||||
* Un `WritableStream` pour écrire des données dans le socket.
|
||||
* Les données peuvent être un `ArrayBuffer`, une `TypedArray`, ou une `DataView`.
|
||||
* Si le socket est connecté, les données sont envoyées à l'adresse distante configurée.
|
||||
* Sinon, chaque écriture doit être un objet avec des propriétés `data`, `remoteAddress`, et `remotePort`.
|
||||
*/
|
||||
readonly writable: WritableStream<ArrayBuffer | ArrayBufferView | { data: ArrayBuffer | ArrayBufferView; remoteAddress: string; remotePort: number; }>;
|
||||
/**
|
||||
* Un `WritableStream` pour écrire des données dans le socket.
|
||||
* Les données peuvent être un `ArrayBuffer`, une `TypedArray`, ou une `DataView`.
|
||||
* Si le socket est connecté, les données sont envoyées à l'adresse distante configurée.
|
||||
* Sinon, chaque écriture doit être un objet avec des propriétés `data`, `remoteAddress`, et `remotePort`.
|
||||
*/
|
||||
readonly writable: WritableStream<
|
||||
| ArrayBuffer
|
||||
| ArrayBufferView
|
||||
| {
|
||||
data: ArrayBuffer | ArrayBufferView;
|
||||
remoteAddress: string;
|
||||
remotePort: number;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* L'adresse locale à laquelle le socket est lié.
|
||||
*/
|
||||
readonly localAddress?: string;
|
||||
/**
|
||||
* L'adresse locale à laquelle le socket est lié.
|
||||
*/
|
||||
readonly localAddress?: string;
|
||||
|
||||
/**
|
||||
* Le port local auquel le socket est lié.
|
||||
*/
|
||||
readonly localPort?: number;
|
||||
/**
|
||||
* Le port local auquel le socket est lié.
|
||||
*/
|
||||
readonly localPort?: number;
|
||||
|
||||
/**
|
||||
* L'adresse distante par défaut pour le socket.
|
||||
*/
|
||||
readonly remoteAddress?: string;
|
||||
/**
|
||||
* L'adresse distante par défaut pour le socket.
|
||||
*/
|
||||
readonly remoteAddress?: string;
|
||||
|
||||
/**
|
||||
* Le port distant par défaut pour le socket.
|
||||
*/
|
||||
readonly remotePort?: number;
|
||||
/**
|
||||
* Le port distant par défaut pour le socket.
|
||||
*/
|
||||
readonly remotePort?: number;
|
||||
|
||||
/**
|
||||
* Ferme le socket UDP.
|
||||
* @param reason La raison de la fermeture.
|
||||
*/
|
||||
close(reason?: any): Promise<void>;
|
||||
/**
|
||||
* Ferme le socket UDP.
|
||||
* @param reason La raison de la fermeture.
|
||||
*/
|
||||
close(reason?: any): Promise<void>;
|
||||
}
|
||||
|
||||
interface UDPSocketOpenInfo {
|
||||
readable: ReadableStream;
|
||||
writable: WritableStream;
|
||||
readable: ReadableStream;
|
||||
writable: WritableStream;
|
||||
|
||||
remoteAddress: string;
|
||||
remotePort: number;
|
||||
remoteAddress: string;
|
||||
remotePort: number;
|
||||
|
||||
localAddress: string;
|
||||
localPort: number;
|
||||
}
|
||||
localAddress: string;
|
||||
localPort: number;
|
||||
}
|
||||
|
||||
+45
-12
@@ -1,22 +1,56 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { resolve } from "path";
|
||||
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { resolve } from 'path'
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
base: "./",
|
||||
build: {
|
||||
assetsInlineLimit: 0,
|
||||
outDir: 'dist',
|
||||
outDir: "dist",
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: '[name].js',
|
||||
chunkFileNames: '[name].js',
|
||||
assetFileNames: '[name].[ext]'
|
||||
entryFileNames: "[name].js",
|
||||
chunkFileNames: "[name].js",
|
||||
assetFileNames: "[name].[ext]",
|
||||
manualChunks(id) {
|
||||
if (!id.includes("node_modules")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
id.includes("@chainsafe") ||
|
||||
id.includes("@libp2p") ||
|
||||
id.includes("it-handshake") ||
|
||||
id.includes("it-pipe") ||
|
||||
id.includes("it-stream") ||
|
||||
id.includes("it-ws") ||
|
||||
id.includes("msgpackr") ||
|
||||
id.includes("stream-to-it") ||
|
||||
id.includes("uint8arraylist")
|
||||
) {
|
||||
return "ligolo-session";
|
||||
}
|
||||
|
||||
if (
|
||||
id.includes("@heroui") ||
|
||||
id.includes("@iconify") ||
|
||||
id.includes("framer-motion")
|
||||
) {
|
||||
return "ui-vendor";
|
||||
}
|
||||
|
||||
if (id.includes("react")) {
|
||||
return "react-vendor";
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
input: {
|
||||
main: resolve(__dirname, 'index.html')
|
||||
main: resolve(__dirname, "index.html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -25,5 +59,4 @@ export default defineConfig({
|
||||
hmr: false,
|
||||
},
|
||||
plugins: [react(), tsconfigPaths(), tailwindcss()],
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user