OP#792 initial commit after fork
This commit is contained in:
58
internal/http/check_port_linux.go
Normal file
58
internal/http/check_port_linux.go
Normal file
@@ -0,0 +1,58 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CheckPortAvailability - check if given host and port is already in use.
|
||||
// Note: The check method tries to listen on given port and closes it.
|
||||
// It is possible to have a disconnected client in this tiny window of time.
|
||||
func CheckPortAvailability(host, port string, opts TCPOptions) (err error) {
|
||||
lc := &net.ListenConfig{
|
||||
Control: func(network, address string, c syscall.RawConn) error {
|
||||
c.Control(func(fdPtr uintptr) {
|
||||
if opts.Interface != "" {
|
||||
// When interface is specified look for specifically port availability on
|
||||
// the specified interface if any.
|
||||
_ = syscall.SetsockoptString(int(fdPtr), syscall.SOL_SOCKET, syscall.SO_BINDTODEVICE, opts.Interface)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
l, err := lc.Listen(ctx, "tcp", net.JoinHostPort(host, port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// As we are able to listen on this network, the port is not in use.
|
||||
// Close the listener and continue check other networks.
|
||||
return l.Close()
|
||||
}
|
||||
46
internal/http/check_port_others.go
Normal file
46
internal/http/check_port_others.go
Normal file
@@ -0,0 +1,46 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CheckPortAvailability - check if given host and port is already in use.
|
||||
// Note: The check method tries to listen on given port and closes it.
|
||||
// It is possible to have a disconnected client in this tiny window of time.
|
||||
func CheckPortAvailability(host, port string, opts TCPOptions) (err error) {
|
||||
lc := &net.ListenConfig{}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
l, err := lc.Listen(ctx, "tcp", net.JoinHostPort(host, port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// As we are able to listen on this network, the port is not in use.
|
||||
// Close the listener and continue check other networks.
|
||||
return l.Close()
|
||||
}
|
||||
64
internal/http/check_port_test.go
Normal file
64
internal/http/check_port_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests for port availability logic written for server startup sequence.
|
||||
func TestCheckPortAvailability(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
l, err := net.Listen("tcp", "localhost:0") // ask kernel for a free port.
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
port := l.Addr().(*net.TCPAddr).Port
|
||||
|
||||
testCases := []struct {
|
||||
host string
|
||||
port int
|
||||
expectedErr error
|
||||
}{
|
||||
{"", port, fmt.Errorf("listen tcp :%v: bind: address already in use", port)},
|
||||
{"127.0.0.1", port, fmt.Errorf("listen tcp 127.0.0.1:%v: bind: address already in use", port)},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
err := CheckPortAvailability(testCase.host, strconv.Itoa(testCase.port), TCPOptions{})
|
||||
switch {
|
||||
case testCase.expectedErr == nil:
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
case err == nil:
|
||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
case testCase.expectedErr.Error() != err.Error():
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
44
internal/http/close.go
Normal file
44
internal/http/close.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
xioutil "github.com/minio/minio/internal/ioutil"
|
||||
)
|
||||
|
||||
// DrainBody close non nil response with any response Body.
|
||||
// convenient wrapper to drain any remaining data on response body.
|
||||
//
|
||||
// Subsequently this allows golang http RoundTripper
|
||||
// to reuse the same connection for future requests.
|
||||
func DrainBody(respBody io.ReadCloser) {
|
||||
// Callers should close resp.Body when done reading from it.
|
||||
// If resp.Body is not closed, the Client's underlying RoundTripper
|
||||
// (typically Transport) may not be able to reuse a persistent TCP
|
||||
// connection to the server for a subsequent "keep-alive" request.
|
||||
if respBody != nil {
|
||||
// Drain any remaining Body and then close the connection.
|
||||
// Without this closing connection would disallow re-using
|
||||
// the same connection for future uses.
|
||||
// - http://stackoverflow.com/a/17961593/4465767
|
||||
defer respBody.Close()
|
||||
xioutil.DiscardReader(respBody)
|
||||
}
|
||||
}
|
||||
80
internal/http/dial_dnscache.go
Normal file
80
internal/http/dial_dnscache.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LookupHost is a function to make custom lookupHost for optional cached DNS requests
|
||||
type LookupHost func(ctx context.Context, host string) (addrs []string, err error)
|
||||
|
||||
// DialContextWithLookupHost is a helper function which returns `net.DialContext` function.
|
||||
// It randomly fetches an IP via custom LookupHost function and dials it by the given dial
|
||||
// function. LookupHost may implement an internal DNS caching implementation, lookupHost
|
||||
// input if nil then net.DefaultResolver.LookupHost is used.
|
||||
//
|
||||
// It dials one by one and returns first connected `net.Conn`.
|
||||
// If it fails to dial all IPs from cache it returns first error. If no baseDialFunc
|
||||
// is given, it sets default dial function.
|
||||
//
|
||||
// You can use returned dial function for `http.Transport.DialContext`.
|
||||
//
|
||||
// In this function, it uses functions from `rand` package. To make it really random,
|
||||
// you MUST call `rand.Seed` and change the value from the default in your application
|
||||
func DialContextWithLookupHost(lookupHost LookupHost, baseDialCtx DialContext) DialContext {
|
||||
if lookupHost == nil {
|
||||
lookupHost = net.DefaultResolver.LookupHost
|
||||
}
|
||||
|
||||
if baseDialCtx == nil {
|
||||
// This is same as which `http.DefaultTransport` uses.
|
||||
baseDialCtx = (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext
|
||||
}
|
||||
|
||||
return func(ctx context.Context, network, addr string) (conn net.Conn, err error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if net.ParseIP(host) != nil {
|
||||
// For IP only setups there is no need for DNS lookups.
|
||||
return baseDialCtx(ctx, "tcp", addr)
|
||||
}
|
||||
|
||||
ips, err := lookupHost(ctx, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
conn, err = baseDialCtx(ctx, "tcp", net.JoinHostPort(ip, port))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return conn, err
|
||||
}
|
||||
}
|
||||
134
internal/http/dial_linux.go
Normal file
134
internal/http/dial_linux.go
Normal file
@@ -0,0 +1,134 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/deadlineconn"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func setTCPParametersFn(opts TCPOptions) func(network, address string, c syscall.RawConn) error {
|
||||
return func(network, address string, c syscall.RawConn) error {
|
||||
c.Control(func(fdPtr uintptr) {
|
||||
// got socket file descriptor to set parameters.
|
||||
fd := int(fdPtr)
|
||||
|
||||
_ = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
|
||||
|
||||
_ = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
|
||||
|
||||
// Enable custom socket send/recv buffers.
|
||||
if opts.SendBufSize > 0 {
|
||||
_ = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_SNDBUF, opts.SendBufSize)
|
||||
}
|
||||
|
||||
if opts.RecvBufSize > 0 {
|
||||
_ = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF, opts.RecvBufSize)
|
||||
}
|
||||
|
||||
if opts.NoDelay {
|
||||
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, unix.TCP_NODELAY, 1)
|
||||
_ = syscall.SetsockoptInt(fd, syscall.SOL_TCP, unix.TCP_CORK, 0)
|
||||
}
|
||||
|
||||
// Enable TCP open
|
||||
// https://lwn.net/Articles/508865/ - 32k queue size.
|
||||
_ = syscall.SetsockoptInt(fd, syscall.SOL_TCP, unix.TCP_FASTOPEN, 32*1024)
|
||||
|
||||
// Enable TCP fast connect
|
||||
// TCPFastOpenConnect sets the underlying socket to use
|
||||
// the TCP fast open connect. This feature is supported
|
||||
// since Linux 4.11.
|
||||
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, unix.TCP_FASTOPEN_CONNECT, 1)
|
||||
|
||||
// Enable TCP quick ACK, John Nagle says
|
||||
// "Set TCP_QUICKACK. If you find a case where that makes things worse, let me know."
|
||||
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, unix.TCP_QUICKACK, 1)
|
||||
|
||||
/// Enable keep-alive
|
||||
{
|
||||
_ = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_KEEPALIVE, 1)
|
||||
|
||||
// The time (in seconds) the connection needs to remain idle before
|
||||
// TCP starts sending keepalive probes
|
||||
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPIDLE, 15)
|
||||
|
||||
// Number of probes.
|
||||
// ~ cat /proc/sys/net/ipv4/tcp_keepalive_probes (defaults to 9, we reduce it to 5)
|
||||
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPCNT, 5)
|
||||
|
||||
// Wait time after successful probe in seconds.
|
||||
// ~ cat /proc/sys/net/ipv4/tcp_keepalive_intvl (defaults to 75 secs, we reduce it to 15 secs)
|
||||
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, 15)
|
||||
}
|
||||
|
||||
// Set tcp user timeout in addition to the keep-alive - tcp-keepalive is not enough to close a socket
|
||||
// with dead end because tcp-keepalive is not fired when there is data in the socket buffer.
|
||||
// https://blog.cloudflare.com/when-tcp-sockets-refuse-to-die/
|
||||
// This is a sensitive configuration, it is better to set it to high values, > 60 secs since it can
|
||||
// affect clients reading data with a very slow pace (disappropriate with socket buffer sizes)
|
||||
if opts.UserTimeout > 0 {
|
||||
_ = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, unix.TCP_USER_TIMEOUT, opts.UserTimeout)
|
||||
}
|
||||
|
||||
if opts.Interface != "" {
|
||||
if h, _, err := net.SplitHostPort(address); err == nil {
|
||||
address = h
|
||||
}
|
||||
// Create socket on specific vrf device.
|
||||
// To catch all kinds of special cases this filters specifically for loopback networks.
|
||||
if ip := net.ParseIP(address); ip != nil && !ip.IsLoopback() {
|
||||
_ = syscall.SetsockoptString(fd, syscall.SOL_SOCKET, syscall.SO_BINDTODEVICE, opts.Interface)
|
||||
}
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DialContext is a function to make custom Dial for internode communications
|
||||
type DialContext func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
|
||||
// NewInternodeDialContext setups a custom dialer for internode communication
|
||||
func NewInternodeDialContext(dialTimeout time.Duration, opts TCPOptions) DialContext {
|
||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: dialTimeout,
|
||||
Control: setTCPParametersFn(opts),
|
||||
}
|
||||
conn, err := dialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts.DriveOPTimeout != nil {
|
||||
// Read deadlines are sufficient for now as per various
|
||||
// scenarios of hung node detection, we may add Write deadlines
|
||||
// if needed later on.
|
||||
return deadlineconn.New(conn).WithReadDeadline(opts.DriveOPTimeout()), nil
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
50
internal/http/dial_others.go
Normal file
50
internal/http/dial_others.go
Normal file
@@ -0,0 +1,50 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO: if possible implement for non-linux platforms, not a priority at the moment
|
||||
//
|
||||
//nolint:unused
|
||||
func setTCPParametersFn(opts TCPOptions) func(network, address string, c syscall.RawConn) error {
|
||||
return func(network, address string, c syscall.RawConn) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DialContext is a function to make custom Dial for internode communications
|
||||
type DialContext func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
|
||||
// NewInternodeDialContext configures a custom dialer for internode communications
|
||||
func NewInternodeDialContext(dialTimeout time.Duration, _ TCPOptions) DialContext {
|
||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: dialTimeout,
|
||||
}
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
}
|
||||
27
internal/http/flush.go
Normal file
27
internal/http/flush.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2015-2025 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Flush the ResponseWriter.
|
||||
func Flush(w http.ResponseWriter) {
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
275
internal/http/headers.go
Normal file
275
internal/http/headers.go
Normal file
@@ -0,0 +1,275 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
// Standard S3 HTTP response constants
|
||||
const (
|
||||
LastModified = "Last-Modified"
|
||||
Date = "Date"
|
||||
ETag = "ETag"
|
||||
ContentType = "Content-Type"
|
||||
ContentMD5 = "Content-Md5"
|
||||
ContentEncoding = "Content-Encoding"
|
||||
Expires = "Expires"
|
||||
ContentLength = "Content-Length"
|
||||
ContentLanguage = "Content-Language"
|
||||
ContentRange = "Content-Range"
|
||||
Connection = "Connection"
|
||||
AcceptRanges = "Accept-Ranges"
|
||||
AmzBucketRegion = "X-Amz-Bucket-Region"
|
||||
ServerInfo = "Server"
|
||||
RetryAfter = "Retry-After"
|
||||
Location = "Location"
|
||||
CacheControl = "Cache-Control"
|
||||
ContentDisposition = "Content-Disposition"
|
||||
Authorization = "Authorization"
|
||||
Action = "Action"
|
||||
Range = "Range"
|
||||
)
|
||||
|
||||
// Non standard S3 HTTP response constants
|
||||
const (
|
||||
XCache = "X-Cache"
|
||||
XCacheLookup = "X-Cache-Lookup"
|
||||
)
|
||||
|
||||
// Standard S3 HTTP request constants
|
||||
const (
|
||||
IfModifiedSince = "If-Modified-Since"
|
||||
IfUnmodifiedSince = "If-Unmodified-Since"
|
||||
IfMatch = "If-Match"
|
||||
IfNoneMatch = "If-None-Match"
|
||||
|
||||
// Request tags used in GetObjectAttributes
|
||||
Checksum = "Checksum"
|
||||
StorageClass = "StorageClass"
|
||||
ObjectSize = "ObjectSize"
|
||||
ObjectParts = "ObjectParts"
|
||||
|
||||
// S3 storage class
|
||||
AmzStorageClass = "x-amz-storage-class"
|
||||
|
||||
// S3 object version ID
|
||||
AmzVersionID = "x-amz-version-id"
|
||||
AmzDeleteMarker = "x-amz-delete-marker"
|
||||
|
||||
// S3 object tagging
|
||||
AmzObjectTagging = "X-Amz-Tagging"
|
||||
AmzTagCount = "x-amz-tagging-count"
|
||||
AmzTagDirective = "X-Amz-Tagging-Directive"
|
||||
|
||||
// S3 transition restore
|
||||
AmzRestore = "x-amz-restore"
|
||||
AmzRestoreExpiryDays = "X-Amz-Restore-Expiry-Days"
|
||||
AmzRestoreRequestDate = "X-Amz-Restore-Request-Date"
|
||||
AmzRestoreOutputPath = "x-amz-restore-output-path"
|
||||
|
||||
// S3 extensions
|
||||
AmzCopySourceIfModifiedSince = "x-amz-copy-source-if-modified-since"
|
||||
AmzCopySourceIfUnmodifiedSince = "x-amz-copy-source-if-unmodified-since"
|
||||
|
||||
AmzCopySourceIfNoneMatch = "x-amz-copy-source-if-none-match"
|
||||
AmzCopySourceIfMatch = "x-amz-copy-source-if-match"
|
||||
|
||||
AmzCopySource = "X-Amz-Copy-Source"
|
||||
AmzCopySourceVersionID = "X-Amz-Copy-Source-Version-Id"
|
||||
AmzCopySourceRange = "X-Amz-Copy-Source-Range"
|
||||
AmzMetadataDirective = "X-Amz-Metadata-Directive"
|
||||
AmzObjectLockMode = "X-Amz-Object-Lock-Mode"
|
||||
AmzObjectLockRetainUntilDate = "X-Amz-Object-Lock-Retain-Until-Date"
|
||||
AmzObjectLockLegalHold = "X-Amz-Object-Lock-Legal-Hold"
|
||||
AmzObjectLockBypassGovernance = "X-Amz-Bypass-Governance-Retention"
|
||||
AmzBucketReplicationStatus = "X-Amz-Replication-Status"
|
||||
|
||||
// AmzSnowballExtract will trigger unpacking of an archive content
|
||||
AmzSnowballExtract = "X-Amz-Meta-Snowball-Auto-Extract"
|
||||
// MinIOSnowballIgnoreDirs will skip creating empty directory objects.
|
||||
MinIOSnowballIgnoreDirs = "X-Amz-Meta-Minio-Snowball-Ignore-Dirs"
|
||||
// MinIOSnowballIgnoreErrors will ignore recoverable errors, typically single files failing to upload.
|
||||
// An error will be printed to console instead.
|
||||
MinIOSnowballIgnoreErrors = "X-Amz-Meta-Minio-Snowball-Ignore-Errors"
|
||||
// MinIOSnowballPrefix will apply this prefix (plus / at end) to all extracted objects
|
||||
MinIOSnowballPrefix = "X-Amz-Meta-Minio-Snowball-Prefix"
|
||||
|
||||
// Object lock enabled
|
||||
AmzObjectLockEnabled = "x-amz-bucket-object-lock-enabled"
|
||||
|
||||
// Multipart parts count
|
||||
AmzMpPartsCount = "x-amz-mp-parts-count"
|
||||
|
||||
// Object date/time of expiration
|
||||
AmzExpiration = "x-amz-expiration"
|
||||
|
||||
// Dummy putBucketACL
|
||||
AmzACL = "x-amz-acl"
|
||||
|
||||
// Signature V4 related constants.
|
||||
AmzContentSha256 = "X-Amz-Content-Sha256"
|
||||
AmzDate = "X-Amz-Date"
|
||||
AmzAlgorithm = "X-Amz-Algorithm"
|
||||
AmzExpires = "X-Amz-Expires"
|
||||
AmzSignedHeaders = "X-Amz-SignedHeaders"
|
||||
AmzSignature = "X-Amz-Signature"
|
||||
AmzCredential = "X-Amz-Credential"
|
||||
AmzSecurityToken = "X-Amz-Security-Token"
|
||||
AmzDecodedContentLength = "X-Amz-Decoded-Content-Length"
|
||||
AmzTrailer = "X-Amz-Trailer"
|
||||
AmzMaxParts = "X-Amz-Max-Parts"
|
||||
AmzPartNumberMarker = "X-Amz-Part-Number-Marker"
|
||||
|
||||
// Constants used for GetObjectAttributes and GetObjectVersionAttributes
|
||||
AmzObjectAttributes = "X-Amz-Object-Attributes"
|
||||
|
||||
AmzMetaUnencryptedContentLength = "X-Amz-Meta-X-Amz-Unencrypted-Content-Length"
|
||||
AmzMetaUnencryptedContentMD5 = "X-Amz-Meta-X-Amz-Unencrypted-Content-Md5"
|
||||
|
||||
// AWS server-side encryption headers for SSE-S3, SSE-KMS and SSE-C.
|
||||
AmzServerSideEncryption = "X-Amz-Server-Side-Encryption"
|
||||
AmzServerSideEncryptionKmsID = AmzServerSideEncryption + "-Aws-Kms-Key-Id"
|
||||
AmzServerSideEncryptionKmsContext = AmzServerSideEncryption + "-Context"
|
||||
AmzServerSideEncryptionCustomerAlgorithm = AmzServerSideEncryption + "-Customer-Algorithm"
|
||||
AmzServerSideEncryptionCustomerKey = AmzServerSideEncryption + "-Customer-Key"
|
||||
AmzServerSideEncryptionCustomerKeyMD5 = AmzServerSideEncryption + "-Customer-Key-Md5"
|
||||
AmzServerSideEncryptionCopyCustomerAlgorithm = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm"
|
||||
AmzServerSideEncryptionCopyCustomerKey = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key"
|
||||
AmzServerSideEncryptionCopyCustomerKeyMD5 = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5"
|
||||
|
||||
AmzEncryptionAES = "AES256"
|
||||
AmzEncryptionKMS = "aws:kms"
|
||||
|
||||
// Signature v2 related constants
|
||||
AmzSignatureV2 = "Signature"
|
||||
AmzAccessKeyID = "AWSAccessKeyId"
|
||||
|
||||
// Response request id.
|
||||
AmzRequestID = "x-amz-request-id"
|
||||
AmzRequestHostID = "x-amz-id-2"
|
||||
|
||||
// Deployment id.
|
||||
MinioDeploymentID = "x-minio-deployment-id"
|
||||
|
||||
// Peer call
|
||||
MinIOPeerCall = "x-minio-from-peer"
|
||||
|
||||
// Server-Status
|
||||
MinIOServerStatus = "x-minio-server-status"
|
||||
|
||||
// Content Checksums
|
||||
AmzChecksumAlgo = "x-amz-checksum-algorithm"
|
||||
AmzChecksumCRC32 = "x-amz-checksum-crc32"
|
||||
AmzChecksumCRC32C = "x-amz-checksum-crc32c"
|
||||
AmzChecksumSHA1 = "x-amz-checksum-sha1"
|
||||
AmzChecksumSHA256 = "x-amz-checksum-sha256"
|
||||
AmzChecksumCRC64NVME = "x-amz-checksum-crc64nvme"
|
||||
AmzChecksumMode = "x-amz-checksum-mode"
|
||||
AmzChecksumType = "x-amz-checksum-type"
|
||||
AmzChecksumTypeFullObject = "FULL_OBJECT"
|
||||
AmzChecksumTypeComposite = "COMPOSITE"
|
||||
|
||||
// S3 Express API related constant reject it.
|
||||
AmzWriteOffsetBytes = "x-amz-write-offset-bytes"
|
||||
|
||||
// Post Policy related
|
||||
AmzMetaUUID = "X-Amz-Meta-Uuid"
|
||||
AmzMetaName = "X-Amz-Meta-Name"
|
||||
|
||||
// Delete special flag to force delete a bucket or a prefix
|
||||
MinIOForceDelete = "x-minio-force-delete"
|
||||
|
||||
// Create special flag to force create a bucket
|
||||
MinIOForceCreate = "x-minio-force-create"
|
||||
|
||||
// Header indicates if the mtime should be preserved by client
|
||||
MinIOSourceMTime = "x-minio-source-mtime"
|
||||
|
||||
// Header indicates if the etag should be preserved by client
|
||||
MinIOSourceETag = "x-minio-source-etag"
|
||||
|
||||
// Writes expected write quorum
|
||||
MinIOWriteQuorum = "x-minio-write-quorum"
|
||||
|
||||
// Reads expected read quorum
|
||||
MinIOReadQuorum = "x-minio-read-quorum"
|
||||
|
||||
// Indicates if we are using default storage class and there was problem loading config
|
||||
// if this header is set to "true"
|
||||
MinIOStorageClassDefaults = "x-minio-storage-class-defaults"
|
||||
|
||||
// Reports number of drives currently healing
|
||||
MinIOHealingDrives = "x-minio-healing-drives"
|
||||
|
||||
// Header indicates if the delete marker should be preserved by client
|
||||
MinIOSourceDeleteMarker = "x-minio-source-deletemarker"
|
||||
|
||||
// Header indicates if the delete marker version needs to be purged.
|
||||
MinIOSourceDeleteMarkerDelete = "x-minio-source-deletemarker-delete"
|
||||
|
||||
// Header indicates permanent delete replication status.
|
||||
MinIODeleteReplicationStatus = "X-Minio-Replication-Delete-Status"
|
||||
// Header indicates delete-marker replication status.
|
||||
MinIODeleteMarkerReplicationStatus = "X-Minio-Replication-DeleteMarker-Status"
|
||||
// Header indicates if its a GET/HEAD proxy request for active-active replication
|
||||
MinIOSourceProxyRequest = "X-Minio-Source-Proxy-Request"
|
||||
// Header indicates that this request is a replication request to create a REPLICA
|
||||
MinIOSourceReplicationRequest = "X-Minio-Source-Replication-Request"
|
||||
// Header checks replication permissions without actually completing replication
|
||||
MinIOSourceReplicationCheck = "X-Minio-Source-Replication-Check"
|
||||
// Header indicates replication reset status.
|
||||
MinIOReplicationResetStatus = "X-Minio-Replication-Reset-Status"
|
||||
// Header indicating target cluster can receive delete marker replication requests because object has been replicated
|
||||
MinIOTargetReplicationReady = "X-Minio-Replication-Ready"
|
||||
// Header asking if cluster can receive delete marker replication request now.
|
||||
MinIOCheckDMReplicationReady = "X-Minio-Check-Replication-Ready"
|
||||
// Header indiicates last tag update time on source
|
||||
MinIOSourceTaggingTimestamp = "X-Minio-Source-Replication-Tagging-Timestamp"
|
||||
// Header indiicates last rtention update time on source
|
||||
MinIOSourceObjectRetentionTimestamp = "X-Minio-Source-Replication-Retention-Timestamp"
|
||||
// Header indiicates last rtention update time on source
|
||||
MinIOSourceObjectLegalHoldTimestamp = "X-Minio-Source-Replication-LegalHold-Timestamp"
|
||||
// Header indicates a Tag operation was performed on one/more peers successfully, though the
|
||||
// current cluster does not have the object yet. This is in a site/bucket replication scenario.
|
||||
MinIOTaggingProxied = "X-Minio-Tagging-Proxied"
|
||||
// Header indicates the actual replicated object size
|
||||
// In case of SSEC objects getting replicated (multipart) actual size would be needed at target
|
||||
MinIOReplicationActualObjectSize = "X-Minio-Replication-Actual-Object-Size"
|
||||
|
||||
// predicted date/time of transition
|
||||
MinIOTransition = "X-Minio-Transition"
|
||||
MinIOLifecycleCfgUpdatedAt = "X-Minio-LifecycleConfig-UpdatedAt"
|
||||
// MinIOCompressed is returned when object is compressed
|
||||
MinIOCompressed = "X-Minio-Compressed"
|
||||
|
||||
// SUBNET related
|
||||
SubnetAPIKey = "x-subnet-api-key"
|
||||
)
|
||||
|
||||
// Common http query params S3 API
|
||||
const (
|
||||
VersionID = "versionId"
|
||||
|
||||
PartNumber = "partNumber"
|
||||
|
||||
UploadID = "uploadId"
|
||||
)
|
||||
|
||||
// http headers sent to webhook targets
|
||||
const (
|
||||
// Reports the version of MinIO server
|
||||
MinIOVersion = "x-minio-version"
|
||||
WebhookEventPayloadCount = "x-minio-webhook-payload-count"
|
||||
)
|
||||
57
internal/http/lambda-headers.go
Normal file
57
internal/http/lambda-headers.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
// Object Lambda headers
|
||||
const (
|
||||
AmzRequestRoute = "x-amz-request-route"
|
||||
AmzRequestToken = "x-amz-request-token"
|
||||
|
||||
AmzFwdStatus = "x-amz-fwd-status"
|
||||
AmzFwdErrorCode = "x-amz-fwd-error-code"
|
||||
AmzFwdErrorMessage = "x-amz-fwd-error-message"
|
||||
AmzFwdHeaderAcceptRanges = "x-amz-fwd-header-accept-ranges"
|
||||
AmzFwdHeaderCacheControl = "x-amz-fwd-header-Cache-Control"
|
||||
AmzFwdHeaderContentDisposition = "x-amz-fwd-header-Content-Disposition"
|
||||
AmzFwdHeaderContentEncoding = "x-amz-fwd-header-Content-Encoding"
|
||||
AmzFwdHeaderContentLanguage = "x-amz-fwd-header-Content-Language"
|
||||
AmzFwdHeaderContentRange = "x-amz-fwd-header-Content-Range"
|
||||
AmzFwdHeaderContentType = "x-amz-fwd-header-Content-Type"
|
||||
AmzFwdHeaderChecksumCrc32 = "x-amz-fwd-header-x-amz-checksum-crc32"
|
||||
AmzFwdHeaderChecksumCrc32c = "x-amz-fwd-header-x-amz-checksum-crc32c"
|
||||
AmzFwdHeaderChecksumSha1 = "x-amz-fwd-header-x-amz-checksum-sha1"
|
||||
AmzFwdHeaderChecksumSha256 = "x-amz-fwd-header-x-amz-checksum-sha256"
|
||||
AmzFwdHeaderDeleteMarker = "x-amz-fwd-header-x-amz-delete-marker"
|
||||
AmzFwdHeaderETag = "x-amz-fwd-header-ETag"
|
||||
AmzFwdHeaderExpires = "x-amz-fwd-header-Expires"
|
||||
AmzFwdHeaderExpiration = "x-amz-fwd-header-x-amz-expiration"
|
||||
AmzFwdHeaderLastModified = "x-amz-fwd-header-Last-Modified"
|
||||
|
||||
AmzFwdHeaderObjectLockMode = "x-amz-fwd-header-x-amz-object-lock-mode"
|
||||
AmzFwdHeaderObjectLockLegalHold = "x-amz-fwd-header-x-amz-object-lock-legal-hold"
|
||||
AmzFwdHeaderObjectLockRetainUntil = "x-amz-fwd-header-x-amz-object-lock-retain-until-date"
|
||||
AmzFwdHeaderMPPartsCount = "x-amz-fwd-header-x-amz-mp-parts-count"
|
||||
AmzFwdHeaderReplicationStatus = "x-amz-fwd-header-x-amz-replication-status"
|
||||
AmzFwdHeaderSSE = "x-amz-fwd-header-x-amz-server-side-encryption"
|
||||
AmzFwdHeaderSSEC = "x-amz-fwd-header-x-amz-server-side-encryption-customer-algorithm"
|
||||
AmzFwdHeaderSSEKMSID = "x-amz-fwd-header-x-amz-server-side-encryption-aws-kms-key-id"
|
||||
AmzFwdHeaderSSECMD5 = "x-amz-fwd-header-x-amz-server-side-encryption-customer-key-MD5"
|
||||
AmzFwdHeaderStorageClass = "x-amz-fwd-header-x-amz-storage-class"
|
||||
AmzFwdHeaderTaggingCount = "x-amz-fwd-header-x-amz-tagging-count"
|
||||
AmzFwdHeaderVersionID = "x-amz-fwd-header-x-amz-version-id"
|
||||
)
|
||||
191
internal/http/listener.go
Normal file
191
internal/http/listener.go
Normal file
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/deadlineconn"
|
||||
)
|
||||
|
||||
type acceptResult struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
lidx int
|
||||
}
|
||||
|
||||
// httpListener - HTTP listener capable of handling multiple server addresses.
|
||||
type httpListener struct {
|
||||
opts TCPOptions
|
||||
listeners []net.Listener // underlying TCP listeners.
|
||||
acceptCh chan acceptResult // channel where all TCP listeners write accepted connection.
|
||||
ctxDoneCh <-chan struct{}
|
||||
ctxCanceler context.CancelFunc
|
||||
}
|
||||
|
||||
// start - starts separate goroutine for each TCP listener. A valid new connection is passed to httpListener.acceptCh.
|
||||
func (listener *httpListener) start() {
|
||||
// Closure to handle listener until httpListener.ctxDoneCh channel is closed.
|
||||
handleListener := func(idx int, ln net.Listener) {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
select {
|
||||
case listener.acceptCh <- acceptResult{conn, err, idx}:
|
||||
case <-listener.ctxDoneCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start separate goroutine for each listener to handle connection.
|
||||
for idx, ln := range listener.listeners {
|
||||
go handleListener(idx, ln)
|
||||
}
|
||||
}
|
||||
|
||||
// Accept - reads from httpListener.acceptCh for one of previously accepted TCP connection and returns the same.
|
||||
func (listener *httpListener) Accept() (conn net.Conn, err error) {
|
||||
select {
|
||||
case result := <-listener.acceptCh:
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return deadlineconn.New(result.conn).WithReadDeadline(listener.opts.IdleTimeout).WithWriteDeadline(listener.opts.IdleTimeout), result.err
|
||||
case <-listener.ctxDoneCh:
|
||||
}
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
|
||||
// Close - closes underneath all TCP listeners.
|
||||
func (listener *httpListener) Close() (err error) {
|
||||
listener.ctxCanceler()
|
||||
|
||||
for i := range listener.listeners {
|
||||
listener.listeners[i].Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Addr - net.Listener interface compatible method returns net.Addr. In case of multiple TCP listeners, it returns '0.0.0.0' as IP address.
|
||||
func (listener *httpListener) Addr() (addr net.Addr) {
|
||||
addr = listener.listeners[0].Addr()
|
||||
if len(listener.listeners) == 1 {
|
||||
return addr
|
||||
}
|
||||
|
||||
if tcpAddr, ok := addr.(*net.TCPAddr); ok {
|
||||
return &net.TCPAddr{
|
||||
IP: net.IPv4zero,
|
||||
Port: tcpAddr.Port,
|
||||
Zone: tcpAddr.Zone,
|
||||
}
|
||||
}
|
||||
panic("unknown address type on listener")
|
||||
}
|
||||
|
||||
// Addrs - returns all address information of TCP listeners.
|
||||
func (listener *httpListener) Addrs() (addrs []net.Addr) {
|
||||
addrs = make([]net.Addr, 0, len(listener.listeners))
|
||||
for i := range listener.listeners {
|
||||
addrs = append(addrs, listener.listeners[i].Addr())
|
||||
}
|
||||
|
||||
return addrs
|
||||
}
|
||||
|
||||
// TCPOptions specify customizable TCP optimizations on raw socket
|
||||
type TCPOptions struct {
|
||||
UserTimeout int // this value is expected to be in milliseconds
|
||||
|
||||
// When the net.Conn is a remote drive this value is honored, we close the connection to remote peer proactively.
|
||||
DriveOPTimeout func() time.Duration
|
||||
|
||||
SendBufSize int // SO_SNDBUF size for the socket connection, NOTE: this sets server and client connection
|
||||
RecvBufSize int // SO_RECVBUF size for the socket connection, NOTE: this sets server and client connection
|
||||
NoDelay bool // Indicates callers to enable TCP_NODELAY on the net.Conn
|
||||
Interface string // This is a VRF device passed via `--interface` flag
|
||||
Trace func(msg string) // Trace when starting.
|
||||
IdleTimeout time.Duration // Incoming TCP read/write timeout
|
||||
}
|
||||
|
||||
// ForWebsocket returns TCPOptions valid for websocket net.Conn
|
||||
func (t TCPOptions) ForWebsocket() TCPOptions {
|
||||
return TCPOptions{
|
||||
UserTimeout: t.UserTimeout,
|
||||
Interface: t.Interface,
|
||||
SendBufSize: t.SendBufSize,
|
||||
RecvBufSize: t.RecvBufSize,
|
||||
NoDelay: true,
|
||||
}
|
||||
}
|
||||
|
||||
// newHTTPListener - creates new httpListener object which is interface compatible to net.Listener.
|
||||
// httpListener is capable to
|
||||
// * listen to multiple addresses
|
||||
// * controls incoming connections only doing HTTP protocol
|
||||
func newHTTPListener(ctx context.Context, serverAddrs []string, opts TCPOptions) (listener *httpListener, listenErrs []error) {
|
||||
listeners := make([]net.Listener, 0, len(serverAddrs))
|
||||
listenErrs = make([]error, len(serverAddrs))
|
||||
|
||||
if opts.Trace == nil {
|
||||
opts.Trace = func(msg string) {} // Noop if not defined.
|
||||
}
|
||||
|
||||
// Unix listener with special TCP options.
|
||||
listenCfg := net.ListenConfig{
|
||||
Control: setTCPParametersFn(opts),
|
||||
}
|
||||
|
||||
for i, serverAddr := range serverAddrs {
|
||||
l, e := listenCfg.Listen(ctx, "tcp", serverAddr)
|
||||
if e != nil {
|
||||
opts.Trace("listenCfg.Listen: " + e.Error())
|
||||
|
||||
listenErrs[i] = e
|
||||
continue
|
||||
}
|
||||
opts.Trace("adding listener to " + l.Addr().String())
|
||||
|
||||
listeners = append(listeners, l)
|
||||
}
|
||||
|
||||
if len(listeners) == 0 {
|
||||
// No listeners initialized, no need to continue
|
||||
return listener, listenErrs
|
||||
}
|
||||
listeners = slices.Clip(listeners)
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
listener = &httpListener{
|
||||
listeners: listeners,
|
||||
acceptCh: make(chan acceptResult, len(listeners)),
|
||||
opts: opts,
|
||||
ctxDoneCh: ctx.Done(),
|
||||
ctxCanceler: cancel,
|
||||
}
|
||||
opts.Trace(fmt.Sprintf("opening %d listeners", len(listener.listeners)))
|
||||
listener.start()
|
||||
|
||||
return listener, listenErrs
|
||||
}
|
||||
328
internal/http/listener_test.go
Normal file
328
internal/http/listener_test.go
Normal file
@@ -0,0 +1,328 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
)
|
||||
|
||||
var serverPort uint32 = 60000
|
||||
|
||||
var getCert = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
certificate, err := getTLSCert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &certificate, nil
|
||||
}
|
||||
|
||||
func getTLSCert() (tls.Certificate, error) {
|
||||
keyPEMBlock := []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEApEkbPrT6wzcWK1W5atQiGptvuBsRdf8MCg4u6SN10QbslA5k
|
||||
6BYRdZfFeRpwAwYyzkumug6+eBJatDZEd7+0FF86yxB7eMTSiHKRZ5Mi5ZyCFsez
|
||||
dndknGBeK6I80s1jd5ZsLLuMKErvbNwSbfX+X6d2mBeYW8Scv9N+qYnNrHHHohvX
|
||||
oxy1gZ18EhhogQhrD22zaqg/jtmOT8ImUiXzB1mKInt2LlSkoRYuBzepkDJrsE1L
|
||||
/cyYZbtcO/ASDj+/qQAuQ66v9pNyJkIQ7bDOUyxaT5Hx9XvbqI1OqUVAdGLLi+eZ
|
||||
IFguFyYd0lemwdN/IDvxftzegTO3cO0D28d1UQIDAQABAoIBAB42x8j3lerTNcOQ
|
||||
h4JLM157WcedSs/NsVQkGaKM//0KbfYo04wPivR6jjngj9suh6eDKE2tqoAAuCfO
|
||||
lzcCzca1YOW5yUuDv0iS8YT//XoHF7HC1pGiEaHk40zZEKCgX3u98XUkpPlAFtqJ
|
||||
euY4SKkk7l24cS/ncACjj/b0PhxJoT/CncuaaJKqmCc+vdL4wj1UcrSNPZqRjDR/
|
||||
sh5DO0LblB0XrqVjpNxqxM60/IkbftB8YTnyGgtO2tbTPr8KdQ8DhHQniOp+WEPV
|
||||
u/iXt0LLM7u62LzadkGab2NDWS3agnmdvw2ADtv5Tt8fZ7WnPqiOpNyD5Bv1a3/h
|
||||
YBw5HsUCgYEA0Sfv6BiSAFEby2KusRoq5UeUjp/SfL7vwpO1KvXeiYkPBh2XYVq2
|
||||
azMnOw7Rz5ixFhtUtto2XhYdyvvr3dZu1fNHtxWo9ITBivqTGGRNwfiaQa58Bugo
|
||||
gy7vCdIE/f6xE5LYIovBnES2vs/ZayMyhTX84SCWd0pTY0kdDA8ePGsCgYEAyRSA
|
||||
OTzX43KUR1G/trpuM6VBc0W6YUNYzGRa1TcUxBP4K7DfKMpPGg6ulqypfoHmu8QD
|
||||
L+z+iQmG9ySSuvScIW6u8LgkrTwZga8y2eb/A2FAVYY/bnelef1aMkis+bBX2OQ4
|
||||
QAg2uq+pkhpW1k5NSS9lVCPkj4e5Ur9RCm9fRDMCgYAf3CSIR03eLHy+Y37WzXSh
|
||||
TmELxL6sb+1Xx2Y+cAuBCda3CMTpeIb3F2ivb1d4dvrqsikaXW0Qse/B3tQUC7kA
|
||||
cDmJYwxEiwBsajUD7yuFE5hzzt9nse+R5BFXfp1yD1zr7V9tC7rnUfRAZqrozgjB
|
||||
D/NAW9VvwGupYRbCon7plwKBgQCRPfeoYGRoa9ji8w+Rg3QaZeGyy8jmfGjlqg9a
|
||||
NyEOyIXXuThYFFmyrqw5NZpwQJBTTDApK/xnK7SLS6WY2Rr1oydFxRzo7KJX5B7M
|
||||
+md1H4gCvqeOuWmThgbij1AyQsgRaDehOM2fZ0cKu2/B+Gkm1c9RSWPMsPKR7JMz
|
||||
AGNFtQKBgQCRCFIdGJHnvz35vJfLoihifCejBWtZbAnZoBHpF3xMCtV755J96tUf
|
||||
k1Tv9hz6WfSkOSlwLq6eGZY2dCENJRW1ft1UelpFvCjbfrfLvoFFLs3gu0lfqXHi
|
||||
CS6fjhn9Ahvz10yD6fd4ixRUjoJvULzI0Sxc1O95SYVF1lIAuVr9Hw==
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
certPEMBlock := []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDXTCCAkWgAwIBAgIJAKlqK5HKlo9MMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMTcwNjE5MTA0MzEyWhcNMjcwNjE3MTA0MzEyWjBF
|
||||
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEApEkbPrT6wzcWK1W5atQiGptvuBsRdf8MCg4u6SN10QbslA5k6BYRdZfF
|
||||
eRpwAwYyzkumug6+eBJatDZEd7+0FF86yxB7eMTSiHKRZ5Mi5ZyCFsezdndknGBe
|
||||
K6I80s1jd5ZsLLuMKErvbNwSbfX+X6d2mBeYW8Scv9N+qYnNrHHHohvXoxy1gZ18
|
||||
EhhogQhrD22zaqg/jtmOT8ImUiXzB1mKInt2LlSkoRYuBzepkDJrsE1L/cyYZbtc
|
||||
O/ASDj+/qQAuQ66v9pNyJkIQ7bDOUyxaT5Hx9XvbqI1OqUVAdGLLi+eZIFguFyYd
|
||||
0lemwdN/IDvxftzegTO3cO0D28d1UQIDAQABo1AwTjAdBgNVHQ4EFgQUqMVdMIA1
|
||||
68Dv+iwGugAaEGUSd0IwHwYDVR0jBBgwFoAUqMVdMIA168Dv+iwGugAaEGUSd0Iw
|
||||
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAjQVoqRv2HlE5PJIX/qk5
|
||||
oMOKZlHTyJP+s2HzOOVt+eCE/jNdfC7+8R/HcPldQs7p9GqH2F6hQ9aOtDhJVEaU
|
||||
pjxCi4qKeZ1kWwqv8UMBXW92eHGysBvE2Gmm/B1JFl8S2GR5fBmheZVnYW893MoI
|
||||
gp+bOoCcIuMJRqCra4vJgrOsQjgRElQvd2OlP8qQzInf/fRqO/AnZPwMkGr3+KZ0
|
||||
BKEOXtmSZaPs3xEsnvJd8wrTgA0NQK7v48E+gHSXzQtaHmOLqisRXlUOu2r1gNCJ
|
||||
rr3DRiUP6V/10CZ/ImeSJ72k69VuTw9vq2HzB4x6pqxF2X7JQSLUCS2wfNN13N0d
|
||||
9A==
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
return tls.X509KeyPair(certPEMBlock, keyPEMBlock)
|
||||
}
|
||||
|
||||
func getNextPort() string {
|
||||
return strconv.Itoa(int(atomic.AddUint32(&serverPort, 1)))
|
||||
}
|
||||
|
||||
func getNonLoopBackIP(t *testing.T) string {
|
||||
localIP4 := set.NewStringSet()
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
t.Fatalf("%s. Unable to get IP addresses of this host.", err)
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
|
||||
if ip.To4() != nil {
|
||||
localIP4.Add(ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Filter ipList by IPs those do not start with '127.'.
|
||||
nonLoopBackIPs := localIP4.FuncMatch(func(ip string, matchString string) bool {
|
||||
return !strings.HasPrefix(ip, "127.")
|
||||
}, "")
|
||||
if len(nonLoopBackIPs) == 0 {
|
||||
t.Fatalf("No non-loop back IP address found for this host")
|
||||
}
|
||||
nonLoopBackIP := nonLoopBackIPs.ToSlice()[0]
|
||||
return nonLoopBackIP
|
||||
}
|
||||
|
||||
func TestNewHTTPListener(t *testing.T) {
|
||||
testCases := []struct {
|
||||
serverAddrs []string
|
||||
tcpKeepAliveTimeout time.Duration
|
||||
readTimeout time.Duration
|
||||
writeTimeout time.Duration
|
||||
expectedListenErrs []bool
|
||||
}{
|
||||
{[]string{"93.184.216.34:65432"}, time.Duration(0), time.Duration(0), time.Duration(0), []bool{true}}, // 1
|
||||
{[]string{"example.org:65432"}, time.Duration(0), time.Duration(0), time.Duration(0), []bool{true}}, // 2
|
||||
{[]string{"unknown-host"}, time.Duration(0), time.Duration(0), time.Duration(0), []bool{true}}, // 3
|
||||
{[]string{"unknown-host:65432"}, time.Duration(0), time.Duration(0), time.Duration(0), []bool{true}}, // 4
|
||||
{[]string{"localhost:65432"}, time.Duration(0), time.Duration(0), time.Duration(0), []bool{false}}, // 5
|
||||
{[]string{"localhost:65432", "93.184.216.34:65432"}, time.Duration(0), time.Duration(0), time.Duration(0), []bool{false, true}}, // 6
|
||||
{[]string{"localhost:65432", "unknown-host:65432"}, time.Duration(0), time.Duration(0), time.Duration(0), []bool{false, true}}, // 7
|
||||
{[]string{"[::1:65432", "unknown-host:-1"}, time.Duration(0), time.Duration(0), time.Duration(0), []bool{true, true}}, // 7
|
||||
{[]string{"localhost:0"}, time.Duration(0), time.Duration(0), time.Duration(0), []bool{false}}, // 8
|
||||
{[]string{"localhost:0"}, time.Duration(0), time.Duration(0), time.Duration(0), []bool{false}}, // 9
|
||||
{[]string{"[::1]:3737", "127.0.0.1:90900"}, time.Duration(0), time.Duration(0), time.Duration(0), []bool{false, true}}, // 10
|
||||
{[]string{"[::1]:3737", "localhost:0"}, time.Duration(0), time.Duration(0), time.Duration(0), []bool{false, false}}, // 10
|
||||
}
|
||||
|
||||
for testIdx, testCase := range testCases {
|
||||
listener, listenErrs := newHTTPListener(t.Context(),
|
||||
testCase.serverAddrs,
|
||||
TCPOptions{},
|
||||
)
|
||||
for i, expectedListenErr := range testCase.expectedListenErrs {
|
||||
if !expectedListenErr {
|
||||
if listenErrs[i] != nil {
|
||||
t.Fatalf("Test %d:, listenErrs[%d] error: expected = <nil>, got = %v", testIdx+1, i, listenErrs[i])
|
||||
}
|
||||
} else if listenErrs[i] == nil {
|
||||
t.Fatalf("Test %d: listenErrs[%d]: expected = %v, got = <nil>", testIdx+1, i, expectedListenErr)
|
||||
}
|
||||
}
|
||||
if listener != nil {
|
||||
listener.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPListenerStartClose(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
nonLoopBackIP := getNonLoopBackIP(t)
|
||||
|
||||
testCases := []struct {
|
||||
serverAddrs []string
|
||||
}{
|
||||
{[]string{"localhost:0"}},
|
||||
{[]string{nonLoopBackIP + ":0"}},
|
||||
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}},
|
||||
{[]string{"localhost:0"}},
|
||||
{[]string{nonLoopBackIP + ":0"}},
|
||||
{[]string{"127.0.0.1:0", nonLoopBackIP + ":0"}},
|
||||
}
|
||||
|
||||
nextTest:
|
||||
for i, testCase := range testCases {
|
||||
listener, errs := newHTTPListener(t.Context(),
|
||||
testCase.serverAddrs,
|
||||
TCPOptions{},
|
||||
)
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "The requested address is not valid in its context") {
|
||||
// Ignore if IP is unbindable.
|
||||
continue nextTest
|
||||
}
|
||||
if strings.Contains(err.Error(), "bind: address already in use") {
|
||||
continue nextTest
|
||||
}
|
||||
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, serverAddr := range listener.Addrs() {
|
||||
conn, err := net.Dial("tcp", serverAddr.String())
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
listener.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPListenerAddr(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
nonLoopBackIP := getNonLoopBackIP(t)
|
||||
var casePorts []string
|
||||
for range 6 {
|
||||
casePorts = append(casePorts, getNextPort())
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
serverAddrs []string
|
||||
expectedAddr string
|
||||
}{
|
||||
{[]string{"localhost:" + casePorts[0]}, "127.0.0.1:" + casePorts[0]},
|
||||
{[]string{nonLoopBackIP + ":" + casePorts[1]}, nonLoopBackIP + ":" + casePorts[1]},
|
||||
{[]string{"127.0.0.1:" + casePorts[2], nonLoopBackIP + ":" + casePorts[2]}, "0.0.0.0:" + casePorts[2]},
|
||||
{[]string{"localhost:" + casePorts[3]}, "127.0.0.1:" + casePorts[3]},
|
||||
{[]string{nonLoopBackIP + ":" + casePorts[4]}, nonLoopBackIP + ":" + casePorts[4]},
|
||||
{[]string{"127.0.0.1:" + casePorts[5], nonLoopBackIP + ":" + casePorts[5]}, "0.0.0.0:" + casePorts[5]},
|
||||
}
|
||||
|
||||
nextTest:
|
||||
for i, testCase := range testCases {
|
||||
listener, errs := newHTTPListener(t.Context(),
|
||||
testCase.serverAddrs,
|
||||
TCPOptions{},
|
||||
)
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "The requested address is not valid in its context") {
|
||||
// Ignore if IP is unbindable.
|
||||
continue nextTest
|
||||
}
|
||||
if strings.Contains(err.Error(), "bind: address already in use") {
|
||||
continue nextTest
|
||||
}
|
||||
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
addr := listener.Addr()
|
||||
if addr.String() != testCase.expectedAddr {
|
||||
t.Fatalf("Test %d: addr: expected = %v, got = %v", i+1, testCase.expectedAddr, addr)
|
||||
}
|
||||
|
||||
listener.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPListenerAddrs(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
nonLoopBackIP := getNonLoopBackIP(t)
|
||||
var casePorts []string
|
||||
for range 6 {
|
||||
casePorts = append(casePorts, getNextPort())
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
serverAddrs []string
|
||||
expectedAddrs set.StringSet
|
||||
}{
|
||||
{[]string{"localhost:" + casePorts[0]}, set.CreateStringSet("127.0.0.1:" + casePorts[0])},
|
||||
{[]string{nonLoopBackIP + ":" + casePorts[1]}, set.CreateStringSet(nonLoopBackIP + ":" + casePorts[1])},
|
||||
{[]string{"127.0.0.1:" + casePorts[2], nonLoopBackIP + ":" + casePorts[2]}, set.CreateStringSet("127.0.0.1:"+casePorts[2], nonLoopBackIP+":"+casePorts[2])},
|
||||
{[]string{"localhost:" + casePorts[3]}, set.CreateStringSet("127.0.0.1:" + casePorts[3])},
|
||||
{[]string{nonLoopBackIP + ":" + casePorts[4]}, set.CreateStringSet(nonLoopBackIP + ":" + casePorts[4])},
|
||||
{[]string{"127.0.0.1:" + casePorts[5], nonLoopBackIP + ":" + casePorts[5]}, set.CreateStringSet("127.0.0.1:"+casePorts[5], nonLoopBackIP+":"+casePorts[5])},
|
||||
}
|
||||
|
||||
nextTest:
|
||||
for i, testCase := range testCases {
|
||||
listener, errs := newHTTPListener(t.Context(),
|
||||
testCase.serverAddrs,
|
||||
TCPOptions{},
|
||||
)
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "The requested address is not valid in its context") {
|
||||
// Ignore if IP is unbindable.
|
||||
continue nextTest
|
||||
}
|
||||
if strings.Contains(err.Error(), "bind: address already in use") {
|
||||
continue nextTest
|
||||
}
|
||||
t.Fatalf("Test %d: error: expected = <nil>, got = %v", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
addrs := listener.Addrs()
|
||||
addrSet := set.NewStringSet()
|
||||
for _, addr := range addrs {
|
||||
addrSet.Add(addr.String())
|
||||
}
|
||||
|
||||
if !addrSet.Equals(testCase.expectedAddrs) {
|
||||
t.Fatalf("Test %d: addr: expected = %v, got = %v", i+1, testCase.expectedAddrs, addrs)
|
||||
}
|
||||
|
||||
listener.Close()
|
||||
}
|
||||
}
|
||||
72
internal/http/request-recorder.go
Normal file
72
internal/http/request-recorder.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
// RequestRecorder - records the
|
||||
// of a given io.Reader
|
||||
type RequestRecorder struct {
|
||||
// Data source to record
|
||||
io.Reader
|
||||
// Response body should be logged
|
||||
LogBody bool
|
||||
|
||||
// internal recording buffer
|
||||
buf bytes.Buffer
|
||||
// total bytes read including header size
|
||||
bytesRead int
|
||||
}
|
||||
|
||||
// Close is a no operation closer
|
||||
func (r *RequestRecorder) Close() error {
|
||||
// no-op
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read reads from the internal reader and counts/save the body in the memory
|
||||
func (r *RequestRecorder) Read(p []byte) (n int, err error) {
|
||||
n, err = r.Reader.Read(p)
|
||||
r.bytesRead += n
|
||||
|
||||
if r.LogBody {
|
||||
r.buf.Write(p[:n])
|
||||
}
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Size returns the body size if the currently read object
|
||||
func (r *RequestRecorder) Size() int {
|
||||
return r.bytesRead
|
||||
}
|
||||
|
||||
// Data returns the bytes that were recorded.
|
||||
func (r *RequestRecorder) Data() []byte {
|
||||
// If body logging is enabled then we return the actual body
|
||||
if r.LogBody {
|
||||
return r.buf.Bytes()
|
||||
}
|
||||
// ... otherwise we return <BLOB> placeholder
|
||||
return blobBody
|
||||
}
|
||||
196
internal/http/response-recorder.go
Normal file
196
internal/http/response-recorder.go
Normal file
@@ -0,0 +1,196 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/gzip"
|
||||
)
|
||||
|
||||
// ResponseRecorder - is a wrapper to trap the http response
|
||||
// status code and to record the response body
|
||||
type ResponseRecorder struct {
|
||||
http.ResponseWriter
|
||||
io.ReaderFrom
|
||||
StatusCode int
|
||||
// Log body of 4xx or 5xx responses
|
||||
LogErrBody bool
|
||||
// Log body of all responses
|
||||
LogAllBody bool
|
||||
|
||||
ttfbHeader time.Duration
|
||||
ttfbBody time.Duration
|
||||
|
||||
StartTime time.Time
|
||||
// number of bytes written
|
||||
bytesWritten int
|
||||
// number of bytes of response headers written
|
||||
headerBytesWritten int
|
||||
// Internal recording buffer
|
||||
headers bytes.Buffer
|
||||
body bytes.Buffer
|
||||
// Indicate if headers are written in the log
|
||||
headersLogged bool
|
||||
}
|
||||
|
||||
// Hijack - hijacks the underlying connection
|
||||
func (lrw *ResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
hj, ok := lrw.ResponseWriter.(http.Hijacker)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("response writer does not support hijacking. Type is %T", lrw.ResponseWriter)
|
||||
}
|
||||
return hj.Hijack()
|
||||
}
|
||||
|
||||
// TTFB of the request - this function needs to be called
|
||||
// when the request is finished to provide accurate data
|
||||
func (lrw *ResponseRecorder) TTFB() time.Duration {
|
||||
if lrw.ttfbBody != 0 {
|
||||
return lrw.ttfbBody
|
||||
}
|
||||
return lrw.ttfbHeader
|
||||
}
|
||||
|
||||
// NewResponseRecorder - returns a wrapped response writer to trap
|
||||
// http status codes for auditing purposes.
|
||||
func NewResponseRecorder(w http.ResponseWriter) *ResponseRecorder {
|
||||
rf, _ := w.(io.ReaderFrom)
|
||||
return &ResponseRecorder{
|
||||
ResponseWriter: w,
|
||||
ReaderFrom: rf,
|
||||
StatusCode: http.StatusOK,
|
||||
StartTime: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
// ErrNotImplemented when a functionality is not implemented
|
||||
var ErrNotImplemented = errors.New("not implemented")
|
||||
|
||||
// ReadFrom implements support for calling internal io.ReaderFrom implementations
|
||||
// returns an error if the underlying ResponseWriter does not implement io.ReaderFrom
|
||||
func (lrw *ResponseRecorder) ReadFrom(r io.Reader) (int64, error) {
|
||||
if lrw.ReaderFrom != nil {
|
||||
n, err := lrw.ReaderFrom.ReadFrom(r)
|
||||
lrw.bytesWritten += int(n)
|
||||
return n, err
|
||||
}
|
||||
return 0, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (lrw *ResponseRecorder) Write(p []byte) (int, error) {
|
||||
if !lrw.headersLogged {
|
||||
// We assume the response code to be '200 OK' when WriteHeader() is not called,
|
||||
// that way following Golang HTTP response behavior.
|
||||
lrw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
n, err := lrw.ResponseWriter.Write(p)
|
||||
lrw.bytesWritten += n
|
||||
if lrw.ttfbBody == 0 {
|
||||
lrw.ttfbBody = time.Now().UTC().Sub(lrw.StartTime)
|
||||
}
|
||||
|
||||
if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody {
|
||||
// If body is > 10MB, drop it.
|
||||
if lrw.bytesWritten+len(p) > 10<<20 {
|
||||
lrw.LogAllBody = false
|
||||
lrw.body = bytes.Buffer{}
|
||||
} else {
|
||||
// Always logging error responses.
|
||||
lrw.body.Write(p)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Write the headers into the given buffer
|
||||
func (lrw *ResponseRecorder) writeHeaders(w io.Writer, statusCode int, headers http.Header) {
|
||||
n, _ := fmt.Fprintf(w, "%d %s\n", statusCode, http.StatusText(statusCode))
|
||||
lrw.headerBytesWritten += n
|
||||
for k, v := range headers {
|
||||
n, _ := fmt.Fprintf(w, "%s: %s\n", k, v[0])
|
||||
lrw.headerBytesWritten += n
|
||||
}
|
||||
}
|
||||
|
||||
// blobBody returns a dummy body placeholder for blob (binary stream)
|
||||
var blobBody = []byte("<BLOB>")
|
||||
|
||||
// gzippedBody returns a dummy body placeholder for gzipped content
|
||||
var gzippedBody = []byte("<GZIP>")
|
||||
|
||||
// Body - Return response body.
|
||||
func (lrw *ResponseRecorder) Body() []byte {
|
||||
if lrw.Header().Get("Content-Encoding") == "gzip" {
|
||||
if lrw.body.Len() > 1<<20 {
|
||||
return gzippedBody
|
||||
}
|
||||
r, err := gzip.NewReader(&lrw.body)
|
||||
if err != nil {
|
||||
return gzippedBody
|
||||
}
|
||||
defer r.Close()
|
||||
b, _ := io.ReadAll(io.LimitReader(r, 10<<20))
|
||||
return b
|
||||
}
|
||||
// If there was an error response or body logging is enabled
|
||||
// then we return the body contents
|
||||
if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody {
|
||||
return lrw.body.Bytes()
|
||||
}
|
||||
// ... otherwise we return the <BLOB> place holder
|
||||
return blobBody
|
||||
}
|
||||
|
||||
// WriteHeader - writes http status code
|
||||
func (lrw *ResponseRecorder) WriteHeader(code int) {
|
||||
if !lrw.headersLogged {
|
||||
lrw.ttfbHeader = time.Now().UTC().Sub(lrw.StartTime)
|
||||
lrw.StatusCode = code
|
||||
lrw.writeHeaders(&lrw.headers, code, lrw.Header())
|
||||
lrw.headersLogged = true
|
||||
lrw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
}
|
||||
|
||||
// Flush - Calls the underlying Flush.
|
||||
func (lrw *ResponseRecorder) Flush() {
|
||||
if flusher, ok := lrw.ResponseWriter.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// Size - returns the number of bytes written
|
||||
func (lrw *ResponseRecorder) Size() int {
|
||||
return lrw.bytesWritten
|
||||
}
|
||||
|
||||
// HeaderSize - returns the number of bytes of response headers written
|
||||
func (lrw *ResponseRecorder) HeaderSize() int {
|
||||
return lrw.headerBytesWritten
|
||||
}
|
||||
238
internal/http/server.go
Normal file
238
internal/http/server.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
var (
|
||||
// GlobalMinIOVersion - is sent in the header to all http targets
|
||||
GlobalMinIOVersion string
|
||||
|
||||
// GlobalDeploymentID - is sent in the header to all http targets
|
||||
GlobalDeploymentID string
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultIdleTimeout for idle inactive connections
|
||||
DefaultIdleTimeout = 30 * time.Second
|
||||
|
||||
// DefaultReadHeaderTimeout for very slow inactive connections
|
||||
DefaultReadHeaderTimeout = 30 * time.Second
|
||||
|
||||
// DefaultMaxHeaderBytes - default maximum HTTP header size in bytes.
|
||||
DefaultMaxHeaderBytes = 1 * humanize.MiByte
|
||||
)
|
||||
|
||||
// Server - extended http.Server supports multiple addresses to serve and enhanced connection handling.
|
||||
type Server struct {
|
||||
http.Server
|
||||
Addrs []string // addresses on which the server listens for new connection.
|
||||
TCPOptions TCPOptions // all the configurable TCP conn specific configurable options.
|
||||
listenerMutex sync.Mutex // to guard 'listener' field.
|
||||
listener *httpListener // HTTP listener for all 'Addrs' field.
|
||||
inShutdown uint32 // indicates whether the server is in shutdown or not
|
||||
requestCount int32 // counter holds no. of request in progress.
|
||||
}
|
||||
|
||||
// GetRequestCount - returns number of request in progress.
|
||||
func (srv *Server) GetRequestCount() int {
|
||||
return int(atomic.LoadInt32(&srv.requestCount))
|
||||
}
|
||||
|
||||
// Init - init HTTP server
|
||||
func (srv *Server) Init(listenCtx context.Context, listenErrCallback func(listenAddr string, err error)) (serve func() error, err error) {
|
||||
// Take a copy of server fields.
|
||||
var tlsConfig *tls.Config
|
||||
if srv.TLSConfig != nil {
|
||||
tlsConfig = srv.TLSConfig.Clone()
|
||||
}
|
||||
handler := srv.Handler // if srv.Handler holds non-synced state -> possible data race
|
||||
|
||||
// Create new HTTP listener.
|
||||
var listener *httpListener
|
||||
listener, listenErrs := newHTTPListener(
|
||||
listenCtx,
|
||||
srv.Addrs,
|
||||
srv.TCPOptions,
|
||||
)
|
||||
|
||||
var interfaceFound bool
|
||||
for i := range listenErrs {
|
||||
if listenErrs[i] != nil {
|
||||
listenErrCallback(srv.Addrs[i], listenErrs[i])
|
||||
} else {
|
||||
interfaceFound = true
|
||||
}
|
||||
}
|
||||
if !interfaceFound {
|
||||
return nil, errors.New("no available interface found")
|
||||
}
|
||||
|
||||
// Wrap given handler to do additional
|
||||
// * return 503 (service unavailable) if the server in shutdown.
|
||||
wrappedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// If server is in shutdown.
|
||||
if atomic.LoadUint32(&srv.inShutdown) != 0 {
|
||||
// To indicate disable keep-alive, server is shutting down.
|
||||
w.Header().Set("Connection", "close")
|
||||
|
||||
// Add 1 minute retry header, incase-client wants to honor it
|
||||
w.Header().Set(RetryAfter, "60")
|
||||
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
w.Write([]byte(http.ErrServerClosed.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
atomic.AddInt32(&srv.requestCount, 1)
|
||||
defer atomic.AddInt32(&srv.requestCount, -1)
|
||||
|
||||
// Handle request using passed handler.
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
srv.listenerMutex.Lock()
|
||||
srv.Handler = wrappedHandler
|
||||
srv.listener = listener
|
||||
srv.listenerMutex.Unlock()
|
||||
|
||||
var l net.Listener = listener
|
||||
if tlsConfig != nil {
|
||||
l = tls.NewListener(listener, tlsConfig)
|
||||
}
|
||||
|
||||
serve = func() error {
|
||||
return srv.Serve(l)
|
||||
}
|
||||
|
||||
return serve, err
|
||||
}
|
||||
|
||||
// Shutdown - shuts down HTTP server.
|
||||
func (srv *Server) Shutdown() error {
|
||||
srv.listenerMutex.Lock()
|
||||
if srv.listener == nil {
|
||||
srv.listenerMutex.Unlock()
|
||||
return http.ErrServerClosed
|
||||
}
|
||||
srv.listenerMutex.Unlock()
|
||||
|
||||
if atomic.AddUint32(&srv.inShutdown, 1) > 1 {
|
||||
// shutdown in progress
|
||||
return http.ErrServerClosed
|
||||
}
|
||||
|
||||
// Close underneath HTTP listener.
|
||||
srv.listenerMutex.Lock()
|
||||
err := srv.listener.Close()
|
||||
srv.listenerMutex.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for opened connection to be closed up to Shutdown timeout.
|
||||
return nil
|
||||
}
|
||||
|
||||
// UseIdleTimeout configure idle connection timeout
|
||||
func (srv *Server) UseIdleTimeout(d time.Duration) *Server {
|
||||
srv.IdleTimeout = d
|
||||
return srv
|
||||
}
|
||||
|
||||
// UseReadTimeout configure connection request read timeout.
|
||||
func (srv *Server) UseReadTimeout(d time.Duration) *Server {
|
||||
srv.ReadTimeout = d
|
||||
return srv
|
||||
}
|
||||
|
||||
// UseReadHeaderTimeout configure read header timeout
|
||||
func (srv *Server) UseReadHeaderTimeout(d time.Duration) *Server {
|
||||
srv.ReadHeaderTimeout = d
|
||||
return srv
|
||||
}
|
||||
|
||||
// UseWriteTimeout configure connection response write timeout.
|
||||
func (srv *Server) UseWriteTimeout(d time.Duration) *Server {
|
||||
srv.WriteTimeout = d
|
||||
return srv
|
||||
}
|
||||
|
||||
// UseHandler configure final handler for this HTTP *Server
|
||||
func (srv *Server) UseHandler(h http.Handler) *Server {
|
||||
srv.Handler = h
|
||||
return srv
|
||||
}
|
||||
|
||||
// UseTLSConfig pass configured TLSConfig for this HTTP *Server
|
||||
func (srv *Server) UseTLSConfig(cfg *tls.Config) *Server {
|
||||
srv.TLSConfig = cfg
|
||||
return srv
|
||||
}
|
||||
|
||||
// UseBaseContext use custom base context for this HTTP *Server
|
||||
func (srv *Server) UseBaseContext(ctx context.Context) *Server {
|
||||
srv.BaseContext = func(listener net.Listener) context.Context {
|
||||
return ctx
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
// UseCustomLogger use customized logger for this HTTP *Server
|
||||
func (srv *Server) UseCustomLogger(l *log.Logger) *Server {
|
||||
srv.ErrorLog = l
|
||||
return srv
|
||||
}
|
||||
|
||||
// UseTCPOptions use custom TCP options on raw socket
|
||||
func (srv *Server) UseTCPOptions(opts TCPOptions) *Server {
|
||||
srv.TCPOptions = opts
|
||||
return srv
|
||||
}
|
||||
|
||||
// NewServer - creates new HTTP server using given arguments.
|
||||
func NewServer(addrs []string) *Server {
|
||||
httpServer := &Server{
|
||||
Addrs: addrs,
|
||||
}
|
||||
// This is not configurable for now.
|
||||
httpServer.MaxHeaderBytes = DefaultMaxHeaderBytes
|
||||
return httpServer
|
||||
}
|
||||
|
||||
// SetMinIOVersion -- MinIO version from the main package is set here
|
||||
func SetMinIOVersion(version string) {
|
||||
GlobalMinIOVersion = version
|
||||
}
|
||||
|
||||
// SetDeploymentID -- Deployment Id from the main package is set here
|
||||
func SetDeploymentID(deploymentID string) {
|
||||
GlobalDeploymentID = deploymentID
|
||||
}
|
||||
78
internal/http/server_test.go
Normal file
78
internal/http/server_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/pkg/v3/certs"
|
||||
)
|
||||
|
||||
func TestNewServer(t *testing.T) {
|
||||
nonLoopBackIP := getNonLoopBackIP(t)
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hello, world")
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
addrs []string
|
||||
handler http.Handler
|
||||
certFn certs.GetCertificateFunc
|
||||
}{
|
||||
{[]string{"127.0.0.1:9000"}, handler, nil},
|
||||
{[]string{nonLoopBackIP + ":9000"}, handler, nil},
|
||||
{[]string{"127.0.0.1:9000", nonLoopBackIP + ":9000"}, handler, nil},
|
||||
{[]string{"127.0.0.1:9000"}, handler, getCert},
|
||||
{[]string{nonLoopBackIP + ":9000"}, handler, getCert},
|
||||
{[]string{"127.0.0.1:9000", nonLoopBackIP + ":9000"}, handler, getCert},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
server := NewServer(testCase.addrs).
|
||||
UseHandler(testCase.handler)
|
||||
if testCase.certFn != nil {
|
||||
server = server.UseTLSConfig(&tls.Config{
|
||||
PreferServerCipherSuites: true,
|
||||
GetCertificate: testCase.certFn,
|
||||
})
|
||||
}
|
||||
if server == nil {
|
||||
t.Fatalf("Case %v: server: expected: <non-nil>, got: <nil>", (i + 1))
|
||||
} else if !reflect.DeepEqual(server.Addrs, testCase.addrs) {
|
||||
t.Fatalf("Case %v: server.Addrs: expected: %v, got: %v", (i + 1), testCase.addrs, server.Addrs)
|
||||
}
|
||||
|
||||
if testCase.certFn == nil {
|
||||
if server.TLSConfig != nil {
|
||||
t.Fatalf("Case %v: server.TLSConfig: expected: <nil>, got: %v", (i + 1), server.TLSConfig)
|
||||
}
|
||||
} else {
|
||||
if server.TLSConfig == nil {
|
||||
t.Fatalf("Case %v: server.TLSConfig: expected: <non-nil>, got: <nil>", (i + 1))
|
||||
}
|
||||
}
|
||||
|
||||
if server.MaxHeaderBytes != DefaultMaxHeaderBytes {
|
||||
t.Fatalf("Case %v: server.MaxHeaderBytes: expected: %v, got: %v", (i + 1), DefaultMaxHeaderBytes, server.MaxHeaderBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
205
internal/http/transports.go
Normal file
205
internal/http/transports.go
Normal file
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/minio/pkg/v3/certs"
|
||||
)
|
||||
|
||||
// tlsClientSessionCacheSize is the cache size for client sessions.
|
||||
var tlsClientSessionCacheSize = 100
|
||||
|
||||
const (
|
||||
// WriteBufferSize 64KiB moving up from 4KiB default
|
||||
WriteBufferSize = 64 << 10
|
||||
|
||||
// ReadBufferSize 64KiB moving up from 4KiB default
|
||||
ReadBufferSize = 64 << 10
|
||||
)
|
||||
|
||||
// ConnSettings - contains connection settings.
|
||||
type ConnSettings struct {
|
||||
DialContext DialContext // Custom dialContext, DialTimeout is ignored if this is already setup.
|
||||
LookupHost LookupHost // Custom lookupHost, is nil on containerized deployments.
|
||||
DialTimeout time.Duration
|
||||
|
||||
// TLS Settings
|
||||
RootCAs *x509.CertPool
|
||||
CipherSuites []uint16
|
||||
CurvePreferences []tls.CurveID
|
||||
|
||||
// HTTP2
|
||||
EnableHTTP2 bool
|
||||
|
||||
// TCP Options
|
||||
TCPOptions TCPOptions
|
||||
}
|
||||
|
||||
func (s ConnSettings) getDefaultTransport(maxIdleConnsPerHost int) *http.Transport {
|
||||
if maxIdleConnsPerHost <= 0 {
|
||||
maxIdleConnsPerHost = 1024
|
||||
}
|
||||
|
||||
dialContext := s.DialContext
|
||||
if dialContext == nil {
|
||||
dialContext = DialContextWithLookupHost(s.LookupHost, NewInternodeDialContext(s.DialTimeout, s.TCPOptions))
|
||||
}
|
||||
|
||||
tlsClientConfig := tls.Config{
|
||||
RootCAs: s.RootCAs,
|
||||
CipherSuites: s.CipherSuites,
|
||||
CurvePreferences: s.CurvePreferences,
|
||||
ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize),
|
||||
}
|
||||
|
||||
// For more details about various values used here refer
|
||||
// https://golang.org/pkg/net/http/#Transport documentation
|
||||
tr := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: dialContext,
|
||||
MaxIdleConnsPerHost: maxIdleConnsPerHost,
|
||||
WriteBufferSize: WriteBufferSize,
|
||||
ReadBufferSize: ReadBufferSize,
|
||||
IdleConnTimeout: 15 * time.Second,
|
||||
ResponseHeaderTimeout: 15 * time.Minute, // Conservative timeout is the default (for MinIO internode)
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: &tlsClientConfig,
|
||||
ForceAttemptHTTP2: s.EnableHTTP2,
|
||||
// Go net/http automatically unzip if content-type is
|
||||
// gzip disable this feature, as we are always interested
|
||||
// in raw stream.
|
||||
DisableCompression: true,
|
||||
}
|
||||
|
||||
// https://github.com/golang/go/issues/23559
|
||||
// https://github.com/golang/go/issues/42534
|
||||
// https://github.com/golang/go/issues/43989
|
||||
// https://github.com/golang/go/issues/33425
|
||||
// https://github.com/golang/go/issues/29246
|
||||
// if tlsConfig != nil {
|
||||
// trhttp2, _ := http2.ConfigureTransports(tr)
|
||||
// if trhttp2 != nil {
|
||||
// // ReadIdleTimeout is the timeout after which a health check using ping
|
||||
// // frame will be carried out if no frame is received on the
|
||||
// // connection. 5 minutes is sufficient time for any idle connection.
|
||||
// trhttp2.ReadIdleTimeout = 5 * time.Minute
|
||||
// // PingTimeout is the timeout after which the connection will be closed
|
||||
// // if a response to Ping is not received.
|
||||
// trhttp2.PingTimeout = dialTimeout
|
||||
// // DisableCompression, if true, prevents the Transport from
|
||||
// // requesting compression with an "Accept-Encoding: gzip"
|
||||
// trhttp2.DisableCompression = true
|
||||
// }
|
||||
// }
|
||||
|
||||
return tr
|
||||
}
|
||||
|
||||
// NewInternodeHTTPTransport returns transport for internode MinIO connections.
|
||||
func (s ConnSettings) NewInternodeHTTPTransport(maxIdleConnsPerHost int) func() http.RoundTripper {
|
||||
tr := s.getDefaultTransport(maxIdleConnsPerHost)
|
||||
|
||||
// Settings specific to internode requests.
|
||||
tr.TLSHandshakeTimeout = 15 * time.Second
|
||||
|
||||
return func() http.RoundTripper {
|
||||
return tr
|
||||
}
|
||||
}
|
||||
|
||||
// NewCustomHTTPProxyTransport is used only for proxied requests, specifically
|
||||
// only supports HTTP/1.1
|
||||
func (s ConnSettings) NewCustomHTTPProxyTransport() func() *http.Transport {
|
||||
s.EnableHTTP2 = false
|
||||
tr := s.getDefaultTransport(0)
|
||||
|
||||
// Settings specific to proxied requests.
|
||||
tr.ResponseHeaderTimeout = 30 * time.Minute
|
||||
|
||||
return func() *http.Transport {
|
||||
return tr
|
||||
}
|
||||
}
|
||||
|
||||
// NewHTTPTransportWithTimeout allows setting a timeout for response headers
|
||||
func (s ConnSettings) NewHTTPTransportWithTimeout(timeout time.Duration) *http.Transport {
|
||||
tr := s.getDefaultTransport(0)
|
||||
|
||||
// Settings specific to this transport.
|
||||
tr.ResponseHeaderTimeout = timeout
|
||||
return tr
|
||||
}
|
||||
|
||||
// NewHTTPTransportWithClientCerts returns a new http configuration used for
|
||||
// communicating with client cert authentication.
|
||||
func (s ConnSettings) NewHTTPTransportWithClientCerts(ctx context.Context, clientCert, clientKey string) (*http.Transport, error) {
|
||||
transport := s.NewHTTPTransportWithTimeout(1 * time.Minute)
|
||||
if clientCert != "" && clientKey != "" {
|
||||
c, err := certs.NewManager(ctx, clientCert, clientKey, tls.LoadX509KeyPair)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c != nil {
|
||||
c.UpdateReloadDuration(10 * time.Second)
|
||||
c.ReloadOnSignal(syscall.SIGHUP) // allow reloads upon SIGHUP
|
||||
transport.TLSClientConfig.GetClientCertificate = c.GetClientCertificate
|
||||
}
|
||||
}
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
// NewRemoteTargetHTTPTransport returns a new http configuration
|
||||
// used while communicating with the remote replication targets.
|
||||
func (s ConnSettings) NewRemoteTargetHTTPTransport(insecure bool) func() *http.Transport {
|
||||
tr := s.getDefaultTransport(0)
|
||||
|
||||
tr.TLSHandshakeTimeout = 10 * time.Second
|
||||
tr.ResponseHeaderTimeout = 0
|
||||
tr.TLSClientConfig.InsecureSkipVerify = insecure
|
||||
|
||||
return func() *http.Transport {
|
||||
return tr
|
||||
}
|
||||
}
|
||||
|
||||
// uaTransport - User-Agent transport
|
||||
type uaTransport struct {
|
||||
ua string
|
||||
rt http.RoundTripper
|
||||
}
|
||||
|
||||
func (u *uaTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req2 := req.Clone(req.Context())
|
||||
req2.Header.Set("User-Agent", u.ua)
|
||||
return u.rt.RoundTrip(req2)
|
||||
}
|
||||
|
||||
// WithUserAgent wraps an existing transport with custom User-Agent
|
||||
func WithUserAgent(rt http.RoundTripper, getUA func() string) http.RoundTripper {
|
||||
return &uaTransport{
|
||||
ua: getUA(),
|
||||
rt: rt,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user