@libp2p/daemon-server 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,513 @@
1
+ /* eslint max-depth: ["error", 6] */
2
+
3
+ import { TCP } from '@libp2p/tcp'
4
+ import { Multiaddr } from '@multiformats/multiaddr'
5
+ import { CID } from 'multiformats/cid'
6
+ import * as lp from 'it-length-prefixed'
7
+ import { pipe } from 'it-pipe'
8
+ import { StreamHandler } from './stream-handler.js'
9
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
10
+ import { passThroughUpgrader } from './util/index.js'
11
+ import {
12
+ Request,
13
+ DHTRequest,
14
+ PeerstoreRequest,
15
+ PSRequest,
16
+ StreamInfo,
17
+ IRequest,
18
+ IStreamInfo,
19
+ IPSRequest,
20
+ IDHTRequest,
21
+ IPeerstoreRequest
22
+ } from '@libp2p/daemon-protocol'
23
+ import type { Listener } from '@libp2p/interfaces/transport'
24
+ import type { Connection, Stream } from '@libp2p/interfaces/connection'
25
+ import type { PeerId } from '@libp2p/interfaces/peer-id'
26
+ import type { AbortOptions } from '@libp2p/interfaces'
27
+ import type { StreamHandler as StreamCallback } from '@libp2p/interfaces/registrar'
28
+ import type { DualDHT } from '@libp2p/interfaces/dht'
29
+ import type { PubSub } from '@libp2p/interfaces/pubsub'
30
+ import type { PeerStore } from '@libp2p/interfaces/peer-store'
31
+ import { ErrorResponse, OkResponse } from './responses.js'
32
+ import { DHTOperations } from './dht.js'
33
+ import { peerIdFromBytes, peerIdFromString } from '@libp2p/peer-id'
34
+ import { PubSubOperations } from './pubsub.js'
35
+ import { logger } from '@libp2p/logger'
36
+
37
+ const LIMIT = 1 << 22 // 4MB
38
+ const log = logger('libp2p:daemon')
39
+
40
+ export interface OpenStream {
41
+ streamInfo: IStreamInfo,
42
+ connection: Stream
43
+ }
44
+
45
+ export interface Libp2p {
46
+ peerId: PeerId
47
+ peerStore: PeerStore
48
+ pubsub?: PubSub
49
+ dht?: DualDHT
50
+
51
+ getConnections: (peerId?: PeerId) => Connection[]
52
+ getPeers: () => PeerId[]
53
+ dial: (peer: PeerId | Multiaddr, options?: AbortOptions) => Promise<Connection>
54
+ handle: (protocol: string | string[], handler: StreamCallback) => Promise<void>
55
+ start: () => void | Promise<void>
56
+ stop: () => void | Promise<void>
57
+ getMultiaddrs: () => Multiaddr[]
58
+ }
59
+
60
+ export interface DaemonInit {
61
+ multiaddr: Multiaddr,
62
+ libp2pNode: any
63
+ }
64
+
65
+ export interface Libp2pServer {
66
+ start: () => Promise<void>
67
+ stop: () => Promise<void>
68
+ getMultiaddrs: () => Multiaddr[]
69
+ }
70
+
71
+ export class Server implements Libp2pServer {
72
+ private multiaddr: Multiaddr
73
+ private libp2p: Libp2p
74
+ private tcp: TCP
75
+ private listener: Listener
76
+ private streamHandlers: Record<string, StreamHandler>
77
+ private dhtOperations?: DHTOperations
78
+ private pubsubOperations?: PubSubOperations
79
+
80
+ constructor (init: DaemonInit) {
81
+ const { multiaddr, libp2pNode } = init
82
+
83
+ this.multiaddr = multiaddr
84
+ this.libp2p = libp2pNode
85
+ this.tcp = new TCP()
86
+ this.listener = this.tcp.createListener({
87
+ handler: this.handleConnection.bind(this),
88
+ upgrader: passThroughUpgrader
89
+ })
90
+ this.streamHandlers = {}
91
+ this._onExit = this._onExit.bind(this)
92
+
93
+ if (libp2pNode.dht != null) {
94
+ this.dhtOperations = new DHTOperations({ dht: libp2pNode.dht })
95
+ }
96
+
97
+ if (libp2pNode.pubsub != null) {
98
+ this.pubsubOperations = new PubSubOperations({ pubsub: libp2pNode.pubsub })
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Connects the daemons libp2p node to the peer provided
104
+ */
105
+ async connect (request: IRequest): Promise<Connection> {
106
+ if (request.connect == null || request.connect.addrs == null) {
107
+ throw new Error('Invalid request')
108
+ }
109
+
110
+ const peer = request.connect.peer
111
+ const addrs = request.connect.addrs.map((a) => new Multiaddr(a))
112
+ const peerId = peerIdFromBytes(peer)
113
+
114
+ await this.libp2p.peerStore.addressBook.set(peerId, addrs)
115
+ return this.libp2p.dial(peerId)
116
+ }
117
+
118
+ /**
119
+ * Opens a stream on one of the given protocols to the given peer
120
+ */
121
+ async openStream (request: IRequest): Promise<OpenStream> {
122
+ if (request.streamOpen == null || request.streamOpen.proto == null) {
123
+ throw new Error('Invalid request')
124
+ }
125
+
126
+ const { peer, proto } = request.streamOpen
127
+
128
+ const peerId = peerIdFromString(uint8ArrayToString(peer, 'base58btc'))
129
+
130
+ const connection = await this.libp2p.dial(peerId)
131
+ const { stream, protocol } = await connection.newStream(proto)
132
+
133
+ return {
134
+ streamInfo: {
135
+ peer: peerId.toBytes(),
136
+ addr: connection.remoteAddr.bytes,
137
+ proto: protocol
138
+ },
139
+ connection: stream
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Sends inbound requests for the given protocol
145
+ * to the unix socket path provided. If an existing handler
146
+ * is registered at the path, it will be overridden.
147
+ *
148
+ * @param {StreamHandlerRequest} request
149
+ * @returns {Promise<void>}
150
+ */
151
+ async registerStreamHandler (request: IRequest): Promise<void> {
152
+ if (request.streamHandler == null || request.streamHandler.proto == null) {
153
+ throw new Error('Invalid request')
154
+ }
155
+
156
+ const protocols = request.streamHandler.proto
157
+ const addr = new Multiaddr(request.streamHandler.addr)
158
+ const addrString = addr.toString()
159
+
160
+ // If we have a handler, end it
161
+ if (this.streamHandlers[addrString]) {
162
+ this.streamHandlers[addrString].close()
163
+ delete this.streamHandlers[addrString]
164
+ }
165
+
166
+ protocols.forEach((proto) => {
167
+ // Connect the client socket with the libp2p connection
168
+ this.libp2p.handle(proto, ({ connection, stream, protocol }) => {
169
+ const message = StreamInfo.encode({
170
+ peer: connection.remotePeer.toBytes(),
171
+ addr: connection.remoteAddr.bytes,
172
+ proto: protocol
173
+ }).finish()
174
+ const encodedMessage = lp.encode.single(message)
175
+
176
+ // Tell the client about the new connection
177
+ // And then begin piping the client and peer connection
178
+
179
+ pipe(
180
+ [encodedMessage, stream.source],
181
+ clientConnection,
182
+ stream.sink
183
+ )
184
+ })
185
+ })
186
+
187
+ const clientConnection = await this.tcp.dial(addr, {
188
+ upgrader: passThroughUpgrader
189
+ })
190
+ }
191
+
192
+ /**
193
+ * Listens for process exit to handle cleanup
194
+ *
195
+ * @private
196
+ * @returns {void}
197
+ */
198
+ _listen () {
199
+ // listen for graceful termination
200
+ process.on('SIGTERM', this._onExit)
201
+ process.on('SIGINT', this._onExit)
202
+ process.on('SIGHUP', this._onExit)
203
+ }
204
+
205
+ _onExit () {
206
+ this.stop({ exit: true })
207
+ }
208
+
209
+ /**
210
+ * Starts the daemon
211
+ */
212
+ async start () {
213
+ this._listen()
214
+ await this.libp2p.start()
215
+ await this.listener.listen(this.multiaddr)
216
+ }
217
+
218
+ getMultiaddrs (): Multiaddr[] {
219
+ return this.listener.getAddrs()
220
+ }
221
+
222
+ /**
223
+ * Stops the daemon
224
+ *
225
+ * @param {object} options
226
+ * @param {boolean} options.exit - If the daemon process should exit
227
+ * @returns {Promise<void>}
228
+ */
229
+ async stop (options = { exit: false }) {
230
+ await this.libp2p.stop()
231
+ await this.listener.close()
232
+ if (options.exit) {
233
+ log('server closed, exiting')
234
+ }
235
+ process.removeListener('SIGTERM', this._onExit)
236
+ process.removeListener('SIGINT', this._onExit)
237
+ process.removeListener('SIGHUP', this._onExit)
238
+ }
239
+
240
+ async * handlePeerStoreRequest (request: IPeerstoreRequest) {
241
+ try {
242
+ switch (request.type) {
243
+ case PeerstoreRequest.Type.GET_PROTOCOLS:
244
+ if (request.id == null) {
245
+ throw new Error('Invalid request')
246
+ }
247
+
248
+ const peerId = peerIdFromBytes(request.id)
249
+ const peer = await this.libp2p.peerStore.get(peerId)
250
+ const protos = peer.protocols
251
+ yield OkResponse({ peerStore: { protos } })
252
+ return
253
+ case PeerstoreRequest.Type.GET_PEER_INFO:
254
+ throw new Error('ERR_NOT_IMPLEMENTED')
255
+ default:
256
+ throw new Error('ERR_INVALID_REQUEST_TYPE')
257
+ }
258
+ } catch (err: any) {
259
+ yield ErrorResponse(err)
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Parses and responds to PSRequests
265
+ */
266
+ async * handlePubsubRequest (request: IPSRequest) {
267
+ try {
268
+ if (this.libp2p.pubsub == null || !this.pubsubOperations) {
269
+ throw new Error('PubSub not configured')
270
+ }
271
+
272
+ switch (request.type) {
273
+ case PSRequest.Type.GET_TOPICS:
274
+ yield * this.pubsubOperations.getTopics()
275
+ return
276
+ case PSRequest.Type.SUBSCRIBE:
277
+ if (request.topic == null) {
278
+ throw new Error('Invalid request')
279
+ }
280
+
281
+ yield * this.pubsubOperations.subscribe(request.topic)
282
+ return
283
+ case PSRequest.Type.PUBLISH:
284
+ if (request.topic == null || request.data == null) {
285
+ throw new Error('Invalid request')
286
+ }
287
+
288
+ yield * this.pubsubOperations.publish(request.topic, request.data)
289
+ return
290
+ default:
291
+ throw new Error('ERR_INVALID_REQUEST_TYPE')
292
+ }
293
+ } catch (err: any) {
294
+ yield ErrorResponse(err)
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Parses and responds to DHTRequests
300
+ */
301
+ async * handleDHTRequest (request: IDHTRequest) {
302
+ try {
303
+ if (this.libp2p.dht == null || !this.dhtOperations) {
304
+ throw new Error('DHT not configured')
305
+ }
306
+
307
+ switch (request.type) {
308
+ case DHTRequest.Type.FIND_PEER:
309
+ if (request.peer == null) {
310
+ throw new Error('Invalid request')
311
+ }
312
+
313
+ yield * this.dhtOperations.findPeer(peerIdFromBytes(request.peer))
314
+ return
315
+ case DHTRequest.Type.FIND_PROVIDERS:
316
+ if (request.cid == null) {
317
+ throw new Error('Invalid request')
318
+ }
319
+
320
+ yield * this.dhtOperations.findProviders(CID.decode(request.cid), request.count ?? 20)
321
+ return
322
+ case DHTRequest.Type.PROVIDE:
323
+ if (request.cid == null) {
324
+ throw new Error('Invalid request')
325
+ }
326
+
327
+ yield * this.dhtOperations.provide(CID.decode(request.cid))
328
+ return
329
+ case DHTRequest.Type.GET_CLOSEST_PEERS:
330
+ if (request.key == null) {
331
+ throw new Error('Invalid request')
332
+ }
333
+
334
+ yield * this.dhtOperations.getClosestPeers(request.key)
335
+ return
336
+ case DHTRequest.Type.GET_PUBLIC_KEY:
337
+ if (request.peer == null) {
338
+ throw new Error('Invalid request')
339
+ }
340
+
341
+ yield * this.dhtOperations.getPublicKey(peerIdFromBytes(request.peer))
342
+ return
343
+ case DHTRequest.Type.GET_VALUE:
344
+ if (request.key == null) {
345
+ throw new Error('Invalid request')
346
+ }
347
+
348
+ yield * this.dhtOperations.getValue(request.key)
349
+ return
350
+ case DHTRequest.Type.PUT_VALUE:
351
+ if (request.key == null || request.value == null) {
352
+ throw new Error('Invalid request')
353
+ }
354
+
355
+ yield * this.dhtOperations.putValue(request.key, request.value)
356
+ return
357
+ default:
358
+ throw new Error('ERR_INVALID_REQUEST_TYPE')
359
+ }
360
+ } catch (err: any) {
361
+ yield ErrorResponse(err)
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Handles requests for the given connection
367
+ */
368
+ async handleConnection (connection: Connection) {
369
+ const daemon = this
370
+ // @ts-expect-error connection may actually be a maconn?
371
+ const streamHandler = new StreamHandler({ stream: connection, maxLength: LIMIT })
372
+
373
+ await pipe(
374
+ streamHandler.decoder,
375
+ source => (async function * () {
376
+ let request: Request
377
+
378
+ for await (let buf of source) {
379
+ try {
380
+ request = Request.decode(buf)
381
+ } catch (err) {
382
+ yield ErrorResponse(new Error('ERR_INVALID_MESSAGE'))
383
+ continue
384
+ }
385
+
386
+ switch (request.type) {
387
+ // Connect to another peer
388
+ case Request.Type.CONNECT: {
389
+ try {
390
+ await daemon.connect(request)
391
+ } catch (err: any) {
392
+ yield ErrorResponse(err)
393
+ break
394
+ }
395
+ yield OkResponse()
396
+ break
397
+ }
398
+ // Get the daemon peer id and addresses
399
+ case Request.Type.IDENTIFY: {
400
+ yield OkResponse({
401
+ identify: {
402
+ id: daemon.libp2p.peerId.toBytes(),
403
+ addrs: daemon.libp2p.getMultiaddrs().map(m => m.bytes)
404
+ }
405
+ })
406
+ break
407
+ }
408
+ // Get a list of our current peers
409
+ case Request.Type.LIST_PEERS: {
410
+ const peers = []
411
+
412
+ for (const peerId of daemon.libp2p.getPeers()) {
413
+ const conn = daemon.libp2p.getConnections(peerId)[0]
414
+
415
+ peers.push({
416
+ id: peerId.toBytes(),
417
+ addrs: [conn.remoteAddr.bytes]
418
+ })
419
+ }
420
+
421
+ yield OkResponse({ peers })
422
+ break
423
+ }
424
+ case Request.Type.STREAM_OPEN: {
425
+ let response
426
+ try {
427
+ response = await daemon.openStream(request)
428
+ } catch (err: any) {
429
+ yield ErrorResponse(err.message)
430
+ break
431
+ }
432
+
433
+ // write the response
434
+ yield OkResponse({
435
+ streamInfo: response.streamInfo
436
+ })
437
+
438
+ const stream = streamHandler.rest()
439
+ // then pipe the connection to the client
440
+ await pipe(
441
+ stream,
442
+ response.connection,
443
+ stream
444
+ )
445
+ // Exit the iterator, no more requests can come through
446
+ return
447
+ }
448
+ case Request.Type.STREAM_HANDLER: {
449
+ try {
450
+ await daemon.registerStreamHandler(request)
451
+ } catch (err: any) {
452
+ yield ErrorResponse(err)
453
+ break
454
+ }
455
+
456
+ // write the response
457
+ yield OkResponse()
458
+ break
459
+ }
460
+ case Request.Type.PEERSTORE: {
461
+ if (request.peerStore == null) {
462
+ yield ErrorResponse(new Error('ERR_INVALID_REQUEST'))
463
+ break
464
+ }
465
+
466
+ yield * daemon.handlePeerStoreRequest(request.peerStore)
467
+ break
468
+ }
469
+ case Request.Type.PUBSUB: {
470
+ if (request.pubsub == null) {
471
+ yield ErrorResponse(new Error('ERR_INVALID_REQUEST'))
472
+ break
473
+ }
474
+
475
+ yield * daemon.handlePubsubRequest(request.pubsub)
476
+ break
477
+ }
478
+ case Request.Type.DHT: {
479
+ if (request.dht == null) {
480
+ yield ErrorResponse(new Error('ERR_INVALID_REQUEST'))
481
+ break
482
+ }
483
+
484
+ yield * daemon.handleDHTRequest(request.dht)
485
+ break
486
+ }
487
+ // Not yet supported or doesn't exist
488
+ default:
489
+ yield ErrorResponse(new Error('ERR_INVALID_REQUEST_TYPE'))
490
+ break
491
+ }
492
+ }
493
+ })(),
494
+ async function (source) {
495
+ for await (const result of source) {
496
+ streamHandler.write(result)
497
+ }
498
+ }
499
+ )
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Creates a daemon from the provided Daemon Options
505
+ */
506
+ export const createServer = async (multiaddr: Multiaddr, libp2pNode: Libp2p): Promise<Libp2pServer> => {
507
+ const daemon = new Server({
508
+ multiaddr,
509
+ libp2pNode
510
+ })
511
+
512
+ return daemon
513
+ }
package/src/pubsub.ts ADDED
@@ -0,0 +1,57 @@
1
+ /* eslint max-depth: ["error", 6] */
2
+
3
+ import {
4
+ PSMessage
5
+ } from '@libp2p/daemon-protocol'
6
+ import { OkResponse } from './responses.js'
7
+ import type { PubSub } from '@libp2p/interfaces/pubsub'
8
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
9
+ import { pushable } from 'it-pushable'
10
+
11
+ export interface PubSubOperationsInit {
12
+ pubsub: PubSub
13
+ }
14
+
15
+ export class PubSubOperations {
16
+ private pubsub: PubSub
17
+
18
+ constructor (init: PubSubOperationsInit) {
19
+ const { pubsub } = init
20
+
21
+ this.pubsub = pubsub
22
+ }
23
+
24
+ async * getTopics () {
25
+ yield OkResponse({
26
+ pubsub: {
27
+ topics: this.pubsub.getTopics()
28
+ }
29
+ })
30
+ }
31
+
32
+ async * subscribe (topic: string) {
33
+ const onMessage = pushable<Uint8Array>()
34
+
35
+ await this.pubsub.addEventListener(topic, (evt) => {
36
+ const msg = evt.detail
37
+
38
+ onMessage.push(PSMessage.encode({
39
+ from: msg.from.toBytes(),
40
+ data: msg.data,
41
+ seqno: msg.sequenceNumber == null ? undefined : uint8ArrayFromString(msg.sequenceNumber.toString(16).padStart(16, '0'), 'base16'),
42
+ topicIDs: [msg.topic],
43
+ signature: msg.signature,
44
+ key: msg.key
45
+ }).finish())
46
+ })
47
+
48
+ yield OkResponse()
49
+ yield * onMessage
50
+ }
51
+
52
+ async * publish (topic: string, data: Uint8Array) {
53
+ this.pubsub.dispatchEvent(new CustomEvent(topic, { detail: data }))
54
+ yield OkResponse()
55
+ }
56
+
57
+ }
@@ -0,0 +1,23 @@
1
+ import { IResponse, Response } from '@libp2p/daemon-protocol'
2
+
3
+ /**
4
+ * Creates and encodes an OK response
5
+ */
6
+ export function OkResponse (data?: Partial<IResponse>): Uint8Array {
7
+ return Response.encode({
8
+ type: Response.Type.OK,
9
+ ...data
10
+ }).finish()
11
+ }
12
+
13
+ /**
14
+ * Creates and encodes an ErrorResponse
15
+ */
16
+ export function ErrorResponse (err: Error): Uint8Array {
17
+ return Response.encode({
18
+ type: Response.Type.ERROR,
19
+ error: {
20
+ msg: err.message
21
+ }
22
+ }).finish()
23
+ }
@@ -0,0 +1,65 @@
1
+ import * as lp from 'it-length-prefixed'
2
+ import { handshake } from 'it-handshake'
3
+ import { logger } from '@libp2p/logger'
4
+ import type { Duplex, Source } from 'it-stream-types'
5
+ import type { Handshake } from 'it-handshake'
6
+
7
+ const log = logger('libp2p:daemon-client:stream-handler')
8
+
9
+ export interface StreamHandlerOptions {
10
+ stream: Duplex<Uint8Array>
11
+ maxLength?: number
12
+ }
13
+
14
+ export class StreamHandler {
15
+ private stream: Duplex<Uint8Array>
16
+ private shake: Handshake
17
+ public decoder: Source<Uint8Array>
18
+ /**
19
+ * Create a stream handler for connection
20
+ */
21
+ constructor (opts: StreamHandlerOptions) {
22
+ const { stream, maxLength } = opts
23
+
24
+ this.stream = stream
25
+ this.shake = handshake(this.stream)
26
+ this.decoder = lp.decode.fromReader(this.shake.reader, { maxDataLength: maxLength ?? 4096 })
27
+ }
28
+
29
+ /**
30
+ * Read and decode message
31
+ */
32
+ async read () {
33
+ // @ts-expect-error decoder is really a generator
34
+ const msg = await this.decoder.next()
35
+ if (msg.value) {
36
+ return msg.value.slice()
37
+ }
38
+ log('read received no value, closing stream')
39
+ // End the stream, we didn't get data
40
+ this.close()
41
+ }
42
+
43
+ write (msg: Uint8Array) {
44
+ log('write message')
45
+ this.shake.write(
46
+ lp.encode.single(msg).slice()
47
+ )
48
+ }
49
+
50
+ /**
51
+ * Return the handshake rest stream and invalidate handler
52
+ */
53
+ rest () {
54
+ this.shake.rest()
55
+ return this.shake.stream
56
+ }
57
+
58
+ /**
59
+ * Close the stream
60
+ */
61
+ close () {
62
+ log('closing the stream')
63
+ this.rest().sink([])
64
+ }
65
+ }
@@ -0,0 +1,30 @@
1
+ import type { Upgrader } from '@libp2p/interfaces/transport'
2
+ import type { Multiaddr } from '@multiformats/multiaddr'
3
+ import { resolve } from 'path'
4
+ import os from 'os'
5
+
6
+ export const passThroughUpgrader: Upgrader = {
7
+ // @ts-expect-error
8
+ upgradeInbound: async maConn => maConn,
9
+ // @ts-expect-error
10
+ upgradeOutbound: async maConn => maConn
11
+ }
12
+
13
+ /**
14
+ * Converts the multiaddr to a nodejs NET compliant option
15
+ * for .connect or .listen
16
+ *
17
+ * @param {Multiaddr} addr
18
+ * @returns {string|object} A nodejs NET compliant option
19
+ */
20
+ export function multiaddrToNetConfig (addr: Multiaddr) {
21
+ const listenPath = addr.getPath()
22
+ // unix socket listening
23
+ if (listenPath) {
24
+ return resolve(listenPath)
25
+ }
26
+ // tcp listening
27
+ return addr.nodeAddress()
28
+ }
29
+
30
+ export const isWindows = os.platform() === 'win32'