Skip to content

Commit 70b0307

Browse files
authored
Merge branch 'main' into 41277_testing
2 parents f97630f + ad041d4 commit 70b0307

File tree

4 files changed

+704
-155
lines changed

4 files changed

+704
-155
lines changed

testing/proxytest/https.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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+
"crypto/rand"
10+
"crypto/rsa"
11+
"crypto/tls"
12+
"errors"
13+
"fmt"
14+
"io"
15+
"log/slog"
16+
"net"
17+
"net/http"
18+
"net/url"
19+
"strings"
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 and sent just as it's
96+
// done for an HTTP request.
97+
resp, err := p.processRequest(req)
98+
if err != nil {
99+
p.httpError(clientTLSConn,
100+
http.StatusBadGateway,
101+
"failed performing request to target", err, log)
102+
return
103+
}
104+
105+
clientResp := http.Response{
106+
ProtoMajor: 1,
107+
ProtoMinor: 1,
108+
StatusCode: resp.StatusCode,
109+
TransferEncoding: append([]string{}, resp.TransferEncoding...),
110+
Trailer: resp.Trailer.Clone(),
111+
Body: resp.Body,
112+
ContentLength: resp.ContentLength,
113+
Header: resp.Header.Clone(),
114+
}
115+
116+
err = clientResp.Write(clientTLSConn)
117+
if err != nil {
118+
p.http500Error(clientTLSConn, "failed writing response body", err, log)
119+
return
120+
}
121+
122+
_ = resp.Body.Close()
123+
}
124+
125+
log.Debug("EOF reached, finishing HTTPS handler")
126+
}
127+
128+
func (p *Proxy) newTLSCert(u *url.URL) (*tls.Certificate, error) {
129+
// generate the certificate key - it needs to be RSA because Elastic Defend
130+
// do not support EC :/
131+
priv, err := rsa.GenerateKey(rand.Reader, 2048)
132+
if err != nil {
133+
return nil, fmt.Errorf("could not create RSA private key: %w", err)
134+
}
135+
host := u.Hostname()
136+
137+
var name string
138+
var ips []net.IP
139+
ip := net.ParseIP(host)
140+
if ip == nil { // host isn't an IP, therefore it must be an DNS
141+
name = host
142+
} else {
143+
ips = append(ips, ip)
144+
}
145+
146+
cert, _, err := certutil.GenerateGenericChildCert(
147+
name,
148+
ips,
149+
priv,
150+
&priv.PublicKey,
151+
p.ca.capriv,
152+
p.ca.cacert)
153+
if err != nil {
154+
return nil, fmt.Errorf("could not generate TLS certificate for %s: %w",
155+
host, err)
156+
}
157+
158+
return cert, nil
159+
}
160+
161+
func (p *Proxy) http500Error(clientCon net.Conn, msg string, err error, log *slog.Logger) {
162+
p.httpError(clientCon, http.StatusInternalServerError, msg, err, log)
163+
}
164+
165+
func (p *Proxy) httpError(clientCon net.Conn, status int, msg string, err error, log *slog.Logger) {
166+
log.Error(msg, "err", err)
167+
168+
resp := http.Response{
169+
StatusCode: status,
170+
ProtoMajor: 1,
171+
ProtoMinor: 1,
172+
Body: io.NopCloser(strings.NewReader(msg)),
173+
Header: http.Header{},
174+
}
175+
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
176+
177+
err = resp.Write(clientCon)
178+
if err != nil {
179+
log.Error("failed writing response", "err", err)
180+
}
181+
}
182+
183+
func hijack(w http.ResponseWriter) (net.Conn, error) {
184+
hijacker, ok := w.(http.Hijacker)
185+
if !ok {
186+
w.WriteHeader(http.StatusInternalServerError)
187+
_, _ = fmt.Fprint(w, "cannot handle request")
188+
return nil, errors.New("http.ResponseWriter does not support hijacking")
189+
}
190+
191+
clientCon, _, err := hijacker.Hijack()
192+
if err != nil {
193+
w.WriteHeader(http.StatusInternalServerError)
194+
_, err = fmt.Fprint(w, "cannot handle request")
195+
196+
return nil, fmt.Errorf("could not Hijack HTTPS CONNECT request: %w", err)
197+
}
198+
199+
return clientCon, err
200+
}
201+
202+
func cleanUpHeaders(h http.Header) {
203+
h.Del("Proxy-Connection")
204+
h.Del("Proxy-Authenticate")
205+
h.Del("Proxy-Authorization")
206+
h.Del("Connection")
207+
}

0 commit comments

Comments
 (0)