Merge pull request #113 from QingdaoU/fix1

fix some issues
This commit is contained in:
李扬
2017-12-26 13:43:07 +08:00
committed by GitHub
22 changed files with 287 additions and 258 deletions

View File

@@ -15,4 +15,4 @@ RUN curl -L $(curl -s https://api.github.com/repos/QingdaoU/OnlineJudgeFE/rele
unzip dist.zip && \ unzip dist.zip && \
rm dist.zip rm dist.zip
CMD sh /app/deploy/run.sh ENTRYPOINT /app/deploy/entrypoint.sh

View File

@@ -1,33 +1,23 @@
import logging import logging
from celery import shared_task from celery import shared_task
from envelopes import Envelope
from options.options import SysOptions from options.options import SysOptions
from utils.shortcuts import send_email
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def send_email(from_name, to_email, to_name, subject, content):
smtp = SysOptions.smtp_config
if not smtp:
return
envlope = Envelope(from_addr=(smtp["email"], from_name),
to_addr=(to_email, to_name),
subject=subject,
html_body=content)
try:
envlope.send(smtp["server"],
login=smtp["email"],
password=smtp["password"],
port=smtp["port"],
tls=smtp["tls"])
return True
except Exception as e:
logger.exception(e)
return False
@shared_task @shared_task
def send_email_async(from_name, to_email, to_name, subject, content): def send_email_async(from_name, to_email, to_name, subject, content):
send_email(from_name, to_email, to_name, subject, content) if not SysOptions.smtp_config:
return
try:
send_email(smtp_config=SysOptions.smtp_config,
from_name=from_name,
to_email=to_email,
to_name=to_name,
subject=subject,
content=content)
except Exception as e:
logger.exception(e)

View File

@@ -1,77 +1,31 @@
<table cellpadding="0" cellspacing="0" align="center" style="text-align:left;font-family:'微软雅黑','黑体',arial;" <div>
width="742"> <table cellpadding="0" align="center"
<tbody> style="overflow:hidden;background:#fff;margin:0 auto;text-align:left;position:relative;font-size:14px; font-family:'lucida Grande',Verdana;line-height:1.5;box-shadow:0 0 3px #ccc;border:1px solid #ccc;border-radius:5px;border-collapse:collapse;">
<tr> <tbody>
<td> <tr>
<table cellpadding="0" cellspacing="0" <th valign="middle"
style="text-align:left;border:1px solid #50a5e6;color:#fff;font-size:18px;" width="740"> style="height:38px;color:#fff; font-size:14px;line-height:38px; font-weight:bold;text-align:left;padding:10px 24px 6px; border-bottom:1px solid #467ec3;background:#518bcb;border-radius:5px 5px 0 0;">
<tbody> {{ website_name }}</th>
<tr height="39" style="background-color:#50a5e6;"> </tr>
<td style="padding-left:15px;font-family:'微软雅黑','黑体',arial;"> <tr>
{{ website_name }} <td>
</td> <div style="padding:20px 35px 40px;">
</tr> <h2 style="font-weight:bold;margin-bottom:5px;font-size:14px;">Hello, {{ username }}:</h2>
</tbody> <p style="margin-top:20px">
</table> Please click <a href="{{ link }}">{{ link }}</a> to reset your password in 20 minutes.
<table cellpadding="0" cellspacing="0" </p>
style="text-align:left;border:1px solid #f0f0f0;border-top:none;color:#585858;background-color:#fafafa;" <p style="margin-top:20px">
width="740"> To protect your account, please do not use simple passwords.
<tbody> </p>
<tr height="25"> <p style="margin-top:20px">
<td></td> If you still have any questions, please contract system administrator.
</tr> </p>
<p style="margin-left:2em;"></p>
<p style="text-indent:0;text-align:right;">{{ website_name }}</p>
</div>
</td>
</tr>
<tr height="40"> </tbody>
<td style="padding-left:25px;padding-right:25px;font-size:18px;font-family:'微软雅黑','黑体',arial;"> </table>
Hello, {{ username }}: </div>
</td>
</tr>
<tr height="15">
<td></td>
</tr>
<tr height="30">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:14px;">
We received a request to reset your password for {{ website_name }}.
</td>
</tr>
<tr height="30">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:14px;">
You can use the following link to reset your password in <span style="color:rgb(255,0,0)">20 minutes.</span>
</td>
</tr>
<tr height="60">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:14px;">
<a href="{{ link }}" target="_blank"
style="color: rgb(255,255,255);text-decoration: none;display: block;min-height: 39px;width: 158px;line-height: 39px;background-color:rgb(80,165,230);font-size:20px;text-align:center;">Reset Password</a>
</td>
</tr>
<tr height="10">
<td></td>
</tr>
<tr height="20">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:12px;">
If the button above doesn't work, please copy the following link to your browser and press enter.
</td>
</tr>
<tr height="30">
<td style="padding-left:55px;padding-right:65px;font-family:'微软雅黑','黑体',arial;">
<a href="{{ link }}" target="_blank" style="color:#0c94de;font-size:12px;">
{{ link }}
</a>
</td>
</tr>
<tr height="20">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:12px;">
If you did not ask that, please ignore this email. It will expire and become useless in 20 minutes.
</td>
</tr>
<tr height="20">
<td></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>

View File

@@ -302,11 +302,11 @@ class ApplyResetPasswordAPI(APIView):
"link": f"{SysOptions.website_base_url}/reset-password/{user.reset_password_token}" "link": f"{SysOptions.website_base_url}/reset-password/{user.reset_password_token}"
} }
email_html = render_to_string("reset_password_email.html", render_data) email_html = render_to_string("reset_password_email.html", render_data)
send_email_async.delay(SysOptions.website_name, send_email_async.delay(from_name=SysOptions.website_name_shortcut,
user.email, to_email=user.email,
user.username, to_username=user.username,
f"{SysOptions.website_name} 登录信息找回邮件", subject=f"Reset your password",
email_html) content=email_html)
return self.success("Succeeded") return self.success("Succeeded")

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-12-24 03:44
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('conf', '0002_auto_20171011_1214'),
]
operations = [
migrations.AddField(
model_name='judgeserver',
name='is_disabled',
field=models.BooleanField(default=False),
),
]

View File

@@ -13,6 +13,7 @@ class JudgeServer(models.Model):
create_time = models.DateTimeField(auto_now_add=True) create_time = models.DateTimeField(auto_now_add=True)
task_number = models.IntegerField(default=0) task_number = models.IntegerField(default=0)
service_url = models.CharField(max_length=256, blank=True, null=True) service_url = models.CharField(max_length=256, blank=True, null=True)
is_disabled = models.BooleanField(default=False)
@property @property
def status(self): def status(self):

View File

@@ -43,4 +43,9 @@ class JudgeServerHeartbeatSerializer(serializers.Serializer):
memory = serializers.FloatField(min_value=0, max_value=100) memory = serializers.FloatField(min_value=0, max_value=100)
cpu = serializers.FloatField(min_value=0, max_value=100) cpu = serializers.FloatField(min_value=0, max_value=100)
action = serializers.ChoiceField(choices=("heartbeat", )) action = serializers.ChoiceField(choices=("heartbeat", ))
service_url = serializers.CharField(max_length=256, required=False) service_url = serializers.CharField(max_length=256)
class EditJudgeServerSerializer(serializers.Serializer):
id = serializers.IntegerField()
is_disabled = serializers.BooleanField()

View File

@@ -43,6 +43,14 @@ class SMTPConfigTest(APITestCase):
resp = self.client.put(self.url, data=data) resp = self.client.put(self.url, data=data)
self.assertSuccess(resp) self.assertSuccess(resp)
@mock.patch("conf.views.send_email")
def test_test_smtp(self, mocked_send_email):
url = self.reverse("smtp_test_api")
self.test_create_smtp_config()
resp = self.client.post(url, data={"email": "test@test.com"})
self.assertSuccess(resp)
mocked_send_email.assert_called_once()
class WebsiteConfigAPITest(APITestCase): class WebsiteConfigAPITest(APITestCase):
def test_create_website_config(self): def test_create_website_config(self):
@@ -58,10 +66,11 @@ class WebsiteConfigAPITest(APITestCase):
self.create_super_admin() self.create_super_admin()
url = self.reverse("website_config_api") url = self.reverse("website_config_api")
data = {"website_base_url": "http://test.com", "website_name": "test name", data = {"website_base_url": "http://test.com", "website_name": "test name",
"website_name_shortcut": "test oj", "website_footer": "<a>test</a>", "website_name_shortcut": "test oj", "website_footer": "<img onerror=alert(1) src=#>",
"allow_register": True, "submission_list_show_all": False} "allow_register": True, "submission_list_show_all": False}
resp = self.client.post(url, data=data) resp = self.client.post(url, data=data)
self.assertSuccess(resp) self.assertSuccess(resp)
self.assertEqual(SysOptions.website_footer, "<img src=\"#\" />")
def test_get_website_config(self): def test_get_website_config(self):
# do not need to login # do not need to login
@@ -74,7 +83,7 @@ class JudgeServerHeartbeatTest(APITestCase):
def setUp(self): def setUp(self):
self.url = self.reverse("judge_server_heartbeat_api") self.url = self.reverse("judge_server_heartbeat_api")
self.data = {"hostname": "testhostname", "judger_version": "1.0.4", "cpu_core": 4, self.data = {"hostname": "testhostname", "judger_version": "1.0.4", "cpu_core": 4,
"cpu": 90.5, "memory": 80.3, "action": "heartbeat"} "cpu": 90.5, "memory": 80.3, "action": "heartbeat", "service_url": "http://127.0.0.1"}
self.token = "test" self.token = "test"
self.hashed_token = hashlib.sha256(self.token.encode("utf-8")).hexdigest() self.hashed_token = hashlib.sha256(self.token.encode("utf-8")).hexdigest()
SysOptions.judge_server_token = self.token SysOptions.judge_server_token = self.token
@@ -85,16 +94,6 @@ class JudgeServerHeartbeatTest(APITestCase):
self.assertSuccess(resp) self.assertSuccess(resp)
server = JudgeServer.objects.first() server = JudgeServer.objects.first()
self.assertEqual(server.ip, "127.0.0.1") self.assertEqual(server.ip, "127.0.0.1")
self.assertEqual(server.service_url, None)
def test_new_heartbeat_service_url(self):
service_url = "http://1.2.3.4:8000/api/judge"
data = self.data
data["service_url"] = service_url
resp = self.client.post(self.url, data=self.data, **self.headers)
self.assertSuccess(resp)
server = JudgeServer.objects.first()
self.assertEqual(server.service_url, service_url)
def test_update_heartbeat(self): def test_update_heartbeat(self):
self.test_new_heartbeat() self.test_new_heartbeat()
@@ -107,9 +106,9 @@ class JudgeServerHeartbeatTest(APITestCase):
class JudgeServerAPITest(APITestCase): class JudgeServerAPITest(APITestCase):
def setUp(self): def setUp(self):
JudgeServer.objects.create(**{"hostname": "testhostname", "judger_version": "1.0.4", self.server = JudgeServer.objects.create(**{"hostname": "testhostname", "judger_version": "1.0.4",
"cpu_core": 4, "cpu_usage": 90.5, "memory_usage": 80.3, "cpu_core": 4, "cpu_usage": 90.5, "memory_usage": 80.3,
"last_heartbeat": timezone.now()}) "last_heartbeat": timezone.now()})
self.url = self.reverse("judge_server_api") self.url = self.reverse("judge_server_api")
self.create_super_admin() self.create_super_admin()
@@ -123,6 +122,11 @@ class JudgeServerAPITest(APITestCase):
self.assertSuccess(resp) self.assertSuccess(resp)
self.assertFalse(JudgeServer.objects.filter(hostname="testhostname").exists()) self.assertFalse(JudgeServer.objects.filter(hostname="testhostname").exists())
def test_disabled_judge_server(self):
resp = self.client.put(self.url, data={"is_disabled": True, "id": self.server.id})
self.assertSuccess(resp)
self.assertTrue(JudgeServer.objects.get(id=self.server.id).is_disabled)
class LanguageListAPITest(APITestCase): class LanguageListAPITest(APITestCase):
def test_get_languages(self): def test_get_languages(self):

View File

@@ -1,9 +1,10 @@
from django.conf.urls import url from django.conf.urls import url
from ..views import SMTPAPI, JudgeServerAPI, WebsiteConfigAPI, TestCasePruneAPI from ..views import SMTPAPI, JudgeServerAPI, WebsiteConfigAPI, TestCasePruneAPI, SMTPTestAPI
urlpatterns = [ urlpatterns = [
url(r"^smtp/?$", SMTPAPI.as_view(), name="smtp_admin_api"), url(r"^smtp/?$", SMTPAPI.as_view(), name="smtp_admin_api"),
url(r"^smtp_test/?$", SMTPTestAPI.as_view(), name="smtp_test_api"),
url(r"^website/?$", WebsiteConfigAPI.as_view(), name="website_config_api"), url(r"^website/?$", WebsiteConfigAPI.as_view(), name="website_config_api"),
url(r"^judge_server/?$", JudgeServerAPI.as_view(), name="judge_server_api"), url(r"^judge_server/?$", JudgeServerAPI.as_view(), name="judge_server_api"),
url(r"^prune_test_case/?$", TestCasePruneAPI.as_view(), name="prune_test_case_api"), url(r"^prune_test_case/?$", TestCasePruneAPI.as_view(), name="prune_test_case_api"),

View File

@@ -12,11 +12,13 @@ from judge.dispatcher import process_pending_task
from judge.languages import languages, spj_languages from judge.languages import languages, spj_languages
from options.options import SysOptions from options.options import SysOptions
from utils.api import APIView, CSRFExemptAPIView, validate_serializer from utils.api import APIView, CSRFExemptAPIView, validate_serializer
from utils.shortcuts import send_email
from utils.xss_filter import XSSHtml
from .models import JudgeServer from .models import JudgeServer
from .serializers import (CreateEditWebsiteConfigSerializer, from .serializers import (CreateEditWebsiteConfigSerializer,
CreateSMTPConfigSerializer, EditSMTPConfigSerializer, CreateSMTPConfigSerializer, EditSMTPConfigSerializer,
JudgeServerHeartbeatSerializer, JudgeServerHeartbeatSerializer,
JudgeServerSerializer, TestSMTPConfigSerializer) JudgeServerSerializer, TestSMTPConfigSerializer, EditJudgeServerSerializer)
class SMTPAPI(APIView): class SMTPAPI(APIView):
@@ -51,7 +53,25 @@ class SMTPTestAPI(APIView):
@super_admin_required @super_admin_required
@validate_serializer(TestSMTPConfigSerializer) @validate_serializer(TestSMTPConfigSerializer)
def post(self, request): def post(self, request):
return self.success({"result": True}) if not SysOptions.smtp_config:
return self.error("Please setup SMTP config at first")
try:
send_email(smtp_config=SysOptions.smtp_config,
from_name=SysOptions.website_name_shortcut,
to_name=request.user.username,
to_email=request.data["email"],
subject="You have successfully configured SMTP",
content="You have successfully configured SMTP")
except Exception as e:
# guess error message encoding
msg = e.smtp_error
try:
# qq mail
msg = msg.decode("gbk")
except Exception:
msg = msg.decode("utf-8", "ignore")
return self.error(msg)
return self.success()
class WebsiteConfigAPI(APIView): class WebsiteConfigAPI(APIView):
@@ -65,6 +85,9 @@ class WebsiteConfigAPI(APIView):
@super_admin_required @super_admin_required
def post(self, request): def post(self, request):
for k, v in request.data.items(): for k, v in request.data.items():
if k == "website_footer":
with XSSHtml() as parser:
v = parser.clean(v)
setattr(SysOptions, k, v) setattr(SysOptions, k, v)
return self.success() return self.success()
@@ -83,6 +106,12 @@ class JudgeServerAPI(APIView):
JudgeServer.objects.filter(hostname=hostname).delete() JudgeServer.objects.filter(hostname=hostname).delete()
return self.success() return self.success()
@validate_serializer(EditJudgeServerSerializer)
@super_admin_required
def put(self, request):
JudgeServer.objects.filter(id=request.data["id"]).update(is_disabled=request.data["is_disabled"])
return self.success()
class JudgeServerHeartbeatAPI(CSRFExemptAPIView): class JudgeServerHeartbeatAPI(CSRFExemptAPIView):
@validate_serializer(JudgeServerHeartbeatSerializer) @validate_serializer(JudgeServerHeartbeatSerializer)
@@ -91,7 +120,6 @@ class JudgeServerHeartbeatAPI(CSRFExemptAPIView):
client_token = request.META.get("HTTP_X_JUDGE_SERVER_TOKEN") client_token = request.META.get("HTTP_X_JUDGE_SERVER_TOKEN")
if hashlib.sha256(SysOptions.judge_server_token.encode("utf-8")).hexdigest() != client_token: if hashlib.sha256(SysOptions.judge_server_token.encode("utf-8")).hexdigest() != client_token:
return self.error("Invalid token") return self.error("Invalid token")
service_url = data.get("service_url")
try: try:
server = JudgeServer.objects.get(hostname=data["hostname"]) server = JudgeServer.objects.get(hostname=data["hostname"])
@@ -99,7 +127,7 @@ class JudgeServerHeartbeatAPI(CSRFExemptAPIView):
server.cpu_core = data["cpu_core"] server.cpu_core = data["cpu_core"]
server.memory_usage = data["memory"] server.memory_usage = data["memory"]
server.cpu_usage = data["cpu"] server.cpu_usage = data["cpu"]
server.service_url = service_url server.service_url = data["service_url"]
server.ip = request.META["HTTP_X_REAL_IP"] server.ip = request.META["HTTP_X_REAL_IP"]
server.last_heartbeat = timezone.now() server.last_heartbeat = timezone.now()
server.save() server.save()
@@ -110,7 +138,7 @@ class JudgeServerHeartbeatAPI(CSRFExemptAPIView):
memory_usage=data["memory"], memory_usage=data["memory"],
cpu_usage=data["cpu"], cpu_usage=data["cpu"],
ip=request.META["REMOTE_ADDR"], ip=request.META["REMOTE_ADDR"],
service_url=service_url, service_url=data["service_url"],
last_heartbeat=timezone.now(), last_heartbeat=timezone.now(),
) )
# 新server上线 处理队列中的防止没有新的提交而导致一直waiting # 新server上线 处理队列中的防止没有新的提交而导致一直waiting

View File

@@ -16,3 +16,4 @@ psycopg2
gunicorn gunicorn
jsonfield jsonfield
XlsxWriter XlsxWriter
raven

3
docs/README.md Normal file
View File

@@ -0,0 +1,3 @@
# DOCUMENT
[Here](http://docs.onlinejudge.me/)

16
docs/data.json Normal file
View File

@@ -0,0 +1,16 @@
{
"update": [
{
"version": "2017-12-25",
"level": 1,
"title": "Update at 2017-12-25",
"details": [
"Fix some issues under IE/Edge",
"Add backend error reporter",
"New email template",
"A more flexible throttling function",
"Other bugs and enhancements"
]
}
]
}

View File

@@ -6,7 +6,6 @@ from urllib.parse import urljoin
import requests import requests
from django.db import transaction from django.db import transaction
from django.db.models import F from django.db.models import F
from django.conf import settings
from account.models import User from account.models import User
from conf.models import JudgeServer from conf.models import JudgeServer
@@ -47,7 +46,7 @@ class DispatcherBase(object):
@staticmethod @staticmethod
def choose_judge_server(): def choose_judge_server():
with transaction.atomic(): with transaction.atomic():
servers = JudgeServer.objects.select_for_update().all().order_by("task_number") servers = JudgeServer.objects.select_for_update().filter(is_disabled=False).order_by("task_number")
servers = [s for s in servers if s.status == "normal"] servers = [s for s in servers if s.status == "normal"]
if servers: if servers:
server = servers[0] server = servers[0]
@@ -154,11 +153,7 @@ class JudgeDispatcher(DispatcherBase):
Submission.objects.filter(id=self.submission.id).update(result=JudgeStatus.JUDGING) Submission.objects.filter(id=self.submission.id).update(result=JudgeStatus.JUDGING)
service_url = server.service_url resp = self._request(urljoin(server.service_url, "/judge"), data=data)
# not set service_url, it should be a linked container
if not service_url:
service_url = settings.DEFAULT_JUDGE_SERVER_SERVICE_URL
resp = self._request(urljoin(service_url, "/judge"), data=data)
if resp["err"]: if resp["err"]:
self.submission.result = JudgeStatus.COMPILE_ERROR self.submission.result = JudgeStatus.COMPILE_ERROR
self.submission.statistic_info["err_info"] = resp["data"] self.submission.statistic_info["err_info"] = resp["data"]

View File

@@ -10,6 +10,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.8/ref/settings/ https://docs.djangoproject.com/en/1.8/ref/settings/
""" """
import os import os
import raven
from copy import deepcopy from copy import deepcopy
if os.environ.get("OJ_ENV") == "production": if os.environ.get("OJ_ENV") == "production":
@@ -29,6 +30,7 @@ VENDOR_APPS = (
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'rest_framework', 'rest_framework',
'raven.contrib.django.raven_compat'
) )
LOCAL_APPS = ( LOCAL_APPS = (
'account', 'account',
@@ -125,6 +127,8 @@ UPLOAD_DIR = f"{DATA_DIR}{UPLOAD_PREFIX}"
STATICFILES_DIRS = [os.path.join(DATA_DIR, "public")] STATICFILES_DIRS = [os.path.join(DATA_DIR, "public")]
LOGGING_HANDLERS = ['console'] if DEBUG else ['console', 'sentry']
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,
@@ -139,21 +143,26 @@ LOGGING = {
'level': 'DEBUG', 'level': 'DEBUG',
'class': 'logging.StreamHandler', 'class': 'logging.StreamHandler',
'formatter': 'standard' 'formatter': 'standard'
},
'sentry': {
'level': 'ERROR',
'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler',
'formatter': 'standard'
} }
}, },
'loggers': { 'loggers': {
'django.request': { 'django.request': {
'handlers': ['console'], 'handlers': LOGGING_HANDLERS,
'level': 'ERROR', 'level': 'ERROR',
'propagate': True, 'propagate': True,
}, },
'django.db.backends': { 'django.db.backends': {
'handlers': ['console'], 'handlers': LOGGING_HANDLERS,
'level': 'ERROR', 'level': 'ERROR',
'propagate': True, 'propagate': True,
}, },
'': { '': {
'handlers': ['console'], 'handlers': LOGGING_HANDLERS,
'level': 'WARNING', 'level': 'WARNING',
'propagate': True, 'propagate': True,
} }
@@ -201,3 +210,7 @@ TOKEN_BUCKET_DEFAULT_CAPACITY = 10
# 单位:每分钟 # 单位:每分钟
TOKEN_BUCKET_FILL_RATE = 2 TOKEN_BUCKET_FILL_RATE = 2
RAVEN_CONFIG = {
'dsn': 'https://b200023b8aed4d708fb593c5e0a6ad3d:1fddaba168f84fcf97e0d549faaeaff0@sentry.io/263057'
}

View File

@@ -21,6 +21,7 @@ class OptionKeys:
submission_list_show_all = "submission_list_show_all" submission_list_show_all = "submission_list_show_all"
smtp_config = "smtp_config" smtp_config = "smtp_config"
judge_server_token = "judge_server_token" judge_server_token = "judge_server_token"
throttling = "throttling"
class OptionDefaultValue: class OptionDefaultValue:
@@ -32,6 +33,8 @@ class OptionDefaultValue:
submission_list_show_all = True submission_list_show_all = True
smtp_config = {} smtp_config = {}
judge_server_token = default_token judge_server_token = default_token
throttling = {"ip": {"capacity": 100, "fill_rate": 0.1, "default_capacity": 50},
"user": {"capacity": 20, "fill_rate": 0.03, "default_capacity": 10}}
class _SysOptionsMeta(type): class _SysOptionsMeta(type):
@@ -180,6 +183,14 @@ class _SysOptionsMeta(type):
def judge_server_token(cls, value): def judge_server_token(cls, value):
cls._set_option(OptionKeys.judge_server_token, value) cls._set_option(OptionKeys.judge_server_token, value)
@property
def throttling(cls):
return cls._get_option(OptionKeys.throttling)
@throttling.setter
def throttling(cls, value):
cls._set_option(OptionKeys.throttling, value)
class SysOptions(metaclass=_SysOptionsMeta): class SysOptions(metaclass=_SysOptionsMeta):
pass pass

View File

@@ -1,6 +1,5 @@
import ipaddress import ipaddress
from django.conf import settings
from account.decorators import login_required, check_contest_permission from account.decorators import login_required, check_contest_permission
from judge.tasks import judge_task from judge.tasks import judge_task
# from judge.dispatcher import JudgeDispatcher # from judge.dispatcher import JudgeDispatcher
@@ -8,7 +7,7 @@ from problem.models import Problem, ProblemRuleType
from contest.models import Contest, ContestStatus, ContestRuleType from contest.models import Contest, ContestStatus, ContestRuleType
from options.options import SysOptions from options.options import SysOptions
from utils.api import APIView, validate_serializer from utils.api import APIView, validate_serializer
from utils.throttling import TokenBucket, BucketController from utils.throttling import TokenBucket
from utils.captcha import Captcha from utils.captcha import Captcha
from utils.cache import cache from utils.cache import cache
from ..models import Submission from ..models import Submission
@@ -19,29 +18,16 @@ from ..serializers import SubmissionSafeModelSerializer, SubmissionListSerialize
class SubmissionAPI(APIView): class SubmissionAPI(APIView):
def throttling(self, request): def throttling(self, request):
user_controller = BucketController(factor=request.user.id, user_bucket = TokenBucket(key=str(request.user.id),
redis_conn=cache, redis_conn=cache, **SysOptions.throttling["user"])
default_capacity=settings.TOKEN_BUCKET_DEFAULT_CAPACITY) can_consume, wait = user_bucket.consume()
user_bucket = TokenBucket(fill_rate=settings.TOKEN_BUCKET_FILL_RATE, if not can_consume:
capacity=settings.TOKEN_BUCKET_DEFAULT_CAPACITY, return "Please wait %d seconds" % (int(wait))
last_capacity=user_controller.last_capacity,
last_timestamp=user_controller.last_timestamp)
if user_bucket.consume():
user_controller.last_capacity -= 1
else:
return "Please wait %d seconds" % int(user_bucket.expected_time() + 1)
ip_controller = BucketController(factor=request.session["ip"], ip_bucket = TokenBucket(key=request.session["ip"],
redis_conn=cache, redis_conn=cache, **SysOptions.throttling["ip"])
default_capacity=settings.TOKEN_BUCKET_DEFAULT_CAPACITY * 3) can_consume, wait = ip_bucket.consume()
if not can_consume:
ip_bucket = TokenBucket(fill_rate=settings.TOKEN_BUCKET_FILL_RATE * 3,
capacity=settings.TOKEN_BUCKET_DEFAULT_CAPACITY * 3,
last_capacity=ip_controller.last_capacity,
last_timestamp=ip_controller.last_timestamp)
if ip_bucket.consume():
ip_controller.last_capacity -= 1
else:
return "Captcha is required" return "Captcha is required"
@validate_serializer(CreateSubmissionSerializer) @validate_serializer(CreateSubmissionSerializer)

View File

@@ -1,14 +1,10 @@
from django.contrib.postgres.fields import JSONField # NOQA from django.contrib.postgres.fields import JSONField # NOQA
from django.db import models from django.db import models
from utils.xss_filter import XssHtml from utils.xss_filter import XSSHtml
class RichTextField(models.TextField): class RichTextField(models.TextField):
def get_prep_value(self, value): def get_prep_value(self, value):
if not value: with XSSHtml() as parser:
value = "" return parser.clean(value or "")
parser = XssHtml()
parser.feed(value)
parser.close()
return parser.getHtml()

View File

@@ -5,6 +5,7 @@ from base64 import b64encode
from io import BytesIO from io import BytesIO
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from envelopes import Envelope
def rand_str(length=32, type="lower_hex"): def rand_str(length=32, type="lower_hex"):
@@ -63,3 +64,15 @@ def timestamp2utcstr(value):
def natural_sort_key(s, _nsre=re.compile(r"(\d+)")): def natural_sort_key(s, _nsre=re.compile(r"(\d+)")):
return [int(text) if text.isdigit() else text.lower() return [int(text) if text.isdigit() else text.lower()
for text in re.split(_nsre, s)] for text in re.split(_nsre, s)]
def send_email(smtp_config, from_name, to_email, to_name, subject, content):
envelope = Envelope(from_addr=(smtp_config["email"], from_name),
to_addr=(to_email, to_name),
subject=subject,
html_body=content)
return envelope.send(smtp_config["server"],
login=smtp_config["email"],
password=smtp_config["password"],
port=smtp_config["port"],
tls=smtp_config["tls"])

View File

@@ -1,90 +1,72 @@
from __future__ import print_function
import time import time
class TokenBucket: class TokenBucket:
def __init__(self, fill_rate, capacity, last_capacity, last_timestamp): """
self.capacity = float(capacity) 注意对于单个key的操作不是线程安全的
self._left_tokens = last_capacity """
self.fill_rate = float(fill_rate) def __init__(self, key, capacity, fill_rate, default_capacity, redis_conn):
self.timestamp = last_timestamp """
:param capacity: 最大容量
:param fill_rate: 填充速度/每秒
:param default_capacity: 初始容量
:param redis_conn: redis connection
"""
self._key = key
self._capacity = capacity
self._fill_rate = fill_rate
self._default_capacity = default_capacity
self._redis_conn = redis_conn
def consume(self, tokens=1): self._last_capacity_key = "last_capacity"
if tokens <= self.tokens: self._last_timestamp_key = "last_timestamp"
self._left_tokens -= tokens
return True
return False
def expected_time(self, tokens=1): def _init_key(self):
_tokens = self.tokens self._last_capacity = self._default_capacity
tokens = max(tokens, _tokens) now = time.time()
return (tokens - _tokens) / self.fill_rate * 60 self._last_timestamp = now
return self._default_capacity, now
@property @property
def tokens(self): def _last_capacity(self):
if self._left_tokens < self.capacity: last_capacity = self._redis_conn.hget(self._key, self._last_capacity_key)
if last_capacity is None:
return self._init_key()[0]
else:
return float(last_capacity)
@_last_capacity.setter
def _last_capacity(self, value):
self._redis_conn.hset(self._key, self._last_capacity_key, value)
@property
def _last_timestamp(self):
return float(self._redis_conn.hget(self._key, self._last_timestamp_key))
@_last_timestamp.setter
def _last_timestamp(self, value):
self._redis_conn.hset(self._key, self._last_timestamp_key, value)
def _try_to_fill(self, now):
delta = self._fill_rate * (now - self._last_timestamp)
return min(self._last_capacity + delta, self._capacity)
def consume(self, num=1):
"""
消耗 num 个 token返回是否成功
:param num:
:return: result: bool, wait_time: float
"""
# print("capacity ", self.fill(time.time()))
if self._last_capacity >= num:
self._last_capacity -= num
return True, 0
else:
now = time.time() now = time.time()
delta = self.fill_rate * ((now - self.timestamp) / 60) cur_num = self._try_to_fill(now)
self._left_tokens = min(self.capacity, self._left_tokens + delta) if cur_num >= num:
self.timestamp = now self._last_capacity = cur_num - num
return self._left_tokens self._last_timestamp = now
return True, 0
else:
class BucketController: return False, (num - cur_num) / self._fill_rate
def __init__(self, factor, redis_conn, default_capacity):
self.default_capacity = default_capacity
self.redis = redis_conn
self.key = "bucket_" + str(factor)
@property
def last_capacity(self):
value = self.redis.hget(self.key, "last_capacity")
if value is None:
self.last_capacity = self.default_capacity
return self.default_capacity
return int(value)
@last_capacity.setter
def last_capacity(self, value):
self.redis.hset(self.key, "last_capacity", value)
@property
def last_timestamp(self):
value = self.redis.hget(self.key, "last_timestamp")
if value is None:
timestamp = int(time.time())
self.last_timestamp = timestamp
return timestamp
return int(value)
@last_timestamp.setter
def last_timestamp(self, value):
self.redis.hset(self.key, "last_timestamp", value)
"""
# # Token bucket, to limit submission rate
# # Demo
success = failure = 0
current_user_id = 1
token_bucket_default_capacity = 50
token_bucket_fill_rate = 10
for i in range(5000):
controller = BucketController(user_id=current_user_id,
redis_conn=redis.Redis(),
default_capacity=token_bucket_default_capacity)
bucket = TokenBucket(fill_rate=token_bucket_fill_rate,
capacity=token_bucket_default_capacity,
last_capacity=controller.last_capacity,
last_timestamp=controller.last_timestamp)
time.sleep(0.05)
if bucket.consume():
success += 1
print(i, ": Accepted")
controller.last_capacity -= 1
else:
failure += 1
print(i, "Dropped, time left ", bucket.expected_time())
print(success, failure)
"""

View File

@@ -30,7 +30,7 @@ import copy
from html.parser import HTMLParser from html.parser import HTMLParser
class XssHtml(HTMLParser): class XSSHtml(HTMLParser):
allow_tags = ['a', 'img', 'br', 'strong', 'b', 'code', 'pre', allow_tags = ['a', 'img', 'br', 'strong', 'b', 'code', 'pre',
'p', 'div', 'em', 'span', 'h1', 'h2', 'h3', 'h4', 'p', 'div', 'em', 'span', 'h1', 'h2', 'h3', 'h4',
'h5', 'h6', 'blockquote', 'ul', 'ol', 'tr', 'th', 'td', 'h5', 'h6', 'blockquote', 'ul', 'ol', 'tr', 'th', 'td',
@@ -53,7 +53,17 @@ class XssHtml(HTMLParser):
self.start = [] self.start = []
self.data = [] self.data = []
def getHtml(self): def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
super().close()
def clean(self, content):
self.feed(content)
return self.get_html()
def get_html(self):
""" """
Get the safe html code Get the safe html code
""" """
@@ -188,11 +198,11 @@ class XssHtml(HTMLParser):
if "__main__" == __name__: if "__main__" == __name__:
parser = XssHtml() with XSSHtml() as parser:
parser.feed("""<p><img src=1 onerror=alert(/xss/)></p><div class="left"> ret = parser.clean("""<p><img src=1 onerror=alert(/xss/)></p><div class="left">
<a href='javascript:prompt(1)'><br />hehe</a></div> <a href='javascript:prompt(1)'><br />hehe</a></div>
<p id="test" onmouseover="alert(1)">&gt;M<svg> <p id="test" onmouseover="alert(1)">&gt;M<svg>
<a href="https://www.baidu.com" target="self">MM</a></p> <a href="https://www.baidu.com" target="self">MM</a></p>
<embed src='javascript:alert(/hehe/)' allowscriptaccess=always />""") <embed src='javascript:alert(/hehe/)' allowscriptaccess=always />
parser.close() <img onerror=alert(1) src=#>""")
print(parser.getHtml()) print(ret)