mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-05-06 15:47:52 +02:00
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:
committed by
GitHub
parent
a674633f6c
commit
6073964768
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user