aboutsummaryrefslogtreecommitdiff
path: root/src/hydrilla/proxy
diff options
context:
space:
mode:
Diffstat (limited to 'src/hydrilla/proxy')
-rw-r--r--src/hydrilla/proxy/addon.py15
-rw-r--r--src/hydrilla/proxy/http_messages.py33
-rw-r--r--src/hydrilla/proxy/policies/__init__.py2
-rw-r--r--src/hydrilla/proxy/policies/base.py95
-rw-r--r--src/hydrilla/proxy/policies/info_pages_templates/info_base.html.jinja89
-rw-r--r--src/hydrilla/proxy/policies/info_pages_templates/js_error_blocked_info.html.jinja22
-rw-r--r--src/hydrilla/proxy/policies/info_pages_templates/js_fallback_allowed_info.html.jinja14
-rw-r--r--src/hydrilla/proxy/policies/info_pages_templates/js_fallback_blocked_info.html.jinja14
-rw-r--r--src/hydrilla/proxy/policies/info_pages_templates/js_rule_allowed_info.html.jinja14
-rw-r--r--src/hydrilla/proxy/policies/info_pages_templates/js_rule_blocked_info.html.jinja14
-rw-r--r--src/hydrilla/proxy/policies/info_pages_templates/js_rule_info.html.jinja37
-rw-r--r--src/hydrilla/proxy/policies/info_pages_templates/payload_info.html.jinja48
-rw-r--r--src/hydrilla/proxy/policies/info_pages_templates/special_page_info.html.jinja17
-rw-r--r--src/hydrilla/proxy/policies/injectable_scripts/popup.js.jinja173
-rw-r--r--src/hydrilla/proxy/policies/misc.py28
-rw-r--r--src/hydrilla/proxy/policies/payload.py7
-rw-r--r--src/hydrilla/proxy/policies/payload_resource.py22
-rw-r--r--src/hydrilla/proxy/policies/rule.py14
-rw-r--r--src/hydrilla/proxy/state.py6
-rw-r--r--src/hydrilla/proxy/state_impl/concrete_state.py2
-rw-r--r--src/hydrilla/proxy/state_impl/rules.py18
-rw-r--r--src/hydrilla/proxy/web_ui/items.py33
-rw-r--r--src/hydrilla/proxy/web_ui/items_import.py9
-rw-r--r--src/hydrilla/proxy/web_ui/rules.py13
-rw-r--r--src/hydrilla/proxy/web_ui/templates/import.html.jinja2
-rw-r--r--src/hydrilla/proxy/web_ui/templates/index.html.jinja4
-rw-r--r--src/hydrilla/proxy/web_ui/templates/items/package_viewversion.html.jinja2
-rw-r--r--src/hydrilla/proxy/web_ui/templates/rules/add.html.jinja4
28 files changed, 677 insertions, 74 deletions
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<mime>[\w-]+/[\w-]+)
-\s*
-(?:
- ;
- (?:[^;]*;)* # match possible parameter other than "charset"
-)
-\s*
-charset= # no whitespace allowed in parameter as per RFC
-(?P<encoding>
- [\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}<script>{popup_script}</script>{doc_rest}'
+ script_tag = f'<script nonce="{nonce}">{popup_script}</script>'
+
+ 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 <koszko@koszko.org>
+
+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() }}
+
+ <title>{{ _('info.base.title') }}</title>
+{% endblock head %}
+
+{% block main %}
+ <h3>
+ {{ _('info.base.heading.page_info') }}
+ </h3>
+
+ {{ label(_('info.base.page_url_label')) }}
+
+ <p>
+ {{ url }}
+ </p>
+
+ <div class="horizontal-separator"></div>
+
+ {{ label(_('info.base.page_policy_label')) }}
+
+ <p class="has-colored-links">
+ {% block site_policy required %}{% endblock %}
+ </p>
+
+ {% block main_rest %}
+ {% endblock %}
+
+ {% block options %}
+ <div class="horizontal-separator"></div>
+
+ {{ 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" %}
+ <a class="{{classes}}" href="{{ hkt_url }}" target="_blank">
+ {{ but_text }}
+ </a>
+ {% 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 <koszko@koszko.org>
+#}
+{% 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 <koszko@koszko.org>
+#}
+{% 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 <koszko@koszko.org>
+#}
+{% 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 <koszko@koszko.org>
+#}
+{% 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 <koszko@koszko.org>
+#}
+{% 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 <koszko@koszko.org>
+
+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 %}
+ <div class="horizontal-separator"></div>
+
+ {{ label(_('info.rule.matched_pattern_label')) }}
+
+ <p>
+ {{ pattern }}
+ </p>
+{% 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 <koszko@koszko.org>
+
+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 %}
+ <div class="horizontal-separator"></div>
+
+ {{ label(_('info.payload.matched_pattern_label')) }}
+
+ <p>
+ {{ payload_data.pattern }}
+ </p>
+{% 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 <koszko@koszko.org>
+#}
+{% 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/<string:item_version_id>/<string:pattern>/<string:lib_identifier>')
-def show_payload(item_version_id: str, pattern: str, lib_identifier: str) \
+@bp.route('/package/viewlibrary/<string:item_version_id>/<string:pattern>/<string:lib_identifier>')
+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/<string:payload_id>/<string:package_identifier>')
+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 %}
+
+ <p>
+ {{ _('web_ui.home.popup_can_be_opened_by') }}
+ </p>
{% 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 %}
<h3>{{ _('web_ui.rules.add.heading') }}</h3>
- <form method="POST">
+ <form method="POST" action="{{ url_for('.add_rule') }}">
{{ 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') }}