add CI, refactoring

This commit is contained in:
Nicolas Chatelain
2026-03-26 15:32:49 +01:00
parent 9dd24bd264
commit 301ab84266
18 changed files with 1231 additions and 476 deletions
+53
View File
@@ -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
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
package-lock=true
+100
View File
@@ -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"
+35 -1
View File
@@ -1 +1,35 @@
# Ligolo-IWA
# Ligolo-IWA
[![GPLv3](https://img.shields.io/badge/License-GPLv3-brightgreen.svg)](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
View File
@@ -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"
/>
+4 -4
View File
@@ -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
View File
@@ -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>
+4 -1
View File
@@ -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,
+13 -13
View File
@@ -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>
+12 -2
View File
@@ -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>
);
+19 -28
View File
@@ -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);
}}
>
+2 -9
View File
@@ -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}`,
);
+433
View File
@@ -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
View File
@@ -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
View File
@@ -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>,
+125 -118
View File
@@ -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
View File
@@ -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()],
})
});