mirror of
https://github.com/Lost-MSth/Arcaea-server.git
synced 2026-02-08 00:37:38 +08:00
[Enhance] Some config options & Login limiter
- Add limiter for login and API login - Add some config options - Delete `setting.py` files
This commit is contained in:
@@ -20,6 +20,7 @@ CODE_MSG = {
|
|||||||
-202: 'User is banned',
|
-202: 'User is banned',
|
||||||
-203: 'Username exists',
|
-203: 'Username exists',
|
||||||
-204: 'Email address exists',
|
-204: 'Email address exists',
|
||||||
|
-205: 'Too many login attempts',
|
||||||
-999: 'Unknown error'
|
-999: 'Unknown error'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ from hashlib import sha256
|
|||||||
from os import urandom
|
from os import urandom
|
||||||
from time import time
|
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
|
from .user import UserOnline
|
||||||
|
|
||||||
|
|
||||||
@@ -57,6 +59,8 @@ class Role:
|
|||||||
|
|
||||||
|
|
||||||
class APIUser(UserOnline):
|
class APIUser(UserOnline):
|
||||||
|
limiter = ArcLimiter(Config.API_LOGIN_RATE_LIMIT, 'api_login')
|
||||||
|
|
||||||
def __init__(self, c=None, user_id=None) -> None:
|
def __init__(self, c=None, user_id=None) -> None:
|
||||||
super().__init__(c, user_id)
|
super().__init__(c, user_id)
|
||||||
self.api_token: str = None
|
self.api_token: str = None
|
||||||
@@ -109,6 +113,9 @@ class APIUser(UserOnline):
|
|||||||
self.password = password
|
self.password = password
|
||||||
if ip is not None:
|
if ip is not None:
|
||||||
self.ip = ip
|
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''', {
|
self.c.execute('''select user_id, password from user where name = :a''', {
|
||||||
'a': self.name})
|
'a': self.name})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ class Config:
|
|||||||
HOST = '0.0.0.0'
|
HOST = '0.0.0.0'
|
||||||
PORT = 80
|
PORT = 80
|
||||||
|
|
||||||
|
USE_GEVENT_WSGI = False
|
||||||
|
USE_PROXY_FIX = False
|
||||||
|
USE_CORS = False
|
||||||
|
|
||||||
GAME_API_PREFIX = '/join/21'
|
GAME_API_PREFIX = '/join/21'
|
||||||
|
|
||||||
ALLOW_APPVERSION = [] # list[str]
|
ALLOW_APPVERSION = [] # list[str]
|
||||||
@@ -36,6 +40,9 @@ class Config:
|
|||||||
|
|
||||||
DOWNLOAD_LINK_PREFIX = ''
|
DOWNLOAD_LINK_PREFIX = ''
|
||||||
|
|
||||||
|
DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT = False
|
||||||
|
NGINX_X_ACCEL_REDIRECT_PREFIX = '/nginx_download/'
|
||||||
|
|
||||||
DOWNLOAD_TIMES_LIMIT = 3000
|
DOWNLOAD_TIMES_LIMIT = 3000
|
||||||
DOWNLOAD_TIME_GAP_LIMIT = 1000
|
DOWNLOAD_TIME_GAP_LIMIT = 1000
|
||||||
|
|
||||||
@@ -69,6 +76,9 @@ class Config:
|
|||||||
SONGLIST_FILE_PATH = './database/songs/songlist'
|
SONGLIST_FILE_PATH = './database/songs/songlist'
|
||||||
SQLITE_DATABASE_PATH = './database/arcaea_database.db'
|
SQLITE_DATABASE_PATH = './database/arcaea_database.db'
|
||||||
|
|
||||||
|
GAME_LOGIN_RATE_LIMIT = '30/5 minutes'
|
||||||
|
API_LOGIN_RATE_LIMIT = '10/5 minutes'
|
||||||
|
|
||||||
|
|
||||||
class ConfigManager:
|
class ConfigManager:
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ class ArcLimiter:
|
|||||||
strategy = strategies.FixedWindowRateLimiter(storage)
|
strategy = strategies.FixedWindowRateLimiter(storage)
|
||||||
|
|
||||||
def __init__(self, limit_str: str = None, namespace: str = None):
|
def __init__(self, limit_str: str = None, namespace: str = None):
|
||||||
self._limits = None
|
self._limits: list = None
|
||||||
self.limits = limit_str
|
self.limits = limit_str
|
||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def limits(self):
|
def limits(self) -> list:
|
||||||
return self._limits
|
return self._limits
|
||||||
|
|
||||||
@limits.setter
|
@limits.setter
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ from .character import UserCharacter, UserCharacterList
|
|||||||
from .config_manager import Config
|
from .config_manager import Config
|
||||||
from .constant import Constant
|
from .constant import Constant
|
||||||
from .error import (ArcError, DataExist, FriendError, InputError, NoAccess,
|
from .error import (ArcError, DataExist, FriendError, InputError, NoAccess,
|
||||||
NoData, UserBan)
|
NoData, RateLimit, UserBan)
|
||||||
from .item import UserItemList
|
from .item import UserItemList
|
||||||
|
from .limiter import ArcLimiter
|
||||||
from .score import Score
|
from .score import Score
|
||||||
from .sql import Connect
|
from .sql import Connect
|
||||||
from .world import Map, UserMap, UserStamina
|
from .world import Map, UserMap, UserStamina
|
||||||
@@ -143,6 +144,8 @@ class UserRegister(User):
|
|||||||
|
|
||||||
class UserLogin(User):
|
class UserLogin(User):
|
||||||
# 密码和token的加密方式为 SHA-256
|
# 密码和token的加密方式为 SHA-256
|
||||||
|
limiter = ArcLimiter(Config.GAME_LOGIN_RATE_LIMIT, 'game_login')
|
||||||
|
|
||||||
def __init__(self, c) -> None:
|
def __init__(self, c) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.c = c
|
self.c = c
|
||||||
@@ -219,6 +222,9 @@ class UserLogin(User):
|
|||||||
if ip:
|
if ip:
|
||||||
self.set_ip(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''', {
|
self.c.execute('''select user_id, password, ban_flag from user where name = :name''', {
|
||||||
'name': self.name})
|
'name': self.name})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from flask import Flask, make_response, request, send_from_directory
|
|||||||
from core.config_manager import Config, ConfigManager
|
from core.config_manager import Config, ConfigManager
|
||||||
|
|
||||||
if os.path.exists('config.py') or os.path.exists('config'):
|
if os.path.exists('config.py') or os.path.exists('config'):
|
||||||
|
# 导入用户自定义配置
|
||||||
ConfigManager.load(import_module('config').Config)
|
ConfigManager.load(import_module('config').Config)
|
||||||
|
|
||||||
|
|
||||||
@@ -29,10 +30,14 @@ from server.func import error_return
|
|||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# from werkzeug.middleware.proxy_fix import ProxyFix
|
if Config.USE_PROXY_FIX:
|
||||||
# app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
|
# 代理修复
|
||||||
# from flask_cors import CORS
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
# CORS(app, supports_credentials=True)
|
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]) # 更改工作路径,以便于愉快使用相对路径
|
os.chdir(sys.path[0]) # 更改工作路径,以便于愉快使用相对路径
|
||||||
@@ -73,10 +78,12 @@ def download(file_path):
|
|||||||
if not x.is_valid:
|
if not x.is_valid:
|
||||||
raise NoAccess('Expired token.')
|
raise NoAccess('Expired token.')
|
||||||
x.download_hit()
|
x.download_hit()
|
||||||
# response = make_response()
|
if Config.DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT:
|
||||||
# response.headers['Content-Type'] = 'application/octet-stream'
|
# nginx X-Accel-Redirect
|
||||||
# response.headers['X-Accel-Redirect'] = '/nginx_download/' + file_path
|
response = make_response()
|
||||||
# return 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)
|
return send_from_directory(Constant.SONG_FILE_FOLDER_PATH, file_path, as_attachment=True, conditional=True)
|
||||||
except ArcError as e:
|
except ArcError as e:
|
||||||
if Config.ALLOW_WARNING_LOG:
|
if Config.ALLOW_WARNING_LOG:
|
||||||
@@ -86,9 +93,12 @@ def download(file_path):
|
|||||||
|
|
||||||
|
|
||||||
def tcp_server_run():
|
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
|
from gevent.pywsgi import WSGIServer
|
||||||
WSGIServer(("127.0.0.1", 5000), app).serve_forever()
|
WSGIServer(host_port, app).serve_forever()
|
||||||
else:
|
else:
|
||||||
if Config.SSL_CERT and Config.SSL_KEY:
|
if Config.SSL_CERT and Config.SSL_KEY:
|
||||||
app.run(Config.HOST, Config.PORT, ssl_context=(
|
app.run(Config.HOST, Config.PORT, ssl_context=(
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
from shutil import copy, copy2
|
from shutil import copy, copy2
|
||||||
|
|
||||||
from core.sql import Connect
|
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
|
from web.system import update_database
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
'''
|
|
||||||
--------------------
|
|
||||||
'''
|
|
||||||
Reference in New Issue
Block a user