Node.js HTTP2 server Error: socket hang up
Asked Answered
E

1

10

Given the latest version of Node.js with experimental HTTP2 support:

$ node -v
v9.2.0

An HTTP2 server:

var options = {
  key: getKey(),
  cert: getCert(),
  allowHTTP1: true
}

var server = http2.createSecureServer(options)
server.on('stream', onstream)
server.on('error', onerror)
server.on('connect', onconnect)
server.on('socketError', onsocketerror)
server.on('frameError', onframeerror)
server.on('remoteSettings', onremotesettings)
server.listen(8443)

function onconnect() {
  console.log('connect')
}

function onremotesettings(settings) {
  console.log('remote settings', settings)
}

function onframeerror(error) {
  console.log('frame error', error)
}

function onsocketerror(error) {
  console.log('socket error', error)
}

function onerror(error) {
  console.log(error)
}

function onstream(stream, headers) {
  console.log('stream')
}

And a request made to it:

var https = require('https')

var options = {
  method: 'GET',
  hostname: 'localhost',
  port: '8443',
  path: '/',
  protocol: 'https:',
  rejectUnauthorized: false,
  agent: false
}

var req = https.request(options, function(res){
  var body = ''
  res.setEncoding('utf8')
  res.on('data', function(data){
    body += data;
  });
  res.on('end', function(){
    callback(null, body)
  })
})

req.end()

It just hangs and eventually says:

Error: socket hang up
at createHangUpError (_http_client.js:330:15)
    at TLSSocket.socketOnEnd (_http_client.js:423:23)
    at TLSSocket.emit (events.js:164:20)
    at endReadableNT (_stream_readable.js:1054:12)
    at _combinedTickCallback (internal/process/next_tick.js:138:11)
    at process._tickCallback (internal/process/next_tick.js:180:9)

If rejectUnauthorized: true is set, then it errors:

Error: self signed certificate
    at TLSSocket.onConnectSecure (_tls_wrap.js:1036:34)
    at TLSSocket.emit (events.js:159:13)
    at TLSSocket._finishInit (_tls_wrap.js:637:8)

Not sure what is going wrong and why it won't get to the point of logging stream.

If I go in the browser and visit https://localhost:8443, and click through the warning messages, it does actually log stream and successfully make the request. But haven't been able to get node to make the request.

I would like to treat this as an HTTP1 server, so don't want to use the HTTP2 client to make the request. But tried using that and same thing.

Eldon answered 23/11, 2017 at 17:6 Comment(2)
related: #47586257Eldon
#47613263Eldon
S
11

HTTP/1 doesn't share the same request semantics as HTTP/2 so HTTP/1 clients need to be detected and handled in a HTTP/2 server. To support both you need to use the HTTP2 Compatibility API.

The "hang" occurs when a HTTP1 client connects to a HTTP/2 server with allowHTTP1: true set but doesn't handle the HTTP/1 request.

The examples are based on the Node documentation example code.

HTTP/1 and /2 Mixed Server

const http2 = require('http2')
const fs = require('fs')

var options = {
  key: fs.readFileSync('server-key.pem'), 
  cert: fs.readFileSync('server-crt.pem'), 
  //ca: fs.readFileSync('ca-crt.pem'), 
  allowHTTP1: true,
}

var server = http2.createSecureServer(options, (req, res) => {
  // detects if it is a HTTPS request or HTTP/2
  const { socket: { alpnProtocol } } = (req.httpVersion === '2.0')
    ? req.stream.session 
    : req

  res.writeHead(200, { 'content-type': 'application/json' })
  res.end(JSON.stringify({
    alpnProtocol,
    httpVersion: req.httpVersion
  }))
})

server.listen(8443)

HTTP/2 Client

const http2 = require('http2')
const fs = require('fs')

const client = http2.connect('https://localhost:8443', {
    ca: fs.readFileSync('ca-crt.pem'),
    rejectUnauthorized: true,
})
client.on('socketError', (err) => console.error(err))
client.on('error', (err) => console.error(err))

const req = client.request({ ':path': '/' })

req.on('response', (headers, flags) => {
  for (const name in headers) {
    console.log('Header: "%s" "%s"', name, headers[name])
  }
})

req.setEncoding('utf8')
let data = ''
req.on('data', chunk => data += chunk)
req.on('end', () => {
  console.log('Data:', data)
  client.destroy()
})
req.end()

Then running:

→ node http2_client.js 
(node:34542) ExperimentalWarning: The http2 module is an experimental API.
Header: ":status" "200"
Header: "content-type" "application/json"
Header: "date" "Sat, 02 Dec 2017 23:27:21 GMT"
Data: {"alpnProtocol":"h2","httpVersion":"2.0"}

HTTP/1 Client

const https = require('https')
const fs = require('fs')

var options = {
  method: 'GET',
  hostname: 'localhost',
  port: '8443',
  path: '/',
  protocol: 'https:',
  ca: fs.readFileSync('ca-crt.pem'),
  rejectUnauthorized: true,
  //agent: false
}

var req = https.request(options, function(res){
  var body = ''
  res.setEncoding('utf8')
  res.on('data', data => body += data)
  res.on('end', ()=> console.log('Body:', body))
})

req.on('response', response => {
  for (const name in response.headers) {
    console.log('Header: "%s" "%s"', name, response.headers[name])
  }
})

req.end()

Then running

→ node http1_client.js 
Header: "content-type" "application/json"
Header: "date" "Sat, 02 Dec 2017 23:27:08 GMT"
Header: "connection" "close"
Header: "transfer-encoding" "chunked"
Body: {"alpnProtocol":false,"httpVersion":"1.1"}

HTTP/2 Server

Using the plain HTTP/2 Server will work with the http2_client but will "hang" for a http1_client. The TLS connection from a HTTP/1 client will be closed when you remove allowHTTP1: true.

const http2 = require('http2')
const fs = require('fs')

var options = {
  key: fs.readFileSync('server-key.pem'), 
  cert: fs.readFileSync('server-crt.pem'), 
  ca: fs.readFileSync('ca-crt.pem'), 
  allowHTTP1: true,
}

var server = http2.createSecureServer(options)
server.on('error', error => console.log(error))
server.on('connect', conn => console.log('connect', conn))
server.on('socketError', error => console.log('socketError', error))
server.on('frameError', error => console.log('frameError', error))
server.on('remoteSettings', settings => console.log('remote settings', settings))

server.on('stream', (stream, headers) => {
  console.log('stream', headers)
  stream.respond({
    'content-type': 'application/html',
    ':status': 200
  })
  console.log(stream.session)
  stream.end(JSON.stringify({
    alpnProtocol: stream.session.socket.alpnProtocol,
    httpVersion: "2"
  }))
})

server.listen(8443)

Certs

With the extended intermediate certificate setup detailed in the gist, the complete certificate chain for the CA needs to be supplied to the clients.

cat ca/x/certs/x.public.pem > caxy.pem
cat ca/y/certs/y.public.pem >> caxy.pem

Then in the clients use this ca in the options.

{ 
  ca: fs.readFileSync('caxy.pem'),
}

These examples were run withe the following simple CA setup from this circle.com article:

To simplify the configuration, let’s grab the following CA configuration file.

wget https://raw.githubusercontent.com/anders94/https-authorized-clients/master/keys/ca.cnf

Next, we’ll create a new certificate authority using this configuration.

openssl req -new -x509 \
  -days 9999 \
  -config ca.cnf \
  -keyout ca-key.pem \
  -out ca-crt.pem

Now that we have our certificate authority in ca-key.pem and ca-crt.pem, let’s generate a private key for the server.

openssl genrsa \
  -out server-key.pem \
  4096

Our next move is to generate a certificate signing request. Again to simplify configuration, let’s use server.cnf as a configuration shortcut.

wget https://raw.githubusercontent.com/anders94/https-authorized-clients/master/keys/server.cnf

Now we’ll generate the certificate signing request.

openssl req -new \
  -config server.cnf \
  -key server-key.pem \
  -out server-csr.pem

Now let’s sign the request.

openssl x509 -req -extfile server.cnf \
  -days 999 \
  -passin "pass:password" \
  -in server-csr.pem \
  -CA ca-crt.pem \
  -CAkey ca-key.pem \
  -CAcreateserial \
  -out server-crt.pem
Seibert answered 2/12, 2017 at 23:30 Comment(9)
Here are the details of my setup. Not sure if this is exactly what I used but close. gist.github.com/lancejpollard/eedfdc9bf554b36279868b335c96d2e2Eldon
Also wondering why it works in the browser but not in node.js. I can't get it to actually get to the first part of the server request code, the request never goes through.Eldon
Modern browsers include http2 supportSeibert
A http2 client works in nodejs. Or do you mean you can't get a request to go though in the above mixed http1/2 server example?Seibert
It works, I have no idea what I did differently. Thank you.Eldon
It gives the error I was getting if I remove rejectUnauthorized: false. Uncaught Error: unable to verify the first certificate. I only want to allow for authorized stuff. Again, the browser works even with rejectUnauthorized: true.Eldon
I also get socket hang up when I try without rejectUnauthorized: false. Not sure.Eldon
To remove rejectUnauthorized the clients require the CA certs, updated.Seibert
Let us continue this discussion in chat.Eldon

© 2022 - 2024 — McMap. All rights reserved.