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
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:
parent
66026abe96
commit
40dce4d88c
2 changed files with 51 additions and 38 deletions
85
helpers.py
85
helpers.py
|
|
@ -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)
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue