I'm using httputil.ReverseProxy with an http.RoundTripper of my own implementation that uses an ssh.Channel as a transport. My RoundTrip method looks approximately like this:
func (c SSHConnection) RoundTrip(req *http.Request) (*http.Response, error) {
ch, err := c.GetChannel()
if err != nil {
return nil, errors.New("couldn't open forwarded-tcpip channel: " + err.Error())
}
// defer ch.Close()
err = req.Write(ch)
if err != nil {
return nil, errors.New("couldn't send request: " + err.Error())
}
return http.ReadResponse(bufio.NewReader(ch), req)
}
func (c SSHConnection) GetChannel() (ssh.Channel, error) {
ch, req, err := c.Conn.OpenChannel("forwarded-tcpip", msg)
if err != nil {
return nil, err
}
go ssh.DiscardRequests(req)
return ch, nil
}
Notice the commented-out defer ch.Close(). Initially I naively closed the connection here, but the response body would sometimes be empty, due to a race between the HTTP proxy's reading of the body and this closing of the SSH channel.
Assuming, for now, that I don't care to do keep-alive, when can I close the ssh.Channel? If I don't, every request starts a new goroutine (because of go ssh.DiscardRequests(req)), so I leak a goroutine on every HTTP requests until the underlying SSH connection is closed.
An http.RoundTripper
should not close the connection until after the response body has been fully consumed, or at the request of the server.
The simplest option is to fully buffer the response and close the connection immediately. In some cases this may actually be the most efficient, if the traffic mostly consists of small, independent requests.
The next option is to hook the closing of the response body to close the channel.
type Body struct {
io.ReadCloser
channel ssh.Channel
}
func (b *Body) Close() error {
b.channel.Close()
return b.ReadCloser.Close()
}
func (c SSHConnection) RoundTrip(req *http.Request) (*http.Response, error) {
ch, err := c.GetChannel()
if err != nil {
return nil, errors.New("couldn't open forwarded-tcpip channel: " + err.Error())
}
err = req.Write(ch)
if err != nil {
return nil, errors.New("couldn't send request: " + err.Error())
}
resp, err := http.ReadResponse(bufio.NewReader(ch), req)
if err != nil {
ch.Close()
return nil, err
}
resp.Body = &Body{
ReadCloser: resp.Body,
channel: ch,
}
return resp, err
}
Lastly, to make the most efficient use of the ssh channels, you could use an existing Transport
with a net.Dialer
which makes the ssh connection, and wraps the channel in a net.Conn
interface.
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments