Initial commit
This commit is contained in:
23
README.md
Normal file
23
README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
staticshield session proxy
|
||||||
|
==========================
|
||||||
|
|
||||||
|
|
||||||
|
## Running it locally
|
||||||
|
|
||||||
|
Create a file (e.g., called `run.sh`) with the following:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
export FLASK_SERVE_DIR="/home/YOURUSER/workspace/somesite/build/html"
|
||||||
|
export FLASK_MOTHERSHIP="http://localhost:8888/api/staticshield"
|
||||||
|
export FLASK_SESSION_COOKIE_NAME="staticshield"
|
||||||
|
|
||||||
|
flask --app staticshield run
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## ruff check and fix
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ruff check --fix --select I .
|
||||||
|
```
|
||||||
72
pyproject.toml
Normal file
72
pyproject.toml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
[tool.pytest.ini_options]
|
||||||
|
markers = [
|
||||||
|
"network: mark a test that needs internet access",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
branch = true
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
precision = 2
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
exclude = [
|
||||||
|
".git",
|
||||||
|
"__pycache__",
|
||||||
|
"flask_session",
|
||||||
|
"build",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
line-length = 120
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
# Use single quotes for non-triple-quoted strings.
|
||||||
|
quote-style = "single"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
ignore = [
|
||||||
|
"D203", "D213", "E501",
|
||||||
|
# Docstring section rules we do not want to use:
|
||||||
|
"D407", "D413", "D405", "D406",
|
||||||
|
# more structured docstring formatting comment out to see what we can do better:
|
||||||
|
"D400", "D401", "D403", "D415", "D200", "D202", "D205", "D209", "D210", "D212",
|
||||||
|
"D100", "D101", "D102", "D103", "D104", "D105", "D106",
|
||||||
|
]
|
||||||
|
select = [
|
||||||
|
"C9",
|
||||||
|
"D",
|
||||||
|
"E",
|
||||||
|
"F",
|
||||||
|
"W",
|
||||||
|
"T10", # find breakpoints in the code
|
||||||
|
"ARG", # unused arguments
|
||||||
|
"TID", # relative imports, banned imports
|
||||||
|
"PT", # pytest style
|
||||||
|
"I", # isort
|
||||||
|
"G", # logging format
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"__init__.py" = ["F401"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.mccabe]
|
||||||
|
max-complexity = 10
|
||||||
|
|
||||||
|
[tool.ruff.lint.pycodestyle]
|
||||||
|
max-doc-length = 180
|
||||||
|
ignore-overlong-task-comments = true
|
||||||
|
|
||||||
|
[tool.ruff.lint.flake8-pytest-style]
|
||||||
|
fixture-parentheses = true
|
||||||
|
mark-parentheses = true
|
||||||
|
|
||||||
|
[tool.ruff.lint.flake8-unused-arguments]
|
||||||
|
ignore-variadic-names = true
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
section-order = ["future", "standard-library","third-party", "first-party", "testing", "local-folder"]
|
||||||
|
[tool.ruff.lint.isort.sections]
|
||||||
|
testing = ["tests"]
|
||||||
|
|
||||||
|
[tool.uv.pip]
|
||||||
|
no-emit-package = ["some-package", "pip", "setuptools", "distribute"] # packages that should not be pinned
|
||||||
2
requirements-dev.in
Normal file
2
requirements-dev.in
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-r requirements.in
|
||||||
|
ruff
|
||||||
2
requirements.in
Normal file
2
requirements.in
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
flask
|
||||||
|
flask-session
|
||||||
116
staticshield.py
Normal file
116
staticshield.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import os
|
||||||
|
import urllib.request
|
||||||
|
from logging.config import dictConfig
|
||||||
|
|
||||||
|
from flask import Flask, redirect, request, send_from_directory, session
|
||||||
|
|
||||||
|
from flask_session import Session
|
||||||
|
|
||||||
|
dictConfig({
|
||||||
|
'version': 1,
|
||||||
|
'formatters': {'default': {
|
||||||
|
'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
|
||||||
|
}},
|
||||||
|
'handlers': {'wsgi': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'stream': 'ext://flask.logging.wsgi_errors_stream',
|
||||||
|
'formatter': 'default'
|
||||||
|
}},
|
||||||
|
'root': {
|
||||||
|
#'level': 'INFO',
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'handlers': ['wsgi']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_prefixed_env()
|
||||||
|
app.config['SESSION_PERMANENT'] = False
|
||||||
|
app.config['SESSION_TYPE'] = 'filesystem'
|
||||||
|
Session(app)
|
||||||
|
|
||||||
|
config_vars = ['SERVE_DIR', 'MOTHERSHIP']
|
||||||
|
for config_var in config_vars:
|
||||||
|
if config_var not in app.config:
|
||||||
|
print(f'FLASK_{config_var} env var not set')
|
||||||
|
os.sys.exit(1)
|
||||||
|
else:
|
||||||
|
app.logger.info('Config env %s with value "%s"', config_var, app.config[config_var])
|
||||||
|
|
||||||
|
# Base dir of the files we want to serve; Flask will take care not to escape this dir
|
||||||
|
SERVE_DIR = app.config['SERVE_DIR']
|
||||||
|
|
||||||
|
# Mothership server and login-url, which will redirect here with a sessionstart/SEKRIT
|
||||||
|
MOTHERSHIP = app.config['MOTHERSHIP']
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/<path:path>', methods=['GET', 'POST'])
|
||||||
|
def all_routes(path):
|
||||||
|
"""Intercept all routes and proxy it through a session.
|
||||||
|
|
||||||
|
Loosely based on https://stackoverflow.com/a/20648053
|
||||||
|
|
||||||
|
:param str path: path of file inside the base SERVE_DIR to be served
|
||||||
|
"""
|
||||||
|
app.logger.info('Requested %s', path)
|
||||||
|
if path.startswith('sessionstart/'):
|
||||||
|
# We got redirected back from the mothership, lets see if the secret we got is really known
|
||||||
|
# The path we should have gotten back is of the format:
|
||||||
|
# /sessionstart/SEKRIT_TOKEN/<the_url_to_redirect_on_here_afterwards>
|
||||||
|
secret_and_redirect = path.split('sessionstart/')[1]
|
||||||
|
secret_redirect_split = secret_and_redirect.split('/')
|
||||||
|
secret = secret_redirect_split[0]
|
||||||
|
redirect_path = '/'
|
||||||
|
if len(secret_redirect_split) > 1:
|
||||||
|
redirect_path = '/'.join(secret_redirect_split[1:])
|
||||||
|
app.logger.info('starting new session with secret "%s"', secret)
|
||||||
|
print(f'afterwards, redirecting to "{redirect_path}"')
|
||||||
|
us = f'{request.host_url}{path}'
|
||||||
|
print(us)
|
||||||
|
# Ask the mothership if the secret is known to them, to prevent someone from just making up a URL
|
||||||
|
# Mothership will invalidate this secret token upon handling this request to prevent replay
|
||||||
|
try:
|
||||||
|
app.logger.info('verifying token "%s"', secret)
|
||||||
|
with urllib.request.urlopen(f'{MOTHERSHIP}/verify/{secret}') as response:
|
||||||
|
challenge_response = response.read()
|
||||||
|
print(challenge_response)
|
||||||
|
# Start session if challenge response was successful
|
||||||
|
session['id'] = secret
|
||||||
|
return redirect(redirect_path)
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
app.logger.error('lolwtf, server not found: %s', str(e.reason))
|
||||||
|
return 'Unable to set up session', 403
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
app.logger.error('lolnope, server says no: %s', str(e.reason))
|
||||||
|
return 'Unable to set up session', 403
|
||||||
|
|
||||||
|
# Fallback to disallow
|
||||||
|
return 'Unable to set up session', 403
|
||||||
|
|
||||||
|
# check if the users exist or not
|
||||||
|
if not session.get('id'):
|
||||||
|
# Our current URL, to which mothership will redirect back including a sessionstart
|
||||||
|
original_url = f'{request.host_url}{path}'
|
||||||
|
callback_url = f'{request.host_url}sessionstart/'
|
||||||
|
app.logger.info('Redirecting to mothership with %s', original_url)
|
||||||
|
# No session yet, redirect to mothership
|
||||||
|
app.logger.debug('%s/%s', MOTHERSHIP, original_url)
|
||||||
|
return redirect(f'{MOTHERSHIP}/login?redirect={original_url}&callback={callback_url}')
|
||||||
|
|
||||||
|
file_path = os.path.join(SERVE_DIR, path)
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
app.logger.info('Serving file %s', str(file_path))
|
||||||
|
# This takes a base directory and a path, and ensures that the path is contained in the directory, which makes it safe to accept user-provided paths.
|
||||||
|
return send_from_directory(SERVE_DIR, path)
|
||||||
|
else:
|
||||||
|
app.logger.error('File not found: %s', str(file_path))
|
||||||
|
return 'Sorry, 404'
|
||||||
|
# if text.startswith('favicon'):
|
||||||
|
# print('hoi')
|
||||||
|
# else:
|
||||||
|
# return redirect(url_for('404_error'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return redirect('/index.html')
|
||||||
Reference in New Issue
Block a user