Skip to content

Commit 31afddc

Browse files
authored
Move mock proxy test server from elastic-agent to elastic-agent-libs for common use (#358)
* add proxy test
1 parent b7f794b commit 31afddc

File tree

4 files changed

+1213
-0
lines changed

4 files changed

+1213
-0
lines changed

testing/proxytest/https.go

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

0 commit comments

Comments
 (0)