feat: partial AUTH support
This commit is contained in:
parent
d0c6f1392b
commit
3648dc212c
7 changed files with 141 additions and 16 deletions
|
|
@ -5,6 +5,8 @@ from typing import Any, Awaitable, Callable, List, Optional, Tuple
|
|||
from fastapi import WebSocket
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .crud import (
|
||||
create_event,
|
||||
delete_events,
|
||||
|
|
@ -74,7 +76,6 @@ class NostrClientManager:
|
|||
if c.relay_id not in self._active_relays:
|
||||
await c.stop(reason=f"Relay '{c.relay_id}' is not active")
|
||||
return False
|
||||
# todo: NIP-42: AUTH
|
||||
return True
|
||||
|
||||
def _set_client_callbacks(self, client):
|
||||
|
|
@ -91,13 +92,20 @@ class NostrClientConnection:
|
|||
self.websocket = websocket
|
||||
self.relay_id = relay_id
|
||||
self.filters: List[NostrFilter] = []
|
||||
self.authenticated = False
|
||||
self.pubkey: str = None
|
||||
self._auth_challenge: str = None
|
||||
self._auth_challenge_created_at = 0
|
||||
|
||||
self._last_event_timestamp = 0 # in seconds
|
||||
self._event_count_per_timestamp = 0
|
||||
|
||||
self.broadcast_event: Optional[
|
||||
Callable[[NostrClientConnection, NostrEvent], Awaitable[None]]
|
||||
] = None
|
||||
self.get_client_config: Optional[Callable[[], RelaySpec]] = None
|
||||
|
||||
self._last_event_timestamp = 0 # in seconds
|
||||
self._event_count_per_timestamp = 0
|
||||
|
||||
|
||||
async def start(self):
|
||||
await self.websocket.accept()
|
||||
|
|
@ -150,13 +158,21 @@ class NostrClientConnection:
|
|||
return await self._handle_request(data[1], NostrFilter.parse_obj(data[2]))
|
||||
if message_type == NostrEventType.CLOSE:
|
||||
self._handle_close(data[1])
|
||||
if message_type == NostrEventType.AUTH:
|
||||
self._handle_auth(data[1])
|
||||
|
||||
return []
|
||||
|
||||
async def _handle_event(self, e: NostrEvent):
|
||||
resp_nip20: List[Any] = ["OK", e.id]
|
||||
logger.info(f"nostr event: [{e.kind}, {e.pubkey}, '{e.content}']")
|
||||
resp_nip20: List[Any] = ["OK", e.id]
|
||||
if not self.authenticated and self.client_config.event_requires_auth(e.kind):
|
||||
await self._send_msg(["AUTH", self._current_auth_challenge()])
|
||||
resp_nip20 += [False, "Relay requires authentication"]
|
||||
await self._send_msg(resp_nip20)
|
||||
return None
|
||||
|
||||
|
||||
valid, message = await self._validate_write(e)
|
||||
if not valid:
|
||||
resp_nip20 += [valid, message]
|
||||
|
|
@ -201,6 +217,9 @@ class NostrClientConnection:
|
|||
await mark_events_deleted(self.relay_id, NostrFilter(ids=ids))
|
||||
|
||||
async def _handle_request(self, subscription_id: str, filter: NostrFilter) -> List:
|
||||
if not self.authenticated and self.client_config.require_auth_filter:
|
||||
return [["AUTH", self._current_auth_challenge()]]
|
||||
|
||||
filter.subscription_id = subscription_id
|
||||
self._remove_filter(subscription_id)
|
||||
if self._can_add_filter():
|
||||
|
|
@ -227,6 +246,9 @@ class NostrClientConnection:
|
|||
def _handle_close(self, subscription_id: str):
|
||||
self._remove_filter(subscription_id)
|
||||
|
||||
def _handle_auth(self):
|
||||
raise ValueError('Not supported')
|
||||
|
||||
def _can_add_filter(self) -> bool:
|
||||
return (
|
||||
self.client_config.max_client_filters != 0
|
||||
|
|
@ -318,3 +340,16 @@ class NostrClientConnection:
|
|||
if created_at > (current_time + self.client_config.created_at_in_future):
|
||||
return False, "created_at is too much into the future"
|
||||
return True, ""
|
||||
|
||||
def _auth_challenge_expired(self):
|
||||
if self._auth_challenge_created_at == 0:
|
||||
return True
|
||||
current_time_seconds = round(time.time())
|
||||
chanllenge_max_age_seconds = 300 # 5 min
|
||||
return (current_time_seconds - self._auth_challenge_created_at) >= chanllenge_max_age_seconds
|
||||
|
||||
def _current_auth_challenge(self):
|
||||
if self._auth_challenge_expired():
|
||||
self._auth_challenge = self.relay_id + ":" + urlsafe_short_hash()
|
||||
self._auth_challenge_created_at = round(time.time())
|
||||
return self._auth_challenge
|
||||
17
models.py
17
models.py
|
|
@ -62,6 +62,17 @@ class StorageSpec(Spec):
|
|||
value *= 1024
|
||||
return value
|
||||
|
||||
|
||||
class AuthSpec(BaseModel):
|
||||
require_auth_events = Field(False, alias="requireAuthEvents")
|
||||
skiped_auth_events = Field([], alias="skipedAuthEvents")
|
||||
require_auth_filter = Field(False, alias="requireAuthFilter")
|
||||
|
||||
def event_requires_auth(self, kind: int) -> bool:
|
||||
if not self.require_auth_events:
|
||||
return False
|
||||
return kind not in self.skiped_auth_events
|
||||
|
||||
class PaymentSpec(BaseModel):
|
||||
is_paid_relay = Field(False, alias="isPaidRelay")
|
||||
cost_to_join = Field(0, alias="costToJoin")
|
||||
|
|
@ -93,7 +104,7 @@ class RelayPublicSpec(FilterSpec, EventSpec, StorageSpec, PaymentSpec):
|
|||
def is_read_only_relay(self):
|
||||
self.free_storage_value == 0 and not self.is_paid_relay
|
||||
|
||||
class RelaySpec(RelayPublicSpec, AuthorSpec, WalletSpec):
|
||||
class RelaySpec(RelayPublicSpec, AuthorSpec, WalletSpec, AuthSpec):
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -135,6 +146,7 @@ class NostrEventType(str, Enum):
|
|||
EVENT = "EVENT"
|
||||
REQ = "REQ"
|
||||
CLOSE = "CLOSE"
|
||||
AUTH = "AUTH"
|
||||
|
||||
|
||||
class NostrEvent(BaseModel):
|
||||
|
|
@ -167,6 +179,9 @@ class NostrEvent(BaseModel):
|
|||
def is_replaceable_event(self) -> bool:
|
||||
return self.kind in [0, 3]
|
||||
|
||||
def is_ephemeral_event(self) -> bool:
|
||||
return self.kind in [22242]
|
||||
|
||||
def is_delete_event(self) -> bool:
|
||||
return self.kind == 5
|
||||
|
||||
|
|
|
|||
|
|
@ -328,6 +328,72 @@
|
|||
</div>
|
||||
</div>
|
||||
<q-separator></q-separator>
|
||||
<div class="row items-center no-wrap q-mb-md q-mt-md">
|
||||
<div class="col-3 q-pr-lg">Require Auth :</div>
|
||||
<div class="col-2 col-sm-4 q-pr-lg">
|
||||
<q-toggle
|
||||
color="secodary"
|
||||
class="q-ml-md q-mr-md"
|
||||
v-model="relay.config.requireAuthFilter"
|
||||
>For Filters</q-toggle
|
||||
>
|
||||
</div>
|
||||
<div class="col-2 col-sm-4 q-pr-lg">
|
||||
<q-toggle
|
||||
color="secodary"
|
||||
class="q-ml-md q-mr-md"
|
||||
v-model="relay.config.requireAuthEvents"
|
||||
>For Events</q-toggle
|
||||
>
|
||||
</div>
|
||||
<div class="col-5 col-sm-5">
|
||||
<q-icon name="info" class="cursor-pointer">
|
||||
<q-tooltip>
|
||||
Require client authentication for accessing different types of
|
||||
resources.
|
||||
</q-tooltip></q-icon
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="relay.config.requireAuthEvents"
|
||||
class="row items-center no-wrap q-mb-md q-mt-md"
|
||||
>
|
||||
<div class="col-3 q-pr-lg">Skip Auth For Events:</div>
|
||||
<div class="col-1">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="skipEventKind"
|
||||
type="number"
|
||||
min="0"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="secondary"
|
||||
icon="add"
|
||||
@click="addSkipAuthForEvent()"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<q-chip
|
||||
v-for="e in relay.config.skipedAuthEvents"
|
||||
:key="e"
|
||||
removable
|
||||
@remove="removeSkipAuthForEvent(e)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
{{ e }}
|
||||
</q-chip>
|
||||
<!-- <q-badge color="secondary" class="q-ml-sm" multi-line>
|
||||
{{ relay.config.skipedAuthEvents }}
|
||||
</q-badge> -->
|
||||
</div>
|
||||
</div>
|
||||
<q-separator></q-separator>
|
||||
<div class="row items-center no-wrap q-mb-md q-mt-md">
|
||||
<div class="col-3 q-pr-lg">Full Storage Action:</div>
|
||||
<div class="col-3 col-sm-4 q-pr-lg">
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ async function relayDetails(path) {
|
|||
name: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
skipEventKind: 0
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -128,6 +129,18 @@ async function relayDetails(path) {
|
|||
deleteBlockedPublicKey: function (pubKey) {
|
||||
this.relay.config.blockedPublicKeys =
|
||||
this.relay.config.blockedPublicKeys.filter(p => p !== pubKey)
|
||||
},
|
||||
addSkipAuthForEvent: function () {
|
||||
value = +this.skipEventKind
|
||||
if (this.relay.config.skipedAuthEvents.indexOf(value) != -1) {
|
||||
return
|
||||
}
|
||||
this.relay.config.skipedAuthEvents.push(value)
|
||||
},
|
||||
removeSkipAuthForEvent: function (eventKind) {
|
||||
value = +eventKind
|
||||
this.relay.config.skipedAuthEvents =
|
||||
this.relay.config.skipedAuthEvents.filter(e => e !== value)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
2
tasks.py
2
tasks.py
|
|
@ -3,13 +3,13 @@ import asyncio
|
|||
from loguru import logger
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import create_account, get_account, update_account
|
||||
from .models import NostrAccount
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
|
|
|
|||
|
|
@ -69,13 +69,9 @@
|
|||
</q-td>
|
||||
|
||||
<q-td key="id" :props="props">
|
||||
<a
|
||||
style="color: unset"
|
||||
:href="props.row.id"
|
||||
target="_blank"
|
||||
>
|
||||
{{props.row.id}}</a
|
||||
>
|
||||
<a style="color: unset" :href="props.row.id" target="_blank">
|
||||
{{props.row.id}}</a
|
||||
>
|
||||
</q-td>
|
||||
<q-td key="toggle" :props="props">
|
||||
<q-toggle
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
from http import HTTPStatus
|
||||
from typing import List, Optional
|
||||
|
||||
from urllib.parse import urlparse
|
||||
from fastapi import Depends, WebSocket, Request
|
||||
|
||||
from fastapi import Depends, Request, WebSocket
|
||||
from fastapi.exceptions import HTTPException
|
||||
from loguru import logger
|
||||
from pydantic.types import UUID4
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue