diff --git a/.gitignore b/.gitignore index 3738d16..02b4e03 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__/ *.pyc *.pem *.conf +*.key config.json test/static flask_session/ diff --git a/app/__init__.py b/app/__init__.py index fefa1d9..cdcf95e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,9 +3,9 @@ from app.request import send_tor_signal from app.utils.session import generate_user_key from app.utils.bangs import gen_bangs_json from app.utils.misc import gen_file_hash, read_config_bool +from base64 import b64encode from datetime import datetime, timedelta from flask import Flask -from flask_session import Session import json import logging.config import os @@ -20,24 +20,15 @@ app = Flask(__name__, static_folder=os.path.dirname( app.wsgi_app = ProxyFix(app.wsgi_app) +dot_env_path = ( + os.path.join(os.path.dirname(os.path.abspath(__file__)), + '../whoogle.env')) + # Load .env file if enabled if os.getenv('WHOOGLE_DOTENV', ''): - dotenv_path = '../whoogle.env' - load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), - dotenv_path)) + load_dotenv(dot_env_path) -# Session values -# NOTE: SESSION_COOKIE_SAMESITE must be set to 'lax' to allow the user's -# previous session to persist when accessing the instance from an external -# link. Setting this value to 'strict' causes Whoogle to revalidate a new -# session, and fail, resulting in cookies being disabled. -# -# This could be re-evaluated if Whoogle ever switches to client side -# configuration instead. app.default_key = generate_user_key() -app.config['SECRET_KEY'] = os.urandom(32) -app.config['SESSION_TYPE'] = 'filesystem' -app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' if os.getenv('HTTPS_ONLY'): app.config['SESSION_COOKIE_NAME'] = '__Secure-session' @@ -86,6 +77,36 @@ app.config['BANG_FILE'] = os.path.join( app.config['BANG_PATH'], 'bangs.json') +# Ensure all necessary directories exist +if not os.path.exists(app.config['CONFIG_PATH']): + os.makedirs(app.config['CONFIG_PATH']) + +if not os.path.exists(app.config['SESSION_FILE_DIR']): + os.makedirs(app.config['SESSION_FILE_DIR']) + +if not os.path.exists(app.config['BANG_PATH']): + os.makedirs(app.config['BANG_PATH']) + +if not os.path.exists(app.config['BUILD_FOLDER']): + os.makedirs(app.config['BUILD_FOLDER']) + +# Session values +app_key_path = os.path.join(app.config['CONFIG_PATH'], 'whoogle.key') +if os.path.exists(app_key_path): + app.config['SECRET_KEY'] = open(app_key_path, 'r').read() +else: + app.config['SECRET_KEY'] = str(b64encode(os.urandom(32))) + with open(app_key_path, 'w') as key_file: + key_file.write(app.config['SECRET_KEY']) + key_file.close() +app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365) + +# NOTE: SESSION_COOKIE_SAMESITE must be set to 'lax' to allow the user's +# previous session to persist when accessing the instance from an external +# link. Setting this value to 'strict' causes Whoogle to revalidate a new +# session, and fail, resulting in cookies being disabled. +app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' + # Config fields that are used to check for updates app.config['RELEASES_URL'] = 'https://github.com/' \ 'benbusby/whoogle-search/releases' @@ -109,15 +130,7 @@ app.config['CSP'] = 'default-src \'none\';' \ 'media-src \'self\';' \ 'connect-src \'self\';' -if not os.path.exists(app.config['CONFIG_PATH']): - os.makedirs(app.config['CONFIG_PATH']) - -if not os.path.exists(app.config['SESSION_FILE_DIR']): - os.makedirs(app.config['SESSION_FILE_DIR']) - -# Generate DDG bang filter, and create path if it doesn't exist yet -if not os.path.exists(app.config['BANG_PATH']): - os.makedirs(app.config['BANG_PATH']) +# Generate DDG bang filter if not os.path.exists(app.config['BANG_FILE']): json.dump({}, open(app.config['BANG_FILE'], 'w')) bangs_thread = threading.Thread( @@ -126,9 +139,6 @@ if not os.path.exists(app.config['BANG_FILE']): bangs_thread.start() # Build new mapping of static files for cache busting -if not os.path.exists(app.config['BUILD_FOLDER']): - os.makedirs(app.config['BUILD_FOLDER']) - cache_busting_dirs = ['css', 'js'] for cb_dir in cache_busting_dirs: full_cb_dir = os.path.join(app.config['STATIC_FOLDER'], cb_dir) @@ -155,8 +165,6 @@ app.jinja_env.globals.update(clean_query=clean_query) app.jinja_env.globals.update( cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f]) -Session(app) - # Attempt to acquire tor identity, to determine if Tor config is available send_tor_signal(Signal.HEARTBEAT) diff --git a/app/models/endpoint.py b/app/models/endpoint.py index eeddc64..83bea79 100644 --- a/app/models/endpoint.py +++ b/app/models/endpoint.py @@ -5,7 +5,6 @@ class Endpoint(Enum): autocomplete = 'autocomplete' home = 'home' healthz = 'healthz' - session = 'session' config = 'config' opensearch = 'opensearch.xml' search = 'search' diff --git a/app/routes.py b/app/routes.py index 5ce1101..f144553 100644 --- a/app/routes.py +++ b/app/routes.py @@ -67,8 +67,7 @@ def auth_required(f): def session_required(f): @wraps(f) def decorated(*args, **kwargs): - if (valid_user_session(session) and - 'cookies_disabled' not in request.args): + if (valid_user_session(session)): g.session_key = session['key'] else: session.pop('_permanent', None) @@ -113,6 +112,7 @@ def session_required(f): @app.before_request def before_request_func(): global bang_json + session.permanent = True # Check for latest version if needed now = datetime.now() @@ -126,43 +126,17 @@ def before_request_func(): request.args if request.method == 'GET' else request.form ) - # Skip pre-request actions if verifying session - if '/session' in request.path and not valid_user_session(session): - return - default_config = json.load(open(app.config['DEFAULT_CONFIG'])) \ if os.path.exists(app.config['DEFAULT_CONFIG']) else {} # Generate session values for user if unavailable - if (not valid_user_session(session) and - 'cookies_disabled' not in request.args): + if (not valid_user_session(session)): session['config'] = default_config session['uuid'] = str(uuid.uuid4()) session['key'] = generate_user_key() - # Skip checking for session on any searches that don't - # require a valid session - if (not Endpoint.autocomplete.in_path(request.path) and - not Endpoint.healthz.in_path(request.path) and - not Endpoint.opensearch.in_path(request.path)): - # reconstruct url if X-Forwarded-Host header present - request_url = get_proxy_host_url(request, - get_request_url(request.url)) - return redirect(url_for( - 'session_check', - session_id=session['uuid'], - follow=request_url), code=307) - else: - g.user_config = Config(**session['config']) - elif 'cookies_disabled' not in request.args: - # Set session as permanent - session.permanent = True - app.permanent_session_lifetime = timedelta(days=365) - g.user_config = Config(**session['config']) - else: - # User has cookies disabled, fall back to immutable default config - session.pop('_permanent', None) - g.user_config = Config(**default_config) + # Establish config values per user session + g.user_config = Config(**session['config']) if not g.user_config.url: g.user_config.url = get_request_url(request.url_root) @@ -209,19 +183,6 @@ def healthz(): return '' -@app.route(f'/{Endpoint.session}/', methods=['GET', 'PUT', 'POST']) -def session_check(session_id): - if 'uuid' in session and session['uuid'] == session_id: - session['valid'] = True - return redirect(request.args.get('follow'), code=307) - else: - follow_url = request.args.get('follow') - req = PreparedRequest() - req.prepare_url(follow_url, {'cookies_disabled': 1}) - session.pop('_permanent', None) - return redirect(req.url, code=307) - - @app.route('/', methods=['GET']) @app.route(f'/{Endpoint.home}', methods=['GET']) @auth_required @@ -246,8 +207,7 @@ def index(): dark=g.user_config.dark), config_disabled=( app.config['CONFIG_DISABLE'] or - not valid_user_session(session) or - 'cookies_disabled' in request.args), + not valid_user_session(session)), config=g.user_config, tor_available=int(os.environ.get('TOR_AVAILABLE')), version_number=app.config['VERSION_NUMBER']) diff --git a/requirements.txt b/requirements.txt index 03b1ebb..24e7115 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ cryptography==3.3.2 cssutils==2.4.0 defusedxml==0.7.1 Flask==1.1.1 -Flask-Session==0.4.0 idna==2.9 itsdangerous==1.1.0 Jinja2==2.11.3