Refactored whoogle session mgmt

Now allows a fallback "default" session to be used if a user's browser
is blocking cookies
main
Ben Busby 2020-06-05 15:24:44 -06:00
parent 64af72abb5
commit 32e837a5e0
6 changed files with 66 additions and 36 deletions

View File

@ -1,28 +1,28 @@
from app.utils.misc import generate_user_keys from app.utils.misc import generate_user_keys
from cryptography.fernet import Fernet
from flask import Flask from flask import Flask
from flask_session import Session from flask_session import Session
import os import os
app = Flask(__name__, static_folder=os.path.dirname(os.path.abspath(__file__)) + '/static') app = Flask(__name__, static_folder=os.path.dirname(os.path.abspath(__file__)) + '/static')
app.user_elements = {} app.user_elements = {}
app.config['SECRET_KEY'] = os.urandom(16) app.default_key_set = generate_user_keys()
app.no_cookie_ips = []
app.config['SECRET_KEY'] = os.urandom(32)
app.config['SESSION_TYPE'] = 'filesystem' app.config['SESSION_TYPE'] = 'filesystem'
app.config['VERSION_NUMBER'] = '0.2.0' app.config['VERSION_NUMBER'] = '0.2.0'
app.config['APP_ROOT'] = os.getenv('APP_ROOT', os.path.dirname(os.path.abspath(__file__))) app.config['APP_ROOT'] = os.getenv('APP_ROOT', os.path.dirname(os.path.abspath(__file__)))
app.config['STATIC_FOLDER'] = os.getenv('STATIC_FOLDER', os.path.join(app.config['APP_ROOT'], 'static')) app.config['STATIC_FOLDER'] = os.getenv('STATIC_FOLDER', os.path.join(app.config['APP_ROOT'], 'static'))
app.config['CONFIG_PATH'] = os.getenv('CONFIG_VOLUME', app.config['STATIC_FOLDER'] + '/config') app.config['CONFIG_PATH'] = os.getenv('CONFIG_VOLUME', os.path.join(app.config['STATIC_FOLDER'], 'config'))
app.config['USER_CONFIG'] = os.path.join(app.config['STATIC_FOLDER'], 'custom_config') app.config['DEFAULT_CONFIG'] = os.path.join(app.config['CONFIG_PATH'], 'config.json')
app.config['SESSION_FILE_DIR'] = app.config['CONFIG_PATH'] app.config['SESSION_FILE_DIR'] = os.path.join(app.config['CONFIG_PATH'], 'session')
app.config['SESSION_COOKIE_SECURE'] = True app.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
if not os.path.exists(app.config['CONFIG_PATH']): if not os.path.exists(app.config['CONFIG_PATH']):
os.makedirs(app.config['CONFIG_PATH']) os.makedirs(app.config['CONFIG_PATH'])
if not os.path.exists(app.config['USER_CONFIG']): if not os.path.exists(app.config['SESSION_FILE_DIR']):
os.makedirs(app.config['USER_CONFIG']) os.makedirs(app.config['SESSION_FILE_DIR'])
sess = Session() Session(app)
sess.init_app(app)
from app import routes from app import routes

View File

@ -37,11 +37,19 @@ def auth_required(f):
@app.before_request @app.before_request
def before_request_func(): def before_request_func():
# Generate secret key for user if unavailable g.request_params = request.args if request.method == 'GET' else request.form
g.cookies_disabled = False
# Generate session values for user if unavailable
if not valid_user_session(session): if not valid_user_session(session):
session['config'] = {'url': request.url_root} session['config'] = json.load(open(app.config['DEFAULT_CONFIG'])) \
session['keys'] = generate_user_keys() if os.path.exists(app.config['DEFAULT_CONFIG']) else {'url': request.url_root}
session['uuid'] = str(uuid.uuid4()) session['uuid'] = str(uuid.uuid4())
session['fernet_keys'] = generate_user_keys(True)
# Flag cookies as possibly disabled in order to prevent against
# unnecessary session directory expansion
g.cookies_disabled = True
if session['uuid'] not in app.user_elements: if session['uuid'] not in app.user_elements:
app.user_elements.update({session['uuid']: 0}) app.user_elements.update({session['uuid']: 0})
@ -63,11 +71,22 @@ def before_request_func():
@app.after_request @app.after_request
def after_request_func(response): def after_request_func(response):
# Regenerate element key if all elements have been served to user
if app.user_elements[session['uuid']] <= 0 and '/element' in request.url: if app.user_elements[session['uuid']] <= 0 and '/element' in request.url:
session['keys']['element_key'] = Fernet.generate_key() # Regenerate element key if all elements have been served to user
session['fernet_keys']['element_key'] = '' if not g.cookies_disabled else app.default_key_set['element_key']
app.user_elements[session['uuid']] = 0 app.user_elements[session['uuid']] = 0
# Check if address consistently has cookies blocked, in which case start removing session
# files after creation.
# Note: This is primarily done to prevent overpopulation of session directories, since browsers that
# block cookies will still trigger Flask's session creation routine with every request.
if g.cookies_disabled and request.remote_addr not in app.no_cookie_ips:
app.no_cookie_ips.append(request.remote_addr)
elif g.cookies_disabled and request.remote_addr in app.no_cookie_ips:
session_list = list(session.keys())
for key in session_list:
session.pop(key)
return response return response
@ -79,6 +98,9 @@ def unknown_page(e):
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
@auth_required @auth_required
def index(): def index():
# Reset keys
session['fernet_keys'] = generate_user_keys(g.cookies_disabled)
return render_template('index.html', return render_template('index.html',
languages=Config.LANGUAGES, languages=Config.LANGUAGES,
countries=Config.COUNTRIES, countries=Config.COUNTRIES,
@ -103,8 +125,7 @@ def opensearch():
@app.route('/autocomplete', methods=['GET', 'POST']) @app.route('/autocomplete', methods=['GET', 'POST'])
def autocomplete(): def autocomplete():
request_params = request.args if request.method == 'GET' else request.form q = g.request_params.get('q')
q = request_params.get('q')
if not q and not request.data: if not q and not request.data:
return jsonify({'?': []}) return jsonify({'?': []})
@ -117,11 +138,10 @@ def autocomplete():
@app.route('/search', methods=['GET', 'POST']) @app.route('/search', methods=['GET', 'POST'])
@auth_required @auth_required
def search(): def search():
# Clear previous elements and generate a new key each time a new search is performed # Reset element counter
app.user_elements[session['uuid']] = 0 app.user_elements[session['uuid']] = 0
session['keys']['element_key'] = Fernet.generate_key()
search_util = RoutingUtils(request, g.user_config, session) search_util = RoutingUtils(request, g.user_config, session, cookies_disabled=g.cookies_disabled)
query = search_util.new_search_query() query = search_util.new_search_query()
# Redirect to home if invalid/blank search # Redirect to home if invalid/blank search
@ -157,7 +177,7 @@ def config():
return json.dumps(g.user_config.__dict__) return json.dumps(g.user_config.__dict__)
elif request.method == 'PUT': elif request.method == 'PUT':
if 'name' in request.args: if 'name' in request.args:
config_pkl = os.path.join(app.config['USER_CONFIG'], request.args.get('name')) config_pkl = os.path.join(app.config['CONFIG_PATH'], request.args.get('name'))
session['config'] = pickle.load(open(config_pkl, 'rb')) if os.path.exists(config_pkl) else session['config'] session['config'] = pickle.load(open(config_pkl, 'rb')) if os.path.exists(config_pkl) else session['config']
return json.dumps(session['config']) return json.dumps(session['config'])
else: else:
@ -167,8 +187,13 @@ def config():
if 'url' not in config_data or not config_data['url']: if 'url' not in config_data or not config_data['url']:
config_data['url'] = g.user_config.url config_data['url'] = g.user_config.url
# Save config by name to allow a user to easily load later
if 'name' in request.args: if 'name' in request.args:
pickle.dump(config_data, open(os.path.join(app.config['USER_CONFIG'], request.args.get('name')), 'wb')) pickle.dump(config_data, open(os.path.join(app.config['CONFIG_PATH'], request.args.get('name')), 'wb'))
# Overwrite default config if user has cookies disabled
if g.cookies_disabled:
open(app.config['DEFAULT_CONFIG'], 'w').write(json.dumps(config_data, indent=4))
session['config'] = config_data session['config'] = config_data
return redirect(config_data['url']) return redirect(config_data['url'])
@ -196,7 +221,7 @@ def imgres():
@app.route('/element') @app.route('/element')
@auth_required @auth_required
def element(): def element():
cipher_suite = Fernet(session['keys']['element_key']) cipher_suite = Fernet(session['fernet_keys']['element_key'])
src_url = cipher_suite.decrypt(request.args.get('url').encode()).decode() src_url = cipher_suite.decrypt(request.args.get('url').encode()).decode()
src_type = request.args.get('type') src_type = request.args.get('type')

View File

@ -4,7 +4,7 @@ const handleUserInput = searchBar => {
xhrRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhrRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhrRequest.onload = function() { xhrRequest.onload = function() {
if (xhrRequest.readyState === 4 && xhrRequest.status !== 200) { if (xhrRequest.readyState === 4 && xhrRequest.status !== 200) {
alert("Error fetching autocomplete results"); // Do nothing if failed to fetch autocomplete results
return; return;
} }

View File

@ -1,9 +1,13 @@
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from flask import current_app as app
SESSION_VALS = ['uuid', 'config', 'keys'] SESSION_VALS = ['uuid', 'config', 'fernet_keys']
def generate_user_keys(): def generate_user_keys(cookies_disabled=False) -> dict:
if cookies_disabled:
return app.default_key_set
# Generate/regenerate unique key per user # Generate/regenerate unique key per user
return { return {
'element_key': Fernet.generate_key(), 'element_key': Fernet.generate_key(),

View File

@ -1,5 +1,5 @@
from app import app
from app.filter import Filter, get_first_link from app.filter import Filter, get_first_link
from app.utils.misc import generate_user_keys
from app.request import gen_query from app.request import gen_query
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from cryptography.fernet import Fernet, InvalidToken from cryptography.fernet import Fernet, InvalidToken
@ -8,13 +8,14 @@ from typing import Any, Tuple
class RoutingUtils: class RoutingUtils:
def __init__(self, request, config, session): def __init__(self, request, config, session, cookies_disabled=False):
self.request_params = request.args if request.method == 'GET' else request.form self.request_params = request.args if request.method == 'GET' else request.form
self.user_agent = request.headers.get('User-Agent') self.user_agent = request.headers.get('User-Agent')
self.feeling_lucky = False self.feeling_lucky = False
self.config = config self.config = config
self.session = session self.session = session
self.query = '' self.query = ''
self.cookies_disabled = cookies_disabled
self.search_type = self.request_params.get('tbm') if 'tbm' in self.request_params else '' self.search_type = self.request_params.get('tbm') if 'tbm' in self.request_params else ''
def __getitem__(self, name): def __getitem__(self, name):
@ -30,8 +31,8 @@ class RoutingUtils:
return hasattr(self, name) return hasattr(self, name)
def new_search_query(self) -> str: def new_search_query(self) -> str:
app.user_elements[self.session['uuid']] = 0 # Generate a new element key each time a new search is performed
self.session['keys']['element_key'] = Fernet.generate_key() self.session['fernet_keys']['element_key'] = generate_user_keys(cookies_disabled=self.cookies_disabled)['element_key']
q = self.request_params.get('q') q = self.request_params.get('q')
@ -40,12 +41,12 @@ class RoutingUtils:
else: else:
# Attempt to decrypt if this is an internal link # Attempt to decrypt if this is an internal link
try: try:
q = Fernet(self.session['keys']['text_key']).decrypt(q.encode()).decode() q = Fernet(self.session['fernet_keys']['text_key']).decrypt(q.encode()).decode()
except InvalidToken: except InvalidToken:
pass pass
# Reset text key # Reset text key
self.session['keys']['text_key'] = Fernet.generate_key() self.session['fernet_keys']['text_key'] = generate_user_keys(cookies_disabled=self.cookies_disabled)['text_key']
# Format depending on whether or not the query is a "feeling lucky" query # Format depending on whether or not the query is a "feeling lucky" query
self.feeling_lucky = q.startswith('! ') self.feeling_lucky = q.startswith('! ')
@ -55,7 +56,7 @@ class RoutingUtils:
def generate_response(self) -> Tuple[Any, int]: def generate_response(self) -> Tuple[Any, int]:
mobile = 'Android' in self.user_agent or 'iPhone' in self.user_agent mobile = 'Android' in self.user_agent or 'iPhone' in self.user_agent
content_filter = Filter(self.session['keys'], mobile=mobile, config=self.config) content_filter = Filter(self.session['fernet_keys'], mobile=mobile, config=self.config)
full_query = gen_query(self.query, self.request_params, self.config, content_filter.near) full_query = gen_query(self.query, self.request_params, self.config, content_filter.near)
get_body = g.user_request.send(query=full_query).text get_body = g.user_request.send(query=full_query).text

View File

@ -13,7 +13,7 @@ def test_valid_session(client):
assert not valid_user_session(session) assert not valid_user_session(session)
session['uuid'] = 'test' session['uuid'] = 'test'
session['keys'] = generate_user_keys() session['fernet_keys'] = generate_user_keys()
session['config'] = {} session['config'] = {}
assert valid_user_session(session) assert valid_user_session(session)
@ -26,11 +26,11 @@ def test_request_key_generation(client):
with client.session_transaction() as session: with client.session_transaction() as session:
assert valid_user_session(session) assert valid_user_session(session)
text_key = session['keys']['text_key'] text_key = session['fernet_keys']['text_key']
rv = client.get('/search?q=test+2') rv = client.get('/search?q=test+2')
assert rv._status_code == 200 assert rv._status_code == 200
with client.session_transaction() as session: with client.session_transaction() as session:
assert valid_user_session(session) assert valid_user_session(session)
assert text_key not in session['keys']['text_key'] assert text_key not in session['fernet_keys']['text_key']