mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
This change allows tsh to use HTTP proxies when HTTP_PROXY/HTTPS_PROXY is set in the environment.
151 lines
4.6 KiB
Go
151 lines
4.6 KiB
Go
/*
|
|
Copyright 2022 Gravitational, Inc.
|
|
|
|
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 client
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/gravitational/teleport/api/constants"
|
|
"github.com/gravitational/trace"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// DialProxy creates a connection to a server via an HTTP Proxy.
|
|
func DialProxy(ctx context.Context, proxyAddr, addr string) (net.Conn, error) {
|
|
return DialProxyWithDialer(ctx, proxyAddr, addr, &net.Dialer{})
|
|
}
|
|
|
|
// DialProxyWithDialer creates a connection to a server via an HTTP Proxy using a specified dialer.
|
|
func DialProxyWithDialer(ctx context.Context, proxyAddr, addr string, dialer ContextDialer) (net.Conn, error) {
|
|
conn, err := dialer.DialContext(ctx, "tcp", proxyAddr)
|
|
if err != nil {
|
|
log.Warnf("Unable to dial to proxy: %v: %v.", proxyAddr, err)
|
|
return nil, trace.ConvertSystemError(err)
|
|
}
|
|
|
|
connectReq := &http.Request{
|
|
Method: http.MethodConnect,
|
|
URL: &url.URL{Opaque: addr},
|
|
Host: addr,
|
|
Header: make(http.Header),
|
|
}
|
|
|
|
if err := connectReq.Write(conn); err != nil {
|
|
log.Warnf("Unable to write to proxy: %v.", err)
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Read in the response. http.ReadResponse will read in the status line, mime
|
|
// headers, and potentially part of the response body. the body itself will
|
|
// not be read, but kept around so it can be read later.
|
|
br := bufio.NewReader(conn)
|
|
// Per the above comment, we're only using ReadResponse to check the status
|
|
// and then hand off the underlying connection to the caller.
|
|
// resp.Body.Close() would drain conn and close it, we don't need to do it
|
|
// here. Disabling bodyclose linter for this edge case.
|
|
//nolint:bodyclose
|
|
resp, err := http.ReadResponse(br, connectReq)
|
|
if err != nil {
|
|
conn.Close()
|
|
log.Warnf("Unable to read response: %v.", err)
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
conn.Close()
|
|
return nil, trace.BadParameter("unable to proxy connection: %v", resp.Status)
|
|
}
|
|
|
|
// Return a bufferedConn that wraps a net.Conn and a *bufio.Reader. this
|
|
// needs to be done because http.ReadResponse will buffer part of the
|
|
// response body in the *bufio.Reader that was passed in. reads must first
|
|
// come from anything buffered, then from the underlying connection otherwise
|
|
// data will be lost.
|
|
return &bufferedConn{
|
|
Conn: conn,
|
|
reader: br,
|
|
}, nil
|
|
}
|
|
|
|
// GetProxyAddress gets the HTTP proxy address to use for a given address, if any.
|
|
func GetProxyAddress(addr string) string {
|
|
envs := []string{
|
|
constants.HTTPSProxy,
|
|
strings.ToLower(constants.HTTPSProxy),
|
|
constants.HTTPProxy,
|
|
strings.ToLower(constants.HTTPProxy),
|
|
}
|
|
|
|
for _, v := range envs {
|
|
envAddr := os.Getenv(v)
|
|
if envAddr == "" {
|
|
continue
|
|
}
|
|
proxyAddr, err := parse(envAddr)
|
|
if err != nil {
|
|
log.Debugf("Unable to parse environment variable %q: %q.", v, envAddr)
|
|
continue
|
|
}
|
|
log.Debugf("Successfully parsed environment variable %q: %q to %q.", v, envAddr, proxyAddr)
|
|
if !useProxy(addr) {
|
|
log.Debugf("Matched NO_PROXY override for %q: %q, going to ignore proxy variable.", v, envAddr)
|
|
return ""
|
|
}
|
|
return proxyAddr
|
|
}
|
|
|
|
log.Debugf("No valid environment variables found.")
|
|
return ""
|
|
}
|
|
|
|
// bufferedConn is used when part of the data on a connection has already been
|
|
// read by a *bufio.Reader. Reads will first try and read from the
|
|
// *bufio.Reader and when everything has been read, reads will go to the
|
|
// underlying connection.
|
|
type bufferedConn struct {
|
|
net.Conn
|
|
reader *bufio.Reader
|
|
}
|
|
|
|
// Read first reads from the *bufio.Reader any data that has already been
|
|
// buffered. Once all buffered data has been read, reads go to the net.Conn.
|
|
func (bc *bufferedConn) Read(b []byte) (n int, err error) {
|
|
if bc.reader.Buffered() > 0 {
|
|
return bc.reader.Read(b)
|
|
}
|
|
return bc.Conn.Read(b)
|
|
}
|
|
|
|
// parse will extract the host:port of the proxy to dial to. If the
|
|
// value is not prefixed by "http", then it will prepend "http" and try.
|
|
func parse(addr string) (string, error) {
|
|
proxyurl, err := url.Parse(addr)
|
|
if err != nil || !strings.HasPrefix(proxyurl.Scheme, "http") {
|
|
proxyurl, err = url.Parse("http://" + addr)
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
return proxyurl.Host, nil
|
|
}
|