From 2877cf6b20dcea1f2f79d2ccffb7419ea9a6a8b7 Mon Sep 17 00:00:00 2001 From: Padreug Date: Mon, 1 Jun 2026 21:44:57 +0200 Subject: [PATCH] Revert "fix: allow HTTP LNURL for RFC1918/loopback baseurls (#2)" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 66026ab. Closes #2 as resolved by switching the dev LNbits to TLS (self-signed cert) instead of carving out plain HTTP for RFC1918 hosts. With HTTPS the producer-side python-lnurl validation accepts any host, AND the lnbits-core consumer-side `lnurlscan` accepts it too — the symmetric problem the carve-out couldn't solve on its own. `create_lnurl_from_baseurl` (#1, `e9d911e`) is kept — it's orthogonal to the transport scheme and still wanted for the nostr-transport `lnurl=null` fix. --- helpers.py | 41 ++------------------------ tests/test_helpers.py | 67 ------------------------------------------- transport_rpcs.py | 4 ++- 3 files changed, 5 insertions(+), 107 deletions(-) delete mode 100644 tests/test_helpers.py diff --git a/helpers.py b/helpers.py index 7a2bba5..31efcff 100644 --- a/helpers.py +++ b/helpers.py @@ -1,11 +1,7 @@ -import ipaddress -from urllib.parse import urlparse - from fastapi import Request from lnbits.settings import settings from lnurl import Lnurl from lnurl import encode as lnurl_encode -from lnurl.helpers import url_encode from shortuuid import uuid from .models import WithdrawLink @@ -33,41 +29,12 @@ def create_lnurl(link: WithdrawLink, req: Request) -> Lnurl: ) from e -def _is_private_network_http(url: str) -> bool: - """True iff `url` is `http://` AND the host is loopback, `localhost`, - or in an RFC1918 private range. These are intrinsically unreachable - from the public internet, so producing an HTTP LNURL pointing at one - is a dev/internal-test scenario, not a production misconfig. - - Extends python-lnurl's existing `localhost` / `127.0.0.1` carve-out - (see `lnurl.types.INSECURE_HOSTS`) to the full RFC1918 set so - cross-device regtest smoke against a LAN-IP LNbits works without - standing up TLS termination. - """ - parsed = urlparse(url) - if parsed.scheme != "http": - return False - host = (parsed.hostname or "").lower() - if host == "localhost": - return True - try: - ip = ipaddress.ip_address(host) - except ValueError: - return False - return ip.is_loopback or ip.is_private - - -def create_lnurl_from_baseurl(link: WithdrawLink) -> tuple[str, str]: +def create_lnurl_from_baseurl(link: WithdrawLink) -> Lnurl: """ Same shape as `create_lnurl`, but composes the callback URL from `settings.lnbits_baseurl` instead of a FastAPI `Request`. Used by the nostr-transport RPC handlers, which have no HTTP request to derive a base URL from. - - Returns `(bech32, url)` — the two fields `_populate_lnurl` writes - onto `WithdrawLink.lnurl` / `lnurl_url`. LAN-local HTTP URLs - (loopback / RFC1918) skip python-lnurl's HTTPS-required check via - the public `lnurl.helpers.url_encode` helper; see #2. """ base = settings.lnbits_baseurl.rstrip("/") if link.is_unique: @@ -78,14 +45,10 @@ def create_lnurl_from_baseurl(link: WithdrawLink) -> tuple[str, str]: else: url = f"{base}/withdraw/api/v1/lnurl/{link.unique_hash}" - if _is_private_network_http(url): - return url_encode(url), url - try: - encoded = lnurl_encode(url) + return lnurl_encode(url) except Exception as e: raise ValueError( f"Error creating LNURL with url: `{url!s}`, " "check your `LNBITS_BASEURL` configuration." ) from e - return str(encoded.bech32), str(encoded.url) diff --git a/tests/test_helpers.py b/tests/test_helpers.py deleted file mode 100644 index 01e1e69..0000000 --- a/tests/test_helpers.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Unit tests for the private-network HTTP detector that gates the -plain-HTTP carve-out in `create_lnurl_from_baseurl`. See #2. - -The full encode path is exercised end-to-end during regtest smoke; -these tests cover the pure host-classification logic so changes to -the carve-out boundary (e.g., adding `.onion`) can be regression-tested -without spinning up a wallet/settings fixture. -""" - -import pytest - -from ..helpers import _is_private_network_http - - -@pytest.mark.parametrize( - "url", - [ - "http://localhost/withdraw/api/v1/lnurl/abc", - "http://localhost:5000/withdraw/api/v1/lnurl/abc", - "http://127.0.0.1/withdraw/api/v1/lnurl/abc", - "http://127.0.0.1:5000/withdraw/api/v1/lnurl/abc", - "http://10.0.0.5:5000/withdraw/api/v1/lnurl/abc", - "http://172.16.0.5:5000/withdraw/api/v1/lnurl/abc", - "http://172.31.255.255:5000/withdraw/api/v1/lnurl/abc", - "http://192.168.0.32:5001/withdraw/api/v1/lnurl/abc", - "http://192.168.255.255/withdraw/api/v1/lnurl/abc", - ], -) -def test_is_private_network_http_accepts_loopback_and_rfc1918(url): - assert _is_private_network_http(url) is True - - -@pytest.mark.parametrize( - "url", - [ - # Public IPv4 - "http://8.8.8.8/withdraw/api/v1/lnurl/abc", - "http://1.1.1.1/withdraw/api/v1/lnurl/abc", - # Just-outside RFC1918 - "http://11.0.0.1/withdraw/api/v1/lnurl/abc", - "http://172.15.0.1/withdraw/api/v1/lnurl/abc", - "http://172.32.0.1/withdraw/api/v1/lnurl/abc", - "http://192.167.0.1/withdraw/api/v1/lnurl/abc", - "http://192.169.0.1/withdraw/api/v1/lnurl/abc", - # Public hostnames (not an IP literal, not localhost) - "http://example.com/withdraw/api/v1/lnurl/abc", - "http://lnbits.example.com/withdraw/api/v1/lnurl/abc", - ], -) -def test_is_private_network_http_rejects_public_hosts(url): - assert _is_private_network_http(url) is False - - -@pytest.mark.parametrize( - "url", - [ - "https://localhost/withdraw/api/v1/lnurl/abc", - "https://127.0.0.1/withdraw/api/v1/lnurl/abc", - "https://192.168.0.32/withdraw/api/v1/lnurl/abc", - "https://example.com/withdraw/api/v1/lnurl/abc", - ], -) -def test_is_private_network_http_rejects_https_scheme(url): - """Detector only fires for `http://`. `https://` always takes the - validated `lnurl_encode` path (which accepts any host).""" - assert _is_private_network_http(url) is False diff --git a/transport_rpcs.py b/transport_rpcs.py index 0fac4d6..193d7c3 100644 --- a/transport_rpcs.py +++ b/transport_rpcs.py @@ -211,7 +211,9 @@ def _populate_lnurl(link: WithdrawLink) -> WithdrawLink: duplicating state LNbits already knows. See aiolabs/withdraw#1. """ try: - link.lnurl, link.lnurl_url = create_lnurl_from_baseurl(link) + encoded = create_lnurl_from_baseurl(link) + link.lnurl = str(encoded.bech32) + link.lnurl_url = str(encoded.url) except ValueError: pass return link