DARKNET: Darkweb Expansion Project & Bitnode (#2139)

This is BN15. It is a really big change; see the PR for all the details.
This commit is contained in:
Michael Ficocelli
2026-02-03 06:40:36 -05:00
committed by GitHub
parent a674633f6c
commit 6073964768
225 changed files with 15010 additions and 526 deletions
+195
View File
@@ -0,0 +1,195 @@
import React, { useState } from "react";
import { Typography, Select, MenuItem, Card } from "@mui/material";
import { cleanUpLabyrinthLocations, DarknetState, getServerState, LogEntry } from "../models/DarknetState";
import {
getLabMaze,
getLabyrinthDetails,
getLabyrinthLocationReport,
getSurroundingsVisualized,
} from "../effects/labyrinth";
import { dnetStyles } from "./dnetStyles";
import { Player } from "@player";
import { useCycleRerender } from "../../ui/React/hooks";
import { findRunningScriptByPid } from "../../Script/ScriptHelpers";
import { GetServerOrThrow } from "../../Server/AllServers";
import { assertPasswordResponse, isPasswordResponse } from "../models/DarknetServerOptions";
export type LabyrinthSummaryProps = {
isAuthenticating: boolean;
};
export const LabyrinthSummary = ({ isAuthenticating }: LabyrinthSummaryProps): React.ReactElement => {
const [currentPerspective, setCurrentPerspective] = useState<number>(-1);
const { classes } = dnetStyles({});
useCycleRerender();
const lab = getLabyrinthDetails();
if (lab.cha > Player.skills.charisma) {
return <Typography color="error">You don't yet have the wits needed to attempt the labyrinth.</Typography>;
}
cleanUpLabyrinthLocations();
const [x, y] = DarknetState.labLocations[currentPerspective] ? DarknetState.labLocations[currentPerspective] : [1, 1];
const surroundings = getSurroundingsVisualized(getLabMaze(), x, y, 3, true, true)
.split("")
.map((c) => `${c}${c}${c}`)
.join("")
.replace("@@@", " @ ")
.replace("XXX", " X ")
.split("\n")
.map((line) => `${line}\n${line.replace("@", " ").replace("X", " ")}`)
.join("");
const getMenuItems = () => {
const darknetScripts = Object.keys(DarknetState.labLocations).map((pid) => findRunningScriptByPid(Number(pid)));
const scriptOptions = [];
for (const script of darknetScripts) {
if (!script) {
continue;
}
const scriptServer = GetServerOrThrow(script.server);
const connectedToLab = scriptServer.serversOnNetwork.includes(lab.name);
scriptOptions.push(
<MenuItem key={script.pid} value={Number(script.pid)} disabled={!connectedToLab}>
{`PID ${script.pid}: ${script.server} - ${!connectedToLab ? "(Not connected to lab)" : script.filename}`}
</MenuItem>,
);
}
if (lab.manual) {
return [
<MenuItem key={-1} value={-1}>
Manual UI
</MenuItem>,
...scriptOptions,
];
}
return scriptOptions;
};
const getMenu = () => {
// With non-manual labyrinth, return immediately if there are no pids navigating the labyrinth.
if (Object.keys(DarknetState.labLocations).length === 1 && !lab.manual) {
return <Typography>(No scripts found)</Typography>;
}
let perspective = currentPerspective;
// This happens when a script navigating the labyrinth dies.
if (perspective !== -1 && !DarknetState.labLocations[perspective]) {
perspective = -1;
}
// With non-manual labyrinth, if there are pids navigating the labyrinth and the perspective is not one of them, set
// the perspective to one of those pids.
if (perspective === -1 && !lab.manual) {
const ids = Object.keys(DarknetState.labLocations).filter((k) => Number(k) !== -1);
perspective = Number(ids[0]);
}
// Set the React state if necessary.
if (perspective !== currentPerspective) {
setCurrentPerspective(perspective);
}
return (
<Select
value={perspective}
label="Perspective to view"
onChange={(val) => {
setCurrentPerspective(Number(val.target.value));
}}
style={{ maxWidth: "250px" }}
>
{getMenuItems()}
</Select>
);
};
const getLogs = () =>
getServerState(lab.name)
.serverLogs.filter((log) => log.pid === currentPerspective)
.slice(0, 2)
.map(stringifyLog)
.join("\n") || "(no response yet)";
const stringifyLog = (log: LogEntry) => {
if (typeof log.message === "string") return log.message;
const json = JSON.stringify(log.message, null, 2);
const surroundings = (log.message.data ?? "").replaceAll("\n", "\n ");
return json.replace(/("data": )("[^"]*")/g, `$1"${surroundings}"`);
};
const getManualFeedback = () => {
if (isAuthenticating) {
return "Travelling...";
}
if (currentPerspective !== -1) {
return `You are following the progress of pid ${currentPerspective} instead of the manual mode.`;
}
const lastLog = getServerState(lab.name).serverLogs.find(
(log) => log.pid === -1 && isPasswordResponse(log.message),
);
if (lastLog == null) {
return "";
}
assertPasswordResponse(lastLog.message);
return lastLog.message.message;
};
const getLocationStatusString = () => {
const dataString = JSON.stringify(getLabyrinthLocationReport(currentPerspective));
// Add a zero width space before the success flag so the text can wrap for better readability
return dataString.replace(`,"success"`, `,\u200B"success"`);
};
return (
<>
<div className={classes.inlineFlexBox}>
<div style={{ width: "50%" }}>
{!lab.manual ? (
<Typography style={{ fontStyle: "italic", paddingRight: "10px" }}>
This lab cannot be completed manually. Select a script PID that is attempting the labyrinth from the
options below to view its progress.
</Typography>
) : (
<>
<Typography>
Manual mode feedback: <br />
{getManualFeedback()}
</Typography>
<Typography>Current Surroundings:</Typography>
<pre className={classes.maze}>{surroundings}</pre>
<Typography>
Current Coordinates: {x},{y}
</Typography>
</>
)}
</div>
<div style={{ width: "50%" }}>
<Typography variant="caption" color="secondary">
Logs scraped via <pre style={{ display: "inline" }}>heartbleed</pre>:
</Typography>
<Card style={{ padding: "8px", minHeight: "270px", marginBottom: "8px" }}>
<div style={{ color: "white" }}>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>{getLogs()}</pre>
</div>
</Card>
<Typography variant="caption" color="secondary">
ns.dnet.labreport:
</Typography>
<Card style={{ padding: "8px" }}>
<div style={{ color: "white" }}>
<pre style={{ whiteSpace: "pre-wrap", margin: 0, fontSize: "10.5px" }}>{getLocationStatusString()}</pre>
</div>
</Card>
</div>
</div>
<br />
<br />
<div style={{ display: "inline-flex", alignItems: "left", gap: "8px" }}>
<Typography>Script/perspective to follow: </Typography>
{getMenu()}
</div>
</>
);
};
+368
View File
@@ -0,0 +1,368 @@
import React, {
useEffect,
useRef,
useState,
useMemo,
useCallback,
type PointerEventHandler,
type WheelEventHandler,
} from "react";
import { Container, Typography, Button, Box, Tooltip } from "@mui/material";
import { ZoomIn, ZoomOut } from "@mui/icons-material";
import { throttle } from "lodash";
import { ServerStatusBox } from "./ServerStatusBox";
import { useRerender } from "../../ui/React/hooks";
import { DarknetEvents, DarknetState } from "../models/DarknetState";
import { SpecialServers } from "../../Server/data/SpecialServers";
import { drawOnCanvas, getPixelPosition } from "./networkCanvas";
import { dnetStyles } from "./dnetStyles";
import { getLabyrinthDetails, isLabyrinthServer } from "../effects/labyrinth";
import { DarknetServer } from "../../Server/DarknetServer";
import { getAllDarknetServers } from "../utils/darknetNetworkUtils";
import { ServerDetailsModal } from "./ServerDetailsModal";
import { AutoCompleteSearchBox } from "../../ui/AutoCompleteSearchBox";
import { getDarknetServerOrThrow } from "../utils/darknetServerUtils";
import { getServerLogs } from "../models/packetSniffing";
import { getTimeoutChance } from "../effects/offlineServerHandling";
import { DocumentationLink } from "../../ui/React/DocumentationLink";
import { Settings } from "../../Settings/Settings";
const DW_NET_WIDTH = 6000;
const DW_NET_HEIGHT = 12000;
const initialSearchLabel = `Search:`;
export function NetworkDisplayWrapper(): React.ReactElement {
const rerender = useRerender();
const draggableBackground = useRef<HTMLDivElement>(null);
const canvas = useRef<HTMLCanvasElement>(null);
const [zoomIndex, setZoomIndex] = useState(DarknetState.zoomIndex);
const [netDisplayDepth, setNetDisplayDepth] = useState<number>(1);
const [searchLabel, setSearchLabel] = useState<string>(initialSearchLabel);
const [serverOpened, setServerOpened] = useState<DarknetServer | null>(null);
const zoomOptions = useMemo(() => [0.12, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.75, 1, 1.3], []);
const { classes } = dnetStyles({});
const instability = getTimeoutChance();
const instabilityText = instability > 0.01 ? `${(instability * 100).toFixed(1)}%` : "< 1%";
const scrollTo = useCallback(
(top: number, left: number) => {
DarknetState.netViewTopScroll = top;
DarknetState.netViewLeftScroll = left;
draggableBackground?.current?.scrollTo({
top: top,
left: left,
behavior: "instant",
});
},
[draggableBackground],
);
useEffect(() => {
const clearSubscription = DarknetEvents.subscribe(() => {
if (canvas.current) {
const lab = getLabyrinthDetails().lab;
const startingDepth = lab && getServerLogs(lab, 1, true).length ? lab.depth : 0;
const deepestServer = DarknetState.Network.flat().reduce((deepest, server) => {
if (server?.hasAdminRights && server.depth > deepest) {
return server.depth;
}
return deepest;
}, startingDepth);
const visibilityMargin = DarknetState.showFullNetwork ? 99 : 3;
setNetDisplayDepth(deepestServer + visibilityMargin);
rerender();
drawOnCanvas(canvas.current);
}
});
canvas.current && drawOnCanvas(canvas.current);
draggableBackground.current?.addEventListener("wheel", (e) => e.preventDefault());
scrollTo(DarknetState.netViewTopScroll, DarknetState.netViewLeftScroll);
return () => {
clearSubscription();
};
}, [rerender, scrollTo]);
useEffect(() => {
DarknetEvents.emit();
}, []);
const allowAuth = (server: DarknetServer | null) =>
!!server &&
(server.hasAdminRights ||
server.serversOnNetwork.some((neighbor) => getDarknetServerOrThrow(neighbor).hasAdminRights));
const darkWebRoot = getDarknetServerOrThrow(SpecialServers.DarkWeb);
const labDetails = getLabyrinthDetails();
const labyrinth = labDetails.lab;
const depth = labDetails.depth;
const handleDragStart: PointerEventHandler<HTMLDivElement> = (pointerEvent) => {
const target = pointerEvent.target as HTMLDivElement;
const background = draggableBackground.current;
if (target.id === "draggableBackgroundTarget") {
background?.setPointerCapture(pointerEvent.pointerId);
}
};
const handleDragEnd: PointerEventHandler<HTMLDivElement> = (pointerEvent) => {
const target = pointerEvent.target as HTMLDivElement;
const background = draggableBackground.current;
if (target.id === "draggableBackgroundTarget") {
background?.releasePointerCapture(pointerEvent.pointerId);
}
DarknetEvents.emit();
};
const handleDrag: PointerEventHandler<HTMLDivElement> = (pointerEvent) => {
const background = draggableBackground.current;
if (background?.hasPointerCapture(pointerEvent.pointerId)) {
scrollTo(background?.scrollTop - pointerEvent.movementY, (background?.scrollLeft ?? 0) - pointerEvent.movementX);
}
};
const zoomIn = useCallback(() => {
if (zoomIndex >= zoomOptions.length - 1) {
return;
}
DarknetState.zoomIndex = Math.max(zoomIndex + 1, 0);
setZoomIndex(DarknetState.zoomIndex);
const zoom = zoomOptions[zoomIndex];
const background = draggableBackground.current;
scrollTo(
(background?.scrollTop ?? 0) + ((background?.clientHeight ?? 0) / 4) * zoom,
(background?.scrollLeft ?? 0) + ((background?.clientWidth ?? 0) / 4) * zoom,
);
}, [zoomIndex, setZoomIndex, zoomOptions, scrollTo]);
const zoomOut = useCallback(() => {
if (zoomIndex <= 0) {
return;
}
DarknetState.zoomIndex = Math.min(zoomIndex - 1, zoomOptions.length - 1);
setZoomIndex(DarknetState.zoomIndex);
const zoom = zoomOptions[zoomIndex];
const background = draggableBackground.current;
scrollTo(
(background?.scrollTop ?? 0) - ((background?.clientHeight ?? 0) / 4) * zoom,
(background?.scrollLeft ?? 0) - ((background?.clientWidth ?? 0) / 4) * zoom,
);
}, [zoomIndex, setZoomIndex, zoomOptions, scrollTo]);
const zoom = useCallback(
(wheelEvent: WheelEvent) => {
const target = wheelEvent.target as HTMLDivElement;
if (!draggableBackground.current || DarknetState.openServer) {
return;
}
if (wheelEvent.deltaY < 0) {
zoomIn();
} else {
zoomOut();
}
if (!target?.parentElement?.getBoundingClientRect()) {
return;
}
},
[draggableBackground, zoomOut, zoomIn],
);
const zoomRef = useRef(zoom);
useEffect(() => {
zoomRef.current = zoom;
}, [zoom]);
// creating throttled callback only once - on mount
const throttledZoom = useMemo(() => {
const func = (wheelEvent: WheelEvent) => {
zoomRef.current?.(wheelEvent);
};
return throttle(func, 200);
}, []);
const handleZoom: WheelEventHandler<HTMLDivElement> = (wheelEvent) => {
wheelEvent.stopPropagation();
throttledZoom(wheelEvent as unknown as WheelEvent);
};
const isWithinScreen = (server: DarknetServer) => {
const { left, top } = getPixelPosition(server, true);
const background = draggableBackground.current;
const buffer = 600;
const visibleAreaLeftEdge = (background?.scrollLeft ?? 0) / zoomOptions[zoomIndex];
const visibleAreaTopEdge = (background?.scrollTop ?? 0) / zoomOptions[zoomIndex];
const visibleAreaRightEdge =
visibleAreaLeftEdge + ((background?.clientWidth ?? 0) / zoomOptions[zoomIndex] ** 2 || window.innerWidth);
const visibleAreaBottomEdge =
visibleAreaTopEdge + ((background?.clientHeight ?? 0) / zoomOptions[zoomIndex] ** 2 || window.innerHeight);
return (
left >= visibleAreaLeftEdge - buffer &&
left <= visibleAreaRightEdge + buffer &&
top >= visibleAreaTopEdge - buffer &&
top <= visibleAreaBottomEdge + buffer
);
};
const search = (selection: string, options: string[], searchTerm: string) => {
if (searchTerm.length === 1) {
return;
} // Ignore single character searches
if (!searchTerm) {
setSearchLabel(initialSearchLabel);
return;
}
const servers = getAllDarknetServers();
const foundServer =
servers.find((s) => s.hostname.toLowerCase() === selection.toLowerCase()) ||
servers.find((s) => s.hostname.toLowerCase() === options[0]?.toLowerCase());
if (!foundServer) {
setSearchLabel(`(No results)`);
return;
} else {
setSearchLabel(initialSearchLabel);
}
const position = getPixelPosition(foundServer, true);
const background = draggableBackground.current;
scrollTo(
position.top * zoomOptions[zoomIndex] - ((background?.clientHeight ?? 0) / 2 - 100),
position.left * zoomOptions[zoomIndex] - (background?.clientWidth ?? 0) / 2,
);
if (allowAuth(foundServer)) {
setServerOpened(foundServer);
}
};
const getAutocompleteSuggestionList = (): string[] => {
const servers = getAllDarknetServers()
.filter((s) => s.depth < netDisplayDepth && !isLabyrinthServer(s.hostname))
.map((s) => s.hostname);
if (labyrinth && netDisplayDepth > depth) {
return [...servers, labyrinth.hostname];
}
return servers;
};
return (
<Container maxWidth={false} disableGutters>
{serverOpened ? (
<ServerDetailsModal
open={!!serverOpened}
onClose={() => setServerOpened(null)}
server={serverOpened}
classes={classes}
/>
) : (
""
)}
{DarknetState.allowMutating ? (
<Box className={`${classes.inlineFlexBox}`}>
<Typography variant={"h5"} sx={{ fontWeight: "bold" }}>
Dark Net
</Typography>
{instability && (
<Tooltip
title={
<>
If too many darknet servers are backdoored, it will increase the chance that authentication <br />
attempts will return a 408 Request Timeout error (even if the password is correct). <br />
Most servers will eventually restart or go offline, which removes backdoors over time.
</>
}
>
<Typography variant={"subtitle1"} sx={{ fontStyle: "italic" }}>
{" "}
Instability: {instabilityText}
</Typography>
</Tooltip>
)}
</Box>
) : (
<Typography variant={"h6"} className={classes.gold}>
[WEBSTORM WARNING]
</Typography>
)}
<div
className={classes.NetWrapper}
ref={draggableBackground}
onPointerDown={handleDragStart}
onPointerUp={handleDragEnd}
onPointerMove={handleDrag}
onWheel={handleZoom}
>
<div
style={{
position: "relative",
width: `${DW_NET_WIDTH}px`,
height: `${DW_NET_HEIGHT}px`,
zoom: zoomOptions[zoomIndex],
cursor: "grab",
}}
id={"draggableBackgroundTarget"}
>
<canvas
ref={canvas}
width={DW_NET_WIDTH}
height={DW_NET_HEIGHT}
style={{ position: "absolute", zIndex: -1 }}
></canvas>
{darkWebRoot && <ServerStatusBox server={darkWebRoot} enableAuth={true} classes={classes} />}
{DarknetState.Network.slice(0, netDisplayDepth).map((row, i) =>
row.map(
(server, j) =>
server &&
isWithinScreen(server) && (
<ServerStatusBox server={server} key={`${i},${j}`} enableAuth={allowAuth(server)} classes={classes} />
),
),
)}
{labyrinth && netDisplayDepth > depth && (
<ServerStatusBox server={labyrinth} enableAuth={allowAuth(labyrinth)} classes={classes} />
)}
</div>
</div>
<div className={classes.zoomContainer}>
<Button className={classes.button} onClick={() => zoomIn()}>
<ZoomIn />
</Button>
<Button className={classes.button} onClick={() => zoomOut()}>
<ZoomOut />
</Button>
</div>
<Box className={`${classes.inlineFlexBox}`}>
<Typography component="div" display="flex">
<Typography display="flex" alignItems="center" paddingRight="1em">
{searchLabel}
</Typography>
<AutoCompleteSearchBox
placeholder="Search for server"
maxSuggestions={6}
suggestionList={getAutocompleteSuggestionList}
ignoredTextRegex={/ /g}
onSelection={(_, selection, options, searchValue) => {
search(selection, options, searchValue);
}}
width={300}
/>
</Typography>
<DocumentationLink
page="programming/darknet.md"
style={{ fontSize: "22px", padding: "0 20px", backgroundColor: Settings.theme.button }}
>
Darknet Docs
</DocumentationLink>
</Box>
</Container>
);
}
+186
View File
@@ -0,0 +1,186 @@
import React, { useState, useRef } from "react";
import { Button, Container, Card, TextField, Typography } from "@mui/material";
import { getPasswordType } from "../controllers/ServerGenerator";
import { dnetStyles } from "./dnetStyles";
import type { DarknetResult } from "@nsdefs";
import { getAuthResult } from "../effects/authentication";
import { DarknetEvents } from "../models/DarknetState";
import { LabyrinthSummary } from "./LabyrinthSummary";
import { getLabyrinthDetails, isLabyrinthServer } from "../effects/labyrinth";
import { ModelIds } from "../Enums";
import { sleep } from "../../utils/Utility";
import { getSharedChars } from "../utils/darknetAuthUtils";
import type { DarknetServer } from "../../Server/DarknetServer";
import { formatObjectWithColoredKeys } from "./uiUtilities";
export type PasswordPromptProps = {
server: DarknetServer;
onClose: () => void;
};
export const PasswordPrompt = ({ server, onClose }: PasswordPromptProps): React.ReactElement => {
const [inputPassword, setInputPassword] = useState(server.hasAdminRights ? server.password : "");
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [lastDarknetResultFromAuth, setLastDarknetResultFromAuth] = useState<DarknetResult | null>(null);
const { classes } = dnetStyles({});
const passwordInput = useRef<HTMLInputElement>(null);
const isLabServer = isLabyrinthServer(server.hostname);
const canEnterLabManually = getLabyrinthDetails().manual;
const disablePasswordInput = (!canEnterLabManually && isLabServer) || server.hasAdminRights;
if (isLabServer && !canEnterLabManually) {
return (
<>
<br />
<br />
<Typography>
The weight of the deep net presses down on you. It seems this place will challenge you to make your own
tools...
</Typography>
<br />
<br />
<LabyrinthSummary isAuthenticating={isAuthenticating} />
</>
);
}
async function attemptPassword(passwordAttempted: string): Promise<void> {
setIsAuthenticating(true);
const sharedChars =
server.modelId === ModelIds.TimingAttack ? getSharedChars(server.password, passwordAttempted) : 0;
const responseTime = 500 + sharedChars * 150;
await sleep(responseTime);
// Cancel if the component unmounted while waiting
if (passwordInput.current === null) {
return;
}
// Manual password entry counts as having two threads, to increase the cha xp slightly during early exploration
const authResult = getAuthResult(server, passwordAttempted, 2, responseTime);
// Do NOT carelessly move these setters, especially when moving them to after DarknetEvents.emit().
// DarknetEvents.emit() makes the parent component rerender, so this component may be unmounted. In that case,
// these calls will set the states of an unmounted component.
setIsAuthenticating(false);
setLastDarknetResultFromAuth(authResult.result);
if (authResult.result.success) {
DarknetEvents.emit("server-unlocked", server);
} else {
// This selects the text inside the password input field so that the player can immediately start typing a new
// guess without needing to clear out the old one.
// Do NOT move this line below DarknetEvents.emit(). DarknetEvents.emit() may make this component unmounted, so
// passwordInput.current may become null unexpectedly. Using the optional chaining operator for accessing
// passwordInput.current is specifically for the case in which somebody mistakenly moves this line.
passwordInput.current?.querySelector("input")?.select();
DarknetEvents.emit();
}
}
const handleSubmit = (e: React.FormEvent): void => {
e.preventDefault();
if (server.hasAdminRights) {
onClose();
return;
}
if (!isAuthenticating) {
attemptPassword(inputPassword).catch((error) => console.error(error));
}
};
let authFeedback;
if (isAuthenticating) {
authFeedback = "Checking password...";
} else {
if (lastDarknetResultFromAuth === null) {
authFeedback = "(no response yet)";
} else {
authFeedback = formatObjectWithColoredKeys(lastDarknetResultFromAuth, ["success", "message", "code"]);
}
}
return (
<>
<div className={classes.inlineFlexBox}>
<div>
<form onSubmit={(e) => handleSubmit(e)}>
<TextField
ref={passwordInput}
label="Password"
type="text"
value={inputPassword}
onChange={(e) => setInputPassword(e.target.value)}
variant="outlined"
autoComplete="off"
autoFocus={!server.hasAdminRights}
disabled={disablePasswordInput}
/>
</form>
<br />
<Button onClick={(e) => handleSubmit(e)} disabled={isAuthenticating}>
Submit Password
</Button>
<br />
<br />
<br />
{!isLabServer && (
<Typography variant="caption" color="secondary">
Logs scraped via <pre style={{ display: "inline" }}>heartbleed</pre>:
</Typography>
)}
</div>
<div style={{ width: "50%" }}>
<Container disableGutters>
{isLabServer ? (
<div style={{ color: "white" }}>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
<span className={classes.serverDetailsText}>Hint:</span> {server.staticPasswordHint}
<br />
<span className={classes.serverDetailsText}>Model:</span> {server.modelId}
<br />
<span className={classes.serverDetailsText}>Required charisma:</span> {server.requiredCharismaSkill}
<br />
</pre>
</div>
) : (
<div style={{ color: "white" }}>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
<span className={classes.serverDetailsText}>Hint:</span> {server.staticPasswordHint}
<br />
{server.passwordHintData && (
<>
<span className={classes.serverDetailsText}>Data: </span> {server.passwordHintData}
<br />
</>
)}
<span className={classes.serverDetailsText}>Length:</span> {server.password.length}
<br />
<span className={classes.serverDetailsText}>Format:</span> {getPasswordType(server.password)}
<br />
<span className={classes.serverDetailsText}>Model:</span> {server.modelId}
<br />
</pre>
</div>
)}
</Container>
<br />
{!isLabServer && (
<Card style={{ padding: "8px", minHeight: "60px", marginBottom: "8px" }}>
<div style={{ color: "white" }}>
{typeof authFeedback !== "string" ? (
authFeedback
) : (
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>{authFeedback}</pre>
)}
</div>
</Card>
)}
</div>
</div>
<br />
{isLabServer && <LabyrinthSummary isAuthenticating={isAuthenticating} />}
</>
);
};
+115
View File
@@ -0,0 +1,115 @@
import React from "react";
import { Modal } from "../../ui/React/Modal";
import { Container, Card, SvgIcon, Typography, Tooltip } from "@mui/material";
import { getIcon } from "./ServerIcon";
import { getServerState } from "../models/DarknetState";
import { ServerSummary } from "./ServerSummary";
import { populateServerLogsWithNoise } from "../models/packetSniffing";
import { isLabyrinthServer } from "../effects/labyrinth";
import { PasswordPrompt } from "./PasswordPrompt";
import { copyToClipboard, formatObjectWithColoredKeys, formatToMaxDigits } from "./uiUtilities";
import { useCycleRerender } from "../../ui/React/hooks";
import type { DarknetServer } from "../../Server/DarknetServer";
import { logBoxBaseZIndex } from "../../ui/React/Constants";
export type DWPasswordPromptModalProps = {
open: boolean;
onClose: () => void;
server: DarknetServer;
classes: {
[key: string]: string;
};
};
export const ServerDetailsModal = ({
open,
onClose,
server,
classes,
}: DWPasswordPromptModalProps): React.ReactElement => {
useCycleRerender();
const icon = getIcon(server.modelId);
populateServerLogsWithNoise(server);
const serverState = getServerState(server.hostname);
const isLabServer = isLabyrinthServer(server.hostname);
const recentLogs = serverState.serverLogs.slice(0, 5);
const ramBlock = server.blockedRam;
const blockedRamString = ramBlock ? formatToMaxDigits(ramBlock, 1) + "+" : "";
const usedRamString = formatToMaxDigits(server.ramUsed - ramBlock, 1);
const serverRamString = `RAM in use: ${blockedRamString}${usedRamString}/${server.maxRam} GB`;
const logContent = recentLogs.map((log, index) => (
<pre
key={index}
color="secondary"
style={{ borderLeft: "1px solid grey", paddingLeft: "3px", whiteSpace: "pre-wrap" }}
>
{typeof log.message === "string"
? log.message
: formatObjectWithColoredKeys(log.message, [
"message",
"data",
"passwordAttempted",
"passwordExpected",
"code",
])}
</pre>
));
const copyHostname = () => copyToClipboard(server.hostname);
return (
<Modal open={open} onClose={onClose} removeFocus={false} sx={{ zIndex: logBoxBaseZIndex - 1 }}>
<>
<Container sx={{ width: "calc(min(900px, 80vw))", minHeight: "500px" }}>
<div className={classes.inlineFlexBox}>
<Typography variant="h5" color={server.hasAdminRights ? "primary" : "secondary"} onClick={copyHostname}>
{server.hostname}
</Typography>
<Tooltip title={`Server Model: ${server.modelId}`}>
<SvgIcon component={icon} color="secondary" />
</Tooltip>
</div>
<br />
{server.hasAdminRights ? (
<>
<Typography>Password: "{server.password}"</Typography>
<br />
<Typography color="secondary">IP: {server.ip}</Typography>
<Typography color="secondary">Required charisma: {server.requiredCharismaSkill}</Typography>
<Tooltip
title={`Ram blocked by server owner: ${ramBlock} GB. Ram in use by scripts: ${
server.ramUsed - ramBlock
} GB.`}
>
<Typography color="secondary">{serverRamString}</Typography>
</Tooltip>
<Typography color="secondary">Model: {server.modelId}</Typography>
<br />
<div style={{ maxWidth: "300px" }}>
<ServerSummary server={server} enableAuth={true} showDetails={true} classes={classes} />
</div>
<br />
{isLabServer && (
<>
<br />
<Typography>You have successfully navigated the labyrinth! Congratulations!</Typography>
</>
)}
</>
) : (
<PasswordPrompt server={server} onClose={onClose} />
)}
{!isLabServer && (
<>
<Card style={{ height: "250px", overflowY: "scroll" }}>
<div style={{ color: "white", paddingLeft: "10px" }}>{logContent}</div>
</Card>
</>
)}
</Container>
</>
</Modal>
);
};
+86
View File
@@ -0,0 +1,86 @@
import {
ConnectedTv,
LaptopMac,
DesktopMac,
Dns,
PhoneIphone,
Terminal,
SatelliteAlt,
Dvr,
Microwave,
ElectricCar,
Blender,
LiveTv,
Subtitles,
Web,
ExitToApp,
SignalWifiStatusbarConnectedNoInternet4,
Calculate,
Watch,
NoCell,
SettingsPower,
VideogameAsset,
AccountBalance,
Elevator,
Fax,
AssuredWorkload,
SvgIconComponent,
} from "@mui/icons-material";
import { ModelIds } from "../Enums";
export const getIcon = (model: string): SvgIconComponent => {
switch (model) {
case ModelIds.EchoVuln:
return ConnectedTv;
case ModelIds.SortedEchoVuln:
return LaptopMac;
case ModelIds.NoPassword:
return PhoneIphone;
case ModelIds.Captcha:
return Dns;
case ModelIds.DefaultPassword:
return LiveTv;
case ModelIds.BufferOverflow:
return Terminal;
case ModelIds.MastermindHint:
return SatelliteAlt;
case ModelIds.TimingAttack:
return Fax;
case ModelIds.LargestPrimeFactor:
return Calculate;
case ModelIds.RomanNumeral:
return Watch;
case ModelIds.DogNames:
return DesktopMac;
case ModelIds.GuessNumber:
return Dvr;
case ModelIds.CommonPasswordDictionary:
return Subtitles;
case ModelIds.EUCountryDictionary:
return Web;
case ModelIds.Yesn_t:
return NoCell;
case ModelIds.BinaryEncodedFeedback:
return SettingsPower;
case ModelIds.SpiceLevel:
return Microwave;
case ModelIds.ConvertToBase10:
return VideogameAsset;
case ModelIds.parsedExpression:
return AccountBalance;
case ModelIds.divisibilityTest:
return ElectricCar;
case ModelIds.tripleModulo:
return Blender;
case ModelIds.globalMaxima:
return Elevator;
case ModelIds.packetSniffer:
return SignalWifiStatusbarConnectedNoInternet4;
case ModelIds.encryptedPassword:
return AssuredWorkload;
case ModelIds.labyrinth:
return ExitToApp;
default:
return ConnectedTv;
}
};
+71
View File
@@ -0,0 +1,71 @@
import React, { useState } from "react";
import { Typography, SvgIcon, Tooltip } from "@mui/material";
import { ServerDetailsModal } from "./ServerDetailsModal";
import { getIcon } from "./ServerIcon";
import { DarknetState } from "../models/DarknetState";
import { getPixelPosition } from "./networkCanvas";
import { ServerSummary } from "./ServerSummary";
import type { DarknetServer } from "../../Server/DarknetServer";
import { DWServerStyles, ServerName } from "./dnetStyles";
export type DWServerProps = {
server: DarknetServer;
enableAuth: boolean;
classes: {
[key: string]: string;
};
};
export function ServerStatusBox({ server, enableAuth, classes }: DWServerProps): React.ReactElement {
const [open, setOpen] = useState(false);
const icon = getIcon(server.modelId);
const authButtonHandler = () => {
DarknetState.openServer = server;
setOpen(true);
};
const handleClose = () => {
DarknetState.openServer = null;
setOpen(false);
};
const getServerStyles = (server: DarknetServer) => {
const position = getPixelPosition(server);
return {
...DWServerStyles,
top: `${position.top}px`,
left: `${position.left}px`,
borderColor: server.hasStasisLink ? "gold" : server.hasAdminRights ? "green" : "grey",
};
};
return (
<>
{open ? <ServerDetailsModal open={open} onClose={handleClose} server={server} classes={classes} /> : ""}
<button
style={{ ...getServerStyles(server), position: "absolute", userSelect: "none" }}
className={classes.DWServer}
onClick={authButtonHandler}
disabled={!enableAuth}
>
<div style={{ padding: 0, margin: 0, width: "100%" }}>
<div style={{ display: "inline-flex", flexDirection: "row", width: "100%", justifyContent: "space-between" }}>
<Tooltip title={`Server Model: ${server.modelId}`}>
<SvgIcon component={icon} color="secondary" />
</Tooltip>
<Typography color={server.hasAdminRights ? "primary" : "secondary"} sx={ServerName}>
{server.hostname}
</Typography>
</div>
<Typography color="secondary" style={{ fontSize: "0.9em" }}>
{server.ip} cha:{server.requiredCharismaSkill}
</Typography>
<br />
<ServerSummary server={server} enableAuth={enableAuth} classes={classes} />
</div>
</button>
</>
);
}
+151
View File
@@ -0,0 +1,151 @@
import React from "react";
import { SvgIcon, Tooltip, Typography } from "@mui/material";
import { Code, Description, Inventory2, LockPerson, Terminal, Bolt, DoorBackSharp } from "@mui/icons-material";
import { formatNumber } from "../../ui/formatNumber";
import { CompletedProgramName } from "@enums";
import { formatToMaxDigits } from "./uiUtilities";
import type { DarknetServer } from "../../Server/DarknetServer";
import { DarknetConstants } from "../Constants";
export type ServerSummaryProps = {
server: DarknetServer;
enableAuth: boolean;
showDetails?: boolean;
classes: {
[key: string]: string;
};
};
export function ServerSummary({
server,
enableAuth,
classes,
showDetails = false,
}: ServerSummaryProps): React.ReactElement {
if (!server.hasAdminRights && enableAuth) {
return <Typography>[ auth required ]</Typography>;
}
if (!server.hasAdminRights && !enableAuth) {
return <Typography color="secondary">(no connection)</Typography>;
}
const cacheCount = server.caches.length;
const dataFiles = Array.from(server.textFiles.keys()).filter((f) => f.endsWith(DarknetConstants.DataFileSuffix));
const textFiles = [...dataFiles, ...server.messages];
const fileCount = textFiles.length;
const textFilesTooltip =
textFiles.length > 0
? `Data files on server: ${textFiles.slice(0, 3).join(", ")}${
textFiles.length > 3 ? ` +${textFiles.length - 3}` : ""
}`
: "No data files on server";
const contractCount = server.contracts.length;
const runningScriptNames = Array.from(server.runningScriptMap.keys()).map((script) => script.replace("*[]", ""));
const runningScriptsTooltip =
runningScriptNames.length > 0
? `Running scripts on server: ${runningScriptNames.slice(0, 3).join(", ")}${
runningScriptNames.length > 3 ? ` +${runningScriptNames.length - 3}` : ""
}`
: "No running scripts on server";
const hasStormSeed = server.programs.includes(CompletedProgramName.stormSeed);
const hasBackdoor = server.backdoorInstalled && !server.hasStasisLink;
const ramBlockedDetails = formatToMaxDigits(server.blockedRam, 2) + "GB";
const ramBlocked = showDetails ? ramBlockedDetails : formatNumber(server.blockedRam, 0);
const runningScriptsComponent = (
<Tooltip key="runningScript" title={<>{runningScriptsTooltip}</>}>
<Typography color={runningScriptNames.length > 0 ? "primary" : "secondary"}>
<SvgIcon component={Terminal} className={classes.serverStatusIcon} />
{runningScriptNames.length}
</Typography>
</Tooltip>
);
const components = [];
if (cacheCount) {
components.push(
<Tooltip key="cache" title={<>Reward cache count: {cacheCount}</>}>
<Typography>
<SvgIcon component={Inventory2} className={`${classes.gold} ${classes.serverStatusIcon}`} />
{cacheCount}
</Typography>
</Tooltip>,
);
}
if (hasStormSeed) {
components.push(
<Tooltip key="stormSeed" title={<>A mysterious executable has been found here...</>}>
<Typography>
<SvgIcon component={Bolt} className={`${classes.gold} ${classes.serverStatusIcon}`} />?
</Typography>
</Tooltip>,
);
}
if (hasBackdoor) {
components.push(
<Tooltip key="backdoor" title={<>Backdoor installed. Warning: this increases darknet instability.</>}>
<Typography>
<SvgIcon component={DoorBackSharp} className={`${classes.red} ${classes.serverStatusIcon}`} />
</Typography>
</Tooltip>,
);
}
if (server.hasStasisLink) {
components.push(
<Tooltip
key="backdoor"
title={
<>
Stasis link installed. This allows connecting to the server remotely, as well as ns.exec from any distance.
</>
}
>
<Typography>
<SvgIcon component={DoorBackSharp} className={`${classes.gold} ${classes.serverStatusIcon}`} />
</Typography>
</Tooltip>,
);
}
if (contractCount) {
components.push(
<Tooltip key="contract" title={<>Coding contract count: {contractCount}</>}>
<Typography>
<SvgIcon component={Code} className={classes.serverStatusIcon} />
{contractCount}
</Typography>
</Tooltip>,
);
}
if (fileCount) {
components.push(
<Tooltip key="file" title={<>{textFilesTooltip}</>}>
<Typography color={fileCount ? "primary" : "secondary"}>
<SvgIcon component={Description} className={classes.serverStatusIcon} />
{fileCount}
</Typography>
</Tooltip>,
);
}
if (server.blockedRam) {
components.push(
<Tooltip
key="ramBlocked"
title={<>Ram blocked by owner: {ramBlockedDetails}. This can be freed up using ns.dnet.memoryReallocation()</>}
>
<Typography color={"secondary"}>
<SvgIcon component={LockPerson} className={classes.serverStatusIcon} />
{ramBlocked}
</Typography>
</Tooltip>,
);
}
const maxIcons = showDetails ? components.length : 2;
const componentsToShow = [...components.slice(0, maxIcons), runningScriptsComponent];
return (
<div style={{ display: "inline-flex", flexDirection: "row", width: "100%", justifyContent: "space-between" }}>
{componentsToShow}
</div>
);
}
+161
View File
@@ -0,0 +1,161 @@
import { Theme } from "@mui/material/styles";
import { makeStyles } from "tss-react/mui";
export const dwColors = ["hack", "hp", "money", "int", "cha", "rep", "success"] as const;
export type dwColors = (typeof dwColors)[number];
export const DW_SERVER_WIDTH = 240;
export const DW_SERVER_HEIGHT = 130;
export const DW_SERVER_GAP_TOP = 120;
export const DW_SERVER_GAP_LEFT = 60;
export const MAP_BORDER_WIDTH = 300;
export const dnetStyles = makeStyles<unknown, dwColors>({ uniqId: "dnetStyles" })((theme: Theme, __, __classes) => ({
DWServer: {
"&:hover": {
backgroundColor: "#333 !important",
},
},
NetWrapper: {
width: "100%",
height: "calc(100vh - 80px)",
overflow: "scroll",
position: "relative",
border: "solid 1px slategray",
},
button: {
color: theme.colors.white,
},
maze: {
color: theme.colors.white,
lineHeight: 0.55,
},
hiddenInput: {
width: 0,
height: 0,
padding: 0,
margin: 0,
opacity: 0,
},
zoomContainer: {
position: "absolute",
top: "calc(90vh - 38px)",
marginLeft: "1px",
display: "grid",
zIndex: 20,
["& > button"]: {
width: "40px",
minWidth: "40px !important",
},
},
inlineFlexBox: {
display: "inline-flex",
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
},
noPadding: {
padding: 0,
},
paddingRight: {
paddingRight: "3px",
},
serverStatusIcon: {
paddingRight: "3px",
position: "relative",
bottom: "-4px",
},
gold: {
color: theme.colors.money,
},
red: {
color: theme.colors.hp,
},
white: {
color: theme.colors.white,
},
authButton: {
["&:disabled"]: {
opacity: 0.5,
},
},
hack: {
borderColor: theme.colors.hack,
},
hp: {
borderColor: theme.colors.hp,
},
money: {
borderColor: theme.colors.money,
},
int: {
borderColor: theme.colors.int,
},
cha: {
borderColor: theme.colors.cha,
},
rep: {
borderColor: theme.colors.rep,
},
success: {
borderColor: theme.colors.success,
},
green: {
borderColor: "green",
},
grey: {
borderColor: "grey",
},
goldBorder: {
borderColor: "gold",
},
serverDetailsText: {
marginLeft: "-2em",
textIndent: "2em",
color: "grey",
},
}));
/*
React by default creates a new <style> element with duplicate css for each copy of each component that uses makeStyles.
To reduce the performance impact of that option, these styles are defined as an object literal and applied directly to
the element's style attribute. Also included is the relevant styles for Mui Button, for the same reason.
This is done instead of adding hundreds of <style> tags into the DOM, which in some cases took multiple seconds
waiting for insertBefore calls and reflowing the page when loading the darknet UI view.
*/
export const DWServerStyles = {
width: `${DW_SERVER_WIDTH}px`,
height: `${DW_SERVER_HEIGHT}px`,
borderWidth: "1px",
borderStyle: "solid",
padding: "8px",
borderRadius: "4px",
zIndex: 10,
cursor: "auto",
backgroundColor: "#000",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
outline: 0,
margin: 0,
verticalAlign: "middle",
textDecoration: "none",
fontFamily: 'JetBrainsMono, "Courier New", monospace',
fontWeight: 500,
fontSize: "0.875rem",
lineHeight: 1.75,
transition:
"background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms",
color: "#0c0",
};
export const DWServerLogStyles = { fontFamily: 'JetBrainsMono, "Courier New", monospace', fontSize: "12px" };
export const ServerName = {
padding: 0,
width: "86%",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
};
+90
View File
@@ -0,0 +1,90 @@
import { DarknetState } from "../models/DarknetState";
import {
DW_SERVER_GAP_LEFT,
DW_SERVER_GAP_TOP,
DW_SERVER_HEIGHT,
DW_SERVER_WIDTH,
MAP_BORDER_WIDTH,
} from "./dnetStyles";
import { SpecialServers } from "../../Server/data/SpecialServers";
import { getNetDepth, isLabyrinthServer } from "../effects/labyrinth";
import { NET_WIDTH } from "../Enums";
import type { DarknetServer } from "../../Server/DarknetServer";
import { getDarknetServerOrThrow } from "../utils/darknetServerUtils";
export const drawOnCanvas = (canvas: HTMLCanvasElement) => {
const ctx = canvas?.getContext("2d");
if (!ctx || !canvas) {
console.error("Could not get canvas context");
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const server of DarknetState.Network.flat()) {
if (
!server ||
(!server.hasAdminRights && !server.serversOnNetwork.find((s) => getDarknetServerOrThrow(s).hasAdminRights))
) {
continue;
}
// draw a line between each server and its connected servers
for (const connectedServerName of server.serversOnNetwork) {
const connectedServer = getDarknetServerOrThrow(connectedServerName);
if (
!connectedServer.hasAdminRights &&
!connectedServer.serversOnNetwork.find((s) => getDarknetServerOrThrow(s).hasAdminRights)
) {
continue;
}
ctx.beginPath();
const connectedColor = "green";
const disconnectedColor = "grey";
ctx.strokeStyle = server.hasAdminRights || connectedServer.hasAdminRights ? connectedColor : disconnectedColor;
const startPosition = getPixelPosition(server, true);
const endPosition = getPixelPosition(connectedServer, true);
ctx.moveTo(startPosition.left, startPosition.top);
ctx.lineTo(endPosition.left, endPosition.top);
ctx.stroke();
}
}
};
export const getPixelPosition = (server: DarknetServer, centered = false) => {
const centeredOffsetHorizontal = centered ? DW_SERVER_WIDTH / 2 : 0;
const centeredOffsetVertical = centered ? DW_SERVER_HEIGHT / 2 : 0;
if (server.hostname === SpecialServers.DarkWeb) {
return {
top: MAP_BORDER_WIDTH * 0.2 + (centered ? centeredOffsetVertical : 0),
left: (DW_SERVER_GAP_LEFT + DW_SERVER_WIDTH) * NET_WIDTH * 0.5 + (centered ? centeredOffsetHorizontal : 0),
};
} else if (isLabyrinthServer(server.hostname)) {
return {
top:
MAP_BORDER_WIDTH +
centeredOffsetVertical +
(DW_SERVER_GAP_TOP + DW_SERVER_HEIGHT) * getNetDepth() +
DW_SERVER_GAP_TOP,
left: (DW_SERVER_GAP_LEFT + DW_SERVER_WIDTH) * NET_WIDTH * 0.5 + (centered ? centeredOffsetHorizontal : 0),
};
}
const coords = getCoordinates(server);
const widthOfServers = (DW_SERVER_GAP_LEFT + DW_SERVER_WIDTH) * coords.y;
const staggeredHorizontalOffset = coords.x % 2 ? DW_SERVER_WIDTH / 2 : 0;
const heightOfServers = (DW_SERVER_GAP_TOP + DW_SERVER_HEIGHT) * coords.x;
return {
top: heightOfServers + MAP_BORDER_WIDTH + centeredOffsetVertical,
left: widthOfServers + MAP_BORDER_WIDTH + centeredOffsetHorizontal + staggeredHorizontalOffset,
};
};
const getCoordinates = (server: DarknetServer) => {
return {
x: server.depth,
y: server.leftOffset,
};
};
+41
View File
@@ -0,0 +1,41 @@
import React from "react";
import { SnackbarEvents } from "../../ui/React/Snackbar";
import { ToastVariant } from "@enums";
import { DWServerLogStyles } from "./dnetStyles";
export const formatToMaxDigits = (value: number, maxDigits: number): string => {
if (value === 0) return "0";
return parseFloat(value.toFixed(maxDigits)).toString();
};
export const copyToClipboard = (text: string): void => {
navigator.clipboard.writeText(text).catch((error) => console.error(error));
SnackbarEvents.emit(`Copied "${text}" to clipboard`, ToastVariant.SUCCESS, 2000);
};
export const formatObjectWithColoredKeys = (obj: Record<string, unknown>, filteredKeys?: string[]) => {
const filteredObject: Record<string, unknown> = {};
if (filteredKeys) {
for (const key of filteredKeys) {
if (key in obj) {
filteredObject[key] = obj[key];
}
}
} else {
Object.assign(filteredObject, obj);
}
return (
<span style={DWServerLogStyles}>
{Object.entries(filteredObject).map(([key, value]) => {
return (
<React.Fragment key={key}>
<span style={{ color: "grey" }}>{key}: </span>
{/* React does not render null, undefined, and boolean values */}
{typeof value !== "boolean" ? value : String(value)}
<br />
</React.Fragment>
);
})}
</span>
);
};