mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
Merge pull request #9490 from gyuho/cors
*: support CORS for v3 HTTP requests
This commit is contained in:
commit
473793be1f
@ -48,6 +48,8 @@ See [code changes](https://github.com/coreos/etcd/compare/v3.3.0...v3.4.0) and [
|
||||
- e.g. exit with error on `ETCD_INITIAL_CLUSTER_TOKEN=abc etcd --initial-cluster-token=def`.
|
||||
- e.g. exit with error on `ETCDCTL_ENDPOINTS=abc.com ETCDCTL_API=3 etcdctl endpoint health --endpoints=def.com`.
|
||||
- Change [`etcdserverpb.AuthRoleRevokePermissionRequest/key,range_end` fields type from `string` to `bytes`](https://github.com/coreos/etcd/pull/9433).
|
||||
- Change [`embed.Config.CorsInfo` in `*cors.CORSInfo` type to `embed.Config.CORS` in `map[string]struct{}` type](https://github.com/coreos/etcd/pull/9490).
|
||||
- Remove [`pkg/cors` package](https://github.com/coreos/etcd/pull/9490).
|
||||
- Move `"github.com/coreos/etcd/snap"` to [`"github.com/coreos/etcd/raftsnap"`](https://github.com/coreos/etcd/pull/9211).
|
||||
- Move `"github.com/coreos/etcd/etcdserver/auth"` to [`"github.com/coreos/etcd/etcdserver/v2auth"`](https://github.com/coreos/etcd/pull/9275).
|
||||
- Move `"github.com/coreos/etcd/error"` to [`"github.com/coreos/etcd/etcdserver/v2error"`](https://github.com/coreos/etcd/pull/9274).
|
||||
@ -78,11 +80,11 @@ See [security doc](https://github.com/coreos/etcd/blob/master/Documentation/op-g
|
||||
- Client origin enforce policy works as follow:
|
||||
- If client connection is secure via HTTPS, allow any hostnames..
|
||||
- If client connection is not secure and `"HostWhitelist"` is not empty, only allow HTTP requests whose Host field is listed in whitelist.
|
||||
- By default, `"HostWhitelist"` is empty, which means insecure server allows all client HTTP requests.
|
||||
- By default, `"HostWhitelist"` is `"*"`, which means insecure server allows all client HTTP requests.
|
||||
- Note that the client origin policy is enforced whether authentication is enabled or not, for tighter controls.
|
||||
- When specifying hostnames, loopback addresses are not added automatically. To allow loopback interfaces, add them to whitelist manually (e.g. `"localhost"`, `"127.0.0.1"`, etc.).
|
||||
- e.g. `etcd --host-whitelist example.com`, then the server will reject all HTTP requests whose Host field is not `example.com` (also rejects requests to `"localhost"`).
|
||||
- TODO: Support `CORS`.
|
||||
- Support [`etcd --cors`](https://github.com/coreos/etcd/pull/9490) in v3 HTTP requests (gRPC gateway).
|
||||
- TODO: Support [TLS cipher suite lists](TODO).
|
||||
- Support [`ttl` field for `etcd` Authentication JWT token](https://github.com/coreos/etcd/pull/8302).
|
||||
- e.g. `etcd --auth-token jwt,pub-key=<pub key path>,priv-key=<priv key path>,sign-method=<sign method>,ttl=5m`.
|
||||
@ -108,6 +110,7 @@ See [security doc](https://github.com/coreos/etcd/blob/master/Documentation/op-g
|
||||
- If not given, etcd queries `_etcd-server-ssl._tcp.[YOUR_HOST]` and `_etcd-server._tcp.[YOUR_HOST]`.
|
||||
- If `--discovery-srv-name="foo"`, then query `_etcd-server-ssl-foo._tcp.[YOUR_HOST]` and `_etcd-server-foo._tcp.[YOUR_HOST]`.
|
||||
- Useful for operating multiple etcd clusters under the same domain.
|
||||
- Support [`etcd --cors`](https://github.com/coreos/etcd/pull/9490) in v3 HTTP requests (gRPC gateway).
|
||||
|
||||
### Added: `embed`
|
||||
|
||||
@ -140,6 +143,7 @@ See [security doc](https://github.com/coreos/etcd/blob/master/Documentation/op-g
|
||||
- To deprecate [`/v3beta`](https://github.com/coreos/etcd/issues/9189) in `v3.5`.
|
||||
- Add API endpoints [`/{v3beta,v3}/lease/leases, /{v3beta,v3}/lease/revoke, /{v3beta,v3}/lease/timetolive`](https://github.com/coreos/etcd/pull/9450).
|
||||
- To deprecate [`/{v3beta,v3}/kv/lease/leases, /{v3beta,v3}/kv/lease/revoke, /{v3beta,v3}/kv/lease/timetolive`](https://github.com/coreos/etcd/issues/9430) in `v3.5`.
|
||||
- Support [`etcd --cors`](https://github.com/coreos/etcd/pull/9490) in v3 HTTP requests (gRPC gateway).
|
||||
|
||||
### Package `raft`
|
||||
|
||||
|
@ -28,7 +28,7 @@ import (
|
||||
|
||||
"github.com/coreos/etcd/compactor"
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/pkg/cors"
|
||||
"github.com/coreos/etcd/pkg/flags"
|
||||
"github.com/coreos/etcd/pkg/netutil"
|
||||
"github.com/coreos/etcd/pkg/srv"
|
||||
"github.com/coreos/etcd/pkg/transport"
|
||||
@ -79,9 +79,8 @@ var (
|
||||
DefaultInitialAdvertisePeerURLs = "http://localhost:2380"
|
||||
DefaultAdvertiseClientURLs = "http://localhost:2379"
|
||||
|
||||
defaultHostname string
|
||||
defaultHostStatus error
|
||||
defaultHostWhitelist = []string{} // if empty, allow all
|
||||
defaultHostname string
|
||||
defaultHostStatus error
|
||||
)
|
||||
|
||||
var (
|
||||
@ -107,7 +106,6 @@ func init() {
|
||||
|
||||
// Config holds the arguments for configuring an etcd server.
|
||||
type Config struct {
|
||||
CorsInfo *cors.CORSInfo
|
||||
LPUrls, LCUrls []url.URL
|
||||
Dir string `json:"data-dir"`
|
||||
WalDir string `json:"wal-dir"`
|
||||
@ -171,6 +169,8 @@ type Config struct {
|
||||
PeerTLSInfo transport.TLSInfo
|
||||
PeerAutoTLS bool
|
||||
|
||||
CORS map[string]struct{}
|
||||
|
||||
// HostWhitelist lists acceptable hostnames from HTTP client requests.
|
||||
// Client origin policy protects against "DNS Rebinding" attacks
|
||||
// to insecure etcd servers. That is, any website can simply create
|
||||
@ -186,16 +186,16 @@ type Config struct {
|
||||
// Note that the client origin policy is enforced whether authentication
|
||||
// is enabled or not, for tighter controls.
|
||||
//
|
||||
// By default, "HostWhitelist" is empty, which allows any hostnames.
|
||||
// By default, "HostWhitelist" is "*", which allows any hostnames.
|
||||
// Note that when specifying hostnames, loopback addresses are not added
|
||||
// automatically. To allow loopback interfaces, leave it empty or add them
|
||||
// to whitelist manually (e.g. "localhost", "127.0.0.1", etc.).
|
||||
// automatically. To allow loopback interfaces, leave it empty or set it "*",
|
||||
// or add them to whitelist manually (e.g. "localhost", "127.0.0.1", etc.).
|
||||
//
|
||||
// CVE-2018-5702 reference:
|
||||
// - https://bugs.chromium.org/p/project-zero/issues/detail?id=1447#c2
|
||||
// - https://github.com/transmission/transmission/pull/468
|
||||
// - https://github.com/coreos/etcd/issues/9353
|
||||
HostWhitelist []string `json:"host-whitelist"`
|
||||
HostWhitelist map[string]struct{}
|
||||
|
||||
Debug bool `json:"debug"`
|
||||
LogPkgLevels string `json:"log-package-levels"`
|
||||
@ -237,11 +237,14 @@ type configYAML struct {
|
||||
|
||||
// configJSON has file options that are translated into Config options
|
||||
type configJSON struct {
|
||||
LPUrlsJSON string `json:"listen-peer-urls"`
|
||||
LCUrlsJSON string `json:"listen-client-urls"`
|
||||
CorsJSON string `json:"cors"`
|
||||
APUrlsJSON string `json:"initial-advertise-peer-urls"`
|
||||
ACUrlsJSON string `json:"advertise-client-urls"`
|
||||
LPUrlsJSON string `json:"listen-peer-urls"`
|
||||
LCUrlsJSON string `json:"listen-client-urls"`
|
||||
APUrlsJSON string `json:"initial-advertise-peer-urls"`
|
||||
ACUrlsJSON string `json:"advertise-client-urls"`
|
||||
|
||||
CORSJSON string `json:"cors"`
|
||||
HostWhitelistJSON string `json:"host-whitelist"`
|
||||
|
||||
ClientSecurityJSON securityConfig `json:"client-transport-security"`
|
||||
PeerSecurityJSON securityConfig `json:"peer-transport-security"`
|
||||
}
|
||||
@ -261,7 +264,6 @@ func NewConfig() *Config {
|
||||
lcurl, _ := url.Parse(DefaultListenClientURLs)
|
||||
acurl, _ := url.Parse(DefaultAdvertiseClientURLs)
|
||||
cfg := &Config{
|
||||
CorsInfo: &cors.CORSInfo{},
|
||||
MaxSnapFiles: DefaultMaxSnapshots,
|
||||
MaxWalFiles: DefaultMaxWALs,
|
||||
Name: DefaultName,
|
||||
@ -283,7 +285,8 @@ func NewConfig() *Config {
|
||||
LogOutput: DefaultLogOutput,
|
||||
Metrics: "basic",
|
||||
EnableV2: DefaultEnableV2,
|
||||
HostWhitelist: defaultHostWhitelist,
|
||||
CORS: map[string]struct{}{"*": {}},
|
||||
HostWhitelist: map[string]struct{}{"*": {}},
|
||||
AuthToken: "simple",
|
||||
PreVote: false, // TODO: enable by default in v3.5
|
||||
}
|
||||
@ -381,12 +384,6 @@ func (cfg *configYAML) configFromFile(path string) error {
|
||||
cfg.LCUrls = []url.URL(u)
|
||||
}
|
||||
|
||||
if cfg.CorsJSON != "" {
|
||||
if err := cfg.CorsInfo.Set(cfg.CorsJSON); err != nil {
|
||||
plog.Panicf("unexpected error setting up cors: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.APUrlsJSON != "" {
|
||||
u, err := types.NewURLs(strings.Split(cfg.APUrlsJSON, ","))
|
||||
if err != nil {
|
||||
@ -411,6 +408,16 @@ func (cfg *configYAML) configFromFile(path string) error {
|
||||
cfg.ListenMetricsUrls = []url.URL(u)
|
||||
}
|
||||
|
||||
if cfg.CORSJSON != "" {
|
||||
uv := flags.NewUniqueURLsWithExceptions(cfg.CORSJSON, "*")
|
||||
cfg.CORS = uv.Values
|
||||
}
|
||||
|
||||
if cfg.HostWhitelistJSON != "" {
|
||||
uv := flags.NewUniqueStringsValue(cfg.HostWhitelistJSON)
|
||||
cfg.HostWhitelist = uv.Values
|
||||
}
|
||||
|
||||
// If a discovery flag is set, clear default initial cluster set by InitialClusterFromName
|
||||
if (cfg.Durl != "" || cfg.DNSCluster != "") && cfg.InitialCluster == defaultInitialCluster {
|
||||
cfg.InitialCluster = ""
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
@ -33,7 +34,6 @@ import (
|
||||
"github.com/coreos/etcd/etcdserver/api/v2v3"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3client"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc"
|
||||
"github.com/coreos/etcd/pkg/cors"
|
||||
"github.com/coreos/etcd/pkg/debugutil"
|
||||
runtimeutil "github.com/coreos/etcd/pkg/runtime"
|
||||
"github.com/coreos/etcd/pkg/transport"
|
||||
@ -168,6 +168,8 @@ func StartEtcd(inCfg *Config) (e *Etcd, err error) {
|
||||
StrictReconfigCheck: cfg.StrictReconfigCheck,
|
||||
ClientCertAuthEnabled: cfg.ClientTLSInfo.ClientCertAuth,
|
||||
AuthToken: cfg.AuthToken,
|
||||
CORS: cfg.CORS,
|
||||
HostWhitelist: cfg.HostWhitelist,
|
||||
InitialCorruptCheck: cfg.ExperimentalInitialCorruptCheck,
|
||||
CorruptCheckTime: cfg.ExperimentalCorruptCheckTime,
|
||||
PreVote: cfg.PreVote,
|
||||
@ -175,17 +177,26 @@ func StartEtcd(inCfg *Config) (e *Etcd, err error) {
|
||||
ForceNewCluster: cfg.ForceNewCluster,
|
||||
}
|
||||
|
||||
srvcfg.HostWhitelist = make(map[string]struct{}, len(cfg.HostWhitelist))
|
||||
for _, h := range cfg.HostWhitelist {
|
||||
if h != "" {
|
||||
srvcfg.HostWhitelist[h] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if e.Server, err = etcdserver.NewServer(srvcfg); err != nil {
|
||||
return e, err
|
||||
}
|
||||
plog.Infof("%s starting with host whitelist %q", e.Server.ID(), cfg.HostWhitelist)
|
||||
|
||||
if len(e.cfg.CORS) > 0 {
|
||||
ss := make([]string, 0, len(e.cfg.CORS))
|
||||
for v := range e.cfg.CORS {
|
||||
ss = append(ss, v)
|
||||
}
|
||||
sort.Strings(ss)
|
||||
plog.Infof("%s starting with cors %q", e.Server.ID(), ss)
|
||||
}
|
||||
if len(e.cfg.HostWhitelist) > 0 {
|
||||
ss := make([]string, 0, len(e.cfg.HostWhitelist))
|
||||
for v := range e.cfg.HostWhitelist {
|
||||
ss = append(ss, v)
|
||||
}
|
||||
sort.Strings(ss)
|
||||
plog.Infof("%s starting with host whitelist %q", e.Server.ID(), ss)
|
||||
}
|
||||
|
||||
// buffer channel so goroutines on closed connections won't wait forever
|
||||
e.errc = make(chan error, len(e.Peers)+len(e.Clients)+2*len(e.sctxs))
|
||||
@ -479,10 +490,6 @@ func (e *Etcd) serveClients() (err error) {
|
||||
plog.Infof("ClientTLS: %s", e.cfg.ClientTLSInfo)
|
||||
}
|
||||
|
||||
if e.cfg.CorsInfo.String() != "" {
|
||||
plog.Infof("cors = %s", e.cfg.CorsInfo)
|
||||
}
|
||||
|
||||
// Start a client server goroutine for each listen address
|
||||
var h http.Handler
|
||||
if e.Config().EnableV2 {
|
||||
@ -497,7 +504,6 @@ func (e *Etcd) serveClients() (err error) {
|
||||
etcdhttp.HandleBasic(mux, e.Server)
|
||||
h = mux
|
||||
}
|
||||
h = http.Handler(&cors.CORSHandler{Handler: h, Info: e.cfg.CorsInfo})
|
||||
|
||||
gopts := []grpc.ServerOption{}
|
||||
if e.cfg.GRPCKeepAliveMinTime > time.Duration(0) {
|
||||
|
@ -116,7 +116,7 @@ func (sctx *serveCtx) serve(
|
||||
httpmux := sctx.createMux(gwmux, handler)
|
||||
|
||||
srvhttp := &http.Server{
|
||||
Handler: wrapMux(s, httpmux),
|
||||
Handler: createAccessController(s, httpmux),
|
||||
ErrorLog: logger, // do not log user error
|
||||
}
|
||||
httpl := m.Match(cmux.HTTP1())
|
||||
@ -159,7 +159,7 @@ func (sctx *serveCtx) serve(
|
||||
httpmux := sctx.createMux(gwmux, handler)
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: wrapMux(s, httpmux),
|
||||
Handler: createAccessController(s, httpmux),
|
||||
TLSConfig: tlscfg,
|
||||
ErrorLog: logger, // do not log user error
|
||||
}
|
||||
@ -250,20 +250,20 @@ func (sctx *serveCtx) createMux(gwmux *gw.ServeMux, handler http.Handler) *http.
|
||||
return httpmux
|
||||
}
|
||||
|
||||
// wrapMux wraps HTTP multiplexer:
|
||||
// createAccessController wraps HTTP multiplexer:
|
||||
// - mutate gRPC gateway request paths
|
||||
// - check hostname whitelist
|
||||
// client HTTP requests goes here first
|
||||
func wrapMux(s *etcdserver.EtcdServer, mux *http.ServeMux) http.Handler {
|
||||
return &httpWrapper{s: s, mux: mux}
|
||||
func createAccessController(s *etcdserver.EtcdServer, mux *http.ServeMux) http.Handler {
|
||||
return &accessController{s: s, mux: mux}
|
||||
}
|
||||
|
||||
type httpWrapper struct {
|
||||
type accessController struct {
|
||||
s *etcdserver.EtcdServer
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func (m *httpWrapper) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
func (ac *accessController) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// redirect for backward compatibilities
|
||||
if req != nil && req.URL != nil && strings.HasPrefix(req.URL.Path, "/v3beta/") {
|
||||
req.URL.Path = strings.Replace(req.URL.Path, "/v3beta/", "/v3/", 1)
|
||||
@ -271,7 +271,7 @@ func (m *httpWrapper) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
if req.TLS == nil { // check origin if client connection is not secure
|
||||
host := httputil.GetHostname(req)
|
||||
if !m.s.IsHostWhitelisted(host) {
|
||||
if !ac.s.AccessController.IsHostWhitelisted(host) {
|
||||
plog.Warningf("rejecting HTTP request from %q to prevent DNS rebinding attacks", host)
|
||||
// TODO: use Go's "http.StatusMisdirectedRequest" (421)
|
||||
// https://github.com/golang/go/commit/4b8a7eafef039af1834ef9bfa879257c4a72b7b5
|
||||
@ -280,7 +280,26 @@ func (m *httpWrapper) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
m.mux.ServeHTTP(rw, req)
|
||||
// Write CORS header.
|
||||
if ac.s.AccessController.OriginAllowed("*") {
|
||||
addCORSHeader(rw, "*")
|
||||
} else if origin := req.Header.Get("Origin"); ac.s.OriginAllowed(origin) {
|
||||
addCORSHeader(rw, origin)
|
||||
}
|
||||
|
||||
if req.Method == "OPTIONS" {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
ac.mux.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
// addCORSHeader adds the correct cors headers given an origin
|
||||
func addCORSHeader(w http.ResponseWriter, origin string) {
|
||||
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
|
||||
w.Header().Add("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Add("Access-Control-Allow-Headers", "accept, content-type, authorization")
|
||||
}
|
||||
|
||||
// https://github.com/transmission/transmission/pull/468
|
||||
@ -297,6 +316,35 @@ This requirement has been added to help prevent "DNS Rebinding" attacks (CVE-201
|
||||
`, host)
|
||||
}
|
||||
|
||||
// WrapCORS wraps existing handler with CORS.
|
||||
// TODO: deprecate this after v2 proxy deprecate
|
||||
func WrapCORS(cors map[string]struct{}, h http.Handler) http.Handler {
|
||||
return &corsHandler{
|
||||
ac: &etcdserver.AccessController{CORS: cors},
|
||||
h: h,
|
||||
}
|
||||
}
|
||||
|
||||
type corsHandler struct {
|
||||
ac *etcdserver.AccessController
|
||||
h http.Handler
|
||||
}
|
||||
|
||||
func (ch *corsHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if ch.ac.OriginAllowed("*") {
|
||||
addCORSHeader(rw, "*")
|
||||
} else if origin := req.Header.Get("Origin"); ch.ac.OriginAllowed(origin) {
|
||||
addCORSHeader(rw, origin)
|
||||
}
|
||||
|
||||
if req.Method == "OPTIONS" {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
ch.h.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func (sctx *serveCtx) registerUserHandler(s string, h http.Handler) {
|
||||
if sctx.userHandlers[s] != nil {
|
||||
plog.Warningf("path %s already registered by user handler", s)
|
||||
|
@ -128,12 +128,22 @@ func newConfig() *config {
|
||||
fs.StringVar(&cfg.configFile, "config-file", "", "Path to the server configuration file")
|
||||
|
||||
// member
|
||||
fs.Var(cfg.ec.CorsInfo, "cors", "Comma-separated white list of origins for CORS (cross-origin resource sharing).")
|
||||
fs.StringVar(&cfg.ec.Dir, "data-dir", cfg.ec.Dir, "Path to the data directory.")
|
||||
fs.StringVar(&cfg.ec.WalDir, "wal-dir", cfg.ec.WalDir, "Path to the dedicated wal directory.")
|
||||
fs.Var(flags.NewURLsValue(embed.DefaultListenPeerURLs), "listen-peer-urls", "List of URLs to listen on for peer traffic.")
|
||||
fs.Var(flags.NewURLsValue(embed.DefaultListenClientURLs), "listen-client-urls", "List of URLs to listen on for client traffic.")
|
||||
fs.Var(flags.NewURLsValue(""), "listen-metrics-urls", "List of URLs to listen on for metrics.")
|
||||
fs.Var(
|
||||
flags.NewUniqueURLsWithExceptions(embed.DefaultListenPeerURLs, ""),
|
||||
"listen-peer-urls",
|
||||
"List of URLs to listen on for peer traffic.",
|
||||
)
|
||||
fs.Var(
|
||||
flags.NewUniqueURLsWithExceptions(embed.DefaultListenClientURLs, ""), "listen-client-urls",
|
||||
"List of URLs to listen on for client traffic.",
|
||||
)
|
||||
fs.Var(
|
||||
flags.NewUniqueURLsWithExceptions("", ""),
|
||||
"listen-metrics-urls",
|
||||
"List of URLs to listen on for metrics.",
|
||||
)
|
||||
fs.UintVar(&cfg.ec.MaxSnapFiles, "max-snapshots", cfg.ec.MaxSnapFiles, "Maximum number of snapshot files to retain (0 is unlimited).")
|
||||
fs.UintVar(&cfg.ec.MaxWalFiles, "max-wals", cfg.ec.MaxWalFiles, "Maximum number of wal files to retain (0 is unlimited).")
|
||||
fs.StringVar(&cfg.ec.Name, "name", cfg.ec.Name, "Human-readable name for this member.")
|
||||
@ -148,8 +158,16 @@ func newConfig() *config {
|
||||
fs.DurationVar(&cfg.ec.GRPCKeepAliveTimeout, "grpc-keepalive-timeout", cfg.ec.GRPCKeepAliveTimeout, "Additional duration of wait before closing a non-responsive connection (0 to disable).")
|
||||
|
||||
// clustering
|
||||
fs.Var(flags.NewURLsValue(embed.DefaultInitialAdvertisePeerURLs), "initial-advertise-peer-urls", "List of this member's peer URLs to advertise to the rest of the cluster.")
|
||||
fs.Var(flags.NewURLsValue(embed.DefaultAdvertiseClientURLs), "advertise-client-urls", "List of this member's client URLs to advertise to the public.")
|
||||
fs.Var(
|
||||
flags.NewUniqueURLsWithExceptions(embed.DefaultInitialAdvertisePeerURLs, ""),
|
||||
"initial-advertise-peer-urls",
|
||||
"List of this member's peer URLs to advertise to the rest of the cluster.",
|
||||
)
|
||||
fs.Var(
|
||||
flags.NewUniqueURLsWithExceptions(embed.DefaultAdvertiseClientURLs, ""),
|
||||
"advertise-client-urls",
|
||||
"List of this member's client URLs to advertise to the public.",
|
||||
)
|
||||
fs.StringVar(&cfg.ec.Durl, "discovery", cfg.ec.Durl, "Discovery URL used to bootstrap the cluster.")
|
||||
fs.Var(cfg.cf.fallback, "discovery-fallback", fmt.Sprintf("Valid values include %q", cfg.cf.fallback.Valids()))
|
||||
|
||||
@ -186,7 +204,13 @@ func newConfig() *config {
|
||||
fs.BoolVar(&cfg.ec.PeerAutoTLS, "peer-auto-tls", false, "Peer TLS using generated certificates")
|
||||
fs.StringVar(&cfg.ec.PeerTLSInfo.CRLFile, "peer-crl-file", "", "Path to the peer certificate revocation list file.")
|
||||
fs.StringVar(&cfg.ec.PeerTLSInfo.AllowedCN, "peer-cert-allowed-cn", "", "Allowed CN for inter peer authentication.")
|
||||
fs.Var(flags.NewStringsValue(""), "host-whitelist", "Comma-separated acceptable hostnames from HTTP client requests, if server is not secure (empty means allow all).")
|
||||
|
||||
fs.Var(
|
||||
flags.NewUniqueURLsWithExceptions("*", "*"),
|
||||
"cors",
|
||||
"Comma-separated white list of origins for CORS, or cross-origin resource sharing, (empty or * means allow all)",
|
||||
)
|
||||
fs.Var(flags.NewUniqueStringsValue("*"), "host-whitelist", "Comma-separated acceptable hostnames from HTTP client requests, if server is not secure (empty means allow all).")
|
||||
|
||||
// logging
|
||||
fs.BoolVar(&cfg.ec.Debug, "debug", false, "Enable debug-level logging for etcd.")
|
||||
@ -261,12 +285,14 @@ func (cfg *config) configFromCmdLine() error {
|
||||
plog.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
cfg.ec.LPUrls = flags.URLsFromFlag(cfg.cf.flagSet, "listen-peer-urls")
|
||||
cfg.ec.APUrls = flags.URLsFromFlag(cfg.cf.flagSet, "initial-advertise-peer-urls")
|
||||
cfg.ec.LCUrls = flags.URLsFromFlag(cfg.cf.flagSet, "listen-client-urls")
|
||||
cfg.ec.ACUrls = flags.URLsFromFlag(cfg.cf.flagSet, "advertise-client-urls")
|
||||
cfg.ec.HostWhitelist = flags.StringsFromFlag(cfg.cf.flagSet, "host-whitelist")
|
||||
cfg.ec.ListenMetricsUrls = flags.URLsFromFlag(cfg.cf.flagSet, "listen-metrics-urls")
|
||||
cfg.ec.LPUrls = flags.UniqueURLsFromFlag(cfg.cf.flagSet, "listen-peer-urls")
|
||||
cfg.ec.APUrls = flags.UniqueURLsFromFlag(cfg.cf.flagSet, "initial-advertise-peer-urls")
|
||||
cfg.ec.LCUrls = flags.UniqueURLsFromFlag(cfg.cf.flagSet, "listen-client-urls")
|
||||
cfg.ec.ACUrls = flags.UniqueURLsFromFlag(cfg.cf.flagSet, "advertise-client-urls")
|
||||
cfg.ec.ListenMetricsUrls = flags.UniqueURLsFromFlag(cfg.cf.flagSet, "listen-metrics-urls")
|
||||
|
||||
cfg.ec.CORS = flags.UniqueURLsMapFromFlag(cfg.cf.flagSet, "cors")
|
||||
cfg.ec.HostWhitelist = flags.UniqueStringsMapFromFlag(cfg.cf.flagSet, "host-whitelist")
|
||||
|
||||
cfg.ec.ClusterState = cfg.cf.clusterState.String()
|
||||
cfg.cp.Fallback = cfg.cf.fallback.String()
|
||||
|
@ -567,10 +567,10 @@ func validateClusteringFlags(t *testing.T, cfg *config) {
|
||||
t.Errorf("initialClusterToken = %v, want %v", cfg.ec.InitialClusterToken, wcfg.ec.InitialClusterToken)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.ec.APUrls, wcfg.ec.APUrls) {
|
||||
t.Errorf("initial-advertise-peer-urls = %v, want %v", cfg.ec.LPUrls, wcfg.ec.LPUrls)
|
||||
t.Errorf("initial-advertise-peer-urls = %v, want %v", cfg.ec.APUrls, wcfg.ec.APUrls)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.ec.ACUrls, wcfg.ec.ACUrls) {
|
||||
t.Errorf("advertise-client-urls = %v, want %v", cfg.ec.LCUrls, wcfg.ec.LCUrls)
|
||||
t.Errorf("advertise-client-urls = %v, want %v", cfg.ec.ACUrls, wcfg.ec.ACUrls)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,6 @@ import (
|
||||
"github.com/coreos/etcd/embed"
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api/etcdhttp"
|
||||
"github.com/coreos/etcd/pkg/cors"
|
||||
"github.com/coreos/etcd/pkg/fileutil"
|
||||
pkgioutil "github.com/coreos/etcd/pkg/ioutil"
|
||||
"github.com/coreos/etcd/pkg/osutil"
|
||||
@ -301,10 +300,7 @@ func startProxy(cfg *config) error {
|
||||
return clientURLs
|
||||
}
|
||||
ph := httpproxy.NewHandler(pt, uf, time.Duration(cfg.cp.ProxyFailureWaitMs)*time.Millisecond, time.Duration(cfg.cp.ProxyRefreshIntervalMs)*time.Millisecond)
|
||||
ph = &cors.CORSHandler{
|
||||
Handler: ph,
|
||||
Info: cfg.ec.CorsInfo,
|
||||
}
|
||||
ph = embed.WrapCORS(cfg.ec.CORS, ph)
|
||||
|
||||
if cfg.isReadonlyProxy() {
|
||||
ph = httpproxy.NewReadonlyHandler(ph)
|
||||
|
309
etcdmain/help.go
309
etcdmain/help.go
@ -21,177 +21,172 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
usageline = `usage: etcd [flags]
|
||||
start an etcd server
|
||||
usageline = `Usage:
|
||||
|
||||
etcd --version
|
||||
show the version of etcd
|
||||
etcd [flags]
|
||||
Start an etcd server.
|
||||
|
||||
etcd -h | --help
|
||||
show the help information about etcd
|
||||
etcd --version
|
||||
Show the version of etcd.
|
||||
|
||||
etcd --config-file
|
||||
path to the server configuration file
|
||||
etcd -h | --help
|
||||
Show the help information about etcd.
|
||||
|
||||
etcd gateway
|
||||
run the stateless pass-through etcd TCP connection forwarding proxy
|
||||
etcd --config-file
|
||||
Path to the server configuration file.
|
||||
|
||||
etcd grpc-proxy
|
||||
run the stateless etcd v3 gRPC L7 reverse proxy
|
||||
`
|
||||
etcd gateway
|
||||
Run the stateless pass-through etcd TCP connection forwarding proxy.
|
||||
|
||||
etcd grpc-proxy
|
||||
Run the stateless etcd v3 gRPC L7 reverse proxy.
|
||||
`
|
||||
flagsline = `
|
||||
member flags:
|
||||
Member:
|
||||
--name 'default'
|
||||
Human-readable name for this member.
|
||||
--data-dir '${name}.etcd'
|
||||
Path to the data directory.
|
||||
--wal-dir ''
|
||||
Path to the dedicated wal directory.
|
||||
--snapshot-count '100000'
|
||||
Number of committed transactions to trigger a snapshot to disk.
|
||||
--heartbeat-interval '100'
|
||||
Time (in milliseconds) of a heartbeat interval.
|
||||
--election-timeout '1000'
|
||||
Time (in milliseconds) for an election to timeout. See tuning documentation for details.
|
||||
--listen-peer-urls 'http://localhost:2380'
|
||||
List of URLs to listen on for peer traffic.
|
||||
--listen-client-urls 'http://localhost:2379'
|
||||
List of URLs to listen on for client traffic.
|
||||
--max-snapshots '` + strconv.Itoa(embed.DefaultMaxSnapshots) + `'
|
||||
Maximum number of snapshot files to retain (0 is unlimited).
|
||||
--max-wals '` + strconv.Itoa(embed.DefaultMaxWALs) + `'
|
||||
Maximum number of wal files to retain (0 is unlimited).
|
||||
--quota-backend-bytes '0'
|
||||
Raise alarms when backend size exceeds the given quota (0 defaults to low space quota).
|
||||
--max-txn-ops '128'
|
||||
Maximum number of operations permitted in a transaction.
|
||||
--max-request-bytes '1572864'
|
||||
Maximum client request size in bytes the server will accept.
|
||||
--grpc-keepalive-min-time '5s'
|
||||
Minimum duration interval that a client should wait before pinging server.
|
||||
--grpc-keepalive-interval '2h'
|
||||
Frequency duration of server-to-client ping to check if a connection is alive (0 to disable).
|
||||
--grpc-keepalive-timeout '20s'
|
||||
Additional duration of wait before closing a non-responsive connection (0 to disable).
|
||||
|
||||
--name 'default'
|
||||
human-readable name for this member.
|
||||
--data-dir '${name}.etcd'
|
||||
path to the data directory.
|
||||
--wal-dir ''
|
||||
path to the dedicated wal directory.
|
||||
--snapshot-count '100000'
|
||||
number of committed transactions to trigger a snapshot to disk.
|
||||
--heartbeat-interval '100'
|
||||
time (in milliseconds) of a heartbeat interval.
|
||||
--election-timeout '1000'
|
||||
time (in milliseconds) for an election to timeout. See tuning documentation for details.
|
||||
--listen-peer-urls 'http://localhost:2380'
|
||||
list of URLs to listen on for peer traffic.
|
||||
--listen-client-urls 'http://localhost:2379'
|
||||
list of URLs to listen on for client traffic.
|
||||
--max-snapshots '` + strconv.Itoa(embed.DefaultMaxSnapshots) + `'
|
||||
maximum number of snapshot files to retain (0 is unlimited).
|
||||
--max-wals '` + strconv.Itoa(embed.DefaultMaxWALs) + `'
|
||||
maximum number of wal files to retain (0 is unlimited).
|
||||
--cors ''
|
||||
comma-separated whitelist of origins for CORS (cross-origin resource sharing).
|
||||
--quota-backend-bytes '0'
|
||||
raise alarms when backend size exceeds the given quota (0 defaults to low space quota).
|
||||
--max-txn-ops '128'
|
||||
maximum number of operations permitted in a transaction.
|
||||
--max-request-bytes '1572864'
|
||||
maximum client request size in bytes the server will accept.
|
||||
--grpc-keepalive-min-time '5s'
|
||||
minimum duration interval that a client should wait before pinging server.
|
||||
--grpc-keepalive-interval '2h'
|
||||
frequency duration of server-to-client ping to check if a connection is alive (0 to disable).
|
||||
--grpc-keepalive-timeout '20s'
|
||||
additional duration of wait before closing a non-responsive connection (0 to disable).
|
||||
Clustering:
|
||||
--initial-advertise-peer-urls 'http://localhost:2380'
|
||||
List of this member's peer URLs to advertise to the rest of the cluster.
|
||||
--initial-cluster 'default=http://localhost:2380'
|
||||
Initial cluster configuration for bootstrapping.
|
||||
--initial-cluster-state 'new'
|
||||
Initial cluster state ('new' or 'existing').
|
||||
--initial-cluster-token 'etcd-cluster'
|
||||
Initial cluster token for the etcd cluster during bootstrap.
|
||||
Specifying this can protect you from unintended cross-cluster interaction when running multiple clusters.
|
||||
--advertise-client-urls 'http://localhost:2379'
|
||||
List of this member's client URLs to advertise to the public.
|
||||
The client URLs advertised should be accessible to machines that talk to etcd cluster. etcd client libraries parse these URLs to connect to the cluster.
|
||||
--discovery ''
|
||||
Discovery URL used to bootstrap the cluster.
|
||||
--discovery-fallback 'proxy'
|
||||
Expected behavior ('exit' or 'proxy') when discovery services fails.
|
||||
"proxy" supports v2 API only.
|
||||
--discovery-proxy ''
|
||||
HTTP proxy to use for traffic to discovery service.
|
||||
--discovery-srv ''
|
||||
DNS srv domain used to bootstrap the cluster.
|
||||
--discovery-srv-name ''
|
||||
Suffix to the dns srv name queried when bootstrapping.
|
||||
--strict-reconfig-check '` + strconv.FormatBool(embed.DefaultStrictReconfigCheck) + `'
|
||||
Reject reconfiguration requests that would cause quorum loss.
|
||||
--pre-vote 'false'
|
||||
Enable to run an additional Raft election phase.
|
||||
--auto-compaction-retention '0'
|
||||
Auto compaction retention length. 0 means disable auto compaction.
|
||||
--auto-compaction-mode 'periodic'
|
||||
Interpret 'auto-compaction-retention' one of: periodic|revision. 'periodic' for duration based retention, defaulting to hours if no time unit is provided (e.g. '5m'). 'revision' for revision number based retention.
|
||||
--enable-v2 '` + strconv.FormatBool(embed.DefaultEnableV2) + `'
|
||||
Accept etcd V2 client requests.
|
||||
|
||||
clustering flags:
|
||||
Security:
|
||||
--cert-file ''
|
||||
Path to the client server TLS cert file.
|
||||
--key-file ''
|
||||
Path to the client server TLS key file.
|
||||
--client-cert-auth 'false'
|
||||
Enable client cert authentication.
|
||||
--client-crl-file ''
|
||||
Path to the client certificate revocation list file.
|
||||
--trusted-ca-file ''
|
||||
Path to the client server TLS trusted CA cert file.
|
||||
--auto-tls 'false'
|
||||
Client TLS using generated certificates.
|
||||
--peer-cert-file ''
|
||||
Path to the peer server TLS cert file.
|
||||
--peer-key-file ''
|
||||
Path to the peer server TLS key file.
|
||||
--peer-client-cert-auth 'false'
|
||||
Enable peer client cert authentication.
|
||||
--peer-trusted-ca-file ''
|
||||
Path to the peer server TLS trusted CA file.
|
||||
--peer-auto-tls 'false'
|
||||
Peer TLS using self-generated certificates if --peer-key-file and --peer-cert-file are not provided.
|
||||
--peer-crl-file ''
|
||||
Path to the peer certificate revocation list file.
|
||||
--cors '*'
|
||||
Comma-separated whitelist of origins for CORS, or cross-origin resource sharing, (empty or * means allow all).
|
||||
--host-whitelist '*'
|
||||
Acceptable hostnames from HTTP client requests, if server is not secure (empty or * means allow all).
|
||||
|
||||
--initial-advertise-peer-urls 'http://localhost:2380'
|
||||
list of this member's peer URLs to advertise to the rest of the cluster.
|
||||
--initial-cluster 'default=http://localhost:2380'
|
||||
initial cluster configuration for bootstrapping.
|
||||
--initial-cluster-state 'new'
|
||||
initial cluster state ('new' or 'existing').
|
||||
--initial-cluster-token 'etcd-cluster'
|
||||
initial cluster token for the etcd cluster during bootstrap.
|
||||
Specifying this can protect you from unintended cross-cluster interaction when running multiple clusters.
|
||||
--advertise-client-urls 'http://localhost:2379'
|
||||
list of this member's client URLs to advertise to the public.
|
||||
The client URLs advertised should be accessible to machines that talk to etcd cluster. etcd client libraries parse these URLs to connect to the cluster.
|
||||
--discovery ''
|
||||
discovery URL used to bootstrap the cluster.
|
||||
--discovery-fallback 'proxy'
|
||||
expected behavior ('exit' or 'proxy') when discovery services fails.
|
||||
"proxy" supports v2 API only.
|
||||
--discovery-proxy ''
|
||||
HTTP proxy to use for traffic to discovery service.
|
||||
--discovery-srv ''
|
||||
dns srv domain used to bootstrap the cluster.
|
||||
--discovery-srv-name ''
|
||||
suffix to the dns srv name queried when bootstrapping.
|
||||
--strict-reconfig-check '` + strconv.FormatBool(embed.DefaultStrictReconfigCheck) + `'
|
||||
reject reconfiguration requests that would cause quorum loss.
|
||||
--pre-vote 'false'
|
||||
enable to run an additional Raft election phase.
|
||||
--auto-compaction-retention '0'
|
||||
auto compaction retention length. 0 means disable auto compaction.
|
||||
--auto-compaction-mode 'periodic'
|
||||
interpret 'auto-compaction-retention' one of: periodic|revision. 'periodic' for duration based retention, defaulting to hours if no time unit is provided (e.g. '5m'). 'revision' for revision number based retention.
|
||||
--enable-v2 '` + strconv.FormatBool(embed.DefaultEnableV2) + `'
|
||||
Accept etcd V2 client requests.
|
||||
Auth:
|
||||
--auth-token 'simple'
|
||||
Specify a v3 authentication token type and its options ('simple' or 'jwt').
|
||||
|
||||
proxy flags (v2 API only):
|
||||
Profiling:
|
||||
--enable-pprof 'false'
|
||||
Enable runtime profiling data via HTTP server. Address is at client URL + "/debug/pprof/"
|
||||
--metrics 'basic'
|
||||
Set level of detail for exported metrics, specify 'extensive' to include histogram metrics.
|
||||
--listen-metrics-urls ''
|
||||
List of URLs to listen on for metrics.
|
||||
|
||||
--proxy 'off'
|
||||
proxy mode setting ('off', 'readonly' or 'on').
|
||||
--proxy-failure-wait 5000
|
||||
time (in milliseconds) an endpoint will be held in a failed state.
|
||||
--proxy-refresh-interval 30000
|
||||
time (in milliseconds) of the endpoints refresh interval.
|
||||
--proxy-dial-timeout 1000
|
||||
time (in milliseconds) for a dial to timeout.
|
||||
--proxy-write-timeout 5000
|
||||
time (in milliseconds) for a write to timeout.
|
||||
--proxy-read-timeout 0
|
||||
time (in milliseconds) for a read to timeout.
|
||||
Logging:
|
||||
--debug 'false'
|
||||
Enable debug-level logging for etcd.
|
||||
--log-package-levels ''
|
||||
Specify a particular log level for each etcd package (eg: 'etcdmain=CRITICAL,etcdserver=DEBUG').
|
||||
--log-output 'default'
|
||||
Specify 'stdout' or 'stderr' to skip journald logging even when running under systemd.
|
||||
|
||||
security flags:
|
||||
v2 Proxy (to be deprecated in v4):
|
||||
--proxy 'off'
|
||||
Proxy mode setting ('off', 'readonly' or 'on').
|
||||
--proxy-failure-wait 5000
|
||||
Time (in milliseconds) an endpoint will be held in a failed state.
|
||||
--proxy-refresh-interval 30000
|
||||
Time (in milliseconds) of the endpoints refresh interval.
|
||||
--proxy-dial-timeout 1000
|
||||
Time (in milliseconds) for a dial to timeout.
|
||||
--proxy-write-timeout 5000
|
||||
Time (in milliseconds) for a write to timeout.
|
||||
--proxy-read-timeout 0
|
||||
Time (in milliseconds) for a read to timeout.
|
||||
|
||||
--cert-file ''
|
||||
path to the client server TLS cert file.
|
||||
--key-file ''
|
||||
path to the client server TLS key file.
|
||||
--client-cert-auth 'false'
|
||||
enable client cert authentication.
|
||||
--client-crl-file ''
|
||||
path to the client certificate revocation list file.
|
||||
--trusted-ca-file ''
|
||||
path to the client server TLS trusted CA cert file.
|
||||
--auto-tls 'false'
|
||||
client TLS using generated certificates.
|
||||
--peer-cert-file ''
|
||||
path to the peer server TLS cert file.
|
||||
--peer-key-file ''
|
||||
path to the peer server TLS key file.
|
||||
--peer-client-cert-auth 'false'
|
||||
enable peer client cert authentication.
|
||||
--peer-trusted-ca-file ''
|
||||
path to the peer server TLS trusted CA file.
|
||||
--peer-auto-tls 'false'
|
||||
peer TLS using self-generated certificates if --peer-key-file and --peer-cert-file are not provided.
|
||||
--peer-crl-file ''
|
||||
path to the peer certificate revocation list file.
|
||||
--host-whitelist ''
|
||||
acceptable hostnames from HTTP client requests, if server is not secure (empty means allow all).
|
||||
Experimental feature:
|
||||
--experimental-initial-corrupt-check 'false'
|
||||
Enable to check data corruption before serving any client/peer traffic.
|
||||
--experimental-corrupt-check-time '0s'
|
||||
Duration of time between cluster corruption check passes.
|
||||
--experimental-enable-v2v3 ''
|
||||
Serve v2 requests through the v3 backend under a given prefix.
|
||||
|
||||
logging flags
|
||||
Unsafe feature:
|
||||
--force-new-cluster 'false'
|
||||
Force to create a new one-member cluster.
|
||||
|
||||
--debug 'false'
|
||||
enable debug-level logging for etcd.
|
||||
--log-package-levels ''
|
||||
specify a particular log level for each etcd package (eg: 'etcdmain=CRITICAL,etcdserver=DEBUG').
|
||||
--log-output 'default'
|
||||
specify 'stdout' or 'stderr' to skip journald logging even when running under systemd.
|
||||
|
||||
profiling flags:
|
||||
--enable-pprof 'false'
|
||||
Enable runtime profiling data via HTTP server. Address is at client URL + "/debug/pprof/"
|
||||
--metrics 'basic'
|
||||
Set level of detail for exported metrics, specify 'extensive' to include histogram metrics.
|
||||
--listen-metrics-urls ''
|
||||
List of URLs to listen on for metrics.
|
||||
|
||||
auth flags:
|
||||
--auth-token 'simple'
|
||||
Specify a v3 authentication token type and its options ('simple' or 'jwt').
|
||||
|
||||
experimental flags:
|
||||
--experimental-initial-corrupt-check 'false'
|
||||
enable to check data corruption before serving any client/peer traffic.
|
||||
--experimental-corrupt-check-time '0s'
|
||||
duration of time between cluster corruption check passes.
|
||||
--experimental-enable-v2v3 ''
|
||||
serve v2 requests through the v3 backend under a given prefix.
|
||||
|
||||
|
||||
Please be CAUTIOUS when using unsafe flags because it will break the guarantees
|
||||
given by the consensus protocol.
|
||||
|
||||
unsafe flags:
|
||||
--force-new-cluster 'false'
|
||||
force to create a new one-member cluster.
|
||||
CAUTIOUS with unsafe flag! It may break the guarantees given by the consensus protocol!
|
||||
`
|
||||
)
|
||||
|
@ -46,6 +46,8 @@ type ServerConfig struct {
|
||||
NewCluster bool
|
||||
PeerTLSInfo transport.TLSInfo
|
||||
|
||||
CORS map[string]struct{}
|
||||
|
||||
// HostWhitelist lists acceptable hostnames from client requests.
|
||||
// If server is insecure (no TLS), server only accepts requests
|
||||
// whose Host header value exists in this white list.
|
||||
|
@ -253,7 +253,7 @@ type EtcdServer struct {
|
||||
leadTimeMu sync.RWMutex
|
||||
leadElectedTime time.Time
|
||||
|
||||
hostWhitelist map[string]struct{}
|
||||
*AccessController
|
||||
}
|
||||
|
||||
// NewServer creates a new EtcdServer from the supplied configuration. The
|
||||
@ -434,16 +434,16 @@ func NewServer(cfg ServerConfig) (srv *EtcdServer, err error) {
|
||||
storage: NewStorage(w, ss),
|
||||
},
|
||||
),
|
||||
id: id,
|
||||
attributes: membership.Attributes{Name: cfg.Name, ClientURLs: cfg.ClientURLs.StringSlice()},
|
||||
cluster: cl,
|
||||
stats: sstats,
|
||||
lstats: lstats,
|
||||
SyncTicker: time.NewTicker(500 * time.Millisecond),
|
||||
peerRt: prt,
|
||||
reqIDGen: idutil.NewGenerator(uint16(id), time.Now()),
|
||||
forceVersionC: make(chan struct{}),
|
||||
hostWhitelist: cfg.HostWhitelist,
|
||||
id: id,
|
||||
attributes: membership.Attributes{Name: cfg.Name, ClientURLs: cfg.ClientURLs.StringSlice()},
|
||||
cluster: cl,
|
||||
stats: sstats,
|
||||
lstats: lstats,
|
||||
SyncTicker: time.NewTicker(500 * time.Millisecond),
|
||||
peerRt: prt,
|
||||
reqIDGen: idutil.NewGenerator(uint16(id), time.Now()),
|
||||
forceVersionC: make(chan struct{}),
|
||||
AccessController: &AccessController{CORS: cfg.CORS, HostWhitelist: cfg.HostWhitelist},
|
||||
}
|
||||
|
||||
srv.applyV2 = &applierV2store{store: srv.v2store, cluster: srv.cluster}
|
||||
@ -673,16 +673,6 @@ func (s *EtcdServer) ReportSnapshot(id uint64, status raft.SnapshotStatus) {
|
||||
s.r.ReportSnapshot(id, status)
|
||||
}
|
||||
|
||||
// IsHostWhitelisted returns true if the host is whitelisted.
|
||||
// If whitelist is empty, allow all.
|
||||
func (s *EtcdServer) IsHostWhitelisted(host string) bool {
|
||||
if len(s.hostWhitelist) == 0 { // allow all
|
||||
return true
|
||||
}
|
||||
_, ok := s.hostWhitelist[host]
|
||||
return ok
|
||||
}
|
||||
|
||||
type etcdProgress struct {
|
||||
confState raftpb.ConfState
|
||||
snapi uint64
|
||||
|
65
etcdserver/server_access_control.go
Normal file
65
etcdserver/server_access_control.go
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright 2018 The etcd Authors
|
||||
//
|
||||
// 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 etcdserver
|
||||
|
||||
import "sync"
|
||||
|
||||
// AccessController controls etcd server HTTP request access.
|
||||
type AccessController struct {
|
||||
corsMu sync.RWMutex
|
||||
CORS map[string]struct{}
|
||||
hostWhitelistMu sync.RWMutex
|
||||
HostWhitelist map[string]struct{}
|
||||
}
|
||||
|
||||
// NewAccessController returns a new "AccessController" with default "*" values.
|
||||
func NewAccessController() *AccessController {
|
||||
return &AccessController{
|
||||
CORS: map[string]struct{}{"*": {}},
|
||||
HostWhitelist: map[string]struct{}{"*": {}},
|
||||
}
|
||||
}
|
||||
|
||||
// OriginAllowed determines whether the server will allow a given CORS origin.
|
||||
// If CORS is empty, allow all.
|
||||
func (ac *AccessController) OriginAllowed(origin string) bool {
|
||||
ac.corsMu.RLock()
|
||||
defer ac.corsMu.RUnlock()
|
||||
if len(ac.CORS) == 0 { // allow all
|
||||
return true
|
||||
}
|
||||
_, ok := ac.CORS["*"]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
_, ok = ac.CORS[origin]
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsHostWhitelisted returns true if the host is whitelisted.
|
||||
// If whitelist is empty, allow all.
|
||||
func (ac *AccessController) IsHostWhitelisted(host string) bool {
|
||||
ac.hostWhitelistMu.RLock()
|
||||
defer ac.hostWhitelistMu.RUnlock()
|
||||
if len(ac.HostWhitelist) == 0 { // allow all
|
||||
return true
|
||||
}
|
||||
_, ok := ac.HostWhitelist["*"]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
_, ok = ac.HostWhitelist[host]
|
||||
return ok
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// 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 cors handles cross-origin HTTP requests (CORS).
|
||||
package cors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CORSInfo map[string]bool
|
||||
|
||||
// Set implements the flag.Value interface to allow users to define a list of CORS origins
|
||||
func (ci *CORSInfo) Set(s string) error {
|
||||
m := make(map[string]bool)
|
||||
for _, v := range strings.Split(s, ",") {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if v != "*" {
|
||||
if _, err := url.Parse(v); err != nil {
|
||||
return fmt.Errorf("Invalid CORS origin: %s", err)
|
||||
}
|
||||
}
|
||||
m[v] = true
|
||||
|
||||
}
|
||||
*ci = CORSInfo(m)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ci *CORSInfo) String() string {
|
||||
o := make([]string, 0)
|
||||
for k := range *ci {
|
||||
o = append(o, k)
|
||||
}
|
||||
sort.StringSlice(o).Sort()
|
||||
return strings.Join(o, ",")
|
||||
}
|
||||
|
||||
// OriginAllowed determines whether the server will allow a given CORS origin.
|
||||
func (c CORSInfo) OriginAllowed(origin string) bool {
|
||||
return c["*"] || c[origin]
|
||||
}
|
||||
|
||||
type CORSHandler struct {
|
||||
Handler http.Handler
|
||||
Info *CORSInfo
|
||||
}
|
||||
|
||||
// addHeader adds the correct cors headers given an origin
|
||||
func (h *CORSHandler) addHeader(w http.ResponseWriter, origin string) {
|
||||
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
|
||||
w.Header().Add("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Add("Access-Control-Allow-Headers", "accept, content-type, authorization")
|
||||
}
|
||||
|
||||
// ServeHTTP adds the correct CORS headers based on the origin and returns immediately
|
||||
// with a 200 OK if the method is OPTIONS.
|
||||
func (h *CORSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Write CORS header.
|
||||
if h.Info.OriginAllowed("*") {
|
||||
h.addHeader(w, "*")
|
||||
} else if origin := req.Header.Get("Origin"); h.Info.OriginAllowed(origin) {
|
||||
h.addHeader(w, origin)
|
||||
}
|
||||
|
||||
if req.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
h.Handler.ServeHTTP(w, req)
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// 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 cors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCORSInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
winfo CORSInfo
|
||||
ws string
|
||||
}{
|
||||
{"", CORSInfo{}, ""},
|
||||
{"http://127.0.0.1", CORSInfo{"http://127.0.0.1": true}, "http://127.0.0.1"},
|
||||
{"*", CORSInfo{"*": true}, "*"},
|
||||
// with space around
|
||||
{" http://127.0.0.1 ", CORSInfo{"http://127.0.0.1": true}, "http://127.0.0.1"},
|
||||
// multiple addrs
|
||||
{
|
||||
"http://127.0.0.1,http://127.0.0.2",
|
||||
CORSInfo{"http://127.0.0.1": true, "http://127.0.0.2": true},
|
||||
"http://127.0.0.1,http://127.0.0.2",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
info := CORSInfo{}
|
||||
if err := info.Set(tt.s); err != nil {
|
||||
t.Errorf("#%d: set error = %v, want nil", i, err)
|
||||
}
|
||||
if !reflect.DeepEqual(info, tt.winfo) {
|
||||
t.Errorf("#%d: info = %v, want %v", i, info, tt.winfo)
|
||||
}
|
||||
if g := info.String(); g != tt.ws {
|
||||
t.Errorf("#%d: info string = %s, want %s", i, g, tt.ws)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSInfoOriginAllowed(t *testing.T) {
|
||||
tests := []struct {
|
||||
set string
|
||||
origin string
|
||||
wallowed bool
|
||||
}{
|
||||
{"http://127.0.0.1,http://127.0.0.2", "http://127.0.0.1", true},
|
||||
{"http://127.0.0.1,http://127.0.0.2", "http://127.0.0.2", true},
|
||||
{"http://127.0.0.1,http://127.0.0.2", "*", false},
|
||||
{"http://127.0.0.1,http://127.0.0.2", "http://127.0.0.3", false},
|
||||
{"*", "*", true},
|
||||
{"*", "http://127.0.0.1", true},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
info := CORSInfo{}
|
||||
if err := info.Set(tt.set); err != nil {
|
||||
t.Errorf("#%d: set error = %v, want nil", i, err)
|
||||
}
|
||||
if g := info.OriginAllowed(tt.origin); g != tt.wallowed {
|
||||
t.Errorf("#%d: allowed = %v, want %v", i, g, tt.wallowed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSHandler(t *testing.T) {
|
||||
info := &CORSInfo{}
|
||||
if err := info.Set("http://127.0.0.1,http://127.0.0.2"); err != nil {
|
||||
t.Fatalf("unexpected set error: %v", err)
|
||||
}
|
||||
h := &CORSHandler{
|
||||
Handler: http.NotFoundHandler(),
|
||||
Info: info,
|
||||
}
|
||||
|
||||
header := func(origin string) http.Header {
|
||||
return http.Header{
|
||||
"Access-Control-Allow-Methods": []string{"POST, GET, OPTIONS, PUT, DELETE"},
|
||||
"Access-Control-Allow-Origin": []string{origin},
|
||||
"Access-Control-Allow-Headers": []string{"accept, content-type, authorization"},
|
||||
}
|
||||
}
|
||||
tests := []struct {
|
||||
method string
|
||||
origin string
|
||||
wcode int
|
||||
wheader http.Header
|
||||
}{
|
||||
{"GET", "http://127.0.0.1", http.StatusNotFound, header("http://127.0.0.1")},
|
||||
{"GET", "http://127.0.0.2", http.StatusNotFound, header("http://127.0.0.2")},
|
||||
{"GET", "http://127.0.0.3", http.StatusNotFound, http.Header{}},
|
||||
{"OPTIONS", "http://127.0.0.1", http.StatusOK, header("http://127.0.0.1")},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
rr := httptest.NewRecorder()
|
||||
req := &http.Request{
|
||||
Method: tt.method,
|
||||
Header: http.Header{"Origin": []string{tt.origin}},
|
||||
}
|
||||
h.ServeHTTP(rr, req)
|
||||
if rr.Code != tt.wcode {
|
||||
t.Errorf("#%d: code = %v, want %v", i, rr.Code, tt.wcode)
|
||||
}
|
||||
// it is set by http package, and there is no need to test it
|
||||
rr.HeaderMap.Del("Content-Type")
|
||||
rr.HeaderMap.Del("X-Content-Type-Options")
|
||||
if !reflect.DeepEqual(rr.HeaderMap, tt.wheader) {
|
||||
t.Errorf("#%d: header = %+v, want %+v", i, rr.HeaderMap, tt.wheader)
|
||||
}
|
||||
}
|
||||
}
|
76
pkg/flags/unique_strings.go
Normal file
76
pkg/flags/unique_strings.go
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright 2018 The etcd Authors
|
||||
//
|
||||
// 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 flags
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UniqueStringsValue wraps a list of unique strings.
|
||||
// The values are set in order.
|
||||
type UniqueStringsValue struct {
|
||||
Values map[string]struct{}
|
||||
}
|
||||
|
||||
// Set parses a command line set of strings, separated by comma.
|
||||
// Implements "flag.Value" interface.
|
||||
// The values are set in order.
|
||||
func (us *UniqueStringsValue) Set(s string) error {
|
||||
us.Values = make(map[string]struct{})
|
||||
for _, v := range strings.Split(s, ",") {
|
||||
us.Values[v] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String implements "flag.Value" interface.
|
||||
func (us *UniqueStringsValue) String() string {
|
||||
return strings.Join(us.stringSlice(), ",")
|
||||
}
|
||||
|
||||
func (us *UniqueStringsValue) stringSlice() []string {
|
||||
ss := make([]string, 0, len(us.Values))
|
||||
for v := range us.Values {
|
||||
ss = append(ss, v)
|
||||
}
|
||||
sort.Strings(ss)
|
||||
return ss
|
||||
}
|
||||
|
||||
// NewUniqueStringsValue implements string slice as "flag.Value" interface.
|
||||
// Given value is to be separated by comma.
|
||||
// The values are set in order.
|
||||
func NewUniqueStringsValue(s string) (us *UniqueStringsValue) {
|
||||
us = &UniqueStringsValue{Values: make(map[string]struct{})}
|
||||
if s == "" {
|
||||
return us
|
||||
}
|
||||
if err := us.Set(s); err != nil {
|
||||
plog.Panicf("new UniqueStringsValue should never fail: %v", err)
|
||||
}
|
||||
return us
|
||||
}
|
||||
|
||||
// UniqueStringsFromFlag returns a string slice from the flag.
|
||||
func UniqueStringsFromFlag(fs *flag.FlagSet, flagName string) []string {
|
||||
return []string((*fs.Lookup(flagName).Value.(*UniqueStringsValue)).stringSlice())
|
||||
}
|
||||
|
||||
// UniqueStringsMapFromFlag returns a map of strings from the flag.
|
||||
func UniqueStringsMapFromFlag(fs *flag.FlagSet, flagName string) map[string]struct{} {
|
||||
return (*fs.Lookup(flagName).Value.(*UniqueStringsValue)).Values
|
||||
}
|
68
pkg/flags/unique_strings_test.go
Normal file
68
pkg/flags/unique_strings_test.go
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright 2018 The etcd Authors
|
||||
//
|
||||
// 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 flags
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewUniqueStrings(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
exp map[string]struct{}
|
||||
rs string
|
||||
}{
|
||||
{ // non-URL but allowed by exception
|
||||
s: "*",
|
||||
exp: map[string]struct{}{"*": {}},
|
||||
rs: "*",
|
||||
},
|
||||
{
|
||||
s: "",
|
||||
exp: map[string]struct{}{},
|
||||
rs: "",
|
||||
},
|
||||
{
|
||||
s: "example.com",
|
||||
exp: map[string]struct{}{"example.com": {}},
|
||||
rs: "example.com",
|
||||
},
|
||||
{
|
||||
s: "localhost,localhost",
|
||||
exp: map[string]struct{}{"localhost": {}},
|
||||
rs: "localhost",
|
||||
},
|
||||
{
|
||||
s: "b.com,a.com",
|
||||
exp: map[string]struct{}{"a.com": {}, "b.com": {}},
|
||||
rs: "a.com,b.com",
|
||||
},
|
||||
{
|
||||
s: "c.com,b.com",
|
||||
exp: map[string]struct{}{"b.com": {}, "c.com": {}},
|
||||
rs: "b.com,c.com",
|
||||
},
|
||||
}
|
||||
for i := range tests {
|
||||
uv := NewUniqueStringsValue(tests[i].s)
|
||||
if !reflect.DeepEqual(tests[i].exp, uv.Values) {
|
||||
t.Fatalf("#%d: expected %+v, got %+v", i, tests[i].exp, uv.Values)
|
||||
}
|
||||
if uv.String() != tests[i].rs {
|
||||
t.Fatalf("#%d: expected %q, got %q", i, tests[i].rs, uv.String())
|
||||
}
|
||||
}
|
||||
}
|
92
pkg/flags/unique_urls.go
Normal file
92
pkg/flags/unique_urls.go
Normal file
@ -0,0 +1,92 @@
|
||||
// Copyright 2018 The etcd Authors
|
||||
//
|
||||
// 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 flags
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
)
|
||||
|
||||
// UniqueURLs contains unique URLs
|
||||
// with non-URL exceptions.
|
||||
type UniqueURLs struct {
|
||||
Values map[string]struct{}
|
||||
uss []url.URL
|
||||
Allowed map[string]struct{}
|
||||
}
|
||||
|
||||
// Set parses a command line set of URLs formatted like:
|
||||
// http://127.0.0.1:2380,http://10.1.1.2:80
|
||||
// Implements "flag.Value" interface.
|
||||
func (us *UniqueURLs) Set(s string) error {
|
||||
if _, ok := us.Values[s]; ok {
|
||||
return nil
|
||||
}
|
||||
if _, ok := us.Allowed[s]; ok {
|
||||
us.Values[s] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
ss, err := types.NewURLs(strings.Split(s, ","))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
us.Values = make(map[string]struct{})
|
||||
us.uss = make([]url.URL, 0)
|
||||
for _, v := range ss {
|
||||
us.Values[v.String()] = struct{}{}
|
||||
us.uss = append(us.uss, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String implements "flag.Value" interface.
|
||||
func (us *UniqueURLs) String() string {
|
||||
all := make([]string, 0, len(us.Values))
|
||||
for u := range us.Values {
|
||||
all = append(all, u)
|
||||
}
|
||||
sort.Strings(all)
|
||||
return strings.Join(all, ",")
|
||||
}
|
||||
|
||||
// NewUniqueURLsWithExceptions implements "url.URL" slice as flag.Value interface.
|
||||
// Given value is to be separated by comma.
|
||||
func NewUniqueURLsWithExceptions(s string, exceptions ...string) *UniqueURLs {
|
||||
us := &UniqueURLs{Values: make(map[string]struct{}), Allowed: make(map[string]struct{})}
|
||||
for _, v := range exceptions {
|
||||
us.Allowed[v] = struct{}{}
|
||||
}
|
||||
if s == "" {
|
||||
return us
|
||||
}
|
||||
if err := us.Set(s); err != nil {
|
||||
plog.Panicf("new UniqueURLs should never fail: %v", err)
|
||||
}
|
||||
return us
|
||||
}
|
||||
|
||||
// UniqueURLsFromFlag returns a slice from urls got from the flag.
|
||||
func UniqueURLsFromFlag(fs *flag.FlagSet, urlsFlagName string) []url.URL {
|
||||
return (*fs.Lookup(urlsFlagName).Value.(*UniqueURLs)).uss
|
||||
}
|
||||
|
||||
// UniqueURLsMapFromFlag returns a map from url strings got from the flag.
|
||||
func UniqueURLsMapFromFlag(fs *flag.FlagSet, urlsFlagName string) map[string]struct{} {
|
||||
return (*fs.Lookup(urlsFlagName).Value.(*UniqueURLs)).Values
|
||||
}
|
93
pkg/flags/unique_urls_test.go
Normal file
93
pkg/flags/unique_urls_test.go
Normal file
@ -0,0 +1,93 @@
|
||||
// Copyright 2018 The etcd Authors
|
||||
//
|
||||
// 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 flags
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewUniqueURLsWithExceptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
exp map[string]struct{}
|
||||
rs string
|
||||
exception string
|
||||
}{
|
||||
{ // non-URL but allowed by exception
|
||||
s: "*",
|
||||
exp: map[string]struct{}{"*": {}},
|
||||
rs: "*",
|
||||
exception: "*",
|
||||
},
|
||||
{
|
||||
s: "",
|
||||
exp: map[string]struct{}{},
|
||||
rs: "",
|
||||
exception: "*",
|
||||
},
|
||||
{
|
||||
s: "https://1.2.3.4:8080",
|
||||
exp: map[string]struct{}{"https://1.2.3.4:8080": {}},
|
||||
rs: "https://1.2.3.4:8080",
|
||||
exception: "*",
|
||||
},
|
||||
{
|
||||
s: "https://1.2.3.4:8080,https://1.2.3.4:8080",
|
||||
exp: map[string]struct{}{"https://1.2.3.4:8080": {}},
|
||||
rs: "https://1.2.3.4:8080",
|
||||
exception: "*",
|
||||
},
|
||||
{
|
||||
s: "http://10.1.1.1:80",
|
||||
exp: map[string]struct{}{"http://10.1.1.1:80": {}},
|
||||
rs: "http://10.1.1.1:80",
|
||||
exception: "*",
|
||||
},
|
||||
{
|
||||
s: "http://localhost:80",
|
||||
exp: map[string]struct{}{"http://localhost:80": {}},
|
||||
rs: "http://localhost:80",
|
||||
exception: "*",
|
||||
},
|
||||
{
|
||||
s: "http://:80",
|
||||
exp: map[string]struct{}{"http://:80": {}},
|
||||
rs: "http://:80",
|
||||
exception: "*",
|
||||
},
|
||||
{
|
||||
s: "https://localhost:5,https://localhost:3",
|
||||
exp: map[string]struct{}{"https://localhost:3": {}, "https://localhost:5": {}},
|
||||
rs: "https://localhost:3,https://localhost:5",
|
||||
exception: "*",
|
||||
},
|
||||
{
|
||||
s: "http://localhost:5,https://localhost:3",
|
||||
exp: map[string]struct{}{"https://localhost:3": {}, "http://localhost:5": {}},
|
||||
rs: "http://localhost:5,https://localhost:3",
|
||||
exception: "*",
|
||||
},
|
||||
}
|
||||
for i := range tests {
|
||||
uv := NewUniqueURLsWithExceptions(tests[i].s, tests[i].exception)
|
||||
if !reflect.DeepEqual(tests[i].exp, uv.Values) {
|
||||
t.Fatalf("#%d: expected %+v, got %+v", i, tests[i].exp, uv.Values)
|
||||
}
|
||||
if uv.String() != tests[i].rs {
|
||||
t.Fatalf("#%d: expected %q, got %q", i, tests[i].rs, uv.String())
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user