B4J=true Group=Default Group ModulesStructureVersion=1 Type=Class Version=10.3 @EndOfDesignText@ ' Class module: Manager ' This handler provides a web administration panel for the jRDC2-Multi server. ' It allows monitoring server status, reloading database configurations, ' viewing performance statistics, restarting external services, and managing user authentication. Sub Class_Globals ' Object to generate JSON responses. Used to display data maps legibly. Dim j As JSONGenerator ' The BCrypt class is not used directly in this module, but is kept for any future plans. ' Private bc As BCrypt End Sub ' Class initialization subroutine. Called when an object of this class is created. Public Sub Initialize ' No specific initialization is required for this class at this time. End Sub ' Main method that handles HTTP requests for the administration panel. ' req: The ServletRequest object containing incoming request information. ' resp: The ServletResponse object for building and sending the response to the client. ' Refactored to work as an API with a static frontend. Sub Handle(req As ServletRequest, resp As ServletResponse) ' Security Block If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then resp.SendRedirect("/login") Return End If Dim Command As String = req.GetParameter("command") ' Main Page Server If Command = "" Then Try resp.ContentType = "text/html; charset=utf-8" resp.Write(File.ReadString(File.DirApp, "www/manager.html")) Catch resp.SendError(500, "Error: No se pudo encontrar el archivo principal del panel (www/manager.html). " & LastException.Message) End Try Return End If ' API Command Handling Select Command.ToLowerCase ' Commands that return JSON (Pool Metrics) Case "getstatsold" resp.ContentType = "application/json; charset=utf-8" Dim allPoolStats As Map allPoolStats.Initialize For Each dbKey As String In Main.listaDeCP Dim connector As RDCConnector = Main.Connectors.Get(dbKey) If connector.IsInitialized Then allPoolStats.Put(dbKey, connector.GetPoolStats) Else allPoolStats.Put(dbKey, CreateMap("Error": "Connector not initialized")) End If Next j.Initialize(allPoolStats) resp.Write(j.ToString) Return Case "getstats" resp.ContentType = "application/json; charset=utf-8" Dim allPoolStats As Map ' We read from the global cache updated by the SSE Timer allPoolStats = Main.LatestPoolStats For Each dbKey As String In Main.listaDeCP If allPoolStats.ContainsKey(dbKey) = False Then allPoolStats.Put(dbKey, CreateMap("Error": "Metrics unavailable/Pool not initialized")) End If Next j.Initialize(allPoolStats) resp.Write(j.ToString) Return Case "slowqueries" resp.ContentType = "application/json; charset=utf-8" Dim results As List results.Initialize Try ' --- 1. VALIDACIÓN Y PARSEO DEFENSIVO DE PARÁMETROS --- ' Capturamos los parámetros numéricos como Strings primero para chequear si están vacíos. Dim limitStr As String = req.GetParameter("limit") Dim minutesStr As String = req.GetParameter("minutes") Dim limit As Int = 0 Dim minutes As Int = 0 ' 1a. Parseo seguro de 'limit'. Evita NumberFormatException si el string es "" o Null. If limitStr <> Null And limitStr.Trim.Length > 0 Then limit = limitStr.As(Int) End If ' 1b. Parseo seguro de 'minutes'. If minutesStr <> Null And minutesStr.Trim.Length > 0 Then minutes = minutesStr.As(Int) End If ' Parámetros de ordenamiento (ya son strings, no necesitan chequeo de NFE) Dim sortby As String = req.GetParameter("sortby").ToLowerCase.Trim ' Columna para ordenar (Default: duration_ms) Dim sortorder As String = req.GetParameter("sortorder").ToUpperCase.Trim ' Orden (Default: DESC) ' Establecer Defaults (ahora esta lógica funciona porque limit y minutes son 0 si el parámetro estaba vacío o ausente) If limit = 0 Then limit = 20 ' Límite de registros (Default: 20) If minutes = 0 Then minutes = 60 ' Filtro de tiempo en minutos (Default: 60 - última hora) ' --- Lista Blanca (Whitelist) para prevención de Inyección SQL en ORDER BY --- Dim allowedSortColumns As List allowedSortColumns.Initialize allowedSortColumns.AddAll(Array As Object("duration_ms", "timestamp", "db_key", "client_ip", "busy_connections", "handler_active_requests", "query_name")) If allowedSortColumns.IndexOf(sortby) = -1 Then sortby = "duration_ms" ' Usar default seguro si no se encuentra End If If sortorder <> "ASC" And sortorder <> "DESC" Then sortorder = "DESC" End If ' --- 2. PREPARACIÓN DE LA CONSULTA SQL --- ' We use SQL_Logs instance to check for the table existence. Dim tableExists As Boolean = Main.SQL_Logs.ExecQuerySingleResult($"SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'"$) <> Null If tableExists = False Then j.Initialize(CreateMap("message": "La tabla de logs ('query_logs') no existe. Habilite 'enableSQLiteLogs=1' en la configuración.")) resp.Write(j.ToString) Return End If ' Calcular el tiempo de corte (flexible) Dim cutOffTimeMs As Long = DateTime.Now - (minutes * 60000) Dim sqlQuery As String = $" SELECT query_name, duration_ms, datetime(timestamp / 1000, 'unixepoch', 'localtime') as timestamp_local, db_key, client_ip, busy_connections, handler_active_requests FROM query_logs WHERE timestamp >= ${cutOffTimeMs} ORDER BY ${sortby} ${sortorder} LIMIT ${limit}"$ Dim rs As ResultSet = Main.SQL_Logs.ExecQuery(sqlQuery) ' --- Execute query on SQL_Logs instance Do While rs.NextRow Dim row As Map row.Initialize row.Put("Query", rs.GetString("query_name")) row.Put("Duracion_ms", rs.GetLong("duration_ms")) row.Put("Fecha_Hora", rs.GetString("timestamp_local")) row.Put("DB_Key", rs.GetString("db_key")) row.Put("Cliente_IP", rs.GetString("client_ip")) row.Put("Conexiones_Ocupadas", rs.GetInt("busy_connections")) row.Put("Peticiones_Activas", rs.GetInt("handler_active_requests")) results.Add(row) Loop rs.Close Dim root As Map root.Initialize root.Put("data", results) ' Añadir meta información para diagnóstico Dim meta As Map = CreateMap("limit_applied": limit, "sorted_by": sortby, "sort_order": sortorder, "minutes_filter": minutes) root.Put("meta", meta) j.Initialize(root) resp.Write(j.ToString) Catch Log("CRITICAL Error getting slow queries in Manager API: " & LastException.Message) resp.Status = 500 Dim root As Map root.Initialize root.Put("data", results) j.Initialize(CreateMap("message": "Error interno al procesar logs. Detalle: " & LastException.Message)) resp.Write(j.ToString) End Try Return Case "logs", "totalrequests", "totalblocked" resp.ContentType = "application/json; charset=utf-8" Dim mp As Map If Command = "logs" And GlobalParameters.mpLogs.IsInitialized Then mp = GlobalParameters.mpLogs If Command = "totalrequests" And GlobalParameters.mpTotalRequests.IsInitialized Then mp = GlobalParameters.mpTotalRequests If Command = "totalblocked" And GlobalParameters.mpBlockConnection.IsInitialized Then mp = GlobalParameters.mpBlockConnection If mp.IsInitialized Then j.Initialize(mp) resp.Write(j.ToString) Else resp.Write("{}") End If Return ' Commands that return PLAIN TEXT Case "ping" resp.ContentType = "text/plain" resp.Write($"Pong ($DateTime{DateTime.Now})"$) Return Case "reload" resp.ContentType = "text/plain; charset=utf-8" Dim sbTemp As StringBuilder sbTemp.Initialize Dim dbKeyToReload As String = req.GetParameter("db").ToUpperCase ' Read optional 'db' parameter (e.g., /manager?command=reload&db=DB3) Dim targets As List ' List of DBKeys to reload. targets.Initialize ' 1. Determine the scope of the reload (selective or total) If dbKeyToReload.Length > 0 Then ' Selective reload If Main.listaDeCP.IndexOf(dbKeyToReload) = -1 Then resp.Write($"ERROR: DBKey '${dbKeyToReload}' is not valid or not configured."$) Return End If targets.Add(dbKeyToReload) sbTemp.Append($"Starting selective reload of ${dbKeyToReload} (Hot-Swap)..."$).Append(" " & CRLF) Else ' Full reload (default behavior) targets.AddAll(Main.listaDeCP) sbTemp.Append($"Starting COMPLETE configuration reload (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF) End If ' 2. Disable the log Timer (if necessary) Dim oldTimerState As Boolean = Main.timerLogs.Enabled If oldTimerState Then Main.timerLogs.Enabled = False sbTemp.Append(" -> Log cleanup timer (SQLite) stopped temporarily.").Append(" " & CRLF) End If Dim reloadSuccessful As Boolean = True Dim oldConnectorsToClose As Map ' We will store the old connectors here. oldConnectorsToClose.Initialize ' 3. Process only the target connectors For Each dbKey As String In targets sbTemp.Append($" -> Processing reload of ${dbKey}..."$).Append(CRLF) Dim newRDC As RDCConnector Try ' Create the new connector with the fresh configuration newRDC.Initialize(dbKey) ' Acquire the lock for atomic replacement Main.MainConnectorsLock.RunMethod("lock", Null) ' Save the old connector (if it exists) Dim oldRDC As RDCConnector = Main.Connectors.Get(dbKey) ' Atomic replacement in the shared global map Main.Connectors.Put(dbKey, newRDC) ' Release the lock immediately Main.MainConnectorsLock.RunMethod("unlock", Null) ' If there was an old connector, save it to close later If oldRDC.IsInitialized Then oldConnectorsToClose.Put(dbKey, oldRDC) End If ' 4. Update log status (Granular) Dim enableLogsSetting As Int = newRDC.config.GetDefault("enableSQLiteLogs", 0) Dim isEnabled As Boolean = (enableLogsSetting = 1) Main.SQLiteLoggingStatusByDB.Put(dbKey, isEnabled) sbTemp.Append($" -> ${dbKey} reloaded. Logs (config): ${isEnabled}"$).Append(CRLF) Catch ' If pool initialization fails, we don't update Main.Connectors ' Ensure the lock is released if an exception occurred before release. If Main.MainConnectorsLock.RunMethod("isHeldByCurrentThread", Null).As(Boolean) Then Main.MainConnectorsLock.RunMethod("unlock", Null) End If sbTemp.Append($" -> CRITICAL ERROR initializing connector for ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF) reloadSuccessful = False Exit End Try Next ' 5. Close the old released pools (OUTSIDE the Lock) If reloadSuccessful Then For Each dbKey As String In oldConnectorsToClose.Keys Dim oldRDC As RDCConnector = oldConnectorsToClose.Get(dbKey) oldRDC.Close ' Clean closure of the C3P0 pool sbTemp.Append($" -> Old pool for ${dbKey} closed cleanly."$).Append(" " & CRLF) Next ' 6. Re-evaluate the global Log status (must check ALL DBs) Main.IsAnySQLiteLoggingEnabled = False For Each dbKey As String In Main.listaDeCP ' We check the log status of EACH active connector If Main.SQLiteLoggingStatusByDB.GetDefault(dbKey, False) Then Main.IsAnySQLiteLoggingEnabled = True Exit End If Next If Main.IsAnySQLiteLoggingEnabled Then Main.timerLogs.Enabled = True sbTemp.Append($" -> Log cleanup timer ACTIVATED (global status: ENABLED)."$).Append(" " & CRLF) Else Main.timerLogs.Enabled = False sbTemp.Append($" -> Log cleanup timer DISABLED (global status: DISABLED)."$).Append(" " & CRLF) End If sbTemp.Append($"Configuration reload completed successfully!"$).Append(" " & CRLF) Else ' If it failed, restore the previous timer state. If oldTimerState Then Main.timerLogs.Enabled = True sbTemp.Append(" -> Restoring Log cleanup timer to ACTIVE state due to reload failure.").Append(" " & CRLF) End If sbTemp.Append($"ERROR: Configuration reload failed! Old connectors are still active."$).Append(" " & CRLF) End If resp.Write(sbTemp.ToString) Return Case "test" resp.ContentType = "text/plain; charset=utf-8" Dim sb As StringBuilder sb.Initialize sb.Append("--- STARTING CONNECTIVITY TEST TO ALL CONFIGURED POOLS ---").Append(CRLF).Append(CRLF) ' We iterate over the list of DB Keys loaded at startup (DB1, DB2, etc.) For Each dbKey As String In Main.listaDeCP Dim success As Boolean = False Dim errorMsg As String = "" Dim con As SQL ' Connection for the test Try ' 1. Get the RDCConnector for this DBKey Dim connector As RDCConnector = Main.Connectors.Get(dbKey) If connector.IsInitialized = False Then errorMsg = "Connector not initialized (check AppStart logs)" Else ' 2. Force acquisition of a connection from the C3P0 pool con = connector.GetConnection(dbKey) If con.IsInitialized Then ' 3. If the connection is valid, close it immediately to return it to the pool con.Close success = True Else errorMsg = "Returned connection is not valid (SQL.IsInitialized = False)" End If End If Catch ' We catch any exception (e.g., JDBC failure, C3P0 timeout) errorMsg = LastException.Message End Try If success Then sb.Append($"* ${dbKey}: Connection acquired and released successfully."$).Append(CRLF) Else ' If it fails, log the error for the administrator. Main.LogServerError("ERROR", "Manager.TestCommand", $"Connectivity test failed for ${dbKey}: ${errorMsg}"$, dbKey, "test_command", req.RemoteAddress) sb.Append($"[FAILED] ${dbKey}: CRITICAL ERROR getting connection. Message: ${errorMsg}"$).Append(CRLF) End If Next sb.Append(CRLF).Append("--- END OF CONNECTION TEST ---").Append(CRLF) ' We keep the original list of loaded config files (this is informational) sb.Append(CRLF).Append("Loaded configuration files:").Append(CRLF) For Each item As String In Main.listaDeCP Dim configName As String = "config" If item <> "DB1" Then configName = configName & "." & item sb.Append($" -> Using ${configName}.properties"$).Append(CRLF) Next resp.Write(sb.ToString) Return Case "rsx", "rpm2", "revivebow", "restartserver" resp.ContentType = "text/plain; charset=utf-8" Dim batFile As String Select Command Case "rsx": batFile = "start.bat" Case "rpm2": batFile = "reiniciaProcesoPM2.bat" Case "reviveBow": batFile = "reiniciaProcesoBow.bat" Case "restartserver": batFile = "restarServer.bat" ' Note: this bat was not defined, command name was used End Select Log($"Executing ${File.DirApp}\${batFile}"$) Try Dim shl As Shell shl.Initialize("shl","cmd", Array("/c", File.DirApp & "\" & batFile & " " & Main.srvr.Port)) shl.WorkingDirectory = File.DirApp shl.Run(-1) resp.Write($"Command '${Command}' executed. Script invoked: ${batFile}"$) Catch resp.Write($"Error executing script for '${Command}': ${LastException.Message}"$) End Try Return Case "paused", "continue" resp.ContentType = "text/plain; charset=utf-ab" If Command = "paused" Then GlobalParameters.IsPaused = 1 resp.Write("Server paused.") Else GlobalParameters.IsPaused = 0 resp.Write("Server resumed.") End If Return Case "block", "unblock" resp.ContentType = "text/plain; charset=utf-8" Dim ip As String = req.GetParameter("IP") If ip = "" Then resp.Write("Error: The IP parameter is required.") Return End If If GlobalParameters.mpBlockConnection.IsInitialized Then If Command = "block" Then GlobalParameters.mpBlockConnection.Put(ip, ip) resp.Write($"IP blocked: ${ip}"$) Else GlobalParameters.mpBlockConnection.Remove(ip) resp.Write($"IP unblocked: ${ip}"$) End If Else resp.Write("Error: The block map is not initialized.") End If Return Case "getconfiginfo" resp.ContentType = "text/plain; charset=utf-8" Dim sbInfo As StringBuilder sbInfo.Initialize Dim allKeys As List allKeys.Initialize allKeys.AddAll(Main.listaDeCP) sbInfo.Append("======================================================================").Append(CRLF) sbInfo.Append($"=== jRDC2-Multi V$1.2{Main.VERSION} CONFIGURATION (ACTIVE) ($DateTime{DateTime.Now}) ==="$).Append(CRLF) sbInfo.Append("======================================================================").Append(CRLF).Append(CRLF) sbInfo.Append("### GLOSSARY OF ALLOWED PARAMETERS IN CONFIG.PROPERTIES (HIKARICP) ###").Append(CRLF) sbInfo.Append("--------------------------------------------------").Append(CRLF) sbInfo.Append("DriverClass: JDBC driver class (e.g., oracle.jdbc.driver.OracleDriver).").Append(CRLF) sbInfo.Append("JdbcUrl: Database connection URL (IP, port, service).").Append(CRLF) sbInfo.Append("User/Password: DB access credentials.").Append(CRLF) sbInfo.Append("ServerPort: B4J server listening port (only taken from config.properties).").Append(CRLF) sbInfo.Append("Debug: If 'true', SQL commands are reloaded on each request (DISABLED, USE RELOAD COMMAND).").Append(CRLF) sbInfo.Append("parameterTolerance: Defines whether to trim (1) or reject (0) SQL parameters exceeding those required by the query.").Append(CRLF) sbInfo.Append("enableSQLiteLogs: Granular control. Enables (1) or disables (0) writing logs to users.db for this DB.").Append(CRLF) sbInfo.Append("pool.hikari.maximumPoolSize: Maximum simultaneous connections allowed. (Recommended N*Cores DB),").Append(CRLF) sbInfo.Append("pool.hikari.minimumIdle: Minimum idle connections. Recommended equal to maximumPoolSize for fixed-size pool,").Append(CRLF) sbInfo.Append("pool.hikari.maxLifetime (ms): Maximum lifetime of a connection. CRITICAL: Must be less than firewall/DB timeout,").Append(CRLF) sbInfo.Append("pool.hikari.connectionTimeout (ms): Maximum time client waits for an available connection (Default: 30000 ms),").Append(CRLF) sbInfo.Append("pool.hikari.idleTimeout (ms): Idle time before retiring the connection (ms). Only applies if minimumIdle < maximumPoolSize,").Append(CRLF) sbInfo.Append("pool.hikari.leakDetectionThreshold (ms): Threshold (ms) to detect unreturned connections (leaks).").Append(CRLF) sbInfo.Append(CRLF) For Each dbKey As String In allKeys Dim connector As RDCConnector = Main.Connectors.Get(dbKey) sbInfo.Append("--------------------------------------------------").Append(CRLF).Append(CRLF) sbInfo.Append($"---------------- ${dbKey} ------------------"$).Append(CRLF).Append(CRLF) If connector.IsInitialized Then Dim configMap As Map = connector.config ' Get metrics and REAL configuration applied by HikariCP Dim poolStats As Map = connector.GetPoolStats sbInfo.Append($"DriverClass: ${configMap.GetDefault("DriverClass", "N/A")}"$).Append(CRLF) sbInfo.Append($"JdbcUrl: ${configMap.GetDefault("JdbcUrl", "N/A")}"$).Append(CRLF) sbInfo.Append($"User: ${configMap.GetDefault("User", "N/A")}"$).Append(CRLF) sbInfo.Append($"ServerPort: ${configMap.GetDefault("ServerPort", "N/A")}"$).Append(CRLF).Append(CRLF) sbInfo.Append("--- POOL CONFIGURATION (HIKARICP - Applied Values) ---").Append(CRLF) sbInfo.Append($"MaximumPoolSize (Applied): ${poolStats.GetDefault("MaxPoolSize", 10).As(Int)}"$).Append(CRLF) sbInfo.Append($"MinimumIdle (Applied): ${poolStats.GetDefault("MinPoolSize", 10).As(Int)}"$).Append(CRLF) ' Report timeouts in Milliseconds (ms) sbInfo.Append($"MaxLifetime (ms): ${poolStats.GetDefault("MaxLifetime", 1800000).As(Long)}"$).Append(CRLF) sbInfo.Append($"ConnectionTimeout (ms): ${poolStats.GetDefault("ConnectionTimeout", 30000).As(Long)}"$).Append(CRLF) sbInfo.Append($"IdleTimeout (ms): ${poolStats.GetDefault("IdleTimeout", 600000).As(Long)}"$).Append(CRLF) sbInfo.Append($"LeakDetectionThreshold (ms): ${poolStats.GetDefault("LeakDetectionThreshold", 0).As(Long)}"$).Append(CRLF).Append(CRLF) If connector.driverProperties.IsInitialized And connector.driverProperties.Size > 0 Then sbInfo.Append("--- JDBC DRIVER PERFORMANCE PROPERTIES (Statement Optimization) ---").Append(CRLF) For Each propKey As String In connector.driverProperties.Keys Dim propValue As Object = connector.driverProperties.Get(propKey) sbInfo.Append($"[Driver] ${propKey}: ${propValue}"$).Append(CRLF) Next sbInfo.Append(CRLF) End If sbInfo.Append("--- RUNTIME STATUS (Dynamic Metrics) ---").Append(CRLF) sbInfo.Append($"Total Connections: ${poolStats.GetDefault("TotalConnections", "N/A")}"$).Append(CRLF) sbInfo.Append($"Busy Connections: ${poolStats.GetDefault("BusyConnections", "N/A")}"$).Append(CRLF) sbInfo.Append($"Idle Connections: ${poolStats.GetDefault("IdleConnections", "N/A")}"$).Append(CRLF) sbInfo.Append($"Handler Active Requests: ${poolStats.GetDefault("HandlerActiveRequests", "N/A")}"$).Append(CRLF).Append(CRLF) sbInfo.Append("--- BEHAVIOR ---").Append(CRLF) sbInfo.Append($"Debug (Reload Queries - DISABLED): ${configMap.GetDefault("Debug", "false")}"$).Append(CRLF) Dim tolerance As Int = configMap.GetDefault("parameterTolerance", 0).As(Int) sbInfo.Append($"ParameterTolerance: ${tolerance} (0=Strict, 1=Enabled)"$).Append(CRLF) Dim isLogsEnabledRuntime As Boolean = Main.SQLiteLoggingStatusByDB.GetDefault(dbKey, False).As(Boolean) Dim logsEnabledRuntimeInt As Int = 0 If isLogsEnabledRuntime Then logsEnabledRuntimeInt = 1 End If sbInfo.Append($"EnableSQLiteLogs: ${logsEnabledRuntimeInt} (0=Disabled, 1=Enabled)"$).Append(CRLF) sbInfo.Append(CRLF) Else sbInfo.Append($"ERROR: Connector ${dbKey} not initialized or failed at startup."$).Append(CRLF).Append(CRLF) End If Next resp.Write(sbInfo.ToString) Return Case "setlogstatus" Log(123) resp.ContentType = "text/plain; charset=utf-8" Dim dbKeyToChange As String = req.GetParameter("db").ToUpperCase Dim status As String = req.GetParameter("status") If Main.listaDeCP.IndexOf(dbKeyToChange) = -1 Then resp.Write($"ERROR: DBKey '${dbKeyToChange}' is not valid."$) Return End If Dim isEnabled As Boolean = (status = "1") Dim resultMsg As String Main.MainConnectorsLock.RunMethod("lock", Null) Try Main.SQLiteLoggingStatusByDB.Put(dbKeyToChange, isEnabled) Private hab As String = "DISABLED" If isEnabled Then hab = "ENABLED" resultMsg = $"Logs for ${dbKeyToChange} ${hab} on-the-fly."$ Main.IsAnySQLiteLoggingEnabled = False For Each dbKey As String In Main.listaDeCP If Main.SQLiteLoggingStatusByDB.GetDefault(dbKey, False) Then Main.IsAnySQLiteLoggingEnabled = True Exit End If Next If Main.IsAnySQLiteLoggingEnabled Then If Main.timerLogs.Enabled = False Then Main.timerLogs.Enabled = True resultMsg = resultMsg & " Cleanup timer ACTIVATED." Else Main.timerLogs.Enabled = False resultMsg = resultMsg & " Cleanup timer DISABLED globally." End If If Main.MainConnectorsLock.RunMethod("isHeldByCurrentThread", Null).As(Boolean) Then Main.MainConnectorsLock.RunMethod("unlock", Null) End If Catch resultMsg = $"CRITICAL ERROR modifying log status: ${LastException.Message}"$ If Main.MainConnectorsLock.RunMethod("isHeldByCurrentThread", Null).As(Boolean) Then Main.MainConnectorsLock.RunMethod("unlock", Null) End If End Try resp.Write(resultMsg) Return Case Else resp.ContentType = "text/plain; charset=utf-8" resp.SendError(404, $"Unknown command: '{Command}'"$) Return End Select End Sub