Foros Club Delphi

Foros Club Delphi (https://www.clubdelphi.com/foros/index.php)
-   Envío de registros y sus respuestas (https://www.clubdelphi.com/foros/forumdisplay.php?f=66)
-   -   Problemas al integrar verifactu en modo servicio (https://www.clubdelphi.com/foros/showthread.php?t=97559)

Galahad 30-06-2025 11:47:02

Problemas al integrar verifactu en modo servicio
 
Buenos días.
Hemos desarrollado una función de envio de registros del verifactu que funciona perfectamente cuando se utiliza desde una aplicación de escritorio.
La hemos trasladado a un servicio con el fin de que al final sea dicho servicio el que se encargue del envio de los xml de las declaraciones.
Si ejecuto dicho servicio con credenciales de un usuario del dominio local funciona,, pero si lo ejecuto por defecto con el usuario genérico del servicio 'localsystem' no funciona, el servicio devuelve un error soap:

Ex: ESOAPHTTPException: Received content of invalid Content-Type setting: text/html - SOAP expects "text/xml".

Supongo que seremos muchos los que utilizamos un servicio para hacer el envio del verifactu , me comentan que podría ser por no indicar un 'contenttype' específico,, pero no entiendo entonces porque si funciona sin problemas tanto desde una aplicación de escritorio como ejecutanto el servicio con un usuario del dominio . el usuario del servicio 'localsystem' en principio ya sabemos que no tiene acceso a la red local,, pero aqui... supuestamente se hace una llamada por soap a un servicio web...
no entiendo..
esta es la llamada genérica que hacemos..


Código Delphi [-]
 res := RespuestaRegFactuSistemaFacturacionType.Create;
      try
        SistemaFacturacion.RespuestaRegFactuSistemaFacturacion(Res) :=
               SistemaFacturacion.GetsfPortTypeVerifactu(false, Rio.URL , Rio ).RegFactuSistemaFacturacion( verifactu );
      except
        on e:exception do
        begin
           texto_error := 'Error SOAP: '+E.message;
         
           exit;
        end;
      end;

sglorka 30-06-2025 13:20:08

Parece que el problema puede ser de acceso al almacén de certificados. Ten en cuenta que Localsystem sólo tiene acceso al almacén de certificados local (LocalMachine ) y si no lo tienes ahí pueden haber problemas. Escribe un log donde puedas ver si el servicio puede hacer un acceso a Internet ( por ejemplo con un ping), si lo puede hacer, tiene todas las papeletas de que es problema de certificado. Debes registrar entonces el certificado en dicho almacén
Como alternativa prueba con NetworkService. Si ninguna de estas te funciona, sólo puedes ejecutarlo con una cuenta de usuario.
En mi caso yo realizo el mismo proceso que tú con el servicio pero lo hago en un entorno .Net

Galahad 30-06-2025 17:06:36

Cita:

Empezado por sglorka (Mensaje 565930)
Parece que el problema puede ser de acceso al almacén de certificados. Ten en cuenta que Localsystem sólo tiene acceso al almacén de certificados local (LocalMachine ) y si no lo tienes ahí pueden haber problemas. Escribe un log donde puedas ver si el servicio puede hacer un acceso a Internet ( por ejemplo con un ping), si lo puede hacer, tiene todas las papeletas de que es problema de certificado. Debes registrar entonces el certificado en dicho almacén
Como alternativa prueba con NetworkService. Si ninguna de estas te funciona, sólo puedes ejecutarlo con una cuenta de usuario.
En mi caso yo realizo el mismo proceso que tú con el servicio pero lo hago en un entorno .Net

Hola sglorka, gracias por contestar.
He comprobado desde el servicio que si que tengo acceso a internet,, en cuanto al certificado,, es el de pruebas de la agencia tributaria , aunque tambien he probado con el mio personal,, si que lo detecta, utilizo esta función para buscar el certificado
Código Delphi [-]

    CERT_STORE_PROV_SYSTEM = 'System';
    CERT_SYSTEM_STORE_CURRENT_USER = $00010000;
    CERT_SYSTEM_STORE_LOCAL_MACHINE = $00020000;
function Buscar_Certificado_Serial2(const Nombre_Certificado: String): string;
var
  hStore: HCERTSTORE;
  CurContext: PCCERT_CONTEXT;
  cbSize: DWORD;
  sNombre: string;

  function BuscarEnAlmacen(dwFlags: DWORD): string;
  begin
    Result := '';
    hStore := CertOpenStore(
      CERT_STORE_PROV_SYSTEM,
      0,
      0,
      dwFlags,
      PChar('MY')
    );
    if hStore = nil then Exit;

    CurContext := nil;
    repeat
      CurContext := CertEnumCertificatesInStore(hStore, CurContext);
      if CurContext = nil then Break;

      cbSize := CertGetNameString(CurContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nil, nil, 0);
      if cbSize > 1 then
      begin
        SetLength(sNombre, cbSize - 1);
        CertGetNameString(CurContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nil, PChar(sNombre), cbSize);
        if SameText(Trim(sNombre), Trim(Nombre_Certificado)) then
        begin
          Result := GetCertSerialNumber(@CurContext^.pCertInfo^.SerialNumber);
          Break;
        end;
      end;
    until False;

    CertCloseStore(hStore, 0);
  end;

begin
  Result := BuscarEnAlmacen(CERT_SYSTEM_STORE_CURRENT_USER);
  if Result = '' then
    Result := BuscarEnAlmacen(CERT_SYSTEM_STORE_LOCAL_MACHINE);
end;

con esta función si que me encuentra el certificado y me lo asigna a la propiedad serialnum del httprio. El certificado lo he instalado tanto en el usuario como en el equipo, lo único destacable es que cuando funciona (utilizándolo desde una aplicación de escritorio me lo localiza en : CERT_SYSTEM_STORE_CURRENT_USER y cuando no funciona (servicio), me lo encuentra en: CERT_SYSTEM_STORE_LOCAL_MACHINE

Chatgpt dice esto del componente httprio que está basado en la unidad WinInet: WinINet no está recomendado para servicios Windows: no es thread-safe, no funciona bien en servicios o contextos sin UI., recomienda el uso para servicios de los componentes: TNetHTTPClient + TNetHTTPRequest
Este es el cliente HTTP moderno basado en la API de sockets de bajo nivel.
El problema es que me tocaria gestionar el xml casi a mano,y luego pelearme con el certificado...

sglorka 30-06-2025 17:49:04

Prueba a utilizar el certificado en .pfx para el servicio poniendo tu la contraseña por código sin tener que acceder al almacén

Galahad 01-07-2025 08:37:15

Cita:

Empezado por sglorka (Mensaje 565941)
Prueba a utilizar el certificado en .pfx para el servicio poniendo tu la contraseña por código sin tener que acceder al almacén

Hola sglorka,,, no ha funcionado.
seguir buscando,,, ´gracias

Neftali [Germán.Estévez] 01-07-2025 09:24:08

Yo te iba a decir que probaras en la misma línea de [sglorka].
Nosotros también hemos tenido problemas con el servicio en relación a los certificados.
El mismo certificado hay que instalarlo en el almacén del usuario (para cuando probamos con la App de envío) y en el almacén de la máquina (para cuando realizamos el envío con el Servicio).
El código de la App y del servicio son iguales, pues comparten una DLL que tiene el código, pero cada uno necesita el certificado en un lugar diferente (por la cuenta con la que se ejecutan).

Ten en cuenta que si lo haces así, el acceso al almacén de certificados de la maquina, necesita una cuenta con "permisos elevados" (al menos en nuestro caso)

ISCOPYME 01-07-2025 09:51:36

Ese mismo problema lo tuve al implementar en envío mediante servicio windows.
En mi caso detecté que no llegaba a lanzarse el evento OnNeedClientCertificateHandler del httprio.
En cuanto cambié el usuario que iniciaba el servicio a uno con privilegios funcionó correctamente.

Galahad 01-07-2025 09:58:41

Gracias por responder
Entiendo que aunque se cargara el certificado de manera dinámica desde un fichero nos pasaria lo mismo, problemas de permiso del usuario que arranca el servicio.
Por lo que comentáis parece ser que se tendrá que hacer eso, aunque es un problema en un contexto de instalar ese servicio en muchos clientes,, en fin.
Queria hacer una prueba sin utilizar componentes httprio, utilizando los componentes nethttpclient ,httprequest,etc... pero creo que tendria el mismo problema

Quim Herrera 02-07-2025 07:11:18

Cita:

Empezado por Galahad (Mensaje 565925)
Buenos días.
Hemos desarrollado una función de envio de registros del verifactu que funciona perfectamente cuando se utiliza desde una aplicación de escritorio.
La hemos trasladado a un servicio con el fin de que al final sea dicho servicio el que se encargue del envio de los xml de las declaraciones.
Si ejecuto dicho servicio con credenciales de un usuario del dominio local funciona,, pero si lo ejecuto por defecto con el usuario genérico del servicio 'localsystem' no funciona, el servicio devuelve un error soap:

Ex: ESOAPHTTPException: Received content of invalid Content-Type setting: text/html - SOAP expects "text/xml".

Supongo que seremos muchos los que utilizamos un servicio para hacer el envio del verifactu , me comentan que podría ser por no indicar un 'contenttype' específico,, pero no entiendo entonces porque si funciona sin problemas tanto desde una aplicación de escritorio como ejecutanto el servicio con un usuario del dominio . el usuario del servicio 'localsystem' en principio ya sabemos que no tiene acceso a la red local,, pero aqui... supuestamente se hace una llamada por soap a un servicio web...
no entiendo..
esta es la llamada genérica que hacemos..


Código Delphi [-]
 res := RespuestaRegFactuSistemaFacturacionType.Create;
      try
        SistemaFacturacion.RespuestaRegFactuSistemaFacturacion(Res) :=
               SistemaFacturacion.GetsfPortTypeVerifactu(false, Rio.URL , Rio ).RegFactuSistemaFacturacion( verifactu );
      except
        on e:exception do
        begin
           texto_error := 'Error SOAP: '+E.message;
         
           exit;
        end;
      end;


Hola,
A mi me pasaba lo mismo. Al final lo solucioné cargando el certificado en pfx y poniendo la contraseña por código y al enviar inicializar COM antes porque si no parece que httprio no funciona bien en el servicio:

Código Delphi [-]
uses Winapi.ActiveX;

   try
      CoInitialize(nil); // Inicialitza COM

      ...

        HTTPRIO1:=THTTPRIO.create(nil);

    .....

        res:=   RespuestaRegFactuSistemaFacturacionType.Create;
        res:=   GetsfPortTypeVerifactu( false, direccion_envio , HTTPRIO1 ).RegFactuSistemaFacturacion( veriFactu );        // enviarlo !

  finally
       CoUninitialize; //limpia COM
   end;

El código para la carga del certificado lo adapté del proyecto Demo con código Verifactu.dll para (delphi 7) que hay en el foro y me funciona bien en Delphi 10.2.3.

Saludos

Galahad 02-07-2025 13:04:28

Cita:

Empezado por Quim Herrera (Mensaje 565984)
Hola,
A mi me pasaba lo mismo. Al final lo solucioné cargando el certificado en pfx y poniendo la contraseña por código y al enviar inicializar COM antes porque si no parece que httprio no funciona bien en el servicio:

Código Delphi [-]
uses Winapi.ActiveX;

   try
      CoInitialize(nil); // Inicialitza COM

      ...

        HTTPRIO1:=THTTPRIO.create(nil);

    .....

        res:=   RespuestaRegFactuSistemaFacturacionType.Create;
        res:=   GetsfPortTypeVerifactu( false, direccion_envio , HTTPRIO1 ).RegFactuSistemaFacturacion( veriFactu );        // enviarlo !

  finally
       CoUninitialize; //limpia COM
   end;

El código para la carga del certificado lo adapté del proyecto Demo con código Verifactu.dll para (delphi 7) que hay en el foro y me funciona bien en Delphi 10.2.3.

Saludos

vale, muchas gracias,, voy a revisar ese proyecto y adaptar esa función a ver si solucionamos el tema..

Galahad 10-07-2025 11:56:08

Cita:

Empezado por Galahad (Mensaje 566014)
vale, muchas gracias,, voy a revisar ese proyecto y adaptar esa función a ver si solucionamos el tema..

Hola, perdonad que moleste de nuevo con esto, pero es que no lo consigo.
He intentado adaptar el ejemplo de carga del proyecto 'Demo Verifactu DLL', pero como tengo delphi 12 y ese proyecto esta pensado para delphi 7 no coinciden algunas clases y no consigo adaptarlo.

por ejemplo en el proyecto de delphi 7 el evento webnodebeforepost esta declarado así:
Código Delphi [-]
procedure TEventosWebNode.WebNodeBeforePost(const HTTPReqResp: THTTPReqResp;
  Data: Pointer);
var
  pStore: HCERTSTORE;
  pCert: PCERT_CONTEXT;
  DataBlob: CRYPT_BIT_BLOB;
  PFX: TBytes;
  pass,cert:string;
begin
  if sesion=-1 then // viene de un validador de nif
  begin
      pass:=nifValidatorPass;
      cert:=nifValidator;
  end
  else     // viene de un objeto verifactu
  begin
      pass:=sesiones[sesion].inicio.passwordCertificado;
      cert:=sesiones[sesion].inicio.nombreCertificado;
  end;


  if (pos('.PFX',uppercase(cert))=0) and
     (pos('.P12',uppercase(cert))=0) then exit;


  cargaCertificado(data, cert, pass);
end;

y yo, en delphi 12 yo la utilizo asi:

Código Delphi [-]
procedure TEventosWebNode.WebNodeBeforePost(const HTTPReqResp: THTTPReqResp;
  Client: THTTPClient );
  //Data: Pointer);
var
  pStore: HCERTSTORE;
  pCert: PCERT_CONTEXT;
  DataBlob: CRYPT_BIT_BLOB;
  PFX: TBytes;
  pass,cert:string;
  puntero: pointer;
begin
  pass    :=  inicio.passwordCertificado;
  cert    :=  inicio.nombreCertificado;

  puntero :=  HTTPReqResp.ClientCertificate;// GetHTTPReqResp;
  if (pos('.PFX',uppercase(cert))=0) and
     (pos('.P12',uppercase(cert))=0) then exit;

  cargaCertificado(puntero, cert, pass);
end;

luego , la función cargacertificado aplico este codigo:
Código Delphi [-]
procedure cargaCertificado( Data:Pointer ; cert,pass:string);
var
  pStore: HCERTSTORE;
  pCert, currentcert: PCERT_CONTEXT;
  //DataBlob: CRYPT_BIT_BLOB;
  DataBlob:  CRYPT_DATA_BLOB;
  PFX: TBytes;
  certname:string;
begin
  pStore := nil;
  pCert := nil;

  PFX := TFile.ReadAllBytes( cert );
  try
    DataBlob.cbData := Length(PFX);
    DataBlob.pbData := @PFX[0];

    // Almacen temporal con el contenido del PFX
    pStore := PFXImportCertStore(DataBlob, PWideChar(Pass), PKCS12_INCLUDE_EXTENDED_PROPERTIES or CRYPT_MACHINE_KEYSET );
    CheckError(pStore);

    // Buscar un certificado con clave privada
    // Solo debería haber uno
    pCert := CertFindCertificateInStore(pStore,
                                        X509_ASN_ENCODING,
                                        0,
                                        CERT_FIND_HAS_PRIVATE_KEY, //CERT_FIND_ANY,
                                        nil,
                                        nil);
    CheckError(pCert);

    // Pasarlo al servicio
    InternetSetOption(Data, INTERNET_OPTION_CLIENT_CERT_CONTEXT, pCert, SizeOf(CERT_CONTEXT));
    finally
    if Assigned(pCert) then
       CertFreeCertificateContext(pCert);

    if Assigned(pStore) then
      CertCloseStore(pStore, 0);
  end;

end;

Un saludo ...

Neftali [Germán.Estévez] 10-07-2025 14:16:37

Si me envías un privado con los cambios o con el fuente de la versión 12, podemos subirlo o ver cómo podemos publicarlo en el FTP, para otros usuarios.
El usuario [seccion_31] me envía un privado con un "weetransfer" o similar, si lo quieres hacer así o subirlo a otro sitio, también va bien.

Galahad 10-07-2025 18:59:50

Cita:

Empezado por Neftali [Germán.Estévez] (Mensaje 566275)
Si me envías un privado con los cambios o con el fuente de la versión 12, podemos subirlo o ver cómo podemos publicarlo en el FTP, para otros usuarios.
El usuario [seccion_31] me envía un privado con un "weetransfer" o similar, si lo quieres hacer así o subirlo a otro sitio, también va bien.

oK, Gracias Netfali.
He cogido de ese código solamente las dos funciones que en principio me hacen falta para el servicio,,, no todo el proyecto.
Intentaré cuando tenga un rato documentarlas un poco y enviarla como me comentas.
Un saludo y gracias..


La franja horaria es GMT +2. Ahora son las 03:06:58.

Powered by vBulletin® Version 3.6.8
Copyright ©2000 - 2026, Jelsoft Enterprises Ltd.
Traducción al castellano por el equipo de moderadores del Club Delphi