package verifactu import ( "bytes" "crypto/tls" "crypto/x509" "fmt" "io" "log" "net/http" "os" "time" "golang.org/x/crypto/pkcs12" ) 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 cert *tls.Certificate if cfg.CertificatePath != "" { c, err := LoadCertificate(cfg.CertificatePath, cfg.CertificatePassword) if err != nil { return nil, fmt.Errorf("loading certificate: %w", err) } cert = c } return &Client{ BaseURL: cfg.BaseURL, HTTPClient: httpClient, Certificate: cert, }, 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 parseP12(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, cert, err := pkcs12.Decode(der, password) if err != nil { return nil, fmt.Errorf("decoding PKCS#12: %w", err) } if cert == nil { return nil, fmt.Errorf("no certificate found in PKCS#12") } tlsCert := &tls.Certificate{ Certificate: [][]byte{cert.Raw}, PrivateKey: key, Leaf: cert, } log.Printf("cert loaded: subject=%s has private key=%v", cert.Subject, key != nil) return tlsCert, nil } func (c *Client) SetCertificate(certPath, password string) error { cert, err := LoadCertificate(certPath, password) if err != nil { return err } c.Certificate = cert 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) } 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, } 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 { return nil, fmt.Errorf("parsing response: %w", err) } return response, nil } 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" }