pax_global_header00006660000000000000000000000064152140462020014506gustar00rootroot0000000000000052 comment=09a10b15e6aa5fdec2f4b2117f86985aed9cb169 golang-k8s-streaming-0.36.2/000077500000000000000000000000001521404620200155375ustar00rootroot00000000000000golang-k8s-streaming-0.36.2/.github/000077500000000000000000000000001521404620200170775ustar00rootroot00000000000000golang-k8s-streaming-0.36.2/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000002251521404620200226770ustar00rootroot00000000000000Sorry, we do not accept changes directly against this repository. Please see CONTRIBUTING.md for information on where and how to contribute instead. golang-k8s-streaming-0.36.2/CONTRIBUTING.md000066400000000000000000000013541521404620200177730ustar00rootroot00000000000000# Contributing guidelines Do not open pull requests directly against this repository, they will be ignored. Instead, please open pull requests against [kubernetes/kubernetes](https://git.k8s.io/kubernetes/). Please follow the same [contributing guide](https://git.k8s.io/kubernetes/CONTRIBUTING.md) you would follow for any other pull request made to kubernetes/kubernetes. This repository is published from [kubernetes/kubernetes/staging/src/k8s.io/cri-streaming](https://git.k8s.io/kubernetes/staging/src/k8s.io/cri-streaming) by the [kubernetes publishing-bot](https://git.k8s.io/publishing-bot). Please see [Staging Directory and Publishing](https://git.k8s.io/community/contributors/devel/sig-architecture/staging.md) for more information golang-k8s-streaming-0.36.2/LICENSE000066400000000000000000000261361521404620200165540ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. golang-k8s-streaming-0.36.2/OWNERS000066400000000000000000000002731521404620200165010ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners approvers: - aojea - dims - liggitt - seans3 reviewers: - aojea - dims - liggitt - seans3 labels: - sig/api-machinery golang-k8s-streaming-0.36.2/README.md000066400000000000000000000036771521404620200170330ustar00rootroot00000000000000> ⚠️ **This is an automatically published [staged repository](https://git.k8s.io/kubernetes/staging#external-repository-staging-area) for Kubernetes**. > Contributions, including issues and pull requests, should be made to the main Kubernetes repository: [https://github.com/kubernetes/kubernetes](https://github.com/kubernetes/kubernetes). > This repository is read-only for importing, and not used for direct contributions. > See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details. # streaming This repository contains the Kubernetes HTTP streaming transport primitives used for: - generic stream upgrade negotiation - SPDY stream connections and round-tripping - WebSocket channel streaming helpers The goal of this module is to provide a dedicated import target for transport utilities shared by CRI streaming, client-go, apiserver, and kubectl. ## Migration notes - The legacy package path `k8s.io/apimachinery/pkg/util/httpstream` was intentionally removed as part of this extraction. - Consumers must migrate imports to: - `k8s.io/streaming/pkg/httpstream` - `k8s.io/streaming/pkg/httpstream/spdy` - `k8s.io/streaming/pkg/httpstream/wsstream` - This extraction does not provide compatibility shims at the old apimachinery path. ## Community, discussion, contribution, and support streaming is maintained as part of [SIG API Machinery](https://github.com/kubernetes/community/tree/master/sig-api-machinery) and [SIG Node](https://github.com/kubernetes/community/tree/master/sig-node) areas. You can reach maintainers of this project at: - Slack: [#sig-node](https://kubernetes.slack.com/messages/sig-node) - Mailing List: [kubernetes-sig-node](https://groups.google.com/forum/#!forum/kubernetes-sig-node) Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/). ### Code of conduct Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). golang-k8s-streaming-0.36.2/SECURITY_CONTACTS000066400000000000000000000010631521404620200202270ustar00rootroot00000000000000# Defined below are the security contacts for this repo. # # They are the contact point for the Product Security Committee to reach out # to for triaging and handling of incoming issues. # # The below names agree to abide by the # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) # and will be removed and replaced if they violate that agreement. # # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE # INSTRUCTIONS AT https://kubernetes.io/security/ cjcullen joelsmith liggitt philips tallclair golang-k8s-streaming-0.36.2/code-of-conduct.md000066400000000000000000000002241521404620200210300ustar00rootroot00000000000000# Kubernetes Community Code of Conduct Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) golang-k8s-streaming-0.36.2/doc.go000066400000000000000000000012541521404620200166350ustar00rootroot00000000000000/* Copyright The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package streaming contains the staged module root for Kubernetes transport streaming primitives. package streaming golang-k8s-streaming-0.36.2/go.mod000066400000000000000000000005661521404620200166540ustar00rootroot00000000000000// This is a generated file. Do not edit directly. module k8s.io/streaming go 1.26.0 godebug default=go1.26 require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/moby/spdystream v0.5.1 golang.org/x/net v0.49.0 k8s.io/klog/v2 v2.140.0 k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 ) require github.com/go-logr/logr v1.4.3 // indirect golang-k8s-streaming-0.36.2/go.sum000066400000000000000000000020521521404620200166710ustar00rootroot00000000000000github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= golang-k8s-streaming-0.36.2/pkg/000077500000000000000000000000001521404620200163205ustar00rootroot00000000000000golang-k8s-streaming-0.36.2/pkg/httpstream/000077500000000000000000000000001521404620200205135ustar00rootroot00000000000000golang-k8s-streaming-0.36.2/pkg/httpstream/doc.go000066400000000000000000000013021521404620200216030ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package httpstream adds multiplexed streaming support to HTTP requests and // responses via connection upgrades. package httpstream golang-k8s-streaming-0.36.2/pkg/httpstream/httpstream.go000066400000000000000000000164621521404620200232460ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package httpstream import ( "errors" "fmt" "io" "net/http" "strings" "time" ) const ( HeaderConnection = "Connection" HeaderUpgrade = "Upgrade" HeaderProtocolVersion = "X-Stream-Protocol-Version" HeaderAcceptedProtocolVersions = "X-Accepted-Stream-Protocol-Versions" ) // NewStreamHandler defines a function that is called when a new Stream is // received. If no error is returned, the Stream is accepted; otherwise, // the stream is rejected. After the reply frame has been sent, replySent is closed. type NewStreamHandler func(stream Stream, replySent <-chan struct{}) error // NoOpNewStreamHandler is a stream handler that accepts a new stream and // performs no other logic. func NoOpNewStreamHandler(stream Stream, replySent <-chan struct{}) error { return nil } // Dialer knows how to open a streaming connection to a server. type Dialer interface { // Dial opens a streaming connection to a server using one of the protocols // specified (in order of most preferred to least preferred). Dial(protocols ...string) (Connection, string, error) } // UpgradeRoundTripper is a type of http.RoundTripper that is able to upgrade // HTTP requests to support multiplexed bidirectional streams. After RoundTrip() // is invoked, if the upgrade is successful, clients may retrieve the upgraded // connection by calling UpgradeRoundTripper.Connection(). type UpgradeRoundTripper interface { http.RoundTripper // NewConnection validates the response and creates a new Connection. NewConnection(resp *http.Response) (Connection, error) } // ResponseUpgrader knows how to upgrade HTTP requests and responses to // add streaming support to them. type ResponseUpgrader interface { // UpgradeResponse upgrades an HTTP response to one that supports multiplexed // streams. newStreamHandler will be called asynchronously whenever the // other end of the upgraded connection creates a new stream. UpgradeResponse(w http.ResponseWriter, req *http.Request, newStreamHandler NewStreamHandler) Connection } // Connection represents an upgraded HTTP connection. type Connection interface { // CreateStream creates a new Stream with the supplied headers. CreateStream(headers http.Header) (Stream, error) // Close resets all streams and closes the connection. Close() error // CloseChan returns a channel that is closed when the underlying connection is closed. CloseChan() <-chan bool // SetIdleTimeout sets the amount of time the connection may remain idle before // it is automatically closed. SetIdleTimeout(timeout time.Duration) // RemoveStreams can be used to remove a set of streams from the Connection. RemoveStreams(streams ...Stream) } // Stream represents a bidirectional communications channel that is part of an // upgraded connection. type Stream interface { io.ReadWriteCloser // Reset closes both directions of the stream, indicating that neither client // or server can use it any more. Reset() error // Headers returns the headers used to create the stream. Headers() http.Header // Identifier returns the stream's ID. Identifier() uint32 } // UpgradeFailureError encapsulates the cause for why the streaming // upgrade request failed. Implements error interface. type UpgradeFailureError struct { Cause error } func (u *UpgradeFailureError) Error() string { return fmt.Sprintf("unable to upgrade streaming request: %s", u.Cause) } // IsUpgradeFailure returns true if the passed error is (or wrapped error contains) // the UpgradeFailureError. func IsUpgradeFailure(err error) bool { if err == nil { return false } var upgradeErr *UpgradeFailureError return errors.As(err, &upgradeErr) } // isHTTPSProxyError returns true if error is Gorilla/Websockets HTTPS Proxy dial error; // false otherwise (see https://github.com/kubernetes/kubernetes/issues/126134). func IsHTTPSProxyError(err error) bool { if err == nil { return false } return strings.Contains(err.Error(), "proxy: unknown scheme: https") } // IsUpgradeRequest returns true if the given request is a connection upgrade request func IsUpgradeRequest(req *http.Request) bool { for _, h := range req.Header[http.CanonicalHeaderKey(HeaderConnection)] { if strings.Contains(strings.ToLower(h), strings.ToLower(HeaderUpgrade)) { return true } } return false } func negotiateProtocol(clientProtocols, serverProtocols []string) string { for i := range clientProtocols { for j := range serverProtocols { if clientProtocols[i] == serverProtocols[j] { return clientProtocols[i] } } } return "" } func commaSeparatedHeaderValues(header []string) []string { var parsedClientProtocols []string for i := range header { for _, clientProtocol := range strings.Split(header[i], ",") { if proto := strings.Trim(clientProtocol, " "); len(proto) > 0 { parsedClientProtocols = append(parsedClientProtocols, proto) } } } return parsedClientProtocols } // Handshake performs a subprotocol negotiation. If the client did request a // subprotocol, Handshake will select the first common value found in // serverProtocols, otherwise it will return an error and write an HTTP BadRequest to the response. // If a match is found, Handshake adds a response header indicating the chosen subprotocol. // If no match is found, HTTP forbidden is returned, along with a response header containing // the list of protocols the server can accept. func Handshake(req *http.Request, w http.ResponseWriter, serverProtocols []string) (string, error) { if len(serverProtocols) == 0 { panic(fmt.Errorf("unable to upgrade: serverProtocols is required")) } values, ok := req.Header[http.CanonicalHeaderKey(HeaderProtocolVersion)] if !ok { err := fmt.Errorf("unable to upgrade: header %s does not exist in request with %d headers", HeaderProtocolVersion, len(req.Header)) http.Error(w, err.Error(), http.StatusBadRequest) return "", err } if len(values) == 0 { err := fmt.Errorf("unable to upgrade: header %s is empty", HeaderProtocolVersion) http.Error(w, err.Error(), http.StatusBadRequest) return "", err } clientProtocols := commaSeparatedHeaderValues(values) if len(clientProtocols) == 0 { err := fmt.Errorf("unable to upgrade: header %s contains %s, but no valid protocols", HeaderProtocolVersion, values) http.Error(w, err.Error(), http.StatusBadRequest) return "", err } negotiatedProtocol := negotiateProtocol(clientProtocols, serverProtocols) if len(negotiatedProtocol) == 0 { for i := range serverProtocols { w.Header().Add(HeaderAcceptedProtocolVersions, serverProtocols[i]) } err := fmt.Errorf("unable to upgrade: unable to negotiate protocol: client supports %v, server accepts %v", clientProtocols, serverProtocols) http.Error(w, err.Error(), http.StatusForbidden) return "", err } w.Header().Add(HeaderProtocolVersion, negotiatedProtocol) return negotiatedProtocol, nil } golang-k8s-streaming-0.36.2/pkg/httpstream/httpstream_test.go000066400000000000000000000143001521404620200242720ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package httpstream import ( "errors" "fmt" "net/http" "reflect" "testing" ) type responseWriter struct { header http.Header statusCode *int } func newResponseWriter() *responseWriter { return &responseWriter{ header: make(http.Header), } } func (r *responseWriter) Header() http.Header { return r.header } func (r *responseWriter) WriteHeader(code int) { r.statusCode = &code } func (r *responseWriter) Write([]byte) (int, error) { return 0, nil } func TestHandshake(t *testing.T) { tests := map[string]struct { clientProtocols []string serverProtocols []string expectedProtocol string expectError bool expectedStatusCode int }{ "no protocol": { clientProtocols: []string{}, serverProtocols: []string{"a", "b"}, expectedProtocol: "", expectError: true, expectedStatusCode: http.StatusBadRequest, }, "empty client protocol header": { clientProtocols: []string{",, "}, serverProtocols: []string{"a", "b"}, expectedProtocol: "", expectError: true, expectedStatusCode: http.StatusBadRequest, }, "no common protocol": { clientProtocols: []string{"c"}, serverProtocols: []string{"a", "b"}, expectedProtocol: "", expectError: true, expectedStatusCode: http.StatusForbidden, }, "no common protocol with comma separated list": { clientProtocols: []string{"c, d"}, serverProtocols: []string{"a", "b"}, expectedProtocol: "", expectError: true, expectedStatusCode: http.StatusForbidden, }, "common protocol": { clientProtocols: []string{"b"}, serverProtocols: []string{"a", "b"}, expectedProtocol: "b", }, "common protocol with comma separated list": { clientProtocols: []string{"b, c"}, serverProtocols: []string{"a", "b"}, expectedProtocol: "b", }, } for name, test := range tests { t.Run(name, func(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "http://www.example.com/", nil) if err != nil { t.Fatalf("%s: error creating request: %v", name, err) } for _, p := range test.clientProtocols { req.Header.Add(HeaderProtocolVersion, p) } w := newResponseWriter() negotiated, err := Handshake(req, w, test.serverProtocols) // verify negotiated protocol if e, a := test.expectedProtocol, negotiated; e != a { t.Fatalf("%s: protocol: expected %q, got %q", name, e, a) } if test.expectError { if err == nil { t.Fatalf("%s: expected error but did not get one", name) } if w.statusCode == nil { t.Fatalf("%s: expected w.statusCode to be set", name) } else if e, a := test.expectedStatusCode, *w.statusCode; e != a { t.Fatalf("%s: w.statusCode: expected %d, got %d", name, e, a) } if test.expectedStatusCode == http.StatusForbidden { if e, a := test.serverProtocols, w.Header()[HeaderAcceptedProtocolVersions]; !reflect.DeepEqual(e, a) { t.Fatalf("%s: accepted server protocols: expected %v, got %v", name, e, a) } } return } if !test.expectError && err != nil { t.Fatalf("%s: unexpected error: %v", name, err) } if w.statusCode != nil { t.Fatalf("%s: unexpected non-nil w.statusCode: %d", name, w.statusCode) } if len(test.expectedProtocol) == 0 { if len(w.Header()[HeaderProtocolVersion]) > 0 { t.Fatalf("%s: unexpected protocol version response header: %s", name, w.Header()[HeaderProtocolVersion]) } return } // verify response headers if e, a := []string{test.expectedProtocol}, w.Header()[HeaderProtocolVersion]; !reflect.DeepEqual(e, a) { t.Fatalf("%s: protocol response header: expected %v, got %v", name, e, a) } }) } t.Run("empty server protocols should panic", func(t *testing.T) { defer func() { if r := recover(); r == nil { t.Errorf("The code did not panic") } }() req, _ := http.NewRequest(http.MethodGet, "http://www.example.com/", nil) req.Header.Add(HeaderProtocolVersion, "a") w := newResponseWriter() _, _ = Handshake(req, w, []string{}) }) } func TestIsUpgradeFailureError(t *testing.T) { testCases := map[string]struct { err error expected bool }{ "nil error should return false": { err: nil, expected: false, }, "Non-upgrade error should return false": { err: fmt.Errorf("this is not an upgrade error"), expected: false, }, "UpgradeFailure error should return true": { err: &UpgradeFailureError{}, expected: true, }, "Wrapped Non-UpgradeFailure error should return false": { err: fmt.Errorf("%s: %w", "first error", errors.New("Non-upgrade error")), expected: false, }, "Wrapped UpgradeFailure error should return true": { err: fmt.Errorf("%s: %w", "first error", &UpgradeFailureError{}), expected: true, }, } for name, test := range testCases { t.Run(name, func(t *testing.T) { actual := IsUpgradeFailure(test.err) if test.expected != actual { t.Errorf("expected upgrade failure %t, got %t", test.expected, actual) } }) } } func TestIsHTTPSProxyError(t *testing.T) { testCases := map[string]struct { err error expected bool }{ "nil error should return false": { err: nil, expected: false, }, "Not HTTPS proxy error should return false": { err: errors.New("this is not an upgrade error"), expected: false, }, "HTTPS proxy error should return true": { err: errors.New("proxy: unknown scheme: https"), expected: true, }, } for name, test := range testCases { t.Run(name, func(t *testing.T) { actual := IsHTTPSProxyError(test.err) if test.expected != actual { t.Errorf("expected HTTPS proxy error %t, got %t", test.expected, actual) } }) } } golang-k8s-streaming-0.36.2/pkg/httpstream/spdy/000077500000000000000000000000001521404620200214725ustar00rootroot00000000000000golang-k8s-streaming-0.36.2/pkg/httpstream/spdy/connection.go000066400000000000000000000154761521404620200241750ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package spdy import ( "net" "net/http" "sync" "time" "github.com/moby/spdystream" "k8s.io/klog/v2" "k8s.io/streaming/pkg/httpstream" ) // connection maintains state about a spdystream.Connection and its associated // streams. type connection struct { conn *spdystream.Connection streams map[uint32]httpstream.Stream streamLock sync.Mutex newStreamHandler httpstream.NewStreamHandler ping func() (time.Duration, error) } // NewClientConnection creates a new SPDY client connection. func NewClientConnection(conn net.Conn) (httpstream.Connection, error) { return NewClientConnectionWithPings(conn, 0) } // NewClientConnectionWithPings creates a new SPDY client connection. // // If pingPeriod is non-zero, a background goroutine will send periodic Ping // frames to the server. Use this to keep idle connections through certain load // balancers alive longer. func NewClientConnectionWithPings(conn net.Conn, pingPeriod time.Duration) (httpstream.Connection, error) { spdyConn, err := spdystream.NewConnection(conn, false) if err != nil { defer conn.Close() return nil, err } return newConnection(spdyConn, httpstream.NoOpNewStreamHandler, pingPeriod, spdyConn.Ping), nil } // NewServerConnection creates a new SPDY server connection. newStreamHandler // will be invoked when the server receives a newly created stream from the // client. func NewServerConnection(conn net.Conn, newStreamHandler httpstream.NewStreamHandler) (httpstream.Connection, error) { return NewServerConnectionWithPings(conn, newStreamHandler, 0) } // NewServerConnectionWithPings creates a new SPDY server connection. // newStreamHandler will be invoked when the server receives a newly created // stream from the client. // // If pingPeriod is non-zero, a background goroutine will send periodic Ping // frames to the server. Use this to keep idle connections through certain load // balancers alive longer. func NewServerConnectionWithPings(conn net.Conn, newStreamHandler httpstream.NewStreamHandler, pingPeriod time.Duration) (httpstream.Connection, error) { spdyConn, err := spdystream.NewConnection(conn, true) if err != nil { defer conn.Close() return nil, err } return newConnection(spdyConn, newStreamHandler, pingPeriod, spdyConn.Ping), nil } // newConnection returns a new connection wrapping conn. newStreamHandler // will be invoked when the server receives a newly created stream from the // client. func newConnection(conn *spdystream.Connection, newStreamHandler httpstream.NewStreamHandler, pingPeriod time.Duration, pingFn func() (time.Duration, error)) httpstream.Connection { c := &connection{ conn: conn, newStreamHandler: newStreamHandler, ping: pingFn, streams: make(map[uint32]httpstream.Stream), } go conn.Serve(c.newSpdyStream) if pingPeriod > 0 && pingFn != nil { go c.sendPings(pingPeriod) } return c } // createStreamResponseTimeout indicates how long to wait for the other side to // acknowledge the new stream before timing out. const createStreamResponseTimeout = 30 * time.Second // Close first sends a reset for all of the connection's streams, and then // closes the underlying spdystream.Connection. func (c *connection) Close() error { c.streamLock.Lock() for _, s := range c.streams { // calling Reset instead of Close ensures that all streams are fully torn down s.Reset() } c.streams = make(map[uint32]httpstream.Stream, 0) c.streamLock.Unlock() // now that all streams are fully torn down, it's safe to call close on the underlying connection, // which should be able to terminate immediately at this point, instead of waiting for any // remaining graceful stream termination. return c.conn.Close() } // RemoveStreams can be used to removes a set of streams from the Connection. func (c *connection) RemoveStreams(streams ...httpstream.Stream) { c.streamLock.Lock() for _, stream := range streams { // It may be possible that the provided stream is nil if timed out. if stream != nil { delete(c.streams, stream.Identifier()) } } c.streamLock.Unlock() } // CreateStream creates a new stream with the specified headers and registers // it with the connection. func (c *connection) CreateStream(headers http.Header) (httpstream.Stream, error) { stream, err := c.conn.CreateStream(headers, nil, false) if err != nil { return nil, err } if err = stream.WaitTimeout(createStreamResponseTimeout); err != nil { return nil, err } c.registerStream(stream) return stream, nil } // registerStream adds the stream s to the connection's list of streams that // it owns. func (c *connection) registerStream(s httpstream.Stream) { c.streamLock.Lock() c.streams[s.Identifier()] = s c.streamLock.Unlock() } // CloseChan returns a channel that, when closed, indicates that the underlying // spdystream.Connection has been closed. func (c *connection) CloseChan() <-chan bool { return c.conn.CloseChan() } // newSpdyStream is the internal new stream handler used by spdystream.Connection.Serve. // It calls connection's newStreamHandler, giving it the opportunity to accept or reject // the stream. If newStreamHandler returns an error, the stream is rejected. If not, the // stream is accepted and registered with the connection. func (c *connection) newSpdyStream(stream *spdystream.Stream) { replySent := make(chan struct{}) err := c.newStreamHandler(stream, replySent) rejectStream := (err != nil) if rejectStream { //nolint:logcheck // Hopefully this never gets triggered. klog.Warningf("Stream rejected: %v", err) stream.Reset() return } c.registerStream(stream) stream.SendReply(http.Header{}, rejectStream) close(replySent) } // SetIdleTimeout sets the amount of time the connection may remain idle before // it is automatically closed. func (c *connection) SetIdleTimeout(timeout time.Duration) { c.conn.SetIdleTimeout(timeout) } func (c *connection) sendPings(period time.Duration) { t := time.NewTicker(period) defer t.Stop() for { select { case <-c.conn.CloseChan(): return case <-t.C: } if _, err := c.ping(); err != nil { //nolint:logcheck // Hopefully this never gets triggered. klog.V(3).Infof("SPDY Ping failed: %v", err) // Continue, in case this is a transient failure. // c.conn.CloseChan above will tell us when the connection is // actually closed. } } } golang-k8s-streaming-0.36.2/pkg/httpstream/spdy/connection_test.go000066400000000000000000000176041521404620200252270ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package spdy import ( "fmt" "io" "net" "net/http" "sync" "sync/atomic" "testing" "time" "github.com/moby/spdystream" "k8s.io/streaming/pkg/httpstream" ) func runProxy(t *testing.T, backendUrl string, proxyUrl chan<- string, proxyDone chan<- struct{}, errCh chan<- error) { listener, err := net.Listen("tcp4", "localhost:0") if err != nil { errCh <- err return } defer listener.Close() proxyUrl <- listener.Addr().String() clientConn, err := listener.Accept() if err != nil { t.Errorf("proxy: error accepting client connection: %v", err) return } backendConn, err := net.Dial("tcp4", backendUrl) if err != nil { t.Errorf("proxy: error dialing backend: %v", err) return } defer backendConn.Close() var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() io.Copy(backendConn, clientConn) }() go func() { defer wg.Done() io.Copy(clientConn, backendConn) }() wg.Wait() proxyDone <- struct{}{} } func runServer(t *testing.T, backendUrl chan<- string, serverDone chan<- struct{}, errCh chan<- error) { listener, err := net.Listen("tcp4", "localhost:0") if err != nil { errCh <- err return } defer listener.Close() backendUrl <- listener.Addr().String() conn, err := listener.Accept() if err != nil { t.Errorf("server: error accepting connection: %v", err) return } streamChan := make(chan httpstream.Stream) replySentChan := make(chan (<-chan struct{})) spdyConn, err := NewServerConnection(conn, func(stream httpstream.Stream, replySent <-chan struct{}) error { streamChan <- stream replySentChan <- replySent return nil }) if err != nil { t.Errorf("server: error creating spdy connection: %v", err) return } stream := <-streamChan replySent := <-replySentChan <-replySent buf := make([]byte, 1) _, err = stream.Read(buf) if err != io.EOF { t.Errorf("server: unexpected read error: %v", err) return } <-spdyConn.CloseChan() raw := spdyConn.(*connection).conn if err := raw.Wait(15 * time.Second); err != nil { t.Errorf("server: timed out waiting for connection closure: %v", err) } serverDone <- struct{}{} } func TestConnectionCloseIsImmediateThroughAProxy(t *testing.T) { errCh := make(chan error) serverDone := make(chan struct{}, 1) backendUrlChan := make(chan string) go runServer(t, backendUrlChan, serverDone, errCh) var backendUrl string select { case err := <-errCh: t.Fatalf("server: error listening: %v", err) case backendUrl = <-backendUrlChan: } proxyDone := make(chan struct{}, 1) proxyUrlChan := make(chan string) go runProxy(t, backendUrl, proxyUrlChan, proxyDone, errCh) var proxyUrl string select { case err := <-errCh: t.Fatalf("error listening: %v", err) case proxyUrl = <-proxyUrlChan: } conn, err := net.Dial("tcp4", proxyUrl) if err != nil { t.Fatalf("client: error connecting to proxy: %v", err) } spdyConn, err := NewClientConnection(conn) if err != nil { t.Fatalf("client: error creating spdy connection: %v", err) } if _, err := spdyConn.CreateStream(http.Header{}); err != nil { t.Fatalf("client: error creating stream: %v", err) } spdyConn.Close() raw := spdyConn.(*connection).conn if err := raw.Wait(15 * time.Second); err != nil { t.Fatalf("client: timed out waiting for connection closure: %v", err) } expired := time.NewTimer(15 * time.Second) defer expired.Stop() i := 0 for { select { case <-expired.C: t.Fatalf("timed out waiting for proxy and/or server closure") case <-serverDone: i++ case <-proxyDone: i++ } if i == 2 { break } } } func TestConnectionPings(t *testing.T) { const pingPeriod = 10 * time.Millisecond timeout := time.After(10 * time.Second) // Set up server connection. listener, err := net.Listen("tcp4", "localhost:0") if err != nil { t.Fatal(err) } defer listener.Close() srvErr := make(chan error, 1) go func() { defer close(srvErr) srvConn, err := listener.Accept() if err != nil { srvErr <- fmt.Errorf("server: error accepting connection: %v", err) return } defer srvConn.Close() spdyConn, err := spdystream.NewConnection(srvConn, true) if err != nil { srvErr <- fmt.Errorf("server: error creating spdy connection: %v", err) return } var pingsSent int64 srvSPDYConn := newConnection( spdyConn, func(stream httpstream.Stream, replySent <-chan struct{}) error { // Echo all the incoming data. go io.Copy(stream, stream) return nil }, pingPeriod, func() (time.Duration, error) { atomic.AddInt64(&pingsSent, 1) return 0, nil }) defer srvSPDYConn.Close() // Wait for the connection to close, to prevent defers from running // early. select { case <-timeout: srvErr <- fmt.Errorf("server: timeout waiting for connection to close") return case <-srvSPDYConn.CloseChan(): } // Count pings sent by the server. gotPings := atomic.LoadInt64(&pingsSent) if gotPings < 1 { t.Errorf("server: failed to send any pings (check logs)") } }() // Set up client connection. clConn, err := net.Dial("tcp4", listener.Addr().String()) if err != nil { t.Fatalf("client: error connecting to proxy: %v", err) } defer clConn.Close() clSPDYConn, err := NewClientConnection(clConn) if err != nil { t.Fatalf("client: error creating spdy connection: %v", err) } defer clSPDYConn.Close() start := time.Now() clSPDYStream, err := clSPDYConn.CreateStream(http.Header{}) if err != nil { t.Fatalf("client: error creating stream: %v", err) } defer clSPDYStream.Close() // Send some data both ways, to make sure pings don't interfere with // regular messages. in := "foo" if _, err := fmt.Fprintln(clSPDYStream, in); err != nil { t.Fatalf("client: error writing data to stream: %v", err) } var out string if _, err := fmt.Fscanln(clSPDYStream, &out); err != nil { t.Fatalf("client: error reading data from stream: %v", err) } if in != out { t.Errorf("client: received data doesn't match sent data: got %q, want %q", out, in) } // Wait for at least 2 pings to get sent each way before closing the // connection. elapsed := time.Since(start) if elapsed < 3*pingPeriod { time.Sleep(3*pingPeriod - elapsed) } clSPDYConn.Close() select { case err, ok := <-srvErr: if ok && err != nil { t.Error(err) } case <-timeout: t.Errorf("timed out waiting for server to exit") } } type fakeStream struct{ id uint32 } func (*fakeStream) Read(p []byte) (int, error) { return 0, nil } func (*fakeStream) Write(p []byte) (int, error) { return 0, nil } func (*fakeStream) Close() error { return nil } func (*fakeStream) Reset() error { return nil } func (*fakeStream) Headers() http.Header { return nil } func (f *fakeStream) Identifier() uint32 { return f.id } func TestConnectionRemoveStreams(t *testing.T) { c := &connection{streams: make(map[uint32]httpstream.Stream)} stream0 := &fakeStream{id: 0} stream1 := &fakeStream{id: 1} stream2 := &fakeStream{id: 2} c.registerStream(stream0) c.registerStream(stream1) if len(c.streams) != 2 { t.Fatalf("should have two streams, has %d", len(c.streams)) } // not exists c.RemoveStreams(stream2) if len(c.streams) != 2 { t.Fatalf("should have two streams, has %d", len(c.streams)) } // remove all existing c.RemoveStreams(stream0, stream1) // remove nil stream should not crash c.RemoveStreams(nil) if len(c.streams) != 0 { t.Fatalf("should not have any streams, has %d", len(c.streams)) } } golang-k8s-streaming-0.36.2/pkg/httpstream/spdy/roundtripper.go000066400000000000000000000407271521404620200245700ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package spdy import ( "bufio" "context" "crypto/tls" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net" "net/http" "net/http/httputil" "net/url" "os" "strings" "time" "golang.org/x/net/proxy" "k8s.io/streaming/pkg/httpstream" utilnet "k8s.io/utils/net" ) // SpdyRoundTripper knows how to upgrade an HTTP request to one that supports // multiplexed streams. After RoundTrip() is invoked, Conn will be set // and usable. SpdyRoundTripper implements the UpgradeRoundTripper interface. type SpdyRoundTripper struct { //tlsConfig holds the TLS configuration settings to use when connecting //to the remote server. tlsConfig *tls.Config /* TODO according to http://golang.org/pkg/net/http/#RoundTripper, a RoundTripper must be safe for use by multiple concurrent goroutines. If this is absolutely necessary, we could keep a map from http.Request to net.Conn. In practice, a client will create an http.Client, set the transport to a new insteace of SpdyRoundTripper, and use it a single time, so this hopefully won't be an issue. */ // conn is the underlying network connection to the remote server. conn net.Conn // Dialer is the dialer used to connect. Used if non-nil. Dialer *net.Dialer // proxier knows which proxy to use given a request, defaults to a proxier that // preserves NO_PROXY CIDR behavior while delegating to http.ProxyFromEnvironment. // Used primarily for mocking the proxy discovery in tests. proxier func(req *http.Request) (*url.URL, error) // pingPeriod is a period for sending Ping frames over established // connections. pingPeriod time.Duration // upgradeTransport is an optional substitute for dialing if present. This field is // mutually exclusive with the "tlsConfig", "Dialer", and "proxier". upgradeTransport http.RoundTripper } type tlsClientConfigHolder interface { TLSClientConfig() *tls.Config } type roundTripperWrapper interface { http.RoundTripper WrappedRoundTripper() http.RoundTripper } type dialFunc func(ctx context.Context, network, addr string) (net.Conn, error) var _ tlsClientConfigHolder = &SpdyRoundTripper{} var _ httpstream.UpgradeRoundTripper = &SpdyRoundTripper{} // NewRoundTripper creates a new SpdyRoundTripper that will use the specified // tlsConfig. func NewRoundTripper(tlsConfig *tls.Config) (*SpdyRoundTripper, error) { return NewRoundTripperWithConfig(RoundTripperConfig{ TLS: tlsConfig, UpgradeTransport: nil, }) } // NewRoundTripperWithProxy creates a new SpdyRoundTripper that will use the // specified tlsConfig and proxy func. func NewRoundTripperWithProxy(tlsConfig *tls.Config, proxier func(*http.Request) (*url.URL, error)) (*SpdyRoundTripper, error) { return NewRoundTripperWithConfig(RoundTripperConfig{ TLS: tlsConfig, Proxier: proxier, UpgradeTransport: nil, }) } // NewRoundTripperWithConfig creates a new SpdyRoundTripper with the specified // configuration. Returns an error if the SpdyRoundTripper is misconfigured. func NewRoundTripperWithConfig(cfg RoundTripperConfig) (*SpdyRoundTripper, error) { // Process UpgradeTransport, which is mutually exclusive to TLSConfig and Proxier. if cfg.UpgradeTransport != nil { if cfg.TLS != nil || cfg.Proxier != nil { return nil, fmt.Errorf("SpdyRoundTripper: UpgradeTransport is mutually exclusive to TLSConfig or Proxier") } tlsConfig, err := tlsConfigForTransport(cfg.UpgradeTransport) if err != nil { return nil, fmt.Errorf("SpdyRoundTripper: unable to retrieve TLS config from UpgradeTransport: %w", err) } cfg.TLS = tlsConfig } if cfg.Proxier == nil { cfg.Proxier = newProxierWithNoProxyCIDR(http.ProxyFromEnvironment) } return &SpdyRoundTripper{ tlsConfig: cfg.TLS, proxier: cfg.Proxier, pingPeriod: cfg.PingPeriod, upgradeTransport: cfg.UpgradeTransport, }, nil } // newProxierWithNoProxyCIDR preserves CIDR matching in NO_PROXY/no_proxy while // delegating all other behavior to the supplied proxy function. func newProxierWithNoProxyCIDR(delegate func(req *http.Request) (*url.URL, error)) func(req *http.Request) (*url.URL, error) { noProxyEnv := os.Getenv("NO_PROXY") if noProxyEnv == "" { noProxyEnv = os.Getenv("no_proxy") } noProxyRules := strings.Split(noProxyEnv, ",") cidrs := make([]*net.IPNet, 0, len(noProxyRules)) for _, noProxyRule := range noProxyRules { noProxyRule = strings.TrimSpace(noProxyRule) if noProxyRule == "" { continue } _, cidr, err := utilnet.ParseCIDRSloppy(noProxyRule) if err == nil { cidrs = append(cidrs, cidr) } } if len(cidrs) == 0 { return delegate } return func(req *http.Request) (*url.URL, error) { ip := utilnet.ParseIPSloppy(req.URL.Hostname()) if ip == nil { return delegate(req) } for _, cidr := range cidrs { if cidr.Contains(ip) { return nil, nil } } return delegate(req) } } // RoundTripperConfig is a set of options for an SpdyRoundTripper. type RoundTripperConfig struct { // TLS configuration used by the round tripper if UpgradeTransport not present. TLS *tls.Config // Proxier is a proxy function invoked on each request. Optional. Proxier func(*http.Request) (*url.URL, error) // PingPeriod is a period for sending SPDY Pings on the connection. // Optional. PingPeriod time.Duration // UpgradeTransport is a subtitute transport used for dialing. If set, // this field will be used instead of "TLS" and "Proxier" for connection creation. // Optional. UpgradeTransport http.RoundTripper } // TLSClientConfig implements pkg/util/net.TLSClientConfigHolder for proper TLS checking during // proxying with a spdy roundtripper. func (s *SpdyRoundTripper) TLSClientConfig() *tls.Config { return s.tlsConfig } // Dial opens a network connection for an upgrade request. func (s *SpdyRoundTripper) Dial(req *http.Request) (net.Conn, error) { var conn net.Conn var err error if s.upgradeTransport != nil { conn, err = dialURLWithTransport(req.Context(), req.URL, s.upgradeTransport) } else { conn, err = s.dial(req) } if err != nil { return nil, err } if err := req.Write(conn); err != nil { conn.Close() return nil, err } return conn, nil } // dial dials the host specified by req, using TLS if appropriate, optionally // using a proxy server if one is configured via environment variables. func (s *SpdyRoundTripper) dial(req *http.Request) (net.Conn, error) { proxyURL, err := s.proxier(req) if err != nil { return nil, err } if proxyURL == nil { return s.dialWithoutProxy(req.Context(), req.URL) } switch proxyURL.Scheme { case "socks5": return s.dialWithSocks5Proxy(req, proxyURL) case "https", "http", "": return s.dialWithHttpProxy(req, proxyURL) } return nil, fmt.Errorf("proxy URL scheme not supported: %s", proxyURL.Scheme) } // dialWithHttpProxy dials the host specified by url through an http or an https proxy. func (s *SpdyRoundTripper) dialWithHttpProxy(req *http.Request, proxyURL *url.URL) (net.Conn, error) { // ensure we use a canonical host with proxyReq targetHost := canonicalAddr(req.URL) // proxying logic adapted from http://blog.h6t.eu/post/74098062923/golang-websocket-with-http-proxy-support proxyReq := http.Request{ Method: http.MethodConnect, URL: &url.URL{}, Host: targetHost, } proxyReq = *proxyReq.WithContext(req.Context()) if pa := s.proxyAuth(proxyURL); pa != "" { proxyReq.Header = http.Header{} proxyReq.Header.Set("Proxy-Authorization", pa) } proxyDialConn, err := s.dialWithoutProxy(proxyReq.Context(), proxyURL) if err != nil { return nil, err } //nolint:staticcheck // SA1019 ignore deprecated httputil.NewProxyClientConn proxyClientConn := httputil.NewProxyClientConn(proxyDialConn, nil) response, err := proxyClientConn.Do(&proxyReq) //nolint:staticcheck // SA1019 ignore deprecated httputil.ErrPersistEOF: it might be // returned from the invocation of proxyClientConn.Do if err != nil && err != httputil.ErrPersistEOF { return nil, err } if response != nil && response.StatusCode >= 300 || response.StatusCode < 200 { return nil, fmt.Errorf("CONNECT request to %s returned response: %s", proxyURL.Redacted(), response.Status) } rwc, _ := proxyClientConn.Hijack() if req.URL.Scheme == "https" { return s.tlsConn(proxyReq.Context(), rwc, targetHost) } return rwc, nil } // dialWithSocks5Proxy dials the host specified by url through a socks5 proxy. func (s *SpdyRoundTripper) dialWithSocks5Proxy(req *http.Request, proxyURL *url.URL) (net.Conn, error) { // ensure we use a canonical host with proxyReq targetHost := canonicalAddr(req.URL) proxyDialAddr := canonicalAddr(proxyURL) var auth *proxy.Auth if proxyURL.User != nil { pass, _ := proxyURL.User.Password() auth = &proxy.Auth{ User: proxyURL.User.Username(), Password: pass, } } dialer := s.Dialer if dialer == nil { dialer = &net.Dialer{ Timeout: 30 * time.Second, } } proxyDialer, err := proxy.SOCKS5("tcp", proxyDialAddr, auth, dialer) if err != nil { return nil, err } // According to the implementation of proxy.SOCKS5, the type assertion will always succeed contextDialer, ok := proxyDialer.(proxy.ContextDialer) if !ok { return nil, errors.New("SOCKS5 Dialer must implement ContextDialer") } proxyDialConn, err := contextDialer.DialContext(req.Context(), "tcp", targetHost) if err != nil { return nil, err } if req.URL.Scheme == "https" { return s.tlsConn(req.Context(), proxyDialConn, targetHost) } return proxyDialConn, nil } // tlsConn returns a TLS client side connection using rwc as the underlying transport. func (s *SpdyRoundTripper) tlsConn(ctx context.Context, rwc net.Conn, targetHost string) (net.Conn, error) { host, _, err := net.SplitHostPort(targetHost) if err != nil { return nil, err } tlsConfig := s.tlsConfig switch { case tlsConfig == nil: tlsConfig = &tls.Config{ServerName: host} case len(tlsConfig.ServerName) == 0: tlsConfig = tlsConfig.Clone() tlsConfig.ServerName = host } tlsConn := tls.Client(rwc, tlsConfig) if err := tlsConn.HandshakeContext(ctx); err != nil { tlsConn.Close() return nil, err } return tlsConn, nil } // dialWithoutProxy dials the host specified by url, using TLS if appropriate. func (s *SpdyRoundTripper) dialWithoutProxy(ctx context.Context, url *url.URL) (net.Conn, error) { dialAddr := canonicalAddr(url) dialer := s.Dialer if dialer == nil { dialer = &net.Dialer{} } if url.Scheme == "http" { return dialer.DialContext(ctx, "tcp", dialAddr) } tlsDialer := tls.Dialer{ NetDialer: dialer, Config: s.tlsConfig, } return tlsDialer.DialContext(ctx, "tcp", dialAddr) } // proxyAuth returns, for a given proxy URL, the value to be used for the Proxy-Authorization header func (s *SpdyRoundTripper) proxyAuth(proxyURL *url.URL) string { if proxyURL == nil || proxyURL.User == nil { return "" } username := proxyURL.User.Username() password, _ := proxyURL.User.Password() auth := username + ":" + password return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) } // RoundTrip executes the Request and upgrades it. After a successful upgrade, // clients may call SpdyRoundTripper.Connection() to retrieve the upgraded // connection. func (s *SpdyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { req = req.Clone(req.Context()) req.Header = req.Header.Clone() req.Header.Add(httpstream.HeaderConnection, httpstream.HeaderUpgrade) req.Header.Add(httpstream.HeaderUpgrade, HeaderSpdy31) conn, err := s.Dial(req) if err != nil { return nil, err } responseReader := bufio.NewReader(conn) resp, err := http.ReadResponse(responseReader, nil) if err != nil { conn.Close() return nil, err } s.conn = conn return resp, nil } // NewConnection validates the upgrade response, creating and returning a new // httpstream.Connection if there were no errors. func (s *SpdyRoundTripper) NewConnection(resp *http.Response) (httpstream.Connection, error) { connectionHeader := strings.ToLower(resp.Header.Get(httpstream.HeaderConnection)) upgradeHeader := strings.ToLower(resp.Header.Get(httpstream.HeaderUpgrade)) if (resp.StatusCode != http.StatusSwitchingProtocols) || !strings.Contains(connectionHeader, strings.ToLower(httpstream.HeaderUpgrade)) || !strings.Contains(upgradeHeader, strings.ToLower(HeaderSpdy31)) { defer resp.Body.Close() responseErrorBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("unable to upgrade connection: unable to read error from server response") } return nil, fmt.Errorf("unable to upgrade connection: %s", upgradeErrorMessage(responseErrorBytes)) } return NewClientConnectionWithPings(s.conn, s.pingPeriod) } func tlsConfigForTransport(transport http.RoundTripper) (*tls.Config, error) { if transport == nil { return nil, nil } switch transport := transport.(type) { case *http.Transport: return transport.TLSClientConfig, nil case tlsClientConfigHolder: return transport.TLSClientConfig(), nil case roundTripperWrapper: return tlsConfigForTransport(transport.WrappedRoundTripper()) default: return nil, fmt.Errorf("transport %T does not expose TLS client config", transport) } } func canonicalAddr(url *url.URL) string { host := url.Hostname() port := url.Port() if len(port) == 0 { switch strings.ToLower(url.Scheme) { case "http", "ws": port = "80" case "https", "wss": port = "443" } } return net.JoinHostPort(host, port) } func upgradeErrorMessage(responseErrorBytes []byte) string { type statusLike struct { Message string `json:"message"` Error string `json:"error"` } var status statusLike if err := json.Unmarshal(responseErrorBytes, &status); err == nil { if msg := strings.TrimSpace(status.Message); msg != "" { return msg } if msg := strings.TrimSpace(status.Error); msg != "" { return msg } } msg := strings.TrimSpace(string(responseErrorBytes)) if msg == "" { return "empty server response" } return msg } func dialURLWithTransport(ctx context.Context, url *url.URL, transport http.RoundTripper) (net.Conn, error) { dialAddr := canonicalAddr(url) dialer, err := dialerFor(transport) if err != nil { dialer = nil } switch url.Scheme { case "http": if dialer != nil { return dialer(ctx, "tcp", dialAddr) } var d net.Dialer return d.DialContext(ctx, "tcp", dialAddr) case "https": tlsConfig, err := tlsConfigForTransport(transport) if err != nil { tlsConfig = nil } if dialer != nil { netConn, err := dialer(ctx, "tcp", dialAddr) if err != nil { return nil, err } if tlsConfig == nil { tlsConfig = &tls.Config{InsecureSkipVerify: true} } else if len(tlsConfig.ServerName) == 0 && !tlsConfig.InsecureSkipVerify { inferredHost := dialAddr if host, _, err := net.SplitHostPort(dialAddr); err == nil { inferredHost = host } tlsConfigCopy := tlsConfig.Clone() tlsConfigCopy.ServerName = inferredHost tlsConfig = tlsConfigCopy } if supportsHTTP11(tlsConfig.NextProtos) { tlsConfig = tlsConfig.Clone() tlsConfig.NextProtos = []string{"http/1.1"} } tlsConn := tls.Client(netConn, tlsConfig) if err := tlsConn.HandshakeContext(ctx); err != nil { netConn.Close() return nil, err } return tlsConn, nil } tlsDialer := tls.Dialer{Config: tlsConfig} return tlsDialer.DialContext(ctx, "tcp", dialAddr) default: return nil, fmt.Errorf("unknown scheme: %s", url.Scheme) } } func dialerFor(transport http.RoundTripper) (dialFunc, error) { if transport == nil { return nil, nil } switch transport := transport.(type) { case *http.Transport: if transport.DialContext != nil { return transport.DialContext, nil } if transport.Dial != nil { return func(ctx context.Context, network, addr string) (net.Conn, error) { return transport.Dial(network, addr) }, nil } return nil, nil case roundTripperWrapper: return dialerFor(transport.WrappedRoundTripper()) default: return nil, fmt.Errorf("unknown transport type: %T", transport) } } func supportsHTTP11(nextProtos []string) bool { if len(nextProtos) == 0 { return true } for _, proto := range nextProtos { if proto == "http/1.1" { return true } } return false } golang-k8s-streaming-0.36.2/pkg/httpstream/spdy/roundtripper_test.go000066400000000000000000001115311521404620200256170ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package spdy import ( "context" "crypto/tls" "crypto/x509" "io" "net" "net/http" "net/http/httptest" "net/http/httputil" "net/url" "reflect" "strconv" "strings" "sync" "testing" "github.com/armon/go-socks5" "k8s.io/streaming/pkg/httpstream" ) type serverHandlerConfig struct { shouldError bool statusCode int connectionHeader string upgradeHeader string } func serverHandler(t *testing.T, config serverHandlerConfig) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { if config.shouldError { if e, a := httpstream.HeaderUpgrade, req.Header.Get(httpstream.HeaderConnection); e != a { t.Fatalf("expected connection=upgrade header, got '%s", a) } w.Header().Set(httpstream.HeaderConnection, config.connectionHeader) w.Header().Set(httpstream.HeaderUpgrade, config.upgradeHeader) w.WriteHeader(config.statusCode) return } streamCh := make(chan httpstream.Stream) responseUpgrader := NewResponseUpgrader() spdyConn := responseUpgrader.UpgradeResponse(w, req, func(s httpstream.Stream, replySent <-chan struct{}) error { streamCh <- s return nil }) if spdyConn == nil { t.Fatal("unexpected nil spdyConn") } defer spdyConn.Close() stream := <-streamCh io.Copy(stream, stream) } } type serverFunc func(http.Handler) *httptest.Server type testHTTPProxyHandler struct { handlerDone sync.WaitGroup hook func(*http.Request) bool httpProxy httputil.ReverseProxy t testing.TB } func newTestHTTPProxyHandler(t testing.TB, hook func(*http.Request) bool) *testHTTPProxyHandler { return &testHTTPProxyHandler{ hook: hook, httpProxy: httputil.ReverseProxy{ Director: func(req *http.Request) { req.URL.Scheme = "http" req.URL.Host = req.Host }, }, t: t, } } func (h *testHTTPProxyHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { h.handlerDone.Add(1) defer h.handlerDone.Done() if h.hook != nil { if ok := h.hook(req); !ok { rw.WriteHeader(http.StatusInternalServerError) return } } if req.Method != http.MethodConnect { h.httpProxy.ServeHTTP(rw, req) return } sconn, err := net.Dial("tcp", req.Host) if err != nil { h.t.Logf("failed to dial proxy backend, host=%s: %v", req.Host, err) rw.WriteHeader(http.StatusInternalServerError) return } defer sconn.Close() hj, ok := rw.(http.Hijacker) if !ok { h.t.Logf("response writer cannot hijack for host=%s", req.Host) rw.WriteHeader(http.StatusInternalServerError) return } rw.WriteHeader(http.StatusOK) conn, brw, err := hj.Hijack() if err != nil { h.t.Logf("failed to hijack client connection, host=%s: %v", req.Host, err) return } defer conn.Close() if err := brw.Flush(); err != nil { h.t.Logf("failed to flush pending writes, host=%s: %v", req.Host, err) return } if _, err := io.Copy(sconn, io.LimitReader(brw, int64(brw.Reader.Buffered()))); err != nil { h.t.Logf("failed to flush buffered reads, host=%s: %v", req.Host, err) return } var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() io.Copy(conn, sconn) }() go func() { defer wg.Done() io.Copy(sconn, conn) }() wg.Wait() } func (h *testHTTPProxyHandler) Wait() { h.handlerDone.Wait() } func httpsServerInvalidHostname(t *testing.T) serverFunc { return func(h http.Handler) *httptest.Server { cert, err := tls.X509KeyPair(exampleCert, exampleKey) if err != nil { t.Errorf("https (invalid hostname): proxy_test: %v", err) } ts := httptest.NewUnstartedServer(h) ts.TLS = &tls.Config{ Certificates: []tls.Certificate{cert}, } ts.StartTLS() return ts } } func httpsServerValidHostname(t *testing.T) serverFunc { return func(h http.Handler) *httptest.Server { cert, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { t.Errorf("https (valid hostname): proxy_test: %v", err) } ts := httptest.NewUnstartedServer(h) ts.TLS = &tls.Config{ Certificates: []tls.Certificate{cert}, } ts.StartTLS() return ts } } func localhostCertPool(t *testing.T) *x509.CertPool { localhostPool := x509.NewCertPool() if !localhostPool.AppendCertsFromPEM(localhostCert) { t.Errorf("error setting up localhostCert pool") } return localhostPool } // be sure to unset environment variable https_proxy (if exported) before testing, otherwise the testing will fail unexpectedly. func TestRoundTripAndNewConnection(t *testing.T) { localhostPool := localhostCertPool(t) testCases := map[string]struct { serverFunc func(http.Handler) *httptest.Server proxyServerFunc func(http.Handler) *httptest.Server proxyAuth *url.Userinfo clientTLS *tls.Config serverConnectionHeader string serverUpgradeHeader string serverStatusCode int shouldError bool }{ "no headers": { serverFunc: httptest.NewServer, serverConnectionHeader: "", serverUpgradeHeader: "", serverStatusCode: http.StatusSwitchingProtocols, shouldError: true, }, "no upgrade header": { serverFunc: httptest.NewServer, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "", serverStatusCode: http.StatusSwitchingProtocols, shouldError: true, }, "no connection header": { serverFunc: httptest.NewServer, serverConnectionHeader: "", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: true, }, "no switching protocol status code": { serverFunc: httptest.NewServer, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusForbidden, shouldError: true, }, "http": { serverFunc: httptest.NewServer, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, }, "https (invalid hostname + InsecureSkipVerify)": { serverFunc: httpsServerInvalidHostname(t), clientTLS: &tls.Config{InsecureSkipVerify: true}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, }, "https (invalid hostname + hostname verification)": { serverFunc: httpsServerInvalidHostname(t), clientTLS: &tls.Config{InsecureSkipVerify: false}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: true, }, "https (valid hostname + RootCAs)": { serverFunc: httpsServerValidHostname(t), clientTLS: &tls.Config{RootCAs: localhostPool}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, }, "proxied http->http": { serverFunc: httptest.NewServer, proxyServerFunc: httptest.NewServer, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, }, "proxied https (invalid hostname + InsecureSkipVerify) -> http": { serverFunc: httptest.NewServer, proxyServerFunc: httpsServerInvalidHostname(t), clientTLS: &tls.Config{InsecureSkipVerify: true}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, }, "proxied https with auth (invalid hostname + InsecureSkipVerify) -> http": { serverFunc: httptest.NewServer, proxyServerFunc: httpsServerInvalidHostname(t), proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), clientTLS: &tls.Config{InsecureSkipVerify: true}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, }, "proxied https (invalid hostname + hostname verification) -> http": { serverFunc: httptest.NewServer, proxyServerFunc: httpsServerInvalidHostname(t), clientTLS: &tls.Config{InsecureSkipVerify: false}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: true, // fails because the client doesn't trust the proxy }, "proxied https (valid hostname + RootCAs) -> http": { serverFunc: httptest.NewServer, proxyServerFunc: httpsServerValidHostname(t), clientTLS: &tls.Config{RootCAs: localhostPool}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, }, "proxied https with auth (valid hostname + RootCAs) -> http": { serverFunc: httptest.NewServer, proxyServerFunc: httpsServerValidHostname(t), proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), clientTLS: &tls.Config{RootCAs: localhostPool}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, }, "proxied https (invalid hostname + InsecureSkipVerify) -> https (invalid hostname)": { serverFunc: httpsServerInvalidHostname(t), proxyServerFunc: httpsServerInvalidHostname(t), clientTLS: &tls.Config{InsecureSkipVerify: true}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, // works because the test proxy ignores TLS errors }, "proxied https with auth (invalid hostname + InsecureSkipVerify) -> https (invalid hostname)": { serverFunc: httpsServerInvalidHostname(t), proxyServerFunc: httpsServerInvalidHostname(t), proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), clientTLS: &tls.Config{InsecureSkipVerify: true}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, // works because the test proxy ignores TLS errors }, "proxied https (invalid hostname + hostname verification) -> https (invalid hostname)": { serverFunc: httpsServerInvalidHostname(t), proxyServerFunc: httpsServerInvalidHostname(t), clientTLS: &tls.Config{InsecureSkipVerify: false}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: true, // fails because the client doesn't trust the proxy }, "proxied https (valid hostname + RootCAs) -> https (valid hostname + RootCAs)": { serverFunc: httpsServerValidHostname(t), proxyServerFunc: httpsServerValidHostname(t), clientTLS: &tls.Config{RootCAs: localhostPool}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, }, "proxied https with auth (valid hostname + RootCAs) -> https (valid hostname + RootCAs)": { serverFunc: httpsServerValidHostname(t), proxyServerFunc: httpsServerValidHostname(t), proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), clientTLS: &tls.Config{RootCAs: localhostPool}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, }, "proxied valid https, proxy auth with chars that percent escape -> valid https": { serverFunc: httpsServerValidHostname(t), proxyServerFunc: httpsServerValidHostname(t), proxyAuth: url.UserPassword("proxy user", "proxypasswd%"), clientTLS: &tls.Config{RootCAs: localhostPool}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, }, } for k, testCase := range testCases { t.Run(k, func(t *testing.T) { server := testCase.serverFunc(serverHandler( t, serverHandlerConfig{ shouldError: testCase.shouldError, statusCode: testCase.serverStatusCode, connectionHeader: testCase.serverConnectionHeader, upgradeHeader: testCase.serverUpgradeHeader, }, )) defer server.Close() t.Logf("Server URL: %v", server.URL) serverURL, err := url.Parse(server.URL) if err != nil { t.Fatalf("error creating request: %s", err) } req, err := http.NewRequest(http.MethodGet, server.URL, nil) if err != nil { t.Fatalf("error creating request: %s", err) } spdyTransport, err := NewRoundTripper(testCase.clientTLS) if err != nil { t.Fatalf("error creating SpdyRoundTripper: %v", err) } var proxierCalled bool var proxyCalledWithHost string var proxyCalledWithAuth bool var proxyCalledWithAuthHeader string if testCase.proxyServerFunc != nil { proxyHandler := newTestHTTPProxyHandler(t, func(req *http.Request) bool { proxyCalledWithHost = req.Host proxyAuthHeaderName := "Proxy-Authorization" _, proxyCalledWithAuth = req.Header[proxyAuthHeaderName] proxyCalledWithAuthHeader = req.Header.Get(proxyAuthHeaderName) return true }) defer proxyHandler.Wait() proxy := testCase.proxyServerFunc(proxyHandler) defer proxy.Close() t.Logf("Proxy URL: %v", proxy.URL) spdyTransport.proxier = func(proxierReq *http.Request) (*url.URL, error) { proxierCalled = true proxyURL, err := url.Parse(proxy.URL) if err != nil { return nil, err } proxyURL.User = testCase.proxyAuth return proxyURL, nil } } client := &http.Client{Transport: spdyTransport} resp, err := client.Do(req) var conn httpstream.Connection if err == nil { conn, err = spdyTransport.NewConnection(resp) } haveErr := err != nil if e, a := testCase.shouldError, haveErr; e != a { t.Fatalf("shouldError=%t, got %t: %v", e, a, err) } if testCase.shouldError { return } defer conn.Close() if resp.StatusCode != http.StatusSwitchingProtocols { t.Fatalf("expected http 101 switching protocols, got %d", resp.StatusCode) } stream, err := conn.CreateStream(http.Header{}) if err != nil { t.Fatalf("error creating client stream: %s", err) } n, err := stream.Write([]byte("hello")) if err != nil { t.Fatalf("error writing to stream: %s", err) } if n != 5 { t.Fatalf("expected to write 5 bytes, but actually wrote %d", n) } b := make([]byte, 5) n, err = stream.Read(b) if err != nil { t.Fatalf("error reading from stream: %s", err) } if n != 5 { t.Fatalf("expected to read 5 bytes, but actually read %d", n) } if e, a := "hello", string(b[0:n]); e != a { t.Fatalf("expected '%s', got '%s'", e, a) } if testCase.proxyServerFunc != nil { if !proxierCalled { t.Fatal("expected to use a proxy but proxier in SpdyRoundTripper wasn't called") } if proxyCalledWithHost != serverURL.Host { t.Fatalf("expected to see a call to the proxy for backend %q, got %q", serverURL.Host, proxyCalledWithHost) } } if testCase.proxyAuth != nil { expectedUsername := testCase.proxyAuth.Username() expectedPassword, _ := testCase.proxyAuth.Password() username, password, ok := (&http.Request{Header: http.Header{"Authorization": []string{proxyCalledWithAuthHeader}}}).BasicAuth() if !ok { t.Fatalf("invalid proxy auth header %s", proxyCalledWithAuthHeader) } if username != expectedUsername || password != expectedPassword { t.Fatalf("expected proxy auth \"%s:%s\", got \"%s:%s\"", expectedUsername, expectedPassword, username, password) } } else if proxyCalledWithAuth { t.Fatalf("proxy authorization unexpected, got %q", proxyCalledWithAuthHeader) } }) } } // Tests SpdyRoundTripper constructors func TestRoundTripConstuctor(t *testing.T) { testCases := map[string]struct { tlsConfig *tls.Config proxier func(req *http.Request) (*url.URL, error) upgradeTransport http.RoundTripper expectedTLSConfig *tls.Config errMsg string }{ "Basic TLSConfig; no error": { tlsConfig: &tls.Config{InsecureSkipVerify: true}, expectedTLSConfig: &tls.Config{InsecureSkipVerify: true}, upgradeTransport: nil, }, "Basic TLSConfig and Proxier: no error": { tlsConfig: &tls.Config{InsecureSkipVerify: true}, proxier: func(req *http.Request) (*url.URL, error) { return nil, nil }, expectedTLSConfig: &tls.Config{InsecureSkipVerify: true}, upgradeTransport: nil, }, "TLSConfig with UpgradeTransport: error": { tlsConfig: &tls.Config{InsecureSkipVerify: true}, upgradeTransport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, expectedTLSConfig: &tls.Config{InsecureSkipVerify: true}, errMsg: "SpdyRoundTripper: UpgradeTransport is mutually exclusive to TLSConfig or Proxier", }, "Proxier with UpgradeTransport: error": { proxier: func(req *http.Request) (*url.URL, error) { return nil, nil }, upgradeTransport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, expectedTLSConfig: &tls.Config{InsecureSkipVerify: true}, errMsg: "SpdyRoundTripper: UpgradeTransport is mutually exclusive to TLSConfig or Proxier", }, "Only UpgradeTransport: no error": { upgradeTransport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, expectedTLSConfig: &tls.Config{InsecureSkipVerify: true}, }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { spdyRoundTripper, err := NewRoundTripperWithConfig( RoundTripperConfig{ TLS: testCase.tlsConfig, Proxier: testCase.proxier, UpgradeTransport: testCase.upgradeTransport, }, ) if testCase.errMsg != "" { if err == nil { t.Fatalf("expected error but received none") } if !strings.Contains(err.Error(), testCase.errMsg) { t.Fatalf("expected error message (%s), got (%s)", err.Error(), testCase.errMsg) } } if testCase.errMsg == "" { if err != nil { t.Fatalf("unexpected error received: %v", err) } actualTLSConfig := spdyRoundTripper.TLSClientConfig() if !reflect.DeepEqual(testCase.expectedTLSConfig, actualTLSConfig) { t.Errorf("expected TLSConfig (%v), got (%v)", testCase.expectedTLSConfig, actualTLSConfig) } } }) } } func TestProxierWithNoProxyCIDR(t *testing.T) { t.Setenv("NO_PROXY", "10.0.0.0/8,2001:db8::/32") t.Setenv("no_proxy", "") expectedProxy, err := url.Parse("http://proxy.example:8080") if err != nil { t.Fatalf("unexpected error: %v", err) } delegateCalls := 0 delegate := func(*http.Request) (*url.URL, error) { delegateCalls++ return expectedProxy, nil } proxier := newProxierWithNoProxyCIDR(delegate) req, err := http.NewRequest(http.MethodGet, "https://10.1.2.3", nil) if err != nil { t.Fatalf("unexpected error: %v", err) } proxyURL, err := proxier(req) if err != nil { t.Fatalf("unexpected error: %v", err) } if proxyURL != nil { t.Fatalf("expected nil proxyURL, got %v", proxyURL) } if delegateCalls != 0 { t.Fatalf("expected delegateCalls=0, got %d", delegateCalls) } req, err = http.NewRequest(http.MethodGet, "https://[2001:db8::1]", nil) if err != nil { t.Fatalf("unexpected error: %v", err) } proxyURL, err = proxier(req) if err != nil { t.Fatalf("unexpected error: %v", err) } if proxyURL != nil { t.Fatalf("expected nil proxyURL, got %v", proxyURL) } if delegateCalls != 0 { t.Fatalf("expected delegateCalls=0, got %d", delegateCalls) } req, err = http.NewRequest(http.MethodGet, "https://192.168.1.10", nil) if err != nil { t.Fatalf("unexpected error: %v", err) } proxyURL, err = proxier(req) if err != nil { t.Fatalf("unexpected error: %v", err) } if proxyURL == nil { t.Fatalf("expected non-nil proxyURL") } if proxyURL.String() != expectedProxy.String() { t.Fatalf("expected proxyURL %q, got %q", expectedProxy.String(), proxyURL.String()) } if delegateCalls != 1 { t.Fatalf("expected delegateCalls=1, got %d", delegateCalls) } req, err = http.NewRequest(http.MethodGet, "https://example.com", nil) if err != nil { t.Fatalf("unexpected error: %v", err) } proxyURL, err = proxier(req) if err != nil { t.Fatalf("unexpected error: %v", err) } if proxyURL == nil { t.Fatalf("expected non-nil proxyURL") } if proxyURL.String() != expectedProxy.String() { t.Fatalf("expected proxyURL %q, got %q", expectedProxy.String(), proxyURL.String()) } if delegateCalls != 2 { t.Fatalf("expected delegateCalls=2, got %d", delegateCalls) } } type Interceptor struct { Authorization socks5.AuthContext proxyCalledWithHost *string } func (i *Interceptor) GetAuthContext() (int, map[string]string) { return int(i.Authorization.Method), i.Authorization.Payload } func (i *Interceptor) Rewrite(ctx context.Context, req *socks5.Request) (context.Context, *socks5.AddrSpec) { *i.proxyCalledWithHost = req.DestAddr.Address() i.Authorization = socks5.AuthContext(*req.AuthContext) return ctx, req.DestAddr } // be sure to unset environment variable https_proxy (if exported) before testing, otherwise the testing will fail unexpectedly. func TestRoundTripSocks5AndNewConnection(t *testing.T) { localhostPool := localhostCertPool(t) socks5Server := func(creds *socks5.StaticCredentials, interceptor *Interceptor) *socks5.Server { var conf *socks5.Config if creds != nil { authenticator := socks5.UserPassAuthenticator{Credentials: creds} conf = &socks5.Config{ AuthMethods: []socks5.Authenticator{authenticator}, Rewriter: interceptor, } } else { conf = &socks5.Config{Rewriter: interceptor} } ts, err := socks5.New(conf) if err != nil { t.Errorf("failed to create sock5 server: %v", err) } return ts } testCases := map[string]struct { clientTLS *tls.Config proxyAuth *url.Userinfo serverConnectionHeader string serverFunc serverFunc serverStatusCode int serverUpgradeHeader string shouldError bool }{ "proxied without auth -> http": { serverFunc: httptest.NewServer, serverConnectionHeader: "Upgrade", serverStatusCode: http.StatusSwitchingProtocols, serverUpgradeHeader: "SPDY/3.1", shouldError: false, }, "proxied with invalid auth -> http": { serverFunc: httptest.NewServer, proxyAuth: url.UserPassword("invalid", "auth"), serverConnectionHeader: "Upgrade", serverStatusCode: http.StatusSwitchingProtocols, serverUpgradeHeader: "SPDY/3.1", shouldError: true, }, "proxied with valid auth -> http": { serverFunc: httptest.NewServer, proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), serverConnectionHeader: "Upgrade", serverStatusCode: http.StatusSwitchingProtocols, serverUpgradeHeader: "SPDY/3.1", shouldError: false, }, "proxied with valid auth -> https (invalid hostname + InsecureSkipVerify)": { serverFunc: httpsServerInvalidHostname(t), proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), clientTLS: &tls.Config{InsecureSkipVerify: true}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, }, "proxied with valid auth -> https (invalid hostname + hostname verification)": { serverFunc: httpsServerInvalidHostname(t), proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), clientTLS: &tls.Config{InsecureSkipVerify: false}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: true, }, "proxied with valid auth -> https (valid hostname + RootCAs)": { serverFunc: httpsServerValidHostname(t), proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), clientTLS: &tls.Config{RootCAs: localhostPool}, serverConnectionHeader: "Upgrade", serverUpgradeHeader: "SPDY/3.1", serverStatusCode: http.StatusSwitchingProtocols, shouldError: false, }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { server := testCase.serverFunc(serverHandler( t, serverHandlerConfig{ shouldError: testCase.shouldError, statusCode: testCase.serverStatusCode, connectionHeader: testCase.serverConnectionHeader, upgradeHeader: testCase.serverUpgradeHeader, }, )) defer server.Close() req, err := http.NewRequest(http.MethodGet, server.URL, nil) if err != nil { t.Fatalf("error creating request: %s", err) } spdyTransport, err := NewRoundTripper(testCase.clientTLS) if err != nil { t.Fatalf("error creating SpdyRoundTripper: %v", err) } var proxierCalled bool var proxyCalledWithHost string interceptor := &Interceptor{proxyCalledWithHost: &proxyCalledWithHost} proxyHandler := socks5Server(nil, interceptor) if testCase.proxyAuth != nil { proxyHandler = socks5Server(&socks5.StaticCredentials{ "proxyuser": "proxypasswd", // Socks5 server static credentials when client authentication is expected }, interceptor) } closed := make(chan struct{}) isClosed := func() bool { select { case <-closed: return true default: return false } } l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("socks5Server: proxy_test: Listen: %v", err) } defer l.Close() go func(shoulderror bool) { conn, err := l.Accept() if err != nil { if isClosed() { return } t.Errorf("error accepting connection: %s", err) } if err := proxyHandler.ServeConn(conn); err != nil && !shoulderror { // If the connection request is closed before the channel is closed // the test will fail with a ServeConn error. Since the test only return // early if expects shouldError=true, the channel is closed at the end of // the test, just before all the deferred connections Close() are executed. if isClosed() { return } t.Errorf("ServeConn error: %s", err) } }(testCase.shouldError) spdyTransport.proxier = func(proxierReq *http.Request) (*url.URL, error) { proxierCalled = true return &url.URL{ Scheme: "socks5", Host: net.JoinHostPort("127.0.0.1", strconv.Itoa(l.Addr().(*net.TCPAddr).Port)), User: testCase.proxyAuth, }, nil } client := &http.Client{Transport: spdyTransport} resp, err := client.Do(req) haveErr := err != nil if e, a := testCase.shouldError, haveErr; e != a { t.Fatalf("shouldError=%t, got %t: %v", e, a, err) } if testCase.shouldError { return } conn, err := spdyTransport.NewConnection(resp) haveErr = err != nil if e, a := testCase.shouldError, haveErr; e != a { t.Fatalf("shouldError=%t, got %t: %v", e, a, err) } if testCase.shouldError { return } defer conn.Close() if resp.StatusCode != http.StatusSwitchingProtocols { t.Fatalf("expected http 101 switching protocols, got %d", resp.StatusCode) } stream, err := conn.CreateStream(http.Header{}) if err != nil { t.Fatalf("error creating client stream: %s", err) } n, err := stream.Write([]byte("hello")) if err != nil { t.Fatalf("error writing to stream: %s", err) } if n != 5 { t.Fatalf("expected to write 5 bytes, but actually wrote %d", n) } b := make([]byte, 5) n, err = stream.Read(b) if err != nil { t.Fatalf("error reading from stream: %s", err) } if n != 5 { t.Fatalf("expected to read 5 bytes, but actually read %d", n) } if e, a := "hello", string(b[0:n]); e != a { t.Fatalf("expected '%s', got '%s'", e, a) } if !proxierCalled { t.Fatal("xpected to use a proxy but proxier in SpdyRoundTripper wasn't called") } serverURL, err := url.Parse(server.URL) if err != nil { t.Fatalf("error creating request: %s", err) } if proxyCalledWithHost != serverURL.Host { t.Fatalf("expected to see a call to the proxy for backend %q, got %q", serverURL.Host, proxyCalledWithHost) } authMethod, authUser := interceptor.GetAuthContext() if testCase.proxyAuth != nil { expectedSocks5AuthMethod := 2 expectedSocks5AuthUser := "proxyuser" if expectedSocks5AuthMethod != authMethod { t.Fatalf("socks5 Proxy authorization unexpected, got %d, expected %d", authMethod, expectedSocks5AuthMethod) } if expectedSocks5AuthUser != authUser["Username"] { t.Fatalf("socks5 Proxy authorization user unexpected, got %q, expected %q", authUser["Username"], expectedSocks5AuthUser) } } else { if authMethod != 0 { t.Fatalf("proxy authentication method unexpected, got %d", authMethod) } if len(authUser) != 0 { t.Fatalf("unexpected proxy user: %v", authUser) } } // The channel must be closed before any of the connections are closed close(closed) }) } } func TestRoundTripPassesContextToDialer(t *testing.T) { urls := []string{"http://127.0.0.1:1233/", "https://127.0.0.1:1233/"} for _, u := range urls { t.Run(u, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } spdyTransport, err := NewRoundTripper(&tls.Config{}) if err != nil { t.Fatalf("error creating SpdyRoundTripper: %v", err) } _, err = spdyTransport.Dial(req) if err == nil || err.Error() != "dial tcp 127.0.0.1:1233: operation was canceled" { t.Errorf("expected error %q, got %v", "dial tcp 127.0.0.1:1233: operation was canceled", err) } }) } } // exampleCert was generated from crypto/tls/generate_cert.go with the following command: // // go run generate_cert.go --rsa-bits 2048 --host example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h var exampleCert = []byte(`-----BEGIN CERTIFICATE----- MIIDADCCAeigAwIBAgIQVHG3Fn9SdWayyLOZKCW1vzANBgkqhkiG9w0BAQsFADAS MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEArTCu9fiIclNgDdWHphewM+JW55dCb5yYGlJgCBvwbOx547M9p+tn zm9QOhsdZDHDZsG9tqnWxE2Nc1HpIJyOlfYsOoonpEoG/Ep6nnK91ngj0bn/JlNy +i/bwU4r97MOukvnOIQez9/D9jAJaOX2+b8/d4lRz9BsqiwJyg+ynZ5tVVYj7aMi vXnd6HOnJmtqutOtr3beucJnkd6XbwRkLUcAYATT+ZihOWRbTuKqhCg6zGkJOoUG f8sX61JjoilxiURA//ftGVbdTCU3DrmGmardp5NNOHbumMYU8Vhmqgx1Bqxb+9he 7G42uW5YWYK/GqJzgVPjjlB2dOGj9KrEWQIDAQABo1AwTjAOBgNVHQ8BAf8EBAMC AqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAWBgNVHREE DzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAig4AIi9xWs1+pLES eeGGdSDoclplFpcbXANnsYYFyLf+8pcWgVi2bOmb2gXMbHFkB07MA82wRJAUTaA+ 2iNXVQMhPCoA7J6ADUbww9doJX2S9HGyArhiV/MhHtE8txzMn2EKNLdhhk3N9rmV x/qRbWAY1U2z4BpdrAR87Fe81Nlj7h45csW9K+eS+NgXipiNTIfEShKgCFM8EdxL 1WXg7r9AvYV3TNDPWTjLsm1rQzzZQ7Uvcf6deWiNodZd8MOT/BFLclDPTK6cF2Hr UU4dq6G4kCwMSxWE4cM3HlZ4u1dyIt47VbkP0rtvkBCXx36y+NXYA5lzntchNFZP uvEQdw== -----END CERTIFICATE-----`) var exampleKey = []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEArTCu9fiIclNgDdWHphewM+JW55dCb5yYGlJgCBvwbOx547M9 p+tnzm9QOhsdZDHDZsG9tqnWxE2Nc1HpIJyOlfYsOoonpEoG/Ep6nnK91ngj0bn/ JlNy+i/bwU4r97MOukvnOIQez9/D9jAJaOX2+b8/d4lRz9BsqiwJyg+ynZ5tVVYj 7aMivXnd6HOnJmtqutOtr3beucJnkd6XbwRkLUcAYATT+ZihOWRbTuKqhCg6zGkJ OoUGf8sX61JjoilxiURA//ftGVbdTCU3DrmGmardp5NNOHbumMYU8Vhmqgx1Bqxb +9he7G42uW5YWYK/GqJzgVPjjlB2dOGj9KrEWQIDAQABAoIBAQClt4CiYaaF5ltx wVDjz6TNcJUBUs3CKE+uWAYFnF5Ii1nyU876Pxj8Aaz9fHZ6Kde0GkwiXY7gFOj1 YHo2tzcELSKS/SEDZcYbYFTGCjq13g1AH74R+SV6WZLn+5m8kPvVrM1ZWap188H5 bmuCkRDqVmIvShkbRW7EwhC35J9fiuW3majC/sjmsxtxyP6geWmu4f5/Ttqahcdb osPZIgIIPzqAkNtkLTi7+meHYI9wlrGhL7XZTwnJ1Oc/Y67zzmbthLYB5YFSLUew rXT58jtSjX4gbiQyheBSrWxW08QE4qYg6jJlAdffHhWv72hJW2MCXhuXp8gJs/Do XLRHGwSBAoGBAMdNtsbe4yae/QeHUPGxNW0ipa0yoTF6i+VYoxvqiRMzDM3+3L8k dgI1rr4330SivqDahMA/odWtM/9rVwJI2B2QhZLMHA0n9ytH007OO9TghgVB12nN xosRYBpKdHXyyvV/MUZl7Jux6zKIzRDWOkF95VVYPcAaxJqd1E5/jJ6JAoGBAN51 QrebA1w/jfydeqQTz1sK01sbO4HYj4qGfo/JarVqGEkm1azeBBPPRnHz3jNKnCkM S4PpqRDased3NIcViXlAgoqPqivZ8mQa/Rb146l7WaTErASHsZ023OGrxsr/Ed6N P3GrmvxVJjebaFNaQ9sP80dLkpgeas0t2TY8iQNRAoGATOcnx8TpUVW3vNfx29DN FLdxxkrq9/SZVn3FMlhlXAsuva3B799ZybB9JNjaRdmmRNsMrkHfaFvU3JHGmRMS kRXa9LHdgRYSwZiNaLMbUyDvlce6HxFPswmZU4u3NGvi9KeHk+pwSgN1BaLTvdNr 1ymE/FF4QlAR3LdZ3JBK6kECgYEA0wW4/CJ31ZIURoW8SNjh4iMqy0nR8SJVR7q9 Y/hU2TKDRyEnoIwaohAFayNCrLUh3W5kVAXa8roB+OgDVAECH5sqOfZ+HorofD19 x8II7ESujLZj1whBXDkm3ovsT7QWZ17lyBZZNvQvBKDPHgKKS8udowv1S4fPGENd wS07a4ECgYEAwLSbmMIVJme0jFjsp5d1wOGA2Qi2ZwGIAVlsbnJtygrU/hSBfnu8 VfyJSCgg3fPe7kChWKlfcOebVKSb68LKRsz1Lz1KdbY0HOJFp/cT4lKmDAlRY9gq LB4rdf46lV0mUkvd2/oofIbTrzukjQSnyfLawb/2uJGV1IkTcZcn9CI= -----END RSA PRIVATE KEY-----`) // localhostCert was generated from crypto/tls/generate_cert.go with the following command: // // go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h var localhostCert = []byte(`-----BEGIN CERTIFICATE----- MIIDGTCCAgGgAwIBAgIRALL5AZcefF4kkYV1SEG6YrMwDQYJKoZIhvcNAQELBQAw EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2 MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEP ADCCAQoCggEBALQ/FHcyVwdFHxARbbD2KBtDUT7Eni+8ioNdjtGcmtXqBv45EC1C JOqqGJTroFGJ6Q9kQIZ9FqH5IJR2fOOJD9kOTueG4Vt1JY1rj1Kbpjefu8XleZ5L SBwIWVnN/lEsEbuKmj7N2gLt5AH3zMZiBI1mg1u9Z5ZZHYbCiTpBrwsq6cTlvR9g dyo1YkM5hRESCzsrL0aUByoo0qRMD8ZsgANJwgsiO0/M6idbxDwv1BnGwGmRYvOE Hxpy3v0Jg7GJYrvnpnifJTs4nw91N5X9pXxR7FFzi/6HTYDWRljvTb0w6XciKYAz bWZ0+cJr5F7wB7ovlbm7HrQIR7z7EIIu2d8CAwEAAaNoMGYwDgYDVR0PAQH/BAQD AgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQFMAMBAf8wLgYDVR0R BCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZI hvcNAQELBQADggEBAFPPWopNEJtIA2VFAQcqN6uJK+JVFOnjGRoCrM6Xgzdm0wxY XCGjsxY5dl+V7KzdGqu858rCaq5osEBqypBpYAnS9C38VyCDA1vPS1PsN8SYv48z DyBwj+7R2qar0ADBhnhWxvYO9M72lN/wuCqFKYMeFSnJdQLv3AsrrHe9lYqOa36s 8wxSwVTFTYXBzljPEnSaaJMPqFD8JXaZK1ryJPkO5OsCNQNGtatNiWAf3DcmwHAT MGYMzP0u4nw47aRz9shB8w+taPKHx2BVwE1m/yp3nHVioOjXqA1fwRQVGclCJSH1 D2iq3hWVHRENgjTjANBPICLo9AZ4JfN6PH19mnU= -----END CERTIFICATE-----`) // localhostKey is the private key for localhostCert. var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAtD8UdzJXB0UfEBFtsPYoG0NRPsSeL7yKg12O0Zya1eoG/jkQ LUIk6qoYlOugUYnpD2RAhn0WofkglHZ844kP2Q5O54bhW3UljWuPUpumN5+7xeV5 nktIHAhZWc3+USwRu4qaPs3aAu3kAffMxmIEjWaDW71nllkdhsKJOkGvCyrpxOW9 H2B3KjViQzmFERILOysvRpQHKijSpEwPxmyAA0nCCyI7T8zqJ1vEPC/UGcbAaZFi 84QfGnLe/QmDsYliu+emeJ8lOzifD3U3lf2lfFHsUXOL/odNgNZGWO9NvTDpdyIp gDNtZnT5wmvkXvAHui+VubsetAhHvPsQgi7Z3wIDAQABAoIBAGmw93IxjYCQ0ncc kSKMJNZfsdtJdaxuNRZ0nNNirhQzR2h403iGaZlEpmdkhzxozsWcto1l+gh+SdFk bTUK4MUZM8FlgO2dEqkLYh5BcMT7ICMZvSfJ4v21E5eqR68XVUqQKoQbNvQyxFk3 EddeEGdNrkb0GDK8DKlBlzAW5ep4gjG85wSTjR+J+muUv3R0BgLBFSuQnIDM/IMB LWqsja/QbtB7yppe7jL5u8UCFdZG8BBKT9fcvFIu5PRLO3MO0uOI7LTc8+W1Xm23 uv+j3SY0+v+6POjK0UlJFFi/wkSPTFIfrQO1qFBkTDQHhQ6q/7GnILYYOiGbIRg2 NNuP52ECgYEAzXEoy50wSYh8xfFaBuxbm3ruuG2W49jgop7ZfoFrPWwOQKAZS441 VIwV4+e5IcA6KkuYbtGSdTYqK1SMkgnUyD/VevwAqH5TJoEIGu0pDuKGwVuwqioZ frCIAV5GllKyUJ55VZNbRr2vY2fCsWbaCSCHETn6C16DNuTCe5C0JBECgYEA4JqY 5GpNbMG8fOt4H7hU0Fbm2yd6SHJcQ3/9iimef7xG6ajxsYrIhg1ft+3IPHMjVI0+ 9brwHDnWg4bOOx/VO4VJBt6Dm/F33bndnZRkuIjfSNpLM51P+EnRdaFVHOJHwKqx uF69kihifCAG7YATgCveeXImzBUSyZUz9UrETu8CgYARNBimdFNG1RcdvEg9rC0/ p9u1tfecvNySwZqU7WF9kz7eSonTueTdX521qAHowaAdSpdJMGODTTXaywm6cPhQ jIfj9JZZhbqQzt1O4+08Qdvm9TamCUB5S28YLjza+bHU7nBaqixKkDfPqzCyilpX yVGGL8SwjwmN3zop/sQXAQKBgC0JMsESQ6YcDsRpnrOVjYQc+LtW5iEitTdfsaID iGGKihmOI7B66IxgoCHMTws39wycKdSyADVYr5e97xpR3rrJlgQHmBIrz+Iow7Q2 LiAGaec8xjl6QK/DdXmFuQBKqyKJ14rljFODP4QuE9WJid94bGqjpf3j99ltznZP 4J8HAoGAJb4eb4lu4UGwifDzqfAPzLGCoi0fE1/hSx34lfuLcc1G+LEu9YDKoOVJ 9suOh0b5K/bfEy9KrVMBBriduvdaERSD8S3pkIQaitIz0B029AbE4FLFf9lKQpP2 KR8NJEkK99Vh/tew6jAMll70xFrE7aF8VLXJVE7w4sQzuvHxl9Q= -----END RSA PRIVATE KEY----- `) golang-k8s-streaming-0.36.2/pkg/httpstream/spdy/upgrade.go000066400000000000000000000077701521404620200234630ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package spdy import ( "bufio" "fmt" "io" "net" "net/http" "strings" "sync/atomic" "time" "k8s.io/streaming/pkg/httpstream" "k8s.io/streaming/pkg/runtime" ) const HeaderSpdy31 = "SPDY/3.1" // responseUpgrader knows how to upgrade HTTP responses. It // implements the httpstream.ResponseUpgrader interface. type responseUpgrader struct { pingPeriod time.Duration } // connWrapper is used to wrap a hijacked connection and its bufio.Reader. All // calls will be handled directly by the underlying net.Conn with the exception // of Read and Close calls, which will consider data in the bufio.Reader. This // ensures that data already inside the used bufio.Reader instance is also // read. type connWrapper struct { net.Conn closed int32 bufReader *bufio.Reader } func (w *connWrapper) Read(b []byte) (n int, err error) { if atomic.LoadInt32(&w.closed) == 1 { return 0, io.EOF } return w.bufReader.Read(b) } func (w *connWrapper) Close() error { err := w.Conn.Close() atomic.StoreInt32(&w.closed, 1) return err } // NewResponseUpgrader returns a new httpstream.ResponseUpgrader that is // capable of upgrading HTTP responses using SPDY/3.1 via the // spdystream package. func NewResponseUpgrader() httpstream.ResponseUpgrader { return NewResponseUpgraderWithPings(0) } // NewResponseUpgraderWithPings returns a new httpstream.ResponseUpgrader that // is capable of upgrading HTTP responses using SPDY/3.1 via the spdystream // package. // // If pingPeriod is non-zero, for each incoming connection a background // goroutine will send periodic Ping frames to the server. Use this to keep // idle connections through certain load balancers alive longer. func NewResponseUpgraderWithPings(pingPeriod time.Duration) httpstream.ResponseUpgrader { return responseUpgrader{pingPeriod: pingPeriod} } // UpgradeResponse upgrades an HTTP response to one that supports multiplexed // streams. newStreamHandler will be called synchronously whenever the // other end of the upgraded connection creates a new stream. func (u responseUpgrader) UpgradeResponse(w http.ResponseWriter, req *http.Request, newStreamHandler httpstream.NewStreamHandler) httpstream.Connection { connectionHeader := strings.ToLower(req.Header.Get(httpstream.HeaderConnection)) upgradeHeader := strings.ToLower(req.Header.Get(httpstream.HeaderUpgrade)) if !strings.Contains(connectionHeader, strings.ToLower(httpstream.HeaderUpgrade)) || !strings.Contains(upgradeHeader, strings.ToLower(HeaderSpdy31)) { errorMsg := fmt.Sprintf("unable to upgrade: missing upgrade headers in request: %#v", req.Header) http.Error(w, errorMsg, http.StatusBadRequest) return nil } hijacker, ok := w.(http.Hijacker) if !ok { errorMsg := "unable to upgrade: unable to hijack response" http.Error(w, errorMsg, http.StatusInternalServerError) return nil } w.Header().Add(httpstream.HeaderConnection, httpstream.HeaderUpgrade) w.Header().Add(httpstream.HeaderUpgrade, HeaderSpdy31) w.WriteHeader(http.StatusSwitchingProtocols) conn, bufrw, err := hijacker.Hijack() if err != nil { runtime.HandleErrorWithContext(req.Context(), err, "Unable to upgrade: error hijacking response") return nil } connWithBuf := &connWrapper{Conn: conn, bufReader: bufrw.Reader} spdyConn, err := NewServerConnectionWithPings(connWithBuf, newStreamHandler, u.pingPeriod) if err != nil { runtime.HandleErrorWithContext(req.Context(), err, "Unable to upgrade: error creating SPDY server connection") return nil } return spdyConn } golang-k8s-streaming-0.36.2/pkg/httpstream/spdy/upgrade_test.go000066400000000000000000000044011521404620200245060ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package spdy import ( "net/http" "net/http/httptest" "testing" ) func TestUpgradeResponse(t *testing.T) { testCases := []struct { connectionHeader string upgradeHeader string shouldError bool }{ { connectionHeader: "", upgradeHeader: "", shouldError: true, }, { connectionHeader: "Upgrade", upgradeHeader: "", shouldError: true, }, { connectionHeader: "", upgradeHeader: "SPDY/3.1", shouldError: true, }, { connectionHeader: "Upgrade", upgradeHeader: "SPDY/3.1", shouldError: false, }, } for i, testCase := range testCases { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { upgrader := NewResponseUpgrader() conn := upgrader.UpgradeResponse(w, req, nil) haveErr := conn == nil if e, a := testCase.shouldError, haveErr; e != a { t.Fatalf("%d: expected shouldErr=%t, got %t", i, testCase.shouldError, haveErr) } if haveErr { return } if conn == nil { t.Fatalf("%d: unexpected nil conn", i) } defer conn.Close() })) defer server.Close() req, err := http.NewRequest(http.MethodGet, server.URL, nil) if err != nil { t.Fatalf("%d: error creating request: %s", i, err) } req.Header.Set("Connection", testCase.connectionHeader) req.Header.Set("Upgrade", testCase.upgradeHeader) client := &http.Client{} resp, err := client.Do(req) if err != nil { t.Fatalf("%d: unexpected non-nil err from client.Do: %s", i, err) } if testCase.shouldError { continue } if resp.StatusCode != http.StatusSwitchingProtocols { t.Fatalf("%d: expected status 101 switching protocols, got %d", i, resp.StatusCode) } } } golang-k8s-streaming-0.36.2/pkg/httpstream/wsstream/000077500000000000000000000000001521404620200223605ustar00rootroot00000000000000golang-k8s-streaming-0.36.2/pkg/httpstream/wsstream/conn.go000066400000000000000000000361741521404620200236570ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package wsstream import ( "encoding/base64" "fmt" "io" "net/http" "strings" "time" "golang.org/x/net/websocket" "k8s.io/klog/v2" "k8s.io/streaming/pkg/httpstream" "k8s.io/streaming/pkg/runtime" ) const WebSocketProtocolHeader = "Sec-Websocket-Protocol" // The Websocket subprotocol "channel.k8s.io" prepends each binary message with a byte indicating // the channel number (zero indexed) the message was sent on. Messages in both directions should // prefix their messages with this channel byte. When used for remote execution, the channel numbers // are by convention defined to match the POSIX file-descriptors assigned to STDIN, STDOUT, and STDERR // (0, 1, and 2). No other conversion is performed on the raw subprotocol - writes are sent as they // are received by the server. // // Example client session: // // CONNECT http://server.com with subprotocol "channel.k8s.io" // WRITE []byte{0, 102, 111, 111, 10} # send "foo\n" on channel 0 (STDIN) // READ []byte{1, 10} # receive "\n" on channel 1 (STDOUT) // CLOSE const ChannelWebSocketProtocol = "channel.k8s.io" // The Websocket subprotocol "base64.channel.k8s.io" base64 encodes each message with a character // indicating the channel number (zero indexed) the message was sent on. Messages in both directions // should prefix their messages with this channel char. When used for remote execution, the channel // numbers are by convention defined to match the POSIX file-descriptors assigned to STDIN, STDOUT, // and STDERR ('0', '1', and '2'). The data received on the server is base64 decoded (and must be // be valid) and data written by the server to the client is base64 encoded. // // Example client session: // // CONNECT http://server.com with subprotocol "base64.channel.k8s.io" // WRITE []byte{48, 90, 109, 57, 118, 67, 103, 111, 61} # send "foo\n" (base64: "Zm9vCgo=") on channel '0' (STDIN) // READ []byte{49, 67, 103, 61, 61} # receive "\n" (base64: "Cg==") on channel '1' (STDOUT) // CLOSE const Base64ChannelWebSocketProtocol = "base64.channel.k8s.io" const streamCloseSignal = 255 type codecType int const ( rawCodec codecType = iota base64Codec ) type ChannelType int const ( IgnoreChannel ChannelType = iota ReadChannel WriteChannel ReadWriteChannel ) // IsWebSocketRequest returns true if the incoming request contains connection upgrade headers // for WebSockets. func IsWebSocketRequest(req *http.Request) bool { if !strings.EqualFold(req.Header.Get("Upgrade"), "websocket") { return false } return httpstream.IsUpgradeRequest(req) } // IsWebSocketRequestWithStreamCloseProtocol returns true if the request contains headers // identifying that it is requesting a websocket upgrade with a remotecommand protocol // version that supports the "CLOSE" signal; false otherwise. func IsWebSocketRequestWithStreamCloseProtocol(req *http.Request) bool { if !IsWebSocketRequest(req) { return false } requestedProtocols := strings.TrimSpace(req.Header.Get(WebSocketProtocolHeader)) for _, requestedProtocol := range strings.Split(requestedProtocols, ",") { if protocolSupportsStreamClose(strings.TrimSpace(requestedProtocol)) { return true } } return false } // IsWebSocketRequestWithTunnelingProtocol returns true if the request contains headers // identifying that it is requesting a websocket upgrade with a tunneling protocol; // false otherwise. func IsWebSocketRequestWithTunnelingProtocol(req *http.Request) bool { if !IsWebSocketRequest(req) { return false } requestedProtocols := strings.TrimSpace(req.Header.Get(WebSocketProtocolHeader)) for _, requestedProtocol := range strings.Split(requestedProtocols, ",") { if protocolSupportsWebsocketTunneling(strings.TrimSpace(requestedProtocol)) { return true } } return false } // IgnoreReceives reads from a WebSocket until it is closed, then returns. If timeout is set, the // read and write deadlines are pushed every time a new message is received. // // Contextual logging: IgnoreReceivesWithLogger should be used instead of IgnoreReceives in code which uses contextual logging. func IgnoreReceives(ws *websocket.Conn, timeout time.Duration) { IgnoreReceivesWithLogger(klog.Background(), ws, timeout) } // IgnoreReceivesWithLogger reads from a WebSocket until it is closed, then returns. If timeout is set, the // read and write deadlines are pushed every time a new message is received. func IgnoreReceivesWithLogger(logger klog.Logger, ws *websocket.Conn, timeout time.Duration) { defer runtime.HandleCrashWithLogger(logger) var data []byte for { resetTimeout(ws, timeout) if err := websocket.Message.Receive(ws, &data); err != nil { return } } } // handshake ensures the provided user protocol matches one of the allowed protocols. It returns // no error if no protocol is specified. func handshake(config *websocket.Config, req *http.Request, allowed []string) error { protocols := config.Protocol if len(protocols) == 0 { protocols = []string{""} } for _, protocol := range protocols { for _, allow := range allowed { if allow == protocol { config.Protocol = []string{protocol} return nil } } } return fmt.Errorf("requested protocol(s) are not supported: %v; supports %v", config.Protocol, allowed) } // ChannelProtocolConfig describes a websocket subprotocol with channels. type ChannelProtocolConfig struct { Binary bool Channels []ChannelType } // NewDefaultChannelProtocols returns a channel protocol map with the // subprotocols "", "channel.k8s.io", "base64.channel.k8s.io" and the given // channels. func NewDefaultChannelProtocols(channels []ChannelType) map[string]ChannelProtocolConfig { return map[string]ChannelProtocolConfig{ "": {Binary: true, Channels: channels}, ChannelWebSocketProtocol: {Binary: true, Channels: channels}, Base64ChannelWebSocketProtocol: {Binary: false, Channels: channels}, } } // Conn supports sending multiple binary channels over a websocket connection. type Conn struct { protocols map[string]ChannelProtocolConfig selectedProtocol string channels []*websocketChannel codec codecType ready chan struct{} ws *websocket.Conn timeout time.Duration } // NewConn creates a WebSocket connection that supports a set of channels. Channels begin each // web socket message with a single byte indicating the channel number (0-N). 255 is reserved for // future use. The channel types for each channel are passed as an array, supporting the different // duplex modes. Read and Write refer to whether the channel can be used as a Reader or Writer. // // The protocols parameter maps subprotocol names to ChannelProtocols. The empty string subprotocol // name is used if websocket.Config.Protocol is empty. func NewConn(protocols map[string]ChannelProtocolConfig) *Conn { return &Conn{ ready: make(chan struct{}), protocols: protocols, } } // SetIdleTimeout sets the interval for both reads and writes before timeout. If not specified, // there is no timeout on the connection. func (conn *Conn) SetIdleTimeout(duration time.Duration) { conn.timeout = duration } // SetWriteDeadline sets a timeout on writing to the websocket connection. The // passed "duration" identifies how far into the future the write must complete // by before the timeout fires. func (conn *Conn) SetWriteDeadline(duration time.Duration) { conn.ws.SetWriteDeadline(time.Now().Add(duration)) //nolint:errcheck } // Open the connection and create channels for reading and writing. It returns // the selected subprotocol, a slice of channels and an error. func (conn *Conn) Open(w http.ResponseWriter, req *http.Request) (string, []io.ReadWriteCloser, error) { // serveHTTPComplete is channel that is closed/selected when "websocket#ServeHTTP" finishes. serveHTTPComplete := make(chan struct{}) // Ensure panic in spawned goroutine is propagated into the parent goroutine. panicChan := make(chan any, 1) go func() { // If websocket server returns, propagate panic if necessary. Otherwise, // signal HTTPServe finished by closing "serveHTTPComplete". defer func() { if p := recover(); p != nil { panicChan <- p } else { close(serveHTTPComplete) } }() websocket.Server{Handshake: conn.handshake, Handler: conn.handle}.ServeHTTP(w, req) }() // In normal circumstances, "websocket.Server#ServeHTTP" calls "initialize" which closes // "conn.ready" and then blocks until serving is complete. select { case <-conn.ready: klog.FromContext(req.Context()).V(8).Info("websocket server initialized--serving") case <-serveHTTPComplete: // websocket server returned before completing initialization; cleanup and return error. conn.closeNonThreadSafe() //nolint:errcheck return "", nil, fmt.Errorf("websocket server finished before becoming ready") case p := <-panicChan: panic(p) } rwc := make([]io.ReadWriteCloser, len(conn.channels)) for i := range conn.channels { rwc[i] = conn.channels[i] } return conn.selectedProtocol, rwc, nil } func (conn *Conn) initialize(ws *websocket.Conn) { negotiated := ws.Config().Protocol conn.selectedProtocol = negotiated[0] p := conn.protocols[conn.selectedProtocol] if p.Binary { conn.codec = rawCodec } else { conn.codec = base64Codec } conn.ws = ws conn.channels = make([]*websocketChannel, len(p.Channels)) for i, t := range p.Channels { switch t { case ReadChannel: conn.channels[i] = newWebsocketChannel(conn, byte(i), true, false) case WriteChannel: conn.channels[i] = newWebsocketChannel(conn, byte(i), false, true) case ReadWriteChannel: conn.channels[i] = newWebsocketChannel(conn, byte(i), true, true) case IgnoreChannel: conn.channels[i] = newWebsocketChannel(conn, byte(i), false, false) } } close(conn.ready) } func (conn *Conn) handshake(config *websocket.Config, req *http.Request) error { supportedProtocols := make([]string, 0, len(conn.protocols)) for p := range conn.protocols { supportedProtocols = append(supportedProtocols, p) } return handshake(config, req, supportedProtocols) } func (conn *Conn) resetTimeout() { if conn.timeout > 0 { conn.ws.SetDeadline(time.Now().Add(conn.timeout)) } } // closeNonThreadSafe cleans up by closing streams and the websocket // connection *without* waiting for the "ready" channel. func (conn *Conn) closeNonThreadSafe() error { for _, s := range conn.channels { s.Close() } var err error if conn.ws != nil { err = conn.ws.Close() } return err } // Close is only valid after Open has been called func (conn *Conn) Close() error { <-conn.ready return conn.closeNonThreadSafe() } // protocolSupportsStreamClose returns true if the passed protocol // supports the stream close signal (currently only V5 remotecommand); // false otherwise. func protocolSupportsStreamClose(protocol string) bool { return protocol == "v5.channel.k8s.io" } // protocolSupportsWebsocketTunneling returns true if the passed protocol // is a tunneled Kubernetes spdy protocol; false otherwise. func protocolSupportsWebsocketTunneling(protocol string) bool { return strings.HasPrefix(protocol, "SPDY/3.1+") && strings.HasSuffix(protocol, ".k8s.io") } // handle implements a websocket handler. func (conn *Conn) handle(ws *websocket.Conn) { conn.initialize(ws) defer conn.Close() supportsStreamClose := protocolSupportsStreamClose(conn.selectedProtocol) // conn.handle is typically used on the server-side and thus we have a request, // but don't assume that and use klog.Background as fallback. logger := klog.Background() if req := ws.Request(); req != nil { logger = klog.FromContext(req.Context()) } for { conn.resetTimeout() var data []byte if err := websocket.Message.Receive(ws, &data); err != nil { if err != io.EOF { logger.Error(err, "Error on socket receive") } break } if len(data) == 0 { continue } if supportsStreamClose && data[0] == streamCloseSignal { if len(data) != 2 { logger.Error(nil, "Single channel byte should follow stream close signal", "receivedLength", len(data)-1) break } else { channel := data[1] if int(channel) >= len(conn.channels) { logger.Error(nil, "Close is targeted for a channel that is not valid, possible protocol error", "channel", channel) break } logger.V(4).Info("Received half-close signal from client, close stream", "channel", channel) conn.channels[channel].Close() // After first Close, other closes are noop. } continue } channel := data[0] if conn.codec == base64Codec { channel = channel - '0' } data = data[1:] if int(channel) >= len(conn.channels) { logger.V(6).Info("Frame is targeted for a reader that is not valid, possible protocol error", "channel", channel) continue } if _, err := conn.channels[channel].DataFromSocket(data); err != nil { logger.Error(err, "Unable to write frame", "sendLength", len(data), "channel", channel, "err", err) continue } } } // write multiplexes the specified channel onto the websocket func (conn *Conn) write(num byte, data []byte) (int, error) { conn.resetTimeout() switch conn.codec { case rawCodec: frame := make([]byte, len(data)+1) frame[0] = num copy(frame[1:], data) if err := websocket.Message.Send(conn.ws, frame); err != nil { return 0, err } case base64Codec: frame := string('0'+num) + base64.StdEncoding.EncodeToString(data) if err := websocket.Message.Send(conn.ws, frame); err != nil { return 0, err } } return len(data), nil } // websocketChannel represents a channel in a connection type websocketChannel struct { conn *Conn num byte r io.Reader w io.WriteCloser read, write bool } // newWebsocketChannel creates a pipe for writing to a websocket. Do not write to this pipe // prior to the connection being opened. It may be no, half, or full duplex depending on // read and write. func newWebsocketChannel(conn *Conn, num byte, read, write bool) *websocketChannel { r, w := io.Pipe() return &websocketChannel{conn, num, r, w, read, write} } func (p *websocketChannel) Write(data []byte) (int, error) { if !p.write { return len(data), nil } return p.conn.write(p.num, data) } // DataFromSocket is invoked by the connection receiver to move data from the connection // into a specific channel. func (p *websocketChannel) DataFromSocket(data []byte) (int, error) { if !p.read { return len(data), nil } switch p.conn.codec { case rawCodec: return p.w.Write(data) case base64Codec: dst := make([]byte, len(data)) n, err := base64.StdEncoding.Decode(dst, data) if err != nil { return 0, err } return p.w.Write(dst[:n]) } return 0, nil } func (p *websocketChannel) Read(data []byte) (int, error) { if !p.read { return 0, io.EOF } return p.r.Read(data) } func (p *websocketChannel) Close() error { return p.w.Close() } golang-k8s-streaming-0.36.2/pkg/httpstream/wsstream/conn_test.go000066400000000000000000000265521521404620200247150ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package wsstream import ( "encoding/base64" "io" "net/http" "net/http/httptest" "reflect" "sync" "testing" "golang.org/x/net/websocket" ) func newServer(handler http.Handler) (*httptest.Server, string) { server := httptest.NewServer(handler) serverAddr := server.Listener.Addr().String() return server, serverAddr } func TestRawConn(t *testing.T) { channels := []ChannelType{ReadWriteChannel, ReadWriteChannel, IgnoreChannel, ReadChannel, WriteChannel} conn := NewConn(NewDefaultChannelProtocols(channels)) s, addr := newServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { conn.Open(w, req) })) defer s.Close() client, err := websocket.Dial("ws://"+addr, "", "http://localhost/") if err != nil { t.Fatal(err) } defer client.Close() <-conn.ready wg := sync.WaitGroup{} // verify we can read a client write wg.Add(1) go func() { defer wg.Done() data, err := io.ReadAll(conn.channels[0]) if err != nil { t.Error(err) return } if !reflect.DeepEqual(data, []byte("client")) { t.Errorf("unexpected server read: %v", data) } }() if n, err := client.Write(append([]byte{0}, []byte("client")...)); err != nil || n != 7 { t.Fatalf("%d: %v", n, err) } // verify we can read a server write wg.Add(1) go func() { defer wg.Done() if n, err := conn.channels[1].Write([]byte("server")); err != nil && n != 6 { t.Errorf("%d: %v", n, err) } }() data := make([]byte, 1024) if n, err := io.ReadAtLeast(client, data, 6); n != 7 || err != nil { t.Fatalf("%d: %v", n, err) } if !reflect.DeepEqual(data[:7], append([]byte{1}, []byte("server")...)) { t.Errorf("unexpected client read: %v", data[:7]) } // verify that an ignore channel is empty in both directions. if n, err := conn.channels[2].Write([]byte("test")); n != 4 || err != nil { t.Errorf("writes should be ignored") } data = make([]byte, 1024) if n, err := conn.channels[2].Read(data); n != 0 || err != io.EOF { t.Errorf("reads should be ignored") } // verify that a write to a Read channel doesn't block if n, err := conn.channels[3].Write([]byte("test")); n != 4 || err != nil { t.Errorf("writes should be ignored") } // verify that a read from a Write channel doesn't block data = make([]byte, 1024) if n, err := conn.channels[4].Read(data); n != 0 || err != io.EOF { t.Errorf("reads should be ignored") } // verify that a client write to a Write channel doesn't block (is dropped) if n, err := client.Write(append([]byte{4}, []byte("ignored")...)); err != nil || n != 8 { t.Fatalf("%d: %v", n, err) } client.Close() wg.Wait() } func TestBase64Conn(t *testing.T) { conn := NewConn(NewDefaultChannelProtocols([]ChannelType{ReadWriteChannel, ReadWriteChannel})) s, addr := newServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { conn.Open(w, req) })) defer s.Close() config, err := websocket.NewConfig("ws://"+addr, "http://localhost/") if err != nil { t.Fatal(err) } config.Protocol = []string{"base64.channel.k8s.io"} client, err := websocket.DialConfig(config) if err != nil { t.Fatal(err) } defer client.Close() <-conn.ready wg := sync.WaitGroup{} wg.Add(1) go func() { defer wg.Done() data, err := io.ReadAll(conn.channels[0]) if err != nil { t.Error(err) return } if !reflect.DeepEqual(data, []byte("client")) { t.Errorf("unexpected server read: %s", string(data)) } }() clientData := base64.StdEncoding.EncodeToString([]byte("client")) if n, err := client.Write(append([]byte{'0'}, clientData...)); err != nil || n != len(clientData)+1 { t.Fatalf("%d: %v", n, err) } wg.Add(1) go func() { defer wg.Done() if n, err := conn.channels[1].Write([]byte("server")); err != nil && n != 6 { t.Errorf("%d: %v", n, err) } }() data := make([]byte, 1024) if n, err := io.ReadAtLeast(client, data, 9); n != 9 || err != nil { t.Fatalf("%d: %v", n, err) } expect := []byte(base64.StdEncoding.EncodeToString([]byte("server"))) if !reflect.DeepEqual(data[:9], append([]byte{'1'}, expect...)) { t.Errorf("unexpected client read: %v", data[:9]) } client.Close() wg.Wait() } type versionTest struct { supported map[string]bool // protocol -> binary requested []string error bool expected string } func versionTests() []versionTest { const ( binary = true base64 = false ) return []versionTest{ { supported: nil, requested: []string{"raw"}, error: true, }, { supported: map[string]bool{"": binary, "raw": binary, "base64": base64}, requested: nil, expected: "", }, { supported: map[string]bool{"": binary, "raw": binary, "base64": base64}, requested: []string{"v1.raw"}, error: true, }, { supported: map[string]bool{"": binary, "raw": binary, "base64": base64}, requested: []string{"v1.raw", "v1.base64"}, error: true, }, { supported: map[string]bool{"": binary, "raw": binary, "base64": base64}, requested: []string{"v1.raw", "raw"}, expected: "raw", }, { supported: map[string]bool{"": binary, "v1.raw": binary, "v1.base64": base64, "v2.raw": binary, "v2.base64": base64}, requested: []string{"v1.raw"}, expected: "v1.raw", }, { supported: map[string]bool{"": binary, "v1.raw": binary, "v1.base64": base64, "v2.raw": binary, "v2.base64": base64}, requested: []string{"v2.base64"}, expected: "v2.base64", }, } } func TestVersionedConn(t *testing.T) { for i, test := range versionTests() { func() { supportedProtocols := map[string]ChannelProtocolConfig{} for p, binary := range test.supported { supportedProtocols[p] = ChannelProtocolConfig{ Binary: binary, Channels: []ChannelType{ReadWriteChannel}, } } conn := NewConn(supportedProtocols) // note that it's not enough to wait for conn.ready to avoid a race here. Hence, // we use a channel. selectedProtocol := make(chan string) s, addr := newServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { p, _, _ := conn.Open(w, req) selectedProtocol <- p })) defer s.Close() config, err := websocket.NewConfig("ws://"+addr, "http://localhost/") if err != nil { t.Fatal(err) } config.Protocol = test.requested client, err := websocket.DialConfig(config) if err != nil { if !test.error { t.Fatalf("test %d: didn't expect error: %v", i, err) } else { return } } defer client.Close() if test.error && err == nil { t.Fatalf("test %d: expected an error", i) } <-conn.ready if got, expected := <-selectedProtocol, test.expected; got != expected { t.Fatalf("test %d: unexpected protocol version: got=%s expected=%s", i, got, expected) } }() } } func TestIsWebSocketRequestWithStreamCloseProtocol(t *testing.T) { tests := map[string]struct { headers map[string]string expected bool }{ "No headers returns false": { headers: map[string]string{}, expected: false, }, "Only connection upgrade header is false": { headers: map[string]string{ "Connection": "upgrade", }, expected: false, }, "Only websocket upgrade header is false": { headers: map[string]string{ "Upgrade": "websocket", }, expected: false, }, "Only websocket and connection upgrade headers is false": { headers: map[string]string{ "Connection": "upgrade", "Upgrade": "websocket", }, expected: false, }, "Missing connection/upgrade header is false": { headers: map[string]string{ "Upgrade": "websocket", WebSocketProtocolHeader: "v5.channel.k8s.io", }, expected: false, }, "Websocket connection upgrade headers with v5 protocol is true": { headers: map[string]string{ "Connection": "upgrade", "Upgrade": "websocket", WebSocketProtocolHeader: "v5.channel.k8s.io", }, expected: true, }, "Websocket connection upgrade headers with wrong case v5 protocol is false": { headers: map[string]string{ "Connection": "upgrade", "Upgrade": "websocket", WebSocketProtocolHeader: "v5.CHANNEL.k8s.io", // header value is case-sensitive }, expected: false, }, "Websocket connection upgrade headers with v4 protocol is false": { headers: map[string]string{ "Connection": "upgrade", "Upgrade": "websocket", WebSocketProtocolHeader: "v4.channel.k8s.io", }, expected: false, }, "Websocket connection upgrade headers with multiple protocols but missing v5 is false": { headers: map[string]string{ "Connection": "upgrade", "Upgrade": "websocket", WebSocketProtocolHeader: "v4.channel.k8s.io,v3.channel.k8s.io,v2.channel.k8s.io", }, expected: false, }, "Websocket connection upgrade headers with multiple protocols including v5 and spaces is true": { headers: map[string]string{ "Connection": "upgrade", "Upgrade": "websocket", WebSocketProtocolHeader: "v5.channel.k8s.io, v4.channel.k8s.io", }, expected: true, }, "Websocket connection upgrade headers with multiple protocols out of order including v5 and spaces is true": { headers: map[string]string{ "Connection": "upgrade", "Upgrade": "websocket", WebSocketProtocolHeader: "v4.channel.k8s.io, v5.channel.k8s.io, v3.channel.k8s.io", }, expected: true, }, "Websocket connection upgrade headers key is case-insensitive": { headers: map[string]string{ "Connection": "upgrade", "Upgrade": "websocket", "sec-websocket-protocol": "v4.channel.k8s.io, v5.channel.k8s.io, v3.channel.k8s.io", }, expected: true, }, } for name, test := range tests { req, err := http.NewRequest(http.MethodGet, "http://www.example.com/", nil) if err != nil { t.Fatalf("unexpected error: %v", err) } for key, value := range test.headers { req.Header.Add(key, value) } actual := IsWebSocketRequestWithStreamCloseProtocol(req) if actual != test.expected { t.Errorf("%s: expected (%t), got (%t)", name, test.expected, actual) } } } func TestProtocolSupportsStreamClose(t *testing.T) { tests := map[string]struct { protocol string expected bool }{ "empty protocol returns false": { protocol: "", expected: false, }, "not binary protocol returns false": { protocol: "base64.channel.k8s.io", expected: false, }, "V1 protocol returns false": { protocol: "channel.k8s.io", expected: false, }, "V4 protocol returns false": { protocol: "v4.channel.k8s.io", expected: false, }, "V5 protocol returns true": { protocol: "v5.channel.k8s.io", expected: true, }, "V5 protocol wrong case returns false": { protocol: "V5.channel.K8S.io", expected: false, }, } for name, test := range tests { actual := protocolSupportsStreamClose(test.protocol) if actual != test.expected { t.Errorf("%s: expected (%t), got (%t)", name, test.expected, actual) } } } golang-k8s-streaming-0.36.2/pkg/httpstream/wsstream/doc.go000066400000000000000000000055631521404620200234650ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package wsstream contains utilities for streaming content over WebSockets. // The Conn type allows callers to multiplex multiple read/write channels over // a single websocket. // // "channel.k8s.io" // // The Websocket RemoteCommand subprotocol "channel.k8s.io" prepends each binary message with a // byte indicating the channel number (zero indexed) the message was sent on. Messages in both // directions should prefix their messages with this channel byte. Used for remote execution, // the channel numbers are by convention defined to match the POSIX file-descriptors assigned // to STDIN, STDOUT, and STDERR (0, 1, and 2). No other conversion is performed on the raw // subprotocol - writes are sent as they are received by the server. // // Example client session: // // CONNECT http://server.com with subprotocol "channel.k8s.io" // WRITE []byte{0, 102, 111, 111, 10} # send "foo\n" on channel 0 (STDIN) // READ []byte{1, 10} # receive "\n" on channel 1 (STDOUT) // CLOSE // // "v2.channel.k8s.io" // // The second Websocket subprotocol version "v2.channel.k8s.io" is the same as version 1, // but it is the first "versioned" subprotocol. // // "v3.channel.k8s.io" // // The third version of the Websocket RemoteCommand subprotocol adds another channel // for terminal resizing events. This channel is prepended with the byte '3', and it // transmits two window sizes (encoding TerminalSize struct) with integers in the range // (0,65536]. // // "v4.channel.k8s.io" // // The fourth version of the Websocket RemoteCommand subprotocol adds a channel for // errors. This channel returns structured errors containing process exit codes. The // error is "apierrors.StatusError{}". // // "v5.channel.k8s.io" // // The fifth version of the Websocket RemoteCommand subprotocol adds a CLOSE signal, // which is sent as the first byte of the message. The second byte is the channel // id. This CLOSE signal is handled by the websocket server by closing the stream, // allowing the other streams to complete transmission if necessary, and gracefully // shutdown the connection. // // Example client session: // // CONNECT http://server.com with subprotocol "v5.channel.k8s.io" // WRITE []byte{0, 102, 111, 111, 10} # send "foo\n" on channel 0 (STDIN) // WRITE []byte{255, 0} # send CLOSE signal (STDIN) // CLOSE package wsstream golang-k8s-streaming-0.36.2/pkg/httpstream/wsstream/stream.go000066400000000000000000000140111521404620200241770ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package wsstream import ( "context" "encoding/base64" "io" "net/http" "sync" "time" "golang.org/x/net/websocket" "k8s.io/klog/v2" "k8s.io/streaming/pkg/runtime" ) // The WebSocket subprotocol "binary.k8s.io" will only send messages to the // client and ignore messages sent to the server. The received messages are // the exact bytes written to the stream. Zero byte messages are possible. const binaryWebSocketProtocol = "binary.k8s.io" // The WebSocket subprotocol "base64.binary.k8s.io" will only send messages to the // client and ignore messages sent to the server. The received messages are // a base64 version of the bytes written to the stream. Zero byte messages are // possible. const base64BinaryWebSocketProtocol = "base64.binary.k8s.io" // ReaderProtocolConfig describes a websocket subprotocol with one stream. type ReaderProtocolConfig struct { Binary bool } // NewDefaultReaderProtocols returns a stream protocol map with the // subprotocols "", "channel.k8s.io", "base64.channel.k8s.io". func NewDefaultReaderProtocols() map[string]ReaderProtocolConfig { return map[string]ReaderProtocolConfig{ "": {Binary: true}, binaryWebSocketProtocol: {Binary: true}, base64BinaryWebSocketProtocol: {Binary: false}, } } // Reader supports returning an arbitrary byte stream over a websocket channel. type Reader struct { logger klog.Logger err chan error r io.Reader ping bool timeout time.Duration protocols map[string]ReaderProtocolConfig selectedProtocol string handleCrash func(ctx context.Context, additionalHandlers ...func(context.Context, interface{})) // overridable for testing } // NewReader creates a WebSocket pipe that will copy the contents of r to a provided // WebSocket connection. If ping is true, a zero length message will be sent to the client // before the stream begins reading. // // The protocols parameter maps subprotocol names to StreamProtocols. The empty string // subprotocol name is used if websocket.Config.Protocol is empty. // //logcheck:context // NewReaderWithLogger should be used instead of NewReader in code which supports contextual logging. func NewReader(r io.Reader, ping bool, protocols map[string]ReaderProtocolConfig) *Reader { return NewReaderWithLogger(klog.Background(), r, ping, protocols) } // NewReaderWithLogger creates a WebSocket pipe that will copy the contents of r to a provided // WebSocket connection. If ping is true, a zero length message will be sent to the client // before the stream begins reading. // // The protocols parameter maps subprotocol names to StreamProtocols. The empty string // subprotocol name is used if websocket.Config.Protocol is empty. func NewReaderWithLogger(logger klog.Logger, r io.Reader, ping bool, protocols map[string]ReaderProtocolConfig) *Reader { return &Reader{ logger: logger, r: r, err: make(chan error), ping: ping, protocols: protocols, handleCrash: runtime.HandleCrashWithContext, } } // SetIdleTimeout sets the interval for both reads and writes before timeout. If not specified, // there is no timeout on the reader. func (r *Reader) SetIdleTimeout(duration time.Duration) { r.timeout = duration } func (r *Reader) handshake(config *websocket.Config, req *http.Request) error { supportedProtocols := make([]string, 0, len(r.protocols)) for p := range r.protocols { supportedProtocols = append(supportedProtocols, p) } return handshake(config, req, supportedProtocols) } // Copy the reader to the response. The created WebSocket is closed after this // method completes. func (r *Reader) Copy(w http.ResponseWriter, req *http.Request) error { go func() { defer r.handleCrash(req.Context()) websocket.Server{Handshake: r.handshake, Handler: r.handle}.ServeHTTP(w, req) }() return <-r.err } // handle implements a WebSocket handler. func (r *Reader) handle(ws *websocket.Conn) { // Close the connection when the client requests it, or when we finish streaming, whichever happens first closeConnOnce := &sync.Once{} closeConn := func() { closeConnOnce.Do(func() { ws.Close() }) } negotiated := ws.Config().Protocol r.selectedProtocol = negotiated[0] defer close(r.err) defer closeConn() go func() { defer runtime.HandleCrashWithLogger(r.logger) // This blocks until the connection is closed. // Client should not send anything. IgnoreReceivesWithLogger(r.logger, ws, r.timeout) // Once the client closes, we should also close closeConn() }() r.err <- messageCopy(ws, r.r, !r.protocols[r.selectedProtocol].Binary, r.ping, r.timeout) } func resetTimeout(ws *websocket.Conn, timeout time.Duration) { if timeout > 0 { ws.SetDeadline(time.Now().Add(timeout)) } } func messageCopy(ws *websocket.Conn, r io.Reader, base64Encode, ping bool, timeout time.Duration) error { buf := make([]byte, 2048) if ping { resetTimeout(ws, timeout) if base64Encode { if err := websocket.Message.Send(ws, ""); err != nil { return err } } else { if err := websocket.Message.Send(ws, []byte{}); err != nil { return err } } } for { resetTimeout(ws, timeout) n, err := r.Read(buf) if err != nil { if err == io.EOF { return nil } return err } if n > 0 { if base64Encode { if err := websocket.Message.Send(ws, base64.StdEncoding.EncodeToString(buf[:n])); err != nil { return err } } else { if err := websocket.Message.Send(ws, buf[:n]); err != nil { return err } } } } } golang-k8s-streaming-0.36.2/pkg/httpstream/wsstream/stream_test.go000066400000000000000000000176221521404620200252510ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package wsstream import ( "bytes" "context" "encoding/base64" "fmt" "io" "net/http" "reflect" "strings" "testing" "time" "golang.org/x/net/websocket" ) func TestStream(t *testing.T) { input := "some random text" //nolint:logcheck // Intentionally uses the old API. r := NewReader(bytes.NewBuffer([]byte(input)), true, NewDefaultReaderProtocols()) r.SetIdleTimeout(time.Second) data, err := readWebSocket(r, t, nil) if !reflect.DeepEqual(data, []byte(input)) { t.Errorf("unexpected server read: %v", data) } if err != nil { t.Fatal(err) } } func TestStreamPing(t *testing.T) { input := "some random text" //nolint:logcheck // Intentionally uses the old API. r := NewReader(bytes.NewBuffer([]byte(input)), true, NewDefaultReaderProtocols()) r.SetIdleTimeout(time.Second) err := expectWebSocketFrames(r, t, nil, [][]byte{ {}, []byte(input), }) if err != nil { t.Fatal(err) } } func TestStreamBase64(t *testing.T) { input := "some random text" encoded := base64.StdEncoding.EncodeToString([]byte(input)) //nolint:logcheck // Intentionally uses the old API. r := NewReader(bytes.NewBuffer([]byte(input)), true, NewDefaultReaderProtocols()) data, err := readWebSocket(r, t, nil, "base64.binary.k8s.io") if !reflect.DeepEqual(data, []byte(encoded)) { t.Errorf("unexpected server read: %v\n%v", data, []byte(encoded)) } if err != nil { t.Fatal(err) } } func TestStreamVersionedBase64(t *testing.T) { input := "some random text" encoded := base64.StdEncoding.EncodeToString([]byte(input)) //nolint:logcheck // Intentionally uses the old API. r := NewReader(bytes.NewBuffer([]byte(input)), true, map[string]ReaderProtocolConfig{ "": {Binary: true}, "binary.k8s.io": {Binary: true}, "base64.binary.k8s.io": {Binary: false}, "v1.binary.k8s.io": {Binary: true}, "v1.base64.binary.k8s.io": {Binary: false}, "v2.binary.k8s.io": {Binary: true}, "v2.base64.binary.k8s.io": {Binary: false}, }) data, err := readWebSocket(r, t, nil, "v2.base64.binary.k8s.io") if !reflect.DeepEqual(data, []byte(encoded)) { t.Errorf("unexpected server read: %v\n%v", data, []byte(encoded)) } if err != nil { t.Fatal(err) } } func TestStreamVersionedCopy(t *testing.T) { for i, test := range versionTests() { func() { supportedProtocols := map[string]ReaderProtocolConfig{} for p, binary := range test.supported { supportedProtocols[p] = ReaderProtocolConfig{ Binary: binary, } } input := "some random text" //nolint:logcheck // Intentionally uses the old API. r := NewReader(bytes.NewBuffer([]byte(input)), true, supportedProtocols) s, addr := newServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { err := r.Copy(w, req) if err != nil { w.WriteHeader(503) } })) defer s.Close() config, err := websocket.NewConfig("ws://"+addr, "http://localhost/") if err != nil { t.Error(err) return } config.Protocol = test.requested client, err := websocket.DialConfig(config) if err != nil { if !test.error { t.Errorf("test %d: didn't expect error: %v", i, err) } return } defer client.Close() if test.error && err == nil { t.Errorf("test %d: expected an error", i) return } <-r.err if got, expected := r.selectedProtocol, test.expected; got != expected { t.Errorf("test %d: unexpected protocol version: got=%s expected=%s", i, got, expected) } }() } } func TestStreamError(t *testing.T) { input := "some random text" errs := &errorReader{ reads: [][]byte{ []byte("some random"), []byte(" text"), }, err: fmt.Errorf("bad read"), } //nolint:logcheck // Intentionally uses the old API. r := NewReader(errs, false, NewDefaultReaderProtocols()) data, err := readWebSocket(r, t, nil) if !reflect.DeepEqual(data, []byte(input)) { t.Errorf("unexpected server read: %v", data) } if err == nil || err.Error() != "bad read" { t.Fatal(err) } } func TestStreamSurvivesPanic(t *testing.T) { input := "some random text" errs := &errorReader{ reads: [][]byte{ []byte("some random"), []byte(" text"), }, panicMessage: "bad read", } //nolint:logcheck // Intentionally uses the old API. r := NewReader(errs, false, NewDefaultReaderProtocols()) // do not call runtime.HandleCrash() in handler. Otherwise, the tests are interrupted. r.handleCrash = func(_ context.Context, additionalHandlers ...func(context.Context, interface{})) { recover() } data, err := readWebSocket(r, t, nil) if !reflect.DeepEqual(data, []byte(input)) { t.Errorf("unexpected server read: %v", data) } if err != nil { t.Fatal(err) } } func TestStreamClosedDuringRead(t *testing.T) { for i := 0; i < 25; i++ { ch := make(chan struct{}) input := "some random text" errs := &errorReader{ reads: [][]byte{ []byte("some random"), []byte(" text"), }, err: fmt.Errorf("stuff"), pause: ch, } //nolint:logcheck // Intentionally uses the old API. r := NewReader(errs, false, NewDefaultReaderProtocols()) data, err := readWebSocket(r, t, func(c *websocket.Conn) { c.Close() close(ch) }) // verify that the data returned by the server on an early close always has a specific error if err == nil || !strings.Contains(err.Error(), "use of closed network connection") { t.Fatal(err) } // verify that the data returned is a strict subset of the input if !bytes.HasPrefix([]byte(input), data) && len(data) != 0 { t.Fatalf("unexpected server read: %q", string(data)) } } } type errorReader struct { reads [][]byte err error panicMessage string pause chan struct{} } func (r *errorReader) Read(p []byte) (int, error) { if len(r.reads) == 0 { if r.pause != nil { <-r.pause } if len(r.panicMessage) != 0 { panic(r.panicMessage) } return 0, r.err } next := r.reads[0] r.reads = r.reads[1:] copy(p, next) return len(next), nil } func readWebSocket(r *Reader, t *testing.T, fn func(*websocket.Conn), protocols ...string) ([]byte, error) { errCh := make(chan error, 1) s, addr := newServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { errCh <- r.Copy(w, req) })) defer s.Close() config, _ := websocket.NewConfig("ws://"+addr, "http://"+addr) config.Protocol = protocols client, err := websocket.DialConfig(config) if err != nil { return nil, err } defer client.Close() if fn != nil { fn(client) } data, err := io.ReadAll(client) if err != nil { return data, err } return data, <-errCh } func expectWebSocketFrames(r *Reader, t *testing.T, fn func(*websocket.Conn), frames [][]byte, protocols ...string) error { errCh := make(chan error, 1) s, addr := newServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { errCh <- r.Copy(w, req) })) defer s.Close() config, _ := websocket.NewConfig("ws://"+addr, "http://"+addr) config.Protocol = protocols ws, err := websocket.DialConfig(config) if err != nil { return err } defer ws.Close() if fn != nil { fn(ws) } for i := range frames { var data []byte if err := websocket.Message.Receive(ws, &data); err != nil { return err } if !reflect.DeepEqual(frames[i], data) { return fmt.Errorf("frame %d did not match expected: %v", data, err) } } var data []byte if err := websocket.Message.Receive(ws, &data); err != io.EOF { return fmt.Errorf("expected no more frames: %v (%v)", err, data) } return <-errCh } golang-k8s-streaming-0.36.2/pkg/runtime/000077500000000000000000000000001521404620200200035ustar00rootroot00000000000000golang-k8s-streaming-0.36.2/pkg/runtime/runtime.go000066400000000000000000000033531521404620200220210ustar00rootroot00000000000000/* Copyright The Kubernetes Authors. 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package runtime import ( "context" "fmt" "k8s.io/klog/v2" ) // HandleError logs an asynchronous error. func HandleError(err error) { if err == nil { return } klog.Background().Error(err, "Unhandled Error") } // HandleErrorWithContext logs an asynchronous error with contextual logging when available. func HandleErrorWithContext(ctx context.Context, err error, msg string, keysAndValues ...interface{}) { if err == nil { return } klog.FromContext(ctx).Error(err, msg, keysAndValues...) } // HandleCrash recovers from panic and logs it. func HandleCrash() { HandleCrashWithLogger(klog.Background()) } // HandleCrashWithContext recovers from panic and logs it with the context logger. func HandleCrashWithContext(ctx context.Context, additionalHandlers ...func(context.Context, interface{})) { if r := recover(); r != nil { for _, fn := range additionalHandlers { fn(ctx, r) } klog.FromContext(ctx).Error(fmt.Errorf("%v", r), "Observed a panic") } } // HandleCrashWithLogger recovers from panic and logs it using the provided logger. func HandleCrashWithLogger(logger klog.Logger) { if r := recover(); r != nil { logger.Error(fmt.Errorf("%v", r), "Observed a panic") } }