...

Source file src/github.com/google/pprof/internal/driver/fetch_test.go

Documentation: github.com/google/pprof/internal/driver

     1  // Copyright 2014 Google Inc. All Rights Reserved.
     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  
    15  package driver
    16  
    17  import (
    18  	"crypto/ecdsa"
    19  	"crypto/elliptic"
    20  	"crypto/rand"
    21  	"crypto/tls"
    22  	"crypto/x509"
    23  	"encoding/pem"
    24  	"fmt"
    25  	"math/big"
    26  	"net"
    27  	"net/http"
    28  	"os"
    29  	"path/filepath"
    30  	"reflect"
    31  	"regexp"
    32  	"runtime"
    33  	"strings"
    34  	"testing"
    35  	"time"
    36  
    37  	"github.com/google/pprof/internal/binutils"
    38  	"github.com/google/pprof/internal/plugin"
    39  	"github.com/google/pprof/internal/proftest"
    40  	"github.com/google/pprof/internal/symbolizer"
    41  	"github.com/google/pprof/internal/transport"
    42  	"github.com/google/pprof/profile"
    43  )
    44  
    45  func TestSymbolizationPath(t *testing.T) {
    46  	if runtime.GOOS == "windows" {
    47  		t.Skip("test assumes Unix paths")
    48  	}
    49  
    50  	// Save environment variables to restore after test
    51  	saveHome := os.Getenv(homeEnv())
    52  	savePath := os.Getenv("PPROF_BINARY_PATH")
    53  
    54  	tempdir, err := os.MkdirTemp("", "home")
    55  	if err != nil {
    56  		t.Fatal("creating temp dir: ", err)
    57  	}
    58  	defer os.RemoveAll(tempdir)
    59  	os.MkdirAll(filepath.Join(tempdir, "pprof", "binaries", "abcde10001"), 0700)
    60  	os.Create(filepath.Join(tempdir, "pprof", "binaries", "abcde10001", "binary"))
    61  
    62  	os.MkdirAll(filepath.Join(tempdir, "pprof", "binaries", "fg"), 0700)
    63  	os.Create(filepath.Join(tempdir, "pprof", "binaries", "fg", "hij10001.debug"))
    64  
    65  	obj := testObj{tempdir}
    66  	os.Setenv(homeEnv(), tempdir)
    67  	for _, tc := range []struct {
    68  		env, file, buildID, want string
    69  		msgCount                 int
    70  	}{
    71  		{"", "/usr/bin/binary", "", "/usr/bin/binary", 0},
    72  		{"", "/usr/bin/binary", "fedcb10000", "/usr/bin/binary", 0},
    73  		{"/usr", "/bin/binary", "", "/usr/bin/binary", 0},
    74  		{"", "/prod/path/binary", "abcde10001", filepath.Join(tempdir, "pprof/binaries/abcde10001/binary"), 0},
    75  		{"/alternate/architecture", "/usr/bin/binary", "", "/alternate/architecture/binary", 0},
    76  		{"/alternate/architecture", "/usr/bin/binary", "abcde10001", "/alternate/architecture/binary", 0},
    77  		{"", "", "fghij10001", filepath.Join(tempdir, "pprof/binaries/fg/hij10001.debug"), 0},
    78  		{"/nowhere:/alternate/architecture", "/usr/bin/binary", "fedcb10000", "/usr/bin/binary", 1},
    79  		{"/nowhere:/alternate/architecture", "/usr/bin/binary", "abcde10002", "/usr/bin/binary", 1},
    80  	} {
    81  		os.Setenv("PPROF_BINARY_PATH", tc.env)
    82  		p := &profile.Profile{
    83  			Mapping: []*profile.Mapping{
    84  				{
    85  					File:    tc.file,
    86  					BuildID: tc.buildID,
    87  				},
    88  			},
    89  		}
    90  		s := &source{}
    91  		locateBinaries(p, s, obj, &proftest.TestUI{T: t, Ignore: tc.msgCount})
    92  		if file := p.Mapping[0].File; file != tc.want {
    93  			t.Errorf("%s:%s:%s, want %s, got %s", tc.env, tc.file, tc.buildID, tc.want, file)
    94  		}
    95  	}
    96  	os.Setenv(homeEnv(), saveHome)
    97  	os.Setenv("PPROF_BINARY_PATH", savePath)
    98  }
    99  
   100  func TestCollectMappingSources(t *testing.T) {
   101  	const startAddress uint64 = 0x40000
   102  	const url = "http://example.com"
   103  	for _, tc := range []struct {
   104  		file, buildID string
   105  		want          plugin.MappingSources
   106  	}{
   107  		{"/usr/bin/binary", "buildId", mappingSources("buildId", url, startAddress)},
   108  		{"/usr/bin/binary", "", mappingSources("/usr/bin/binary", url, startAddress)},
   109  		{"", "", mappingSources(url, url, startAddress)},
   110  	} {
   111  		p := &profile.Profile{
   112  			Mapping: []*profile.Mapping{
   113  				{
   114  					File:    tc.file,
   115  					BuildID: tc.buildID,
   116  					Start:   startAddress,
   117  				},
   118  			},
   119  		}
   120  		got := collectMappingSources(p, url)
   121  		if !reflect.DeepEqual(got, tc.want) {
   122  			t.Errorf("%s:%s, want %v, got %v", tc.file, tc.buildID, tc.want, got)
   123  		}
   124  	}
   125  }
   126  
   127  func TestUnsourceMappings(t *testing.T) {
   128  	for _, tc := range []struct {
   129  		os, file, buildID, want string
   130  	}{
   131  		{"any", "/usr/bin/binary", "buildId", "/usr/bin/binary"},
   132  		{"any", "http://example.com", "", ""},
   133  		{"windows", `C:\example.exe`, "", `C:\example.exe`},
   134  		{"windows", `c:/example.exe`, "", `c:/example.exe`},
   135  	} {
   136  		t.Run(tc.file+"-"+tc.os, func(t *testing.T) {
   137  			if tc.os != "any" && tc.os != runtime.GOOS {
   138  				t.Skipf("%s only test", tc.os)
   139  			}
   140  
   141  			p := &profile.Profile{
   142  				Mapping: []*profile.Mapping{
   143  					{
   144  						File:    tc.file,
   145  						BuildID: tc.buildID,
   146  					},
   147  				},
   148  			}
   149  			unsourceMappings(p)
   150  			if got := p.Mapping[0].File; got != tc.want {
   151  				t.Errorf("%s:%s, want %s, got %s", tc.file, tc.buildID, tc.want, got)
   152  			}
   153  		})
   154  	}
   155  }
   156  
   157  type testObj struct {
   158  	home string
   159  }
   160  
   161  func (o testObj) Open(file string, start, limit, offset uint64, relocationSymbol string) (plugin.ObjFile, error) {
   162  	switch file {
   163  	case "/alternate/architecture/binary":
   164  		return testFile{file, "abcde10001"}, nil
   165  	case "/usr/bin/binary":
   166  		return testFile{file, "fedcb10000"}, nil
   167  	case filepath.Join(o.home, "pprof/binaries/abcde10001/binary"):
   168  		return testFile{file, "abcde10001"}, nil
   169  	case filepath.Join(o.home, "pprof/binaries/fg/hij10001.debug"):
   170  		return testFile{file, "fghij10001"}, nil
   171  	}
   172  	return nil, fmt.Errorf("not found: %s", file)
   173  }
   174  func (testObj) Demangler(_ string) func(names []string) (map[string]string, error) {
   175  	return func(names []string) (map[string]string, error) { return nil, nil }
   176  }
   177  func (testObj) Disasm(file string, start, end uint64, intelSyntax bool) ([]plugin.Inst, error) {
   178  	return nil, nil
   179  }
   180  
   181  type testFile struct{ name, buildID string }
   182  
   183  func (f testFile) Name() string                                               { return f.name }
   184  func (testFile) ObjAddr(addr uint64) (uint64, error)                          { return addr, nil }
   185  func (f testFile) BuildID() string                                            { return f.buildID }
   186  func (testFile) SourceLine(addr uint64) ([]plugin.Frame, error)               { return nil, nil }
   187  func (testFile) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) { return nil, nil }
   188  func (testFile) Close() error                                                 { return nil }
   189  
   190  func TestFetch(t *testing.T) {
   191  	const path = "testdata/"
   192  	type testcase struct {
   193  		source, execName string
   194  	}
   195  	ts := []testcase{
   196  		{path + "go.crc32.cpu", ""},
   197  		{path + "go.nomappings.crash", "/bin/gotest.exe"},
   198  		{"http://localhost/profile?file=cppbench.cpu", ""},
   199  	}
   200  	// Test that paths with a colon character are recognized as file paths
   201  	// if the file exists, rather than as a URL. We have to skip this test
   202  	// on Windows since the colon char is not allowed in Windows paths.
   203  	if runtime.GOOS != "windows" {
   204  		src := filepath.Join(path, "go.crc32.cpu")
   205  		dst := filepath.Join(t.TempDir(), "go.crc32.cpu_2023-11-11_01:02:03")
   206  		data, err := os.ReadFile(src)
   207  		if err != nil {
   208  			t.Fatalf("read src file %s failed: %#v", src, err)
   209  		}
   210  		err = os.WriteFile(dst, data, 0644)
   211  		if err != nil {
   212  			t.Fatalf("create dst file %s failed: %#v", dst, err)
   213  		}
   214  		ts = append(ts, testcase{dst, ""})
   215  	}
   216  	for _, tc := range ts {
   217  		p, _, _, err := grabProfile(&source{ExecName: tc.execName}, tc.source, nil, testObj{}, &proftest.TestUI{T: t}, &httpTransport{})
   218  		if err != nil {
   219  			t.Fatalf("%s: %s", tc.source, err)
   220  		}
   221  		if len(p.Sample) == 0 {
   222  			t.Errorf("%s: want non-zero samples", tc.source)
   223  		}
   224  		if e := tc.execName; e != "" {
   225  			switch {
   226  			case len(p.Mapping) == 0 || p.Mapping[0] == nil:
   227  				t.Errorf("%s: want mapping[0].execName == %s, got no mappings", tc.source, e)
   228  			case p.Mapping[0].File != e:
   229  				t.Errorf("%s: want mapping[0].execName == %s, got %s", tc.source, e, p.Mapping[0].File)
   230  			}
   231  		}
   232  	}
   233  }
   234  
   235  func TestFetchWithBase(t *testing.T) {
   236  	baseConfig := currentConfig()
   237  	defer setCurrentConfig(baseConfig)
   238  
   239  	type WantSample struct {
   240  		values []int64
   241  		labels map[string][]string
   242  	}
   243  
   244  	const path = "testdata/"
   245  	type testcase struct {
   246  		desc              string
   247  		sources           []string
   248  		bases             []string
   249  		diffBases         []string
   250  		normalize         bool
   251  		wantSamples       []WantSample
   252  		wantParseErrorMsg string
   253  		wantFetchErrorMsg string
   254  	}
   255  
   256  	testcases := []testcase{
   257  		{
   258  			"not normalized base is same as source",
   259  			[]string{path + "cppbench.contention"},
   260  			[]string{path + "cppbench.contention"},
   261  			nil,
   262  			false,
   263  			nil,
   264  			"",
   265  			"",
   266  		},
   267  		{
   268  			"not normalized base is same as source",
   269  			[]string{path + "cppbench.contention"},
   270  			[]string{path + "cppbench.contention"},
   271  			nil,
   272  			false,
   273  			nil,
   274  			"",
   275  			"",
   276  		},
   277  		{
   278  			"not normalized single source, multiple base (all profiles same)",
   279  			[]string{path + "cppbench.contention"},
   280  			[]string{path + "cppbench.contention", path + "cppbench.contention"},
   281  			nil,
   282  			false,
   283  			[]WantSample{
   284  				{
   285  					values: []int64{-2700, -608881724},
   286  					labels: map[string][]string{},
   287  				},
   288  				{
   289  					values: []int64{-100, -23992},
   290  					labels: map[string][]string{},
   291  				},
   292  				{
   293  					values: []int64{-200, -179943},
   294  					labels: map[string][]string{},
   295  				},
   296  				{
   297  					values: []int64{-100, -17778444},
   298  					labels: map[string][]string{},
   299  				},
   300  				{
   301  					values: []int64{-100, -75976},
   302  					labels: map[string][]string{},
   303  				},
   304  				{
   305  					values: []int64{-300, -63568134},
   306  					labels: map[string][]string{},
   307  				},
   308  			},
   309  			"",
   310  			"",
   311  		},
   312  		{
   313  			"not normalized, different base and source",
   314  			[]string{path + "cppbench.contention"},
   315  			[]string{path + "cppbench.small.contention"},
   316  			nil,
   317  			false,
   318  			[]WantSample{
   319  				{
   320  					values: []int64{1700, 608878600},
   321  					labels: map[string][]string{},
   322  				},
   323  				{
   324  					values: []int64{100, 23992},
   325  					labels: map[string][]string{},
   326  				},
   327  				{
   328  					values: []int64{200, 179943},
   329  					labels: map[string][]string{},
   330  				},
   331  				{
   332  					values: []int64{100, 17778444},
   333  					labels: map[string][]string{},
   334  				},
   335  				{
   336  					values: []int64{100, 75976},
   337  					labels: map[string][]string{},
   338  				},
   339  				{
   340  					values: []int64{300, 63568134},
   341  					labels: map[string][]string{},
   342  				},
   343  			},
   344  			"",
   345  			"",
   346  		},
   347  		{
   348  			"normalized base is same as source",
   349  			[]string{path + "cppbench.contention"},
   350  			[]string{path + "cppbench.contention"},
   351  			nil,
   352  			true,
   353  			nil,
   354  			"",
   355  			"",
   356  		},
   357  		{
   358  			"normalized single source, multiple base (all profiles same)",
   359  			[]string{path + "cppbench.contention"},
   360  			[]string{path + "cppbench.contention", path + "cppbench.contention"},
   361  			nil,
   362  			true,
   363  			nil,
   364  			"",
   365  			"",
   366  		},
   367  		{
   368  			"normalized different base and source",
   369  			[]string{path + "cppbench.contention"},
   370  			[]string{path + "cppbench.small.contention"},
   371  			nil,
   372  			true,
   373  			[]WantSample{
   374  				{
   375  					values: []int64{-229, -369},
   376  					labels: map[string][]string{},
   377  				},
   378  				{
   379  					values: []int64{29, 0},
   380  					labels: map[string][]string{},
   381  				},
   382  				{
   383  					values: []int64{57, 1},
   384  					labels: map[string][]string{},
   385  				},
   386  				{
   387  					values: []int64{29, 80},
   388  					labels: map[string][]string{},
   389  				},
   390  				{
   391  					values: []int64{29, 0},
   392  					labels: map[string][]string{},
   393  				},
   394  				{
   395  					values: []int64{86, 288},
   396  					labels: map[string][]string{},
   397  				},
   398  			},
   399  			"",
   400  			"",
   401  		},
   402  		{
   403  			"not normalized diff base is same as source",
   404  			[]string{path + "cppbench.contention"},
   405  			nil,
   406  			[]string{path + "cppbench.contention"},
   407  			false,
   408  			[]WantSample{
   409  				{
   410  					values: []int64{2700, 608881724},
   411  					labels: map[string][]string{},
   412  				},
   413  				{
   414  					values: []int64{100, 23992},
   415  					labels: map[string][]string{},
   416  				},
   417  				{
   418  					values: []int64{200, 179943},
   419  					labels: map[string][]string{},
   420  				},
   421  				{
   422  					values: []int64{100, 17778444},
   423  					labels: map[string][]string{},
   424  				},
   425  				{
   426  					values: []int64{100, 75976},
   427  					labels: map[string][]string{},
   428  				},
   429  				{
   430  					values: []int64{300, 63568134},
   431  					labels: map[string][]string{},
   432  				},
   433  				{
   434  					values: []int64{-2700, -608881724},
   435  					labels: map[string][]string{"pprof::base": {"true"}},
   436  				},
   437  				{
   438  					values: []int64{-100, -23992},
   439  					labels: map[string][]string{"pprof::base": {"true"}},
   440  				},
   441  				{
   442  					values: []int64{-200, -179943},
   443  					labels: map[string][]string{"pprof::base": {"true"}},
   444  				},
   445  				{
   446  					values: []int64{-100, -17778444},
   447  					labels: map[string][]string{"pprof::base": {"true"}},
   448  				},
   449  				{
   450  					values: []int64{-100, -75976},
   451  					labels: map[string][]string{"pprof::base": {"true"}},
   452  				},
   453  				{
   454  					values: []int64{-300, -63568134},
   455  					labels: map[string][]string{"pprof::base": {"true"}},
   456  				},
   457  			},
   458  			"",
   459  			"",
   460  		},
   461  		{
   462  			"diff_base and base both specified",
   463  			[]string{path + "cppbench.contention"},
   464  			[]string{path + "cppbench.contention"},
   465  			[]string{path + "cppbench.contention"},
   466  			false,
   467  			nil,
   468  			"-base and -diff_base flags cannot both be specified",
   469  			"",
   470  		},
   471  		{
   472  			"input profiles with different sample types (non empty intersection)",
   473  			[]string{path + "cppbench.cpu", path + "cppbench.cpu_no_samples_type"},
   474  			[]string{path + "cppbench.cpu", path + "cppbench.cpu_no_samples_type"},
   475  			nil,
   476  			false,
   477  			nil,
   478  			"",
   479  			"",
   480  		},
   481  		{
   482  			"input profiles with different sample types (empty intersection)",
   483  			[]string{path + "cppbench.cpu", path + "cppbench.contention"},
   484  			[]string{path + "cppbench.cpu", path + "cppbench.contention"},
   485  			nil,
   486  			false,
   487  			nil,
   488  			"",
   489  			"problem fetching source profiles: profiles have empty common sample type list",
   490  		},
   491  	}
   492  
   493  	for _, tc := range testcases {
   494  		t.Run(tc.desc, func(t *testing.T) {
   495  			setCurrentConfig(baseConfig)
   496  			f := testFlags{
   497  				stringLists: map[string][]string{
   498  					"base":      tc.bases,
   499  					"diff_base": tc.diffBases,
   500  				},
   501  				bools: map[string]bool{
   502  					"normalize": tc.normalize,
   503  				},
   504  			}
   505  			f.args = tc.sources
   506  
   507  			o := setDefaults(&plugin.Options{
   508  				UI:            &proftest.TestUI{T: t, AllowRx: "Local symbolization failed|Some binary filenames not available"},
   509  				Flagset:       f,
   510  				HTTPTransport: transport.New(nil),
   511  			})
   512  			src, _, err := parseFlags(o)
   513  
   514  			if tc.wantParseErrorMsg != "" {
   515  				if err == nil {
   516  					t.Fatalf("got nil, want error %q", tc.wantParseErrorMsg)
   517  				}
   518  
   519  				if gotErrMsg := err.Error(); gotErrMsg != tc.wantParseErrorMsg {
   520  					t.Fatalf("got error %q, want error %q", gotErrMsg, tc.wantParseErrorMsg)
   521  				}
   522  				return
   523  			}
   524  
   525  			if err != nil {
   526  				t.Fatalf("got error %q, want no error", err)
   527  			}
   528  
   529  			p, err := fetchProfiles(src, o)
   530  
   531  			if tc.wantFetchErrorMsg != "" {
   532  				if err == nil {
   533  					t.Fatalf("got nil, want error %q", tc.wantFetchErrorMsg)
   534  				}
   535  
   536  				if gotErrMsg := err.Error(); gotErrMsg != tc.wantFetchErrorMsg {
   537  					t.Fatalf("got error %q, want error %q", gotErrMsg, tc.wantFetchErrorMsg)
   538  				}
   539  				return
   540  			}
   541  
   542  			if err != nil {
   543  				t.Fatalf("got error %q, want no error", err)
   544  			}
   545  
   546  			if got, want := len(p.Sample), len(tc.wantSamples); got != want {
   547  				t.Fatalf("got %d samples want %d", got, want)
   548  			}
   549  
   550  			for i, sample := range p.Sample {
   551  				if !reflect.DeepEqual(tc.wantSamples[i].values, sample.Value) {
   552  					t.Errorf("for sample %d got values %v, want %v", i, sample.Value, tc.wantSamples[i])
   553  				}
   554  				if !reflect.DeepEqual(tc.wantSamples[i].labels, sample.Label) {
   555  					t.Errorf("for sample %d got labels %v, want %v", i, sample.Label, tc.wantSamples[i].labels)
   556  				}
   557  			}
   558  		})
   559  	}
   560  }
   561  
   562  // mappingSources creates MappingSources map with a single item.
   563  func mappingSources(key, source string, start uint64) plugin.MappingSources {
   564  	return plugin.MappingSources{
   565  		key: []struct {
   566  			Source string
   567  			Start  uint64
   568  		}{
   569  			{Source: source, Start: start},
   570  		},
   571  	}
   572  }
   573  
   574  type httpTransport struct{}
   575  
   576  func (tr *httpTransport) RoundTrip(req *http.Request) (*http.Response, error) {
   577  	values := req.URL.Query()
   578  	file := values.Get("file")
   579  
   580  	if file == "" {
   581  		return nil, fmt.Errorf("want .../file?profile, got %s", req.URL.String())
   582  	}
   583  
   584  	t := &http.Transport{}
   585  	t.RegisterProtocol("file", http.NewFileTransport(http.Dir("testdata/")))
   586  
   587  	c := &http.Client{Transport: t}
   588  	return c.Get("file:///" + file)
   589  }
   590  
   591  func closedError() string {
   592  	if runtime.GOOS == "plan9" {
   593  		return "listen hungup"
   594  	}
   595  	return "use of closed"
   596  }
   597  
   598  func TestHTTPSInsecure(t *testing.T) {
   599  	if runtime.GOOS == "nacl" || runtime.GOOS == "js" {
   600  		t.Skip("test assumes tcp available")
   601  	}
   602  	saveHome := os.Getenv(homeEnv())
   603  	tempdir, err := os.MkdirTemp("", "home")
   604  	if err != nil {
   605  		t.Fatal("creating temp dir: ", err)
   606  	}
   607  	defer os.RemoveAll(tempdir)
   608  
   609  	// pprof writes to $HOME/pprof by default which is not necessarily
   610  	// writeable (e.g. on a Debian buildd) so set $HOME to something we
   611  	// know we can write to for the duration of the test.
   612  	os.Setenv(homeEnv(), tempdir)
   613  	defer os.Setenv(homeEnv(), saveHome)
   614  
   615  	baseConfig := currentConfig()
   616  	defer setCurrentConfig(baseConfig)
   617  
   618  	tlsCert, _, _ := selfSignedCert(t, "")
   619  	tlsConfig := &tls.Config{Certificates: []tls.Certificate{tlsCert}}
   620  
   621  	l, err := tls.Listen("tcp", "localhost:0", tlsConfig)
   622  	if err != nil {
   623  		t.Fatalf("net.Listen: got error %v, want no error", err)
   624  	}
   625  
   626  	donec := make(chan error, 1)
   627  	go func(donec chan<- error) {
   628  		donec <- http.Serve(l, nil)
   629  	}(donec)
   630  	defer func() {
   631  		if got, want := <-donec, closedError(); !strings.Contains(got.Error(), want) {
   632  			t.Fatalf("Serve got error %v, want %q", got, want)
   633  		}
   634  	}()
   635  	defer l.Close()
   636  
   637  	outputTempFile, err := os.CreateTemp("", "profile_output")
   638  	if err != nil {
   639  		t.Fatalf("Failed to create tempfile: %v", err)
   640  	}
   641  	defer os.Remove(outputTempFile.Name())
   642  	defer outputTempFile.Close()
   643  
   644  	address := "https+insecure://" + l.Addr().String() + "/debug/pprof/goroutine"
   645  	s := &source{
   646  		Sources:   []string{address},
   647  		Timeout:   10,
   648  		Symbolize: "remote",
   649  	}
   650  	o := &plugin.Options{
   651  		Obj:           &binutils.Binutils{},
   652  		UI:            &proftest.TestUI{T: t, AllowRx: "Saved profile in"},
   653  		HTTPTransport: transport.New(nil),
   654  	}
   655  	o.Sym = &symbolizer.Symbolizer{Obj: o.Obj, UI: o.UI}
   656  	p, err := fetchProfiles(s, o)
   657  	if err != nil {
   658  		t.Fatal(err)
   659  	}
   660  	if len(p.SampleType) == 0 {
   661  		t.Fatalf("fetchProfiles(%s) got empty profile: len(p.SampleType)==0", address)
   662  	}
   663  	if len(p.Function) == 0 {
   664  		t.Fatalf("fetchProfiles(%s) got non-symbolized profile: len(p.Function)==0", address)
   665  	}
   666  	if err := checkProfileHasFunction(p, "TestHTTPSInsecure"); err != nil {
   667  		t.Fatalf("fetchProfiles(%s) %v", address, err)
   668  	}
   669  }
   670  
   671  func TestHTTPSWithServerCertFetch(t *testing.T) {
   672  	if runtime.GOOS == "nacl" || runtime.GOOS == "js" {
   673  		t.Skip("test assumes tcp available")
   674  	}
   675  	saveHome := os.Getenv(homeEnv())
   676  	tempdir, err := os.MkdirTemp("", "home")
   677  	if err != nil {
   678  		t.Fatal("creating temp dir: ", err)
   679  	}
   680  	defer os.RemoveAll(tempdir)
   681  
   682  	// pprof writes to $HOME/pprof by default which is not necessarily
   683  	// writeable (e.g. on a Debian buildd) so set $HOME to something we
   684  	// know we can write to for the duration of the test.
   685  	os.Setenv(homeEnv(), tempdir)
   686  	defer os.Setenv(homeEnv(), saveHome)
   687  
   688  	baseConfig := currentConfig()
   689  	defer setCurrentConfig(baseConfig)
   690  
   691  	cert, certBytes, keyBytes := selfSignedCert(t, "localhost")
   692  	cas := x509.NewCertPool()
   693  	cas.AppendCertsFromPEM(certBytes)
   694  
   695  	tlsConfig := &tls.Config{
   696  		RootCAs:      cas,
   697  		Certificates: []tls.Certificate{cert},
   698  		ClientAuth:   tls.RequireAndVerifyClientCert,
   699  		ClientCAs:    cas,
   700  	}
   701  
   702  	l, err := tls.Listen("tcp", "localhost:0", tlsConfig)
   703  	if err != nil {
   704  		t.Fatalf("net.Listen: got error %v, want no error", err)
   705  	}
   706  
   707  	donec := make(chan error, 1)
   708  	go func(donec chan<- error) {
   709  		donec <- http.Serve(l, nil)
   710  	}(donec)
   711  	defer func() {
   712  		if got, want := <-donec, closedError(); !strings.Contains(got.Error(), want) {
   713  			t.Fatalf("Serve got error %v, want %q", got, want)
   714  		}
   715  	}()
   716  	defer l.Close()
   717  
   718  	outputTempFile, err := os.CreateTemp("", "profile_output")
   719  	if err != nil {
   720  		t.Fatalf("Failed to create tempfile: %v", err)
   721  	}
   722  	defer os.Remove(outputTempFile.Name())
   723  	defer outputTempFile.Close()
   724  
   725  	// Get port from the address, so request to the server can be made using
   726  	// the host name specified in certificates.
   727  	_, portStr, err := net.SplitHostPort(l.Addr().String())
   728  	if err != nil {
   729  		t.Fatalf("cannot get port from URL: %v", err)
   730  	}
   731  	address := "https://" + "localhost:" + portStr + "/debug/pprof/goroutine"
   732  	s := &source{
   733  		Sources:   []string{address},
   734  		Timeout:   10,
   735  		Symbolize: "remote",
   736  	}
   737  
   738  	certTempFile, err := os.CreateTemp("", "cert_output")
   739  	if err != nil {
   740  		t.Errorf("cannot create cert tempfile: %v", err)
   741  	}
   742  	defer os.Remove(certTempFile.Name())
   743  	defer certTempFile.Close()
   744  	certTempFile.Write(certBytes)
   745  
   746  	keyTempFile, err := os.CreateTemp("", "key_output")
   747  	if err != nil {
   748  		t.Errorf("cannot create key tempfile: %v", err)
   749  	}
   750  	defer os.Remove(keyTempFile.Name())
   751  	defer keyTempFile.Close()
   752  	keyTempFile.Write(keyBytes)
   753  
   754  	f := &testFlags{
   755  		strings: map[string]string{
   756  			"tls_cert": certTempFile.Name(),
   757  			"tls_key":  keyTempFile.Name(),
   758  			"tls_ca":   certTempFile.Name(),
   759  		},
   760  	}
   761  	o := &plugin.Options{
   762  		Obj:           &binutils.Binutils{},
   763  		UI:            &proftest.TestUI{T: t, AllowRx: "Saved profile in"},
   764  		Flagset:       f,
   765  		HTTPTransport: transport.New(f),
   766  	}
   767  
   768  	o.Sym = &symbolizer.Symbolizer{Obj: o.Obj, UI: o.UI, Transport: o.HTTPTransport}
   769  	p, err := fetchProfiles(s, o)
   770  	if err != nil {
   771  		t.Fatal(err)
   772  	}
   773  	if len(p.SampleType) == 0 {
   774  		t.Fatalf("fetchProfiles(%s) got empty profile: len(p.SampleType)==0", address)
   775  	}
   776  	if len(p.Function) == 0 {
   777  		t.Fatalf("fetchProfiles(%s) got non-symbolized profile: len(p.Function)==0", address)
   778  	}
   779  	if err := checkProfileHasFunction(p, "TestHTTPSWithServerCertFetch"); err != nil {
   780  		t.Fatalf("fetchProfiles(%s) %v", address, err)
   781  	}
   782  }
   783  
   784  func checkProfileHasFunction(p *profile.Profile, fname string) error {
   785  	for _, f := range p.Function {
   786  		if strings.Contains(f.Name, fname) {
   787  			return nil
   788  		}
   789  	}
   790  	return fmt.Errorf("got %s, want function %q", p.String(), fname)
   791  }
   792  
   793  // selfSignedCert generates a self-signed certificate, and returns the
   794  // generated certificate, and byte arrays containing the certificate and
   795  // key associated with the certificate.
   796  func selfSignedCert(t *testing.T, host string) (tls.Certificate, []byte, []byte) {
   797  	privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   798  	if err != nil {
   799  		t.Fatalf("failed to generate private key: %v", err)
   800  	}
   801  	b, err := x509.MarshalECPrivateKey(privKey)
   802  	if err != nil {
   803  		t.Fatalf("failed to marshal private key: %v", err)
   804  	}
   805  	bk := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b})
   806  
   807  	tmpl := x509.Certificate{
   808  		SerialNumber: big.NewInt(1),
   809  		NotBefore:    time.Now(),
   810  		NotAfter:     time.Now().Add(10 * time.Minute),
   811  		IsCA:         true,
   812  		DNSNames:     []string{host},
   813  	}
   814  
   815  	b, err = x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, privKey.Public(), privKey)
   816  	if err != nil {
   817  		t.Fatalf("failed to create cert: %v", err)
   818  	}
   819  	bc := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: b})
   820  
   821  	cert, err := tls.X509KeyPair(bc, bk)
   822  	if err != nil {
   823  		t.Fatalf("failed to create TLS key pair: %v", err)
   824  	}
   825  	return cert, bc, bk
   826  }
   827  

View as plain text