Skip to content

Commit 3037186

Browse files
committed
[IMP] certificate, l10n_es_edi_*: Factor patched HTTPAdapter for certificates models
We generalize the CertificateAdapter that is needed to make zeep read our own Certificate model for the session. task-4477745 closes odoo#210572 Signed-off-by: Ricardo Gomes Rodrigues (rigr) <[email protected]>
1 parent 9b9e020 commit 3037186

File tree

4 files changed

+60
-52
lines changed

4 files changed

+60
-52
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .certificate_adapter import CertificateAdapter
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from base64 import b64decode
2+
3+
import requests
4+
from OpenSSL.crypto import FILETYPE_PEM, load_certificate, load_privatekey
5+
from urllib3.contrib.pyopenssl import inject_into_urllib3
6+
from urllib3.util.ssl_ import create_urllib3_context
7+
8+
9+
class CertificateAdapter(requests.adapters.HTTPAdapter):
10+
11+
def __init__(self, *args, ciphers=None, **kwargs):
12+
self._context_args = {}
13+
if ciphers:
14+
self._context_args['ciphers'] = ciphers
15+
super().__init__(*args, **kwargs)
16+
17+
def init_poolmanager(self, *args, **kwargs):
18+
""" We need inject_into_urllib3 as it forces the adapter to use PyOpenSSL.
19+
With PyOpenSSL, we can further patch the code to make it do what we want
20+
(with the use of SSLContext)
21+
"""
22+
# OVERRIDE
23+
inject_into_urllib3()
24+
kwargs['ssl_context'] = create_urllib3_context(**self._context_args)
25+
return super().init_poolmanager(*args, **kwargs)
26+
27+
def cert_verify(self, conn, url, verify, cert):
28+
""" The original method wants to check for an existing file
29+
at the cert location. As we use in-memory objects,
30+
we skip the check and assign it manually.
31+
"""
32+
# OVERRIDE
33+
super().cert_verify(conn, url, verify, None)
34+
conn.cert_file = cert
35+
conn.key_file = None
36+
37+
def get_connection(self, url, proxies=None):
38+
""" Reads the certificate from a certificate.certificate rather than from the filesystem """
39+
# OVERRIDE
40+
conn = super().get_connection(url, proxies=proxies)
41+
context = conn.conn_kw['ssl_context']
42+
43+
def patched_load_cert_chain(certificate, keyfile=None, password=None):
44+
certificate = certificate.sudo()
45+
pem, key = map(b64decode, (certificate.pem_certificate, certificate.private_key_id.pem_key))
46+
context._ctx.use_certificate(load_certificate(FILETYPE_PEM, pem))
47+
context._ctx.use_privatekey(load_privatekey(FILETYPE_PEM, key))
48+
49+
context.load_cert_chain = patched_load_cert_chain
50+
return conn

addons/l10n_es_edi_sii/models/account_edi_format.py

Lines changed: 7 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,21 @@
1-
# -*- coding: utf-8 -*-
2-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
1+
import json
2+
import math
33
from collections import defaultdict
4-
from urllib3.util.ssl_ import create_urllib3_context
5-
from urllib3.contrib.pyopenssl import inject_into_urllib3
6-
from OpenSSL.crypto import load_certificate, load_privatekey, FILETYPE_PEM
74

8-
from odoo import fields, models, _
5+
import requests
6+
7+
from odoo import _, fields, models
98
from odoo.exceptions import UserError
109
from odoo.tools import html_escape, zeep
1110
from odoo.tools.float_utils import float_round
1211

13-
import base64
14-
import math
15-
import json
16-
import requests
17-
12+
from odoo.addons.certificate.tools import CertificateAdapter
1813

1914
# Custom patches to perform the WSDL requests.
2015
# Avoid failure on servers where the DH key is too small
2116
EUSKADI_CIPHERS = "DEFAULT:!DH"
2217

2318

24-
class PatchedHTTPAdapter(requests.adapters.HTTPAdapter):
25-
""" An adapter to block DH ciphers which may not work for the tax agencies called"""
26-
27-
def init_poolmanager(self, *args, **kwargs):
28-
# OVERRIDE
29-
inject_into_urllib3()
30-
kwargs['ssl_context'] = create_urllib3_context(ciphers=EUSKADI_CIPHERS)
31-
return super().init_poolmanager(*args, **kwargs)
32-
33-
def cert_verify(self, conn, url, verify, cert):
34-
# OVERRIDE
35-
# The last parameter is only used by the super method to check if the file exists.
36-
# In our case, cert is an odoo record 'certificate.certificate' so not a path to a file.
37-
# By putting 'None' as last parameter, we ensure the check about TLS configuration is
38-
# still made without checking temporary files exist.
39-
super().cert_verify(conn, url, verify, None)
40-
conn.cert_file = cert
41-
conn.key_file = None
42-
43-
def get_connection(self, url, proxies=None):
44-
# OVERRIDE
45-
# Patch the OpenSSLContext to decode the certificate in-memory.
46-
conn = super().get_connection(url, proxies=proxies)
47-
context = conn.conn_kw['ssl_context']
48-
49-
def patched_load_cert_chain(l10n_es_odoo_certificate, keyfile=None, password=None):
50-
certificate = l10n_es_odoo_certificate
51-
cert_obj = load_certificate(FILETYPE_PEM, base64.b64decode(certificate.sudo().pem_certificate))
52-
pkey_obj = load_privatekey(FILETYPE_PEM, base64.b64decode(certificate.sudo().private_key_id.pem_key))
53-
54-
context._ctx.use_certificate(cert_obj)
55-
context._ctx.use_privatekey(pkey_obj)
56-
57-
context.load_cert_chain = patched_load_cert_chain
58-
59-
return conn
60-
61-
6219
class AccountEdiFormat(models.Model):
6320
_inherit = 'account.edi.format'
6421

@@ -511,7 +468,7 @@ def _l10n_es_edi_call_web_service_sign_common(self, invoices, info_list, cancel=
511468

512469
session = requests.Session()
513470
session.cert = company.l10n_es_sii_certificate_id
514-
session.mount('https://', PatchedHTTPAdapter())
471+
session.mount('https://', CertificateAdapter(ciphers=EUSKADI_CIPHERS))
515472

516473
client = zeep.Client(connection_vals['url'], operation_timeout=60, timeout=60, session=session)
517474

addons/l10n_es_edi_tbai/models/l10n_es_edi_tbai_document.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from requests.exceptions import RequestException
1212

1313
from odoo import _, api, fields, models, release
14-
from odoo.addons.l10n_es_edi_sii.models.account_edi_format import PatchedHTTPAdapter
14+
from odoo.addons.certificate.tools import CertificateAdapter
1515
from odoo.addons.l10n_es_edi_tbai.models.l10n_es_edi_tbai_agencies import get_key
1616
from odoo.addons.l10n_es_edi_tbai.models.xml_utils import (
1717
NS_MAP,
@@ -190,7 +190,7 @@ def _post_to_agency(self, env, is_sale):
190190
def _send_request_to_agency(*args, **kwargs):
191191
session = requests.Session()
192192
session.cert = kwargs.pop('pkcs12_data')
193-
session.mount("https://", PatchedHTTPAdapter())
193+
session.mount("https://", CertificateAdapter())
194194
response = session.request('post', *args, **kwargs)
195195
response.raise_for_status()
196196
response_xml = None

0 commit comments

Comments
 (0)