From 2bf75cadc089cff17dc4e75d76b99c31b7dd66cf Mon Sep 17 00:00:00 2001 From: Jose Alberto Guerra Ugalde Date: Mon, 1 Sep 2025 20:37:37 -0600 Subject: [PATCH] - VERSION 5.09.01 - Se corrigieron errores en "Manager". - Se cambiaron nombres de handlers. - Se corrigio un error en la ruta de "www/login.html". --- B4AHandler.bas | 319 ++++++++++++++++++ ChangePassHandler.bas | 53 +++ DBHandlerJSON.bas | 266 +++++++++++++++ DoLoginHandler.bas | 42 +++ Files/config.DB2.properties | 85 +++++ Files/config.DB3.properties | 76 +++++ Files/config.DB4.properties | 77 +++++ Files/config.properties | 72 +++-- Files/login.html | 21 ++ Files/reiniciaProcesoBow.bat | 9 + Files/www/login.html | 21 ++ HandlerB4X.bas | 609 +++++++++++++++++++++++++++++++++++ LoginHandler.bas | 23 ++ LogoutHandler.bas | 17 + Manager.bas | 340 +++++++++++++------ README.md | 137 +++++--- Readme1.md | 105 ++++++ jRDC_Multi.b4j | 124 +++++-- jRDC_Multi.b4j.meta | 16 +- reiniciaProcesoBow.bat | 9 + 20 files changed, 2217 insertions(+), 204 deletions(-) create mode 100644 B4AHandler.bas create mode 100644 ChangePassHandler.bas create mode 100644 DBHandlerJSON.bas create mode 100644 DoLoginHandler.bas create mode 100644 Files/config.DB2.properties create mode 100644 Files/config.DB3.properties create mode 100644 Files/config.DB4.properties create mode 100644 Files/login.html create mode 100644 Files/reiniciaProcesoBow.bat create mode 100644 Files/www/login.html create mode 100644 HandlerB4X.bas create mode 100644 LoginHandler.bas create mode 100644 LogoutHandler.bas create mode 100644 Readme1.md create mode 100644 reiniciaProcesoBow.bat diff --git a/B4AHandler.bas b/B4AHandler.bas new file mode 100644 index 0000000..901e9cf --- /dev/null +++ b/B4AHandler.bas @@ -0,0 +1,319 @@ +B4J=true +Group=Default Group +ModulesStructureVersion=1 +Type=Class +Version=4.19 +@EndOfDesignText@ +'Handler class +Sub Class_Globals +' #if VERSION1 + Private const T_NULL = 0, T_STRING = 1, T_SHORT = 2, T_INT = 3, T_LONG = 4, T_FLOAT = 5 _ + ,T_DOUBLE = 6, T_BOOLEAN = 7, T_BLOB = 8 As Byte + Private bc As ByteConverter + Private cs As CompressedStreams +' #end if + Private DateTimeMethods As Map + Private Connector As RDCConnector +End Sub + +Public Sub Initialize + DateTimeMethods = CreateMap(91: "getDate", 92: "getTime", 93: "getTimestamp") +End Sub + +Sub Handle(req As ServletRequest, resp As ServletResponse) + Log("********************* DB1 ********************") + Dim start As Long = DateTime.Now + Dim q As String + Dim in As InputStream = req.InputStream + Dim method As String = req.GetParameter("method") + Connector = Main.Connectors.Get("DB1") + Dim con As SQL + Try + con = Connector.GetConnection("DB1") + If method = "query2" Then + q = ExecuteQuery2("DB1", con, in, resp) + '#if VERSION1 + Else if method = "query" Then + in = cs.WrapInputStream(in, "gzip") + q = ExecuteQuery("DB1", con, in, resp) + Else if method = "batch" Then + in = cs.WrapInputStream(in, "gzip") + q = ExecuteBatch("DB1", con, in, resp) + '#end if + Else if method = "batch2" Then + q = ExecuteBatch2("DB1", con, in, resp) + Else + Log("Unknown method: " & method) + resp.SendError(500, "unknown method") + End If + Catch + Log(LastException) + resp.SendError(500, LastException.Message) + End Try + If con <> Null And con.IsInitialized Then con.Close + Log($"Command: ${q}, took: ${DateTime.Now - start}ms, client=${req.RemoteAddress}"$) +End Sub + +Private Sub ExecuteQuery2 (DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String + Dim ser As B4XSerializator + Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in)) + Dim cmd As DBCommand = m.Get("command") + Dim limit As Int = m.Get("limit") + Dim rs As ResultSet = con.ExecQuery2(Connector.GetCommand(DB, cmd.Name), cmd.Parameters) + If limit <= 0 Then limit = 0x7fffffff 'max int + Dim jrs As JavaObject = rs + Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) + Dim cols As Int = rs.ColumnCount + Dim res As DBResult + res.Initialize + res.columns.Initialize + res.Tag = Null 'without this the Tag properly will not be serializable. + For i = 0 To cols - 1 + res.columns.Put(rs.GetColumnName(i), i) + Next + res.Rows.Initialize + Do While rs.NextRow And limit > 0 + Dim row(cols) As Object + For i = 0 To cols - 1 + Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1)) + 'check whether it is a blob field + If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then + row(i) = rs.GetBlob2(i) + Else If ct = 2005 Then + row(i) = rs.GetString2(i) + Else if ct = 2 Or ct = 3 Then + row(i) = rs.GetDouble2(i) + Else If DateTimeMethods.ContainsKey(ct) Then + Dim SQLTime As JavaObject = jrs.RunMethodJO(DateTimeMethods.Get(ct), Array(i + 1)) + If SQLTime.IsInitialized Then + row(i) = SQLTime.RunMethod("getTime", Null) + Else + row(i) = Null + End If + Else + row(i) = jrs.RunMethod("getObject", Array(i + 1)) + End If + Next + res.Rows.Add(row) + Loop + rs.Close + Dim data() As Byte = ser.ConvertObjectToBytes(res) + resp.OutputStream.WriteBytes(data, 0, data.Length) + Return "query: " & cmd.Name +End Sub + +Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String + Dim ser As B4XSerializator + Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in)) + Dim commands As List = m.Get("commands") + Dim res As DBResult + res.Initialize + res.columns = CreateMap("AffectedRows (N/A)": 0) + res.Rows.Initialize + res.Tag = Null + Try + con.BeginTransaction + For Each cmd As DBCommand In commands + con.ExecNonQuery2(Connector.GetCommand(DB, cmd.Name), _ + cmd.Parameters) + Next + res.Rows.Add(Array As Object(0)) + con.TransactionSuccessful + Catch + con.Rollback + Log(LastException) + resp.SendError(500, LastException.Message) + End Try + Dim data() As Byte = ser.ConvertObjectToBytes(res) + resp.OutputStream.WriteBytes(data, 0, data.Length) + Return $"batch (size=${commands.Size})"$ +End Sub + +'#if VERSION1 + +Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String + Dim clientVersion As Float = ReadObject(in) 'ignore + Dim numberOfStatements As Int = ReadInt(in) + Dim res(numberOfStatements) As Int + Try + con.BeginTransaction + For i = 0 To numberOfStatements - 1 + Dim queryName As String = ReadObject(in) + Dim params As List = ReadList(in) + con.ExecNonQuery2(Connector.GetCommand(DB, queryName), _ + params) + Next + con.TransactionSuccessful + + Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") + WriteObject(Main.VERSION, out) + WriteObject("batch", out) + WriteInt(res.Length, out) + For Each r As Int In res + WriteInt(r, out) + Next + out.Close + Catch + con.Rollback + Log(LastException) + resp.SendError(500, LastException.Message) + End Try + Return $"batch (size=${numberOfStatements})"$ +End Sub + +Private Sub ExecuteQuery(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String +' Log("==== ExecuteQuery ==== ") + Dim clientVersion As Float = ReadObject(in) 'ignore + Dim queryName As String = ReadObject(in) + Dim limit As Int = ReadInt(in) + Dim params As List = ReadList(in) +' Log("EL QUERY: |" & queryName & "|") + Private theSql As String = Connector.GetCommand(DB, queryName) +' Log(theSql) +' Log(params) +' Log(params.Size) + Dim rs As ResultSet = con.ExecQuery2(theSql, params) + If limit <= 0 Then limit = 0x7fffffff 'max int + Dim jrs As JavaObject = rs + Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) + Dim cols As Int = rs.ColumnCount + Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") + WriteObject(Main.VERSION, out) + WriteObject("query", out) + WriteInt(rs.ColumnCount, out) +' Log($"cols: ${cols}"$) + For i = 0 To cols - 1 + WriteObject(rs.GetColumnName(i), out) + Next + + Do While rs.NextRow And limit > 0 + WriteByte(1, out) + For i = 0 To cols - 1 + Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1)) + 'check whether it is a blob field + If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then + WriteObject(rs.GetBlob2(i), out) + Else + WriteObject(jrs.RunMethod("getObject", Array(i + 1)), out) + End If + Next + Loop + WriteByte(0, out) + out.Close + rs.Close + + Return "query: " & queryName +End Sub + +Private Sub WriteByte(value As Byte, out As OutputStream) + out.WriteBytes(Array As Byte(value), 0, 1) +End Sub + +Private Sub WriteObject(o As Object, out As OutputStream) + Dim data() As Byte + If o = Null Then + out.WriteBytes(Array As Byte(T_NULL), 0, 1) + Else If o Is Short Then + out.WriteBytes(Array As Byte(T_SHORT), 0, 1) + data = bc.ShortsToBytes(Array As Short(o)) + Else If o Is Int Then + out.WriteBytes(Array As Byte(T_INT), 0, 1) + data = bc.IntsToBytes(Array As Int(o)) + Else If o Is Float Then + out.WriteBytes(Array As Byte(T_FLOAT), 0, 1) + data = bc.FloatsToBytes(Array As Float(o)) + Else If o Is Double Then + out.WriteBytes(Array As Byte(T_DOUBLE), 0, 1) + data = bc.DoublesToBytes(Array As Double(o)) + Else If o Is Long Then + out.WriteBytes(Array As Byte(T_LONG), 0, 1) + data = bc.LongsToBytes(Array As Long(o)) + Else If o Is Boolean Then + out.WriteBytes(Array As Byte(T_BOOLEAN), 0, 1) + Dim b As Boolean = o + Dim data(1) As Byte + If b Then data(0) = 1 Else data(0) = 0 + Else If GetType(o) = "[B" Then + data = o + out.WriteBytes(Array As Byte(T_BLOB), 0, 1) + WriteInt(data.Length, out) + Else 'If o Is String Then (treat all other values as string) + out.WriteBytes(Array As Byte(T_STRING), 0, 1) + data = bc.StringToBytes(o, "UTF8") + WriteInt(data.Length, out) + End If + If data.Length > 0 Then out.WriteBytes(data, 0, data.Length) +End Sub + +Private Sub ReadObject(In As InputStream) As Object + Dim data(1) As Byte + In.ReadBytes(data, 0, 1) + Select data(0) + Case T_NULL + Return Null + Case T_SHORT + Dim data(2) As Byte + Return bc.ShortsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_INT + Dim data(4) As Byte + Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_LONG + Dim data(8) As Byte + Return bc.LongsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_FLOAT + Dim data(4) As Byte + Return bc.FloatsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_DOUBLE + Dim data(8) As Byte + Return bc.DoublesFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_BOOLEAN + Dim b As Byte = ReadByte(In) + Return b = 1 + Case T_BLOB + Dim len As Int = ReadInt(In) + Dim data(len) As Byte + Return ReadBytesFully(In, data, data.Length) + Case Else + Dim len As Int = ReadInt(In) + Dim data(len) As Byte + ReadBytesFully(In, data, data.Length) + Return BytesToString(data, 0, data.Length, "UTF8") + End Select +End Sub + +Private Sub ReadBytesFully(In As InputStream, Data() As Byte, Len As Int) As Byte() + Dim count = 0, Read As Int + Do While count < Len And Read > -1 + Read = In.ReadBytes(Data, count, Len - count) + count = count + Read + Loop + Return Data +End Sub + +Private Sub WriteInt(i As Int, out As OutputStream) + Dim data() As Byte + data = bc.IntsToBytes(Array As Int(i)) + out.WriteBytes(data, 0, data.Length) +End Sub + +Private Sub ReadInt(In As InputStream) As Int + Dim data(4) As Byte + Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) +End Sub + +Private Sub ReadByte(In As InputStream) As Byte + Dim data(1) As Byte + In.ReadBytes(data, 0, 1) + Return data(0) +End Sub + +Private Sub ReadList(in As InputStream) As List + Dim len As Int = ReadInt(in) + Dim l1 As List + l1.Initialize + For i = 0 To len - 1 + l1.Add(ReadObject(in)) + Next + Return l1 +End Sub +'#end If \ No newline at end of file diff --git a/ChangePassHandler.bas b/ChangePassHandler.bas new file mode 100644 index 0000000..de14585 --- /dev/null +++ b/ChangePassHandler.bas @@ -0,0 +1,53 @@ +B4J=true +Group=Default Group +ModulesStructureVersion=1 +Type=Class +Version=10.3 +@EndOfDesignText@ +'Class module: ChangePassHandler +Sub Class_Globals + Private bc As BCrypt +End Sub + +Public Sub Initialize +' bc.Initialize ' <<--- CORRECCIÓN 1: Descomentado para que el objeto se cree. +End Sub + +Public Sub Handle(req As ServletRequest, resp As ServletResponse) + Log("--- CHANGEPASSHANDLER FUE LLAMADO ---") ' <--- ¡PON ESTA LÍNEA AQUÍ! + If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then + resp.SendRedirect("/login") + Return + End If + + Dim currentUser As String = req.GetSession.GetAttribute("username") + Dim currentPass As String = req.GetParameter("current_password") + Dim newPass As String = req.GetParameter("new_password") + Dim confirmPass As String = req.GetParameter("confirm_password") + + If newPass <> confirmPass Then + resp.Write("") + Return + End If + + Try + Dim storedHash As String = Main.SQL1.ExecQuerySingleResult2("SELECT password_hash FROM users WHERE username = ?", Array As String(currentUser)) + + Log("--- Probando con contraseña fija ---") + Log("Valor de la BD (storedHash): " & storedHash) + If storedHash = Null Or bc.checkpw("12345", storedHash) = False Then ' <<--- CAMBIO CLAVE AQUÍ + resp.Write("") + Return + End If + + ' <<--- CORRECCIÓN 2: Usamos el método seguro y consistente con 'Main'. + Dim newHashedPass As String = bc.hashpw(newPass, bc.gensalt) + Main.SQL1.ExecNonQuery2("UPDATE users SET password_hash = ? WHERE username = ?", Array As Object(newHashedPass, currentUser)) + + resp.Write("") + + Catch + Log(LastException) + resp.Write("") + End Try +End Sub \ No newline at end of file diff --git a/DBHandlerJSON.bas b/DBHandlerJSON.bas new file mode 100644 index 0000000..4adaea3 --- /dev/null +++ b/DBHandlerJSON.bas @@ -0,0 +1,266 @@ +B4J=true +Group=Default Group +ModulesStructureVersion=1 +Type=Class +Version=10.3 +@EndOfDesignText@ +' Handler class for JSON requests from Web Clients (JavaScript/axios) +' VERSIÓN 16 (Comentarios y Mensajes en Español): +' - Se añaden comentarios detallados a la versión con mensajes de error en español. +' - Revisa que el 'query' exista en config.properties antes de continuar. +' - Asegura que la conexión a la BD se cierre en todos los 'Return' para evitar fugas. +Sub Class_Globals + ' Declara una variable privada para mantener una instancia del conector RDC. + ' Este objeto maneja la comunicación con la base de datos. + Private Connector As RDCConnector +End Sub + +' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase. +' En este caso, no se necesita ninguna inicialización específica. +Public Sub Initialize + +End Sub + +' Este es el método principal que maneja las peticiones HTTP entrantes (req) y prepara la respuesta (resp). +Sub Handle(req As ServletRequest, resp As ServletResponse) + Log("============== DB1JsonHandler ==============") + ' --- Headers CORS (Cross-Origin Resource Sharing) --- + ' Estos encabezados son necesarios para permitir que un cliente web (ej. una página con JavaScript) + ' que se encuentra en un dominio diferente pueda hacer peticiones a este servidor. + resp.SetHeader("Access-Control-Allow-Origin", "*") ' Permite peticiones desde cualquier origen. + resp.SetHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") ' Métodos HTTP permitidos. + resp.SetHeader("Access-Control-Allow-Headers", "Content-Type") ' Encabezados permitidos en la petición. + + ' El método OPTIONS es una "petición de comprobación previa" (preflight request) que envían los navegadores + ' para verificar los permisos CORS antes de enviar la petición real (ej. POST). + ' Si es una petición OPTIONS, simplemente terminamos la ejecución sin procesar nada más. + If req.Method = "OPTIONS" Then Return + + ' Establece "DB1" como el nombre de la base de datos por defecto. + Dim DB As String = "DB1" + ' Obtiene el objeto conector para la base de datos por defecto desde el objeto Main. + Connector = Main.Connectors.Get(DB) + ' Declara una variable para la conexión SQL. + Dim con As SQL + + ' Inicia un bloque Try...Catch para manejar posibles errores durante la ejecución. + Try + ' Obtiene el valor del parámetro 'j' de la petición. Se espera que contenga una cadena JSON. + Dim jsonString As String = req.GetParameter("j") + ' Verifica si el parámetro 'j' es nulo o está vacío. + If jsonString = Null Or jsonString = "" Then + ' Si falta el parámetro, envía una respuesta de error 400 (Bad Request) y termina la ejecución. + SendErrorResponse(resp, 400, "Falta el parametro 'j' en el URL") + Return + End If + + ' Crea un objeto JSONParser para analizar la cadena JSON. + Dim parser As JSONParser + parser.Initialize(jsonString) + ' Convierte la cadena JSON en un objeto Map, que es como un diccionario (clave-valor). + Dim RootMap As Map = parser.NextObject + + ' Extrae los datos necesarios del JSON. + Dim execType As String = RootMap.GetDefault("exec", "") ' Tipo de ejecución: "executeQuery" o "executeCommand". + Dim queryName As String = RootMap.Get("query") ' Nombre del comando SQL (definido en config.properties). + Dim paramsMap As Map = RootMap.Get("params") ' Un mapa con los parámetros para la consulta. +' Log(RootMap) + ' Verifica si en el JSON se especificó un nombre de base de datos diferente con la clave "dbx". + If RootMap.Get("dbx") <> Null Then DB = RootMap.Get("dbx") ' Si se especifica, usamos la BD indicada, si no, se queda "DB1". + + ' Valida que el nombre de la base de datos (DB) exista en la lista de conexiones configuradas en Main. + If Main.listaDeCP.IndexOf(DB) = -1 Then + SendErrorResponse(resp, 400, "Parametro 'DB' invalido. El nombre '" & DB & "' no es válido.") + ' Se añade Return para detener la ejecución si la BD no es válida. + Return + End If + + ' Prepara una lista para almacenar las claves de los parámetros. + Dim paramKeys As List + paramKeys.Initialize + ' Si el mapa de parámetros existe y está inicializado... + If paramsMap <> Null And paramsMap.IsInitialized Then + ' ...itera sobre todas las claves y las añade a la lista 'paramKeys'. + For Each key As String In paramsMap.Keys + paramKeys.Add(key) + Next + End If + ' Ordena las claves alfabéticamente. Esto es crucial para asegurar que los parámetros + ' se pasen a la consulta SQL en un orden consistente y predecible. + paramKeys.Sort(True) + + ' Prepara una lista para almacenar los valores de los parámetros en el orden correcto. + Dim orderedParams As List + orderedParams.Initialize + ' Itera sobre la lista de claves ya ordenada. + For Each key As String In paramKeys + ' Añade el valor correspondiente a cada clave a la lista 'orderedParams'. + orderedParams.Add(paramsMap.Get(key)) + Next + + ' Obtiene una conexión a la base de datos del pool de conexiones. + con = Connector.GetConnection(DB) + ' Obtiene la cadena SQL del archivo de configuración usando el nombre de la consulta (queryName). + Dim sqlCommand As String = Connector.GetCommand(DB, queryName) + + ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE >>> + ' Comprueba si el comando SQL (query) especificado en el JSON fue encontrado en el archivo de configuración. + If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then + ' Si no se encontró el comando, crea un mensaje de error claro. + Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ + ' Registra el error en el log del servidor para depuración. + Log(errorMessage) + ' Envía una respuesta de error 400 (Bad Request) al cliente en formato JSON. + SendErrorResponse(resp, 400, errorMessage) + ' Cierra la conexión a la base de datos antes de salir para evitar fugas de conexión. + If con <> Null And con.IsInitialized Then con.Close + ' Detiene la ejecución del método Handle para esta petición. + Return + End If + ' <<< FIN NUEVA VALIDACIÓN >>> + + ' Comprueba el tipo de ejecución solicitado ("executeQuery" o "executeCommand"). + If execType.ToLowerCase = "executequery" Then + ' Declara una variable para almacenar el resultado de la consulta. + Dim rs As ResultSet + + ' Si el comando SQL contiene placeholders ('?'), significa que espera parámetros. + If sqlCommand.Contains("?") or orderedParams.Size > 0 Then + ' ================================================================= + ' === VALIDACIÓN DE CONTEO DE PARÁMETROS ========================== + ' ================================================================= + ' Calcula cuántos parámetros espera la consulta contando el número de '?'. + Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length + ' Obtiene cuántos parámetros se recibieron. + Dim receivedParams As Int = orderedParams.Size + ' Compara si la cantidad de parámetros esperados y recibidos es diferente. + + Log($"expectedParams: ${expectedParams}, receivedParams: ${receivedParams}"$) + + If expectedParams <> receivedParams Then + ' Si no coinciden, envía un error 400 detallado. + SendErrorResponse(resp, 400, $"Número de parametros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$) + ' Cierra la conexión antes de salir para evitar fugas. + If con <> Null And con.IsInitialized Then con.Close + ' Detiene la ejecución para evitar un error en la base de datos. + Return + End If + ' ================================================================= + ' Ejecuta la consulta pasando el comando SQL y la lista ordenada de parámetros. + rs = con.ExecQuery2(sqlCommand, orderedParams) + Else + ' Si no hay '?', ejecuta la consulta directamente sin parámetros. + rs = con.ExecQuery(sqlCommand) + End If + + ' --- Procesamiento de resultados --- + ' Prepara una lista para almacenar todas las filas del resultado. + Dim ResultList As List + ResultList.Initialize + ' Usa un objeto JavaObject para acceder a los metadatos del resultado (info de columnas). + Dim jrs As JavaObject = rs + Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) + ' Obtiene el número de columnas en el resultado. + Dim cols As Int = rsmd.RunMethod("getColumnCount", Null) + + ' Itera sobre cada fila del resultado (ResultSet). + Do While rs.NextRow + ' Crea un mapa para almacenar los datos de la fila actual (columna -> valor). + Dim RowMap As Map + RowMap.Initialize + ' Itera sobre cada columna de la fila. + For i = 1 To cols + ' Obtiene el nombre de la columna. + Dim ColumnName As String = rsmd.RunMethod("getColumnName", Array(i)) + ' Obtiene el valor de la columna. + Dim value As Object = jrs.RunMethod("getObject", Array(i)) + ' Añade la pareja (nombre_columna, valor) al mapa de la fila. + RowMap.Put(ColumnName, value) + Next + ' Añade el mapa de la fila a la lista de resultados. + ResultList.Add(RowMap) + Loop + ' Cierra el ResultSet para liberar recursos de la base de datos. + rs.Close + + ' Envía una respuesta de éxito con la lista de resultados en formato JSON. + SendSuccessResponse(resp, CreateMap("result": ResultList)) + + Else If execType.ToLowerCase = "executecommand" Then + ' Si es un comando (INSERT, UPDATE, DELETE), también valida los parámetros. + If sqlCommand.Contains("?") Then + ' ================================================================= + ' === VALIDACIÓN DE CONTEO DE PARÁMETROS (para Comandos) ========== + ' ================================================================= + Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length + Dim receivedParams As Int = orderedParams.Size + If expectedParams <> receivedParams Then + SendErrorResponse(resp, 400, $"Número de parametros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$) + ' Cierra la conexión antes de salir. + If con <> Null And con.IsInitialized Then con.Close + ' Detiene la ejecución. + Return + End If + ' ================================================================= + End If + + ' Ejecuta el comando que no devuelve resultados (NonQuery) con sus parámetros. + con.ExecNonQuery2(sqlCommand, orderedParams) + ' Envía una respuesta de éxito con un mensaje de confirmación. + SendSuccessResponse(resp, CreateMap("message": "Command executed successfully")) + + Else + ' Si el valor de 'exec' no es ni "executeQuery" ni "executeCommand", envía un error. + SendErrorResponse(resp, 400, "Parametro 'exec' inválido. '" & execType & "' no es un valor permitido.") + End If + + Catch + ' Si ocurre cualquier error inesperado en el bloque Try... + ' Registra la excepción completa en el log del servidor para diagnóstico. + Log(LastException) + ' Envía una respuesta de error 500 (Internal Server Error) con el mensaje de la excepción. + SendErrorResponse(resp, 500, LastException.Message) + End Try + + ' Este bloque se ejecuta siempre al final, haya habido error o no, *excepto si se usó Return antes*. + ' Comprueba si el objeto de conexión fue inicializado y sigue abierto. + If con <> Null And con.IsInitialized Then + ' Cierra la conexión para devolverla al pool y que pueda ser reutilizada. + ' Esto es fundamental para no agotar las conexiones a la base de datos. + con.Close + End If +End Sub + + +' --- Subrutinas de ayuda para respuestas JSON --- + +' Construye y envía una respuesta JSON de éxito. +Private Sub SendSuccessResponse(resp As ServletResponse, dataMap As Map) + ' Añade el campo "success": true al mapa de datos para indicar que todo salió bien. + dataMap.Put("success", True) + ' Crea un generador de JSON. + Dim jsonGenerator As JSONGenerator + jsonGenerator.Initialize(dataMap) + ' Establece el tipo de contenido de la respuesta a "application/json". + resp.ContentType = "application/json" + ' Escribe la cadena JSON generada en el cuerpo de la respuesta HTTP. + resp.Write(jsonGenerator.ToString) +End Sub + +' Construye y envía una respuesta JSON de error. +Private Sub SendErrorResponse(resp As ServletResponse, statusCode As Int, errorMessage As String) + ' Personaliza el mensaje de error si es un error común de parámetros de Oracle o JDBC. + If errorMessage.Contains("Índice de columnas no válido") Or errorMessage.Contains("ORA-17003") Then errorMessage = "NUMERO DE PARAMETROS EQUIVOCADO: " & errorMessage + ' Crea un mapa con el estado de error y el mensaje. + Dim resMap As Map = CreateMap("success": False, "error": errorMessage) + ' Genera la cadena JSON a partir del mapa. + Dim jsonGenerator As JSONGenerator + jsonGenerator.Initialize(resMap) + ' Establece el código de estado HTTP (ej. 400 para error del cliente, 500 para error del servidor). + resp.Status = statusCode + ' Establece el tipo de contenido y escribe la respuesta de error. + resp.ContentType = "application/json" + resp.Write(jsonGenerator.ToString) +End Sub + + diff --git a/DoLoginHandler.bas b/DoLoginHandler.bas new file mode 100644 index 0000000..bb21d58 --- /dev/null +++ b/DoLoginHandler.bas @@ -0,0 +1,42 @@ +B4J=true +Group=Default Group +ModulesStructureVersion=1 +Type=Class +Version=10.3 +@EndOfDesignText@ +'Class module: DoLoginHandler +Sub Class_Globals + Private bc As BCrypt +End Sub + +Public Sub Initialize +' bc.Initialize +End Sub + +Public Sub Handle(req As ServletRequest, resp As ServletResponse) + ' Limpiamos el input del usuario para evitar errores + Dim u As String = req.GetParameter("username").Trim.ToLowerCase + Dim p As String = req.GetParameter("password") + + Try + ' Buscamos el hash en la base de datos usando el usuario limpio + Dim storedHash As String = Main.SQL1.ExecQuerySingleResult2("SELECT password_hash FROM users WHERE username = ?", Array As String(u)) + + ' Verificamos la contraseña contra el hash + If storedHash <> Null And bc.checkpw(p, storedHash) Then + ' CREDENCIALES CORRECTAS + ' 1. Autorizamos la sesión + req.GetSession.SetAttribute("user_is_authorized", True) + ' 2. ¡Y guardamos el nombre de usuario! (Esta es la línea que faltaba) + req.GetSession.SetAttribute("username", u) + + resp.SendRedirect("/manager") + Else + ' Credenciales incorrectas + resp.SendRedirect("/login") + End If + Catch + Log(LastException) + resp.SendRedirect("/login") + End Try +End Sub \ No newline at end of file diff --git a/Files/config.DB2.properties b/Files/config.DB2.properties new file mode 100644 index 0000000..5281325 --- /dev/null +++ b/Files/config.DB2.properties @@ -0,0 +1,85 @@ +#Lines starting with '#' are comments. +#Backslash character at the end of line means that the command continues in the next line. + +DriverClass=oracle.jdbc.driver.OracleDriver +#JdbcUrl=jdbc:mysql://localhost/test?characterEncoding=utf8 + +#SQL Server +#DriverClass=net.sourceforge.jtds.jdbc.Driver + +# este para produccion GHAN JdbcUrl=jdbc:oracle:thin:@//192.168.15.53:1521/DBKMT +#GOHAN ---> server +#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT +#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT +JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT + +# SVR-KEYMON-PRODUCCION--> Usuario +User=SALMA +Password=SALMAD2016M + +#User=TORRADOCONAUTO +#Password=TORRADOCONAUTOD2016M + + +#--> Puertos +#SAC - DFR - MDA / GOHAN -->COBRANZA +#ServerPort=1783 +#GUNA - SALMA - DURAKELO - DBC / SVR-KEYMON-PRODUCCION --> DISTRIBUIDORAS +ServerPort=9010 +#CMG - TORRADO / TRUNKS -->COBRANZA/ GM +#ServerPort=1781 + +#If Debug is true then this file will be reloaded on every query. +#This is useful if you need to modify the queries. +Debug=true + +#SQL COMMANDS + +################## +################# +################ S O P O R T E +################# +################## + +sql.traeConexion=select 'DB2' as conexion from dual +sql.select_soporte=select * from GUNA.soporte +sql.select_conexion=SELECT 'OK' AS VALOR FROM DUAL +sql.select_version=select cat_ve_version from cat_version +sql.select_version_GV2=select cat_ve_version from GUNA.cat_version +sql.selectAlmacen=select * from cat_almacen where cat_al_id = ? +sql.sv=select * from cat_rutas where CAT_RU_RUTA = ? + +sql.update_usuario_guna_nobajas=UPDATE GUNA.CAT_LOGINS SET CAT_LO_ESTATUS = 'Activo',CAT_LO_CONECTADO ='0' WHERE CAT_LO_ESTATUS != 'Baja' and CAT_LO_USUARIO = (?) + +sql.proc_usuario=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN SP_ACTIVAR_USUARIO( '''||(?)||''',Cursor_SYS); end;'); END; + +sql.select_almacenes_KELL=select CAT_AG_ID, CAT_AG_NOMBRE from KELLOGGS.cat_agencias order by CAT_AG_NOMBRE +sql.select_almacenes_GUNA=select CAT_AG_ID, CAT_AG_NOMBRE from GUNA.cat_agencias order by CAT_AG_NOMBRE +sql.select_almacenes_SALMA=select CAT_AG_ID, CAT_AG_NOMBRE from SALMA.cat_agencias order by CAT_AG_NOMBRE +sql.select_almacenes_DANVIT=select CAT_AG_ID, CAT_AG_NOMBRE from DANVIT.cat_agencias order by CAT_AG_NOMBRE + +sql.proc_QUITAR_VENTA_KELL=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_QUITAR_VENTA_X_TIPO( '''||(?)||''', '''||(?)||''', '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; +sql.proc_QUITAR_VENTA_GUNA=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN GUNA.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS, '''||(?)||'''); end;'); END; +sql.proc_QUITAR_VENTA_SALMA=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN SALMA.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; +sql.proc_QUITAR_VENTA_DANVIT=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN DANVIT.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; +sql.proc_QUITAR_PAGOPAGARE_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_ELIMINAS_PAGOS_PAGARES_REP( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; +sql.proc_LIBERA_BANDERA_FACTURACION_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_LIBERA_FACTURACION(Cursor_SYS); end;'); END; +sql.proc_LIBERA_BANDERA_CARGAFORANEA_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_LLENAR_FILTROS ( '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; + + +sql.proc_QUITAR_TICKET_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_QUITAR_TICKET( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; + +sql.revisa_liquidada_Guna=SELECT COUNT(*) as liquidada FROM GUNA.HIST_VENTAS_DETALLE WHERE trunc(HVD_DTESYNC) = trunc(sysdate) and hvd_almacen = (?) and hvd_ruta = (?) AND (HVD_DESCUENTO != 0 or HVD_FECHA_AVION IS NOT NULL) +sql.revisa_liquidada_Kell=SELECT COUNT(*) as liquidada FROM KELLOGGS.HIST_VENTAS_DETALLE WHERE trunc(HVD_DTESYNC) = trunc(sysdate) and hvd_almacen = (?) and hvd_ruta = (?) and HVD_TIPOVENTA = (?) AND HVD_ESTATUS = 'Liquidado' + +sql.select_todos_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_lo_ruta from cat_logins left join cat_agencias on cat_lo_agencia = cat_ag_id where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_lo_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_todosGUNA_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from cat_logins left join cat_agencias on cat_lo_agencia = cat_ag_id left join cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_todosKELLOGGS_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from KELLOGGS.cat_logins left join KELLOGGS.cat_agencias on cat_lo_agencia = cat_ag_id left join KELLOGGS.cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_todosSALMA_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_lo_ruta as cat_ru_ruta from SALMA.cat_logins left join SALMA.cat_agencias on cat_lo_agencia = cat_ag_id where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_lo_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_todosDANVIT_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from DANVIT.cat_logins left join DANVIT.cat_agencias on cat_lo_agencia = cat_ag_id left join DANVIT.cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_ventaXrutaGuna_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa +sql.select_ventaXrutaKelloggs_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from KELLOGGS.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) and hvd_tipoventa=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa +sql.select_ventaXrutaSalma_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from SALMA.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa +sql.select_ventaXrutaDanvit_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from DANVIT.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa +sql.select_prodsTicket_Kelloggs=SELECT HVD_CLIENTE CLIENTE, HVD_PROID PRODUCTO_ID, HVD_PRONOMBRE NOMBRE_PRODUCTO, HVD_CANT CANTIDAD, HVD_COSTO_TOT COSTO_TOTAL, HVD_RUTA RUTA, HVD_CODPROMO CODPROMO,NVL(HVD_TIPOVENTA,' ') TIPOVENTA, NVL(HVD_ESTATUS,' ') ESTATUS, hvd_cedis FROM KELLOGGS.HIST_VENTAS_DETALLE WHERE TRUNC(HVD_FECHA) = TRUNC(SYSDATE) AND HVD_ALMACEN = (?) AND HVD_CLIENTE = (?) and hvd_rechazo is null ORDER BY HVD_CODPROMO, HVD_PRONOMBRE + diff --git a/Files/config.DB3.properties b/Files/config.DB3.properties new file mode 100644 index 0000000..e4bf6f5 --- /dev/null +++ b/Files/config.DB3.properties @@ -0,0 +1,76 @@ +#Lines starting with '#' are comments. +#Backslash character at the end of line means that the command continues in the next line. + +DriverClass=oracle.jdbc.driver.OracleDriver +#JdbcUrl=jdbc:mysql://localhost/test?characterEncoding=utf8 + +#SQL Server +#DriverClass=net.sourceforge.jtds.jdbc.Driver + +# este para produccion GHAN JdbcUrl=jdbc:oracle:thin:@//192.168.15.53:1521/DBKMT +#GOHAN ---> server +#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT +#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT +JdbcUrl=jdbc:oracle:thin:@//192.168.101.12:1521/DBKMT + + +# SVR-KEYMON-PRODUCCION--> Usuario +#User=GUNA +#Password=GUNAD2015M + +User=TORRADOCONAUTO +Password=TORRADOCONAUTOD2016M + +#--> Puertos +#SAC - DFR - MDA / GOHAN -->COBRANZA +#ServerPort=1783 +#GUNA - SALMA - DURAKELO - DBC / SVR-KEYMON-PRODUCCION --> DISTRIBUIDORAS +ServerPort=9010 +#CMG - TORRADO / TRUNKS -->COBRANZA/ GM +#ServerPort=1781 + +#If Debug is true then this file will be reloaded on every query. +#This is useful if you need to modify the queries. +Debug=true + +#SQL COMMANDS + +################## +################# +################ S O P O R T E +################# +################## + +sql.traeConexion=select 'DB3' as conexion from dual +sql.select_soporte=select * from GUNA.soporte + +sql.select_almacenes_KELL=select CAT_AG_ID, CAT_AG_NOMBRE from KELLOGGS.cat_agencias order by CAT_AG_NOMBRE +sql.select_almacenes_GUNA=select CAT_AG_ID, CAT_AG_NOMBRE from GUNA.cat_agencias order by CAT_AG_NOMBRE +sql.select_almacenes_SALMA=select CAT_AG_ID, CAT_AG_NOMBRE from SALMA.cat_agencias order by CAT_AG_NOMBRE +sql.select_almacenes_DANVIT=select CAT_AG_ID, CAT_AG_NOMBRE from DANVIT.cat_agencias order by CAT_AG_NOMBRE + +sql.proc_QUITAR_VENTA_KELL=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_QUITAR_VENTA_X_TIPO( '''||(?)||''', '''||(?)||''', '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; +sql.proc_QUITAR_VENTA_GUNA=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN GUNA.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS, '''||(?)||'''); end;'); END; +sql.proc_QUITAR_VENTA_SALMA=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN SALMA.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; +sql.proc_QUITAR_VENTA_DANVIT=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN DANVIT.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; +sql.proc_QUITAR_PAGOPAGARE_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_ELIMINAS_PAGOS_PAGARES_REP( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; +sql.proc_LIBERA_BANDERA_FACTURACION_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_LIBERA_FACTURACION(Cursor_SYS); end;'); END; +sql.proc_LIBERA_BANDERA_CARGAFORANEA_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_LLENAR_FILTROS ( '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; + + +sql.proc_QUITAR_TICKET_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_QUITAR_TICKET( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; + +sql.revisa_liquidada_Guna=SELECT COUNT(*) as liquidada FROM GUNA.HIST_VENTAS_DETALLE WHERE trunc(HVD_DTESYNC) = trunc(sysdate) and hvd_almacen = (?) and hvd_ruta = (?) AND (HVD_DESCUENTO != 0 or HVD_FECHA_AVION IS NOT NULL) +sql.revisa_liquidada_Kell=SELECT COUNT(*) as liquidada FROM KELLOGGS.HIST_VENTAS_DETALLE WHERE trunc(HVD_DTESYNC) = trunc(sysdate) and hvd_almacen = (?) and hvd_ruta = (?) and HVD_TIPOVENTA = (?) AND HVD_ESTATUS = 'Liquidado' + +sql.select_todos_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_lo_ruta from cat_logins left join cat_agencias on cat_lo_agencia = cat_ag_id where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_lo_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_todosGUNA_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from cat_logins left join cat_agencias on cat_lo_agencia = cat_ag_id left join cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_todosKELLOGGS_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from KELLOGGS.cat_logins left join KELLOGGS.cat_agencias on cat_lo_agencia = cat_ag_id left join KELLOGGS.cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_todosSALMA_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_lo_ruta as cat_ru_ruta from SALMA.cat_logins left join SALMA.cat_agencias on cat_lo_agencia = cat_ag_id where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_lo_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_todosDANVIT_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from DANVIT.cat_logins left join DANVIT.cat_agencias on cat_lo_agencia = cat_ag_id left join DANVIT.cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_ventaXrutaGuna_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa +sql.select_ventaXrutaKelloggs_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from KELLOGGS.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) and hvd_tipoventa=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa +sql.select_ventaXrutaSalma_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from SALMA.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa +sql.select_ventaXrutaDanvit_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from DANVIT.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa +sql.select_prodsTicket_Kelloggs=SELECT HVD_CLIENTE CLIENTE, HVD_PROID PRODUCTO_ID, HVD_PRONOMBRE NOMBRE_PRODUCTO, HVD_CANT CANTIDAD, HVD_COSTO_TOT COSTO_TOTAL, HVD_RUTA RUTA, HVD_CODPROMO CODPROMO,NVL(HVD_TIPOVENTA,' ') TIPOVENTA, NVL(HVD_ESTATUS,' ') ESTATUS, hvd_cedis FROM KELLOGGS.HIST_VENTAS_DETALLE WHERE TRUNC(HVD_FECHA) = TRUNC(SYSDATE) AND HVD_ALMACEN = (?) AND HVD_CLIENTE = (?) and hvd_rechazo is null ORDER BY HVD_CODPROMO, HVD_PRONOMBRE + diff --git a/Files/config.DB4.properties b/Files/config.DB4.properties new file mode 100644 index 0000000..6a6392a --- /dev/null +++ b/Files/config.DB4.properties @@ -0,0 +1,77 @@ +#Lines starting with '#' are comments. +#Backslash character at the end of line means that the command continues in the next line. + +DriverClass=oracle.jdbc.driver.OracleDriver +#JdbcUrl=jdbc:mysql://localhost/test?characterEncoding=utf8 + +#SQL Server +#DriverClass=net.sourceforge.jtds.jdbc.Driver + +# este para produccion GHAN JdbcUrl=jdbc:oracle:thin:@//192.168.15.53:1521/DBKMT +#GOHAN ---> server +#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT +#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT +JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT + +# SVR-KEYMON-PRODUCCION--> Usuario +User=SALMA +Password=SALMAD2016M + +#User=TORRADOCONAUTO +#Password=TORRADOCONAUTOD2016M + + +#--> Puertos +#SAC - DFR - MDA / GOHAN -->COBRANZA +#ServerPort=1783 +#GUNA - SALMA - DURAKELO - DBC / SVR-KEYMON-PRODUCCION --> DISTRIBUIDORAS +ServerPort=9000 +#CMG - TORRADO / TRUNKS -->COBRANZA/ GM +#ServerPort=1781 + +#If Debug is true then this file will be reloaded on every query. +#This is useful if you need to modify the queries. +Debug=true + +#SQL COMMANDS + +################## +################# +################ S O P O R T E +################# +################## + +sql.traeConexion=select 'DB4' as conexion from dual +sql.select_soporte=select * from GUNA.soporte +sql.select_conexion=SELECT 'OK' AS VALOR FROM DUAL + +sql.select_almacenes_KELL=select CAT_AG_ID, CAT_AG_NOMBRE from KELLOGGS.cat_agencias order by CAT_AG_NOMBRE +sql.select_almacenes_GUNA=select CAT_AG_ID, CAT_AG_NOMBRE from GUNA.cat_agencias order by CAT_AG_NOMBRE +sql.select_almacenes_SALMA=select CAT_AG_ID, CAT_AG_NOMBRE from SALMA.cat_agencias order by CAT_AG_NOMBRE +sql.select_almacenes_DANVIT=select CAT_AG_ID, CAT_AG_NOMBRE from DANVIT.cat_agencias order by CAT_AG_NOMBRE + +sql.proc_QUITAR_VENTA_KELL=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_QUITAR_VENTA_X_TIPO( '''||(?)||''', '''||(?)||''', '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; +sql.proc_QUITAR_VENTA_GUNA=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN GUNA.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS, '''||(?)||'''); end;'); END; +sql.proc_QUITAR_VENTA_SALMA=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN SALMA.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; +sql.proc_QUITAR_VENTA_DANVIT=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN DANVIT.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; +sql.proc_QUITAR_PAGOPAGARE_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_ELIMINAS_PAGOS_PAGARES_REP( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; +sql.proc_LIBERA_BANDERA_FACTURACION_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_LIBERA_FACTURACION(Cursor_SYS); end;'); END; +sql.proc_LIBERA_BANDERA_CARGAFORANEA_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_LLENAR_FILTROS ( '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; + + +sql.proc_QUITAR_TICKET_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_QUITAR_TICKET( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END; + +sql.revisa_liquidada_Guna=SELECT COUNT(*) as liquidada FROM GUNA.HIST_VENTAS_DETALLE WHERE trunc(HVD_DTESYNC) = trunc(sysdate) and hvd_almacen = (?) and hvd_ruta = (?) AND (HVD_DESCUENTO != 0 or HVD_FECHA_AVION IS NOT NULL) +sql.revisa_liquidada_Kell=SELECT COUNT(*) as liquidada FROM KELLOGGS.HIST_VENTAS_DETALLE WHERE trunc(HVD_DTESYNC) = trunc(sysdate) and hvd_almacen = (?) and hvd_ruta = (?) and HVD_TIPOVENTA = (?) AND HVD_ESTATUS = 'Liquidado' + +sql.select_todos_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_lo_ruta from cat_logins left join cat_agencias on cat_lo_agencia = cat_ag_id where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_lo_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_todosGUNA_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from cat_logins left join cat_agencias on cat_lo_agencia = cat_ag_id left join cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_todosKELLOGGS_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from KELLOGGS.cat_logins left join KELLOGGS.cat_agencias on cat_lo_agencia = cat_ag_id left join KELLOGGS.cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_todosSALMA_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_lo_ruta as cat_ru_ruta from SALMA.cat_logins left join SALMA.cat_agencias on cat_lo_agencia = cat_ag_id where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_lo_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_todosDANVIT_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from DANVIT.cat_logins left join DANVIT.cat_agencias on cat_lo_agencia = cat_ag_id left join DANVIT.cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20 +sql.select_ventaXrutaGuna_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa +sql.select_ventaXrutaKelloggs_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from KELLOGGS.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) and hvd_tipoventa=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa +sql.select_ventaXrutaSalma_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from SALMA.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa +sql.select_ventaXrutaDanvit_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from DANVIT.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa +sql.select_prodsTicket_Kelloggs=SELECT HVD_CLIENTE CLIENTE, HVD_PROID PRODUCTO_ID, HVD_PRONOMBRE NOMBRE_PRODUCTO, HVD_CANT CANTIDAD, HVD_COSTO_TOT COSTO_TOTAL, HVD_RUTA RUTA, HVD_CODPROMO CODPROMO,NVL(HVD_TIPOVENTA,' ') TIPOVENTA, NVL(HVD_ESTATUS,' ') ESTATUS, hvd_cedis FROM KELLOGGS.HIST_VENTAS_DETALLE WHERE TRUNC(HVD_FECHA) = TRUNC(SYSDATE) AND HVD_ALMACEN = (?) AND HVD_CLIENTE = (?) and hvd_rechazo is null ORDER BY HVD_CODPROMO, HVD_PRONOMBRE + diff --git a/Files/config.properties b/Files/config.properties index d29470b..458ed53 100644 --- a/Files/config.properties +++ b/Files/config.properties @@ -1,29 +1,57 @@ #Lines starting with '#' are comments. #Backslash character at the end of line means that the command continues in the next line. + +DriverClass=oracle.jdbc.driver.OracleDriver +#JdbcUrl=jdbc:mysql://localhost/test?characterEncoding=utf8 -#DATABASE CONFIGURATION -DriverClass=com.mysql.jdbc.Driver -JdbcUrl=jdbc:mysql://localhost/test?characterEncoding=utf8 -User=root -Password= -#Java server port -ServerPort=17178 - -#example of MS SQL Server configuration: +#SQL Server #DriverClass=net.sourceforge.jtds.jdbc.Driver -#JdbcUrl=jdbc:jtds:sqlserver:/// -#example of postegres configuration: -#JdbcUrl=jdbc:postgresql://localhost/test -#DriverClass=org.postgresql.Driver +# este para produccion GHAN JdbcUrl=jdbc:oracle:thin:@//192.168.15.53:1521/DBKMT +#GOHAN ---> server +#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT +#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT +JdbcUrl=jdbc:oracle:thin:@//192.168.101.10:1521/DBKMT?oracle.jdbc.defaultClientIdentifier=jRDC_Multi + + +# SVR-KEYMON-PRODUCCION--> Usuario +User=GUNA +Password=GUNAD2015M + +#User=TORRADOCONAUTO +#Password=TORRADOCONAUTOD2016M + + +#--> Puertos +#SAC - DFR - MDA / GOHAN -->COBRANZA +#ServerPort=1783 +#GUNA - SALMA - DURAKELO - DBC / SVR-KEYMON-PRODUCCION --> DISTRIBUIDORAS +ServerPort=9010 +#CMG - TORRADO / TRUNKS -->COBRANZA/ GM +#ServerPort=1781 + +#If Debug is true then this file will be reloaded on every query. +#This is useful if you need to modify the queries. +Debug=true #SQL COMMANDS -sql.create_table=CREATE TABLE IF NOT EXISTS animals (\ - id INTEGER PRIMARY KEY AUTO_INCREMENT,\ - name CHAR(30) NOT NULL,\ - image BLOB) -sql.insert_animal=INSERT INTO animals VALUES (null, ?,?) -sql.select_animal=SELECT name, image, id FROM animals WHERE id = ?; -sql.create_table=CREATE TABLE article (col1 numeric(10,4) ,col2 text); -sql.select=select * from article -sql.insert=INSERT INTO article VALUES(?, ?) \ No newline at end of file + +################## +################# +################ S O P O R T E +################# +################## + +sql.select_revisaClienteCredito_GUNA2=select (select count(CAT_CL_CODIGO) from GUNA.CAT_CLIENTES where CAT_CL_CODIGO = ? and CAT_CL_IDALMACEN <> '100') as cuantos, (select count(ID_CLIENTE) from GUNA.CAT_CLIENTES_CREDITO where ID_CLIENTE = ?) as cuantosCredito from DUAL + +sql.traeConexion=select 'DB1' as conexion from dual +sql.traeConexion2=select 'DB1' as conexion from dual +sql.select_soporte=select * from GUNA.soporte +sql.select_conexion=SELECT 'OK' AS VALOR FROM DUAL +sql.selectAlmacen=select cat_al_id, cat_al_desc, cat_al_archftp from cat_almacen where cat_al_id = ? +sql.select_version=select cat_ve_version from cat_version +sql.select_version_GV2=select cat_ve_version from GUNA.cat_version + +sql.update_usuario_guna_nobajas=UPDATE GUNA.CAT_LOGINS SET CAT_LO_ESTATUS = 'Activo',CAT_LO_CONECTADO ='0' WHERE CAT_LO_ESTATUS != 'Baja' and CAT_LO_USUARIO = (?) + +sql.proc_usuario=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN SP_ACTIVAR_USUARIO( '''||(?)||''',Cursor_SYS); end;'); END; diff --git a/Files/login.html b/Files/login.html new file mode 100644 index 0000000..60a6738 --- /dev/null +++ b/Files/login.html @@ -0,0 +1,21 @@ + + + + + Login jRDC Server + + + +
+

Acceso al Manager

+ + + +
+ + \ No newline at end of file diff --git a/Files/reiniciaProcesoBow.bat b/Files/reiniciaProcesoBow.bat new file mode 100644 index 0000000..439bb91 --- /dev/null +++ b/Files/reiniciaProcesoBow.bat @@ -0,0 +1,9 @@ +@rem Este script reinicia el proceso en PM2 del servidor de jRDC2 + +@rem estas lineas sirven para que el archivo bat corra en modo administrador. +set "params=%*" +cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && ""%~s0"" %params%", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && exit /B ) + +pm2 restart BotSoporte_4.0 + +exit \ No newline at end of file diff --git a/Files/www/login.html b/Files/www/login.html new file mode 100644 index 0000000..60a6738 --- /dev/null +++ b/Files/www/login.html @@ -0,0 +1,21 @@ + + + + + Login jRDC Server + + + +
+

Acceso al Manager

+ + + +
+ + \ No newline at end of file diff --git a/HandlerB4X.bas b/HandlerB4X.bas new file mode 100644 index 0000000..b7b2dfb --- /dev/null +++ b/HandlerB4X.bas @@ -0,0 +1,609 @@ +B4J=true +Group=Default Group +ModulesStructureVersion=1 +Type=Class +Version=10.3 +@EndOfDesignText@ +' Handler genérico para peticiones desde clientes B4A/B4i (DBRequestManager) +' Determina la base de datos a utilizar dinámicamente a partir de la URL de la petición. +' Versión con validación de parámetros y errores en texto plano. +Sub Class_Globals + ' Estas constantes y variables solo se compilan si se usa la #if VERSION1, + ' lo que sugiere que es para dar soporte a una versión antigua del protocolo de comunicación. +' #if VERSION1 + ' Constantes para identificar los tipos de datos en la serialización personalizada (protocolo V1). + Private const T_NULL = 0, T_STRING = 1, T_SHORT = 2, T_INT = 3, T_LONG = 4, T_FLOAT = 5 _ + ,T_DOUBLE = 6, T_BOOLEAN = 7, T_BLOB = 8 As Byte + ' Utilidades para convertir entre tipos de datos y arrays de bytes. + Private bc As ByteConverter + ' Utilidad para comprimir/descomprimir streams de datos (usado en V1). + Private cs As CompressedStreams +' #end if + + ' Mapa para convertir tipos de columna JDBC de fecha/hora a métodos de obtención de datos. + Private DateTimeMethods As Map + ' Objeto que gestiona las conexiones a las diferentes bases de datos definidas en config.properties. + Private Connector As RDCConnector +End Sub + +' Se ejecuta una vez cuando se crea una instancia de esta clase. +Public Sub Initialize + ' Inicializa el mapa que asocia los códigos de tipo de columna de fecha/hora de JDBC + ' con los nombres de los métodos correspondientes para leerlos correctamente. + DateTimeMethods = CreateMap(91: "getDate", 92: "getTime", 93: "getTimestamp") +End Sub + +' Método principal que maneja cada petición HTTP que llega a este servlet. +Sub Handle(req As ServletRequest, resp As ServletResponse) + ' === INICIO DE LA LÓGICA DINÁMICA === + ' Extrae la URI completa de la petición (ej. /DB1/endpoint). + Dim URI As String = req.RequestURI + ' Variable para almacenar la "llave" o identificador de la base de datos (ej. "DB1"). + Dim dbKey As String + + ' Comprueba si la URI tiene contenido y empieza con "/". + If URI.Length > 1 And URI.StartsWith("/") Then + ' Extrae la parte de la URI que viene después del primer "/". + dbKey = URI.Substring(1) + ' Si la llave contiene más "/", se queda solo con la primera parte. + ' Esto permite URLs como /DB1/clientes o /DB2/productos, extrayendo "DB1" o "DB2". + If dbKey.Contains("/") Then + dbKey = dbKey.SubString2(0, dbKey.IndexOf("/")) + End If + Else + ' Si la URI está vacía o es "/", usa "DB1" como la base de datos por defecto. + dbKey = "DB1" + End If + + ' Convierte la llave a mayúsculas para que no sea sensible a mayúsculas/minúsculas (ej. "db1" se convierte en "DB1"). + dbKey = dbKey.ToUpperCase + + ' Verifica si la llave de la base de datos extraída existe en la configuración de conectores. + If Main.Connectors.ContainsKey(dbKey) = False Then + ' Si no existe, crea un mensaje de error claro. + Dim ErrorMsg As String = $"Invalid DB key specified in URL: '${dbKey}'. Valid keys are: ${Main.listaDeCP}"$ + ' Registra el error en el log del servidor. + Log(ErrorMsg) + ' Envía una respuesta de error 400 (Bad Request) al cliente en formato de texto plano. + SendPlainTextError(resp, 400, ErrorMsg) + ' Termina la ejecución de este método. + Return + End If + ' === FIN DE LA LÓGICA DINÁMICA === + + Log("********************* " & dbKey & " ********************") + ' Guarda el tiempo de inicio para medir la duración de la petición. + Dim start As Long = DateTime.Now + ' Variable para almacenar el nombre del comando SQL a ejecutar. + Dim q As String + ' Obtiene el stream de entrada de la petición, que contiene los datos enviados por el cliente. + Dim in As InputStream = req.InputStream + ' Obtiene el parámetro "method" de la URL (ej. ?method=query2). + Dim method As String = req.GetParameter("method") + ' Obtiene el conector correspondiente a la base de datos seleccionada. + Connector = Main.Connectors.Get(dbKey) + ' Declara la variable para la conexión a la base de datos. + Dim con As SQL + Try + ' Obtiene una conexión del pool de conexiones. + con = Connector.GetConnection(dbKey) + Log("Metodo: " & method) + ' Determina qué función ejecutar basándose en el parámetro "method". + If method = "query2" Then + ' Ejecuta una consulta usando el protocolo más nuevo (B4XSerializator). + q = ExecuteQuery2(dbKey, con, in, resp) + '#if VERSION1 + Else if method = "query" Then + ' Protocolo antiguo: descomprime el stream y ejecuta la consulta. + in = cs.WrapInputStream(in, "gzip") + q = ExecuteQuery(dbKey, con, in, resp) + Else if method = "batch" Then + ' Protocolo antiguo: descomprime el stream y ejecuta un lote de comandos. + in = cs.WrapInputStream(in, "gzip") + q = ExecuteBatch(dbKey, con, in, resp) + '#end if + Else if method = "batch2" Then + ' Ejecuta un lote de comandos usando el protocolo más nuevo. + q = ExecuteBatch2(dbKey, con, in, resp) + Else + ' Si el método es desconocido, lo registra y envía un error. + Log("Unknown method: " & method) + SendPlainTextError(resp, 500, "unknown method") + End If + Catch + ' Si ocurre cualquier error en el bloque Try, lo captura. + Log(LastException) + ' Envía un error 500 (Internal Server Error) al cliente con el mensaje de la excepción. + SendPlainTextError(resp, 500, LastException.Message) + End Try + ' Asegura que la conexión a la BD se cierre y se devuelva al pool. + If con <> Null And con.IsInitialized Then con.Close + ' Registra en el log el comando ejecutado, cuánto tiempo tardó y la IP del cliente. + Log($"Command: ${q}, took: ${DateTime.Now - start}ms, client=${req.RemoteAddress}"$) +End Sub + +' Ejecuta una consulta única usando el protocolo V2 (B4XSerializator). +Private Sub ExecuteQuery2 (DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String + ' Objeto para deserializar los datos enviados desde el cliente. + Dim ser As B4XSerializator + ' Convierte el stream de entrada a un array de bytes y luego a un objeto Mapa. + Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in)) + ' Extrae el objeto DBCommand del mapa. + Dim cmd As DBCommand = m.Get("command") + ' Extrae el límite de filas a devolver. + Dim limit As Int = m.Get("limit") + + ' Obtiene la sentencia SQL correspondiente al nombre del comando desde config.properties. + Dim sqlCommand As String = Connector.GetCommand(DB, cmd.Name) + + ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE >>> + ' Comprueba si el comando no fue encontrado en el archivo de configuración. + If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then + Dim errorMessage As String = $"El comando '${cmd.Name}' no fue encontrado en el config.properties de '${DB}'."$ + Log(errorMessage) + ' Envía un error 400 (Bad Request) al cliente informando del problema. + SendPlainTextError(resp, 400, errorMessage) + Return "error" ' Retorna un texto para el log. + End If + ' <<< FIN NUEVA VALIDACIÓN >>> + + ' --- INICIO VALIDACIÓN DE PARÁMETROS --- + ' Comprueba si el SQL espera parámetros o si se recibieron parámetros. + If sqlCommand.Contains("?") Or (cmd.Parameters <> Null And cmd.Parameters.Length > 0) Then + ' Cuenta cuántos '?' hay en la sentencia SQL para saber cuántos parámetros se esperan. + Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length + ' Cuenta cuántos parámetros se recibieron. + Dim receivedParams As Int + If cmd.Parameters = Null Then receivedParams = 0 Else receivedParams = cmd.Parameters.Length + + ' Compara el número de parámetros esperados con los recibidos. + If expectedParams <> receivedParams Then + Dim errorMessage As String = $"Número de parametros equivocado para "${cmd.Name}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$ + Log(errorMessage) + ' Si no coinciden, envía un error 400 al cliente. + SendPlainTextError(resp, 400, errorMessage) + Return "error" + End If + End If + ' --- FIN VALIDACIÓN --- + + ' Ejecuta la consulta SQL con los parámetros proporcionados. + Dim rs As ResultSet = con.ExecQuery2(sqlCommand, cmd.Parameters) + ' Si el límite es 0 o negativo, lo establece a un valor muy alto (máximo entero). + If limit <= 0 Then limit = 0x7fffffff 'max int + ' Obtiene el objeto Java subyacente del ResultSet para acceder a métodos adicionales. + Dim jrs As JavaObject = rs + ' Obtiene los metadatos del ResultSet (información sobre las columnas). + Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) + ' Obtiene el número de columnas del resultado. + Dim cols As Int = rs.ColumnCount + ' Crea un objeto DBResult para empaquetar la respuesta. + Dim res As DBResult + res.Initialize + res.columns.Initialize + res.Tag = Null + ' Llena el mapa de columnas con el nombre de cada columna y su índice. + For i = 0 To cols - 1 + res.columns.Put(rs.GetColumnName(i), i) + Next + ' Inicializa la lista de filas. + res.Rows.Initialize + ' Itera sobre cada fila del ResultSet, hasta llegar al límite. + Do While rs.NextRow And limit > 0 + Dim row(cols) As Object + ' Itera sobre cada columna de la fila actual. + For i = 0 To cols - 1 + ' Obtiene el tipo de dato de la columna según JDBC. + Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1)) + ' Maneja diferentes tipos de datos para leerlos de la forma correcta. + If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then ' Tipos BLOB/binarios + row(i) = rs.GetBlob2(i) + Else If ct = 2005 Then ' Tipo CLOB (texto largo) + row(i) = rs.GetString2(i) + Else if ct = 2 Or ct = 3 Then ' Tipos numéricos que pueden tener decimales + row(i) = rs.GetDouble2(i) + Else If DateTimeMethods.ContainsKey(ct) Then ' Tipos de Fecha/Hora + ' Obtiene el objeto de tiempo/fecha de Java. + Dim SQLTime As JavaObject = jrs.RunMethodJO(DateTimeMethods.Get(ct), Array(i + 1)) + If SQLTime.IsInitialized Then + ' Lo convierte a milisegundos (Long) para B4X. + row(i) = SQLTime.RunMethod("getTime", Null) + Else + row(i) = Null + End If + Else ' Para todos los demás tipos de datos + ' Usa getObject que funciona para la mayoría de los tipos estándar. + row(i) = jrs.RunMethod("getObject", Array(i + 1)) + End If + Next + ' Añade la fila completa a la lista de resultados. + res.Rows.Add(row) + limit = limit - 1 + Loop + ' Cierra el ResultSet para liberar recursos. + rs.Close + ' Serializa el objeto DBResult completo a un array de bytes. + Dim data() As Byte = ser.ConvertObjectToBytes(res) + ' Escribe los datos serializados en el stream de respuesta. + resp.OutputStream.WriteBytes(data, 0, data.Length) + ' Devuelve el nombre del comando para el log. + Return "query: " & cmd.Name +End Sub + +' Ejecuta un lote de comandos (INSERT, UPDATE, DELETE) usando el protocolo V2. +Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String + Dim ser As B4XSerializator + ' Deserializa el mapa que contiene la lista de comandos. + Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in)) + ' Obtiene la lista de objetos DBCommand. + Dim commands As List = m.Get("commands") + ' Prepara un objeto DBResult para la respuesta (aunque para batch no devuelve datos, solo confirmación). + Dim res As DBResult + res.Initialize + res.columns = CreateMap("AffectedRows (N/A)": 0) + res.Rows.Initialize + res.Tag = Null + Try + ' Inicia una transacción. Todos los comandos del lote se ejecutarán como una unidad. + con.BeginTransaction + ' Itera sobre cada comando en la lista. + For Each cmd As DBCommand In commands + ' Obtiene la sentencia SQL para el comando actual. + Dim sqlCommand As String = Connector.GetCommand(DB, cmd.Name) + + ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE DENTRO DEL BATCH >>> + If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then + con.Rollback ' Deshace la transacción si un comando es inválido. + Dim errorMessage As String = $"El comando '${cmd.Name}' no fue encontrado en el config.properties de '${DB}'."$ + Log(errorMessage) + SendPlainTextError(resp, 400, errorMessage) + Return "error" + End If + ' <<< FIN NUEVA VALIDACIÓN >>> + + ' --- INICIO VALIDACIÓN DE PARÁMETROS DENTRO DEL BATCH --- + If sqlCommand.Contains("?") Or (cmd.Parameters <> Null And cmd.Parameters.Length > 0) Then + Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length + Dim receivedParams As Int + If cmd.Parameters = Null Then receivedParams = 0 Else receivedParams = cmd.Parameters.Length + + ' Si el número de parámetros no coincide, deshace la transacción y envía error. + If expectedParams <> receivedParams Then + con.Rollback + Dim errorMessage As String = $"Número de parametros equivocado para "${cmd.Name}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$ + Log(errorMessage) + SendPlainTextError(resp, 400, errorMessage) + Return "error" + End If + End If + ' --- FIN VALIDACIÓN --- + + ' Ejecuta el comando (no es una consulta, no devuelve filas). + con.ExecNonQuery2(sqlCommand, cmd.Parameters) + Next + ' Añade una fila simbólica al resultado para indicar éxito. + res.Rows.Add(Array As Object(0)) + ' Si todos los comandos se ejecutaron sin error, confirma la transacción. + con.TransactionSuccessful + Catch + ' Si cualquier comando falla, se captura el error. + con.Rollback ' Se deshacen todos los cambios hechos en la transacción. + Log(LastException) + SendPlainTextError(resp, 500, LastException.Message) + End Try + ' Serializa y envía la respuesta al cliente. + Dim data() As Byte = ser.ConvertObjectToBytes(res) + resp.OutputStream.WriteBytes(data, 0, data.Length) + ' Devuelve un resumen para el log. + Return $"batch (size=${commands.Size})"$ +End Sub + +' Código compilado condicionalmente para el protocolo antiguo (V1). +'#if VERSION1 + +' Ejecuta un lote de comandos usando el protocolo V1. +Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String + ' Lee y descarta la versión del cliente. + Dim clientVersion As Float = ReadObject(in) 'ignore + ' Lee cuántos comandos vienen en el lote. + Dim numberOfStatements As Int = ReadInt(in) + Dim res(numberOfStatements) As Int ' Array para resultados (aunque no se usa). + Try + con.BeginTransaction + ' Itera para procesar cada comando del lote. + For i = 0 To numberOfStatements - 1 + ' Lee el nombre del comando y la lista de parámetros usando el deserializador V1. + Dim queryName As String = ReadObject(in) + Dim params As List = ReadList(in) + + Dim sqlCommand As String = Connector.GetCommand(DB, queryName) + + ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> + If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then + con.Rollback + Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ + Log(errorMessage) + SendPlainTextError(resp, 400, errorMessage) + Return "error" + End If + ' <<< FIN NUEVA VALIDACIÓN >>> + + ' --- INICIO VALIDACIÓN DE PARÁMETROS DENTRO DEL BATCH (V1) --- + If sqlCommand.Contains("?") Or (params <> Null And params.Size > 0) Then + Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length + Dim receivedParams As Int + If params = Null Then receivedParams = 0 Else receivedParams = params.Size + + If expectedParams <> receivedParams Then + con.Rollback + Dim errorMessage As String = $"Número de parametros equivocado para "${queryName}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$ + Log(errorMessage) + SendPlainTextError(resp, 400, errorMessage) + Return "error" + End If + End If + ' --- FIN VALIDACIÓN --- + + ' Ejecuta el comando. + con.ExecNonQuery2(sqlCommand, params) + Next + ' Confirma la transacción. + con.TransactionSuccessful + + ' Comprime la salida antes de enviarla. + Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") + ' Escribe la respuesta usando el serializador V1. + WriteObject(Main.VERSION, out) + WriteObject("batch", out) + WriteInt(res.Length, out) + For Each r As Int In res + WriteInt(r, out) + Next + out.Close + Catch + con.Rollback + Log(LastException) + SendPlainTextError(resp, 500, LastException.Message) + End Try + Return $"batch (size=${numberOfStatements})"$ +End Sub + +' Ejecuta una consulta única usando el protocolo V1. +Private Sub ExecuteQuery(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String + Log("====================== ExecuteQuery =====================") + ' Deserializa los datos de la petición usando el protocolo V1. + Dim clientVersion As Float = ReadObject(in) 'ignore + Dim queryName As String = ReadObject(in) + Dim limit As Int = ReadInt(in) + Dim params As List = ReadList(in) + ' Obtiene la sentencia SQL. + Dim theSql As String = Connector.GetCommand(DB, queryName) +' Log(444 & "|" & theSql) + + ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> + If theSql = Null Or theSql ="null" Or theSql.Trim = "" Then + Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ + Log(errorMessage) + SendPlainTextError(resp, 400, errorMessage) + Return "error" + End If + ' <<< FIN NUEVA VALIDACIÓN >>> + + ' --- INICIO VALIDACIÓN DE PARÁMETROS (V1) --- + If theSql.Contains("?") Or (params <> Null And params.Size > 0) Then + Dim expectedParams As Int = theSql.Length - theSql.Replace("?", "").Length + Dim receivedParams As Int + If params = Null Then receivedParams = 0 Else receivedParams = params.Size + + If expectedParams <> receivedParams Then + Dim errorMessage As String = $"Número de parametros equivocado para "${queryName}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$ + Log(errorMessage) + SendPlainTextError(resp, 400, errorMessage) + Return "error" + End If + End If + ' --- FIN VALIDACIÓN --- + + ' Ejecuta la consulta. + Dim rs As ResultSet = con.ExecQuery2(theSql, params) + If limit <= 0 Then limit = 0x7fffffff 'max int + Dim jrs As JavaObject = rs + Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) + Dim cols As Int = rs.ColumnCount + ' Comprime el stream de salida. + Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") + ' Escribe la cabecera de la respuesta V1. + WriteObject(Main.VERSION, out) + WriteObject("query", out) + WriteInt(rs.ColumnCount, out) + ' Escribe los nombres de las columnas. + For i = 0 To cols - 1 + WriteObject(rs.GetColumnName(i), out) + Next + + ' Itera sobre las filas del resultado. + Do While rs.NextRow And limit > 0 + ' Escribe un byte '1' para indicar que viene una fila. + WriteByte(1, out) + ' Itera sobre las columnas de la fila. + For i = 0 To cols - 1 + Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1)) + ' Maneja los tipos de datos binarios de forma especial. + If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then + WriteObject(rs.GetBlob2(i), out) + Else + ' Escribe el valor de la columna. + WriteObject(jrs.RunMethod("getObject", Array(i + 1)), out) + End If + Next + limit = limit - 1 + Loop + ' Escribe un byte '0' para indicar el fin de las filas. + WriteByte(0, out) + out.Close + rs.Close + + Return "query: " & queryName +End Sub + +' Escribe un único byte en el stream de salida. +Private Sub WriteByte(value As Byte, out As OutputStream) + out.WriteBytes(Array As Byte(value), 0, 1) +End Sub + +' Serializador principal para el protocolo V1. Escribe un objeto al stream. +Private Sub WriteObject(o As Object, out As OutputStream) + Dim data() As Byte + ' Escribe un byte de tipo seguido de los datos. + If o = Null Then + out.WriteBytes(Array As Byte(T_NULL), 0, 1) + Else If o Is Short Then + out.WriteBytes(Array As Byte(T_SHORT), 0, 1) + data = bc.ShortsToBytes(Array As Short(o)) + Else If o Is Int Then + out.WriteBytes(Array As Byte(T_INT), 0, 1) + data = bc.IntsToBytes(Array As Int(o)) + Else If o Is Float Then + out.WriteBytes(Array As Byte(T_FLOAT), 0, 1) + data = bc.FloatsToBytes(Array As Float(o)) + Else If o Is Double Then + out.WriteBytes(Array As Byte(T_DOUBLE), 0, 1) + data = bc.DoublesToBytes(Array As Double(o)) + Else If o Is Long Then + out.WriteBytes(Array As Byte(T_LONG), 0, 1) + data = bc.LongsToBytes(Array As Long(o)) + Else If o Is Boolean Then + out.WriteBytes(Array As Byte(T_BOOLEAN), 0, 1) + Dim b As Boolean = o + Dim data(1) As Byte + If b Then data(0) = 1 Else data(0) = 0 + Else If GetType(o) = "[B" Then ' Si el objeto es un array de bytes (BLOB) + data = o + out.WriteBytes(Array As Byte(T_BLOB), 0, 1) + ' Escribe la longitud de los datos antes de los datos mismos. + WriteInt(data.Length, out) + Else ' Trata todo lo demás como un String + out.WriteBytes(Array As Byte(T_STRING), 0, 1) + data = bc.StringToBytes(o, "UTF8") + ' Escribe la longitud del string antes del string. + WriteInt(data.Length, out) + End If + ' Escribe los bytes del dato. + If data.Length > 0 Then out.WriteBytes(data, 0, data.Length) +End Sub + +' Deserializador principal para el protocolo V1. Lee un objeto del stream. +Private Sub ReadObject(In As InputStream) As Object + ' Lee el primer byte para determinar el tipo de dato. + Dim data(1) As Byte + In.ReadBytes(data, 0, 1) + Select data(0) + Case T_NULL + Return Null + Case T_SHORT + Dim data(2) As Byte + Return bc.ShortsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_INT + Dim data(4) As Byte + Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_LONG + Dim data(8) As Byte + Return bc.LongsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_FLOAT + Dim data(4) As Byte + Return bc.FloatsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_DOUBLE + Dim data(8) As Byte + Return bc.DoublesFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_BOOLEAN + Dim b As Byte = ReadByte(In) + Return b = 1 + Case T_BLOB + ' Lee la longitud, luego lee esa cantidad de bytes. + Dim len As Int = ReadInt(In) + Dim data(len) As Byte + Return ReadBytesFully(In, data, data.Length) + Case Else ' T_STRING + ' Lee la longitud, luego lee esa cantidad de bytes y los convierte a string. + Dim len As Int = ReadInt(In) + Dim data(len) As Byte + ReadBytesFully(In, data, data.Length) + Return BytesToString(data, 0, data.Length, "UTF8") + End Select +End Sub + +' Se asegura de leer exactamente la cantidad de bytes solicitada del stream. +Private Sub ReadBytesFully(In As InputStream, Data() As Byte, Len As Int) As Byte() + Dim count = 0, Read As Int + ' Sigue leyendo en un bucle hasta llenar el buffer, por si los datos llegan en partes. + Do While count < Len And Read > -1 + Read = In.ReadBytes(Data, count, Len - count) + count = count + Read + Loop + Return Data +End Sub + +' Escribe un entero (4 bytes) en el stream. +Private Sub WriteInt(i As Int, out As OutputStream) + Dim data() As Byte + data = bc.IntsToBytes(Array As Int(i)) + out.WriteBytes(data, 0, data.Length) +End Sub + +' Lee un entero (4 bytes) del stream. +Private Sub ReadInt(In As InputStream) As Int + Dim data(4) As Byte + Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) +End Sub + +' Lee un solo byte del stream. +Private Sub ReadByte(In As InputStream) As Byte + Dim data(1) As Byte + In.ReadBytes(data, 0, 1) + Return data(0) +End Sub + +' Lee una lista de objetos del stream (protocolo V1). +Private Sub ReadList(in As InputStream) As List + ' Primero lee la cantidad de elementos en la lista. + Dim len As Int = ReadInt(in) + Dim l1 As List + l1.Initialize + ' Luego lee cada objeto uno por uno y lo añade a la lista. + For i = 0 To len - 1 + l1.Add(ReadObject(in)) + Next + Return l1 +End Sub +'#end If + +' Envía una respuesta de error en formato de texto plano. +' Esto evita la página de error HTML por defecto que genera resp.SendError. +' resp: El objeto ServletResponse para enviar la respuesta. +' statusCode: El código de estado HTTP (ej. 400 para Bad Request, 500 para Internal Server Error). +' errorMessage: El mensaje de error que se enviará al cliente. +Private Sub SendPlainTextError(resp As ServletResponse, statusCode As Int, errorMessage As String) + Try + ' Establece el código de estado HTTP (ej. 400, 500). + resp.Status = statusCode + + ' Define el tipo de contenido como texto plano, con codificación UTF-8 para soportar acentos. + resp.ContentType = "text/plain; charset=utf-8" + + ' Obtiene el OutputStream de la respuesta para escribir los datos directamente. + Dim out As OutputStream = resp.OutputStream + + ' Convierte el mensaje de error a un array de bytes usando UTF-8. + Dim data() As Byte = errorMessage.GetBytes("UTF8") + + ' Escribe los bytes en el stream de salida. + out.WriteBytes(data, 0, data.Length) + + ' Cierra el stream para asegurar que todos los datos se envíen correctamente. + out.Close + Catch + ' Si algo falla al intentar enviar la respuesta de error, lo registra en el log + ' para que no se pierda la causa original del problema. + Log("Error sending plain text error response: " & LastException) + End Try +End Sub diff --git a/LoginHandler.bas b/LoginHandler.bas new file mode 100644 index 0000000..f523e88 --- /dev/null +++ b/LoginHandler.bas @@ -0,0 +1,23 @@ +B4J=true +Group=Default Group +ModulesStructureVersion=1 +Type=Class +Version=10.3 +@EndOfDesignText@ +'Class module: LoginHandler +Sub Class_Globals +End Sub + +Public Sub Initialize +End Sub + +Public Sub Handle(req As ServletRequest, resp As ServletResponse) + resp.ContentType = "text/html" + ' Suponiendo que el archivo login.html está en la carpeta www de tu proyecto + Try + resp.Write(File.ReadString(File.DirApp, "www/login.html")) + Catch + Log("Error: No se encontró el archivo www/login.html") + resp.Write("Error: Archivo de login no encontrado.") + End Try +End Sub \ No newline at end of file diff --git a/LogoutHandler.bas b/LogoutHandler.bas new file mode 100644 index 0000000..ef5b024 --- /dev/null +++ b/LogoutHandler.bas @@ -0,0 +1,17 @@ +B4J=true +Group=Default Group +ModulesStructureVersion=1 +Type=Class +Version=10.3 +@EndOfDesignText@ +'Class module: LogoutHandler +Sub Class_Globals +End Sub + +Public Sub Initialize +End Sub + +Public Sub Handle(req As ServletRequest, resp As ServletResponse) + req.GetSession.Invalidate ' Cierra la sesión + resp.SendRedirect("/login") ' Manda al usuario a la página de login +End Sub \ No newline at end of file diff --git a/Manager.bas b/Manager.bas index a634fa0..f644ed2 100644 --- a/Manager.bas +++ b/Manager.bas @@ -15,133 +15,283 @@ Public Sub Initialize End Sub Sub Handle(req As ServletRequest, resp As ServletResponse) + ' 1. --- Bloque de Seguridad --- + If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then + resp.SendRedirect("/login") + Return + End If + Dim Command As String = req.GetParameter("command") If Command = "" Then Command = "ping" Log($"Command: ${Command}"$) + + ' --- MANEJO ESPECIAL PARA SNAPSHOT --- + ' El comando "snapshot" no devuelve HTML, sino una imagen. Lo manejamos por separado al principio. + If Command = "snapshot" Then + Try + resp.ContentType = "image/png" + Dim robot, toolkit As JavaObject + robot.InitializeNewInstance("java.awt.Robot", Null) + toolkit.InitializeStatic("java.awt.Toolkit") + Dim screenRect As JavaObject + screenRect.InitializeNewInstance("java.awt.Rectangle", Array As Object( _ + toolkit.RunMethodJO("getDefaultToolkit", Null).RunMethod("getScreenSize", Null))) + Dim image As JavaObject = robot.RunMethod("createScreenCapture", Array As Object(screenRect)) + Dim ImageIO As JavaObject + ImageIO.InitializeStatic("javax.imageio.ImageIO").RunMethod("write", Array As Object(image, "png", resp.OutputStream)) + Catch + resp.SendError(500, LastException.Message) + End Try + Return ' Detenemos la ejecución aquí para no enviar más HTML. + End If + ' --- FIN DE MANEJO ESPECIAL --- + + ' Para todos los demás comandos, construimos la página HTML resp.ContentType = "text/html" - If Command = "reload" Then 'Reload config.properties -' rdcc.Initialize + Dim sb As StringBuilder + sb.Initialize + + ' --- Estilos y JavaScript (igual que antes) --- + sb.Append("") + sb.Append("") + sb.Append("") + + ' --- Cabecera, Botón y Formulario Oculto (igual que antes) --- + sb.Append("

Panel de Administración jRDC

") + sb.Append($"Bienvenido, ${req.GetSession.GetAttribute("username")}
"$) + sb.Append("
") +' sb.Append("") + sb.Append("") + + ' --- Resultado del Comando --- + sb.Append("

Resultado del Comando: '" & Command & "'

") + sb.Append("
") + + ' ========================================================================= + ' ### INICIO DE TU LÓGICA DE COMANDOS INTEGRADA ### + ' ========================================================================= + If Command = "reload" Then Private estaDB As String = "" -' Log(Main.listaDeCP) - resp.Write($"Test | Reload | Reiniciar | Revive Bow |

"$) For i = 0 To Main.listaDeCP.Size - 1 Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).Initialize(Main.listaDeCP.get(i)) If Main.listaDeCP.get(i) <> "DB1" Then estaDB = "." & Main.listaDeCP.get(i) Else estaDB = "" - resp.Write($"Recargando config${estaDB}.properties ($DateTime{DateTime.Now})
"$) - resp.Write($"Queries en config.properties: ${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).commands.Size}
"$) - resp.Write($"JdbcUrl: ${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).config.Get("JdbcUrl")}
"$) - resp.Write($"User: ${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).config.Get("User")}
"$) - resp.Write($"ServerPort: ${Main.srvr.Port}

"$) + sb.Append($"Recargando config${estaDB}.properties ($DateTime{DateTime.Now})
"$) + sb.Append($"Queries en config.properties: ${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).commands.Size}
"$) + sb.Append($"JdbcUrl: ${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).config.Get("JdbcUrl")}
"$) + sb.Append($"User: ${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).config.Get("User")}
"$) + sb.Append($"ServerPort: ${Main.srvr.Port}

"$) Next -' Public shl As Shell -' shl.Initialize("shl11","cmd",Array("/c","restart.bat")) -' shl.WorkingDirectory = GlobalParameters.WorkingDirectory -' shl.Run(-1) - else If Command = "stop" Then -' Public shl As Shell -' shl.Initialize("shl","cmd",Array("/c","stop.bat")) -' shl.WorkingDirectory = GlobalParameters.WorkingDirectory -' shl.Run(-1) - else If Command = "rsx" Then 'Reiniciamos el servidor DBReq - resp.Write($"Test | Reload | Reiniciar | Revive Bow |

"$) + Else If Command = "test" Then + Try + Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("") + sb.Append("Connection successful.

") + Private estaDB As String = "" + Log(Main.listaDeCP) + For i = 0 To Main.listaDeCP.Size - 1 + If Main.listaDeCP.get(i) <> "" Then estaDB = "." & Main.listaDeCP.get(i) + sb.Append($"Using config${estaDB}.properties
"$) + Next + con.Close + Catch + resp.Write("Error fetching connection.") + End Try + Else If Command = "stop" Then + ' Public shl As Shell... + Else If Command = "rsx" Then Log($"Ejecutamos ${File.DirApp}\start.bat"$) - resp.Write($"Ejecutamos ${File.DirApp}\start.bat"$) - Public shl As Shell - shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\start.bat " & Main.srvr.Port)) - shl.WorkingDirectory = File.DirApp - shl.Run(-1) - else If Command = "rpm2" Then 'Reiniciamos el proceso DBReq en PM2 - resp.Write($"Test | Reload | Reiniciar | Revive Bow |

"$) + sb.Append($"Ejecutamos ${File.DirApp}\start.bat"$) + ' Public shl As Shell... + Else If Command = "rpm2" Then Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$) - resp.Write($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$) - Public shl As Shell - shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\reiniciaProcesoPM2.bat " & Main.srvr.Port)) - shl.WorkingDirectory = File.DirApp - shl.Run(-1) - else If Command = "reviveBow" Then 'Reiniciamos el proceso DBReq en PM2 - resp.Write($"Test | Reload | Reiniciar | Revive Bow |

"$) + sb.Append($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$) + ' Public shl As Shell... + Else If Command = "reviveBow" Then Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoBow.bat"$) - resp.Write($"Ejecutamos ${File.DirApp}\reiniciaProcesoBow.bat

"$) - resp.Write($"!!!BOW REINICIANDO!!!"$) + sb.Append($"Ejecutamos ${File.DirApp}\reiniciaProcesoBow.bat

"$) + sb.Append($"!!!BOW REINICIANDO!!!"$) Public shl As Shell shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\reiniciaProcesoBow.bat " & Main.srvr.Port)) shl.WorkingDirectory = File.DirApp shl.Run(-1) - else If Command = "paused" Then + Else If Command = "paused" Then GlobalParameters.IsPaused = 1 - else If Command = "continue" Then + sb.Append("Servidor pausado.") + Else If Command = "continue" Then GlobalParameters.IsPaused = 0 - else If Command = "logs" Then + sb.Append("Servidor reanudado.") + Else If Command = "logs" Then If GlobalParameters.mpLogs.IsInitialized Then j.Initialize(GlobalParameters.mpLogs) - j.ToString - resp.Write(j.ToString) + sb.Append(j.ToString) End If - else If Command = "block" Then - Dim BlockedConIP As String = req.GetParameter("IP") + Else If Command = "block" Then + Dim BlockedConIP As String = req.GetParameter("IP") If GlobalParameters.mpBlockConnection.IsInitialized Then - GlobalParameters.mpBlockConnection.Put(BlockedConIP,BlockedConIP) + GlobalParameters.mpBlockConnection.Put(BlockedConIP, BlockedConIP) + sb.Append("IP bloqueada: " & BlockedConIP) End If - else If Command = "unblock" Then - Dim UnBlockedConIP As String = req.GetParameter("IP") + Else If Command = "unblock" Then + Dim UnBlockedConIP As String = req.GetParameter("IP") If GlobalParameters.mpBlockConnection.IsInitialized Then GlobalParameters.mpBlockConnection.Remove(UnBlockedConIP) + sb.Append("IP desbloqueada: " & UnBlockedConIP) End If - else if Command = "restartserver" Then + Else If Command = "restartserver" Then Log($"Ejecutamos ${File.DirApp}/restarServer.bat"$) -' Public shl As Shell -' shl.Initialize("shl","cmd",Array("/c","retartserver.bat")) -' shl.WorkingDirectory = GlobalParameters.WorkingDirectory -' shl.Run(-1) - else if Command = "snapshot" Then - Try - resp.ContentType = "image/png" - Dim robot, toolkit, rectangle, ImageIO As JavaObject - robot.InitializeNewInstance("java.awt.Robot", Null) - toolkit.InitializeStatic("java.awt.Toolkit") - Dim rectangle As JavaObject - rectangle.InitializeNewInstance("java.awt.Rectangle", Array As Object( _ - toolkit.RunMethodJO("getDefaultToolkit", Null).RunMethod("getScreenSize", Null))) - Dim image As JavaObject = robot.RunMethod("createScreenCapture", Array As Object(rectangle)) - ImageIO.InitializeStatic("javax.imageio.ImageIO").RunMethod("write", Array As Object( _ - image, "png", resp.OutputStream)) 'the image is written to the response - Catch - resp.SendError(500, LastException.Message) - End Try - else if Command = "runatstartup" Then - '----- You Need to go to the folder on the server : C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp -' ------ then right click - Properties - Security - Edit - Add --> "Everyone" then OK -- then check Full Control (Allow) -- OK - File.Copy("C:\jrdcinterface","startup.bat","C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp","startup.bat") - else if Command = "stoprunatstartup" Then - '----- You Need to go to the folder on the server : C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp -' ------ then right click - Properties - Security - Edit - Add --> "Everyone" then OK -- then check Full Control (Allow) -- OK - File.Delete("C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp","startup.bat") + sb.Append("Reiniciando servidor...") + Else If Command = "runatstartup" Then + File.Copy("C:\jrdcinterface", "startup.bat", "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp", "startup.bat") + sb.Append("Script de inicio añadido.") + Else If Command = "stoprunatstartup" Then + File.Delete("C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp", "startup.bat") + sb.Append("Script de inicio eliminado.") + Else If Command = "totalrequests" Then + If GlobalParameters.mpTotalRequests.IsInitialized Then + j.Initialize(GlobalParameters.mpTotalRequests) + sb.Append(j.ToString) + End If + Else If Command = "totalblocked" Then + If GlobalParameters.mpBlockConnection.IsInitialized Then +' j.Initialize(Global.mpBlockConnection) + sb.Append(j.ToString) + End If + Else If Command = "totalcon" Then + If GlobalParameters.mpTotalConnections.IsInitialized Then + j.Initialize(GlobalParameters.mpTotalConnections) + sb.Append(j.ToString) + End If + Else If Command = "ping" Then + sb.Append($"Pong ($DateTime{DateTime.Now})"$) + End If + ' ========================================================================= + ' ### FIN DE TU LÓGICA DE COMANDOS ### + ' ========================================================================= + + ' --- Cerramos la página y la enviamos --- + sb.Append("

Cerrar Sesión | Cambiar Contraseña

") + resp.Write(sb.ToString) + + If GlobalParameters.mpLogs.IsInitialized Then GlobalParameters.mpLogs.Put(Command, "Manager : " & Command & " - Time : " & DateTime.Time(DateTime.Now)) +End Sub + +Sub Handle0(req As ServletRequest, resp As ServletResponse) + ' 1. --- Bloque de Seguridad (se mantiene igual) --- + If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then + resp.SendRedirect("/login") + Return + End If + + Dim Command As String = req.GetParameter("command") + If Command = "" Then Command = "ping" + Log($"Command: ${Command}"$) + resp.ContentType = "text/html" + + ' 2. --- Construimos la ESTRUCTURA de la página --- + Dim sb As StringBuilder + sb.Initialize + + ' Estilos para la página + sb.Append("") + + ' Cabecera y bienvenida + sb.Append("

Panel de Administración jRDC

") + sb.Append($"Bienvenido, ${req.GetSession.GetAttribute("username")}
"$) + + ' Menú de navegación (se define una sola vez) + sb.Append("") + + ' Formulario para cambiar contraseña + sb.Append("
") + sb.Append("

Cambiar Contraseña

") + sb.Append("
") + sb.Append("Contraseña Actual:
") + sb.Append("Nueva Contraseña:
") + sb.Append("Confirmar Nueva Contraseña:
") + sb.Append("") + sb.Append("
") + + ' Sección para el resultado del comando + sb.Append("

Resultado del Comando: '" & Command & "'

") + sb.Append("
") + + ' 3. --- Lógica de TUS COMANDOS (modificada para usar sb.Append) --- + If Command = "reload" Then + Private estaDB As String = "" + For i = 0 To Main.listaDeCP.Size - 1 + Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).Initialize(Main.listaDeCP.get(i)) + If Main.listaDeCP.get(i) <> "DB1" Then estaDB = "." & Main.listaDeCP.get(i) Else estaDB = "" + sb.Append($"Recargando config${estaDB}.properties ($DateTime{DateTime.Now})
"$) + sb.Append($"Queries en config.properties: ${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).commands.Size}
"$) + sb.Append($"JdbcUrl: ${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).config.Get("JdbcUrl")}
"$) + sb.Append($"User: ${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).config.Get("User")}
"$) + sb.Append($"ServerPort: ${Main.srvr.Port}

"$) + Next + else If Command = "stop" Then + ' Tu código para "stop" + else If Command = "rsx" Then + Log($"Ejecutamos ${File.DirApp}\start.bat"$) + sb.Append($"Ejecutamos ${File.DirApp}\start.bat"$) + Public shl As Shell + shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\start.bat " & Main.srvr.Port)) + shl.WorkingDirectory = File.DirApp + shl.Run(-1) + else If Command = "rpm2" Then + Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$) + sb.Append($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$) + Public shl As Shell + shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\reiniciaProcesoPM2.bat " & Main.srvr.Port)) + shl.WorkingDirectory = File.DirApp + shl.Run(-1) + else If Command = "reviveBow" Then + Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoBow.bat"$) + sb.Append($"Ejecutamos ${File.DirApp}\reiniciaProcesoBow.bat

"$) + sb.Append($"!!!BOW REINICIANDO!!!"$) + Public shl As Shell + shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\reiniciaProcesoBow.bat " & Main.srvr.Port)) + shl.WorkingDirectory = File.DirApp + shl.Run(-1) else if Command = "totalrequests" Then If GlobalParameters.mpTotalRequests.IsInitialized Then j.Initialize(GlobalParameters.mpTotalRequests) - j.ToString - resp.Write(j.ToString) - End If - else if Command = "totalblocked" Then - If GlobalParameters.mpBlockConnection.IsInitialized Then - j.Initialize(GlobalParameters.mpBlockConnection) - j.ToString - resp.Write(j.ToString) - End If - else if Command = "totalcon" Then - If GlobalParameters.mpTotalConnections.IsInitialized Then - j.Initialize(GlobalParameters.mpTotalConnections) - j.ToString - resp.Write(j.ToString) + sb.Append(j.ToString) End If else if Command = "ping" Then - resp.Write($"Test | Reload | Reiniciar | Revive Bow |

"$) - resp.Write($"Pong"$) + sb.Append($"Pong ($DateTime{DateTime.Now})"$) End If + '...(aquí continuaría el resto de tus Else If)... + + + ' 4. --- Cerramos la página y la enviamos TODA JUNTA --- + sb.Append("
") ' Cierre de div.output + sb.Append("

Cerrar Sesión

") + sb.Append("") + + resp.Write(sb.ToString) ' Se envía toda la página de una vez + + ' Lógica final de logs (se mantiene igual) If GlobalParameters.mpLogs.IsInitialized Then GlobalParameters.mpLogs.Put(Command, "Manager : " & Command & " - Time : " & DateTime.Time(DateTime.Now)) End Sub -'Sub shl11_ProcessCompleted (Success As Boolean, ExitCode As Int, StdOut As String, StdErr As String) -' If Success Then -' Log(Success) -' End If -'End Sub + diff --git a/README.md b/README.md index 6c7140c..db67d98 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,105 @@ -# Servidor jRDC2-Multi Mod (B4J) +# **Servidor jRDC2-Multi Mod (B4J)** -## 1. Introducción +## **1\. Introducción** Este proyecto es una versión modificada del servidor [jRDC2 original](https://www.b4x.com/android/forum/threads/b4x-jrdc2-b4j-implementation-of-rdc-remote-database-connector.61801/#content), diseñada para actuar como un backend robusto y flexible. Su función principal es recibir peticiones HTTP, ejecutar comandos SQL predefinidos contra una base de datos y devolver los resultados en un formato estructurado. -Ha sido adaptado para servir tanto a clientes nativos (B4A/B4i) como a clientes web modernos (JavaScript, a través de frameworks como NodeJS, React, Vue, Angular, etc.). +Ha sido adaptado para servir tanto a clientes nativos (`B4A/B4i`) como a clientes web modernos (`JavaScript`, a través de frameworks como `NodeJS, React, Vue, Angular, etc`.). -## 2. Características Principales +## **2\. Características Principales** -* **Soporte para Múltiples Bases de Datos**: Puede cargar y gestionar hasta 4 archivos de configuración (`config.properties`) simultáneamente. -* **Comandos SQL Externalizados**: Las sentencias SQL se definen en los archivos de configuración, permitiendo modificarlas sin recompilar el servidor. -* **Doble Handler de Peticiones**: Incluye un handler clásico para clientes B4X y un handler JSON para clientes web. -* **Validaciones de Seguridad**: Verifica la existencia de comandos y la correspondencia en el número de parámetros. -* **Administración Remota**: Permite verificar el estado, recargar la configuración y reiniciar el servidor a través de URLs específicas. +- **Soporte para Múltiples Bases de Datos**: Puede cargar y gestionar hasta 4 archivos de configuración (`config.properties`) simultáneamente. +- **Comandos SQL Externalizados**: Las sentencias SQL se definen en los archivos de configuración, permitiendo modificarlas sin recompilar el servidor. +- **Doble Handler de Peticiones**: Incluye un handler clásico para clientes B4X y un handler JSON para clientes web. +- **Validaciones de Seguridad**: Verifica la existencia de comandos y la correspondencia en el número de parámetros. +- **Administración Remota**: Permite verificar el estado, recargar la configuración y reiniciar el servidor a través de URLs específicas. -## 3. Configuración +## **3\. Configuración** -### 3.1. Archivos de Configuración +### **3.1. Archivos de Configuración** El sistema está preparado para manejar hasta **cuatro configuraciones de bases de datos** (de `DB1` a `DB4`). No es necesario tener los cuatro archivos; el servidor cargará únicamente los que encuentre. La nomenclatura de los archivos es fundamental: -* `config.properties` (para `DB1`) -* `config.DB2.properties` -* `config.DB3.properties` -* `config.DB4.properties` +- `config.properties` (para `DB1`) +- `config.DB2.properties` +- `config.DB3.properties` +- `config.DB4.properties` **Notas importantes:** -* El **puerto** del servidor se toma **únicamente** del archivo principal `config.properties`, sin importar lo que digan los demás. -* Los datos de conexión (`JdbcUrl`, usuario, contraseña) sí se toman del archivo correspondiente a cada base de datos. +- El **puerto** del servidor se toma **únicamente** del archivo principal `config.properties`, sin importar lo que digan los demás. +- Los datos de conexión (`JdbcUrl`, `usuario`, `contraseña`) sí se toman del archivo correspondiente a cada base de datos. -### 3.2. Añadir Drivers de Bases de Datos Adicionales +### **3.2. Añadir Drivers de Bases de Datos Adicionales** -Si necesitas conectarte a otros tipos de bases de datos (ej. Oracle), debes agregar el archivo del controlador `.jar` al proyecto antes de compilar. En el módulo `Main`, añade una línea como la siguiente: +Si necesitas conectarte a otros tipos de bases de datos (ej. Oracle), debes agregar el archivo del controlador .jar al proyecto antes de compilar. En el módulo `Main`, añade una línea como la siguiente: ```b4x -' Este es el nombre del archivo .jar, en este caso "C:\Ruta\LibsAdicionales\ojdbc11.jar" +' Este es el nombre del archivo .jar, en este caso "C:\\Ruta\\LibsAdicionales\\ojdbc11.jar" #AdditionalJar: ojdbc11 -```` +``` Al compilar, el driver se incluirá en el `.jar` final del servidor, por lo que no será necesario copiarlo por separado al directorio de producción. -## 4\. Uso del Handler Clásico (Para Clientes B4X) +## **4\. Uso del Handler Clásico (Para Clientes B4X)** Este handler mantiene la compatibilidad con `DBRequestManager`. La selección de la base de datos se realiza dinámicamente a través de la URL. - * Para `config.properties` =\> `http://tu-dominio.com:8090` - * Para `config.DB2.properties` =\> `http://tu-dominio.com:8090/DB2` - * Para `config.DB3.properties` =\> `http://tu-dominio.com:8090/DB3` - * Para `config.DB4.properties` =\> `http://tu-dominio.com:8090/DB4` +- Para `config.properties` \=\> `http://tu-dominio.com:8090` +- Para `config.DB2.properties` \=\> `http://tu-dominio.com:8090/DB2` +- Para `config.DB3.properties` \=\> `http://tu-dominio.com:8090/DB3` +- Para `config.DB4.properties` \=\> `http://tu-dominio.com:8090/DB4` -## 5\. Uso del `DB1JsonHandler` (Para Clientes Web) +## **5\. Uso del DBHandlerJSON (Para Clientes Web)** -Este handler es para clientes que se comunican vía JSON. +Este handler está diseñado para clientes que se comunican vía `JSON`, como aplicaciones web JavaScript. -### 5.1. Endpoint y Métodos +### **5.1. Endpoint y Métodos de Envío** -Las peticiones van al endpoint `/DBJ` y deben incluir un parámetro `j` con el JSON. Soporta `GET` y `POST`. +Las peticiones van dirigidas al endpoint `/DBJ`. El handler es flexible y acepta datos de dos maneras: -**Ejemplo con `GET`:** -`http://tu-dominio.com:8090/db1json?j={"dbx":"DB2","query":"get_user","exec":"executeQuery","params":{"par1":"CDAZA"}}` +**Método Recomendado: POST con Body JSON** -### 5.2. Formato del Parámetro `j` +Esta es la forma más limpia y estándar para las APIs modernas. -```json +- **Método HTTP**: POST +- **URL**: http://tu-dominio.com:8090/DBJ +- **Header Requerido**: Content-Type: application/json +- **Body (Payload)**: El objeto JSON se envía directamente en el cuerpo de la petición. + +**Ejemplo de Body:** + +``` +{ + "dbx": "DB2", + "query": "get\_user", + "exec": "executeQuery", + "params": { + "par1": "CDAZA" + } +} +``` + +**Método Legacy: GET con Parámetro `j`** + +Este método se mantiene por retrocompatibilidad. + +- **Método HTTP**: GET (o POST con Content-Type: application/x-www-form-urlencoded) +- **URL**: El JSON completo se envía como el valor del parámetro `j` en la URL. + +Ejemplo con GET: +http://tu-dominio.com:8090/DBJ?j={"dbx":"DB2","query":"get\_user","exec":"executeQuery","params":{"par1":"CDAZA"}} + +### **5.2. Formato del Payload JSON** + +La estructura del objeto JSON es la misma para ambos métodos: + +``` { "exec": "executeQuery", - "query": "nombre_del_comando_sql", + "query": "nombre\_del\_comando\_sql", "dbx": "DB1", "params": { "par1": "valor1", @@ -77,29 +108,29 @@ Las peticiones van al endpoint `/DBJ` y deben incluir un parámetro `j` con el J } ``` - * `exec`: `"executeQuery"` (para `SELECT`) o `"executeCommand"` (para `INSERT`, `UPDATE`, etc.). - * `query`: Nombre del comando SQL en el archivo de configuración (ej. `select_user`). - * `dbx` (opcional): La llave de la BD (`DB1`, `DB2`, etc.). Si se omite, el default es `DB1`. - * `params` (opcional): Objeto con los parámetros. +- `exec`: `"executeQuery"` (para SELECT) o `"executeCommand"` (para INSERT, UPDATE, DELETE). +- `query`: Nombre del comando SQL tal como está definido en el archivo de configuración (ej. `select\_user`). +- `dbx` (opcional): La llave de la base de datos (`DB1`, `DB2`, etc.). Si se omite, se usará **DB1** por defecto. +- `params` (opcional): Un objeto que contiene los parámetros para la consulta SQL. -### 5.3. ¡Importante\! Envío de Parámetros +### **5.3. ¡Importante\! Envío de Parámetros** -El servidor ordena las claves de los parámetros alfabéticamente. Para asegurar el orden correcto, **nombra las claves secuencialmente**: `"par1"`, `"par2"`, etc. +El servidor ordena las claves de los parámetros alfabéticamente antes de pasarlos a la consulta SQL. Para asegurar que los valores se asignen al `?` correcto, **debes nombrar las claves de los parámetros de forma secuencial**: `"par1"`, `"par2"`, `"par3"`, etc. -> **Nota para más de 9 parámetros**: Usa un cero inicial para mantener el orden (`"par01"`, `"par02"`, ..., `"par10"`). +**Nota para más de 9 parámetros**: Si tienes 10 o más parámetros, usa un cero inicial para mantener el orden alfabético correcto (ej. `"par01"`, `"par02"`, ..., `"par10"`). -### 5.4. Respuestas JSON +### **5.4. Respuestas JSON** -Las respuestas siempre incluyen `"success": true` o `"success": false`, con los datos en `"result"` o el error en `"error"`. +Las respuestas del servidor siempre son en formato JSON e incluyen un campo booleano `success`. -## 6\. Administración del Servidor +- **Si success es true**, los datos se encontrarán en la llave `result`. +- **Si success es false**, el mensaje de error se encontrará en la llave `error`. -Se pueden ejecutar comandos de gestión directamente desde un navegador. +## **6\. Administración del Servidor** - * **Verificar Estado**: `http://tu-dominio.com:8090/test` - * **Recargar Configuración**: `http://tu-dominio.com:8090/manager?command=reload` - (Vuelve a leer todos los archivos `config.*.properties` sin reiniciar el servidor). - * **Reiniciar Servidor (Estándar)**: `http://tu-dominio.com:8090/manager?command=rsx` - (Ejecuta los scripts `start.bat`, `start2.bat` y `stop.bat`). - * **Reiniciar Servidor (con PM2)**: `http://tu-dominio.com:8090/manager?command=rpm2` - (Ejecuta `reiniciaProcesoPM2.bat` y asume que el nombre del proceso es "RDC-Multi". Modificar el `.bat` si el nombre es diferente). +Se pueden ejecutar comandos de gestión directamente desde un navegador o una herramienta como cURL. + +- **Verificar Estado**: `http://tu-dominio.com:8090/test` +- **Recargar Configuración**: `http://tu-dominio.com:8090/manager?command=reload` (Vuelve a leer todos los archivos `config.\*.properties` sin reiniciar el servidor). +- **Reiniciar Servidor (Estándar)**: `http://tu-dominio.com:8090/manager?command=rsx` (Ejecuta los scripts `start.bat`, `start2.bat` y `stop.bat`). +- **Reiniciar Servidor (con PM2)**: `http://tu-dominio.com:8090/manager?command=rpm2` (Ejecuta `reiniciaProcesoPM2.bat` y asume que el nombre del proceso es "RDC-Multi". Modificar el `.bat` si el nombre es diferente). diff --git a/Readme1.md b/Readme1.md new file mode 100644 index 0000000..85c3cdd --- /dev/null +++ b/Readme1.md @@ -0,0 +1,105 @@ +# Servidor jRDC2-Multi Modificado (B4J) + +## 1. Introducción + +Este proyecto es una versión modificada del servidor [jRDC2 original](https://www.b4x.com/android/forum/threads/b4x-jrdc2-b4j-implementation-of-rdc-remote-database-connector.61801/#content), diseñada para actuar como un backend robusto y flexible. Su función principal es recibir peticiones HTTP, ejecutar comandos SQL predefinidos contra una base de datos y devolver los resultados en un formato estructurado. + +Ha sido adaptado para servir tanto a clientes nativos (B4A/B4i) como a clientes web modernos (JavaScript, a través de frameworks como React, Vue, Angular, etc.). + +## 2. Características Principales + +* **Soporte para Múltiples Bases de Datos**: Puede cargar y gestionar hasta 4 archivos de configuración (`config.properties`) simultáneamente. +* **Comandos SQL Externalizados**: Las sentencias SQL se definen en los archivos de configuración, permitiendo modificarlas sin recompilar el servidor. +* **Doble Handler de Peticiones**: Incluye un handler clásico para clientes B4X y un handler JSON para clientes web. +* **Validaciones de Seguridad**: Verifica la existencia de comandos y la correspondencia en el número de parámetros. +* **Administración Remota**: Permite verificar el estado, recargar la configuración y reiniciar el servidor a través de URLs específicas. + +## 3. Configuración + +### 3.1. Archivos de Configuración + +El sistema está preparado para manejar hasta **cuatro configuraciones de bases de datos** (de `DB1` a `DB4`). No es necesario tener los cuatro archivos; el servidor cargará únicamente los que encuentre. + +La nomenclatura de los archivos es fundamental: + +* `config.properties` (para `DB1`) +* `config.DB2.properties` +* `config.DB3.properties` +* `config.DB4.properties` + +**Notas importantes:** + +* El **puerto** del servidor se toma **únicamente** del archivo principal `config.properties`, sin importar lo que digan los demás. +* Los datos de conexión (`JdbcUrl`, usuario, contraseña) sí se toman del archivo correspondiente a cada base de datos. + +### 3.2. Añadir Drivers de Bases de Datos Adicionales + +Si necesitas conectarte a otros tipos de bases de datos (ej. Oracle), debes agregar el archivo del controlador `.jar` al proyecto antes de compilar. En el módulo `Main`, añade una línea como la siguiente: + +```b4x +' Este es el nombre del archivo .jar, en este caso "C:\Ruta\Adicional\ojdbc11.jar" +#AdditionalJar: ojdbc11 +```` + +Al compilar, el driver se incluirá en el `.jar` final del servidor, por lo que no será necesario copiarlo por separado al directorio de producción. + +## 4\. Uso del Handler Clásico (Para Clientes B4X) + +Este handler mantiene la compatibilidad con `DBRequestManager`. La selección de la base de datos se realiza dinámicamente a través de la URL. + + * Para `config.properties` =\> `http://tu-dominio.com:8090` + * Para `config.DB2.properties` =\> `http://tu-dominio.com:8090/DB2` + * Para `config.DB3.properties` =\> `http://tu-dominio.com:8090/DB3` + * Para `config.DB4.properties` =\> `http://tu-dominio.com:8090/DB4` + +## 5\. Uso del `DB1JsonHandler` (Para Clientes Web) + +Este handler es para clientes que se comunican vía JSON. + +### 5.1. Endpoint y Métodos + +Las peticiones van al endpoint `/DBJ` y deben incluir un parámetro `j` con el JSON. Soporta `GET` y `POST`. + +**Ejemplo con `GET`:** +`http://tu-dominio.com:8090/db1json?j={"dbx":"DB2","query":"get_user","exec":"executeQuery","params":{"par1":"CDAZA"}}` + +### 5.2. Formato del Parámetro `j` + +```json +{ + "exec": "executeQuery", + "query": "nombre_del_comando_sql", + "dbx": "DB1", + "params": { + "par1": "valor1", + "par2": 123 + } +} +``` + + * `exec`: `"executeQuery"` (para `SELECT`) o `"executeCommand"` (para `INSERT`, `UPDATE`, etc.). + * `query`: Nombre del comando SQL en el archivo de configuración. + * `dbx` (opcional): La llave de la BD (`DB1`, `DB2`, etc.). Si se omite, usa `DB1`. + * `params` (opcional): Objeto con los parámetros. + +### 5.3. ¡Importante\! Envío de Parámetros + +El servidor ordena las claves de los parámetros alfabéticamente. Para asegurar el orden correcto, **nombra las claves secuencialmente**: `"par1"`, `"par2"`, etc. + +> **Nota para más de 9 parámetros**: Usa un cero inicial para mantener el orden (`"par01"`, `"par02"`, ..., `"par10"`). + +### 5.4. Respuestas JSON + +Las respuestas siempre incluyen `"success": true` o `"success": false`, con los datos en `"result"` o el error en `"error"`. + +## 6\. Administración del Servidor + +Se pueden ejecutar comandos de gestión directamente desde un navegador. + + * **Verificar Estado**: `http://tu-dominio.com:8090/test` + * **Recargar Configuración**: `http://tu-dominio.com:8090/manager?command=reload` + (Vuelve a leer todos los archivos `config.*.properties` sin reiniciar el servidor). + * **Reiniciar Servidor (Estándar)**: `http://tu-dominio.com:8090/manager?command=rsx` + (Ejecuta los scripts `start.bat`, `start2.bat` y `stop.bat`). + * **Reiniciar Servidor (con PM2)**: `http://tu-dominio.com:8090/manager?command=rpm2` + (Ejecuta `reiniciaProcesoPM2.bat` y asume que el nombre del proceso es "RDC-Multi". Modificar el `.bat` si el nombre es diferente). diff --git a/jRDC_Multi.b4j b/jRDC_Multi.b4j index 674d2a4..432e1b0 100644 --- a/jRDC_Multi.b4j +++ b/jRDC_Multi.b4j @@ -1,30 +1,53 @@ AppType=StandardJava Build1=Default,b4j.JRDCMulti -File1=config.properties +File1=config.DB2.properties +File10=stop.bat +File2=config.DB3.properties +File3=config.DB4.properties +File4=config.properties +File5=login.html +File6=reiniciaProcesoBow.bat +File7=reiniciaProcesoPM2.bat +File8=start.bat +File9=start2.bat FileGroup1=Default Group +FileGroup10=Default Group +FileGroup2=Default Group +FileGroup3=Default Group +FileGroup4=Default Group +FileGroup5=Default Group +FileGroup6=Default Group +FileGroup7=Default Group +FileGroup8=Default Group +FileGroup9=Default Group Group=Default Group -Library1=javaobject -Library2=jcore -Library3=jrandomaccessfile -Library4=jserver -Library5=jshell -Library6=json -Library7=jsql -Library8=byteconverter -Module1=DB1Handler -Module10=RDCConnector -Module11=TestHandler -Module2=DB1JsonHandler +Library1=byteconverter +Library2=javaobject +Library3=jcore +Library4=jrandomaccessfile +Library5=jserver +Library6=jshell +Library7=json +Library8=jsql +Library9=bcrypt +Module1=B4AHandler +Module10=LoginHandler +Module11=LogoutHandler +Module12=Manager +Module13=ping +Module14=RDCConnector +Module15=TestHandler +Module2=ChangePassHandler Module3=DB2Handler Module4=DB3Handler Module5=DB4Handler -Module6=DBHandlerGenerico -Module7=GlobalParameters -Module8=Manager -Module9=ping -NumberOfFiles=1 -NumberOfLibraries=8 -NumberOfModules=11 +Module6=DBHandlerJSON +Module7=DoLoginHandler +Module8=GlobalParameters +Module9=HandlerB4X +NumberOfFiles=10 +NumberOfLibraries=9 +NumberOfModules=15 Version=10.3 @EndOfDesignText@ 'Non-UI application (console / server application) @@ -48,6 +71,8 @@ Version=10.3 '#AdditionalJar: mysql-connector-java-5.1.27-bin '#AdditionalJar: postgresql-42.7.0 #AdditionalJar: ojdbc11 +' Librería para manejar la base de datos SQLite +#AdditionalJar: sqlite-jdbc-3.7.2 Sub Process_Globals Public srvr As Server @@ -57,9 +82,15 @@ Sub Process_Globals Dim listaDeCP As List Dim cpFiles As List Public Connectors, commandsMap As Map + Public SQL1 As SQL ' Objeto SQL para la base de datos de usuarios + Private bc As BCrypt End Sub Sub AppStart (Args() As String) + ' --- INICIO DE CAMBIOS --- + ' Inicializamos la base de datos. Se creará si no existe. + InitializeSQLiteDatabase + ' --- FIN DE CAMBIOS --- listaDeCP.Initialize srvr.Initialize("") Dim con As RDCConnector @@ -94,24 +125,53 @@ Sub AppStart (Args() As String) End If srvr.AddHandler("/ping", "ping", False) ' Agrega un manejador a la ruta "/test", asignando las solicitudes a la clase TestHandler, el último parámetro indica si el manejador debe ejecutar en un nuevo hilo (False en este caso) srvr.AddHandler("/test", "TestHandler", False) ' Agrega un manejador a la ruta "/test", asignando las solicitudes a la clase TestHandler, el último parámetro indica si el manejador debe ejecutar en un nuevo hilo (False en este caso) + + ' --- INICIO DE CAMBIOS --- + ' 1. Rutas para el sistema de Login + srvr.AddHandler("/login", "LoginHandler", False) ' Sirve la página de login + srvr.AddHandler("/dologin", "DoLoginHandler", False) ' Procesa el intento de login + srvr.AddHandler("/logout", "LogoutHandler", False) ' Cierra la sesión + srvr.AddHandler("/changepass", "ChangePassHandler", False) + ' 2. El handler del manager se queda igual, pero ahora estará protegido srvr.AddHandler("/manager", "Manager", False) -' srvr.AddHandler("/db1", "DB1Handler", False) -' srvr.AddHandler("/DB1", "DB1Handler", False) -' srvr.AddHandler("/db2", "DB2Handler", False) -' srvr.AddHandler("/DB2", "DB2Handler", False) -' srvr.AddHandler("/db3", "DB3Handler", False) -' srvr.AddHandler("/DB3", "DB3Handler", False) -' srvr.AddHandler("/db4", "DB4Handler", False) -' srvr.AddHandler("/DB4", "DB4Handler", False) - srvr.AddHandler("/DBJ", "DB1JsonHandler", False) - srvr.AddHandler("/dbrquery", "DB1JsonHandler", False) + ' --- FIN DE CAMBIOS --- + + srvr.AddHandler("/DBJ", "DBHandlerJSON", False) + srvr.AddHandler("/dbrquery", "DBHandlerJSON", False) ' srvr.AddHandler("/*", "DB1Handler", False) ' Si no se especifica una base de datos, entonces asignamos la solicitud a la DB1. - srvr.AddHandler("/*", "DBHandlerGenerico", False) + srvr.AddHandler("/*", "HandlerB4X", False) srvr.Start Log("===========================================================") Log($"-=== jRDC is running on port: ${srvr.port} (version = $1.2{VERSION}) ===-"$) Log("===========================================================") StartMessageLoop -End Sub \ No newline at end of file +End Sub + +' Nueva subrutina para crear y configurar la base de datos de usuarios +Sub InitializeSQLiteDatabase + Dim dbFileName As String = "users.db" + ' Si la base de datos no existe en la carpeta del .jar, la creamos + If File.Exists(File.DirApp, dbFileName) = False Then + Log("Creando nueva base de datos de usuarios: " & dbFileName) + ' Inicializamos la conexión + SQL1.InitializeSQLite(File.DirApp, dbFileName, True) + ' Creamos la tabla de usuarios + Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)" + SQL1.ExecNonQuery(createUserTable) + + ' Creamos un usuario por defecto para el primer inicio + Dim defaultUser As String = "admin" + Dim defaultPass As String = "12345" + Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt) ' bc.HashPassword(defaultPass) + + SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass)) + Log($"Usuario por defecto creado -> user: ${defaultUser}, pass: ${defaultPass}"$) + Else + ' Si ya existe, solo la abrimos + SQL1.InitializeSQLite(File.DirApp, dbFileName, True) + Log("Base de datos de usuarios cargada.") + End If +End Sub +' --- FIN DE CAMBIOS --- \ No newline at end of file diff --git a/jRDC_Multi.b4j.meta b/jRDC_Multi.b4j.meta index 12c1fec..f59b034 100644 --- a/jRDC_Multi.b4j.meta +++ b/jRDC_Multi.b4j.meta @@ -2,6 +2,10 @@ ModuleBookmarks1= ModuleBookmarks10= ModuleBookmarks11= +ModuleBookmarks12= +ModuleBookmarks13= +ModuleBookmarks14= +ModuleBookmarks15= ModuleBookmarks2= ModuleBookmarks3= ModuleBookmarks4= @@ -14,6 +18,10 @@ ModuleBreakpoints0= ModuleBreakpoints1= ModuleBreakpoints10= ModuleBreakpoints11= +ModuleBreakpoints12= +ModuleBreakpoints13= +ModuleBreakpoints14= +ModuleBreakpoints15= ModuleBreakpoints2= ModuleBreakpoints3= ModuleBreakpoints4= @@ -26,6 +34,10 @@ ModuleClosedNodes0= ModuleClosedNodes1= ModuleClosedNodes10= ModuleClosedNodes11= +ModuleClosedNodes12= +ModuleClosedNodes13= +ModuleClosedNodes14= +ModuleClosedNodes15= ModuleClosedNodes2= ModuleClosedNodes3=4,5,6 ModuleClosedNodes4= @@ -34,6 +46,6 @@ ModuleClosedNodes6= ModuleClosedNodes7= ModuleClosedNodes8= ModuleClosedNodes9= -NavigationStack=DBHandlerGenerico,ExecuteQuery,374,0,DB1JsonHandler,SendSuccessResponse,238,0,DB1JsonHandler,SendErrorResponse,255,0,Main,AppStart,76,0,RDCConnector,GetConnection,105,0,RDCConnector,LoadConfigMap,81,0,RDCConnector,GetCommand,98,0,RDCConnector,LoadSQLCommands,110,0,Manager,Initialize,9,0,Manager,Handle,124,6,DB1JsonHandler,Handle,121,6,DBHandlerGenerico,Handle,53,0 +NavigationStack=DoLoginHandler,Class_Globals,2,0,DoLoginHandler,Initialize,7,0,Manager,Initialize,98,0,ChangePassHandler,Handle,27,0,DoLoginHandler,Handle,9,0,Manager,Handle0,202,6,Main,Process_Globals,24,0,Main,AppStart,97,0,TestHandler,Handle,11,0,Manager,Handle,46,6,LoginHandler,Handle,16,1 SelectedBuild=0 -VisibleModules=6,2,10,8,9,11 +VisibleModules=9,6,14,12,15 diff --git a/reiniciaProcesoBow.bat b/reiniciaProcesoBow.bat new file mode 100644 index 0000000..439bb91 --- /dev/null +++ b/reiniciaProcesoBow.bat @@ -0,0 +1,9 @@ +@rem Este script reinicia el proceso en PM2 del servidor de jRDC2 + +@rem estas lineas sirven para que el archivo bat corra en modo administrador. +set "params=%*" +cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && ""%~s0"" %params%", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && exit /B ) + +pm2 restart BotSoporte_4.0 + +exit \ No newline at end of file