Skip to content

Commit

Permalink
new: Show current user and config on web
Browse files Browse the repository at this point in the history
  • Loading branch information
Rafiot committed Jun 17, 2024
1 parent 59503d6 commit eee8e32
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 43 deletions.
3 changes: 2 additions & 1 deletion lookyloo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from .context import Context # noqa
from .indexing import Indexing # noqa
from .lookyloo import Lookyloo, CaptureSettings # noqa
from .helpers import CaptureSettings # noqa
from .lookyloo import Lookyloo # noqa

logging.getLogger(__name__).addHandler(logging.NullHandler())

Expand Down
2 changes: 1 addition & 1 deletion lookyloo/capturecache.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ def _quick_init(self) -> None:
if hasattr(cc, 'timestamp'):
recent_captures[uuid] = cc.timestamp.timestamp()
if recent_captures:
self.redis.zadd('recent_captures', recent_captures)
self.redis.zadd('recent_captures', recent_captures, nx=True)

def _get_capture_dir(self, uuid: str) -> str:
# Try to get from the recent captures cache in redis
Expand Down
28 changes: 28 additions & 0 deletions lookyloo/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@


from har2tree import CrawledTree, HostNode, URLNode
from lacuscore import CaptureSettings as LacuscoreCaptureSettings
from playwrightcapture import get_devices
from publicsuffixlist import PublicSuffixList # type: ignore[import-untyped]
from pytaxonomies import Taxonomies # type: ignore[attr-defined]
Expand Down Expand Up @@ -392,3 +393,30 @@ def _aggregate_version(self, details: dict[str, str]) -> str | None:

def __str__(self) -> str:
return f'OS: {self.platform} - Browser: {self.browser} {self.version} - UA: {self.string}'


class CaptureSettings(LacuscoreCaptureSettings, total=False):
'''The capture settings that can be passed to Lookyloo'''
listing: int | None
not_queued: int | None
auto_report: bool | str | dict[str, str] | None # {'email': , 'comment': , 'recipient_mail':}
dnt: str | None
browser_name: str | None
os: str | None
parent: str | None


# overwrite set to True means the settings in the config file overwrite the settings
# provided by the user. False will simply append the settings from the config file if they
# don't exist.
class UserCaptureSettings(CaptureSettings, total=False):
overwrite: bool


@lru_cache(64)
def load_user_config(username: str) -> UserCaptureSettings | None:
user_config_path = get_homedir() / 'config' / 'users' / f'{username}.json'
if not user_config_path.exists():
return None
with user_config_path.open() as _c:
return json.load(_c)
34 changes: 4 additions & 30 deletions lookyloo/lookyloo.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

from collections import defaultdict
from datetime import date, datetime, timedelta, timezone
from functools import lru_cache
from email.message import EmailMessage
from functools import cached_property
from io import BytesIO
Expand All @@ -34,7 +33,8 @@
CaptureStatus as CaptureStatusCore,
# CaptureResponse as CaptureResponseCore)
# CaptureResponseJson as CaptureResponseJsonCore,
CaptureSettings as CaptureSettingsCore)
# CaptureSettings as CaptureSettingsCore
)
from PIL import Image, UnidentifiedImageError
from playwrightcapture import get_devices
from puremagic import from_string, PureError # type: ignore[import-untyped]
Expand All @@ -58,7 +58,8 @@
from .helpers import (get_captures_dir, get_email_template,
get_resources_hashes, get_taxonomies,
uniq_domains, ParsedUserAgent, load_cookies, UserAgents,
get_useragent_for_requests, load_takedown_filters
get_useragent_for_requests, load_takedown_filters,
CaptureSettings, UserCaptureSettings, load_user_config
)
from .modules import (MISPs, PhishingInitiative, UniversalWhois,
UrlScan, VirusTotal, Phishtank, Hashlookup,
Expand All @@ -68,33 +69,6 @@
from playwright.async_api import Cookie


class CaptureSettings(CaptureSettingsCore, total=False):
'''The capture settings that can be passed to Lookyloo'''
listing: int | None
not_queued: int | None
auto_report: bool | str | dict[str, str] | None # {'email': , 'comment': , 'recipient_mail':}
dnt: str | None
browser_name: str | None
os: str | None
parent: str | None


# overwrite set to True means the settings in the config file overwrite the settings
# provided by the user. False will simply append the settings from the config file if they
# don't exist.
class UserCaptureSettings(CaptureSettings, total=False):
overwrite: bool


@lru_cache(64)
def load_user_config(username: str) -> UserCaptureSettings | None:
user_config_path = get_homedir() / 'config' / 'users' / f'{username}.json'
if not user_config_path.exists():
return None
with user_config_path.open() as _c:
return json.load(_c)


class Lookyloo():

def __init__(self, cache_max_size: int | None=None) -> None:
Expand Down
13 changes: 9 additions & 4 deletions website/web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from lookyloo import Lookyloo, CaptureSettings
from lookyloo.default import get_config
from lookyloo.exceptions import MissingUUID, NoValidHarFile, LacusUnreachable
from lookyloo.helpers import get_taxonomies, UserAgents, load_cookies
from lookyloo.helpers import get_taxonomies, UserAgents, load_cookies, UserCaptureSettings, load_user_config

if sys.version_info < (3, 9):
from pytz import all_timezones_set
Expand All @@ -52,7 +52,7 @@
from .genericapi import api as generic_api
from .helpers import (User, build_users_table, get_secret_key,
load_user_from_request, src_request_ip, sri_load,
get_lookyloo_instance, get_indexing)
get_lookyloo_instance, get_indexing, build_keys_table)
from .proxied import ReverseProxied

logging.config.dictConfig(get_config('logging'))
Expand All @@ -73,6 +73,7 @@
# Auth stuff
login_manager = flask_login.LoginManager()
login_manager.init_app(app)
build_keys_table()

# User agents manager
user_agents = UserAgents()
Expand Down Expand Up @@ -1314,6 +1315,7 @@ def index_generic(show_hidden: bool=False, show_error: bool=True, category: str
cached.redirects))
titles = sorted(titles, key=lambda x: (x[2], x[3]), reverse=True)
return render_template('index.html', titles=titles, public_domain=lookyloo.public_domain,
show_hidden=show_hidden,
show_project_page=get_config('generic', 'show_project_page'),
version=pkg_version)

Expand Down Expand Up @@ -1422,13 +1424,14 @@ def search() -> str | Response | WerkzeugResponse:
return render_template('search.html')


def _prepare_capture_template(user_ua: str | None, predefined_url: str | None=None) -> str:
def _prepare_capture_template(user_ua: str | None, predefined_url: str | None=None, *, user_config: UserCaptureSettings | None=None) -> str:
return render_template('capture.html', user_agents=user_agents.user_agents,
default=user_agents.default,
personal_ua=user_ua,
default_public=get_config('generic', 'default_public'),
devices=lookyloo.get_playwright_devices(),
predefined_url_to_capture=predefined_url if predefined_url else '',
user_config=user_config,
has_global_proxy=True if lookyloo.global_proxy else False)


Expand Down Expand Up @@ -1496,8 +1499,10 @@ def submit_capture() -> str | Response | WerkzeugResponse:

@app.route('/capture', methods=['GET', 'POST'])
def capture_web() -> str | Response | WerkzeugResponse:
user_config: UserCaptureSettings | None = None
if flask_login.current_user.is_authenticated:
user = flask_login.current_user.get_id()
user_config = load_user_config(user)
else:
user = src_request_ip(request)

Expand Down Expand Up @@ -1609,7 +1614,7 @@ def capture_web() -> str | Response | WerkzeugResponse:
return redirect(url_for('tree', tree_uuid=perma_uuid))

# render template
return _prepare_capture_template(user_ua=request.headers.get('User-Agent'))
return _prepare_capture_template(user_ua=request.headers.get('User-Agent'), user_config=user_config)


@app.route('/simple_capture', methods=['GET', 'POST'])
Expand Down
19 changes: 16 additions & 3 deletions website/web/genericapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@

import flask_login # type: ignore[import-untyped]
from flask import request, send_file, Response
from flask_restx import Namespace, Resource, fields # type: ignore[import-untyped]
from flask_restx import Namespace, Resource, fields, abort # type: ignore[import-untyped]
from werkzeug.security import check_password_hash

from lacuscore import CaptureStatus as CaptureStatusCore
from pylacus import CaptureStatus as CaptureStatusPy
from lookyloo import CaptureSettings, Lookyloo
from lookyloo.comparator import Comparator
from lookyloo.exceptions import MissingUUID, NoValidHarFile
from lookyloo.helpers import load_user_config, UserCaptureSettings

from .helpers import build_users_table, load_user_from_request, src_request_ip, get_lookyloo_instance, get_indexing
from .helpers import (build_users_table, load_user_from_request, src_request_ip,
get_lookyloo_instance, get_indexing)

api = Namespace('GenericAPI', description='Generic Lookyloo API', path='/')

Expand All @@ -34,7 +36,7 @@
def api_auth_check(method): # type: ignore[no-untyped-def]
if flask_login.current_user.is_authenticated or load_user_from_request(request):
return method
return 'Authentication required.', 403
abort(403, 'Authentication required.')


token_request_fields = api.model('AuthTokenFields', {
Expand All @@ -49,6 +51,17 @@ def handle_no_HAR_file_exception(error: Any) -> tuple[dict[str, str], int]:
return {'message': str(error)}, 400


@api.route('/json/get_user_config')
@api.doc(description='Get the configuration of the user (if any)', security='apikey')
class UserConfig(Resource): # type: ignore[misc]
method_decorators = [api_auth_check]

def get(self) -> UserCaptureSettings | None | tuple[dict[str, str], int]:
if not flask_login.current_user.is_authenticated:
return {'error': 'User not authenticated.'}, 401
return load_user_config(flask_login.current_user.get_id())


@api.route('/json/get_token')
@api.doc(description='Get the API token required for authenticated calls')
class AuthToken(Resource): # type: ignore[misc]
Expand Down
9 changes: 6 additions & 3 deletions website/web/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from werkzeug.security import generate_password_hash

from lookyloo import Lookyloo, Indexing
from lookyloo.default import get_config, get_homedir
from lookyloo.default import get_config, get_homedir, LookylooException

__global_lookyloo_instance = None

Expand Down Expand Up @@ -57,9 +57,12 @@ def is_valid_username(username: str) -> bool:

@lru_cache(64)
def build_keys_table() -> dict[str, str]:
keys_table = {}
keys_table: dict[str, str] = {}
for username, authstuff in build_users_table().items():
if 'authkey' in authstuff:
if authstuff['authkey'] in keys_table:
existing_user = keys_table[authstuff['authkey']]
raise LookylooException(f'Duplicate authkey found for {existing_user} and {username}.')
keys_table[authstuff['authkey']] = username
return keys_table

Expand All @@ -85,7 +88,7 @@ def build_users_table() -> dict[str, dict[str, str]]:
users_table[username] = {}
users_table[username]['password'] = generate_password_hash(authstuff)
users_table[username]['authkey'] = hashlib.pbkdf2_hmac('sha256', get_secret_key(),
authstuff.encode(),
f'{username}{authstuff}'.encode(),
100000).hex()

elif isinstance(authstuff, list) and len(authstuff) == 2:
Expand Down
31 changes: 31 additions & 0 deletions website/web/templates/capture.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,37 @@
</a>
</center>
{{ render_messages(container=True, dismissible=True) }}
{% if current_user.is_authenticated %}
<p class="lead">You are logged-in as <strong>{{ current_user.id }}</strong>
</br>
{% if user_config %}
{% if user_config['overwrite'] == true %}
The settings in your users configuration file will overwrite the settings you configure in the form below.
{% else %}
The settings in your users configuration file will only be used if you don't overwrite them in the form below.
{% endif %}
<p>
<dl class="row">
{% for key, value in user_config.items() %}
{% if key != 'overwrite' %}
<dt class="col-sm-3">{{ key }}</dt>
<dd class="col-sm-9">
{% if value is mapping %}
<dl class="row">
{% for sub_key, sub_value in value.items() %}
<dt class="col-sm-4">{{ sub_key}}</dt>
<dd class="col-sm-8">{{ sub_value }}</dd>
{% endfor %}
</dl>
{% else %}
{{ value }}
{% endif %}
<dd>
{% endif %}
{% endfor %}
<dl>
{% endif %}
{% endif %}
<form role="form" action="{{ url_for('capture_web') }}" method=post enctype=multipart/form-data>
<div class="row mb-3">
<div class="col-sm-10">
Expand Down
11 changes: 10 additions & 1 deletion website/web/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,17 @@ <h4>Web forensics tool</h4>
<a href="{{ url_for('simple_capture') }}">
<button class="new-capture-button btn btn-primary">Takedown process</button>
</a>
<br>
<p class="lead">
You are logged-in as <strong>{{ current_user.id }}</strong>,
{% if show_hidden == false %}
and you can check the <a href="{{ url_for('index_hidden') }}">hidden</a> captures.
{% else %}
and you're looking at the hidden captures. Go back to the <a href="{{ url_for('index') }}">public</a> captures.
{% endif %}
<p>
{% endif %}
<br><br>
<br>
{{ render_messages(container=True, dismissible=True) }}
</center>

Expand Down

0 comments on commit eee8e32

Please sign in to comment.