dnaeon / go-vcr

Record and replay your HTTP interactions for fast, deterministic and accurate tests
BSD 2-Clause "Simplified" License
1.26k stars 77 forks source link

does support client with mTLS ? #95

Closed shufanhao closed 5 months ago

shufanhao commented 5 months ago

I am trying to call a server which requires client certificate(mTLS), but looks like it doesn't work. After code is completed, the recorder.cassette.Interactions is empty, nothing was recorded. Not sure if go-vcr support mTLS case ?

dnaeon commented 5 months ago

Hey there,

Did you stop the recorder?

If it wasn't stopped it will not persist the interactions.

Also, please share some sample code, so we can look at it.

Thanks!

On Tue, May 14, 2024, 17:05 Frank @.***> wrote:

I am trying to call a server which requires client certificate(mTLS), but looks like it doesn't work. After code is completed, the recorder.cassette.Interactions is empty, nothing was recorded. Not sure if go-vcr support mTLS case ?

— Reply to this email directly, view it on GitHub https://github.com/dnaeon/go-vcr/issues/95, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAB3JC2TOJTZ23UU7E6JZ7LZCIKZJAVCNFSM6AAAAABHWIIUF2VHI2DSMVQWIX3LMV43ASLTON2WKOZSGI4TKNJWGA4TANI . You are receiving this because you are subscribed to this thread.Message ID: @.***>

shufanhao commented 5 months ago

I am using it in a terraform provider acceptance testing framework, i can provide example code, thanks

---- Replied Message ---- | From | Marin Atanasov @.> | | Date | 05/14/2024 23:10 | | To | dnaeon/go-vcr @.> | | Cc | Frank @.>, Author @.> | | Subject | Re: [dnaeon/go-vcr] does support client with mTLS ? (Issue #95) |

Hey there,

Did you stop the recorder?

If it wasn't stopped it will not persist the interactions.

Also, please share some sample code, so we can look at it.

Thanks!

On Tue, May 14, 2024, 17:05 Frank @.***> wrote:

I am trying to call a server which requires client certificate(mTLS), but looks like it doesn't work. After code is completed, the recorder.cassette.Interactions is empty, nothing was recorded. Not sure if go-vcr support mTLS case ?

— Reply to this email directly, view it on GitHub https://github.com/dnaeon/go-vcr/issues/95, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAB3JC2TOJTZ23UU7E6JZ7LZCIKZJAVCNFSM6AAAAABHWIIUF2VHI2DSMVQWIX3LMV43ASLTON2WKOZSGI4TKNJWGA4TANI . You are receiving this because you are subscribed to this thread.Message ID: @.***>

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.Message ID: @.***>

shufanhao commented 5 months ago

@dnaeon thanks for helping to take look. I think the root cause is we couldn't pass a TLSClientConfig to recorder which doesn't support mTLS.

here is my example code:

var testRSACertificate = fromHex("3082024b308201b4a003020102020900e8f09d3fe25beaa6300d06092a864886f70d01010b0500301f310b3009060355040a1302476f3110300e06035504031307476f20526f6f74301e170d3136303130313030303030305a170d3235303130313030303030305a301a310b3009060355040a1302476f310b300906035504031302476f30819f300d06092a864886f70d010101050003818d0030818902818100db467d932e12270648bc062821ab7ec4b6a25dfe1e5245887a3647a5080d92425bc281c0be97799840fb4f6d14fd2b138bc2a52e67d8d4099ed62238b74a0b74732bc234f1d193e596d9747bf3589f6c613cc0b041d4d92b2b2423775b1c3bbd755dce2054cfa163871d1e24c4f31d1a508baab61443ed97a77562f414c852d70203010001a38193308190300e0603551d0f0101ff0404030205a0301d0603551d250416301406082b0601050507030106082b06010505070302300c0603551d130101ff0402300030190603551d0e041204109f91161f43433e49a6de6db680d79f60301b0603551d230414301280104813494d137e1631bba301d5acab6e7b30190603551d1104123010820e6578616d706c652e676f6c616e67300d06092a864886f70d01010b0500038181009d30cc402b5b50a061cbbae55358e1ed8328a9581aa938a495a1ac315a1a84663d43d32dd90bf297dfd320643892243a00bccf9c7db74020015faad3166109a276fd13c3cce10c5ceeb18782f16c04ed73bbb343778d0c1cf10fa1d8408361c94c722b9daedb4606064df4c1b33ec0d1bd42d4dbfe3d1360845c21d33be9fae7")
var testRSAPrivateKey, _ = x509.ParsePKCS1PrivateKey(fromHex("3082025b02010002818100db467d932e12270648bc062821ab7ec4b6a25dfe1e5245887a3647a5080d92425bc281c0be97799840fb4f6d14fd2b138bc2a52e67d8d4099ed62238b74a0b74732bc234f1d193e596d9747bf3589f6c613cc0b041d4d92b2b2423775b1c3bbd755dce2054cfa163871d1e24c4f31d1a508baab61443ed97a77562f414c852d702030100010281800b07fbcf48b50f1388db34b016298b8217f2092a7c9a04f77db6775a3d1279b62ee9951f7e371e9de33f015aea80660760b3951dc589a9f925ed7de13e8f520e1ccbc7498ce78e7fab6d59582c2386cc07ed688212a576ff37833bd5943483b5554d15a0b9b4010ed9bf09f207e7e9805f649240ed6c1256ed75ab7cd56d9671024100fded810da442775f5923debae4ac758390a032a16598d62f059bb2e781a9c2f41bfa015c209f966513fe3bf5a58717cbdb385100de914f88d649b7d15309fa49024100dd10978c623463a1802c52f012cfa72ff5d901f25a2292446552c2568b1840e49a312e127217c2186615aae4fb6602a4f6ebf3f3d160f3b3ad04c592f65ae41f02400c69062ca781841a09de41ed7a6d9f54adc5d693a2c6847949d9e1358555c9ac6a8d9e71653ac77beb2d3abaf7bb1183aa14278956575dbebf525d0482fd72d90240560fe1900ba36dae3022115fd952f2399fb28e2975a1c3e3d0b679660bdcb356cc189d611cfdd6d87cd5aea45aa30a2082e8b51e94c2f3dd5d5c6036a8a615ed0240143993d80ece56f877cb80048335701eb0e608cc0c1ca8c2227b52edf8f1ac99c562f2541b5ce81f0515af1c5b4770dba53383964b4b725ff46fdec3d08907df"))

func fromHex(s string) []byte {
    b, _ := hex.DecodeString(s)
    return b
}

func TestRetryHttpIntercept(t *testing.T) {
    rec, err := recorder.NewWithOptions(&recorder.Options{
        CassetteName:       fmt.Sprintf("cassettes/%s", t.Name()),
        Mode:               recorder.ModeRecordOnce,
        SkipRequestLatency: true,
    })

    rec.SetMatcher(func(req *http.Request, c cassette.Request) bool {
        // ignore hostname prefixes and URI paths on replays
        return req.Method == c.Method
    })

    if err != nil {
        t.Fatalf("Failed to create recorder: %v", err)
    }

    defer func() {
        if err := rec.Stop(); err != nil {
            log.Printf("Failed to stop recorder: %v", err)
        }
    }()

    client, _ := testclient.NewTestClient(testclient.Config{
        TLSCert: nil,
        URL:     "https://google.com",
    })

    tlsClient := rec.GetDefaultClient()

    // If I use a client mTLS, it doesn't work
    c0 := tls.Certificate{
        Certificate: [][]byte{testRSACertificate},
        PrivateKey:  testRSAPrivateKey,
    }
    tlsClient.Transport = &http.Transport{
        TLSClientConfig: &tls.Config{
            Certificates: []tls.Certificate{c0},
            // RootCAs:      "",
        },
    }

    client.SetHttpClient(tlsClient)
    _, err = client.Get()

    if err != nil {
        t.Fatalf("Failed to make request: %v", err)
    }
}
dnaeon commented 5 months ago

Hey @shufanhao ,

I cannot test the code at the moment, but I think you should be able to use the RealTransport Option to configure the TLSClientConfig, e.g.

        realTransport := &http.Transport{TLSClientConfig{...}}

    rec, err := recorder.NewWithOptions(&recorder.Options{
        CassetteName:       fmt.Sprintf("cassettes/%s", t.Name()),
        Mode:               recorder.ModeRecordOnce,
        SkipRequestLatency: true,
                RealTransport:      realTransport,
    })

       ...
shufanhao commented 5 months ago

@dnaeon thanks, finally it works, but I didn't understand it why it works. What's realTransport and why we need it ? I see a lot of example code, directly use a default realTransport

func TestRetryHttpIntercept(t *testing.T) {
    httpClient := cleanhttp.DefaultPooledClient()
    transport := httpClient.Transport.(*http.Transport)
    transport.MaxIdleConnsPerHost = 10
    tlsConfig := transport.TLSClientConfig

    c0 := tls.Certificate{
        Certificate: [][]byte{testRSACertificate},
        PrivateKey:  testRSAPrivateKey,
    }
    if tlsConfig == nil {
        transport.TLSClientConfig = &tls.Config{
            Certificates: []tls.Certificate{c0},
            MinVersion:   tls.VersionTLS12,
            // RootCAs:      "",
        }
    }

    rec, err := recorder.NewWithOptions(&recorder.Options{
        CassetteName:       fmt.Sprintf("cassettes/%s", t.Name()),
        Mode:               recorder.ModeRecordOnly,
        SkipRequestLatency: true,
        RealTransport:      httpClient.Transport,
    })

    rec.SetMatcher(func(req *http.Request, c cassette.Request) bool {
        // ignore hostname prefixes and URI paths on replays
        return req.Method == c.Method
    })

    if err != nil {
        t.Fatalf("Failed to create recorder: %v", err)
    }

    defer func() {
        if err := rec.Stop(); err != nil {
            log.Printf("Failed to stop recorder: %v", err)
        }
    }()

    // without this code, it doesn't work
    httpClient.Transport = rec

    client, _ := testclient.NewTestClient(testclient.Config{
        TLSCert: nil,
        URL:     "https://google.com",
    })

    client.SetHttpClient(httpClient)
    _, err = client.Get()

    if err != nil {
        t.Fatalf("Failed to make request: %v", err)
    }
}
dnaeon commented 5 months ago

Hey @shufanhao ,

Your initial code replaced the HTTP transport with a new one, and for that reason it was no longer running in the context of VCR.

    tlsClient := rec.GetDefaultClient()

    // If I use a client mTLS, it doesn't work
    c0 := tls.Certificate{
        Certificate: [][]byte{testRSACertificate},
        PrivateKey:  testRSAPrivateKey,
    }
    tlsClient.Transport = &http.Transport{
        TLSClientConfig: &tls.Config{
            Certificates: []tls.Certificate{c0},
            // RootCAs:      "",
        },
    }

The RealTransport as the docstring mentions is used for making the actual HTTP calls to the remote system, when there was no previous interaction recorded. Setting that one up makes sure that you can reach your HTTP endpoint and authenticate properly.