fix: extend RFC1918 LNURL carve-out to the HTTP-views path
Some checks failed
lint.yml / fix: extend RFC1918 LNURL carve-out to the HTTP-views path (push) Failing after 0s

#2 added the loopback/RFC1918 carve-out to the nostr-transport helper
(`create_lnurl_from_baseurl`) but `views.py` / `views_api.py` still call
`create_lnurl`, which went straight through `lnurl_encode` and got the
same `InvalidUrl` rejection. Visible as a 500 "Error creating LNURL …
check your webserver proxy configuration." on the admin UI when LNbits
itself is on `http://192.168.x.x:port`.

Extract the encode + carve-out logic into `_encode_lnurl(url, hint)` and
route both `create_lnurl` and `create_lnurl_from_baseurl` through it.
Both now return the same `_EncodedLnurl` dataclass (a minimal duck for
`.bech32`/`.url`) — `Lnurl` itself can't be returned in the LAN-local
case because its `__new__` re-runs python-lnurl's host validation on
bech32-decode.

Call sites in views.py / views_api.py unchanged: they already access
`.bech32` and `.url`, which the dataclass exposes. `_populate_lnurl`
back to attribute access too.
This commit is contained in:
Padreug 2026-06-01 21:35:04 +02:00
commit 40dce4d88c
2 changed files with 51 additions and 38 deletions

View file

@ -1,9 +1,9 @@
import ipaddress import ipaddress
from dataclasses import dataclass
from urllib.parse import urlparse from urllib.parse import urlparse
from fastapi import Request from fastapi import Request
from lnbits.settings import settings from lnbits.settings import settings
from lnurl import Lnurl
from lnurl import encode as lnurl_encode from lnurl import encode as lnurl_encode
from lnurl.helpers import url_encode from lnurl.helpers import url_encode
from shortuuid import uuid from shortuuid import uuid
@ -11,26 +11,18 @@ from shortuuid import uuid
from .models import WithdrawLink from .models import WithdrawLink
def create_lnurl(link: WithdrawLink, req: Request) -> Lnurl: @dataclass(frozen=True)
if link.is_unique: class _EncodedLnurl:
usescssv = link.usescsv.split(",") """Minimal duck-typed stand-in for `lnurl.Lnurl` exposing just the
tohash = link.id + link.unique_hash + usescssv[link.number] `.bech32` and `.url` attributes the callers (views.py, views_api.py,
multihash = uuid(name=tohash) transport_rpcs.py) read. We can't always return a real `Lnurl` —
url = req.url_for( its `__new__` re-runs python-lnurl's HTTPS-required host check on
"withdraw.api_lnurl_multi_response", bech32-decode, which rejects RFC1918/loopback HTTP URLs even after
unique_hash=link.unique_hash, we've intentionally encoded one. See #2.
id_unique_hash=multihash, """
)
else:
url = req.url_for("withdraw.api_lnurl_response", unique_hash=link.unique_hash)
try: bech32: str
return lnurl_encode(str(url)) url: str
except Exception as e:
raise ValueError(
f"Error creating LNURL with url: `{url!s}`, "
"check your webserver proxy configuration."
) from e
def _is_private_network_http(url: str) -> bool: def _is_private_network_http(url: str) -> bool:
@ -57,17 +49,46 @@ def _is_private_network_http(url: str) -> bool:
return ip.is_loopback or ip.is_private return ip.is_loopback or ip.is_private
def create_lnurl_from_baseurl(link: WithdrawLink) -> tuple[str, str]: def _encode_lnurl(url: str, error_hint: str) -> _EncodedLnurl:
"""Bech32-encode `url` as an LNURL. LAN-local HTTP URLs (loopback /
RFC1918) skip python-lnurl's HTTPS-required host validation via the
public `lnurl.helpers.url_encode` helper. Everything else goes
through the validated `lnurl_encode` path. See #2.
"""
if _is_private_network_http(url):
return _EncodedLnurl(bech32=url_encode(url), url=url)
try:
encoded = lnurl_encode(url)
except Exception as e:
raise ValueError(
f"Error creating LNURL with url: `{url!s}`, {error_hint}"
) from e
return _EncodedLnurl(bech32=str(encoded.bech32), url=str(encoded.url))
def create_lnurl(link: WithdrawLink, req: Request) -> _EncodedLnurl:
if link.is_unique:
usescssv = link.usescsv.split(",")
tohash = link.id + link.unique_hash + usescssv[link.number]
multihash = uuid(name=tohash)
url = req.url_for(
"withdraw.api_lnurl_multi_response",
unique_hash=link.unique_hash,
id_unique_hash=multihash,
)
else:
url = req.url_for("withdraw.api_lnurl_response", unique_hash=link.unique_hash)
return _encode_lnurl(str(url), "check your webserver proxy configuration.")
def create_lnurl_from_baseurl(link: WithdrawLink) -> _EncodedLnurl:
""" """
Same shape as `create_lnurl`, but composes the callback URL from Same shape as `create_lnurl`, but composes the callback URL from
`settings.lnbits_baseurl` instead of a FastAPI `Request`. Used by `settings.lnbits_baseurl` instead of a FastAPI `Request`. Used by
the nostr-transport RPC handlers, which have no HTTP request to the nostr-transport RPC handlers, which have no HTTP request to
derive a base URL from. 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("/") base = settings.lnbits_baseurl.rstrip("/")
if link.is_unique: if link.is_unique:
@ -78,14 +99,4 @@ def create_lnurl_from_baseurl(link: WithdrawLink) -> tuple[str, str]:
else: else:
url = f"{base}/withdraw/api/v1/lnurl/{link.unique_hash}" url = f"{base}/withdraw/api/v1/lnurl/{link.unique_hash}"
if _is_private_network_http(url): return _encode_lnurl(url, "check your `LNBITS_BASEURL` configuration.")
return url_encode(url), url
try:
encoded = 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)

View file

@ -211,7 +211,9 @@ def _populate_lnurl(link: WithdrawLink) -> WithdrawLink:
duplicating state LNbits already knows. See aiolabs/withdraw#1. duplicating state LNbits already knows. See aiolabs/withdraw#1.
""" """
try: try:
link.lnurl, link.lnurl_url = create_lnurl_from_baseurl(link) encoded = create_lnurl_from_baseurl(link)
link.lnurl = encoded.bech32
link.lnurl_url = encoded.url
except ValueError: except ValueError:
pass pass
return link return link