mirror of https://github.com/k3s-io/k3s
410 lines
12 KiB
Go
410 lines
12 KiB
Go
package goproxy
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/tls"
|
|
"errors"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
type ConnectActionLiteral int
|
|
|
|
const (
|
|
ConnectAccept = iota
|
|
ConnectReject
|
|
ConnectMitm
|
|
ConnectHijack
|
|
ConnectHTTPMitm
|
|
ConnectProxyAuthHijack
|
|
)
|
|
|
|
var (
|
|
OkConnect = &ConnectAction{Action: ConnectAccept, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
|
|
MitmConnect = &ConnectAction{Action: ConnectMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
|
|
HTTPMitmConnect = &ConnectAction{Action: ConnectHTTPMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
|
|
RejectConnect = &ConnectAction{Action: ConnectReject, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
|
|
httpsRegexp = regexp.MustCompile(`^https:\/\/`)
|
|
)
|
|
|
|
type ConnectAction struct {
|
|
Action ConnectActionLiteral
|
|
Hijack func(req *http.Request, client net.Conn, ctx *ProxyCtx)
|
|
TLSConfig func(host string, ctx *ProxyCtx) (*tls.Config, error)
|
|
}
|
|
|
|
func stripPort(s string) string {
|
|
ix := strings.IndexRune(s, ':')
|
|
if ix == -1 {
|
|
return s
|
|
}
|
|
return s[:ix]
|
|
}
|
|
|
|
func (proxy *ProxyHttpServer) dial(network, addr string) (c net.Conn, err error) {
|
|
if proxy.Tr.Dial != nil {
|
|
return proxy.Tr.Dial(network, addr)
|
|
}
|
|
return net.Dial(network, addr)
|
|
}
|
|
|
|
func (proxy *ProxyHttpServer) connectDial(network, addr string) (c net.Conn, err error) {
|
|
if proxy.ConnectDial == nil {
|
|
return proxy.dial(network, addr)
|
|
}
|
|
return proxy.ConnectDial(network, addr)
|
|
}
|
|
|
|
func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request) {
|
|
ctx := &ProxyCtx{Req: r, Session: atomic.AddInt64(&proxy.sess, 1), proxy: proxy}
|
|
|
|
hij, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
panic("httpserver does not support hijacking")
|
|
}
|
|
|
|
proxyClient, _, e := hij.Hijack()
|
|
if e != nil {
|
|
panic("Cannot hijack connection " + e.Error())
|
|
}
|
|
|
|
ctx.Logf("Running %d CONNECT handlers", len(proxy.httpsHandlers))
|
|
todo, host := OkConnect, r.URL.Host
|
|
for i, h := range proxy.httpsHandlers {
|
|
newtodo, newhost := h.HandleConnect(host, ctx)
|
|
|
|
// If found a result, break the loop immediately
|
|
if newtodo != nil {
|
|
todo, host = newtodo, newhost
|
|
ctx.Logf("on %dth handler: %v %s", i, todo, host)
|
|
break
|
|
}
|
|
}
|
|
switch todo.Action {
|
|
case ConnectAccept:
|
|
if !hasPort.MatchString(host) {
|
|
host += ":80"
|
|
}
|
|
targetSiteCon, err := proxy.connectDial("tcp", host)
|
|
if err != nil {
|
|
httpError(proxyClient, ctx, err)
|
|
return
|
|
}
|
|
ctx.Logf("Accepting CONNECT to %s", host)
|
|
proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
|
|
|
|
targetTCP, targetOK := targetSiteCon.(*net.TCPConn)
|
|
proxyClientTCP, clientOK := proxyClient.(*net.TCPConn)
|
|
if targetOK && clientOK {
|
|
go copyAndClose(ctx, targetTCP, proxyClientTCP)
|
|
go copyAndClose(ctx, proxyClientTCP, targetTCP)
|
|
} else {
|
|
go func() {
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
go copyOrWarn(ctx, targetSiteCon, proxyClient, &wg)
|
|
go copyOrWarn(ctx, proxyClient, targetSiteCon, &wg)
|
|
wg.Wait()
|
|
proxyClient.Close()
|
|
targetSiteCon.Close()
|
|
|
|
}()
|
|
}
|
|
|
|
case ConnectHijack:
|
|
ctx.Logf("Hijacking CONNECT to %s", host)
|
|
proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
|
|
todo.Hijack(r, proxyClient, ctx)
|
|
case ConnectHTTPMitm:
|
|
proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
|
|
ctx.Logf("Assuming CONNECT is plain HTTP tunneling, mitm proxying it")
|
|
targetSiteCon, err := proxy.connectDial("tcp", host)
|
|
if err != nil {
|
|
ctx.Warnf("Error dialing to %s: %s", host, err.Error())
|
|
return
|
|
}
|
|
for {
|
|
client := bufio.NewReader(proxyClient)
|
|
remote := bufio.NewReader(targetSiteCon)
|
|
req, err := http.ReadRequest(client)
|
|
if err != nil && err != io.EOF {
|
|
ctx.Warnf("cannot read request of MITM HTTP client: %+#v", err)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
req, resp := proxy.filterRequest(req, ctx)
|
|
if resp == nil {
|
|
if err := req.Write(targetSiteCon); err != nil {
|
|
httpError(proxyClient, ctx, err)
|
|
return
|
|
}
|
|
resp, err = http.ReadResponse(remote, req)
|
|
if err != nil {
|
|
httpError(proxyClient, ctx, err)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
}
|
|
resp = proxy.filterResponse(resp, ctx)
|
|
if err := resp.Write(proxyClient); err != nil {
|
|
httpError(proxyClient, ctx, err)
|
|
return
|
|
}
|
|
}
|
|
case ConnectMitm:
|
|
proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
|
|
ctx.Logf("Assuming CONNECT is TLS, mitm proxying it")
|
|
// this goes in a separate goroutine, so that the net/http server won't think we're
|
|
// still handling the request even after hijacking the connection. Those HTTP CONNECT
|
|
// request can take forever, and the server will be stuck when "closed".
|
|
// TODO: Allow Server.Close() mechanism to shut down this connection as nicely as possible
|
|
tlsConfig := defaultTLSConfig
|
|
if todo.TLSConfig != nil {
|
|
var err error
|
|
tlsConfig, err = todo.TLSConfig(host, ctx)
|
|
if err != nil {
|
|
httpError(proxyClient, ctx, err)
|
|
return
|
|
}
|
|
}
|
|
go func() {
|
|
//TODO: cache connections to the remote website
|
|
rawClientTls := tls.Server(proxyClient, tlsConfig)
|
|
if err := rawClientTls.Handshake(); err != nil {
|
|
ctx.Warnf("Cannot handshake client %v %v", r.Host, err)
|
|
return
|
|
}
|
|
defer rawClientTls.Close()
|
|
clientTlsReader := bufio.NewReader(rawClientTls)
|
|
for !isEof(clientTlsReader) {
|
|
req, err := http.ReadRequest(clientTlsReader)
|
|
var ctx = &ProxyCtx{Req: req, Session: atomic.AddInt64(&proxy.sess, 1), proxy: proxy}
|
|
if err != nil && err != io.EOF {
|
|
return
|
|
}
|
|
if err != nil {
|
|
ctx.Warnf("Cannot read TLS request from mitm'd client %v %v", r.Host, err)
|
|
return
|
|
}
|
|
req.RemoteAddr = r.RemoteAddr // since we're converting the request, need to carry over the original connecting IP as well
|
|
ctx.Logf("req %v", r.Host)
|
|
|
|
if !httpsRegexp.MatchString(req.URL.String()) {
|
|
req.URL, err = url.Parse("https://" + r.Host + req.URL.String())
|
|
}
|
|
|
|
// Bug fix which goproxy fails to provide request
|
|
// information URL in the context when does HTTPS MITM
|
|
ctx.Req = req
|
|
|
|
req, resp := proxy.filterRequest(req, ctx)
|
|
if resp == nil {
|
|
if err != nil {
|
|
ctx.Warnf("Illegal URL %s", "https://"+r.Host+req.URL.Path)
|
|
return
|
|
}
|
|
removeProxyHeaders(ctx, req)
|
|
resp, err = ctx.RoundTrip(req)
|
|
if err != nil {
|
|
ctx.Warnf("Cannot read TLS response from mitm'd server %v", err)
|
|
return
|
|
}
|
|
ctx.Logf("resp %v", resp.Status)
|
|
}
|
|
resp = proxy.filterResponse(resp, ctx)
|
|
defer resp.Body.Close()
|
|
|
|
text := resp.Status
|
|
statusCode := strconv.Itoa(resp.StatusCode) + " "
|
|
if strings.HasPrefix(text, statusCode) {
|
|
text = text[len(statusCode):]
|
|
}
|
|
// always use 1.1 to support chunked encoding
|
|
if _, err := io.WriteString(rawClientTls, "HTTP/1.1"+" "+statusCode+text+"\r\n"); err != nil {
|
|
ctx.Warnf("Cannot write TLS response HTTP status from mitm'd client: %v", err)
|
|
return
|
|
}
|
|
// Since we don't know the length of resp, return chunked encoded response
|
|
// TODO: use a more reasonable scheme
|
|
resp.Header.Del("Content-Length")
|
|
resp.Header.Set("Transfer-Encoding", "chunked")
|
|
if err := resp.Header.Write(rawClientTls); err != nil {
|
|
ctx.Warnf("Cannot write TLS response header from mitm'd client: %v", err)
|
|
return
|
|
}
|
|
if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil {
|
|
ctx.Warnf("Cannot write TLS response header end from mitm'd client: %v", err)
|
|
return
|
|
}
|
|
chunked := newChunkedWriter(rawClientTls)
|
|
if _, err := io.Copy(chunked, resp.Body); err != nil {
|
|
ctx.Warnf("Cannot write TLS response body from mitm'd client: %v", err)
|
|
return
|
|
}
|
|
if err := chunked.Close(); err != nil {
|
|
ctx.Warnf("Cannot write TLS chunked EOF from mitm'd client: %v", err)
|
|
return
|
|
}
|
|
if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil {
|
|
ctx.Warnf("Cannot write TLS response chunked trailer from mitm'd client: %v", err)
|
|
return
|
|
}
|
|
}
|
|
ctx.Logf("Exiting on EOF")
|
|
}()
|
|
case ConnectProxyAuthHijack:
|
|
proxyClient.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\n"))
|
|
todo.Hijack(r, proxyClient, ctx)
|
|
case ConnectReject:
|
|
if ctx.Resp != nil {
|
|
if err := ctx.Resp.Write(proxyClient); err != nil {
|
|
ctx.Warnf("Cannot write response that reject http CONNECT: %v", err)
|
|
}
|
|
}
|
|
proxyClient.Close()
|
|
}
|
|
}
|
|
|
|
func httpError(w io.WriteCloser, ctx *ProxyCtx, err error) {
|
|
if _, err := io.WriteString(w, "HTTP/1.1 502 Bad Gateway\r\n\r\n"); err != nil {
|
|
ctx.Warnf("Error responding to client: %s", err)
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
ctx.Warnf("Error closing client connection: %s", err)
|
|
}
|
|
}
|
|
|
|
func copyOrWarn(ctx *ProxyCtx, dst io.Writer, src io.Reader, wg *sync.WaitGroup) {
|
|
if _, err := io.Copy(dst, src); err != nil {
|
|
ctx.Warnf("Error copying to client: %s", err)
|
|
}
|
|
wg.Done()
|
|
}
|
|
|
|
func copyAndClose(ctx *ProxyCtx, dst, src *net.TCPConn) {
|
|
if _, err := io.Copy(dst, src); err != nil {
|
|
ctx.Warnf("Error copying to client: %s", err)
|
|
}
|
|
|
|
dst.CloseWrite()
|
|
src.CloseRead()
|
|
}
|
|
|
|
func dialerFromEnv(proxy *ProxyHttpServer) func(network, addr string) (net.Conn, error) {
|
|
https_proxy := os.Getenv("HTTPS_PROXY")
|
|
if https_proxy == "" {
|
|
https_proxy = os.Getenv("https_proxy")
|
|
}
|
|
if https_proxy == "" {
|
|
return nil
|
|
}
|
|
return proxy.NewConnectDialToProxy(https_proxy)
|
|
}
|
|
|
|
func (proxy *ProxyHttpServer) NewConnectDialToProxy(https_proxy string) func(network, addr string) (net.Conn, error) {
|
|
u, err := url.Parse(https_proxy)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if u.Scheme == "" || u.Scheme == "http" {
|
|
if strings.IndexRune(u.Host, ':') == -1 {
|
|
u.Host += ":80"
|
|
}
|
|
return func(network, addr string) (net.Conn, error) {
|
|
connectReq := &http.Request{
|
|
Method: "CONNECT",
|
|
URL: &url.URL{Opaque: addr},
|
|
Host: addr,
|
|
Header: make(http.Header),
|
|
}
|
|
c, err := proxy.dial(network, u.Host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
connectReq.Write(c)
|
|
// Read response.
|
|
// Okay to use and discard buffered reader here, because
|
|
// TLS server will not speak until spoken to.
|
|
br := bufio.NewReader(c)
|
|
resp, err := http.ReadResponse(br, connectReq)
|
|
if err != nil {
|
|
c.Close()
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
resp, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.Close()
|
|
return nil, errors.New("proxy refused connection" + string(resp))
|
|
}
|
|
return c, nil
|
|
}
|
|
}
|
|
if u.Scheme == "https" {
|
|
if strings.IndexRune(u.Host, ':') == -1 {
|
|
u.Host += ":443"
|
|
}
|
|
return func(network, addr string) (net.Conn, error) {
|
|
c, err := proxy.dial(network, u.Host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c = tls.Client(c, proxy.Tr.TLSClientConfig)
|
|
connectReq := &http.Request{
|
|
Method: "CONNECT",
|
|
URL: &url.URL{Opaque: addr},
|
|
Host: addr,
|
|
Header: make(http.Header),
|
|
}
|
|
connectReq.Write(c)
|
|
// Read response.
|
|
// Okay to use and discard buffered reader here, because
|
|
// TLS server will not speak until spoken to.
|
|
br := bufio.NewReader(c)
|
|
resp, err := http.ReadResponse(br, connectReq)
|
|
if err != nil {
|
|
c.Close()
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 500))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.Close()
|
|
return nil, errors.New("proxy refused connection" + string(body))
|
|
}
|
|
return c, nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TLSConfigFromCA(ca *tls.Certificate) func(host string, ctx *ProxyCtx) (*tls.Config, error) {
|
|
return func(host string, ctx *ProxyCtx) (*tls.Config, error) {
|
|
config := *defaultTLSConfig
|
|
ctx.Logf("signing for %s", stripPort(host))
|
|
cert, err := signHost(*ca, []string{stripPort(host)})
|
|
if err != nil {
|
|
ctx.Warnf("Cannot sign host certificate with provided CA: %s", err)
|
|
return nil, err
|
|
}
|
|
config.Certificates = append(config.Certificates, cert)
|
|
return &config, nil
|
|
}
|
|
}
|