Skip to content

Commit c8069ee

Browse files
committed
wip: initial commit; basic proxy setup
0 parents  commit c8069ee

3 files changed

Lines changed: 369 additions & 0 deletions

File tree

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/anfragment/zen
2+
3+
go 1.18

go.sum

Whitespace-only changes.

main.go

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
// https://github.com/eliben/code-for-blog/blob/master/2022/go-and-proxies/connect-mitm-proxy.go
2+
package main
3+
4+
import (
5+
"bufio"
6+
"crypto"
7+
"crypto/ecdsa"
8+
"crypto/elliptic"
9+
"crypto/rand"
10+
"crypto/tls"
11+
"crypto/x509"
12+
"crypto/x509/pkix"
13+
"encoding/pem"
14+
"flag"
15+
"io"
16+
"io/ioutil"
17+
"log"
18+
"math/big"
19+
"net"
20+
"net/http"
21+
"net/url"
22+
"runtime/debug"
23+
"strings"
24+
"time"
25+
)
26+
27+
// createCert creates a new certificate/private key pair for the given domains,
28+
// signed by the parent/parentKey certificate. hoursValid is the duration of
29+
// the new certificate's validity.
30+
func createCert(dnsNames []string, parent *x509.Certificate, parentKey crypto.PrivateKey, hoursValid int) (cert []byte, priv []byte) {
31+
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
32+
if err != nil {
33+
log.Panicf("Failed to generate private key: %v", err)
34+
}
35+
36+
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
37+
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
38+
if err != nil {
39+
log.Panicf("Failed to generate serial number: %v", err)
40+
}
41+
42+
template := x509.Certificate{
43+
SerialNumber: serialNumber,
44+
Subject: pkix.Name{
45+
Organization: []string{"Sample MITM proxy"},
46+
},
47+
DNSNames: dnsNames,
48+
NotBefore: time.Now(),
49+
NotAfter: time.Now().Add(time.Duration(hoursValid) * time.Hour),
50+
51+
KeyUsage: x509.KeyUsageDigitalSignature,
52+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
53+
BasicConstraintsValid: true,
54+
}
55+
56+
derBytes, err := x509.CreateCertificate(rand.Reader, &template, parent, &privateKey.PublicKey, parentKey)
57+
if err != nil {
58+
log.Panicf("Failed to create certificate: %v", err)
59+
}
60+
pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
61+
if pemCert == nil {
62+
log.Panic("failed to encode certificate to PEM")
63+
}
64+
65+
privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
66+
if err != nil {
67+
log.Panicf("Unable to marshal private key: %v", err)
68+
}
69+
pemKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
70+
if pemCert == nil {
71+
log.Panic("failed to encode key to PEM")
72+
}
73+
74+
return pemCert, pemKey
75+
}
76+
77+
// loadX509KeyPair loads a certificate/key pair from files, and unmarshals them
78+
// into data structures from the x509 package. Note that private key types in Go
79+
// don't have a shared named interface and use `any` (for backwards
80+
// compatibility reasons).
81+
func loadX509KeyPair(certFile, keyFile string) (cert *x509.Certificate, key any, err error) {
82+
cf, err := ioutil.ReadFile(certFile)
83+
if err != nil {
84+
return nil, nil, err
85+
}
86+
87+
kf, err := ioutil.ReadFile(keyFile)
88+
if err != nil {
89+
return nil, nil, err
90+
}
91+
certBlock, _ := pem.Decode(cf)
92+
cert, err = x509.ParseCertificate(certBlock.Bytes)
93+
if err != nil {
94+
return nil, nil, err
95+
}
96+
97+
keyBlock, _ := pem.Decode(kf)
98+
key, err = x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
99+
if err != nil {
100+
return nil, nil, err
101+
}
102+
103+
return cert, key, nil
104+
}
105+
106+
// mitmProxy is a type implementing http.Handler that serves as a MITM proxy
107+
// for CONNECT tunnels. Create new instances of mitmProxy using createMitmProxy.
108+
type mitmProxy struct {
109+
caCert *x509.Certificate
110+
caKey any
111+
}
112+
113+
// createMitmProxy creates a new MITM proxy. It should be passed the filenames
114+
// for the certificate and private key of a certificate authority trusted by the
115+
// client's machine.
116+
func createMitmProxy(caCertFile, caKeyFile string) *mitmProxy {
117+
caCert, caKey, err := loadX509KeyPair(caCertFile, caKeyFile)
118+
if err != nil {
119+
log.Panic("Error loading CA certificate/key:", err)
120+
}
121+
log.Printf("loaded CA certificate and key; IsCA=%v\n", caCert.IsCA)
122+
123+
return &mitmProxy{
124+
caCert: caCert,
125+
caKey: caKey,
126+
}
127+
}
128+
129+
func (p *mitmProxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
130+
defer func() {
131+
if r := recover(); r != nil {
132+
log.Printf("panic serving %v: %v. stacktrace from panic: %s", req.URL, r, string(debug.Stack()))
133+
http.Error(w, "internal server error", http.StatusInternalServerError)
134+
}
135+
}()
136+
137+
if req.Method == http.MethodConnect {
138+
p.proxyConnect(w, req)
139+
} else {
140+
p.proxyHTTP(w, req)
141+
}
142+
}
143+
144+
func (p *mitmProxy) proxyHTTP(w http.ResponseWriter, req *http.Request) {
145+
// The "Host:" header is promoted to Request.Host and is removed from
146+
// request.Header by net/http, so we print it out explicitly.
147+
log.Println(req.RemoteAddr, "\t\t", req.Method, "\t\t", req.URL, "\t\t Host:", req.Host)
148+
log.Println("\t\t\t\t\t", req.Header)
149+
150+
if req.URL.Scheme != "http" {
151+
msg := "unsupported protocol scheme " + req.URL.Scheme
152+
http.Error(w, msg, http.StatusBadRequest)
153+
log.Println(msg)
154+
return
155+
}
156+
157+
client := &http.Client{}
158+
// When a http.Request is sent through an http.Client, RequestURI should not
159+
// be set (see documentation of this field).
160+
req.RequestURI = ""
161+
162+
removeHopHeaders(req.Header)
163+
removeConnectionHeaders(req.Header)
164+
165+
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
166+
appendHostToXForwardHeader(req.Header, clientIP)
167+
}
168+
169+
resp, err := client.Do(req)
170+
if err != nil {
171+
http.Error(w, err.Error(), http.StatusInternalServerError)
172+
log.Println(err)
173+
return
174+
}
175+
defer resp.Body.Close()
176+
177+
log.Println(req.RemoteAddr, " ", resp.Status)
178+
179+
removeHopHeaders(resp.Header)
180+
removeConnectionHeaders(resp.Header)
181+
182+
copyHeader(w.Header(), resp.Header)
183+
w.WriteHeader(resp.StatusCode)
184+
io.Copy(w, resp.Body)
185+
}
186+
187+
// Hop-by-hop headers. These are removed when sent to the backend.
188+
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
189+
// Note: this may be out of date, see RFC 7230 Section 6.1
190+
var hopHeaders = []string{
191+
"Connection",
192+
"Proxy-Connection",
193+
"Keep-Alive",
194+
"Proxy-Authenticate",
195+
"Proxy-Authorization",
196+
"Te", // canonicalized version of "TE"
197+
"Trailer", // spelling per https://www.rfc-editor.org/errata_search.php?eid=4522
198+
"Transfer-Encoding",
199+
"Upgrade",
200+
}
201+
202+
func copyHeader(dst, src http.Header) {
203+
for k, vv := range src {
204+
for _, v := range vv {
205+
dst.Add(k, v)
206+
}
207+
}
208+
}
209+
210+
func removeHopHeaders(header http.Header) {
211+
for _, h := range hopHeaders {
212+
header.Del(h)
213+
header.Del(strings.ToLower(h))
214+
}
215+
}
216+
217+
// removeConnectionHeaders removes hop-by-hop headers listed in the "Connection"
218+
// header of h. See RFC 7230, section 6.1
219+
func removeConnectionHeaders(h http.Header) {
220+
for _, f := range h["Connection"] {
221+
for _, sf := range strings.Split(f, ",") {
222+
if sf = strings.TrimSpace(sf); sf != "" {
223+
h.Del(sf)
224+
}
225+
}
226+
}
227+
}
228+
229+
var client = &http.Client{
230+
// do not follow redirects, let the client handle them
231+
CheckRedirect: func(req *http.Request, via []*http.Request) error {
232+
return http.ErrUseLastResponse
233+
},
234+
}
235+
236+
func appendHostToXForwardHeader(header http.Header, host string) {
237+
// If we aren't the first proxy retain prior
238+
// X-Forwarded-For information as a comma+space
239+
// separated list and fold multiple headers into one.
240+
if prior, ok := header["X-Forwarded-For"]; ok {
241+
host = strings.Join(prior, ", ") + ", " + host
242+
}
243+
header.Set("X-Forwarded-For", host)
244+
}
245+
246+
// proxyConnect implements the MITM proxy for CONNECT tunnels.
247+
func (p *mitmProxy) proxyConnect(w http.ResponseWriter, proxyReq *http.Request) {
248+
log.Printf("CONNECT requested to %v (from %v)", proxyReq.Host, proxyReq.RemoteAddr)
249+
250+
// "Hijack" the client connection to get a TCP (or TLS) socket we can read
251+
// and write arbitrary data to/from.
252+
hj, ok := w.(http.Hijacker)
253+
if !ok {
254+
log.Panic("http server doesn't support hijacking connection")
255+
}
256+
257+
clientConn, _, err := hj.Hijack()
258+
if err != nil {
259+
log.Panic("http hijacking failed")
260+
}
261+
262+
// proxyReq.Host will hold the CONNECT target host, which will typically have
263+
// a port - e.g. example.org:443
264+
// To generate a fake certificate for example.org, we have to first split off
265+
// the host from the port.
266+
host, _, err := net.SplitHostPort(proxyReq.Host)
267+
if err != nil {
268+
log.Panic("error splitting host/port:", err)
269+
}
270+
271+
// Create a fake TLS certificate for the target host, signed by our CA. The
272+
// certificate will be valid for 10 days - this number can be changed.
273+
pemCert, pemKey := createCert([]string{host}, p.caCert, p.caKey, 240)
274+
tlsCert, err := tls.X509KeyPair(pemCert, pemKey)
275+
if err != nil {
276+
log.Panic(err)
277+
}
278+
279+
// Send an HTTP OK response back to the client; this initiates the CONNECT
280+
// tunnel. From this point on the client will assume it's connected directly
281+
// to the target.
282+
if _, err := clientConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")); err != nil {
283+
log.Panic("error writing status to client:", err)
284+
}
285+
286+
// Configure a new TLS server, pointing it at the client connection, using
287+
// our certificate. This server will now pretend being the target.
288+
tlsConfig := &tls.Config{
289+
PreferServerCipherSuites: true,
290+
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
291+
MinVersion: tls.VersionTLS12,
292+
Certificates: []tls.Certificate{tlsCert},
293+
}
294+
295+
tlsConn := tls.Server(clientConn, tlsConfig)
296+
// tls.Conn.Close() also closes the underlying net.Conn
297+
defer tlsConn.Close()
298+
299+
connReader := bufio.NewReader(tlsConn)
300+
301+
r, err := http.ReadRequest(connReader)
302+
if err != nil && err != io.EOF {
303+
log.Panic(err)
304+
} else if err == io.EOF {
305+
return
306+
}
307+
308+
changeRequestToTarget(r, proxyReq.Host)
309+
310+
resp, err := client.Do(r)
311+
if err != nil {
312+
log.Println("error sending request to target:", err)
313+
return
314+
}
315+
defer resp.Body.Close()
316+
317+
if err := resp.Write(tlsConn); err != nil {
318+
log.Println("error writing response back:", err)
319+
}
320+
}
321+
322+
// changeRequestToTarget modifies req to be re-routed to the given target;
323+
// the target should be taken from the Host of the original tunnel (CONNECT)
324+
// request.
325+
func changeRequestToTarget(req *http.Request, targetHost string) {
326+
targetUrl := addrToUrl(targetHost)
327+
targetUrl.Path = req.URL.Path
328+
targetUrl.RawQuery = req.URL.RawQuery
329+
// Unescape the path - the client will have escaped it, but the target
330+
// server will expect it unescaped since it's part of the HTTP request.
331+
path, err := url.PathUnescape(targetUrl.String())
332+
if err != nil {
333+
log.Panic(err)
334+
}
335+
req.URL, err = url.Parse(path)
336+
if err != nil {
337+
log.Panicf("error parsing target URL %v: %v", path, err)
338+
}
339+
// Make sure this is unset for sending the request through a client
340+
req.RequestURI = ""
341+
}
342+
343+
func addrToUrl(addr string) *url.URL {
344+
if !strings.HasPrefix(addr, "https") {
345+
addr = "https://" + addr
346+
}
347+
u, err := url.Parse(addr)
348+
if err != nil {
349+
log.Panic(err)
350+
}
351+
return u
352+
}
353+
354+
func main() {
355+
var addr = flag.String("addr", "127.0.0.1:9999", "proxy address")
356+
caCertFile := flag.String("cacertfile", "", "certificate .pem file for trusted CA")
357+
caKeyFile := flag.String("cakeyfile", "", "key .pem file for trusted CA")
358+
flag.Parse()
359+
360+
proxy := createMitmProxy(*caCertFile, *caKeyFile)
361+
362+
log.Println("Starting proxy server on", *addr)
363+
if err := http.ListenAndServe(*addr, proxy); err != nil {
364+
log.Panic("ListenAndServe:", err)
365+
}
366+
}

0 commit comments

Comments
 (0)