Ignore:
Timestamp:
7 Jun 2025, 02:06:59 (3 days ago)
Author:
uli
Message:

Support Google Recaptcha v3.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • main/waeup.kofa/branches/uli-recaptcha/src/waeup/kofa/browser/captcha.py

    r15012 r18083  
    2121"""
    2222import grok
     23import decimal
    2324import json
     25import logging
    2426import urllib
    2527import urllib2
     
    3335    ICaptchaRequest, ICaptchaResponse, ICaptcha, ICaptchaConfig,
    3436    ICaptchaManager)
    35 from waeup.kofa.interfaces import IUniversity
     37from waeup.kofa.interfaces import IUniversity, IReCaptchaConfig
    3638
    3739#
     
    103105    grok.implements(ICaptcha)
    104106
    105     def verify(self, request):
     107    def verify(self, request, action="submit"):
    106108        return CaptchaResponse(True, None)
    107109
    108     def display(self, error_code=None):
     110    def display(self, error_code=None, action="submit"):
    109111        return u''
    110112
     
    139141    chal_field = 'challenge'
    140142
    141     def verify(self, request):
     143    def verify(self, request, action='submit'):
    142144        """Verify that a solution sent equals the challenge.
    143145        """
     
    149151        return CaptchaResponse(is_valid=False)
    150152
    151     def display(self, error_code=None):
     153    def display(self, error_code=None, action='submit'):
    152154        """Display challenge and input field for solution as HTML.
    153155        """
     
    164166
    165167##
    166 ## ReCaptcha (v2)
     168## ReCaptcha (v3)
    167169##
    168170class ReCaptcha(StaticCaptcha):
     
    170172
    171173    This is the Kofa implementation to support captchas as provided by
    172     http://www.google.com/recaptcha (v2).
     174    http://www.google.com/recaptcha (v3).
    173175
    174176    ReCaptcha is widely used and adopted in web applications. See the
     
    226228        - Google will track all activity of our clients
    227229    """
    228 
    229230    grok.implements(ICaptcha)
    230231
     
    240241    VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'
    241242
    242     def verify(self, request):
     243    @property
     244    def sitekey(self):
     245        return self.get_config()['public_key']
     246
     247    @property
     248    def logger(self):
     249        # if there is no site, we are most probably in a testing env
     250        return getattr(grok.getSite(), "logger", logging.getLogger("dummy"))
     251
     252    @property
     253    def ob_class(self):
     254        return self.__implemented__.__name__.replace('waeup.kofa.', '')
     255
     256    def get_config(self):
     257        if not getattr(self, "config", None):
     258            self.config = queryUtility(IReCaptchaConfig, default={
     259                'public_key': self.PUBLIC_KEY,
     260                'private_key': self.PRIVATE_KEY,
     261                'hostname': u'localhost',
     262                'min_score': decimal.Decimal("0.5")
     263                })
     264        self.logger.debug(u'%s : getting ReCaptchaConfig: %s' % (
     265            self.ob_class, self.config))
     266        return self.config
     267
     268    def verify(self, request, action='submit'):
    243269        """Grab challenge/solution from HTTP request and verify it.
    244270
     
    249275        Returns a :class:`CaptchaResponse` indicating that the
    250276        verification failed or succeeded.
    251         """
     277
     278        Possible error codes (official Google codes):
     279            missing-input-secret    The secret parameter is missing.
     280            invalid-input-secret    The secret parameter is invalid or malformed.
     281            missing-input-response  The response parameter is missing.
     282            invalid-input-response  The response parameter is invalid or malformed.
     283            bad-request     The request is invalid or malformed.
     284            timeout-or-duplicate    The response is no longer valid: either is too old or has been used previously.
     285        We also have our own ones:
     286            invalid-action          The passed action does not match the one we expected
     287            insufficient-score      The returned score is too low; talking to a bot
     288            invalid-hostname        The captcha was solved on a different domain.
     289        """
     290        remote_ip = request.get('HTTP_X_FORWARDED_FOR', '127.0.0.1')
    252291        form = getattr(request, 'form', {})
    253292        token = form.get(self.token_field, None)
     293        oc = self.ob_class
     294        self.logger.debug(u'%s: get token from form: %s' % (oc, token))
    254295        if not token:
    255296            # on first-time display, we won't get a token
    256297            return CaptchaResponse(is_valid=False)
     298        conf = self.get_config()
    257299        params = urllib.urlencode(
    258300            {
    259                 'secret': self.PRIVATE_KEY,
     301                'secret': conf['private_key'],
    260302                'response': token,
    261                 'remoteip': '127.0.0.1',
     303                'remoteip': remote_ip,
    262304                })
    263         request = urllib2.Request(
     305        self.logger.debug(u'%s: send validation request to Google' % oc)
     306        v_request = urllib2.Request(
    264307            url = self.VERIFY_URL,
    265308            data = params,
     
    269312                }
    270313            )
    271         conn = urllib2.urlopen(request)
    272         ret_vals = json.loads(conn.read())
    273         conn.close()
     314        try:
     315            conn = urllib2.urlopen(v_request)
     316            ret_vals = json.loads(conn.read())
     317        except:
     318            conn.close()
     319            return CaptchaResponse(is_valid=False, error_code="could-not-reach-verification-server")
     320        self.logger.debug(u'%s: got answer from Google: %s' % (oc, ret_vals))
     321        err_codes = ret_vals.get('error-codes', [])
    274322        if ret_vals.get('success', False) is True:
    275             return CaptchaResponse(is_valid=True)
     323            r_action = ret_vals.get('action', None)
     324            score = ret_vals.get('score', decimal.Decimal("0.0"))
     325            hostname = ret_vals.get('hostname', '')
     326            if r_action != action:
     327                err_codes.append('invalid-action')
     328                self.logger.warn(
     329                    u'%s: invalid action code: %s (expected: %s)' %
     330                    (oc, r_action, action))
     331            elif score < conf['min_score']:
     332                err_codes.append('insufficent-score')
     333                self.logger.debug(u'%s: insufficient score: %s' % (oc, score))
     334            elif not hostname.endswith(conf['hostname']):
     335                err_codes.append('invalid-hostname')
     336                self.logger.warn(
     337                    u'%s: cought captcha from illegal hostname: %s' % (
     338                        oc, hostname))
     339            else:
     340                self.logger.debug(u'%s: captcha accepted' % oc)
     341                return CaptchaResponse(is_valid=True)
     342        self.logger.info(u'%s: captcha invalid/refused, form-data: %s' % (
     343            oc, form))
    276344        return CaptchaResponse(
    277             is_valid=False, error_code="%s" % ret_vals['error-codes'])
    278 
    279     def display(self, error_code=None):
     345            is_valid=False, error_code="%s" % err_codes)
     346
     347    def display(self, error_code=None, action='submit'):
    280348        """Display captcha widget snippet.
    281349
     
    290358        form).
    291359        """
    292         error_param = ''
     360        html = u''
     361        # TODO: display errors as warnings
    293362        if error_code:
    294             error_param = '&error=%s' % error_code
    295 
    296         html = (
    297             u'<script type="text/javascript" '
    298             u'src="%(ApiServer)s" async defer>'
     363            html = u"<p class='warning'>Captcha-Error: Are you a bot?</p><!-- %s -->" % error_code
     364        html += (
     365            # Main event handler, triggered when a submit button is clicked.
     366            # Requests the captcha token for the form and also copies name and
     367            # value of the clicked button to a hidden field `fakebtn` defined
     368            # below.
     369            u'<script class="kofa-script-recaptcha-v3" '
     370            u' src="https://www.google.com/recaptcha/api.js?render=%(sitekey)s">'
     371            u'</script>\n'
     372            u'<script class="kofa-script-recaptcha-v3"> '
     373            u'  function onClickRecaptcha(e) {'
     374            u'    e.preventDefault();'
     375            u'    grecaptcha.ready(function() {'
     376            u'      grecaptcha.execute("%(sitekey)s", {action: "%(action)s"}).then('
     377            u'        function(token) {'
     378            u'          document.getElementById("g-recaptcha-token").value = token;'
     379            u'          var fakebtn = document.getElementById("g-recaptcha-fakebtn");'
     380            u'          fakebtn.name = e.target.name;'
     381            u'          fakebtn.value = e.target.value;'
     382            u'          document.querySelector("form").submit();});});}'
     383            u'</script>\n'
     384        ) % {
     385            'sitekey': self.sitekey,
     386            'action': action,
     387        }
     388        html += (
     389            # Form fields we use to (1) embed the captcha token and (2) a field
     390            # to embed the name and value of the submit button/input clicked.
     391            u'<input id="g-recaptcha-token" type="hidden" name="%(token_field)s"/>\n'
     392            u'<input id="g-recaptcha-fakebtn" type="hidden"/>\n'
     393            ) % {'token_field': self.token_field}
     394        html += (
     395            # install event handler `onClickRecaptcha` defined above, but only
     396            # after document (HTML) loaded.
     397            u'<script> window.addEventListener("load", function () {'
     398            u'  if (typeof onClickRecaptcha === "function") {'
     399            u'document.querySelector("*[type=\'submit\']").addEventListener('
     400            u'"click", onClickRecaptcha);}})'
    299401            u'</script>'
    300             u'<div class="g-recaptcha" data-sitekey="%(SiteKey)s"></div>' % {
    301                 'ApiServer': "https://www.google.com/recaptcha/api.js",
    302                 'SiteKey': self.PUBLIC_KEY,
    303                 }
    304         )
     402            )
    305403        return html
    306404
Note: See TracChangeset for help on using the changeset viewer.