feat: refactor and fix based on integration tests

This commit is contained in:
antonleviathan 2023-02-10 20:03:07 -05:00
parent 7e793c63bd
commit d1535cf76e
No known key found for this signature in database
GPG Key ID: 44A86CFF1FDF0E85
9 changed files with 107 additions and 94 deletions

View File

@ -1,5 +1,5 @@
import { ContextMetadataKey, EventKinds } from '../constants/base'
import cluster from 'cluster'
import { ContextMetadataKey } from '../constants/base'
import { EventEmitter } from 'stream'
import { IncomingMessage as IncomingHttpMessage } from 'http'
import { randomBytes } from 'crypto'
@ -22,7 +22,6 @@ import { messageSchema } from '../schemas/message-schema'
import { Settings } from '../@types/settings'
import { SocketAddress } from 'net'
const debug = createLogger('web-socket-adapter')
const debugHeartbeat = debug.extend('heartbeat')
@ -99,7 +98,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
this.subscriptions.set(subscriptionId, filters)
}
public setNewAuthChallenge() {
public setNewAuthChallenge(): string {
const challenge = randomBytes(16).toString('hex')
this.authChallenge = {
createdAt: new Date(),
@ -182,33 +181,8 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
const message = attemptValidation(messageSchema)(JSON.parse(raw.toString('utf8')))
debug('recv client msg: %o', message)
if (
!this.authenticated
&& message[1].kind !== EventKinds.AUTH
&& this.settings().authentication.enabled
) {
switch(message[0]) {
case MessageType.REQ: {
const challenge = this.setNewAuthChallenge()
this.sendMessage(createAuthMessage(challenge))
return
}
case MessageType.EVENT: {
const challenge = this.setNewAuthChallenge()
this.sendMessage(createCommandResult(message[1].id, false, 'rejected: unauthorized'))
this.sendMessage(createAuthMessage(challenge))
return
}
default: {
const challenge = this.setNewAuthChallenge()
this.sendMessage(createCommandResult(message[1].id, false, 'rejected: unauthorized'))
this.sendMessage(createAuthMessage(challenge))
return
}
}
}
const requiresAuthentication = this.isAuthenticationRequired(message)
if (requiresAuthentication) return
message[ContextMetadataKey] = {
remoteAddress: this.clientAddress,
@ -322,4 +296,37 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
this.removeAllListeners()
this.client.removeAllListeners()
}
private isAuthenticationRequired(message): boolean {
if (
!this.authenticated
&& message[0] !== MessageType.AUTH
&& message[0] !== MessageType.CLOSE
&& this.settings().authentication.enabled
) {
switch(message[0]) {
case MessageType.REQ: {
const challenge = this.setNewAuthChallenge()
this.sendMessage(createAuthMessage(challenge))
return true
}
case MessageType.EVENT: {
const challenge = this.setNewAuthChallenge()
this.sendMessage(createCommandResult(message[1].id, false, 'rejected: unauthorized'))
this.sendMessage(createAuthMessage(challenge))
return true
}
default: {
const challenge = this.setNewAuthChallenge()
this.sendMessage(createCommandResult(message[1].id, false, 'rejected: unauthorized'))
this.sendMessage(createAuthMessage(challenge))
return true
}
}
}
return false
}
}

View File

@ -1,12 +1,12 @@
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'
@ -30,16 +30,6 @@ export const messageHandlerFactory = (
)
}
if (isSignedAuthEvent(message[1])) {
return new AuthEventMessageHandler(
adapter,
signedAuthEventStrategyFactory(),
userRepository,
createSettings,
slidingWindowRateLimiterFactory,
)
}
return new EventMessageHandler(
adapter,
eventStrategyFactory(eventRepository),
@ -50,6 +40,15 @@ export const messageHandlerFactory = (
}
case MessageType.REQ:
return new SubscribeMessageHandler(adapter, eventRepository, createSettings)
case MessageType.AUTH: {
return new AuthEventMessageHandler(
adapter,
signedAuthEventStrategyFactory(),
userRepository,
createSettings,
slidingWindowRateLimiterFactory,
)
}
case MessageType.CLOSE:
return new UnsubscribeMessageHandler(adapter,)
default:

View File

@ -17,16 +17,17 @@ export class SignedAuthEventStrategy implements IEventStrategy<Event, Promise<vo
public async execute(event: Event): Promise<void> {
debug('received signedAuth event: %o', event)
const { challenge, createdAt } = this.webSocket.getClientAuthChallengeData()
const verified = await isValidSignedAuthEvent(event, challenge)
const verified = isValidSignedAuthEvent(event, challenge)
const timeIsWithinBounds = (createdAt.getTime() + permittedChallengeResponseTimeDelayMs) < Date.now()
const timeIsWithinBounds = (createdAt.getTime() + permittedChallengeResponseTimeDelayMs) > Date.now()
debug('banana', timeIsWithinBounds, verified)
if (verified && timeIsWithinBounds) {
this.webSocket.setClientToAuthenticated()
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, 'authentication: succeeded'))
return
}
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, 'authentication: failed'))
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'authentication: failed'))
}
}

View File

@ -140,13 +140,13 @@ export const isSignedAuthEvent = (event: Event): boolean => {
return false
}
export const isValidSignedAuthEvent = async (event: Event, challenge: string): Promise<boolean> => {
export const isValidSignedAuthEvent = (event: Event, challenge: string): boolean => {
const signedAuthEvent = isSignedAuthEvent(event)
if (signedAuthEvent) {
const sig = event.tags.find(tag => tag.length >= 2 && tag[0] === EventTags.Challenge)
const tag = event.tags.find(tag => tag.length >= 2 && tag[0] === EventTags.Challenge)
return secp256k1.schnorr.verify(sig[1], challenge, event.pubkey)
return tag[1] === challenge
}
return false
@ -327,7 +327,7 @@ export const getEventExpiration = (event: Event): number | undefined => {
const expirationTime = Number(rawExpirationTime)
if ((Number.isSafeInteger(expirationTime) && Math.log10(expirationTime))) {
return expirationTime
}
}
}
export const getEventProofOfWork = (eventId: EventId): number => {

View File

@ -132,6 +132,35 @@ export async function sendEvent(ws: WebSocket, event: Event, successful = true)
})
}
export async function sendAuthMessage(ws: WebSocket, event: Event, successful = true) {
return new Promise<OutgoingMessage>((resolve, reject) => {
const observable = streams.get(ws) as Observable<OutgoingMessage>
const sub = observable.subscribe((message: OutgoingMessage) => {
if (message[0] === MessageType.OK && message[1] === event.id) {
if (message[2] === successful) {
sub.unsubscribe()
resolve(message)
} else {
sub.unsubscribe()
reject(new Error(message[3]))
}
} else if (message[0] === MessageType.NOTICE) {
sub.unsubscribe()
reject(new Error(message[1]))
}
})
ws.send(JSON.stringify(['AUTH', event]), (err) => {
if (err) {
sub.unsubscribe()
reject(err)
}
})
})
}
export async function waitForNextEvent(ws: WebSocket, subscription: string, content?: string): Promise<Event> {
return new Promise((resolve, reject) => {
const observable = streams.get(ws) as Observable<OutgoingMessage>

View File

@ -11,3 +11,11 @@ Feature: NIP-42
When Alice sends a text_note event with content "hello nostr" unsuccessfully
And Alice receives an authentication challenge
Then Alice sends a signed_challenge_event
Scenario: Alice authenticates and sends an event
Given someone called Alice
And the relay requires the client to authenticate
When Alice sends a text_note event with content "hello nostr" unsuccessfully
And Alice receives an authentication challenge
Then Alice sends a signed_challenge_event
Then Alice sends a text_note event with content "hello nostr" successfully

View File

@ -6,9 +6,10 @@ import {
import chai from 'chai'
import sinonChai from 'sinon-chai'
import { createEvent, sendEvent, waitForAuth } from '../helpers'
import { EventKinds } from '../../../../src/constants/base'
import { createEvent, sendAuthMessage, waitForAuth } from '../helpers'
import { EventKinds, EventTags } from '../../../../src/constants/base'
import { SettingsStatic } from '../../../../src/utils/settings'
import { Tag } from '../../../../src/@types/base'
import { WebSocket } from 'ws'
chai.use(sinonChai)
@ -31,8 +32,13 @@ Then(/(\w+) sends a signed_challenge_event/, async function (name: string) {
const challenge = this.parameters.challenges[name].pop()
const ws = this.parameters.clients[name] as WebSocket
const { pubkey, privkey } = this.parameters.identities[name]
const tags: Tag[] = [
[EventTags.Relay, 'ws://yoda.test.relay'],
[EventTags.Challenge, challenge],
]
const event: any = await createEvent({ pubkey, kind: EventKinds.AUTH, tags }, privkey)
await sendAuthMessage(ws, event, true)
const event: any = await createEvent({ pubkey, kind: EventKinds.AUTH, content: challenge }, privkey)
await sendEvent(ws, event, true)
this.parameters.events[name].push(event)
})

View File

@ -51,6 +51,7 @@ BeforeAll({ timeout: 1000 }, async function () {
assocPath( ['limits', 'event', 'rateLimits'], []),
assocPath( ['limits', 'invoice', 'rateLimits'], []),
assocPath( ['limits', 'connection', 'rateLimits'], []),
assocPath( ['info', 'relay_url'], 'ws://yoda.test.relay'),
)(settings) as any
worker = workerFactory()

View File

@ -609,65 +609,27 @@ describe('NIP-40', () => {
describe('isValidSignedAuthEvent', async () => {
it('returns true if event is valid client auth event', async () => {
const challengeHex = '7468656368616c6c656e67657249554a415057494f4f414949444f4149414f5039393039'
const signedHexChallenge = '9161696c52b5471c8d3b4a1649f134731136a5237d67e3ad40451e4ecd2bce1f2787d8043a411f7427e1de30bbbb288d00ef5652df0577fa1191e612344ac8b8'
event.pubkey = 'c9892fe983a9d85a570c706a582db6288a6a53102efee28871a5a6048a579154'
event.kind = EventKinds.AUTH
event.tags = [
[EventTags.Relay, 'wss://eden.nostr.land'],
[EventTags.Challenge, signedHexChallenge],
[EventTags.Challenge, 'test'],
]
expect(await isValidSignedAuthEvent(event, challengeHex)).to.equal(true)
expect(await isValidSignedAuthEvent(event, 'test')).to.equal(true)
})
})
describe('isValidSignedAuthEvent', async () => {
it('returns false if challenge is different', async () => {
const challengeHex = '6468656368616c6c656e67657249554a415057494f4f414949444f4149414f5039393039'
const signedHexChallenge = '9161696c52b5471c8d3b4a1649f134731136a5237d67e3ad40451e4ecd2bce1f2787d8043a411f7427e1de30bbbb288d00ef5652df0577fa1191e612344ac8b8'
event.pubkey = 'c9892fe983a9d85a570c706a582db6288a6a53102efee28871a5a6048a579154'
event.kind = EventKinds.AUTH
event.tags = [
[EventTags.Relay, 'wss://eden.nostr.land'],
[EventTags.Challenge, signedHexChallenge],
[EventTags.Challenge, 'incorrectChallenge'],
]
expect(await isValidSignedAuthEvent(event, challengeHex)).to.equal(false)
})
})
describe('isValidSignedAuthEvent', async () => {
it('returns false if signed challenge is incorrect', async () => {
const challengeHex = '7468656368616c6c656e67657249554a415057494f4f414949444f4149414f5039393039'
const signedHexChallenge = '0161696c52b5471c8d3b4a1649f134731136a5237d67e3ad40451e4ecd2bce1f2787d8043a411f7427e1de30bbbb288d00ef5652df0577fa1191e612344ac8b8'
event.pubkey = 'c9892fe983a9d85a570c706a582db6288a6a53102efee28871a5a6048a579154'
event.kind = EventKinds.AUTH
event.tags = [
[EventTags.Relay, 'wss://eden.nostr.land'],
[EventTags.Challenge, signedHexChallenge],
]
expect(await isValidSignedAuthEvent(event, challengeHex)).to.equal(false)
})
})
describe('isValidSignedAuthEvent', async () => {
it('returns true if event is valid client auth event', async () => {
const challengeHex = '7468656368616c6c656e67657249554a415057494f4f414949444f4149414f5039393039'
const signedHexChallenge = '9161696c52b5471c8d3b4a1649f134731136a5237d67e3ad40451e4ecd2bce1f2787d8043a411f7427e1de30bbbb288d00ef5652df0577fa1191e612344ac8b8'
event.pubkey = 'a9892fe983a9d85a570c706a582db6288a6a53102efee28871a5a6048a579154'
event.kind = EventKinds.AUTH
event.tags = [
[EventTags.Relay, 'wss://eden.nostr.land'],
[EventTags.Challenge, signedHexChallenge],
]
expect(await isValidSignedAuthEvent(event, challengeHex)).to.equal(false)
expect(isValidSignedAuthEvent(event, 'challenge')).to.equal(false)
})
})
})