func TestSignerTLS(t *testing.T) { // Issue a certificate using OpenSSL newCert, newKey, err := issueCert([]string{"mancave.local", "1.1.1.1", "localhost"}) orFatal("issueCert", err, t) _, err = openssl.LoadCertificateFromPEM(serverCert) orFatal("ParseTemplateCertificateTLS", err, t) _, err = openssl.LoadCertificateFromPEM(rootCertCA) orFatal("ParseCACertificateTLS", err, t) expected := "key verifies with Go" certpool := x509.NewCertPool() certpool.AddCert(GopenSSLProxyCA.Leaf) // Go's built-in tranport works just fine. // tr := &http.Transport{ // TLSClientConfig: &tls.Config{RootCAs: certpool}, // } // OpenSSL based transport which crashes on FreeBSD tlsConfig, err := openssl.NewCtx() tr := &transport.Transport{ TLSClientConfig: tlsConfig, } srv := &http.Server{ // Addr: ":8443", Handler: ConstantHanlder(expected), } sslCtx, err := openssl.NewCtx() orFatal("NewContext", err, t) sslCtx.UseCertificate(newCert) sslCtx.UsePrivateKey(*newKey) l, err := openssl.Listen("tcp", ":8443", sslCtx) orFatal("NewListener", err, t) go srv.Serve(l) // Making sure to wait long enough // until the server start listening time.Sleep(2 * time.Second) req, err := http.NewRequest("GET", "https://localhost:8443", nil) orFatal("NewRequest", err, t) resp, err := tr.RoundTrip(req) orFatal("RoundTrip", err, t) txt, err := ioutil.ReadAll(resp.Body) orFatal("ioutil.ReadAll", err, t) if string(txt) != expected { t.Errorf("Expected '%s' got '%s'", expected, string(txt)) } }
func TLSConfigFromCA() func(host string, ctx *ProxyCtx) (*openssl.Ctx, error) { return func(host string, ctx *ProxyCtx) (*openssl.Ctx, error) { // Create new OpenSSL context context, err := openssl.NewCtx() if err != nil { ctx.Warnf("Cannot create new OpenSSL context: %s", err) return nil, err } ctx.Logf("signing for %s", stripPort(host)) cert, privKey, err := issueCert([]string{stripPort(host)}) if err != nil { ctx.Warnf("Cannot sign host certificate with provided CA: %s", err) return nil, err } // Use newly generated private key and certificate context.UsePrivateKey(*privKey) context.UseCertificate(cert) return context, nil } }
// New proxy server, logs to StdErr by default func NewProxyHttpServer() *ProxyHttpServer { clientTLSCtx, _ := openssl.NewCtx() proxy := ProxyHttpServer{ Logger: log.New(os.Stderr, "", log.LstdFlags), reqHandlers: []ReqHandler{}, respHandlers: []RespHandler{}, httpsHandlers: []HttpsHandler{}, NonproxyHandler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { http.Error(w, "This is a proxy server. Does not respond to non-proxy requests.", 500) }), Tr: &transport.Transport{TLSClientConfig: clientTLSCtx, Proxy: http.ProxyFromEnvironment}, } proxy.ConnectDial = dialerFromEnv(&proxy) return &proxy }
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")) go copyAndClose(ctx, targetSiteCon, proxyClient) go copyAndClose(ctx, proxyClient, targetSiteCon) 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 } } 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, err := openssl.NewCtx() if err != nil { httpError(proxyClient, ctx, err) return } 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, err := openssl.Server(proxyClient, tlsConfig) if err != nil { ctx.Warnf("Cannot open Server %v %v", r.Host, err) return } 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) 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) req.URL, err = url.Parse("https://" + r.Host + req.URL.String()) // Bug fix which gopensslproxy 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) 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 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 (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 } if resp.StatusCode != 200 { resp, _ := ioutil.ReadAll(resp.Body) 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) { clientCtx, err := openssl.NewCtx() if err != nil { return nil, err } c, err := openssl.Dial(network, u.Host, clientCtx, 0) if err != nil { return nil, err } 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 } if resp.StatusCode != 200 { body, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 500)) resp.Body.Close() c.Close() return nil, errors.New("proxy refused connection" + string(body)) } return c, nil } } return nil }
// getConn dials and creates a new persistConn to the target as // specified in the connectMethod. This includes doing a proxy CONNECT // and/or setting up TLS. If this doesn't return an error, the persistConn // is ready to write requests to. func (t *Transport) getConn(cm *connectMethod) (*persistConn, error) { if pc := t.getIdleConn(cm); pc != nil { return pc, nil } conn, raddr, ip, err := t.dial("tcp", cm.addr()) if err != nil { if cm.proxyURL != nil { err = fmt.Errorf("http: error connecting to proxy %s: %v", cm.proxyURL, err) } return nil, err } pa := cm.proxyAuth() pconn := &persistConn{ t: t, cacheKey: cm.String(), conn: conn, reqch: make(chan requestAndChan, 50), host: raddr, ip: ip, } switch { case cm.proxyURL == nil: // Do nothing. case cm.targetScheme == "http": pconn.isProxy = true if pa != "" { pconn.mutateHeaderFunc = func(h http.Header) { h.Set("Proxy-Authorization", pa) } } case cm.targetScheme == "https": connectReq := &http.Request{ Method: "CONNECT", URL: &url.URL{Opaque: cm.targetAddr}, Host: cm.targetAddr, Header: make(http.Header), } if pa != "" { connectReq.Header.Set("Proxy-Authorization", pa) } connectReq.Write(conn) // Read response. // Okay to use and discard buffered reader here, because // TLS server will not speak until spoken to. br := bufio.NewReader(conn) resp, err := http.ReadResponse(br, connectReq) if err != nil { conn.Close() return nil, err } if resp.StatusCode != 200 { f := strings.SplitN(resp.Status, " ", 2) conn.Close() return nil, errors.New(f[1]) } } if cm.targetScheme == "https" { // Initiate TLS and check remote host name against certificate. t.TLSClientConfig, err = openssl.NewCtx() if err != nil { return nil, err } conn, err := openssl.Client(conn, t.TLSClientConfig) if err != nil { return nil, err } if err = conn.Handshake(); err != nil { return nil, err } if t.TLSClientConfig == nil || t.TLSClientConfig.VerifyMode() == 0 { if err = conn.VerifyHostname(cm.tlsHost()); err != nil { return nil, err } } pconn.conn = conn } pconn.br = bufio.NewReader(pconn.conn) pconn.bw = bufio.NewWriter(pconn.conn) go pconn.readLoop() return pconn, nil }