@libp2p/http-utils 1.0.1 → 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.
@@ -3,6 +3,7 @@
3
3
  ".:Middleware": "https://libp2p.github.io/js-libp2p-http/interfaces/_libp2p_http-utils.Middleware.html",
4
4
  "MiddlewareOptions": "https://libp2p.github.io/js-libp2p-http/interfaces/_libp2p_http.index.MiddlewareOptions.html",
5
5
  ".:MiddlewareOptions": "https://libp2p.github.io/js-libp2p-http/interfaces/_libp2p_http-utils.MiddlewareOptions.html",
6
+ "Libp2pSocket": "https://libp2p.github.io/js-libp2p-http/classes/_libp2p_http-utils.Libp2pSocket.html",
6
7
  "Request": "https://libp2p.github.io/js-libp2p-http/classes/_libp2p_http-utils.Request.html",
7
8
  "Response": "https://libp2p.github.io/js-libp2p-http/classes/_libp2p_http-utils.Response.html",
8
9
  "HeaderInfo": "https://libp2p.github.io/js-libp2p-http/interfaces/_libp2p_http-utils.HeaderInfo.html",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@libp2p/http-utils",
3
- "version": "1.0.1",
3
+ "version": "2.0.0",
4
4
  "description": "Shared utils and common code for HTTP modules",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "homepage": "https://github.com/libp2p/js-libp2p-http/tree/main/packages/http-utils#readme",
@@ -139,21 +139,21 @@
139
139
  },
140
140
  "dependencies": {
141
141
  "@achingbrain/http-parser-js": "^0.5.9",
142
- "@libp2p/interface": "^2.10.2",
143
- "@libp2p/peer-id": "^5.1.5",
144
- "@multiformats/multiaddr": "^12.4.0",
145
- "@multiformats/multiaddr-to-uri": "^11.0.0",
146
- "@multiformats/uri-to-multiaddr": "^9.0.1",
147
- "it-byte-stream": "^2.0.2",
148
- "it-queueless-pushable": "^2.0.1",
149
- "it-to-browser-readablestream": "^2.0.11",
150
- "multiformats": "^13.3.6",
142
+ "@libp2p/interface": "^3.0.2",
143
+ "@libp2p/peer-id": "^6.0.3",
144
+ "@libp2p/utils": "^7.0.4",
145
+ "@multiformats/multiaddr": "^13.0.1",
146
+ "@multiformats/multiaddr-to-uri": "^12.0.0",
147
+ "@multiformats/uri-to-multiaddr": "^10.0.0",
148
+ "it-to-browser-readablestream": "^2.0.12",
149
+ "multiformats": "^13.4.1",
150
+ "race-event": "^1.6.1",
151
151
  "readable-stream": "^4.7.0",
152
152
  "uint8arraylist": "^2.4.8",
153
153
  "uint8arrays": "^5.1.0"
154
154
  },
155
155
  "devDependencies": {
156
- "aegir": "^47.0.16"
156
+ "aegir": "^47.0.22"
157
157
  },
158
158
  "browser": {
159
159
  "node:stream": "readable-stream"
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,115 +1,157 @@
1
1
  import { Duplex } from 'node:stream'
2
- import { byteStream } from 'it-byte-stream'
3
- import type { Connection, Stream } from '@libp2p/interface'
2
+ import type { Connection, Logger, Stream } from '@libp2p/interface'
4
3
  import type { Socket, SocketConnectOpts, AddressInfo, SocketReadyState } from 'node:net'
5
4
 
6
5
  const MAX_TIMEOUT = 2_147_483_647
7
6
 
8
- class Libp2pSocket extends Duplex {
7
+ export class Libp2pSocket extends Duplex {
9
8
  public readonly autoSelectFamilyAttemptedAddresses = []
10
9
  public readonly connecting = false
11
10
  public readonly pending = false
12
- public readonly remoteAddress: string
11
+ public remoteAddress: string
13
12
  public bytesRead: number
14
13
  public bytesWritten: number
15
14
  public timeout = MAX_TIMEOUT
16
15
  public allowHalfOpen: boolean
17
16
 
18
- private readonly stream: Stream
17
+ #initStream: Promise<Stream>
18
+ #stream?: Stream
19
19
 
20
- constructor (stream: Stream, connection: Connection) {
21
- const bytes = byteStream(stream)
20
+ #log?: Logger
22
21
 
23
- super({
24
- write: (chunk, encoding, cb) => {
25
- this.stream.log('write %d bytes', chunk.byteLength)
22
+ constructor (stream: Stream, connection: Connection)
23
+ constructor (initStream: Promise<{ stream: Stream, connection: Connection }>)
24
+ constructor (...args: any[]) {
25
+ super()
26
26
 
27
- this.bytesWritten += chunk.byteLength
28
- bytes.write(chunk)
27
+ this.bytesRead = 0
28
+ this.bytesWritten = 0
29
+ this.allowHalfOpen = true
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
+ }
41
+ }
42
+
43
+ private gotStream ({ stream, connection }: { stream: Stream, connection: Connection }): Stream {
44
+ this.#log = stream.log.newScope('libp2p-socket')
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)
82
+ }
83
+
84
+ _write (chunk: Uint8Array, encoding: string, cb: (err?: Error) => void): void {
85
+ this.#log?.('write %d bytes', chunk.byteLength)
86
+
87
+ this.bytesWritten += chunk.byteLength
88
+
89
+ this.getStream(stream => {
90
+ if (!stream.send(chunk)) {
91
+ stream.onDrain()
29
92
  .then(() => {
30
93
  cb()
31
- }, err => {
94
+ }, (err) => {
32
95
  cb(err)
33
96
  })
34
- },
35
- read: (size) => {
36
- this.stream.log('asked to read %d bytes', size)
37
-
38
- void Promise.resolve().then(async () => {
39
- try {
40
- while (true) {
41
- const chunk = await bytes.read({
42
- signal: AbortSignal.timeout(this.timeout)
43
- })
44
-
45
- if (chunk == null) {
46
- this.stream.log('socket readable end closed')
47
- this.push(null)
48
- return
49
- }
50
-
51
- this.bytesRead += chunk.byteLength
52
-
53
- this.stream.log('socket read %d bytes', chunk.byteLength)
54
- const more = this.push(chunk.subarray())
55
-
56
- if (!more) {
57
- break
58
- }
59
- }
60
- } catch (err: any) {
61
- this.destroy(err)
62
- }
63
- })
64
- },
65
- destroy: (err, cb) => {
66
- this.stream.log('destroy with %d bytes buffered - %e', this.bufferSize, err)
97
+ } else {
98
+ cb()
99
+ }
100
+ })
101
+ }
67
102
 
68
- if (err != null) {
69
- bytes.unwrap().abort(err)
70
- cb()
71
- } else {
72
- bytes.unwrap().close()
73
- .then(() => {
74
- cb()
75
- })
76
- .catch(err => {
77
- stream.abort(err)
78
- cb(err)
79
- })
80
- }
81
- },
82
- final: (cb) => {
83
- this.stream.log('final')
84
-
85
- bytes.unwrap().closeWrite()
103
+ _read (size: number): void {
104
+ this.#log?.('asked to read %d bytes', size)
105
+ this.getStream(stream => {
106
+ stream.resume()
107
+ })
108
+ }
109
+
110
+ _destroy (err: Error, cb: (err?: Error) => void): void {
111
+ this.#log?.('destroy with %d bytes buffered - %e', this.bufferSize, err)
112
+
113
+ this.getStream(stream => {
114
+ if (err != null) {
115
+ stream.abort(err)
116
+ cb()
117
+ } else {
118
+ stream.close()
86
119
  .then(() => {
87
120
  cb()
88
121
  })
89
122
  .catch(err => {
90
- bytes.unwrap().abort(err)
123
+ stream.abort(err)
91
124
  cb(err)
92
125
  })
93
126
  }
94
127
  })
128
+ }
95
129
 
96
- this.stream = stream
97
- this.remoteAddress = connection.remoteAddr.toString()
98
- this.bytesRead = 0
99
- this.bytesWritten = 0
100
- this.allowHalfOpen = true
130
+ _final (cb: (err?: Error) => void): void {
131
+ this.#log?.('final')
132
+
133
+ this.getStream(stream => {
134
+ stream.close()
135
+ .then(() => {
136
+ cb()
137
+ })
138
+ .catch(err => {
139
+ stream.abort(err)
140
+ cb(err)
141
+ })
142
+ })
101
143
  }
102
144
 
103
145
  public get readyState (): SocketReadyState {
104
- if (this.stream.status === 'closed') {
146
+ if (this.#stream?.status === 'closed') {
105
147
  return 'closed'
106
148
  }
107
149
 
108
- if (this.stream.writeStatus === 'closed' || this.stream.writeStatus === 'closing') {
150
+ if (this.#stream?.writeStatus === 'closed' || this.#stream?.writeStatus === 'closing') {
109
151
  return 'readOnly'
110
152
  }
111
153
 
112
- if (this.stream.readStatus === 'closed' || this.stream.readStatus === 'closing') {
154
+ if (this.#stream?.readStatus === 'closed' || this.#stream?.readStatus === 'closing') {
113
155
  return 'writeOnly'
114
156
  }
115
157
 
@@ -121,7 +163,7 @@ class Libp2pSocket extends Duplex {
121
163
  }
122
164
 
123
165
  destroySoon (): void {
124
- this.stream.log('destroySoon with %d bytes buffered', this.bufferSize)
166
+ this.#log?.('destroySoon with %d bytes buffered', this.bufferSize)
125
167
  this.destroy()
126
168
  }
127
169
 
@@ -130,24 +172,27 @@ class Libp2pSocket extends Duplex {
130
172
  connect (port: number, connectionListener?: () => void): this
131
173
  connect (path: string, connectionListener?: () => void): this
132
174
  connect (...args: any[]): this {
133
- this.stream.log('connect %o', args)
175
+ this.#log?.('connect %o', args)
134
176
  return this
135
177
  }
136
178
 
137
179
  setEncoding (encoding?: BufferEncoding): this {
138
- this.stream.log('setEncoding %s', encoding)
180
+ this.#log?.('setEncoding %s', encoding)
139
181
  return this
140
182
  }
141
183
 
142
184
  resetAndDestroy (): this {
143
- this.stream.log('resetAndDestroy')
144
- this.stream.abort(new Error('Libp2pSocket.resetAndDestroy'))
185
+ this.#log?.('resetAndDestroy')
186
+
187
+ this.getStream(stream => {
188
+ stream.abort(new Error('Libp2pSocket.resetAndDestroy'))
189
+ })
145
190
 
146
191
  return this
147
192
  }
148
193
 
149
194
  setTimeout (timeout: number, callback?: () => void): this {
150
- this.stream.log('setTimeout %d', timeout)
195
+ this.#log?.('setTimeout %d', timeout)
151
196
 
152
197
  if (callback != null) {
153
198
  this.addListener('timeout', callback)
@@ -159,31 +204,31 @@ class Libp2pSocket extends Duplex {
159
204
  }
160
205
 
161
206
  setNoDelay (noDelay?: boolean): this {
162
- this.stream.log('setNoDelay %b', noDelay)
207
+ this.#log?.('setNoDelay %b', noDelay)
163
208
 
164
209
  return this
165
210
  }
166
211
 
167
212
  setKeepAlive (enable?: boolean, initialDelay?: number): this {
168
- this.stream.log('setKeepAlive %b %d', enable, initialDelay)
213
+ this.#log?.('setKeepAlive %b %d', enable, initialDelay)
169
214
 
170
215
  return this
171
216
  }
172
217
 
173
218
  address (): AddressInfo | Record<string, any> {
174
- this.stream.log('address')
219
+ this.#log?.('address')
175
220
 
176
221
  return {}
177
222
  }
178
223
 
179
224
  unref (): this {
180
- this.stream.log('unref')
225
+ this.#log?.('unref')
181
226
 
182
227
  return this
183
228
  }
184
229
 
185
230
  ref (): this {
186
- this.stream.log('ref')
231
+ this.#log?.('ref')
187
232
 
188
233
  return this
189
234
  }