Skip to content

Commit 0a972da

Browse files
authored
Merge pull request #9379 from gyuho/fix-election
*: fix server panic on invalid Election Proclaim/Resign HTTP requests
2 parents 48ff9e6 + 2f909a9 commit 0a972da

File tree

3 files changed

+135
-4
lines changed

3 files changed

+135
-4
lines changed

e2e/util.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,14 @@ func spawnWithExpect(args []string, expected string) error {
4242
}
4343

4444
func spawnWithExpects(args []string, xs ...string) error {
45+
_, err := spawnWithExpectLines(args, xs...)
46+
return err
47+
}
48+
49+
func spawnWithExpectLines(args []string, xs ...string) ([]string, error) {
4550
proc, err := spawnCmd(args)
4651
if err != nil {
47-
return err
52+
return nil, err
4853
}
4954
// process until either stdout or stderr contains
5055
// the expected string
@@ -57,7 +62,7 @@ func spawnWithExpects(args []string, xs ...string) error {
5762
l, lerr := proc.ExpectFunc(lineFunc)
5863
if lerr != nil {
5964
proc.Close()
60-
return fmt.Errorf("%v (expected %q, got %q)", lerr, txt, lines)
65+
return nil, fmt.Errorf("%v (expected %q, got %q)", lerr, txt, lines)
6166
}
6267
lines = append(lines, l)
6368
if strings.Contains(l, txt) {
@@ -67,9 +72,9 @@ func spawnWithExpects(args []string, xs ...string) error {
6772
}
6873
perr := proc.Close()
6974
if len(xs) == 0 && proc.LineCount() != noOutputLineCount { // expect no output
70-
return fmt.Errorf("unexpected output (got lines %q, line count %d)", lines, proc.LineCount())
75+
return nil, fmt.Errorf("unexpected output (got lines %q, line count %d)", lines, proc.LineCount())
7176
}
72-
return perr
77+
return lines, perr
7378
}
7479

7580
func closeWithTimeout(p *expect.ExpectProcess, d time.Duration) error {

e2e/v3_curl_test.go

+115
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
package e2e
1616

1717
import (
18+
"encoding/base64"
1819
"encoding/json"
1920
"path"
21+
"strconv"
2022
"testing"
2123

24+
epb "github.com/coreos/etcd/etcdserver/api/v3election/v3electionpb"
2225
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
2326
"github.com/coreos/etcd/pkg/testutil"
2427

@@ -245,3 +248,115 @@ func testV3CurlAuth(cx ctlCtx) {
245248
cx.t.Fatalf("failed testV3CurlAuth auth put with curl using prefix (%s) (%v)", p, err)
246249
}
247250
}
251+
252+
func TestV3CurlCampaignNoTLS(t *testing.T) {
253+
for _, p := range apiPrefix {
254+
testCtl(t, testV3CurlCampaign, withApiPrefix(p), withCfg(configNoTLS))
255+
}
256+
}
257+
258+
func testV3CurlCampaign(cx ctlCtx) {
259+
cdata, err := json.Marshal(&epb.CampaignRequest{
260+
Name: []byte("/election-prefix"),
261+
Value: []byte("v1"),
262+
})
263+
if err != nil {
264+
cx.t.Fatal(err)
265+
}
266+
cargs := cURLPrefixArgs(cx.epc, "POST", cURLReq{
267+
endpoint: path.Join(cx.apiPrefix, "/election/campaign"),
268+
value: string(cdata),
269+
})
270+
lines, err := spawnWithExpectLines(cargs, `"leader":{"name":"`)
271+
if err != nil {
272+
cx.t.Fatalf("failed post campaign request (%s) (%v)", cx.apiPrefix, err)
273+
}
274+
if len(lines) != 1 {
275+
cx.t.Fatalf("len(lines) expected 1, got %+v", lines)
276+
}
277+
278+
var cresp campaignResponse
279+
if err = json.Unmarshal([]byte(lines[0]), &cresp); err != nil {
280+
cx.t.Fatalf("failed to unmarshal campaign response %v", err)
281+
}
282+
ndata, err := base64.StdEncoding.DecodeString(cresp.Leader.Name)
283+
if err != nil {
284+
cx.t.Fatalf("failed to decode leader key %v", err)
285+
}
286+
kdata, err := base64.StdEncoding.DecodeString(cresp.Leader.Key)
287+
if err != nil {
288+
cx.t.Fatalf("failed to decode leader key %v", err)
289+
}
290+
291+
rev, _ := strconv.ParseInt(cresp.Leader.Rev, 10, 64)
292+
lease, _ := strconv.ParseInt(cresp.Leader.Lease, 10, 64)
293+
pdata, err := json.Marshal(&epb.ProclaimRequest{
294+
Leader: &epb.LeaderKey{
295+
Name: ndata,
296+
Key: kdata,
297+
Rev: rev,
298+
Lease: lease,
299+
},
300+
Value: []byte("v2"),
301+
})
302+
if err != nil {
303+
cx.t.Fatal(err)
304+
}
305+
if err = cURLPost(cx.epc, cURLReq{
306+
endpoint: path.Join(cx.apiPrefix, "/election/proclaim"),
307+
value: string(pdata),
308+
expected: `"revision":`,
309+
}); err != nil {
310+
cx.t.Fatalf("failed post proclaim request (%s) (%v)", cx.apiPrefix, err)
311+
}
312+
}
313+
314+
func TestV3CurlProclaimMissiongLeaderKeyNoTLS(t *testing.T) {
315+
for _, p := range apiPrefix {
316+
testCtl(t, testV3CurlProclaimMissiongLeaderKey, withApiPrefix(p), withCfg(configNoTLS))
317+
}
318+
}
319+
320+
func testV3CurlProclaimMissiongLeaderKey(cx ctlCtx) {
321+
pdata, err := json.Marshal(&epb.ProclaimRequest{Value: []byte("v2")})
322+
if err != nil {
323+
cx.t.Fatal(err)
324+
}
325+
if err != nil {
326+
cx.t.Fatal(err)
327+
}
328+
if err = cURLPost(cx.epc, cURLReq{
329+
endpoint: path.Join(cx.apiPrefix, "/election/proclaim"),
330+
value: string(pdata),
331+
expected: `{"error":"\"leader\" field must be provided","code":2}`,
332+
}); err != nil {
333+
cx.t.Fatalf("failed post proclaim request (%s) (%v)", cx.apiPrefix, err)
334+
}
335+
}
336+
337+
func TestV3CurlResignMissiongLeaderKeyNoTLS(t *testing.T) {
338+
for _, p := range apiPrefix {
339+
testCtl(t, testV3CurlResignMissiongLeaderKey, withApiPrefix(p), withCfg(configNoTLS))
340+
}
341+
}
342+
343+
func testV3CurlResignMissiongLeaderKey(cx ctlCtx) {
344+
if err := cURLPost(cx.epc, cURLReq{
345+
endpoint: path.Join(cx.apiPrefix, "/election/resign"),
346+
value: `{}`,
347+
expected: `{"error":"\"leader\" field must be provided","code":2}`,
348+
}); err != nil {
349+
cx.t.Fatalf("failed post resign request (%s) (%v)", cx.apiPrefix, err)
350+
}
351+
}
352+
353+
// to manually decode; JSON marshals integer fields with
354+
// string types, so can't unmarshal with epb.CampaignResponse
355+
type campaignResponse struct {
356+
Leader struct {
357+
Name string `json:"name,omitempty"`
358+
Key string `json:"key,omitempty"`
359+
Rev string `json:"rev,omitempty"`
360+
Lease string `json:"lease,omitempty"`
361+
} `json:"leader,omitempty"`
362+
}

etcdserver/api/v3election/election.go

+11
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@ package v3election
1616

1717
import (
1818
"context"
19+
"errors"
1920

2021
"github.com/coreos/etcd/clientv3"
2122
"github.com/coreos/etcd/clientv3/concurrency"
2223
epb "github.com/coreos/etcd/etcdserver/api/v3election/v3electionpb"
2324
)
2425

26+
// ErrMissingLeaderKey is returned when election API request
27+
// is missing the "leader" field.
28+
var ErrMissingLeaderKey = errors.New(`"leader" field must be provided`)
29+
2530
type electionServer struct {
2631
c *clientv3.Client
2732
}
@@ -51,6 +56,9 @@ func (es *electionServer) Campaign(ctx context.Context, req *epb.CampaignRequest
5156
}
5257

5358
func (es *electionServer) Proclaim(ctx context.Context, req *epb.ProclaimRequest) (*epb.ProclaimResponse, error) {
59+
if req.Leader == nil {
60+
return nil, ErrMissingLeaderKey
61+
}
5462
s, err := es.session(ctx, req.Leader.Lease)
5563
if err != nil {
5664
return nil, err
@@ -98,6 +106,9 @@ func (es *electionServer) Leader(ctx context.Context, req *epb.LeaderRequest) (*
98106
}
99107

100108
func (es *electionServer) Resign(ctx context.Context, req *epb.ResignRequest) (*epb.ResignResponse, error) {
109+
if req.Leader == nil {
110+
return nil, ErrMissingLeaderKey
111+
}
101112
s, err := es.session(ctx, req.Leader.Lease)
102113
if err != nil {
103114
return nil, err

0 commit comments

Comments
 (0)