def run_upgrade(): upgrade() utils.set_config('ctf_version', __version__)
def create_app(config='CTFd.config.Config'): app = CTFdFlask(__name__) with app.app_context(): app.config.from_object(config) theme_loader = ThemeLoader(os.path.join(app.root_path, 'themes'), followlinks=True) app.jinja_loader = theme_loader from CTFd.models import db, Teams, Solves, Challenges, Fails, Flags, Tags, Files, Tracking url = create_database() # This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in # This is mostly so we can force MySQL's charset app.config['SQLALCHEMY_DATABASE_URI'] = str(url) # Register database db.init_app(app) # Register Flask-Migrate migrations.init_app(app, db) # Alembic sqlite support is lacking so we should just create_all anyway if url.drivername.startswith('sqlite'): db.create_all() stamp() else: # This creates tables instead of db.create_all() # Allows migrations to happen properly upgrade() from CTFd.models import ma ma.init_app(app) app.db = db app.VERSION = __version__ from CTFd.cache import cache cache.init_app(app) app.cache = cache # If you have multiple workers you must have a shared cache socketio.init_app(app, async_mode=app.config.get('SOCKETIO_ASYNC_MODE'), message_queue=app.config.get('CACHE_REDIS_URL')) if app.config.get('REVERSE_PROXY'): app.wsgi_app = ProxyFix(app.wsgi_app) version = utils.get_config('ctf_version') # Upgrading from an older version of CTFd if version and (StrictVersion(version) < StrictVersion(__version__)): if confirm_upgrade(): run_upgrade() else: exit() if not version: utils.set_config('ctf_version', __version__) if not utils.get_config('ctf_theme'): utils.set_config('ctf_theme', 'core') update_check(force=True) init_request_processors(app) init_template_filters(app) init_template_globals(app) # Importing here allows tests to use sensible names (e.g. api instead of api_bp) from CTFd.views import views from CTFd.teams import teams from CTFd.users import users from CTFd.challenges import challenges from CTFd.scoreboard import scoreboard from CTFd.auth import auth from CTFd.admin import admin from CTFd.api import api from CTFd.events import events from CTFd.errors import page_not_found, forbidden, general_error, gateway_error app.register_blueprint(views) app.register_blueprint(teams) app.register_blueprint(users) app.register_blueprint(challenges) app.register_blueprint(scoreboard) app.register_blueprint(auth) app.register_blueprint(api) app.register_blueprint(events) app.register_blueprint(admin) app.register_error_handler(404, page_not_found) app.register_error_handler(403, forbidden) app.register_error_handler(500, general_error) app.register_error_handler(502, gateway_error) init_plugins(app) return app
def import_ctf(backup, erase=True): if not zipfile.is_zipfile(backup): raise zipfile.BadZipfile if erase: drop_database() create_database() # We explicitly do not want to upgrade or stamp here. # The import will have this information. side_db = dataset.connect(get_app_config('SQLALCHEMY_DATABASE_URI')) sqlite = get_app_config('SQLALCHEMY_DATABASE_URI').startswith('sqlite') postgres = get_app_config('SQLALCHEMY_DATABASE_URI').startswith('postgres') backup = zipfile.ZipFile(backup) members = backup.namelist() max_content_length = get_app_config('MAX_CONTENT_LENGTH') for f in members: if f.startswith('/') or '..' in f: # Abort on malicious zip files raise zipfile.BadZipfile info = backup.getinfo(f) if max_content_length: if info.file_size > max_content_length: raise zipfile.LargeZipFile first = [ 'db/teams.json', 'db/users.json', 'db/challenges.json', 'db/dynamic_challenge.json', 'db/flags.json', 'db/hints.json', 'db/unlocks.json', 'db/awards.json', 'db/tags.json', 'db/submissions.json', 'db/solves.json', 'db/files.json', 'db/notifications.json', 'db/pages.json', 'db/tracking.json', 'db/config.json', ] for item in first: if item in members: members.remove(item) members = first + members alembic_version = json.loads( backup.open( 'db/alembic_version.json').read())["results"][0]["version_num"] upgrade(revision=alembic_version) members.remove('db/alembic_version.json') for member in members: if member.startswith('db/'): table_name = member[3:-5] try: # Try to open a file but skip if it doesn't exist. data = backup.open(member).read() except KeyError: continue if data: table = side_db[table_name] saved = json.loads(data) for entry in saved['results']: # This is a hack to get SQLite to properly accept datetime values from dataset # See Issue #246 if sqlite: for k, v in entry.items(): if isinstance(v, six.string_types): match = re.match( r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d", v) if match: entry[k] = datetime.datetime.strptime( v, '%Y-%m-%dT%H:%M:%S.%f') continue match = re.match( r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}", v) if match: entry[k] = datetime.datetime.strptime( v, '%Y-%m-%dT%H:%M:%S') continue table.insert(entry) db.session.commit() if postgres: # TODO: This should be sanitized even though exports are basically SQL dumps # Databases are so hard # https://stackoverflow.com/a/37972960 side_db.engine.execute( "SELECT setval(pg_get_serial_sequence('{table_name}', 'id'), coalesce(max(id)+1,1), false) FROM {table_name}" .format(table_name=table_name)) # Extracting files files = [f for f in backup.namelist() if f.startswith('uploads/')] uploader = get_uploader() for f in files: filename = f.split(os.sep, 1) if len(filename ) < 2: # just an empty uploads directory (e.g. uploads/) continue filename = filename[ 1] # Get the second entry in the list (the actual filename) source = backup.open(f) uploader.store(fileobj=source, filename=filename) cache.clear()