Cloned DMOJ
This commit is contained in:
parent
f623974b58
commit
49dc9ff10c
513 changed files with 132349 additions and 39 deletions
137
judge/utils/pwned.py
Normal file
137
judge/utils/pwned.py
Normal file
|
@ -0,0 +1,137 @@
|
|||
"""
|
||||
Based on https://github.com/ubernostrum/pwned-passwords-django.
|
||||
|
||||
Original license:
|
||||
|
||||
Copyright (c) 2018, James Bennett
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
* Neither the name of the author nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."""
|
||||
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.password_validation import CommonPasswordValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.six import string_types
|
||||
from django.utils.translation import gettext as _, ungettext
|
||||
|
||||
from judge.utils.unicode import utf8bytes
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
API_ENDPOINT = 'https://api.pwnedpasswords.com/range/{}'
|
||||
REQUEST_TIMEOUT = 2.0 # 2 seconds
|
||||
|
||||
|
||||
def _get_pwned(prefix):
|
||||
"""
|
||||
Fetches a dict of all hash suffixes from Pwned Passwords for a
|
||||
given SHA-1 prefix.
|
||||
"""
|
||||
try:
|
||||
response = requests.get(
|
||||
url=API_ENDPOINT.format(prefix),
|
||||
timeout=getattr(
|
||||
settings,
|
||||
'PWNED_PASSWORDS_API_TIMEOUT',
|
||||
REQUEST_TIMEOUT,
|
||||
),
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException:
|
||||
# Gracefully handle timeouts and HTTP error response codes.
|
||||
log.warning('Skipped Pwned Passwords check due to error', exc_info=True)
|
||||
return None
|
||||
|
||||
results = {}
|
||||
for line in response.text.splitlines():
|
||||
line_suffix, _, times = line.partition(':')
|
||||
results[line_suffix] = int(times)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def pwned_password(password):
|
||||
"""
|
||||
Checks a password against the Pwned Passwords database.
|
||||
"""
|
||||
if not isinstance(password, string_types):
|
||||
raise TypeError('Password values to check must be strings.')
|
||||
password_hash = hashlib.sha1(utf8bytes(password)).hexdigest().upper()
|
||||
prefix, suffix = password_hash[:5], password_hash[5:]
|
||||
results = _get_pwned(prefix)
|
||||
if results is None:
|
||||
# Gracefully handle timeouts and HTTP error response codes.
|
||||
return None
|
||||
return results.get(suffix, 0)
|
||||
|
||||
|
||||
class PwnedPasswordsValidator(object):
|
||||
"""
|
||||
Password validator which checks the Pwned Passwords database.
|
||||
"""
|
||||
DEFAULT_HELP_MESSAGE = _("Your password can't be a commonly used password.")
|
||||
DEFAULT_PWNED_MESSAGE = _('This password is too common.')
|
||||
|
||||
def __init__(self, error_message=None, help_message=None):
|
||||
self.help_message = help_message or self.DEFAULT_HELP_MESSAGE
|
||||
error_message = error_message or self.DEFAULT_PWNED_MESSAGE
|
||||
|
||||
# If there is no plural, use the same message for both forms.
|
||||
if isinstance(error_message, string_types):
|
||||
singular, plural = error_message, error_message
|
||||
else:
|
||||
singular, plural = error_message
|
||||
self.error_message = {
|
||||
'singular': singular,
|
||||
'plural': plural,
|
||||
}
|
||||
|
||||
def validate(self, password, user=None):
|
||||
amount = pwned_password(password)
|
||||
if amount is None:
|
||||
# HIBP API failure. Instead of allowing a potentially compromised
|
||||
# password, check Django's list of common passwords generated from
|
||||
# the same database.
|
||||
CommonPasswordValidator().validate(password, user)
|
||||
elif amount:
|
||||
raise ValidationError(
|
||||
ungettext(
|
||||
self.error_message['singular'],
|
||||
self.error_message['plural'],
|
||||
amount,
|
||||
),
|
||||
params={'amount': amount},
|
||||
code='pwned_password',
|
||||
)
|
||||
|
||||
def get_help_text(self):
|
||||
return self.help_message
|
Loading…
Add table
Add a link
Reference in a new issue