mirror of
https://github.com/Cameri/nostream.git
synced 2025-07-07 13:49:53 +02:00
feat: refactor and fix based on integration tests
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import { ContextMetadataKey, EventKinds } from '../constants/base'
|
|
||||||
import cluster from 'cluster'
|
import cluster from 'cluster'
|
||||||
|
import { ContextMetadataKey } from '../constants/base'
|
||||||
import { EventEmitter } from 'stream'
|
import { EventEmitter } from 'stream'
|
||||||
import { IncomingMessage as IncomingHttpMessage } from 'http'
|
import { IncomingMessage as IncomingHttpMessage } from 'http'
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
@ -22,7 +22,6 @@ import { messageSchema } from '../schemas/message-schema'
|
|||||||
import { Settings } from '../@types/settings'
|
import { Settings } from '../@types/settings'
|
||||||
import { SocketAddress } from 'net'
|
import { SocketAddress } from 'net'
|
||||||
|
|
||||||
|
|
||||||
const debug = createLogger('web-socket-adapter')
|
const debug = createLogger('web-socket-adapter')
|
||||||
const debugHeartbeat = debug.extend('heartbeat')
|
const debugHeartbeat = debug.extend('heartbeat')
|
||||||
|
|
||||||
@ -99,7 +98,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
|||||||
this.subscriptions.set(subscriptionId, filters)
|
this.subscriptions.set(subscriptionId, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
public setNewAuthChallenge() {
|
public setNewAuthChallenge(): string {
|
||||||
const challenge = randomBytes(16).toString('hex')
|
const challenge = randomBytes(16).toString('hex')
|
||||||
this.authChallenge = {
|
this.authChallenge = {
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -182,33 +181,8 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
|||||||
const message = attemptValidation(messageSchema)(JSON.parse(raw.toString('utf8')))
|
const message = attemptValidation(messageSchema)(JSON.parse(raw.toString('utf8')))
|
||||||
debug('recv client msg: %o', message)
|
debug('recv client msg: %o', message)
|
||||||
|
|
||||||
if (
|
const requiresAuthentication = this.isAuthenticationRequired(message)
|
||||||
!this.authenticated
|
if (requiresAuthentication) return
|
||||||
&& 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message[ContextMetadataKey] = {
|
message[ContextMetadataKey] = {
|
||||||
remoteAddress: this.clientAddress,
|
remoteAddress: this.clientAddress,
|
||||||
@ -322,4 +296,37 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
|||||||
this.removeAllListeners()
|
this.removeAllListeners()
|
||||||
this.client.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 { IEventRepository, IUserRepository } from '../@types/repositories'
|
||||||
import { IncomingMessage, MessageType } from '../@types/messages'
|
import { IncomingMessage, MessageType } from '../@types/messages'
|
||||||
import { isDelegatedEvent, isSignedAuthEvent } from '../utils/event'
|
|
||||||
import { AuthEventMessageHandler } from '../handlers/auth-event-message-handler'
|
import { AuthEventMessageHandler } from '../handlers/auth-event-message-handler'
|
||||||
import { createSettings } from './settings-factory'
|
import { createSettings } from './settings-factory'
|
||||||
import { DelegatedEventMessageHandler } from '../handlers/delegated-event-message-handler'
|
import { DelegatedEventMessageHandler } from '../handlers/delegated-event-message-handler'
|
||||||
import { delegatedEventStrategyFactory } from './delegated-event-strategy-factory'
|
import { delegatedEventStrategyFactory } from './delegated-event-strategy-factory'
|
||||||
import { EventMessageHandler } from '../handlers/event-message-handler'
|
import { EventMessageHandler } from '../handlers/event-message-handler'
|
||||||
import { eventStrategyFactory } from './event-strategy-factory'
|
import { eventStrategyFactory } from './event-strategy-factory'
|
||||||
|
import { isDelegatedEvent } from '../utils/event'
|
||||||
import { IWebSocketAdapter } from '../@types/adapters'
|
import { IWebSocketAdapter } from '../@types/adapters'
|
||||||
import { signedAuthEventStrategyFactory } from './auth-event-strategy-factory'
|
import { signedAuthEventStrategyFactory } from './auth-event-strategy-factory'
|
||||||
import { slidingWindowRateLimiterFactory } from './rate-limiter-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(
|
return new EventMessageHandler(
|
||||||
adapter,
|
adapter,
|
||||||
eventStrategyFactory(eventRepository),
|
eventStrategyFactory(eventRepository),
|
||||||
@ -50,6 +40,15 @@ export const messageHandlerFactory = (
|
|||||||
}
|
}
|
||||||
case MessageType.REQ:
|
case MessageType.REQ:
|
||||||
return new SubscribeMessageHandler(adapter, eventRepository, createSettings)
|
return new SubscribeMessageHandler(adapter, eventRepository, createSettings)
|
||||||
|
case MessageType.AUTH: {
|
||||||
|
return new AuthEventMessageHandler(
|
||||||
|
adapter,
|
||||||
|
signedAuthEventStrategyFactory(),
|
||||||
|
userRepository,
|
||||||
|
createSettings,
|
||||||
|
slidingWindowRateLimiterFactory,
|
||||||
|
)
|
||||||
|
}
|
||||||
case MessageType.CLOSE:
|
case MessageType.CLOSE:
|
||||||
return new UnsubscribeMessageHandler(adapter,)
|
return new UnsubscribeMessageHandler(adapter,)
|
||||||
default:
|
default:
|
||||||
|
@ -17,16 +17,17 @@ export class SignedAuthEventStrategy implements IEventStrategy<Event, Promise<vo
|
|||||||
public async execute(event: Event): Promise<void> {
|
public async execute(event: Event): Promise<void> {
|
||||||
debug('received signedAuth event: %o', event)
|
debug('received signedAuth event: %o', event)
|
||||||
const { challenge, createdAt } = this.webSocket.getClientAuthChallengeData()
|
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) {
|
if (verified && timeIsWithinBounds) {
|
||||||
this.webSocket.setClientToAuthenticated()
|
this.webSocket.setClientToAuthenticated()
|
||||||
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, 'authentication: succeeded'))
|
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, 'authentication: succeeded'))
|
||||||
return
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isValidSignedAuthEvent = async (event: Event, challenge: string): Promise<boolean> => {
|
export const isValidSignedAuthEvent = (event: Event, challenge: string): boolean => {
|
||||||
const signedAuthEvent = isSignedAuthEvent(event)
|
const signedAuthEvent = isSignedAuthEvent(event)
|
||||||
|
|
||||||
if (signedAuthEvent) {
|
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
|
return false
|
||||||
@ -327,7 +327,7 @@ export const getEventExpiration = (event: Event): number | undefined => {
|
|||||||
const expirationTime = Number(rawExpirationTime)
|
const expirationTime = Number(rawExpirationTime)
|
||||||
if ((Number.isSafeInteger(expirationTime) && Math.log10(expirationTime))) {
|
if ((Number.isSafeInteger(expirationTime) && Math.log10(expirationTime))) {
|
||||||
return expirationTime
|
return expirationTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getEventProofOfWork = (eventId: EventId): number => {
|
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> {
|
export async function waitForNextEvent(ws: WebSocket, subscription: string, content?: string): Promise<Event> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const observable = streams.get(ws) as Observable<OutgoingMessage>
|
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
|
When Alice sends a text_note event with content "hello nostr" unsuccessfully
|
||||||
And Alice receives an authentication challenge
|
And Alice receives an authentication challenge
|
||||||
Then Alice sends a signed_challenge_event
|
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 chai from 'chai'
|
||||||
import sinonChai from 'sinon-chai'
|
import sinonChai from 'sinon-chai'
|
||||||
|
|
||||||
import { createEvent, sendEvent, waitForAuth } from '../helpers'
|
import { createEvent, sendAuthMessage, waitForAuth } from '../helpers'
|
||||||
import { EventKinds } from '../../../../src/constants/base'
|
import { EventKinds, EventTags } from '../../../../src/constants/base'
|
||||||
import { SettingsStatic } from '../../../../src/utils/settings'
|
import { SettingsStatic } from '../../../../src/utils/settings'
|
||||||
|
import { Tag } from '../../../../src/@types/base'
|
||||||
import { WebSocket } from 'ws'
|
import { WebSocket } from 'ws'
|
||||||
|
|
||||||
chai.use(sinonChai)
|
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 challenge = this.parameters.challenges[name].pop()
|
||||||
const ws = this.parameters.clients[name] as WebSocket
|
const ws = this.parameters.clients[name] as WebSocket
|
||||||
const { pubkey, privkey } = this.parameters.identities[name]
|
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)
|
this.parameters.events[name].push(event)
|
||||||
})
|
})
|
||||||
|
@ -51,6 +51,7 @@ BeforeAll({ timeout: 1000 }, async function () {
|
|||||||
assocPath( ['limits', 'event', 'rateLimits'], []),
|
assocPath( ['limits', 'event', 'rateLimits'], []),
|
||||||
assocPath( ['limits', 'invoice', 'rateLimits'], []),
|
assocPath( ['limits', 'invoice', 'rateLimits'], []),
|
||||||
assocPath( ['limits', 'connection', 'rateLimits'], []),
|
assocPath( ['limits', 'connection', 'rateLimits'], []),
|
||||||
|
assocPath( ['info', 'relay_url'], 'ws://yoda.test.relay'),
|
||||||
)(settings) as any
|
)(settings) as any
|
||||||
|
|
||||||
worker = workerFactory()
|
worker = workerFactory()
|
||||||
|
@ -609,65 +609,27 @@ describe('NIP-40', () => {
|
|||||||
|
|
||||||
describe('isValidSignedAuthEvent', async () => {
|
describe('isValidSignedAuthEvent', async () => {
|
||||||
it('returns true if event is valid client auth event', async () => {
|
it('returns true if event is valid client auth event', async () => {
|
||||||
const challengeHex = '7468656368616c6c656e67657249554a415057494f4f414949444f4149414f5039393039'
|
|
||||||
const signedHexChallenge = '9161696c52b5471c8d3b4a1649f134731136a5237d67e3ad40451e4ecd2bce1f2787d8043a411f7427e1de30bbbb288d00ef5652df0577fa1191e612344ac8b8'
|
|
||||||
|
|
||||||
event.pubkey = 'c9892fe983a9d85a570c706a582db6288a6a53102efee28871a5a6048a579154'
|
event.pubkey = 'c9892fe983a9d85a570c706a582db6288a6a53102efee28871a5a6048a579154'
|
||||||
event.kind = EventKinds.AUTH
|
event.kind = EventKinds.AUTH
|
||||||
event.tags = [
|
event.tags = [
|
||||||
[EventTags.Relay, 'wss://eden.nostr.land'],
|
[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 () => {
|
describe('isValidSignedAuthEvent', async () => {
|
||||||
it('returns false if challenge is different', async () => {
|
it('returns false if challenge is different', async () => {
|
||||||
const challengeHex = '6468656368616c6c656e67657249554a415057494f4f414949444f4149414f5039393039'
|
|
||||||
const signedHexChallenge = '9161696c52b5471c8d3b4a1649f134731136a5237d67e3ad40451e4ecd2bce1f2787d8043a411f7427e1de30bbbb288d00ef5652df0577fa1191e612344ac8b8'
|
|
||||||
|
|
||||||
event.pubkey = 'c9892fe983a9d85a570c706a582db6288a6a53102efee28871a5a6048a579154'
|
event.pubkey = 'c9892fe983a9d85a570c706a582db6288a6a53102efee28871a5a6048a579154'
|
||||||
event.kind = EventKinds.AUTH
|
event.kind = EventKinds.AUTH
|
||||||
event.tags = [
|
event.tags = [
|
||||||
[EventTags.Relay, 'wss://eden.nostr.land'],
|
[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)
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
Reference in New Issue
Block a user