diff --git a/eng/pipelines/templates/steps/build-test.yml b/eng/pipelines/templates/steps/build-test.yml index ad23a784121f..e48c585ae5d8 100644 --- a/eng/pipelines/templates/steps/build-test.yml +++ b/eng/pipelines/templates/steps/build-test.yml @@ -44,7 +44,7 @@ steps: displayName: "Install Coverage and Junit Dependencies" - ${{ if eq(parameters.TestProxy, true) }}: - - template: /eng/common/testproxy/test-proxy-docker.yml + - template: /eng/common/testproxy/test-proxy-tool.yml - task: PowerShell@2 displayName: 'Run Tests' @@ -60,9 +60,8 @@ steps: - ${{ if eq(parameters.TestProxy, true) }}: - pwsh: | - # ambitious_azsdk_test_proxy is the hardcoded container name used - # by the test proxy startup script - docker logs ambitious_azsdk_test_proxy + # $(Build.SourcesDirectory)/test-proxy.log is the hardcoded output log location for the test-proxy-tool.yml + cat $(Build.SourcesDirectory)/test-proxy.log displayName: 'Dump Test Proxy logs' condition: succeededOrFailed() diff --git a/sdk/azidentity/go.mod b/sdk/azidentity/go.mod index c6b130bfc85f..2c1ef6adee47 100644 --- a/sdk/azidentity/go.mod +++ b/sdk/azidentity/go.mod @@ -2,6 +2,8 @@ module github.com/Azure/azure-sdk-for-go/sdk/azidentity go 1.16 +replace github.com/Azure/azure-sdk-for-go/sdk/internal => ../internal + require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.0 github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.0 diff --git a/sdk/azidentity/go.sum b/sdk/azidentity/go.sum index 25fa40f704b4..e64d1e0be1ce 100644 --- a/sdk/azidentity/go.sum +++ b/sdk/azidentity/go.sum @@ -1,8 +1,5 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.0 h1:8wVJL0HUP5yDFXvotdewORTw7Yu88JbreWN/mobSvsQ= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.0/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= -github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= -github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.0 h1:HMbyI+KfvL+XyuWekow/nWbRxsAhB6+DVzgQTIABecU= -github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.0/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 h1:WVsrXCnHlDDX8ls+tootqRE87/hL9S/g4ewig9RsD/c= github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/sdk/azidentity/live_test.go b/sdk/azidentity/live_test.go index fb81d63e6c64..8b6d3bb7a039 100644 --- a/sdk/azidentity/live_test.go +++ b/sdk/azidentity/live_test.go @@ -6,7 +6,6 @@ package azidentity import ( "context" "fmt" - "log" "net/http" "net/url" "os" @@ -87,29 +86,29 @@ func init() { } func TestMain(m *testing.M) { - switch recording.GetRecordMode() { - case recording.PlaybackMode: - // enable BodilessMatcher because we don't record request bodies - // TODO: add an API for this to sdk/internal - req, err := http.NewRequest("POST", "http://localhost:5000/Admin/SetMatcher", http.NoBody) + if recording.GetRecordMode() == recording.PlaybackMode || recording.GetRecordMode() == recording.RecordingMode { + // Start from a fresh proxy + err := recording.ResetProxy(nil) if err != nil { panic(err) } - req.Header["x-abstraction-identifier"] = []string{"BodilessMatcher"} - res, err := http.DefaultClient.Do(req) + + // At the end of testing we want to reset as to not interfere with other tests. + defer func() { + err := recording.ResetProxy(nil) + if err != nil { + panic(err) + } + }() + } + + switch recording.GetRecordMode() { + case recording.PlaybackMode: + err := recording.SetBodilessMatcher(nil, nil) if err != nil { panic(err) } - if res.StatusCode != http.StatusOK { - log.Panicf("failed to enable BodilessMatcher: %v", res) - } - // TODO: reset matcher case recording.RecordingMode: - // remove default sanitizers such as the OAuth response sanitizer - err := recording.ResetProxy(nil) - if err != nil { - panic(err) - } // replace path variables with fake values to simplify matching (the real values aren't secret) pathVars := map[string]string{ liveManagedIdentity.clientID: fakeClientID, @@ -120,7 +119,7 @@ func TestMain(m *testing.M) { } for target, replacement := range pathVars { if target != "" { - err = recording.AddURISanitizer(replacement, target, nil) + err := recording.AddURISanitizer(replacement, target, nil) if err != nil { panic(err) } @@ -139,7 +138,7 @@ func TestMain(m *testing.M) { // remove token request bodies (which are form encoded) because they contain // secrets, are irrelevant in matching, and are formed by MSAL anyway // (note: Cloud Shell would need an exemption from this, and that would be okay--its requests contain no secrets) - err = recording.AddBodyRegexSanitizer("{}", `^\S+=.*`, nil) + err := recording.AddBodyRegexSanitizer("{}", `^\S+=.*`, nil) if err != nil { panic(err) } @@ -150,15 +149,10 @@ func TestMain(m *testing.M) { panic(err) } } - defer func() { - err := recording.ResetProxy(nil) - if err != nil { - panic(err) - } - // TODO: reset matcher - }() } - os.Exit(m.Run()) + val := m.Run() + + os.Exit(val) } func initRecording(t *testing.T) (policy.ClientOptions, func()) { diff --git a/sdk/data/aztables/go.mod b/sdk/data/aztables/go.mod index bc0ff54ffd01..f0ab0b5ec01b 100644 --- a/sdk/data/aztables/go.mod +++ b/sdk/data/aztables/go.mod @@ -2,6 +2,8 @@ module github.com/Azure/azure-sdk-for-go/sdk/data/aztables go 1.16 +replace github.com/Azure/azure-sdk-for-go/sdk/internal => ../../internal + require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.0 diff --git a/sdk/data/aztables/go.sum b/sdk/data/aztables/go.sum index e832be9f3173..467c446de456 100644 --- a/sdk/data/aztables/go.sum +++ b/sdk/data/aztables/go.sum @@ -2,9 +2,6 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.0 h1:8wVJL0HUP5yDFXvotdewORTw github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.0/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.0 h1:bLRntPH25SkY1uZ/YZW+dmxNky9r1fAHvDFrzluo+4Q= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.0/go.mod h1:TmXReXZ9yPp5D5TBRMTAtyz+UyOl15Py4hL5E5p6igQ= -github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= -github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.0 h1:HMbyI+KfvL+XyuWekow/nWbRxsAhB6+DVzgQTIABecU= -github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.0/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 h1:WVsrXCnHlDDX8ls+tootqRE87/hL9S/g4ewig9RsD/c= github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/sdk/data/aztables/proxy_test.go b/sdk/data/aztables/proxy_test.go index 553d3e5a7987..98f4b72c00fc 100644 --- a/sdk/data/aztables/proxy_test.go +++ b/sdk/data/aztables/proxy_test.go @@ -21,7 +21,15 @@ import ( func TestMain(m *testing.M) { // 1. Set up session level sanitizers - if recording.GetRecordMode() == "record" { + switch recording.GetRecordMode() { + case recording.PlaybackMode: + err := recording.SetDefaultMatcher(nil, &recording.SetDefaultMatcherOptions{ + ExcludedHeaders: []string{":path", ":auth", ":method", ":scheme"}, + }) + if err != nil { + panic(err) + } + case recording.RecordingMode: for _, val := range []string{"TABLES_COSMOS_ACCOUNT_NAME", "TABLES_STORAGE_ACCOUNT_NAME"} { account, ok := os.LookupEnv(val) if !ok { @@ -34,8 +42,8 @@ func TestMain(m *testing.M) { panic(err) } } - } + } // Run tests exitVal := m.Run() diff --git a/sdk/internal/CHANGELOG.md b/sdk/internal/CHANGELOG.md index 068b3dcd7bc5..d08c5820adf5 100644 --- a/sdk/internal/CHANGELOG.md +++ b/sdk/internal/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.9.1 (Unreleased) ### Features Added +* Adds a `CustomDefaultMatcher` that adds headers `:path`, `:authority`, `:method`, and `:scheme` to the ### Breaking Changes diff --git a/sdk/internal/recording/matchers.go b/sdk/internal/recording/matchers.go index 401f3d7bf8af..952c8791f851 100644 --- a/sdk/internal/recording/matchers.go +++ b/sdk/internal/recording/matchers.go @@ -7,8 +7,11 @@ package recording import ( - "fmt" + "bytes" + "encoding/json" + "io/ioutil" "net/http" + "strings" "testing" ) @@ -19,24 +22,90 @@ type MatcherOptions struct { // SetBodilessMatcher adjusts the "match" operation to exclude the body when matching a request to a recording's entries. // Pass in `nil` for `t` if you want the bodiless matcher to apply everywhere func SetBodilessMatcher(t *testing.T, options *MatcherOptions) error { + f := false + return SetDefaultMatcher(t, &SetDefaultMatcherOptions{ + CompareBodies: &f, + }) +} + +type SetDefaultMatcherOptions struct { + CompareBodies *bool + ExcludedHeaders []string + IgnoredHeaders []string + IgnoreQueryOrdering *bool +} + +func (s *SetDefaultMatcherOptions) fillOptions() { + f := false + t := true + if s == nil { + s = &SetDefaultMatcherOptions{ + CompareBodies: &t, + IgnoreQueryOrdering: &f, + } + return + } + + if s.CompareBodies == nil { + s.CompareBodies = &t + } + if s.IgnoreQueryOrdering == nil { + s.IgnoreQueryOrdering = &f + } +} + +func addDefaults(added []string) []string { + if added == nil { + return nil + } + needToAdd := []string{":path", ":authority", ":method", ":scheme"} + for _, a := range added { + for idx, n := range needToAdd { + if a == n { + needToAdd = append(needToAdd[:idx], needToAdd[idx+1:]...) + } + } + } + return append(added, needToAdd...) +} + +// SetDefaultMatcher adjusts the "match" operation to exclude the body when matching a request to a recording's entries. +// Pass in `nil` for `t` if you want the bodiless matcher to apply everywhere +func SetDefaultMatcher(t *testing.T, options *SetDefaultMatcherOptions) error { if recordMode != PlaybackMode { return nil } + options.fillOptions() req, err := http.NewRequest("POST", "http://localhost:5000/Admin/SetMatcher", http.NoBody) if err != nil { panic(err) } - req.Header["x-abstraction-identifier"] = []string{"BodilessMatcher"} + req.Header["x-abstraction-identifier"] = []string{"CustomDefaultMatcher"} if t != nil { req.Header["x-recording-id"] = []string{GetRecordingId(t)} } - res, err := http.DefaultClient.Do(req) + if !(*options.CompareBodies) { + options.ExcludedHeaders = append(options.ExcludedHeaders, "Content-Length") + } + + marshalled, err := json.MarshalIndent(struct { + CompareBodies *bool `json:"compareBodies,omitempty"` + ExcludedHeaders string `json:"excludedHeaders,omitempty"` + IncludedHeaders string `json:"includedHeaders,omitempty"` + IgnoreQueryOrdering *bool `json:"ignoreQueryOrdering,omitempty"` + }{ + CompareBodies: options.CompareBodies, + ExcludedHeaders: strings.Join(addDefaults(options.ExcludedHeaders), ","), + IncludedHeaders: strings.Join(options.IgnoredHeaders, ","), + IgnoreQueryOrdering: options.IgnoreQueryOrdering, + }, "", "") if err != nil { return err } - if res.StatusCode != http.StatusOK { - return fmt.Errorf("failed to enable BodilessMatcher: %v", res) - } - return nil + + req.Body = ioutil.NopCloser(bytes.NewReader(marshalled)) + req.ContentLength = int64(len(marshalled)) + + return handleProxyResponse(client.Do(req)) } diff --git a/sdk/internal/recording/matchers_test.go b/sdk/internal/recording/matchers_test.go index 80b966ffb0c1..a6e6acf005a8 100644 --- a/sdk/internal/recording/matchers_test.go +++ b/sdk/internal/recording/matchers_test.go @@ -113,3 +113,63 @@ func TestSetBodilessMatcherNilTest(t *testing.T) { err = ResetProxy(nil) require.NoError(t, err) } + +func TestSetDefaultMatcher(t *testing.T) { + temp := recordMode + recordMode = RecordingMode + defer func() { recordMode = temp }() + + err := Start(t, packagePath, nil) + require.NoError(t, err) + + req, err := http.NewRequest("POST", "https://localhost:5001", nil) + require.NoError(t, err) + + req.Header.Set(UpstreamURIHeader, "https://bing.com") + req.Header.Set(ModeHeader, GetRecordMode()) + req.Header.Set(IDHeader, GetRecordingId(t)) + + client, err := GetHTTPClient(t) + require.NoError(t, err) + + _, err = client.Do(req) + require.NoError(t, err) + + err = Stop(t, nil) + require.NoError(t, err) + + // Run a second request to with different body to verify it works + recordMode = PlaybackMode + + err = Start(t, packagePath, nil) + require.NoError(t, err) + + err = SetDefaultMatcher(nil, &SetDefaultMatcherOptions{ExcludedHeaders: []string{"ExampleHeader"}}) + require.NoError(t, err) + + req, err = http.NewRequest("POST", "https://localhost:5001", nil) + require.NoError(t, err) + + req.Header.Set(UpstreamURIHeader, "https://bing.com") + req.Header.Set(ModeHeader, GetRecordMode()) + req.Header.Set(IDHeader, GetRecordingId(t)) + req.Header.Set("ExampleHeader", "blah-blah-blah") + + err = handleProxyResponse(client.Do(req)) + require.NoError(t, err) + + err = Stop(t, nil) + require.NoError(t, err) + + err = ResetProxy(nil) + require.NoError(t, err) +} + +func TestAddDefaults(t *testing.T) { + require.Equal(t, 4, len(addDefaults([]string{}))) + require.Equal(t, 4, len(addDefaults([]string{":path"}))) + require.Equal(t, 4, len(addDefaults([]string{":path", ":authority"}))) + require.Equal(t, 4, len(addDefaults([]string{":path", ":authority", ":method"}))) + require.Equal(t, 4, len(addDefaults([]string{":path", ":authority", ":method", ":scheme"}))) + require.Equal(t, 5, len(addDefaults([]string{":path", ":authority", ":method", ":scheme", "extra"}))) +} diff --git a/sdk/internal/recording/recording.go b/sdk/internal/recording/recording.go index 53443753fa65..936028d54237 100644 --- a/sdk/internal/recording/recording.go +++ b/sdk/internal/recording/recording.go @@ -476,6 +476,22 @@ func init() { if ok := certPool.AppendCertsFromPEM(cert); !ok { log.Println("no certs appended, using system certs only") } + + // Set a Default matcher that ignores :path, :scheme, :authority, and :method headers + err = SetDefaultMatcher( + nil, + &SetDefaultMatcherOptions{ExcludedHeaders: []string{ + ":authority", + ":method", + ":path", + ":scheme", + }}, + ) + if err != nil { + log.Println("could not set the default matcher") + } else { + log.Println("default matcher was set ") + } } var recordMode string diff --git a/sdk/internal/recording/testdata/recordings/TestSetDefaultMatcher.json b/sdk/internal/recording/testdata/recordings/TestSetDefaultMatcher.json new file mode 100644 index 000000000000..ed3e94a9703c --- /dev/null +++ b/sdk/internal/recording/testdata/recordings/TestSetDefaultMatcher.json @@ -0,0 +1,36 @@ +{ + "Entries": [ + { + "RequestUri": "https://bing.com/", + "RequestMethod": "POST", + "RequestHeaders": { + ":method": "POST", + "Accept-Encoding": "gzip", + "Content-Length": "0", + "User-Agent": "Go-http-client/2.0" + }, + "RequestBody": null, + "StatusCode": 301, + "ResponseHeaders": { + "Accept-CH": "Sec-CH-UA-Arch, Sec-CH-UA-Bitness, Sec-CH-UA-Full-Version, Sec-CH-UA-Mobile, Sec-CH-UA-Model, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version", + "Cache-Control": "private", + "Content-Encoding": "gzip", + "Content-Length": "2943", + "Content-Type": "text/html; charset=utf-8", + "Date": "Wed, 26 Jan 2022 21:26:25 GMT", + "Location": "https://www.bing.com/?toWww=1\u0026redig=36D16E58BC3C46DC988D7753868E4F82\u0026toWww=1\u0026redig=B3FED55D25074AD2B1EFEA57D4E28847\u0026toWww=1\u0026redig=3542BC2407354C2B8946B4C66FCED165\u0026toWww=1\u0026redig=100A2DEF35DB4995858E0B5307C88DEE\u0026toWww=1\u0026redig=EFC20904D23348E9BE09AE4995CFADC7\u0026toWww=1\u0026redig=3404832CF38E41A5A730D8785F5E236A\u0026toWww=1\u0026redig=386345D1C6C34575B78049D25AA9B32E\u0026toWww=1\u0026redig=AC7B616A21A24DD9B292D9C29F5E2241\u0026toWww=1\u0026redig=AECF00D83B654B6E87ADD5D907FF2D96\u0026toWww=1\u0026redig=4B8D6950EE54455DA622D092C2461852\u0026toWww=1\u0026redig=8B5DCF6002804730994D8A060780C298\u0026toWww=1\u0026redig=817EB78E5F40451EBB80DFF0D7D61704\u0026toWww=1\u0026redig=BA5002CA24E743CFA3A5BE316105D264\u0026toWww=1\u0026redig=6048082DF9B44EBABF759817297CC027\u0026toWww=1\u0026redig=67A6F168987F47E481818E524A7FD608\u0026toWww=1\u0026redig=D57719045309432697D23569DA7781A4\u0026toWww=1\u0026redig=D7D685405D994F57A643BD7C7792068B\u0026toWww=1\u0026redig=4060BD3DCE9E41FC9C269D83601E5E84\u0026toWww=1\u0026redig=C0F5F7A3C76F452092438DE5B92CC49D\u0026toWww=1\u0026redig=4C88C00D3E3A4DD7BB483CBFA467146F\u0026toWww=1\u0026redig=605B790A04784A6A96C8AB3142222802\u0026toWww=1\u0026redig=0D9535924D3A41C8827AA3A26596A132\u0026toWww=1\u0026redig=EC9078B10F1449108D38D4E256414B30\u0026toWww=1\u0026redig=02614D8BEF064BF8822DEF7D5BC0F2BD\u0026toWww=1\u0026redig=A8C611933FF04D7384190EEB5B1BDF07\u0026toWww=1\u0026redig=96D2DFA70F2C4C1A99659C0252CEE07B\u0026toWww=1\u0026redig=83742D119D4244F99CE9C424AB265F36\u0026toWww=1\u0026redig=5A9F37CEB0744CAABCED6CED250F04CA\u0026toWww=1\u0026redig=461DA675C2554DE9A6737218E19DDDE9\u0026toWww=1\u0026redig=253A116BECEA4B7E9D6ADA85F9A7A3BE\u0026toWww=1\u0026redig=F7AD3071B1C745829B3E3630DCE477A6\u0026toWww=1\u0026redig=04D55828E7B44B07B697668840CD9DD6\u0026toWww=1\u0026redig=3424EE37C1DF423A8621C03918BFD456\u0026toWww=1\u0026redig=A33B381D37094F598285EBFD97A03327\u0026toWww=1\u0026redig=024BCA4BF7034FB9906CEB7D7AC0D5AA\u0026toWww=1\u0026redig=B03235A52A4B47EF9DCDDACD8D5606D8\u0026toWww=1\u0026redig=2279AF2C63A842B5BC66B84F06F46A61\u0026toWww=1\u0026redig=BD11B17E000640A7A37F0A208052A635\u0026toWww=1\u0026redig=068B2075D3E2489CADDACD43BE952D47\u0026toWww=1\u0026redig=05A9BCBE75FC4A2DBFC763FB5F4196E1\u0026toWww=1\u0026redig=6EBF752C32D04B5FB9BF092C3A8834DC\u0026toWww=1\u0026redig=6D64BBE8F50F4D4E9434A52526DA8C3C\u0026toWww=1\u0026redig=439989E7C6324EC7B2058EF5CDEC1F96\u0026toWww=1\u0026redig=926E3EA38A7D4A8CB7274D9D96271E25\u0026toWww=1\u0026redig=B2741CADB7F940B28B2549A1F86B67C1\u0026toWww=1\u0026redig=4F8659EAF2644F3CBDD103FAE7B8409F\u0026toWww=1\u0026redig=0C86A2B69CF3488585D23537BFC672C9\u0026toWww=1\u0026redig=62FDEE14D5FE48E7A2D63C2441AB72BF\u0026toWww=1\u0026redig=6EE10DB5ABC94C8B81A978780B04F4B0\u0026toWww=1\u0026redig=C77184D416F442A489B2610E53338EB0\u0026toWww=1\u0026redig=DC3E29A039B1494FA6D4D3E2917CBDA1", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload", + "Vary": "Accept-Encoding", + "X-Cache": "CONFIG_NOCACHE", + "X-MSEdge-Ref": "Ref A: AC0B87B220834185ABC2A50A2F0B3247 Ref B: ASHEDGE1313 Ref C: 2022-01-26T21:26:26Z", + "X-SNR-Routing": "1" + }, + "ResponseBody": [ + "\u003Chtml\u003E\u003Chead\u003E\u003Ctitle\u003EObject moved\u003C/title\u003E\u003C/head\u003E\u003Cbody\u003E\r\n", + "\u003Ch2\u003EObject moved to \u003Ca href=\u0022https://www.bing.com:443/?toWww=1\u0026amp;redig=36D16E58BC3C46DC988D7753868E4F82\u0026amp;toWww=1\u0026amp;redig=B3FED55D25074AD2B1EFEA57D4E28847\u0026amp;toWww=1\u0026amp;redig=3542BC2407354C2B8946B4C66FCED165\u0026amp;toWww=1\u0026amp;redig=100A2DEF35DB4995858E0B5307C88DEE\u0026amp;toWww=1\u0026amp;redig=EFC20904D23348E9BE09AE4995CFADC7\u0026amp;toWww=1\u0026amp;redig=3404832CF38E41A5A730D8785F5E236A\u0026amp;toWww=1\u0026amp;redig=386345D1C6C34575B78049D25AA9B32E\u0026amp;toWww=1\u0026amp;redig=AC7B616A21A24DD9B292D9C29F5E2241\u0026amp;toWww=1\u0026amp;redig=AECF00D83B654B6E87ADD5D907FF2D96\u0026amp;toWww=1\u0026amp;redig=4B8D6950EE54455DA622D092C2461852\u0026amp;toWww=1\u0026amp;redig=8B5DCF6002804730994D8A060780C298\u0026amp;toWww=1\u0026amp;redig=817EB78E5F40451EBB80DFF0D7D61704\u0026amp;toWww=1\u0026amp;redig=BA5002CA24E743CFA3A5BE316105D264\u0026amp;toWww=1\u0026amp;redig=6048082DF9B44EBABF759817297CC027\u0026amp;toWww=1\u0026amp;redig=67A6F168987F47E481818E524A7FD608\u0026amp;toWww=1\u0026amp;redig=D57719045309432697D23569DA7781A4\u0026amp;toWww=1\u0026amp;redig=D7D685405D994F57A643BD7C7792068B\u0026amp;toWww=1\u0026amp;redig=4060BD3DCE9E41FC9C269D83601E5E84\u0026amp;toWww=1\u0026amp;redig=C0F5F7A3C76F452092438DE5B92CC49D\u0026amp;toWww=1\u0026amp;redig=4C88C00D3E3A4DD7BB483CBFA467146F\u0026amp;toWww=1\u0026amp;redig=605B790A04784A6A96C8AB3142222802\u0026amp;toWww=1\u0026amp;redig=0D9535924D3A41C8827AA3A26596A132\u0026amp;toWww=1\u0026amp;redig=EC9078B10F1449108D38D4E256414B30\u0026amp;toWww=1\u0026amp;redig=02614D8BEF064BF8822DEF7D5BC0F2BD\u0026amp;toWww=1\u0026amp;redig=A8C611933FF04D7384190EEB5B1BDF07\u0026amp;toWww=1\u0026amp;redig=96D2DFA70F2C4C1A99659C0252CEE07B\u0026amp;toWww=1\u0026amp;redig=83742D119D4244F99CE9C424AB265F36\u0026amp;toWww=1\u0026amp;redig=5A9F37CEB0744CAABCED6CED250F04CA\u0026amp;toWww=1\u0026amp;redig=461DA675C2554DE9A6737218E19DDDE9\u0026amp;toWww=1\u0026amp;redig=253A116BECEA4B7E9D6ADA85F9A7A3BE\u0026amp;toWww=1\u0026amp;redig=F7AD3071B1C745829B3E3630DCE477A6\u0026amp;toWww=1\u0026amp;redig=04D55828E7B44B07B697668840CD9DD6\u0026amp;toWww=1\u0026amp;redig=3424EE37C1DF423A8621C03918BFD456\u0026amp;toWww=1\u0026amp;redig=A33B381D37094F598285EBFD97A03327\u0026amp;toWww=1\u0026amp;redig=024BCA4BF7034FB9906CEB7D7AC0D5AA\u0026amp;toWww=1\u0026amp;redig=B03235A52A4B47EF9DCDDACD8D5606D8\u0026amp;toWww=1\u0026amp;redig=2279AF2C63A842B5BC66B84F06F46A61\u0026amp;toWww=1\u0026amp;redig=BD11B17E000640A7A37F0A208052A635\u0026amp;toWww=1\u0026amp;redig=068B2075D3E2489CADDACD43BE952D47\u0026amp;toWww=1\u0026amp;redig=05A9BCBE75FC4A2DBFC763FB5F4196E1\u0026amp;toWww=1\u0026amp;redig=6EBF752C32D04B5FB9BF092C3A8834DC\u0026amp;toWww=1\u0026amp;redig=6D64BBE8F50F4D4E9434A52526DA8C3C\u0026amp;toWww=1\u0026amp;redig=439989E7C6324EC7B2058EF5CDEC1F96\u0026amp;toWww=1\u0026amp;redig=926E3EA38A7D4A8CB7274D9D96271E25\u0026amp;toWww=1\u0026amp;redig=B2741CADB7F940B28B2549A1F86B67C1\u0026amp;toWww=1\u0026amp;redig=4F8659EAF2644F3CBDD103FAE7B8409F\u0026amp;toWww=1\u0026amp;redig=0C86A2B69CF3488585D23537BFC672C9\u0026amp;toWww=1\u0026amp;redig=62FDEE14D5FE48E7A2D63C2441AB72BF\u0026amp;toWww=1\u0026amp;redig=6EE10DB5ABC94C8B81A978780B04F4B0\u0026amp;toWww=1\u0026amp;redig=C77184D416F442A489B2610E53338EB0\u0026amp;toWww=1\u0026amp;redig=DC3E29A039B1494FA6D4D3E2917CBDA1\u0022\u003Ehere\u003C/a\u003E.\u003C/h2\u003E\r\n", + "\u003C/body\u003E\u003C/html\u003E\r\n" + ] + } + ], + "Variables": {} +}