This file is indexed.

/usr/lib/python2.7/dist-packages/sagenb/notebook/challenge.py is in python-sagenb 1.0.1+ds1-2.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
# -*- coding: utf-8 -*
r"""
Notebook Registration Challenges

This module includes support for challenge-response tests posed to
users registering for new Sage notebook accounts.  These \ **C**\
ompletely \ **A**\ utomated \ **P**\ ublic \ **T**\ uring tests to
tell \ **C**\ omputers and \ **H**\ umans \ **A**\ part, or CAPTCHAs,
may be simple math questions, requests for special registration codes,
or reCAPTCHAs_.

.. _reCAPTCHAs: http://recaptcha.net/

AUTHORS:

- reCAPTCHA_ is written by Ben Maurer and maintained by Josh
  Bronson. It is licensed under a MIT/X11 license.  The reCAPTCHA
  challenge implemented in :class:`reCAPTCHAChallenge` is adapted from
  `this Python API`_, which is also available here_.

.. _reCAPTCHA: http://recaptcha.net/
.. _this Python API: http://pypi.python.org/pypi/recaptcha-client
.. _here: http://code.google.com/p/recaptcha

"""

import os
import random
import re

from six.moves.urllib.parse import urlencode
from six.moves.urllib.request import urlopen, Request

from sagenb.notebook.template import template
from flask_babel import gettext, lazy_gettext
_ = lazy_gettext

class ChallengeResponse(object):
    """
    A simple challenge response class that indicates whether a
    response is empty, correct, or incorrect, and, if it's incorrect,
    includes an optional error code.
    """
    def __init__(self, is_valid, error_code = None):
        """
        Instantiates a challenge response.

        INPUT:

        - ``is_valid`` - a boolean or None; whether there response is
          valid

        - ``error_code`` - a string (default: None); an optional error
          code if ``is_valid`` is False

        TESTS::

            sage: from sagenb.notebook.challenge import ChallengeResponse
            sage: resp = ChallengeResponse(False, 'Wrong! Please try again.')
            sage: resp.is_valid
            False
            sage: resp.error_code
            'Wrong! Please try again.'

        """
        self.is_valid = is_valid
        self.error_code = error_code


class AbstractChallenge(object):
    """
    An abstract class with a suggested common interface for specific
    challenge-response schemes.
    """
    def __init__(self, conf, **kwargs):
        """
        Instantiates an abstract challenge.

        INPUT:

        - ``conf`` - a :class:`ServerConfiguration`; a notebook server
          configuration instance

        - ``kwargs`` - a dictionary of keyword arguments

        TESTS::

            sage: from sagenb.notebook.challenge import AbstractChallenge
            sage: tmp = tmp_dir(ext='.sagenb')
            sage: import sagenb.notebook.notebook as n
            sage: nb = n.Notebook(tmp)
            sage: chal = AbstractChallenge(nb.conf())

        """
        pass

    def html(self, **kwargs):
        """
        Returns HTML for the challenge, e.g., to insert into a new
        account registration page.

        INPUT:

        - ``kwargs`` - a dictionary of keywords arguments

        OUTPUT:

        - a string; HTML form representation of the challenge,
          including a field for the response, supporting hidden
          fields, JavaScript code, etc.

        TESTS::

            sage: from sagenb.notebook.challenge import AbstractChallenge
            sage: tmp = tmp_dir(ext='.sagenb')
            sage: import sagenb.notebook.notebook as n
            sage: nb = n.Notebook(tmp)
            sage: chal = AbstractChallenge(nb.conf())
            sage: chal.html()
            Traceback (most recent call last):
            ...
            NotImplementedError
        """
        raise NotImplementedError

    def is_valid_response(self, **kwargs):
        """
        Returns the status of a challenge response.

        INPUT:

        - ``kwargs`` - a dictionary of keyword arguments

        OUTPUT:

        - a :class:`ChallengeResponse` instance

        TESTS::

            sage: from sagenb.notebook.challenge import AbstractChallenge
            sage: tmp = tmp_dir(ext='.sagenb')
            sage: import sagenb.notebook.notebook as n
            sage: nb = n.Notebook(tmp)
            sage: chal = AbstractChallenge(nb.conf())
            sage: chal.is_valid_response()
            Traceback (most recent call last):
            ...
            NotImplementedError
        """
        raise NotImplementedError


class NotConfiguredChallenge(AbstractChallenge):
    """
    A fallback challenge used when an administrator has not configured
    a specific method.
    """
    def html(self, **kwargs):
        """
        Returns a suggestion to inform the Notebook server's
        administrator about the misconfigured challenge.

        INPUT:

        - ``conf`` - a :class:`ServerConfiguration`; an instance of the
          server's configuration

        - ``kwargs`` - a dictionary of keyword arguments

        OUTPUT:

        - a string

        TESTS::

            sage: from sagenb.notebook.challenge import NotConfiguredChallenge
            sage: tmp = tmp_dir(ext='.sagenb')
            sage: import sagenb.notebook.notebook as n
            sage: nb = n.Notebook(tmp)
            sage: chal = NotConfiguredChallenge(nb.conf())
            sage: print(chal.html())
            Please ask the server administrator to configure a challenge!

        """
        return _("Please ask the server administrator to configure a challenge!")

    def is_valid_response(self, **kwargs):
        """
        Always reports a failed response, for the sake of security.

        INPUT:

        - ``kwargs`` - a dictionary of keyword arguments

        OUTPUT:

        - a :class:`ChallengeResponse` instance
 
       TESTS::

            sage: from sagenb.notebook.challenge import NotConfiguredChallenge
            sage: tmp = tmp_dir(ext='.sagenb')
            sage: import sagenb.notebook.notebook as n
            sage: nb = n.Notebook(tmp)
            sage: chal = NotConfiguredChallenge(nb.conf())
            sage: chal.is_valid_response().is_valid
            False
            sage: chal.is_valid_response().error_code
            ''

        """
        return ChallengeResponse(False, '')


# HTML template for :class:`SimpleChallenge`.
SIMPLE_TEMPLATE = u"""<p>%(question)s</p>
<input type="text" id="simple_response_field" name="simple_response_field" class="entry" tabindex="5" />
<input type="hidden" value="%(untranslated_question)s" id="simple_challenge_field" name="simple_challenge_field" class="entry" />
"""

old_tr = _
_ = lambda s: s

# A set of sample questions for :class:`SimpleChallenge`.
QUESTIONS = {
    _('Is pi > e?') : _('y|yes'),
    _('What is 3 times 8?') : _('24|twenty-four'),
    _('What is 2 plus 3?') : _('5|five'),
    _('How many bits are in one byte?') : _('8|eight'),
    _('What is the largest prime factor of 15?') : _('5|five'),
#    'What is the smallest perfect number?' : r'6|six',
#    'What is our class registration code?' : r'XYZ123',
#    'What is the smallest integer expressible as the sum of two positive cubes in two distinct ways?' : r'1729',
#    'How many permutations of ABCD agree with it in no position? For example, BDCA matches ABCD only in position 3.' : r'9|nine',
}

# QUESTIONS is now dict of str->str
#let's make answers lazy translated:
for key in QUESTIONS: QUESTIONS[key] = old_tr(QUESTIONS[key])

_ = old_tr
del old_tr

def agree(response, answer):
    """
    Returns whether a challenge response agrees with the answer.

    INPUT:

    - ``response`` - a string; the user's response to a posed challenge

    - ``answer`` - a string; the challenge's right answer as a regular
      expression

    OUTPUT:

    - a boolean; whether the response agrees with the answer

    TESTS::

        sage: from sagenb.notebook.challenge import agree
        sage: agree('0', r'0|zero')
        True
        sage: agree('eighty', r'8|eight')
        False

    """
    response = re.sub(r'\s+', ' ', response.strip())
    m = re.search(r'^(' + answer + r')$', response, re.IGNORECASE)
    if m:
        return True
    else:
        return False


class SimpleChallenge(AbstractChallenge):
    """
    A simple question and answer challenge.
    """
    def html(self, **kwargs):
        """
        Returns a HTML form posing a randomly chosen question.

        INPUT:

        - ``kwargs`` - a dictionary of keyword arguments

        OUTPUT:

        - a string; the HTML form

        TESTS::

            sage: from sagenb.notebook.challenge import SimpleChallenge
            sage: tmp = tmp_dir(ext='.sagenb')
            sage: import sagenb.notebook.notebook as n
            sage: nb = n.Notebook(tmp)
            sage: chal = SimpleChallenge(nb.conf())
            sage: chal.html() # random
            '...What is the largest prime factor of 1001?...'
            
        """
        question = random.choice([q for q in QUESTIONS])
        return SIMPLE_TEMPLATE % { 'question' : gettext(question),
                                   'untranslated_question': question }

    def is_valid_response(self, req_args = {}, **kwargs):
        """
        Returns the status of a user's answer to the challenge
        question.

        INPUT:

        - ``req_args`` - a string:list dictionary; the arguments of
          the remote client's HTTP POST request

        - ``kwargs`` - a dictionary of extra keyword arguments

        OUTPUT:

        - a :class:`ChallengeResponse` instance

        TESTS::

            sage: from sagenb.notebook.challenge import SimpleChallenge
            sage: tmp = tmp_dir(ext='.sagenb')
            sage: import sagenb.notebook.notebook as n
            sage: nb = n.Notebook(tmp)
            sage: chal = SimpleChallenge(nb.conf())
            sage: req = {}
            sage: chal.is_valid_response(req).is_valid
            sage: chal.is_valid_response(req).error_code
            ''
            sage: from sagenb.notebook.challenge import QUESTIONS
            sage: ques, ans = sorted(QUESTIONS.items())[0]
            sage: ans = ans.split('|')[0]
            sage: print(ques)
            How many bits are in one byte?
            sage: print(ans)
            8
            sage: req['simple_response_field'] = ans
            sage: chal.is_valid_response(req).is_valid
            False
            sage: chal.is_valid_response(req).error_code
            ''
            sage: req['simple_challenge_field'] = ques
            sage: chal.is_valid_response(req).is_valid
            True
            sage: chal.is_valid_response(req).error_code
            ''

        """
        response_field = req_args.get('simple_response_field', None)
        if not (response_field and len(response_field)):
            return ChallengeResponse(None, '')

        challenge_field = req_args.get('simple_challenge_field', None)
        if not (challenge_field and len(challenge_field)):
            return ChallengeResponse(False, '')
        if agree(response_field, gettext(QUESTIONS[challenge_field])):
            return ChallengeResponse(True, '')
        else:
            return ChallengeResponse(False, '')


RECAPTCHA_SERVER = "http://api.recaptcha.net"
RECAPTCHA_SSL_SERVER = "https://api-secure.recaptcha.net"
RECAPTCHA_VERIFY_SERVER = "api-verify.recaptcha.net"

class reCAPTCHAChallenge(AbstractChallenge):
    """
    A reCAPTCHA_ challenge adapted from `this Python API`_, also
    hosted here_, written by Ben Maurer and maintained by Josh
    Bronson.

    .. _reCAPTCHA: http://recaptcha.net/
    .. _this Python API: http://pypi.python.org/pypi/recaptcha-client
    .. _here: http://code.google.com/p/recaptcha
    """
    def __init__(self, conf, remote_ip = '', is_secure = False, lang = 'en',
                 **kwargs):
        """
        Instantiates a reCAPTCHA challenge.

        INPUT:

        - ``conf`` - a :class:`ServerConfiguration`; an instance of the
          notebook server's configuration

        - ``remote_ip`` - a string (default: ''); the user's IP
          address, **required** by reCAPTCHA

        - ``is_secure`` - a boolean (default: False); whether the
          user's connection is secure, e.g., over SSL

        - ``lang`` - a string (default 'en'); the language used for
          the reCAPTCHA interface.  As of October 2009, the
          pre-defined choices are 'en', 'nl', 'fr', 'de', 'pt', 'ru',
          'es', and 'tr'

        - ``kwargs`` - a dictionary of extra keyword arguments

        ATTRIBUTES:

        - ``public_key`` - a string; a **site-specific** public
          key obtained at the `reCAPTCHA site`_.

        - ``private_key`` - a string; a **site-specific** private
          key obtained at the `reCAPTCHA site`_.

        .. _reCAPTCHA site: http://recaptcha.net/whyrecaptcha.html

        Currently, the keys are read from ``conf``'s
        ``recaptcha_public_key`` and ``recaptcha_private_key``
        settings.

        TESTS::

            sage: from sagenb.notebook.challenge import reCAPTCHAChallenge
            sage: tmp = tmp_dir(ext='.sagenb')
            sage: import sagenb.notebook.notebook as n
            sage: nb = n.Notebook(tmp)
            sage: chal = reCAPTCHAChallenge(nb.conf(), remote_ip = 'localhost')

        """
        self.remote_ip = remote_ip
        if is_secure:
            self.api_server = RECAPTCHA_SSL_SERVER
        else:
            self.api_server = RECAPTCHA_SERVER

        self.lang = lang
        self.public_key = conf['recaptcha_public_key']
        self.private_key = conf['recaptcha_private_key']

    def html(self, error_code = None, **kwargs):
        """
        Returns HTML and JavaScript for a reCAPTCHA challenge and
        response field.

        INPUT:

        - ``error_code`` - a string (default: None); an optional error
          code to embed in the HTML, giving feedback about the user's
          *previous* response
        
        - ``kwargs`` - a dictionary of extra keyword arguments

        OUTPUT:
        
        - a string; HTML and JavaScript to render the reCAPTCHA
          challenge

        TESTS::

            sage: from sagenb.flask_version import base # random output -- depends on warnings issued by other sage packages
            sage: app = base.create_app(tmp_dir(ext='.sagenb'))
            sage: ctx = app.app_context()
            sage: ctx.push()
            sage: nb = base.notebook
            sage: from sagenb.notebook.challenge import reCAPTCHAChallenge
            sage: chal = reCAPTCHAChallenge(nb.conf(), remote_ip = 'localhost')
            sage: chal.html()
            u'...recaptcha...'
            sage: chal.html('incorrect-captcha-sol')
            u'...incorrect-captcha-sol...'

        """
        error_param = ''
        if error_code:
            error_param = '&error=%s' % error_code

        template_dict = { 'api_server' : self.api_server,
                          'public_key' : self.public_key,
                          'error_param' : error_param,
                          'lang' : self.lang }

        return template(os.path.join('html', 'recaptcha.html'),
                        **template_dict)

    def is_valid_response(self, req_args = {}, **kwargs):
        """
        Submits a reCAPTCHA request for verification and returns its
        status.
        
        INPUT:

        - ``req_args`` - a dictionary; the arguments of the responding
          user's HTTP POST request

        - ``kwargs`` - a dictionary of extra keyword arguments

        OUTPUT:

        - a :class:`ChallengeResponse` instance; whether the user's
          response is empty, accepted, or rejected, with an optional
          error string

        TESTS::

            sage: from sagenb.notebook.challenge import reCAPTCHAChallenge
            sage: tmp = tmp_dir(ext='.sagenb')
            sage: import sagenb.notebook.notebook as n
            sage: nb = n.Notebook(tmp)
            sage: chal = reCAPTCHAChallenge(nb.conf(), remote_ip = 'localhost')
            sage: req = {}
            sage: chal.is_valid_response(req).is_valid
            sage: chal.is_valid_response(req).error_code
            ''
            sage: req['recaptcha_response_field'] = ['subplotTimes']
            sage: chal.is_valid_response(req).is_valid
            False
            sage: chal.is_valid_response(req).error_code
            'incorrect-captcha-sol'
            sage: req['simple_challenge_field'] = ['VBORw0KGgoANSUhEUgAAAB']
            sage: chal.is_valid_response(req).is_valid # random
            False
            sage: chal.is_valid_response(req).error_code # random
            'incorrect-captcha-sol'

        """
        response_field = req_args.get('recaptcha_response_field', [None])[0]
        if not (response_field and len(response_field)):
            return ChallengeResponse(None, '')

        challenge_field = req_args.get('recaptcha_challenge_field', [None])[0]
        if not (challenge_field and len(challenge_field)):
            return ChallengeResponse(False, 'incorrect-captcha-sol')

        def encode_if_necessary(s):
            if isinstance(s, unicode):
                return s.encode('utf-8')
            return s

        params = urlencode({
                'privatekey': encode_if_necessary(self.private_key),
                'remoteip' :  encode_if_necessary(self.remote_ip),
                'challenge':  encode_if_necessary(challenge_field),
                'response' :  encode_if_necessary(response_field)
                })

        request = Request(
            url = "http://%s/verify" % RECAPTCHA_VERIFY_SERVER,
            data = params,
            headers = {
                "Content-type": "application/x-www-form-urlencoded",
                "User-agent": "reCAPTCHA Python"
                }
            )

        httpresp = urlopen(request)
        return_values = httpresp.read().splitlines();
        httpresp.close();
        return_code = return_values[0]

        if (return_code == "true"):
            return ChallengeResponse(True)
        else:
            return ChallengeResponse(False, return_values[1])


class ChallengeDispatcher(object):
    """
    A simple dispatcher class that provides access to a specific
    challenge.
    """
    def __init__(self, conf, **kwargs):
        """
        Uses the server's configuration to select and set up a
        challenge.

        INPUT:

        - ``conf`` - a :class:`ServerConfiguration`; a server
          configuration instance

        - ``kwargs`` - a dictionary of keyword arguments

        ATTRIBUTES:

        - ``type`` - a string; the type of challenge to set up

        Currently, ``type`` is read from ``conf``'s ``challenge_type``
        setting.

        TESTS::

            sage: from sagenb.notebook.challenge import ChallengeDispatcher
            sage: tmp = tmp_dir(ext='.sagenb')
            sage: import sagenb.notebook.notebook as n
            sage: nb = n.Notebook(tmp)
            sage: disp = ChallengeDispatcher(nb.conf())
            sage: disp.type # random
            'recaptcha'

        """
        self.type = conf['challenge_type']

        if self.type == 'recaptcha':
            # Very simple test for public and private reCAPTCHA keys.
            if conf['recaptcha_public_key'] and conf['recaptcha_private_key']:
                self.challenge = reCAPTCHAChallenge(conf, **kwargs)
            else:
                self.challenge = NotConfiguredChallenge(conf, **kwargs)

        elif self.type == 'simple':
            self.challenge = SimpleChallenge(conf, **kwargs)

        else:
            self.challenge = NotConfiguredChallenge(conf, **kwargs)

    def __call__(self):
        """
        Returns a previously set up challenge.

        OUTPUT:

        - an instantiated subclass of :class:`AbstractChallenge`.

        TESTS::

            sage: from sagenb.notebook.challenge import ChallengeDispatcher
            sage: tmp = tmp_dir(ext='.sagenb')
            sage: import sagenb.notebook.notebook as n
            sage: nb = n.Notebook(tmp)
            sage: nb.conf()['challenge_type'] = 'simple'
            sage: disp = ChallengeDispatcher(nb.conf())
            sage: disp().html() # random
            '<p>...'
            sage: nb.conf()['challenge_type'] = 'mistake'
            sage: disp = ChallengeDispatcher(nb.conf())
            sage: print(disp().html())
            Please ask the server administrator to configure a challenge!

        """
        return self.challenge


def challenge(conf, **kwargs):
    """
    Wraps an instance of :class:`ChallengeDispatcher` and returns an
    instance of a specific challenge.

    INPUT:

    - ``conf`` - a :class:`ServerConfiguration`; a server configuration
      instance

    - ``kwargs`` - a dictionary of keyword arguments

    OUTPUT:

    - an instantiated subclass of :class:`AbstractChallenge`

    TESTS::

        sage: from sagenb.notebook.challenge import challenge
        sage: tmp = tmp_dir(ext='.sagenb')
        sage: import sagenb.notebook.notebook as n
        sage: nb = n.Notebook(tmp)
        sage: nb.conf()['challenge_type'] = 'simple'
        sage: chal = challenge(nb.conf())
        sage: chal.html() # random
        '<p>...'

    """
    return ChallengeDispatcher(conf, **kwargs)()