@libp2p/http-utils 1.0.2 → 2.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 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
@@ -16,96 +14,132 @@ export class Libp2pSocket extends Duplex {
16
14
  public timeout = MAX_TIMEOUT
17
15
  public allowHalfOpen: boolean
18
16
 
17
+ #initStream: Promise<Stream>
19
18
  #stream?: Stream
20
- #bytes?: ByteStream<Stream>
21
19
 
22
- constructor () {
20
+ #log?: Logger
21
+
22
+ constructor (stream: Stream, connection: Connection)
23
+ constructor (initStream: Promise<{ stream: Stream, connection: Connection }>)
24
+ constructor (...args: any[]) {
23
25
  super()
24
26
 
25
27
  this.bytesRead = 0
26
28
  this.bytesWritten = 0
27
29
  this.allowHalfOpen = true
28
30
  this.remoteAddress = ''
31
+
32
+ if (args.length === 2) {
33
+ this.gotStream({ stream: args[0], connection: args[1] })
34
+ this.#initStream = Promise.resolve(args[0])
35
+ } else {
36
+ this.#initStream = args[0].then(this.gotStream.bind(this), (err: any) => {
37
+ this.emit('error', err)
38
+ throw err
39
+ })
40
+ }
29
41
  }
30
42
 
31
- setStream (stream: Stream, connection: Connection): void {
32
- this.#bytes = byteStream(stream)
33
- this.#stream = stream
43
+ private gotStream ({ stream, connection }: { stream: Stream, connection: Connection }): Stream {
44
+ this.#log = stream.log.newScope('libp2p-socket')
34
45
  this.remoteAddress = connection.remoteAddr.toString()
46
+
47
+ stream.addEventListener('message', (evt) => {
48
+ this.push(evt.data.subarray())
49
+ })
50
+
51
+ stream.addEventListener('close', (evt) => {
52
+ if (evt.error != null) {
53
+ this.destroy(evt.error)
54
+ } else {
55
+ this.push(null)
56
+ }
57
+ })
58
+
59
+ stream.pause()
60
+
61
+ this.emit('connect')
62
+
63
+ return stream
64
+ }
65
+
66
+ getStream (cb: (stream: Stream) => void): void {
67
+ if (this.#stream != null) {
68
+ cb(this.#stream)
69
+ return
70
+ }
71
+
72
+ this.#initStream.then(stream => {
73
+ this.#stream = stream
74
+ cb(stream)
75
+ }, (err) => {
76
+ this.emit('error', err)
77
+ })
78
+ }
79
+
80
+ destroy (error?: Error): this {
81
+ return super.destroy(error)
35
82
  }
36
83
 
37
84
  _write (chunk: Uint8Array, encoding: string, cb: (err?: Error) => void): void {
38
- this.#stream?.log('write %d bytes', chunk.byteLength)
85
+ this.#log?.('write %d bytes', chunk.byteLength)
39
86
 
40
87
  this.bytesWritten += chunk.byteLength
41
- this.#bytes?.write(chunk)
42
- .then(() => {
88
+
89
+ this.getStream(stream => {
90
+ if (!stream.send(chunk)) {
91
+ stream.onDrain()
92
+ .then(() => {
93
+ cb()
94
+ }, (err) => {
95
+ cb(err)
96
+ })
97
+ } else {
43
98
  cb()
44
- }, err => {
45
- cb(err)
46
- })
99
+ }
100
+ })
47
101
  }
48
102
 
49
103
  _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
104
+ this.#log?.('asked to read %d bytes', size)
105
+ this.getStream(stream => {
106
+ stream.resume()
107
+ })
108
+ }
66
109
 
67
- this.#stream?.log('socket read %d bytes', chunk.byteLength)
68
- const more = this.push(chunk.subarray())
110
+ _destroy (err: Error, cb: (err?: Error) => void): void {
111
+ this.#log?.('destroy with %d bytes buffered - %e', this.bufferSize, err)
69
112
 
70
- if (!more) {
71
- break
72
- }
73
- }
74
- } catch (err: any) {
75
- this.destroy(err)
113
+ this.getStream(stream => {
114
+ if (err != null) {
115
+ stream.abort(err)
116
+ cb()
117
+ } else {
118
+ stream.close()
119
+ .then(() => {
120
+ cb()
121
+ })
122
+ .catch(err => {
123
+ stream.abort(err)
124
+ cb(err)
125
+ })
76
126
  }
77
127
  })
78
128
  }
79
129
 
80
- _destroy (err: Error, cb: (err?: Error) => void): void {
81
- this.#stream?.log('destroy with %d bytes buffered - %e', this.bufferSize, err)
130
+ _final (cb: (err?: Error) => void): void {
131
+ this.#log?.('final')
82
132
 
83
- if (err != null) {
84
- this.#bytes?.unwrap().abort(err)
85
- cb()
86
- } else {
87
- this.#bytes?.unwrap().close()
133
+ this.getStream(stream => {
134
+ stream.close()
88
135
  .then(() => {
89
136
  cb()
90
137
  })
91
138
  .catch(err => {
92
- this.#stream?.abort(err)
139
+ stream.abort(err)
93
140
  cb(err)
94
141
  })
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
- })
142
+ })
109
143
  }
110
144
 
111
145
  public get readyState (): SocketReadyState {
@@ -129,7 +163,7 @@ export class Libp2pSocket extends Duplex {
129
163
  }
130
164
 
131
165
  destroySoon (): void {
132
- this.#stream?.log('destroySoon with %d bytes buffered', this.bufferSize)
166
+ this.#log?.('destroySoon with %d bytes buffered', this.bufferSize)
133
167
  this.destroy()
134
168
  }
135
169
 
@@ -138,24 +172,27 @@ export class Libp2pSocket extends Duplex {
138
172
  connect (port: number, connectionListener?: () => void): this
139
173
  connect (path: string, connectionListener?: () => void): this
140
174
  connect (...args: any[]): this {
141
- this.#stream?.log('connect %o', args)
175
+ this.#log?.('connect %o', args)
142
176
  return this
143
177
  }
144
178
 
145
179
  setEncoding (encoding?: BufferEncoding): this {
146
- this.#stream?.log('setEncoding %s', encoding)
180
+ this.#log?.('setEncoding %s', encoding)
147
181
  return this
148
182
  }
149
183
 
150
184
  resetAndDestroy (): this {
151
- this.#stream?.log('resetAndDestroy')
152
- this.#stream?.abort(new Error('Libp2pSocket.resetAndDestroy'))
185
+ this.#log?.('resetAndDestroy')
186
+
187
+ this.getStream(stream => {
188
+ stream.abort(new Error('Libp2pSocket.resetAndDestroy'))
189
+ })
153
190
 
154
191
  return this
155
192
  }
156
193
 
157
194
  setTimeout (timeout: number, callback?: () => void): this {
158
- this.#stream?.log('setTimeout %d', timeout)
195
+ this.#log?.('setTimeout %d', timeout)
159
196
 
160
197
  if (callback != null) {
161
198
  this.addListener('timeout', callback)
@@ -167,31 +204,31 @@ export class Libp2pSocket extends Duplex {
167
204
  }
168
205
 
169
206
  setNoDelay (noDelay?: boolean): this {
170
- this.#stream?.log('setNoDelay %b', noDelay)
207
+ this.#log?.('setNoDelay %b', noDelay)
171
208
 
172
209
  return this
173
210
  }
174
211
 
175
212
  setKeepAlive (enable?: boolean, initialDelay?: number): this {
176
- this.#stream?.log('setKeepAlive %b %d', enable, initialDelay)
213
+ this.#log?.('setKeepAlive %b %d', enable, initialDelay)
177
214
 
178
215
  return this
179
216
  }
180
217
 
181
218
  address (): AddressInfo | Record<string, any> {
182
- this.#stream?.log('address')
219
+ this.#log?.('address')
183
220
 
184
221
  return {}
185
222
  }
186
223
 
187
224
  unref (): this {
188
- this.#stream?.log('unref')
225
+ this.#log?.('unref')
189
226
 
190
227
  return this
191
228
  }
192
229
 
193
230
  ref (): this {
194
- this.#stream?.log('ref')
231
+ this.#log?.('ref')
195
232
 
196
233
  return this
197
234
  }
@@ -204,8 +241,5 @@ export class Libp2pSocket extends Duplex {
204
241
  }
205
242
 
206
243
  export function streamToSocket (stream: Stream, connection: Connection): Socket {
207
- const socket = new Libp2pSocket()
208
- socket.setStream(stream, connection)
209
-
210
- return socket
244
+ return new Libp2pSocket(stream, connection)
211
245
  }