"Fossies" - the Fresh Open Source Software Archive

Member "LinOTP-release-2.11/linotpd/src/linotp/provider/smsprovider/HttpSMSProvider.py" (12 Nov 2019, 21961 Bytes) of package /linux/misc/LinOTP-release-2.11.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "HttpSMSProvider.py" see the Fossies "Dox" file reference documentation.

    1 # -*- coding: utf-8 -*-
    2 #
    3 #    LinOTP - the open source solution for two factor authentication
    4 #    Copyright (C) 2010 - 2019 KeyIdentity GmbH
    5 #
    6 #    This file is part of LinOTP smsprovider.
    7 #
    8 #    This program is free software: you can redistribute it and/or
    9 #    modify it under the terms of the GNU Affero General Public
   10 #    License, version 3, as published by the Free Software Foundation.
   11 #
   12 #    This program is distributed in the hope that it will be useful,
   13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
   14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   15 #    GNU Affero General Public License for more details.
   16 #
   17 #    You should have received a copy of the
   18 #               GNU Affero General Public License
   19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
   20 #
   21 #
   22 #    E-mail: linotp@keyidentity.com
   23 #    Contact: www.linotp.org
   24 #    Support: www.keyidentity.com
   25 #
   26 """This is the SMSClass to send SMS via HTTP Gateways"""
   27 
   28 from linotp.provider.smsprovider import ISMSProvider
   29 from linotp.provider import provider_registry
   30 from linotp.provider import ProviderNotAvailable
   31 from linotp.lib.type_utils import parse_timeout
   32 
   33 import socket
   34 
   35 import base64
   36 import re
   37 
   38 import urllib
   39 import httplib2
   40 import urllib2
   41 
   42 import requests
   43 from requests.auth import HTTPBasicAuth
   44 from requests.auth import HTTPDigestAuth
   45 
   46 from urlparse import urlparse
   47 
   48 
   49 import logging
   50 log = logging.getLogger(__name__)
   51 
   52 # on debian squeeze the httplib is too old and does not contain
   53 # a socks module. So we take an elder one, which does satiisfy
   54 # the import BUT it does not work as well as the former urllib
   55 # proxy does not work :-(
   56 
   57 try:
   58     import httplib2.socks as socks
   59     log.info('Using httplib2.socks')
   60 except ImportError:
   61     import socks as socks
   62     log.info('Using socksipy socks')
   63 
   64 
   65 def http2lib_get_proxy_info(proxy_url):
   66     """
   67     helper to parse the proxyurl and to create the proxy_info object
   68 
   69     :param proxy_url: proxy url string
   70     :return: ProxyInfo object
   71     """
   72     proxy_params = {}
   73     proxy_host = None
   74     proxy_port = 8888
   75 
   76     parts = urlparse(proxy_url)
   77     net_loc = parts[1]
   78 
   79     if "@" in net_loc:
   80         puser, server = net_loc.split('@')
   81         if ':' in puser:
   82             proxy_user, proxy_pass = puser.split(':')
   83             proxy_params["proxy_user"] = proxy_user
   84             proxy_params["proxy_pass"] = proxy_pass
   85     else:
   86         server = net_loc
   87 
   88     if ':' in server:
   89         proxy_host, port = server.split(':')
   90         proxy_port = int(port)
   91     else:
   92         proxy_host = server
   93 
   94     # using httplib2:
   95     # the proxy spec and url + enc. parameters must be of
   96     # type string str() - otherwise the following error will occur:
   97     # : GeneralProxyError: (5, 'bad input') :
   98 
   99     proxy_info = httplib2.ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP,
  100                                     proxy_host=proxy_host,
  101                                     proxy_port=proxy_port,
  102                                     **proxy_params)
  103     return proxy_info
  104 
  105 @provider_registry.class_entry('HttpSMSProvider')
  106 @provider_registry.class_entry('linotp.provider.smsprovider.HttpSMSProvider')
  107 @provider_registry.class_entry('smsprovider.HttpSMSProvider.HttpSMSProvider')
  108 @provider_registry.class_entry('smsprovider.HttpSMSProvider')
  109 class HttpSMSProvider(ISMSProvider):
  110 
  111     def __init__(self):
  112         self.config = {}
  113 
  114     def _submitMessage(self, phone, message):
  115         '''
  116         send out a message to a phone via an http sms connector
  117         :param phone: the phone number
  118         :param message: the message to submit to the phone
  119         '''
  120         url = self.config.get('URL', None)
  121         if url is None:
  122             return
  123 
  124         log.debug("[submitMessage] submitting message "
  125                   "%s to %s" % (message, phone))
  126 
  127         method = self.config.get('HTTP_Method', 'GET')
  128         username = self.config.get('USERNAME', None)
  129         password = self.config.get('PASSWORD', None)
  130 
  131         log.debug("[submitMessage] by method %s" % method)
  132         parameter = self.getParameters(message, phone)
  133 
  134         log.debug("[submitMessage] Now doing the Request")
  135 
  136         # urlib2 has problems with authentication AND https
  137         # below a test of urllib and httplib which shows, that
  138         # we should use in case of Basic Auth and https the httplib:
  139 
  140         # NO_PROX  --  HTTPS Basic Auth  -- urllib  -- : Fail
  141         # NO_PROX  --  HTTPS  --            urllib  -- : Ok
  142         # NO_PROX  --  HTTP Basic Auth  --  urllib  -- : Ok
  143         # NO_PROX  --  HTTP  --             urllib  -- : Ok
  144 
  145         # PROX  --     HTTPS Basic Auth  -- urllib  -- : Fail
  146         # PROX  --     HTTPS  --            urllib  -- : Ok
  147         # PROX  --     HTTP Basic Auth  --  urllib  -- : Ok
  148         # PROX  --     HTTP  --             urllib  -- : Ok
  149 
  150         # NO_PROX  -- HTTPS Basic Auth  --  httplib  -- : OK
  151         # NO_PROX  -- HTTPS  --             httplib  -- : OK
  152         # NO_PROX  -- HTTP Basic Auth  --   httplib  -- : OK
  153         # NO_PROX  -- HTTP  --              httplib  -- : OK
  154 
  155         # PROX  --    HTTPS Basic Auth  -- httplib  -- : OK
  156         # PROX  --    HTTPS  --            httplib  -- : OK
  157         # PROX  --    HTTP Basic Auth  --  httplib  -- : Fail
  158         # PROX  --    HTTP  --             httplib  -- : Fail
  159 
  160         basic_auth = False
  161         https = False
  162 
  163         # there might be the basic authentication in the request url
  164         # like http://user:passw@hostname:port/path
  165         if password is None and username is None:
  166             parsed_url = urlparse(url)
  167             if "@" in parsed_url[1]:
  168                 puser, _server = parsed_url[1].split('@')
  169                 username, password = puser.split(':')
  170 
  171         if username and password is not None:
  172             basic_auth = True
  173 
  174         if url.startswith('https:'):
  175             https = True
  176 
  177         preferred_lib = self.config.get(
  178             'PREFERRED_HTTPLIB', 'requests').strip().lower()
  179 
  180         if preferred_lib and preferred_lib in ['requests', 'urllib', 'httplib']:
  181             lib = preferred_lib
  182         else:
  183             lib = 'requests'
  184 
  185         if lib == 'urllib':
  186             if basic_auth == True and https == True:
  187                 lib = 'httplib'
  188 
  189         # ------------------------------------------------------------------ --
  190 
  191         # setup method call for http request
  192 
  193         http_lib = getattr(self, lib + '_request')
  194 
  195         try:
  196             ret = http_lib(url, parameter, username, password, method)
  197             return ret
  198         except Exception as exx:
  199             log.warning("Failed to access the HTTP SMS Service with %s: %r"
  200                         % (lib, exx))
  201             raise exx
  202 
  203         return False
  204 
  205 
  206     def getParameters(self, message, phone):
  207 
  208         urldata = {}
  209 
  210         # transfer the phone key
  211         phoneKey = self.config.get('SMS_PHONENUMBER_KEY', "phone")
  212         urldata[phoneKey] = phone
  213         log.debug("[getParameters] urldata: %s" % urldata)
  214 
  215         # transfer the sms key
  216         messageKey = self.config.get('SMS_TEXT_KEY', "sms")
  217         urldata[messageKey] = message
  218         log.debug("[getParameters] urldata: %s" % urldata)
  219 
  220         params = self.config.get('PARAMETER', {})
  221         urldata.update(params)
  222 
  223         log.debug("[getParameters] urldata: %s" % urldata)
  224 
  225         return urldata
  226 
  227     def _check_success(self, reply):
  228         '''
  229         Check the success according to the reply
  230 
  231         if RETURN_SUCCESS_REGEX, RETURN_SUCCES,
  232             RETURN_FAIL_REGEX or RETURN_FAIL is defined
  233         :param reply: the reply from the http request
  234 
  235         :return: True or raises an Exception
  236         '''
  237 
  238         log.debug("[_check_success] entering with config %s" % self.config)
  239         log.debug("[_check_success] entering with reply %s" % reply)
  240 
  241         if "RETURN_SUCCESS_REGEX" in self.config:
  242             ret = re.search(self.config["RETURN_SUCCESS_REGEX"], reply)
  243             if ret is not None:
  244                 log.debug("[_check_success] sending SMS success")
  245             else:
  246                 log.warning("[_check_success] failed to send SMS. "
  247                             "Reply does not match the RETURN_SUCCESS_REGEX "
  248                             "definition")
  249                 raise Exception("We received a none success reply from the "
  250                                 "SMS Gateway.")
  251 
  252         elif "RETURN_FAIL_REGEX" in self.config:
  253             ret = re.search(self.config["RETURN_FAIL_REGEX"], reply)
  254             if ret is not None:
  255                 log.warning("[_check_success] sending SMS fail")
  256                 raise Exception("We received a predefined error from the "
  257                                 "SMS Gateway.")
  258             else:
  259                 log.debug("[_check_success] sending sms success full. "
  260                           "The reply does not match the RETURN_FAIL_REGEX "
  261                           "definition")
  262 
  263         elif "RETURN_SUCCESS" in self.config:
  264             success = self.config.get("RETURN_SUCCESS")
  265             log.debug("[_check_success] success: %s" % success)
  266             if reply[:len(success)] == success:
  267                 log.debug("[_check_success] sending SMS success")
  268             else:
  269                 log.warning("[_check_success] failed to send SMS. Reply does "
  270                             "not match the RETURN_SUCCESS definition")
  271                 raise Exception("We received a none success reply from the "
  272                                 "SMS Gateway.")
  273 
  274         elif "RETURN_FAIL" in self.config:
  275             fail = self.config.get("RETURN_FAIL")
  276             log.debug("[_check_success] fail: %s" % fail)
  277             if reply[:len(fail)] == fail:
  278                 log.warning("[_check_success] sending SMS fail")
  279                 raise Exception("We received a predefined error from the "
  280                                 "SMS Gateway.")
  281             else:
  282                 log.debug("[_check_success] sending sms success full. "
  283                           "The reply does not match the RETURN_FAIL "
  284                           "definition")
  285         return True
  286 
  287 
  288     def requests_request(self, url, parameter,
  289                          username=None, password=None, method='GET'):
  290 
  291         try:
  292             pparams = {}
  293 
  294             if 'timeout' in self.config and self.config['timeout']:
  295                 pparams['timeout'] = parse_timeout(self.config['timeout'])
  296 
  297             if 'PROXY' in self.config and self.config['PROXY']:
  298 
  299                 if isinstance(self.config['PROXY'], (str, unicode)):
  300                     proxy_defintion = {
  301                         "http": self.config['PROXY'],
  302                         "https": self.config['PROXY']
  303                         }
  304 
  305                 elif isinstance(self.config['PROXY'], dict):
  306                     proxy_defintion = self.config['PROXY']
  307 
  308                 pparams['proxies'] = proxy_defintion
  309 
  310             if username and password is not None:
  311                 auth = None
  312                 auth_type = self.config.get(
  313                     'AUTH_TYPE', 'basic').lower().strip()
  314 
  315                 if auth_type == 'basic':
  316                     auth = HTTPBasicAuth(username, password)
  317 
  318                 if auth_type == 'digest':
  319                     auth = HTTPDigestAuth(username, password)
  320 
  321                 if auth:
  322                     pparams['auth'] = auth
  323 
  324             # -------------------------------------------------------------- --
  325 
  326             # fianly execute the request
  327 
  328             if method == 'GET':
  329                 response = requests.get(url, params=parameter, **pparams)
  330             else:
  331                 response = requests.post(url, data=parameter, **pparams)
  332 
  333             reply = response.text
  334             # some providers like clickatell have no response.status!
  335             log.debug("HttpSMSProvider >>%s...%s<<", reply[:20], reply[-20:])
  336             ret = self._check_success(reply)
  337 
  338         except (requests.exceptions.ConnectTimeout,
  339                 requests.exceptions.ConnectionError,
  340                 requests.exceptions.Timeout,
  341                 requests.exceptions.ReadTimeout,
  342                 requests.exceptions.TooManyRedirects) as exc:
  343 
  344             log.exception("HttpSMSProvider timed out")
  345             raise ProviderNotAvailable("Failed to send SMS - timed out %r" % exc)
  346 
  347         except Exception as exc:
  348             log.error("HttpSMSProvider %r" % exc)
  349             raise Exception("Failed to send SMS. %r" % exc)
  350 
  351         return ret
  352 
  353     def httplib_request(self, url, parameter,
  354                         username=None, password=None, method='GET'):
  355         """
  356         build the urllib request and check the response for success or fail
  357 
  358         :param url: target url
  359         :param parameter: additonal parameter to append to the url request
  360         :param username: basic authentication with username (optional)
  361         :param password: basic authentication with password (optional)
  362         :param method: run an GET or POST request
  363 
  364         :return: False or True
  365         """
  366 
  367         #httplib2.debuglevel = 4
  368 
  369         ret = False
  370         http_params = {}
  371         headers = {}
  372 
  373         log.debug("Do the request to %s with %s" % (url, parameter))
  374 
  375         if 'PROXY' in self.config:
  376             proxy_url = None
  377 
  378             proxy = self.config['PROXY']
  379 
  380             if isinstance(proxy, dict):
  381                 if url.startswith('https') and 'https' in proxy:
  382                     proxy_url = proxy['https']
  383                 elif url.startswith('http') and 'http' in proxy:
  384                     proxy_url = proxy['http']
  385 
  386             elif isinstance(proxy, (str, unicode)):
  387                 proxy_url = proxy
  388 
  389             if proxy_url:
  390                 http_params['proxy_info'] = http2lib_get_proxy_info(proxy_url)
  391 
  392         if 'timeout' in self.config:
  393 
  394             parsed_timeout = parse_timeout(self.config['timeout'])
  395 
  396             if isinstance(parsed_timeout, tuple):
  397                 timeout = int(parsed_timeout[0])
  398             else:
  399                 timeout = int(parsed_timeout)
  400 
  401             http_params['timeout'] = timeout
  402 
  403         http_params["disable_ssl_certificate_validation"] = True
  404 
  405         try:
  406             # test if httplib is compiled with ssl - will raise a TypeError
  407             # TypeError: __init__() got an unexpected keyword argument
  408             # 'disable_ssl_certificate_validation'
  409             http = httplib2.Http(**http_params)
  410 
  411         except TypeError as exx:
  412             log.warning("httplib2 'disable_ssl_certificate_validation' "
  413                         "attribute error: %r" % exx)
  414             # so we remove the ssl param from the arguments
  415             del http_params["disable_ssl_certificate_validation"]
  416             # and retry
  417             http = httplib2.Http(**http_params)
  418 
  419         # for backward compatibility we have to support url with the format
  420         # http://user:pass@server:port/path
  421         # so we extract the url_user and the url_pass and use them if
  422         # not overruled by the explicit parameters username and password
  423         url_user = None
  424         url_pass = None
  425         parsed_url = urlparse(url)
  426 
  427         if "@" in parsed_url[1]:
  428             puser, server = parsed_url[1].split('@')
  429             url_user, url_pass = puser.split(':')
  430 
  431             # now rewrite the url to not contain the user anymore
  432             url = url.replace(parsed_url[1], server)
  433 
  434         if username and password is not None:
  435             http.add_credentials(name=username, password=password)
  436         elif url_user and url_pass is not None:
  437             http.add_credentials(name=url_user, password=url_pass)
  438 
  439         #! the parameters to the httplib / proxy must be of type str()
  440         encoded_params = ''
  441         if parameter is not None and len(parameter) > 0:
  442             encoded_params = self.urlencode(parameter)
  443 
  444         call_url = str(url)
  445 
  446         try:
  447             # do a GET request - which has no body but all params
  448             # added to the url
  449             if method == 'GET':
  450                 call_data = None
  451                 if len(encoded_params) > 0:
  452                     # extend the url with our parameters
  453                     call_url = "%s?%s" % (call_url, encoded_params)
  454 
  455             # or do a POST request - the more secure default and fallback
  456             else:
  457                 method = 'POST'
  458                 headers["Content-type"] = "application/x-www-form-urlencoded"
  459                 call_data = encoded_params
  460 
  461             # using httplib2:
  462             # the proxy spec and url + enc. parameters must be of
  463             # type string str() - otherwise the following error will occur:
  464             # : GeneralProxyError: (5, 'bad input') :
  465 
  466             (_resp, reply) = http.request(call_url, method=method,
  467                                           headers=headers,
  468                                           body=call_data)
  469 
  470             # some providers like clickatell have no response.status!
  471             log.debug("HttpSMSProvider >>%s...%s<<", reply[:20], reply[-20:])
  472             ret = self._check_success(reply)
  473 
  474         except (httplib2.HttpLib2Error, socket.error) as exc:
  475             raise ProviderNotAvailable(
  476                         "Failed to send SMS - timed out %r" % exc)
  477 
  478         except Exception as exc:
  479             log.exception("Failed to send SMS")
  480             raise ProviderNotAvailable("Failed to send SMS. %r" % exc)
  481 
  482         return ret
  483 
  484     def urllib_request(self, url, parameter,
  485                        username=None, password=None, method='GET'):
  486         """
  487         build the urllib request and check the response for success or fail
  488 
  489         :param url: target url
  490         :param parameter: additonal parameter to append to the url request
  491         :param username: basic authentication with username (optional)
  492         :param password: basic authentication with password (optional)
  493         :param method: run an GET or POST request
  494 
  495         :return: False or True
  496         """
  497         try:
  498             headers = {}
  499             handlers = []
  500             pparams = {}
  501 
  502             if 'PROXY' in self.config and self.config['PROXY']:
  503 
  504                 proxy_handler = None
  505 
  506                 if isinstance(self.config['PROXY'], (str, unicode)):
  507                     # for simplicity we set both protocols
  508                     proxy_handler = urllib2.ProxyHandler({
  509                         "http": self.config['PROXY'],
  510                         "https": self.config['PROXY']}
  511                     )
  512 
  513                 elif isinstance(self.config['PROXY'], dict):
  514                     proxy_defintion = self.config['PROXY']
  515                     proxy_handler = urllib2.ProxyHandler(proxy_defintion)
  516 
  517                 if proxy_handler:
  518                     handlers.append(proxy_handler)
  519                     log.debug("using Proxy: %r" % self.config['PROXY'])
  520 
  521             if username and password is not None:
  522 
  523                 password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
  524                 password_mgr.add_password(None, url, username, password)
  525                 auth_handler = urllib2.HTTPBasicAuthHandler(password_mgr)
  526                 handlers.append(auth_handler)
  527 
  528             timeout = None
  529             if 'timeout' in self.config and self.config['timeout']:
  530                 timeout = parse_timeout(self.config['timeout'])
  531 
  532             opener = urllib2.build_opener(*handlers)
  533             urllib2.install_opener(opener)
  534 
  535             full_url = str(url)
  536 
  537             encoded_params = None
  538             if parameter is not None and len(parameter) > 0:
  539                 encoded_params = self.urlencode(parameter)
  540 
  541             if method == 'GET':
  542                 c_data = None
  543                 if encoded_params:
  544                     full_url = "%s?%s" % (url, encoded_params)
  545             else:
  546                 headers["Content-type"] = "application/x-www-form-urlencoded"
  547                 c_data = encoded_params
  548 
  549             requ = urllib2.Request(full_url, data=c_data, headers=headers)
  550             if username and password is not None:
  551                 base64string = base64.encodestring(
  552                     '%s:%s' % (username, password)).replace('\n', '')
  553                 requ.add_header("Authorization", "Basic %s" % base64string)
  554 
  555             response = urllib2.urlopen(requ, timeout=timeout)
  556             reply = response.read()
  557 
  558             # some providers like clickatell have no response.status!
  559             log.debug("HttpSMSProvider >>%s...%s<<", reply[:20], reply[-20:])
  560             ret = self._check_success(reply)
  561 
  562         except (urllib2.URLError, socket.timeout) as exc:
  563             log.exception("HttpSMSProvider urllib timeout exception")
  564             raise ProviderNotAvailable("Failed to send SMS -timed out %r" % exc)
  565 
  566         except Exception as exc:
  567             log.exception("HttpSMSProvider urllib")
  568             raise Exception("Failed to send SMS. %r" % exc)
  569 
  570         return ret
  571 
  572     @staticmethod
  573     def urlencode(parameter):
  574         """
  575         helper method:
  576           urllib.urlencode does by default url_quote, which converts ' ' spaces
  577           into '+' symbol, which is not understood by all HTTPSMSProviders
  578           This helper uses urllibquote to build the encoded parameter string
  579 
  580         :param parameter: dictionary
  581         :return: urlencoded string of type str() as unicode is not supported
  582 
  583         """
  584         encoded_params = ''
  585         if type(parameter) == dict:
  586             params = []
  587             for key, value in parameter.items():
  588                 key = unicode(key).encode('utf-8')
  589                 if value:
  590                     value = unicode(value).encode('utf-8')
  591                     params.append("%s=%s" % (key, urllib.quote(value)))
  592                 else:
  593                     params.append("%s" % key)
  594             encoded_params = "&".join(params)
  595         return str(encoded_params)
  596 
  597     def loadConfig(self, configDict):
  598 
  599         if not configDict:
  600             raise Exception('missing configuration')
  601 
  602         self.config = configDict
  603 
  604 ##eof##########################################################################