source: ogAgent-Git/src/opengnsys/RESTApi.py @ fdd4e6d

exec-ogbrowserfix-urllog-sess-lenmainno-tlsoggitsched-tasktls
Last change on this file since fdd4e6d was fdd4e6d, checked in by Natalia Serrano <natalia.serrano@…>, 4 weeks ago

refs #1935 look for cert files on startup

  • Property mode set to 100644
File size: 8.3 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (c) 201 Virtual Cable S.L.
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without modification,
7# are permitted provided that the following conditions are met:
8#
9#    * Redistributions of source code must retain the above copyright notice,
10#      this list of conditions and the following disclaimer.
11#    * Redistributions in binary form must reproduce the above copyright notice,
12#      this list of conditions and the following disclaimer in the documentation
13#      and/or other materials provided with the distribution.
14#    * Neither the name of Virtual Cable S.L. nor the names of its contributors
15#      may be used to endorse or promote products derived from this software
16#      without specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29"""
30@author: Adolfo Gómez, dkmaster at dkmon dot com
31"""
32
33# pylint: disable-msg=E1101,W0703
34
35
36
37import os
38import requests
39import logging
40import json
41import warnings
42
43from .log import logger
44
45from .utils import exceptionToMessage
46
47VERIFY_CERT = False  # Do not check server certificate
48TIMEOUT = 5  # Connection timout, in seconds
49
50
51class RESTError(Exception):
52    ERRCODE = 0
53
54
55class ConnectionError(RESTError):
56    ERRCODE = -1
57
58
59# Disable warnings log messages
60try:
61    import urllib3  # @UnusedImport
62    requests_log = logging.getLogger ('urllib3')
63    requests_log.setLevel (logging.INFO)
64except Exception:
65    from requests.packages import urllib3  # @Reimport
66    requests_log = logging.getLogger ('requests.packages.urllib3')
67    requests_log.setLevel (logging.INFO)
68
69try:
70    urllib3.disable_warnings()  # @UndefinedVariable
71    warnings.simplefilter("ignore")
72except Exception:
73    pass  # In fact, isn't too important, but wil log warns to logging file
74
75
76class REST(object):
77    """
78    Simple interface to remote REST apis.
79    The constructor expects the "base url" as parameter, that is, the url that will be common on all REST requests
80    Remember that this is a helper for "easy of use". You can provide your owns using requests lib for example.
81    Examples:
82       v = REST('https://example.com/rest/v1/') (Can omit trailing / if desired)
83       v.sendMessage('hello?param1=1&param2=2')
84         This will generate a GET message to https://example.com/rest/v1/hello?param1=1&param2=2, and return the
85         deserialized JSON result or an exception
86       v.sendMessage('hello?param1=1&param2=2', {'name': 'mario' })
87         This will generate a POST message to https://example.com/rest/v1/hello?param1=1&param2=2, with json encoded
88         body {'name': 'mario' }, and also returns
89         the deserialized JSON result or raises an exception in case of error
90    """
91
92    def __init__(self, url, ca_file=None, crt_file=None, key_file=None):
93        """
94        Initializes the REST helper
95        url is the full url of the REST API Base, as for example "https://example.com/rest/v1".
96        @param url The url of the REST API Base. The trailing '/' can be included or omitted, as desired.
97        """
98        self.endpoint = url
99
100        if self.endpoint[-1] != '/':
101            self.endpoint += '/'
102
103        # Some OSs ships very old python requests lib implementations, workaround them...
104        try:
105            self.newerRequestLib = requests.__version__.split('.')[0] >= '1'
106        except Exception:
107            self.newerRequestLib = False  # I no version, guess this must be an old requests
108
109        if not self.newerRequestLib:
110            logger.debug ('TLS not available: python requests library is old')
111
112        self.use_tls = url.startswith ('https')
113        if self.use_tls:
114            if not ca_file or not crt_file or not key_file:
115                raise Exception ('missing TLS parameters in REST constructor')
116
117            errs = 0
118            for f in [ca_file, crt_file, key_file]:
119                if not os.path.exists (f):
120                    logger.error (f'{f}: No such file or directory')
121                    errs += 1
122            if errs:
123                raise Exception ('TLS files not found')
124
125        # Disable logging requests messages except for errors, ...
126        logging.getLogger("requests").setLevel(logging.CRITICAL)
127        # Tries to disable all warnings
128        try:
129            warnings.simplefilter("ignore")  # Disables all warnings
130        except Exception:
131            pass
132
133    def _getUrl(self, method):
134        """
135        Internal method
136        Composes the URL based on "method"
137        @param method: Method to append to base url for composition
138        """
139        url = self.endpoint + method
140
141        return url
142
143    def _request(self, url, data=None):
144        """
145        Launches the request
146        @param url: The url to obtain
147        @param data: if None, the request will be sent as a GET request. If != None, the request will be sent as a POST,
148        with data serialized as JSON in the body.
149        """
150        try:
151            if data is None:
152                logger.debug('Requesting using GET (no data provided) {}'.format(url))
153                # Old requests version does not support verify, but it do not checks ssl certificate by default
154                if self.newerRequestLib:
155                    if self.use_tls:
156                        logger.debug ('nati: using TLS for GET')
157                        ## TODO enviar mi certificado y comprobar el de ogcore
158                        r = requests.get(url, verify=VERIFY_CERT, timeout=TIMEOUT)
159                    else:
160                        r = requests.get(url, verify=VERIFY_CERT, timeout=TIMEOUT)
161                else:
162                    r = requests.get(url)
163            else:  # POST
164                logger.debug('Requesting using POST {}, data: {}'.format(url, data))
165                if self.newerRequestLib:
166                    if self.use_tls:
167                        logger.debug ('nati: using TLS for POST')
168                        ## TODO enviar mi certificado y comprobar el de ogcore
169                        r = requests.post(url, data=data, headers={'content-type': 'application/json'}, verify=VERIFY_CERT, timeout=TIMEOUT)
170                    else:
171                        r = requests.post(url, data=data, headers={'content-type': 'application/json'}, verify=VERIFY_CERT, timeout=TIMEOUT)
172                else:
173                    r = requests.post(url, data=data, headers={'content-type': 'application/json'})
174
175            r.raise_for_status()
176            ct = r.headers['Content-Type']
177            if 'application/json' != ct:
178                raise Exception (f'response content-type is not "application/json" but "{ct}"')
179            r = json.loads(r.content)  # Using instead of r.json() to make compatible with old requests lib versions
180        except requests.exceptions.RequestException as e:
181            code = e.response.status_code
182            logger.warning (f'request failed, HTTP code "{code}"')
183            return None
184        except Exception as e:
185            raise ConnectionError(exceptionToMessage(e))
186
187        return r
188
189    def sendMessage(self, msg, data=None, processData=True):
190        """
191        Sends a message to remote REST server
192        @param data: if None or omitted, message will be a GET, else it will send a POST
193        @param processData: if True, data will be serialized to json before sending, else, data will be sent as "raw"
194        """
195        #logger.debug('Invoking post message {} with data {}'.format(msg, data))
196
197        if processData and data is not None:
198            data = json.dumps(data)
199
200        url = self._getUrl(msg)
201        #logger.debug('Requesting {}'.format(url))
202
203        try:
204            res = self._request(url, data)
205            return res
206        except:
207            logger.exception()
208            return None
Note: See TracBrowser for help on using the repository browser.