hapijs / hapi

The Simple, Secure Framework Developers Trust
https://hapi.dev
Other
14.54k stars 1.33k forks source link

vhost routing with http2 (and TLS) does not work #4385

Open muratyanikbas opened 1 year ago

muratyanikbas commented 1 year ago

Support plan

Context

What are you trying to achieve or the steps to reproduce?

import hapi from '@hapi/hapi'
import {createSecureServer} from 'node:http2'
import {dirname, join} from 'node:path'
import {fileURLToPath} from 'node:url'
import { readFileSync } from 'node:fs'

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

let app = {}

let http2Settings = {
    headerTableSize: 4096,
    enablePush: true,
    initialWindowSize: 65535,
    maxFrameSize: 16384,
    maxConcurrentStreams: 4294967295,
    maxHeaderListSize: 65535,
    enableConnectProtocol: false
}   

let secureServerOptions = {
    allowHTTP1: true,
    /* maxDeflateDynamicTableSize, */
    maxSettings: 32,
    maxSessionMemory: 10,
    maxHeaderListPairs: 128,
    maxOutstandingPings: 10,
    /* maxSendHeaderBlockLength, */
    /* paddingStrategy, */
    peerMaxConcurrentStreams: 100,
    maxSessionInvalidFrames: 1000,
    maxSessionRejectedStreams: 100,
    settings: http2Settings
}

let secureServer = createSecureServer(secureServerOptions)

const cipherValueIntermediate = 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'
const cipher = cipherValueIntermediate

const minVersion = 'TLSv1.2'
const maxVersion = 'TLSv1.3'
const dhparam = readFileSync(join(__dirname, '/security/dhparam.pem'))
const ecdhCurve = 'auto'
const honorCipherOrder = false

let context = {
    /* ca, */
    cert: readFileSync(join(__dirname, '/letsencrypt/live/example.com/fullchain.pem')),
    key: readFileSync(join(__dirname, '/letsencrypt/live/example.com/privkey.pem')),
    cipher,
    minVersion,
    maxVersion,
    dhparam,
    ecdhCurve,
    honorCipherOrder
}

let context2 = {
    /* ca, */
    cert: readFileSync(join(__dirname, '/letsencrypt/live/api.example.com/fullchain.pem')),
    key: readFileSync(join(__dirname, '/letsencrypt/live/api.example.com/privkey.pem')),
    cipher,
    minVersion,
    maxVersion,
    dhparam,
    ecdhCurve,
    honorCipherOrder
}

secureServer.addContext('example.com', context)
secureServer.addContext('api.example.com', context2)

async function vhostBug(app) {
    let serverOptions = {
        address: '0.0.0.0',
        app,
        listener: secureServer,
        autoListen: true,
        tls: true,
        port: 443
    }

    let hapiServer = hapi.Server(serverOptions)

    await hapiServer.start()

    // vhost example.com
    // does not work
    hapiServer.route({
        method: 'GET',
        path: '/',
        vhost: 'example.com',
        handler: function (request, h) {
            return 'example.com'
        }
    })

    // vhost api.example.com
    // does not work
    hapiServer.route({
        method: 'GET',
        path: '/',
        vhost: 'api.example.com',
        handler: function (request, h) {
            return 'api.example.com'
        }
    })

    // no vhost
    // works for both api.example.com and example.com
    hapiServer.route({
        method: 'GET',
        path: '/',
        handler: function (request, h) {
            return '*.example.com'
        }
    })
}

vhostBug(app)

What was the result you got?

The routes with vhost in use under http2 are not served. The routes do not match. With an http context it works well (TLS disabled).

What result did you expect?

Working vhost routes as expected.

kanongil commented 1 year ago

Hapi does not support http2. If the compatibility wrappers you are using does not work correctly, it is a node.js issue.

The host is extracted here from the request here: https://github.com/hapijs/hapi/blob/5999a6027fdedfa28c2317921e2fae56f4f55bcd/lib/request.js#L632

kanongil commented 1 year ago

FYI, last I checked the node.js HTTP/2 implementation is very buggy, and I would advise anyone to stay far away from it. A HTTP2 client is much better served through a reverse proxy, like nginx, haproxy or varnish, which can talk HTTP/2 to the client, and translate to HTTP/1.1 for the hapi server.

muratyanikbas commented 1 year ago

HTTP2 works a bit differently. This line solves my problem spontaneously but there are certainly other problems that can be encountered.

const host = req.headers[':authority'] ? req.headers[':authority'].trim() : '';

The headers under HTTP2 differ significantly. Here is an example between HTTP and HTTP2:

[Object: null prototype] {
  ':method': 'GET',
  ':authority': 'example.com:9000',
  ':scheme': 'https',
  ':path': '/',
  pragma: 'no-cache',
  'cache-control': 'no-cache',
  'sec-ch-ua': '"Chromium";v="106", "Google Chrome";v="106", "Not;A=Brand";v="99"',
  'sec-ch-ua-mobile': '?0',
  'sec-ch-ua-platform': '"Linux"',
  'upgrade-insecure-requests': '1',
  'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36',
  accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
  'sec-fetch-site': 'none',
  'sec-fetch-mode': 'navigate',
  'sec-fetch-user': '?1',
  'sec-fetch-dest': 'document',
  'accept-encoding': 'gzip, deflate, br',
  'accept-language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7',
  [Symbol(nodejs.http2.sensitiveHeaders)]: []
}
{
  host: 'example.com:9000',
  connection: 'keep-alive',
  pragma: 'no-cache',
  'cache-control': 'no-cache',
  'upgrade-insecure-requests': '1',
  'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36',
  accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
  'accept-encoding': 'gzip, deflate',
  'accept-language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'
}

The interface should be compatible in itself. But the protocol is organized a bit differently, here are the headers. Support for the HTTP2 interface would be useful. The API documentation states: _An optional node HTTP (or HTTPS) http.Server object (or an object with a compatible interface)._