diff --git a/latest version/api/api_code.py b/latest version/api/api_code.py index 0f4fca8..50e76ea 100644 --- a/latest version/api/api_code.py +++ b/latest version/api/api_code.py @@ -20,6 +20,7 @@ CODE_MSG = { -202: 'User is banned', -203: 'Username exists', -204: 'Email address exists', + -205: 'Too many login attempts', -999: 'Unknown error' } diff --git a/latest version/core/api_user.py b/latest version/core/api_user.py index ae32aee..5e84fdb 100644 --- a/latest version/core/api_user.py +++ b/latest version/core/api_user.py @@ -2,7 +2,9 @@ from hashlib import sha256 from os import urandom from time import time -from .error import NoAccess, NoData, UserBan +from .config_manager import Config +from .error import NoAccess, NoData, RateLimit, UserBan +from .limiter import ArcLimiter from .user import UserOnline @@ -57,6 +59,8 @@ class Role: class APIUser(UserOnline): + limiter = ArcLimiter(Config.API_LOGIN_RATE_LIMIT, 'api_login') + def __init__(self, c=None, user_id=None) -> None: super().__init__(c, user_id) self.api_token: str = None @@ -109,6 +113,9 @@ class APIUser(UserOnline): self.password = password if ip is not None: self.ip = ip + if not self.limiter.hit(name): + raise RateLimit('Too many login attempts', api_error_code=-205) + self.c.execute('''select user_id, password from user where name = :a''', { 'a': self.name}) x = self.c.fetchone() diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 96b3de2..1b60079 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -6,6 +6,10 @@ class Config: HOST = '0.0.0.0' PORT = 80 + USE_GEVENT_WSGI = False + USE_PROXY_FIX = False + USE_CORS = False + GAME_API_PREFIX = '/join/21' ALLOW_APPVERSION = [] # list[str] @@ -36,6 +40,9 @@ class Config: DOWNLOAD_LINK_PREFIX = '' + DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT = False + NGINX_X_ACCEL_REDIRECT_PREFIX = '/nginx_download/' + DOWNLOAD_TIMES_LIMIT = 3000 DOWNLOAD_TIME_GAP_LIMIT = 1000 @@ -69,6 +76,9 @@ class Config: SONGLIST_FILE_PATH = './database/songs/songlist' SQLITE_DATABASE_PATH = './database/arcaea_database.db' + GAME_LOGIN_RATE_LIMIT = '30/5 minutes' + API_LOGIN_RATE_LIMIT = '10/5 minutes' + class ConfigManager: diff --git a/latest version/core/limiter.py b/latest version/core/limiter.py index bdabed9..4cc6245 100644 --- a/latest version/core/limiter.py +++ b/latest version/core/limiter.py @@ -7,12 +7,12 @@ class ArcLimiter: strategy = strategies.FixedWindowRateLimiter(storage) def __init__(self, limit_str: str = None, namespace: str = None): - self._limits = None + self._limits: list = None self.limits = limit_str self.namespace = namespace @property - def limits(self): + def limits(self) -> list: return self._limits @limits.setter diff --git a/latest version/core/user.py b/latest version/core/user.py index f04c29b..75a0bb9 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -7,8 +7,9 @@ from .character import UserCharacter, UserCharacterList from .config_manager import Config from .constant import Constant from .error import (ArcError, DataExist, FriendError, InputError, NoAccess, - NoData, UserBan) + NoData, RateLimit, UserBan) from .item import UserItemList +from .limiter import ArcLimiter from .score import Score from .sql import Connect from .world import Map, UserMap, UserStamina @@ -143,6 +144,8 @@ class UserRegister(User): class UserLogin(User): # 密码和token的加密方式为 SHA-256 + limiter = ArcLimiter(Config.GAME_LOGIN_RATE_LIMIT, 'game_login') + def __init__(self, c) -> None: super().__init__() self.c = c @@ -219,6 +222,9 @@ class UserLogin(User): if ip: self.set_ip(ip) + if not self.limiter.hit(name): + raise RateLimit('Too many login attempts.', 123) + self.c.execute('''select user_id, password, ban_flag from user where name = :name''', { 'name': self.name}) x = self.c.fetchone() diff --git a/latest version/main.py b/latest version/main.py index 2081e1a..689ff93 100644 --- a/latest version/main.py +++ b/latest version/main.py @@ -12,6 +12,7 @@ from flask import Flask, make_response, request, send_from_directory from core.config_manager import Config, ConfigManager if os.path.exists('config.py') or os.path.exists('config'): + # 导入用户自定义配置 ConfigManager.load(import_module('config').Config) @@ -29,10 +30,14 @@ from server.func import error_return app = Flask(__name__) -# from werkzeug.middleware.proxy_fix import ProxyFix -# app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) -# from flask_cors import CORS -# CORS(app, supports_credentials=True) +if Config.USE_PROXY_FIX: + # 代理修复 + from werkzeug.middleware.proxy_fix import ProxyFix + app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) +if Config.USE_CORS: + # 服务端跨域 + from flask_cors import CORS + CORS(app, supports_credentials=True) os.chdir(sys.path[0]) # 更改工作路径,以便于愉快使用相对路径 @@ -73,10 +78,12 @@ def download(file_path): if not x.is_valid: raise NoAccess('Expired token.') x.download_hit() - # response = make_response() - # response.headers['Content-Type'] = 'application/octet-stream' - # response.headers['X-Accel-Redirect'] = '/nginx_download/' + file_path - # return response + if Config.DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT: + # nginx X-Accel-Redirect + response = make_response() + response.headers['Content-Type'] = 'application/octet-stream' + response.headers['X-Accel-Redirect'] = Config.NGINX_X_ACCEL_REDIRECT_PREFIX + file_path + return response return send_from_directory(Constant.SONG_FILE_FOLDER_PATH, file_path, as_attachment=True, conditional=True) except ArcError as e: if Config.ALLOW_WARNING_LOG: @@ -86,9 +93,12 @@ def download(file_path): def tcp_server_run(): - if False: + if Config.USE_GEVENT_WSGI: + # 异步 gevent WSGI server + host_port = (Config.HOST, Config.PORT) + app.logger.info('Running gevent WSGI server... (%s:%s)' % host_port) from gevent.pywsgi import WSGIServer - WSGIServer(("127.0.0.1", 5000), app).serve_forever() + WSGIServer(host_port, app).serve_forever() else: if Config.SSL_CERT and Config.SSL_KEY: app.run(Config.HOST, Config.PORT, ssl_context=( diff --git a/latest version/server/init.py b/latest version/server/init.py index 304327f..4e2e22f 100644 --- a/latest version/server/init.py +++ b/latest version/server/init.py @@ -1,7 +1,8 @@ import os from shutil import copy, copy2 + from core.sql import Connect -from database.database_initialize import main, ARCAEA_SERVER_VERSION +from database.database_initialize import ARCAEA_SERVER_VERSION, main from web.system import update_database diff --git a/latest version/setting.py b/latest version/setting.py deleted file mode 100644 index 3dd9bc8..0000000 --- a/latest version/setting.py +++ /dev/null @@ -1,258 +0,0 @@ -class Config(): - ''' - This is the setting file. You can change some parameters here. - ''' - - ''' - -------------------- - 主机的地址和端口号 - Host and port of your server - ''' - HOST = '0.0.0.0' - PORT = '80' - ''' - -------------------- - ''' - - ''' - -------------------- - 游戏API地址前缀 - Game API's URL prefix - ''' - GAME_API_PREFIX = '/join/21' - ''' - -------------------- - ''' - - ''' - -------------------- - 允许使用的游戏版本,若为空,则默认全部允许 - Allowed game versions - If it is blank, all are allowed. - ''' - ALLOW_APPVERSION = ['3.12.6', '3.12.6c', - '4.0.256', '4.0.256c', '4.1.0', '4.1.0c'] - ''' - -------------------- - ''' - - ''' - -------------------- - 联机功能相关设置,请确保与Link Play服务器端的设置一致 - Setting of your link play server - Please ensure that the settings on the side of Link Play server are consistent. - ''' - # SET_LINKPLAY_SERVER_AS_SUB_PROCESS: 是否同时在本地启动Link Play服务器 - # SET_LINKPLAY_SERVER_AS_SUB_PROCESS: If it is `True`, the link play server will run with the main server locally at the same time. - SET_LINKPLAY_SERVER_AS_SUB_PROCESS = True - # LINKPLAY_HOST: 对主服务器来说的Link Play服务器的地址 - # LINKPLAY_HOST: The address of the linkplay server based on the main server. If it is blank, the link play feature will be disabled. - LINKPLAY_HOST = '0.0.0.0' - LINKPLAY_UDP_PORT = 10900 - LINKPLAY_TCP_PORT = 10901 - LINKPLAY_AUTHENTICATION = 'my_link_play_server' - # LINKPLAY_DISPLAY_HOST: 对客户端来说的Link Play服务器地址,如果为空,则自动获取 - # LINKPLAY_DISPLAY_HOST: The address of the linkplay server based on the client. If it is blank, the host of link play server for the client will be obtained automatically. - LINKPLAY_DISPLAY_HOST = '' - ''' - -------------------- - ''' - - ''' - -------------------- - SSL证书路径 - 留空则使用HTTP - SSL certificate path - If left blank, use HTTP. - ''' - SSL_CERT = '' # *.pem - SSL_KEY = '' # *.key - ''' - -------------------- - ''' - - ''' - -------------------- - 愚人节模式开关 - Switch of April Fool's Day - ''' - IS_APRILFOOLS = True - ''' - -------------------- - ''' - - ''' - -------------------- - 世界排名的最大显示数量 - The largest number of global rank - ''' - WORLD_RANK_MAX = 200 - ''' - -------------------- - ''' - - ''' - -------------------- - 世界模式当前活动图设置 - Current available maps in world mode - ''' - AVAILABLE_MAP = [] # Ex. ['test', 'test2'] - ''' - -------------------- - ''' - - ''' - -------------------- - Web后台管理页面的用户名和密码 - Username and password of web background management page - ''' - USERNAME = 'admin' - PASSWORD = 'admin' - ''' - -------------------- - ''' - - ''' - -------------------- - Web后台管理页面的session秘钥,如果不知道是什么,请不要修改 - Session key of web background management page - If you don't know what it is, please don't modify it. - ''' - SECRET_KEY = '1145141919810' - ''' - -------------------- - ''' - - ''' - -------------------- - API接口完全控制权限Token,留空则不使用 - API interface full control permission Token - If you don't want to use it, leave it blank. - ''' - API_TOKEN = '' - ''' - -------------------- - ''' - - ''' - -------------------- - 歌曲下载地址前缀,留空则自动获取 - Song download address prefix - If left blank, it will be obtained automatically. - ''' - DOWNLOAD_LINK_PREFIX = '' # http://***.com/download/ - ''' - -------------------- - ''' - - ''' - -------------------- - 玩家歌曲下载的24小时次数限制,每个文件算一次 - Player's song download limit times in 24 hours, once per file - ''' - DOWNLOAD_TIMES_LIMIT = 3000 - ''' - 歌曲下载链接的有效时长,单位:秒 - Effective duration of song download link, unit: seconds - ''' - DOWNLOAD_TIME_GAP_LIMIT = 1000 - ''' - -------------------- - ''' - - ''' - -------------------- - Arcaea登录的最大允许设备数量,最小值为1 - The maximum number of devices allowed to log in Arcaea, minimum: 1 - ''' - LOGIN_DEVICE_NUMBER_LIMIT = 1 - ''' - 是否允许同设备多应用共存登录 - 请注意,这个选项设置为True时,下一个选项将自动变为False - If logging in from multiple applications on the same device is allowed - Note that when this option is set to True, the next option automatically becomes False - ''' - ALLOW_LOGIN_SAME_DEVICE = False - ''' - 24小时内登陆设备数超过最大允许设备数量时,是否自动封号(1天、3天、7天、15天、31天) - When the number of login devices exceeds the maximum number of devices allowed to log in Arcaea within 24 hours, whether the account will be automatically banned (1 day, 3 days, 7 days, 15 days, 31 days) - ''' - ALLOW_BAN_MULTIDEVICE_USER_AUTO = True - ''' - -------------------- - ''' - - ''' - -------------------- - 是否记录详细的服务器日志 - If recording detailed server logs is enabled - ''' - ALLOW_INFO_LOG = False - ALLOW_WARNING_LOG = False - ''' - -------------------- - ''' - - ''' - -------------------- - 用户注册时的默认记忆源点数量 - The default amount of memories at the time of user registration - ''' - DEFAULT_MEMORIES = 0 - ''' - -------------------- - ''' - - ''' - -------------------- - 数据库更新时,是否采用最新的角色数据,如果你想采用最新的官方角色数据 - 注意:如果是,旧的数据将丢失;如果否,某些角色的数据变动将无法同步 - If using the latest character data when updating database. If you want to only keep newest official character data, please set it `True`. - Note: If `True`, the old data will be lost; If `False`, the data changes of some characters will not be synchronized. - ''' - UPDATE_WITH_NEW_CHARACTER_DATA = True - ''' - -------------------- - ''' - - ''' - -------------------- - 是否全解锁搭档 - If unlocking all partners is enabled - ''' - CHARACTER_FULL_UNLOCK = True - ''' - -------------------- - ''' - - ''' - -------------------- - 是否全解锁世界歌曲 - If unlocking all world songs is enabled - ''' - WORLD_SONG_FULL_UNLOCK = True - ''' - -------------------- - ''' - - ''' - -------------------- - 是否全解锁世界场景 - If unlocking all world sceneries is enabled - ''' - WORLD_SCENERY_FULL_UNLOCK = True - ''' - -------------------- - ''' - - ''' - -------------------- - 是否强制使用全解锁云端存档 - If forcing full unlocked cloud save is enabled - 请注意,当前对于最终结局的判定为固定在`Testify`解锁之前 - Please note that the current setting of the finale state is before the unlock of `Testify` - ''' - SAVE_FULL_UNLOCK = False - ''' - -------------------- - '''