Merge pull request #9490 from gyuho/cors

*: support CORS for v3 HTTP requests
This commit is contained in:
Gyuho Lee 2018-03-27 09:04:26 -07:00 committed by GitHub
commit 473793be1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 713 additions and 460 deletions

View File

@ -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`

View File

@ -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 = ""

View File

@ -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) {

View File

@ -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)

View File

@ -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()

View File

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

View File

@ -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)

View File

@ -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!
`
)

View File

@ -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.

View File

@ -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

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

View File

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

View File

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

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

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

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