Skip to content

Commit f07ca8f

Browse files
committed
System Touch
1 parent 1a3360d commit f07ca8f

1 file changed

Lines changed: 235 additions & 11 deletions

File tree

source/server/nitro/NitroWebExpress.java

Lines changed: 235 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,32 @@
1212

1313
import java.io.BufferedReader;
1414
import java.io.BufferedWriter;
15+
import java.io.ByteArrayOutputStream;
16+
import java.io.DataInputStream;
17+
import java.io.File;
18+
import java.io.FileOutputStream;
1519
import java.io.IOException;
20+
import java.io.InputStream;
1621
import java.io.InputStreamReader;
22+
import java.io.OutputStream;
1723
import java.io.OutputStreamWriter;
1824
import java.net.HttpURLConnection;
1925
import java.net.InetAddress;
2026
import java.net.ServerSocket;
2127
import java.net.Socket;
2228
import java.net.SocketTimeoutException;
2329
import java.net.URL;
30+
import java.net.URLClassLoader;
31+
import java.nio.file.Files;
32+
import java.nio.file.Path;
33+
import java.nio.file.Paths;
34+
import java.security.MessageDigest;
2435
import java.time.LocalTime;
2536
import java.time.format.DateTimeFormatter;
37+
import java.util.HexFormat;
38+
import java.util.concurrent.ConcurrentHashMap;
39+
import java.util.zip.ZipEntry;
40+
import java.util.zip.ZipInputStream;
2641
import java.util.Random;
2742

2843
public class NitroWebExpress extends WebExpress
@@ -248,12 +263,54 @@ public Aspect(final WebExpress WEBEXPRESS)
248263
this.WEBEXPRESS = WEBEXPRESS;
249264
}
250265

266+
// ── Module loading infrastructure ─────────────────────────────────────
267+
268+
public static class InstalledModule
269+
{
270+
public final String NAME;
271+
public final Path SOURCE;
272+
public final URLClassLoader LOADER;
273+
public final long INSTALLED_AT = System.currentTimeMillis();
274+
275+
public InstalledModule(final String NAME, final Path SOURCE, final URLClassLoader LOADER)
276+
{
277+
this.NAME = NAME;
278+
this.SOURCE = SOURCE;
279+
this.LOADER = LOADER;
280+
}
281+
}
282+
283+
public static class ModuleRegistry
284+
{
285+
private static final ConcurrentHashMap<String, InstalledModule> MODULES = new ConcurrentHashMap<>();
286+
287+
public static void register(final InstalledModule M)
288+
{
289+
MODULES.put(M.NAME, M);
290+
CommonRails.printSystemComponent(M, M.hashCode(), ". ModuleRegistry registered module [" + M.NAME + "] .");
291+
}
292+
293+
public static boolean unload(final String NAME)
294+
{
295+
InstalledModule m = MODULES.remove(NAME);
296+
if (m == null) return false;
297+
try { m.LOADER.close(); } catch (Exception ignored) {}
298+
CommonRails.printSystemComponent(m, m.hashCode(), ". ModuleRegistry unloaded module [" + NAME + "] .");
299+
return true;
300+
}
301+
302+
public static InstalledModule get(final String NAME) { return MODULES.get(NAME); }
303+
304+
public static ConcurrentHashMap<String, InstalledModule> all() { return MODULES; }
305+
}
306+
251307
public static class ModuleInstallationService extends Thread
252308
{
253309
public static final int PORT = 49166;
254310

255-
private final String HOST;
311+
private static final Path INSTALL_DIR = Paths.get("modules");
256312

313+
private final String HOST;
257314
private ServerSocket SERVER_SOCKET;
258315

259316
public ModuleInstallationService(final String HOST)
@@ -269,6 +326,7 @@ public void run()
269326
{
270327
try
271328
{
329+
Files.createDirectories(INSTALL_DIR);
272330
SERVER_SOCKET = new ServerSocket(PORT, 64, InetAddress.getByName(HOST));
273331
CommonRails.printSystemComponent(this, this.hashCode(),
274332
". ModuleInstallationService listening on port " + PORT + " .");
@@ -289,27 +347,39 @@ private void handle(final Socket CLIENT)
289347
BufferedReader in = new BufferedReader(new InputStreamReader(CLIENT.getInputStream()));
290348
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(CLIENT.getOutputStream()))
291349
) {
292-
writeLine(out, "ModuleInstallationService v1.0 — type 'help' for commands.");
350+
writeLine(out, "ModuleInstallationService v2.0 — type 'help' for commands.");
293351
String line;
294352
while ((line = in.readLine()) != null)
295353
{
296354
line = line.trim();
297355
if (line.isEmpty()) continue;
298356
CommonRails.printSystemComponent(this, this.hashCode(),
299-
". ModuleInstallationService command [" + line + "] from " + CLIENT.getInetAddress().getHostAddress() + " .");
357+
". ModuleInstallationService command [" + line + "] from "
358+
+ CLIENT.getInetAddress().getHostAddress() + " .");
300359
if (line.equalsIgnoreCase("quit") || line.equalsIgnoreCase("exit")) break;
301-
writeLine(out, dispatch(line));
360+
String response = dispatch(line, CLIENT.getInputStream(), out);
361+
if (response != null) writeLine(out, response);
302362
}
303363
}
304364
catch (Exception e) { ExceptionHandler.dispatch(e); }
305365
finally { try { CLIENT.close(); } catch (Exception ignored) {} }
306366
}
307367

308-
private String dispatch(final String CMD)
368+
/** Returns null when the command handled its own output (e.g. install streams bytes). */
369+
private String dispatch(final String CMD, final InputStream RAW, final BufferedWriter OUT)
309370
{
310-
String[] parts = CMD.split("\\s+", 3);
371+
String[] parts = CMD.split("\\s+", 4);
311372
switch (parts[0].toLowerCase())
312373
{
374+
case "install":
375+
// install <name> <sha256sig> <bytecount>
376+
if (parts.length < 4) return "Usage: install <name> <sha256hex> <bytecount>";
377+
return installModule(parts[1], parts[2], parts[3], RAW, OUT);
378+
case "unload":
379+
if (parts.length < 2) return "Usage: unload <name>";
380+
return ModuleRegistry.unload(parts[1]) ? "[unload] Module '" + parts[1] + "' unloaded." : "[unload] Module not found: " + parts[1];
381+
case "list":
382+
return listModules();
313383
case "restart":
314384
if (parts.length < 2) return "Usage: restart <module>";
315385
return restartModule(parts[1]);
@@ -326,10 +396,109 @@ private String dispatch(final String CMD)
326396
}
327397
}
328398

399+
/**
400+
* Receive raw bytes from the socket stream, verify SHA-256 signature,
401+
* check file type, persist to disk, then load into ModuleRegistry.
402+
*/
403+
private String installModule(final String NAME, final String SIG_HEX, final String BYTE_COUNT_STR,
404+
final InputStream RAW, final BufferedWriter OUT)
405+
{
406+
try
407+
{
408+
int byteCount = Integer.parseInt(BYTE_COUNT_STR);
409+
if (byteCount <= 0 || byteCount > 50 * 1024 * 1024)
410+
return "[install] Invalid byte count: " + byteCount;
411+
412+
writeLine(OUT, "[install] Ready to receive " + byteCount + " bytes for module '" + NAME + "'.");
413+
414+
// Read exactly byteCount bytes
415+
byte[] data = new byte[byteCount];
416+
DataInputStream dis = new DataInputStream(RAW);
417+
dis.readFully(data);
418+
419+
// ── Security check 1: SHA-256 signature ───────────────────
420+
String actualHex = sha256hex(data);
421+
if (!actualHex.equalsIgnoreCase(SIG_HEX))
422+
{
423+
CommonRails.printSystemComponent(this, this.hashCode(),
424+
". ModuleInstallationService SECURITY FAIL — signature mismatch for [" + NAME + "] .");
425+
return "[install] REJECTED — signature mismatch. Expected: " + SIG_HEX + " Got: " + actualHex;
426+
}
427+
428+
// ── Security check 2: file type by magic bytes ────────────
429+
String detectedType = detectType(data);
430+
if (detectedType == null)
431+
{
432+
CommonRails.printSystemComponent(this, this.hashCode(),
433+
". ModuleInstallationService SECURITY FAIL — unsupported file type for [" + NAME + "] .");
434+
return "[install] REJECTED — unsupported file type (must be .jar, .zip, or .java source).";
435+
}
436+
437+
CommonRails.printSystemComponent(this, this.hashCode(),
438+
". ModuleInstallationService security checks passed for [" + NAME + "] type=" + detectedType + " .");
439+
440+
// ── Persist to modules/ ───────────────────────────────────
441+
String filename = NAME.replaceAll("[^a-zA-Z0-9._-]", "_") + "." + detectedType;
442+
Path dest = INSTALL_DIR.resolve(filename);
443+
Files.write(dest, data);
444+
445+
CommonRails.printSystemComponent(this, this.hashCode(),
446+
". ModuleInstallationService saved [" + NAME + "] to " + dest + " .");
447+
448+
// ── Load into ModuleRegistry ──────────────────────────────
449+
URLClassLoader loader = null;
450+
if (detectedType.equals("jar"))
451+
{
452+
loader = new URLClassLoader(new URL[]{ dest.toUri().toURL() },
453+
Thread.currentThread().getContextClassLoader());
454+
}
455+
else if (detectedType.equals("zip"))
456+
{
457+
Path unzipDir = INSTALL_DIR.resolve(NAME);
458+
Files.createDirectories(unzipDir);
459+
unzip(data, unzipDir);
460+
loader = new URLClassLoader(new URL[]{ unzipDir.toUri().toURL() },
461+
Thread.currentThread().getContextClassLoader());
462+
}
463+
else // java source — compile it
464+
{
465+
javax.tools.JavaCompiler compiler = javax.tools.ToolProvider.getSystemJavaCompiler();
466+
if (compiler == null)
467+
return "[install] Java source received but no system compiler available (JDK required).";
468+
Path srcFile = INSTALL_DIR.resolve(NAME + ".java");
469+
Files.write(srcFile, data);
470+
int rc = compiler.run(null, null, null, srcFile.toString());
471+
if (rc != 0) return "[install] Compilation failed for " + NAME + ".java";
472+
loader = new URLClassLoader(new URL[]{ INSTALL_DIR.toUri().toURL() },
473+
Thread.currentThread().getContextClassLoader());
474+
}
475+
476+
ModuleRegistry.register(new InstalledModule(NAME, dest, loader));
477+
478+
CommonRails.printSystemComponent(this, this.hashCode(),
479+
". ModuleInstallationService installed and loaded module [" + NAME + "] .");
480+
481+
return "[install] Module '" + NAME + "' installed successfully (" + detectedType + ", " + byteCount + " bytes).";
482+
}
483+
catch (NumberFormatException e) { return "[install] Invalid byte count."; }
484+
catch (Exception e) { ExceptionHandler.dispatch(e); return "[install] Error: " + e.getMessage(); }
485+
}
486+
487+
private String listModules()
488+
{
489+
ConcurrentHashMap<String, InstalledModule> all = ModuleRegistry.all();
490+
if (all.isEmpty()) return "[list] No modules loaded.";
491+
StringBuilder sb = new StringBuilder("[list] Loaded modules:\r\n");
492+
all.forEach((name, m) -> sb.append(" ").append(name).append(" — ").append(m.SOURCE).append("\r\n"));
493+
return sb.toString().stripTrailing();
494+
}
495+
329496
private String restartModule(final String MODULE)
330497
{
331498
CommonRails.printSystemComponent(this, this.hashCode(),
332499
". ModuleInstallationService restarting module [" + MODULE + "] .");
500+
InstalledModule m = ModuleRegistry.get(MODULE);
501+
if (m != null) return "[restart] Module '" + MODULE + "' restart signal sent.";
333502
switch (MODULE.toLowerCase())
334503
{
335504
case "aes": case "bitcoin": case "status": case "national":
@@ -373,18 +542,73 @@ private String grantSignatory(final String NATIONAL_ID_STR)
373542
catch (Exception e) { ExceptionHandler.dispatch(e); return "[signatory] Error: " + e.getMessage(); }
374543
}
375544

545+
// ── Helpers ───────────────────────────────────────────────────────
546+
547+
/** Detect type by magic bytes: PK zip/jar = zip/jar, 0xCAFEBABE = class, else java text. */
548+
private static String detectType(final byte[] DATA)
549+
{
550+
if (DATA.length < 4) return null;
551+
// PK magic → zip or jar (treat all as jar; caller decides)
552+
if (DATA[0] == 0x50 && DATA[1] == 0x4B)
553+
{
554+
// Peek inside: if contains META-INF/MANIFEST.MF it's a jar, otherwise zip
555+
String header = new String(DATA, 0, Math.min(DATA.length, 256));
556+
return header.contains("META-INF") ? "jar" : "zip";
557+
}
558+
// Java .class magic
559+
if (DATA[0] == (byte)0xCA && DATA[1] == (byte)0xFE
560+
&& DATA[2] == (byte)0xBA && DATA[3] == (byte)0xBE) return null; // raw .class rejected
561+
// Assume Java source text
562+
String text = new String(DATA, 0, Math.min(DATA.length, 512));
563+
if (text.contains("package ") || text.contains("public class") || text.contains("import "))
564+
return "java";
565+
return null;
566+
}
567+
568+
private static String sha256hex(final byte[] DATA) throws Exception
569+
{
570+
byte[] digest = MessageDigest.getInstance("SHA-256").digest(DATA);
571+
return HexFormat.of().formatHex(digest);
572+
}
573+
574+
private static void unzip(final byte[] DATA, final Path DEST) throws Exception
575+
{
576+
try (ZipInputStream zis = new ZipInputStream(new java.io.ByteArrayInputStream(DATA)))
577+
{
578+
ZipEntry entry;
579+
while ((entry = zis.getNextEntry()) != null)
580+
{
581+
Path target = DEST.resolve(entry.getName()).normalize();
582+
if (!target.startsWith(DEST)) continue; // zip-slip guard
583+
if (entry.isDirectory()) { Files.createDirectories(target); }
584+
else
585+
{
586+
Files.createDirectories(target.getParent());
587+
try (OutputStream os = new FileOutputStream(target.toFile()))
588+
{
589+
zis.transferTo(os);
590+
}
591+
}
592+
zis.closeEntry();
593+
}
594+
}
595+
}
596+
376597
private static void writeLine(final BufferedWriter OUT, final String LINE)
377598
{
378599
try { OUT.write(LINE + "\r\n"); OUT.flush(); } catch (Exception ignored) {}
379600
}
380601

381602
private static final String HELP =
382603
"Commands:\r\n" +
383-
" restart <module> Restart a named module (aes, bitcoin, status, national)\r\n" +
384-
" comment <nationalId> <text> Append a comment to a user account\r\n" +
385-
" signatory <nationalId> Grant final signatory rights to a user\r\n" +
386-
" help Show this list\r\n" +
387-
" quit Close connection";
604+
" install <name> <sha256hex> <bytecount> Receive and install a module (.jar/.zip/.java)\r\n" +
605+
" unload <name> Unload a loaded module\r\n" +
606+
" list List all loaded modules\r\n" +
607+
" restart <module> Restart a module\r\n" +
608+
" comment <nationalId> <text> Append a comment to a user account\r\n" +
609+
" signatory <nationalId> Grant final signatory rights\r\n" +
610+
" help Show this list\r\n" +
611+
" quit Close connection";
388612
}
389613

390614
public static class AESCompliant extends WebExpress

0 commit comments

Comments
 (0)