From 79d255719ed75688ad3d27379c3e15591db3b2eb Mon Sep 17 00:00:00 2001 From: xvzc Date: Tue, 3 Sep 2024 16:07:11 +0900 Subject: [PATCH] fix: packet processing for http request (#230) * fix: http proxy * chore: make a function for setting connection timeout * chore: rename parameters for consistency --- packet/http.go | 30 ++++--- proxy/handler/conn.go | 14 ++++ proxy/handler/http.go | 124 ++++++++++++++++++++++++++++ proxy/handler/https.go | 178 +++++++++++++++++++++++++++++++++++++++++ proxy/handler/io.go | 26 ++++++ proxy/proxy.go | 16 +++- 6 files changed, 369 insertions(+), 19 deletions(-) create mode 100644 proxy/handler/conn.go create mode 100644 proxy/handler/http.go create mode 100644 proxy/handler/https.go create mode 100644 proxy/handler/io.go diff --git a/packet/http.go b/packet/http.go index ea86b1a..47e3859 100644 --- a/packet/http.go +++ b/packet/http.go @@ -2,6 +2,7 @@ package packet import ( "bufio" + "bytes" "io" "net" "net/http" @@ -96,29 +97,26 @@ func (p *HttpRequest) IsConnectMethod() bool { func (p *HttpRequest) Tidy() { s := string(p.raw) - lines := strings.Split(s, "\r\n") + parts := strings.Split(s, "\r\n\r\n") + meta := strings.Split(parts[0], "\r\n") - lines[0] = p.method + " " + p.path + " " + p.version + meta[0] = p.method + " " + p.path + " " + p.version - for i := 0; i < len(lines); i++ { - if strings.HasPrefix(lines[i], "Proxy-Connection") { - lines[i] = "" - } - } + var buf bytes.Buffer + buf.Grow(len(p.raw)) - result := "" - - for i := 0; i < len(lines); i++ { - if lines[i] == "" { + crLF := []byte{0xD, 0xA} + for _, m := range meta { + if strings.HasPrefix(m, "Proxy-Connection") { continue } - - result += lines[i] + "\r\n" + buf.WriteString(m) + buf.Write(crLF) } + buf.Write(crLF) + buf.WriteString(parts[1]) - result += "\r\n" - - p.raw = []byte(result) + p.raw = buf.Bytes() } func parse(rdr io.Reader) (*HttpRequest, error) { diff --git a/proxy/handler/conn.go b/proxy/handler/conn.go new file mode 100644 index 0000000..302c8a2 --- /dev/null +++ b/proxy/handler/conn.go @@ -0,0 +1,14 @@ +package handler + +import ( + "net" + "time" +) + +func setConnectionTimeout(conn *net.TCPConn, timeout int) error { + if timeout <= 0 { + return nil + } + + return conn.SetReadDeadline(time.Now().Add(time.Millisecond * time.Duration(timeout))) +} diff --git a/proxy/handler/http.go b/proxy/handler/http.go new file mode 100644 index 0000000..4545c21 --- /dev/null +++ b/proxy/handler/http.go @@ -0,0 +1,124 @@ +package handler + +import ( + "context" + "net" + "strconv" + "time" + + "github.com/xvzc/SpoofDPI/packet" + "github.com/xvzc/SpoofDPI/util" + "github.com/xvzc/SpoofDPI/util/log" +) + +type HttpHandler struct { + bufferSize int + protocol string + port int + timeout int +} + +func NewHttpHandler(timeout int) *HttpHandler { + return &HttpHandler{ + bufferSize: 1024, + protocol: "HTTP", + port: 80, + timeout: timeout, + } +} + +func (h *HttpHandler) Serve(ctx context.Context, lConn *net.TCPConn, pkt *packet.HttpRequest, ip string) { + ctx = util.GetCtxWithScope(ctx, h.protocol) + logger := log.GetCtxLogger(ctx) + + // Create a connection to the requested server + var port int = 80 + var err error + if pkt.Port() != "" { + port, err = strconv.Atoi(pkt.Port()) + if err != nil { + logger.Debug().Msgf("error while parsing port for %s aborting..", pkt.Domain()) + } + } + + rConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.ParseIP(ip), Port: port}) + if err != nil { + lConn.Close() + logger.Debug().Msgf("%s", err) + return + } + + logger.Debug().Msgf("new connection to the server %s -> %s", rConn.LocalAddr(), pkt.Domain()) + + go h.deliverResponse(ctx, rConn, lConn, pkt.Domain(), lConn.RemoteAddr().String()) + go h.deliverRequest(ctx, lConn, rConn, lConn.RemoteAddr().String(), pkt.Domain()) + + _, err = rConn.Write(pkt.Raw()) + if err != nil { + logger.Debug().Msgf("error sending request to %s: %s", pkt.Domain(), err) + return + } +} + +func (h *HttpHandler) deliverRequest(ctx context.Context, from *net.TCPConn, to *net.TCPConn, fd string, td string) { + ctx = util.GetCtxWithScope(ctx, h.protocol) + logger := log.GetCtxLogger(ctx) + + defer func() { + from.Close() + to.Close() + + logger.Debug().Msgf("closing proxy connection: %s -> %s", fd, td) + }() + + for { + err := setConnectionTimeout(from, h.timeout) + if err != nil { + logger.Debug().Msgf("error while setting connection deadline for %s: %s", fd, err) + } + + pkt, err := packet.ReadHttpRequest(from) + if err != nil { + logger.Debug().Msgf("error reading from %s: %s", fd, err) + return + } + + pkt.Tidy() + + if _, err := to.Write(pkt.Raw()); err != nil { + logger.Debug().Msgf("error Writing to %s", td) + return + } + } +} + +func (h *HttpHandler) deliverResponse(ctx context.Context, from *net.TCPConn, to *net.TCPConn, fd string, td string) { + ctx = util.GetCtxWithScope(ctx, h.protocol) + logger := log.GetCtxLogger(ctx) + + defer func() { + from.Close() + to.Close() + + logger.Debug().Msgf("closing proxy connection: %s -> %s", fd, td) + }() + + buf := make([]byte, h.bufferSize) + for { + err := setConnectionTimeout(from, h.timeout) + if err != nil { + logger.Debug().Msgf("error while setting connection deadline for %s: %s", fd, err) + } + + bytesRead, err := ReadBytes(ctx, from, buf) + if err != nil { + logger.Debug().Msgf("error reading from %s: %s", fd, err) + return + } + + if _, err := to.Write(bytesRead); err != nil { + logger.Debug().Msgf("error Writing to %s", td) + return + } + } +} diff --git a/proxy/handler/https.go b/proxy/handler/https.go new file mode 100644 index 0000000..80020db --- /dev/null +++ b/proxy/handler/https.go @@ -0,0 +1,178 @@ +package handler + +import ( + "context" + "net" + "regexp" + "strconv" + "time" + + "github.com/xvzc/SpoofDPI/packet" + "github.com/xvzc/SpoofDPI/util" + "github.com/xvzc/SpoofDPI/util/log" +) + +type HttpsHandler struct { + bufferSize int + protocol string + port int + timeout int + windowsize int + exploit bool + allowedPatterns []*regexp.Regexp +} + +func NewHttpsHandler(timeout int, windowSize int, allowedPatterns []*regexp.Regexp, exploit bool) *HttpsHandler { + return &HttpsHandler{ + bufferSize: 1024, + protocol: "HTTPS", + port: 443, + timeout: timeout, + windowsize: windowSize, + allowedPatterns: allowedPatterns, + exploit: exploit, + } +} + +func (h *HttpsHandler) Serve(ctx context.Context, lConn *net.TCPConn, initPkt *packet.HttpRequest, ip string) { + ctx = util.GetCtxWithScope(ctx, h.protocol) + logger := log.GetCtxLogger(ctx) + + // Create a connection to the requested server + var err error + if initPkt.Port() != "" { + h.port, err = strconv.Atoi(initPkt.Port()) + if err != nil { + logger.Debug().Msgf("error parsing port for %s aborting..", initPkt.Domain()) + } + } + + rConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.ParseIP(ip), Port: h.port}) + if err != nil { + lConn.Close() + logger.Debug().Msgf("%s", err) + return + } + + logger.Debug().Msgf("new connection to the server %s -> %s", rConn.LocalAddr(), initPkt.Domain()) + + _, err = lConn.Write([]byte(initPkt.Version() + " 200 Connection Established\r\n\r\n")) + if err != nil { + logger.Debug().Msgf("error sending 200 connection established to the client: %s", err) + return + } + + logger.Debug().Msgf("sent connection estabalished to %s", lConn.RemoteAddr()) + + // Read client hello + m, err := packet.ReadTLSMessage(lConn) + if err != nil || !m.IsClientHello() { + logger.Debug().Msgf("error reading client hello from %s: %s", lConn.RemoteAddr().String(), err) + return + } + clientHello := m.Raw + + logger.Debug().Msgf("client sent hello %d bytes", len(clientHello)) + + // Generate a go routine that reads from the server + go h.communicate(ctx, rConn, lConn, initPkt.Domain(), lConn.RemoteAddr().String()) + go h.communicate(ctx, lConn, rConn, lConn.RemoteAddr().String(), initPkt.Domain()) + + if h.exploit { + logger.Debug().Msgf("writing chunked client hello to %s", initPkt.Domain()) + chunks := splitInChunks(ctx, clientHello, h.windowsize) + if _, err := writeChunks(rConn, chunks); err != nil { + logger.Debug().Msgf("error writing chunked client hello to %s: %s", initPkt.Domain(), err) + return + } + } else { + logger.Debug().Msgf("writing plain client hello to %s", initPkt.Domain()) + if _, err := rConn.Write(clientHello); err != nil { + logger.Debug().Msgf("error writing plain client hello to %s: %s", initPkt.Domain(), err) + return + } + } +} + +func (h *HttpsHandler) communicate(ctx context.Context, from *net.TCPConn, to *net.TCPConn, fd string, td string) { + ctx = util.GetCtxWithScope(ctx, h.protocol) + logger := log.GetCtxLogger(ctx) + + defer func() { + from.Close() + to.Close() + + logger.Debug().Msgf("closing proxy connection: %s -> %s", fd, td) + }() + + buf := make([]byte, h.bufferSize) + for { + err := setConnectionTimeout(from, h.timeout) + if err != nil { + logger.Debug().Msgf("error while setting connection deadline for %s: %s", fd, err) + } + + bytesRead, err := ReadBytes(ctx, from, buf) + if err != nil { + logger.Debug().Msgf("error reading from %s: %s", fd, err) + return + } + + if _, err := to.Write(bytesRead); err != nil { + logger.Debug().Msgf("error Writing to %s", td) + return + } + } +} + +func splitInChunks(ctx context.Context, bytes []byte, size int) [][]byte { + logger := log.GetCtxLogger(ctx) + + var chunks [][]byte + var raw []byte = bytes + + logger.Debug().Msgf("window-size: %d", size) + + if size > 0 { + for { + if len(raw) == 0 { + break + } + + // necessary check to avoid slicing beyond + // slice capacity + if len(raw) < size { + size = len(raw) + } + + chunks = append(chunks, raw[0:size]) + raw = raw[size:] + } + + return chunks + } + + // When the given window-size <= 0 + + if len(raw) < 1 { + return [][]byte{raw} + } + + logger.Debug().Msg("using legacy fragmentation") + + return [][]byte{raw[:1], raw[1:]} +} + +func writeChunks(conn *net.TCPConn, c [][]byte) (n int, err error) { + total := 0 + for i := 0; i < len(c); i++ { + b, err := conn.Write(c[i]) + if err != nil { + return 0, nil + } + + total += b + } + + return total, nil +} diff --git a/proxy/handler/io.go b/proxy/handler/io.go new file mode 100644 index 0000000..93af1f4 --- /dev/null +++ b/proxy/handler/io.go @@ -0,0 +1,26 @@ +package handler + +import ( + "context" + "errors" + "net" +) + +func ReadBytes(ctx context.Context, conn *net.TCPConn, dest []byte) ([]byte, error) { + n, err := readBytesInternal(ctx, conn, dest) + return dest[:n], err +} + +func readBytesInternal(ctx context.Context, conn *net.TCPConn, dest []byte) (int, error) { + totalRead, err := conn.Read(dest) + if err != nil { + var opError *net.OpError + switch { + case errors.As(err, &opError) && opError.Timeout(): + return totalRead, errors.New("timed out") + default: + return totalRead, err + } + } + return totalRead, nil +} diff --git a/proxy/proxy.go b/proxy/proxy.go index 7bdceb5..774302b 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -9,6 +9,7 @@ import ( "github.com/xvzc/SpoofDPI/dns" "github.com/xvzc/SpoofDPI/packet" + "github.com/xvzc/SpoofDPI/proxy/handler" "github.com/xvzc/SpoofDPI/util" "github.com/xvzc/SpoofDPI/util/log" ) @@ -25,6 +26,10 @@ type Proxy struct { allowedPattern []*regexp.Regexp } +type Handler interface { + Serve(ctx context.Context, lConn *net.TCPConn, pkt *packet.HttpRequest, ip string) +} + func New(config *util.Config) *Proxy { return &Proxy{ addr: config.Addr, @@ -74,7 +79,9 @@ func (pxy *Proxy) Start(ctx context.Context) { return } - logger.Debug().Msgf("request from %s\n\n%s", conn.RemoteAddr(), pkt.Raw()) + pkt.Tidy() + + logger.Debug().Msgf("request from %s\n\n%s", conn.RemoteAddr(), string(pkt.Raw())) if !pkt.IsValidMethod() { logger.Debug().Msgf("unsupported method: %s", pkt.Method()) @@ -100,11 +107,14 @@ func (pxy *Proxy) Start(ctx context.Context) { return } + var h Handler if pkt.IsConnectMethod() { - pxy.handleHttps(ctx, conn.(*net.TCPConn), matched, pkt, ip) + h = handler.NewHttpsHandler(pxy.timeout, pxy.windowSize, pxy.allowedPattern, matched) } else { - pxy.handleHttp(ctx, conn.(*net.TCPConn), pkt, ip) + h = handler.NewHttpHandler(pxy.timeout) } + + h.Serve(ctx, conn.(*net.TCPConn), pkt, ip) }() } }