Resolves #6074. Adds new option to configure HTTP Server's MaxHeaderBytes with option `-http-max-header-bytes`

Adds tests for behavior
This commit is contained in:
Michael Montgomery 2020-10-29 12:38:19 -05:00
parent aabd0130a9
commit 1c0a46849a
8 changed files with 90 additions and 4 deletions

View File

@ -775,9 +775,10 @@ func (a *Agent) listenHTTP() ([]apiServer, error) {
a.configReloaders = append(a.configReloaders, srv.ReloadConfig)
a.httpHandlers = srv
httpServer := &http.Server{
Addr: l.Addr().String(),
TLSConfig: tlscfg,
Handler: srv.handler(a.config.EnableDebug),
Addr: l.Addr().String(),
TLSConfig: tlscfg,
Handler: srv.handler(a.config.EnableDebug),
MaxHeaderBytes: a.config.HTTPMaxHeaderBytes,
}
// Load the connlimit helper into the server
@ -802,6 +803,7 @@ func (a *Agent) listenHTTP() ([]apiServer, error) {
}
return fmt.Errorf("%s server %s failed: %w", proto, l.Addr(), err)
},
MaxHeaderBytes: a.config.HTTPMaxHeaderBytes,
})
}
return nil

View File

@ -239,6 +239,63 @@ func TestAgent_ReconnectConfigSettings(t *testing.T) {
}()
}
func TestAgent_HTTPMaxHeaderBytes(t *testing.T) {
tests := []struct {
name string
maxHeaderBytes int
expectError bool
expectedHTTPResponse int
}{
{
"max header bytes 1 returns 431 http response when too large headers are sent",
1,
false,
431,
},
{
"max header bytes 0 returns 200 http response, as the http.DefaultMaxHeaderBytes size of 1MB is used",
0,
false,
200,
},
{
"negative maxHeaderBytes returns 200 http response, as the http.DefaultMaxHeaderBytes size of 1MB is used",
-10,
false,
200,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := NewTestAgent(t, fmt.Sprintf(`
http_config {
max_header_bytes = %d
}
`, tt.maxHeaderBytes))
defer a.Shutdown()
require.Equal(t, tt.maxHeaderBytes, a.Agent.config.HTTPMaxHeaderBytes)
req, err := http.NewRequest(http.MethodGet, "http://"+a.HTTPAddr()+"/v1/health/state/passing", nil)
require.NoError(t, err, "unexpected error creating new http request")
// This is directly pulled from the testing of request limits in the net/http source
// https://github.com/golang/go/blob/go1.15.3/src/net/http/serve_test.go#L2897-L2900
var bytesPerHeader = len("header12345: val12345\r\n")
t.Logf("bytesPerHeader: %d", bytesPerHeader)
for i := 0; i < ((tt.maxHeaderBytes+4096)/bytesPerHeader)+1; i++ {
req.Header.Set(fmt.Sprintf("header%05d", i), fmt.Sprintf("val%05d", i))
}
var res *http.Response
res, err = http.DefaultClient.Do(req)
require.True(t, (tt.expectError && (err != nil)) || !tt.expectError && (err == nil))
require.Equal(t, tt.expectedHTTPResponse, res.StatusCode, "expected a '%d' http response, got '%d'", tt.expectedHTTPResponse, res.StatusCode)
})
}
}
func TestAgent_ReconnectConfigWanDisabled(t *testing.T) {
t.Parallel()

View File

@ -37,6 +37,8 @@ type apiServer struct {
Run func() error
// Shutdown function used to stop the server
Shutdown func(context.Context) error
MaxHeaderBytes int
}
// NewAPIServers returns an empty apiServers that is ready to Start servers.

View File

@ -918,6 +918,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
HTTPAddrs: httpAddrs,
HTTPSAddrs: httpsAddrs,
HTTPBlockEndpoints: c.HTTPConfig.BlockEndpoints,
HTTPMaxHeaderBytes: b.intVal(c.HTTPConfig.MaxHeaderBytes),
HTTPResponseHeaders: c.HTTPConfig.ResponseHeaders,
AllowWriteHTTPFrom: b.cidrsVal("allow_write_http_from", c.HTTPConfig.AllowWriteHTTPFrom),
HTTPUseCache: b.boolValWithDefault(c.HTTPConfig.UseCache, true),

View File

@ -613,6 +613,7 @@ type HTTPConfig struct {
AllowWriteHTTPFrom []string `json:"allow_write_http_from,omitempty" hcl:"allow_write_http_from" mapstructure:"allow_write_http_from"`
ResponseHeaders map[string]string `json:"response_headers,omitempty" hcl:"response_headers" mapstructure:"response_headers"`
UseCache *bool `json:"use_cache,omitempty" hcl:"use_cache" mapstructure:"use_cache"`
MaxHeaderBytes *int `json:"max_header_bytes,omitempty" hcl:"max_header_bytes" mapstructure:"max_header_bytes"`
}
type Performance struct {

View File

@ -75,6 +75,7 @@ func AddFlags(fs *flag.FlagSet, f *BuilderOpts) {
add(&f.Config.HTTPConfig.AllowWriteHTTPFrom, "allow-write-http-from", "Only allow write endpoint calls from given network. CIDR format, can be specified multiple times.")
add(&f.Config.EncryptKey, "encrypt", "Provides the gossip encryption key.")
add(&f.Config.Ports.GRPC, "grpc-port", "Sets the gRPC API port to listen on (currently needed for Envoy xDS only).")
add(&f.Config.HTTPConfig.MaxHeaderBytes, "http-max-header-bytes", "Sets the HTTP Server's Max Header Bytes.")
add(&f.Config.Ports.HTTP, "http-port", "Sets the HTTP API port to listen on.")
add(&f.Config.Ports.HTTPS, "https-port", "Sets the HTTPS API port to listen on.")
add(&f.Config.StartJoinAddrsLAN, "join", "Address of an agent to join at start time. Can be specified multiple times.")

View File

@ -779,6 +779,14 @@ type RuntimeConfig struct {
// hcl: limits{ http_max_conns_per_client = 200 }
HTTPMaxConnsPerClient int
// HTTPMaxHeaderBytes controls the maximum number of bytes the
// server will read parsing the request header's keys and
// values, including the request line. It does not limit the
// size of the request body.
//
// If zero, or negative, http.DefaultMaxHeaderBytes is used.
HTTPMaxHeaderBytes int
// HTTPSHandshakeTimeout is the time allowed for HTTPS client to complete the
// TLS handshake and send first bytes of the request.
//

View File

@ -509,6 +509,17 @@ func TestBuilder_BuildAndValidate_ConfigFlagsAndEdgecases(t *testing.T) {
rt.DataDir = dataDir
},
},
{
desc: "-http-max-header-bytes",
args: []string{
`-http-max-header-bytes=1`,
`-data-dir=` + dataDir,
},
patch: func(rt *RuntimeConfig) {
rt.HTTPMaxHeaderBytes = 1
rt.DataDir = dataDir
},
},
{
desc: "-join",
args: []string{
@ -5074,7 +5085,8 @@ func TestFullConfig(t *testing.T) {
"M6TKa9NP": "xjuxjOzQ",
"JRCrHZed": "rl0mTx81"
},
"use_cache": false
"use_cache": false,
"max_header_bytes": 10
},
"key_file": "IEkkwgIA",
"leave_on_terminate": true,
@ -5761,6 +5773,7 @@ func TestFullConfig(t *testing.T) {
"JRCrHZed" = "rl0mTx81"
}
use_cache = false
max_header_bytes = 10
}
key_file = "IEkkwgIA"
leave_on_terminate = true
@ -6538,6 +6551,7 @@ func TestFullConfig(t *testing.T) {
HTTPResponseHeaders: map[string]string{"M6TKa9NP": "xjuxjOzQ", "JRCrHZed": "rl0mTx81"},
HTTPSAddrs: []net.Addr{tcpAddr("95.17.17.19:15127")},
HTTPMaxConnsPerClient: 100,
HTTPMaxHeaderBytes: 10,
HTTPSHandshakeTimeout: 2391 * time.Millisecond,
HTTPSPort: 15127,
HTTPUseCache: false,