From 37b3cf9fb2a56cfa980844f527d834916b38cca8 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Tue, 25 Oct 2022 11:30:45 +0200 Subject: [proxy] make Haketilo popup functional * Ad hoc payload creation was additionally fixed in this commit. * Addition on newly created script blocking/allowing rules to pattern tree was additionally fixed in this commit. It is no longer necessary to restart Haketilo for new rules to come into effect. --- src/hydrilla/proxy/addon.py | 15 +- src/hydrilla/proxy/http_messages.py | 33 +--- src/hydrilla/proxy/policies/__init__.py | 2 +- src/hydrilla/proxy/policies/base.py | 95 ++++++++--- .../info_pages_templates/info_base.html.jinja | 89 +++++++++++ .../js_error_blocked_info.html.jinja | 22 +++ .../js_fallback_allowed_info.html.jinja | 14 ++ .../js_fallback_blocked_info.html.jinja | 14 ++ .../js_rule_allowed_info.html.jinja | 14 ++ .../js_rule_blocked_info.html.jinja | 14 ++ .../info_pages_templates/js_rule_info.html.jinja | 37 +++++ .../info_pages_templates/payload_info.html.jinja | 48 ++++++ .../special_page_info.html.jinja | 17 ++ .../policies/injectable_scripts/popup.js.jinja | 173 ++++++++++++++++++++- src/hydrilla/proxy/policies/misc.py | 28 +++- src/hydrilla/proxy/policies/payload.py | 7 + src/hydrilla/proxy/policies/payload_resource.py | 22 ++- src/hydrilla/proxy/policies/rule.py | 14 ++ src/hydrilla/proxy/state.py | 6 + src/hydrilla/proxy/state_impl/concrete_state.py | 2 + src/hydrilla/proxy/state_impl/rules.py | 18 +++ src/hydrilla/proxy/web_ui/items.py | 33 +++- src/hydrilla/proxy/web_ui/items_import.py | 9 +- src/hydrilla/proxy/web_ui/rules.py | 13 ++ .../proxy/web_ui/templates/import.html.jinja | 2 +- .../proxy/web_ui/templates/index.html.jinja | 4 + .../templates/items/package_viewversion.html.jinja | 2 +- .../proxy/web_ui/templates/rules/add.html.jinja | 4 +- 28 files changed, 677 insertions(+), 74 deletions(-) create mode 100644 src/hydrilla/proxy/policies/info_pages_templates/info_base.html.jinja create mode 100644 src/hydrilla/proxy/policies/info_pages_templates/js_error_blocked_info.html.jinja create mode 100644 src/hydrilla/proxy/policies/info_pages_templates/js_fallback_allowed_info.html.jinja create mode 100644 src/hydrilla/proxy/policies/info_pages_templates/js_fallback_blocked_info.html.jinja create mode 100644 src/hydrilla/proxy/policies/info_pages_templates/js_rule_allowed_info.html.jinja create mode 100644 src/hydrilla/proxy/policies/info_pages_templates/js_rule_blocked_info.html.jinja create mode 100644 src/hydrilla/proxy/policies/info_pages_templates/js_rule_info.html.jinja create mode 100644 src/hydrilla/proxy/policies/info_pages_templates/payload_info.html.jinja create mode 100644 src/hydrilla/proxy/policies/info_pages_templates/special_page_info.html.jinja (limited to 'src/hydrilla/proxy') diff --git a/src/hydrilla/proxy/addon.py b/src/hydrilla/proxy/addon.py index 68b3cd5..98894e7 100644 --- a/src/hydrilla/proxy/addon.py +++ b/src/hydrilla/proxy/addon.py @@ -32,11 +32,12 @@ from addon script. import sys import re +import threading +import secrets import typing as t import dataclasses as dc import traceback as tb -from threading import Lock from pathlib import Path from contextlib import contextmanager from urllib.parse import urlparse @@ -139,6 +140,8 @@ class PassedOptions: self.haketilo_launch_browser is not None) +Lock = threading.Lock + @dc.dataclass class HaketiloAddon: initial_options: PassedOptions = PassedOptions() @@ -343,7 +346,15 @@ class HaketiloAddon: with self.http_safe_event_handling(flow): handling = self.get_flow_handling(flow) - result = handling.policy.consume_response(handling.full_http_info) + new_nonce = secrets.token_urlsafe(8) + setattr(policies.response_work_data, 'nonce', new_nonce) + + try: + http_info = handling.full_http_info + result = handling.policy.consume_response(http_info) + finally: + delattr(policies.response_work_data, 'nonce') + if result is not None: headers_bin = result.headers.items_bin() diff --git a/src/hydrilla/proxy/http_messages.py b/src/hydrilla/proxy/http_messages.py index 718022f..74f1f02 100644 --- a/src/hydrilla/proxy/http_messages.py +++ b/src/hydrilla/proxy/http_messages.py @@ -30,6 +30,7 @@ """ import re +import cgi import dataclasses as dc import typing as t import sys @@ -120,42 +121,18 @@ def make_parsed_url(url: t.Union[str, url_patterns.ParsedUrl]) \ return url_patterns.parse_url(url) if isinstance(url, str) else url -# For details of 'Content-Type' header's structure, see: -# https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.1 -content_type_reg = re.compile(r''' -^ -(?P[\w-]+/[\w-]+) -\s* -(?: - ; - (?:[^;]*;)* # match possible parameter other than "charset" -) -\s* -charset= # no whitespace allowed in parameter as per RFC -(?P - [\w-]+ - | - "[\w-]+" # quotes are optional per RFC -) -(?:;[^;]+)* # match possible parameter other than "charset" -$ # forbid possible dangling characters after closing '"' -''', re.VERBOSE | re.IGNORECASE) - @dc.dataclass(frozen=True) class HasHeadersMixin: headers: IHeaders def deduce_content_type(self) -> tuple[t.Optional[str], t.Optional[str]]: - content_type = self.headers.get('content-type') - if content_type is None: - return (None, None) - - match = content_type_reg.match(content_type) - if match is None: + content_type_header = self.headers.get('content-type') + if content_type_header is None: return (None, None) - mime, encoding = match.group('mime'), match.group('encoding') + mime, options = cgi.parse_header(content_type_header) + encoding = options.get('charset') if encoding is not None: encoding = encoding.lower() diff --git a/src/hydrilla/proxy/policies/__init__.py b/src/hydrilla/proxy/policies/__init__.py index 2276177..93c3d4f 100644 --- a/src/hydrilla/proxy/policies/__init__.py +++ b/src/hydrilla/proxy/policies/__init__.py @@ -4,7 +4,7 @@ # # Available under the terms of Creative Commons Zero v1.0 Universal. -from .base import * +from .base import PolicyPriority, Policy, PolicyFactory, response_work_data from .payload import PayloadPolicyFactory diff --git a/src/hydrilla/proxy/policies/base.py b/src/hydrilla/proxy/policies/base.py index 7ce5105..1626b5c 100644 --- a/src/hydrilla/proxy/policies/base.py +++ b/src/hydrilla/proxy/policies/base.py @@ -31,10 +31,10 @@ import enum import re +import threading import dataclasses as dc import typing as t -from threading import Lock from abc import ABC, abstractmethod from hashlib import sha256 from base64 import b64encode @@ -43,24 +43,60 @@ import jinja2 from immutables import Map -from ... url_patterns import ParsedUrl +from ...translations import translation as make_translation +from ... import url_patterns +from ... import common_jinja_templates from .. import state from .. import http_messages from .. import csp -loader = jinja2.PackageLoader(__package__, package_path='injectable_scripts') -jinja_env = jinja2.Environment( - loader = loader, +_info_loader = jinja2.PackageLoader( + __package__, + package_path = 'info_pages_templates' +) +_combined_loader = common_jinja_templates.combine_with_loaders([_info_loader]) +_jinja_info_env = jinja2.Environment( + loader = _combined_loader, + autoescape = jinja2.select_autoescape(['html.jinja']), + lstrip_blocks = True, + extensions = ['jinja2.ext.i18n', 'jinja2.ext.do'] +) +_jinja_info_env.install_gettext_translations(make_translation()) # type: ignore +_jinja_info_env.globals['url_patterns'] = url_patterns +_jinja_info_lock = threading.Lock() + +def get_info_template(template_file_name: str) -> jinja2.Template: + with _jinja_info_lock: + return _jinja_info_env.get_template(template_file_name) + + +_jinja_script_loader = jinja2.PackageLoader( + __package__, + package_path = 'injectable_scripts' +) +_jinja_script_env = jinja2.Environment( + loader = _jinja_script_loader, + autoescape = False, lstrip_blocks = True, - autoescape = False + extensions = ['jinja2.ext.do'] ) -jinja_lock = Lock() +_jinja_script_lock = threading.Lock() +def get_script_template(template_file_name: str) -> jinja2.Template: + with _jinja_script_lock: + return _jinja_script_env.get_template(template_file_name) -popup_script = jinja_env.get_template('popup.js.jinja').render() -popup_script_sha256_bytes = sha256(popup_script.encode()).digest() -popup_script_sha256_b64 = b64encode(popup_script_sha256_bytes).decode() + +response_work_data = threading.local() + +def response_nonce() -> str: + """ + When called multiple times during consume_response(), each time returns the + same unpredictable string unique to this response. The string is used as a + nonce for script elements. + """ + return response_work_data.nonce class PolicyPriority(int, enum.Enum): @@ -140,7 +176,9 @@ class Policy(ABC): -> t.Mapping[str, t.Sequence[str]]: if (self.current_popup_settings.popup_enabled and http_info.is_likely_a_page): - return {'script-src': [f"'sha256-{popup_script_sha256_b64}'"]} + nonce_source = f"'nonce-{response_nonce()}'" + directives = ('script-src', 'style-src', 'frame-src') + return dict((directive, [nonce_source]) for directive in directives) else: return Map() @@ -167,8 +205,26 @@ class Policy(ABC): ) -> t.Union[str, bytes]: popup_settings = self.current_popup_settings - if (popup_settings.popup_enabled and - http_info.is_likely_a_page): + if popup_settings.popup_enabled: + nonce = response_nonce() + + popup_page = self.make_info_page(http_info) + if popup_page is None: + template = get_info_template('special_page_info.html.jinja') + popup_page = template.render( + url = http_info.request_info.url.orig_url + ) + + template = get_script_template('popup.js.jinja') + popup_script = template.render( + popup_page_b64 = b64encode(popup_page.encode()).decode(), + nonce_b64 = b64encode(nonce.encode()).decode(), + # TODO: add an option to configure popup style in the web UI. + # Then start passing the real style value. + #popup_style = popup_settings.style.value + popup_style = 'D' + ) + if encoding is None: encoding = 'utf-8' @@ -180,16 +236,15 @@ class Policy(ABC): dotype_decl = body[0:doctype_decl_len] doc_rest = body[doctype_decl_len:] - return f'{dotype_decl}{doc_rest}' + script_tag = f'' + + return dotype_decl + script_tag + doc_rest else: return http_info.response_info.body def _modify_response_body(self, http_info: http_messages.FullHTTPInfo) \ -> bytes: - if not http_messages.is_likely_a_page( - request_info = http_info.request_info, - response_info = http_info.response_info - ): + if not http_info.is_likely_a_page: return http_info.response_info.body data = http_info.response_info.body @@ -252,6 +307,10 @@ class Policy(ABC): body = new_body ) + def make_info_page(self, http_info: http_messages.FullHTTPInfo) \ + -> t.Optional[str]: + return None + @dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] class PolicyFactory(ABC): diff --git a/src/hydrilla/proxy/policies/info_pages_templates/info_base.html.jinja b/src/hydrilla/proxy/policies/info_pages_templates/info_base.html.jinja new file mode 100644 index 0000000..0785039 --- /dev/null +++ b/src/hydrilla/proxy/policies/info_pages_templates/info_base.html.jinja @@ -0,0 +1,89 @@ +{# +SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + +Proxy info page with information about other page - base template. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior + +Dual licensed under +* GNU General Public License v3.0 or later and +* Creative Commons Attribution Share Alike 4.0 International. + +You can choose to use either of these licenses or both. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use of this +code in a proprietary work, I am not going to enforce this in court. +#} +{% extends "base.html.jinja" %} + +{% block style %} + {{ super() }} + + #main { + padding: 0 10px; + } +{% endblock %} + +{% block head %} + {{ super() }} + + {{ _('info.base.title') }} +{% endblock head %} + +{% block main %} +

+ {{ _('info.base.heading.page_info') }} +

+ + {{ label(_('info.base.page_url_label')) }} + +

+ {{ url }} +

+ +
+ + {{ label(_('info.base.page_policy_label')) }} + + + + {% block main_rest %} + {% endblock %} + + {% block options %} +
+ + {{ label(_('info.base.more_config_options_label')) }} + + {% set site_pattern = url_patterns.pattern_for_domain(url)|urlencode %} + {% set page_pattern = url_patterns.normalize_pattern(url)|urlencode %} + + {% + for pattern, hkt_url_fmt, but_text in [ + (site_pattern, 'https://hkt.mitm.it/rules/viewbypattern?pattern={}', + _('info.base.this_site_script_blocking_button')), + + (site_pattern, 'https://hkt.mitm.it/import?pattern={}', + _('info.base.this_site_payload_button')), + + (page_pattern, 'https://hkt.mitm.it/rules/viewbypattern?pattern={}', + _('info.base.this_page_script_blocking_button')), + + (page_pattern, 'https://hkt.mitm.it/import?pattern={}', + _('info.base.this_page_payload_button')) + ] + %} + {% set hkt_url = hkt_url_fmt.format(pattern) %} + {% set classes = "green-button block-with-bottom-margin" %} + + {{ but_text }} + + {% endfor %} + {% endblock options %} +{% endblock main %} diff --git a/src/hydrilla/proxy/policies/info_pages_templates/js_error_blocked_info.html.jinja b/src/hydrilla/proxy/policies/info_pages_templates/js_error_blocked_info.html.jinja new file mode 100644 index 0000000..c76d42b --- /dev/null +++ b/src/hydrilla/proxy/policies/info_pages_templates/js_error_blocked_info.html.jinja @@ -0,0 +1,22 @@ +{# +SPDX-License-Identifier: CC0-1.0 + +Proxy info page with information about page with JS blocked after an error. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior +#} +{% extends "info_base.html.jinja" %} + +{% block site_policy %} + {{ _('info.js_error_blocked.html')|safe }} +{% endblock %} + +{% block main_rest %} + {% if settings.advanced_user %} + {{ label(_('info.js_error_blocked.stacktrace')) }} + + {{ verbatim(traceback) }} + {% endif %} +{% endblock %} diff --git a/src/hydrilla/proxy/policies/info_pages_templates/js_fallback_allowed_info.html.jinja b/src/hydrilla/proxy/policies/info_pages_templates/js_fallback_allowed_info.html.jinja new file mode 100644 index 0000000..71f3151 --- /dev/null +++ b/src/hydrilla/proxy/policies/info_pages_templates/js_fallback_allowed_info.html.jinja @@ -0,0 +1,14 @@ +{# +SPDX-License-Identifier: CC0-1.0 + +Proxy info page with information about page with JS allowed by default policy. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior +#} +{% extends "info_base.html.jinja" %} + +{% block site_policy %} + {{ _('info.js_fallback_allowed') }} +{% endblock %} diff --git a/src/hydrilla/proxy/policies/info_pages_templates/js_fallback_blocked_info.html.jinja b/src/hydrilla/proxy/policies/info_pages_templates/js_fallback_blocked_info.html.jinja new file mode 100644 index 0000000..3e8719a --- /dev/null +++ b/src/hydrilla/proxy/policies/info_pages_templates/js_fallback_blocked_info.html.jinja @@ -0,0 +1,14 @@ +{# +SPDX-License-Identifier: CC0-1.0 + +Proxy info page with information about page with JS blocked by default policy. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior +#} +{% extends "info_base.html.jinja" %} + +{% block site_policy %} + {{ _('info.js_fallback_blocked') }} +{% endblock %} diff --git a/src/hydrilla/proxy/policies/info_pages_templates/js_rule_allowed_info.html.jinja b/src/hydrilla/proxy/policies/info_pages_templates/js_rule_allowed_info.html.jinja new file mode 100644 index 0000000..fe74602 --- /dev/null +++ b/src/hydrilla/proxy/policies/info_pages_templates/js_rule_allowed_info.html.jinja @@ -0,0 +1,14 @@ +{# +SPDX-License-Identifier: CC0-1.0 + +Proxy info page with information about page with JS allowed by a rule. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior +#} +{% extends "js_rule_info.html.jinja" %} + +{% block site_policy %} + {{ format_html_with_rule_url(_('info.js_allowed.html.rule{url}_is_used')) }} +{% endblock %} diff --git a/src/hydrilla/proxy/policies/info_pages_templates/js_rule_blocked_info.html.jinja b/src/hydrilla/proxy/policies/info_pages_templates/js_rule_blocked_info.html.jinja new file mode 100644 index 0000000..e84d371 --- /dev/null +++ b/src/hydrilla/proxy/policies/info_pages_templates/js_rule_blocked_info.html.jinja @@ -0,0 +1,14 @@ +{# +SPDX-License-Identifier: CC0-1.0 + +Proxy info page with information about page with JS blocked by a rule. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior +#} +{% extends "js_rule_info.html.jinja" %} + +{% block site_policy %} + {{ format_html_with_rule_url(_('info.js_blocked.html.rule{url}_is_used')) }} +{% endblock %} diff --git a/src/hydrilla/proxy/policies/info_pages_templates/js_rule_info.html.jinja b/src/hydrilla/proxy/policies/info_pages_templates/js_rule_info.html.jinja new file mode 100644 index 0000000..b808827 --- /dev/null +++ b/src/hydrilla/proxy/policies/info_pages_templates/js_rule_info.html.jinja @@ -0,0 +1,37 @@ +{# +SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + +Proxy info page with information about page with JS blocked or allowed by a +rule - template for firther extending. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior + +Dual licensed under +* GNU General Public License v3.0 or later and +* Creative Commons Attribution Share Alike 4.0 International. + +You can choose to use either of these licenses or both. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use of this +code in a proprietary work, I am not going to enforce this in court. +#} +{% extends "info_base.html.jinja" %} + +{% macro format_html_with_rule_url(msg_fmt) %} + {% set url_fmt = 'https://hkt.mitm.it/rules/viewbypattern?pattern={pattern}' %} + {{ msg_fmt.format(url=url_fmt.format(pattern=pattern)|e)|safe }} +{% endmacro %} + +{% block main_rest %} +
+ + {{ label(_('info.rule.matched_pattern_label')) }} + +

+ {{ pattern }} +

+{% endblock %} diff --git a/src/hydrilla/proxy/policies/info_pages_templates/payload_info.html.jinja b/src/hydrilla/proxy/policies/info_pages_templates/payload_info.html.jinja new file mode 100644 index 0000000..a71ca25 --- /dev/null +++ b/src/hydrilla/proxy/policies/info_pages_templates/payload_info.html.jinja @@ -0,0 +1,48 @@ +{# +SPDX-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + +Proxy info page with information about page with payload. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior + +Dual licensed under +* GNU General Public License v3.0 or later and +* Creative Commons Attribution Share Alike 4.0 International. + +You can choose to use either of these licenses or both. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use of this +code in a proprietary work, I am not going to enforce this in court. +#} +{% extends "info_base.html.jinja" %} + +{% macro format_html_with_package_identifier_and_url(msg_fmt) %} + {% set package_identifier = payload_data.mapping_identifier|e %} + {% set url_fmt = 'https://hkt.mitm.it/package/viewbypayload/{payload_id}/{package_identifier}' %} + {% + set url = url_fmt.format( + payload_id = payload_data.ref.id, + package_identifier = package_identifier + ) + %} + {{ msg_fmt.format(identifier=package_identifier, url=url|e)|safe }} +{% endmacro %} + +{% block site_policy %} + {% set fmt = _('info.payload.html.package_{identifier}{url}_is_used') %} + {{ format_html_with_package_identifier_and_url(fmt) }} +{% endblock %} + +{% block main_rest %} +
+ + {{ label(_('info.payload.matched_pattern_label')) }} + +

+ {{ payload_data.pattern }} +

+{% endblock %} diff --git a/src/hydrilla/proxy/policies/info_pages_templates/special_page_info.html.jinja b/src/hydrilla/proxy/policies/info_pages_templates/special_page_info.html.jinja new file mode 100644 index 0000000..2f7a9d3 --- /dev/null +++ b/src/hydrilla/proxy/policies/info_pages_templates/special_page_info.html.jinja @@ -0,0 +1,17 @@ +{# +SPDX-License-Identifier: CC0-1.0 + +Proxy info page with information about page handled by special policy. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior +#} +{% extends "info_base.html.jinja" %} + +{% block site_policy %} + {{ _('info.special_page') }} +{% endblock %} + +{% block options %} +{% endblock %} diff --git a/src/hydrilla/proxy/policies/injectable_scripts/popup.js.jinja b/src/hydrilla/proxy/policies/injectable_scripts/popup.js.jinja index 653b7df..593673b 100644 --- a/src/hydrilla/proxy/policies/injectable_scripts/popup.js.jinja +++ b/src/hydrilla/proxy/policies/injectable_scripts/popup.js.jinja @@ -45,6 +45,177 @@ code in a proprietary program, I am not going to enforce this in court. #} (function(){ - console.log('TODO: make Haketilo able to actually display a popup') document.currentScript.remove(); + + /* + * To slightly decrease the chance of accidental popup breakage we snapshot + * methods that other code might redefine. + */ + function get_setter(obj, name) { + return Object.getOwnPropertyDescriptor(obj, name).set; + } + + const ElementPrototype = [0, 0, 0] + .reduce(n => Object.getPrototypeOf(n), document.documentElement); + + const prepend_fun = ElementPrototype.prepend; + const setattr_fun = ElementPrototype.setAttribute; + const remove_fun = ElementPrototype.remove; + const setinner_fun = get_setter(ElementPrototype, "innerHTML"); + const open_fun = window.open; + + const shortcut = "HKT"; + const nonce = atob("{{nonce_b64}}"); + const popup_style = "{{popup_style}}"; + const popup_html = atob("{{popup_page_b64}}"); + const popup_container = document.createElement("div"); + const popup_frame = document.createElement("iframe"); + + function make_style(styles_obj) { + return Object.entries(styles_obj) + .map(([key, val]) => `${key}: ${val} !important`) + .join(';'); + } + + const frame_style = make_style({ + "position": "absolute", + "left": "50%", + "top": "50%", + "transform": "translate(-50%, -50%)", + "display": "block", + "visibility": "visible", + "min-width": "initial", + "width": "600px", + "max-width": "calc(100vw - 20px)", + "min-height": "initial", + "height": "700px", + "max-height": "calc(100vh - 20px)", + "background-color": "#fff", + "opacity": "100%", + "margin": 0, + "padding": 0, + "border": "none", + "border-radius": "5px" + }); + + const container_style = make_style({ + "position": "fixed", + "left": "0", + "top": "0", + "transform": "initial", + "z-index": 2147483647, + "display": "block", + "visibility": "visible", + "min-width": "100vw", + "max-width": "100vw", + "min-height": "100vh", + "max-height": "100vh", + "background-color": "#0008", + "opacity": "100%", + "margin": 0, + "padding": 0, + "border": "none", + "border-radius": 0 + }); + + const popup_blob_opts = {type: "text/html;charset=UTF-8"}; + const popup_blob = new Blob([popup_html], popup_blob_opts); + const popup_url = URL.createObjectURL(popup_blob); + + function show_popup_dialog() { + setattr_fun.call(popup_frame, "srcdoc", popup_html); + setattr_fun.call(popup_frame, "nonce", nonce); + setattr_fun.call(popup_frame, "style", frame_style); + + setattr_fun.call(popup_container, "style", container_style); + setinner_fun.call(popup_container, ""); + prepend_fun.call(popup_container, popup_frame); + + prepend_fun.call(document.body, popup_container); + } + + let popup_newtab_wanted = false; + + function show_popup_newtab() { + /* + * We cannot open popup directly here because browsers block window + * creation attempts from "keypress" event handlers. Instead, we set a + * flag to have "click" event handler open the popup. + */ + popup_newtab_wanted = true; + console.info(`You typed "${shortcut}". Please click anywhere on the page to show Haketilo page information.`); + } + + function show_popup() { + if (popup_style === "T") { + show_popup_newtab(); + } else { + /* popup_syle === "D" */ + show_popup_dialog(); + } + } + + function hide_popup_dialog() { + remove_fun.call(popup_container); + } + + let letters_matched = 0; + + function matches_previous(letter) { + return letters_matched > 0 && letter === shortcut[letters_matched - 1]; + } + + function match_letter(letter) { + if (letter !== shortcut[letters_matched] && !matches_previous(letter)) + letters_matched = 0; + + if (letter === shortcut[letters_matched]) { + if (++letters_matched === shortcut.length) { + letters_matched = 0; + return true; + } + } + + return false; + } + + function consume_keypress(event) { + if (!event.isTrusted) + return; + + if (match_letter(event.key)) + show_popup(); + } + + function cancel_event(event) { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + } + + function consume_click(event) { + if (!event.isTrusted) + return; + + if (popup_style === "T") { + if (popup_newtab_wanted) { + popup_newtab_wanted = false; + cancel_event(event); + window.open( + popup_url, + "_blank", + "popup,width=600px,height=700px" + ); + } + } else { + /* popup_syle === "D" */ + if (event.target === popup_container) { + hide_popup_dialog(); + cancel_event(event); + } + } + } + + document.addEventListener("keypress", consume_keypress, {capture: true}); + document.addEventListener("click", consume_click, {capture: true}); })(); diff --git a/src/hydrilla/proxy/policies/misc.py b/src/hydrilla/proxy/policies/misc.py index 350f3dc..0ff4596 100644 --- a/src/hydrilla/proxy/policies/misc.py +++ b/src/hydrilla/proxy/policies/misc.py @@ -29,9 +29,10 @@ ..... """ +import enum +import traceback as tb import dataclasses as dc import typing as t -import enum from abc import ABC, abstractmethod @@ -44,16 +45,39 @@ from .rule import AllowPolicy, BlockPolicy class FallbackAllowPolicy(AllowPolicy): priority = base.PolicyPriority._ONE + def make_info_page(self, http_info: http_messages.FullHTTPInfo) \ + -> t.Optional[str]: + template = base.get_info_template('js_fallback_allowed_info.html.jinja') + return template.render(url=http_info.request_info.url.orig_url) + class FallbackBlockPolicy(BlockPolicy): priority = base.PolicyPriority._ONE + def make_info_page(self, http_info: http_messages.FullHTTPInfo) \ + -> t.Optional[str]: + template = base.get_info_template('js_fallback_blocked_info.html.jinja') + return template.render(url=http_info.request_info.url.orig_url) + @dc.dataclass(frozen=True) class ErrorBlockPolicy(BlockPolicy): - """....""" error: Exception + @property + def traceback(self) -> str: + lines = tb.format_exception(None, self.error, self.error.__traceback__) + return ''.join(lines) + + def make_info_page(self, http_info: http_messages.FullHTTPInfo) \ + -> t.Optional[str]: + template = base.get_info_template('js_error_blocked_info.html.jinja') + return template.render( + url = http_info.request_info.url.orig_url, + settings = self.haketilo_settings, + traceback = self.traceback + ) + class MitmItPagePolicy(base.Policy): """ diff --git a/src/hydrilla/proxy/policies/payload.py b/src/hydrilla/proxy/policies/payload.py index 3252c6a..55851cc 100644 --- a/src/hydrilla/proxy/policies/payload.py +++ b/src/hydrilla/proxy/policies/payload.py @@ -173,6 +173,13 @@ class PayloadInjectPolicy(PayloadAwarePolicy): return soup.decode() + def make_info_page(self, http_info: http_messages.FullHTTPInfo) \ + -> t.Optional[str]: + return base.get_info_template('payload_info.html.jinja').render( + url = http_info.request_info.url.orig_url, + payload_data = self.payload_data + ) + class _PayloadHasProblemsError(HaketiloException): pass diff --git a/src/hydrilla/proxy/policies/payload_resource.py b/src/hydrilla/proxy/policies/payload_resource.py index 38cfd21..0d73242 100644 --- a/src/hydrilla/proxy/policies/payload_resource.py +++ b/src/hydrilla/proxy/policies/payload_resource.py @@ -261,18 +261,16 @@ class PayloadResourcePolicy(PayloadAwarePolicy): request_info: http_messages.RequestInfo ) -> MessageInfo: if path[0] == 'page_init_script.js': - with base.jinja_lock: - template = base.jinja_env.get_template( - 'page_init_script.js.jinja' - ) - token = self.payload_data.unique_token - base_url = self._assets_base_url(request_info.url) - ver_str = json.dumps(haketilo_version) - js = template.render( - unique_token_encoded = encode_string_for_js(token), - assets_base_url_encoded = encode_string_for_js(base_url), - haketilo_version = encode_string_for_js(ver_str) - ) + template = base.get_script_template('page_init_script.js.jinja') + + token = self.payload_data.unique_token + base_url = self._assets_base_url(request_info.url) + ver_str = json.dumps(haketilo_version) + js = template.render( + unique_token_encoded = encode_string_for_js(token), + assets_base_url_encoded = encode_string_for_js(base_url), + haketilo_version = encode_string_for_js(ver_str) + ) return http_messages.ResponseInfo.make( status_code = 200, diff --git a/src/hydrilla/proxy/policies/rule.py b/src/hydrilla/proxy/policies/rule.py index 8c5e69b..1f39295 100644 --- a/src/hydrilla/proxy/policies/rule.py +++ b/src/hydrilla/proxy/policies/rule.py @@ -67,11 +67,25 @@ class BlockPolicy(base.Policy): class RuleAllowPolicy(AllowPolicy): pattern: ParsedPattern + def make_info_page(self, http_info: http_messages.FullHTTPInfo) \ + -> t.Optional[str]: + return base.get_info_template('js_rule_allowed_info.html.jinja').render( + url = http_info.request_info.url.orig_url, + pattern = self.pattern.orig_url + ) + @dc.dataclass(frozen=True) class RuleBlockPolicy(BlockPolicy): pattern: ParsedPattern + def make_info_page(self, http_info: http_messages.FullHTTPInfo) \ + -> t.Optional[str]: + return base.get_info_template('js_rule_blocked_info.html.jinja').render( + url = http_info.request_info.url.orig_url, + pattern = self.pattern.orig_url + ) + @dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] class RulePolicyFactory(base.PolicyFactory): diff --git a/src/hydrilla/proxy/state.py b/src/hydrilla/proxy/state.py index d21a392..72eaaa0 100644 --- a/src/hydrilla/proxy/state.py +++ b/src/hydrilla/proxy/state.py @@ -171,6 +171,10 @@ class RuleStore(Store[RuleRef]): def add(self, pattern: str, allow: bool) -> RuleRef: ... + @abstractmethod + def get_by_pattern(self, pattern: str) -> RuleRef: + ... + class RepoNameInvalid(HaketiloException): pass @@ -445,6 +449,8 @@ class PayloadData: explicitly_enabled: bool unique_token: str + mapping_identifier: str + pattern: str pattern_path_segments: tuple[str, ...] eval_allowed: bool cors_bypass_allowed: bool diff --git a/src/hydrilla/proxy/state_impl/concrete_state.py b/src/hydrilla/proxy/state_impl/concrete_state.py index 5df7c34..a5c547f 100644 --- a/src/hydrilla/proxy/state_impl/concrete_state.py +++ b/src/hydrilla/proxy/state_impl/concrete_state.py @@ -367,6 +367,8 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): ref = payload_ref, explicitly_enabled = enabled_status == 'E', unique_token = token, + mapping_identifier = identifier, + pattern = pattern, pattern_path_segments = pattern_path_segments, eval_allowed = eval_allowed, cors_bypass_allowed = cors_bypass_allowed, diff --git a/src/hydrilla/proxy/state_impl/rules.py b/src/hydrilla/proxy/state_impl/rules.py index 2fed2c1..1761b04 100644 --- a/src/hydrilla/proxy/state_impl/rules.py +++ b/src/hydrilla/proxy/state_impl/rules.py @@ -148,6 +148,8 @@ class ConcreteRuleStore(st.RuleStore): (rule_id,), = cursor.fetchall() + self.state.rebuild_structures(payloads=False) + return ConcreteRuleRef(str(rule_id), self.state) def get_display_infos(self, allow: t.Optional[bool] = None) \ @@ -176,3 +178,19 @@ class ConcreteRuleStore(st.RuleStore): result.append(st.RuleDisplayInfo(ref, pattern, allow_scripts)) return result + + def get_by_pattern(self, pattern: str) -> st.RuleRef: + with self.state.cursor() as cursor: + cursor.execute( + 'SELECT rule_id FROM rules WHERE pattern = ?;', + (url_patterns.normalize_pattern(pattern),) + ) + + rows = cursor.fetchall() + + if rows == []: + raise st.MissingItemError() + + (rule_id,), = rows + + return ConcreteRuleRef(str(rule_id), self.state) diff --git a/src/hydrilla/proxy/web_ui/items.py b/src/hydrilla/proxy/web_ui/items.py index 808fb6d..d0f0f2e 100644 --- a/src/hydrilla/proxy/web_ui/items.py +++ b/src/hydrilla/proxy/web_ui/items.py @@ -390,8 +390,8 @@ def show_required_mapping( return flask.redirect(url) -@bp.route('/package/viewpayload///') -def show_payload(item_version_id: str, pattern: str, lib_identifier: str) \ +@bp.route('/package/viewlibrary///') +def show_package_library(item_version_id: str, pattern: str, lib_identifier: str) \ -> werkzeug.Response: state = _app.get_haketilo_state() @@ -406,10 +406,35 @@ def show_payload(item_version_id: str, pattern: str, lib_identifier: str) \ item_version_id = resource_ver_ref.id ) except st.MissingItemError: - resource_ref = \ - state.resource_store().get_by_identifier(lib_identifier) + resource_ref = state.resource_store().get_by_identifier( + lib_identifier + ) url = flask.url_for('.show_library', item_id=resource_ref.id) except st.MissingItemError: flask.abort(404) return flask.redirect(url) + +@bp.route('/package/viewbypayload//') +def show_payload_package(payload_id: str, package_identifier: str) \ + -> werkzeug.Response: + state = _app.get_haketilo_state() + + try: + ref = state.payload_store().get(payload_id) + + try: + mapping_ver_ref = ref.get_display_info().mapping_info.ref + url = flask.url_for( + '.show_package_version', + item_version_id = mapping_ver_ref.id + ) + except st.MissingItemError: + mapping_ref = state.mapping_store().get_by_identifier( + package_identifier + ) + url = flask.url_for('.show_package', item_id=mapping_ref.id) + except st.MissingItemError: + flask.abort(404) + + return flask.redirect(url) diff --git a/src/hydrilla/proxy/web_ui/items_import.py b/src/hydrilla/proxy/web_ui/items_import.py index a5b5f18..f94768f 100644 --- a/src/hydrilla/proxy/web_ui/items_import.py +++ b/src/hydrilla/proxy/web_ui/items_import.py @@ -51,7 +51,13 @@ bp = flask.Blueprint('import', __package__) @bp.route('/import', methods=['GET']) def items_import(errors: t.Mapping[str, bool] = {}) -> werkzeug.Response: - html = flask.render_template('import.html.jinja', **errors) + pattern = flask.request.args.get('pattern') + if pattern is None: + extra_args = {} + else: + extra_args = {'pattern': normalize_pattern(pattern)} + + html = flask.render_template('import.html.jinja', **errors, **extra_args) return flask.make_response(html, 200) def items_import_from_file() -> werkzeug.Response: @@ -172,7 +178,6 @@ def item_import_ad_hoc() -> werkzeug.Response: try: builder_args = ['-s', str(source_dir), '-d', str(malcontent_dir)] build.perform(builder_args, standalone_mode=False) - build.perform(['-s', str(source_dir), '-d', '/tmp/haketilodebug'], standalone_mode=False) _app.get_haketilo_state().import_items(malcontent_dir) except: import traceback diff --git a/src/hydrilla/proxy/web_ui/rules.py b/src/hydrilla/proxy/web_ui/rules.py index 56753a3..606d33f 100644 --- a/src/hydrilla/proxy/web_ui/rules.py +++ b/src/hydrilla/proxy/web_ui/rules.py @@ -107,3 +107,16 @@ def alter_rule(rule_id: str) -> werkzeug.Response: flask.abort(404) return flask.redirect(flask.url_for('.show_rule', rule_id=rule_id)) + +@bp.route('/rules/viewbypattern') +def show_pattern_rule() -> werkzeug.Response: + pattern = flask.request.args['pattern'] + + try: + store = _app.get_haketilo_state().rule_store() + rule_ref = store.get_by_pattern(pattern) + except st.MissingItemError: + html = flask.render_template('rules/add.html.jinja', pattern=pattern) + return flask.make_response(html, 200) + + return flask.redirect(flask.url_for('.show_rule', rule_id=rule_ref.id)) diff --git a/src/hydrilla/proxy/web_ui/templates/import.html.jinja b/src/hydrilla/proxy/web_ui/templates/import.html.jinja index 6ec9947..7f3be50 100644 --- a/src/hydrilla/proxy/web_ui/templates/import.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/import.html.jinja @@ -103,7 +103,7 @@ code in a proprietary work, I am not going to enforce this in court. {% if invalid_ad_hoc_patterns is defined %} {{ error_note(_('web_ui.err.invalid_ad_hoc_patterns')) }} {% endif %} - {{ form_field('patterns', height=3) }} + {{ form_field('patterns', height=3, initial_value=pattern|default(none)) }} {{ label(_('web_ui.import.script_text_field_label'), 'script_text') }} {{ form_field('script_text', required=false, height=15) }} diff --git a/src/hydrilla/proxy/web_ui/templates/index.html.jinja b/src/hydrilla/proxy/web_ui/templates/index.html.jinja index 2b49361..ff74369 100644 --- a/src/hydrilla/proxy/web_ui/templates/index.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/index.html.jinja @@ -299,4 +299,8 @@ code in a proprietary work, I am not going to enforce this in court. {{ _('web_ui.home.payloadon_popup_no') }} {% endif %} {% endcall %} + +

+ {{ _('web_ui.home.popup_can_be_opened_by') }} +

{% endblock main %} diff --git a/src/hydrilla/proxy/web_ui/templates/items/package_viewversion.html.jinja b/src/hydrilla/proxy/web_ui/templates/items/package_viewversion.html.jinja index fe816ab..386c0c8 100644 --- a/src/hydrilla/proxy/web_ui/templates/items/package_viewversion.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/items/package_viewversion.html.jinja @@ -75,7 +75,7 @@ code in a proprietary work, I am not going to enforce this in court. {% set encoded = patterns[0]|urlencode|replace('/', '%2F') %} {% set url = url_for( - '.show_payload', + '.show_package_library', item_version_id = version_display_info.ref.id, pattern = encoded, lib_identifier = lib_identifier diff --git a/src/hydrilla/proxy/web_ui/templates/rules/add.html.jinja b/src/hydrilla/proxy/web_ui/templates/rules/add.html.jinja index 6d21ccd..9e4b869 100644 --- a/src/hydrilla/proxy/web_ui/templates/rules/add.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/rules/add.html.jinja @@ -24,14 +24,14 @@ code in a proprietary work, I am not going to enforce this in court. {% block main %}

{{ _('web_ui.rules.add.heading') }}

-
+ {{ label(_('web_ui.rules.add.pattern_field_label'), 'pattern') }} {% if rule_pattern_invalid is defined %} {{ error_note(_('web_ui.err.rule_pattern_invalid')) }} {% endif %} - {{ form_field('pattern') }} + {{ form_field('pattern', initial_value=pattern|default(none)) }} {{ label(_('web_ui.rules.add.block_or_allow_label'), 'allow') }} -- cgit v1.2.3