@libp2p/http-utils 1.0.2 → 2.0.1

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 CHANGED
@@ -7,18 +7,19 @@
7
7
  import { HTTPParser } from '@achingbrain/http-parser-js'
8
8
  import { InvalidParametersError, isPeerId, ProtocolError } from '@libp2p/interface'
9
9
  import { peerIdFromString } from '@libp2p/peer-id'
10
- import { isMultiaddr, multiaddr } from '@multiformats/multiaddr'
10
+ import { getNetConfig } from '@libp2p/utils'
11
+ import { CODE_P2P, isMultiaddr, multiaddr } from '@multiformats/multiaddr'
11
12
  import { multiaddrToUri } from '@multiformats/multiaddr-to-uri'
12
13
  import { uriToMultiaddr } from '@multiformats/uri-to-multiaddr'
13
- import { queuelessPushable } from 'it-queueless-pushable'
14
14
  import itToBrowserReadableStream from 'it-to-browser-readablestream'
15
15
  import { base36 } from 'multiformats/bases/base36'
16
16
  import { base64pad } from 'multiformats/bases/base64'
17
17
  import { sha1 } from 'multiformats/hashes/sha1'
18
+ import { raceEvent } from 'race-event'
18
19
  import { Uint8ArrayList } from 'uint8arraylist'
19
20
  import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
20
21
  import { Request } from './request.js'
21
- import type { AbortOptions, PeerId, Stream } from '@libp2p/interface'
22
+ import type { AbortOptions, PeerId, Stream, StreamMessageEvent } from '@libp2p/interface'
22
23
  import type { Multiaddr } from '@multiformats/multiaddr'
23
24
 
24
25
  const DNS_CODECS = ['dns', 'dns4', 'dns6', 'dnsaddr']
@@ -89,10 +90,10 @@ export function streamToRequest (info: HeaderInfo, stream: Stream): globalThis.R
89
90
  }
90
91
 
91
92
  if ((init.method !== 'GET' || info.upgrade) && init.method !== 'HEAD') {
92
- let source: AsyncGenerator<any> = stream.source
93
+ let source: AsyncIterable<any> = stream
93
94
 
94
95
  if (!info.upgrade) {
95
- source = takeBytes(stream.source, info.headers.get('content-length'))
96
+ source = takeBytes(stream, info.headers.get('content-length'))
96
97
  }
97
98
 
98
99
  init.body = itToBrowserReadableStream<Uint8Array>(source)
@@ -104,13 +105,7 @@ export function streamToRequest (info: HeaderInfo, stream: Stream): globalThis.R
104
105
  }
105
106
 
106
107
  export async function responseToStream (res: Response, stream: Stream): Promise<void> {
107
- const pushable = queuelessPushable<Uint8Array>()
108
- stream.sink(pushable)
109
- .catch(err => {
110
- stream.abort(err)
111
- })
112
-
113
- await pushable.push(uint8ArrayFromString([
108
+ stream.send(uint8ArrayFromString([
114
109
  `HTTP/1.1 ${res.status} ${res.statusText}`,
115
110
  ...writeHeaders(res.headers),
116
111
  '',
@@ -118,28 +113,29 @@ export async function responseToStream (res: Response, stream: Stream): Promise<
118
113
  ].join('\r\n')))
119
114
 
120
115
  if (res.body == null) {
121
- await pushable.end()
116
+ await stream.close().catch(err => {
117
+ stream.abort(err)
118
+ })
122
119
  return
123
120
  }
124
121
 
125
122
  const reader = res.body.getReader()
126
- let result = await reader.read()
127
123
 
128
124
  while (true) {
125
+ const result = await reader.read()
126
+
129
127
  if (result.value != null) {
130
- await pushable.push(result.value)
128
+ if (!stream.send(result.value)) {
129
+ await stream.onDrain()
130
+ }
131
131
  }
132
132
 
133
133
  if (result.done) {
134
134
  break
135
135
  }
136
-
137
- result = await reader.read()
138
136
  }
139
137
 
140
- await pushable.end()
141
-
142
- await stream.closeWrite()
138
+ await stream.close()
143
139
  .catch(err => {
144
140
  stream.abort(err)
145
141
  })
@@ -187,7 +183,7 @@ export function writeHeaders (headers: Headers): string[] {
187
183
  return output
188
184
  }
189
185
 
190
- async function * takeBytes (source: AsyncGenerator<Uint8ArrayList>, bytes?: number | string | null): AsyncGenerator<Uint8Array> {
186
+ async function * takeBytes (source: AsyncIterable<Uint8Array | Uint8ArrayList>, bytes?: number | string | null): AsyncGenerator<Uint8Array> {
191
187
  bytes = parseInt(`${bytes ?? ''}`)
192
188
 
193
189
  if (bytes == null || isNaN(bytes)) {
@@ -329,12 +325,16 @@ export function getHost (addresses: URL | Multiaddr[], headers: Headers): string
329
325
  // try to use remote PeerId as domain
330
326
  if (!isValidHost(host) && Array.isArray(addresses)) {
331
327
  for (const address of addresses) {
332
- const peerStr = address.getPeerId()
328
+ const peerStr = address.getComponents()
329
+ .findLast(c => c.code === CODE_P2P)?.value
333
330
 
334
331
  // try to extract port from multiaddr if it is available
335
332
  try {
336
- const options = address.toOptions()
337
- port = options.port
333
+ const config = getNetConfig(address)
334
+
335
+ if (config.port != null) {
336
+ port = config.port
337
+ }
338
338
  } catch {}
339
339
 
340
340
  if (peerStr != null) {
@@ -350,9 +350,11 @@ export function getHost (addresses: URL | Multiaddr[], headers: Headers): string
350
350
  if (!isValidHost(host) && Array.isArray(addresses)) {
351
351
  for (const address of addresses) {
352
352
  try {
353
- const options = address.toOptions()
353
+ const config = getNetConfig(address)
354
354
 
355
- host = options.host
355
+ if (config.host != null) {
356
+ host = config.host
357
+ }
356
358
  break
357
359
  } catch {}
358
360
  }
@@ -490,55 +492,57 @@ export async function getServerUpgradeHeaders (headers: Headers | Record<string,
490
492
  /**
491
493
  * Reads HTTP headers from an incoming stream
492
494
  */
493
- export async function readHeaders (stream: Stream): Promise<HeaderInfo> {
494
- return new Promise<any>((resolve, reject) => {
495
- const parser = new HTTPParser('REQUEST')
496
- const source = queuelessPushable<Uint8ArrayList>()
497
- const earlyData = new Uint8ArrayList()
498
- let headersComplete = false
499
-
500
- parser[HTTPParser.kOnHeadersComplete] = (info) => {
501
- headersComplete = true
502
- const headers = new Headers()
503
-
504
- // set incoming headers
505
- for (let i = 0; i < info.headers.length; i += 2) {
506
- headers.set(info.headers[i].toLowerCase(), info.headers[i + 1])
507
- }
495
+ export async function readHeaders (stream: Stream, options?: AbortOptions): Promise<HeaderInfo> {
496
+ const parser = new HTTPParser('REQUEST')
497
+ const earlyData = new Uint8ArrayList()
498
+ let headerInfo: HeaderInfo | undefined
508
499
 
509
- resolve({
510
- ...info,
511
- headers,
512
- raw: earlyData,
513
- method: HTTPParser.methods[info.method]
514
- })
500
+ parser[HTTPParser.kOnHeadersComplete] = (info) => {
501
+ const headers = new Headers()
502
+
503
+ // set incoming headers
504
+ for (let i = 0; i < info.headers.length; i += 2) {
505
+ headers.set(info.headers[i].toLowerCase(), info.headers[i + 1])
515
506
  }
516
507
 
517
- // replace source with request body
518
- const streamSource = stream.source
519
- stream.source = source
520
-
521
- Promise.resolve().then(async () => {
522
- for await (const chunk of streamSource) {
523
- // only use the message parser until the headers have been read
524
- if (!headersComplete) {
525
- earlyData.append(chunk)
526
- parser.execute(chunk.subarray())
527
- } else {
528
- await source.push(new Uint8ArrayList(chunk))
529
- }
508
+ headerInfo = {
509
+ ...info,
510
+ headers,
511
+ raw: earlyData,
512
+ method: HTTPParser.methods[info.method]
513
+ }
514
+ }
515
+
516
+ try {
517
+ while (true) {
518
+ const { data } = await raceEvent<StreamMessageEvent>(stream, 'message', options?.signal)
519
+ const buf = data.subarray()
520
+
521
+ const read = parser.execute(buf, 0, buf.byteLength)
522
+
523
+ if (read instanceof Error) {
524
+ throw read
530
525
  }
531
526
 
532
- await source.end()
533
- })
534
- .catch((err: Error) => {
535
- stream.abort(err)
536
- reject(err)
537
- })
538
- .finally(() => {
539
- parser.finish()
540
- })
541
- })
527
+ // collect raw header bytes
528
+ earlyData.append(buf.subarray(0, read))
529
+
530
+ if (read < buf.byteLength) {
531
+ // reading headers finished and we have early data
532
+ stream.push(buf.subarray(read))
533
+ }
534
+
535
+ if (headerInfo != null) {
536
+ return headerInfo
537
+ }
538
+ }
539
+ } catch (err: any) {
540
+ stream.abort(err)
541
+ } finally {
542
+ parser.finish()
543
+ }
544
+
545
+ throw new Error('Failed to read header info from request')
542
546
  }
543
547
 
544
548
  /**
@@ -1,7 +1,5 @@
1
1
  import { Duplex } from 'node:stream'
2
- import { byteStream } from 'it-byte-stream'
3
- import type { Connection, Stream } from '@libp2p/interface'
4
- import type { ByteStream } from 'it-byte-stream'
2
+ import type { Connection, Logger, Stream } from '@libp2p/interface'
5
3
  import type { Socket, SocketConnectOpts, AddressInfo, SocketReadyState } from 'node:net'
6
4
 
7
5
  const MAX_TIMEOUT = 2_147_483_647
@@ -15,97 +13,135 @@ export class Libp2pSocket extends Duplex {
15
13
  public bytesWritten: number
16
14
  public timeout = MAX_TIMEOUT
17
15
  public allowHalfOpen: boolean
16
+ public remoteFamily: string | undefined
17
+ public remotePort: number | undefined
18
18
 
19
+ #initStream: Promise<Stream>
19
20
  #stream?: Stream
20
- #bytes?: ByteStream<Stream>
21
21
 
22
- constructor () {
22
+ #log?: Logger
23
+
24
+ constructor (stream: Stream, connection: Connection)
25
+ constructor (initStream: Promise<{ stream: Stream, connection: Connection }>)
26
+ constructor (...args: any[]) {
23
27
  super()
24
28
 
25
29
  this.bytesRead = 0
26
30
  this.bytesWritten = 0
27
31
  this.allowHalfOpen = true
28
32
  this.remoteAddress = ''
33
+
34
+ if (args.length === 2) {
35
+ this.gotStream({ stream: args[0], connection: args[1] })
36
+ this.#initStream = Promise.resolve(args[0])
37
+ } else {
38
+ this.#initStream = args[0].then(this.gotStream.bind(this), (err: any) => {
39
+ this.emit('error', err)
40
+ throw err
41
+ })
42
+ }
29
43
  }
30
44
 
31
- setStream (stream: Stream, connection: Connection): void {
32
- this.#bytes = byteStream(stream)
33
- this.#stream = stream
45
+ private gotStream ({ stream, connection }: { stream: Stream, connection: Connection }): Stream {
46
+ this.#log = stream.log.newScope('libp2p-socket')
34
47
  this.remoteAddress = connection.remoteAddr.toString()
48
+
49
+ stream.addEventListener('message', (evt) => {
50
+ this.push(evt.data.subarray())
51
+ })
52
+
53
+ stream.addEventListener('close', (evt) => {
54
+ if (evt.error != null) {
55
+ this.destroy(evt.error)
56
+ } else {
57
+ this.push(null)
58
+ }
59
+ })
60
+
61
+ stream.pause()
62
+
63
+ this.emit('connect')
64
+
65
+ return stream
66
+ }
67
+
68
+ getStream (cb: (stream: Stream) => void): void {
69
+ if (this.#stream != null) {
70
+ cb(this.#stream)
71
+ return
72
+ }
73
+
74
+ this.#initStream.then(stream => {
75
+ this.#stream = stream
76
+ cb(stream)
77
+ }, (err) => {
78
+ this.emit('error', err)
79
+ })
80
+ }
81
+
82
+ destroy (error?: Error): this {
83
+ return super.destroy(error)
35
84
  }
36
85
 
37
86
  _write (chunk: Uint8Array, encoding: string, cb: (err?: Error) => void): void {
38
- this.#stream?.log('write %d bytes', chunk.byteLength)
87
+ this.#log?.('write %d bytes', chunk.byteLength)
39
88
 
40
89
  this.bytesWritten += chunk.byteLength
41
- this.#bytes?.write(chunk)
42
- .then(() => {
90
+
91
+ this.getStream(stream => {
92
+ if (!stream.send(chunk)) {
93
+ stream.onDrain()
94
+ .then(() => {
95
+ cb()
96
+ }, (err) => {
97
+ cb(err)
98
+ })
99
+ } else {
43
100
  cb()
44
- }, err => {
45
- cb(err)
46
- })
101
+ }
102
+ })
47
103
  }
48
104
 
49
105
  _read (size: number): void {
50
- this.#stream?.log('asked to read %d bytes', size)
51
-
52
- void Promise.resolve().then(async () => {
53
- try {
54
- while (true) {
55
- const chunk = await this.#bytes?.read({
56
- signal: AbortSignal.timeout(this.timeout)
57
- })
58
-
59
- if (chunk == null) {
60
- this.#stream?.log('socket readable end closed')
61
- this.push(null)
62
- return
63
- }
64
-
65
- this.bytesRead += chunk.byteLength
106
+ this.#log?.('asked to read %d bytes', size)
107
+ this.getStream(stream => {
108
+ stream.resume()
109
+ })
110
+ }
66
111
 
67
- this.#stream?.log('socket read %d bytes', chunk.byteLength)
68
- const more = this.push(chunk.subarray())
112
+ _destroy (err: Error, cb: (err?: Error) => void): void {
113
+ this.#log?.('destroy with %d bytes buffered - %e', this.bufferSize, err)
69
114
 
70
- if (!more) {
71
- break
72
- }
73
- }
74
- } catch (err: any) {
75
- this.destroy(err)
115
+ this.getStream(stream => {
116
+ if (err != null) {
117
+ stream.abort(err)
118
+ cb()
119
+ } else {
120
+ stream.close()
121
+ .then(() => {
122
+ cb()
123
+ })
124
+ .catch(err => {
125
+ stream.abort(err)
126
+ cb(err)
127
+ })
76
128
  }
77
129
  })
78
130
  }
79
131
 
80
- _destroy (err: Error, cb: (err?: Error) => void): void {
81
- this.#stream?.log('destroy with %d bytes buffered - %e', this.bufferSize, err)
132
+ _final (cb: (err?: Error) => void): void {
133
+ this.#log?.('final')
82
134
 
83
- if (err != null) {
84
- this.#bytes?.unwrap().abort(err)
85
- cb()
86
- } else {
87
- this.#bytes?.unwrap().close()
135
+ this.getStream(stream => {
136
+ stream.close()
88
137
  .then(() => {
89
138
  cb()
90
139
  })
91
140
  .catch(err => {
92
- this.#stream?.abort(err)
141
+ stream.abort(err)
93
142
  cb(err)
94
143
  })
95
- }
96
- }
97
-
98
- _final (cb: (err?: Error) => void): void {
99
- this.#stream?.log('final')
100
-
101
- this.#bytes?.unwrap().closeWrite()
102
- .then(() => {
103
- cb()
104
- })
105
- .catch(err => {
106
- this.#bytes?.unwrap().abort(err)
107
- cb(err)
108
- })
144
+ })
109
145
  }
110
146
 
111
147
  public get readyState (): SocketReadyState {
@@ -129,7 +165,7 @@ export class Libp2pSocket extends Duplex {
129
165
  }
130
166
 
131
167
  destroySoon (): void {
132
- this.#stream?.log('destroySoon with %d bytes buffered', this.bufferSize)
168
+ this.#log?.('destroySoon with %d bytes buffered', this.bufferSize)
133
169
  this.destroy()
134
170
  }
135
171
 
@@ -138,24 +174,27 @@ export class Libp2pSocket extends Duplex {
138
174
  connect (port: number, connectionListener?: () => void): this
139
175
  connect (path: string, connectionListener?: () => void): this
140
176
  connect (...args: any[]): this {
141
- this.#stream?.log('connect %o', args)
177
+ this.#log?.('connect %o', args)
142
178
  return this
143
179
  }
144
180
 
145
181
  setEncoding (encoding?: BufferEncoding): this {
146
- this.#stream?.log('setEncoding %s', encoding)
182
+ this.#log?.('setEncoding %s', encoding)
147
183
  return this
148
184
  }
149
185
 
150
186
  resetAndDestroy (): this {
151
- this.#stream?.log('resetAndDestroy')
152
- this.#stream?.abort(new Error('Libp2pSocket.resetAndDestroy'))
187
+ this.#log?.('resetAndDestroy')
188
+
189
+ this.getStream(stream => {
190
+ stream.abort(new Error('Libp2pSocket.resetAndDestroy'))
191
+ })
153
192
 
154
193
  return this
155
194
  }
156
195
 
157
196
  setTimeout (timeout: number, callback?: () => void): this {
158
- this.#stream?.log('setTimeout %d', timeout)
197
+ this.#log?.('setTimeout %d', timeout)
159
198
 
160
199
  if (callback != null) {
161
200
  this.addListener('timeout', callback)
@@ -167,31 +206,31 @@ export class Libp2pSocket extends Duplex {
167
206
  }
168
207
 
169
208
  setNoDelay (noDelay?: boolean): this {
170
- this.#stream?.log('setNoDelay %b', noDelay)
209
+ this.#log?.('setNoDelay %b', noDelay)
171
210
 
172
211
  return this
173
212
  }
174
213
 
175
214
  setKeepAlive (enable?: boolean, initialDelay?: number): this {
176
- this.#stream?.log('setKeepAlive %b %d', enable, initialDelay)
215
+ this.#log?.('setKeepAlive %b %d', enable, initialDelay)
177
216
 
178
217
  return this
179
218
  }
180
219
 
181
220
  address (): AddressInfo | Record<string, any> {
182
- this.#stream?.log('address')
221
+ this.#log?.('address')
183
222
 
184
223
  return {}
185
224
  }
186
225
 
187
226
  unref (): this {
188
- this.#stream?.log('unref')
227
+ this.#log?.('unref')
189
228
 
190
229
  return this
191
230
  }
192
231
 
193
232
  ref (): this {
194
- this.#stream?.log('ref')
233
+ this.#log?.('ref')
195
234
 
196
235
  return this
197
236
  }
@@ -204,8 +243,5 @@ export class Libp2pSocket extends Duplex {
204
243
  }
205
244
 
206
245
  export function streamToSocket (stream: Stream, connection: Connection): Socket {
207
- const socket = new Libp2pSocket()
208
- socket.setStream(stream, connection)
209
-
210
- return socket
246
+ return new Libp2pSocket(stream, connection)
211
247
  }