OP#792 initial commit after fork

This commit is contained in:
2026-09-27 11:03:48 +02:00
parent 1fc33f4f1f
commit 55b0890196
1338 changed files with 428609 additions and 57 deletions

View 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()
}

View 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()
}

View 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
View 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)
}
}

View 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
View 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
}
}

View 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
View 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
View 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"
)

View 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
View 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
}

View 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()
}
}

View 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
}

View 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
View 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
}

View 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
View 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,
}
}