feat: partial AUTH support

This commit is contained in:
Vlad Stan 2023-02-14 17:26:40 +02:00
parent d0c6f1392b
commit 3648dc212c
7 changed files with 141 additions and 16 deletions

View file

@ -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,12 +158,20 @@ 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:
@ -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

View file

@ -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

View file

@ -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">

View file

@ -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)
}
},

View file

@ -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())

View file

@ -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

View file

@ -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