Skip to content

Commit fd3a299

Browse files
committed
proxytest: proxy HTTPS request using MITM
The proxytest now can proxy HTTPS requests using a men in the middle (MITM) approach to allow to fully control the requests between the proxy and the target server.
1 parent fc05e0d commit fd3a299

File tree

7 files changed

+685
-162
lines changed

7 files changed

+685
-162
lines changed

NOTICE.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,11 +1264,11 @@ SOFTWARE
12641264

12651265
--------------------------------------------------------------------------------
12661266
Dependency : github.com/elastic/elastic-agent-libs
1267-
Version: v0.12.1
1267+
Version: v0.13.0
12681268
Licence type (autodetected): Apache-2.0
12691269
--------------------------------------------------------------------------------
12701270

1271-
Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-agent-libs@v0.12.1/LICENSE:
1271+
Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-agent-libs@v0.13.0/LICENSE:
12721272

12731273
Apache License
12741274
Version 2.0, January 2004

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ require (
1414
github.com/dolmen-go/contextio v0.0.0-20200217195037-68fc5150bcd5
1515
github.com/elastic/elastic-agent-autodiscover v0.9.0
1616
github.com/elastic/elastic-agent-client/v7 v7.16.0
17-
github.com/elastic/elastic-agent-libs v0.12.1
17+
github.com/elastic/elastic-agent-libs v0.13.0
1818
github.com/elastic/elastic-agent-system-metrics v0.11.3
1919
github.com/elastic/elastic-transport-go/v8 v8.6.0
2020
github.com/elastic/go-elasticsearch/v8 v8.15.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,8 @@ github.com/elastic/elastic-agent-autodiscover v0.9.0 h1:+iWIKh0u3e8I+CJa3FfWe9h0
264264
github.com/elastic/elastic-agent-autodiscover v0.9.0/go.mod h1:5iUxLHhVdaGSWYTveSwfJEY4RqPXTG13LPiFoxcpFd4=
265265
github.com/elastic/elastic-agent-client/v7 v7.16.0 h1:yKGq2+CxAuW8Kh0EoNl202tqAyQKfBcPRawVKs2Jve0=
266266
github.com/elastic/elastic-agent-client/v7 v7.16.0/go.mod h1:6h+f9QdIr3GO2ODC0Y8+aEXRwzbA5W4eV4dd/67z7nI=
267-
github.com/elastic/elastic-agent-libs v0.12.1 h1:5jkxMx15Bna8cq7/Sz/XUIVUXfNWiJ80iSk4ICQ7KJ0=
268-
github.com/elastic/elastic-agent-libs v0.12.1/go.mod h1:5CR02awPrBr+tfmjBBK+JI+dMmHNQjpVY24J0wjbC7M=
267+
github.com/elastic/elastic-agent-libs v0.13.0 h1:I0ZKvjIqT8ka7d2gX6Ta3nXrCZKysZ+N8VIKIuqqir0=
268+
github.com/elastic/elastic-agent-libs v0.13.0/go.mod h1:5CR02awPrBr+tfmjBBK+JI+dMmHNQjpVY24J0wjbC7M=
269269
github.com/elastic/elastic-agent-system-metrics v0.11.3 h1:LDzRwP8kxvsYEtMDgMSKZs1TgPcSEukit+/EAP5Y28A=
270270
github.com/elastic/elastic-agent-system-metrics v0.11.3/go.mod h1:saqLKe9fuyuAo6IADAnnuy1kaBI7VNlxfwMo8KzSRyQ=
271271
github.com/elastic/elastic-transport-go/v8 v8.6.0 h1:Y2S/FBjx1LlCv5m6pWAF2kDJAHoSjSRSJCApolgfthA=

testing/proxytest/https.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License 2.0;
3+
// you may not use this file except in compliance with the Elastic License 2.0.
4+
5+
package proxytest
6+
7+
import (
8+
"bufio"
9+
"bytes"
10+
"crypto/rand"
11+
"crypto/rsa"
12+
"crypto/tls"
13+
"errors"
14+
"fmt"
15+
"io"
16+
"log/slog"
17+
"net"
18+
"net/http"
19+
"net/url"
20+
21+
"github.com/elastic/elastic-agent-libs/testing/certutil"
22+
)
23+
24+
func (p *Proxy) serveHTTPS(w http.ResponseWriter, r *http.Request) {
25+
log := loggerFromReqCtx(r)
26+
log.Debug("handling CONNECT")
27+
28+
clientCon, err := hijack(w)
29+
if err != nil {
30+
p.http500Error(clientCon, "cannot handle request", err, log)
31+
return
32+
}
33+
defer clientCon.Close()
34+
35+
// Hijack successful, w is now useless, let's make sure it isn't used by
36+
// mistake ;)
37+
w = nil //nolint:ineffassign,wastedassign // w is now useless, let's make sure it isn't used by mistake ;)
38+
log.Debug("hijacked request")
39+
40+
// ==================== CONNECT accepted, let the client know
41+
_, err = clientCon.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
42+
if err != nil {
43+
p.http500Error(clientCon, "failed to send 200-OK after CONNECT", err, log)
44+
return
45+
}
46+
47+
// ==================== TLS handshake
48+
// client will proceed to perform the TLS handshake with the "target",
49+
// which we're impersonating.
50+
51+
// generate a TLS certificate matching the target's host
52+
cert, err := p.newTLSCert(r.URL)
53+
if err != nil {
54+
p.http500Error(clientCon, "failed generating certificate", err, log)
55+
return
56+
}
57+
58+
tlscfg := p.TLS.Clone()
59+
tlscfg.Certificates = []tls.Certificate{*cert}
60+
clientTLSConn := tls.Server(clientCon, tlscfg)
61+
defer clientTLSConn.Close()
62+
err = clientTLSConn.Handshake()
63+
if err != nil {
64+
p.http500Error(clientCon, "failed TLS handshake with client", err, log)
65+
return
66+
}
67+
68+
clientTLSReader := bufio.NewReader(clientTLSConn)
69+
70+
notEOF := func(r *bufio.Reader) bool {
71+
_, err = r.Peek(1)
72+
return !errors.Is(err, io.EOF)
73+
}
74+
// ==================== Handle the actual request
75+
for notEOF(clientTLSReader) {
76+
// read request from the client sent after the 1s CONNECT request
77+
req, err := http.ReadRequest(clientTLSReader)
78+
if err != nil {
79+
p.http500Error(clientTLSConn, "failed reading client request", err, log)
80+
return
81+
}
82+
83+
// carry over the original remote addr
84+
req.RemoteAddr = r.RemoteAddr
85+
86+
// the read request is relative to the host from the original CONNECT
87+
// request and without scheme. Therefore, set them in the new request.
88+
req.URL, err = url.Parse("https://" + r.Host + req.URL.String())
89+
if err != nil {
90+
p.http500Error(clientTLSConn, "failed reading request URL from client", err, log)
91+
return
92+
}
93+
cleanUpHeaders(req.Header)
94+
95+
// now the request is ready, it can be altered just as an HTTP request
96+
// can.
97+
resp, err := p.processRequest(req)
98+
if err != nil {
99+
p.http500Error(clientTLSConn, "failed performing request to target", err, log)
100+
return
101+
}
102+
103+
// Send response from target to client
104+
// 1st - the status code
105+
_, err = clientTLSConn.Write([]byte("HTTP/1.1 " + resp.Status + "\r\n"))
106+
if err != nil {
107+
p.http500Error(clientTLSConn, "failed writing response status line", err, log)
108+
return
109+
}
110+
111+
// 2nd - the headers
112+
if err = resp.Header.Write(clientTLSConn); err != nil {
113+
p.http500Error(clientTLSConn, "failed writing TLS response header", err, log)
114+
return
115+
}
116+
117+
// 3rd - indicates the headers are done and the body will follow
118+
if _, err = clientTLSConn.Write([]byte("\r\n")); err != nil {
119+
p.http500Error(clientTLSConn, "failed writing TLS header/body separator", err, log)
120+
return
121+
}
122+
123+
// copy the body else
124+
_, err = io.CopyBuffer(clientTLSConn, resp.Body, make([]byte, 4096))
125+
if err != nil {
126+
p.http500Error(clientTLSConn, "failed writing response body", err, log)
127+
return
128+
}
129+
130+
_ = resp.Body.Close()
131+
}
132+
133+
log.Debug("EOF reached, finishing HTTPS handler")
134+
}
135+
136+
func (p *Proxy) newTLSCert(u *url.URL) (*tls.Certificate, error) {
137+
// generate the certificate key - it needs to be RSA because Elastic Defend
138+
// do not support EC :/
139+
priv, err := rsa.GenerateKey(rand.Reader, 2048)
140+
if err != nil {
141+
return nil, fmt.Errorf("could not create RSA private key: %w", err)
142+
}
143+
host := u.Hostname()
144+
145+
var name string
146+
var ips []net.IP
147+
ip := net.ParseIP(host)
148+
if ip == nil { // host isn't an IP, therefore it must be an DNS
149+
name = host
150+
} else {
151+
ips = append(ips, ip)
152+
}
153+
154+
cert, _, err := certutil.GenerateGenericChildCert(
155+
name,
156+
ips,
157+
priv,
158+
&priv.PublicKey,
159+
p.ca.capriv,
160+
p.ca.cacert)
161+
if err != nil {
162+
return nil, fmt.Errorf("could not generate TLS certificate for %s: %w",
163+
host, err)
164+
}
165+
166+
return cert, nil
167+
}
168+
169+
func (p *Proxy) http500Error(clientCon net.Conn, msg string, err error, log *slog.Logger) {
170+
p.httpError(clientCon, http.StatusInternalServerError, msg, err, log)
171+
}
172+
173+
func (p *Proxy) httpError(clientCon net.Conn, status int, msg string, err error, log *slog.Logger) {
174+
log.Error(msg, "err", err)
175+
176+
_, err = clientCon.Write(generateHTTPResponse(status, []byte(msg)))
177+
if err != nil {
178+
log.Error("failed writing response", "err", err)
179+
}
180+
}
181+
182+
func hijack(w http.ResponseWriter) (net.Conn, error) {
183+
hijacker, ok := w.(http.Hijacker)
184+
if !ok {
185+
w.WriteHeader(http.StatusInternalServerError)
186+
_, _ = fmt.Fprint(w, "cannot handle request")
187+
return nil, errors.New("http.ResponseWriter does not support hijacking")
188+
}
189+
190+
clientCon, _, err := hijacker.Hijack()
191+
if err != nil {
192+
w.WriteHeader(http.StatusInternalServerError)
193+
_, err = fmt.Fprint(w, "cannot handle request")
194+
195+
return nil, fmt.Errorf("could not Hijack HTTPS CONNECT request: %w", err)
196+
}
197+
198+
return clientCon, err
199+
}
200+
201+
func cleanUpHeaders(h http.Header) {
202+
h.Del("Proxy-Connection")
203+
h.Del("Proxy-Authenticate")
204+
h.Del("Proxy-Authorization")
205+
h.Del("Connection")
206+
}
207+
208+
func generateHTTPResponse(statusCode int, body []byte) []byte {
209+
resp := bytes.Buffer{}
210+
resp.WriteString(fmt.Sprintf("HTTP/1.1 %d %s\r\n",
211+
statusCode, http.StatusText(statusCode)))
212+
resp.WriteString("Content-Type: text/plain\r\n")
213+
resp.WriteString(fmt.Sprintf("Content-Length: %d\r\n", len(body)))
214+
resp.WriteString("\r\n")
215+
if len(body) > 0 {
216+
resp.Write(body)
217+
}
218+
219+
return resp.Bytes()
220+
}

0 commit comments

Comments
 (0)