From c7c1ff32a0bd5fc1ae8d88f581af403041034f4d Mon Sep 17 00:00:00 2001 From: "Arthur K." Date: Fri, 7 Nov 2025 19:35:10 +0300 Subject: [PATCH] --- README.md | 3 + openwrt_pbr.sh | 54 +++++++++++++++++ poller.py | 148 +++++++++++++++++++++++++++++++++++++++++++++ socket_wildcard.py | 45 ++++++++++++++ 4 files changed, 250 insertions(+) create mode 100644 README.md create mode 100644 openwrt_pbr.sh create mode 100644 poller.py create mode 100644 socket_wildcard.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..af9053a --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +some naturally occuring scripts + +i dont' have a place to put them, but deleting them feels like a waste diff --git a/openwrt_pbr.sh b/openwrt_pbr.sh new file mode 100644 index 0000000..92cb144 --- /dev/null +++ b/openwrt_pbr.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +help() { + echo 'up/down [DEVICE]' + echo 'status [DEVICE]' +} + +is_available() { + uci show pbr | grep -q "pbr.$1=policy" +} + + +# EXIT_SUCCESS if enabled +# EXIT_FAILURE if disabled +status() { + uci get pbr."$1".enabled >/dev/null 2>&1 +} + +up() { + is_available "$1" || exit 1 + status "$1" || exit 1 + uci del pbr."$1".enabled >/dev/null + uci commit >/dev/null + /etc/init.d/pbr reload > /dev/null +} + +down() { + is_available "$1" || exit 1 + status "$1" && exit 1 + uci set pbr."$1".enabled=0 >/dev/null + uci commit >/dev/null + /etc/init.d/pbr reload > /dev/null +} + +toggle() { + is_available "$1" || exit 1 + if status "$1"; then + up "$1" + else + down "$1" + fi +} + +if [ $# -lt 2 ]; then + help >&2 + exit 1 +fi + +case "$1" in + "up") up "$2";; + "down") down "$2" ;; + "toggle") toggle "$2" ;; + "status") status "$2" && echo '0' || echo '1' ;; +esac diff --git a/poller.py b/poller.py new file mode 100644 index 0000000..2a8cfd2 --- /dev/null +++ b/poller.py @@ -0,0 +1,148 @@ +import json +import random +import time +import requests +from collections.abc import Callable, Generator +from dataclasses import dataclass +from enum import Enum +from typing import Any + +class Poller: + @dataclass + class ChangedValue[T]: + value: T + + @dataclass + class FoundValue: + value: str + + class _LogTypes(Enum): + NOTHING = "Nothing found." + CHANGED = "The value has changed, but nothing was found." + FOUND = "Found!" + + def __init__( + self, + filter: Callable[[requests.Response], str | None], + requester: Callable[[], requests.Response], + transformer: Callable[[requests.Response], Any] = lambda x: x.text, + base_delay: tuple[int, int] = (20 * 60, 45 * 60), + retry_delay: tuple[int, int] = (3 * 60, 10 * 60) + ) -> None: + """ + Initialize a Poller instance for periodically polling an API endpoint + until a specific result is found. + This class repeatedly sends requests to an API and inspects the responses + using a filter function. If the filter detects the desired result, + polling stops. Otherwise, it continues after a delay. A transformer + function is used to normalize or clean the response content (e.g., removing + noise such as timestamps or metadata) to avoid unnecessary triggering of + the "changed" state. + :param filter: A callable that examines a ``requests.Response`` object and + returns a unique string (e.g., a key, ID, or result indicator) + if the target condition is met. Returns ``None`` if polling + should continue. This is the key signal for terminating the poll. + :type filter: Callable[[requests.Response], str | None] + :param requester: A callable that performs the actual API request and returns + a ``requests.Response`` object. This abstracts the HTTP + communication from the polling logic. + :type requester: Callable[[], requests.Response] + :param transformer: A callable used to clean or transform the response for + change detection purposes. It helps filter out inconsequential + differences like timestamps, headers, etc., that shouldn't + be interpreted as meaningful changes. Defaults to + ``lambda x: x.text``. + :type transformer: Callable[[requests.Response], Any], optional + :param base_delay: A tuple representing the range of time in seconds to wait + between polling cycles when no change is detected. This helps + reduce the frequency of API calls when responses remain stable. + :type base_delay: tuple[int, int], optional + :param retry_delay: A tuple representing the range of time in seconds to wait + after a change is detected (but not a final result), often + to give the API time to settle or provide consistent data. + :type retry_delay: tuple[int, int], optional + :returns: None + :rtype: None + """ + self.prev_resp = None + self.filter = filter + self.transformer = transformer + self.requester = requester + self.base_delay = base_delay + self.retry_delay = retry_delay + + + @staticmethod + def _log(t: _LogTypes, r: Any): + print(f'[{time.strftime("%X %x %Z")}] {t.value} Response: {r}') + + def _set_and_return[T](self, value: T) -> tuple[tuple[int, int], ChangedValue[T] | None]: + if self.prev_resp != value: + self.prev_resp = value + self._log(self._LogTypes.CHANGED, value) + return self.retry_delay, self.ChangedValue(value) + self._log(self._LogTypes.NOTHING, value) + return self.base_delay, None + + def poll(self) -> Generator[ChangedValue | FoundValue | None]: + while True: + r = self.requester() + + match self.filter(r): + case None: + delay, value = self._set_and_return(self.transformer(r)) + yield value + case value: + self._log(self._LogTypes.FOUND, value) + yield self.FoundValue(value) + return + + time.sleep(random.randint(*delay)) + + +TOKEN = "" +ADMIN_ID = 0 +TG_API_URL = f'https://api.telegram.org/bot{TOKEN}/sendMessage' + +def send_message(prefix: str, body: Any): + prefix += '\n' + body = str(body) + + entities = { + 'type': 'pre', + 'offset': len(prefix) + 1, + 'length': len(body) + } + + r = requests.post(TG_API_URL, data = { + 'chat_id': ADMIN_ID, + 'entities': json.dumps([entities]), + 'text': prefix + body, + }) + if r.status_code != 200: + print(r.text) + + +def make_request() -> requests.Response: + return requests.get("https://google.com") + + +def my_filter(r: requests.Response) -> str | None: + if r.status_code != 200: + return + return r.text + + +def main(): + print('Running!') + poller = Poller(my_filter, make_request) + + for resp in poller.poll(): + match resp: + case Poller.ChangedValue(x): + send_message("🟡", x) + case Poller.FoundValue(x): + send_message("🟢", x) + +if __name__ == "__main__": + main() diff --git a/socket_wildcard.py b/socket_wildcard.py new file mode 100644 index 0000000..8cd177d --- /dev/null +++ b/socket_wildcard.py @@ -0,0 +1,45 @@ +import socket +import struct + +# use with +# iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 2000 + +SO_ORIGINAL_DST = 80 # from linux/netfilter_ipv4.h + +def get_original_dst(conn): + """Return (ip, port) of the original destination before REDIRECT.""" + # Get raw sockaddr_in (16 bytes) + odest = conn.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, 16) + port, raw_ip = struct.unpack_from('!2xH4s', odest) + ip = socket.inet_ntoa(raw_ip) + return ip, port + +def main(): + HOST = '0.0.0.0' + PORT = 2000 + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((HOST, PORT)) + s.listen(10) + + print(f"Listening on {HOST}:{PORT}") + + while True: + conn, addr = s.accept() + try: + orig_ip, orig_port = get_original_dst(conn) + print(f"Connection from {addr}, originally to {orig_ip}:{orig_port}") + except OSError as e: + print(f"Could not get original destination: {e}") + + with conn: + while True: + data = conn.recv(1024) + if not data: + break + conn.sendall(data) + +if __name__ == "__main__": + main() +