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
|
*.pyc
|
||||||
*.pem
|
*.pem
|
||||||
*.conf
|
*.conf
|
||||||
|
*.key
|
||||||
config.json
|
config.json
|
||||||
test/static
|
test/static
|
||||||
flask_session/
|
flask_session/
|
||||||
|
|
|
@ -3,9 +3,9 @@ from app.request import send_tor_signal
|
||||||
from app.utils.session import generate_user_key
|
from app.utils.session import generate_user_key
|
||||||
from app.utils.bangs import gen_bangs_json
|
from app.utils.bangs import gen_bangs_json
|
||||||
from app.utils.misc import gen_file_hash, read_config_bool
|
from app.utils.misc import gen_file_hash, read_config_bool
|
||||||
|
from base64 import b64encode
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_session import Session
|
|
||||||
import json
|
import json
|
||||||
import logging.config
|
import logging.config
|
||||||
import os
|
import os
|
||||||
|
@ -20,24 +20,15 @@ app = Flask(__name__, static_folder=os.path.dirname(
|
||||||
|
|
||||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
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
|
# Load .env file if enabled
|
||||||
if os.getenv('WHOOGLE_DOTENV', ''):
|
if os.getenv('WHOOGLE_DOTENV', ''):
|
||||||
dotenv_path = '../whoogle.env'
|
load_dotenv(dot_env_path)
|
||||||
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
||||||
dotenv_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.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'):
|
if os.getenv('HTTPS_ONLY'):
|
||||||
app.config['SESSION_COOKIE_NAME'] = '__Secure-session'
|
app.config['SESSION_COOKIE_NAME'] = '__Secure-session'
|
||||||
|
@ -86,6 +77,36 @@ app.config['BANG_FILE'] = os.path.join(
|
||||||
app.config['BANG_PATH'],
|
app.config['BANG_PATH'],
|
||||||
'bangs.json')
|
'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
|
# Config fields that are used to check for updates
|
||||||
app.config['RELEASES_URL'] = 'https://github.com/' \
|
app.config['RELEASES_URL'] = 'https://github.com/' \
|
||||||
'benbusby/whoogle-search/releases'
|
'benbusby/whoogle-search/releases'
|
||||||
|
@ -109,15 +130,7 @@ app.config['CSP'] = 'default-src \'none\';' \
|
||||||
'media-src \'self\';' \
|
'media-src \'self\';' \
|
||||||
'connect-src \'self\';'
|
'connect-src \'self\';'
|
||||||
|
|
||||||
if not os.path.exists(app.config['CONFIG_PATH']):
|
# Generate DDG bang filter
|
||||||
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'])
|
|
||||||
if not os.path.exists(app.config['BANG_FILE']):
|
if not os.path.exists(app.config['BANG_FILE']):
|
||||||
json.dump({}, open(app.config['BANG_FILE'], 'w'))
|
json.dump({}, open(app.config['BANG_FILE'], 'w'))
|
||||||
bangs_thread = threading.Thread(
|
bangs_thread = threading.Thread(
|
||||||
|
@ -126,9 +139,6 @@ if not os.path.exists(app.config['BANG_FILE']):
|
||||||
bangs_thread.start()
|
bangs_thread.start()
|
||||||
|
|
||||||
# Build new mapping of static files for cache busting
|
# 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']
|
cache_busting_dirs = ['css', 'js']
|
||||||
for cb_dir in cache_busting_dirs:
|
for cb_dir in cache_busting_dirs:
|
||||||
full_cb_dir = os.path.join(app.config['STATIC_FOLDER'], cb_dir)
|
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(
|
app.jinja_env.globals.update(
|
||||||
cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f])
|
cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f])
|
||||||
|
|
||||||
Session(app)
|
|
||||||
|
|
||||||
# Attempt to acquire tor identity, to determine if Tor config is available
|
# Attempt to acquire tor identity, to determine if Tor config is available
|
||||||
send_tor_signal(Signal.HEARTBEAT)
|
send_tor_signal(Signal.HEARTBEAT)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ class Endpoint(Enum):
|
||||||
autocomplete = 'autocomplete'
|
autocomplete = 'autocomplete'
|
||||||
home = 'home'
|
home = 'home'
|
||||||
healthz = 'healthz'
|
healthz = 'healthz'
|
||||||
session = 'session'
|
|
||||||
config = 'config'
|
config = 'config'
|
||||||
opensearch = 'opensearch.xml'
|
opensearch = 'opensearch.xml'
|
||||||
search = 'search'
|
search = 'search'
|
||||||
|
|
|
@ -67,8 +67,7 @@ def auth_required(f):
|
||||||
def session_required(f):
|
def session_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
if (valid_user_session(session) and
|
if (valid_user_session(session)):
|
||||||
'cookies_disabled' not in request.args):
|
|
||||||
g.session_key = session['key']
|
g.session_key = session['key']
|
||||||
else:
|
else:
|
||||||
session.pop('_permanent', None)
|
session.pop('_permanent', None)
|
||||||
|
@ -113,6 +112,7 @@ def session_required(f):
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def before_request_func():
|
def before_request_func():
|
||||||
global bang_json
|
global bang_json
|
||||||
|
session.permanent = True
|
||||||
|
|
||||||
# Check for latest version if needed
|
# Check for latest version if needed
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
@ -126,43 +126,17 @@ def before_request_func():
|
||||||
request.args if request.method == 'GET' else request.form
|
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'])) \
|
default_config = json.load(open(app.config['DEFAULT_CONFIG'])) \
|
||||||
if os.path.exists(app.config['DEFAULT_CONFIG']) else {}
|
if os.path.exists(app.config['DEFAULT_CONFIG']) else {}
|
||||||
|
|
||||||
# Generate session values for user if unavailable
|
# Generate session values for user if unavailable
|
||||||
if (not valid_user_session(session) and
|
if (not valid_user_session(session)):
|
||||||
'cookies_disabled' not in request.args):
|
|
||||||
session['config'] = default_config
|
session['config'] = default_config
|
||||||
session['uuid'] = str(uuid.uuid4())
|
session['uuid'] = str(uuid.uuid4())
|
||||||
session['key'] = generate_user_key()
|
session['key'] = generate_user_key()
|
||||||
|
|
||||||
# Skip checking for session on any searches that don't
|
# Establish config values per user session
|
||||||
# require a valid session
|
g.user_config = Config(**session['config'])
|
||||||
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)
|
|
||||||
|
|
||||||
if not g.user_config.url:
|
if not g.user_config.url:
|
||||||
g.user_config.url = get_request_url(request.url_root)
|
g.user_config.url = get_request_url(request.url_root)
|
||||||
|
@ -209,19 +183,6 @@ def healthz():
|
||||||
return ''
|
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('/', methods=['GET'])
|
||||||
@app.route(f'/{Endpoint.home}', methods=['GET'])
|
@app.route(f'/{Endpoint.home}', methods=['GET'])
|
||||||
@auth_required
|
@auth_required
|
||||||
|
@ -246,8 +207,7 @@ def index():
|
||||||
dark=g.user_config.dark),
|
dark=g.user_config.dark),
|
||||||
config_disabled=(
|
config_disabled=(
|
||||||
app.config['CONFIG_DISABLE'] or
|
app.config['CONFIG_DISABLE'] or
|
||||||
not valid_user_session(session) or
|
not valid_user_session(session)),
|
||||||
'cookies_disabled' in request.args),
|
|
||||||
config=g.user_config,
|
config=g.user_config,
|
||||||
tor_available=int(os.environ.get('TOR_AVAILABLE')),
|
tor_available=int(os.environ.get('TOR_AVAILABLE')),
|
||||||
version_number=app.config['VERSION_NUMBER'])
|
version_number=app.config['VERSION_NUMBER'])
|
||||||
|
|
|
@ -9,7 +9,6 @@ cryptography==3.3.2
|
||||||
cssutils==2.4.0
|
cssutils==2.4.0
|
||||||
defusedxml==0.7.1
|
defusedxml==0.7.1
|
||||||
Flask==1.1.1
|
Flask==1.1.1
|
||||||
Flask-Session==0.4.0
|
|
||||||
idna==2.9
|
idna==2.9
|
||||||
itsdangerous==1.1.0
|
itsdangerous==1.1.0
|
||||||
Jinja2==2.11.3
|
Jinja2==2.11.3
|
||||||
|
|
Loading…
Reference in New Issue