mirror of
https://github.com/Cameri/nostream.git
synced 2025-03-17 21:31:48 +01:00
feat: refactor and fix based on integration tests
This commit is contained in:
parent
7e793c63bd
commit
d1535cf76e
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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'))
|
||||
}
|
||||
}
|
||||
|
@ -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 => {
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user