mirror of
https://github.com/Cameri/nostream.git
synced 2025-04-02 08:58:20 +02:00
feat: inital commit for nip-42
This commit is contained in:
parent
7897a18e24
commit
dc0e8f3139
@ -16,6 +16,8 @@ export type IWebSocketAdapter = EventEmitter & {
|
||||
getClientId(): string
|
||||
getClientAddress(): string
|
||||
getSubscriptions(): Map<string, SubscriptionFilter[]>
|
||||
getClientChallenge?(): string,
|
||||
setClientToAuthenticated?()
|
||||
}
|
||||
|
||||
export interface ICacheAdapter {
|
||||
|
@ -4,6 +4,7 @@ import { SubscriptionFilter, SubscriptionId } from './subscription'
|
||||
import { ContextMetadataKey } from '../constants/base'
|
||||
|
||||
export enum MessageType {
|
||||
AUTH = 'AUTH',
|
||||
REQ = 'REQ',
|
||||
EVENT = 'EVENT',
|
||||
CLOSE = 'CLOSE',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import cluster from 'cluster'
|
||||
import { EventEmitter } from 'stream'
|
||||
import { IncomingMessage as IncomingHttpMessage } from 'http'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { WebSocket } from 'ws'
|
||||
|
||||
import { ContextMetadata, Factory } from '../@types/base'
|
||||
@ -32,6 +33,8 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
private clientAddress: SocketAddress
|
||||
private alive: boolean
|
||||
private subscriptions: Map<SubscriptionId, SubscriptionFilter[]>
|
||||
private authChallenge: { createdAt: Date, challenge: Buffer }
|
||||
private authenticated: boolean
|
||||
|
||||
public constructor(
|
||||
private readonly client: WebSocket,
|
||||
@ -96,8 +99,24 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
this.subscriptions.set(subscriptionId, filters)
|
||||
}
|
||||
|
||||
public setNewAuthChallenge() {
|
||||
this.authChallenge = {
|
||||
createdAt: new Date(),
|
||||
challenge: randomBytes(32),
|
||||
}
|
||||
}
|
||||
|
||||
public setClientToAuthenticated() {
|
||||
this.authenticated = true
|
||||
}
|
||||
|
||||
public getClientAuthChallenge() {
|
||||
return this.authChallenge
|
||||
}
|
||||
|
||||
public onBroadcast(event: Event): void {
|
||||
this.webSocketServer.emit(WebSocketServerAdapterEvent.Broadcast, event)
|
||||
|
||||
if (cluster.isWorker && typeof process.send === 'function') {
|
||||
process.send({
|
||||
eventName: WebSocketServerAdapterEvent.Broadcast,
|
||||
|
@ -22,6 +22,7 @@ export enum EventKinds {
|
||||
REPLACEABLE_LAST = 19999,
|
||||
// Ephemeral events
|
||||
EPHEMERAL_FIRST = 20000,
|
||||
AUTH = 22242,
|
||||
EPHEMERAL_LAST = 29999,
|
||||
// Parameterized replaceable events
|
||||
PARAMETERIZED_REPLACEABLE_FIRST = 30000,
|
||||
@ -37,6 +38,8 @@ export enum EventTags {
|
||||
Delegation = 'delegation',
|
||||
Deduplication = 'd',
|
||||
Expiration = 'expiration',
|
||||
Relay = 'relay',
|
||||
Challenge = 'challenge'
|
||||
}
|
||||
|
||||
export enum PaymentsProcessors {
|
||||
|
11
src/factories/auth-event-strategy-factory.ts
Normal file
11
src/factories/auth-event-strategy-factory.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Event } from '../@types/event'
|
||||
import { Factory } from '../@types/base'
|
||||
import { IEventStrategy } from '../@types/message-handlers'
|
||||
import { IWebSocketAdapter } from '../@types/adapters'
|
||||
import { SignedAuthEventStrategy } from '../handlers/event-strategies/auth-event-strategy'
|
||||
|
||||
export const signedAuthEventStrategyFactory = (
|
||||
): Factory<IEventStrategy<Event, Promise<void>>, [Event, IWebSocketAdapter]> =>
|
||||
([, adapter]: [Event, IWebSocketAdapter]) => {
|
||||
return new SignedAuthEventStrategy(adapter)
|
||||
}
|
@ -1,12 +1,14 @@
|
||||
import { IEventRepository, IUserRepository } from '../@types/repositories'
|
||||
import { IncomingMessage, MessageType } from '../@types/messages'
|
||||
import { isDelegatedEvent, isSignedAuthEvent } from '../utils/event'
|
||||
import { AuthEventMessageHandler } from '../handlers/auth-event-message-handler'
|
||||
import { createSettings } from './settings-factory'
|
||||
import { DelegatedEventMessageHandler } from '../handlers/delegated-event-message-handler'
|
||||
import { delegatedEventStrategyFactory } from './delegated-event-strategy-factory'
|
||||
import { EventMessageHandler } from '../handlers/event-message-handler'
|
||||
import { eventStrategyFactory } from './event-strategy-factory'
|
||||
import { isDelegatedEvent } from '../utils/event'
|
||||
import { IWebSocketAdapter } from '../@types/adapters'
|
||||
import { signedAuthEventStrategyFactory } from './auth-event-strategy-factory'
|
||||
import { slidingWindowRateLimiterFactory } from './rate-limiter-factory'
|
||||
import { SubscribeMessageHandler } from '../handlers/subscribe-message-handler'
|
||||
import { UnsubscribeMessageHandler } from '../handlers/unsubscribe-message-handler'
|
||||
@ -28,6 +30,16 @@ export const messageHandlerFactory = (
|
||||
)
|
||||
}
|
||||
|
||||
if (isSignedAuthEvent(message[1])) {
|
||||
return new AuthEventMessageHandler(
|
||||
adapter,
|
||||
signedAuthEventStrategyFactory(),
|
||||
userRepository,
|
||||
createSettings,
|
||||
slidingWindowRateLimiterFactory,
|
||||
)
|
||||
}
|
||||
|
||||
return new EventMessageHandler(
|
||||
adapter,
|
||||
eventStrategyFactory(eventRepository),
|
||||
|
68
src/handlers/auth-event-message-handler.ts
Normal file
68
src/handlers/auth-event-message-handler.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { createCommandResult } from '../utils/messages'
|
||||
import { createLogger } from '../factories/logger-factory'
|
||||
import { DelegatedEvent } from '../@types/event'
|
||||
import { EventMessageHandler } from './event-message-handler'
|
||||
import { IMessageHandler } from '../@types/message-handlers'
|
||||
import { IncomingEventMessage } from '../@types/messages'
|
||||
import { isSignedAuthEvent } from '../utils/event'
|
||||
import { WebSocketAdapterEvent } from '../constants/adapter'
|
||||
|
||||
const debug = createLogger('delegated-event-message-handler')
|
||||
|
||||
export class AuthEventMessageHandler extends EventMessageHandler implements IMessageHandler {
|
||||
public async handleMessage(message: IncomingEventMessage): Promise<void> {
|
||||
const [, event] = message
|
||||
|
||||
let reason = await this.isEventValid(event)
|
||||
if (reason) {
|
||||
debug('event %s rejected: %s', event.id, reason)
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason))
|
||||
return
|
||||
}
|
||||
|
||||
if (await this.isRateLimited(event)) {
|
||||
debug('event %s rejected: rate-limited')
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'rate-limited: slow down'))
|
||||
return
|
||||
}
|
||||
|
||||
reason = this.canAcceptEvent(event)
|
||||
if (reason) {
|
||||
debug('event %s rejected: %s', event.id, reason)
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason))
|
||||
return
|
||||
}
|
||||
|
||||
reason = await this.isUserAdmitted(event)
|
||||
if (reason) {
|
||||
debug('event %s rejected: %s', event.id, reason)
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason))
|
||||
return
|
||||
}
|
||||
|
||||
const strategy = this.strategyFactory([event, this.webSocket])
|
||||
|
||||
if (typeof strategy?.execute !== 'function') {
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'error: event not supported'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await strategy.execute(event)
|
||||
} catch (error) {
|
||||
console.error('error handling message', message, error)
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'error: unable to process event'))
|
||||
}
|
||||
}
|
||||
|
||||
protected async isEventValid(event: DelegatedEvent): Promise<string | undefined> {
|
||||
const reason = await super.isEventValid(event)
|
||||
if (reason) {
|
||||
return reason
|
||||
}
|
||||
|
||||
if (!await isSignedAuthEvent(event)) {
|
||||
return 'invalid: auth verification failed'
|
||||
}
|
||||
}
|
||||
}
|
33
src/handlers/event-strategies/auth-event-strategy.ts
Normal file
33
src/handlers/event-strategies/auth-event-strategy.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { createLogger } from '../../factories/logger-factory'
|
||||
import { Event } from '../../@types/event'
|
||||
import { IEventStrategy } from '../../@types/message-handlers'
|
||||
import { isValidSignedAuthEvent } from '../../utils/event'
|
||||
import { IWebSocketAdapter } from '../../@types/adapters'
|
||||
|
||||
const debug = createLogger('default-event-strategy')
|
||||
|
||||
export class SignedAuthEventStrategy implements IEventStrategy<Event, Promise<void>> {
|
||||
public constructor(
|
||||
private readonly webSocket: IWebSocketAdapter,
|
||||
) { }
|
||||
|
||||
// TODO this is how we send out events, we need to do this
|
||||
// in the message handler (verify if this is true)
|
||||
public async execute(event: Event): Promise<void> {
|
||||
debug('received signedAuth event: %o', event)
|
||||
const clientChallenge = this.webSocket.getClientChallenge()
|
||||
const verified = await isValidSignedAuthEvent(event, clientChallenge)
|
||||
|
||||
if (verified) {
|
||||
this.webSocket.setClientToAuthenticated()
|
||||
}
|
||||
|
||||
// NOTE: we can add a message here if auth fails
|
||||
// this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, 'auth error'))
|
||||
|
||||
// NOTE: we can add a message here if auth succeeds
|
||||
// if (verified) {
|
||||
// this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, 'successful auth'))
|
||||
// }
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import { EventKindsRange } from '../@types/settings'
|
||||
import { fromBuffer } from './transform'
|
||||
import { getLeadingZeroBits } from './proof-of-work'
|
||||
import { isGenericTagQuery } from './filter'
|
||||
import { MessageType } from '../@types/messages'
|
||||
import { RuneLike } from './runes/rune-like'
|
||||
import { SubscriptionFilter } from '../@types/subscription'
|
||||
import { WebSocketServerAdapterEvent } from '../constants/adapter'
|
||||
@ -114,6 +115,32 @@ export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Eve
|
||||
return true
|
||||
}
|
||||
|
||||
export const isSignedAuthEvent = (event: Event): boolean => {
|
||||
if (!event.content || !event.tags) return false
|
||||
|
||||
const evenKindIsValid = event.kind !== EventKinds.AUTH
|
||||
const hasAuthDeclaration = event.content[0] === MessageType.AUTH && event.content[1]
|
||||
const relay = event.tags.some((tag) => tag.length === 2 && tag[0] === EventTags.Relay)
|
||||
const challenge = event.tags.some((tag) => tag.length === 2 && tag[0] === EventTags.Challenge)
|
||||
|
||||
return Boolean(evenKindIsValid && hasAuthDeclaration && relay && challenge)
|
||||
}
|
||||
|
||||
export const isValidSignedAuthEvent = async (event: Event, challenge: string): Promise<boolean> => {
|
||||
const signedAuthEvent = isSignedAuthEvent(event)
|
||||
|
||||
if (signedAuthEvent) {
|
||||
const sig = event.tags.find(tag => tag.length >= 2 && tag[0] === EventTags.Challenge)
|
||||
|
||||
const isValidSignedChallenge = secp256k1.schnorr.verify(sig[1], challenge, event.pubkey)
|
||||
if (isValidSignedChallenge) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const isDelegatedEvent = (event: Event): boolean => {
|
||||
return event.tags.some((tag) => tag.length === 4 && tag[0] === EventTags.Delegation)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user