feat: inital commit for nip-42

This commit is contained in:
Anton Livaja 2023-02-06 13:58:25 -05:00 committed by antonleviathan
parent 7897a18e24
commit dc0e8f3139
No known key found for this signature in database
GPG Key ID: 44A86CFF1FDF0E85
9 changed files with 177 additions and 1 deletions

View File

@ -16,6 +16,8 @@ export type IWebSocketAdapter = EventEmitter & {
getClientId(): string
getClientAddress(): string
getSubscriptions(): Map<string, SubscriptionFilter[]>
getClientChallenge?(): string,
setClientToAuthenticated?()
}
export interface ICacheAdapter {

View File

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

View File

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

View File

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

View 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)
}

View File

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

View 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'
}
}
}

View 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'))
// }
}
}

View File

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