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

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

refs #1936 authenticate to ogcore and check ogcore cert

  • 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
47TIMEOUT = 5  # Connection timout, in seconds
48
49
50class RESTError(Exception):
51    ERRCODE = 0
52
53
54class ConnectionError(RESTError):
55    ERRCODE = -1
56
57
58# Disable warnings log messages
59try:
60    import urllib3  # @UnusedImport
61    requests_log = logging.getLogger ('urllib3')
62    requests_log.setLevel (logging.INFO)
63except Exception:
64    from requests.packages import urllib3  # @Reimport
65    requests_log = logging.getLogger ('requests.packages.urllib3')
66    requests_log.setLevel (logging.INFO)
67
68try:
69    urllib3.disable_warnings()  # @UndefinedVariable
70    warnings.simplefilter("ignore")
71except Exception:
72    pass  # In fact, isn't too important, but wil log warns to logging file
73
74
75class REST(object):
76    """
77    Simple interface to remote REST apis.
78    The constructor expects the "base url" as parameter, that is, the url that will be common on all REST requests
79    Remember that this is a helper for "easy of use". You can provide your owns using requests lib for example.
80    Examples:
81       v = REST('https://example.com/rest/v1/') (Can omit trailing / if desired)
82       v.sendMessage('hello?param1=1&param2=2')
83         This will generate a GET message to https://example.com/rest/v1/hello?param1=1&param2=2, and return the
84         deserialized JSON result or an exception
85       v.sendMessage('hello?param1=1&param2=2', {'name': 'mario' })
86         This will generate a POST message to https://example.com/rest/v1/hello?param1=1&param2=2, with json encoded
87         body {'name': 'mario' }, and also returns
88         the deserialized JSON result or raises an exception in case of error
89    """
90
91    def __init__(self, url, ca_file=None, crt_file=None, key_file=None):
92        """
93        Initializes the REST helper
94        url is the full url of the REST API Base, as for example "https://example.com/rest/v1".
95        @param url The url of the REST API Base. The trailing '/' can be included or omitted, as desired.
96        """
97        self.endpoint = url
98
99        if self.endpoint[-1] != '/':
100            self.endpoint += '/'
101
102        # Some OSs ships very old python requests lib implementations, workaround them...
103        try:
104            self.newerRequestLib = requests.__version__.split('.')[0] >= '1'
105        except Exception:
106            self.newerRequestLib = False  # I no version, guess this must be an old requests
107
108        if not self.newerRequestLib:
109            logger.debug ('TLS not available: python requests library is old')
110
111        self.use_tls = url.startswith ('https')
112        if self.use_tls:
113            if not ca_file or not crt_file or not key_file:
114                raise Exception ('missing TLS parameters in REST constructor')
115
116            errs = 0
117            for f in [ca_file, crt_file, key_file]:
118                if not os.path.exists (f):
119                    logger.error (f'{f}: No such file or directory')
120                    errs += 1
121            if errs:
122                raise Exception ('TLS files not found')
123
124        self.ca_file  = ca_file
125        self.crt_file = crt_file
126        self.key_file = key_file
127
128        # Disable logging requests messages except for errors, ...
129        logging.getLogger("requests").setLevel(logging.CRITICAL)
130        # Tries to disable all warnings
131        try:
132            warnings.simplefilter("ignore")  # Disables all warnings
133        except Exception:
134            pass
135
136    def _getUrl(self, method):
137        """
138        Internal method
139        Composes the URL based on "method"
140        @param method: Method to append to base url for composition
141        """
142        url = self.endpoint + method
143
144        return url
145
146    def _request(self, url, data=None):
147        """
148        Launches the request
149        @param url: The url to obtain
150        @param data: if None, the request will be sent as a GET request. If != None, the request will be sent as a POST,
151        with data serialized as JSON in the body.
152        """
153        try:
154            if data is None:
155                logger.debug('Requesting using GET (no data provided) {}'.format(url))
156                # Old requests version does not support verify, but it do not checks ssl certificate by default
157                if self.newerRequestLib:
158                    if self.use_tls:
159                        logger.debug ('nati: using TLS for GET')
160                        r = requests.get(url, cert=(self.crt_file, self.key_file), verify=self.ca_file, timeout=TIMEOUT)
161                    else:
162                        r = requests.get(url, timeout=TIMEOUT)
163                else:
164                    r = requests.get(url)
165            else:  # POST
166                logger.debug('Requesting using POST {}, data: {}'.format(url, data))
167                if self.newerRequestLib:
168                    if self.use_tls:
169                        logger.debug ('nati: using TLS for POST')
170                        r = requests.post(url, data=data, headers={'content-type': 'application/json'}, cert=(self.crt_file, self.key_file), verify=self.ca_file, timeout=TIMEOUT)
171                    else:
172                        r = requests.post(url, data=data, headers={'content-type': 'application/json'}, timeout=TIMEOUT)
173                else:
174                    r = requests.post(url, data=data, headers={'content-type': 'application/json'})
175
176            r.raise_for_status()
177            ct = r.headers['Content-Type']
178            if 'application/json' != ct:
179                raise Exception (f'response content-type is not "application/json" but "{ct}"')
180            r = json.loads(r.content)  # Using instead of r.json() to make compatible with old requests lib versions
181        except requests.exceptions.RequestException as e:
182            code = e.response.status_code
183            logger.warning (f'request failed, HTTP code "{code}"')
184            return None
185        except Exception as e:
186            raise ConnectionError(exceptionToMessage(e))
187
188        return r
189
190    def sendMessage(self, msg, data=None, processData=True):
191        """
192        Sends a message to remote REST server
193        @param data: if None or omitted, message will be a GET, else it will send a POST
194        @param processData: if True, data will be serialized to json before sending, else, data will be sent as "raw"
195        """
196        #logger.debug('Invoking post message {} with data {}'.format(msg, data))
197
198        if processData and data is not None:
199            data = json.dumps(data)
200
201        url = self._getUrl(msg)
202        #logger.debug('Requesting {}'.format(url))
203
204        try:
205            res = self._request(url, data)
206            return res
207        except:
208            logger.exception()
209            return None
Note: See TracBrowser for help on using the repository browser.