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