Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http_api (ticdc): check --cert-allowed-cn before add server common name #3628

Merged
merged 9 commits into from
Dec 14, 2021
6 changes: 6 additions & 0 deletions cdc/capture/capture.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ func NewCapture(pdClient pd.Client, kvStorage tidbkv.Storage, etcdClient *etcd.C
}
}

func NewCapture4Test() *Capture {
return &Capture{
info: &model.CaptureInfo{ID: "capture-for-test", AdvertiseAddr: "127.0.0.1", Version: "test"},
}
}

func (c *Capture) reset(ctx context.Context) error {
c.captureMu.Lock()
defer c.captureMu.Unlock()
Expand Down
10 changes: 0 additions & 10 deletions cdc/capture/http_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -672,11 +672,6 @@ func (h *HTTPHandler) ListCapture(c *gin.Context) {
// @Failure 500,400 {object} model.HTTPError
// @Router /api/v1/status [get]
func (h *HTTPHandler) ServerStatus(c *gin.Context) {
if !h.capture.IsOwner() {
h.forwardToOwner(c)
return
}

status := model.ServerStatus{
Version: version.ReleaseVersion,
GitHash: version.GitHash,
Expand All @@ -697,11 +692,6 @@ func (h *HTTPHandler) ServerStatus(c *gin.Context) {
// @Failure 500 {object} model.HTTPError
// @Router /api/v1/health [get]
func (h *HTTPHandler) Health(c *gin.Context) {
if !h.capture.IsOwner() {
h.forwardToOwner(c)
return
}

ctx := c.Request.Context()

if _, err := h.capture.GetOwner(ctx); err != nil {
Expand Down
14 changes: 10 additions & 4 deletions cdc/http_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,17 @@ func (s *Server) startStatusHTTP() error {
prometheus.DefaultGatherer = registry
router.Any("/metrics", gin.WrapH(promhttp.Handler()))

err := conf.Security.AddSelfCommonName()
if err != nil {
log.Error("status server set tls config failed", zap.Error(err))
return errors.Trace(err)
// if CertAllowedCN was specified, we should add server's common name
// otherwise, https requests sent to non-owner capture can't be forward
// to owner
if len(conf.Security.CertAllowedCN) != 0 {
err := conf.Security.AddSelfCommonName()
if err != nil {
log.Error("status server set tls config failed", zap.Error(err))
return errors.Trace(err)
}
}

tlsConfig, err := conf.Security.ToTLSConfigWithVerify()
if err != nil {
log.Error("status server get tls config failed", zap.Error(err))
Expand Down
146 changes: 146 additions & 0 deletions cdc/http_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,27 @@ package cdc

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"

"github.com/pingcap/check"
"github.com/pingcap/failpoint"
"github.com/pingcap/ticdc/cdc/capture"
"github.com/pingcap/ticdc/cdc/model"
"github.com/pingcap/ticdc/pkg/config"
cerrors "github.com/pingcap/ticdc/pkg/errors"
"github.com/pingcap/ticdc/pkg/retry"
security2 "github.com/pingcap/ticdc/pkg/security"
"github.com/pingcap/ticdc/pkg/util/testleak"
"github.com/pingcap/tidb/br/pkg/httputil"
"github.com/tikv/pd/pkg/tempurl"
"go.etcd.io/etcd/clientv3/concurrency"
)

Expand Down Expand Up @@ -159,3 +170,138 @@ func testHandleFailpoint(c *check.C) {
})
c.Assert(failpointHit, check.IsFalse)
}

func (s *httpStatusSuite) TestServerTLSWithoutCommonName(c *check.C) {
defer testleak.AfterTest(c)
addr := tempurl.Alloc()[len("http://"):]
// Do not specify common name
security, err := security2.NewCredential4Test("")
c.Assert(err, check.IsNil)
conf := config.GetDefaultServerConfig()
conf.Addr = addr
conf.AdvertiseAddr = addr
conf.Security = &security
config.StoreGlobalServerConfig(conf)

server, err := NewServer([]string{"https://127.0.0.1:2379"})
server.capture = capture.NewCapture4Test()
c.Assert(err, check.IsNil)
err = server.startStatusHTTP()
c.Assert(err, check.IsNil)
defer func() {
c.Assert(server.statusServer.Close(), check.IsNil)
}()

statusURL := fmt.Sprintf("https://%s/api/v1/status", addr)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// test cli sends request without a cert will success
err = retry.Do(ctx, func() error {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
cli := &http.Client{Transport: tr}
resp, err := cli.Get(statusURL)
if err != nil {
return err
}
decoder := json.NewDecoder(resp.Body)
captureInfo := &model.CaptureInfo{}
err = decoder.Decode(captureInfo)
c.Assert(err, check.IsNil)
c.Assert(captureInfo.ID, check.Equals, server.capture.Info().ID)
resp.Body.Close()
return nil
}, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50), retry.WithIsRetryableErr(cerrors.IsRetryableError))
c.Assert(err, check.IsNil)

// test cli sends request with a cert will success
err = retry.Do(ctx, func() error {
tlsConfig, err := security.ToTLSConfigWithVerify()
if err != nil {
c.Assert(err, check.IsNil)
}
cli := httputil.NewClient(tlsConfig)
resp, err := cli.Get(statusURL)
if err != nil {
return err
}
decoder := json.NewDecoder(resp.Body)
captureInfo := &model.CaptureInfo{}
err = decoder.Decode(captureInfo)
c.Assert(err, check.IsNil)
c.Assert(captureInfo.ID, check.Equals, server.capture.Info().ID)
resp.Body.Close()
return nil
}, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50), retry.WithIsRetryableErr(cerrors.IsRetryableError))
c.Assert(err, check.IsNil)
}

//
func (s *httpStatusSuite) TestServerTLSWithCommonName(c *check.C) {
defer testleak.AfterTest(c)
addr := tempurl.Alloc()[len("http://"):]
// specify a common name
security, err := security2.NewCredential4Test("test")
c.Assert(err, check.IsNil)
conf := config.GetDefaultServerConfig()
conf.Addr = addr
conf.AdvertiseAddr = addr
conf.Security = &security
config.StoreGlobalServerConfig(conf)

server, err := NewServer([]string{"https://127.0.0.1:2379"})
server.capture = capture.NewCapture4Test()
c.Assert(err, check.IsNil)
err = server.startStatusHTTP()
c.Assert(err, check.IsNil)
defer func() {
c.Assert(server.statusServer.Close(), check.IsNil)
}()

statusURL := fmt.Sprintf("https://%s/api/v1/status", addr)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// test cli sends request without a cert will fail
err = retry.Do(ctx, func() error {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
cli := &http.Client{Transport: tr}
resp, err := cli.Get(statusURL)
if err != nil {
return err
}
decoder := json.NewDecoder(resp.Body)
captureInfo := &model.CaptureInfo{}
err = decoder.Decode(captureInfo)
c.Assert(err, check.IsNil)
c.Assert(captureInfo.ID, check.Equals, server.capture.Info().ID)
resp.Body.Close()
return nil
}, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50), retry.WithIsRetryableErr(cerrors.IsRetryableError))
c.Assert(strings.Contains(err.Error(), "remote error: tls: bad certificate"), check.IsTrue)

// test cli sends request with a cert will success
err = retry.Do(ctx, func() error {
tlsConfig, err := security.ToTLSConfigWithVerify()
if err != nil {
c.Assert(err, check.IsNil)
}
cli := httputil.NewClient(tlsConfig)
resp, err := cli.Get(statusURL)
if err != nil {
return err
}
decoder := json.NewDecoder(resp.Body)
captureInfo := &model.CaptureInfo{}
err = decoder.Decode(captureInfo)
c.Assert(err, check.IsNil)
c.Assert(captureInfo.ID, check.Equals, server.capture.Info().ID)
resp.Body.Close()
return nil
}, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50), retry.WithIsRetryableErr(cerrors.IsRetryableError))
c.Assert(err, check.IsNil)
}
132 changes: 132 additions & 0 deletions pkg/security/test_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright 2021 PingCAP, Inc.
//
// 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,
// See the License for the specific language governing permissions and
// limitations under the License.

package security

import "os"

// All these certificates or keys are use for testing only.
var certPem = `-----BEGIN CERTIFICATE-----
MIIDjzCCAnegAwIBAgIUWBTDQm4xOYDxZBTkpCQouREtT8QwDQYJKoZIhvcNAQEL
BQAwVzELMAkGA1UEBhMCQ04xEDAOBgNVBAgTB0JlaWppbmcxEDAOBgNVBAcTB0Jl
aWppbmcxEDAOBgNVBAoTB1BpbmdDQVAxEjAQBgNVBAMTCU15IG93biBDQTAgFw0y
MDAyMTgwOTExMDBaGA8yMTIwMDEyNTA5MTEwMFowFjEUMBIGA1UEAxMLdGlkYi1z
ZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCr2ZxAb+dItEQz
avuza0IoIT/UolC9XTGaQiCUUPZUMN9hb4KYEwTks1ZthHovTIJUdTwHtpWfDUWx
uIXhOlRjfD+viY4aXtBsaK8xi9F7o2HbFQ5O9y3AXK/YW+u0FfWtnn/xAtvPUgUc
61NXtBTMvNard9ICIXW+FWxLcFSaHpC9ZTyr13KWmZRDbai1JFeaKvATMW30r7Dd
Ur1npppzt7ZdG6tU/FuqBBSrZtuVSGKLwVx0JQDw16eVan4friY3ZPNVoZvkKyvt
I1LsBYMQ8p+ijtbcftvMqWZFVw95F1+3C2JIjWN9ujGmvJr+dtPIE8T/J8tT9Jif
9vz16nOLAgMBAAGjgZEwgY4wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsG
AQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRVB/Bvdzvh
6WQRWpc9SzcbXLz77zAfBgNVHSMEGDAWgBSdAhKsS8BKSOidoGCUYNeaFma4/zAP
BgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQAAqg5pgGQqORKRSdlY
wzVvzKaulpvjZfVMM6YiOUtmlU0CGWq7E3gLFzkvebpU0KsFlbyZ92h/2Fw5Ay2b
kxkCy18mJ4lGkvF0cU4UD3XheFMvD2QWWRX4WPpAhStofrWOXeyq3Div2+fQjMJd
kyeWUzPU7T467IWUHOWNsFAjfVHNsmG45qLGt+XQckHTvASX5IvN+5tkRUCW30vO
b3BdDQUFglGTUFU2epaZGTti0SYiRiY+9R3zFWX4uBcEBYhk9e/0BU8FqdWW5GjI
pFpH9t64CjKIdRQXpIn4cogK/GwyuRuDPV/RkMjrIqOi7pGejXwyDe9avHFVR6re
oowA
-----END CERTIFICATE-----`

var caPem = `-----BEGIN CERTIFICATE-----
MIIDgDCCAmigAwIBAgIUHWvlRJydvYTR0ot3b8f6IlSHcGUwDQYJKoZIhvcNAQEL
BQAwVzELMAkGA1UEBhMCQ04xEDAOBgNVBAgTB0JlaWppbmcxEDAOBgNVBAcTB0Jl
aWppbmcxEDAOBgNVBAoTB1BpbmdDQVAxEjAQBgNVBAMTCU15IG93biBDQTAgFw0y
MDAyMTgwNzQxMDBaGA8yMTIwMDEyNTA3NDEwMFowVzELMAkGA1UEBhMCQ04xEDAO
BgNVBAgTB0JlaWppbmcxEDAOBgNVBAcTB0JlaWppbmcxEDAOBgNVBAoTB1BpbmdD
QVAxEjAQBgNVBAMTCU15IG93biBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAOAdNtjanFhPaKJHQjr7+h/Cpps5bLc6S1vmgi/EIi9PKv3eyDgtlW1r
As2sjXRMHjcuZp2hHJ9r9FrMQD1rQQq5vJzQqM+eyWLc2tyZWXNWkZVvpjU4Hy5k
jZFLXoyHgAvps/LGu81F5Lk5CvLHswWTyGQUCFi1l/cYcQg6AExh2pO/WJu4hQhe
1mBBIKsJhZ5b5tWruLeI+YIjD1oo1ADMHYLK1BHON2fUmUHRGbrYKu4yCuyip3wn
rbVlpabn7l1JBMatCUJLHR6VWQ2MNjrOXAEUYm4xGEN+tUYyUOGl5mHFguLl3OIn
wj+1dT3WOr/NraPYlwVOnAd9GNbPJj0CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEG
MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ0CEqxLwEpI6J2gYJRg15oWZrj/
MA0GCSqGSIb3DQEBCwUAA4IBAQCf8xRf7q1xAaGrc9HCPvN4OFkxDwz1CifrvrLR
ZgIWGUdCHDW2D1IiWKZQWeJKC1otA5x0hrS5kEGfkLFhADEU4txwp70DQaBArPti
pSgheIEbaT0H3BUTYSgS3VL2HjxN5OVMN6jNG3rWyxnJpNOCsJhhJXPK50CRZ7fk
Dcodj6FfEM2bfp2bGkxyVtUch7eepfUVbslXa7jE7Y8M3cr9NoLUcSP6D1RJWkNd
dBQoUsb6Ckq27ozEKOgwuBVv4BrrbFN//+7WHP8Vy6sSMyd+dJLBi6wehJjQhIz6
vqLWE81rSJuxZqjLpCkFdeEF+9SRjWegU0ZDM4V+YeX53BPC
-----END CERTIFICATE-----
`

var keyPem = `-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAq9mcQG/nSLREM2r7s2tCKCE/1KJQvV0xmkIglFD2VDDfYW+C
mBME5LNWbYR6L0yCVHU8B7aVnw1FsbiF4TpUY3w/r4mOGl7QbGivMYvRe6Nh2xUO
TvctwFyv2FvrtBX1rZ5/8QLbz1IFHOtTV7QUzLzWq3fSAiF1vhVsS3BUmh6QvWU8
q9dylpmUQ22otSRXmirwEzFt9K+w3VK9Z6aac7e2XRurVPxbqgQUq2bblUhii8Fc
dCUA8NenlWp+H64mN2TzVaGb5Csr7SNS7AWDEPKfoo7W3H7bzKlmRVcPeRdftwti
SI1jfboxprya/nbTyBPE/yfLU/SYn/b89epziwIDAQABAoIBACPlI08OULgN90Tq
LsLuP3ZUY5nNgaHcKnU3JMj2FE3Hm5ElkpijOF1w3Dep+T+R8pMjnbNavuvnAMy7
ZzOBVIknNcI7sDPv5AcQ4q8trkbt/I2fW0rBNIw+j/hYUuZdw+BNABpeZ31pe2nr
+Y+TLNkLBKfyMiqBxK88mE81mmZKblyvXCawW0A/iDDJ7fPNqoGF+y9ylTYaNRPk
aJGnaEZobJ4Lm5tSqW4gRX2ft6Hm67RkvVaopPFnlkvfusXUTFUqEVQCURRUqXbf
1ah2chUHxj22UdY9540H5yVNgEP3oR+uS/hbZqxKcJUTznUW5th3CyQPIKMlGlcB
p+zWlTECgYEAxlY4zGJw4QQwGYMKLyWCSHUgKYrKu2Ub2JKJFMTdsoj9H7DI+WHf
lQaO9NCOo2lt0ofYM1MzEpI5Cl/aMrPw+mwquBbxWdMHXK2eSsUQOVo9HtUjgK2t
J2AYFCfsYndo+hCj3ApMHgiY3sghTCXeycvT52bm11VeNVcs3pKxIYMCgYEA3dAJ
PwIfAB8t+6JCP2yYH4ExNjoMNYMdXqhz4vt3UGwgskRqTW6qdd9JvrRQ/JPvGpDy
T375h/+lLw0E4ljsnOPGSzbXNf4bYRHTwPOL+LqVM4Bg90hjclqphElHChxep1di
WcdArB0oae/l4M96z3GjfnXIUVOp8K6BUQCab1kCgYAFFAQUR5j4SfEpVg+WsXEq
hcUzCxixv5785pOX8opynctNWmtq5zSgTjCu2AAu8u4a69t/ROwT16aaO2YM0kqj
Ps3BNOUtFZgkqVVaOL13mnXiKjbkfo3majFzoqoMw13uuSpY4fKc+j9fxOQFXRrd
M9jTHfFfJhJpbzf44uyiHQKBgFIPwzvyVvG+l05/Ky83x9fv/frn4thxV45LmAQj
sHKqbjZFpWZcSOgu4aOSJlwrhsw3T84lVcAAzmXn1STAbVll01jEQz6QciSpacP6
1pAAx240UqtptpD6BbkROxz8ffA/Hf3E/6Itb2QyAsP3PqI8kpYYkTG1WCvZA7Kq
HHiRAoGAXbUZ25LcrmyuxKWpbty8fck1tjKPvclQB35rOx6vgnfW6pcKMeebYvgq
nJka/QunEReOH/kGxAd/+ymvUBuFQCfFg3Aus+DtAuh9AkBr+cIyPjJqynnIT87J
MbkOw4uEhDJAtGUR9o1j83N1f05bnEwssXiXR0LZPylb9Qzc4tg=
-----END RSA PRIVATE KEY-----
`

// NewCredential4Test return a Credential for testing
func NewCredential4Test(cn string) (Credential, error) {
res := Credential{}
if cn != "" {
res.CertAllowedCN = append(res.CertAllowedCN, cn)
}
cert, err := os.CreateTemp("", "ticdc-test-cert")
if err != nil {
return res, err
}
_, err = cert.Write([]byte(certPem))
if err != nil {
return res, err
}

ca, err := os.CreateTemp("", "ticdc-test-ca")
if err != nil {
return res, err
}
_, err = ca.Write([]byte(caPem))
if err != nil {
return res, err
}

key, err := os.CreateTemp("", "ticdc-test-key")
if err != nil {
return res, err
}
_, err = key.Write([]byte(keyPem))
if err != nil {
return res, err
}

res.CertPath = cert.Name()
res.CAPath = ca.Name()
res.KeyPath = key.Name()

return res, nil
}