fix: allow HTTP LNURL for RFC1918/loopback baseurls (#2)
Some checks failed
lint.yml / fix: allow HTTP LNURL for RFC1918/loopback baseurls (#2) (push) Failing after 0s

`python-lnurl`'s `lnurl_encode` rejects HTTP URLs whose host isn't
`localhost`/`127.0.0.1`/`.onion`, so a regtest LNbits on a LAN IP
(e.g. `http://192.168.0.32:5001`) made `_populate_lnurl` swallow
`InvalidUrl` and leave `link.lnurl=None` — breaking the LAN-local
cross-device smoke flow.

Extend the existing localhost carve-out to the full RFC1918 set:
loopback, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`. These are
intrinsically unreachable from the public internet, so producing an
HTTP LNURL pointing at one is unambiguously a dev/internal scenario.
For matching URLs, skip `lnurl_encode`'s host validation by calling
the public `lnurl.helpers.url_encode` directly (which bech32-encodes
without URL validation). Everything else still goes through the
validated path — production with HTTP + public IP/hostname stays
rejected.

`create_lnurl_from_baseurl` now returns `(bech32, url)` directly
rather than a `Lnurl` instance, since the private-network branch
can't construct a real `Lnurl` (its `__new__` re-runs the same host
validation on bech32-decode). The caller `_populate_lnurl` was the
only consumer.

Test coverage on `_is_private_network_http` covers the carve-out
boundary (loopback, RFC1918, the just-outside-RFC1918 ranges, public
hosts, and the `https://` case). The full encode path is exercised
via regtest smoke.

Closes #2.
This commit is contained in:
Padreug 2026-06-01 21:14:48 +02:00
commit 66026abe96
3 changed files with 107 additions and 5 deletions

67
tests/test_helpers.py Normal file
View file

@ -0,0 +1,67 @@
"""
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