@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/dist/index.min.js +24 -25
- package/dist/index.min.js.map +4 -4
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +65 -63
- package/dist/src/index.js.map +1 -1
- package/dist/src/stream-to-socket.d.ts +8 -2
- package/dist/src/stream-to-socket.d.ts.map +1 -1
- package/dist/src/stream-to-socket.js +96 -67
- package/dist/src/stream-to-socket.js.map +1 -1
- package/package.json +11 -11
- package/src/index.ts +74 -70
- package/src/stream-to-socket.ts +109 -75
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 {
|
|
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:
|
|
93
|
+
let source: AsyncIterable<any> = stream
|
|
93
94
|
|
|
94
95
|
if (!info.upgrade) {
|
|
95
|
-
source = takeBytes(stream
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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
|
|
337
|
-
|
|
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
|
|
353
|
+
const config = getNetConfig(address)
|
|
354
354
|
|
|
355
|
-
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
/**
|
package/src/stream-to-socket.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { Duplex } from 'node:stream'
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
this.#
|
|
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.#
|
|
85
|
+
this.#log?.('write %d bytes', chunk.byteLength)
|
|
39
86
|
|
|
40
87
|
this.bytesWritten += chunk.byteLength
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
}
|
|
45
|
-
|
|
46
|
-
})
|
|
99
|
+
}
|
|
100
|
+
})
|
|
47
101
|
}
|
|
48
102
|
|
|
49
103
|
_read (size: number): void {
|
|
50
|
-
this.#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
68
|
-
|
|
110
|
+
_destroy (err: Error, cb: (err?: Error) => void): void {
|
|
111
|
+
this.#log?.('destroy with %d bytes buffered - %e', this.bufferSize, err)
|
|
69
112
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
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
|
-
|
|
81
|
-
this.#
|
|
130
|
+
_final (cb: (err?: Error) => void): void {
|
|
131
|
+
this.#log?.('final')
|
|
82
132
|
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
175
|
+
this.#log?.('connect %o', args)
|
|
142
176
|
return this
|
|
143
177
|
}
|
|
144
178
|
|
|
145
179
|
setEncoding (encoding?: BufferEncoding): this {
|
|
146
|
-
this.#
|
|
180
|
+
this.#log?.('setEncoding %s', encoding)
|
|
147
181
|
return this
|
|
148
182
|
}
|
|
149
183
|
|
|
150
184
|
resetAndDestroy (): this {
|
|
151
|
-
this.#
|
|
152
|
-
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
219
|
+
this.#log?.('address')
|
|
183
220
|
|
|
184
221
|
return {}
|
|
185
222
|
}
|
|
186
223
|
|
|
187
224
|
unref (): this {
|
|
188
|
-
this.#
|
|
225
|
+
this.#log?.('unref')
|
|
189
226
|
|
|
190
227
|
return this
|
|
191
228
|
}
|
|
192
229
|
|
|
193
230
|
ref (): this {
|
|
194
|
-
this.#
|
|
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
|
-
|
|
208
|
-
socket.setStream(stream, connection)
|
|
209
|
-
|
|
210
|
-
return socket
|
|
244
|
+
return new Libp2pSocket(stream, connection)
|
|
211
245
|
}
|