1// Copyright 2015 The etcd Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package v2http 16 17import ( 18 "context" 19 "encoding/json" 20 "errors" 21 "fmt" 22 "io/ioutil" 23 "net/http" 24 "net/url" 25 "path" 26 "strconv" 27 "strings" 28 "time" 29 30 "go.etcd.io/etcd/api/v3/etcdserverpb" 31 "go.etcd.io/etcd/client/pkg/v3/types" 32 "go.etcd.io/etcd/server/v3/etcdserver" 33 "go.etcd.io/etcd/server/v3/etcdserver/api" 34 "go.etcd.io/etcd/server/v3/etcdserver/api/etcdhttp" 35 "go.etcd.io/etcd/server/v3/etcdserver/api/membership" 36 "go.etcd.io/etcd/server/v3/etcdserver/api/v2auth" 37 "go.etcd.io/etcd/server/v3/etcdserver/api/v2error" 38 "go.etcd.io/etcd/server/v3/etcdserver/api/v2http/httptypes" 39 stats "go.etcd.io/etcd/server/v3/etcdserver/api/v2stats" 40 "go.etcd.io/etcd/server/v3/etcdserver/api/v2store" 41 42 "github.com/jonboulle/clockwork" 43 "go.uber.org/zap" 44) 45 46const ( 47 authPrefix = "/v2/auth" 48 keysPrefix = "/v2/keys" 49 machinesPrefix = "/v2/machines" 50 membersPrefix = "/v2/members" 51 statsPrefix = "/v2/stats" 52) 53 54// NewClientHandler generates a muxed http.Handler with the given parameters to serve etcd client requests. 55func NewClientHandler(lg *zap.Logger, server etcdserver.ServerPeer, timeout time.Duration) http.Handler { 56 if lg == nil { 57 lg = zap.NewNop() 58 } 59 mux := http.NewServeMux() 60 etcdhttp.HandleBasic(lg, mux, server) 61 etcdhttp.HandleMetricsHealth(lg, mux, server) 62 handleV2(lg, mux, server, timeout) 63 return requestLogger(lg, mux) 64} 65 66func handleV2(lg *zap.Logger, mux *http.ServeMux, server etcdserver.ServerV2, timeout time.Duration) { 67 sec := v2auth.NewStore(lg, server, timeout) 68 kh := &keysHandler{ 69 lg: lg, 70 sec: sec, 71 server: server, 72 cluster: server.Cluster(), 73 timeout: timeout, 74 clientCertAuthEnabled: server.ClientCertAuthEnabled(), 75 } 76 77 sh := &statsHandler{ 78 lg: lg, 79 stats: server, 80 } 81 82 mh := &membersHandler{ 83 lg: lg, 84 sec: sec, 85 server: server, 86 cluster: server.Cluster(), 87 timeout: timeout, 88 clock: clockwork.NewRealClock(), 89 clientCertAuthEnabled: server.ClientCertAuthEnabled(), 90 } 91 92 mah := &machinesHandler{cluster: server.Cluster()} 93 94 sech := &authHandler{ 95 lg: lg, 96 sec: sec, 97 cluster: server.Cluster(), 98 clientCertAuthEnabled: server.ClientCertAuthEnabled(), 99 } 100 mux.HandleFunc("/", http.NotFound) 101 mux.Handle(keysPrefix, kh) 102 mux.Handle(keysPrefix+"/", kh) 103 mux.HandleFunc(statsPrefix+"/store", sh.serveStore) 104 mux.HandleFunc(statsPrefix+"/self", sh.serveSelf) 105 mux.HandleFunc(statsPrefix+"/leader", sh.serveLeader) 106 mux.Handle(membersPrefix, mh) 107 mux.Handle(membersPrefix+"/", mh) 108 mux.Handle(machinesPrefix, mah) 109 handleAuth(mux, sech) 110} 111 112type keysHandler struct { 113 lg *zap.Logger 114 sec v2auth.Store 115 server etcdserver.ServerV2 116 cluster api.Cluster 117 timeout time.Duration 118 clientCertAuthEnabled bool 119} 120 121func (h *keysHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 122 if !allowMethod(w, r.Method, "HEAD", "GET", "PUT", "POST", "DELETE") { 123 return 124 } 125 126 w.Header().Set("X-Etcd-Cluster-ID", h.cluster.ID().String()) 127 128 ctx, cancel := context.WithTimeout(context.Background(), h.timeout) 129 defer cancel() 130 clock := clockwork.NewRealClock() 131 startTime := clock.Now() 132 rr, noValueOnSuccess, err := parseKeyRequest(r, clock) 133 if err != nil { 134 writeKeyError(h.lg, w, err) 135 return 136 } 137 // The path must be valid at this point (we've parsed the request successfully). 138 if !hasKeyPrefixAccess(h.lg, h.sec, r, r.URL.Path[len(keysPrefix):], rr.Recursive, h.clientCertAuthEnabled) { 139 writeKeyNoAuth(w) 140 return 141 } 142 if !rr.Wait { 143 reportRequestReceived(rr) 144 } 145 resp, err := h.server.Do(ctx, rr) 146 if err != nil { 147 err = trimErrorPrefix(err, etcdserver.StoreKeysPrefix) 148 writeKeyError(h.lg, w, err) 149 reportRequestFailed(rr, err) 150 return 151 } 152 switch { 153 case resp.Event != nil: 154 if err := writeKeyEvent(w, resp, noValueOnSuccess); err != nil { 155 // Should never be reached 156 h.lg.Warn("failed to write key event", zap.Error(err)) 157 } 158 reportRequestCompleted(rr, startTime) 159 case resp.Watcher != nil: 160 ctx, cancel := context.WithTimeout(context.Background(), defaultWatchTimeout) 161 defer cancel() 162 handleKeyWatch(ctx, h.lg, w, resp, rr.Stream) 163 default: 164 writeKeyError(h.lg, w, errors.New("received response with no Event/Watcher")) 165 } 166} 167 168type machinesHandler struct { 169 cluster api.Cluster 170} 171 172func (h *machinesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 173 if !allowMethod(w, r.Method, "GET", "HEAD") { 174 return 175 } 176 endpoints := h.cluster.ClientURLs() 177 w.Write([]byte(strings.Join(endpoints, ", "))) 178} 179 180type membersHandler struct { 181 lg *zap.Logger 182 sec v2auth.Store 183 server etcdserver.ServerV2 184 cluster api.Cluster 185 timeout time.Duration 186 clock clockwork.Clock 187 clientCertAuthEnabled bool 188} 189 190func (h *membersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 191 if !allowMethod(w, r.Method, "GET", "POST", "DELETE", "PUT") { 192 return 193 } 194 if !hasWriteRootAccess(h.lg, h.sec, r, h.clientCertAuthEnabled) { 195 writeNoAuth(h.lg, w, r) 196 return 197 } 198 w.Header().Set("X-Etcd-Cluster-ID", h.cluster.ID().String()) 199 200 ctx, cancel := context.WithTimeout(context.Background(), h.timeout) 201 defer cancel() 202 203 switch r.Method { 204 case "GET": 205 switch trimPrefix(r.URL.Path, membersPrefix) { 206 case "": 207 mc := newMemberCollection(h.cluster.Members()) 208 w.Header().Set("Content-Type", "application/json") 209 if err := json.NewEncoder(w).Encode(mc); err != nil { 210 h.lg.Warn("failed to encode members response", zap.Error(err)) 211 } 212 case "leader": 213 id := h.server.Leader() 214 if id == 0 { 215 writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusServiceUnavailable, "During election")) 216 return 217 } 218 m := newMember(h.cluster.Member(id)) 219 w.Header().Set("Content-Type", "application/json") 220 if err := json.NewEncoder(w).Encode(m); err != nil { 221 h.lg.Warn("failed to encode members response", zap.Error(err)) 222 } 223 default: 224 writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusNotFound, "Not found")) 225 } 226 227 case "POST": 228 req := httptypes.MemberCreateRequest{} 229 if ok := unmarshalRequest(h.lg, r, &req, w); !ok { 230 return 231 } 232 now := h.clock.Now() 233 m := membership.NewMember("", req.PeerURLs, "", &now) 234 _, err := h.server.AddMember(ctx, *m) 235 switch { 236 case err == membership.ErrIDExists || err == membership.ErrPeerURLexists: 237 writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusConflict, err.Error())) 238 return 239 case err != nil: 240 h.lg.Warn( 241 "failed to add a member", 242 zap.String("member-id", m.ID.String()), 243 zap.Error(err), 244 ) 245 writeError(h.lg, w, r, err) 246 return 247 } 248 res := newMember(m) 249 w.Header().Set("Content-Type", "application/json") 250 w.WriteHeader(http.StatusCreated) 251 if err := json.NewEncoder(w).Encode(res); err != nil { 252 h.lg.Warn("failed to encode members response", zap.Error(err)) 253 } 254 255 case "DELETE": 256 id, ok := getID(h.lg, r.URL.Path, w) 257 if !ok { 258 return 259 } 260 _, err := h.server.RemoveMember(ctx, uint64(id)) 261 switch { 262 case err == membership.ErrIDRemoved: 263 writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusGone, fmt.Sprintf("Member permanently removed: %s", id))) 264 case err == membership.ErrIDNotFound: 265 writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", id))) 266 case err != nil: 267 h.lg.Warn( 268 "failed to remove a member", 269 zap.String("member-id", id.String()), 270 zap.Error(err), 271 ) 272 writeError(h.lg, w, r, err) 273 default: 274 w.WriteHeader(http.StatusNoContent) 275 } 276 277 case "PUT": 278 id, ok := getID(h.lg, r.URL.Path, w) 279 if !ok { 280 return 281 } 282 req := httptypes.MemberUpdateRequest{} 283 if ok := unmarshalRequest(h.lg, r, &req, w); !ok { 284 return 285 } 286 m := membership.Member{ 287 ID: id, 288 RaftAttributes: membership.RaftAttributes{PeerURLs: req.PeerURLs.StringSlice()}, 289 } 290 _, err := h.server.UpdateMember(ctx, m) 291 switch { 292 case err == membership.ErrPeerURLexists: 293 writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusConflict, err.Error())) 294 case err == membership.ErrIDNotFound: 295 writeError(h.lg, w, r, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", id))) 296 case err != nil: 297 h.lg.Warn( 298 "failed to update a member", 299 zap.String("member-id", m.ID.String()), 300 zap.Error(err), 301 ) 302 writeError(h.lg, w, r, err) 303 default: 304 w.WriteHeader(http.StatusNoContent) 305 } 306 } 307} 308 309type statsHandler struct { 310 lg *zap.Logger 311 stats stats.Stats 312} 313 314func (h *statsHandler) serveStore(w http.ResponseWriter, r *http.Request) { 315 if !allowMethod(w, r.Method, "GET") { 316 return 317 } 318 w.Header().Set("Content-Type", "application/json") 319 w.Write(h.stats.StoreStats()) 320} 321 322func (h *statsHandler) serveSelf(w http.ResponseWriter, r *http.Request) { 323 if !allowMethod(w, r.Method, "GET") { 324 return 325 } 326 w.Header().Set("Content-Type", "application/json") 327 w.Write(h.stats.SelfStats()) 328} 329 330func (h *statsHandler) serveLeader(w http.ResponseWriter, r *http.Request) { 331 if !allowMethod(w, r.Method, "GET") { 332 return 333 } 334 stats := h.stats.LeaderStats() 335 if stats == nil { 336 etcdhttp.WriteError(h.lg, w, r, httptypes.NewHTTPError(http.StatusForbidden, "not current leader")) 337 return 338 } 339 w.Header().Set("Content-Type", "application/json") 340 w.Write(stats) 341} 342 343// parseKeyRequest converts a received http.Request on keysPrefix to 344// a server Request, performing validation of supplied fields as appropriate. 345// If any validation fails, an empty Request and non-nil error is returned. 346func parseKeyRequest(r *http.Request, clock clockwork.Clock) (etcdserverpb.Request, bool, error) { 347 var noValueOnSuccess bool 348 emptyReq := etcdserverpb.Request{} 349 350 err := r.ParseForm() 351 if err != nil { 352 return emptyReq, false, v2error.NewRequestError( 353 v2error.EcodeInvalidForm, 354 err.Error(), 355 ) 356 } 357 358 if !strings.HasPrefix(r.URL.Path, keysPrefix) { 359 return emptyReq, false, v2error.NewRequestError( 360 v2error.EcodeInvalidForm, 361 "incorrect key prefix", 362 ) 363 } 364 p := path.Join(etcdserver.StoreKeysPrefix, r.URL.Path[len(keysPrefix):]) 365 366 var pIdx, wIdx uint64 367 if pIdx, err = getUint64(r.Form, "prevIndex"); err != nil { 368 return emptyReq, false, v2error.NewRequestError( 369 v2error.EcodeIndexNaN, 370 `invalid value for "prevIndex"`, 371 ) 372 } 373 if wIdx, err = getUint64(r.Form, "waitIndex"); err != nil { 374 return emptyReq, false, v2error.NewRequestError( 375 v2error.EcodeIndexNaN, 376 `invalid value for "waitIndex"`, 377 ) 378 } 379 380 var rec, sort, wait, dir, quorum, stream bool 381 if rec, err = getBool(r.Form, "recursive"); err != nil { 382 return emptyReq, false, v2error.NewRequestError( 383 v2error.EcodeInvalidField, 384 `invalid value for "recursive"`, 385 ) 386 } 387 if sort, err = getBool(r.Form, "sorted"); err != nil { 388 return emptyReq, false, v2error.NewRequestError( 389 v2error.EcodeInvalidField, 390 `invalid value for "sorted"`, 391 ) 392 } 393 if wait, err = getBool(r.Form, "wait"); err != nil { 394 return emptyReq, false, v2error.NewRequestError( 395 v2error.EcodeInvalidField, 396 `invalid value for "wait"`, 397 ) 398 } 399 // TODO(jonboulle): define what parameters dir is/isn't compatible with? 400 if dir, err = getBool(r.Form, "dir"); err != nil { 401 return emptyReq, false, v2error.NewRequestError( 402 v2error.EcodeInvalidField, 403 `invalid value for "dir"`, 404 ) 405 } 406 if quorum, err = getBool(r.Form, "quorum"); err != nil { 407 return emptyReq, false, v2error.NewRequestError( 408 v2error.EcodeInvalidField, 409 `invalid value for "quorum"`, 410 ) 411 } 412 if stream, err = getBool(r.Form, "stream"); err != nil { 413 return emptyReq, false, v2error.NewRequestError( 414 v2error.EcodeInvalidField, 415 `invalid value for "stream"`, 416 ) 417 } 418 419 if wait && r.Method != "GET" { 420 return emptyReq, false, v2error.NewRequestError( 421 v2error.EcodeInvalidField, 422 `"wait" can only be used with GET requests`, 423 ) 424 } 425 426 pV := r.FormValue("prevValue") 427 if _, ok := r.Form["prevValue"]; ok && pV == "" { 428 return emptyReq, false, v2error.NewRequestError( 429 v2error.EcodePrevValueRequired, 430 `"prevValue" cannot be empty`, 431 ) 432 } 433 434 if noValueOnSuccess, err = getBool(r.Form, "noValueOnSuccess"); err != nil { 435 return emptyReq, false, v2error.NewRequestError( 436 v2error.EcodeInvalidField, 437 `invalid value for "noValueOnSuccess"`, 438 ) 439 } 440 441 // TTL is nullable, so leave it null if not specified 442 // or an empty string 443 var ttl *uint64 444 if len(r.FormValue("ttl")) > 0 { 445 i, err := getUint64(r.Form, "ttl") 446 if err != nil { 447 return emptyReq, false, v2error.NewRequestError( 448 v2error.EcodeTTLNaN, 449 `invalid value for "ttl"`, 450 ) 451 } 452 ttl = &i 453 } 454 455 // prevExist is nullable, so leave it null if not specified 456 var pe *bool 457 if _, ok := r.Form["prevExist"]; ok { 458 bv, err := getBool(r.Form, "prevExist") 459 if err != nil { 460 return emptyReq, false, v2error.NewRequestError( 461 v2error.EcodeInvalidField, 462 "invalid value for prevExist", 463 ) 464 } 465 pe = &bv 466 } 467 468 // refresh is nullable, so leave it null if not specified 469 var refresh *bool 470 if _, ok := r.Form["refresh"]; ok { 471 bv, err := getBool(r.Form, "refresh") 472 if err != nil { 473 return emptyReq, false, v2error.NewRequestError( 474 v2error.EcodeInvalidField, 475 "invalid value for refresh", 476 ) 477 } 478 refresh = &bv 479 if refresh != nil && *refresh { 480 val := r.FormValue("value") 481 if _, ok := r.Form["value"]; ok && val != "" { 482 return emptyReq, false, v2error.NewRequestError( 483 v2error.EcodeRefreshValue, 484 `A value was provided on a refresh`, 485 ) 486 } 487 if ttl == nil { 488 return emptyReq, false, v2error.NewRequestError( 489 v2error.EcodeRefreshTTLRequired, 490 `No TTL value set`, 491 ) 492 } 493 } 494 } 495 496 rr := etcdserverpb.Request{ 497 Method: r.Method, 498 Path: p, 499 Val: r.FormValue("value"), 500 Dir: dir, 501 PrevValue: pV, 502 PrevIndex: pIdx, 503 PrevExist: pe, 504 Wait: wait, 505 Since: wIdx, 506 Recursive: rec, 507 Sorted: sort, 508 Quorum: quorum, 509 Stream: stream, 510 } 511 512 if pe != nil { 513 rr.PrevExist = pe 514 } 515 516 if refresh != nil { 517 rr.Refresh = refresh 518 } 519 520 // Null TTL is equivalent to unset Expiration 521 if ttl != nil { 522 expr := time.Duration(*ttl) * time.Second 523 rr.Expiration = clock.Now().Add(expr).UnixNano() 524 } 525 526 return rr, noValueOnSuccess, nil 527} 528 529// writeKeyEvent trims the prefix of key path in a single Event under 530// StoreKeysPrefix, serializes it and writes the resulting JSON to the given 531// ResponseWriter, along with the appropriate headers. 532func writeKeyEvent(w http.ResponseWriter, resp etcdserver.Response, noValueOnSuccess bool) error { 533 ev := resp.Event 534 if ev == nil { 535 return errors.New("cannot write empty Event") 536 } 537 w.Header().Set("Content-Type", "application/json") 538 w.Header().Set("X-Etcd-Index", fmt.Sprint(ev.EtcdIndex)) 539 w.Header().Set("X-Raft-Index", fmt.Sprint(resp.Index)) 540 w.Header().Set("X-Raft-Term", fmt.Sprint(resp.Term)) 541 542 if ev.IsCreated() { 543 w.WriteHeader(http.StatusCreated) 544 } 545 546 ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix) 547 if noValueOnSuccess && 548 (ev.Action == v2store.Set || ev.Action == v2store.CompareAndSwap || 549 ev.Action == v2store.Create || ev.Action == v2store.Update) { 550 ev.Node = nil 551 ev.PrevNode = nil 552 } 553 return json.NewEncoder(w).Encode(ev) 554} 555 556func writeKeyNoAuth(w http.ResponseWriter) { 557 e := v2error.NewError(v2error.EcodeUnauthorized, "Insufficient credentials", 0) 558 e.WriteTo(w) 559} 560 561// writeKeyError logs and writes the given Error to the ResponseWriter. 562// If Error is not an etcdErr, the error will be converted to an etcd error. 563func writeKeyError(lg *zap.Logger, w http.ResponseWriter, err error) { 564 if err == nil { 565 return 566 } 567 switch e := err.(type) { 568 case *v2error.Error: 569 e.WriteTo(w) 570 default: 571 switch err { 572 case etcdserver.ErrTimeoutDueToLeaderFail, etcdserver.ErrTimeoutDueToConnectionLost: 573 if lg != nil { 574 lg.Warn( 575 "v2 response error", 576 zap.String("internal-server-error", err.Error()), 577 ) 578 } 579 default: 580 if lg != nil { 581 lg.Warn( 582 "unexpected v2 response error", 583 zap.String("internal-server-error", err.Error()), 584 ) 585 } 586 } 587 ee := v2error.NewError(v2error.EcodeRaftInternal, err.Error(), 0) 588 ee.WriteTo(w) 589 } 590} 591 592func handleKeyWatch(ctx context.Context, lg *zap.Logger, w http.ResponseWriter, resp etcdserver.Response, stream bool) { 593 wa := resp.Watcher 594 defer wa.Remove() 595 ech := wa.EventChan() 596 var nch <-chan bool 597 if x, ok := w.(http.CloseNotifier); ok { 598 nch = x.CloseNotify() 599 } 600 601 w.Header().Set("Content-Type", "application/json") 602 w.Header().Set("X-Etcd-Index", fmt.Sprint(wa.StartIndex())) 603 w.Header().Set("X-Raft-Index", fmt.Sprint(resp.Index)) 604 w.Header().Set("X-Raft-Term", fmt.Sprint(resp.Term)) 605 w.WriteHeader(http.StatusOK) 606 607 // Ensure headers are flushed early, in case of long polling 608 w.(http.Flusher).Flush() 609 610 for { 611 select { 612 case <-nch: 613 // Client closed connection. Nothing to do. 614 return 615 case <-ctx.Done(): 616 // Timed out. net/http will close the connection for us, so nothing to do. 617 return 618 case ev, ok := <-ech: 619 if !ok { 620 // If the channel is closed this may be an indication of 621 // that notifications are much more than we are able to 622 // send to the client in time. Then we simply end streaming. 623 return 624 } 625 ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix) 626 if err := json.NewEncoder(w).Encode(ev); err != nil { 627 // Should never be reached 628 lg.Warn("failed to encode event", zap.Error(err)) 629 return 630 } 631 if !stream { 632 return 633 } 634 w.(http.Flusher).Flush() 635 } 636 } 637} 638 639func trimEventPrefix(ev *v2store.Event, prefix string) *v2store.Event { 640 if ev == nil { 641 return nil 642 } 643 // Since the *Event may reference one in the store history 644 // history, we must copy it before modifying 645 e := ev.Clone() 646 trimNodeExternPrefix(e.Node, prefix) 647 trimNodeExternPrefix(e.PrevNode, prefix) 648 return e 649} 650 651func trimNodeExternPrefix(n *v2store.NodeExtern, prefix string) { 652 if n == nil { 653 return 654 } 655 n.Key = strings.TrimPrefix(n.Key, prefix) 656 for _, nn := range n.Nodes { 657 trimNodeExternPrefix(nn, prefix) 658 } 659} 660 661func trimErrorPrefix(err error, prefix string) error { 662 if e, ok := err.(*v2error.Error); ok { 663 e.Cause = strings.TrimPrefix(e.Cause, prefix) 664 } 665 return err 666} 667 668func unmarshalRequest(lg *zap.Logger, r *http.Request, req json.Unmarshaler, w http.ResponseWriter) bool { 669 ctype := r.Header.Get("Content-Type") 670 semicolonPosition := strings.Index(ctype, ";") 671 if semicolonPosition != -1 { 672 ctype = strings.TrimSpace(strings.ToLower(ctype[0:semicolonPosition])) 673 } 674 if ctype != "application/json" { 675 writeError(lg, w, r, httptypes.NewHTTPError(http.StatusUnsupportedMediaType, fmt.Sprintf("Bad Content-Type %s, accept application/json", ctype))) 676 return false 677 } 678 b, err := ioutil.ReadAll(r.Body) 679 if err != nil { 680 writeError(lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, err.Error())) 681 return false 682 } 683 if err := req.UnmarshalJSON(b); err != nil { 684 writeError(lg, w, r, httptypes.NewHTTPError(http.StatusBadRequest, err.Error())) 685 return false 686 } 687 return true 688} 689 690func getID(lg *zap.Logger, p string, w http.ResponseWriter) (types.ID, bool) { 691 idStr := trimPrefix(p, membersPrefix) 692 if idStr == "" { 693 http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) 694 return 0, false 695 } 696 id, err := types.IDFromString(idStr) 697 if err != nil { 698 writeError(lg, w, nil, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", idStr))) 699 return 0, false 700 } 701 return id, true 702} 703 704// getUint64 extracts a uint64 by the given key from a Form. If the key does 705// not exist in the form, 0 is returned. If the key exists but the value is 706// badly formed, an error is returned. If multiple values are present only the 707// first is considered. 708func getUint64(form url.Values, key string) (i uint64, err error) { 709 if vals, ok := form[key]; ok { 710 i, err = strconv.ParseUint(vals[0], 10, 64) 711 } 712 return 713} 714 715// getBool extracts a bool by the given key from a Form. If the key does not 716// exist in the form, false is returned. If the key exists but the value is 717// badly formed, an error is returned. If multiple values are present only the 718// first is considered. 719func getBool(form url.Values, key string) (b bool, err error) { 720 if vals, ok := form[key]; ok { 721 b, err = strconv.ParseBool(vals[0]) 722 } 723 return 724} 725 726// trimPrefix removes a given prefix and any slash following the prefix 727// e.g.: trimPrefix("foo", "foo") == trimPrefix("foo/", "foo") == "" 728func trimPrefix(p, prefix string) (s string) { 729 s = strings.TrimPrefix(p, prefix) 730 s = strings.TrimPrefix(s, "/") 731 return 732} 733 734func newMemberCollection(ms []*membership.Member) *httptypes.MemberCollection { 735 c := httptypes.MemberCollection(make([]httptypes.Member, len(ms))) 736 737 for i, m := range ms { 738 c[i] = newMember(m) 739 } 740 741 return &c 742} 743 744func newMember(m *membership.Member) httptypes.Member { 745 tm := httptypes.Member{ 746 ID: m.ID.String(), 747 Name: m.Name, 748 PeerURLs: make([]string, len(m.PeerURLs)), 749 ClientURLs: make([]string, len(m.ClientURLs)), 750 } 751 752 copy(tm.PeerURLs, m.PeerURLs) 753 copy(tm.ClientURLs, m.ClientURLs) 754 755 return tm 756} 757