diff --git a/package.json b/package.json index d11a9cd..0c1b607 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "postcss-loader": "^6.2.0", "prettier": "^2.5.0", "style-loader": "^3.0.0", + "tailwind-scrollbar": "^1.3.1", "tailwindcss": "^2.2.19", "ts-loader": "^9.2.2", "typescript": "^4.0.2" diff --git a/resources/server.properties b/resources/server.properties index 1c5707d..3accca9 100644 --- a/resources/server.properties +++ b/resources/server.properties @@ -1,8 +1,8 @@ #Minecraft server properties enable-jmx-monitoring=false rcon.port=25575 -enable-command-block=false gamemode=survival +enable-command-block=false enable-query=true level-name=world motd=A Minecraft Server @@ -10,10 +10,10 @@ query.port=25565 pvp=true difficulty=easy network-compression-threshold=256 -max-tick-time=60000 require-resource-pack=false -max-players=20 +max-tick-time=60000 use-native-transport=true +max-players=20 online-mode=true enable-status=true allow-flight=false @@ -27,8 +27,10 @@ enable-rcon=true sync-chunk-writes=true op-permission-level=4 prevent-proxy-connections=false +hide-online-players=false resource-pack= entity-broadcast-range-percentage=100 +simulation-distance=10 rcon.password=multiserver player-idle-timeout=0 force-gamemode=false @@ -38,7 +40,6 @@ white-list=false broadcast-console-to-ops=true spawn-npcs=true spawn-animals=true -snooper-enabled=true function-permission-level=2 text-filtering-config= spawn-monsters=true diff --git a/src/app.global.css b/src/app.global.css index ff51ded..6d637e8 100644 --- a/src/app.global.css +++ b/src/app.global.css @@ -9,6 +9,5 @@ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: auto; - max-width: 38rem; - padding: 2rem; + padding: 1rem; } diff --git a/src/components/Repl.tsx b/src/components/Repl.tsx new file mode 100644 index 0000000..381e39c --- /dev/null +++ b/src/components/Repl.tsx @@ -0,0 +1,87 @@ +import React, { useState, useEffect, useRef } from "react"; + +import "../app.global.css"; + +interface ReplCommandData { + command: string; + data: string; +} + +interface ReplProps { + exec(command: string): Promise; +} + +const Repl = ({ exec }: ReplProps): JSX.Element => { + const [history, setHistory] = useState([]); + const [commands, setCommands] = useState([]); + const [command, setCommand] = useState(""); + const [isExec, setIsExec] = useState(false); + const [placeholder, setPlaceholder] = useState(""); + const [index, setIndex] = useState(0); + + const log = useRef(null); + const term = useRef(null); + + useEffect(() => { + setCommands(history.filter(({ command }) => command)); + + if (term.current) term.current.scrollTop = term.current?.scrollHeight; + }, [history]); + + useEffect(() => { + if (index && commands[index - 1]) + setCommand(commands[index - 1].command); + else if (!index) setCommand(""); + }, [index]); + + return ( +
+            
+ {[...history].reverse().map((e, i) => ( +
+ + {">"} {e.command} + +
{e.data}
+
+ ))} +
+ {placeholder && ( + + {">"} {placeholder} + + )} +
+ {!isExec && {"> "}} + setCommand(e.target.value)} + onKeyDown={async (e) => { + if (e.code === "Enter") { + if (command) { + setPlaceholder(command); + setCommand(""); + + const data = await exec(command); + setPlaceholder(""); + + setHistory([{ command, data }, ...history]); + return; + } + setHistory([{ command, data: "" }, ...history]); + } else if (e.code === "ArrowUp") { + if (index < commands.length) + return setIndex(index + 1); + } else if (e.code === "ArrowDown") { + if (index > 0) return setIndex(index - 1); + } + }} + /> +
+
+ ); +}; + +export default Repl; diff --git a/src/lib/instances/createInstance.ts b/src/lib/instances/createInstance.ts index 2613f15..a872aac 100644 --- a/src/lib/instances/createInstance.ts +++ b/src/lib/instances/createInstance.ts @@ -50,7 +50,7 @@ export async function createInstance( log.silly("Writing eula.txt"); await fs.writeFile(path.join(instanceRoot, "eula.txt"), "eula=true"); - log.silly("Writing server.properties using 1.17.1 template"); + log.silly("Writing server.properties using 1.18.1 template"); await fs.copyFile( path.join(resourcesPath, "server.properties"), path.join(instanceRoot, "server.properties") @@ -120,6 +120,64 @@ export async function createInstance( stream.on("finish", () => stream.close()); }); + // apply fix for log4j vulnerability (CVE-2021-44228) + const [_, versionMajor] = opts.version.split(".").map(Number); + + if (versionMajor < 18 || opts.version == "1.18") { + let log4jArg = ""; + + if (versionMajor >= 7 && versionMajor <= 11) { + log.info("Applying log4j fix for 1.7-1.11.2"); + + https.get( + "https://launcher.mojang.com/v1/objects/4bb89a97a66f350bc9f73b3ca8509632682aea2e/log4j2_17-111.xml", + (res) => { + const stream = createWriteStream( + path.join(instanceRoot, "log4j2_17-111.xml") + ); + res.pipe(stream); + stream.on("finish", () => stream.close()); + } + ); + + log4jArg = "-Dlog4j.configurationFile=log4j2_17-111.xml"; + } else if (versionMajor >= 12 && versionMajor <= 16) { + log.info("Applying log4j fix for 1.12-1.16.5"); + + https.get( + "https://launcher.mojang.com/v1/objects/02937d122c86ce73319ef9975b58896fc1b491d1/log4j2_112-116.xml", + (res) => { + const stream = createWriteStream( + path.join(instanceRoot, "log4j2_112-116.xml") + ); + res.pipe(stream); + stream.on("finish", () => stream.close()); + } + ); + + log4jArg = "-Dlog4j.configurationFile=log4j2_112-116.xml"; + } else if (versionMajor === 17 || opts.version === "1.18") { + log4jArg = "-Dlog4j2.formatMsgNoLookups=true"; + } + + log.debug("Rewriting config file to add log4j fix JVM argument"); + await fs.writeFile( + path.join(instanceRoot, "multiserver.config.json"), + JSON.stringify( + { + ...opts, + jvmArgs: + (opts.jvmArgs ?? "") + + `${ + (opts.jvmArgs?.length ?? 0) > 0 ? " " : "" + }${log4jArg}`, + }, + undefined, + 4 + ) + ); + } + log.info("Server creation complete"); return true; } catch (e) { diff --git a/src/lib/instances/runInstance.ts b/src/lib/instances/runInstance.ts index b5e66ba..e779f6c 100644 --- a/src/lib/instances/runInstance.ts +++ b/src/lib/instances/runInstance.ts @@ -41,7 +41,7 @@ export async function runInstance( let oldPlayers: string[] = []; server.stdout.on("data", (data) => { - log.debug(`SERVER ${info.name} info: ${String(data)}`); + log.debug(`SERVER ${info.name} info: ${String(data).trim()}`); if (String(data).includes("RCON running on 0.0.0.0:25575")) { log.info("Server loading complete, RCON connecting"); @@ -57,15 +57,13 @@ export async function runInstance( }); server.stderr.on("data", (data) => { - log.debug(`SERVER ${info.name} error: ${String(data)}`); + log.debug(`SERVER ${info.name} error: ${String(data).trim()}`); if (!window.isDestroyed()) window.webContents.send("stderr", String(data)); }); // eslint-disable-next-line @typescript-eslint/no-misused-promises const playerQuery = setInterval(async () => { - log.debug("interval running"); - try { const results = await queryFull("localhost"); diff --git a/src/windows/RunWindow.tsx b/src/windows/RunWindow.tsx index d2358e9..dc563de 100644 --- a/src/windows/RunWindow.tsx +++ b/src/windows/RunWindow.tsx @@ -2,15 +2,20 @@ import { Button, TextField } from "@mui/material"; import React, { useState, useEffect } from "react"; import ReactDOM from "react-dom"; import Helmet from "react-helmet"; +import Repl from "../components/Repl"; import "../app.global.css"; import { InstanceInfo } from "../types"; +interface ServerOutput { + type: "stdout" | "stderr"; + content: string; +} + const RunWindow = (): JSX.Element => { const [info, setInfo] = useState(null); const [players, setPlayers] = useState([]); - const [stdout, setStdout] = useState([]); - const [stderr, setStderr] = useState([]); + const [output, setOutput] = useState([]); const [command, setCommand] = useState(""); @@ -18,18 +23,22 @@ const RunWindow = (): JSX.Element => { server.onInfo(setInfo); server.onPlayers(setPlayers); - server.onStdout((out) => setStdout([...stdout, out])); - server.onStderr((err) => setStderr([...stderr, err])); + server.onStdout((out) => + setOutput((s) => [...s, { type: "stdout", content: out }]) + ); + server.onStderr((err) => + setOutput((s) => [...s, { type: "stderr", content: err }]) + ); }, []); return ( -
+
{`Running server ${info?.info.name ?? ""}`} -
-
+
+

Players

    {players.map((p) => ( @@ -37,19 +46,28 @@ const RunWindow = (): JSX.Element => { ))}
-
-

Log

+
+
+

Log

+
+                            {output.map((o) => (
+                                
+ {o.content} +
+ ))} +
+
-
{stdout.join("\n")}
-
{stderr.join("\n")}
+
- setCommand(e.target.value)} - /> -
); diff --git a/tailwind.config.js b/tailwind.config.js index 280d7e6..dccffce 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,7 @@ const colors = require("tailwindcss/colors"); module.exports = { + mode: "jit", purge: [ "./src/windows/**/*.{js,jsx,ts,tsx}", "./src/components/**/*.{js,jsx,ts,tsx}", @@ -19,5 +20,8 @@ module.exports = { variants: { extend: {}, }, - plugins: [], + plugins: [require("tailwind-scrollbar")], + variants: { + scrollbar: ["rounded"], + }, }; diff --git a/yarn.lock b/yarn.lock index 73a50b5..66c5a2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6660,7 +6660,14 @@ table@^6.0.9: string-width "^4.2.3" strip-ansi "^6.0.1" -tailwindcss@^2.2.19: +tailwind-scrollbar@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/tailwind-scrollbar/-/tailwind-scrollbar-1.3.1.tgz#271d7c405bcddb7b5f5f77d7f43758c89e389767" + integrity sha512-FeYuLxLtCRMO4PmjPJCzm5wQouFro2BInZXKPxqg54DR/55NAHoS8uNYWMiRG5l6qsLkWBfVEM34gq2XAQUwVg== + dependencies: + tailwindcss ">1.9.6" + +tailwindcss@>1.9.6, tailwindcss@^2.2.19: version "2.2.19" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-2.2.19.tgz#540e464832cd462bb9649c1484b0a38315c2653c" integrity sha512-6Ui7JSVtXadtTUo2NtkBBacobzWiQYVjYW0ZnKaP9S1ZCKQ0w7KVNz+YSDI/j7O7KCMHbOkz94ZMQhbT9pOqjw==