mirror of
https://github.com/versity/versitygw.git
synced 2026-07-02 16:54:25 +00:00
6f1dfe84e5
Expose S3 option injection in embed gateway config so integrators can attach request middleware and other server behaviors without needing to re-implement RunVersityGW().
1724 lines
55 KiB
Go
1724 lines
55 KiB
Go
// Copyright 2026 Versity Software
|
|
// This file is licensed under the Apache License, Version 2.0
|
|
// (the "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
// Package embedgw provides a high-level entry point for running the VersityGW
|
|
// S3 gateway as a library, making it easy to embed the gateway into other
|
|
// applications.
|
|
//
|
|
// Note: only a single gateway instance per process is currently supported.
|
|
// Several subsystems (bucket-name validation, debug logging) rely on
|
|
// package-level globals that would race if RunVersityGW were called
|
|
// concurrently from multiple goroutines.
|
|
package embedgw
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
"github.com/versity/versitygw/auth"
|
|
"github.com/versity/versitygw/backend"
|
|
"github.com/versity/versitygw/debuglogger"
|
|
"github.com/versity/versitygw/metrics"
|
|
"github.com/versity/versitygw/s3api"
|
|
"github.com/versity/versitygw/s3api/middlewares"
|
|
"github.com/versity/versitygw/s3api/utils"
|
|
"github.com/versity/versitygw/s3event"
|
|
"github.com/versity/versitygw/s3log"
|
|
"github.com/versity/versitygw/website"
|
|
"github.com/versity/versitygw/webui"
|
|
)
|
|
|
|
const awsDefaultRegion = "us-east-1"
|
|
|
|
// Config holds all configuration options for running the VersityGW gateway.
|
|
type Config struct {
|
|
// RootUserAccess is the access key ID for the root account. The root
|
|
// account is granted full authorization to all API requests after
|
|
// authentication. Required.
|
|
RootUserAccess string
|
|
// RootUserSecret is the secret access key for the root account. Required.
|
|
RootUserSecret string
|
|
// Region is the AWS region name reported to S3 clients (e.g. "us-east-1").
|
|
// Defaults to "us-east-1" when empty.
|
|
Region string
|
|
|
|
// Ports is the list of S3 API listening addresses. Each entry can be
|
|
// "host:port" to bind a specific interface, or ":port" to bind all
|
|
// interfaces. Hostnames are resolved to all matching IPs. UNIX domain
|
|
// sockets are supported as absolute or relative paths, or Linux abstract
|
|
// namespace sockets prefixed with "@" (e.g. "@versitygw-s3"). Multiple
|
|
// entries are supported (e.g. [":7070", "localhost:9090"]). Required.
|
|
Ports []string
|
|
|
|
// AdminPorts is the list of admin API listening addresses. Accepts the
|
|
// same formats as Ports. When empty, the admin API is served on the same
|
|
// endpoints as the S3 API. Setting this allows finer-grained firewall
|
|
// control over the admin endpoint with optionally separate TLS certs.
|
|
AdminPorts []string
|
|
|
|
// MaxConnections is the maximum number of concurrent TCP connections
|
|
// accepted by the S3 API server.
|
|
MaxConnections int
|
|
// MaxRequests is the maximum number of concurrent in-flight S3 requests.
|
|
// Should not exceed MaxConnections; if it does, a warning is logged.
|
|
MaxRequests int
|
|
|
|
// AdminMaxConnections is the maximum concurrent TCP connections for the
|
|
// separate admin server. Only used when AdminPorts is non-empty.
|
|
AdminMaxConnections int
|
|
// AdminMaxRequests is the maximum concurrent in-flight requests for the
|
|
// admin server. Should not exceed AdminMaxConnections.
|
|
AdminMaxRequests int
|
|
|
|
// MultipartMaxParts is the maximum number of parts allowed in a single
|
|
// multipart upload. The S3 specification allows up to 10,000 parts;
|
|
// the default value of 10000 matches the AWS S3 maximum. Clients that
|
|
// attempt to upload more parts than this limit receive an error.
|
|
MultipartMaxParts int
|
|
|
|
// CertFile is the path to the TLS certificate file for the S3 API server.
|
|
// Both CertFile and KeyFile must be provided together to enable TLS.
|
|
CertFile string
|
|
// KeyFile is the path to the TLS private key file for the S3 API server.
|
|
KeyFile string
|
|
|
|
// AdminCertFile is the path to the TLS certificate for the admin server.
|
|
// Both AdminCertFile and AdminKeyFile must be provided together. When
|
|
// empty and AdminPorts is set, the admin server runs without TLS.
|
|
AdminCertFile string
|
|
// AdminKeyFile is the path to the TLS private key for the admin server.
|
|
AdminKeyFile string
|
|
|
|
// CORSAllowOrigin sets the default Access-Control-Allow-Origin response
|
|
// header value applied when no bucket-level CORS configuration exists and
|
|
// for all admin API responses. When WebuiPorts is set and this is empty,
|
|
// it defaults to "*". For production, set this to a specific origin
|
|
// (e.g. "https://webui.example.com") to restrict cross-origin access.
|
|
CORSAllowOrigin string
|
|
|
|
// Debug enables verbose debug logging to stdout, including details for
|
|
// signature verification steps. Not intended for production use.
|
|
Debug bool
|
|
// IAMDebug enables verbose IAM subsystem debug logging.
|
|
IAMDebug bool
|
|
// Quiet suppresses per-request summary logging to stdout.
|
|
Quiet bool
|
|
// Readonly restricts the gateway to read-only S3 operations; all write
|
|
// requests are rejected.
|
|
Readonly bool
|
|
// KeepAlive enables HTTP keep-alive on S3 API connections.
|
|
KeepAlive bool
|
|
// DisableACLs disables ACL enforcement at the gateway level. All ACL
|
|
// headers on requests are ignored and no access control is enforced via
|
|
// bucket ACLs. PutBucketAcl returns AccessControlListNotSupported.
|
|
// Prefer bucket policies over ACLs when this is enabled.
|
|
DisableACLs bool
|
|
// DisableStrictBucketNames allows legacy or non-DNS-compliant bucket
|
|
// names by skipping strict validation. By default, bucket name validation
|
|
// follows the rules described in the AWS S3 documentation.
|
|
DisableStrictBucketNames bool
|
|
|
|
// VirtualDomain enables virtual-hosted-style bucket addressing. Set to
|
|
// the base domain name (e.g. "s3.example.com") so that bucket access uses
|
|
// the form "https://<bucket>.s3.example.com/". Path-style addressing
|
|
// remains enabled alongside it. Each bucket typically requires a DNS
|
|
// entry pointing to the gateway.
|
|
VirtualDomain string
|
|
|
|
// HealthPath is the URL path for unauthenticated health-check requests
|
|
// (e.g. "/healthz"). The endpoint returns HTTP 200 for GET requests and
|
|
// is commonly used by load balancers. Any bucket whose name matches the
|
|
// path segment is masked while this is set.
|
|
HealthPath string
|
|
|
|
// SocketPerm is the octal file-mode string for UNIX domain socket
|
|
// permissions (e.g. "0660" for owner+group read/write). Has no effect on
|
|
// TCP/IP addresses or Linux abstract "@" namespace sockets. When empty,
|
|
// permissions are determined by the process umask.
|
|
SocketPerm string
|
|
|
|
// IAM Backends
|
|
//
|
|
// The gateway supports five external IAM backends. At most one may be
|
|
// active at a time. When the fields for more than one backend are
|
|
// populated, the first match in the following priority order wins:
|
|
//
|
|
// 1. IAMDir -- local directory
|
|
// 2. LDAPServerURL -- LDAP
|
|
// 3. S3IAMEndpoint -- S3-backed
|
|
// 4. VaultEndpointURL -- HashiCorp Vault
|
|
// 5. IpaHost -- FreeIPA
|
|
//
|
|
// Configuring an IAM backend is optional. When none of the trigger fields
|
|
// above are set, the gateway runs in single-account mode: only the root
|
|
// account (RootUserAccess/RootUserSecret) exists and the user management
|
|
// API is unavailable.
|
|
//
|
|
// The IAMCache fields below apply to all backends except single-account
|
|
// mode.
|
|
|
|
// IAMDir enables the local file-based IAM backend. Set to the directory
|
|
// path where account files are stored. Account data is plain text
|
|
// protected only by filesystem permissions; suitable for development but
|
|
// not recommended for production deployments.
|
|
IAMDir string
|
|
|
|
// LDAP IAM backend. Activated when LDAPServerURL is non-empty.
|
|
|
|
// LDAPServerURL is the URL of the LDAP server (e.g. "ldap://ldap.example.com:389").
|
|
LDAPServerURL string
|
|
// LDAPBindDN is the distinguished name used to bind to the LDAP server.
|
|
LDAPBindDN string
|
|
// LDAPPassword is the password for LDAPBindDN.
|
|
LDAPPassword string
|
|
// LDAPQueryBase is the base DN for user search queries.
|
|
LDAPQueryBase string
|
|
// LDAPObjClasses is the LDAP object class filter for user entries.
|
|
LDAPObjClasses string
|
|
// LDAPAccessAttr is the LDAP attribute that holds the S3 access key ID.
|
|
LDAPAccessAttr string
|
|
// LDAPSecretAttr is the LDAP attribute that holds the S3 secret key.
|
|
LDAPSecretAttr string
|
|
// LDAPRoleAttr is the LDAP attribute that holds the user role.
|
|
LDAPRoleAttr string
|
|
// LDAPUserIDAttr is the LDAP attribute that holds the POSIX user ID.
|
|
LDAPUserIDAttr string
|
|
// LDAPGroupIDAttr is the LDAP attribute that holds the POSIX group ID.
|
|
LDAPGroupIDAttr string
|
|
// LDAPProjectIDAttr is the LDAP attribute that holds the project ID.
|
|
LDAPProjectIDAttr string
|
|
// LDAPTLSSkipVerify disables TLS certificate verification for the LDAP
|
|
// connection. Use only in development or trusted internal environments.
|
|
LDAPTLSSkipVerify bool
|
|
|
|
// HashiCorp Vault IAM backend. Activated when VaultEndpointURL is non-empty.
|
|
|
|
// VaultEndpointURL is the HashiCorp Vault server URL
|
|
// (e.g. "https://vault.example.com:8200").
|
|
VaultEndpointURL string
|
|
// VaultNamespace is the Vault namespace to use (Vault Enterprise only).
|
|
VaultNamespace string
|
|
// VaultSecretStoragePath is the KV secrets engine path where account
|
|
// data is stored.
|
|
VaultSecretStoragePath string
|
|
// VaultSecretStorageNamespace is the Vault namespace for the secrets
|
|
// storage path (Vault Enterprise only).
|
|
VaultSecretStorageNamespace string
|
|
// VaultAuthMethod is the Vault authentication method to use
|
|
// (e.g. "token", "approle").
|
|
VaultAuthMethod string
|
|
// VaultAuthNamespace is the Vault namespace used for authentication
|
|
// (Vault Enterprise only).
|
|
VaultAuthNamespace string
|
|
// VaultMountPath is the mount path of the auth method in Vault.
|
|
VaultMountPath string
|
|
// VaultRootToken is the Vault token used when VaultAuthMethod is "token".
|
|
VaultRootToken string
|
|
// VaultRoleID is the AppRole role ID used when VaultAuthMethod is "approle".
|
|
VaultRoleID string
|
|
// VaultRoleSecret is the AppRole secret ID.
|
|
VaultRoleSecret string
|
|
// VaultServerCert is the path to the CA certificate used to verify the
|
|
// Vault server's TLS certificate.
|
|
VaultServerCert string
|
|
// VaultClientCert is the path to the client TLS certificate for mutual
|
|
// TLS authentication with Vault.
|
|
VaultClientCert string
|
|
// VaultClientCertKey is the path to the private key for VaultClientCert.
|
|
VaultClientCertKey string
|
|
|
|
// S3-backed IAM backend. Activated when S3IAMEndpoint is non-empty.
|
|
|
|
// S3IAMAccess is the access key ID for the S3-backed IAM backend.
|
|
S3IAMAccess string
|
|
// S3IAMSecret is the secret key for the S3-backed IAM backend.
|
|
S3IAMSecret string
|
|
// S3IAMRegion is the AWS region of the S3-backed IAM bucket.
|
|
S3IAMRegion string
|
|
// S3IAMBucket is the bucket name that stores IAM account data.
|
|
S3IAMBucket string
|
|
// S3IAMEndpoint is the endpoint URL for the S3-backed IAM service.
|
|
// Useful when using a non-AWS S3-compatible store.
|
|
S3IAMEndpoint string
|
|
// S3IAMDisableSSLVerify disables TLS certificate verification for the
|
|
// S3-backed IAM connection. Use only in development or trusted internal
|
|
// environments.
|
|
S3IAMDisableSSLVerify bool
|
|
|
|
// FreeIPA IAM backend. Activated when IpaHost is non-empty.
|
|
|
|
// IpaHost is the hostname or URL of the FreeIPA server.
|
|
IpaHost string
|
|
// IpaVaultName is the name of the FreeIPA vault used to store credentials.
|
|
IpaVaultName string
|
|
// IpaUser is the FreeIPA username for authentication.
|
|
IpaUser string
|
|
// IpaPassword is the FreeIPA password for authentication.
|
|
IpaPassword string
|
|
// IpaInsecure disables TLS certificate verification for the FreeIPA
|
|
// connection.
|
|
IpaInsecure bool
|
|
|
|
// IAM Cache
|
|
//
|
|
// The gateway maintains an in-memory cache of IAM account lookups to
|
|
// reduce load on the external IAM backend. The cache applies to all
|
|
// backends except single-account mode. All fields are optional.
|
|
|
|
// IAMCacheDisable disables the in-memory IAM account cache. By default,
|
|
// accounts are cached to reduce backend lookup frequency.
|
|
IAMCacheDisable bool
|
|
// IAMCacheTTL is the time-to-live in seconds for cached IAM entries.
|
|
IAMCacheTTL int
|
|
// IAMCachePrune is the interval in seconds between cache prune runs that
|
|
// remove expired entries.
|
|
IAMCachePrune int
|
|
|
|
// Access Logging
|
|
//
|
|
// Records details of every S3 and admin API request. All three outputs
|
|
// are independent and can be enabled simultaneously in any combination.
|
|
// All are optional; omit or leave empty to disable that output.
|
|
|
|
// AccessLog is the file path for S3 request access logs in the AWS S3
|
|
// access log format. Use absolute paths; relative paths may break if the
|
|
// server changes its working directory. Empty disables file logging.
|
|
AccessLog string
|
|
// LogWebhookURL is an HTTP(S) URL that receives S3 access log entries as
|
|
// JSON-encoded POST requests. Can be set alongside AccessLog.
|
|
LogWebhookURL string
|
|
// AdminLogFile is the file path for admin API request logs.
|
|
AdminLogFile string
|
|
|
|
// Metrics
|
|
//
|
|
// The gateway can emit operational metrics to StatsD and DogStatsD.
|
|
// Both backends may be active simultaneously; set either or both.
|
|
// All fields are optional. When neither StatsdServers nor DogstatsServers
|
|
// is set, metrics are disabled.
|
|
|
|
// MetricsService is the service name label attached to all emitted metrics.
|
|
// Defaults to the system hostname when empty.
|
|
MetricsService string
|
|
// StatsdServers is a comma-separated list of StatsD server addresses
|
|
// (e.g. "localhost:8125").
|
|
StatsdServers string
|
|
// DogstatsServers is a comma-separated list of DogStatsD server addresses.
|
|
DogstatsServers string
|
|
|
|
// Bucket Event Notifications
|
|
//
|
|
// The gateway can forward S3 bucket events (object created, deleted, etc.)
|
|
// to an external message broker or webhook. At most one event sink may be
|
|
// active at a time. When more than one sink's fields are populated, the
|
|
// first match in the following priority order wins:
|
|
//
|
|
// 1. EventWebhookURL -- HTTP/S webhook
|
|
// 2. KafkaURL -- Apache Kafka
|
|
// 3. NatsURL -- NATS
|
|
// 4. RabbitmqURL -- RabbitMQ
|
|
//
|
|
// Configuring event notifications is optional. When none of the trigger
|
|
// fields above are set, event notifications are disabled.
|
|
//
|
|
// EventConfigFilePath applies to whichever sink is active and can be set
|
|
// regardless of which sink is chosen.
|
|
|
|
// KafkaURL is the broker URL for Kafka event notifications
|
|
// (e.g. "kafka://broker:9092").
|
|
KafkaURL string
|
|
// KafkaTopic is the Kafka topic name for bucket event messages.
|
|
KafkaTopic string
|
|
// KafkaKey is the optional Kafka message key.
|
|
KafkaKey string
|
|
// NatsURL is the NATS server URL for event notifications
|
|
// (e.g. "nats://localhost:4222").
|
|
NatsURL string
|
|
// NatsTopic is the NATS subject for bucket event messages.
|
|
NatsTopic string
|
|
// RabbitmqURL is the RabbitMQ connection URL
|
|
// (e.g. "amqp://user:pass@rabbitmq:5672/").
|
|
RabbitmqURL string
|
|
// RabbitmqExchange is the RabbitMQ exchange to publish events to.
|
|
// Leave empty to use the default exchange.
|
|
RabbitmqExchange string
|
|
// RabbitmqRoutingKey is the routing key for RabbitMQ event messages.
|
|
// Leave empty to use no routing key.
|
|
RabbitmqRoutingKey string
|
|
// EventWebhookURL is an HTTP(S) URL that receives bucket event
|
|
// notifications as POST requests.
|
|
EventWebhookURL string
|
|
// EventConfigFilePath is the path to a JSON event filter configuration
|
|
// file that controls which events are forwarded to the active event sink.
|
|
// When empty, all events are forwarded. Generate a default config with:
|
|
// versitygw utils gen-event-filter-config --path <dir>
|
|
EventConfigFilePath string
|
|
|
|
// WebUI
|
|
//
|
|
// The browser-based management WebUI can be served in two independent
|
|
// modes, which may be enabled simultaneously:
|
|
//
|
|
// - Standalone server (WebuiPorts): the WebUI runs on its own dedicated
|
|
// listening address(es), separate from the S3 endpoint.
|
|
//
|
|
// - Embedded on the S3 endpoint (WebuiS3Prefix): the WebUI is served
|
|
// directly from the S3 port under a URL path prefix. Useful when only
|
|
// one listening port is available.
|
|
//
|
|
// Both modes are optional. Leave WebuiPorts empty and WebuiS3Prefix empty
|
|
// to disable the WebUI entirely.
|
|
|
|
// WebuiPorts is the list of listening addresses for the standalone WebUI
|
|
// server. Accepts the same formats as Ports. When empty, the WebUI server
|
|
// is disabled.
|
|
WebuiPorts []string
|
|
// WebuiCertFile is the path to the TLS certificate for the WebUI server.
|
|
// When empty and gateway TLS (CertFile/KeyFile) is configured, the WebUI
|
|
// inherits those certs. Both WebuiCertFile and WebuiKeyFile must be
|
|
// provided together.
|
|
WebuiCertFile string
|
|
// WebuiKeyFile is the path to the TLS private key for the WebUI server.
|
|
WebuiKeyFile string
|
|
// WebuiNoTLS forces the WebUI to use plain HTTP even when TLS certificates
|
|
// are available. Useful when TLS is terminated by a reverse proxy in front
|
|
// of the WebUI.
|
|
WebuiNoTLS bool
|
|
// WebuiGateways overrides the S3 gateway URLs provided to the WebUI. By
|
|
// default the gateway auto-detects URLs from Ports. Set this when running
|
|
// behind a reverse proxy or load balancer where the auto-detected URLs are
|
|
// incorrect (e.g. ["https://s3.example.com", "http://192.168.1.1:7070"]).
|
|
WebuiGateways []string
|
|
// WebuiAdminGateways overrides the admin gateway URLs provided to the
|
|
// WebUI. By default the gateway auto-detects URLs from AdminPorts, or
|
|
// reuses WebuiGateways when AdminPorts is empty.
|
|
WebuiAdminGateways []string
|
|
// WebuiPathPrefix is the URL path prefix under which the WebUI and its
|
|
// API endpoints are served (e.g. "/ui"). Must start with "/" and be a
|
|
// single path segment with no trailing slash. Leave empty to serve from
|
|
// the root path.
|
|
WebuiPathPrefix string
|
|
|
|
// WebuiS3Prefix mounts the WebUI directly on the S3 API endpoint at the
|
|
// given path prefix (e.g. "/ui"). Requests matching the prefix are routed
|
|
// to the WebUI instead of S3. Any bucket whose name equals the prefix
|
|
// segment is masked. Leave empty to disable WebUI hosting on the S3
|
|
// endpoint.
|
|
WebuiS3Prefix string
|
|
|
|
// Static website hosting endpoint
|
|
//
|
|
// WebsitePorts is the list of listening addresses for the static website
|
|
// hosting endpoint. Accepts the same formats as Ports. When empty, the
|
|
// website endpoint is disabled.
|
|
WebsitePorts []string
|
|
// WebsiteDomain is the base domain for website virtual-host routing. For
|
|
// example, host "blog.example.com" serves bucket "blog" when this is
|
|
// "example.com". When empty, the full request hostname is used as the
|
|
// bucket name.
|
|
WebsiteDomain string
|
|
// WebsiteCertFile is the path to the TLS certificate for the website
|
|
// endpoint. When empty and gateway TLS (CertFile/KeyFile) is configured,
|
|
// the website endpoint inherits those certs. Both WebsiteCertFile and
|
|
// WebsiteKeyFile must be provided together.
|
|
WebsiteCertFile string
|
|
// WebsiteKeyFile is the path to the TLS private key for the website
|
|
// endpoint.
|
|
WebsiteKeyFile string
|
|
// WebsiteNoTLS forces the website endpoint to use plain HTTP even when TLS
|
|
// certificates are available.
|
|
WebsiteNoTLS bool
|
|
|
|
// SigHup is an optional channel that signals the gateway to reload TLS
|
|
// certificates and rotate log files (equivalent to SIGHUP). When nil,
|
|
// this feature is disabled.
|
|
SigHup <-chan struct{}
|
|
|
|
// S3Options are appended to the internally built s3api options before the
|
|
// S3 server is created. This allows callers to inject custom behavior such
|
|
// as request middleware.
|
|
S3Options []s3api.Option
|
|
|
|
// Version, Build, and BuildTime are displayed in the startup banner.
|
|
// All three are optional; omit or leave empty to suppress the field.
|
|
Version string
|
|
Build string
|
|
BuildTime string
|
|
}
|
|
|
|
// TODO: remove gatewayRunning once package-level globals (bucket-name
|
|
// validation, debug logging) are eliminated and concurrent calls are safe.
|
|
var gatewayRunning atomic.Bool
|
|
|
|
// RunVersityGW starts the VersityGW gateway with the supplied backend and
|
|
// configuration. It blocks until ctx is cancelled, or an error occurs. All
|
|
// subsystems are gracefully shut down before the function returns.
|
|
//
|
|
// Only one instance may run per process at a time. Calling RunVersityGW
|
|
// concurrently or a second time before the first call returns will return an
|
|
// error.
|
|
func RunVersityGW(ctx context.Context, be backend.Backend, cfg *Config) error {
|
|
if !gatewayRunning.CompareAndSwap(false, true) {
|
|
return fmt.Errorf("embedgw: RunVersityGW is already running; only one instance per process is supported")
|
|
}
|
|
defer gatewayRunning.Store(false)
|
|
|
|
if cfg.RootUserAccess == "" || cfg.RootUserSecret == "" {
|
|
return fmt.Errorf("root user access and secret key must be provided")
|
|
}
|
|
|
|
err := validateWebUIPathPrefix("WebuiPathPrefix", cfg.WebuiPathPrefix)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cfg.MaxConnections < 1 {
|
|
return fmt.Errorf("max-connections must be positive")
|
|
}
|
|
if cfg.MaxRequests < 1 {
|
|
return fmt.Errorf("max-requests must be positive")
|
|
}
|
|
if cfg.MaxRequests > cfg.MaxConnections {
|
|
log.Printf("WARNING: max-requests (%d) exceeds max-connections (%d) which could allow for gateway to panic before throttling requests",
|
|
cfg.MaxRequests, cfg.MaxConnections)
|
|
}
|
|
if cfg.MultipartMaxParts < 1 {
|
|
return fmt.Errorf("mp-max-parts must be positive")
|
|
}
|
|
|
|
if len(cfg.Ports) == 0 {
|
|
return fmt.Errorf("no ports specified")
|
|
}
|
|
|
|
if cfg.Region == "" {
|
|
cfg.Region = awsDefaultRegion
|
|
}
|
|
|
|
// WebUI runs in a browser and typically talks to the gateway/admin APIs cross-origin
|
|
// (different port). If no bucket CORS configuration exists, those API responses need
|
|
// a default Access-Control-Allow-Origin to be usable from the WebUI.
|
|
corsAllowOrigin := cfg.CORSAllowOrigin
|
|
if len(cfg.WebuiPorts) > 0 && strings.TrimSpace(corsAllowOrigin) == "" {
|
|
corsAllowOrigin = "*"
|
|
webuiScheme := "http"
|
|
if !cfg.WebuiNoTLS && (strings.TrimSpace(cfg.WebuiCertFile) != "" || strings.TrimSpace(cfg.CertFile) != "") {
|
|
webuiScheme = "https"
|
|
}
|
|
|
|
var suggestion string
|
|
var allOrigins []string
|
|
for _, addr := range cfg.WebuiPorts {
|
|
ips, ipsErr := getMatchingIPs(addr)
|
|
_, webPrt, prtErr := net.SplitHostPort(addr)
|
|
if ipsErr == nil && prtErr == nil && len(ips) > 0 {
|
|
for _, ip := range ips {
|
|
allOrigins = append(allOrigins, fmt.Sprintf("%s://%s:%s", webuiScheme, ip, webPrt))
|
|
}
|
|
}
|
|
}
|
|
if len(allOrigins) > 0 {
|
|
suggestion = fmt.Sprintf("consider setting it to one of: %s (or your public hostname)", strings.Join(allOrigins, ", "))
|
|
} else {
|
|
suggestion = fmt.Sprintf("consider setting it to %s://<host>:<port>", webuiScheme)
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "WARNING: WebuiPorts is set but CORSAllowOrigin is not; defaulting to '*'; %s\n", suggestion)
|
|
}
|
|
|
|
if err := validatePortConflicts(cfg.Ports, cfg.AdminPorts, cfg.WebuiPorts, cfg.WebsitePorts); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := validateWebUIPathPrefix("WebuiS3Prefix", cfg.WebuiS3Prefix); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Pre-validate gateway URL lists once; both the WebuiS3Prefix block and the
|
|
// WebuiPorts block need these, so validate here to avoid doing it twice.
|
|
var validatedWebuiGateways []string
|
|
if len(cfg.WebuiGateways) > 0 {
|
|
validatedWebuiGateways, err = validateGatewayURLs(cfg.WebuiGateways, "WebuiGateways")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var validatedWebuiAdminGateways []string
|
|
if len(cfg.WebuiAdminGateways) > 0 {
|
|
validatedWebuiAdminGateways, err = validateGatewayURLs(cfg.WebuiAdminGateways, "WebuiAdminGateways")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
utils.SetBucketNameValidationStrict(!cfg.DisableStrictBucketNames)
|
|
|
|
var parsedSocketPerm os.FileMode
|
|
if cfg.SocketPerm != "" {
|
|
perm, err := strconv.ParseUint(cfg.SocketPerm, 8, 32)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid SocketPerm value %q: must be an octal integer (e.g. '0660'): %w", cfg.SocketPerm, err)
|
|
}
|
|
parsedSocketPerm = os.FileMode(perm)
|
|
}
|
|
|
|
opts := []s3api.Option{
|
|
s3api.WithConcurrencyLimiter(cfg.MaxConnections, cfg.MaxRequests),
|
|
s3api.WithMpMaxParts(cfg.MultipartMaxParts),
|
|
}
|
|
if cfg.SocketPerm != "" {
|
|
opts = append(opts, s3api.WithSocketPerm(parsedSocketPerm))
|
|
}
|
|
if corsAllowOrigin != "" {
|
|
opts = append(opts, s3api.WithCORSAllowOrigin(corsAllowOrigin))
|
|
}
|
|
|
|
if cfg.CertFile != "" || cfg.KeyFile != "" {
|
|
if cfg.CertFile == "" {
|
|
return fmt.Errorf("TLS key specified without cert file")
|
|
}
|
|
if cfg.KeyFile == "" {
|
|
return fmt.Errorf("TLS cert specified without key file")
|
|
}
|
|
cs := utils.NewCertStorage()
|
|
if err := cs.SetCertificate(cfg.CertFile, cfg.KeyFile); err != nil {
|
|
return fmt.Errorf("tls: load certs: %v", err)
|
|
}
|
|
opts = append(opts, s3api.WithTLS(cs))
|
|
}
|
|
if len(cfg.AdminPorts) == 0 {
|
|
opts = append(opts, s3api.WithAdminServer())
|
|
}
|
|
if cfg.Quiet {
|
|
opts = append(opts, s3api.WithQuiet())
|
|
}
|
|
if cfg.HealthPath != "" {
|
|
opts = append(opts, s3api.WithHealth(cfg.HealthPath))
|
|
}
|
|
if cfg.Readonly {
|
|
opts = append(opts, s3api.WithReadOnly())
|
|
}
|
|
if cfg.VirtualDomain != "" {
|
|
opts = append(opts, s3api.WithHostStyle(cfg.VirtualDomain))
|
|
}
|
|
if cfg.KeepAlive {
|
|
opts = append(opts, s3api.WithKeepAlive())
|
|
}
|
|
if cfg.DisableACLs {
|
|
opts = append(opts, s3api.WithDisableACL())
|
|
}
|
|
if len(cfg.S3Options) > 0 {
|
|
opts = append(opts, cfg.S3Options...)
|
|
}
|
|
if cfg.Debug {
|
|
debuglogger.SetDebugEnabled()
|
|
}
|
|
if cfg.IAMDebug {
|
|
debuglogger.SetIAMDebugEnabled()
|
|
}
|
|
|
|
iam, err := auth.New(&auth.Opts{
|
|
RootAccount: auth.Account{
|
|
Access: cfg.RootUserAccess,
|
|
Secret: cfg.RootUserSecret,
|
|
Role: auth.RoleAdmin,
|
|
},
|
|
Dir: cfg.IAMDir,
|
|
LDAPServerURL: cfg.LDAPServerURL,
|
|
LDAPBindDN: cfg.LDAPBindDN,
|
|
LDAPPassword: cfg.LDAPPassword,
|
|
LDAPQueryBase: cfg.LDAPQueryBase,
|
|
LDAPObjClasses: cfg.LDAPObjClasses,
|
|
LDAPAccessAtr: cfg.LDAPAccessAttr,
|
|
LDAPSecretAtr: cfg.LDAPSecretAttr,
|
|
LDAPRoleAtr: cfg.LDAPRoleAttr,
|
|
LDAPUserIdAtr: cfg.LDAPUserIDAttr,
|
|
LDAPGroupIdAtr: cfg.LDAPGroupIDAttr,
|
|
LDAPProjectIdAtr: cfg.LDAPProjectIDAttr,
|
|
LDAPTLSSkipVerify: cfg.LDAPTLSSkipVerify,
|
|
VaultEndpointURL: cfg.VaultEndpointURL,
|
|
VaultNamespace: cfg.VaultNamespace,
|
|
VaultSecretStoragePath: cfg.VaultSecretStoragePath,
|
|
VaultSecretStorageNamespace: cfg.VaultSecretStorageNamespace,
|
|
VaultAuthMethod: cfg.VaultAuthMethod,
|
|
VaultAuthNamespace: cfg.VaultAuthNamespace,
|
|
VaultMountPath: cfg.VaultMountPath,
|
|
VaultRootToken: cfg.VaultRootToken,
|
|
VaultRoleId: cfg.VaultRoleID,
|
|
VaultRoleSecret: cfg.VaultRoleSecret,
|
|
VaultServerCert: cfg.VaultServerCert,
|
|
VaultClientCert: cfg.VaultClientCert,
|
|
VaultClientCertKey: cfg.VaultClientCertKey,
|
|
S3Access: cfg.S3IAMAccess,
|
|
S3Secret: cfg.S3IAMSecret,
|
|
S3Region: cfg.S3IAMRegion,
|
|
S3Bucket: cfg.S3IAMBucket,
|
|
S3Endpoint: cfg.S3IAMEndpoint,
|
|
S3DisableSSlVerfiy: cfg.S3IAMDisableSSLVerify,
|
|
CacheDisable: cfg.IAMCacheDisable,
|
|
CacheTTL: cfg.IAMCacheTTL,
|
|
CachePrune: cfg.IAMCachePrune,
|
|
IpaHost: cfg.IpaHost,
|
|
IpaVaultName: cfg.IpaVaultName,
|
|
IpaUser: cfg.IpaUser,
|
|
IpaPassword: cfg.IpaPassword,
|
|
IpaInsecure: cfg.IpaInsecure,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("setup iam: %w", err)
|
|
}
|
|
|
|
loggers, err := s3log.InitLogger(&s3log.LogConfig{
|
|
LogFile: cfg.AccessLog,
|
|
WebhookURL: cfg.LogWebhookURL,
|
|
AdminLogFile: cfg.AdminLogFile,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("setup logger: %w", err)
|
|
}
|
|
|
|
metricsManager, err := metrics.NewManager(ctx, metrics.Config{
|
|
ServiceName: cfg.MetricsService,
|
|
StatsdServers: cfg.StatsdServers,
|
|
DogStatsdServers: cfg.DogstatsServers,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("init metrics manager: %w", err)
|
|
}
|
|
|
|
evSender, err := s3event.InitEventSender(&s3event.EventConfig{
|
|
KafkaURL: cfg.KafkaURL,
|
|
KafkaTopic: cfg.KafkaTopic,
|
|
KafkaTopicKey: cfg.KafkaKey,
|
|
NatsURL: cfg.NatsURL,
|
|
NatsTopic: cfg.NatsTopic,
|
|
RabbitmqURL: cfg.RabbitmqURL,
|
|
RabbitmqExchange: cfg.RabbitmqExchange,
|
|
RabbitmqRoutingKey: cfg.RabbitmqRoutingKey,
|
|
WebhookURL: cfg.EventWebhookURL,
|
|
FilterConfigFilePath: cfg.EventConfigFilePath,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("init bucket event notifications: %w", err)
|
|
}
|
|
|
|
if cfg.WebuiS3Prefix != "" {
|
|
s3SSLEnabled := cfg.CertFile != ""
|
|
s3AdmSSLEnabled := s3SSLEnabled
|
|
if len(cfg.AdminPorts) > 0 {
|
|
s3AdmSSLEnabled = cfg.AdminCertFile != ""
|
|
}
|
|
|
|
var s3WebGateways []string
|
|
if len(validatedWebuiGateways) > 0 {
|
|
s3WebGateways = validatedWebuiGateways
|
|
} else {
|
|
for _, p := range cfg.Ports {
|
|
urls, err := buildServiceURLs(p, s3SSLEnabled)
|
|
if err != nil {
|
|
return fmt.Errorf("webui-s3-prefix: build gateway URLs: %w", err)
|
|
}
|
|
s3WebGateways = append(s3WebGateways, urls...)
|
|
}
|
|
sortGatewayURLs(s3WebGateways)
|
|
}
|
|
|
|
s3WebAdminGateways := s3WebGateways
|
|
if len(validatedWebuiAdminGateways) > 0 {
|
|
s3WebAdminGateways = validatedWebuiAdminGateways
|
|
} else if len(cfg.AdminPorts) > 0 {
|
|
s3WebAdminGateways = nil
|
|
for _, admPort := range cfg.AdminPorts {
|
|
urls, err := buildServiceURLs(admPort, s3AdmSSLEnabled)
|
|
if err != nil {
|
|
return fmt.Errorf("webui-s3-prefix: build admin gateway URLs: %w", err)
|
|
}
|
|
s3WebAdminGateways = append(s3WebAdminGateways, urls...)
|
|
}
|
|
sortGatewayURLs(s3WebAdminGateways)
|
|
}
|
|
|
|
opts = append(opts, s3api.WithWebUI(cfg.WebuiS3Prefix, &webui.ServerConfig{
|
|
Gateways: s3WebGateways,
|
|
AdminGateways: s3WebAdminGateways,
|
|
Region: cfg.Region,
|
|
}))
|
|
}
|
|
|
|
srv, err := s3api.New(be, middlewares.RootUserConfig{
|
|
Access: cfg.RootUserAccess,
|
|
Secret: cfg.RootUserSecret,
|
|
}, cfg.Region, iam, loggers.S3Logger, loggers.AdminLogger, evSender, metricsManager, opts...)
|
|
if err != nil {
|
|
return fmt.Errorf("init gateway: %v", err)
|
|
}
|
|
|
|
var admSrv *s3api.S3AdminServer
|
|
|
|
if len(cfg.AdminPorts) > 0 {
|
|
if cfg.AdminMaxConnections < 1 {
|
|
return fmt.Errorf("admin-max-connections must be positive")
|
|
}
|
|
if cfg.AdminMaxRequests < 1 {
|
|
return fmt.Errorf("admin-max-requests must be positive")
|
|
}
|
|
if cfg.AdminMaxRequests > cfg.AdminMaxConnections {
|
|
log.Printf("WARNING: admin-max-requests (%d) exceeds admin-max-connections (%d) which could allow for gateway to panic before throttling requests",
|
|
cfg.AdminMaxRequests, cfg.AdminMaxConnections)
|
|
}
|
|
|
|
admOpts := []s3api.AdminOpt{
|
|
s3api.WithAdminConcurrencyLimiter(cfg.AdminMaxConnections, cfg.AdminMaxRequests),
|
|
}
|
|
|
|
if corsAllowOrigin != "" {
|
|
admOpts = append(admOpts, s3api.WithAdminCORSAllowOrigin(corsAllowOrigin))
|
|
}
|
|
|
|
if cfg.AdminCertFile != "" || cfg.AdminKeyFile != "" {
|
|
if cfg.AdminCertFile == "" {
|
|
return fmt.Errorf("TLS key specified without cert file")
|
|
}
|
|
if cfg.AdminKeyFile == "" {
|
|
return fmt.Errorf("TLS cert specified without key file")
|
|
}
|
|
cs := utils.NewCertStorage()
|
|
if err = cs.SetCertificate(cfg.AdminCertFile, cfg.AdminKeyFile); err != nil {
|
|
return fmt.Errorf("tls: load certs: %v", err)
|
|
}
|
|
admOpts = append(admOpts, s3api.WithAdminSrvTLS(cs))
|
|
}
|
|
if cfg.Quiet {
|
|
admOpts = append(admOpts, s3api.WithAdminQuiet())
|
|
}
|
|
if cfg.Debug {
|
|
admOpts = append(admOpts, s3api.WithAdminDebug())
|
|
}
|
|
if cfg.SocketPerm != "" {
|
|
admOpts = append(admOpts, s3api.WithAdminSocketPerm(parsedSocketPerm))
|
|
}
|
|
|
|
admSrv = s3api.NewAdminServer(be, middlewares.RootUserConfig{Access: cfg.RootUserAccess, Secret: cfg.RootUserSecret}, cfg.Region, iam, loggers.AdminLogger, srv.Router.Ctrl, admOpts...)
|
|
}
|
|
|
|
var webSrv *webui.Server
|
|
webTLSCert := ""
|
|
webTLSKey := ""
|
|
if len(cfg.WebuiPorts) > 0 {
|
|
for _, addr := range cfg.WebuiPorts {
|
|
if utils.IsUnixSocketPath(addr) {
|
|
continue
|
|
}
|
|
_, webPrt, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
return fmt.Errorf("webui listen address must be in the form ':port' or 'host:port': %w", err)
|
|
}
|
|
webPortNum, err := strconv.Atoi(webPrt)
|
|
if err != nil {
|
|
return fmt.Errorf("webui port must be a number: %w", err)
|
|
}
|
|
if webPortNum < 0 || webPortNum > 65535 {
|
|
return fmt.Errorf("webui port must be between 0 and 65535")
|
|
}
|
|
}
|
|
|
|
var webOpts []webui.Option
|
|
if !cfg.WebuiNoTLS {
|
|
webTLSCert = cfg.WebuiCertFile
|
|
webTLSKey = cfg.WebuiKeyFile
|
|
if webTLSCert == "" && webTLSKey == "" {
|
|
webTLSCert = cfg.CertFile
|
|
webTLSKey = cfg.KeyFile
|
|
}
|
|
if webTLSCert != "" || webTLSKey != "" {
|
|
if webTLSCert == "" {
|
|
return fmt.Errorf("webui TLS key specified without cert file")
|
|
}
|
|
if webTLSKey == "" {
|
|
return fmt.Errorf("webui TLS cert specified without key file")
|
|
}
|
|
cs := utils.NewCertStorage()
|
|
if err := cs.SetCertificate(webTLSCert, webTLSKey); err != nil {
|
|
return fmt.Errorf("tls: load certs: %v", err)
|
|
}
|
|
webOpts = append(webOpts, webui.WithTLS(cs))
|
|
}
|
|
}
|
|
|
|
sslEnabled := cfg.CertFile != ""
|
|
admSSLEnabled := sslEnabled
|
|
if len(cfg.AdminPorts) > 0 {
|
|
admSSLEnabled = cfg.AdminCertFile != ""
|
|
}
|
|
|
|
var gateways []string
|
|
if len(validatedWebuiGateways) > 0 {
|
|
gateways = validatedWebuiGateways
|
|
} else {
|
|
for _, p := range cfg.Ports {
|
|
urls, err := buildServiceURLs(p, sslEnabled)
|
|
if err != nil {
|
|
return fmt.Errorf("webui: build gateway URLs: %w", err)
|
|
}
|
|
gateways = append(gateways, urls...)
|
|
}
|
|
sortGatewayURLs(gateways)
|
|
}
|
|
|
|
adminGateways := gateways
|
|
if len(validatedWebuiAdminGateways) > 0 {
|
|
adminGateways = validatedWebuiAdminGateways
|
|
} else if len(cfg.AdminPorts) > 0 {
|
|
adminGateways = nil
|
|
for _, admPort := range cfg.AdminPorts {
|
|
urls, err := buildServiceURLs(admPort, admSSLEnabled)
|
|
if err != nil {
|
|
return fmt.Errorf("webui: build admin gateway URLs: %w", err)
|
|
}
|
|
adminGateways = append(adminGateways, urls...)
|
|
}
|
|
sortGatewayURLs(adminGateways)
|
|
}
|
|
|
|
if cfg.Quiet {
|
|
webOpts = append(webOpts, webui.WithQuiet())
|
|
}
|
|
if cfg.WebuiPathPrefix != "" {
|
|
webOpts = append(webOpts, webui.WithPathPrefix(cfg.WebuiPathPrefix))
|
|
}
|
|
if cfg.SocketPerm != "" {
|
|
webOpts = append(webOpts, webui.WithSocketPerm(parsedSocketPerm))
|
|
}
|
|
|
|
webSrv, err = webui.NewServer(&webui.ServerConfig{
|
|
Gateways: gateways,
|
|
AdminGateways: adminGateways,
|
|
Region: cfg.Region,
|
|
}, webOpts...)
|
|
if err != nil {
|
|
return fmt.Errorf("init webui: %w", err)
|
|
}
|
|
}
|
|
|
|
var wsSrv *website.Server
|
|
wsTLSCert := ""
|
|
wsTLSKey := ""
|
|
if len(cfg.WebsitePorts) > 0 {
|
|
for _, addr := range cfg.WebsitePorts {
|
|
if utils.IsUnixSocketPath(addr) {
|
|
continue
|
|
}
|
|
_, wsPrt, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
return fmt.Errorf("website listen address must be in the form ':port' or 'host:port': %w", err)
|
|
}
|
|
wsPortNum, err := strconv.Atoi(wsPrt)
|
|
if err != nil {
|
|
return fmt.Errorf("website port must be a number: %w", err)
|
|
}
|
|
if wsPortNum < 0 || wsPortNum > 65535 {
|
|
return fmt.Errorf("website port must be between 0 and 65535")
|
|
}
|
|
}
|
|
|
|
var wsOpts []website.Option
|
|
if !cfg.WebsiteNoTLS {
|
|
wsTLSCert = cfg.WebsiteCertFile
|
|
wsTLSKey = cfg.WebsiteKeyFile
|
|
if wsTLSCert == "" && wsTLSKey == "" {
|
|
wsTLSCert = cfg.CertFile
|
|
wsTLSKey = cfg.KeyFile
|
|
}
|
|
if wsTLSCert != "" || wsTLSKey != "" {
|
|
if wsTLSCert == "" {
|
|
return fmt.Errorf("website TLS key specified without cert file")
|
|
}
|
|
if wsTLSKey == "" {
|
|
return fmt.Errorf("website TLS cert specified without key file")
|
|
}
|
|
cs := utils.NewCertStorage()
|
|
if err := cs.SetCertificate(wsTLSCert, wsTLSKey); err != nil {
|
|
return fmt.Errorf("tls: load certs: %v", err)
|
|
}
|
|
wsOpts = append(wsOpts, website.WithTLS(cs))
|
|
}
|
|
}
|
|
|
|
if cfg.Quiet {
|
|
wsOpts = append(wsOpts, website.WithQuiet())
|
|
}
|
|
if cfg.SocketPerm != "" {
|
|
wsOpts = append(wsOpts, website.WithSocketPerm(parsedSocketPerm))
|
|
}
|
|
|
|
wsSrv = website.NewServer(be, cfg.WebsiteDomain, wsOpts...)
|
|
}
|
|
|
|
if !cfg.Quiet {
|
|
cfg.printBanner()
|
|
}
|
|
|
|
servers := 1
|
|
if len(cfg.AdminPorts) > 0 {
|
|
servers++
|
|
}
|
|
if len(cfg.WebuiPorts) > 0 {
|
|
servers++
|
|
}
|
|
if len(cfg.WebsitePorts) > 0 {
|
|
servers++
|
|
}
|
|
|
|
c := make(chan error, servers)
|
|
go func() { c <- srv.ServeMultiPort(cfg.Ports) }()
|
|
if len(cfg.AdminPorts) > 0 {
|
|
go func() { c <- admSrv.ServeMultiPort(cfg.AdminPorts) }()
|
|
}
|
|
if len(cfg.WebuiPorts) > 0 {
|
|
go func() { c <- webSrv.ServeMultiPort(cfg.WebuiPorts) }()
|
|
}
|
|
if len(cfg.WebsitePorts) > 0 {
|
|
go func() { c <- wsSrv.ServeMultiPort(cfg.WebsitePorts) }()
|
|
}
|
|
|
|
// build a nil-safe sighup channel so the select below is always valid
|
|
var sigHup <-chan struct{}
|
|
if cfg.SigHup != nil {
|
|
sigHup = cfg.SigHup
|
|
} else {
|
|
sigHup = make(chan struct{}) // never receives
|
|
}
|
|
|
|
Loop:
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
break Loop
|
|
case err = <-c:
|
|
break Loop
|
|
case <-sigHup:
|
|
if loggers.S3Logger != nil {
|
|
err = loggers.S3Logger.HangUp()
|
|
if err != nil {
|
|
err = fmt.Errorf("HUP s3 logger: %w", err)
|
|
break Loop
|
|
}
|
|
}
|
|
if loggers.AdminLogger != nil {
|
|
err = loggers.AdminLogger.HangUp()
|
|
if err != nil {
|
|
err = fmt.Errorf("HUP admin logger: %w", err)
|
|
break Loop
|
|
}
|
|
}
|
|
if cfg.CertFile != "" && cfg.KeyFile != "" {
|
|
reloadErr := srv.CertStorage.SetCertificate(cfg.CertFile, cfg.KeyFile)
|
|
if reloadErr != nil {
|
|
debuglogger.InternalError(fmt.Errorf("srv cert reload failed: %w", reloadErr))
|
|
} else {
|
|
fmt.Printf("srv cert reloaded (cert: %s, key: %s)\n", cfg.CertFile, cfg.KeyFile)
|
|
}
|
|
}
|
|
if len(cfg.AdminPorts) > 0 && cfg.AdminCertFile != "" && cfg.AdminKeyFile != "" {
|
|
reloadErr := admSrv.CertStorage.SetCertificate(cfg.AdminCertFile, cfg.AdminKeyFile)
|
|
if reloadErr != nil {
|
|
debuglogger.InternalError(fmt.Errorf("admSrv cert reload failed: %w", reloadErr))
|
|
} else {
|
|
fmt.Printf("admSrv cert reloaded (cert: %s, key: %s)\n", cfg.AdminCertFile, cfg.AdminKeyFile)
|
|
}
|
|
}
|
|
if len(cfg.WebuiPorts) > 0 && webTLSCert != "" && webTLSKey != "" {
|
|
reloadErr := webSrv.CertStorage.SetCertificate(webTLSCert, webTLSKey)
|
|
if reloadErr != nil {
|
|
debuglogger.InternalError(fmt.Errorf("webSrv cert reload failed: %w", reloadErr))
|
|
} else {
|
|
fmt.Printf("webSrv cert reloaded (cert: %s, key: %s)\n", webTLSCert, webTLSKey)
|
|
}
|
|
}
|
|
if len(cfg.WebsitePorts) > 0 && wsTLSCert != "" && wsTLSKey != "" {
|
|
reloadErr := wsSrv.CertStorage.SetCertificate(wsTLSCert, wsTLSKey)
|
|
if reloadErr != nil {
|
|
debuglogger.InternalError(fmt.Errorf("wsSrv cert reload failed: %w", reloadErr))
|
|
} else {
|
|
fmt.Printf("wsSrv cert reloaded (cert: %s, key: %s)\n", wsTLSCert, wsTLSKey)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
saveErr := err
|
|
|
|
err = srv.ShutDown()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "shutdown api server: %v\n", err)
|
|
}
|
|
|
|
if admSrv != nil {
|
|
err := admSrv.Shutdown()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "shutdown admin server: %v\n", err)
|
|
}
|
|
}
|
|
|
|
if webSrv != nil {
|
|
err := webSrv.Shutdown()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "shutdown webui server: %v\n", err)
|
|
}
|
|
}
|
|
|
|
if wsSrv != nil {
|
|
err := wsSrv.Shutdown()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "shutdown website server: %v\n", err)
|
|
}
|
|
}
|
|
|
|
be.Shutdown()
|
|
|
|
err = iam.Shutdown()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "shutdown iam: %v\n", err)
|
|
}
|
|
|
|
if loggers.S3Logger != nil {
|
|
err := loggers.S3Logger.Shutdown()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "shutdown s3 logger: %v\n", err)
|
|
}
|
|
}
|
|
if loggers.AdminLogger != nil {
|
|
err := loggers.AdminLogger.Shutdown()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "shutdown admin logger: %v\n", err)
|
|
}
|
|
}
|
|
|
|
if evSender != nil {
|
|
err := evSender.Close()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "close event sender: %v\n", err)
|
|
}
|
|
}
|
|
|
|
if metricsManager != nil {
|
|
metricsManager.Close()
|
|
}
|
|
|
|
return saveErr
|
|
}
|
|
|
|
const (
|
|
columnWidth = 70
|
|
title = "VersityGW"
|
|
)
|
|
|
|
func (cfg Config) printBanner() {
|
|
ssl := cfg.CertFile != "" || cfg.KeyFile != ""
|
|
admSSL := cfg.AdminCertFile != "" || cfg.AdminKeyFile != ""
|
|
webuiSsl := !cfg.WebuiNoTLS && (cfg.WebuiCertFile != "" || cfg.WebuiKeyFile != "" || cfg.CertFile != "" || cfg.KeyFile != "")
|
|
websiteSsl := !cfg.WebsiteNoTLS && (cfg.WebsiteCertFile != "" || cfg.WebsiteKeyFile != "" || cfg.CertFile != "" || cfg.KeyFile != "")
|
|
|
|
if len(cfg.Ports) == 0 {
|
|
fmt.Fprintf(os.Stderr, "No ports specified\n")
|
|
return
|
|
}
|
|
|
|
var allInterfaces []string
|
|
var allPorts []string
|
|
interfaceMap := make(map[string]bool)
|
|
|
|
for _, portSpec := range cfg.Ports {
|
|
if utils.IsUnixSocketPath(portSpec) {
|
|
allPorts = append(allPorts, portSpec)
|
|
if !interfaceMap[portSpec] {
|
|
interfaceMap[portSpec] = true
|
|
allInterfaces = append(allInterfaces, portSpec)
|
|
}
|
|
continue
|
|
}
|
|
interfaces, err := getMatchingIPs(portSpec)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to match local IP addresses for %s: %v\n", portSpec, err)
|
|
continue
|
|
}
|
|
_, prt, err := net.SplitHostPort(portSpec)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to parse port %s: %v\n", portSpec, err)
|
|
continue
|
|
}
|
|
allPorts = append(allPorts, prt)
|
|
for _, ip := range interfaces {
|
|
key := net.JoinHostPort(ip, prt)
|
|
if !interfaceMap[key] {
|
|
interfaceMap[key] = true
|
|
allInterfaces = append(allInterfaces, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(allInterfaces) == 0 {
|
|
fmt.Fprintf(os.Stderr, "Failed to resolve any listening addresses\n")
|
|
return
|
|
}
|
|
|
|
var allAdmInterfaces []string
|
|
admInterfaceMap := make(map[string]bool)
|
|
for _, admPort := range cfg.AdminPorts {
|
|
if utils.IsUnixSocketPath(admPort) {
|
|
if !admInterfaceMap[admPort] {
|
|
admInterfaceMap[admPort] = true
|
|
allAdmInterfaces = append(allAdmInterfaces, admPort)
|
|
}
|
|
continue
|
|
}
|
|
interfaces, err := getMatchingIPs(admPort)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to match admin port local IP addresses for %s: %v\n", admPort, err)
|
|
continue
|
|
}
|
|
_, prt, err := net.SplitHostPort(admPort)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to parse admin port %s: %v\n", admPort, err)
|
|
continue
|
|
}
|
|
for _, ip := range interfaces {
|
|
key := net.JoinHostPort(ip, prt)
|
|
if !admInterfaceMap[key] {
|
|
admInterfaceMap[key] = true
|
|
allAdmInterfaces = append(allAdmInterfaces, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
versionStr := fmt.Sprintf("Version %v, Build %v", cfg.Version, cfg.Build)
|
|
if cfg.BuildTime != "" {
|
|
versionStr += fmt.Sprintf(", BuildTime %v", cfg.BuildTime)
|
|
}
|
|
var urls []string
|
|
|
|
for _, addrPort := range allInterfaces {
|
|
if utils.IsUnixSocketPath(addrPort) {
|
|
urls = append(urls, "unix:"+addrPort)
|
|
continue
|
|
}
|
|
ip, prt, err := net.SplitHostPort(addrPort)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
hostPort := net.JoinHostPort(ip, prt)
|
|
u := fmt.Sprintf("http://%s", hostPort)
|
|
if ssl {
|
|
u = fmt.Sprintf("https://%s", hostPort)
|
|
}
|
|
urls = append(urls, u)
|
|
}
|
|
|
|
var boundHost string
|
|
if len(cfg.Ports) == 1 {
|
|
if utils.IsUnixSocketPath(cfg.Ports[0]) {
|
|
boundHost = fmt.Sprintf("(unix socket: %s)", cfg.Ports[0])
|
|
} else {
|
|
hst, prt, _ := net.SplitHostPort(cfg.Ports[0])
|
|
if hst == "" {
|
|
hst = "0.0.0.0"
|
|
}
|
|
boundHost = fmt.Sprintf("(bound on host %s and port %s)", hst, prt)
|
|
}
|
|
} else {
|
|
portList := strings.Join(allPorts, ", ")
|
|
boundHost = fmt.Sprintf("(bound on ports: %s)", portList)
|
|
}
|
|
|
|
lines := []string{
|
|
centerText(title),
|
|
centerText(versionStr),
|
|
centerText(boundHost),
|
|
centerText(""),
|
|
}
|
|
|
|
if len(allAdmInterfaces) > 0 {
|
|
lines = append(lines, leftText("S3 service listening on:"))
|
|
} else {
|
|
lines = append(lines, leftText("Admin/S3 service listening on:"))
|
|
}
|
|
|
|
for _, u := range urls {
|
|
lines = append(lines, leftText(" "+u))
|
|
}
|
|
|
|
if len(allAdmInterfaces) > 0 {
|
|
lines = append(lines, centerText(""), leftText("Admin service listening on:"))
|
|
for _, addrPort := range allAdmInterfaces {
|
|
if utils.IsUnixSocketPath(addrPort) {
|
|
lines = append(lines, leftText(" unix:"+addrPort))
|
|
continue
|
|
}
|
|
ip, prt, err := net.SplitHostPort(addrPort)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
hostPort := net.JoinHostPort(ip, prt)
|
|
u := fmt.Sprintf("http://%s", hostPort)
|
|
if admSSL {
|
|
u = fmt.Sprintf("https://%s", hostPort)
|
|
}
|
|
lines = append(lines, leftText(" "+u))
|
|
}
|
|
}
|
|
|
|
if len(cfg.WebuiPorts) > 0 {
|
|
var allWebInterfaces []string
|
|
webInterfaceMap := make(map[string]bool)
|
|
|
|
for _, webuiAddr := range cfg.WebuiPorts {
|
|
if strings.TrimSpace(webuiAddr) == "" {
|
|
continue
|
|
}
|
|
if utils.IsUnixSocketPath(webuiAddr) {
|
|
if !webInterfaceMap[webuiAddr] {
|
|
webInterfaceMap[webuiAddr] = true
|
|
allWebInterfaces = append(allWebInterfaces, webuiAddr)
|
|
}
|
|
continue
|
|
}
|
|
webInterfaces, err := getMatchingIPs(webuiAddr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to match webui port local IP addresses for %s: %v\n", webuiAddr, err)
|
|
continue
|
|
}
|
|
_, webPrt, err := net.SplitHostPort(webuiAddr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to parse webui port %s: %v\n", webuiAddr, err)
|
|
continue
|
|
}
|
|
for _, ip := range webInterfaces {
|
|
key := net.JoinHostPort(ip, webPrt)
|
|
if !webInterfaceMap[key] {
|
|
webInterfaceMap[key] = true
|
|
allWebInterfaces = append(allWebInterfaces, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(allWebInterfaces) > 0 {
|
|
lines = append(lines, centerText(""), leftText("WebUI listening on:"))
|
|
for _, addrPort := range allWebInterfaces {
|
|
if utils.IsUnixSocketPath(addrPort) {
|
|
lines = append(lines, leftText(" unix:"+addrPort))
|
|
continue
|
|
}
|
|
ip, prt, err := net.SplitHostPort(addrPort)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
hostPort := net.JoinHostPort(ip, prt)
|
|
u := fmt.Sprintf("http://%s", hostPort)
|
|
if webuiSsl {
|
|
u = fmt.Sprintf("https://%s", hostPort)
|
|
}
|
|
lines = append(lines, leftText(" "+u+cfg.WebuiPathPrefix))
|
|
}
|
|
}
|
|
}
|
|
|
|
if cfg.WebuiS3Prefix != "" {
|
|
lines = append(lines, centerText(""), leftText("WebUI embedded on S3 service at:"))
|
|
for _, addrPort := range allInterfaces {
|
|
ip, prt, err := net.SplitHostPort(addrPort)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
hostPort := net.JoinHostPort(ip, prt)
|
|
u := fmt.Sprintf("http://%s", hostPort)
|
|
if ssl {
|
|
u = fmt.Sprintf("https://%s", hostPort)
|
|
}
|
|
lines = append(lines, leftText(" "+u+cfg.WebuiS3Prefix))
|
|
}
|
|
}
|
|
|
|
if len(cfg.WebsitePorts) > 0 {
|
|
var allWebsiteInterfaces []string
|
|
websiteInterfaceMap := make(map[string]bool)
|
|
|
|
for _, websiteAddr := range cfg.WebsitePorts {
|
|
if strings.TrimSpace(websiteAddr) == "" {
|
|
continue
|
|
}
|
|
if utils.IsUnixSocketPath(websiteAddr) {
|
|
if !websiteInterfaceMap[websiteAddr] {
|
|
websiteInterfaceMap[websiteAddr] = true
|
|
allWebsiteInterfaces = append(allWebsiteInterfaces, websiteAddr)
|
|
}
|
|
continue
|
|
}
|
|
websiteInterfaces, err := getMatchingIPs(websiteAddr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to match website port local IP addresses for %s: %v\n", websiteAddr, err)
|
|
continue
|
|
}
|
|
_, websitePrt, err := net.SplitHostPort(websiteAddr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to parse website port %s: %v\n", websiteAddr, err)
|
|
continue
|
|
}
|
|
for _, ip := range websiteInterfaces {
|
|
key := net.JoinHostPort(ip, websitePrt)
|
|
if !websiteInterfaceMap[key] {
|
|
websiteInterfaceMap[key] = true
|
|
allWebsiteInterfaces = append(allWebsiteInterfaces, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(allWebsiteInterfaces) > 0 {
|
|
domainInfo := ""
|
|
if cfg.WebsiteDomain != "" {
|
|
domainInfo = fmt.Sprintf(" (domain: %s)", cfg.WebsiteDomain)
|
|
}
|
|
lines = append(lines,
|
|
centerText(""),
|
|
leftText("Website endpoint listening on:"+domainInfo),
|
|
)
|
|
for _, addrPort := range allWebsiteInterfaces {
|
|
if utils.IsUnixSocketPath(addrPort) {
|
|
lines = append(lines, leftText(" unix:"+addrPort))
|
|
continue
|
|
}
|
|
ip, prt, err := net.SplitHostPort(addrPort)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
hostPort := net.JoinHostPort(ip, prt)
|
|
u := fmt.Sprintf("http://%s", hostPort)
|
|
if websiteSsl {
|
|
u = fmt.Sprintf("https://%s", hostPort)
|
|
}
|
|
lines = append(lines, leftText(" "+u))
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Println("┌" + strings.Repeat("─", columnWidth-2) + "┐")
|
|
for _, line := range lines {
|
|
fmt.Printf("│%-*s│\n", columnWidth-2, line)
|
|
}
|
|
fmt.Println("└" + strings.Repeat("─", columnWidth-2) + "┘")
|
|
}
|
|
|
|
func centerText(text string) string {
|
|
padding := max((columnWidth-2-len(text))/2, 0)
|
|
return strings.Repeat(" ", padding) + text
|
|
}
|
|
|
|
func leftText(text string) string {
|
|
if len(text) > columnWidth-2 {
|
|
return text
|
|
}
|
|
return text + strings.Repeat(" ", columnWidth-2-len(text))
|
|
}
|
|
|
|
// getMatchingIPs returns all IP addresses that the server will listen on
|
|
// for the given address specification.
|
|
func getMatchingIPs(spec string) ([]string, error) {
|
|
if utils.IsUnixSocketPath(spec) {
|
|
return []string{spec}, nil
|
|
}
|
|
|
|
ips, err := utils.ResolveHostnameIPs(spec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve hostname: %v", err)
|
|
}
|
|
|
|
if len(ips) == 1 && ips[0] == "" {
|
|
return getAllLocalIPs()
|
|
}
|
|
|
|
var result []string
|
|
for _, ip := range ips {
|
|
parsedIP := net.ParseIP(ip)
|
|
if parsedIP == nil {
|
|
continue
|
|
}
|
|
if parsedIP.IsLinkLocalUnicast() || parsedIP.IsLinkLocalMulticast() || parsedIP.IsInterfaceLocalMulticast() {
|
|
continue
|
|
}
|
|
result = append(result, ip)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// getAllLocalIPs returns all non-link-local IP addresses from local interfaces.
|
|
func getAllLocalIPs() ([]string, error) {
|
|
var result []string
|
|
|
|
interfaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, iface := range interfaces {
|
|
addrs, err := iface.Addrs()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, addr := range addrs {
|
|
ipAddr, _, err := net.ParseCIDR(addr.String())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if ipAddr.IsLinkLocalUnicast() || ipAddr.IsInterfaceLocalMulticast() || ipAddr.IsLinkLocalMulticast() {
|
|
continue
|
|
}
|
|
result = append(result, ipAddr.String())
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func buildServiceURLs(spec string, ssl bool) ([]string, error) {
|
|
if utils.IsUnixSocketPath(spec) {
|
|
return nil, nil
|
|
}
|
|
|
|
interfaces, err := getMatchingIPs(spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, prt, err := net.SplitHostPort(spec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse address/port: %w", err)
|
|
}
|
|
if len(interfaces) == 0 {
|
|
interfaces = []string{"localhost"}
|
|
}
|
|
|
|
scheme := "http"
|
|
if ssl {
|
|
scheme = "https"
|
|
}
|
|
urls := make([]string, 0, len(interfaces))
|
|
for _, ip := range interfaces {
|
|
urls = append(urls, fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(ip, prt)))
|
|
}
|
|
return urls, nil
|
|
}
|
|
|
|
func isLocalhost(u string) bool {
|
|
return strings.Contains(u, "localhost") ||
|
|
strings.Contains(u, "127.0.0.1") ||
|
|
strings.Contains(u, "[::1]")
|
|
}
|
|
|
|
func validateGatewayURLs(urls []string, urlType string) ([]string, error) {
|
|
if len(urls) == 0 {
|
|
return urls, nil
|
|
}
|
|
|
|
var validURLs []string
|
|
for _, urlStr := range urls {
|
|
if strings.TrimSpace(urlStr) == "" {
|
|
continue
|
|
}
|
|
parsedURL, err := url.Parse(urlStr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "WARNING: invalid %s URL %q: %v\n", urlType, urlStr, err)
|
|
continue
|
|
}
|
|
if parsedURL.Scheme == "" {
|
|
fmt.Fprintf(os.Stderr, "WARNING: invalid %s URL %q: missing scheme (must be http:// or https://)\n", urlType, urlStr)
|
|
continue
|
|
}
|
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
|
fmt.Fprintf(os.Stderr, "WARNING: invalid %s URL %q: unsupported scheme %q (must be http or https)\n", urlType, urlStr, parsedURL.Scheme)
|
|
continue
|
|
}
|
|
if parsedURL.Host == "" {
|
|
fmt.Fprintf(os.Stderr, "WARNING: invalid %s URL %q: missing host\n", urlType, urlStr)
|
|
continue
|
|
}
|
|
validURLs = append(validURLs, urlStr)
|
|
}
|
|
|
|
if len(validURLs) == 0 {
|
|
return nil, fmt.Errorf("%s URLs specified but none are valid", urlType)
|
|
}
|
|
|
|
return validURLs, nil
|
|
}
|
|
|
|
func validateWebUIPathPrefix(option, prefix string) error {
|
|
if prefix == "" {
|
|
return nil
|
|
}
|
|
if strings.TrimSpace(prefix) != prefix {
|
|
return fmt.Errorf("invalid %v %q: must not contain leading or trailing whitespace", option, prefix)
|
|
}
|
|
if !strings.HasPrefix(prefix, "/") {
|
|
return fmt.Errorf("invalid %v %q: must start with '/' (example: '/ui')", option, prefix)
|
|
}
|
|
if strings.HasSuffix(prefix, "/") {
|
|
return fmt.Errorf("invalid %v %q: must not end with '/'", option, prefix)
|
|
}
|
|
if strings.Count(prefix, "/") > 1 {
|
|
return fmt.Errorf("invalid %v %q: only a single path segment is allowed (example: '/ui')", option, prefix)
|
|
}
|
|
if strings.ContainsAny(prefix, "?#") {
|
|
return fmt.Errorf("invalid %v %q: query strings and fragments are not allowed", option, prefix)
|
|
}
|
|
if strings.Contains(prefix, "\\") {
|
|
return fmt.Errorf("invalid %v %q: backslashes are not allowed", option, prefix)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sortGatewayURLs(urls []string) {
|
|
if len(urls) <= 1 {
|
|
return
|
|
}
|
|
var nonLocal []string
|
|
var local []string
|
|
for _, u := range urls {
|
|
if isLocalhost(u) {
|
|
local = append(local, u)
|
|
} else {
|
|
nonLocal = append(nonLocal, u)
|
|
}
|
|
}
|
|
copy(urls, nonLocal)
|
|
copy(urls[len(nonLocal):], local)
|
|
}
|
|
|
|
// validatePortConflicts checks for port conflicts across the S3 API, admin,
|
|
// WebUI, and website port lists before the servers are started.
|
|
//
|
|
// A bare port spec (e.g. ":7071") binds to all interfaces and conflicts with
|
|
// any other spec on the same port number. Two identical "ip:port" specs are
|
|
// allowed and will be caught by the OS later. UNIX socket paths are checked
|
|
// for duplicate path conflicts only and never conflict with TCP specs.
|
|
func validatePortConflicts(ports, admPorts, webuiPorts, websitePorts []string) error {
|
|
type portSpec struct {
|
|
spec string
|
|
port string
|
|
isBare bool
|
|
isUnix bool
|
|
portType string
|
|
}
|
|
|
|
var allSpecs []portSpec
|
|
|
|
for _, p := range ports {
|
|
if utils.IsUnixSocketPath(p) {
|
|
allSpecs = append(allSpecs, portSpec{spec: p, port: p, isUnix: true, portType: "s3"})
|
|
continue
|
|
}
|
|
_, port, err := net.SplitHostPort(p)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
allSpecs = append(allSpecs, portSpec{
|
|
spec: p,
|
|
port: port,
|
|
isBare: strings.HasPrefix(p, ":"),
|
|
portType: "s3",
|
|
})
|
|
}
|
|
|
|
for _, p := range admPorts {
|
|
if utils.IsUnixSocketPath(p) {
|
|
allSpecs = append(allSpecs, portSpec{spec: p, port: p, isUnix: true, portType: "admin"})
|
|
continue
|
|
}
|
|
_, port, err := net.SplitHostPort(p)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
allSpecs = append(allSpecs, portSpec{
|
|
spec: p,
|
|
port: port,
|
|
isBare: strings.HasPrefix(p, ":"),
|
|
portType: "admin",
|
|
})
|
|
}
|
|
|
|
for _, p := range webuiPorts {
|
|
if utils.IsUnixSocketPath(p) {
|
|
allSpecs = append(allSpecs, portSpec{spec: p, port: p, isUnix: true, portType: "webui"})
|
|
continue
|
|
}
|
|
_, port, err := net.SplitHostPort(p)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
allSpecs = append(allSpecs, portSpec{
|
|
spec: p,
|
|
port: port,
|
|
isBare: strings.HasPrefix(p, ":"),
|
|
portType: "webui",
|
|
})
|
|
}
|
|
|
|
for _, p := range websitePorts {
|
|
if utils.IsUnixSocketPath(p) {
|
|
allSpecs = append(allSpecs, portSpec{spec: p, port: p, isUnix: true, portType: "website"})
|
|
continue
|
|
}
|
|
_, port, err := net.SplitHostPort(p)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
allSpecs = append(allSpecs, portSpec{
|
|
spec: p,
|
|
port: port,
|
|
isBare: strings.HasPrefix(p, ":"),
|
|
portType: "website",
|
|
})
|
|
}
|
|
|
|
for i, spec1 := range allSpecs {
|
|
for j, spec2 := range allSpecs {
|
|
if i >= j {
|
|
continue
|
|
}
|
|
if spec1.isUnix || spec2.isUnix {
|
|
if spec1.isUnix && spec2.isUnix && spec1.spec == spec2.spec {
|
|
return fmt.Errorf("duplicate unix socket path: %s port %s conflicts with %s port %s",
|
|
spec1.portType, spec1.spec, spec2.portType, spec2.spec)
|
|
}
|
|
continue
|
|
}
|
|
if spec1.port != spec2.port {
|
|
continue
|
|
}
|
|
if !spec1.isBare && !spec2.isBare && spec1.spec == spec2.spec {
|
|
continue
|
|
}
|
|
if spec1.isBare || spec2.isBare {
|
|
return fmt.Errorf("port conflict: %s port %s conflicts with %s port %s (bare port specs bind to all interfaces)",
|
|
spec1.portType, spec1.spec, spec2.portType, spec2.spec)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|