github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/cdc/server/server_test.go (about)

     1  // Copyright 2020 PingCAP, Inc.
     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  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package server
    15  
    16  import (
    17  	"context"
    18  	"crypto/tls"
    19  	"encoding/json"
    20  	"fmt"
    21  	"net/http"
    22  	"net/url"
    23  	"os"
    24  	"os/user"
    25  	"path/filepath"
    26  	"runtime"
    27  	"strings"
    28  	"sync"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/golang/mock/gomock"
    33  	"github.com/pingcap/tiflow/cdc/capture"
    34  	"github.com/pingcap/tiflow/cdc/model"
    35  	"github.com/pingcap/tiflow/pkg/config"
    36  	cerrors "github.com/pingcap/tiflow/pkg/errors"
    37  	"github.com/pingcap/tiflow/pkg/etcd"
    38  	mock_etcd "github.com/pingcap/tiflow/pkg/etcd/mock"
    39  	"github.com/pingcap/tiflow/pkg/httputil"
    40  	"github.com/pingcap/tiflow/pkg/retry"
    41  	"github.com/pingcap/tiflow/pkg/security"
    42  	"github.com/pingcap/tiflow/pkg/util"
    43  	"github.com/stretchr/testify/require"
    44  	"github.com/tikv/pd/pkg/utils/tempurl"
    45  	clientv3 "go.etcd.io/etcd/client/v3"
    46  	"go.etcd.io/etcd/server/v3/embed"
    47  	"golang.org/x/sync/errgroup"
    48  )
    49  
    50  type testServer struct {
    51  	server    *server
    52  	e         *embed.Etcd
    53  	clientURL *url.URL
    54  	ctx       context.Context
    55  	cancel    context.CancelFunc
    56  	errg      *errgroup.Group
    57  }
    58  
    59  func newServer(t *testing.T) *testServer {
    60  	var err error
    61  	dir := t.TempDir()
    62  	s := &testServer{}
    63  	s.clientURL, s.e, err = etcd.SetupEmbedEtcd(dir)
    64  	require.Nil(t, err)
    65  
    66  	pdEndpoints := []string{
    67  		"http://" + s.clientURL.Host,
    68  		"http://invalid-pd-host:2379",
    69  	}
    70  	server, err := New(pdEndpoints)
    71  	require.Nil(t, err)
    72  	require.NotNil(t, server)
    73  	s.server = server
    74  
    75  	s.ctx, s.cancel = context.WithCancel(context.Background())
    76  	client, err := clientv3.New(clientv3.Config{
    77  		Endpoints:   s.server.pdEndpoints,
    78  		Context:     s.ctx,
    79  		DialTimeout: 5 * time.Second,
    80  	})
    81  	require.Nil(t, err)
    82  
    83  	etcdClient, err := etcd.NewCDCEtcdClient(s.ctx, client, etcd.DefaultCDCClusterID)
    84  	require.Nil(t, err)
    85  	s.server.etcdClient = etcdClient
    86  
    87  	s.errg = util.HandleErrWithErrGroup(s.ctx, s.e.Err(), func(e error) { t.Log(e) })
    88  	return s
    89  }
    90  
    91  func (s *testServer) close(t *testing.T) {
    92  	s.server.Close()
    93  	s.e.Close()
    94  	s.cancel()
    95  	err := s.errg.Wait()
    96  	if err != nil {
    97  		t.Errorf("Error group error: %s", err)
    98  	}
    99  }
   100  
   101  func TestServerBasic(t *testing.T) {
   102  	t.Parallel()
   103  	s := newServer(t)
   104  	defer s.close(t)
   105  	testSetUpDataDir(t, s)
   106  }
   107  
   108  func testSetUpDataDir(t *testing.T, s *testServer) {
   109  	conf := config.GetGlobalServerConfig()
   110  	// DataDir is not set, and no changefeed exist, use the default
   111  	conf.DataDir = ""
   112  	err := s.server.setUpDir(s.ctx)
   113  	require.Nil(t, err)
   114  	require.Equal(t, defaultDataDir, conf.DataDir)
   115  	require.Equal(t, filepath.Join(defaultDataDir, config.DefaultSortDir), conf.Sorter.SortDir)
   116  
   117  	// DataDir is not set, but has existed changefeed, use the one with the largest available space
   118  	conf.DataDir = ""
   119  	dir := t.TempDir()
   120  	err = s.server.etcdClient.SaveChangeFeedInfo(s.ctx,
   121  		&model.ChangeFeedInfo{SortDir: dir}, model.DefaultChangeFeedID("a"))
   122  	require.Nil(t, err)
   123  
   124  	err = s.server.etcdClient.SaveChangeFeedInfo(s.ctx,
   125  		&model.ChangeFeedInfo{}, model.DefaultChangeFeedID("b"))
   126  	require.Nil(t, err)
   127  
   128  	err = s.server.setUpDir(s.ctx)
   129  	require.Nil(t, err)
   130  
   131  	require.Equal(t, dir, conf.DataDir)
   132  	require.Equal(t, filepath.Join(dir, config.DefaultSortDir), conf.Sorter.SortDir)
   133  
   134  	conf.DataDir = t.TempDir()
   135  	// DataDir has been set, just use it
   136  	err = s.server.setUpDir(s.ctx)
   137  	require.Nil(t, err)
   138  	require.NotEqual(t, "", conf.DataDir)
   139  	require.Equal(t, filepath.Join(conf.DataDir, config.DefaultSortDir), conf.Sorter.SortDir)
   140  }
   141  
   142  func TestCheckDir(t *testing.T) {
   143  	me, err := user.Current()
   144  	require.Nil(t, err)
   145  	if me.Name == "root" || runtime.GOOS == "windows" {
   146  		t.Skip("test case is running as a superuser or in windows")
   147  	}
   148  
   149  	dir := t.TempDir()
   150  	_, err = checkDir(dir)
   151  	require.Nil(t, err)
   152  
   153  	readOnly := filepath.Join(dir, "readOnly")
   154  	err = os.MkdirAll(readOnly, 0o400)
   155  	require.Nil(t, err)
   156  	_, err = checkDir(readOnly)
   157  	require.Error(t, err)
   158  
   159  	writeOnly := filepath.Join(dir, "writeOnly")
   160  	err = os.MkdirAll(writeOnly, 0o200)
   161  	require.Nil(t, err)
   162  	_, err = checkDir(writeOnly)
   163  	require.Error(t, err)
   164  
   165  	file := filepath.Join(dir, "file")
   166  	f, err := os.Create(file)
   167  	require.Nil(t, err)
   168  	require.Nil(t, f.Close())
   169  	_, err = checkDir(file)
   170  	require.Error(t, err)
   171  }
   172  
   173  const retryTime = 20
   174  
   175  func TestServerTLSWithoutCommonName(t *testing.T) {
   176  	addr := tempurl.Alloc()[len("http://"):]
   177  	// Do not specify common name
   178  	_, securityCfg, err := security.NewServerCredential4Test("")
   179  	require.Nil(t, err)
   180  	conf := config.GetDefaultServerConfig()
   181  	conf.Addr = addr
   182  	conf.AdvertiseAddr = addr
   183  	conf.Security = securityCfg
   184  	config.StoreGlobalServerConfig(conf)
   185  
   186  	server, err := New([]string{"https://127.0.0.1:2379"})
   187  	cp := capture.NewCapture4Test(nil)
   188  	ctrl := gomock.NewController(t)
   189  	etcdClient := mock_etcd.NewMockCDCEtcdClient(ctrl)
   190  	etcdClient.EXPECT().GetClusterID().Return("abcd").AnyTimes()
   191  	cp.EtcdClient = etcdClient
   192  	server.capture = cp
   193  	require.Nil(t, err)
   194  	err = server.startStatusHTTP(server.tcpServer.HTTP1Listener())
   195  	require.Nil(t, err)
   196  	defer func() {
   197  		require.Nil(t, server.statusServer.Close())
   198  	}()
   199  
   200  	statusURL := fmt.Sprintf("https://%s/api/v1/status", addr)
   201  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   202  	defer cancel()
   203  
   204  	var wg sync.WaitGroup
   205  	wg.Add(1)
   206  	go func() {
   207  		defer wg.Done()
   208  		err := server.tcpServer.Run(ctx)
   209  		require.ErrorContains(t, err, "ErrTCPServerClosed")
   210  	}()
   211  
   212  	// test cli sends request without a cert will success
   213  	err = retry.Do(ctx, func() error {
   214  		tr := &http.Transport{
   215  			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
   216  		}
   217  		cli := httputil.NewTestClient(tr)
   218  		resp, err := cli.Get(ctx, statusURL)
   219  		if err != nil {
   220  			return err
   221  		}
   222  		defer resp.Body.Close()
   223  		decoder := json.NewDecoder(resp.Body)
   224  		captureInfo := &model.CaptureInfo{}
   225  		err = decoder.Decode(captureInfo)
   226  		require.Nil(t, err)
   227  		info, err := server.capture.Info()
   228  		require.Nil(t, err)
   229  		require.Equal(t, info.ID, captureInfo.ID)
   230  		return nil
   231  	}, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50),
   232  		retry.WithIsRetryableErr(cerrors.IsRetryableError))
   233  	require.Nil(t, err)
   234  
   235  	// test cli sends request with a cert will success
   236  	err = retry.Do(ctx, func() error {
   237  		cli, err := httputil.NewClient(securityCfg)
   238  		require.Nil(t, err)
   239  		resp, err := cli.Get(ctx, statusURL)
   240  		if err != nil {
   241  			return err
   242  		}
   243  		decoder := json.NewDecoder(resp.Body)
   244  		captureInfo := &model.CaptureInfo{}
   245  		err = decoder.Decode(captureInfo)
   246  		require.Nil(t, err)
   247  		info, err := server.capture.Info()
   248  		require.Nil(t, err)
   249  		require.Equal(t, info.ID, captureInfo.ID)
   250  		resp.Body.Close()
   251  		return nil
   252  	}, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50),
   253  		retry.WithIsRetryableErr(cerrors.IsRetryableError))
   254  	require.Nil(t, err)
   255  
   256  	cancel()
   257  	wg.Wait()
   258  }
   259  
   260  func TestServerTLSWithCommonNameAndRotate(t *testing.T) {
   261  	addr := tempurl.Alloc()[len("http://"):]
   262  	// specify a common name
   263  	ca, securityCfg, err := security.NewServerCredential4Test("server")
   264  	securityCfg.CertAllowedCN = append(securityCfg.CertAllowedCN, "client1")
   265  	require.Nil(t, err)
   266  
   267  	conf := config.GetDefaultServerConfig()
   268  	conf.Addr = addr
   269  	conf.AdvertiseAddr = addr
   270  	conf.Security = securityCfg
   271  	config.StoreGlobalServerConfig(conf)
   272  
   273  	server, err := New([]string{"https://127.0.0.1:2379"})
   274  	cp := capture.NewCapture4Test(nil)
   275  	ctrl := gomock.NewController(t)
   276  	etcdClient := mock_etcd.NewMockCDCEtcdClient(ctrl)
   277  	etcdClient.EXPECT().GetClusterID().Return("abcd").AnyTimes()
   278  	cp.EtcdClient = etcdClient
   279  	server.capture = cp
   280  	require.Nil(t, err)
   281  	err = server.startStatusHTTP(server.tcpServer.HTTP1Listener())
   282  	require.Nil(t, err)
   283  	defer func() {
   284  		require.Nil(t, server.statusServer.Close())
   285  	}()
   286  
   287  	statusURL := fmt.Sprintf("https://%s/api/v1/status", addr)
   288  	ctx, cancel := context.WithTimeout(context.Background(), 40*time.Second)
   289  	defer cancel()
   290  
   291  	var wg sync.WaitGroup
   292  	wg.Add(1)
   293  	go func() {
   294  		defer wg.Done()
   295  		err := server.tcpServer.Run(ctx)
   296  		require.ErrorContains(t, err, "ErrTCPServerClosed")
   297  	}()
   298  
   299  	// test cli sends request without a cert will fail
   300  	err = retry.Do(ctx, func() error {
   301  		tr := &http.Transport{
   302  			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
   303  		}
   304  		cli := httputil.NewTestClient(tr)
   305  		require.Nil(t, err)
   306  		resp, err := cli.Get(ctx, statusURL)
   307  		if err != nil {
   308  			return err
   309  		}
   310  		decoder := json.NewDecoder(resp.Body)
   311  		captureInfo := &model.CaptureInfo{}
   312  		err = decoder.Decode(captureInfo)
   313  		require.Nil(t, err)
   314  		info, err := server.capture.Info()
   315  		require.Nil(t, err)
   316  		require.Equal(t, info.ID, captureInfo.ID)
   317  		resp.Body.Close()
   318  		return nil
   319  	}, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50),
   320  		retry.WithIsRetryableErr(cerrors.IsRetryableError))
   321  	require.True(t,
   322  		strings.Contains(err.Error(), "remote error: tls: bad certificate") ||
   323  			strings.Contains(err.Error(), "remote error: tls: certificate required"),
   324  		"bad err: %s", err.Error(),
   325  	)
   326  
   327  	testTlSClient := func(securityCfg *security.Credential) error {
   328  		return retry.Do(ctx, func() error {
   329  			cli, err := httputil.NewClient(securityCfg)
   330  			require.Nil(t, err)
   331  			resp, err := cli.Get(ctx, statusURL)
   332  			if err != nil {
   333  				return err
   334  			}
   335  			decoder := json.NewDecoder(resp.Body)
   336  			captureInfo := &model.CaptureInfo{}
   337  			err = decoder.Decode(captureInfo)
   338  			require.Nil(t, err)
   339  			info, err := server.capture.Info()
   340  			require.Nil(t, err)
   341  			require.Equal(t, info.ID, captureInfo.ID)
   342  			resp.Body.Close()
   343  			return nil
   344  		}, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50),
   345  			retry.WithIsRetryableErr(cerrors.IsRetryableError))
   346  	}
   347  
   348  	// test cli sends request with a cert will success
   349  
   350  	// test peer success
   351  	require.NoError(t, testTlSClient(securityCfg))
   352  
   353  	// test rotate
   354  	serverCert, serverkey, err := ca.GenerateCerts("rotate")
   355  	require.NoError(t, err)
   356  	err = os.WriteFile(securityCfg.CertPath, serverCert, 0o600)
   357  	require.NoError(t, err)
   358  	err = os.WriteFile(securityCfg.KeyPath, serverkey, 0o600)
   359  	require.NoError(t, err)
   360  	// peer fail due to invalid common name `rotate`
   361  	require.ErrorContains(t, testTlSClient(securityCfg), "client certificate authentication failed")
   362  
   363  	cert, key, err := ca.GenerateCerts("client1")
   364  	require.NoError(t, err)
   365  	certPath, err := security.WriteFile("ticdc-test-client-cert", cert)
   366  	require.NoError(t, err)
   367  	keyPath, err := security.WriteFile("ticdc-test-client-key", key)
   368  	require.NoError(t, err)
   369  	require.NoError(t, testTlSClient(&security.Credential{
   370  		CAPath:        securityCfg.CAPath,
   371  		CertPath:      certPath,
   372  		KeyPath:       keyPath,
   373  		CertAllowedCN: []string{"rotate"},
   374  	}))
   375  
   376  	cancel()
   377  	wg.Wait()
   378  }