diff --git a/crud.py b/crud.py index 7b377df..deb2028 100644 --- a/crud.py +++ b/crud.py @@ -14,7 +14,7 @@ async def get_or_create_lnurlp_settings() -> LnurlpSettings: if row: return LnurlpSettings(**row) else: - settings = LnurlpSettings(nostr_private_key=PrivateKey().hex()) + settings = LnurlpSettings(nostr_private_key=PrivateKey().hex(),allow_insecure_http=False) await db.execute( insert_query("lnurlp.settings", settings), (*settings.dict().values(),) ) diff --git a/manifest.json b/manifest.json index cc0c3d2..aeb3d81 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "repos": [ { "id": "lnurlp", - "organisation": "lnbits", + "organisation": "oren-z0", "repository": "lnurlp" } ] diff --git a/migrations.py b/migrations.py index d39bc7c..12da2d8 100644 --- a/migrations.py +++ b/migrations.py @@ -174,3 +174,13 @@ async def m009_add_settings(db): ); """ ) + +async def m010_add_allow_insecure_http_to_settings(db): + """ + Add extension settings table + """ + await db.execute( + """ + ALTER TABLE lnurlp.settings ADD COLUMN allow_insecure_http BOOLEAN; + """ + ) diff --git a/models.py b/models.py index 22a8ace..7706936 100644 --- a/models.py +++ b/models.py @@ -5,6 +5,7 @@ from fastapi import Request from fastapi.param_functions import Query from lnurl import encode as lnurl_encode +from lnurl.helpers import url_encode from lnurl.types import LnurlPayMetadata from pydantic import BaseModel @@ -14,6 +15,7 @@ class LnurlpSettings(BaseModel): nostr_private_key: str + allow_insecure_http: bool | None @property def private_key(self) -> PrivateKey: @@ -69,14 +71,14 @@ def from_row(cls, row: Row) -> "PayLink": data["max"] /= data["fiat_base_multiplier"] return cls(**data) - def lnurl(self, req: Request) -> str: + def lnurl(self, req: Request, allow_insecure_http = False) -> str: url = req.url_for("lnurlp.api_lnurl_response", link_id=self.id) url_str = str(url) if url.netloc.endswith(".onion"): # change url string scheme to http url_str = url_str.replace("https://", "http://") - return lnurl_encode(url_str) + return url_encode(url_str) if allow_insecure_http else lnurl_encode(url_str) @property def lnurlpay_metadata(self) -> LnurlPayMetadata: diff --git a/static/js/index.js b/static/js/index.js index 6c3711b..b1e1c2c 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -36,6 +36,11 @@ new Vue({ type: 'str', description: 'Nostr private key used to zap', name: 'nostr_private_key' + }, + { + type: 'bool', + description: 'Allow insecure http: advance usage only, local domain names encoded in lightning-addresses and lnurls may be broken, and communication may be insecure.', + name: 'allow_insecure_http' } ], domain: window.location.host, diff --git a/views.py b/views.py index d56c13e..e5d7b79 100644 --- a/views.py +++ b/views.py @@ -8,7 +8,7 @@ from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse -from .crud import get_pay_link +from .crud import get_pay_link, get_or_create_lnurlp_settings lnurlp_generic_router = APIRouter() @@ -34,7 +34,8 @@ async def display(request: Request, link_id): raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist." ) - ctx = {"request": request, "lnurl": link.lnurl(req=request)} + settings = await get_or_create_lnurlp_settings() + ctx = {"request": request, "lnurl": link.lnurl(req=request, allow_insecure_http=settings.allow_insecure_http)} return lnurlp_renderer().TemplateResponse("lnurlp/display.html", ctx) @@ -45,5 +46,6 @@ async def print_qr(request: Request, link_id): raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist." ) - ctx = {"request": request, "lnurl": link.lnurl(req=request)} + settings = await get_or_create_lnurlp_settings() + ctx = {"request": request, "lnurl": link.lnurl(req=request, allow_insecure_http=settings.allow_insecure_http)} return lnurlp_renderer().TemplateResponse("lnurlp/print_qr.html", ctx) diff --git a/views_api.py b/views_api.py index 8d04dab..b02e5de 100644 --- a/views_api.py +++ b/views_api.py @@ -51,8 +51,9 @@ async def api_links( wallet_ids = user.wallet_ids if user else [] try: + settings = await get_or_create_lnurlp_settings() return [ - {**link.dict(), "lnurl": link.lnurl(req)} + {**link.dict(), "lnurl": link.lnurl(req, settings.allow_insecure_http)} for link in await get_pay_links(wallet_ids) ] @@ -87,7 +88,8 @@ async def api_link_retrieve( detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN ) - return {**link.dict(), **{"lnurl": link.lnurl(r)}} + settings = await get_or_create_lnurlp_settings() + return {**link.dict(), **{"lnurl": link.lnurl(r, settings.allow_insecure_http)}} async def check_username_exists(username: str): @@ -197,7 +199,8 @@ async def api_link_create_or_update( link = await create_pay_link(data) assert link - return {**link.dict(), "lnurl": link.lnurl(request)} + settings = await get_or_create_lnurlp_settings() + return {**link.dict(), "lnurl": link.lnurl(request, settings.allow_insecure_http)} @lnurlp_api_router.delete("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) diff --git a/views_lnurl.py b/views_lnurl.py index 02dbfe3..c3b48e6 100644 --- a/views_lnurl.py +++ b/views_lnurl.py @@ -7,6 +7,7 @@ from lnurl import LnurlErrorResponse, LnurlPayActionResponse, LnurlPayResponse from lnurl.models import MessageAction, UrlAction from lnurl.types import ( + Url, ClearnetUrl, DebugUrl, LightningInvoice, @@ -22,6 +23,14 @@ increment_pay_link, ) +class InsecureClearnetUrl(Url): + tld_required = False + allowed_schemes = {"http", "https"} + +class InsecureLnurlPayResponse(LnurlPayResponse): + callback: Union[ClearnetUrl, OnionUrl, DebugUrl, InsecureClearnetUrl] + + lnurlp_lnurl_router = APIRouter() @@ -110,8 +119,9 @@ async def api_lnurl_callback( action: Optional[Union[MessageAction, UrlAction]] = None if link.success_url: + settings = await get_or_create_lnurlp_settings() url = parse_obj_as( - Union[DebugUrl, OnionUrl, ClearnetUrl], # type: ignore + Union[DebugUrl, OnionUrl, ClearnetUrl, InsecureClearnetUrl] if settings.allow_insecure_http else Union[DebugUrl, OnionUrl, ClearnetUrl], # type: ignore str(link.success_url), ) desc = parse_obj_as(Max144Str, link.success_text) @@ -150,24 +160,32 @@ async def api_lnurl_response( url = url.include_query_params(webhook_data=webhook_data) link.domain = request.url.netloc + settings = await get_or_create_lnurlp_settings() callback_url = parse_obj_as( - Union[DebugUrl, OnionUrl, ClearnetUrl], # type: ignore + Union[DebugUrl, OnionUrl, ClearnetUrl, InsecureClearnetUrl] if settings.allow_insecure_http else Union[DebugUrl, OnionUrl, ClearnetUrl], # type: ignore str(url), ) - resp = LnurlPayResponse( - callback=callback_url, - minSendable=MilliSatoshi(round(link.min * rate) * 1000), - maxSendable=MilliSatoshi(round(link.max * rate) * 1000), - metadata=link.lnurlpay_metadata, - ) + if settings.allow_insecure_http: + resp = InsecureLnurlPayResponse( + callback=callback_url, + minSendable=MilliSatoshi(round(link.min * rate) * 1000), + maxSendable=MilliSatoshi(round(link.max * rate) * 1000), + metadata=link.lnurlpay_metadata, + ) + else: + resp = LnurlPayResponse( + callback=callback_url, + minSendable=MilliSatoshi(round(link.min * rate) * 1000), + maxSendable=MilliSatoshi(round(link.max * rate) * 1000), + metadata=link.lnurlpay_metadata, + ) params = resp.dict() if link.comment_chars > 0: params["commentAllowed"] = link.comment_chars if link.zaps: - settings = await get_or_create_lnurlp_settings() params["allowsNostr"] = True params["nostrPubkey"] = settings.public_key return params