1212
1313import java .io .BufferedReader ;
1414import java .io .BufferedWriter ;
15+ import java .io .ByteArrayOutputStream ;
16+ import java .io .DataInputStream ;
17+ import java .io .File ;
18+ import java .io .FileOutputStream ;
1519import java .io .IOException ;
20+ import java .io .InputStream ;
1621import java .io .InputStreamReader ;
22+ import java .io .OutputStream ;
1723import java .io .OutputStreamWriter ;
1824import java .net .HttpURLConnection ;
1925import java .net .InetAddress ;
2026import java .net .ServerSocket ;
2127import java .net .Socket ;
2228import java .net .SocketTimeoutException ;
2329import 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 ;
2435import java .time .LocalTime ;
2536import 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 ;
2641import java .util .Random ;
2742
2843public 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