ProyectoGrupal/VerifactuMidAPI/verifactu/client.go

260 lines
6.2 KiB
Go
Raw Permalink Normal View History

package verifactu
import (
"bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
"VerifactuMidAPI/internal/cert"
)
type Client struct {
BaseURL string
HTTPClient *http.Client
Certificate *tls.Certificate
}
type ClientConfig struct {
BaseURL string
Timeout time.Duration
CertificatePath string
CertificatePassword string
}
func NewClient(cfg ClientConfig) (*Client, error) {
httpClient := &http.Client{
Timeout: cfg.Timeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: false,
},
},
}
var tlsCert *tls.Certificate
if cfg.CertificatePath != "" {
c, err := LoadCertificate(cfg.CertificatePath, cfg.CertificatePassword)
if err != nil {
return nil, fmt.Errorf("loading certificate: %w", err)
}
tlsCert = c
}
return &Client{
BaseURL: cfg.BaseURL,
HTTPClient: httpClient,
Certificate: tlsCert,
}, nil
}
func LoadCertificate(certPath, password string) (*tls.Certificate, error) {
der, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf("reading cert file: %w", err)
}
return LoadCertificateFromBytes(der, password)
}
func LoadCertificateFromBytes(der []byte, password string) (*tls.Certificate, error) {
return parseP12(der, password)
}
func parseP12(der []byte, password string) (*tls.Certificate, error) {
key, x509Cert, chain, err := cert.DecodeLeafAndKey(der, password)
if err != nil {
return nil, fmt.Errorf("decoding PKCS#12: %w", err)
}
if x509Cert == nil {
return nil, fmt.Errorf("no certificate found in PKCS#12")
}
tlsCert := &tls.Certificate{
Certificate: [][]byte{x509Cert.Raw},
PrivateKey: key,
Leaf: x509Cert,
}
for _, c := range chain {
tlsCert.Certificate = append(tlsCert.Certificate, c.Raw)
}
log.Printf("cert loaded: subject=%s, expires=%s, chain=%d", x509Cert.Subject.CommonName, x509Cert.NotAfter.Format("2006-01-02"), len(chain))
return tlsCert, nil
}
func (c *Client) SetCertificate(certPath, password string) error {
tlsCert, err := LoadCertificate(certPath, password)
if err != nil {
return err
}
c.Certificate = tlsCert
return nil
}
func (c *Client) SendAlta(data AltaData) (*Response, error) {
env, err := BuildAltaSOAPRequest(data)
if err != nil {
return nil, fmt.Errorf("building Alta request: %w", err)
}
if xmlBytes, err2 := env.ToBytes(); err2 == nil {
log.Printf("[SOAP-Alta] XML generado (%d bytes):\n%s", len(xmlBytes), string(xmlBytes))
}
return c.SendRequest(env)
}
func (c *Client) SendAnulacion(data AltaData) (*Response, error) {
env, err := BuildAnulacionSOAPRequest(data)
if err != nil {
return nil, fmt.Errorf("building Anulacion request: %w", err)
}
return c.SendRequest(env)
}
func (c *Client) SendRequest(env *SOAPEnvelope) (*Response, error) {
body, err := env.ToBytes()
if err != nil {
return nil, fmt.Errorf("marshaling request: %w", err)
}
req, err := http.NewRequest("POST", c.BaseURL, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Content-Type", "text/xml; charset=utf-8")
req.Header.Set("SOAPAction", "")
if c.Certificate != nil {
log.Printf("Using client certificate with TLS")
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{*c.Certificate},
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12,
}
c.HTTPClient.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("sending request: %w", err)
}
defer resp.Body.Close()
log.Printf("AEAT response status: %d", resp.StatusCode)
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP error: %d - %s", resp.StatusCode, string(respBody))
}
response, err := ParseResponse(respBody)
if err != nil {
log.Printf("[AEAT] Respuesta no es SOAP:\n%s", string(respBody))
return nil, fmt.Errorf("parsing response: %w", err)
}
return response, nil
}
// CertSubject extrae el NIF y nombre del emisor del certificado cargado.
func (c *Client) CertSubject() (nif, nombre string) {
if c.Certificate == nil || c.Certificate.Leaf == nil {
return "", ""
}
subj := c.Certificate.Leaf.Subject
nif = normalizeNIF(subj.SerialNumber)
// Fallback: buscar NIF: en el CommonName (ej. ACCV: "NOMBRE - NIF:12345678A")
if nif == "" {
nif = extractNIFFromCN(subj.CommonName)
}
if len(subj.Organization) > 0 {
nombre = subj.Organization[0]
} else {
// Usar la parte del CN antes del separador, sin el NIF
nombre = cleanNombre(subj.CommonName)
}
return
}
func normalizeNIF(serial string) string {
s := strings.ToUpper(strings.TrimSpace(serial))
for _, prefix := range []string{"IDCES-", "VATES-", "ES"} {
if strings.HasPrefix(s, prefix) {
return s[len(prefix):]
}
}
return s
}
// extractNIFFromCN busca patrones como "NIF:12345678A" o "- 12345678A" en el CN.
func extractNIFFromCN(cn string) string {
upper := strings.ToUpper(cn)
// Patrón "NIF:XXXXXXXXX"
if idx := strings.Index(upper, "NIF:"); idx != -1 {
rest := strings.TrimSpace(cn[idx+4:])
end := strings.IndexAny(rest, " ,;)")
if end == -1 {
end = len(rest)
}
return strings.ToUpper(rest[:end])
}
return ""
}
// cleanNombre devuelve el CN sin el fragmento del NIF.
func cleanNombre(cn string) string {
if idx := strings.Index(strings.ToUpper(cn), " - NIF:"); idx != -1 {
return strings.TrimSpace(cn[:idx])
}
return cn
}
func (c *Client) SetRootCA(caPath string) error {
caData, err := os.ReadFile(caPath)
if err != nil {
return fmt.Errorf("reading CA file: %w", err)
}
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(caData)
if transport, ok := c.HTTPClient.Transport.(*http.Transport); ok {
if transport.TLSClientConfig == nil {
transport.TLSClientConfig = &tls.Config{}
}
transport.TLSClientConfig.RootCAs = pool
}
return nil
}
func GetTestURL() string {
return "https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"
}
func GetProdURL() string {
return "https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"
}