mirror of
https://git.proxmox.com/git/package-rebuilds
synced 2025-08-22 06:36:36 +00:00
1192 lines
28 KiB
JavaScript
1192 lines
28 KiB
JavaScript
'use strict'
|
|
|
|
const { createSecureServer } = require('http2')
|
|
const { createReadStream, readFileSync } = require('fs')
|
|
const { once } = require('events')
|
|
const { Blob } = require('buffer')
|
|
const { Writable, pipeline, PassThrough, Readable } = require('stream')
|
|
|
|
const { test, plan } = require('tap')
|
|
const { gte } = require('semver')
|
|
const pem = require('https-pem')
|
|
|
|
const { Client, Agent } = require('..')
|
|
|
|
const isGreaterThanv20 = gte(process.version.slice(1), '20.0.0')
|
|
// NOTE: node versions <16.14.1 have a bug which changes the order of pseudo-headers
|
|
// https://github.com/nodejs/node/pull/41735
|
|
const hasPseudoHeadersOrderFix = gte(process.version.slice(1), '16.14.1')
|
|
|
|
plan(23)
|
|
|
|
test('Should support H2 connection', async t => {
|
|
const body = []
|
|
const server = createSecureServer(pem)
|
|
|
|
server.on('stream', (stream, headers, _flags, rawHeaders) => {
|
|
t.equal(headers['x-my-header'], 'foo')
|
|
t.equal(headers[':method'], 'GET')
|
|
stream.respond({
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'x-custom-h2': 'hello',
|
|
':status': 200
|
|
})
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.plan(6)
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
const response = await client.request({
|
|
path: '/',
|
|
method: 'GET',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
}
|
|
})
|
|
|
|
response.body.on('data', chunk => {
|
|
body.push(chunk)
|
|
})
|
|
|
|
await once(response.body, 'end')
|
|
t.equal(response.statusCode, 200)
|
|
t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
|
|
t.equal(response.headers['x-custom-h2'], 'hello')
|
|
t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
|
|
})
|
|
|
|
test('Should support H2 connection(multiple requests)', async t => {
|
|
const server = createSecureServer(pem)
|
|
|
|
server.on('stream', async (stream, headers, _flags, rawHeaders) => {
|
|
t.equal(headers['x-my-header'], 'foo')
|
|
t.equal(headers[':method'], 'POST')
|
|
const reqData = []
|
|
stream.on('data', chunk => reqData.push(chunk.toString()))
|
|
await once(stream, 'end')
|
|
const reqBody = reqData.join('')
|
|
t.equal(reqBody.length > 0, true)
|
|
stream.respond({
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'x-custom-h2': 'hello',
|
|
':status': 200
|
|
})
|
|
stream.end(`hello h2! ${reqBody}`)
|
|
})
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.plan(21)
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
const sendBody = `seq ${i}`
|
|
const body = []
|
|
const response = await client.request({
|
|
path: '/',
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'x-my-header': 'foo'
|
|
},
|
|
body: Readable.from(sendBody)
|
|
})
|
|
|
|
response.body.on('data', chunk => {
|
|
body.push(chunk)
|
|
})
|
|
|
|
await once(response.body, 'end')
|
|
t.equal(response.statusCode, 200)
|
|
t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
|
|
t.equal(response.headers['x-custom-h2'], 'hello')
|
|
t.equal(Buffer.concat(body).toString('utf8'), `hello h2! ${sendBody}`)
|
|
}
|
|
})
|
|
|
|
test('Should support H2 connection (headers as array)', async t => {
|
|
const body = []
|
|
const server = createSecureServer(pem)
|
|
|
|
server.on('stream', (stream, headers) => {
|
|
t.equal(headers['x-my-header'], 'foo')
|
|
t.equal(headers['x-my-drink'], 'coffee,tea')
|
|
t.equal(headers[':method'], 'GET')
|
|
stream.respond({
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'x-custom-h2': 'hello',
|
|
':status': 200
|
|
})
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.plan(7)
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
const response = await client.request({
|
|
path: '/',
|
|
method: 'GET',
|
|
headers: ['x-my-header', 'foo', 'x-my-drink', ['coffee', 'tea']]
|
|
})
|
|
|
|
response.body.on('data', chunk => {
|
|
body.push(chunk)
|
|
})
|
|
|
|
await once(response.body, 'end')
|
|
t.equal(response.statusCode, 200)
|
|
t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
|
|
t.equal(response.headers['x-custom-h2'], 'hello')
|
|
t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
|
|
})
|
|
|
|
test('Should support H2 connection(POST Buffer)', async t => {
|
|
const server = createSecureServer({ ...pem, allowHTTP1: false })
|
|
|
|
server.on('stream', async (stream, headers, _flags, rawHeaders) => {
|
|
t.equal(headers[':method'], 'POST')
|
|
const reqData = []
|
|
stream.on('data', chunk => reqData.push(chunk.toString()))
|
|
await once(stream, 'end')
|
|
t.equal(reqData.join(''), 'hello!')
|
|
stream.respond({
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'x-custom-h2': 'hello',
|
|
':status': 200
|
|
})
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.plan(6)
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
const sendBody = 'hello!'
|
|
const body = []
|
|
const response = await client.request({
|
|
path: '/',
|
|
method: 'POST',
|
|
body: sendBody
|
|
})
|
|
|
|
response.body.on('data', chunk => {
|
|
body.push(chunk)
|
|
})
|
|
|
|
await once(response.body, 'end')
|
|
t.equal(response.statusCode, 200)
|
|
t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
|
|
t.equal(response.headers['x-custom-h2'], 'hello')
|
|
t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
|
|
})
|
|
|
|
test('Should support H2 GOAWAY (server-side)', async t => {
|
|
const body = []
|
|
const server = createSecureServer(pem)
|
|
|
|
server.on('stream', (stream, headers) => {
|
|
t.equal(headers['x-my-header'], 'foo')
|
|
t.equal(headers[':method'], 'GET')
|
|
stream.respond({
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'x-custom-h2': 'hello',
|
|
':status': 200
|
|
})
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
server.on('session', session => {
|
|
setTimeout(() => {
|
|
session.goaway(204)
|
|
}, 1000)
|
|
})
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.plan(9)
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
const response = await client.request({
|
|
path: '/',
|
|
method: 'GET',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
}
|
|
})
|
|
|
|
response.body.on('data', chunk => {
|
|
body.push(chunk)
|
|
})
|
|
|
|
await once(response.body, 'end')
|
|
t.equal(response.statusCode, 200)
|
|
t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
|
|
t.equal(response.headers['x-custom-h2'], 'hello')
|
|
t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
|
|
|
|
const [url, disconnectClient, err] = await once(client, 'disconnect')
|
|
|
|
t.type(url, URL)
|
|
t.same(disconnectClient, [client])
|
|
t.equal(err.message, 'HTTP/2: "GOAWAY" frame received with code 204')
|
|
})
|
|
|
|
test('Should throw if bad allowH2 has been pased', async t => {
|
|
try {
|
|
// eslint-disable-next-line
|
|
new Client('https://localhost:1000', {
|
|
allowH2: 'true'
|
|
})
|
|
t.fail()
|
|
} catch (error) {
|
|
t.equal(error.message, 'allowH2 must be a valid boolean value')
|
|
}
|
|
})
|
|
|
|
test('Should throw if bad maxConcurrentStreams has been pased', async t => {
|
|
try {
|
|
// eslint-disable-next-line
|
|
new Client('https://localhost:1000', {
|
|
allowH2: true,
|
|
maxConcurrentStreams: {}
|
|
})
|
|
t.fail()
|
|
} catch (error) {
|
|
t.equal(
|
|
error.message,
|
|
'maxConcurrentStreams must be a possitive integer, greater than 0'
|
|
)
|
|
}
|
|
|
|
try {
|
|
// eslint-disable-next-line
|
|
new Client('https://localhost:1000', {
|
|
allowH2: true,
|
|
maxConcurrentStreams: -1
|
|
})
|
|
t.fail()
|
|
} catch (error) {
|
|
t.equal(
|
|
error.message,
|
|
'maxConcurrentStreams must be a possitive integer, greater than 0'
|
|
)
|
|
}
|
|
})
|
|
|
|
test(
|
|
'Request should fail if allowH2 is false and server advertises h1 only',
|
|
{ skip: isGreaterThanv20 },
|
|
async t => {
|
|
const server = createSecureServer(
|
|
{
|
|
...pem,
|
|
allowHTTP1: false,
|
|
ALPNProtocols: ['http/1.1']
|
|
},
|
|
(req, res) => {
|
|
t.fail('Should not create a valid h2 stream')
|
|
}
|
|
)
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
allowH2: false,
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
}
|
|
})
|
|
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
const response = await client.request({
|
|
path: '/',
|
|
method: 'GET',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
}
|
|
})
|
|
|
|
t.equal(response.statusCode, 403)
|
|
}
|
|
)
|
|
|
|
test(
|
|
'[v20] Request should fail if allowH2 is false and server advertises h1 only',
|
|
{ skip: !isGreaterThanv20 },
|
|
async t => {
|
|
const server = createSecureServer(
|
|
{
|
|
...pem,
|
|
allowHTTP1: false,
|
|
ALPNProtocols: ['http/1.1']
|
|
},
|
|
(req, res) => {
|
|
t.fail('Should not create a valid h2 stream')
|
|
}
|
|
)
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
allowH2: false,
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
}
|
|
})
|
|
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
t.plan(2)
|
|
|
|
try {
|
|
await client.request({
|
|
path: '/',
|
|
method: 'GET',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
}
|
|
})
|
|
} catch (error) {
|
|
t.equal(
|
|
error.message,
|
|
'Client network socket disconnected before secure TLS connection was established'
|
|
)
|
|
t.equal(error.code, 'ECONNRESET')
|
|
}
|
|
}
|
|
)
|
|
|
|
test('Should handle h2 continue', async t => {
|
|
const requestBody = []
|
|
const server = createSecureServer(pem, () => {})
|
|
const responseBody = []
|
|
|
|
server.on('checkContinue', (request, response) => {
|
|
t.equal(request.headers.expect, '100-continue')
|
|
t.equal(request.headers['x-my-header'], 'foo')
|
|
t.equal(request.headers[':method'], 'POST')
|
|
response.writeContinue()
|
|
|
|
request.on('data', chunk => requestBody.push(chunk))
|
|
|
|
response.writeHead(200, {
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'x-custom-h2': 'foo'
|
|
})
|
|
response.end('hello h2!')
|
|
})
|
|
|
|
t.plan(7)
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
expectContinue: true,
|
|
allowH2: true
|
|
})
|
|
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
const response = await client.request({
|
|
path: '/',
|
|
method: 'POST',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
},
|
|
expectContinue: true
|
|
})
|
|
|
|
response.body.on('data', chunk => {
|
|
responseBody.push(chunk)
|
|
})
|
|
|
|
await once(response.body, 'end')
|
|
|
|
t.equal(response.statusCode, 200)
|
|
t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
|
|
t.equal(response.headers['x-custom-h2'], 'foo')
|
|
t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
|
|
})
|
|
|
|
test('Dispatcher#Stream', t => {
|
|
const server = createSecureServer(pem)
|
|
const expectedBody = 'hello from client!'
|
|
const bufs = []
|
|
let requestBody = ''
|
|
|
|
server.on('stream', async (stream, headers) => {
|
|
stream.setEncoding('utf-8')
|
|
stream.on('data', chunk => {
|
|
requestBody += chunk
|
|
})
|
|
|
|
stream.respond({ ':status': 200, 'x-custom': 'custom-header' })
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
t.plan(4)
|
|
|
|
server.listen(0, async () => {
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
await client.stream(
|
|
{ path: '/', opaque: { bufs }, method: 'POST', body: expectedBody },
|
|
({ statusCode, headers, opaque: { bufs } }) => {
|
|
t.equal(statusCode, 200)
|
|
t.equal(headers['x-custom'], 'custom-header')
|
|
|
|
return new Writable({
|
|
write (chunk, _encoding, cb) {
|
|
bufs.push(chunk)
|
|
cb()
|
|
}
|
|
})
|
|
}
|
|
)
|
|
|
|
t.equal(Buffer.concat(bufs).toString('utf-8'), 'hello h2!')
|
|
t.equal(requestBody, expectedBody)
|
|
})
|
|
})
|
|
|
|
test('Dispatcher#Pipeline', t => {
|
|
const server = createSecureServer(pem)
|
|
const expectedBody = 'hello from client!'
|
|
const bufs = []
|
|
let requestBody = ''
|
|
|
|
server.on('stream', async (stream, headers) => {
|
|
stream.setEncoding('utf-8')
|
|
stream.on('data', chunk => {
|
|
requestBody += chunk
|
|
})
|
|
|
|
stream.respond({ ':status': 200, 'x-custom': 'custom-header' })
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
t.plan(5)
|
|
|
|
server.listen(0, () => {
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
pipeline(
|
|
new Readable({
|
|
read () {
|
|
this.push(Buffer.from(expectedBody))
|
|
this.push(null)
|
|
}
|
|
}),
|
|
client.pipeline(
|
|
{ path: '/', method: 'POST', body: expectedBody },
|
|
({ statusCode, headers, body }) => {
|
|
t.equal(statusCode, 200)
|
|
t.equal(headers['x-custom'], 'custom-header')
|
|
|
|
return pipeline(body, new PassThrough(), () => {})
|
|
}
|
|
),
|
|
new Writable({
|
|
write (chunk, _, cb) {
|
|
bufs.push(chunk)
|
|
cb()
|
|
}
|
|
}),
|
|
err => {
|
|
t.error(err)
|
|
t.equal(Buffer.concat(bufs).toString('utf-8'), 'hello h2!')
|
|
t.equal(requestBody, expectedBody)
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
test('Dispatcher#Connect', t => {
|
|
const server = createSecureServer(pem)
|
|
const expectedBody = 'hello from client!'
|
|
let requestBody = ''
|
|
|
|
server.on('stream', async (stream, headers) => {
|
|
stream.setEncoding('utf-8')
|
|
stream.on('data', chunk => {
|
|
requestBody += chunk
|
|
})
|
|
|
|
stream.respond({ ':status': 200, 'x-custom': 'custom-header' })
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
t.plan(6)
|
|
|
|
server.listen(0, () => {
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
let result = ''
|
|
client.connect({ path: '/' }, (err, { socket }) => {
|
|
t.error(err)
|
|
socket.on('data', chunk => {
|
|
result += chunk
|
|
})
|
|
socket.on('response', headers => {
|
|
t.equal(headers[':status'], 200)
|
|
t.equal(headers['x-custom'], 'custom-header')
|
|
t.notOk(socket.closed)
|
|
})
|
|
|
|
// We need to handle the error event although
|
|
// is not controlled by Undici, the fact that a session
|
|
// is destroyed and destroys subsequent streams, causes
|
|
// unhandled errors to surface if not handling this event.
|
|
socket.on('error', () => {})
|
|
|
|
socket.once('end', () => {
|
|
t.equal(requestBody, expectedBody)
|
|
t.equal(result, 'hello h2!')
|
|
})
|
|
socket.end(expectedBody)
|
|
})
|
|
})
|
|
})
|
|
|
|
test('Dispatcher#Upgrade', t => {
|
|
const server = createSecureServer(pem)
|
|
|
|
server.on('stream', async (stream, headers) => {
|
|
stream.end()
|
|
})
|
|
|
|
t.plan(1)
|
|
|
|
server.listen(0, async () => {
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
try {
|
|
await client.upgrade({ path: '/' })
|
|
} catch (error) {
|
|
t.equal(error.message, 'Upgrade not supported for H2')
|
|
}
|
|
})
|
|
})
|
|
|
|
test('Dispatcher#destroy', async t => {
|
|
const promises = []
|
|
const server = createSecureServer(pem)
|
|
|
|
server.on('stream', (stream, headers) => {
|
|
setTimeout(stream.end.bind(stream), 1500)
|
|
})
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.plan(4)
|
|
t.teardown(server.close.bind(server))
|
|
|
|
promises.push(
|
|
client.request({
|
|
path: '/',
|
|
method: 'GET',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
}
|
|
})
|
|
)
|
|
|
|
promises.push(
|
|
client.request({
|
|
path: '/',
|
|
method: 'GET',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
}
|
|
})
|
|
)
|
|
|
|
promises.push(
|
|
client.request({
|
|
path: '/',
|
|
method: 'GET',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
}
|
|
})
|
|
)
|
|
|
|
promises.push(
|
|
client.request({
|
|
path: '/',
|
|
method: 'GET',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
}
|
|
})
|
|
)
|
|
|
|
await client.destroy()
|
|
|
|
const results = await Promise.allSettled(promises)
|
|
|
|
t.equal(results[0].status, 'rejected')
|
|
t.equal(results[1].status, 'rejected')
|
|
t.equal(results[2].status, 'rejected')
|
|
t.equal(results[3].status, 'rejected')
|
|
})
|
|
|
|
test('Should handle h2 request with body (string or buffer) - dispatch', t => {
|
|
const server = createSecureServer(pem)
|
|
const expectedBody = 'hello from client!'
|
|
const response = []
|
|
const requestBody = []
|
|
|
|
server.on('stream', async (stream, headers) => {
|
|
stream.on('data', chunk => requestBody.push(chunk))
|
|
|
|
stream.respond({
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'x-custom-h2': headers['x-my-header'],
|
|
':status': 200
|
|
})
|
|
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
t.plan(7)
|
|
|
|
server.listen(0, () => {
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
client.dispatch(
|
|
{
|
|
path: '/',
|
|
method: 'POST',
|
|
headers: {
|
|
'x-my-header': 'foo',
|
|
'content-type': 'text/plain'
|
|
},
|
|
body: expectedBody
|
|
},
|
|
{
|
|
onConnect () {
|
|
t.ok(true)
|
|
},
|
|
onError (err) {
|
|
t.error(err)
|
|
},
|
|
onHeaders (statusCode, headers) {
|
|
t.equal(statusCode, 200)
|
|
t.equal(headers['content-type'], 'text/plain; charset=utf-8')
|
|
t.equal(headers['x-custom-h2'], 'foo')
|
|
},
|
|
onData (chunk) {
|
|
response.push(chunk)
|
|
},
|
|
onBodySent (body) {
|
|
t.equal(body.toString('utf-8'), expectedBody)
|
|
},
|
|
onComplete () {
|
|
t.equal(Buffer.concat(response).toString('utf-8'), 'hello h2!')
|
|
t.equal(
|
|
Buffer.concat(requestBody).toString('utf-8'),
|
|
'hello from client!'
|
|
)
|
|
}
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
test('Should handle h2 request with body (stream)', async t => {
|
|
const server = createSecureServer(pem)
|
|
const expectedBody = readFileSync(__filename, 'utf-8')
|
|
const stream = createReadStream(__filename)
|
|
const requestChunks = []
|
|
const responseBody = []
|
|
|
|
server.on('stream', async (stream, headers) => {
|
|
t.equal(headers[':method'], 'PUT')
|
|
t.equal(headers[':path'], '/')
|
|
t.equal(headers[':scheme'], 'https')
|
|
|
|
stream.respond({
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'x-custom-h2': headers['x-my-header'],
|
|
':status': 200
|
|
})
|
|
|
|
for await (const chunk of stream) {
|
|
requestChunks.push(chunk)
|
|
}
|
|
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
t.plan(8)
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
const response = await client.request({
|
|
path: '/',
|
|
method: 'PUT',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
},
|
|
body: stream
|
|
})
|
|
|
|
for await (const chunk of response.body) {
|
|
responseBody.push(chunk)
|
|
}
|
|
|
|
t.equal(response.statusCode, 200)
|
|
t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
|
|
t.equal(response.headers['x-custom-h2'], 'foo')
|
|
t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
|
|
t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
|
|
})
|
|
|
|
test('Should handle h2 request with body (iterable)', async t => {
|
|
const server = createSecureServer(pem)
|
|
const expectedBody = 'hello'
|
|
const requestChunks = []
|
|
const responseBody = []
|
|
const iterableBody = {
|
|
[Symbol.iterator]: function * () {
|
|
const end = expectedBody.length - 1
|
|
for (let i = 0; i < end + 1; i++) {
|
|
yield expectedBody[i]
|
|
}
|
|
|
|
return expectedBody[end]
|
|
}
|
|
}
|
|
|
|
server.on('stream', async (stream, headers) => {
|
|
t.equal(headers[':method'], 'POST')
|
|
t.equal(headers[':path'], '/')
|
|
t.equal(headers[':scheme'], 'https')
|
|
|
|
stream.on('data', chunk => requestChunks.push(chunk))
|
|
|
|
stream.respond({
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'x-custom-h2': headers['x-my-header'],
|
|
':status': 200
|
|
})
|
|
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
t.plan(8)
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
const response = await client.request({
|
|
path: '/',
|
|
method: 'POST',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
},
|
|
body: iterableBody
|
|
})
|
|
|
|
response.body.on('data', chunk => {
|
|
responseBody.push(chunk)
|
|
})
|
|
|
|
await once(response.body, 'end')
|
|
|
|
t.equal(response.statusCode, 200)
|
|
t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
|
|
t.equal(response.headers['x-custom-h2'], 'foo')
|
|
t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
|
|
t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
|
|
})
|
|
|
|
test('Should handle h2 request with body (Blob)', { skip: !Blob }, async t => {
|
|
const server = createSecureServer(pem)
|
|
const expectedBody = 'asd'
|
|
const requestChunks = []
|
|
const responseBody = []
|
|
const body = new Blob(['asd'], {
|
|
type: 'application/json'
|
|
})
|
|
|
|
server.on('stream', async (stream, headers) => {
|
|
t.equal(headers[':method'], 'POST')
|
|
t.equal(headers[':path'], '/')
|
|
t.equal(headers[':scheme'], 'https')
|
|
|
|
stream.on('data', chunk => requestChunks.push(chunk))
|
|
|
|
stream.respond({
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'x-custom-h2': headers['x-my-header'],
|
|
':status': 200
|
|
})
|
|
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
t.plan(8)
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
const response = await client.request({
|
|
path: '/',
|
|
method: 'POST',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
},
|
|
body
|
|
})
|
|
|
|
response.body.on('data', chunk => {
|
|
responseBody.push(chunk)
|
|
})
|
|
|
|
await once(response.body, 'end')
|
|
|
|
t.equal(response.statusCode, 200)
|
|
t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
|
|
t.equal(response.headers['x-custom-h2'], 'foo')
|
|
t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
|
|
t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
|
|
})
|
|
|
|
test(
|
|
'Should handle h2 request with body (Blob:ArrayBuffer)',
|
|
{ skip: !Blob },
|
|
async t => {
|
|
const server = createSecureServer(pem)
|
|
const expectedBody = 'hello'
|
|
const requestChunks = []
|
|
const responseBody = []
|
|
const buf = Buffer.from(expectedBody)
|
|
const body = new ArrayBuffer(buf.byteLength)
|
|
|
|
buf.copy(new Uint8Array(body))
|
|
|
|
server.on('stream', async (stream, headers) => {
|
|
t.equal(headers[':method'], 'POST')
|
|
t.equal(headers[':path'], '/')
|
|
t.equal(headers[':scheme'], 'https')
|
|
|
|
stream.on('data', chunk => requestChunks.push(chunk))
|
|
|
|
stream.respond({
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'x-custom-h2': headers['x-my-header'],
|
|
':status': 200
|
|
})
|
|
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
t.plan(8)
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
const response = await client.request({
|
|
path: '/',
|
|
method: 'POST',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
},
|
|
body
|
|
})
|
|
|
|
response.body.on('data', chunk => {
|
|
responseBody.push(chunk)
|
|
})
|
|
|
|
await once(response.body, 'end')
|
|
|
|
t.equal(response.statusCode, 200)
|
|
t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
|
|
t.equal(response.headers['x-custom-h2'], 'foo')
|
|
t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!')
|
|
t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody)
|
|
}
|
|
)
|
|
|
|
test('Agent should support H2 connection', async t => {
|
|
const body = []
|
|
const server = createSecureServer(pem)
|
|
|
|
server.on('stream', (stream, headers) => {
|
|
t.equal(headers['x-my-header'], 'foo')
|
|
t.equal(headers[':method'], 'GET')
|
|
stream.respond({
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
'x-custom-h2': 'hello',
|
|
':status': 200
|
|
})
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Agent({
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.plan(6)
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
const response = await client.request({
|
|
origin: `https://localhost:${server.address().port}`,
|
|
path: '/',
|
|
method: 'GET',
|
|
headers: {
|
|
'x-my-header': 'foo'
|
|
}
|
|
})
|
|
|
|
response.body.on('data', chunk => {
|
|
body.push(chunk)
|
|
})
|
|
|
|
await once(response.body, 'end')
|
|
t.equal(response.statusCode, 200)
|
|
t.equal(response.headers['content-type'], 'text/plain; charset=utf-8')
|
|
t.equal(response.headers['x-custom-h2'], 'hello')
|
|
t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!')
|
|
})
|
|
|
|
test(
|
|
'Should provide pseudo-headers in proper order',
|
|
{ skip: !hasPseudoHeadersOrderFix },
|
|
async t => {
|
|
const server = createSecureServer(pem)
|
|
server.on('stream', (stream, _headers, _flags, rawHeaders) => {
|
|
t.same(rawHeaders, [
|
|
':authority',
|
|
`localhost:${server.address().port}`,
|
|
':method',
|
|
'GET',
|
|
':path',
|
|
'/',
|
|
':scheme',
|
|
'https'
|
|
])
|
|
|
|
stream.respond({
|
|
'content-type': 'text/plain; charset=utf-8',
|
|
':status': 200
|
|
})
|
|
stream.end()
|
|
})
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
const response = await client.request({
|
|
path: '/',
|
|
method: 'GET'
|
|
})
|
|
|
|
t.equal(response.statusCode, 200)
|
|
}
|
|
)
|
|
|
|
test('The h2 pseudo-headers is not included in the headers', async t => {
|
|
const server = createSecureServer(pem)
|
|
|
|
server.on('stream', (stream, headers) => {
|
|
stream.respond({
|
|
':status': 200
|
|
})
|
|
stream.end('hello h2!')
|
|
})
|
|
|
|
server.listen(0)
|
|
await once(server, 'listening')
|
|
|
|
const client = new Client(`https://localhost:${server.address().port}`, {
|
|
connect: {
|
|
rejectUnauthorized: false
|
|
},
|
|
allowH2: true
|
|
})
|
|
|
|
t.plan(2)
|
|
t.teardown(server.close.bind(server))
|
|
t.teardown(client.close.bind(client))
|
|
|
|
const response = await client.request({
|
|
path: '/',
|
|
method: 'GET'
|
|
})
|
|
|
|
await response.body.text()
|
|
|
|
t.equal(response.statusCode, 200)
|
|
t.equal(response.headers[':status'], undefined)
|
|
})
|