├── .gitignore ├── .node-version ├── .python-version ├── .travis.yml ├── LICENSE ├── README.md ├── package.pip └── src ├── lepus ├── __init__.py ├── admin │ ├── __init__.py │ ├── serializers.py │ └── views.py ├── handlers.py ├── internal │ ├── __init__.py │ ├── serializers.py │ └── views.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20150913_1811.py │ └── __init__.py ├── models.py ├── permissions.py ├── serializers.py ├── settings.py ├── signals.py ├── templates │ └── rest_framework │ │ └── api.html ├── tests │ ├── __init__.py │ └── test_team_model.py ├── urls.py ├── views.py └── wsgi.py └── manage.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | .project 25 | .pydevproject 26 | .settings 27 | 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # For development 52 | 53 | *.sqlite3 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | 64 | #don't save .idea folder 65 | .idea/ 66 | *.iml 67 | 68 | # Uploaded files 69 | src/question 70 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v0.12.7 2 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | lepus 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.4" 5 | 6 | install: "pip install -r package.pip" 7 | 8 | script: 9 | - cd src/ 10 | - python manage.py test 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 lepus-ctf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lepus RESTful CTF Score Server 2 | 3 | LepusはRESTfulなCTFスコアサーバーです。 4 | 5 | [![Build Status](https://travis-ci.org/lepus-ctf/lepus-api.svg?branch=feat%2Freadme)](https://travis-ci.org/lepus-ctf/lepus-api) 6 | 7 | ## Requirement 8 | 9 | * Python 3.4 10 | * see `package.pip` 11 | 12 | ## Quick Start 13 | 14 | * `git clone https://github.com/lepus-ctf/lepus-api.git` 15 | * `cd lepus-api` 16 | * `pip install -r package.pip` 17 | * `cd src` 18 | * `python manage.py migrate` to create database. 19 | * `python manage.py createsuperuser` to create superuser. 20 | * `python manage.py runserver` to running webserver. 21 | * Open `http://localhost:8000/api/` for testing. 22 | 23 | ## Push Notification 24 | 25 | WebSocketによるリアルタイムのプッシュ通知は [lepus-api-push](https://github.com/lepus-ctf/lepus-api-push) と組み合わせて提供されています。 26 | 27 | ## Copyright and license 28 | Code and documentation copyright [Lepus CTF](http://lepus-ctf.org/). 29 | Code released under MIT License [(See LICENSE)](https://github.com/lepus-ctf/lepus-api/blob/master/LICENSE) 30 | Docs released under [Creative Commons License 4.0 BY](http://creativecommons.org/licenses/by/4.0/legalcode.ja). 31 | -------------------------------------------------------------------------------- /package.pip: -------------------------------------------------------------------------------- 1 | django<1.9 2 | Markdown 3 | django-filter 4 | djangorestframework 5 | drf-nested-routers 6 | django-debug-toolbar 7 | requests 8 | -------------------------------------------------------------------------------- /src/lepus/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepus-ctf/lepus-api/516e6b952bf78c9fcc29fa4fdb9632b217259338/src/lepus/__init__.py -------------------------------------------------------------------------------- /src/lepus/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib import admin 3 | from lepus.models import Category, Question, Team, User, Flag, Answer, Notice, File, \ 4 | UserConnection, Config, AttackPoint 5 | 6 | class NoticeAdmin(admin.ModelAdmin): 7 | list_display = ("id", "title", "body", "is_public") 8 | list_editable = ("title", "body", "is_public") 9 | 10 | admin.site.register(Notice, NoticeAdmin) 11 | 12 | 13 | class TeamAdmin(admin.ModelAdmin): 14 | list_display = ("id", "name", "last_score_time") 15 | 16 | admin.site.register(Team, TeamAdmin) 17 | 18 | class UserConnectionInline(admin.TabularInline): 19 | model = UserConnection 20 | extra = 0 21 | readonly_fields = ("id", "ip", "updated_at") 22 | fields = ("id", "ip", "updated_at") 23 | 24 | class UserAdmin(admin.ModelAdmin): 25 | list_display = ("id", "team", "username", "is_staff", "is_superuser") 26 | list_filter = ("team", "is_staff", "is_superuser") 27 | inlines = [UserConnectionInline, ] 28 | 29 | admin.site.register(User, UserAdmin) 30 | 31 | 32 | class CategoryAdmin(admin.ModelAdmin): 33 | list_display = ("id", "name", "ordering") 34 | list_editable = ("name", "ordering") 35 | 36 | admin.site.register(Category, CategoryAdmin) 37 | 38 | 39 | class QuestionAdmin(admin.ModelAdmin): 40 | list_display = ("id", "category", "title", "ordering", "is_public") 41 | list_filter = ("category", "is_public") 42 | list_editable = ("ordering",) 43 | 44 | admin.site.register(Question, QuestionAdmin) 45 | 46 | class FileAdmin(admin.ModelAdmin): 47 | list_display = ("question", "file", "is_public") 48 | list_filter = ("question", "is_public") 49 | 50 | admin.site.register(File, FileAdmin) 51 | 52 | class FlagAdmin(admin.ModelAdmin): 53 | list_display = ("id", "question", "flag", "point") 54 | list_filter = ("question",) 55 | 56 | admin.site.register(Flag, FlagAdmin) 57 | 58 | 59 | class AnswerAdmin(admin.ModelAdmin): 60 | list_display = ("id", "team", "user", "question", "answer", "flag", "created_at") 61 | list_filter = ("team", "user", "question",) 62 | 63 | admin.site.register(Answer, AnswerAdmin) 64 | 65 | 66 | class AttackPointAdmin(admin.ModelAdmin): 67 | list_display = ("id", "team", "user", "question", "point", "token", "created_at") 68 | list_filter = ("team", "user", "question",) 69 | 70 | admin.site.register(AttackPoint, AttackPointAdmin) 71 | 72 | 73 | class ConfigAdmin(admin.ModelAdmin): 74 | list_display = ("id", "key", "value",) 75 | 76 | admin.site.register(Config, ConfigAdmin) 77 | -------------------------------------------------------------------------------- /src/lepus/admin/serializers.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | from rest_framework import serializers 4 | 5 | from lepus.models import * 6 | from lepus.serializers import BaseSerializer, TeamSerializer, CategorySerializer 7 | from lepus.signals import send_realtime_event 8 | 9 | class AdminUserSerializer(BaseSerializer): 10 | class Meta: 11 | model = User 12 | fields = ("id", "username", "password", "team", "seat", "points", "ip", "last_score_time", "created_at", "updated_at") 13 | read_only_fields = ("id", "points", "last_score_time", "ip", "created_at", "updated_at") 14 | extra_kwargs = {'password': {'write_only': True, 'required': False}} 15 | 16 | def create(self, validated_data): 17 | password = validated_data.pop("password", None) 18 | instance = super(AdminUserSerializer, self).create(validated_data) 19 | if password: 20 | instance.set_password(password) 21 | else: 22 | instance.password = "!" 23 | instance.save() 24 | return instance 25 | 26 | def update(self, instance, validated_data): 27 | password = validated_data.pop("password", None) 28 | instance = super(AdminUserSerializer, self).update(instance, validated_data) 29 | if password: 30 | instance.set_password(password) 31 | instance.save() 32 | return instance 33 | 34 | 35 | class AdminTeamSerilaizer(TeamSerializer): 36 | pass 37 | 38 | class AdminCategorySerializer(CategorySerializer): 39 | pass 40 | 41 | class AdminQuestionSerializer(BaseSerializer): 42 | """問題""" 43 | class Meta: 44 | model = Question 45 | fields = ('id', 'category', 'ordering', 'title', 'sentence', 'max_answers', 'max_failure', 'is_public', 'created_at', 'updated_at') 46 | read_only_fields = ('id', 'points', 'created_at', 'updated_at') 47 | 48 | class AdminFlagSerializer(BaseSerializer): 49 | class Meta: 50 | model = Flag 51 | fields = ("id", "question", "flag", "point", "teams", "created_at", "updated_at") 52 | read_only_fields = ("id", "teams", "created_at", "updated_at") 53 | 54 | class AdminAnswerSerializer(BaseSerializer): 55 | class Meta: 56 | model = Answer 57 | fields = ("id", "user", "team", "question", "answer", "flag", "is_correct", "created_at", "updated_at") 58 | read_only_fields = ("is_correct",) 59 | 60 | class AdminNoticeSerializer(BaseSerializer): 61 | class Meta: 62 | model = Notice 63 | fields = ('id', 'title', 'body', 'is_public', 'created_at', 'updated_at') 64 | read_only_fields = ('id', 'created_at', 'updated_at') 65 | 66 | class AdminYoutubeSerializer(serializers.Serializer): 67 | video_id = serializers.RegexField(regex="^[a-zA-Z0-9_-]{11}$", max_length=20, required=False, error_messages={"invalid":"INVALID"}) 68 | forced = serializers.BooleanField() 69 | 70 | def create(self, validated_data): 71 | video_id = validated_data.get("video_id", None) 72 | forced = validated_data.get("forced", False) 73 | if not video_id: 74 | video_id = None 75 | 76 | data = { 77 | "type":"youtube", 78 | "video_id": video_id, 79 | "forced": forced 80 | } 81 | send_realtime_event(data) 82 | 83 | return { 84 | "video_id": video_id, 85 | "forced": forced 86 | } 87 | -------------------------------------------------------------------------------- /src/lepus/admin/views.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | from rest_framework.routers import DefaultRouter 3 | from rest_framework import permissions, viewsets, mixins 4 | from lepus.admin.serializers import AdminUserSerializer, AdminTeamSerilaizer, AdminCategorySerializer, \ 5 | AdminQuestionSerializer, AdminFlagSerializer, AdminAnswerSerializer, AdminNoticeSerializer, \ 6 | AdminYoutubeSerializer 7 | from lepus.models import * 8 | from lepus.views import DynamicDepthMixins 9 | 10 | router = DefaultRouter() 11 | 12 | class AdminUserViewSet(DynamicDepthMixins, viewsets.ModelViewSet): 13 | serializer_class = AdminUserSerializer 14 | queryset = User.objects.filter(team__isnull=False) 15 | permission_classes = (permissions.IsAdminUser,) 16 | 17 | router.register("users", AdminUserViewSet) 18 | 19 | 20 | class AdminTeamViewSet(DynamicDepthMixins, viewsets.ModelViewSet): 21 | serializer_class = AdminTeamSerilaizer 22 | queryset = Team.objects.all() 23 | permission_classes = (permissions.IsAdminUser,) 24 | 25 | router.register("teams", AdminTeamViewSet) 26 | 27 | 28 | class AdminCategoryViewSet(DynamicDepthMixins, viewsets.ModelViewSet): 29 | serializer_class = AdminCategorySerializer 30 | queryset = Category.objects.all() 31 | permission_classes = (permissions.IsAdminUser,) 32 | 33 | router.register("categories", AdminCategoryViewSet) 34 | 35 | 36 | class AdminQuestionViewSet(DynamicDepthMixins, viewsets.ModelViewSet): 37 | serializer_class = AdminQuestionSerializer 38 | queryset = Question.objects.all() 39 | permission_classes = (permissions.IsAdminUser,) 40 | 41 | router.register("questions", AdminQuestionViewSet) 42 | 43 | 44 | class AdminFlagViewSet(DynamicDepthMixins, viewsets.ModelViewSet): 45 | serializer_class = AdminFlagSerializer 46 | queryset = Flag.objects.all() 47 | permission_classes = (permissions.IsAdminUser,) 48 | 49 | router.register("flags", AdminFlagViewSet) 50 | 51 | 52 | class AdminAnswerViewSet(mixins.ListModelMixin, 53 | mixins.RetrieveModelMixin, 54 | mixins.DestroyModelMixin, 55 | DynamicDepthMixins, 56 | viewsets.GenericViewSet): 57 | serializer_class = AdminAnswerSerializer 58 | queryset = Answer.objects.all() 59 | permission_classes = (permissions.IsAdminUser,) 60 | 61 | router.register("answers", AdminAnswerViewSet) 62 | 63 | 64 | class AdminNoticeViewSet(DynamicDepthMixins, viewsets.ModelViewSet): 65 | serializer_class = AdminNoticeSerializer 66 | queryset = Notice.objects.all() 67 | permission_classes = (permissions.IsAdminUser,) 68 | 69 | router.register("notices", AdminNoticeViewSet) 70 | 71 | 72 | class AdminYouTubeViewSet(mixins.CreateModelMixin, viewsets.ViewSet): 73 | serializer_class = AdminYoutubeSerializer 74 | 75 | def get_serializer(self, data=None): 76 | if data: 77 | return self.serializer_class(data=data) 78 | return self.serializer_class() 79 | 80 | router.register("youtube", AdminYouTubeViewSet, base_name="youtube") 81 | -------------------------------------------------------------------------------- /src/lepus/handlers.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | from django.http import Http404 3 | from django.core.exceptions import PermissionDenied 4 | from rest_framework import exceptions, status 5 | from rest_framework.compat import set_rollback 6 | from rest_framework.response import Response 7 | 8 | from lepus.serializers import ValidationErrorDetail 9 | 10 | def exception_handler(exc, context): 11 | if isinstance(exc, exceptions.ValidationError): 12 | errors = [] 13 | message = "Validation failed." 14 | 15 | for field, field_errors in exc.detail.items(): 16 | 17 | if field == "non_field_errors": 18 | field = None 19 | 20 | for error in field_errors: 21 | 22 | if isinstance(error, ValidationErrorDetail): 23 | message = error.message 24 | error = {"error": error.error} 25 | else: 26 | error = {"error": error} 27 | 28 | if field: 29 | error["field"] = field 30 | errors.append(error) 31 | 32 | data = { 33 | "message": message, 34 | "errors": errors 35 | } 36 | return Response(data, status=422) 37 | 38 | if isinstance(exc, exceptions.APIException): 39 | if isinstance(exc.detail, list): 40 | errors = [{"error": e} for e in exc.detail] 41 | data = {"message": "API exception occurred.", "errors": errors} 42 | elif isinstance(exc.detail, dict): 43 | message = exc.detail.get("message", "API exception occurred.") 44 | errors = [{"error": e} for e in exc.detail.get("errors", [])] 45 | data = {'message': message, "errors": errors} 46 | else: 47 | data = {'message': exc.detail, "errors":[]} 48 | 49 | set_rollback() 50 | return Response(data, status=exc.status_code) 51 | 52 | elif isinstance(exc, Http404): 53 | data = { 54 | "message":"Not found.", 55 | "errors":[ 56 | {"error":"not_found"} 57 | ] 58 | } 59 | set_rollback() 60 | return Response(data, status=status.HTTP_404_NOT_FOUND) 61 | 62 | elif isinstance(exc, PermissionDenied): 63 | data = { 64 | "message":"Permision denied.", 65 | "errors":[ 66 | {"error":"permission_denied"} 67 | ] 68 | } 69 | set_rollback() 70 | return Response(data, status=status.HTTP_403_FORBIDDEN) 71 | 72 | # Note: Unhandled exceptions will raise a 500 error. 73 | return None 74 | -------------------------------------------------------------------------------- /src/lepus/internal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepus-ctf/lepus-api/516e6b952bf78c9fcc29fa4fdb9632b217259338/src/lepus/internal/__init__.py -------------------------------------------------------------------------------- /src/lepus/internal/serializers.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | from rest_framework import serializers 4 | 5 | from lepus.models import * 6 | 7 | class AttackPointSerializer(serializers.ModelSerializer): 8 | class Meta: 9 | model = AttackPoint 10 | fields = ('id', 'user', 'team', 'question', 'point', 'token', 'created_at', 'updated_at') 11 | read_only_fields = ('id', 'team', 'created_at', 'updated_at') 12 | 13 | def validate(self, validated_data): 14 | user = validated_data.get("user") 15 | if user: 16 | validated_data["team"] = user.team 17 | return validated_data 18 | 19 | 20 | class UserSerializer(serializers.ModelSerializer): 21 | class Meta: 22 | model = User 23 | fields = ("id", "username", "ip") 24 | -------------------------------------------------------------------------------- /src/lepus/internal/views.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | from rest_framework.views import APIView 4 | from rest_framework.decorators import api_view 5 | from rest_framework.response import Response 6 | from rest_framework import viewsets, mixins 7 | from rest_framework.routers import DefaultRouter 8 | 9 | from lepus.models import * 10 | from lepus.internal.serializers import AttackPointSerializer, UserSerializer 11 | 12 | router = DefaultRouter() 13 | 14 | class AttackPointViewSet(mixins.CreateModelMixin, 15 | viewsets.GenericViewSet): 16 | serializer_class = AttackPointSerializer 17 | queryset = AttackPoint.objects.all() 18 | 19 | router.register("attackpoints", AttackPointViewSet) 20 | 21 | class UserViewSet(viewsets.ReadOnlyModelViewSet): 22 | serializer_class = UserSerializer 23 | queryset = User.objects.all() 24 | 25 | def get_queryset(self): 26 | ip = self.request.GET.get("ip") 27 | if ip: 28 | queryset = User.objects.by_ip(ip) 29 | else: 30 | queryset = self.queryset 31 | return queryset 32 | 33 | 34 | router.register("users", UserViewSet) 35 | -------------------------------------------------------------------------------- /src/lepus/middleware.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | from lepus.models import UserConnection 4 | 5 | class UserConnectionMiddleware(object): 6 | def process_request(self, request): 7 | user = request.user 8 | ip = request.META.get("HTTP_X_REAL_IP") 9 | if not ip: 10 | ip = request.META.get("REMOTE_ADDR") 11 | 12 | if user.is_authenticated() and ip: 13 | UserConnection.update(user, ip) 14 | 15 | from lepus.signals import * 16 | -------------------------------------------------------------------------------- /src/lepus/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.core.validators 6 | from django.conf import settings 7 | import lepus.models 8 | import django.utils.timezone 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | ('auth', '0006_require_contenttypes_0002'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='User', 20 | fields=[ 21 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 22 | ('password', models.CharField(verbose_name='password', max_length=128)), 23 | ('last_login', models.DateTimeField(blank=True, verbose_name='last login', null=True)), 24 | ('is_superuser', models.BooleanField(verbose_name='superuser status', help_text='Designates that this user has all permissions without explicitly assigning them.', default=False)), 25 | ('username', models.CharField(unique=True, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], verbose_name='username', error_messages={'unique': 'A user with that username already exists.'}, max_length=30)), 26 | ('first_name', models.CharField(blank=True, verbose_name='first name', max_length=30)), 27 | ('last_name', models.CharField(blank=True, verbose_name='last name', max_length=30)), 28 | ('email', models.EmailField(blank=True, verbose_name='email address', max_length=254)), 29 | ('is_staff', models.BooleanField(verbose_name='staff status', help_text='Designates whether the user can log into this admin site.', default=False)), 30 | ('is_active', models.BooleanField(verbose_name='active', help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', default=True)), 31 | ('date_joined', models.DateTimeField(verbose_name='date joined', default=django.utils.timezone.now)), 32 | ('created_at', models.DateTimeField(verbose_name='作成日時', auto_now_add=True)), 33 | ('updated_at', models.DateTimeField(verbose_name='最終更新日時', auto_now=True)), 34 | ('seat', models.CharField(blank=True, verbose_name='座席', max_length=32)), 35 | ('last_score_time', models.DateTimeField(blank=True, verbose_name='最終得点日時', null=True)), 36 | ('groups', models.ManyToManyField(to='auth.Group', related_name='user_set', blank=True, verbose_name='groups', help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_query_name='user')), 37 | ], 38 | options={ 39 | 'verbose_name': 'user', 40 | 'verbose_name_plural': 'users', 41 | 'abstract': False, 42 | }, 43 | managers=[ 44 | ('objects', lepus.models.UserManager()), 45 | ], 46 | ), 47 | migrations.CreateModel( 48 | name='Answer', 49 | fields=[ 50 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 51 | ('created_at', models.DateTimeField(verbose_name='作成日時', auto_now_add=True)), 52 | ('updated_at', models.DateTimeField(verbose_name='最終更新日時', auto_now=True)), 53 | ('answer', models.CharField(verbose_name='解答', max_length=256)), 54 | ], 55 | ), 56 | migrations.CreateModel( 57 | name='AttackPoint', 58 | fields=[ 59 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 60 | ('created_at', models.DateTimeField(verbose_name='作成日時', auto_now_add=True)), 61 | ('updated_at', models.DateTimeField(verbose_name='最終更新日時', auto_now=True)), 62 | ('token', models.CharField(unique=True, verbose_name='トークン', max_length=256)), 63 | ('point', models.IntegerField(verbose_name='得点')), 64 | ], 65 | options={ 66 | 'abstract': False, 67 | }, 68 | ), 69 | migrations.CreateModel( 70 | name='Category', 71 | fields=[ 72 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 73 | ('created_at', models.DateTimeField(verbose_name='作成日時', auto_now_add=True)), 74 | ('updated_at', models.DateTimeField(verbose_name='最終更新日時', auto_now=True)), 75 | ('name', models.CharField(verbose_name='カテゴリ名', max_length=50)), 76 | ('ordering', models.IntegerField(verbose_name='表示順序', default=100)), 77 | ], 78 | options={ 79 | 'ordering': ('ordering',), 80 | }, 81 | ), 82 | migrations.CreateModel( 83 | name='Config', 84 | fields=[ 85 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 86 | ('created_at', models.DateTimeField(verbose_name='作成日時', auto_now_add=True)), 87 | ('updated_at', models.DateTimeField(verbose_name='最終更新日時', auto_now=True)), 88 | ('key', models.CharField(unique=True, verbose_name='設定項目', max_length=256)), 89 | ('value_str', models.TextField(verbose_name='シリアライズされた値')), 90 | ], 91 | options={ 92 | 'abstract': False, 93 | }, 94 | ), 95 | migrations.CreateModel( 96 | name='File', 97 | fields=[ 98 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 99 | ('created_at', models.DateTimeField(verbose_name='作成日時', auto_now_add=True)), 100 | ('updated_at', models.DateTimeField(verbose_name='最終更新日時', auto_now=True)), 101 | ('name', models.CharField(verbose_name='ファイル名', max_length=256)), 102 | ('file', models.FileField(upload_to='question/', verbose_name='ファイル', max_length=256)), 103 | ('is_public', models.BooleanField(verbose_name='公開するか', default=True)), 104 | ], 105 | options={ 106 | 'abstract': False, 107 | }, 108 | ), 109 | migrations.CreateModel( 110 | name='Flag', 111 | fields=[ 112 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 113 | ('created_at', models.DateTimeField(verbose_name='作成日時', auto_now_add=True)), 114 | ('updated_at', models.DateTimeField(verbose_name='最終更新日時', auto_now=True)), 115 | ('flag', models.CharField(unique=True, verbose_name='Flag', max_length=200)), 116 | ('point', models.IntegerField(verbose_name='得点')), 117 | ], 118 | options={ 119 | 'abstract': False, 120 | }, 121 | ), 122 | migrations.CreateModel( 123 | name='Notice', 124 | fields=[ 125 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 126 | ('created_at', models.DateTimeField(verbose_name='作成日時', auto_now_add=True)), 127 | ('updated_at', models.DateTimeField(verbose_name='最終更新日時', auto_now=True)), 128 | ('title', models.CharField(verbose_name='タイトル', max_length=80)), 129 | ('body', models.TextField(verbose_name='本文')), 130 | ('is_public', models.BooleanField(verbose_name='公開にするか', default=False)), 131 | ], 132 | options={ 133 | 'ordering': ['created_at'], 134 | }, 135 | ), 136 | migrations.CreateModel( 137 | name='Question', 138 | fields=[ 139 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 140 | ('created_at', models.DateTimeField(verbose_name='作成日時', auto_now_add=True)), 141 | ('updated_at', models.DateTimeField(verbose_name='最終更新日時', auto_now=True)), 142 | ('ordering', models.IntegerField(unique=True, verbose_name='表示順序', default=100)), 143 | ('title', models.CharField(verbose_name='タイトル', max_length=50)), 144 | ('sentence', models.TextField(verbose_name='問題文')), 145 | ('max_answers', models.IntegerField(blank=True, verbose_name='最大回答者数', null=True)), 146 | ('max_failure', models.IntegerField(blank=True, verbose_name='最大回答数', null=True)), 147 | ('is_public', models.BooleanField(verbose_name='公開にするか', default=False)), 148 | ('category', models.ForeignKey(to='lepus.Category', verbose_name='カテゴリ')), 149 | ], 150 | options={ 151 | 'ordering': ('ordering',), 152 | }, 153 | ), 154 | migrations.CreateModel( 155 | name='Team', 156 | fields=[ 157 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 158 | ('created_at', models.DateTimeField(verbose_name='作成日時', auto_now_add=True)), 159 | ('updated_at', models.DateTimeField(verbose_name='最終更新日時', auto_now=True)), 160 | ('name', models.CharField(unique=True, verbose_name='チーム名', max_length=32)), 161 | ('password', models.CharField(verbose_name='チームパスワード', max_length=128)), 162 | ('last_score_time', models.DateTimeField(blank=True, verbose_name='最終得点日時', null=True)), 163 | ], 164 | options={ 165 | 'abstract': False, 166 | }, 167 | ), 168 | migrations.CreateModel( 169 | name='UserConnection', 170 | fields=[ 171 | ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')), 172 | ('created_at', models.DateTimeField(verbose_name='作成日時', auto_now_add=True)), 173 | ('updated_at', models.DateTimeField(verbose_name='最終更新日時', auto_now=True)), 174 | ('ip', models.GenericIPAddressField(verbose_name='IPアドレス')), 175 | ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='ユーザー')), 176 | ], 177 | options={ 178 | 'ordering': ('-updated_at',), 179 | }, 180 | ), 181 | migrations.AddField( 182 | model_name='flag', 183 | name='question', 184 | field=models.ForeignKey(to='lepus.Question', verbose_name='問題'), 185 | ), 186 | migrations.AddField( 187 | model_name='file', 188 | name='question', 189 | field=models.ForeignKey(to='lepus.Question', verbose_name='問題'), 190 | ), 191 | migrations.AlterUniqueTogether( 192 | name='category', 193 | unique_together=set([('name', 'ordering')]), 194 | ), 195 | migrations.AddField( 196 | model_name='attackpoint', 197 | name='question', 198 | field=models.ForeignKey(to='lepus.Question', verbose_name='問題'), 199 | ), 200 | migrations.AddField( 201 | model_name='attackpoint', 202 | name='team', 203 | field=models.ForeignKey(to='lepus.Team', verbose_name='チーム'), 204 | ), 205 | migrations.AddField( 206 | model_name='attackpoint', 207 | name='user', 208 | field=models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='ユーザー'), 209 | ), 210 | migrations.AddField( 211 | model_name='answer', 212 | name='flag', 213 | field=models.ForeignKey(to='lepus.Flag', blank=True, null=True), 214 | ), 215 | migrations.AddField( 216 | model_name='answer', 217 | name='question', 218 | field=models.ForeignKey(to='lepus.Question', verbose_name='問題'), 219 | ), 220 | migrations.AddField( 221 | model_name='answer', 222 | name='team', 223 | field=models.ForeignKey(to='lepus.Team', verbose_name='チーム'), 224 | ), 225 | migrations.AddField( 226 | model_name='answer', 227 | name='user', 228 | field=models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='ユーザー'), 229 | ), 230 | migrations.AddField( 231 | model_name='user', 232 | name='team', 233 | field=models.ForeignKey(to='lepus.Team', blank=True, verbose_name='チーム', null=True), 234 | ), 235 | migrations.AddField( 236 | model_name='user', 237 | name='user_permissions', 238 | field=models.ManyToManyField(to='auth.Permission', related_name='user_set', blank=True, verbose_name='user permissions', help_text='Specific permissions for this user.', related_query_name='user'), 239 | ), 240 | migrations.AlterUniqueTogether( 241 | name='userconnection', 242 | unique_together=set([('user', 'ip')]), 243 | ), 244 | migrations.AlterUniqueTogether( 245 | name='answer', 246 | unique_together=set([('team', 'flag')]), 247 | ), 248 | ] 249 | -------------------------------------------------------------------------------- /src/lepus/migrations/0002_auto_20150913_1811.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('lepus', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name='category', 16 | options={'ordering': ('ordering', 'id')}, 17 | ), 18 | migrations.AlterModelOptions( 19 | name='question', 20 | options={'ordering': ('category__ordering', 'ordering', 'id')}, 21 | ), 22 | migrations.AlterField( 23 | model_name='category', 24 | name='name', 25 | field=models.CharField(max_length=50, verbose_name='カテゴリ名', unique=True), 26 | ), 27 | migrations.AlterField( 28 | model_name='question', 29 | name='ordering', 30 | field=models.IntegerField(verbose_name='表示順序', default=100), 31 | ), 32 | migrations.AlterUniqueTogether( 33 | name='category', 34 | unique_together=set([]), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /src/lepus/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepus-ctf/lepus-api/516e6b952bf78c9fcc29fa4fdb9632b217259338/src/lepus/migrations/__init__.py -------------------------------------------------------------------------------- /src/lepus/models.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import pickle 3 | import hashlib 4 | import time 5 | import datetime 6 | from django.utils.timezone import utc 7 | import base64 8 | from django.conf import settings 9 | from django.db import models 10 | from django.contrib.auth.models import UserManager as DjangoUserManager 11 | from django.contrib.auth.models import AbstractUser 12 | from django.core.urlresolvers import reverse 13 | from django.contrib.auth.hashers import make_password, check_password 14 | from os.path import normpath, join 15 | from lepus.settings import BASE_DIR 16 | 17 | class Templete(models.Model): 18 | """全てのモデルで共通のフィールドを含んだAbstract Model""" 19 | class Meta: 20 | abstract = True 21 | created_at = models.DateTimeField("作成日時", auto_now_add=True) 22 | updated_at = models.DateTimeField("最終更新日時", auto_now=True) 23 | 24 | class Category(Templete): 25 | """問題のカテゴリ""" 26 | class Meta: 27 | ordering = ('ordering', 'id',) 28 | 29 | name = models.CharField("カテゴリ名", max_length=50, unique=True) 30 | ordering = models.IntegerField("表示順序", default=100) 31 | 32 | def __str__(self): 33 | return self.name 34 | 35 | 36 | class QuestionManager(models.Manager): 37 | def public(self): 38 | return self.get_queryset().filter(is_public=True) 39 | 40 | class Question(Templete): 41 | """問題""" 42 | class Meta: 43 | ordering = ('category', 'ordering', 'id',) 44 | 45 | category = models.ForeignKey(Category, verbose_name="カテゴリ") 46 | ordering = models.IntegerField("表示順序", default=100) 47 | title = models.CharField("タイトル", max_length=50) 48 | sentence = models.TextField("問題文") 49 | max_answers = models.IntegerField("最大回答者数", blank=True, null=True) 50 | max_failure = models.IntegerField("最大回答数", blank=True, null=True) 51 | is_public = models.BooleanField("公開にするか", blank=True, default=False) 52 | 53 | objects = QuestionManager() 54 | 55 | @property 56 | def points(self): 57 | return sum([o.point for o in self.flag_set.all()]) 58 | 59 | def __str__(self): 60 | return self.title 61 | 62 | @property 63 | def files(self): 64 | return filter(lambda f:f.is_public, self.file_set.all()) 65 | 66 | 67 | class Flag(Templete): 68 | """正解のフラグと得点""" 69 | flag = models.CharField("Flag", max_length=200, unique=True) 70 | question = models.ForeignKey(Question, verbose_name="問題") 71 | point = models.IntegerField("得点") 72 | 73 | def __str__(self): 74 | return self.flag 75 | 76 | @property 77 | def teams(self): 78 | return [a["team_id"] for a in Answer.objects.filter(flag=self).values("team_id")] 79 | 80 | class FileManager(models.Manager): 81 | def public(self): 82 | return self.get_queryset().filter(is_public=True, question__is_public=True) 83 | 84 | class File(Templete): 85 | """問題に添付するファイル""" 86 | question = models.ForeignKey(Question, verbose_name="問題") 87 | name = models.CharField("ファイル名", max_length=256) 88 | file = models.FileField(upload_to='question/', max_length=256, verbose_name="ファイル") 89 | is_public = models.BooleanField("公開するか", blank=True, default=True) 90 | 91 | objects = FileManager() 92 | 93 | @property 94 | def url(self): 95 | return reverse("download_file", args=(self.id, self.name)) 96 | 97 | @property 98 | def size(self): 99 | return self.file.size 100 | 101 | def __str__(self): 102 | return self.name 103 | 104 | class Team(Templete): 105 | """チーム""" 106 | name = models.CharField("チーム名", max_length=32, unique=True) 107 | password = models.CharField("チームパスワード", max_length=128) 108 | last_score_time = models.DateTimeField("最終得点日時", blank=True, null=True) 109 | 110 | def __str__(self): 111 | return self.name 112 | 113 | def set_password(self, password): 114 | self.password = make_password(password) 115 | 116 | def check_password(self, raw_password): 117 | """ 118 | Return a boolean of whether the raw_password was correct. Handles 119 | hashing formats behind the scenes. 120 | """ 121 | def setter(raw_password): 122 | self.set_password(raw_password) 123 | # Password hash upgrades shouldn't be considered password changes. 124 | self._password = None 125 | self.save(update_fields=["password"]) 126 | return check_password(raw_password, self.password, setter) 127 | 128 | @property 129 | def token(self): 130 | sha1 = hashlib.sha1() 131 | sha1.update("{0}_{1}_{2}".format( 132 | settings.TEAM_TOKEN_SECRET_KEY, 133 | self.id, 134 | int(time.time() / settings.TEAM_TOKEN_INTERVAL 135 | )).encode("utf-8")) 136 | return sha1.hexdigest() 137 | 138 | @property 139 | def points(self): 140 | answers = filter(lambda a:a.flag_id, self.answer_set.all()) 141 | points = 0 142 | for answer in answers: 143 | points += answer.flag.point 144 | 145 | for attack_point in self.attackpoint_set.all(): 146 | points += attack_point.point 147 | 148 | return points 149 | 150 | @property 151 | def questions(self): 152 | data = {} 153 | answers = filter(lambda a:a.flag_id, self.answer_set.all()) 154 | 155 | for answer in answers: 156 | question = answer.flag.question 157 | if not question.id in data: 158 | data[question.id] = { 159 | "id": question.id, 160 | "flags":0, 161 | "points":0 162 | } 163 | data[question.id]["flags"] += 1 164 | data[question.id]["points"] += answer.flag.point 165 | 166 | return data.values() 167 | 168 | 169 | class UserManager(DjangoUserManager): 170 | def by_ip(self, ip): 171 | try: 172 | user_connection = UserConnection.objects.filter(ip=ip).order_by("-updated_at")[0] 173 | except IndexError: 174 | return User.objects.none() 175 | return self.get_queryset().filter(id=user_connection.user.id) 176 | 177 | 178 | class User(AbstractUser, Templete): 179 | """チームに属するユーザ""" 180 | team = models.ForeignKey(Team, verbose_name="チーム", blank=True, null=True) 181 | seat = models.CharField("座席", max_length=32, blank=True) 182 | last_score_time = models.DateTimeField("最終得点日時", blank=True, null=True) 183 | 184 | objects = UserManager() 185 | 186 | def __str__(self): 187 | return self.username 188 | 189 | @property 190 | def points(self): 191 | answers = Answer.objects.filter(user=self).exclude(flag=None) 192 | points = 0 193 | for answer in answers: 194 | points += answer.flag.point 195 | 196 | for attack_point in AttackPoint.objects.filter(user=self): 197 | points += attack_point.point 198 | 199 | return points 200 | 201 | @property 202 | def ip(self): 203 | try: 204 | user_connection = self.userconnection_set.order_by("-updated_at")[0] 205 | except IndexError: 206 | return None 207 | return user_connection.ip 208 | 209 | class UserConnection(Templete): 210 | """ユーザの接続元を管理するモデル""" 211 | class Meta: 212 | unique_together = (('user', 'ip'),) 213 | ordering = ("-updated_at",) 214 | 215 | user = models.ForeignKey(User, verbose_name="ユーザー") 216 | ip = models.GenericIPAddressField("IPアドレス") 217 | 218 | @classmethod 219 | def update(cls, user, ip): 220 | """アクセス元IPの更新""" 221 | user_connection, created = cls.objects.get_or_create(user=user, ip=ip) 222 | if not created: 223 | user_connection.updated_at = datetime.datetime.utcnow().replace(tzinfo=utc) 224 | user_connection.save() 225 | return user_connection 226 | 227 | class Answer(Templete): 228 | """回答履歴""" 229 | class Meta: 230 | unique_together = (('team', 'flag'),) 231 | user = models.ForeignKey(User, verbose_name="ユーザー") 232 | team = models.ForeignKey(Team, verbose_name="チーム") 233 | question = models.ForeignKey(Question, verbose_name="問題") 234 | flag = models.ForeignKey(Flag, blank=True, null=True) 235 | answer = models.CharField("解答", max_length=256) 236 | 237 | @property 238 | def is_correct(self): 239 | return self.flag is not None 240 | 241 | class AttackPoint(Templete): 242 | """攻撃点記録""" 243 | user = models.ForeignKey(User, verbose_name="ユーザー") 244 | team = models.ForeignKey(Team, verbose_name="チーム") 245 | question = models.ForeignKey(Question, verbose_name="問題") 246 | token = models.CharField("トークン", max_length=256, unique=True) 247 | point = models.IntegerField("得点") 248 | 249 | class Config(Templete): 250 | """設定用モデル""" 251 | key = models.CharField("設定項目", max_length=256, unique=True) 252 | value_str = models.TextField("シリアライズされた値") 253 | 254 | def __str__(self): 255 | return self.key 256 | 257 | def get_value(self): 258 | return pickle.loads(base64.b64decode(self.value_str)) 259 | def set_value(self, value): 260 | self.value_str = base64.b64encode(pickle.dumps(value)) 261 | value = property(get_value, set_value) 262 | 263 | @classmethod 264 | def get(cls, key, default=None): 265 | try: 266 | config = cls.objects.get(key=key) 267 | return config.value 268 | except Config.DoesNotExist: 269 | return default 270 | 271 | @classmethod 272 | def set(cls, key, value): 273 | config, created = Config.objects.get_or_create(key=key) 274 | config.value = value 275 | config.save() 276 | return config 277 | 278 | class Notice(Templete): 279 | """お知らせ""" 280 | class Meta: 281 | ordering = ['created_at'] 282 | title = models.CharField("タイトル", max_length=80) 283 | body = models.TextField("本文") 284 | is_public = models.BooleanField("公開にするか", blank=True, default=False) 285 | 286 | def __str__(self): 287 | return self.title 288 | -------------------------------------------------------------------------------- /src/lepus/permissions.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.utils.timezone import utc 3 | 4 | from rest_framework.exceptions import APIException 5 | from rest_framework.permissions import BasePermission 6 | 7 | from lepus.models import Config 8 | 9 | 10 | class TimeException(APIException): 11 | status_code = 503 12 | 13 | def __init__(self, message="", error=""): 14 | self.detail = {"message": message, "errors": [error, ]} 15 | super(APIException, self).__init__(self.detail) 16 | 17 | 18 | class IsClosed(BasePermission): 19 | def has_permission(self, request, view): 20 | end = Config.get(key='end_at') 21 | now = datetime.datetime.utcnow().replace(tzinfo=utc) 22 | 23 | if end: 24 | if end < now: 25 | raise TimeException(message="CTF closed.", error="CLOSED") 26 | 27 | return True 28 | 29 | 30 | class IsStarted(BasePermission): 31 | def has_permission(self, request, view): 32 | start = Config.get(key='start_at') 33 | now = datetime.datetime.utcnow().replace(tzinfo=utc) 34 | 35 | if start: 36 | if start > now: 37 | raise TimeException(message="CTF isn't started.", error="NOT_STARTED") 38 | 39 | return True 40 | -------------------------------------------------------------------------------- /src/lepus/serializers.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | from django.contrib.auth import authenticate 3 | from django.conf import settings 4 | 5 | from datetime import datetime 6 | 7 | from lepus import models 8 | 9 | from rest_framework import serializers, status, exceptions, fields 10 | 11 | 12 | class ValidationErrorDetail(object): 13 | def __init__(self, error="", message=""): 14 | self.error = error 15 | self.message = message 16 | 17 | 18 | class ValidationError(serializers.ValidationError): 19 | 20 | def __init__(self, error="", message=""): 21 | detail = ValidationErrorDetail(message=message, error=error) 22 | super(ValidationError, self).__init__(detail) 23 | 24 | 25 | class BaseSerializer(serializers.ModelSerializer): 26 | 27 | def __init__(self, *args, **kwargs): 28 | super(BaseSerializer, self).__init__(*args, **kwargs) 29 | for k, field in self.fields.items(): 30 | if field.read_only: 31 | continue 32 | field.error_messages.update({ 33 | "required":"required", 34 | "null":"required", 35 | "blank":"required", 36 | "max_length":"too_long", 37 | "min_length":"too_short", 38 | "min_value":"too_small", 39 | "max_value":"too_big", 40 | "invalid":"invalid", 41 | "max_string_length":"too_long", 42 | "does_not_exist":"not_found", 43 | "incorrect_type":"numeric_is_required" 44 | }) 45 | 46 | if isinstance(field, fields.IntegerField): 47 | field.error_messages.update({ 48 | "invalid":"numeric_is_required" 49 | }) 50 | 51 | 52 | class CategorySerializer(BaseSerializer): 53 | """カテゴリ""" 54 | 55 | class Meta: 56 | model = models.Category 57 | fields = ('id', 'name', 'ordering', 'created_at', 'updated_at') 58 | read_only_fields = ('id', 'created_at', 'updated_at') 59 | 60 | 61 | class FileSerializer(BaseSerializer): 62 | class Meta: 63 | model = models.File 64 | fields = ('url', 'name', 'size', 'created_at', 'updated_at') 65 | 66 | 67 | class QuestionSerializer(BaseSerializer): 68 | """問題""" 69 | 70 | class Meta: 71 | model = models.Question 72 | fields = ( 73 | 'id', 'category', 'ordering', 'title', 'sentence', 'max_answers', 'files', 'max_failure', 74 | 'created_at', 'updated_at', 'points' 75 | ) 76 | read_only_fields = ('points', ) 77 | 78 | files = FileSerializer(many=True, read_only=True) 79 | 80 | 81 | class TeamSerializer(BaseSerializer): 82 | class Meta: 83 | model = models.Team 84 | fields = ( 85 | 'id', 'name', 'password', 'token', 'points', 'last_score_time', 'created_at', 'updated_at', 86 | 'questions') 87 | read_only_fields = ('id', 'token', 'points', 'last_score_time', 'questions', 'created_at', 'updated_at') 88 | extra_kwargs = {'password': {'write_only': True, 'required': False}} 89 | 90 | def create(self, validated_data): 91 | password = validated_data.pop("password", None) 92 | instance = super(TeamSerializer, self).create(validated_data) 93 | if password: 94 | instance.set_password(validated_data["password"]) 95 | else: 96 | instance.password = "!" 97 | instance.save() 98 | return instance 99 | 100 | def update(self, instance, validated_data): 101 | password = validated_data.pop("password", None) 102 | instance = super(TeamSerializer, self).update(instance, validated_data) 103 | if password: 104 | instance.set_password(password) 105 | instance.save() 106 | return instance 107 | 108 | 109 | class UserSerializer(BaseSerializer): 110 | class Meta: 111 | model = models.User 112 | fields = ("id", "username", "password", "team", "points", "last_score_time", "team_name", "team_password", "is_staff") 113 | read_only_fields = ("id", "team", "points", "last_score_time", "is_staff") 114 | extra_kwargs = {'password': {'write_only': True}} 115 | 116 | team_name = serializers.CharField(write_only=True, allow_null=False) 117 | team_password = serializers.CharField(write_only=True, allow_null=False) 118 | 119 | def validate_password(self, value): 120 | # FIXME:許可するパターンを指定 121 | return value 122 | 123 | def validate(self, data): 124 | team = None 125 | try: 126 | team = models.Team.objects.get(name=data.get("team_name")) 127 | if not team.check_password(data.get("team_password")): 128 | raise models.Team.DoesNotExist() 129 | except models.Team.DoesNotExist: 130 | if team or not settings.ALLOW_CREATE_USER: 131 | # パスワード間違い,または,登録が禁止されている場合 132 | raise ValidationError(message="Invalid credentials. Team name or password is invalid.", error="INVALID_CREDENTIALS") 133 | 134 | data["team"] = team 135 | return data 136 | 137 | def create(self, validated_data): 138 | team = validated_data["team"] 139 | if not team: 140 | # チームの作成 141 | team = models.Team(name=validated_data["team_name"]) 142 | team.set_password(validated_data["team_password"]) 143 | team.save() 144 | 145 | # ユーザーの作成とパスワードの設定 146 | user_data = { 147 | "team": team, 148 | "username": validated_data["username"] 149 | } 150 | user = models.User(**user_data) 151 | user.set_password(validated_data["password"]) 152 | user.save() 153 | 154 | return user 155 | 156 | 157 | class AuthSerializer(serializers.Serializer): 158 | username = serializers.CharField(max_length=30, allow_null=False, error_messages={"required":"required", "blank":"required"}) 159 | password = serializers.CharField(allow_null=False, error_messages={"required":"required", "blank":"required"}) 160 | 161 | def validate(self, data): 162 | self._user_cache = None 163 | if data.get("username") and data.get("password"): 164 | user = authenticate(username=data['username'], password=data['password']) 165 | if user: 166 | self._user_cache = user 167 | 168 | if not self._user_cache: 169 | raise ValidationError(message="Authentication failure. Username or password is invalid.", error="INVALID_CREDENTIALS") 170 | return data 171 | 172 | def get_user(self): 173 | return self._user_cache 174 | 175 | 176 | class AnswerSerializer(BaseSerializer): 177 | class Meta: 178 | model = models.Answer 179 | fields = ('question', 'answer', 'is_correct') 180 | read_only_fields = ('is_correct',) 181 | 182 | def validate(self, data): 183 | question = data['question'] 184 | user = self.context.get('request').user 185 | team = user.team 186 | 187 | # 対応するFlagの取得 188 | try: 189 | flag = models.Flag.objects.get(question=question, flag=data['answer']) 190 | except models.Flag.DoesNotExist: 191 | # 間違いを記録 192 | answer = models.Answer(question=question, team=team, user=user, answer=data['answer'], flag=None) 193 | answer.save() 194 | raise ValidationError(message="This answer is not correct.", error="INCORRECT_ANSWER") 195 | 196 | # 重複を許さない 197 | if flag and models.Answer.objects.filter(team=team, flag=flag).exists(): 198 | raise ValidationError(message="The flag is already submitted.", error="ALREADY_SUBMITTED") 199 | 200 | # questionにおいて制限数が1以上の時,無制限に解答を受け付ける 201 | if question.max_failure and question.max_failure >= 0: 202 | if question.max_failure <= models.Answer.objects.filter(question=question, team=team).count(): 203 | raise ValidationError(message="Failure count is exceed. You can't submit for this question.", error="MAX_FAILURE") 204 | 205 | if question.max_answers and question.max_answers >= 0: 206 | if question.max_answers <= models.Answer.objects.filter(flag=flag, question=question).count(): 207 | raise ValidationError(message="Teams count is exceed. You can't submit for this question.", error="MAX_ANSWERS") 208 | 209 | return data 210 | 211 | def create(self, validated_data): 212 | user = self.context.get('request').user 213 | team = user.team 214 | 215 | question = validated_data['question'] 216 | answer = validated_data['answer'] 217 | flag = None 218 | try: 219 | flag = models.Flag.objects.get(question=question, flag=answer) 220 | except models.Flag.DoesNotExist: 221 | pass 222 | 223 | # 正解時に最終得点日時を更新する 224 | if flag: 225 | team.last_score_time = datetime.now() 226 | user.last_score_time = datetime.now() 227 | team.save() 228 | user.save() 229 | 230 | if not models.Answer.objects.filter(flag=flag).exists(): 231 | # ファーストボーナス 232 | bonus_point = int(settings.FIRST_BONUS_RATE * flag.point) 233 | if bonus_point > 0: 234 | models.AttackPoint( 235 | user=user, team=team, question=question, 236 | token="first_bonus_{0}".format(flag.id), 237 | point=bonus_point 238 | ).save() 239 | 240 | data = { 241 | "user": user, 242 | "team": team, 243 | "question": question, 244 | "answer": answer, 245 | "flag": flag 246 | } 247 | answer = models.Answer(**data) 248 | answer.save() 249 | return answer 250 | 251 | 252 | class AttackPointSerializer(BaseSerializer): 253 | class Meta: 254 | model = models.AttackPoint 255 | fields = ('id', 'user', 'team', 'question', 'token', 'point') 256 | read_only_fields = ('id', 'user', 'team', 'created_at', 'updated_at') 257 | 258 | 259 | class NoticeSerializer(BaseSerializer): 260 | class Meta: 261 | model = models.Notice 262 | fields = ('id', 'title', 'body', 'created_at', 'updated_at') 263 | 264 | 265 | class ConfigSerializer(BaseSerializer): 266 | class Meta: 267 | model = models.Config 268 | fields = ('id', 'value') 269 | readonly_fields = ('id', 'value') 270 | 271 | id = serializers.CharField(source='key') 272 | -------------------------------------------------------------------------------- /src/lepus/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for lepus project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'nhn)d8h)l2o%ok9ahc1_&5(p1e@3f0j0hynvnvplwdd01zlz6q' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | # Define custom user model 31 | AUTH_USER_MODEL = 'lepus.User' 32 | 33 | 34 | # Application definition 35 | 36 | INSTALLED_APPS = ( 37 | 'lepus', 38 | 'rest_framework', 39 | 'rest_framework.authtoken', 40 | 'django.contrib.admin', 41 | 'django.contrib.auth', 42 | 'django.contrib.contenttypes', 43 | 'django.contrib.sessions', 44 | 'django.contrib.messages', 45 | 'django.contrib.staticfiles', 46 | 'debug_toolbar', 47 | ) 48 | 49 | MIDDLEWARE_CLASSES = ( 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | 'django.middleware.security.SecurityMiddleware', 58 | 'lepus.middleware.UserConnectionMiddleware', 59 | ) 60 | 61 | ROOT_URLCONF = 'lepus.urls' 62 | 63 | TEMPLATES = [ 64 | { 65 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 66 | 'DIRS': [], 67 | 'APP_DIRS': True, 68 | 'OPTIONS': { 69 | 'context_processors': [ 70 | 'django.template.context_processors.debug', 71 | 'django.template.context_processors.request', 72 | 'django.contrib.auth.context_processors.auth', 73 | 'django.contrib.messages.context_processors.messages', 74 | ], 75 | }, 76 | }, 77 | ] 78 | 79 | WSGI_APPLICATION = 'lepus.wsgi.application' 80 | 81 | 82 | # supporting filtering 83 | REST_FRAMEWORK = { 84 | 'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',), 85 | 'EXCEPTION_HANDLER': 'lepus.handlers.exception_handler' 86 | } 87 | 88 | 89 | # Database 90 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 91 | 92 | DATABASES = { 93 | 'default': { 94 | 'ENGINE': 'django.db.backends.sqlite3', 95 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 96 | } 97 | } 98 | 99 | 100 | # Internationalization 101 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 102 | 103 | LANGUAGE_CODE = 'ja-jp' 104 | 105 | TIME_ZONE = 'Asia/Tokyo' 106 | 107 | USE_I18N = True 108 | 109 | USE_L10N = True 110 | 111 | USE_TZ = False 112 | 113 | 114 | # Static files (CSS, JavaScript, Images) 115 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 116 | 117 | STATIC_URL = '/static/' 118 | 119 | TEAM_TOKEN_SECRET_KEY = "CHANGE_ME" 120 | TEAM_TOKEN_INTERVAL = 300 121 | 122 | ALLOW_CREATE_USER = True 123 | 124 | FIRST_BONUS_RATE = 0.1 125 | 126 | PUSH_EVENT_URL = "http://localhost:8001/events/" 127 | -------------------------------------------------------------------------------- /src/lepus/signals.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import requests 3 | import json 4 | import logging 5 | from django.core.signals import request_finished 6 | from django.dispatch import Signal, receiver 7 | from django.db.models.signals import post_save 8 | from django.conf import settings 9 | from lepus.models import Answer, Notice, Category, Question, File 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | def send_realtime_event(data): 14 | if settings.PUSH_EVENT_URL: 15 | headers = {'Content-type': 'application/json', 'Accept': 'application/json'} 16 | try: 17 | response = requests.post(settings.PUSH_EVENT_URL, data=json.dumps(data), headers=headers) 18 | except: 19 | logger.error("Cannot send push event %s.", data.get("type")) 20 | 21 | @receiver(post_save, sender=Answer) 22 | def on_answer_sent(sender, **kwargs): 23 | if kwargs["created"]: 24 | answer = kwargs["instance"] 25 | 26 | if answer.is_correct: 27 | # For Users 28 | data = { 29 | "type":"answer", 30 | "user":answer.user.id, 31 | "team":answer.team.id, 32 | "question":answer.flag.question_id 33 | } 34 | send_realtime_event(data) 35 | 36 | # For Admin 37 | data = { 38 | "type":"answer", 39 | "user":answer.user.id, 40 | "team":answer.team.id, 41 | "answer":answer.answer, 42 | "flag":answer.flag.id if answer.flag else None, 43 | "is_correct":answer.is_correct, 44 | "is_admin":True 45 | } 46 | send_realtime_event(data) 47 | 48 | def on_changed(sender, **kwargs): 49 | instance = kwargs["instance"] 50 | 51 | if hasattr(instance, "is_public") and not instance.is_public: 52 | return 53 | 54 | data = { 55 | "type": "update", 56 | "id": instance.id 57 | } 58 | if isinstance(instance, Category): 59 | data["model"] = "category" 60 | if isinstance(instance, Question): 61 | data["model"] = "question" 62 | if isinstance(instance, File): 63 | data["model"] = "file" 64 | if isinstance(instance, Notice): 65 | data["model"] = "notice" 66 | 67 | send_realtime_event(data) 68 | 69 | post_save.connect(on_changed, sender=Category) 70 | post_save.connect(on_changed, sender=Question) 71 | post_save.connect(on_changed, sender=File) 72 | post_save.connect(on_changed, sender=Notice) 73 | -------------------------------------------------------------------------------- /src/lepus/templates/rest_framework/api.html: -------------------------------------------------------------------------------- 1 | {% extends "rest_framework/base.html" %} 2 | 3 | {% block title %}Lepus API{% endblock %} 4 | 5 | 6 | {% block branding %} 7 | 8 | Lepus CTF Score Server API 9 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /src/lepus/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepus-ctf/lepus-api/516e6b952bf78c9fcc29fa4fdb9632b217259338/src/lepus/tests/__init__.py -------------------------------------------------------------------------------- /src/lepus/tests/test_team_model.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from lepus.models import Team 3 | 4 | 5 | class TeamModelTests(TestCase): 6 | 7 | def test_password(self): 8 | team = Team(name="team") 9 | 10 | # 空パスワードで認証されないことを確認 11 | self.assertFalse(team.check_password("")) 12 | 13 | # パスワードを設定 14 | team.set_password("password") 15 | self.assertFalse(team.check_password("invalid")) 16 | self.assertTrue(team.check_password("password")) 17 | 18 | # パスワードの変更 19 | team.set_password("newpassword") 20 | self.assertFalse(team.check_password("invalid")) 21 | self.assertFalse(team.check_password("password")) 22 | self.assertTrue(team.check_password("newpassword")) 23 | -------------------------------------------------------------------------------- /src/lepus/urls.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | """lepus URL Configuration 4 | 5 | The `urlpatterns` list routes URLs to views. For more information please see: 6 | https://docs.djangoproject.com/en/1.8/topics/http/urls/ 7 | Examples: 8 | Function views 9 | 1. Add an import: from my_app import views 10 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 11 | Class-based views 12 | 1. Add an import: from other_app.views import Home 13 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 14 | Including another URLconf 15 | 1. Add an import: from blog import urls as blog_urls 16 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 17 | """ 18 | from django.conf.urls import include, url 19 | from django.contrib import admin 20 | from rest_framework.routers import DefaultRouter 21 | 22 | from lepus.views import AuthViewSet, UserViewSet, QuestionViewSet, TeamViewSet, \ 23 | CategoryViewSet, NoticeViewSet, AnswerViewSet, download_file, ConfigViewSet 24 | 25 | from lepus.admin.views import router as admin_router 26 | from lepus.internal.views import router as internal_router 27 | 28 | router = DefaultRouter() 29 | router.register(r'auth', AuthViewSet, base_name="auth") 30 | router.register(r'users', UserViewSet) 31 | router.register(r'questions', QuestionViewSet) 32 | router.register(r'answers', AnswerViewSet, base_name="answers") 33 | router.register(r'teams', TeamViewSet) 34 | router.register(r'categories', CategoryViewSet) 35 | router.register(r'notices', NoticeViewSet) 36 | router.register(r'configurations', ConfigViewSet) 37 | 38 | urlpatterns = [ 39 | url(r'^admin/', include(admin.site.urls)), 40 | url(r'^files/(\d+)/(.+)$', download_file, name="download_file"), 41 | url(r'^adminapi/', include(admin_router.urls, namespace='adminapi')), 42 | url(r'^internalapi/', include(internal_router.urls, namespace='internalapi')), 43 | url(r'^api/', include(router.urls, namespace='api')) 44 | ] 45 | -------------------------------------------------------------------------------- /src/lepus/views.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import mimetypes 3 | 4 | from rest_framework.decorators import list_route 5 | from rest_framework import permissions, viewsets, filters, status, mixins 6 | from django.contrib.auth import login, logout 7 | from django.shortcuts import get_object_or_404 8 | from django.http import Http404 9 | from rest_framework.response import Response 10 | from django.http import HttpResponse 11 | 12 | from lepus.permissions import IsClosed, IsStarted 13 | from .serializers import AuthSerializer, TeamSerializer, UserSerializer, QuestionSerializer, CategorySerializer, \ 14 | AnswerSerializer, NoticeSerializer, ConfigSerializer 15 | from .models import * 16 | 17 | 18 | class DynamicDepthMixins(object): 19 | def get_serializer_class(self, *args, **kwargs): 20 | serializer_class = super(DynamicDepthMixins, self).get_serializer_class() 21 | serializer_class.Meta.depth = 0 22 | 23 | if self.request.method == "GET": 24 | include = self.request.GET.get("include", "").lower() 25 | if include in ("1", "true"): 26 | serializer_class.Meta.depth = 1 27 | 28 | return serializer_class 29 | 30 | 31 | class AuthViewSet(viewsets.ViewSet): 32 | serializer_class = AuthSerializer 33 | 34 | def list(self, request, *args, **kwargs): 35 | if request.user.is_authenticated(): 36 | return Response(UserSerializer(request.user).data) 37 | 38 | return Response({"message": "Authentication is required.", "errors": [{"error": "unauthorized"}]}, 39 | status=status.HTTP_401_UNAUTHORIZED) 40 | 41 | def create(self, request, *args, **kwargs): 42 | if request.user.is_authenticated(): 43 | logout(request) 44 | 45 | serializer = self.serializer_class(data=request.data) 46 | if serializer.is_valid(raise_exception=True): 47 | login(request, serializer.get_user()) 48 | return self.list(request, *args, **kwargs) 49 | 50 | @list_route(methods=["post"]) 51 | def logout(self, request, *args, **kwargs): 52 | logout(request) 53 | return self.list(request, *args, **kwargs) 54 | 55 | 56 | class UserViewSet(mixins.CreateModelMixin, 57 | mixins.ListModelMixin, 58 | mixins.RetrieveModelMixin, 59 | DynamicDepthMixins, 60 | viewsets.GenericViewSet): 61 | serializer_class = UserSerializer 62 | queryset = User.objects.filter(id=-1) 63 | permission_classes = () 64 | 65 | def get_queryset(self): 66 | if self.request.user.is_authenticated(): 67 | return User.objects.filter(team=self.request.user.team) 68 | return self.queryset 69 | 70 | 71 | class CategoryViewSet(DynamicDepthMixins, viewsets.ReadOnlyModelViewSet): 72 | serializer_class = CategorySerializer 73 | queryset = serializer_class.Meta.model.objects.all() # FIXME:Questionが存在しないCategoryを隠す 74 | permission_classes = (permissions.IsAuthenticated,) 75 | 76 | 77 | class QuestionViewSet(DynamicDepthMixins, viewsets.ReadOnlyModelViewSet): 78 | serializer_class = QuestionSerializer 79 | queryset = serializer_class.Meta.model.objects.public().prefetch_related("flag_set", "file_set") 80 | permission_classes = (permissions.IsAuthenticated, IsStarted) 81 | 82 | filter_backends = (filters.DjangoFilterBackend,) 83 | filter_fields = ('category',) 84 | 85 | 86 | def download_file(request, file_id, filename=""): 87 | f = get_object_or_404(File.objects.public(), id=file_id) 88 | if filename != f.name: 89 | raise Http404 90 | mime_type = mimetypes.guess_type(f.file.name)[0] 91 | response = HttpResponse(content_type=mime_type) 92 | response['Content-Disposition'] = 'attachment; filename=%s' % f.name 93 | response.write(f.file.read()) 94 | return response 95 | 96 | 97 | class TeamViewSet(DynamicDepthMixins, viewsets.ReadOnlyModelViewSet): 98 | serializer_class = TeamSerializer 99 | queryset = serializer_class.Meta.model.objects.prefetch_related("answer_set", "attackpoint_set", "answer_set__flag", "answer_set__flag__question") 100 | permission_classes = (permissions.IsAuthenticated,) 101 | 102 | 103 | class AnswerViewSet(mixins.CreateModelMixin, 104 | DynamicDepthMixins, 105 | viewsets.GenericViewSet): 106 | serializer_class = AnswerSerializer 107 | queryset = Answer.objects.filter(id=-1) 108 | permission_classes = (permissions.IsAuthenticated, IsStarted, IsClosed) 109 | 110 | 111 | class NoticeViewSet(DynamicDepthMixins, viewsets.ReadOnlyModelViewSet): 112 | serializer_class = NoticeSerializer 113 | queryset = serializer_class.Meta.model.objects.filter(is_public=True) 114 | permission_classes = (permissions.AllowAny,) 115 | 116 | 117 | class ConfigViewSet(DynamicDepthMixins, viewsets.ReadOnlyModelViewSet): 118 | lookup_field = "key" 119 | serializer_class = ConfigSerializer 120 | queryset = serializer_class.Meta.model.objects.all() 121 | permission_classes = (permissions.AllowAny,) 122 | 123 | 124 | # TODO:AttackPointのAPIを開発する 125 | -------------------------------------------------------------------------------- /src/lepus/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for lepus project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lepus.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lepus.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | --------------------------------------------------------------------------------