Refactor session behavior, remove `Flask-Session` dep
Sessions are no longer validated using the "/session/..." route. This created a lot of problems due to buggy/unexpected behavior coming from the Flask-Session dependency, which is (more or less) no longer maintained. Sessions are also no longer strictly server-side-only. The majority of information that was being stored in user sessions was aesthetic only, aside from the session specific key used to encrypt URLs. This key is still unique per user, but is not (or shouldn't be) in anyone's threat model to keep absolutely 100% private from everyone. Especially paranoid users of Whoogle can easily modify the code to use a randomly generated encryption key that is reset on session invalidation (and set invalidation time to a short enough period for their liking). Ultimately, this should result in much more stable sessions per client. There shouldn't be decryption issues with element URLs or queries during result page navigation.main
parent
77f617e984
commit
32ad39d0e1
|
@ -4,6 +4,7 @@ __pycache__/
|
|||
*.pyc
|
||||
*.pem
|
||||
*.conf
|
||||
*.key
|
||||
config.json
|
||||
test/static
|
||||
flask_session/
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ class Endpoint(Enum):
|
|||
autocomplete = 'autocomplete'
|
||||
home = 'home'
|
||||
healthz = 'healthz'
|
||||
session = 'session'
|
||||
config = 'config'
|
||||
opensearch = 'opensearch.xml'
|
||||
search = 'search'
|
||||
|
|
|
@ -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}/<session_id>', 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'])
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue