-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathgameServer.py
More file actions
219 lines (166 loc) · 7.28 KB
/
gameServer.py
File metadata and controls
219 lines (166 loc) · 7.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import logging
import mimetypes
import os
# Import the Flask webserver
from flask import Flask
from markupsafe import escape
from werkzeug import Response
# import the other modules, belonging to this app
import app.config as gameConfig
from app.model.LevelLoader.JsonLevelList import JsonLevelList
import app.router.routerGame as routerGame
from app.authentication import auth, populate_data
# import all routes, belonging to this app
import app.router.routerStatic as routerStatic
from app.model.GroupStats import GroupStats
from app.prometheusMetrics import ServerMetrics
from app.storage.ParticipantLogger import ParticipantLogger
from app.storage.crashReport import openCrashReporterFile
from app.storage.database import ReverSimDatabase
from app.storage.modelFormatError import ModelFormatError
from app.storage.participantScreenshots import ScreenshotWriter
from app.utilsGame import safe_join
# Fix mime type on Windows https://github.com/pallets/flask/issues/1045
mimetypes.add_type('text/css', '.css')
mimetypes.add_type('text/html', '.html')
mimetypes.add_type('text/javascript', '.js')
mimetypes.add_type('image/png', '.png')
mimetypes.add_type('image/svg+xml', '.svg')
mimetypes.add_type('application/json', '.json')
class DefaultFlaskSettings:
"""The default settings that are applied if not overridden by a file.
You can specify the path to the Flask config with the `FLASK_CONFIG`
environment variable.
"""
# Limit the upload file size
MAX_CONTENT_LENGTH = 2 * 1024 * 1024 # 2MB max content length
MAX_FORM_MEMORY_SIZE = MAX_CONTENT_LENGTH
SQLALCHEMY_DATABASE_URI = "sqlite+pysqlite:///statistics/reversim.db" # instance/statistics/reversim.db
SQLALCHEMY_ECHO = False
SQLALCHEMY_ENGINE_OPTIONS = {
"connect_args": {
# time in seconds after which Database Is Locked will be thrown
"timeout": 10 # TODO Choose short in production to recover faster from deadlocks
}
}
def createCrashReporter(app: Flask):
# If crash reporter is enabled, open file to write client errors into
if gameConfig.getInt('crashReportLevel') > 0:
crashReporterFilePath: str = safe_join(
app.instance_path,
app.config.get('CLIENT_ERROR_LOG', 'statistics/crash_reporter.log') # type: ignore
)
openCrashReporterFile(
crashReporterFilePath,
gameConfig.getGroupsDisabledErrorLogging(),
errorLevel=gameConfig.getInt('crashReportLevel')
)
def initLegacyLogFile(app: Flask):
"""Init the Legacy Logfile writer, create the necessary folder structure"""
ParticipantLogger.baseFolder = os.path.join(
app.instance_path, "statistics/LogFiles"
)
try:
os.makedirs(ParticipantLogger.baseFolder, exist_ok=True)
except Exception:
logging.exception(f'Unable to create folder "{ParticipantLogger.baseFolder}"')
def initScreenshotWriter(app: Flask):
"""Init the Screenshot writer, create the necessary folder structure"""
ScreenshotWriter.screenshotFolder = os.path.join(
app.instance_path, "statistics/canvasPics"
)
try:
os.makedirs(ScreenshotWriter.screenshotFolder, exist_ok=True)
except Exception:
logging.exception(f'Unable to create folder "{ScreenshotWriter.screenshotFolder}"')
def createMinimalApp():
""""""
instancePath = os.environ.get("REVERSIM_INSTANCE", "./instance")
# Start the webserver
app = Flask(__name__,
static_url_path='',
static_folder='./static',
template_folder='./templates',
instance_path=os.path.abspath(instancePath),
instance_relative_config=True
)
# Load config object and then override with config file if it exists
# NOTE config.from_object does not accept a Dict, only a class!
app.config.from_object(DefaultFlaskSettings)
#app.config.from_envvar('FLASK_CONFIG', silent=True) # TODO Allow overriding Flask config
# Load game config
REVERSIM_CONF = os.environ.get("REVERSIM_CONFIG", "conf/gameConfig.json")
gameConfig.loadGameConfig(REVERSIM_CONF, app.instance_path)
JsonLevelList.singleton = JsonLevelList.fromFile(instanceFolder=app.instance_path)
return app
# Create the webserver
def createApp():
"""Flask app factory pattern"""
logging.basicConfig(
level=logging.INFO,
)
app = createMinimalApp()
# Init Flask Routes
routerStatic.initAssetRouter()
app.register_blueprint(routerStatic.routerStatic)
app.register_blueprint(routerGame.routerGame)
app.register_blueprint(routerStatic.routerAssets)
logging.info(f'Instance path: {app.instance_path}')
# Init the Database
ReverSimDatabase.createDatabase(app)
# Generate the default Bearer token for the /metrics endpoint
with app.app_context():
populate_data(instance_path=app.instance_path)
# Init the Legacy logger and Screenshots
initScreenshotWriter(app)
initLegacyLogFile(app)
# Init Prometheus (must be done before Flask context is created)
try:
ServerMetrics.createPrometheus(app, auth_provider=auth.login_required) # type: ignore
except Exception as e:
logging.warning(f'The Prometheus metrics failed to initialize: "{e}"')
logging.warning('This can be safely ignored when not in production')
return app
flaskInstance = createApp()
@flaskInstance.before_request
def post_flask_init():
"""Called after the flask application context was created
Have to resort to a `app.before_request` hook that gets deleted, since the Flask team
decided to remove the only method that allowed a post init hook to be implemented.
https://stackoverflow.com/a/77949082
And no just using `with app.app_context():` inside of `createApp()` is not feasible,
as some things must not be loaded when using e.g. the `flask db` cli.
"""
logging.info("Running post-init")
flaskInstance.before_request_funcs[None].remove(post_flask_init)
# GroupStats depends on database, so upgrades must be done by now
with flaskInstance.app_context():
GroupStats.createGroupCounters()
# Init Crash reporter (depends on Prometheus)
createCrashReporter(flaskInstance)
# set response headers
@flaskInstance.after_request # type: ignore
def apply_caching(response: Response) -> Response:
"""Apply response caching headers to every request to protect against XSS attacks"""
# Prevents external sites from embedding your site in an iframe
#response.headers["X-Frame-Options"] = "SAMEORIGIN"
# Tells the browser to convert all HTTP requests to HTTPS, preventing man-in-the-middle (MITM) attacks.
#response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
# Tell the browser where it can load various types of resource from
#response.headers['Content-Security-Policy'] = "default-src 'self'"
# Forces the browser to honor the response content type instead of trying to detect it,
# which can be abused to generate a cross-site scripting (XSS) attack.
#response.headers['X-Content-Type-Options'] = 'nosniff'
# The browser will try to prevent reflected XSS attacks by not loading the page if the request
# contains something that looks like JavaScript and the response contains the same data.
response.headers['X-XSS-Protection'] = '1; mode=block'
return response
@flaskInstance.errorhandler(ModelFormatError)
def handle_model_format_errors(e: Exception):
"""Throw a meaningful error if something is wrong in the gameConfig.json, or if the
player is trying to access an unknown group"""
# NOTE: The escape method is necessary to prevent injection attacks!
return escape(e), 500
# If the script is run from the command line, start the local flask debug server
if __name__ == "__main__":
flaskInstance.run()