Skip to content

Commit

Permalink
feat: refactor and fix based on integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
antonleviathan committed Feb 11, 2023
1 parent d742f2f commit 9a8a149
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 94 deletions.
67 changes: 37 additions & 30 deletions src/adapters/web-socket-adapter.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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')

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
}
21 changes: 10 additions & 11 deletions src/factories/message-handler-factory.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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),
Expand All @@ -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:
Expand Down
7 changes: 4 additions & 3 deletions src/handlers/event-strategies/auth-event-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
}
}
8 changes: 4 additions & 4 deletions src/utils/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 => {
Expand Down
29 changes: 29 additions & 0 deletions test/integration/features/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
8 changes: 8 additions & 0 deletions test/integration/features/nip-42/nip-42.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 10 additions & 4 deletions test/integration/features/nip-42/nip-42.feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
})
1 change: 1 addition & 0 deletions test/integration/features/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
46 changes: 4 additions & 42 deletions test/unit/utils/event.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
]

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],
[EventTags.Challenge, 'incorrectChallenge'],
]

expect(await isValidSignedAuthEvent(event, challengeHex)).to.equal(false)
expect(isValidSignedAuthEvent(event, 'challenge')).to.equal(false)
})
})
})

0 comments on commit 9a8a149

Please sign in to comment.